Embedded C track

Lesson 12 · Part 2: 8051 (Keil C51 / AT89C52)

7-Segment Display (Multiplexing)

Lookup tables and dynamic driving

Part 2: 8051 (Keil C51 / AT89C52)9 min readFree lesson

If we previously made one LED blink, this time we display numbers that people can read. A 7-segment display uses 7 bar-shaped LEDs (ag) and a decimal point (dp) to draw 09 and some letters. The key points are twofold: ① a lookup table that converts numbers into bit patterns for "which segments to light up," ② multiplexing (dynamic driving) to light multiple digits while saving pins.

In this session, on top of the port output from parts 6~7, we add new tools: a lookup table and time division. By turning on the 4-digit number "1234" one digit at a time very quickly, the persistence of vision effect makes it appear as if all four digits are lit simultaneously.

Simulator Note: μVision simulator does not have a graphic 7-segment display. Therefore, in this session, we use the Port Window/Logic Analyzer to check if "segment patterns (P2) and digit selections (P0) cycle through the correct values," and the number → segment conversion itself is verified on a PC using pc_test.c.

Learning Objectives

  • Explain the segment layout (a~g, dp) of the 7-segment display and the difference in common Cathode/Anode.
  • Create a lookup table that converts 0~9 to segment bit patterns.
  • Separate digit selection and segment output to drive multiple digits.
  • Make 4 digits appear simultaneously using multiplexing (time division + persistence of vision).
  • Apply the sequence "turn off → change → turn on" to prevent ghosting.

Target Environment

  • Compiler: Keil C51.
  • Device: AT89C52 (μVision simulator).
  • Verification: μVision simulator (Port 0 / Port 2 window, Logic Analyzer). Conversion from number to code is done on PC gcc(pc_test.c).

Key Concepts

1) 7-segment structure and wiring

A digit is made up of 7 segments (a~g) and a decimal point (dp). One side of all segments is commonly connected, and depending on the polarity of the connection, there are two types.

Type Common pin Segment ON condition Note
Common cathode Connect all LEDs' (−) to GND Segment pin = 1 Assumed in this text
Common anode Connect all LEDs' (+) to VCC Segment pin = 0 Covered in Assignment 1

The story of 7 active segments in High/Low-and is the same. Here we assume common cathode (bit = 1 is ON).

2) Segment bit mapping

Map the segments to port bits as follows (this text convention).

aaa          Bits:  a=0  b=1  c=2  d=3  e=4  f=5  g=6  dp=7
f   b         Number 0 = a,b,c,d,e,f ON, g OFF
  f   b                = 0b00111111 = 0x3F
   ggg
  e   c
  e   c
   ddd  (dp)

3) 0~9 lookup table

Since the segments to be lit are fixed for each number, make a table in advance and use SEG_CODE[n] to retrieve it. This is faster and easier to read than calculating the bits every time.

Number Segments to light Code Number Segments to light Code
0 a b c d e f 0x3F 5 a c d f g 0x6D
1 b c 0x06 6 a c d e f g 0x7D
2 a b d e g 0x5B 7 a b c 0x07
3 a b c d g 0x4F 8 a b c d e f g 0x7F
4 b c f g 0x66 9 a b c d f g 0x6F

4) Digit selection — Reason for saving pins

If you connect each of the 4 digits separately with 7 pins each, you need 28 pins. Instead, the segment pins (a~g) are shared among the 4 digits, and you use the digit select pins to choose "which digit to turn on." Then 7 (segments) + 4 (digits) = 11 pins are sufficient. However, since the segment lines are shared, only one digit can be lit at a time.

5) Multiplexing and Afterimage

If only one digit can be lit, how do we display all four digits? Turn them on alternately very quickly. Digit 0 → Digit 1 → Digit 2 → Digit 3 → back to Digit 0 … by repeating this at intervals of hundreds of µs, the eye perceives all four digits as being on simultaneously due to afterimage.

