news 2026/9/27 20:49:12

5个IO驱动20个LED:188数码管动态扫描与IO复用实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
5个IO驱动20个LED:188数码管动态扫描与IO复用实战

1. 项目缘起:为什么5个IO能点亮20个灯

第一次看到“188数码管”这个型号的时候,我下意识以为是某种1.88英寸的大尺寸数码管,查了资料才发现完全不是那么回事。188数码管其实是一种内部LED已经做了矩阵化复用的显示器件,它把20个发光段(或者叫20个灯)按照行和列的方式组织起来,外部只引出5根IO线。这5根线里,通常有3根负责“选列”,2根负责“选行”,通过行列交叉扫描的方式,理论上可以驱动3×2=6个交叉点,但188数码管内部还做了进一步的复用和极性切换,最终能点亮20个独立的段。

这个设计思路和键盘矩阵、LED点阵屏是一个道理:用时间换空间,用扫描换引脚。单片机最宝贵的资源之一就是IO口,尤其是51单片机,一个DIP40封装看起来引脚不少,但真正能自由使用的IO也就32个左右,还要分给串口、中断、定时器、外部存储器等外设。如果每个LED都独占一个IO,20个灯就要吃掉20个引脚,剩下的资源根本不够做其他事情。188数码管用5个IO解决20个灯的驱动问题,本质上是一种硬件层面的压缩编码。

我这次实战用的主控是STC89C52,也就是最经典的51单片机,晶振11.0592MHz,开发环境是Keil C51。选51单片机不是因为性能强,而是因为它足够简单、足够便宜,而且188数码管的扫描频率要求不高,51完全能胜任。如果你手头是STM32,逻辑是一样的,只是GPIO的配置方式从直接写寄存器变成了HAL库或者LL库调用,后面我会专门讲STM32下的移植要点。

这个项目适合谁看?如果你正在学单片机,已经会点亮单个LED,但还没做过矩阵扫描类的项目,那188数码管是一个非常好的进阶练习。它涉及IO口复用、动态扫描、定时器中断、视觉暂留这几个核心概念,搞懂之后再去玩LED点阵屏、数码管多路复用、甚至LCD的COM/SEG驱动,都会轻松很多。

提示:188数码管不是标准型号,不同厂家生产的188数码管内部结构可能不同,引脚定义和扫描时序也会有差异。拿到手第一件事是找卖家要规格书,或者用万用表二极管档逐个引脚测试,确认内部LED的连接方式。

2. 硬件设计:5个IO怎么分配才合理

2.1 188数码管的内部结构推测与验证

拿到188数码管实物后,我先用万用表二极管档做了一遍引脚扫描。具体方法是:红表笔固定在一个引脚上,黑表笔依次碰其他4个引脚,观察哪些段会微亮。反过来再测一遍。通过这种方法,我大致画出了内部的连接关系。

根据实测,188数码管的5个引脚中,有3个是“列选”引脚,2个是“行选”引脚。每个LED段连接在一个列选和一个行选之间,但并不是简单的3×2矩阵,因为那样只有6个交叉点。实际上,188数码管内部还利用了LED的单向导电特性,通过改变行列引脚的极性来实现更多组合。比如,当列选引脚输出高电平、行选引脚输出低电平时,点亮的是正向连接的LED;当列选输出低、行选输出高时,点亮的是反向并联的LED。这样3×2的物理矩阵就能扩展出12个甚至更多的逻辑段。

我实测的188数码管有20个段,具体分配是:3个列选引脚分别对应6、7、7个段,2个行选引脚则负责在这些段之间做分组。扫描的时候,先选中第一行,然后快速切换3个列选引脚,点亮该行下的所有段;再选中第二行,重复上述过程。只要扫描频率足够高,人眼看到的就是20个段同时亮起。

2.2 限流电阻的计算与选型

LED的限流电阻是必须的,不管你是用51单片机还是STM32。188数码管内部的LED正向压降一般在1.8V到2.2V之间,工作电流取3mA到5mA就足够亮了,太大电流会导致单片机IO口总电流超标。

以51单片机为例,STC89C52的单个IO口最大灌电流是20mA,整个芯片的总灌电流不能超过120mA。如果20个段同时点亮,平均每个段分到的电流只有6mA,再考虑到扫描占空比,实际瞬时电流可以稍微大一点。我选的是220Ω的限流电阻,计算过程如下:

