5个IO驱动20个LED:188数码管动态扫描与IO复用实战
1. 项目缘起为什么5个IO能点亮20个灯第一次看到“188数码管”这个型号的时候我下意识以为是某种1.88英寸的大尺寸数码管查了资料才发现完全不是那么回事。188数码管其实是一种内部LED已经做了矩阵化复用的显示器件它把20个发光段或者叫20个灯按照行和列的方式组织起来外部只引出5根IO线。这5根线里通常有3根负责“选列”2根负责“选行”通过行列交叉扫描的方式理论上可以驱动3×26个交叉点但188数码管内部还做了进一步的复用和极性切换最终能点亮20个独立的段。这个设计思路和键盘矩阵、LED点阵屏是一个道理用时间换空间用扫描换引脚。单片机最宝贵的资源之一就是IO口尤其是51单片机一个DIP40封装看起来引脚不少但真正能自由使用的IO也就32个左右还要分给串口、中断、定时器、外部存储器等外设。如果每个LED都独占一个IO20个灯就要吃掉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正向压降Vf2.0V单片机IO口输出低电平VOL0.3V那么电阻上的压降是5V - 2.0V - 0.3V 2.7V。取电流I5mA则R2.7V/5mA540Ω。但考虑到扫描占空比只有1/2或1/3实际亮度会降低所以我把电阻减小到220Ω瞬时电流约12mA平均电流约4mA到6mA亮度刚好。注意如果你用的是STM32IO口的驱动能力更强但也要注意总电流限制。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工作在模式116位定时器时定时时间的计算公式是定时时间 (65536 - 初值) × 12 / 晶振频率晶振频率是11.0592MHz机器周期是12/11.0592MHz ≈ 1.085us。要定时1ms需要的机器周期数是1ms / 1.085us ≈ 921.6取整为922。初值就是65536 - 922 64614转换成十六进制是0xFC66。所以TH00xFCTL00x66。在中断服务函数里我定义了一个状态变量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列11高阻低高阻高低行1列22高阻高阻低高低行1列33低高阻高阻低高行2列14高阻低高阻低高行2列25高阻高阻低低高行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_state0时会读取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工作在模式116位定时器 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数码管的亮度可以通过调节扫描占空比来控制。比如原本每个状态持续1ms6个状态总共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个段搞晕。