
简介这份资源是 TM16xx 系列芯片的驱动程序源码包面向需要驱动 TM1640 单色 LED 显示模块的嵌入式开发者。TM1640 常用于电子表、数字显示屏等设备驱动采用模块化设计包含头文件与实现文件可直接调用初始化、写数据、刷新显示等接口实现 7 段数码管数字与字符显示便于 C 语言嵌入式项目移植。包内共 2 个文件1 个 .h 头文件与 1 个 .c 源文件压缩包约 2KBdisplay.h 声明初始化、数据写入与显示更新函数及结构体display.c 则基于 GPIO 或 SPI/I2C 实现底层读写并附带数码管数据编码逻辑。资源页已有 834 人浏览学习。对初次接触 TM16xx 驱动的开发者这份资源提供可直接使用的驱动骨架与清晰代码组织省去从头配置通信协议的步骤配合 GPIO、SPI/I2C 及数码管编码原理说明还能帮助理解底层驱动与硬件交互的完整流程。1. TM1640 到底是什么从一份 zip 里的驱动源码头疼说起买过数码管模块或点阵屏的人大概率见过这类东西店家发来一个 TM1640.zip解压出来基本是源文件加一份 datasheet。不少人因为搜“TM1640 驱动程序”走进来其中一半是被 Windows 数字签名弹窗逼来的对着 LED 驱动芯片找电脑驱动方向从一开始就偏了。TM1640 是 TM16xx 系列里的 LED 驱动控制芯片内部集成串行接口、数据锁存器和驱动电路。一片能直接控制 16×8 共 128 个 LED 点最常见用法是 8 位数码管或点阵组合单片机只要占用三根普通 GPIO就能把显存里的数据刷到屏幕。相比并行总线它省引脚代价是协议不标准型号之间时序有差异。这篇按这类芯片的通用做法先讲协议再给最小驱动移植到 STM32、ESP32、51 都行最容易被绕进去的坑也会提前说明。2. 先把 TM1640 的协议读懂CLK/DIO/STB 三根线如何点亮 128 个点2.1 引脚与供电3.3V 单片机能带 5V 模块吗TM1640 常见封装是 SOP24 或 DIP24模块上一般只引出四五个引脚。接线前先认清每根线的角色VDD 和 GND 是电源STB 是帧同步CLK 是时钟DIO 是数据有些模块叫 DIN。以一块 8 位共阴数码管模块为例VDD 接 5V 还是 3.3V 要看模块上的电平转换和限流电阻很多成品模块板载了 74HC245 或三极管阵列3.3V 逻辑电平可以直接对接如果是裸芯片则以 TM1640 手册给出的输入高电平阈值为准不建议 3.3V 输出直连 5V 供电的芯片输入端。引脚方向作用STB输入帧同步拉低开始一次通信拉高结束CLK输入时钟上升沿锁存数据DIO输入/输出串行数据MSB 先发VDD电源供电典型 5VGND电源地线上拉电阻这一项容易被忽略。CLK 和 DIO 在空闲时最好保持确定电平我一般会各加一个 4.7kΩ 到 VDD 的上拉如果板上已经焊了就不必重复。电源方面TM1640 驱动 8 位数码管全亮时峰值电流能到几十毫安级别用开发板自带的 3.3V LDO 容易掉压导致亮度不均建议单独供电共地。2.2 帧结构与时序没有起始位、没有 ACK和 I2C 是两回事很多第一次接触 TM1640 的人会把它当成 I2C 设备理由很简单引脚叫法和 CLK/DIO 的组合太像 I2C 了。但 TM16xx 系列是“类 I2C”而非标准 I2C最典型的差别是TM1640 用 STB 拉低表示一帧开始STB 拉高表示帧结束并没有 I2C 里的 START/STOP 条件和 ACK 应答位。数据在 CLK 为低时变化CLK 上升沿被芯片锁存每字节 8 位、MSB first这些和 I2C 的发送逻辑相似但主机不能指望从机回 ACK也不能像标准 I2C 那样做多主机抢总线。一帧典型的时序顺序是先把 STB 拉低然后连续送出若干个字节命令字节和数据字节最后把 STB 拉高。这里有个容易写错的地方STB 必须在整帧传输期间保持低电平不能每发一个字节就拉高一次。CLK 空闲时高或低都可以只要满足数据手册上的建立/保持时间即可常见做法是让 CLK 空闲为低每个 bit 先拉低 CLK、改 DIO、再拉高 CLK。少数模块没有引出 STB而是直接接低驱动里就需要把 STB 固定置低协议用法相同。时序参数方面一般的 TM1640 datasheet 会给出 CLK 频率上限和建立时间。GPIO 模拟时不需要抠得太细只要每 bit 延时不小于几百纳秒就能稳定工作真正重要的不是速度而是 DIO 的电平变化必须发生在 CLK 低电平期间。有了这个基本认识读任何一份 TM16xx 的手册时序图都不会发怵。2.3 三类命令和 16 字节显存0x40、0xC0、0x88 背后的设计TM1640 的控制方式围绕三组命令展开和 TM1637/TM1638 一脉相承数据命令设置读写模式与地址模式地址命令指定显存起始地址显示控制命令负责亮度和开关。常用命令如下表具体取值以你手里的 datasheet 为准。功能命令字说明数据命令0x40自动地址递增连续写显存数据命令0x44固定地址写每次写一个字节地址命令0xC0~0xCF16 字节显存地址0xC0 是第一字节显示控制0x88关显示显示控制0x89~0x8F开显示低 3 位对应 1~7 档亮度显示控制命令是很多人用错的重灾区。常见例程里只看到 0x88 和 0x8F 两个值以为亮度不可调其实 0x89 到 0x8F 之间的档位都被漏掉了0x8F 只是最大亮度那一档。显存一共 16 字节每个字节对应 8 个段合计 128 点写入时从地址 0xC0 开始地址会自动加 1直到收到 STB 拉高的一帧结束。命令设计上有一点要区分0x40 数据命令里的自动地址模式和 0x44 固定地址模式只管后续数据字节的地址行为并不等于写显存本身。显存地址要靠 0xC0 这条命令来指定。也就是说一次完整的“往某个地址写数据”操作至少需要两帧第一帧发 0x40 或 0x44第二帧发地址命令加数据。有些简化例程会把 0x40 和 0xC0 连在一帧里发短帧做得快但对芯片的命令解析边界要求更高不一定每种型号都买账。3. 手写一份可以抄作业的 TM1640 驱动从初始化到显存刷新3.1 先选实现方式GPIO 模拟比硬件 I2C 更省心驱动写法大体有三条路直接操作寄存器写 GPIO 模拟时序、用 MCU 的硬件 I2C 外设、用厂商或模块附带的源码。硬件 I2C 这条路看起来最省事实际最折腾原因是 TM1640 没有标准 I2C 的 ACK 位很多 MCU 的 I2C 外设在主机模式下把“没收到 ACK”当成总线错误处理轻则报错重则卡死即便能关掉 ACK 检查还要处理 R/W 位、起始停止条件的差异。有同行拿 STM32F4 的 I2C 外设来做这类固件设计结果在时序对齐上花的时间比手写驱动还长。我一般选 GPIO 模拟理由很直白三根线都是普通输出驱动逻辑十几行换平台只改引脚宏。下面的代码基于 STM32 HAL 风格编写但只用了 GPIO 置位、复位和延时移植到 ESP32、51、GD32 时只需要替换 HAL_GPIO_WritePin 这一层的实现。模块的 STB、CLK、DIO 分别接三个 GPIO全部配置为推挽输出如果选的是带按键扫描的 TM1638DIO 还要切换输入方向那是另一个话题。3.2 引脚定义与底层字节发送先把“一帧”写对先给出引脚的宏定义和三个底层函数。延时函数用简单的空循环实际值不敏感只要保证每 bit 的时钟周期落在几十微秒以内即可。#define TM1640_STB_PORT GPIOA #define TM1640_STB_PIN GPIO_PIN_6 #define TM1640_CLK_PORT GPIOA #define TM1640_CLK_PIN GPIO_PIN_7 #define TM1640_DIO_PORT GPIOA #define TM1640_DIO_PIN GPIO_PIN_8 static void tm1640_delay(void) { for (volatile int i 0; i 20; i) { ; } } static void tm1640_write_byte(uint8_t dat) { for (int i 0; i 8; i) { HAL_GPIO_WritePin(TM1640_CLK_PORT, TM1640_CLK_PIN, GPIO_PIN_RESET); if (dat 0x80) { HAL_GPIO_WritePin(TM1640_DIO_PORT, TM1640_DIO_PIN, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(TM1640_DIO_PORT, TM1640_DIO_PIN, GPIO_PIN_RESET); } dat 1; HAL_GPIO_WritePin(TM1640_CLK_PORT, TM1640_CLK_PIN, GPIO_PIN_SET); tm1640_delay(); } }这段代码的精髓在循环体里的顺序先把 CLK 拉低再改变 DIO最后拉高 CLK 锁存数据。这个顺序保证了 DIO 的建立时间满足要求如果先改 DIO 再拉低 CLK在 CLK 还是高电平的时刻 DIO 跳变芯片可能把跳变误判成别的信号。每发送一位后把数据左移做到 MSB first符合 TM1640 对字节位序的要求。延时放在 CLK 拉高之后作用是给芯片留出采样时间而不是改变 DIO 与 CLK 的相对位置。接下来是一帧的开始和结束函数。开始就是 STB 拉低结束就是 STB 拉高结束之后可以加一个小延时给芯片一点处理命令的余量再开始下一帧。static void tm1640_frame_start(void) { HAL_GPIO_WritePin(TM1640_STB_PORT, TM1640_STB_PIN, GPIO_PIN_RESET); tm1640_delay(); } static void tm1640_frame_end(void) { HAL_GPIO_WritePin(TM1640_STB_PORT, TM1640_STB_PIN, GPIO_PIN_SET); tm1640_delay(); }3.3 写显存自动地址递增与固定地址两种姿势有了字节发送和一帧控制写显示内容就很简单了。最常用的方式是先把显示缓冲区整体刷进显存发数据命令 0x40然后发地址命令 0xC0最后连续发 16 个字节。注意这三段可以在同一个 STB 低电平的“长帧”里完成也可以分开成普通帧但 STB 不能中途反复跳变否则芯片的命令机可能提前复位。void tm1640_update_display(uint8_t *buf, uint8_t len) { tm1640_frame_start(); tm1640_write_byte(0x40); /* 数据命令自动地址递增 */ tm1640_frame_end(); tm1640_frame_start(); tm1640_write_byte(0xC0); /* 地址命令从第 0 字节开始 */ for (uint8_t i 0; i len; i) { tm1640_write_byte(buf[i]); } tm1640_frame_end(); }这里把数据命令单独放一帧地址和显存数据放另一帧是兼容性最好的写法。len 建议固定为 16 或按你的模块实际显存字节数设置8 位数码管通常用 8 字节或 16 字节取决于每位是 8 段还是 16 段。如果 len 超过 16地址会自动回绕覆盖前面的字节写代码时要注意别越界。固定地址模式则适合只修改某一个字节的场景比如只想改第 3 位的数字不必整屏重刷。先发 0x44再发 0xC0address紧跟一个数据字节一帧内完成void tm1640_set_one_byte(uint8_t addr, uint8_t dat) { tm1640_frame_start(); tm1640_write_byte(0x44); /* 数据命令固定地址 */ tm1640_frame_end(); tm1640_frame_start(); tm1640_write_byte(0xC0 (addr 0x0F)); /* 地址命令0x00~0x0F */ tm1640_write_byte(dat); tm1640_frame_end(); }两个函数放在同一个文件里就能覆盖绝大多数显示需求定期整体刷新用 update_display局部闪烁或跑马灯用 set_one_byte。注意 0xC0 命令的地址只有低 4 位有效地址在 0x00~0x0F 之间如果直接写 0xC016会被截断回 0这也是常见的显存错位原因之一。3.4 显示开关与亮度把 0x89~0x8F 用起来点亮屏幕不等于“上电就亮”还要主动发一条显示控制命令。很多例程只发一次 0x8F 就认为完事实际在需要低功耗或做闪烁效果时开关和亮度要能随时切换。显示控制命令的低 3 位从 1 到 7正好对应七档亮度0x88 是关显示。void tm1640_display(uint8_t on, uint8_t brightness) { uint8_t cmd 0x88; /* 0x88: 关显示 */ if (on) { if (brightness 0) brightness 1; cmd (uint8_t)(0x88 | (brightness 0x07)); /* 0x89~0x8F */ } tm1640_frame_start(); tm1640_write_byte(cmd); tm1640_frame_end(); }调用时 tm1640_display(1, 5) 就能以第 5 档亮度点亮屏幕。这里有个容易忽略的细节关显示命令 0x88 并不会清空显存只是把输出关掉重新发 0x8F 时之前的字符会原样回来所以用这条命令做闪烁动画最合适不需要额外保存一份“上次的显示内容”。数码管段码映射通常会单独维护一张表比如共阴数码管 0~9 对应 0xC0、0xF9、0xA4 等经典段码。需要关心的是显存字节顺序与数码管位选的对应关系不同厂家模块可能把“第 1 位”映射到地址 0xC0 还是 0xC7第一次点亮时先写一个 0x00~0xFF 的递增图案观察屏幕变化方向比猜可靠得多。4. 移植到 ESP32 / STM32F4 / 51TM1640 驱动“换壳”的三个关键点4.1 把引脚和延时抽成宏换平台只改调用层上一章代码里的 HAL_GPIO_WritePin 是 STM32 风格移植到别的平台时最省事的做法不是逐行重写而是把引脚操作统一包一层。常见做法是定义一组接口宏例如 TM1640_DIO_SET 和 TM1640_DIO_RESET底层分别映射到平台各自的库函数延时函数单独抽出来因为裸机循环、RTOS 的 vTaskDelay、HAL 的 delay 机制差异很大。#define TM1640_STB_SET() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_SET) #define TM1640_STB_RESET() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_RESET) #define TM1640_CLK_SET() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET) #define TM1640_CLK_RESET() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET) #define TM1640_DIO_SET() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET) #define TM1640_DIO_RESET() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET)换到 ESP32 时把 SET/RESET 的实现替换成 gpio_set_level换到 51 时直接写 sbit 定义的引脚赋值。底层函数 tm1640_write_byte 保持结构不变协议逻辑部分完全不用动。很多模块厂家的示例源码之所以难移植就是引脚操作和协议逻辑写在一起改一个引脚要全文搜索 GPIO_PIN_6 这类名字所以自己整理时建议从一开始就分两层。延时用微秒级还是毫秒级取决于你的 CLK 频率。GPIO 模拟的最高速度受限于延时函数正常控制在几十 kHz 到几百 kHz 就足够数码管显示不需要高速刷新反而把 CLK 放慢能显著降低长线干扰。我试过把 CLK 周期拉到 1ms 仍然能正常显示所以“延时太小导致翻车”的概率远低于“延时过大导致动画闪烁”。4.2 电平混接的两种处理上拉方案和电平转换芯片很多现成模块自带 5V 供电和板载限流电阻但 MCU 是 3.3V 系统。这时先看模块是否已经做了电平适配如果 DIO/CLK/STB 上直接有上拉到 VDD 的电阻3.3V 高电平通常也能被识别如果没有有两条路可走。第一条是改供电把 TM1640 的 VDD 接到 3.3V很多共阴数码管模块在 3.3V 下亮度仍然可用第二条是加电平转换对 CLK 和 DIO 这种单向信号可以用两个 2N7002 搭一个简单的电平转换电路成本几毛钱或者直接用模块带的 74HC245 缓冲。一个常见的认识误区是“3.3V 推挽输出直接接 5V 输入引脚没问题因为高电平阈值低”。实际裸芯片场景下如果 DIO 和 CLK 被模块内部上拉到 5V3.3V 推挽输出在高电平状态下可能拉不动这个上拉造成波形变成中间电平。症状就是显示时好时坏、乱码概率高。排查方法很简单示波器看 CLK 高电平是不是真的到 5V没有示波器就用万用表量静态电平不能量到干净的高电平就先处理上拉。4.3 从 TM1640 到 TM16xx 系列公共抽象与差异清单TM16xx 系列里有几个常用型号命令体系高度相似做产品选型时很容易互相替换。TM1637 是两线没有 STB常用于 4 位数码管模块TM1638 带 3×8 键盘扫描常用于按键显示一体板TM1640 是三线、显存最大适合点阵和多位显示。它们的共同点是数据命令、地址命令的高位模式接近显示控制命令都在 0x88~0x8F 这一档。型号控制线显存规模典型按键扫描常见场景TM1637CLK/DIO 两线较小无4 位数码管TM1638STB/CLK/DIO 三线8 字节有3×8按键数码管TM1640STB/CLK/DIO 三线16 字节无8 位数码管/点阵做驱动抽象时可以把“写显存”“设亮度”“刷新”三个函数作为统一接口不同型号只改显存大小和帧格式。移植 TM1638 时多出来的键盘读函数单独实现不影响显示部分。这套思路对后续换屏、换 MCU 都友好关键是别把型号特有的东西写进公共层。比如 TM1640 的固定地址命令 0x44 在 TM1637 上可能不支持公共代码就只暴露写显存和亮度型号差异留在底层适配里。如果项目同时用到多个显示模块还可以在公共接口里加函数指针或平台条件编译把“TM1640 驱动”“TM16xx 系列驱动”做成可配置组件。这一步看项目复杂度单屏场景不必过度设计双屏或按键显示一体板场景值得做。5. TM1640 驱动最容易翻车的 5 个点现象、原因、排查顺序下面这五条按生产环境里遇到频次排序前两条偏硬件后三条偏时序和地址映射排查时按这个顺序走会省时间。5.1 上电后屏幕全灭但代码看起来完全正常现象按例程初始化、刷显存、开显示都执行了数码管一个点都不亮万用表量 VDD 也有电压。原因通常不在代码而在 STB 引脚没有被正确接管——模块上的 STB 标注可能对应芯片的片选或使能脚如果 STB 悬空芯片可能一直处于复位或非选中状态有些开发板的某些引脚默认被调试器或板载 LED 占用也会造成同样的现象。解决确认 STB 有明确的上拉或下拉设置初始化里把 STB 置为高电平等发命令时再拉低同时检查 GPIO 模式是否被复用成了别的外设功能。提示遇到全灭先别改时序量电平比看代码快得多。5.2 屏幕乱码或出现“鬼影”偶尔又恢复正常现象同一段代码有时显示正确有时某位乱跳或出现不应亮的段。原因大多是 DIO 电平在 CLK 下降沿附近发生跳变造成芯片把相邻 bit 的相位读错中断频繁的 MCU 上时序函数被打断也会出现类似症状。解决严格保证“CLK 拉低后改 DIOCLK 拉高后延时”并尽量把 CLK 拉低到 DIO 变化之间的代码压缩到最少如果用了中断较多的工程发送字节期间临时关中断也值得一试。5.3 写完显存没反应但亮度命令有效现象显示控制命令能调亮灭说明通信链路是通的但数据刷不进去。原因多半是发送顺序不对——比如把 0xC0 地址命令和 0x40 数据命令放在同一个 STB 帧里不少 TM16xx 芯片要求命令字节之间要有 STB 的重新同步混帧会得到不确定的命令边界。解决按第 3 章的写法把数据命令单独成帧地址和数据再放一帧顺序调整为“先 0x40、再 0xC0数据”一帧一帧来。固定地址模式只发一个字节时地址命令之后不能再带多余字节否则会把第二个字节也当成显存数据写进下一个地址。5.4 亮度只有两档调不细腻现象0x88 和 0x8F 都有反应但中间的档位效果不明显。原因亮度档位不是线性变化低档位在数码管这类高亮负载上肉眼差异很小另外有些模块在 PCB 上做了恒流电阻亮度主要由电阻决定命令字的档位被“钳”住了。解决如果你确实需要大范围调光优先调整供电或限流电阻命令字档位当作辅助手段别指望它做到无级调光。验证时可以用逻辑分析仪看显示控制命令字是否落在 0x89~0x8F 区间如果在区间内而亮度变化不明显重点查板子上的限流电阻。5.5 显存地址错位数字显示位置和代码对不上现象想在第一位显示“1”实际出现在第四位刷整屏时字符顺序反了。原因模块厂家对位选和段选的定义不一样你的段码表和地址递增方向需要镜像。解决写一个递增的二进制测试图案比如 buf[0]0x01、buf[1]0x02……观察亮灯位置反推地址映射关系再调整段码表的排列顺序。这类问题不是芯片的错是模块硬件排布决定的按实际观察修正即可。五条排下来共性是先信波形、再信代码先查电平、再查时序。遇到死活调不通的情况保留最小复现环境把无关功能全删掉往往几分钟就能定位。6. 给驱动留一条“后悔药”用逻辑分析仪验证时序与显存 dump改驱动最怕的不是报错而是“看起来没反应”的静默失败。我的习惯是在驱动里常驻一个调试开关用宏把显存内容和命令帧开关包起来出错时打开 dump配合逻辑分析仪一次性抓完三根线的波形比肉眼猜快得多。#define TM1640_DEBUG 1 #if TM1640_DEBUG extern void tm1640_dump_buf(uint8_t *buf, uint8_t len); #endif逻辑分析仪接 CLK、DIO、STB 三根线采样率建议 1MHz 以上触发条件用 STB 下降沿。抓到波形后重点看三件事STB 低电平是否覆盖整帧CLK 的上升沿处 DIO 是否稳定每帧第一个字节是否为预期命令字。用支持的协议解析插件按“自定义 3 线”或“TM16xx”解码能直接列出命令序列比对着二进制数数轻松。调亮度时我习惯先在示波器上看占空比而不是只看人眼亮度——人眼对低亮度档的差异不敏感判断会失真亮度的验证放到最终整机阶段做驱动阶段只确认命令字符合预期。时至今日我仍会犯一个低级错误改完代码忘了在真机上把延时关闭后的高速模式重新验证一遍导致调试模式下正常、发布模式乱码。现在我把“高速模式回归”写进了自检清单每次改完底层函数都跑一遍全量显示用例。如果你打算长期维护多个显示项目强烈建议把这份驱动整理成不依赖具体芯片型号的抽象层显存大小、引脚映射、亮度范围全部参数化。这套东西花半天时间做后面每次换屏都能省回一倍不止。希望这些踩过的坑和留的调试后路能帮到你至少让你在屏幕上出现第一行数字时少走几步弯路。本文还有配套的精品资源点击获取