简介:源码包内含基于HK32F030M微控制器的TM1624数码管驱动完整工程,覆盖工业控制、智能家居和仪器仪表等需要数字或字符显示的嵌入式场景,适合具备一定单片机基础、希望快速上手SPI接口外设开发的初中级工程师。压缩包共380个文件,约7.3MB,以h头文件、c源代码、uvprojx工程文件、hex烧录文件及PDF说明文档为主,同时包含o/d/crf等编译中间文件、sct链接脚本和keilkill批处理,可在Keil环境中完整还原工程并重新构建。驱动代码围绕初始化、SPI通信、数码管数据编码、动态扫描与显示控制五个方面展开,详细展示了HK32F030M的GPIO和SPI时钟参数配置、TM1624的段码与位码组织方式,以及如何通过扫描时序避免闪烁,还提供了通信异常时的错误检查和恢复思路。已有481人学习下载,对于使用HK32F030M或同类Cortex-M0芯片开发数码管显示功能的开发者,可直接复用或移植其中驱动逻辑并快速迁移到实际项目。
1. HK32F030M 驱动 TM1624 数码管:两个引脚点亮一排数字
做小家电、仪表盘或者充电器面板的工程师,多半遇到过这种尴尬:产品只要加上数码管显示,MCU 的引脚就紧张得算不过来了。七段数码管加四个位选,直接占掉十一二个 GPIO,还没算按键和通讯。我接手过的一个模拟项目 X 就是这么被卡住的,后来换了 TM1624 这类专用驱动芯片,两个引脚搞定了全部显示和按键扫描,板子尺寸也小了一圈。
HK32F030M 这个国产 MCU 配 TM1624,算是低成本显示方案里的常见组合。TM1624 承担了段码锁存、位选扫描、按键回读这些杂活,MCU 只需要通过 CLK 和 DIN 两根线把数据送过去就行。驱动代码看着不长,真正让人挠头的是时序细节——数据位是高位在前还是低位在前、CLK 空闲电平是哪边、地址自增和固定地址怎么切换,这些手册里都有,但翻车往往就翻在“我以为我懂了”。这篇文章从协议讲起,把最小可用代码、段码表生成和常见坑一次性说透,新手可以照着抄,老手能跳着看参数。
2. TM1624 到底在等什么:通信协议与 HK32F030M 的引脚规划
2.1 一根 CLK 一根 DIN:TM1624 的通信时序读懂这两点
TM1624 的通信协议本质上是一个时钟驱动的串行移位协议,连名字都不需要纠结,把它当成一台对时序有轻微强迫症的设备就行。它的接口通常只有两根信号线——CLK 和 DIN(部分封装会把数据线做成双向 DIO,后文避坑章节会单独说),再加上供电和地,总共四个脚。
数据发送的基本过程是这样的:MCU 先把 DIN 拉到对应电平,然后在 CLK 上产生一个跳变沿,TM1624 在这个边沿锁存 DIN 上的电平;反复八次,凑齐一字节;最后给出一个特殊的结束条件,芯片才把这一字节当命令或数据收下。难点在于,不同的 TM16xx 系列对“空闲时 CLK 是高还是低”“数据是在上升沿还是下降沿锁存”的约定不完全一致,所以写驱动前必须对着数据手册的时序图把这两个问题确认掉,否则后面全是乱码。
/* TM1624 时序参数——以手册为准,先确认这两个宏再写代码 */ #define TM1624_CLK_IDLE_HIGH 0 /* CLK 空闲电平:0=低,1=高 */ #define TM1624_DATA_EDGE 1 /* 锁存边沿:0=下降沿,1=上升沿 */我一般建议拿到芯片之后干的第一件事,不是打开例程,而是拿逻辑分析仪或者示波器抓一遍已知能工作的参考板波形,把 CLK 空闲电平、数据建立时间和锁存边沿这三个参数标到手册上。这套协议的时序容忍度还算宽,几百纳秒的建立时间偏差一般都能跑,但位序和边沿搞反,神仙难救。宽度方面,CLK 频率不要超过芯片手册规定值,常见做法是把一比特的翻转周期控制在 1μs 以上,HK32F030M 的 GPIO 翻转速度绰绰有余。
2.2 HK32F030M 引脚分配:为什么两个普通 GPIO 就够了
TM1624 的时钟频率通常在 1MHz 以下,这个速度对 MCU 的 GPIO 来说毫无压力。HK32F030M 的 GPIO 配置成推挽输出,翻转速率档位选到最高即可,不需要开复用功能,也不需要占用 SPI 外设。我习惯把 CLK 和 DIN 放在同一个 GPIO 端口的相邻两个引脚上,这样初始化代码可以连续写,逻辑分析仪挂探头也方便。
| 信号 | 推荐引脚 | 配置 | 说明 |
|---|---|---|---|
| CLK | PB0 | 推挽输出,最高翻转速率 | 空闲电平按手册设置,建议加 10k 下拉或上拉固定电平 |
| DIN | PB1 | 推挽输出,最高翻转速率 | 发送数据线;若芯片为双向 DIO 模式则需配置为输入/输出切换 |
| VCC | 3.3V | 电源 | 注意 TM1624 的工作电压范围,部分版本不支持 5V 直接供电 |
| GND | GND | 地线 | 与 MCU 共地,走线尽量短 |
有人会问,既然 MCU 自带 SPI,为什么不用硬件 SPI 直接驱动 TM1624?我也踩过这个坑。硬件 SPI 的确能把发送速率拉高,但 TM1624 的命令格式里包含“起始位 + 命令 + 结束位”这种自定义结构,和 SPI 的每帧格式对不上,最后要么拿 GPIO 模拟起始位再切回 SPI 发送,要么在中断里折腾片选,复杂度反而上去了。GPIO 模拟时序的代码可读性高,遇到时序问题也好排查,所以我最终一直用纯模拟方式。
值得一提的是,DIN 这根线在发送方向上是 MCU 输出,但如果这款 TM1624 支持按键扫描回读,同一根线在接收窗口内会变成输入。做硬件设计时在 DIN 上加一个 10k 上拉电阻,能让双向切换时电平不会悬空,少一些“偶尔读回全 0 或全 1”的玄学问题。
3. 从寄存器到手写驱动:HK32F030M 点亮 TM1624 的最小可用代码
3.1 底层引脚初始化与字节发送:先让波形对起来
驱动代码的骨架是这三个函数:引脚初始化、发送一个字节、发送一帧命令或数据。引脚初始化没什么特别,就是标准的 GPIO 配置,但有一个细节值得强调:初始化顺序最好是“先配置 CLK 到空闲电平,再拉高/拉低 DIN”,避免上电瞬间 DIN 和 CLK 同时跳变导致 TM1624 误判一个伪起始位。
/* tm1624_drv.c —— HK32F030M + TM1624 底层驱动片段 */ #include "hk32f030m_gpio.h" /* 按实际工程的头文件路径调整 */ #define TM1624_CLK_PORT GPIOB #define TM1624_CLK_PIN GPIO_PIN_0 #define TM1624_DIN_PORT GPIOB #define TM1624_DIN_PIN GPIO_PIN_1 static void TM1624_GPIO_Init(void) { GPIO_InitTypeDef gpio = {0}; /* 打开 GPIOB 时钟,HK32F030M 的 GPIO 挂载在 APB 总线上 */ __HAL_RCC_GPIOB_CLK_ENABLE(); /* CLK 推挽输出,翻转速率选最高档,保证边沿足够陡峭 */ gpio.Pin = TM1624_CLK_PIN; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(TM1624_CLK_PORT, &gpio); /* DIN 同样配置为推挽输出 */ gpio.Pin = TM1624_DIN_PIN; HAL_GPIO_Init(TM1624_DIN_PORT, &gpio); /* 关键一步:先把 CLK 置于空闲电平,再让 DIN 静止 */ HAL_GPIO_WritePin(TM1624_CLK_PORT, TM1624_CLK_PIN, TM1624_CLK_IDLE_HIGH ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(TM1624_DIN_PORT, TM1624_DIN_PIN, GPIO_PIN_RESET); }逻辑说明:HK32F030M 的 HAL 库用法和主流 STM32 风格接近,如果你的工程用的是标准外设库,对应改成 GPIO_InitStructure 写法就好。这段代码把两个引脚都配成推挽输出,HAL_GPIO_WritePin 控制电平;最后的电平预置是为了保证上电后 DIN 和 CLK 不处于跳变过程中。
参数说明:GPIO_SPEED_FREQ_HIGH 对应最高翻转速率,对 TM1624 这种低速设备来说不是必需的,但预留一点边沿余量对长线传输有帮助。如果你把 CLK 放在另外的端口,记得同时打开对应端口的时钟,否则 HAL_GPIO_Init 会静默失败,代码看不出来,逻辑分析仪上则是一根平线——这类问题排查起来很费时间。
字节发送函数是模拟时序的核心。它的逻辑是:对要发送的每一位数据,先把 DIN 拉高或拉低,然后翻转 CLK 产生锁存沿,最后恢复 CLK 空闲电平。位循环顺序按数据手册定义,通常是从高位到低位,但不同芯片可能相反,所以我把位序抽成了宏,方便一键调换。
/* 发送一字节,位序和边沿由宏控制 */ #define TM1624_BIT_ORDER_LSB 0 /* 0=MSB先行,1=LSB先行 */ #if TM1624_BIT_ORDER_LSB #define TM1624_SEND_LOOP_START 0 #define TM1624_SEND_LOOP_END 8 #define TM1624_SEND_LOOP_STEP 1 #else #define TM1624_SEND_LOOP_START 7 #define TM1624_SEND_LOOP_END -1 #define TM1624_SEND_LOOP_STEP -1 #endif static void TM1624_SendByte(uint8_t data) { int8_t i; for (i = TM1624_SEND_LOOP_START; i != TM1624_SEND_LOOP_END; i += TM1624_SEND_LOOP_STEP) { /* 先置数据,再拉时钟,保证数据在边沿前已经稳定 */ HAL_GPIO_WritePin(TM1624_DIN_PORT, TM1624_DIN_PIN, (data & (1u << i)) ? GPIO_PIN_SET : GPIO_PIN_RESET); TM1624_DelayHalfBit(); /* 数据建立时间 */ /* CLK 跳到锁存边沿,TM1624 此刻读取 DIN */ HAL_GPIO_WritePin(TM1624_CLK_PORT, TM1624_CLK_PIN, TM1624_DATA_EDGE ? GPIO_PIN_SET : GPIO_PIN_RESET); TM1624_DelayHalfBit(); /* 保持时间 */ /* CLK 回到空闲电平 */ HAL_GPIO_WritePin(TM1624_CLK_PORT, TM1624_CLK_PIN, TM1624_CLK_IDLE_HIGH ? GPIO_PIN_RESET : GPIO_PIN_SET); TM1624_DelayHalfBit(); } }逻辑说明:这个函数把每比特分成数据建立、边沿锁存、电平恢复三个阶段,TM1624_DelayHalfBit 提供一个半位周期的延时。数据手册里一般会给出 CLK 最小高低电平宽度,换算下来就是这里的延时长度。比如要求最小 300ns,而 MCU 主频 48MHz,一个空循环大约能拖出几百纳秒,用 NOP 循环或者 DWT 延时都能满足。
参数说明:TM1624_BIT_ORDER_LSB 这个宏是排查乱码时的第一嫌疑。如果你的代码看着完全正常,但显示出来每个数字都错位,往往是这个宏的值反了。DelayHalfBit 的延时长度不要盲目加大,太长的时钟周期虽不影响功能,但在做按键扫描回读时会拖慢整体响应速度,合理范围在 0.5μs 到 10μs 之间。
3.2 命令字与段码表:显示控制、亮度调节和地址自增
TM1624 的数据手册会把指令分成几类:数据命令设置(决定后续操作是写显示还是读按键)、地址命令(决定从哪个显示内存地址开始)、显示控制命令(开关显示和调亮度)。这三类命令靠不同的命令头区分,发送方式和普通数据字节一样,只是芯片内部按命令头解析。
/* 常见命令字定义——务必对照你手上芯片的 datasheet 核对 */ #define TM1624_CMD_DATA_AUTO 0x40 /* 数据命令:地址自增,写显示 */ #define TM1624_CMD_DATA_FIXED 0x44 /* 数据命令:固定地址写显示 */ #define TM1624_CMD_DISPLAY_ON 0x8F /* 显示控制:开显示,最大亮度 */ #define TM1624_CMD_DISPLAY_OFF 0x80 /* 显示控制:关显示 */ static void TM1624_WriteCmd(uint8_t cmd) { /* 发送起始位:DIN 拉低,CLK 产生一个完整脉冲 */ HAL_GPIO_WritePin(TM1624_DIN_PORT, TM1624_DIN_PIN, GPIO_PIN_RESET); TM1624_SendByte(cmd); /* 结束位:DIN 拉高,CLK 再产生一个脉冲 */ HAL_GPIO_WritePin(TM1624_DIN_PORT, TM1624_DIN_PIN, GPIO_PIN_SET); TM1624_SendByte(0x00); /* 结束脉冲,高电平伴随时钟 */ }逻辑说明:起始位和结束位的定义是 TM1624 这类芯片和普通 SPI 最大的不同。普通 SPI 靠片选信号包住一帧,TM1624 靠 DIN 在开始时的下拉和结束时的上拉来标示帧边界。因此发送命令时不能只发一个字节,还要把起始和结束的“包装”一起发出去,否则芯片会把后续的数据流解析错位。
参数说明:TM1624_CMD_DATA_AUTO 和 TM1624_CMD_DATA_FIXED 的区别在于后续写显示数据时,地址是否自动递增。自动模式适合一次把多位数码管的段码连续写入,固定模式适合只改某一位而不影响其他人。实际使用中,我习惯每次刷新时用自动模式连发全部位的数据,代码简单且刷新速度快。
接下来是段码表。七段数码管带小数点的标准编码是共阴极接法下的“位段对应表”,数字 0~9 的段码有固定规律,但不同厂家的 PCB 走线可能让段和引脚的对应关系变化,所以这张表一定要自己验一遍,别直接抄网上的。
/* 标准共阴数码管段码表:bit0=a, bit1=b, ... , bit7=dp */ static const uint8_t SEG_NUM[] = { 0x3F, /* 0: a b c d e f */ 0x06, /* 1: b c */ 0x5B, /* 2: a b d e g */ 0x4F, /* 3: a b c d g */ 0x66, /* 4: b c f g */ 0x6D, /* 5: a c d f g */ 0x7D, /* 6: a c d e f g */ 0x07, /* 7: a b c */ 0x7F, /* 8: all */ 0x6F, /* 9: a b c d f g */ }; /* 显示控制:亮度等级 0~7,命令字 = 0x88 + level */ void TM1624_SetBrightness(uint8_t level) { if (level > 7) level = 7; TM1624_WriteCmd(0x88 + level); }逻辑说明:SEG_NUM 数组的排列依据是“bit0 对应 a 段、bit1 对应 b 段……bit7 对应小数点”。如果你的板子上数码管的 a 段接的是 TM1624 的 SEG1,而这个 SEG1 在芯片内部对应数据位 bit2,那这张表就要整体移位。调这种问题最直接的办法是在显示函数里单独写入测试值 0x01,看哪一段亮,然后调整映射关系。
参数说明:0x88 起的显示控制命令,低三位是亮度等级,值越大越亮,但要注意亮度等级不是线性变化,0 和 1 之间可能差别很小,1 和 7 之间差异明显。如果产品需要夜间模式,可以在主循环里根据环境光照传感器调整这个值,不用重新初始化。
3.3 封装显示函数:把要显示的数字变成 TM1624 的 RAM 数据
有了字节发送函数和段码表,剩下的就是把业务层的“显示 123”翻译成 TM1624 显示内存的写入序列。这一步的常见做法是:先选中首地址,然后按自动递增模式把每一位的段码按顺序写入。需要显示小数点的场景,就把段码的最高位置 1。
/* 显示一个无符号整数,digits 指定总位数,带前导零 */ void TM1624_ShowNumber(uint32_t num, uint8_t digits) { uint8_t i; uint8_t buf[8] = {0}; for (i = 0; i < digits; i++) { buf[i] = SEG_NUM[num % 10]; num /= 10; } /* 写入命令:地址自增模式 */ TM1624_WriteCmd(TM1624_CMD_DATA_AUTO); /* 发送起始地址,以手册为准,常见为 0xC0 */ TM1624_WriteCmd(0xC0); /* 按位发送段码,低位对应显示器的低位 */ for (i = 0; i < digits; i++) { TM1624_SendByte(buf[i]); } /* 结束帧 */ HAL_GPIO_WritePin(TM1624_DIN_PORT, TM1624_DIN_PIN, GPIO_PIN_SET); TM1624_SendByte(0x00); }逻辑说明:buf 数组从低位开始存段码,循环里先取个位再取十位,这样写出来的显示顺序是从右往左的,符合一般数码管显示习惯。起始地址命令 0xC0 在 TM1624 上对应第一个显示内存单元,如果你的产品是 4 位一体数码管,digits 传 4,buf 只用到前 4 个元素。发送完最后一个段码后,DIN 拉高并补一个时钟脉冲作为结束,和命令发送的结束格式保持统一。
参数说明:这个函数有一个隐含限制——它不支持显示字母。产品如果需要显示“Err”“AbC”这类字符串,就得扩展一个字符段码表,把 A~F 和常见符号加进去。另外,如果 TM1624 的显示内存和你数码管的位选顺序刚好相反,把 for 循环里的发送顺序倒过来即可,不需要改段码表。
4. 避坑指南:TM1624 驱动最常见的 5 个翻车现场
4.1 现象:显示全是乱码,数字完全对不上
原因:位序搞反或者段码表映射错误。我第一次调这块芯片的时候,显示 1 出来的是类似横杠的图形,查了半天硬件,最后发现 TM1624 数据手册里明确写了“MSB first”,而我参考的例程是 LSB first。这种问题用逻辑分析仪也看不出来,因为波形本身是完整的,只是位序反着。
解决:把代码里的 TM1624_BIT_ORDER_LSB 宏从 0 改成 1,或者反过来。改完再显示一次 0x01 测试值,确认点亮的是预期的 a 段。如果改完位序依然不对,给段码表做一个索引映射,用测试值逐位验证每个 bit 对应哪个笔画,然后把表重排。这是唯一可靠的办法,靠猜是浪费时间。
4.2 现象:上电后第一位常亮,后面几位怎么发数据都不亮
原因:上电时 TM1624 的显示内存是随机的,而驱动初始化只配置了 GPIO,没有关闭显示或清空 RAM。首位常亮通常是因为该地址本来就被写入了 0xFF,而你又没发送过显示控制命令,芯片处于默认开启状态。
解决:初始化函数里先发送 TM1624_CMD_DISPLAY_OFF 关显示,然后写入全 0 清空显示内存,最后再发开显示命令。顺序不能变,否则会出现上电瞬间闪烁一下再熄灭的现象。这个顺序我在所有项目里都固定下来,已经成为习惯。
4.3 现象:显示亮度不均匀,左边亮右边暗
原因:TM1624 内部扫描位数和实际数码管位数不匹配。如果你接的是 4 位数码管,但芯片工作在 6 位扫描模式,多出来的两个空位会分摊扫描时间,导致亮度下降和不匀。这是数据手册里“显示模式设置命令”的作用范围,容易被忽略。
解决:在初始化里显式设置显示模式,匹配实际使用的数码管位数。发完模式命令之后,再设置亮度等级。如果重新上电后依然不均匀,检查模式命令是否被后续的清空 RAM 操作覆盖——有些芯片的清空命令会顺带重置显示模式,此时需要把模式设置放在清空之后。
4.4 现象:偶尔闪一下,或者显示内容错位一两位
原因:主循环里频繁调用显示函数,发送一帧的过程中被中断打断,导致 CLK 和 DIN 的电平状态被撕裂。这种问题在带定时器中断的工程里很常见,因为 TM1624 的时序对中间插入的长时间高电平会解析成错误的帧边界。
解决:在发送帧期间暂时关闭可能产生长延时的中断,或者把发送函数放进临界区。我一般会在 TM1624_WriteCmd 开头关中断,结束位发完再开。注意临界区时间要短,一帧数据大约几十微秒,对毫秒级的中断任务几乎无影响。如果项目里有无线射频这类对中断响应极其敏感的外设,更好的方案是给显示驱动单独开一个 DMA 缓冲,用定时器按位推送数据,但代码复杂度会明显上升。
4.5 现象:按键回读总是读到 0xFF 或者偶尔丢键
原因:DIN 线在双向模式下没有正确切换方向。TM1624 的按键扫描功能要求 MCU 在发送读命令后,把 DIN 配置为输入,然后从线上读回按键状态。如果 GPIO 还是输出模式,读到的自然全是高电平或不确定值。
解决:在读命令发送完成后,把 DIN 引脚切换为输入模式,并使能内部上拉。读回每一比特时,留意 TM1624 是在 CLK 的哪个边沿更新 DIN——多数芯片会在下降沿更新,那么 MCU 应该在上升沿读取。读完最后一比特后,再把引脚切回输出模式。整个切换过程要加 1μs 左右的延时,让电平稳定后再操作时钟,否则读回的比特流偶发出错。
5. 进阶:按键回读、多芯片级联与工程化封装
5.1 按键回读:同一根数据线上的双向时序
TM1624 的按键扫描是它相比纯数码管驱动芯片的核心卖点。使用时,按键矩阵的行列分别接到 TM1624 的 KEY 引脚和 GRID 引脚,MCU 发送读按键命令后,芯片会把扫描结果通过 DIN 逐位送回。代码的核心在于方向切换和时钟配合:发送完读命令后,把 DIN 改成输入模式,然后在每个时钟边沿读取电平,直到收满 N 个比特。我见过不少工程师卡在这一步,不是硬件问题,而是切换方向后没有等 GPIO 稳定就开始采数据。
/* 读取按键状态,返回值为按键位图,0 表示无按键 */ uint16_t TM1624_ReadKeys(void) { uint16_t key = 0; uint8_t i; TM1624_WriteCmd(0x42); /* 读按键命令字,以手册为准 */ /* DIN 切输入,使能上拉 */ GPIO_InitTypeDef gpio = {0}; gpio.Pin = TM1624_DIN_PIN; gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_PULLUP; HAL_GPIO_Init(TM1624_DIN_PORT, &gpio); TM1624_DelayHalfBit(); /* 方向切换稳定延时 */ for (i = 0; i < 16; i++) { /* CLK 产生一个脉冲,上升沿后读取 DIN */ HAL_GPIO_WritePin(TM1624_CLK_PORT, TM1624_CLK_PIN, GPIO_PIN_SET); TM1624_DelayHalfBit(); if (HAL_GPIO_ReadPin(TM1624_DIN_PORT, TM1624_DIN_PIN) == GPIO_PIN_SET) { key |= (1u << i); } HAL_GPIO_WritePin(TM1624_CLK_PORT, TM1624_CLK_PIN, GPIO_PIN_RESET); TM1624_DelayHalfBit(); } /* 切回输出模式,准备下一次显示刷新 */ gpio.Mode = GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(TM1624_DIN_PORT, &gpio); return key; }逻辑说明:GPIO 切换的稳定延时是容易忽略的细节。推挽输出切到输入模式后,引脚处于高阻态,内部上拉开始给寄生电容充电,如果立即采样,可能读到不稳定的中间电平。1μs 的等待在 48MHz 主频下大约几十个空循环,实现成本很低,却能显著降低偶发错键的概率。
参数说明:读回比特数取决于芯片支持的按键数量,常见的是 8 位或 16 位。如果你的 TM1624 版本只支持 8 个按键,循环条件改成 i < 8 即可,多余的位读回全是 0。按键位图与物理按键的对应关系,需要对照芯片手册的 KEY1~KEYn 与 GRID 交叉表,建议在调试阶段写一个循环点亮测试,逐个按键确认,别依赖想当然的映射。
5.2 多芯片级联:时钟线共享,数据线分路
一个 TM1624 最多驱动十几位数码管,如果产品面板需要二十位甚至更多,可以考虑级联两颗芯片。常见的接法是把两颗芯片的 CLK 并联到同一个 GPIO,DIN 各接一个独立 GPIO,发送时先送第一颗的数据再送第二颗,或按相反顺序,取决于硬件上哪颗离 MCU 更近。由于 TM1624 没有片选脚,两颗芯片会同时收到所有时钟脉冲,所以发送时必须保证同一时刻只有一颗芯片的 DIN 有有效数据,另一颗的 DIN 保持空闲电平。
/* 级联发送示例:din0 接第一颗,din1 接第二颗 */ void TM1624_SendFrame(uint8_t chip0_buf[], uint8_t chip1_buf[], uint8_t len) { TM1624_WriteCmdShared(TM1624_CMD_DATA_AUTO); TM1624_WriteCmdShared(0xC0); for (uint8_t i = 0; i < len; i++) { TM1624_SendByteToChip(chip0_buf[i], CHIP0_DIN); TM1624_SendByteToChip(chip1_buf[i], CHIP1_DIN); } TM1624_EndFrameShared(); }逻辑说明:两颗芯片的 CLK 是同一个,所以每次时钟脉冲到来时,两颗芯片都在各自 DIN 上采样。上面的代码交替发送两芯片同一地址的数据,保证时钟节拍一致。如果先发完 chip0 全部数据再发 chip1,第一颗芯片会继续接收属于第二颗的字节,导致显示错位。这是级联最常见的坑。
参数说明:级联数量增加后,每帧的总时长线性增长,因此主循环的刷新频率要相应降低。对于人眼来说,50Hz 以上的刷新率已经足够稳定,一帧数据如果超过 2ms,刷新率会跌到 500Hz 以下,可能出现轻微闪烁,建议把显示刷新放到定时器中断里均匀触发,而不是在 while 循环里忙等。
5.3 工程化封装:把“能用”的代码变成“好用”的模块
最后聊点代码习惯。接手过太多驱动代码,功能没问题,但维护起来让人头疼,典型症状是:魔法数字满天飞、段码表散落在主文件里、延时函数各写各的。我现在的做法是,把 TM1624 相关代码单独成一个模块,对外只暴露 TM1624_Init、TM1624_ShowNumber、TM1624_SetBrightness、TM1624_ReadKeys 四个接口,头文件里不出现任何寄存器操作。内部实现中,所有命令字用宏定义,延时函数统一为 TM1624_DelayHalfBit,并在头文件顶部留一段注释说明时序参数如何根据手册调整。
验证驱动是否正确,我的顺序是固定的:先发显示测试命令点亮所有段,确认硬件接线没问题;再一位一位发送 0x01 到 0x80 测试值,画出实际段码表与手册对照;然后显示 0~9 数字,观察笔画是否正确;最后加按键回读和亮度调节,全部通过后再接业务代码。这个过程大约一个晚上就能跑完,但能省掉后面至少一周的联调时间。我有个改不掉的习惯:拿到任何新芯片,先把时序图画在纸上,再写第一行代码。这个习惯帮我避开了至少三次因为数据位序导致的翻车。希望帮到你。
本文还有配套的精品资源,点击获取