ARTICLE DETAIL

资讯详情

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

188数码管驱动全解析:查理复用原理、贴片与直插差异及STM32实战

188数码管驱动全解析:查理复用原理、贴片与直插差异及STM32实战 188数码管这个玩意儿说它简单是真简单说它坑多也是真坑多。我最早接触它是在一个电表改造项目里当时需要用一个3位半的显示模块空间卡得特别死普通的三位七段数码管加驱动芯片根本塞不进去。后来同事扔给我一个5脚封装的188数码管说这玩意儿能省一半以上的IO口我当时还半信半疑。结果一上手才发现这东西确实巧妙但贴片型和直插型之间的差异以及查理复用Charlieplexing的驱动逻辑如果不搞清楚代码写出来要么亮度不均要么直接烧管子。这篇文章主要面向正在用或者准备用188数码管的嵌入式开发者尤其是那些在PCB上已经选了贴片型、但手头只有直插型做验证的朋友。我会把两种封装在电气特性、引脚定义、驱动波形上的差异全部拆开讲清楚然后给出一套可以直接跑的完整驱动代码基于STM32 HAL库但逻辑是通用的你移植到其他平台也不费劲。中间会穿插我自己踩过的坑比如为什么你的数码管有一段总是比别的暗为什么贴片型在低温下会出现鬼影这些在数据手册里基本不会写。1. 188数码管到底是个什么东西1.1 从“5脚”这个数字说起普通的三位共阳数码管你要驱动它至少需要3个位选引脚加上8个段选引脚一共11个IO口。如果位数更多IO口消耗是线性增长的。而188数码管之所以只有5个脚是因为它内部采用了查理复用Charlieplexing的拓扑结构。简单说就是利用GPIO的三态特性高电平、低电平、高阻态让N个引脚能够驱动N×(N-1)个独立的LED。5个引脚理论上可以驱动20个LED而一个3位半的188数码管正好需要8段×3位再加一个小数点或者负号总共二十几个LED所以5个脚刚好够用。我第一次拿到这个管子的时候用万用表二极管档去测发现任意两个脚之间正反向都能测到不同的压降当时就意识到这不是普通的共阳或共阴结构。它的内部是一个LED矩阵每个LED连接在两个引脚之间方向不同。你要点亮某一个特定的段必须让电流从特定的引脚流入从另一个特定的引脚流出同时其他引脚全部置为高阻态否则就会串亮。1.2 188这个型号命名的含义市面上叫“188”的数码管其实有好几种但最常见的是一种3位半、字高约0.56英寸的封装。所谓3位半就是它能显示三位整数加一位半位半位通常只显示“1”或者负号。这种规格在电子秤、温度计、电压表上非常常见。它的内部LED排布是固定的但不同厂家生产的188引脚定义可能完全不同。我手头就有两种一种是从左到右依次是COM1、COM2、COM3、COM4、COM5另一种顺序完全打乱。所以你在写代码之前第一件事必须是拿到具体型号的数据手册或者自己用可调电源加限流电阻把每个LED的对应关系测出来。注意不要假设所有188数码管的引脚定义都一样我见过同一批次不同生产日期引脚顺序都不同的情况。测引脚对应关系是绕不过去的步骤。1.3 为什么它省IO但驱动复杂省IO的代价就是驱动逻辑变复杂了。普通数码管你只要给位选通电、段选给电平就行而188数码管需要你动态地切换引脚方向。具体来说你要显示某一位的某个段需要把对应的两个引脚一个设为推挽输出高一个设为推挽输出低其余三个引脚全部设为高阻输入。而且这个过程必须快速轮询利用人眼视觉暂留才能看到稳定的显示。如果你的轮询频率低于50Hz就会看到明显的闪烁如果高于200Hz又可能因为GPIO翻转速度不够导致亮度下降。我实测下来STM32F103在72MHz主频下用HAL库的GPIO翻转5脚查理复用的刷新率做到400Hz左右是没问题的再高就有点吃力了。所以一般建议刷新率控制在100Hz到200Hz之间既不会闪亮度也够。2. 贴片型与直插型的硬件差异2.1 封装尺寸与散热能力贴片型188数码管通常是SMD封装比如常见的尺寸是19mm×12.5mm×7mm左右引脚是鸥翼型或者J型直接贴在PCB表面。直插型则是DIP封装引脚是长直的需要穿孔焊接。从散热角度来说直插型因为引脚长、接触空气面积大散热能力明显好于贴片型。这一点在驱动电流较大的时候特别重要。我做过一个对比测试同样的驱动电流每段10mA直插型连续点亮2小时后表面温度在38℃左右而贴片型能到52℃。虽然都没超过极限但贴片型在高温环境下长期工作光衰会更快。所以如果你的产品是在户外或者高温环境使用建议优先选直插型或者把贴片型的驱动电流降下来用提高刷新率的方式来补偿亮度。2.2 引脚间距与焊接难度贴片型的引脚间距通常是1.27mm或者2.54mm直插型是标准的2.54mm。对于手工焊接来说直插型友好太多了。贴片型如果你没有热风枪或者回流焊设备用普通烙铁焊很容易连锡尤其是5个脚挨得近的时候。我早期用贴片型做样板焊了5块板子有3块是连锡导致段位错乱的。后来学乖了先用细烙铁头加助焊剂焊完用放大镜检查再用吸锡线清理多余焊锡。另外贴片型在回流焊过程中如果焊盘设计不合理容易出现立碑现象。我的经验是焊盘长度要比引脚长出0.3mm左右宽度不要超过引脚宽度的1.2倍这样锡膏融化时的表面张力才能把引脚拉平。2.3 电气参数的实际差异虽然理论上贴片型和直插型的LED芯片是一样的但实际测试中我发现两者的正向压降有细微差别。直插型的正向压降通常在1.9V到2.1V之间红色贴片型在1.8V到2.0V之间。这个差别在5V供电时影响不大但如果你用3.3V供电就需要仔细算限流电阻了。举个例子假设你用3.3V供电LED正向压降2.0V想要5mA电流那么限流电阻是(3.3-2.0)/0.005 260Ω。但如果你用的是贴片型压降1.8V同样的电阻下电流就是(3.3-1.8)/260 5.77mA亮度会偏高。所以如果你在直插型上调好了亮度换成贴片型时最好重新算一下电阻或者用PWM调光来统一。参数直插型贴片型正向压降红1.9-2.1V1.8-2.0V最大连续电流20mA15mA热阻较低较高引脚间距2.54mm1.27/2.54mm焊接方式穿孔回流/手工2.4 光学特性的肉眼可见区别除了电气参数两者的光学效果也不一样。直插型的发光面是凸起的视角比较大通常有120度左右从侧面看也能看清。贴片型的发光面是平的视角窄一些大概90度到100度。这意味着如果你的产品面板开孔比较深贴片型从侧面看可能会偏暗。我在一个面板表项目里就遇到过这个问题客户反馈说从侧面看数字发虚后来换成直插型就解决了。另外贴片型的段与段之间的隔离做得更好不容易串光所以对比度更高看起来更清晰。直插型因为封装工艺的原因段间有时候会有轻微的光晕尤其是在暗环境下。3. 查理复用驱动的核心原理3.1 三态GPIO如何控制LED矩阵查理复用的核心就是利用GPIO的三种状态输出高、输出低、高阻输入。假设你有5个引脚编号为P1到P5。你想点亮连接在P1和P2之间的LED并且电流从P1流向P2那么你就把P1设为输出高P2设为输出低P3、P4、P5全部设为高阻输入。这时候电流只能从P1经过LED到P2其他路径因为高阻态而断开。但这里有个关键点LED是有方向的。如果你把P1设为低P2设为高那么电流就会反向那个LED不会亮但连接在P2和P1之间的反向LED如果有的话可能会亮。所以查理复用的引脚分配必须和LED矩阵的物理连接一一对应不能随便改。3.2 为什么需要快速轮询因为5个引脚一次只能点亮一个LED或者一组同向的LED而一个188数码管有二十多个LED所以你必须在极短的时间内轮流点亮每一个需要亮的段。人眼的视觉暂留时间大约是1/24秒所以只要你在1/24秒内把所有段都刷新一遍看起来就是同时亮的。实际工程中为了留余量通常刷新率做到100Hz以上也就是每10ms完成一轮。但这里有个陷阱如果你每一段点亮的时间太短亮度就会不够。假设一轮有20个段刷新率100Hz那么每一段点亮的时间是10ms/20 0.5ms。这个时间对于LED来说太短了亮度会明显下降。解决办法是提高驱动电流或者减少同时点亮的段数比如只点亮需要显示的段而不是全部扫描。我一般会把刷新率降到80Hz左右每段点亮时间1ms以上同时把电流提到10mA这样亮度就比较理想了。3.3 鬼影现象的产生与消除鬼影是查理复用驱动中最常见的问题。所谓鬼影就是当你点亮某一段时其他不该亮的段会微微发亮。产生的原因通常有两个一是GPIO切换时存在过渡状态比如从高阻切换到输出低的过程中引脚电压会经过一个中间值导致其他LED短暂导通二是PCB走线之间的寄生电容耦合。消除鬼影的方法有几个。第一在切换引脚状态之前先把所有引脚设为高阻等一小段时间比如1微秒再设置新的状态。第二在输出低和输出高之间插入一个高阻态过渡。第三降低GPIO的翻转速度减少高频耦合。我在STM32上实测把GPIO的翻转速度设置为2MHz而不是50MHz鬼影就基本消失了。提示如果你用的是HAL库在切换引脚状态时不要直接用HAL_GPIO_WritePin连续写两个引脚因为这两个操作之间会有微秒级的延迟可能导致短暂的错误导通。正确做法是先全部设为高阻再一次性设置。4. 完整驱动代码实现4.1 引脚定义与段码表首先定义5个引脚。假设我们用的是STM32F103C8T6引脚分别是PA0到PA4。然后我们需要建立一个段码表把每一个要显示的数字映射到需要点亮的LED组合上。因为查理复用的LED连接是固定的所以这个表必须根据你的硬件实际连接来写。#include stm32f1xx_hal.h #define PIN_NUM 5 GPIO_TypeDef* GPIO_PORT[PIN_NUM] {GPIOA, GPIOA, GPIOA, GPIOA, GPIOA}; uint16_t GPIO_PIN[PIN_NUM] {GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2, GPIO_PIN_3, GPIO_PIN_4}; // 定义每个LED对应的引脚对 (阳极引脚索引, 阴极引脚索引) typedef struct { uint8_t anode; uint8_t cathode; } LED_Map; // 假设的LED映射表实际要根据你的数码管调整 // 这里以3位数字为例每一位有7段 LED_Map led_map[3][7] { // 第一位 {{0,1},{0,2},{0,3},{1,2},{1,3},{2,3},{0,4}}, // 第二位 {{1,4},{2,4},{3,4},{0,1},{0,2},{0,3},{1,2}}, // 第三位 {{1,3},{2,3},{0,4},{1,4},{2,4},{3,4},{0,1}} }; // 数字0-9的段码每一位对应一个段是否点亮 const uint8_t digit_seg[10] { 0x3F, // 0: 0011 1111 0x06, // 1: 0000 0110 0x5B, // 2: 0101 1011 0x4F, // 3: 0100 1111 0x66, // 4: 0110 0110 0x6D, // 5: 0110 1101 0x7D, // 6: 0111 1101 0x07, // 7: 0000 0111 0x7F, // 8: 0111 1111 0x6F // 9: 0110 1111 };4.2 核心扫描函数扫描函数是驱动的心脏。它的任务是在每一轮刷新中依次点亮所有需要亮的段。为了提高效率我通常会把所有引脚先设为高阻然后根据当前要显示的段设置对应的阳极和阴极引脚。void set_pins_high_z(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); } void set_pin_output(uint8_t idx, GPIO_PinState state) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN[idx]; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; // 低速减少鬼影 HAL_GPIO_Init(GPIO_PORT[idx], GPIO_InitStruct); HAL_GPIO_WritePin(GPIO_PORT[idx], GPIO_PIN[idx], state); } void scan_display(uint8_t digits[3]) { for (int d 0; d 3; d) { uint8_t seg digit_seg[digits[d]]; for (int s 0; s 7; s) { if (seg (1 s)) { set_pins_high_z(); // 短暂延时确保高阻态稳定 for (volatile int i 0; i 10; i); set_pin_output(led_map[d][s].anode, GPIO_PIN_SET); set_pin_output(led_map[d][s].cathode, GPIO_PIN_RESET); // 点亮时间根据刷新率调整 HAL_Delay(1); // 实际应用中不要用HAL_Delay用定时器 } } } }上面这段代码是为了演示逻辑实际使用中不能用HAL_Delay因为它是阻塞的会导致刷新率极低。正确的做法是用一个定时器中断在中断里切换段或者用DMA加定时器的方式。我一般会用TIM2做一个1kHz的中断在中断里调用扫描函数的一个步骤。4.3 用定时器中断实现非阻塞刷新volatile uint8_t current_digit 0; volatile uint8_t current_seg 0; volatile uint8_t display_buffer[3] {0, 0, 0}; void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); set_pins_high_z(); // 找到下一个需要点亮的段 while (1) { uint8_t seg digit_seg[display_buffer[current_digit]]; if (seg (1 current_seg)) { set_pin_output(led_map[current_digit][current_seg].anode, GPIO_PIN_SET); set_pin_output(led_map[current_digit][current_seg].cathode, GPIO_PIN_RESET); current_seg; if (current_seg 7) { current_seg 0; current_digit; if (current_digit 3) current_digit 0; } break; } current_seg; if (current_seg 7) { current_seg 0; current_digit; if (current_digit 3) current_digit 0; } } } }这个中断服务函数每1ms执行一次每次点亮一个段。因为一轮有21个段3位×7段所以刷新率是1000/21 ≈ 47.6Hz。这个频率有点低可能会看到轻微闪烁。你可以把定时器中断频率提高到2kHz这样刷新率就是95Hz基本就看不出来了。4.4 亮度均衡的处理技巧因为查理复用是轮流点亮每个段点亮的时间是一样的所以理论上亮度应该均匀。但实际上由于不同段的LED正向压降略有差异以及PCB走线电阻不同亮度会有细微差别。我通常会在段码表里给每个段加一个亮度补偿系数用PWM的方式微调点亮时间。不过这样会大大增加代码复杂度对于大多数应用来说没必要。如果你确实遇到某一段特别暗可以先检查限流电阻是否一致再检查PCB走线是否过细。5. 贴片与直插在代码层面的适配差异5.1 引脚映射表的调整虽然驱动逻辑是一样的但贴片型和直插型的内部LED连接顺序可能不同。我手头的直插型188第一位数字的a段连接在引脚1和2之间而贴片型的a段连接在引脚1和3之间。这意味着你不能直接把直插型的段码表复制到贴片型上必须重新测一遍。测的方法很简单写一个测试程序依次点亮每一对引脚组合观察哪个段亮。或者用万用表二极管档红表笔接一个脚黑表笔依次碰其他脚记录哪个段亮。这个过程大概需要十分钟但能省去后面几个小时的调试。5.2 驱动电流的重新计算前面提到贴片型的最大连续电流通常比直插型低。直插型可以做到20mA每段贴片型最好控制在10mA以内。所以如果你从直插型切换到贴片型限流电阻要重新算。假设供电3.3V直插型压降2.0V想要15mA电阻是(3.3-2.0)/0.015 86.7Ω。换成贴片型压降1.8V想要10mA电阻是(3.3-1.8)/0.010 150Ω。如果你不换电阻贴片型就会过流长期工作会加速光衰。5.3 刷新率与鬼影的权衡贴片型因为引脚间距小寄生电容比直插型大所以更容易出现鬼影。我在贴片型上测试时发现即使加了高阻过渡偶尔还是会有微弱的鬼影。后来我把刷新率从100Hz降到80Hz同时把GPIO速度设为最低档鬼影就完全消失了。直插型则没有这个问题100Hz下也很干净。所以如果你用的是贴片型建议在代码里把刷新率稍微降一点或者增加高阻过渡的时间。项目直插型建议贴片型建议刷新率100-200Hz80-120Hz驱动电流10-15mA5-10mAGPIO速度中速低速高阻过渡1us2-3us6. 实际调试中遇到的几个坑6.1 段位错乱引脚顺序搞反了我第一次焊好板子写代码显示数字“1”的时候结果第三位亮了第一位没亮。查了半天代码最后发现是段码表里的引脚索引写反了。因为查理复用的阳极和阴极是相对的如果你把阳极和阴极搞反电流方向就反了LED不亮但反向的LED可能会亮。所以调试时如果发现显示错乱第一件事就是检查引脚映射表。6.2 亮度不均限流电阻精度不够我用的是5%精度的碳膜电阻结果发现不同段的亮度差异肉眼可见。后来换成1%精度的金属膜电阻亮度就均匀多了。虽然成本高一点但对于显示效果要求高的场合这个钱不能省。6.3 低温鬼影贴片型的通病冬天在室外测试时贴片型数码管出现了明显的鬼影温度回到室温就消失了。后来查资料才知道低温下LED的结电容会变化导致寄生耦合更严重。解决办法是在低温环境下提高高阻过渡时间或者换用直插型。如果你的产品要在低温下工作这一点必须考虑。6.4 刷新率与功耗的矛盾提高刷新率可以消除闪烁但会增加功耗因为GPIO翻转次数多了。我在一个电池供电的项目里把刷新率从100Hz降到50Hz功耗降低了约15%但闪烁就明显了。最后折中到70Hz配合低功耗模式才勉强达到续航要求。所以刷新率不是越高越好要根据实际场景权衡。7. 移植到其他平台的注意事项7.1 从STM32到Arduino如果你用ArduinoGPIO的三态控制稍微麻烦一点因为Arduino的digitalWrite只能设置高低不能直接设置高阻。你需要用pinMode(pin, INPUT)来设置高阻用pinMode(pin, OUTPUT)来设置输出。但pinMode切换本身有开销所以刷新率会低一些。我实测Arduino Uno在5脚查理复用下刷新率只能做到60Hz左右再高就不稳定了。7.2 从裸机到RTOS如果你在RTOS环境下比如FreeRTOS扫描函数最好放在一个高优先级的任务里或者直接用定时器中断。不要放在低优先级任务里因为任务调度延迟会导致刷新率不稳定出现闪烁。我一般会用硬件定时器中断这样不受任务调度影响。7.3 从3.3V到5V系统5V系统下LED的限流电阻要重新算。而且5V下GPIO的翻转速度更快鬼影可能更严重。建议在5V系统里把GPIO速度设为最低并且增加高阻过渡时间。另外5V下贴片型的发热会更明显驱动电流要再降一点。8. 一些实用的调试工具和方法8.1 用逻辑分析仪抓波形逻辑分析仪是调试查理复用最有用的工具。你可以抓5个引脚的波形看切换时序是否正确高阻过渡是否足够。我用的是一款8通道的廉价逻辑分析仪配合开源软件足够用了。抓波形的时候重点看两个引脚从高阻切换到输出的瞬间有没有毛刺。8.2 用手机慢动作视频看闪烁如果没有逻辑分析仪可以用手机的慢动作视频功能拍数码管然后逐帧看。如果看到某一段在闪烁说明刷新率不够或者该段的点亮时间太短。这个方法虽然土但很直观。8.3 用可调电源测正向压降在写代码之前先用可调电源加限流电阻测每个LED的正向压降记录下来。这样你在算限流电阻的时候就有依据不会盲目选值。我一般会测10个样本取平均值。9. 关于188数码管选型的个人建议如果你在做新产品设计我建议优先考虑直插型除非你的PCB空间实在有限。直插型的散热好、焊接容易、鬼影少虽然体积大一点但可靠性高很多。贴片型适合那些对体积要求极高、且工作环境温度不苛刻的场合。另外不管你选哪种一定要买带数据手册的不要买那种只有图片没有参数的。我吃过亏买了一批便宜货结果引脚定义和手册完全对不上浪费了两天时间。还有一点188数码管的颜色也有讲究。红色最便宜、最常用但亮度相对低。绿色和蓝色亮度高但正向压降也高限流电阻要重新算。白色和黄色介于两者之间。如果你要做户外显示建议选高亮红色或者绿色。最后如果你需要显示负数或者小数点记得在段码表里预留对应的位。188数码管的半位通常只能显示“1”或者负号不能显示其他数字。这一点在设计显示逻辑的时候要提前考虑好。
返回列表