简介:这份资源面向嵌入式开发初学者与MCU外设驱动实践者,聚焦HK32F030M微控制器通过SPI接口驱动TM1624芯片、点亮四位数码管的完整工程。它解决的是从芯片手册到可运行代码之间的落地问题,适合已掌握C语言与基础单片机知识、希望熟悉SPI时序与数码管动态扫描的开发者参考。压缩包共159个文件,约4.61MB,以42个h头文件、25个c源文件为核心,涵盖SPI、定时器、USART、I2C、ADC等外设驱动,另含23个d依赖文件、23个o目标文件、22个crf编译记录及uvprojx、uvoptx工程配置,并附2份pdf手册与hex烧录文件,构成可直接编译的Keil工程。目前已有338人学习下载。读者可从中获取SPI通信初始化、TM1624段码与位选控制、定时中断刷新显示等关键实现,配合原理图与数据手册理解电路连接与功耗管理,是练习MCU与显示驱动集成的实用素材。
1. HK32F030M 驱动 TM1624 四位数码管:为什么这颗国产 MCU 值得拿来练手
手上有一块 HK32F030M 的最小系统板,又翻出一片 TM1624 驱动的四位数码管模块,想把它们凑成一套能跑起来的显示方案——这个组合在国产 MCU 替代的浪潮里其实很有代表性。HK32F030M 是航顺基于 Cortex-M0 内核做的通用型 MCU,主频、Flash、SRAM 和引脚布局都对标常见的 F030 系列,价格和供货在当下环境里更友好;TM1624 则是一颗带键盘扫描接口的 LED 驱动控制专用芯片,通过类 I2C 的两线串行接口接收显示数据,能直接驱动共阴数码管,省掉 MCU 端大量 IO 和限流电阻的麻烦。把这两者接起来,本质上是练三件事:GPIO 模拟时序、显示数据编码、以及显示刷新与主循环的节奏控制。适合刚接触国产 MCU 的嵌入式新手,也适合想把手头显示模块快速跑通的老手——下面这套路径我在几块不同批次的板子上都复现过,参数和坑一并写清楚。
2. 先搞懂 TM1624 的通信时序:两根线怎么把四位数码管点亮
2.1 TM1624 的引脚定义与最小系统接线
TM1624 常见封装是 SOP16 或 DIP16,和它功能相近的还有 TM1620、TM1628 等型号,区别主要在驱动位数和键盘扫描路数。四位数码管模块上一般已经集成了 TM1624、限流电阻和数码管本体,对外只引出几根线。核心引脚如下:
| 引脚名 | 方向 | 作用 | 接到 HK32F030M |
|---|---|---|---|
| VDD | 电源 | 3.3V 或 5V 供电 | 3.3V |
| GND | 电源 | 地 | GND |
| DIN | 输入 | 串行数据输入 | 任意 GPIO,如 PA1 |
| CLK | 输入 | 串行时钟输入 | 任意 GPIO,如 PA2 |
| STB | 输入 | 片选/使能,低有效 | 任意 GPIO,如 PA3 |
接线本身不复杂,但有两个容易忽略的点:一是 TM1624 的供电电压要和数码管共阴/共阳类型匹配,模块上通常已经处理好,直接按模块丝印接即可;二是 DIN、CLK、STB 三根线在 HK32F030M 这端建议选同一组 GPIO,方便后面用寄存器批量操作,减少时序抖动。我一般会把这三根线放在 PA1、PA2、PA3,避开 SWD 下载口占用的 PA13、PA14。
2.2 类 I2C 时序的四个关键阶段
TM1624 的通信协议不是标准 I2C,但形似:STB 拉低开始一次传输,数据在 CLK 上升沿被采样,低位先发(LSB first)。一次完整的显示更新分四步:
- 发命令字 0x40,设置写显示寄存器模式,地址自动加一;
- 发起始地址 0xC0,告诉芯片从哪个显示寄存器开始写;
- 连续发 N 个字节的显示数据,每个字节对应一个数码管位;
- 发命令字 0x8F,打开显示并设置亮度(低四位是亮度,0x8F 即最亮)。
这四步里,第 1 步和第 4 步是命令,第 2、3 步是数据。很多新手第一次点不亮,问题就出在只发了数据没发 0x8F 显示开命令,或者亮度位设成了 0x80(显示关)。下面用 GPIO 模拟的方式把时序写出来。
2.3 用 GPIO 模拟时序的完整代码
#include "hk32f030m.h" #define TM_DIN_PORT GPIOA #define TM_DIN_PIN GPIO_Pin_1 #define TM_CLK_PORT GPIOA #define TM_CLK_PIN GPIO_Pin_2 #define TM_STB_PORT GPIOA #define TM_STB_PIN GPIO_Pin_3 /* 短延时,保证时序满足 TM1624 的最小脉宽要求 */ static void tm_delay(void) { for (volatile uint8_t i = 0; i < 8; i++); } /* 发送一个字节,LSB first */ static void tm_send_byte(uint8_t data) { for (uint8_t i = 0; i < 8; i++) { GPIO_ResetBits(TM_CLK_PORT, TM_CLK_PIN); /* CLK 拉低 */ tm_delay(); if (data & 0x01) GPIO_SetBits(TM_DIN_PORT, TM_DIN_PIN); else GPIO_ResetBits(TM_DIN_PORT, TM_DIN_PIN); tm_delay(); GPIO_SetBits(TM_CLK_PORT, TM_CLK_PIN); /* 上升沿采样 */ tm_delay(); data >>= 1; } } /* 一次完整的显示刷新:命令 + 地址 + 数据 + 显示开 */ void tm_display_update(uint8_t *seg_data, uint8_t len) { GPIO_ResetBits(TM_STB_PORT, TM_STB_PIN); /* STB 拉低,开始传输 */ tm_delay(); tm_send_byte(0x40); /* 写显示寄存器,地址自增 */ GPIO_SetBits(TM_STB_PORT, TM_STB_PIN); /* 命令结束,STB 拉高 */ tm_delay(); GPIO_ResetBits(TM_STB_PORT, TM_STB_PIN); tm_delay(); tm_send_byte(0xC0); /* 起始地址 */ for (uint8_t i = 0; i < len; i++) tm_send_byte(seg_data[i]); /* 连续写显示数据 */ GPIO_SetBits(TM_STB_PORT, TM_STB_PIN); tm_delay(); GPIO_ResetBits(TM_STB_PORT, TM_STB_PIN); tm_delay(); tm_send_byte(0x8F); /* 显示开,亮度最大 */ GPIO_SetBits(TM_STB_PORT, TM_STB_PIN); tm_delay(); }这段代码里tm_delay的循环次数不是随便写的。TM1624 手册里对 CLK 高低电平宽度有最小值要求,HK32F030M 在 48MHz 主频下,一个空循环大约几十纳秒,8 次循环能覆盖住最小脉宽。如果主频更高或者编译器优化等级不同,这个值要相应调整——最稳妥的办法是用逻辑分析仪抓一次波形,确认 CLK 频率在 100kHz 到 500kHz 之间。tm_send_byte里先拉低 CLK 再准备 DIN,是为了保证 DIN 在 CLK 上升沿之前已经稳定,这个顺序反了会出现数据错位,显示乱码。
3. 显示数据编码:从数字到段码的映射与亮度调节
3.1 共阴数码管的段码表怎么建
TM1624 每个显示寄存器字节对应一个数码管位,字节的 bit0 到 bit7 分别对应段 A 到段 G 和小数点 DP(具体映射以模块原理图为准,不同厂家布线可能不同)。共阴数码管的段码是「1 亮 0 灭」,所以数字 0 到 9 的段码可以这样建表:
/* 共阴数码管段码表,bit0~bit7 对应 A~DP */ const uint8_t seg_code[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 */ };这个表不是通用的。如果你的模块是共阳数码管,段码要取反;如果模块厂家把段顺序打乱了,比如 bit0 对应 G 而不是 A,那整张表都要重新排。我遇到过一批模块,段顺序是 A、B、C、D、E、F、G、DP 正常,但小数点 DP 在 bit7 之外还单独占了一个寄存器位,这种就只能对着模块原理图逐位核对。建议第一次跑通时先写一个固定值比如 0xFF,看是不是所有段全亮,确认硬件没问题再调段码表。
3.2 显示缓冲区与刷新节奏
四位数码管需要四个字节的显示缓冲区,每次要显示新内容时先更新缓冲区,再调用tm_display_update一次性刷过去。不要在中断里逐位刷新,TM1624 是静态驱动,数据写进去就保持,不需要像动态扫描那样频繁刷新。主循环里按需调用即可,比如每 100ms 更新一次传感器读数。
uint8_t disp_buf[4] = {0}; /* 把四位数字写入显示缓冲区 */ void update_disp_buf(uint16_t value) { disp_buf[0] = seg_code[value / 1000 % 10]; disp_buf[1] = seg_code[value / 100 % 10]; disp_buf[2] = seg_code[value / 10 % 10]; disp_buf[3] = seg_code[value % 10]; }这里value是 0 到 9999 的整数,超过四位会截断。如果要显示小数,比如 12.34,可以单独处理小数点:把disp_buf[1]或disp_buf[2]或上 0x80(假设 DP 在 bit7)。这个逻辑放在update_disp_buf里根据标志位判断即可。
3.3 亮度调节的两种方式
TM1624 的亮度通过显示开命令的低四位控制,0x88 到 0x8F 对应 8 级亮度,0x80 是显示关。如果觉得最亮还是刺眼,可以降到 0x8A 左右。另一种方式是在硬件上改限流电阻,但模块上一般已经焊好,改起来麻烦。软件调亮度更灵活,但要注意:每次改亮度都要重新发一次显示开命令,不能只改数据。我一般会在初始化时设一个默认亮度,需要调的时候再单独发命令。
4. 避坑与排查:四位数码管不亮、乱码、闪烁的常见原因
4.1 上电后完全不亮
现象:接线检查过,代码也跑了,数码管一点反应没有。 原因:最常见的是显示开命令没发,或者亮度位设成了 0x80。其次是 STB 没有在每次传输结束后拉高,导致芯片一直处于接收状态,后续命令被当成数据。 解决:用万用表量 DIN、CLK、STB 三根线在空闲时是不是高电平(TM1624 内部有上拉,但外部最好也加上拉电阻)。然后在tm_display_update最后一步确认发的是 0x8F 而不是 0x80。如果还不行,把 CLK 频率降到 100kHz 以下再试。
4.2 显示数字乱码或段位错位
现象:数字能显示,但笔画不对,比如 1 显示成 7,或者四个位顺序反了。 原因:段码表映射和模块实际布线不一致,或者显示数据字节顺序反了。TM1624 的显示寄存器地址从 0xC0 开始,依次对应第 1 位到第 4 位,但有些模块把第 4 位放在 0xC0,第 1 位放在 0xC3。 解决:先发{0x01, 0x02, 0x04, 0x08}四个值,看哪一位的哪个段亮,反推出实际映射关系。段码表不要照抄网上的,对着自己模块的原理图重新算一遍。
4.3 显示闪烁或亮度不均
现象:数码管能亮,但肉眼可见闪烁,或者不同位亮度不一样。 原因:如果是在主循环里频繁调用tm_display_update,每次都会重发命令和数据,虽然 TM1624 是静态驱动,但过于频繁的通信会占用 CPU 时间,导致其他任务被拖慢,间接影响显示节奏。亮度不均则可能是限流电阻精度不够,或者某一位的段码数据被意外修改。 解决:把刷新频率控制在 10Hz 到 50Hz 之间,不要每次循环都刷。亮度不均先检查硬件,软件上确认disp_buf没有被其他任务越界写入。
4.4 通信偶尔失败,重启后正常
现象:跑一段时间后显示卡死或乱码,断电重启又好了。 原因:GPIO 模拟时序对中断敏感,如果 DIN、CLK、STB 操作过程中被高优先级中断打断,时序会被拉长,TM1624 可能误判。另外,如果 MCU 供电和数码管供电共用一路且走线较长,电压跌落也会导致通信异常。 解决:在tm_send_byte前后关中断,或者把这三根线的操作放到临界区里。供电上,数码管和 MCU 的电源走线分开,在模块入口处加一个 100uF 电解电容和 0.1uF 陶瓷电容。
4.5 用 5V 供电时正常,3.3V 时异常
现象:模块标称 3.3V 到 5V 都能用,但 3.3V 下显示偏暗或通信失败。 原因:TM1624 在 3.3V 时驱动能力下降,如果数码管是共阳且限流电阻偏大,段电流不够,亮度就低。通信方面,3.3V 下 CLK 上升沿变缓,如果上拉电阻太大,边沿更差。 解决:优先用 5V 给数码管模块供电,MCU 保持 3.3V,两者共地。如果必须 3.3V,把限流电阻换小一档,或者把 CLK 上拉电阻降到 4.7k。
5. 进阶技巧:把显示驱动封装成可复用模块并做运行时验证
5.1 封装成独立文件,接口只留三个函数
跑通之后,我习惯把 TM1624 的驱动单独抽成tm1624.c和tm1624.h,对外只暴露三个函数:tm1624_init()负责 GPIO 初始化和上电清屏,tm1624_set_brightness(uint8_t level)负责调亮度,tm1624_show_number(uint16_t value)负责把数字刷到数码管。这样换到另一块板子上,只需要改头文件里的引脚宏定义,业务代码一行不用动。
/* tm1624.h */ #ifndef __TM1624_H #define __TM1624_H #include "hk32f030m.h" void tm1624_init(void); void tm1624_set_brightness(uint8_t level); /* level: 0~7 */ void tm1624_show_number(uint16_t value); /* value: 0~9999 */ #endiftm1624_init里除了配置 GPIO 为推挽输出,还要先把 STB、CLK、DIN 都拉高,再发一次清屏数据(四个字节全 0),最后发 0x8F 开显示。这个顺序不能省,否则上电瞬间数码管可能显示随机段码。
5.2 用定时器做非阻塞刷新
如果主循环里有其他耗时任务,比如串口收包或 ADC 采样,直接在循环里调tm1624_show_number可能会因为通信耗时导致显示更新不及时。更稳的做法是用一个 10ms 的定时器中断置标志位,主循环检测到标志位再刷新。注意不要在中断里直接调tm1624_show_number,因为 GPIO 模拟时序对中断嵌套敏感,放在主循环里更安全。
volatile uint8_t disp_update_flag = 0; /* 定时器中断里每 50ms 置一次标志 */ void TIM_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); disp_update_flag = 1; } } /* 主循环 */ while (1) { if (disp_update_flag) { disp_update_flag = 0; tm1624_show_number(current_value); } /* 其他任务 */ }这个结构的好处是显示刷新和业务逻辑解耦,current_value由其他任务更新,显示任务只负责把最新值刷出去。50ms 对应 20Hz 刷新率,肉眼完全看不出闪烁,也不会占用太多 CPU。
5.3 运行时验证:用变量替代固定值做自检
调试阶段不要只显示固定数字,那样看不出数据通路是否正常。我一般会写一个自检模式:上电后先显示1234持续 1 秒,再显示8888持续 1 秒,最后显示0000。8888用来检查所有段是否都能亮,0000用来检查灭段是否正常。如果8888有段不亮,说明硬件虚焊或段码表对应位错了;如果0000有段微亮,说明该段有漏电或限流电阻偏小。
void tm1624_self_test(void) { tm1624_show_number(1234); delay_ms(1000); tm1624_show_number(8888); delay_ms(1000); tm1624_show_number(0); delay_ms(1000); }这个自检函数在每次改完硬件或换模块后跑一遍,能省掉很多来回排查的时间。我自己的习惯是,凡是带显示模块的项目,第一版代码里一定留这个自检入口,后面调试业务逻辑时心里有底。
5.4 从四位数码管扩展到更多位的思路
TM1624 本身支持最多 4 位到 6 位(具体看型号后缀),如果以后要驱动更多位数,有两种路子:一是换 TM1628 或 TM1638 这类支持更多位的芯片,通信协议基本兼容,代码改动主要在显示缓冲区长度和地址范围;二是用多片 TM1624 级联,STB 分开控制,CLK 和 DIN 共用。级联时要注意每片芯片的地址是独立的,发数据前先选中对应的 STB。这个扩展思路在需要显示更多字段的场景里很实用,比如同时显示温度、湿度和时间。
希望帮到你。
本文还有配套的精品资源,点击获取