假设LED正向压降Vf=2.0V,单片机IO口输出低电平VOL=0.3V,那么电阻上的压降是5V - 2.0V - 0.3V = 2.7V。取电流I=5mA,则R=2.7V/5mA=540Ω。但考虑到扫描占空比只有1/2或1/3,实际亮度会降低,所以我把电阻减小到220Ω,瞬时电流约12mA,平均电流约4mA到6mA,亮度刚好。

注意:如果你用的是STM32,IO口的驱动能力更强,但也要注意总电流限制。STM32F103的单个IO最大灌电流是25mA,所有IO加起来不能超过150mA。220Ω的电阻同样适用。

2.3 51单片机最小系统与188数码管的接线

我用的51单片机最小系统包括:STC89C52芯片、11.0592MHz晶振、两个22pF瓷片电容、一个10uF电解电容加一个10K电阻组成的复位电路。188数码管的5个引脚分别接到P1.0到P1.4,其中P1.0到P1.2作为列选,P1.3和P1.4作为行选。限流电阻串在列选引脚和数码管之间,每个列选引脚一个220Ω电阻。

这里有个细节:限流电阻放在列选还是行选?我选择放在列选引脚上,因为列选引脚在扫描过程中切换频率最高,放在这里可以保证每个段在任何时刻都有独立的限流。如果放在行选引脚上,当多个列同时选中时,行选引脚上的电流会叠加,可能导致电阻发热或压降变化。

接线完成后,我先写了一个最简单的测试程序:让P1.0输出低电平,P1.3输出高电平,其他引脚高阻态,观察哪个段亮。然后逐个组合测试,记录下每个段对应的引脚极性。这一步很关键,因为不同厂家的188数码管引脚定义可能完全不同,照搬网上的代码很可能点不亮。

3. 驱动原理:动态扫描与视觉暂留

3.1 动态扫描的基本原理

动态扫描的核心思想是:同一时刻只点亮一部分LED,通过快速轮换,利用人眼的视觉暂留效应,让人感觉所有LED都在同时亮。人眼的视觉暂留时间大约是1/24秒,也就是说,只要扫描频率高于24Hz,人眼就分辨不出闪烁。实际工程中,为了保险起见,扫描频率一般取50Hz以上,最好在100Hz到200Hz之间。

188数码管的扫描方式是这样的:一个完整的扫描周期分为2个行选阶段,每个行选阶段内再分3个列选阶段,总共6个状态。每个状态持续约1ms到2ms,整个周期就是6ms到12ms,对应扫描频率约83Hz到166Hz,完全满足视觉暂留的要求。

具体到代码层面,我用定时器0产生1ms的中断,在中断服务函数里切换扫描状态。每进一次中断,就切换到下一个状态,点亮对应的段。主循环里只需要更新显示缓冲区的内容,中断服务函数会根据缓冲区自动完成扫描。

3.2 定时器中断的配置与计算

51单片机的定时器0工作在模式1(16位定时器)时,定时时间的计算公式是:

定时时间 = (65536 - 初值) × 12 / 晶振频率

晶振频率是11.0592MHz,机器周期是12/11.0592MHz ≈ 1.085us。要定时1ms,需要的机器周期数是1ms / 1.085us ≈ 921.6,取整为922。初值就是65536 - 922 = 64614,转换成十六进制是0xFC66。所以TH0=0xFC,TL0=0x66。

在中断服务函数里,我定义了一个状态变量scan_state,取值范围0到5。每次中断,scan_state加1,如果超过5就归零。然后根据scan_state的值,设置P1口的输出状态。具体对应关系如下表:

scan_stateP1.0 (列1)P1.1 (列2)P1.2 (列3)P1.3 (行1)P1.4 (行2)点亮的段
0低高阻高阻高低行1列1
1高阻低高阻高低行1列2
2高阻高阻低高低行1列3
3低高阻高阻低高行2列1
4高阻低高阻低高行2列2
5高阻高阻低低高行2列3

提示:51单片机的P1口作为普通IO使用时,写1就是高电平,写0就是低电平。但188数码管需要“高阻态”来关闭某个列选,51单片机没有真正的高阻态输出模式,只能通过写1让引脚输出高电平,此时LED因为两端都是高电平而熄灭。严格来说这不是高阻,但效果一样。

3.3 显示缓冲区的设计

显示缓冲区是一个字节数组,每个字节对应一个段的状态。我定义了20个段,所以缓冲区大小是20字节。但实际上,由于扫描状态只有6个,每个状态对应多个段,所以缓冲区可以设计成6个字节,每个字节的每一位对应一个段。

