FriendlyWire.com


beginner-friendly electronics tutorials and projects


Discover the joy of understanding electronics!

TLC5916 tutorial: best breadboard 7-segment LED driver!

July 24, 2020 tutorial

Whenever you have a project that needs to show some information to the user, 7-segment displays are a great way to do that. They come in all kinds of sizes and colors and are easy to drive. But what if you are working on breadboards? Usually, breadboard circuits are rather messy with a lot of wires and cables flinging around, and it's hard to use 7-segment displays on them.

In this tutorial I will show you how to use the TLC5916 driver IC to control as many 7-segment displays that you like. It is a very versatile IC as we will see, and not that difficult to control. And second, I will show you a method to mount 7-segment displays right on top of their TLC5916 driver ICs which allows for a super clean and space-saving breadboard layout:

If you want to read up a bit more on what we are talking about today I recommend these three articles for background material:

But don't worry, I will try to keep everything as self-contained as possible so you don't have to open 27 browser tabs to keep up. Let's go!

What you need

If you want to follow along, then this is all you need:

As always, there is a detailed breakdown of all components in the components box on the side. Let's go through the basics:

  • We will use the PIC16F1455 as our microcontroller to control the display. You can of course use any controller you like, but this tutorial will be focused on that one.
  • For each digit you want to display you need a common anode 7-segment LED display (more on that later), a 1kΩ resistor, the TLC5916 driver IC, and a 100nF bypass capacitor for stability.
  • The PIC16F1455 also gets a separate 100nF capacitor, and the large 100μF capacitor serves as bulk capacitance that smoothes out current surges when switching a lot of LEDs at the same time. I definitely recommend it, don't omit it :)

Other than that you also need the PICkit3 (as well as a simple six-terminal connecting cable) to flash the .hex file onto the controller. If these words don't mean anything to you: don't worry :) I have a detailed introduction for you right here: Your first simple PIC microcontroller project! :)

Schematic

So how does the schematic look like? Here it is for one display:

As usual, let's go through step by step:

  • The PIC16F1455 is the brain of this circuit and sends out the display data to the TLC5016 driver IC over the data lines SDI, CLK, LE, and /OE. I will talk more about those in a second :)
  • G1 is a 4.5V battery source, which consists of three AAA batteries). It's not a good idea to power this circuit on batteries forever, but for this simple tutorial I did not want to complicate things.
  • The potentiometer R1 is connected to the PIC16F1455's analog to digital converter and we will use it to adjust the brightness of the LEDs.
  • Speaking of LEDs, those are all collected in the 7-segment display LED1. It is a common anode model, which is very important. This circuit won't work for common cathode displays.
  • JP1 is the ICSP programming adapter. This sounds a bit confusing, I know, but basically this is where we plug in the PICkit3 to transfer the .hex file from our computer onto the PIC16F1455. If we don't do that the PIC won't know what to do: the PIC16F1455 is a microcontroller, and microcontrollers need .hex file to tell them what do to.
  • IC2 is the TLC5916 driver IC, the star of the show. Its outputs Q1-Q8 are connected to the 7-segment display's LED cathodes, and these outputs can sink a current that is adjusted by the resistor R2. More on that later.
  • As I wrote above, R2 sets the current at which the TLC5916 drives the LEDs. Here it is set to 18.75mA, and we will get back to that shortly.
  • The funny looking symbol IC2P denotes the power connections to the TLC5916 at pin 16 (VDD) and pin 1 (ground). You can learn more about reading schematics here.
  • The capacitors C1 and C2 are bypass capacitors that help with stability at high frequencies, and C3 is a bulk capacitor that helps out with current surges if a lot of LEDs are turned on and off. They might not seem necessary, but trust me, your life will be simpler with them installed.
  • The label “NEXT” at the SDO pin of the TLC5916 is where you can connect additional displays.

And that's it, basically. Now on the photos above you saw more than one display. Here is the schematic for four displays:

You see, it's basically identical to the first one, with only IC2, LED1, C2, and R2 copied three additional times.

  • The SDO output of IC2 is connected to the SDI input of IC3, and IC3's SDO output is again connected to the SDI input of IC4, and so on. This is called cascading, and the data that we send into the first driver (IC2 in this case) can be passed along that way.
  • All other inputs are connected in parallel: all CLK pins are connected to each other, all LE pins, and all /OE pins.
  • This means that the PIC16F1455 does not need any additional outputs to drive more displays. Isn't that great?