The order is important. If you change the segment pattern while the digit is on, an incorrect pattern will briefly appear on that digit at the moment of change, causing ghosting. Therefore, always:

1) Turn off all digits  →  2) Write new segment pattern  →  3) Turn on only that digit

Creating a μVision Project

  1. Project → New µVision Project… → name it c51_seg7.
  2. Device: Atmel → AT89C52.
  3. Include STARTUP.A51 (Yes).
  4. After adding main.c, copy and paste the code below.
  5. Options for Target → Debug → Use Simulator.
  6. Rebuild(F7) → Start Debug(Ctrl+F5).
  7. Observation: Display Peripherals → I/O-Ports → Port 0 (digit select) and Port 2 (segments). Or add P0.0~P0.3 and P2.0~P2.6 to View → Analysis Windows → Logic Analyzer.

Examples

Example 1 — main.c: 4-Digit Multiplexing

Turn on "1234" one digit at a time quickly alternately. To prevent afterimage, turn off → write → turn on for each digit.

#include <reg52.h>

/*
 *Lesson 12 Example — 4-digit 7-segment dynamic driving (multiplexing).
 *Target: AT89C52, Compiler: Keil C51.
 *
 *Wiring assumptions (common cathode):
 *- Segments a~g, dp connected to P2. Segment turns ON when bit=1.
 *       a=P2.0  b=P2.1  c=P2.2  d=P2.3  e=P2.4  f=P2.5  g=P2.6  dp=P2.7
 *- Digit selection via lower 4 bits of P0. Turns the digit ON when bit=1 (1 bit per digit).
 *       digit0=P0.0  digit1=P0.1  digit2=P0.2  digit3=P0.3
 *
 *Multiplexing: only one digit is ON at a time, cycling through the 4 digits very quickly.
 *Thanks to human eye persistence of vision, it appears as if all 4 digits are ON simultaneously.
 */

#define SEG_PORT  P2     /*Segment pattern output*/
#define DIG_PORT  P0     /*Digit selection*/

/*Common cathode 0~9 segment codes (bit=1 → segment ON).
 *Bit order: a(0) b(1) c(2) d(3) e(4) f(5) g(6) dp(7)*/
static const unsigned char SEG_CODE[10] = {
    0x3F, /* 0 */ 0x06, /* 1 */ 0x5B, /* 2 */ 0x4F, /* 3 */ 0x66, /* 4 */
    0x6D, /* 5 */ 0x7D, /* 6 */ 0x07, /* 7 */ 0x7F, /* 8 */ 0x6F  /* 9 */
};

/*Short software delay (digit hold time). For exact timing, refer to Lesson 9 timer.*/
static void delay(unsigned int count)
{
    unsigned int i;
    for (i = 0; i < count; i++) {
        /*Sending time*/
    }
}

void main(void)
{
    /*4 digits to display. digits[0] is the leftmost (digit 0). Here: "1234".*/
    unsigned char digits[4] = { 1, 2, 3, 4 };
    unsigned char d;

    while (1) {
        for (d = 0; d < 4; d++) {
            DIG_PORT = 0x00;                       /*1) Turn off all digits (to prevent ghosting)*/
            SEG_PORT = SEG_CODE[digits[d]];        /*2) Segment pattern for this digit*/
            DIG_PORT = (unsigned char)(1u << d);   /*3) Turn only this digit ON*/
            delay(150);                            /*4) Hold briefly then move to the next digit*/
        }
    }
}

Example 2 — pc_test.c: Lookup Table Verification

Without hardware, draw which segments light up for 0~9 in letters (capital letters = ON, - = OFF).

#include <stdio.h>

/*
 *Lecture 12 PC Verification — Outputs the common cathode segment codes for 0~9 and the segments that light up.
 *Checks only the accuracy of the lookup table without hardware.
 */

static const unsigned char SEG_CODE[10] = {
    0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};