更直观的做法是:定义一个unsigned char display_buf[6],其中display_buf[0]到display_buf[5]分别对应6个扫描状态。每个字节的8个位可以控制8个段,但188数码管每个状态最多只有7个段,所以完全够用。主循环里只需要修改这个数组,中断服务函数会根据数组的值决定P1口的输出。

比如,要点亮行1列1下的第3个段,就把display_buf[0]的第3位置1。中断服务函数在scan_state=0时,会读取display_buf[0],如果第3位是1,就设置P1口输出对应的电平组合;如果是0,就关闭该段。

这种设计的优点是主循环和中断服务函数解耦,主循环只管更新缓冲区,中断只管扫描,互不干扰。缺点是缓冲区到P1口输出的映射关系需要根据实际接线来调整,换一个厂家的188数码管可能就要改映射表。

4. 代码实现:从零搭建188数码管驱动

4.1 51单片机下的完整驱动代码

下面是我在STC89C52上实测通过的完整代码。代码分为三个部分:头文件和宏定义、定时器0中断服务函数、主函数。

#include <reg52.h> // 定义188数码管的引脚 sbit COL1 = P1^0; sbit COL2 = P1^1; sbit COL3 = P1^2; sbit ROW1 = P1^3; sbit ROW2 = P1^4; // 显示缓冲区,6个扫描状态,每个状态一个字节 unsigned char display_buf[6] = {0}; // 扫描状态变量 unsigned char scan_state = 0; // 定时器0初始化,1ms中断一次 void Timer0_Init() { TMOD &= 0xF0; // 清除定时器0的模式位 TMOD |= 0x01; // 定时器0工作在模式1,16位定时器 TH0 = 0xFC; // 初值高8位 TL0 = 0x66; // 初值低8位 ET0 = 1; // 使能定时器0中断 EA = 1; // 使能总中断 TR0 = 1; // 启动定时器0 } // 定时器0中断服务函数 void Timer0_ISR() interrupt 1 { // 重新加载初值 TH0 = 0xFC; TL0 = 0x66; // 先关闭所有段,防止拖影 COL1 = 1; COL2 = 1; COL3 = 1; ROW1 = 1; ROW2 = 1; // 根据scan_state设置引脚状态 switch(scan_state) { case 0: ROW1 = 1; ROW2 = 0; COL1 = 0; COL2 = 1; COL3 = 1; break; case 1: ROW1 = 1; ROW2 = 0; COL1 = 1; COL2 = 0; COL3 = 1; break; case 2: ROW1 = 1; ROW2 = 0; COL1 = 1; COL2 = 1; COL3 = 0; break; case 3: ROW1 = 0; ROW2 = 1; COL1 = 0; COL2 = 1; COL3 = 1; break; case 4: ROW1 = 0; ROW2 = 1; COL1 = 1; COL2 = 0; COL3 = 1; break; case 5: ROW1 = 0; ROW2 = 1; COL1 = 1; COL2 = 1; COL3 = 0; break; } // 根据display_buf的内容决定是否点亮对应的段 // 这里需要根据实际接线,把display_buf的位映射到具体的段 // 由于188数码管内部结构不同,这里省略具体映射,只保留框架 // 切换到下一个扫描状态 scan_state++; if(scan_state >= 6) { scan_state = 0; } } // 主函数 void main() { Timer0_Init(); // 初始化显示缓冲区,这里以点亮所有段为例 // 实际使用时,根据188数码管的内部结构,设置对应的位 display_buf[0] = 0xFF; display_buf[1] = 0xFF; display_buf[2] = 0xFF; display_buf[3] = 0xFF; display_buf[4] = 0xFF; display_buf[5] = 0xFF; while(1) { // 主循环里可以更新display_buf的内容 // 比如做流水灯、数字显示等效果 } }

这段代码的核心是中断服务函数里的扫描逻辑。每次中断先关闭所有段,然后根据scan_state设置行列引脚,最后根据display_buf决定是否点亮。关闭所有段这一步很重要,如果不做,切换状态时会出现“拖影”,也就是上一个状态的段还没完全熄灭,下一个状态的段就亮了,看起来像是多个段同时亮。

4.2 STM32下的移植要点

如果你用的是STM32,逻辑完全一样,但GPIO的配置方式不同。STM32的GPIO可以配置为推挽输出、开漏输出、上拉输入、下拉输入等模式。对于188数码管的驱动,我建议把列选引脚配置为推挽输出,行选引脚也配置为推挽输出。推挽输出的驱动能力强,高低电平切换速度快。

