简介:面向嵌入式开发者的HK32F030M数码管显示驱动工程资源,以HK32F030M微控制器为主控,通过SPI接口控制TM1624驱动芯片,实现四位数码管的动态扫描显示。适合学习MCU编程、SPI协议及数码管驱动应用的中高级嵌入式爱好者及相关项目工程师。
资源包共159个文件,容量约4.61MB,包含完整Keil工程文件(uvprojx/uvoptx)、C语言源码及头文件、编译生成的hex/axf文件,另有pdf数据手册和htm、txt说明文档,覆盖从源码到烧录验证的全流程,便于直接查阅和二次开发。
已有331人学习下载,资源中提供了SPI初始化、定时器中断更新显示等关键驱动代码,配合数据手册可快速掌握TM1624段码/位选控制逻辑与低功耗动态扫描设计思路,对完善嵌入式显示项目或课程设计很有参考价值。 最近在调一块由 HK32F030M 和 TM1624 组成的四位数码管显示驱动板,简单说就是让 MCU 通过三根 IO 把要显示的数字交给 TM1624,剩下的段扫描、位扫描和刷新全部由这颗 LED 驱动芯片自己完成。项目本身不复杂,但如果你直接搜 TM1624,会发现问题很多:资料经常跟 TM1628、TM1629 混在一起,引脚定义各说各话,命令字也容易对不上。这篇文章把我实际搭这套方案的过程整理出来,从选型原因、硬件连接、协议时序、代码实现一路讲到调试时踩过的坑,给要做数码管显示驱动的人一个可以照着做的参考。
光看标题,可能有人会问:四位数码管而已,MCU 引脚直接灌电流不也能点吗?或者用 74HC595 串转并也行。我最早也这么想,直到把亮度、引脚占用、刷新占用和量产一致性放在一起比,才发现专用 LED 驱动芯片在这里确实更合适。下面不绕弯子,直接讲这套方案。
1. 为什么我放弃了595和MAX7219,最终选了TM1624
1.1 HK32F030M的资源底子
HK32F030M 是 Cortex-M0 内核,我用的这颗是宽压供电版本,2.4~5.5V。这听起来只是电源范围问题,实际对驱动数码管影响很大:TM1624 在 5V 供电时输入高电平门槛大约是 0.7×VDD,也就是 3.5V 左右,而如果 MCU 只有 3.3V,推挽输出约 3.3V,就差那么零点几伏,容易时好时坏。让 HK32F030M 直接 5V 供电,IO 高电平接近 5V,TM1624 这边就完全不用考虑电平转换。这是我这套方案能省掉一堆麻烦的前提。至于主频、Flash、RAM,HK32F030M 完全够用,因为显示刷新根本不占用它,MCU 只在数字变化时写几个字节。
1.2 为什么不选74HC595
74HC595 是很常见的串转并方案,三根线也能驱动四位数码管,但它的本质只是扩展 IO,不是显示驱动。四位数码管要稳定显示,必须动态扫描:MCU 开定时器中断,每 1~2ms 切换一次位选,同时往段选送码,还要处理切换瞬间的拖影消隐。这套逻辑写起来不难,难在它跟主程序其他中断抢 CPU,一旦主程序里有耗时操作或者更高级的中断,数码管就会闪、亮度下降。595 输出的是普通推挽电平,每个段还需要串限流电阻,不同批次 LED 的压降差异会让亮度一致性一般。TM1624 自带显示 RAM,MCU 把数据写进去之后,扫描、刷新都由芯片自己完成,不占用 CPU 时间,这是最大的区别。BOM 也更省,SEG 那边基本不用外接限流电阻。
1.3 为什么不选MAX7219
MAX7219 大家更熟悉,SPI 接口,自带 BCD 解码,接法也简单。但对这个项目来说,它有几个让我不舒服的地方:BCD 解码只方便显示 0~9 数字,想显示字母、负号、自定义符号就不够灵活,必须绕回全段码控制;MAX7219 的段电流靠一个外接电阻设定,量产时电阻精度的差异会直接体现在亮度差异上;它还需要处理 5V 供电和 MCU 电平匹配的问题。TM1624 这边,段码表完全由软件定义,驱动输出接近恒流结构,不用每个段都放限流电阻,亮度一致性更可控。做产品而不是做实验板的话,这个差异很实际。当然 MAX7219 不是不能用,只是在这个“四位数码管+显示内容自定义”的场景里,TM1624 的贴合度更高。
同样是一个四位数码管的显示任务,我用下面这张表记录了几种方案的差异。这里的“占用MCU资源”指的是长跑任务,比如定时刷新和扫描切换。表里的结论基于我实际调试的主观判断,具体到你的产品,还要结合成本、供货和你熟悉的方案来权衡。不过有一点我可以肯定:如果你想要的是“写一次数据,芯片自己把显示维持住”,TM1624 这类带显示 RAM 的 LED 驱动芯片是比 595 更省心的选择。
| 方案 | 占用IO | 是否自带扫描 | 限流电阻 | 自定义符号 | 典型问题 |
|---|---|---|---|---|---|
| GPIO直接驱动 | 12个左右 | 否 | 需要 | 可以 | 占用IO太多,只能静态 |
| 74HC595 | 3个 | 否 | 需要 | 需要软件转换 | 刷新占用MCU,有拖影风险 |
| MAX7219 | 3~4个 | 是 | 一个电阻 | BCD不便 | 字母类显示麻烦,亮度靠外置电阻 |
| TM1624 | 3个 | 是 | 基本不用 | 方便 | 资料少,命令字要仔细看手册 |
2. HK32F030M和TM1624之间的硬件连接,细节比想象中多
2.1 引脚分配
TM1624 常用引脚包括 VDD、GND、STB、CLK、DIO、SEG1~SEG8、GRID1~GRID4。我的板子上 CLK 接 PB1,DIO 接 PB0,STB 接 PB10。选这三个引脚没有特别高深的理由,主要是布线方便:PB0/PB1 离 MCU 电源脚近,而且默认不带特殊复用功能,不占调试串口和烧录口。要提醒的是,不要一上来就把 CLK 接到 PA9/PA10 这类串口脚,或者 PB2 这类 BOOT 脚,后面调试或量产烧录时容易互相干扰。如果你用的封装引脚更少,引脚分配要重新核一遍,以原理图和封装图为准。显示部分用的是四位共阴数码管,公共端接 GRID1~GRID4,段引脚接 SEG1~SEG8。注意这里“共阴”是我手头这颗 TM1624 的典型接法,有些型号可能设计成共阳结构,换芯片时不要把公共端接反,否则显示乱且可能过流。
2.2 电平匹配与IO模式
这颗 TM1624 的输入高电平门槛约 0.7×VDD,所以当 VDD=5V 时,控制信号高电平最好能在 3.5V 以上。我直接让 HK32F030M 和 TM1624 都用 5V 供电,MCU 推挽输出高电平约为 VDD 减去很小压降,识别没有问题。如果你的 MCU 必须工作在 3.3V,CLK、DIO、STB 这三根线一定要做电平转换,不要指望在 5V 芯片输入端串个电阻或者只靠上拉就能稳定工作。DIO 这根线尤其要注意:有些 TM16xx 系列芯片的 DIO 是双向口,用于读按键或读状态。如果只做显示,可以把 DIO 一直配置成推挽输出;如果以后要扩展按键扫描,就得随时切换方向,PCB 上不要在 DIO 上并太大电容,否则方向切换瞬间电平跟不上。我在这版只做显示,所以 DIO 始终输出。
2.3 电源与去耦的实测经验
硬件上最容易忽略的是电源。TM1624 驱动数码管时段电流是阶梯变化的,几位同时点亮瞬间电流可能到几十毫安甚至上百毫安,如果电源和地线太细,VDD 会被拉出明显毛刺。我在这块板上给 TM1624 的 VDD 和 GND 之间放了 10uF 电解电容加 0.1uF 陶瓷电容,并且都靠近芯片电源脚;HK32F030M 的电源也单独加 0.1uF 去耦。第一次画板时我把地线走得很细,结果一显示带小数的“8.8.8.8.”就偶发重启,后来把地线加宽、电源走线缩短,问题才消失。如果测试中遇到复位、乱码,先怀疑电源,再怀疑时序,这个排查顺序能省很多时间。
3. TM1624的三线协议拆解:命令字、地址和段码到底怎么对应
3.1 时序基础
首先明确一点:TM1624 的三线口不是标准 SPI,别直接用 MCU 的 SPI 外设去接。它的名字虽然叫 CLK/DIO/STB,但时序更像“GPIO 模拟串行总线”。一次传输流程是这样的:先把 STB 拉低,表示开始;CLK 上升沿时,DIO 上的电平被锁存进芯片;每 8 个 CLK 周期形成一个字节;数据先发低位;最后把 STB 拉高表示结束。因为不是标准 SPI,也没有从机的片选自动控制,所以 STB 的拉低拉高必须由程序严格管理,不能省。网上有些代码把 CLK 的极性搞反了,现象就是显示完全错乱或者干脆不亮。判断标准很简单:用示波器看,DIO 上的数据必须在 CLK 上升沿之前稳定,这就是“上升沿锁存”的含义。
3.2 命令字和地址布局
关于 TM1624 的命令字,网上资料少且乱,跟 TM1628、TM1629 混着出现,我一开始也抄错过。我手里这颗 TM1624,一次完整的写显示数据过程是这样:STB 拉低,发一个数据命令字节(表示接下来写显示寄存器、地址自动加一),再发一个起始地址,然后连续发送 4 个字节的段码数据,最后 STB 拉高。显示开关和亮度控制是单独的显示控制命令,我在代码里用一个宏定义,方便根据手册调整。我这里的记录是:数据命令 0x44,起始地址 0xC0,显示控制命令基础值 0x88,低三位是亮度占空比,bit3 是显示开关。需要强调:不同批次的 TM1624 命令字布局可能有差异,如果你手里的手册和我下面的定义不一致,务必按手册改,不要照抄网上 TM1628 的代码。这个“命令字不确定”是 TM1624 最大的门槛,但也别怕,只要 STB 时序正确,命令字试错成本很低。
3.3 段码映射
前面说过,TM1624 的 RAM 地址对应 GRID,也就是数码管的位。段码的 bit 位对应的是 SEG 引脚,通常 bit0 对应 a 段,bit1 对应 b 段,一直到 bit7 对应 dp。共阴数码管常用的数字 0~9 的段码,我从 0x3F 开始排:0x3F、0x06、0x5B、0x4F、0x66、0x6D、0x7D、0x07、0x7F、0x6F。这个表后面代码里会直接用。在这里要特别提醒:段码表是针对“共阴+bit0=a”的约定来的,如果你的数码管是共阳,段码要全部取反;如果 PCB 上 SEG 线序被打乱,代码里看到的数字就会错乱,这时不要怀疑协议,去对照原理图重排段码表的位序。显示小数点是 bit7 置 1,显示负号就用 0x40,显示字母 C 用 0x39,这些都是自定义显示时的灵活之处。
4. 基于HK32F030M的驱动代码:初始化、段码表与显示函数
4.1 GPIO初始化
代码使用航顺固件库,接口和 STM32F0 的标准外设库很像。初始化时要把 PB0/PB1/PB10 都设成推挽输出,速率 2MHz 就够了,TM1624 不是高速器件,拉得太快反而容易产生振铃。另外我习惯把三根线初始化为高,尤其是 STB,防止 MCU 复位期间引脚高阻导致 TM1624 收到半截命令。具体代码:
#include "hk32f030m.h" #define TM1624_STB_PORT GPIOB #define TM1624_STB_PIN GPIO_PIN_10 #define TM1624_CLK_PORT GPIOB #define TM1624_CLK_PIN GPIO_PIN_1 #define TM1624_DIO_PORT GPIOB #define TM1624_DIO_PIN GPIO_PIN_0 static void TM1624_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = TM1624_STB_PIN | TM1624_CLK_PIN | TM1624_DIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB, TM1624_STB_PIN | TM1624_CLK_PIN | TM1624_DIO_PIN); }4.2 写字节和命令帧
接下来是底层时序。写字节时要注意按位判断 DIO 电平,然后在 CLK 上升沿锁存。我用的延时函数很简陋,就是一个计数器,目的是把 CLK 周期压到 1us 以上;如果你把主频调过,这个延时时间会变,最好用定时器或者 SysTick 做精确延时。数据先发低位,很多移植代码翻车就是这里没改。代码如下:
static void TM1624_Delay(void) { volatile uint32_t i = 20; while (i--); } static void TM1624_WriteByte(uint8_t dat) { uint8_t i; for (i = 0; i < 8; i++) { if (dat & 0x01) { GPIO_SetBits(TM1624_DIO_PORT, TM1624_DIO_PIN); } else { GPIO_ResetBits(TM1624_DIO_PORT, TM1624_DIO_PIN); } GPIO_SetBits(TM1624_CLK_PORT, TM1624_CLK_PIN); TM1624_Delay(); GPIO_ResetBits(TM1624_CLK_PORT, TM1624_CLK_PIN); TM1624_Delay(); dat >>= 1; } }地址自动递增意味着,只要在起始地址后连续发字节,TM1624 会自己把后面的数据填到后续地址,不用每个地址都发一次命令。这既省时间,也能避免中途 STB 抖动造成的地址错乱。写数据帧函数如下:
static void TM1624_WriteData(uint8_t addr, uint8_t *buf, uint8_t len) { uint8_t i; GPIO_ResetBits(TM1624_STB_PORT, TM1624_STB_PIN); TM1624_WriteByte(0x44); // 数据命令:写显示寄存器,地址自动加一 TM1624_WriteByte(addr); // 起始地址 for (i = 0; i < len; i++) { TM1624_WriteByte(buf[i]); } GPIO_SetBits(TM1624_STB_PORT, TM1624_STB_PIN); }4.3 段码表与显示函数
段码表我直接放到数组里,从 0 到 9 的顺序排列,后面要显示字母或者负号再往里面补。四个数字按照千位、百位、十位、个位放进 buf[0] 到 buf[3],然后一次写进 TM1624。这里我加了首消隐参数,让显示 123 的时候不要出现 0123,产品界面上会好看很多;如果想让数值右对齐,也可以只消隐不右移。下面这段是核心:
static const uint8_t FontTable[] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; static void TM1624_DisplayOn(uint8_t brightness) { uint8_t cmd = 0x88; // 显示控制命令基础值 if (brightness > 7) brightness = 7; cmd |= brightness; GPIO_ResetBits(TM1624_STB_PORT, TM1624_STB_PIN); TM1624_WriteByte(cmd); GPIO_SetBits(TM1624_STB_PORT, TM1624_STB_PIN); } void TM1624_ShowNumber(uint16_t value, uint8_t blankLeading) { uint8_t buf[4]; buf[0] = FontTable[(value / 1000) % 10]; buf[1] = FontTable[(value / 100) % 10]; buf[2] = FontTable[(value / 10) % 10]; buf[3] = FontTable[value % 10]; if (blankLeading) { if (value < 1000) buf[0] = 0x00; if (value < 100) buf[1] = 0x00; if (value < 10) buf[2] = 0x00; } TM1624_WriteData(0xC0, buf, 4); }DisplayOn 的亮度参数命令我用了基础值 0x88,低三位就是亮度挡位。你拿到的 TM1624 如果低三位含义不同,直接改这个函数里的 cmd。调用顺序上,main 函数里先做 GPIO 初始化,再关显示、清显示 RAM,然后开显示,最后才显示想要的内容。上电清屏这一步不能省,因为 TM1624 的显示 RAM 上电内容是随机的,不清屏的话数码管可能闪一下乱码。下面的 main 片段就是标准流程:
int main(void) { uint8_t clear[4] = {0x00, 0x00, 0x00, 0x00}; TM1624_GPIO_Init(); TM1624_WriteData(0xC0, clear, 4); // 清屏 TM1624_DisplayOn(5); TM1624_ShowNumber(1234, 1); while (1) { // 这里写业务逻辑,显示不会闪 } }4.4 为什么这里不需要自己写动态刷新
很多第一次用 TM1624 的人都会不放心:我是不是还要在定时器里刷新?不用。TM1624 内部有显示 RAM 和扫描电路,你只要把段码写进 RAM,它会自动轮询 GRID 并输出对应的 SEG 信号。这意味着当你把 1234 四个字节写进去之后,CPU 就可以去干别的活了。这个特性在小系统里非常舒服,尤其 HK32F030M 本身资源不多,省掉一个定时中断,对系统实时性很有帮助。如果显示内容不更新,数码管也会一直稳定显示,不会像 595 动态扫描那样一停就闪。我在产品里让 MCU 长时间进入低功耗,数码管依然保持显示,这个效果用 GPIO+595 很难做到。
5. 实测排错记录:四个最容易翻车的地方
5.1 上电瞬间数码管乱闪
第一个坑是上电瞬间乱码。原因是 TM1624 先于 MCU 完成复位,此时显示 RAM 随机,SEG/GRID 可能已经输出,数码管会闪一下。软件上我在 main 开头的 GPIO 初始化之后立刻清显示 RAM,再开显示,能压住大部分乱码;硬件上,把 MCU 的复位上电时间稍微拉长一点,或者给 STB 加一个 10k 下拉电阻,避免复位期间 STB 悬空误触发。如果对时序要求很严格,可以用一颗带 RC 延时的复位芯片控制 MCU 启动顺序。
5.2 数字错位和段乱
第二个坑是显示错位。可能是地址方向搞反:如果你的板子 GRID1 在右边,而我代码里 buf[0] 对应最左边,那么 1234 会显示成 4321。改法很简单,把 buf 数组倒过来填。也可能是 SEG 顺序乱。我建议先写一个自检程序,依次点亮 SEG1~SEG8,然后对着数码管记录哪一段亮,再生成新的段码表。这个表一旦确认,就不要再动 PCB 走线了。还有一次我把数据发成高位在前,整个数码管显示的字符完全不对,把 WriteByte 里的 dat >>= 1 改成 dat <<= 1 就正常。排查顺序建议是:先确认位序,再确认段序,最后才怀疑命令字。
5.3 长时间运行后偶发花屏
第三个坑是花屏。当 MCU 主频高、GPIO 翻转快,又没有加延时,杜邦线一长,CLK/DIO 上的振铃会让 TM1624 收到错误数据。表现为运行几分钟突然显示一个乱码,下一次写数据又恢复正常。我解决的方法是加大 TM1624_Delay,把 CLK 频率控制在 500kHz 以下,同时在 STB 拉低前让 DIO 先稳定。另一个原因是中断里写显示:如果在定时器中断或串口中断里调用 TM1624_WriteData,一旦主程序同时在写,两次 STB 帧可能互相穿插,协议就乱了。建议所有 TM1624 操作都放在同一个任务里,必要时加个互斥标志。
5.4 亮度不均和干扰处理
第四个坑是亮度不均。TM1624 输出接近恒流驱动,但 GRID 公共端的走线压降、数码管本身每段 LED 的压差都会造成位与位之间的亮度差。如果只是某一位暗,查公共端走线和插接件接触;如果是整片亮度太低,查 VDD 电压。还有一点,不要把亮度命令开到最高挡。我试过在最高挡位下,个别段明显偏亮,降低一挡之后均匀度好了很多。如果你的设备周围有电机、继电器这类干扰源,信号线上串 100 欧姆电阻、TM1624 电源脚加强去耦会有帮助。最后,量产前记得把命令字定义和段码表固化成正式头文件,并标注手册版本,不然三个月后你自己都分不清当前这颗料该用哪套命令。
本文还有配套的精品资源,点击获取