And that's it! Just one final note: make sure that your power supply can handle the current. A 7-segment display can consume up to 160mA (20mA per segment plus the decimal point), so the current adds up quickly. So why am I using batteries to drive this circuit? Surely they will drain in no time! Yes, you are right. I decided to use batteries to keep the circuit simple and compact, but if you want to use this circuit in the real world then you should definitely use a dedicated power supply.

TLC5916 in a nutshell

Okay, let's talk about the TLC5916 driver IC. In a nutshell: it is a constant current source that can be controlled like a shift register. You can learn more about shift registers here and here, but in a nutshell: they work like conveyor belts for bits:

This is an animation I made for the CD4094 shift register, so the pins have different names, but the idea is the same. Here is the TLC5916 all by itself:

  • SDI stands for “Serial Data In“ and it is the DATA input in our picture above.
  • CLK is the clock input.
  • LE stands for “Latch Enable” and is the STROBE input in the picture above. A pulse on this pin transfers the state on the conveyor belt into the output latch Q1-Q8.
  • /OE stands for “Output Enable” and is an active-low pin. If it is low, the state of the latches Q1-Q8 is displayed on the outputs (in other words: normal operation). If it is high, however, the outputs are overridden and turned off. This is useful for blanking the display.

Here is the timing diagram for sending data to the display. Here we are sending the binary number 0b11010100 (212 in decimal):

We start with the most significant bit and end with the least significant bit. We always set SDI to the current bit, and then pulse CLK from 0 to 1 and then to 0 again. After doing this eight times, we also pulse the LE pin. If /OE is low, the data then appears on the outputs Q1-Q8 (here in orange).

After all the digital stuff, let's talk more about the electronic aspects. We want to drive a common anode 7-segment display:

The TLC5916 acts as a constant current source like this:

How do you adjust the current? This is where the external resistor comes in:

In our schematic above, R2 is equal to 1kΩ which corresponds to a current of 18.75mA. And the best part about it: the supply voltage can be 4.5V, 5V, 6V, but the TLC5916 keeps the current regulated at 18.75mA. There are just two things to keep in mind:

  • LEDs have a forward voltage (read more here) which is their minimum voltage required to start glowing. Clearly the supply voltage must be larger than that. If you have LEDs in series, these voltages add up, so keep that in mind. Two LEDs with 2.2V forward voltage each require at least 4.4V of supply voltage, and so on.
  • The difference between the supply voltage and the sum of the forward voltages is converted into heat inside the TLC5916, so make sure that these voltages are not too far apart. In our previous example of two LEDs in series, with a forward voltage of 2.2V, 5V would be acceptable as an operating voltage, but anything much higher, like 9V or 12V, would generate too much dissipation inside the TLC5916 and cause it to shut down.

But yeah, that's the main idea of using the TLC5916 :)

TLC5916 special mode

There is also a special mode that allows us to take the regulated current and adjust it in 128 steps from 25%-100%, which we can use to adjust the brightness of the LEDs. This can also be used for dimming and is in many ways superior to PWM because it does not lead to any flickering of the display and is camera-friendly for that reason :)

But how do we activate the special mode? It's actually very simple. The TLC5916's datasheet tells us that we have to apply the following pulses:

Then the TLC5916 is in special mode, and we can transmit seven values that control the current. I call B0-B5 and HC. This is how we send them:

This looks a bit confusing, but it's actually quite simple:

  • Think of B0-B5 as a binary number between 0 and 63 (six bits). Let's call that number #.
  • Think of HC as a selector for which region we are controlling. HC=0 is 25%-50%, and HC=1 is the regime 50%-100%.
  • This means that sending out HC=0 and the number # allows us to control the current in the range of 25%-50% in 64 steps. If #=0 then we are at minimum current of 25%, and if #=63 we are at maximum brightness, just shy of 50%.
  • If we send out HC=1 instead then the number #=0 means 50% current, and #=63 means just shy of 100% of the current.
  • When we are cascading multiple TLC5916 ICs, we have to send these command bits multiple times (once for each driver), because the data is passed along. This is nice and allows us to control the brightness of each display separately.

And now we can leave the special mode again, and that can be done like this:

After that, the TLC5916 is in normal mode again and works as a regular shift register. This whole section shows you that it is possible to dim the LED's brightness from 25%-100%. If you want to dim all the way to 0, however, you will have to use PWM instead, and go check out Chapter 8 below for that :)