STM32的定时器配置比51复杂一些,但HAL库已经把底层寄存器操作封装好了。用STM32CubeMX生成代码的话,只需要配置一个定时器,开启中断,然后在中断回调函数里写扫描逻辑。定时器的预分频器和自动重装载值根据系统时钟计算,比如系统时钟72MHz,预分频器设为71,自动重装载值设为999,就是1ms中断一次。

STM32的GPIO操作可以用HAL_GPIO_WritePin函数,也可以用LL库直接写寄存器。如果追求扫描速度,建议用LL库或者直接操作BSRR寄存器,这样中断服务函数的执行时间更短,扫描更稳定。

注意:STM32的IO口在复位后默认是浮空输入状态,使用前必须配置为输出模式。另外,STM32的IO口有最大输出速度限制,配置为2MHz或10MHz就够了,50MHz反而会增加EMI。

4.3 显示效果优化:亮度调节与呼吸灯

188数码管的亮度可以通过调节扫描占空比来控制。比如,原本每个状态持续1ms,6个状态总共6ms。如果把每个状态的持续时间缩短到0.5ms,总周期变成3ms,扫描频率提高到333Hz,亮度会降低,但闪烁感更小。反过来,如果延长到2ms,总周期12ms,扫描频率83Hz,亮度提高,但接近视觉暂留的临界值,可能会有轻微闪烁。

更精细的亮度调节方法是PWM调光。在扫描状态内,再插入一个PWM周期,通过改变PWM的占空比来调节每个段的平均电流。比如,在1ms的扫描状态内,前0.5ms点亮,后0.5ms熄灭,亮度就是50%。这种方法需要更高的定时器频率,51单片机可能吃不消,但STM32完全没问题。

我实测下来,51单片机用1ms的扫描周期,亮度已经足够,不需要额外的PWM调光。如果觉得太亮,可以增大限流电阻;如果觉得太暗,可以减小限流电阻或者提高扫描频率。

5. 调试实录:踩过的坑与排查技巧

5.1 常见问题速查表

问题现象可能原因排查方法解决方法
所有段都不亮电源未接通或单片机未运行用万用表测单片机VCC和GND检查电源线和晶振电路
部分段不亮引脚接线错误或LED损坏逐个引脚测试二极管档重新接线或更换数码管
所有段同时亮,无法单独控制扫描频率太低或中断未生效用示波器测P1口波形检查定时器配置和中断使能
亮度不均匀限流电阻不一致或扫描占空比不同测量每个列选引脚的电流统一限流电阻,调整扫描状态持续时间
有拖影切换状态时未关闭所有段观察中断服务函数是否先关闭再设置在设置新状态前先关闭所有段
闪烁明显扫描频率低于50Hz计算扫描周期减小定时器初值,提高中断频率
单片机发热IO口总电流超标测量总电流增大限流电阻或减少同时点亮的段数

5.2 我踩过的三个坑

第一个坑:以为188数码管是共阴或共阳。刚开始我按照共阴数码管的思路,把列选引脚接低电平,行选引脚接高电平,结果只有部分段亮,而且亮度很奇怪。后来用万用表测才发现,188数码管内部不是简单的共阴或共阳结构,而是行列交叉加极性复用。这意味着你不能用驱动普通数码管的思路来驱动它,必须按照扫描的方式来。

第二个坑:限流电阻放在行选引脚上。我一开始把220Ω电阻放在行选引脚上,觉得这样只需要两个电阻,省事。结果发现,当多个列同时选中时,行选引脚上的电流叠加,导致电阻压降增大,LED亮度明显下降。后来把电阻移到列选引脚上,每个列选引脚一个电阻,亮度就均匀了。

第三个坑:中断服务函数里用了浮点运算。我一开始想在中断里做亮度调节,用了浮点数计算占空比。结果51单片机处理浮点运算很慢,中断服务函数执行时间超过了1ms,导致扫描频率下降,出现闪烁。后来把浮点运算改成查表法,用整数运算,问题解决。

提示:51单片机的中断服务函数应该尽可能短小精悍,避免浮点运算、长循环、函数调用。所有复杂的计算都放在主循环里,中断里只做最简单的状态切换和IO操作。

5.3 用示波器验证扫描波形

调试扫描类项目,示波器是最有用的工具。我把示波器的探头接在列选引脚上,观察波形。正常的扫描波形应该是:每个列选引脚在1/6的时间里输出低电平,其余时间输出高电平。如果波形不对称,说明扫描状态分配不均匀;如果波形有毛刺,说明中断服务函数里有干扰。