/*Lit segments are indicated by the letters a~g, and off segments are indicated by '-'.*/
static void print_seg(unsigned char code)
{
    static const char names[7] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
    int i;
    for (i = 0; i < 7; i++) {
        putchar((code & (1u << i)) ? names[i] : '-');
    }
}

int main(void)
{
    int n;
    for (n = 0; n <= 9; n++) {
        printf("%d -> 0x%02X  ", n, SEG_CODE[n]);
        print_seg(SEG_CODE[n]);
        putchar('\n');
    }
    return 0;
}

Checking with Simulator

  • Port 2 (segments) changes in the order of digits: 0x06 (1) → 0x5B (2) → 0x4F (3) → 0x66 (4) ...
  • Port 0 (digit selection) shifts one bit at a time as 0x01 → 0x02 → 0x04 → 0x08, synchronized with the pattern and timing of P2.
  • Looking at it with a Logic Analyzer, at any given moment only one P0 pin is High, and the number pattern for that digit appears on P2.
  • Expected Behavior: (If it were a real display) "1234" appears stably across 4 digits. In simulation, it can be checked through synchronized cycles of P0/P2 values.

Verification on a PC

gcc pc_test.c -o pc_test && ./pc_test

Expected output (stdout):

0 -> 0x3F  ABCDEF-
1 -> 0x06  -BC----
2 -> 0x5B  AB-DE-G
3 -> 0x4F  ABCD--G
4 -> 0x66  -BC--FG
5 -> 0x6D  A-CD-FG
6 -> 0x7D  A-CDEFG
7 -> 0x07  ABC----
8 -> 0x7F  ABCDEFG
9 -> 0x6F  ABCD-FG

ABCDEF- (digit 0) has segments a~f lit and g off, meaning the middle bar is missing, forming the "0" shape.

Common Mistakes

Q. The digits appear overlapping or there is a faint afterimage.

A. This happens because the digits are not turned off before changing the segment pattern. If the pattern changes while a digit is on, the incorrect number may briefly appear in that position. Always follow the order: Turn off (DIG_PORT=0) → Write pattern → Turn on the digit.

Q. Only one digit is bright while the others are dim.

A. The hold time per digit (delay) is too long, or it stops on only one digit. All 4 digits need to cycle fast enough (usually 1~5 ms per digit, total refresh rate over 50Hz) to appear at the same brightness. In simulation, check whether P0 cycles through all 4 bits.

Q. The display is reversed (4321).

A. This occurs if the order of digits[] or the mapping of digit selection bits (1<<d) is reversed. Decide which physical digit digits[0] connects to and maintain consistency.

Q. I bought a common anode display, but all the segments turn on or all turn off.

A. This code assumes a common cathode (bit=1 is ON). For a common anode, it is the opposite of ON/OFF, so you need to invert the segment code (~). Try creating the conversion yourself in Exercise 1.

Q. I want to place the lookup table in ROM instead of RAM.

A. In Keil C51, if you add keywords like code similar to unsigned char code SEG_CODE[10] = {...};, it will be placed in program memory (ROM) to save RAM. This track used the regular const for portability, but if RAM is tight on the 8051, code is useful.

Summary

  • 7-segment displays draw numbers with a~g (+dp), and for common cathode, bit=1 is ON.
  • Number→pattern is quickly converted using the lookup table SEG_CODE[n].
  • Even when increasing the number of digits, sharing segment lines saves pins (only one digit active at a time).
  • Multiplexing: quickly alternating the active digits makes it appear that all are on due to persistence of vision.
  • Preventing ghosting: follow the sequence of off → write pattern → turn on digit.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/c51_seg7/main.cC
#include <reg52.h>