Construction

Okay, it's time, let's get our hands dirty and start building the circuit. But before we do that we have to prepare the 7-segment LED displays. When you look at the TLC5916 driver IC and the pinout of a typical 7-segment displays you notice that they both have four pins on either side:

This means if we bend the LED's pins just slightly and extend them with solid wire we can plug the 7-segment display right on top of the TLC5916:

This is a bit difficult to describe in photos, so check out my YouTube video for more details. This procedure can be done for large displays and smaller ones alike:

With the 7-segment LEDs prepared in this way, let's move on!

  • Step 1

    Place the two 400-pin breadboards in front of you and connect them to each other :)

    Make sure that row 1 faces left on both breadboards, which makes it easier for you to follow along the rest of the construction guide.

  • Step 2

    Place the PIC16F1455 in row 8, and the four TLC5916 driver ICs in rows 19, 2, 12, and 22. Make sure their notches point to the left.

  • Step 3

    Connect the power rails between the two breadboards. For the upper rail we need to connect both VDD and ground, and for the lower rail it is enough to connect just ground.

  • Step 4

    Connect the power pins to all integrated circuits. The PIC16F1455 has VDD at pin 1 and ground at pin 14. The TLC5916 ICs have it exactly opposite: their VDD pin is at pin 16 and their ground pin is at pin 1. For this reason I always recommend to use colored wires.

  • Step 5

    Connect both power rails on both sides of the left breadboard.

  • Step 6

    Insert the 100μF bulk capacitor C6 in the power rail. Make sure its negative terminal is connected to the negative power rail (blue) and its positive terminal is connected to the positive power rail (red). Electrolytic capacitors such as C6 have a big minus sign on their negative terminal, so this is easy to spot.

    Then, insert the 100nF bypass capacitors C1-C5 between the VDD and ground pins of all the ICs. The capacitors C2-C5 can be plugged in either way, but try to place them as close to their chip as possible.

  • Step 7

    Now work on the ICSP connector, which will be plugged in rows 1-6 later. Start with the MCLR cable from row 1 to pin 4, the VDD wire from row 2 to pin 1, and the ground wire from pin 3 to pin 14.

  • Step 8

    Continue with the ICSP connector. The PGD wire (row 4) goes into pin 10, and the PGC wire (row 5) goes into pin 9.

    I know that right now it looks like these wires are not going anywhere, but later we will plug the PICkit3 into rows 1-6 of the breadboard to flash the PIC16F1455. Stay tuned :)

  • Step 9

    Place the 1kΩ current-adjusting resistors R2-R5 between pin 15 of each TLC5916 driver IC an ground.

  • Step 10

    Now connect the data lines (green color). The first connection is from pin 7 of the PIC16F1455 to pin 2 (SDI) of the TLC5916. All other connections are from the TLC5916's SDO pin (pin 14) to the SDI pin of the next driver (pin 2). I hope that makes sense :)

  • Step 11

    Let's move on to the /OE connection (red). These are all in parallel, and start at pin 5 of the PIC16F1455 and connect to pin 13 of each TLC5916 driver IC. These wires look a bit messy, but that is because I connected them to the same breadboard connection hole to save space.

  • Step 12

    The clock lines (yellow) are also connected in parallel. Pin 6 of the PIC16F1455 is connected to all pins 3 of the TLC5916 driver ICs. We need to be space efficient with these wires so that we can later plug the 7-segment display on top of them, which is why they are inserted diagonally.

  • Step 13

    Last, the Latch Enable pins connect pin 8 of the PIC16F1455 to pins 4 of all the TLC5916 driver ICs. We use the diagonal method again to connect those to save some space.

  • Step 14

    Now it's time for the magic moment: plugging in the 7-segment displays. If you have followed the instructions from above then this installation process will be super smooth. Make sure to connect them correctly so that their pins connect to the TLC5916's pins 5-8 at the bottom and to pins 9-12 on the top. Their anodes plug into the positive power rail at the top.

  • Step 15

    Plug in the potentiometer R1. The red and black wires are the outside terminals of the potentiometer, and the green wire is the center terminal.

  • Step 16

    And now connect power. As I said above, it kind of works with the 4.5V battery compartment, but if you want full brightness of the larger 7-segment display I recommend to connect 5V instead.

    Also, make sure to connect the power as close to the 7-segment anodes as possible, because the breadboard power rails have an internal resistance, which for larger currents can lead to substantial voltage drops of around 0.5V. For this reason I connected the power to the top right.

