FB pixel

Visualize Data with 7-segment LED Indicator Using MCC | Embedded C Programming - Part 7

Published


Hello again! In this tutorial I will keep talking about the connection of the 7-segment LED indicator. If you didn’t read the previous tutorial, you can skip it because I’ll repeat all the theoretical information here as well - I’m just providing another approach based on the MPLAB Code Configurator (or MCC). So the task for this tutorial is the following: display a 3-digit number with a 3-digit 7-segment LED indicator (which I will go over in a moment).

We will use this indicator in several of the next tutorials so this is preparation work to simplify our life in the future.

The schematic diagram to implement the current task is much more tangled than what we’ve seen previously (Figure 1).

Figure 1 - Schematics Diagram with the PIC18F14K50 with 7-segment LED Indicator
Figure 1 - Schematics Diagram with the PIC18F14K50 with 7-segment LED Indicator

As you may see, it has many more parts. Except for the familiar MCU PIC18F14K50 (DD1) and the PICKit programmer (X1), it consists of the 7-segment indicator (7Seg1) and eight 330 Ohm resistors (R1-R8).

The 7-segment indicator used can be any type with dynamic indication. It’s easy to distinguish them with the static indicators. If the pin number is less than 12, it’s most likely the dynamic indicator, otherwise it’s a static one.

The difference between them is that in a static indicator every LED segment has its own pin (Figure 2, 3) while in a dynamic indicator the segments of the same name are internally connected together and have only one common pin (Figure 4, 5).

Figure 2 - Static LED Indicator with Common Cathode
Figure 2 - Static LED Indicator with Common Cathode
Figure 3 - Static LED Indicator with Common Anode
Figure 3 - Static LED Indicator with Common Anode
Figure 4 - Dynamic LED Indicator with Common Cathode
Figure 4 - Dynamic LED Indicator with Common Cathode
Figure 5 - Dynamic LED Indicator with Common Anode
Figure 5 - Dynamic LED Indicator with Common Anode

So, as you can see in Figure 2, 3, static indicators have one common pin (cathode, Figure 2, or anode, Figure 3) and separate pins for all segments of each digit. While in dynamic indicators (Figure 4, 5) all segments A, B, C etc. of different digits are connected together and have only one pin. Each type has its own cons and pros. Static indicator has a simpler control algorithm but requires more pins to connect. Dynamic indicator requires much less pins to connect but needs a more complex algorithm to show the numbers. For now, the dynamic indicators are much more widespread than static indicators because of the easier connection. You can compare the number of required pins for static and dynamic indicators with different digit numbers in Table 1.

As you can see, the difference becomes more and more significant with an increasing number of digits. And wasting 36 MCU pins just for connecting a 4-digit indicator is not a good idea, obviously very inefficient. Even 12 pins for dynamic indication can be a lot for MCUs with a small pin-count. For such applications one can use the module with the 7-segment LED driver, for instance TM1637, which was described here: Digital Thermometer - Part 16 Microcontroller Basics (PIC10F200).

That is all theory, so let’s return to our schematics diagram (Figure 1) and put it in practice. Frankly, I took the first indicator that I found in my heap of parts. And it was a 3-digit common anode indicator of the 2381BS-1 type. But you can use any one you have. The pinout of them is usually the same. And if you have the common cathode indicator, it’s also fine, I’ll explain how to change the code to use it.

The R1-R8 resistors limit the current that is flowing through the indicator. Their value is not 1 kOhm like we used in the previous tutorials but 330 Ohms. This is because in dynamic indication we switch between the digits at a high frequency, so the average current through each LED is smaller. If we have 3 digits then the current is ⅓ the size and thus we need a resistor with ⅓ the resistance.

The connection of the indicator to the MCU is not random (well, it’s semi-random but let’s talk about it later). The best option is to connect all the segments to one port of the MCU as this will significantly simplify the code. In the PIC18F14K50 MCU, only PORTC has all eight pins, so we will use it.

Within the port, the connection can be random. I connected the indicator in such a way because it was more convenient for me to put the resistors on the breadboard like this. But you can invent your own connection, I’ll also explain how to change the program to use it. Just keep in mind that all segments should be connected to PORTC.

The common digits pins can be connected randomly but I decided to connect them all to PORTB to simplify the initialization. Besides, PORTA is, by default, used by other MCU modules so it’s better to keep it untouched while possible.