/*
 *Lesson 12 Example — 4-digit 7-segment dynamic driving (multiplexing).
 *Target: AT89C52, Compiler: Keil C51.
 *
 *Wiring assumptions (common cathode):
 *- Segments a~g, dp connected to P2. Segment turns ON when bit=1.
 *       a=P2.0  b=P2.1  c=P2.2  d=P2.3  e=P2.4  f=P2.5  g=P2.6  dp=P2.7
 *- Digit selection via lower 4 bits of P0. Turns the digit ON when bit=1 (1 bit per digit).
 *       digit0=P0.0  digit1=P0.1  digit2=P0.2  digit3=P0.3
 *
 *Multiplexing: only one digit is ON at a time, cycling through the 4 digits very quickly.
 *Thanks to human eye persistence of vision, it appears as if all 4 digits are ON simultaneously.
 */

#define SEG_PORT  P2     /*Segment pattern output*/
#define DIG_PORT  P0     /*Digit selection*/

/*Common cathode 0~9 segment codes (bit=1 → segment ON).
 *Bit order: a(0) b(1) c(2) d(3) e(4) f(5) g(6) dp(7)*/
static const unsigned char SEG_CODE[10] = {
    0x3F, /* 0 */ 0x06, /* 1 */ 0x5B, /* 2 */ 0x4F, /* 3 */ 0x66, /* 4 */
    0x6D, /* 5 */ 0x7D, /* 6 */ 0x07, /* 7 */ 0x7F, /* 8 */ 0x6F  /* 9 */
};

/*Short software delay (digit hold time). For exact timing, refer to Lesson 9 timer.*/
static void delay(unsigned int count)
{
    unsigned int i;
    for (i = 0; i < count; i++) {
        /*Sending time*/
    }
}

void main(void)
{
    /*4 digits to display. digits[0] is the leftmost (digit 0). Here: "1234".*/
    unsigned char digits[4] = { 1, 2, 3, 4 };
    unsigned char d;

    while (1) {
        for (d = 0; d < 4; d++) {
            DIG_PORT = 0x00;                       /*1) Turn off all digits (to prevent ghosting)*/
            SEG_PORT = SEG_CODE[digits[d]];        /*2) Segment pattern for this digit*/
            DIG_PORT = (unsigned char)(1u << d);   /*3) Turn only this digit ON*/
            delay(150);                            /*4) Hold briefly then move to the next digit*/
        }
    }
}
src/c51_seg7/pc_test.cC
#include <stdio.h>

/*
 *Lecture 12 PC Verification — Outputs the common cathode segment codes for 0~9 and the segments that light up.
 *Checks only the accuracy of the lookup table without hardware.
 */

static const unsigned char SEG_CODE[10] = {
    0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};

/*Lit segments are indicated by the letters a~g, and off segments are indicated by '-'.*/
static void print_seg(unsigned char code)
{
    static const char names[7] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
    int i;
    for (i = 0; i < 7; i++) {
        putchar((code & (1u << i)) ? names[i] : '-');
    }
}

int main(void)
{
    int n;
    for (n = 0; n <= 9; n++) {
        printf("%d -> 0x%02X  ", n, SEG_CODE[n]);
        print_seg(SEG_CODE[n]);
        putchar('\n');
    }
    return 0;
}

Assignment

Both problems are pure logic (PC gcc verification). Since the 7-segment display itself has no graphics in the simulator, the code conversion and digit decomposition corresponding to the "brain" of the display are precisely graded on the PC.

gcc main.c -o prog && ./prog

Problem 1 — Create a Common Anode Code Table

  • File name: homework_01/main.c
  • Key concepts: Common cathode ↔ common anode, bit inversion, 8-bit masking
  • Platform: Common (PC gcc)

Requirements

  • From the common cathode code table in the tutorial (0x3F, 0x06, …, 0x6F), create a common anode code table to display 0–9.
  • Since the common anode is the opposite of ON/OFF (bit=0 is ON), the anode code = bit inversion of the cathode code.
  • ~ results are split into unsigned char and output as two-digit 8-bit hexadecimal. The format n -> 0xNN must match the expected output character by character.

Expected output