The software

Nothing is happening? Right.... We have to flash the .hex file onto the controller first. And before doing that we need to write the program that controls the TLC5916. I have the complete source code in the appendix if you are interested. Here I just want to mention the details related to the TLC5916. Let's take a look!

After initializing the PIC16F1455 with configuration bits to use its internal oscillator, in line 32 the first important thing happens:

// how are your segments connected to the TLC5916 driver?
// key:                       a  b  c  d  e  f  g  .
unsigned char seg_to_bit[] = {6, 5, 3, 2, 1, 7, 8, 4};

This is where we tell the software which output of the TLC5916 corresponds to which segment of the display. For example, segment a is connected to Q6. If you want to connect things differently, this is where you change it.

Skipping foward a bit, after initializing the internal oscillator to 4Mhz (lines 53-57) and configuring the ADC (lines 59-81) the main loop begins. Let's have a closer look. First, we read out the ADC value like this:

    // main loop
    while (1) {
        
        // get potentiometer position
        GO = 1; while (GO);
        brightness = ADRESH >> 1;

We divide the ADC value by 2 and store the result in the variable brightness, which, depending on the potentiometer position, will have a value of 0-127. Next, we switch the TLC5916 into special mode:

        // switch to special mode
        nOE = 1;
        CLK=1;CLK=0;
        nOE = 0;
        CLK=1;CLK=0;
        nOE = 1;
        CLK=1;CLK=0;
        LE=1;
        CLK=1;CLK=0;
        LE=0;
        CLK=1;CLK=0;

This is the first timing diagram we talked about in Chapter 4. Then we can send the brightness data, using the seventh bit of the ADC value as the HC bit (line 119):

        // send current value
        for (int j=0; j<4; j++) {
            SDI = (brightness >> 0) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 1) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 2) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 3) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 4) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 5) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 6) & 1; CLK = 1; CLK = 0; // HC
            SDI = 1; CLK = 1; CLK = 0; // always 1
            SDI = 0;
        }
        LE=1;
        LE=0;

We have to do this four times (line 111) because we are using four displays. Then we can leave the special mode again, like so:

        // go back to normal mode
        nOE = 1;
        CLK=1;CLK=0;
        nOE = 0;
        CLK=1;CLK=0;
        nOE = 1;
        CLK=1;CLK=0;
        CLK=1;CLK=0;
        CLK=1;CLK=0;
        nOE = 0;

This corresponds to the last timing diagram we talked about in Chapter 4.

Now that we are in normal mode, let's send out some data. In my case I wanted to be cute and send out 5916 (and variations thereof) so it looks a bit convoluted, but here we go:

        // send current numbers (anything from 0 to 9)
        sendValue( convertNumberToPattern( (number + 11)%10 ));
        sendValue( convertNumberToPattern( (number + 6)%10 ));
        sendValue( convertNumberToPattern( (number + 4)%10 ));
        sendValue( convertNumberToPattern(number) );
        LE = 1;
        LE = 0;

OK, let's dig in. The function sendValue sends out an eight bit value to the shift registers. Now we need to convert our numerical information into the correct 7-segment pattern, and that is where the function convertNumberToPattern comes in. And after sending our data, we need to pulse the LE pin as usual.

