Many people encounter the issue of "ghosting" when implementing multiplexed scanning to drive seven-segment displays. Recently, I had a project that used multiplexed scanning, so I decided to write an article documenting the process for others to reference in the future.
To explain the specifics of "ghosting," you can refer to the comparison images below. The left image shows each digit with a "ghost" of the adjacent right digit, whereas the right image demonstrates a properly handled multiplexed scanning process.

This project's requirements were not complicated: the client wanted a square wave generator with an adjustable output frequency of 1~6 kHz, along with a power-off memory feature. After restoring power, the device should output the last adjusted frequency. Of course, the frequency adjustment should be visible to the user, so I opted for a four-digit seven-segment display.
For this project, I chose the
Nuvoton MG51FB9AE because it operates at 24 MHz, whereas the commonly used N76S003 runs at 16 MHz. These two chips are pin-to-pin compatible. The higher clock speed is needed not for CPU performance but for output frequency resolution. The higher the input frequency for PWM or Timer, the greater the division steps for the output, and consequently, the higher the frequency resolution. Readers who require precise output frequency or duty cycle should consider this aspect in their selection criteria.
What is multiplexed scanning?The schematic below is part of the project's circuit. It has an
Everlight ELF-511 - a standard four-digit common cathode seven-segment display. The segments a~g and dot of each digit are directly connected inside the display, while the common cathode for each digit is separate.

The signal from P0 will only activate one of Q1~Q4 at a time, and while active, P1 sends corresponding data for segments a~g and dot. In short, this process lights up each digit in turn. When the switching speed exceeds the persistence of vision, all digits appear continuously illuminated.
Advantages of multiplexed scanningIf each digit of the four-digit seven-segment display were independently controlled, its common points would directly connect to the power supply, and requiring 32 pins to control (4 × segments a~g and dot).
With multiplexed scanning, only 12 pins are needed: 8 for segments a~g and dot and 4 for scanning lines. This reduction is significant for design, benefiting BOM cost, circuit size, and layout complexity. For example, with just 12 pins, the project can be completed using the MG51FB9AE in TSSOP20 package. If 32 pins were required, an MCU with at least a 48-pin package would be necessary.
Firmware implementationFirst, an array `u8segments[16]` is created to map 0~F (hexadecimal 0x0~0xF). Through modulo operations and a switch-case statement, the units, tens, hundreds, and thousands digits of a number `u32num` are extracted, looked up in `u8segments[]`, and sent to P1. The switch-case selects the current digit based on `u8digit`, which is simultaneously output to P0 as the scanning line signal. The variable `u8digit` shifts left one position with each iteration (e.g., 0x10 -> 0x20 -> 0x40 -> 0x80) before cycling back to 0x10. This process synchronizes the active digit with the number being output to P1. The entire routine is wrapped into a `showNum()` function, which is repeatedly called every 1 ms to complete the scanning process.
Common mistakes: Incorrect sequence causing ghostingSwitching scanning lines before updating the segment data can lead to ghosting. Since the program executes sequentially, so when the scanning line switches to the next digit, the previous digit's data is still being output, causing the previous digit to briefly appear. Observing the implementation, the scan moves from right to left, which explains the shadowing from the adjacent right digit.

void showNum(uint32_t u32num)
{
static uint8_t u8digit = 0x10;
P0 = u8digit;
switch (u8digit) {
case 0x10:
P1 = u8segments[u32num % 10];
break;
case 0x20:
P1 = u8segments[u32num / 10 % 10];
break;
case 0x40:
P1 = u8segments[u32num / 100 % 10];
break;
case 0x80:
P1 = u8segments[u32num / 1000];
break;
default:
u8digit = 0x10;
}
if (u8digit < 0x80)
u8digit = u8digit << 1;
else
u8digit = 0x10;
}
Correct implementationTo resolve this, the output must be briefly disabled before switching to the next digit to prevent the previous digit from momentarily displaying. In this case, I first set `P0 = 0;` to disable the scanning line output, then update `P1` with the segment data for the next digit. Finally, I set `P0 = u8digit;` to activate the scanning line for the new digit. Alternatively, the output can be briefly disabled by turning off the data lines. A possible sequence is as follows:
Disable scanning lines > Output new data > Update scanning lines
Disable data lines > Update scanning lines > Output new data

void showNum(uint32_t u32num)
{
static uint8_t u8digit = 0x10;
P0 = 0;
switch (u8digit) {
case 0x10:
P1 = u8segments[u32num % 10];
break;
case 0x20:
P1 = u8segments[u32num / 10 % 10];
break;
case 0x40:
P1 = u8segments[u32num / 100 % 10];
break;
case 0x80:
P1 = u8segments[u32num / 1000];
break;
default:
u8digit = 0x10;
}
P0 = u8digit;
if (u8digit < 0x80)
u8digit = u8digit << 1;
else
u8digit = 0x10;
}
Full code:
#include "numicro_8051.h"
__code const uint8_t u8segments[16] = {
// abcdefg.
0b11111100, 0b01100000, 0b11011010, 0b11110010, 0b01100110, 0b10110110, 0b00111110, 0b11100000,
0b11111110, 0b11100110, 0b11101110, 0b00111110, 0b10011100, 0b01111010, 0b10011110, 0b10001110
};
void showNum(uint32_t u32num)
{
static uint8_t u8digit = 0x10;
P0 = 0;
switch (u8digit) {
case 0x10:
P1 = u8segments[u32num % 10];
break;
case 0x20:
P1 = u8segments[u32num / 10 % 10];
break;
case 0x40:
P1 = u8segments[u32num / 100 % 10];
break;
case 0x80:
P1 = u8segments[u32num / 1000];
break;
default:
u8digit = 0x10;
}
P0 = u8digit;
if (u8digit < 0x80)
u8digit = u8digit << 1;
else
u8digit = 0x10;
}
void main(void)
{
uint32_t u32num = 1234;
TA = 0xAA;
TA = 0x55;
CKEN |= 0xC0; // EXTEN
while(!(CKSWT & 0x08)); // ECLKST
TA = 0xAA;
TA = 0x55;
CKSWT |= 0x02; // Switch to external clock source
while(CKEN & 0x01); // CKSWTF
TA = 0xAA;
TA = 0x55;
CKEN &= 0xDF; // Disable HIRC
P0M1 &= 0x0F;
P0M2 |= 0xF0;
/* P1 as push-pull output mode */
P1M1 = 0x00;
P1M2 = 0xFF;
CKCON |= 0x08; // Timer 0 source from Fsys directly
TH0 = (uint8_t)(41536 >> 8); // 65536 - 24000
TL0 = (uint8_t)(41536 & 0xFF);
TMOD |= 0x01; // Timer 0 mode 1
TCON |= 0x10; // Timer 0 run
while(1)
{
showNum(u32num);
/* 1 ms delay */
while (!(TCON & 0x20)); // Wait until timer 0 overflow
TH0 = (uint8_t)(41536 >> 8); // 65536 - 24000
TL0 = (uint8_t)(41536 & 0xFF);
TCON &= 0xDF; // Clear TF0
}
}