0 -> 0xC0
1 -> 0xF9
2 -> 0xA4
3 -> 0xB0
4 -> 0x99
5 -> 0x92
6 -> 0x82
7 -> 0xF8
8 -> 0x80
9 -> 0x90

Hint

  • unsigned char anode = (unsigned char)~SEG_CC[n];
  • Without casting, ~ is expanded to int and output like 0xFFFFFFC0. This can be prevented with (unsigned char) or & 0xFF.
  • Output is printf("%d -> 0x%02X\n", n, anode);.

Problem 2 — Decompose the Number to be Displayed by Digit

  • File name: homework_02/main.c
  • Key concepts: Extracting digits from By-division/remainder, lookup table indexing
  • Platform: Common (PC gcc)

Requirements

  • Decompose variable n = 2025 into the four digits of Thousand/Hundred/Ten/One. digit[0] is the thousands place (leftmost).
  • Print each digit together with its common cathode segment code (SEG_CODE[place value]).
  • Format digit[i]=value code=0xNN must match the expected output character by character.

Expected output

digit[0]=2 code=0x5B
digit[1]=0 code=0x3F
digit[2]=2 code=0x5B
digit[3]=5 code=0x6D

Hint

  • Thousand = n/1000%10, Hundred = n/100%10, Ten = n/10%10, One = n%10.
  • If you omit %10, a two-digit value like 20 goes into the hundreds place.
  • Use the extracted digit value directly as the index for SEG_CODE[] (range 0~9).

Check your answers

After solving it yourself, compare it with the reference solution below. The solution files include comments on key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/main.cC
#include <stdio.h>

/*
 *Answer 1 — Creating a common anode segment code table
 *
 *Key Points:
 *- If a bit=1 was ON in the common cathode, it is the exact opposite in the common anode, where bit=0 is ON.
 *- Therefore, anode code = bit inversion (~) of the cathode code. Since we handle it in 8 bits,
 *use (unsigned char) to remove the upper bits garbage.
 *
 *Common mistake:
 *- If the ~ result is printed as int directly with 0x%X, it may come out like 0xFFFFFFC0.
 *→ This can be prevented by casting to (unsigned char) or masking with & 0xFF.
 */

static const unsigned char SEG_CC[10] = {  /*Common cathode 0~9*/
    0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};

int main(void)
{
    int n;
    for (n = 0; n <= 9; n++) {
        unsigned char anode = (unsigned char)~SEG_CC[n];
        printf("%d -> 0x%02X\n", n, anode);
    }
    return 0;
}
homework_02/main.cC
#include <stdio.h>

/*
 *Answer 2 — Break down the number to display by each digit and attach segment codes.
 *
 *Key Points:
 *- Extract the thousands/hundreds/tens/ones digits using division and remainder.
 *thousands = n / 1000 % 10, hundreds = n / 100 % 10, tens = n / 10 % 10, ones = n % 10
 *- Placing digit[0] at the far left (thousands place) matches the multiplexing order.
 *
 *Common mistake:
 *- Forgetting %10 can result in a two-digit number like 20 appearing in the hundreds place.
 *- If the digit order (left→right) is reversed, the display appears backward.
 */

static const unsigned char SEG_CODE[10] = {
    0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};

int main(void)
{
    unsigned int n = 2025;     /*Value to display*/
    unsigned char digit[4];
    int i;

    digit[0] = (unsigned char)(n / 1000 % 10);  /*Thousands*/
    digit[1] = (unsigned char)(n / 100  % 10);  /*Hundreds*/
    digit[2] = (unsigned char)(n / 10   % 10);  /*Tens*/
    digit[3] = (unsigned char)(n        % 10);  /*Ones*/

    for (i = 0; i < 4; i++) {
        printf("digit[%d]=%u code=0x%02X\n",
               i, (unsigned)digit[i], SEG_CODE[digit[i]]);
    }
    return 0;
}

Learn this lesson with a mentor

Work through the Embedded C track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now