简介这份资源面向使用STM8单片机进行LED扩展控制的嵌入式开发者核心是用5个GPIO口驱动20个LED并实现独立控制采用查理复用Charlieplexing技术大幅节省IO资源。包内共3个文件包含1个C源文件、1个头文件和1张效果图压缩包约474KB代码结构精简挂载到自有工程即可使用。头文件中通过宏定义即可更换所用IO口控制单个LED亮灭只需在参数表中选择对应项若需同时点亮多个LED则需配置扫描逻辑例如让LED1亮10ms后关闭、再让LED2亮10ms后关闭并循环执行同时需注意LED驱动能力受IO负载限制。该方案可用于电池电量阶梯显示、流水灯及各类LED显示场景作者已自写自测验证可用。目前已有3266人学习下载适合希望以少量引脚实现多路LED控制的初学者与工程人员参考借鉴。1. 用 5 个 GPIO 点亮 20 颗 LED查理复用到底省在哪如果你手头只有一颗 8 位单片机引脚已经被按键、传感器、通信接口占得七七八八偏偏面板上还要排 20 颗状态指示灯第一反应多半是加一颗 IO 扩展芯片或者上 74HC595 级联。但还有一种更“抠门”的玩法——查理复用Charlieplexing用 5 个 GPIO 直接驱动 20 颗 LED而且每颗都能独立点亮、独立熄灭不需要额外的驱动芯片。它的核心逻辑并不复杂LED 是单向导通的把 n 个 IO 两两组合就能得到 n×(n-1) 种有向通路5 个 IO 正好凑出 20 条一颗 LED 占一条。代价是同一时刻只能点亮一颗靠人眼视觉暂留做扫描刷新。这套方案特别适合引脚紧张、LED 数量又多的低成本控制板STM8 这类资源有限的 8 位机跑起来刚刚好。下面我按实际拆过的思路把原理、扫描代码、限流电阻计算和几个容易翻车的地方一次讲清。2. 查理复用的电气原理与 IO 组合推导2.1 为什么 n 个 IO 能带 n×(n-1) 颗 LED普通并联接法里每颗 LED 都要独占一个 IO 加一个公共端n 个 IO 最多带 n 颗。查理复用的思路是把 IO 当成“可正可负”的驱动源任意两个 IO 之间只要一个输出高、一个输出低就形成一条电流通路中间串一颗 LED。因为 LED 有极性A 高 B 低点亮的是 A→B 方向那颗B 高 A 低点亮的是 B→A 方向那颗所以每对 IO 能挂两颗。组合数就是排列数从 n 个 IO 里取两个有序排列数量为 n×(n-1)。5 个 IO 代入5×420正好 20 颗。这个数字不是巧合是查理复用的标准容量公式。如果只要 12 颗4 个 IO 就够4×312要 30 颗6 个 IO 能到 306×530。选 IO 数量时先算这个公式再留出复位、下载、晶振占用的引脚。2.2 单颗 LED 的等效回路与限流电阻每一颗 LED 串在一条 IO 对之间但注意如果每颗都单独串电阻20 颗就要 20 个电阻PCB 会很难看。常见做法是每条 IO 串一个电阻共 5 个放在 IO 和 LED 网络之间。这样任意一条通路上电流都会经过两个 IO 的电阻一个在源端、一个在汇端等效限流电阻是 2R。假设 LED 正向压降 Vf≈2.0V红光STM8 供电 5V目标电流 5mA那么R_total (Vcc - Vf) / I (5 - 2.0) / 0.005 600Ω 单条 IO 电阻 R R_total / 2 300Ω实际取 330Ω 标准值即可电流约 4.5mA亮度够用又不会超 IO 的灌电流上限。STM8 单个 IO 一般能承受 20mA 灌电流但 20 颗扫描时是分时导通平均电流很低峰值只要不超过单脚极限就安全。提示如果混用不同颜色的 LEDVf 差异会导致亮度不均最好同批同色或者按 Vf 分组后分别算电阻。2.3 扫描刷新率与视觉暂留的边界查理复用同一时刻只点亮一颗20 颗轮流点一遍叫一帧。人眼要感觉不到闪烁帧率至少 50Hz也就是每帧 20ms 以内单颗点亮时间 ≤1ms。但 1ms 太短亮度会暗通常取帧率 100Hz 左右单颗 500μs20 颗扫完 10ms。STM8 用定时器中断做这个节拍很合适主频 16MHz 下 500μs 对应 8000 个机器周期中断里只做“关上一颗、开下一颗”的切换负担很轻。刷新率和亮度是一对矛盾刷新率越高越不闪但单颗导通时间越短、平均亮度越低。解决办法是提高峰值电流比如把目标电流从 5mA 提到 10mA平均亮度就回来了前提是 IO 峰值电流不超限。这个权衡在后面调亮度时会反复用到。3. 在 STM8 上落地扫描驱动GPIO 配置与定时器中断3.1 硬件连接与 IO 分配先定 5 个 IO比如 PD0~PD4。每颗 LED 对应一个“阳极 IO”和一个“阴极 IO”共 20 组。接线时把 LED 阳极接到阳极 IO 那侧的电阻后阴极接到阴极 IO 那侧的电阻后。下面用一张表把 20 颗的映射列清楚这是写代码前必须定死的。LED 编号阳极 IO阴极 IO点亮条件LED0PD0PD1PD0 高PD1 低LED1PD1PD0PD1 高PD0 低LED2PD0PD2PD0 高PD2 低LED3PD2PD0PD2 高PD0 低…………LED19PD4PD3PD4 高PD3 低规律是固定一个 IO 做阳极遍历其余 4 个做阴极得到 4 颗换下一个做阳极再遍历其余累计 20 颗。代码里用一个二维数组存这 20 组 (阳极, 阴极) 即可。3.2 GPIO 初始化推挽输出与速度档位STM8 的 GPIO 要配成推挽输出才能同时拉高拉低开漏输出拉高需要外部上拉不适合这里。初始化代码如下#include stm8s.h // 5 个 IO 全部配为推挽输出低速档即可 void GPIO_Init_Charlie(void) { // PD0~PD4 输出模式 PD_DDR | 0x1F; // 方向寄存器1输出 PD_CR1 | 0x1F; // 推挽输出 PD_CR2 ~0x1F; // 低速降低 EMI // 初始全部输出低LED 全灭 PD_ODR ~0x1F; }PD_DDR置 1 表示输出PD_CR1置 1 选推挽PD_CR2清零选低速。低速档的翻转沿缓一点对 LED 扫描这种低频场景完全够用还能少一点电磁干扰。初始化后所有 IO 拉低此时任意两颗 LED 两端都是低没有压差全灭这是安全的上电状态。3.3 定时器中断里的扫描状态机扫描逻辑放在定时器中断里每 500μs 触发一次切换当前点亮的 LED。核心是“先关旧、再开新”避免两颗同时导通造成串扰。// 20 组 (阳极, 阴极) 映射用位掩码表示 const uint8_t led_anode[20] {0x01,0x02,0x01,0x04, /* ... 共20项 */}; const uint8_t led_cathode[20] {0x02,0x01,0x04,0x01, /* ... */}; volatile uint8_t cur_led 0; uint8_t led_state[20] {0}; // 每颗的亮灭状态1亮 // 定时器中断服务函数500us 一次 INTERRUPT_HANDLER(TIM4_UPD_OVF_IRQHandler, 23) { TIM4_ClearITPendingBit(TIM4_IT_UPDATE); // 1. 关掉当前 LED两个 IO 都拉低 PD_ODR ~(led_anode[cur_led] | led_cathode[cur_led]); // 2. 指向下一颗 cur_led; if (cur_led 20) cur_led 0; // 3. 如果这颗该亮设置阳极高、阴极低 if (led_state[cur_led]) { PD_ODR | led_anode[cur_led]; // 阳极拉高 PD_ODR ~led_cathode[cur_led]; // 阴极拉低 } }逻辑说明中断进来先把上一颗的两个 IO 都拉低彻底断开然后cur_led自增并回绕最后根据led_state决定是否点亮新的一颗。led_state是全局数组主循环里改它就能控制任意一颗的亮灭互不干扰。参数上定时器周期决定刷新率20 颗 × 500μs 10ms 一帧即 100Hz肉眼无闪烁。3.4 主循环里怎么独立控制每一颗主循环不需要管扫描只负责改led_state。比如做一个流水灯void main(void) { CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1); // 16MHz GPIO_Init_Charlie(); Timer4_Init(); // 配置 500us 中断 enableInterrupts(); uint8_t i 0; while (1) { for (i 0; i 20; i) led_state[i] 0; led_state[i % 20] 1; // 只亮一颗循环移动 delay_ms(100); } }这里每次只把一颗置 1其余清零扫描中断会自动把它点亮。想同时亮多颗就把对应位置 1 即可扫描会轮流照顾到。注意led_state被中断和主循环同时访问8 位机上是字节操作一般不会撕裂但严谨做法是改的时候关一下中断。注意如果发现某颗 LED 微亮不该亮的时候有微弱光多半是扫描切换瞬间两个 IO 状态重叠检查中断里是否严格先关后开。4. 避坑与排查查理复用最容易翻车的五个点4.1 现象两颗 LED 同时微亮或该灭的灭不掉原因扫描切换时没有先把上一颗的两个 IO 都拉低直接去设置下一颗导致两对 IO 短暂同时导通电流从一颗串到另一颗。解决中断里严格按“先关旧、再开新”的顺序关旧时把阳极和阴极 IO 都写 0确认断开后再操作新的一对。4.2 现象整体亮度明显偏暗尤其扫到后面的 LED原因单颗导通时间太短或者限流电阻取太大。查理复用平均电流 峰值电流 / 20如果峰值只有 5mA平均才 0.25mA自然暗。解决把峰值电流提到 10~15mA减小电阻或降低刷新率到 60~80Hz 换取更长导通时间。两者取其一别同时把 IO 峰值电流顶到极限。4.3 现象某几颗 LED 完全不亮换位置又好了原因IO 组合映射表写错或者某条 IO 的电阻虚焊。查理复用的映射是排列不是组合LED0 和 LED1 用的是同一对 IO 但方向相反很容易在数组里写重或写漏。解决用一张 20 行的映射表逐条核对万用表量每条 IO 到 LED 网络的通断。4.4 现象上电瞬间所有 LED 闪一下原因GPIO 初始化前IO 默认是浮空输入电平不确定可能形成意外通路。解决初始化第一步就把 5 个 IO 全部配成输出并拉低再配其他外设。STM8 复位后 IO 默认输入浮空这个顺序不能省。4.5 现象扫描时单片机复位或程序跑飞原因峰值电流太大5 个 IO 同时有较大灌电流拉低了电源或者中断里操作了非原子变量。解决量一下 Vcc 在扫描时的波动必要时加去耦电容中断里只做 IO 操作别放耗时逻辑led_state的读写加临界区保护。5. 进阶用 PWM 调亮度与按键复用同一组 IO5.1 在扫描节拍上叠加 PWM 调光查理复用本身是“开/关”扫描想调亮度可以在每个 500μs 时隙里再切一次占空比。做法是把定时器中断频率提到 10kHz每 20 次中断算一个 LED 时隙前 N 次点亮、后 20-N 次熄灭N 就是亮度等级。这样 20 颗 × 20 次 400 次中断一帧10kHz 下 40ms 一帧帧率 25Hz偏低所以实际会把中断提到 20kHz 或减少亮度等级数。STM8 在 16MHz 下跑 20kHz 中断没问题但中断里代码要极简。// 20kHz 中断每 20 次构成一个 LED 时隙 volatile uint8_t tick 0; volatile uint8_t bright 10; // 亮度 0~20 INTERRUPT_HANDLER(TIM4_UPD_OVF_IRQHandler, 23) { TIM4_ClearITPendingBit(TIM4_IT_UPDATE); if (tick 0) { // 时隙开始关旧开新 PD_ODR ~(led_anode[cur_led] | led_cathode[cur_led]); cur_led (cur_led 1) % 20; } if (led_state[cur_led] tick bright) { PD_ODR | led_anode[cur_led]; PD_ODR ~led_cathode[cur_led]; } else { PD_ODR ~(led_anode[cur_led] | led_cathode[cur_led]); } tick (tick 1) % 20; }bright从 0 到 20 变化就是 0~100% 亮度。代价是中断频率高了 40 倍CPU 占用上升主循环能做的事变少。如果项目里还有通信、采样得评估一下余量。5.2 按键和 LED 共用 IO 的可行性引脚实在不够时可以把按键也挂到同一组 IO 上利用扫描的“空档”读键。思路是在每帧扫完 20 颗后留一个时隙把所有 IO 拉低然后逐个 IO 配成输入上拉读电平判断按键。因为按键和 LED 共享网络按下时相当于在两条 IO 间加了通路读的时候要避开正在点亮的时隙。这个玩法对时序要求高容易和 LED 微亮问题混在一起建议先用示波器确认扫描波形干净再上按键。方案IO 数LED 数是否可调光额外芯片直接驱动2020否无74HC595 级联320是3 片查理复用520可叠加 PWM无从表里能看出查理复用在 IO 数和成本上优势明显代价是软件复杂度和扫描开销。选型时先看 CPU 余量和实时性要求再看引脚预算。5.3 验证扫描是否正常的两个土办法第一个办法把led_state固定成只亮一颗用手机慢动作录像拍回放看是不是只有一颗亮、其余全黑。如果有拖影说明切换有重叠。第二个办法在中断里翻转一个空闲 IO用示波器看方波频率确认是不是 100Hz对应 20 颗 × 500μs。频率不对就查定时器重装值和中断周期。从那以后我每次做查理复用都强制先把映射表打印出来逐条核对再上电测单颗最后才跑流水灯。这套顺序能挡掉八成低级错误。希望帮到你。本文还有配套的精品资源点击获取