如果没有示波器,也可以用逻辑分析仪,价格便宜很多,而且能同时抓多路信号。我用的是一款8通道的逻辑分析仪,配合开源软件,能清楚地看到6个扫描状态的切换顺序和持续时间。

6. 扩展玩法:从188数码管到更多显示项目

6.1 用188数码管做电子时钟

188数码管有20个段,足够显示4位数字(每位5个段,其中7段数码管需要7个段,但188数码管的段不是标准7段排列,需要重新设计字形)。我试过用188数码管做一个简单的电子时钟,显示“HH:MM”格式。由于段数不够,只能显示小时和分钟,秒用一个小圆点闪烁表示。

字形设计是个体力活。我把每个数字需要的段列出来,然后映射到display_buf的对应位。比如数字“0”需要点亮6个段,数字“1”需要点亮2个段。映射表做好之后,显示就很简单了,主循环里只需要根据当前时间更新display_buf。

6.2 移植到STM32做PWM调光

51单片机的IO口驱动能力有限,做PWM调光比较吃力。STM32的定时器资源丰富,可以轻松实现16位PWM。我把188数码管接到STM32F103C8T6上,用TIM1的4个通道分别控制4个列选引脚,用TIM2做扫描定时器。这样每个列选引脚的亮度都可以独立调节,实现了真正的PWM调光。

STM32的代码结构比51复杂,但HAL库把底层封装得很好。用STM32CubeMX配置好定时器和GPIO后,生成的代码框架里只需要填充中断回调函数和主循环逻辑。我实测下来,STM32的扫描频率可以做到1kHz以上,完全没有闪烁,亮度调节也很平滑。

6.3 188数码管与51单片机串口通讯结合

热词里提到了“51单片机串口通讯使用那个定时器”,这个问题在188数码管项目里也会遇到。51单片机只有一个串口,通常用定时器1做波特率发生器,定时器0做扫描定时器。这样两个定时器各司其职,互不干扰。

我试过用串口接收电脑发来的显示数据,然后更新188数码管的显示内容。串口中断的优先级要低于定时器0中断,否则扫描会卡顿。在Keil C51里,可以通过IP寄存器设置中断优先级。定时器0设为高优先级,串口设为低优先级,这样扫描不会被打断。

注意:51单片机的串口中断和定时器中断共享中断向量,如果串口中断服务函数执行时间过长,会影响扫描频率。建议串口中断里只做数据接收,把数据处理放在主循环里。

6.4 从188数码管到LED点阵屏的进阶

搞懂了188数码管的行列扫描原理,再去玩LED点阵屏就很容易了。LED点阵屏本质上就是188数码管的放大版:8×8点阵有8行8列,需要16个IO,但通过74HC595等移位寄存器可以进一步减少IO数量。扫描原理完全一样,只是点阵屏的段数更多,扫描频率要求更高。

我后来用51单片机加74HC595驱动了一块16×16的LED点阵屏,显示滚动文字。核心代码和188数码管几乎一样,只是把display_buf从6字节扩展到了32字节,扫描状态从6个扩展到了16个。定时器中断频率提高到200us一次,扫描频率约312Hz,显示效果很稳定。

7. 个人经验总结与实操建议

这个项目我断断续续做了大概一周,从最开始的“点不亮”到后来的“稳定显示”,中间踩了不少坑。最大的体会是:扫描类项目的核心不是代码,而是硬件时序。代码写得再漂亮,如果硬件接线不对、限流电阻选错、扫描频率不合适,照样点不亮。

如果你准备动手做188数码管驱动,我的建议是:先不要急着写代码,先用万用表把数码管的内部结构测清楚,画出引脚映射表。然后写一个最简单的测试程序,逐个点亮每个段,确认硬件没问题。最后再写扫描代码,从低频开始调试,用示波器或逻辑分析仪观察波形,逐步提高扫描频率。

另外,188数码管的型号比较杂,不同厂家的产品可能完全不兼容。我买了三家不同店铺的188数码管,内部结构都不一样。所以网上的代码只能参考,不能照搬。一定要根据自己手头的实物来调整引脚定义和扫描时序。

最后分享一个小技巧:如果觉得20个段太多,调试起来麻烦,可以先只接5个段,把扫描逻辑跑通,再逐步增加段数。这样出问题的时候容易定位,不会一上来就被20个段搞晕。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 20:47:05

科研组会PPT设计:面向思维表达的结构化模板

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 20:46:24

拆解BOM选型:安世、唯捷创芯、纽迪瑞三类国产芯片实战策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华