简介:TM1640.zip 是一份面向嵌入式开发者的 TM16xx 系列 LED 显示驱动源码包,专为驱动 TM1640 单色 LED 驱动芯片而设计,适用于 7 段数码管数字或字符显示场景,如电子表、数字面板等。资源面向具备 C 语言与基础单片机知识的开发者,帮助其快速集成显示功能,省去从零编写底层驱动的工作。压缩包体积仅 2KB,总计 2 个文件,包含 1 个 .h 头文件与 1 个 .c 实现文件,结构精简。头文件负责函数声明与结构体定义,涵盖初始化、数据写入及显示刷新等接口;实现文件则完成 GPIO/SPI/I2C 等底层通信控制以及数码管数据编码逻辑,整体模块化设计便于移植到不同硬件平台。已有 834 人学习浏览,适合正在开发嵌入式显示模块、需要参考驱动封装思路或直接复用代码的开发者。通过研读该驱动,读者可了解 TM1640 的初始化流程、显示数据组织方式以及底层时序控制要点,为后续扩展 TM16xx 系列其他芯片的学习提供良好基础。
1. TM1640.zip 里的 TM16xx 系列驱动程序:两个源文件如何把数码管点亮
这套 TM1640.zip 里的 TM16xx 系列驱动程序,核心只有 display.h 和 display.c 两个源文件,但它解决的问题相当具体:用单片机上的两根普通 GPIO,按照 TM1640 芯片的串行协议,把要显示的数字和字符送到 7 段数码管上。做电子时钟、温湿度显示、小家电状态屏这类项目,最常见的显示方案就是它,而不是那些动辄几百行、还要接一堆引脚的 LCD 屏。这份驱动薄得有点意外,但薄不代表简单,时钟时序、段码表、亮度寄存器、地址自增策略一个都不能省。新手拿它入门数码管驱动很合适,老手也能从它的接口组织方式里看出多芯片工程该往哪个方向改造。下面从协议开始拆,因为驱动文件里每个函数为什么这么写,全部由协议决定。
2. 先读懂 TM1640 的通信协议:CLK/DIO 双线时序与 RAM 映射
2.1 为什么 TM1640 不需要 SPI/I2C 外设:双线自定义协议
TM1640 是 8 段 × 16 位的单色 LED 显示驱动芯片,一套芯片可以驱动 16 个 7 段数码管,或者 128 个 LED 组成的点阵,工作电压常见 3.3V 到 5V,内部自带 RC 振荡器,所以主控侧不需要额外的时钟源。它的控制口只有两根线:CLK 和 DIO。CLK 是移位时钟,DIO 是双向数据线,写入状态下由主控驱动电平。
这里要先纠正一个常见的误读:TM1640 用的既不是 I2C,也不是标准 SPI,而是一种自定义的两线串行协议。网上很多文章说「类 I2C」或「兼容 SPI」,这是不准确的。标准 I2C 有设备地址和应答位,TM1640 没有地址概念,所有命令直接以字节序列下发;标准 SPI 以 CS 片选界定传输帧,TM1640 用 CLK 高电平期间 DIO 的电平跳变来产生起始和停止条件。这就是为什么很多人在 STM32 上直接挂硬件 I2C 驱动 TM1640 失败,而换成 GPIO 模拟反而一调就好。
TM16xx 系列里,TM1640、TM1637、TM1638 的寄存器结构相似,但命令字节和数据格式并不完全一致。display.h/display.c 这套驱动是按 TM1640 的命令集写的,如果换成 TM1637,需要把数据命令设置位和显示控制命令的低位调整,不能无脑套用。后面章节会讲到具体差异点。
2.2 起始、数据位、停止:三个时序节点决定成败
TM1640 的整帧时序可以拆成三个节点,全部以 CLK 电平为基准:
| 时序节点 | CLK 状态 | DIO 行为 | 说明 |
|---|---|---|---|
| 起始条件 | CLK 高电平 | DIO 从高变低 | 通知芯片开始接收命令 |
| 数据位 | CLK 上升沿 | DIO 电平被采样 | 低位先传输,每次 8 位 |
| 停止条件 | CLK 高电平 | DIO 从低变高 | 通知芯片本次传输结束 |
数据位期间有一点必须严格遵守:DIO 只能在 CLK 低电平期间改变电平,CLK 高电平期间必须保持稳定,否则芯片会把这次电平跳变误判为起始或停止条件。很多乱码问题的根源就在这里——主控在 CLK 高电平期间切换了 DIO,导致芯片帧同步丢失。
写入方向是先发低位(LSB first),这和大部分人的直觉相反。比如要发送 0x3F(二进制 0011 1111),正确顺序是先送 bit0(1),再送 bit1(1),依此类推,最后才是 bit7(0)。如果把方向搞反,0x3F 会变成 0xFC,显示出来就是完全错误的段码。这份驱动里 tm1640_byte 函数用dat >>= 1循环移位,就是专门匹配 LSB first 的写法。
2.3 四类命令与 RAM 映射:0x40、0xC0、0x8F 的配合
TM1640 的命令只有四类,每一类都以命令字节开头,后续跟随数据。驱动里所有函数最终都在拼装这四类命令:
| 命令类别 | 命令字节范围 | 功能 |
|---|---|---|
| 数据命令设置 | 0x40 ~ 0x4F | 设置写模式、地址自增方式 |
| 显示控制命令 | 0x80 ~ 0x8F | 开关显示、设置亮度等级 |
| 地址命令 | 0xC0 ~ 0xCF | 指定显示 RAM 的起始地址 |
| 数据写入(跟随地址后) | 无固定范围 | 写入 1~16 字节的段码数据 |
数据命令设置里,0x40 是最常用的组合:普通模式、地址自动加一、写显示寄存器。0x44 则是固定地址模式,每写一个字节都要重新指定地址,效率低,但在需要单点更新某一位数码管时有价值。
地址命令配合 RAM 使用,TM1640 内部有 16 字节显示 RAM,地址从 0xC0 到 0xCF,每个地址对应一位数码管的 8 个段。写入流程是:先发起始条件,发送 0x40,再发停止条件;然后发起始条件,发送 0xC0(起始地址),接着连续发送 n 个数据字节,最后发停止条件。如果写入超过 16 字节,地址会从 0xC0 回卷,这点在驱动封装时必须防住,否则缓冲长度越界会导致前面几位被覆盖。
显示控制命令的低 4 位是亮度等级,0x88 对应最低亮度,0x8F 对应最亮,共 8 级;0x80 到 0x87 则用于关闭显示。有经验的驱动会把亮度和开关拆成两个独立函数,避免在初始化时只发了开关命令没发亮度命令,导致屏幕全黑这种哭笑不得的情况。
3. 拆解 display.h 与 display.c:驱动文件每个函数的职责
3.1 头文件里到底声明了什么:从初始化到刷新的函数清单
TM1640 的驱动从使用者视角看,只需要四个动作:初始化芯片、写数据、更新显示、调亮度。所以 display.h 里的典型声明如下:
#ifndef __DISPLAY_H #define __DISPLAY_H #include <stdint.h> #define DISPLAY_GRID_NUM 16 void init_TM1640(void); void write_data(uint8_t addr, uint8_t *buf, uint8_t len); void display_update(uint8_t *buf, uint8_t len); void display_on(uint8_t brightness); void display_off(void); #endif这个头文件有意做了两件事:关闭外部可见的数据结构,只暴露操作接口。init_TM1640负责发送数据命令设置和默认显示控制命令;write_data是最底层的写 RAM 函数,addr 是起始地址,buf 是段码数据指针,len 是数据长度;display_update在 write_data 之上封装,多了边界检查和缓存更新逻辑;display_on和display_off分别对应发送 0x88|brightness 和 0x80 命令。
设计上值得注意的一点是 DISPLAY_GRID_NUM 被定义为 16,这是 TM1640 的固有参数,不是随意选的。所有内部数组、循环边界都以这个宏为准,移植到其他 TM16xx 芯片时只需要修改这个宏和相关命令字节。
3.2 实现文件的核心:GPIO 模拟时序与字节收发
display.c 的关键在于把协议时序用 GPIO 电平变化精确还原。通常这套驱动会按下面的架子组织:
#include "display.h" #include "platform_gpio.h" // 平台相关,提供 PIN_DIN/PIN_CLK 的读写 #define TM1640_DIN_HIGH() gpio_write(PIN_DIN, 1) #define TM1640_DIN_LOW() gpio_write(PIN_DIN, 0) #define TM1640_CLK_HIGH() gpio_write(PIN_CLK, 1) #define TM1640_CLK_LOW() gpio_write(PIN_CLK, 0) static void tm1640_start(void) { TM1640_CLK_HIGH(); TM1640_DIN_HIGH(); TM1640_DIN_LOW(); // CLK 高电平时 DIO 拉低,产生起始条件 TM1640_CLK_LOW(); } static void tm1640_stop(void) { TM1640_CLK_LOW(); TM1640_DIN_LOW(); TM1640_CLK_HIGH(); // CLK 高电平时 DIO 拉高,产生停止条件 TM1640_DIN_HIGH(); } static void tm1640_byte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { TM1640_CLK_LOW(); if (dat & 0x01) TM1640_DIN_HIGH(); else TM1640_DIN_LOW(); dat >>= 1; // 低位先传 TM1640_CLK_HIGH(); // 上升沿采样 } } void init_TM1640(void) { tm1640_start(); tm1640_byte(0x40); // 数据命令:地址自增,写显示 RAM tm1640_stop(); tm1640_start(); tm1640_byte(0x8F); // 最亮模式打开显示 tm1640_stop(); }tm1640_start和tm1640_stop对应时序表的起始/停止条件,顺序不能反。先拉高 CLK 再拉低 DIO,这是芯片识别起始的唯一方式;停止条件则是 CLK 高电平时 DIO 从低到高。
tm1640_byte在每次循环里先拉低 CLK,再把 DIO 设置成当前 bit 值,最后拉高 CLK 让芯片采样。注意dat >>= 1的位置在拉高 CLK 之前,确保下一位数据在下一个上升沿到来前已经稳定在 DIO 上。如果把移位放在 CLK 拉高之后,高位数据会提前出现在 DIO 上,造成帧错位。
3.3 段码表与数据编码:从数字 0 到字符 F 的映射
驱动里一定要有一张段码表,常见实现如下:
static const uint8_t seg_table[] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71 };这个表对应显示 0~9、A~F。每个字节的 bit0~bit6 对应数码管的 a~g 七个段,bit7 对应小数点。0x3F 即 0b00111111,点亮 a、b、c、d、e、f 六段,显示数字 0。TM1640 是源极驱动结构,内部输出管为 PMOS,配合共阴数码管使用时,段码 bit 为 1 点亮,bit 为 0 熄灭。如果硬件上接了共阳数码管,需要把整个表逻辑取反,否则会出现数字对但背景全亮的反显效果。
display_update 的封装逻辑通常是:把用户传入的显示数据先经过段码表查表转换,存到内部的 shadow buffer,再调用 write_data 把所有数据一次性刷进芯片。这样做的好处是避免逐位写入时中间状态的闪烁,这个细节在动态刷新场景下影响非常大。
4. 把驱动移植到 STM32:GPIO 配置、延时与实际点亮验证
4.1 改写底层 IO 接口:四个宏替换平台差距
拿到这套驱动后,第一件事不是读代码,而是确认平台 GPIO 操作接口。display.c 把 IO 操作收敛成四个宏,这是移植的关键点。在 STM32 HAL 库环境下,修改如下:
#define TM1640_DIN_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET) #define TM1640_DIN_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET) #define TM1640_CLK_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET) #define TM1640_CLK_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET)GPIO 模式推荐推挽输出带上拉。TM1640 的 DIO 是双向引脚,虽然本驱动只做写入不读按键数据,但推挽输出能保证 DIO 在高电平时的驱动能力,避免因为外部上拉电阻不足导致高电平幅度不够、芯片采样误判。CLK 引脚不需要特殊处理,普通推挽输出即可。
4.2 初始化到显示数字的完整流程:main 函数里跑一遍
硬件连接确定后,初始化代码可以这样组织:
void gpio_config(void) { GPIO_InitTypeDef gpio = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_PULLUP; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &gpio); } int main(void) { uint8_t display_buf[2]; HAL_Init(); gpio_config(); display_buf[0] = seg_table[1]; // 显示 '1' display_buf[1] = seg_table[2]; // 显示 '2' init_TM1640(); write_data(0xC0, display_buf, 2); while (1) { // 主循环,无需持续刷新 } }这里有个容易忽略的点:TM1640 内部有锁存机制,写入显示 RAM 后只要不断电,显示状态会保持,不需要像动态扫描数码管那样频繁刷新。主循环里可以完全不做显示相关操作,这在低功耗场景下很关键——省掉了每毫秒一次的中断刷新负担。
write_data(0xC0, display_buf, 2)中,0xC0 是第 1 位地址,2 是数据长度,表示写入第 1、2 位。如果要从第 3 位开始,地址换成 0xC2 即可。地址自增模式下,数据字节按顺序填充到连续地址。
4.3 验证显示是否正常:全亮测试与逐位扫点
调试新板子时,初始化后第一件事做全亮测试,确认所有段都能点亮:
void test_full_segments(void) { uint8_t test[16]; for (uint8_t i = 0; i < 16; i++) test[i] = 0xFF; // 所有段全部点亮 write_data(0xC0, test, 16); }全亮正常后,再逐位扫点验证地址映射。比如分别向 0xC0、0xC1、0xC2 写入 0x06,观察是否依次点亮第 1、2、3 位上的数字 1。如果出现地址错位,检查 PCB 上数码管的位引脚连接顺序。扫描过程中配合示波器看 CLK 和 DIO 波形,能确认时序是否标准:正常波形下 DIO 只在 CLK 低电平期间跳变,CLK 高电平期间保持稳定,每一帧以 CLK 高时 DIO 的下降沿开头、上升沿收尾。
5. 避坑排查:5 条让 TM1640 出图的真实踩坑记录
5.1 显示乱码没有规律:先查起始条件是否合规
现象:初始化后数码管显示内容完全不可预测,每次上电乱码还不同。
原因:TM1640 的起始条件被错误实现。常见写法是先拉低 DIO 再拉高 CLK,或者干脆省略起始条件直接发数据。芯片收不到合法起始帧,内部逻辑不断尝试重新同步,输出就是随机段码。
解决:起始条件必须严格按「CLK 高 → DIO 拉低 → CLK 低」的顺序执行。我在 tm1640_start 函数里强制固定这个顺序,并且把停止条件独立成函数,不允许在数据传输中途调用任何可能导致 DIO 在高电平时跳变的操作。
5.2 数字对但整体反显:共阴共阳接反了
现象:显示数字 0 时,除了 0 的六段不亮,其他所有段全亮,像底片一样。
原因:TM1640 是源极驱动,适合共阴数码管。硬件上接了共阳数码管,段码逻辑正好相反。
解决:两个方案任选,第一换共阴数码管;第二在驱动里把所有段码取反,即原来 0x3F 变成 ~0x3F。取反方案要注意小数点位也会跟着反转,需要一并处理。
5.3 黑屏但芯片供电正常:亮度命令发成了 0x80 而不是 0x88
现象:初始化代码执行完,数码管一个段都不亮,用万用表量 CLK/DIO 都有波形。
原因:显示控制命令的低 4 位同时包含开关和亮度位。0x80 到 0x87 是关闭显示,0x88 到 0x8F 才是打开显示并设置亮度。有人想设最低亮度,直接发了 0x80,结果把显示关了。
解决:亮度等级从 0 到 7,命令字节是 0x88 | brightness。驱动里 display_on(brightness) 必须强制限定 brightness 范围为 0~7,并在这个范围基础上或上 0x88。
5.4 刷新时明显闪烁:缺影子缓存
现象:每隔几百毫秒调用一次 write_data 更新显示,数码管每次更新都闪一下。
原因:TM1640 写入 RAM 是逐字节的,如果直接往芯片地址写数据,前几个字节更新后、后几个字节还没到,芯片处于新旧数据混合状态,视觉上就是闪烁。
解决:在驱动内部维护一个 shadow buffer。上层应用只修改 shadow buffer,display_update 再把整个 buffer 一次性刷到芯片。因为一次帧传输在几百微秒内完成,人眼感知不到中间状态。类似的提交策略在 LCD 驱动里也能看到,LM 的显示驱动里把这个设计叫帧缓存。
5.5 同一套代码从 51 板搬到 STM32 不工作:GPIO 模式差异
现象:驱动在 51 单片机上正常,换到 STM32 后 CLK 波形异常,DIO 高电平只有 1V 多。
原因:51 的准双向口内部有上拉,而 STM32 的 GPIO 默认可能是浮空输入。把 GPIO 配成复用时序没问题的方式,但引脚模式不对,输出高电平时驱动能力不足。
解决:将 CLK 和 DIO 配置为推挽输出带上拉,或者外部加 4.7k~10k 上拉电阻到 VCC。DIO 电平被拉高后,芯片才能正确识别高电平,起始条件里的下降沿也才有足够的摆幅。
6. 把单屏驱动改造成显示引擎:显示缓存、滚动与 SPI 加速
如果你只是点亮固定数字,直接调用 write_data 就够了。但项目稍微复杂一点,比如要显示实时变化的温湿度、做滚动字幕,这套驱动就得往上叠一层显示管理逻辑。我习惯的做法是维护一个 16 字节的显示缓存数组作为唯一数据源,应用层只改缓存,由一个定时器周期性调用 display_update 把缓存刷到芯片。这样即使多个任务同时改不同位的显示内容,也不会互相踩踏。
滚动效果就是把缓存数组循环移位后重新刷新。比如要实现左移,把第 i 位的段码赋给第 i-1 位,最右位填入新数据,然后调一次 display_update。配合 16 位缓存和几毫秒的刷新周期,肉眼看到的就是平滑流动。这个方案的额外好处是支持小数点和自定义符号——小数点段是 bit7,段码表里 0x80 或上数字段码即可;自定义符号直接造段码字节,比如显示进度条的某个位置,就把对应段位置 1。
关于 SPI 加速,要说清楚一件事:TM1640 的时序本身可以用硬件 SPI 发送数据,因为 SPI 模式 0 也在上升沿采样,和 TM1640 兼容,但起始和停止条件必须用 GPIO 额外模拟。先拉低 CLK 对应 SPI 的 CPOL=0,再通过软件控制 DIO 产生起始条件,之后 SPI 硬件负责按 LSB 先行发送数据字节。STM32 的 SPI 需要配置 LSBFIRST 位才能匹配 TM1640 的低位先传,这个位在标准库和 HAL 库里都有对应配置项。实际测试下来,硬件 SPI 比 GPIO 模拟快不了太多,因为控制的是 16 个字节的数据量级,反而 GPIO 模拟的时序更容易控制和调试。
我从那以后每次拿到 TM16xx 系列芯片,第一件事不是写代码,而是对照数据手册把起始条件、LSB 顺序和亮度寄存器画在一张卡片上,贴在调试台前面。显示驱动出问题,百分之八十都出在这三件事上。这份驱动虽小,但它把这三件事都封装好了,你只要在移植时别破坏这几个关键节点就行。希望帮到你。
本文还有配套的精品资源,点击获取