OK, that’s all about the connection. Let’s now briefly talk about the dynamic indication algorithm. It’s not hard actually. As all segments of the same name are connected together, if we apply the voltage to all common pins at once, the same segment will light on in all positions simultaneously, which is not good if we want to display different digits in different positions. Thus to display different digits in different positions we need to switch between them quickly. The human eye doesn’t see flickering if the switching frequency is higher than 50 Hz. So we need to do the following:

  1. At the start of a loop turn off all digits (apply low voltage to all common pins for common anode indicators, or high voltage to all common pins for common cathode indicators)
  2. Apply the correct voltage (low for common anode indicators, or high for common cathode indicators) to the segments which form the digits we want to display.
  3. Turn on the corresponding digit position by applying high voltage for common anode indicator, or low voltage for common cathode indicator to the corresponding common pin.
  4. Perform a short delay (about 5ms) to let the digit be turned on for this time.
  5. Select the next digit position and return to step 1.

The first step is needed to avoid so-called shadows when the previous digit is dimly displayed in the next position.

OK, as we now have all theoretical background, let’s consider the program code.

First we need to create a new project and open the MCC. Then implement the same settings and changes as in this tutorial, except for the Pin Module. Now right click on all pins of PORTC and pins RB5, RB6, RB7, and configure them all as GPIO outputs. Then give a Custom Name to each pin according to the schematics diagram. Finally, you should have the following configuration (Figure 6).

Figure 6 - Pin Module Configuration
Figure 6 - Pin Module Configuration

Warning! Please pay attention to the “Start High” column. If you have a common anode indicator like me, you should set all segment pins high, and all common pins low (Figure 6) to turn off all segments. If you have the common cathode indicator, you need to invert the “Start High” column values: set segment pins low and common pins high.

These are all the settings required to be done with the MCC. Now you can click the “Generate” button, wait until the IDE creates the required files, and then open the “main.c” file. Then you’ll need to write the following code in it.

#include "mcc_generated_files/mcc.h"

void show_digit (uint8_t digit) //Function to generate a digit on the indicator

{

switch (digit)          //Select digit

{

case 0:             //If digit is 0

         SEG_A_SetLow(); //Turn on segment A

         SEG_B_SetLow(); //Turn on segment B

         SEG_C_SetLow(); //Turn on segment C

         SEG_D_SetLow(); //Turn on segment D

         SEG_E_SetLow(); //Turn on segment E

         SEG_F_SetLow(); //Turn on segment F

         SEG_G_SetHigh(); //Turn off segment G

         break;

     case 1:             //If digit is 1

         SEG_A_SetHigh(); //Turn off segment A

         SEG_B_SetLow(); //Turn on segment B

         SEG_C_SetLow(); //Turn on segment C

         SEG_D_SetHigh(); //Turn off segment D

         SEG_E_SetHigh(); //Turn off segment E

         SEG_F_SetHigh(); //Turn off segment F

         SEG_G_SetHigh(); //Turn off segment G

         break;   

      case 2:            //If digit is 2

         SEG_A_SetLow(); //Turn on segment A

         SEG_B_SetLow(); //Turn on segment B

         SEG_C_SetHigh(); //Turn off segment C

         SEG_D_SetLow(); //Turn on segment D

         SEG_E_SetLow(); //Turn on segment E

         SEG_F_SetHigh(); //Turn off segment F

         SEG_G_SetLow(); //Turn on segment G

         break;   

     case 3:             //If digit is 3

         SEG_A_SetLow(); //Turn on segment A

         SEG_B_SetLow(); //Turn on segment B

         SEG_C_SetLow(); //Turn on segment C

         SEG_D_SetLow(); //Turn on segment D

         SEG_E_SetHigh(); //Turn off segment E

         SEG_F_SetHigh(); //Turn off segment F

         SEG_G_SetLow(); //Turn on segment G

         break;  

     case 4:             //If digit is 4

         SEG_A_SetHigh(); //Turn off segment A

         SEG_B_SetLow(); //Turn on segment B

         SEG_C_SetLow(); //Turn on segment C

         SEG_D_SetHigh(); //Turn off segment D

         SEG_E_SetHigh(); //Turn off segment E

         SEG_F_SetLow(); //Turn on segment F

         SEG_G_SetLow(); //Turn on segment G

         break;

     case 5:             //If digit is 5

         SEG_A_SetLow(); //Turn on segment A

         SEG_B_SetHigh(); //Turn off segment B

         SEG_C_SetLow(); //Turn on segment C

         SEG_D_SetLow(); //Turn on segment D

         SEG_E_SetHigh(); //Turn off segment E

         SEG_F_SetLow(); //Turn on segment F

         SEG_G_SetLow(); //Turn on segment G

         break;

     case 6:             //If digit is 6

         SEG_A_SetLow(); //Turn on segment A

         SEG_B_SetHigh(); //Turn off segment B

         SEG_C_SetLow(); //Turn on segment C

         SEG_D_SetLow(); //Turn on segment D

         SEG_E_SetLow(); //Turn on segment E

         SEG_F_SetLow(); //Turn on segment F

         SEG_G_SetLow(); //Turn on segment G

         break;

     case 7:             //If digit is 7

         SEG_A_SetLow(); //Turn on segment A

         SEG_B_SetLow(); //Turn on segment B

         SEG_C_SetLow(); //Turn on segment C

         SEG_D_SetHigh(); //Turn off segment D

         SEG_E_SetHigh(); //Turn off segment E

         SEG_F_SetHigh(); //Turn off segment F

         SEG_G_SetHigh(); //Turn off segment G

         break;

     case 8:             //If digit is 8

         SEG_A_SetLow(); //Turn on segment A

         SEG_B_SetLow(); //Turn on segment B

         SEG_C_SetLow(); //Turn on segment C

         SEG_D_SetLow(); //Turn on segment D

         SEG_E_SetLow(); //Turn on segment E

         SEG_F_SetLow(); //Turn on segment F

         SEG_G_SetLow(); //Turn on segment G

         break;

     case 9:             //If digit is 9

         SEG_A_SetLow(); //Turn on segment A

         SEG_B_SetLow(); //Turn on segment B

         SEG_C_SetLow(); //Turn on segment C

         SEG_D_SetLow(); //Turn on segment D

         SEG_E_SetHigh(); //Turn off segment E

         SEG_F_SetLow(); //Turn on segment F

         SEG_G_SetLow(); //Turn on segment G

         break;    

}

}

