尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Charlieplexing驱动LED点阵:用三态GPIO让少量引脚控制更多LED

Charlieplexing驱动LED点阵:用三态GPIO让少量引脚控制更多LED 做LED点阵或者小显示面板的时候最烦的一件事就是单片机引脚不够用。8个LED倒是小事但想做出8x8、5x4这种阵列动辄十几个引脚很多小封装单片机根本没有这么多IO。后来我在一个老外的开源项目里看到了Charlieplexing这个方案3个引脚点亮6个LED5个引脚直接上到20个刷新出来一点不闪。这篇文章就把我从原理、电路到代码踩过的坑一整套整理出来给正准备做引脚紧张的小显示项目的同学做个参考。Charlieplexing这个名字听起来挺唬人但它并不是什么高深算法而是一种利用GPIO三态特性来驱动LED的接线和扫描方法。核心思想就一句话让每个引脚既能当高电平输出也能当低电平输出还能当高阻输入一个引脚当三个用从而用极少的引脚驱动尽可能多的LED。下面的内容我会从最小电路讲起逐步拆解公式、电路拓扑、代码实现和常见翻车点最后再聊聊它到底适合什么样的项目。1. Charlieplexing是怎么工作的先从三态和LED特性说起1.1 三态输出GPIO不只是能输出高和低绝大多数人用单片机控制LED时只会用到GPIO的两种状态输出高电平点亮和输出低电平熄灭。但GPIO其实还有第三种状态——输入模式。在输入模式下引脚对外呈现高阻态既不向外输出电流也不吸收电流相当于这个引脚和外部电路“断开”了。Charlieplexing的全部秘密就是把这三态组合起来用。假设有两个引脚A和BA输出高、B输出低电流就会从A流向B反过来A输出低、B输出高电流就从B流向A如果其中一个引脚变成高阻输入那这条回路就断开了什么都不会发生。这三态组合看起来简单但当你把多个引脚两两配对时组合数量会爆炸式增长这就是Charlieplexing能省引脚的根本原因。你可能会问普通LED驱动如果用“输出高/输出低”两态也能工作为什么要引入高阻答案是为了隔离。在Charlieplexing电路里所有LED都挂在引脚之间如果你不把不参与当前点亮的引脚设成高阻电流就会顺着其他路径串过去造成“偷亮”或短路。高阻态就是那个“与当前工作无关的引脚全部隔离”的开关。1.2 LED的单向导通特性反向并联才能双向点亮LED本质上是一个二极管电流只能从阳极流向阴极反过来基本不导通。这个特性平时是好事但在Charlieplexing里必须要处理因为我们希望同一对引脚之间的两个LED能“正着亮一个、反着亮另一个”。做法很简单把两个LED反向并联也就是一个的阳极接A引脚、阴极接B引脚另一个的阳极接B引脚、阴极接A引脚。这样当A输出高、B输出低时第一个LED正向导通发光反过来当B输出高、A输出低时第二个LED发光。两个LED虽然物理上共用一对引脚但永远只有一个会亮正好对应了电流方向的两种可能。这就是Charlieplexing的最小单位一对引脚、两个反向并联的LED、一次只能亮一个。看明白这个最小单位后面所有扩展都是它的排列组合。1.3 两个引脚到三个引脚数量是怎么涨上去的1.3.1 两个引脚只能点亮2个LED。这和直接用两态驱动一个LED相比好像没区别但注意两个引脚之间可以接反向并联的一对LED所以两个引脚就能点2个。1.3.2 三个引脚就不一样了。引脚之间两两配对一共可以形成3对组合A-B、B-C、A-C。每一对能接2个反向并联的LED所以总共能接3×26个LED。也就是说3个引脚就能点亮6个LED这比普通矩阵扫描的3个引脚最多控制4个LED2行1列要多。推广到n个引脚任意两个引脚之间都能接一对反向并联LED总共的LED数量就是组合数C(n,2)乘以2也就是n(n-1)。这就是Charlieplexing的核心公式后面算引脚够不够用全靠这个式子。2. 电路怎么搭从最小单元扩展到完整LED矩阵2.1 引脚数量与可驱动LED数量一张表看清收益很多人一听说n(n-1)就觉得夸张我们直接列几个常见数字看一下引脚数可驱动LED数22364125206307428561090这个增长速度是很可观的。8个引脚可以驱动56个LED如果用传统逐行扫描矩阵8个引脚最多做4行4列只能控制16个LED。Charlieplexing直接把可用LED数量翻了3倍多这就是它最吸引人的地方。但是注意这个公式给的是“最多能接”的LED数量不是“所有LED同时亮”的数量。具体能不能全部按预期发光还取决于扫描方式和占空比这一点在第3章会详细说。2.2 接线方式引脚对之间交叉连接实际画电路时不需要把每个引脚和所有其他引脚都物理拉一条线。正确的接法是对每一组引脚对在两个引脚之间接一对反向并联LED。比如3个引脚有A-B、B-C、A-C三组那就接三组反向并联LED总共6个。有人会问A-B之间接了两个LEDB-C之间又接了两个LED那A引脚到底连了多少线以A为例它要和所有其他引脚B、C分别构成一组所以A上面一共引出两条支路每条支路上有两个反向并联LED。n个引脚时每个引脚要和n-1个其他引脚配对所以每个引脚上的LED数是2(n-1)总LED数就是n×2(n-1)再除以2因为每对LED被两个引脚共享一次正好等于n(n-1)。这里有个容易犯的错不要把同一个引脚上的多个LED串联起来也不要让一个LED跨接在非配对引脚上。一旦接错扫描时会出现不可预期的电流路径轻则亮度乱掉重则烧引脚。2.3 为什么同一时刻只能亮一个LED扫描分时是核心Charlieplexing电路里LED之间是交叉连接的如果同时设置多对引脚为高/低可能会出现一个引脚同时要输出高和输出低的冲突。所以标准的Charlieplexing工作方式是同一时刻只让一个LED亮其余LED对应的引脚全部设为高阻然后靠快速扫描点亮不同LED利用人眼视觉暂留形成“多个灯同时在亮”的效果。用人话说就是“轮流站岗”。第1毫秒点亮LED1第2毫秒点亮LED2第3毫秒点亮LED3……只要一轮扫描总时间足够短一般少于16毫秒人眼就会觉得所有该亮的LED是一起亮着的。这种工作方式决定了Charlieplexing的亮度上限。假设有20个LED刷新率100Hz一个周期10毫秒平均每个LED只分到0.5毫秒的光照时间。LED的峰值电流可以设得稍微大一点但平均亮度肯定比直接常亮的LED低。这是Charlieplexing绕不开的代价选型时一定要想清楚。2.4 和普通扫描矩阵相比省了引脚多了什么麻烦传统LED扫描矩阵是把LED排成行和列一次点亮一整行软件简单亮度也容易做高。但引脚数是按行数列数来算的8个引脚最多做4×416个LED数量上不去。Charlieplexing牺牲了“同时点亮多颗灯”的能力换来了LED数量的大幅提升。软件上从“给整行写数据”变成了“逐个设置三态”每个LED的扫描顺序要自己编排代码复杂度上升不少。另外由于每个LED都要独立从引脚引出PCB布局会比矩阵复杂走线也更密。所以我的建议是如果LED数量在20个以内而且单片机引脚确实紧张Charlieplexing是很好的选择如果LED数量超过50并且对亮度要求高那更好的方案是加移位寄存器或LED驱动芯片第6章我会详细对比。3. 驱动代码怎么写三态切换、扫描刷新与亮度控制3.1 先封装一个“三态设置函数”不管用什么MCUCharlieplexing的代码核心都差不多一个能把任意引脚设置为“输出高/输出低/输入高阻”的函数以及一张描述“哪个LED对应哪些引脚”的映射表。以常见的Arduino平台为例我把三态封装成下面这个函数。// state: 1 输出高, -1 输出低, 0 输入高阻 void setPinTristate(uint8_t pin, int8_t state) { switch (state) { case 1: pinMode(pin, OUTPUT); digitalWrite(pin, HIGH); break; case -1: pinMode(pin, OUTPUT); digitalWrite(pin, LOW); break; default: pinMode(pin, INPUT); digitalWrite(pin, LOW); break; } }注意输入高阻态一定要用pinMode(pin, INPUT)不能只digitalWrite LOW了事。在Arduino里如果pinMode是INPUTdigitalWrite LOW会把内部上拉关掉引脚才是真正的高阻。如果误用INPUT_PULLUP引脚会通过内部上拉电阻对外输出微弱电流可能造成漏电路径。3.2 查表法3引脚6LED的完整点亮代码对于3引脚6LED我推荐用查表法。先定义一张映射表表里每个元素记录一个LED对应的“高电平引脚索引”和“低电平引脚索引”。// 引脚定义 const uint8_t pinList[3] {2, 3, 4}; // LED映射表[高引脚, 低引脚] // LED0: 引脚0高、引脚1低 // LED1: 引脚1高、引脚0低 // LED2: 引脚0高、引脚2低 // LED3: 引脚2高、引脚0低 // LED4: 引脚1高、引脚2低 // LED5: 引脚2高、引脚1低 const int8_t ledMap[6][2] { {0, 1}, {1, 0}, {0, 2}, {2, 0}, {1, 2}, {2, 1} }; void lightLed(uint8_t ledIndex) { int8_t hi ledMap[ledIndex][0]; int8_t lo ledMap[ledIndex][1]; for (uint8_t i 0; i 3; i) { if (i hi) { setPinTristate(pinList[i], 1); } else if (i lo) { setPinTristate(pinList[i], -1); } else { setPinTristate(pinList[i], 0); } } }点亮某个LED时把对应的引脚设为高另一个设为低其余全部高阻。这样电流路径就是唯一的不会串到别的LED上去。主循环里的扫描就很简单了void loop() { for (uint8_t i 0; i 6; i) { lightLed(i); delayMicroseconds(200); } }这个循环会反复点亮6个LED每个亮200微秒一轮下来1.2毫秒刷新率约833Hz亮度很均匀肉眼完全不会闪。如果你只想让某些LED亮可以加一个“本次是否点亮”的判断不点亮的LED直接跳过这样能提高有效占空比。3.3 刷新频率怎么定不闪烁的底线扫描型显示最怕的就是闪烁。人眼对50Hz以下的光强度变化比较敏感所以刷新率至少要做到60Hz以上。考虑到不同人眼的敏感度和环境光我一般把刷新率控制在100Hz以上才放心。刷新率 1 / (LED数量 × 单次点亮时间)。以20个LED为例如果每个LED点亮200微秒一轮就是4毫秒刷新率250Hz妥妥没问题。如果每个LED点亮5毫秒一轮就是100毫秒刷新率10Hz肉眼看起来就变成明显的一亮一灭了。所以选单次点亮时间之前先按这个公式反推一遍再结合亮度需求去调。如果LED很多、又要保证刷新率单灯时间就会被压得很短平均亮度下降这时候要么提高峰值电流要么换驱动芯片。3.4 用占空比调节每个LED的亮度Charlieplexing不是只能做“亮/灭”二值显示它也可以做多级亮度只是要在扫描周期里给每个LED分配不同的点亮时长。最简单的做法是把一轮扫描时间固定下来例如设为10毫秒然后在一个周期内按灰度等级给每个LED分配若干时间片。假设计算机表示8级灰度就把10毫秒分成7等份亮度1的LED点亮1/7时间亮度7的LED点亮全部时间。时间片划分得越细灰度等级越高但占用的CPU时间也越多。在代码里可以这样写每个LED维护一个0到255的亮度值在定时器中断里每1毫秒切换一次把当前周期的值跟亮度阈值比较超过阈值就点亮否则熄灭。用查表法实现256级灰度也完全可行我做过一个8引脚56个LED的6级灰度点阵用Arduino跑起来CPU占用还不到一半。这里有个经验不要在主循环里用delay来做灰度控制一旦有别的任务按键扫描、串口发送插进来时序会漂移亮度就会抖动。更稳的做法是把扫描放在定时器中断里主循环只负责修改每个LED的目标亮度值。4. 限流电阻、供电电压怎么选这些参数决定LED寿命4.1 限流电阻计算从欧姆定律到实际选值Charlieplexing电路里的限流电阻计算和普通LED驱动没有任何区别核心公式是R (VCC - Vf) / I其中VCC是供电电压Vf是LED正向压降I是目标工作电流。举例来说3.3V系统驱动红色LED红色LED的正向压降大约1.8V目标电流5mA那么电阻就是(3.3-1.8)/0.005300Ω。实际选值可以取270Ω到330ΩLED亮度差别不大。如果是5V系统驱动蓝色LED蓝色LED正向压降大概3.2V目标电流10mA电阻就是(5-3.2)/0.01180Ω选180Ω或200Ω都行。表里列几个常见组合方便快速选型供电LED颜色正向压降目标电流计算电阻实际取值3.3V红1.8V5mA300Ω330Ω3.3V绿2.0V5mA260Ω270Ω5V红1.8V10mA320Ω330Ω5V蓝3.2V10mA180Ω200Ω注意LED正向压降会随颜色、批次和温度变化所以计算值只是一个起点。实际焊接后用万用表测一下LED两端电压和流过电流再微调电阻值才是正经做法。4.2 电阻该串在哪每个LED独立电阻最省心Charlieplexing的限流电阻有一个 placement 选择问题是每个LED单独串一个电阻还是每对引脚共用一个电阻从原理上讲因为同一时刻只有一条电流路径在一对引脚之间接一个公共电阻也可以限制LED电流。但是——各LED压降不同特别是不同颜色混用的时候公共电阻会让亮度差异更明显。另外如果某个LED损坏短路公共电阻把整对引脚的电流都放大到危险值排查也很麻烦。所以我个人更推荐每个LED串一个独立电阻。虽然电阻数量多但优点很实际电流路径清楚亮度一致性好坏了一个也能快速定位。手工焊接虽然费点时间但换来的是调试时的省心。如果实在想省电阻在“整批LED型号完全一致”的前提下可以用每对引脚一个公共电阻但不要混色。4.3 红绿蓝白LED压降不同混色项目如何保持亮度均匀混色LED是Charlieplexing里最容易翻车的地方。红色LED压降约1.8V蓝色LED压降约3.2V如果供给同一个电压、用同一个阻值蓝色LED的电流会明显比红色小亮度也暗得多。解决方式是分颜色单独计算电阻。上表里已经给了常见组合核心思路是让每个LED的实际工作电流接近目标值而不是让每个电阻阻值相同。比如3.3V供电下红色LED用330Ω蓝色LED可能只亮一点点那就需要把蓝色LED的电阻降到很低甚至接近0。但蓝色LED压降3.2V3.3V供电的余量只有0.1VLED电流受电压波动影响非常大所以我一般不建议在3.3V系统里直接驱动蓝色LED除非LED是低压降版本。如果你在做一个多色混用的Charlieplexing点阵最稳妥的做法是先选一个对所有颜色都能留出足够余量的供电电压比如5V然后按每个颜色的Vf分别算电阻最后在代码里用占空比微调各颜色亮度。用Vf和阻值算完初值再肉眼调到一个看起来均衡的状态这才是常态。4.4 MCU引脚电流上限与总功耗估算Charlieplexing一次只点亮一个LED单引脚电流就是LED的目标电流。看似很安全但在扫描模式下某个引脚在一轮里可能被反复当作高电平输出端平均电流会随占空比变化。计算引脚平均电流时用这个公式I_avg I_peak × DD是引脚被点亮的时间占比。假设某个引脚在一轮10毫秒中被当作高电平输出了5个LED、每个点亮200微秒D就是(5×0.2)/100.1。如果峰值电流10mA平均电流只有1mA远低于MCU GPIO的连续输出能力。但“远低于”不是“不用看”。有些MCU的引脚在输出高电平时有总电流限制比如所有引脚sink/source电流总和不能超过某个值。做8引脚56个LED这种大型点阵时最好算一下每个引脚的平均电流别超过数据手册里对单个引脚的连续电流限制否则芯片会发热长期工作会损伤IO。5. 实操中容易踩的坑反向电压、亮度不均与闪烁排查5.1 反向电压对LED的影响为什么5V系统要小心Charlieplexing里没有被点亮的那个反向并联LED会承受反向电压。举个例子一对反向并联LED正向LED导通时反向LED两端近似承受电源电压。多数小信号LED的最大反向耐压是5V左右所以3.3V供电没问题5V供电就处在临界状态了部分蓝色、白色LED反向耐压更低时间长了可能漏电甚至损坏。我实际测试过在5V系统里做Charlieplexing长时间点亮同一组LED个别反向LED的亮度会慢慢变暗就是反向电压导致的性能退化。解决思路有三个第一如果项目能接受把供电电压降到3.3V这是最省事的第二选择反向耐压参数更高的LED比如标称12V或30V的型号第三在每条LED支路上额外串联一个普通二极管用它的反向耐压来保护LED但这样成本、体积、压降都会增加小项目一般不这么干。所以做新项目前先看一下数据手册里Reversed Voltage反向电压那一栏比出了问题再想办法要好得多。3.3V系统基本不会踩这个坑5V系统一定要当心。5.2 引脚驱动能力不足怎么判断与规避如果点亮LED后发现同一组里几个灯亮度特别低先怀疑引脚驱动能力。不同单片机的GPIO拉电流和灌电流能力不一样有些只有几毫安有些可以到20mA以上。Charlieplexing里单个LED的峰值电流会直接落在引脚上所以选型时要把LED目标电流和MCU的source/sink能力对齐。另外还要注意同一时刻虽然只有一个LED亮但在扫描切换的一瞬间引脚状态会发生跳变可能会产生瞬间浪涌电流。所以LED的限流电阻不要为了追求亮度而选得过小一般把单灯峰值电流控制在10mA以内比较安全。如果确实需要更高亮度建议换用更高驱动能力的MCU或者加一级三极管/MOS管驱动虽然Charlieplexing的精髓是省引脚但必要时也可以为了安全多做一层驱动。5.3 闪烁与亮度不均扫描顺序和时间分配的技巧闪烁和亮度不均是我被问得最多的问题。先说闪烁它通常是刷新率不够导致的。你把LED数量×单灯点亮时间算一下如果一轮超过16毫秒就会开始闪。优化办法是缩短单灯时间但亮度会掉所以更合理的方式是只扫描“实际需要点亮”的LED而不是所有LED。比如显示一个简单图案时可能只有10个LED需要亮那扫描这10个就够了刷新率自然提上去。亮度不均则多半是扫描占空比不一致造成的。如果在扫描循环里某些LED被多次点亮而另一些只被点亮一次那它们看起来就会有明显的亮度差异。解决方法是保证每个LED在一轮扫描中的点亮次数一致不要因为图案变化就把某几个LED的点亮频率调高。做灰度显示时也尽量用统一的灰度映射表避免不同LED之间出现固定偏差。5.4 常见问题速查表现象可能原因解决办法所有LED都不亮电源未接好、代码三态设置错误、公共限流电阻过大检查VCC/GND确认引脚模式函数正确万用表测LED两端电压某几个LED不亮LED映射表配置错误、LED损坏、焊接虚焊核对ledMap表用单灯测试程序单独点亮每个LED亮度明显不一致不同颜色LED压降/效率不同、限流电阻未按颜色区分、扫描占空比不同按Vf分别计算电阻保持每个LED每轮点亮次数一致闪烁刷新率低于60Hz、单灯点亮时间过长、中断被阻塞缩短单灯时间只扫描实际点亮的LED把扫描放进定时器中断引脚发热或芯片烫引脚电流过大、同时点亮多个LED导致引脚冲突降低单灯电流检查代码里是否出现同一引脚被设成高和低的冲突6. 什么时候别用Charlieplexing方案对比与扩展玩法6.1 与移位寄存器、LED驱动芯片的取舍Charlieplexing不是万能的。LED数量多、亮度要求高、开发周期紧的时候加芯片往往比省引脚更靠谱。方案引脚占用同时点亮LED数量软件复杂度成本适用场景Charlieplexing极少1个靠扫描高低LED数量少、引脚紧张、密度不高传统扫描矩阵行列整行中低中等数量LED、亮度要求较高74HC595级联3个全部静态驱动低中大量LED、不需要PWM灰度MAX7219等驱动芯片SPI接口全部芯片内部扫描低高8x8点阵、要求高亮度且省CPU上面表格是给选型用的。我的经验是LED少于20个、引脚非常紧Charlieplexing优势最大LED在20到50个之间看情况——如果可以用3根IO换更多LEDCharlieplexing还可以一旦超过50个或需要高亮度显示直接用MAX7219这类芯片开发效率高得多显示效果也更稳。6.2 适合Charlieplexing的真实场景Charlieplexing最适合的场景是“少量引脚、少量LED、低功耗、低成本”的小项目。比如用一颗8脚单片机做桌面迷你时钟5个引脚留出来驱动20个LED做4×5或5×4点阵显示时分剩下3个引脚还能接按键和蜂鸣器整板不带任何驱动芯片成本压得很低。另一个常见场景是电平指示器比如音频频谱显示器或者电量显示条。用4个引脚驱动12个LED做一个12段电平条比用两片595省事得多。还有做动态艺术灯板、小型像素屏的只要LED数量不超过30个Charlieplexing都能很体面地完成工作。这些项目的共同特点是显示内容简单、LED数量不多、系统对芯片成本敏感。如果你做的是高刷新率游戏屏或者大尺寸户外点阵Charlieplexing会力不从心。6.3 扩展方向点阵时钟、键盘扫描和低引脚MCU除了驱动LEDCharlieplexing的思路还可以用在按键检测上。原理一样n个引脚可以扫描n(n-1)个按键。每个按键串联一个二极管防止多键同时按下时电流倒灌按键按下时相当于接通两个引脚通过三态扫描就能检测。我做过一个6引脚控制20键的小键盘在低引脚MCU上省出很多IO效果不错。另一个扩展方向是配合低引脚MCU比如ATtiny系列、8脚PIC、STM32G0的超小封装型号。这类芯片价格便宜、体积小但引脚特别宝贵。用Charlieplexing驱动LED/按键后一颗8脚芯片能完成显示、输入、通信三件事做小穿戴设备和桌面摆件非常合适。6.4 一个完整的8引脚56个LED点阵实现思路最后给一个可以直接参考的实现思路8个引脚驱动56个LED排列成7×8的像素点阵。每个引脚需要拉线到7个引脚对整体布线会比矩阵密集但用双面板做并不难。代码结构分成三层最底层是引脚三态设置函数中间层是根据LED索引切换引脚的lightLed函数最上层是定时器中断里的扫描循环。因为56个LED一轮扫描耗时较长我建议扫描周期用800微秒内完成一次lightLed切换一轮约45毫秒刷新率约22Hz图案简单时减少扫描数量后刷新率能提到60Hz以上。做这种大型点阵时一定要给每个LED单独串电阻不能在引脚公共端用一个电阻代替否则不同像素的亮度会非常不均匀。另外显示图案前先写一个全亮测试程序逐个点亮每个LED确认映射表没有错位再上完整的显示逻辑。我第一次做56个LED点阵时就是因为映射表里两个引脚索引写反花了一晚上排查最后发现是数组下标问题。最后说点我自己的习惯。在做Charlieplexing项目时我一般会在引脚和LED之间预留测试点方便用示波器或万用表测量信号。软件上坚持把扫描逻辑放在定时器中断里主循环只改显示缓冲区的数据这样任何时候加按键、串口上报等任务都不会影响显示时序。如果你刚开始接触Charlieplexing别一上来就搞56个LED先用3个引脚点亮6个LED跑通流程理解了三态切换和扫描时序再去扩展数量基本就不会被那些坑卡住了。这个方案虽然不新但在引脚紧张的小项目里它确实是一个性价比很高的选择。
返回列表