5个IO驱动18颗LED显示“188”:查理复用原理与实战

发布时间:2026/9/17 2:52:41
5个IO驱动18颗LED显示“188”:查理复用原理与实战 最近在做一个小设备上面要固定显示“188”这个三位数字——就是价格牌、倒计时器、工装治具上很常见的那种显示需求。按常规玩法驱动三位数码管至少要7根段选线加3根位选线也就是10个IO。可我手上这颗TM52F1363总共也没剩几个IO其他功能还得占线于是我把主意打到了查理复用Charlieplexing上。最后的结果是5个IO口驱动18颗LED拼出了“188”这个显示单元。先把丑话说前面这不是5个IO驱动188个数码管5个IO的查理复用理论极限是20颗LED。“188”在这个项目里指的是显示目标整块显示单元由18颗独立LED组成。本文从原理、接线、代码到实测踩坑全部记录一遍给同样在IO紧张里挣扎的嵌入式朋友一份能直接抄的作业。1. “188”到底是什么需求来源与硬边界1.1 三个字符背后的LED账本项目场景是一个小型控制面板需要固定显示“188”三个字符。拆开看左边的“1”只需要2段LED中间和右边的“8”各需要7段LED这就已经是16颗了。如果还希望在小数点位置做文章比如显示成“18.8”或“188.”这种价格格式就得再加2颗小数点LED合计18颗。这18颗LED就是整个项目的硬件基础。为什么强调这个数字因为5个IO的查理复用刚好能驱动5×(5-1)20颗LED18颗意味着方案要贴着上限走多一颗都塞不进去这算是查理复用的一次“极致压榨”。1.2 一个容易踩的前提成品数码管做不了查理复用很多人看到“数码管”三个字下意识就拿起抽屉里的共阴三位数码管结果怎么接都点不亮最后跑来问是不是算法写错了。问题不在算法在硬件结构。成品共阴数码管把所有LED段的阴极在内部连到一起引出一个公共端共阳数码管则是所有阳极共用。这种结构下每颗LED只有一端是独立的另一端钉死在公共端上。而查理复用的前提是每颗LED的两端都能接到任意两个不同的IO上通过“一个IO输出高、另一个IO输出低”来点亮它。成品数码管是星形连接不是两两交叉的网络自然没法套查理复用。所以这个项目用的是独立LED在PCB上按“188”的字形摆放每一颗LED的阳极和阴极都单独引线这是查理复用的正确打开方式。如果你非要用成品数码管也得选那种每段LED引脚全独立出来的特殊封装普通共阴共阳管直接放弃。2. 查理复用原理5个IO为什么能点亮18颗LED2.1 三态IO是基础不是可选项平时写单片机程序IO口要么输出高、要么输出低最多再读个输入三态这个概念用得不多。查理复用恰恰把第三种状态——高阻输入态当成了核心工具。点亮一颗LED的本质是什么一颗LED的阳极接IOA阴极接IOBIOA输出高、IOB输出低电流从A穿过LED流到B这颗LED就亮了。如果这时候有第三个IO还在输出低电平而它恰好和IOA之间也有一颗LED就会出现一条不该有的电流通路这就是鬼影的雏形。所以查理复用的铁律是任意时刻只能有一对IO处于输出状态其余全部切成高阻输入把引脚和内部电路完全断开不让电流有任何可抄的近路。2.2 n×(n-1)是怎么算出来的查理复用的容量推导一点也不玄乎。5个IO口先挑一个输出高有5种选法再从剩下的4个里挑一个输出低有4种选法。5×420这就是5个IO能驱动的LED数量上限。关键在于这20个组合并不是20个无序点对而是有序点对。拿IOA和IOB这对来说“A高B低”能点亮一颗LED“B高A低”能点亮另一颗LED。所以每两个IO之间可以放两颗极性相反的LED5个IO一共C(5,2)10对每对两颗总共20颗。打个比方5个人两两握手不区分谁主动就是10种组合区分谁先伸手就是20种每只“主动的手”都能点亮一颗LED。也正因为每个LED都对应一个“谁高谁低”的组合所以查理复用不需要额外的行列选通信号这是它比传统矩阵扫描省IO的根本原因。2.3 不能同时点亮两颗以上只能用扫描换视觉既然同一时刻只能有一对IO在驱动那一轮扫描就只能点一颗LED整个“188”数字是靠快速轮流点亮各段、利用人眼视觉暂留“骗”出来的。你看到的是一个完整的“188”实际上每一颗LED都在以极低的占空比工作。这个特点决定了方案的亮度天花板占空比低平均亮度就低要补偿亮度就得拉高峰值电流但峰值电流又受IO驱动能力和LED额定电流限制。这一堆矛盾会在第5节详细说这里先记住结论——查理复用本质上是“用时间换引脚”。2.4 和传统方案的对比驱动方式5个IO口可驱动的LED数量说明直接驱动约2颗每颗LED独占两个IO行列扫描4~6颗5个IO做2×2矩阵已经很勉强查理复用20颗每对IO双向利用这张表能看出查理复用在引脚利用率上的碾压优势。但这个优势不是免费的代价是刷新逻辑复杂、亮度偏低、不能同时点亮多颗LED。项目里要权衡清楚不要盲目上。3. 18颗LED怎么挂到5根线上接线表与电阻方案3.1 LED与IO组合的映射关系设5根线为A、B、C、D、E对应MCU的P1.0到P1.4。18颗LED的分配方案我做了下面这张表每行代表一颗LED箭头方向表示点亮时的电流方向也就是“阳极接X、阴极接Y”。段名所属字符点亮时电流方向说明L_B左边“1”A→B左上竖L_C左边“1”B→A右上竖M_A中间“8”A→C上横M_B中间“8”C→A左上竖M_C中间“8”A→D右上竖M_D中间“8”D→A中横M_E中间“8”A→E左下竖M_F中间“8”E→A右下竖M_G中间“8”B→C下横R_A右边“8”C→B上横R_B右边“8”B→D左上竖R_C右边“8”D→B右上竖R_D右边“8”B→E中横R_E右边“8”E→B左下竖R_F右边“8”C→D右下竖R_G右边“8”D→C下横DP1小数点1C→E“18.8”的小数点DP2小数点2E→C“188.”的小数点A-B、A-C、A-D、A-E、B-C、B-D、B-E、C-D、C-E这9对IO全用满了每对正反两个方向各一颗。剩下的D-E这对IO暂时空着以后想加状态灯或者复用按键正好有地方接。这张映射表不是唯一的只要每个LED都占用一个唯一的有序组合就行不重不漏即可。PCB布线时注意一点5根干线从MCU出来后在显示区走一圈每颗LED就近挂在对应两根线上。距离差异会造成走线电阻不同但LED是毫安级电流几厘米的走线压降基本可以忽略不用刻意做等长。3.2 每对IO只需要一颗限流电阻刚上手查理复用的人最容易踩的坑就是给每颗LED单独配一个限流电阻。做完才发现同一对IO上的两颗反并联LED完全可以共用一个电阻。道理很简单同一时刻只会有一颗导通。当A高B低时电流走A→R→LED1→B当B高A低时电流走B→LED2→R→A。两颗LED极性相反天然互斥电阻串在两根IO之间的公共支路上两个方向都能限流。所以10对IO只需要10颗电阻而不是18颗。板子面积和BOM成本都能省一大截。IOA ---[R]---*---[LED1]---*--- IOB | | *---[LED2]---*LED1阳极朝左、LED2阳极朝右R是公共限流电阻。这个结构就是查理复用最小的硬件单元。3.3 电阻取值和亮度预估电阻取值得先定峰值电流。扫描方式下LED是脉冲点亮的假设一轮扫描18步每颗LED亮1步占空比就是1/18。想让平均亮度接近1.5mA的直流效果峰值电流就得做到1.5×1827mA——这个数字对很多LED和IO口都已经偏大了。所以实际项目里一般把平均亮度目标定低些比如峰值电流12~15mA室内显示完全够看。红色LED在3.3V供电下的压降大约2.0V再算上IO内部的压降约0.2V限流电阻取R ≈ (3.3 - 2.0 - 0.2) / 0.012 ≈ 92Ω市面上取标称100Ω就很合适。如果系统是5V供电同样的思路算下来大约220~330Ω。蓝、绿、白LED压降在3.0V左右3.3V供电时留给电阻的压降空间非常小要么电阻取到10Ω以下基本接近直连要么提高供电电压否则亮度很难做上去。这也是为什么查理复用方案里我最推荐用红色LED。4. TM52F1363上的驱动实现4.1 三态IO在TM52系列上的配置思路TM52F1363是十速科技的增强型8051内核MCU工作电压宽、价格低做这种IO紧张的小项目很合适。它的IO口模式通过端口配置寄存器切换不同封装的数据手册上寄存器命名和位定义略有差异但逻辑都是同一套把引脚配成推挽输出、准双向、开漏或高阻输入。查理复用关心的就是推挽输出和高阻输入之间的快速切换。这里给一份通用风格的代码示意寄存器名以你手头芯片手册为准// 以TM52系列端口配置寄存器为例实际命名请参照数据手册 #define CHARLIE_PIN_MASK 0x1F // 低5位对应P1.0 - P1.4 // 把5个IO全部切成高阻输入 void charlie_all_input(void) { P1M1 | CHARLIE_PIN_MASK; P1M0 | CHARLIE_PIN_MASK; P1 ~CHARLIE_PIN_MASK; } // 点亮 anode 为高、cathode 为低的这颗LED void charlie_on(unsigned char anode, unsigned char cathode) { charlie_all_input(); // 先全部高阻关断上一颗 P1 | (1 anode); // 高脚预写1 P1 ~(1 cathode); // 低脚预写0 // 只把目标两脚切到推挽输出其余保持高阻 P1M1 ~((1 anode) | (1 cathode)); P1M0 | ((1 anode) | (1 cathode)); }这里的操作顺序是刻意的先把所有IO切高阻再写端口数据寄存器最后才把目标两脚切到推挽输出。顺序反了的话IO还处于输出状态时写端口数据可能会瞬间输出一个错误电平点亮不该亮的LED。4.2 段码表和定时器扫描显示缓冲区用一个18字节的数组led_buf[]1代表这一颗LED要亮。扫描函数在定时器中断里跑每次中断只点亮一颗LED18次中断走完一轮。// 接线表每一行是LED的阳极、阴极所在IO编号按第3节的映射表填写 const unsigned char led_table[18][2] { {0,1}, {1,0}, {0,2}, {2,0}, {0,3}, {3,0}, {0,4}, {4,0}, {1,2}, {2,1}, {1,3}, {3,1}, {1,4}, {4,1}, {2,3}, {3,2}, {2,4}, {4,2} }; unsigned char led_buf[18]; unsigned char cur_idx 0; // 每次定时器中断调用一次 void charlie_step(void) { unsigned char i; charlie_all_input(); // 关掉上一个LED留出死区 for (i 0; i 18; i) { cur_idx (cur_idx 1) % 18; if (led_buf[cur_idx]) { break; } } if (led_buf[cur_idx]) { charlie_on(led_table[cur_idx][0], led_table[cur_idx][1]); } }这段代码的巧妙之处在于不论当前有哪些LED需要亮中断周期都是固定的。cur_idx始终在18个位置里循环灭着的段被跳过亮的段被点亮所以整个扫描帧周期恒定为18个中断周期不会出现“亮的LED越多刷新率越低”的问题。4.3 点亮“188”和主循环固定显示“188”时只需要初始化一次显示缓冲区void init_display_188(void) { // 左边“1”L_B、L_C亮 led_buf[0] 1; led_buf[1] 1; // 中间“8”M_A ~ M_G全亮 led_buf[2] led_buf[3] led_buf[4] 1; led_buf[5] led_buf[6] led_buf[7] led_buf[8] 1; // 右边“8”R_A ~ R_G全亮 led_buf[9] led_buf[10] led_buf[11] 1; led_buf[12] led_buf[13] led_buf[14] led_buf[15] 1; // 小数点先不亮 led_buf[16] 0; led_buf[17] 0; }初始化定时器把中断周期设在250μs左右一轮扫描18步就是4.5ms等效刷新率约222Hz肉眼看不出闪烁。主循环只需要开全局中断然后该干嘛干嘛显示完全由中断维护void main(void) { timer0_init_250us(); // 初始化定时器250us中断一次 init_display_188(); EA 1; // 开总中断 while (1) { // 其他业务逻辑 } }如果以后想让数字变化直接改led_buf里的内容就行。不过要记住第一位的硬件只接了2段它没法显示除“1”以外的数字这是“188专用显示”的天然限制。5. 实测中的三个坑鬼影、亮度不匀、IO发热5.1 鬼影切换瞬间的短路路径查理复用跑起来最容易看到的现象是“毛刺”就是不该亮的LED隐约亮一下这叫鬼影。根源在IO切换的瞬间假设上一颗LED是A高B低下一颗是C高D低如果B还没来得及从低电平释放C已经输出高电平而B和C之间恰好还有一颗LED它就会在这个瞬间被点亮。解决办法就是4.1节代码里已经体现的每一次切换之前先把全部IO切成高阻留出几个NOP的死区时间再切目标引脚。高阻状态下所有输出路径都断开了旧状态不会和新状态交叉。实测下来只要保证“全高阻→死区→输出”这个顺序鬼影基本能消除。如果还看到轻微鬼影在charlie_all_input之后加三五个空指令效果立竿见影。5.2 亮度忽明忽暗帧周期不稳定的连锁反应查理复用方案里有一类隐蔽的亮度问题如果你用“只扫描需要亮的LED”的思路——比如这次的18颗LED只亮了16颗你让扫描函数跳过灭的两颗直接缩短一轮的步数——那么显示不同数字时因为亮的颗数不同每颗LED的占空比会变化看起来就是亮度跟着显示内容一起跳变。这也是我坚持固定扫描18步、灭的段也占一个中断周期的原因。不管当前实际亮了多少颗每颗LED的点亮间隔都是固定的18个中断周期占空比恒定亮度自然稳定。这个经验是拿示波器一点一点跟出来的建议直接抄。5.3 IO发热和电流极限查理复用在同一时刻只有2个IO在出力看上去单片IO的电流压力不大。但要注意峰值电流取值红色LED压降按2.0V算3.3V供电、100Ω电阻时峰值电流约11mAIO口一般能承受20~30mA单看没有风险。问题是脉冲电流的瞬时速率比较高加上扫描切换瞬间多个引脚同时翻转如果供电走线细、退耦电容离芯片远就可能出现VDD跌落或者地弹表现为亮度闪烁甚至系统复位。应对措施有三条一是电源引脚旁边放一个100nF和一个10μF电容位置尽量贴近MCU二是峰值电流不要一味往30mA顶15mA以内是比较舒服的工作点三是如果扫描时发现IO发烫优先检查是不是某颗LED接反了——接反的LED虽然不亮但反向击穿时可能把电流导到别的地方产生额外损耗。还有一个不常见的坑高速扫描时LED结电容和走线电感形成的振铃可能在IO上产生超过VDD的尖峰电压。低压MCU余量不大时可以在靠近IO的地方加一个5V肖特基二极管做钳位但这属于锦上添花低速扫描基本用不上。6. 往下走从5个IO到6个IO以及按键复用6.1 6个IO能直接把通用三位数码管拿下5个IO的20颗LED容量卡死了“全功能三位数码管”的路但如果IO多一点比如6个6×(6-1)30颗LED情况就完全不一样。一套标准的3位7段数码管不带小数点是21颗LED带3个小数点是24颗全塞进30颗的容量里还有富余。这意味着只要把IO从5个加到6个就能把“188专用显示”升级成“任意三位数字显示”接线和代码逻辑一模一样只是映射表变长、扫描步数变多。如果你在选型阶段手里还有什么功能要和显示抢IO强烈建议算一笔账多留1个IO显示方案的天花板能从专用跳到通用。6.2 空闲IO复用出按键功能第3节里空出的D-E这对IO除了接状态灯还能用来做按键检测。思想还是那个“交叉”的思路显示扫描期间D-E不参与任何LED驱动可以完全置为高阻在按键检测窗口把D输出高、E输出低如果它们之间被一个按键短接IOE就能读到低电平。反过来E高D低又能识别另一个方向的按键。类似的拓扑再配合IO的内部上下拉几个IO就能撬动比IO数量更多的按键这就是网上常说的“两个IO口做4个按键”的原理。做按键复用要特别注意时序隔离检测按键时必须先把所有IO切成高阻确保LED完全关断再切到按键检测模式。否则LED支路会对电平判断形成干扰按一次键可能出现闪好几下LED的怪现象。这几个功能都放在定时器的不同时间片里做逻辑上不冲突。我在这个项目上最大的体会是查理复用的原理确实不复杂但真正决定成败的是硬件结构。第一次我拿着成品共阴数码管试按常规思路怎么接都不对后来才想明白每颗LED必须独立引出两端这个前提。把映射表和电阻方案先定死代码反而是最顺理成章的一步。如果大家也想在TM52F1363或者其他51内核MCU上复刻建议先把LED接线图画清楚再动手顺序一旦搞反排查鬼影和亮度问题会让你怀疑人生。