uint16_t number = 678;  //Number to display

uint8_t digit;          //Digit number that is currently displayed

void main(void)

{

// Initialize the device

SYSTEM_Initialize();

while (1)           //Main loop of the program

{

     DIG_1_SetLow(); //Turn off digit 1

     DIG_2_SetLow(); //Turn off digit 2

     DIG_3_SetLow(); //Turn off digit 3

     switch (digit)  //Which digit to display

     {

         case 0:     //If digit 1

             show_digit(number / 100); //Display the hundreds

             DIG_1_SetHigh();   //Turn on digit 1

             break;

         case 1:     //If digit 2

             show_digit((number % 100) / 10); //Display the tens

             DIG_2_SetHigh(); //Turn on digit 2

             break;    

         case 2:     //If digit 3

             show_digit(number % 10); //Display the ones

             DIG_3_SetHigh();   //Turn on digit 3

             break;

     }

     __delay_ms(5);  //Perform the delay to let the segment be on for some time

     digit ++;       //Select next digit

     if (digit > 2)  //If digit number is higher than 2

         digit = 0;  //Then set the digit as 0

}

}


We’ll start considering the code with line 3 as the previous lines were described in previous tutorials.

In lines 3-98 there is a function called “show_digit”. It doesn’t return any value, so its type is “void”, and it gets one parameter of “uint8_t” type (which means unsigned 8-bit type, equal to “unsigned char”) called “digit”. This function accepts the digit to display in the current indicator position, and forms its shape. The branching is performed by means of the “switch” construction (lines 5-97). It allows you to check the parameter values and perform different actions according to them. As you may notice, here we check the parameter “digit”, if it is 0, 1, 2, … 9. Then we form the corresponding digit shape. Let’s consider the forming of one of the digits, for example, “0” (lines 7-15). If you look at the 7-segment indicator (fig. 1), you can see that to form the “0” digit you need to turn on the segments A, B, C, D, E, and F, and turn off the segment G. This is exactly what we do in lines 8-14. To turn the segment on, we set its pin low, and to turn it off, we set it high (Warning! For the common cathode indicator it’s vice versa: to turn the segment on, we set its pin high, and to turn it off, we set it low).

Forming the shapes of other digits is the same, so I’ll skip the explanation of lines 16-96. You may check them by yourself or even expand the list by adding some letters.