A simpler example: if you wanted your display to show 1234 instead, this is what you would have to do:

        // make the four displays show "1234"
        sendValue( convertNumberToPattern( 4 );
        sendValue( convertNumberToPattern( 3 );
        sendValue( convertNumberToPattern( 2 );
        sendValue( convertNumberToPattern( 1 );
        LE = 1;
        LE = 0;

We have to send it backwards because here the first display is mounted on the left. A small detail :)

We are almost done with the main loop! The last part is again specific to this example and just increases the variable counter from 0 to 9 and then resets back to 0. The variable idle is used as a dummy variable so that the program does not count up too fast:

        // increment number and reset at 10
        idle++;
        if (idle >= 100) {
            idle=0;
            number++;
            if (number >= 10) {
                number = 0;
            }
        }
        
    }
    
    return;
    
}

And that's the main loop. I hope it wasn't too confusing. Now let's take a look at the function convertNumberToPattern. Here it is:

// This function converts a number into the 7-segment pattern.
unsigned char convertNumberToPattern (unsigned char number) {
    
    // what is the number?
    switch (number) {
        
        // numbers       abcdefg.
        case 0: return 0b11111100;
        case 1: return 0b01100000;
        case 2: return 0b11011010;
        case 3: return 0b11110010;
        case 4: return 0b01100110;
        case 5: return 0b10110110;
        case 6: return 0b10111110;
        case 7: return 0b11100000;
        case 8: return 0b11111110;
        case 9: return 0b11110110;
        
        // default symbol is an empty space
        default: return 0b00000000;
        
    }
    
}

All it does is take the variable value and convert it into a 7-segment pattern, where the most significant bit is the segment a, and so on (see the key in line 168). I included this extra step and sorted the segments alphabetically to make it easier for humans to insert new characters/numbers/symbols. But this means that in the sendValue function we have to unscramble this, and also distribute the segments to the right outputs according to the seg_to_bit[] array from line 34. This looks a bit messy:

// This function sends an eight-bit value
// to the TLC5916 driver ICs.
void sendValue (unsigned char seg_pattern) {

    // store decoded segment pattern here
    unsigned char bit_pattern = 0;
    
    // decode segment pattern into bit pattern
    for (int n = 0; n < 8; n++) {
        if ((seg_pattern >> (7-n)) & 1) {
            bit_pattern |= 1 << (seg_to_bit[n] - 1);
        }
    }
    
    // send bit pattern bit by bit,
    // starting with the most significant bit
	for (int n = 7; n >= 0; n--) {
        
        SDI = (bit_pattern >> n) & 1;
        CLK = 1;
		CLK = 0;
        
	}
    
    // set data pin back to zero
    SDI = 0;

}

As I said, lines 194-199 are a bit messy, but on the plus side it is very convenient to change locations of the segments with the seg_to_bit[] array from line 34. A worthy trade off I think. The rest (lines 201-212) are simpler and just send out the data, bit by bit, as we learned in Chapter 3. At the end we also reset the SDI pin to 0 just so that it doesn't randomly stay on if it doesn't need to after a transmission.

And that's it! I hope this makes sense. If you see something crazy get in touch on social media and I will do my best to make sense of it :)

Flashing the PIC16F1455

Okay, now that we understand the code, we can compile it into a .hex file using MPLAB X IDE with its XC8 compiler, but you can also download the .hex file right here in the resources box.

Now it's time to connect the PICkit3 to our circuit.

A few things to note:

  • Make sure that the yellow cable of the connector is connected to the PICkit3 at the location of the little triangle.
  • On the breadboard, the yellow wire has to be connected to row 1.
  • And last, make sure that the power supply is turned ON.

Then plug in the USB end into your computer and use the MPLAB X IPE to flash the .hex file on to the PIC16F1455.

Dimming in the PWM mode

It is also possible to dim the display in the range of 0-100% with PWM, and we actually learned how to do this in the ADC tutorial :) You can find the PWM source code in the appendix and use it as well, if you like.

YouTube video

I covered this entire tutorial in a dedicated YouTube video:

Final thoughts

You did it, congrats! I hope I could convince you that the TLC5916 is an amazingly useful IC that can help you tremendously when you want to work with LEDs on a breadboard.

Last, I want to mention the scrolling text display. Haven't we already used the CD4094 shift register to drive 7-segment displays? Why the change of heart?

Quite frankly: it is a bad idea to use the CD4094 as an LED driver. At 5V its maximum output current is 1mA. Yes, 1mA, and that is really not a lot. In our case of the scrolling text display we kind of got away with it because those displays don't need a lot of current for an acceptable brightness, but a few weeks later I tried using the big 1"/2.54cm displays of this tutorial here with the CD4094. The result was rather poor:

So at first I decided to find other displays that work with the CD4094, but that would have been a bad idea. Always solve the real problem, if you can, and don't waste time on treating symptoms :) So in my case that meant looking for a better driver, which is how I found the TLC5916. I am glad that I did. It took me a while to write this tutorial, but I think it was worth it. And yes, in the picture above you can see on of my next projects, so stay tuned!

Let me know what you think on and I look forward to hearing from you. Have a great day!

Appendix: The full source code

Here you can find the full source code. The code (as well as the .hex file) are also available for download in the resources box.

/*
 * File:   main.c
 * Author: boos
 *
 * Created on July 7, 2020, 8:03 PM
 */

