资讯动态

5根GPIO驱动20颗LED:查理复用原理与STM8实战

发布时间:2026/10/15 16:56:34 来源:尧图企业网站定制
简介这份资源面向使用STM8单片机进行LED扩展控制的开发者尤其是需要以少量IO口驱动多颗LED的嵌入式初学者与进阶者。核心方案采用查理复用技术仅用5个GPIO即可独立控制20个LED解决了IO资源紧张场景下的多灯独立控制问题可应用于电量阶梯显示、流水灯及各类LED显示项目。压缩包共3个文件包含1个C源文件、1个头文件和1张效果图整体约474KB源码结构精简头文件中通过宏定义即可更换IO口控制单个LED只需在参数表中选择同时亮灭多个LED则需配置扫描逻辑如依次点亮并维持10ms后关闭循环执行。目前已有3266人学习下载作者自写自测可用可直接挂载到自己的工程中帮助读者快速理解查理复用原理、掌握扫描驱动思路并完成LED阶梯显示等实际功能。1. 5 根线点亮 20 颗灯查理复用到底省在哪手头只有 5 个 GPIO却要独立控制 20 个 LED第一反应多半是“上扩展芯片”或者“换引脚多的 MCU”。但如果你拆过那些廉价小家电的面板会发现它们常常只用几根线就驱动了一大片灯靠的就是查理复用Charlieplexing。它的核心思路很朴素LED 是单向导通的把 N 根 IO 两两组合能排出 N×(N-1) 种有向配对每一对正好对应一颗灯5 根线就是 5×420 颗一颗不多一颗不少。这套方案特别适合引脚紧张、灯又要独立亮灭的场景比如状态指示阵列、简易面板、玩具灯效。代价是它把“省引脚”的账转嫁到了“分时扫描”和“三态控制”上同一时刻只能点亮一颗灯靠人眼余晖扫出“全亮”的错觉而且任意两颗灯之间都可能存在寄生通路稍不注意就出现“鬼影”。这篇就按 STM8 的实际写法把 20 颗灯的查理复用从原理、扫描时序、代码到踩坑讲透新手能照着跑熟手能看清参数边界。2. 查理复用的电气原理与 5 线 20 灯映射2.1 为什么是 N×(N-1) 而不是 N²普通矩阵是行线加列线行列交叉点亮N 根线最多驱动 (N/2)² 颗灯因为行和列要分开。查理复用不分行列任意两根线都能当一对电极只要保证电流方向唯一确定是哪颗灯。对 5 根线来说任选一根做阳极、另一根做阴极就有 5×420 种有序组合每种组合对应一颗独立的 LED。关键在于“有序”线 A 到线 B 是一颗灯线 B 到线 A 是另一颗灯两颗灯反向并联在同一对线上。所以物理连接上每对线之间挂两颗方向相反的 LED。5 根线一共有 C(5,2)10 对线每对挂 2 颗正好 20 颗。2.2 三态输出是查理复用的命门普通推挽输出只有高、低两种状态查理复用必须用第三种状态高阻输入/浮空。原因在于当你点亮线 A 到线 B 这颗灯时其余 3 根线如果还保持推挽输出就会和 A、B 形成额外的电流通路把不该亮的灯点亮或者让该亮的灯变暗。正确做法是把当前要亮的阳极线设为推挽高阴极线设为推挽低其余所有线全部设为高阻输入。这样电流只有一条路可走就是目标 LED。STM8 的 GPIO 支持通过 DDR、CR1、CR2 三个寄存器配置成推挽输出、开漏输出、上拉输入、浮空输入四种模式浮空输入就是我们要的高阻态。2.3 5 线 20 灯的编号与真值表先把 5 根线编号为 P1~P520 颗灯按“阳极线→阴极线”的有序对编号。下面这张表就是扫描时每颗灯对应的引脚状态H 表示推挽高L 表示推挽低Z 表示高阻灯编号阳极阴极其余线LED1P1P2ZLED2P2P1ZLED3P1P3ZLED4P3P1ZLED5P1P4ZLED6P4P1ZLED7P1P5ZLED8P5P1ZLED9P2P3ZLED10P3P2ZLED11P2P4ZLED12P4P2ZLED13P2P5ZLED14P5P2ZLED15P3P4ZLED16P4P3ZLED17P3P5ZLED18P5P3ZLED19P4P5ZLED20P5P4Z这张表建议直接做成常量数组存进代码扫描时按索引取阳极、阴极引脚号比每次现算组合要快也不容易算错。2.4 限流电阻到底放几颗这是查理复用最容易被问的问题。常见做法有两种每颗 LED 串一颗电阻或者只在每根 IO 线上串一颗。前者最稳20 颗灯 20 颗电阻但占板面积后者只要 5 颗电阻但有个前提——同一时刻只有一颗灯导通所以每根线上的电流就是那一颗灯的电流电阻压降算得准。我一般用第二种5 颗电阻分别串在 P1~P5 上。阻值按 (VIO - VF) / IF 算比如 3.3V 供电、红灯 VF 约 1.9V、想要 5mA就是 (3.3-1.9)/0.005 ≈ 280Ω取 270Ω 或 330Ω 都行。注意扫描占空比是 1/20瞬时电流要按峰值算但平均亮度会低想要更亮就得加大瞬时电流同时确认 IO 总电流不超手册上限。3. STM8 上的三态切换与扫描代码实现3.1 GPIO 模式寄存器怎么配STM8 每个端口有五个寄存器ODR输出数据、IDR输入数据、DDR方向、CR1、CR2。配置成推挽输出是 DDR1、CR11、CR20配置成浮空输入是 DDR0、CR10、CR20。切换三态本质上就是改 DDR 和 CR1 这两位。下面这段是单颗灯点亮的底层函数直接操作寄存器避免库函数的开销#include stm8s.h // 5 根线对应的端口和引脚按实际接线改 // 这里假设 P1~P5 分别接 PC1~PC5 #define LED_PORT GPIOC #define PIN_ANODE_MASK(pin) (1 (pin)) #define PIN_CATHODE_MASK(pin) (1 (pin)) // 把某根线设为推挽高 static void line_high(uint8_t pin) { LED_PORT-DDR | (1 pin); // 方向设为输出 LED_PORT-CR1 | (1 pin); // 推挽 LED_PORT-CR2 ~(1 pin); LED_PORT-ODR | (1 pin); // 输出高 } // 把某根线设为推挽低 static void line_low(uint8_t pin) { LED_PORT-DDR | (1 pin); LED_PORT-CR1 | (1 pin); LED_PORT-CR2 ~(1 pin); LED_PORT-ODR ~(1 pin); // 输出低 } // 把某根线设为高阻浮空输入 static void line_hiz(uint8_t pin) { LED_PORT-DDR ~(1 pin); // 方向设为输入 LED_PORT-CR1 ~(1 pin); // 浮空无上拉 LED_PORT-CR2 ~(1 pin); }逻辑说明line_high 和 line_low 都先把 DDR 置 1 让引脚变输出区别只在 ODR 那一位。line_hiz 把 DDR 清 0 变输入同时清掉 CR1 的上拉这样引脚就是真正的高阻不会往电路里灌电流。参数上唯一要改的是 LED_PORT 和引脚号如果你把 5 根线分散在不同端口就得把端口也做成参数传进来。3.2 单颗灯点亮与全灭有了三态函数点亮一颗灯就是“阳极高、阴极低、其余高阻”。为了代码清晰我把 20 颗灯的阳极、阴极引脚号做成两张常量表// 20 颗灯的阳极引脚号PC 端口内 static const uint8_t anode_pin[20] { 1, 2, 1, 3, 1, 4, 1, 5, 2, 3, 2, 4, 2, 5, 3, 4, 3, 5, 4, 5 }; // 对应的阴极引脚号 static const uint8_t cathode_pin[20] { 2, 1, 3, 1, 4, 1, 5, 1, 3, 2, 4, 2, 5, 2, 4, 3, 5, 3, 5, 4 }; // 点亮第 idx 颗灯0~19其余全部高阻 void led_on(uint8_t idx) { uint8_t i; // 先把 5 根线全部设为高阻清掉上一次的状态 for (i 1; i 5; i) { line_hiz(i); } line_high(anode_pin[idx]); line_low(cathode_pin[idx]); } // 全部熄灭 void led_all_off(void) { uint8_t i; for (i 1; i 5; i) { line_hiz(i); } }逻辑说明led_on 先无条件把 5 根线全部拉高阻这一步不能省否则上一次扫描残留的输出状态会和这一次叠加出现鬼影。然后再设阳极和阴极。参数 idx 范围 0~19越界会读到数组外的值实际用的时候最好加个断言或者取模。3.3 扫描刷新与亮度控制20 颗灯要“同时亮”靠的是快速轮流点亮。刷新率低于 50Hz 会看到闪烁一般做到 100Hz 以上比较稳也就是每颗灯点亮时间不超过 1/(20×100)500μs。下面是一个用定时器中断驱动的扫描框架// 显示缓冲区1 表示该灯亮0 表示灭 static uint8_t led_buffer[20] {0}; // 当前扫描到第几颗 static uint8_t scan_idx 0; // 定时器中断里调用频率 20 × 刷新率 // 例如刷新率 100Hz则中断频率 2kHz周期 500us void scan_isr(void) { uint8_t i; // 关掉上一颗 led_all_off(); // 如果当前这颗在缓冲区里是亮的就点亮 if (led_buffer[scan_idx]) { led_on(scan_idx); } // 指向下一颗 scan_idx; if (scan_idx 20) { scan_idx 0; } }逻辑说明中断频率决定了单颗灯的点亮时长也决定了整体刷新率。scan_idx 每次加一循环 0~19。缓冲区 led_buffer 是上层逻辑和底层扫描的解耦点你想让哪颗亮就把对应位置 1扫描层负责把它扫出来。参数上中断周期 500μs 对应 100Hz 刷新如果觉得暗可以缩短周期提高刷新率但要注意中断开销如果 MCU 忙不过来可以降到 50Hz 刷新代价是轻微闪烁。亮度控制有两种常见做法一是 PWM 调每颗灯的点亮占空比二是整体调扫描周期。前者更精细但代码复杂后者简单但会改变刷新率。我一般先用后者把功能跑通需要精细调光再上 PWM。4. 查理复用的避坑与常见问题排查4.1 鬼影不该亮的灯微微发亮现象点亮某颗灯时旁边几颗灯有微弱余光暗环境下特别明显。原因扫描切换的瞬间上一颗灯的引脚状态还没完全释放或者某根线在高阻和输出之间切换时出现了短暂的中间态导致寄生通路导通。另一个常见原因是切换顺序不对先设了新的阳极旧的阴极还没拉高阻两颗灯同时导通了一小段时间。解决每次点亮前先执行 led_all_off把所有线拉高阻再设新的阳极和阴极。如果还有鬼影检查是不是某根线被别的外设复用或者 PCB 上走线太长引入了容性耦合。4.2 亮度不均靠近端口的灯更亮现象同一批 LED接在不同引脚上的亮度肉眼可见不一致。原因STM8 不同引脚的驱动能力有差异加上每根线串的限流电阻如果误差大电流就不一致。另外扫描占空比虽然是 1/20但如果某颗灯的点亮时间被中断打断实际占空比会变。解决限流电阻用 1% 精度的别用 5% 的凑合。如果亮度差异还是明显可以在缓冲区里给每颗灯单独设一个亮度系数用 PWM 补偿。PCB 上尽量让 5 根线等长减少线间差异。4.3 某颗灯完全不亮现象20 颗里有一颗怎么都不亮其他正常。原因先查硬件那颗 LED 焊反了或者虚焊。查理复用里每对线挂两颗反向 LED如果其中一颗焊反就会和另一颗同向导致两颗都异常。再查软件anode_pin 和 cathode_pin 表里那颗灯的引脚号是不是写错了或者索引越界。解决用万用表二极管档直接测那颗 LED 两端确认极性。软件上把 20 颗灯逐个单独点亮测试能快速定位是硬件还是映射表的问题。4.4 整体闪烁或亮度偏低现象所有灯都在闪或者整体偏暗。原因刷新率不够。中断周期太长或者中断里做了太多事导致实际扫描变慢。另一个原因是限流电阻太大瞬时电流不够。解决先量中断的实际执行时间确保 scan_isr 里没有耗时操作。把中断周期从 500μs 降到 200μs 试试如果闪烁消失但 MCU 负载高就得权衡。电阻方面在 IO 总电流允许的前提下适当减小阻值提高瞬时亮度。4.5 上电瞬间全部微亮现象刚上电、程序还没初始化时所有灯都有微弱亮光。原因STM8 复位后 GPIO 默认是浮空输入但外部如果有残留电荷或者电源上升沿通过 LED 结电容耦合就会有微弱电流。更常见的是初始化顺序问题先配了输出再配数据中间有个短暂的全高或全低状态。解决初始化时先把 ODR 清零、再配 DDR 和 CR1确保引脚从高阻开始。如果还有微亮在 5 根线上各加一颗下拉电阻比如 100kΩ把浮空时的电位拉死。5. 把 20 颗灯做成可复用驱动的几个进阶技巧功能跑通只是第一步真正在产品里用起来还得解决“怎么组织灯效”和“怎么验证扫描正确”这两件事。我一般会把显示层和效果层彻底分开底层只管 led_buffer 这 20 个 bit上层用一套简单的动画函数去改缓冲区比如呼吸、流水、闪烁。这样换灯效不用动扫描代码换硬件也不用动效果代码。一个实用的技巧是用位运算批量操作缓冲区。20 个 bit 用 3 个字节就能装下流水灯就是循环移位// 用 3 字节表示 20 颗灯的状态 static uint8_t buf[3] {0}; // 设置第 idx 颗灯 void buf_set(uint8_t idx, uint8_t on) { uint8_t byte idx / 8; uint8_t bit idx % 8; if (on) buf[byte] | (1 bit); else buf[byte] ~(1 bit); } // 流水灯整体左移一位 void buf_shift_left(void) { uint8_t carry (buf[0] 7) 1; buf[0] (buf[0] 1) | ((buf[1] 7) 1); buf[1] (buf[1] 1) | ((buf[2] 7) 1); buf[2] (buf[2] 1) | carry; // 只保留低 20 位有效 buf[2] 0x0F; }逻辑说明buf_set 把线性索引映射到字节和位buf_shift_left 做跨字节的循环移位carry 把最高位补回最低位形成闭环流水。buf[2] 只用低 4 位所以移位后要掩掉高 4 位否则会引入无效灯。参数上如果你改成 6 根线 30 颗灯字节数变成 4移位逻辑要相应扩展。验证扫描是否正确我有个笨但有效的办法写一个自检模式让 20 颗灯按编号依次单独点亮 500ms人眼跟着数。如果顺序和编号对得上说明映射表和扫描逻辑都没错。这个自检模式在调试阶段能省下大量示波器时间量产前也可以留一个隐藏入口方便售后排查。最后说个血泪经验查理复用的代码最容易翻车的地方不是扫描算法而是初始化顺序和引脚复用冲突。我踩过一次坑某根线被复用成了串口 TX初始化时串口把引脚配成了推挽输出结果扫描时那颗灯一直微亮查了半天才定位到外设冲突。从那以后我养成了一个习惯凡是查理复用的引脚初始化函数里第一件事就是把所有相关外设关掉再配 GPIO。这个习惯帮我省了不少后悔药。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