So, let’s now consider how this function works in general. We invoke it with the parameter that corresponds to the digit we want to display (0 to 9). Then this parameter is checked inside the “switch” construction, and if the match is found, the function forms the shape of the corresponding digit by setting the required segment pins high or low.

In line 100, we define the variable of “uint16_t” type which represents the unsigned 16-bit number, called “number”. This is the number we want to display in the indicator. You can write any value here. I wrote “678” just for example.

In line 101, we define the variable “digit” which represents the number of the digit position which is currently lit up. Please pay attention, that I will start counting from 0 as it is customary in C language. So the “digit” value of “0” will correspond to the first position, the value of “1” will correspond to the second position and so on and so forth.

That is all the definitions required, so in line 103 the main function of the program starts.

In line 106, the SYSTEM_Initialize() function is invoked, configuring all required peripherals, including the GPIOs.

Now we can implement the main endless loop of the program in which we will perform the algorithm I described earlier (lines 108-132).

In lines 110-112, we set all common pins of the indicator low to turn off all the digits positions. (Warning! If you use the common cathode indicator, you need to set all the common pins high by replacing the “SetLow” suffix with the “SetHigh”).

Then in lines 113-127 there is a “switch” construction which allows one to check the variable “digit” and light the corresponding position with the required digit. But first let me describe the algorithm of splitting the number into digits. It’s a bit different from what I explained in the PIC10F200 tutorials. This algorithm can be applied when we know exactly the length of the number we want to split. In our case this length is 3 digits. So we need to split the number into hundreds, tens, and ones.

To get the hundreds value we just divide the number by 100. As this is the integer division, it will just truncate the modulo and leave the required digit. In our case:

678 / 100 = 6 (integer division)

Getting the tens value is a bit more complex. First, we need to get the modulo of division of the number by 100, and then divide the obtained result by 10:

678 % 100 = 78 (modulo)
78 / 10 = 7 (integer division)

Getting the ones is quite simple. We just need to get the modulo of the division of the number by 10:

678 % 10 = 8 (modulo)

To see how this can expand, let me show this algorithm for a 4-digit number:

  • thousands: number / 1000
  • hundreds: (number % 1000) / 100
  • tens: (number % 100) / 10
  • ones: number % 10

And now let’s return to our program. In lines 115-118, we process the first position of the indicator (when “digit” = 0). In this case, we invoke the “show_digit” function with the parameter that corresponds to the hundreds of the number (line 116):

show_digit(number / 100)

Thus, the shape of the required digit will be loaded to the corresponding pins. Now we only need to set the high level at the “DIG_1” pin to light up the first position (line 117). (Warning! If you use the common cathode indicator, you need to apply the low level to the “DIG_1” pin by writing DIG_1_SetLow()).

Displaying the tens (line 119-122) and ones (lines 123-126) are the same, you just pass the digits that correspond to tens and ones as the parameter of the “show_digit” function and apply the high (or low if you use the indicator with the common cathode) level to the required “DIG_x” pin.

In line 128, we perform a short 5ms delay. In this case, the general delay for three digits will be 15ms which gives the frequency of 1 / 0.015 = 67 Hz. This is higher than 50 Hz, so we will not notice the flickering. Also, I don’t recommend making the delay too short as this will make the digits dimmer.

In line 129, we increment the “digit” variable to select the next digit position for the next loop iteration. Then we check if the value becomes higher than 2 (line 130). As the only accepted values are 0, 1, and 2 (if you use a 4-digit indicator, then the value 3 is also accepted) then we reset the value of the “digit” to 0 when its value is higher than 2 (line 131).

And that’s all for now. This time we’ve strengthened our knowledge in configuring the GPIOs using the MCC and also discovered the algorithm for implementing dynamic indication.

If you read the previous tutorial you may notice that this one is much longer. And the code size is also much bigger (540 bytes vs. 222 bytes). On the other hand this variant of the program is more universal because it doesn’t require connecting all segment pins to one port (which happens very often actually). So it’s good to know both options.

As homework, I want you to add a button to the schematics (you can connect it to the RB6 pin like we did in the previous tutorial, and reconnect the indicator’s digit2 pin to the RB4), count the number of presses of the button and display it on the indicator.

Make Bread with our CircuitBread Toaster!

Get the latest tools and tutorials, fresh from the toaster.

What are you looking for?