// CONFIG1
#pragma config FOSC = INTOSC    // Oscillator Selection Bits (INTOSC oscillator: I/O function on CLKIN pin)
#pragma config WDTE = OFF       // Watchdog Timer Enable (WDT disabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable (PWRT disabled)
#pragma config MCLRE = OFF      // MCLR Pin Function Select (MCLR/VPP pin function is digital input)
#pragma config CP = OFF         // Flash Program Memory Code Protection (Program memory code protection is disabled)
#pragma config BOREN = OFF      // Brown-out Reset Enable (Brown-out Reset disabled)
#pragma config CLKOUTEN = OFF   // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
#pragma config IESO = OFF       // Internal/External Switchover Mode (Internal/External Switchover Mode is disabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled)

// CONFIG2
#pragma config WRT = OFF        // Flash Memory Self-Write Protection (Write protection off)
#pragma config CPUDIV = NOCLKDIV// CPU System Clock Selection Bit (NO CPU system divide)
#pragma config USBLSCLK = 48MHz // USB Low SPeed Clock Selection bit (System clock expects 48 MHz, FS/LS USB CLKENs divide-by is set to 8.)
#pragma config PLLMULT = 3x     // PLL Multiplier Selection Bit (3x Output Frequency Selected)
#pragma config PLLEN = ENABLED  // PLL Enable Bit (3x or 4x PLL Enabled)
#pragma config STVREN = ON      // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will cause a Reset)
#pragma config BORV = LO        // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LPBOR = OFF      // Low-Power Brown Out Reset (Low-Power BOR is disabled)
#pragma config LVP = OFF        // Low-Voltage Programming Enable (Low-voltage programming disabled)

#include <xc.h>

// how are your segments connected to the TLC5916 driver?
// key:                       a  b  c  d  e  f  g  .
unsigned char seg_to_bit[] = {6, 5, 3, 2, 1, 7, 8, 4};

// declare functions
void sendValue (unsigned char value);
unsigned char convertNumberToPattern (unsigned char number);

// define locations of shift register controls
#define LE RC2
#define SDI RC3
#define CLK RC4
#define nOE RC5

// variables
unsigned char number=0, brightness=0;
int idle=0;

// main function
void main (void) {
    
    // set internal oscillator to 4MHz
    IRCF0 = 1;
    IRCF1 = 0;
    IRCF2 = 1;
    IRCF3 = 1;
    
    // ADC settings
    
	// ADC sampling frequency per bit is F_osc/16
	ADCS0 = 1;
	ADCS1 = 0;
    ADCS2 = 1;
    
    // result alignment
    ADFM = 0;
    
    // RA4 as analog input
	TRISA4 = 1;
    ANSA4 = 1;
    
    // select channel AN3 (which corresponds to RA4)
    CHS0 = 1;
    CHS1 = 1;
    CHS2 = 0;
    CHS3 = 0;
    CHS4 = 0;
    
    // turn the ADC on
	ADON = 1;
    
    // set outputs and disable analog features on pins RC2 and RC3
    TRISC2 = 0;
    TRISC3 = 0;
    TRISC4 = 0;
    TRISC5 = 0;
    ANSC2 = 0;
    ANSC3 = 0;
    
    
    // main loop
    while (1) {
        
        // get potentiometer position
        GO = 1; while (GO);
        brightness = ADRESH >> 1;
        
        // switch to special mode
        nOE = 1;
        CLK=1;CLK=0;
        nOE = 0;
        CLK=1;CLK=0;
        nOE = 1;
        CLK=1;CLK=0;
        LE=1;
        CLK=1;CLK=0;
        LE=0;
        CLK=1;CLK=0;
        
        // send current value
        for (int j=0; j<4; j++) {
            SDI = (brightness >> 0) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 1) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 2) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 3) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 4) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 5) & 1; CLK = 1; CLK = 0;
            SDI = (brightness >> 6) & 1; CLK = 1; CLK = 0; // HC
            SDI = 1; CLK = 1; CLK = 0; // always 1
        }
        LE=1;
        LE=0;
        
        
        // go back to normal mode
        nOE = 1;
        CLK=1;CLK=0;
        nOE = 0;
        CLK=1;CLK=0;
        nOE = 1;
        CLK=1;CLK=0;
        CLK=1;CLK=0;
        CLK=1;CLK=0;
        nOE = 0;
        
        // send current numbers (anything from 0 to 9)
        sendValue( convertNumberToPattern( (number + 11)%10 ));
        sendValue( convertNumberToPattern( (number + 6)%10 ));
        sendValue( convertNumberToPattern( (number + 4)%10 ));
        sendValue( convertNumberToPattern(number) );
        LE = 1;
        LE = 0;
        
        // increment number and reset at 10
        idle++;
        if (idle >= 100) {
            idle=0;
            number++;
            if (number >= 10) {
                number = 0;
            }
        }
        
    }
    
    return;
    
}

// This function converts a number into the 7-segment pattern.
unsigned char convertNumberToPattern (unsigned char number) {
    
    // what is the number?
    switch (number) {
        
        // numbers       abcdefg.
        case 0: return 0b11111100;
        case 1: return 0b01100000;
        case 2: return 0b11011010;
        case 3: return 0b11110010;
        case 4: return 0b01100110;
        case 5: return 0b10110110;
        case 6: return 0b10111110;
        case 7: return 0b11100000;
        case 8: return 0b11111110;
        case 9: return 0b11110110;
        
        // default symbol is an empty space
        default: return 0b00000000;
        
    }
    
}

// This function sends an eight-bit value
// to the TLC5916 driver ICs.
void sendValue (unsigned char seg_pattern) {

    // store decoded segment pattern here
    unsigned char bit_pattern = 0;
    
    // decode segment pattern into bit pattern
    for (int n = 0; n < 8; n++) {
        if ((seg_pattern >> (7-n)) & 1) {
            bit_pattern |= 1 << (seg_to_bit[n] - 1);
        }
    }
    
    // send bit pattern bit by bit,
    // starting with the most significant bit
	for (int n = 7; n >= 0; n--) {
        
        SDI = (bit_pattern >> n) & 1;
        CLK = 1;
		CLK = 0;
        
	}
    
    // set data pin back to zero
    SDI = 0;

}

Appendix 2: The full source code (PWM version)

Here you can find the full source code of the PWM version. The code (as well as the .hex file) are also available for download in the resources box.

/*
 * File:   main.c
 * Author: boos
 *
 * Created on July 7, 2020, 8:03 PM
 */

// CONFIG1
#pragma config FOSC = INTOSC    // Oscillator Selection Bits (INTOSC oscillator: I/O function on CLKIN pin)
#pragma config WDTE = OFF       // Watchdog Timer Enable (WDT disabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable (PWRT disabled)
#pragma config MCLRE = OFF      // MCLR Pin Function Select (MCLR/VPP pin function is digital input)
#pragma config CP = OFF         // Flash Program Memory Code Protection (Program memory code protection is disabled)
#pragma config BOREN = OFF      // Brown-out Reset Enable (Brown-out Reset disabled)
#pragma config CLKOUTEN = OFF   // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
#pragma config IESO = OFF       // Internal/External Switchover Mode (Internal/External Switchover Mode is disabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled)

// CONFIG2
#pragma config WRT = OFF        // Flash Memory Self-Write Protection (Write protection off)
#pragma config CPUDIV = NOCLKDIV// CPU System Clock Selection Bit (NO CPU system divide)
#pragma config USBLSCLK = 48MHz // USB Low SPeed Clock Selection bit (System clock expects 48 MHz, FS/LS USB CLKENs divide-by is set to 8.)
#pragma config PLLMULT = 3x     // PLL Multiplier Selection Bit (3x Output Frequency Selected)
#pragma config PLLEN = ENABLED  // PLL Enable Bit (3x or 4x PLL Enabled)
#pragma config STVREN = ON      // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will cause a Reset)
#pragma config BORV = LO        // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LPBOR = OFF      // Low-Power Brown Out Reset (Low-Power BOR is disabled)
#pragma config LVP = OFF        // Low-Voltage Programming Enable (Low-voltage programming disabled)

#include <xc.h>

// how are your segments connected to the TLC5916 driver?
// key:                       a  b  c  d  e  f  g  .
unsigned char seg_to_bit[] = {6, 5, 3, 2, 1, 7, 8, 4};

// declare functions
void sendValue (unsigned char value);
unsigned char convertNumberToPattern (unsigned char number);

// define locations of shift register controls
#define LE RC2
#define SDI RC3
#define CLK RC4
#define nOE RC5

// variables
unsigned char number=0, brightness=0;
int idle=0;

// main function
void main (void) {
    
    // set internal oscillator to 4MHz
    IRCF0 = 1;
    IRCF1 = 0;
    IRCF2 = 1;
    IRCF3 = 1;
    
    // ADC settings
    
	// ADC sampling frequency per bit is F_osc/16
	ADCS0 = 1;
	ADCS1 = 0;
    ADCS2 = 1;
    
    // result alignment
    ADFM = 0;
    
    // RA4 as analog input
	TRISA4 = 1;
    ANSA4 = 1;
    
    // select channel AN3 (which corresponds to RA4)
    CHS0 = 1;
    CHS1 = 1;
    CHS2 = 0;
    CHS3 = 0;
    CHS4 = 0;
    
    // turn the ADC on
	ADON = 1;
    
    // set outputs and disable analog features on pins RC2 and RC3
    TRISC2 = 0;
    TRISC3 = 0;
    TRISC4 = 0;
    TRISC5 = 0;
    ANSC2 = 0;
    ANSC3 = 0;

    // PWM settings

    // configure TIMER2
    PR2 = 0xff;
    T2CON = 0b100;
    
    // turn on PWM module 1 at RC5
    PWM1EN = 1; PWM1OE = 1;
    
    // main loop
    while (1) {
        
        // get potentiometer position
        GO = 1; while (GO);
        brightness = ADRESH;
        
        // update PWM duty cycle
        PWM1DCH = brightness;
        
        // send current numbers (anything from 0 to 9)
        sendValue( convertNumberToPattern( (number + 11)%10 ));
        sendValue( convertNumberToPattern( (number + 6)%10 ));
        sendValue( convertNumberToPattern( (number + 4)%10 ));
        sendValue( convertNumberToPattern(number) );
        LE = 1;
        LE = 0;
        
        // increment number and reset at 10
        idle++;
        if (idle >= 100) {
            idle=0;
            number++;
            if (number >= 10) {
                number = 0;
            }
        }
        
    }
    
    return;
    
}

// This function converts a number into the 7-segment pattern.
unsigned char convertNumberToPattern (unsigned char number) {
    
    // what is the number?
    switch (number) {
        
        // numbers       abcdefg.
        case 0: return 0b11111100;
        case 1: return 0b01100000;
        case 2: return 0b11011010;
        case 3: return 0b11110010;
        case 4: return 0b01100110;
        case 5: return 0b10110110;
        case 6: return 0b10111110;
        case 7: return 0b11100000;
        case 8: return 0b11111110;
        case 9: return 0b11110110;
        
        // default symbol is an empty space
        default: return 0b00000000;
        
    }
    
}

// This function sends an eight-bit value
// to the TLC5916 driver ICs.
void sendValue (unsigned char seg_pattern) {

    // store decoded segment pattern here
    unsigned char bit_pattern = 0;
    
    // decode segment pattern into bit pattern
    for (int n = 0; n < 8; n++) {
        if ((seg_pattern >> (7-n)) & 1) {
            bit_pattern |= 1 << (seg_to_bit[n] - 1);
        }
    }
    
    // send bit pattern bit by bit,
    // starting with the most significant bit
	for (int n = 7; n >= 0; n--) {
        
        SDI = (bit_pattern >> n) & 1;
        CLK = 1;
		CLK = 0;
        
	}
    
    // set data pin back to zero
    SDI = 0;

}

About FriendlyWire

Beginner-friendly electronics tutorials and projects. Discover the joy of electronics! Keep reading.

Components Needed

1×400-pin breadboard (link)
1×3×AAA 4.5V battery compartment (link)
3×AAA 1.5V battery
1×PIC16F1455 microcontroller (link)
1*×TLC5916 LED driver (link)
1*×common anode 7-segment LED display (link, kit)
1*×1kΩ resistor (standard 1/4W) (link, kit)
1×4.7kΩ potentiometer (standard 1/4W, can be anything from 5kΩ to 50kΩ) (link, kit)
1×100μF capacitor (link, kit)
2*×100nF ceramic capacitor (link, kit)

*If you want to include more digits you need more of these components.

You also need AWG 24/0.6mm single-stranded wire (link). Click on the items above to learn more.

Tools Needed

1×PICkit3 programmer
1×side cutter
1×pliers

Let's build a community

How did you get interested in electronics? What do you want to learn? Connect and share your story!

Tag Cloud

  • TLC5916
  • 7-segment displays
  • breadboard electronics
  • constant current
  • LED driver
  • shift register
  • PIC16F1455
  • MPLAB X IDE
  • MPLAB X IPE
  • XC8 compiler
  • beginner-friendly
  • schematic
  • tutorial