简介这份驱动程序压缩包面向使用STC15系列单片机的嵌入式开发者提供完整的TM1640 LED驱动解决方案用于控制7段数码管显示数字或字符可应用在数字时钟、仪表盘等小型电子设备上。资源共4个文件包含2个头文件与2个C源文件分别承担寄存器定义、函数接口声明以及延时和核心显示逻辑的实现压缩包仅3KB结构精简便于直接加入工程编译调试。已有1083人学习使用。通过阅读和调用该驱动开发者可以快速完成TM1640的初始化、命令发送、动态扫描与数据更新等流程省去自行查阅数据手册编写底层驱动的时间适合电子制作和嵌入式入门及进阶开发场景。1. TM1640驱动两根IO口就能点亮8位数码管但翻车点比你想的多很多开发板会把 TM1640 标注成“I2C 接口”实际上它走的是一套非常像 I2C 但又不是 I2C 的专用串行协议两根 GPIO 线就能驱动 8 个数码管看起来简单到不能再简单。可等你真把驱动写出来接到板子上乱码、不亮、闪烁、亮度调不了这类问题会一个个冒出来。这篇文章从协议时序讲到驱动封装再讲到我实际调板时踩过的坑目标只有一个让你拿到这份 TM1640 驱动程序.rar 之后能在自己工程里快速跑起来并且出了问题知道去哪查。适合做温控器、小家电面板、仪表显示、DIY LED 模块的开发者也适合刚开始接触 LED 驱动芯片的新手。2. 先讲透协议TM1640 不是 I2C为什么驱动要自己软模拟2.1 从 TM1640 内部那 16 字节 RAM 说起TM1640 的核心是内部 16 个字节的显示 RAM地址从 0x00 到 0x0F每一位都对应一个 LED 段。数码管通常有 8 段7 段加一个小数点所以一个字节正好驱动一个数码管16 字节可以驱动 16 个数码管。实际使用中驱动 8 位数码管是最常见的接法也就是 8 个字节的显示缓冲区。这里要特别理解一个概念你写驱动不是在“点亮数码管”这个抽象层面操作而是在往芯片内部这 16 字节里填 0 和 1。比如你要在第一位显示数字“1”对应段码是 0x06那写到地址 0x00 的 RAM 里芯片就会自动把对应段的引脚拉到高电平。这套设计带来的好处是 MCU 几乎不占资源CPU 只需要把段码通过两根线送进去之后刷新显示就完全由 TM1640 自己完成。这跟直接用 GPIO 扫描数码管是两回事前者是 MCU 把数据交给芯片后者是 MCU 自己刷屏。放在带 RTOS 或者需要低功耗的项目里TM1640 的优势非常明显。2.2 三类指令就够了数据指令、地址指令、显示控制指令TM1640 的指令集不复杂真正用到的就三类。第一类数据指令常见值为 0x40 和 0x44。0x40 表示“写数据到显示寄存器地址自动加 1”0x44 表示“固定地址写数据”。我通常用 0x40这样一次发送一串字节芯片会自动递增内部地址效率高。如果你只想改某一位数码管用 0x44 也不会出错但需要额外发地址代码反而更啰嗦。第二类地址指令范围是 0xC0 到 0xC7也有芯片支持到 0xC8。这个地址指向你要操作的 RAM 起始位置。比如你的数码管从第一个位置开始显示地址就是 0xC0如果硬件把数码管接在后面几个位就对应改这个地址。第三类显示控制指令范围是 0x88 到 0x8F。0x88 是关闭显示0x89 到 0x8F 是开启显示并且亮度从低到高递增。默认状态下芯片可能处于关显示状态所以初始化时必须发一条 0x88 以上的指令否则你会发现数据明明写进去了数码管却什么都不亮。把这三类指令组合起来整个驱动就变成了一个固定流程发 0x40 选数据写入模式发 0xC0 选起始地址发 N 个字节的段码最后发 0x8X 控制亮灭和亮度。2.3 时序细节起始位、数据位、停止位一个都不能错TM1640 的两根线是 CLK 和 DIN。CLK 是时钟线DIN 是数据线。它的通讯帧格式和 I2C 有相似的地方但不完全相同最大区别是它没有 ACK 应答位你写完一个字节不需要等从设备回应。标准时序是先把 CLK 拉高、DIN 拉高然后 DIN 拉低此时 CLK 保持高电平形成起始位接着在 CLK 上升沿之前先设置 DIN 的电平再拉高 CLK让芯片在上升沿采样 DIN如此重复 8 次发送一个字节字节发送完毕后把 DIN 拉高CLK 保持高形成停止位。这里有一个非常容易被忽视的细节TM1640 的数据手册上写的采样边沿可能是上升沿但实际画原理图时有些兼容芯片是在下降沿采样。网上不少移植失败的案例都栽在这里。我一般的做法是严格按照芯片手册上的时序参数写延时不要省略延时函数。下面给一个最基础的软模拟时序代码#include stm32f10x.h // 以某款MCU为例 #define TM1640_CLK_PORT GPIOB #define TM1640_DIN_PORT GPIOB #define TM1640_CLK_PIN GPIO_Pin_5 #define TM1640_DIN_PIN GPIO_Pin_7 #define CLK_HIGH() GPIO_SetBits(TM1640_CLK_PORT, TM1640_CLK_PIN) #define CLK_LOW() GPIO_ResetBits(TM1640_CLK_PORT, TM1640_CLK_PIN) #define DIN_HIGH() GPIO_SetBits(TM1640_DIN_PORT, TM1640_DIN_PIN) #define DIN_LOW() GPIO_ResetBits(TM1640_DIN_PORT, TM1640_DIN_PIN) /* 延时函数在主频72MHz下约5us实际以芯片手册为准 */ static void tm1640_delay(void) { volatile uint32_t i 30; while (i--) { __NOP(); } } /* 起始位CLK高电平期间DIN拉低 */ static void tm1640_start(void) { DIN_HIGH(); CLK_HIGH(); tm1640_delay(); DIN_LOW(); tm1640_delay(); } /* 停止位CLK高电平期间DIN拉高 */ static void tm1640_stop(void) { CLK_LOW(); DIN_LOW(); CLK_HIGH(); tm1640_delay(); DIN_HIGH(); tm1640_delay(); } /* 发送一个字节低位在前 */ static void tm1640_write_byte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { CLK_LOW(); if (dat 0x01) { DIN_HIGH(); } else { DIN_LOW(); } tm1640_delay(); CLK_HIGH(); tm1640_delay(); dat 1; } }这里面最关键的两个参数是延时时间和数据位顺序。TM1640 是低位先发也就是 LSB first这和很多人的习惯相反。如果你用高位先发显示内容会整体错乱而且是那种“每个数字都不对”的错乱看着像段码表错了实际是位序错了。延时时间直接影响通讯成功率。延时太长刷新速度慢扫个 8 位数码管没问题但如果你要做动态亮度调节或者高频刷新就明显吃力延时太短数据建立时间不够芯片采到的是不确定电平。我一般把延时调在 2us 到 5us 之间时钟线频率控制在 500kHz 以下这样兼容性最好。3. 封装成完整驱动模块显示缓冲区、段码表、刷新函数3.1 定义段码表共阴共阳别搞反TM1640 驱动的是共阴数码管也就是公共阴极接 GND某一笔点亮需要对应引脚输出高电平。如果你用的是共阳数码管段码表要整体取反否则显示出来的数字会缺笔甚至只亮不该亮的段。这里直接给出数字 0 到 9、字母 A 到 F 的段码/* 共阴数码管段码位序dp g f e d c b a */ const uint8_t tm1640_seg_table[] { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F, // 9 0x77, // A 0x7C, // b 0x39, // C 0x5E, // d 0x79, // E 0x71, // F };这里要注意位序问题。不同厂家的数码管引脚排序不一样段码表看起来一样实际显示却可能缺段。最稳妥的做法是先点亮全亮段用 0x7F 确认数码管是正常工作的再逐段测试确定真正的段码表。3.2 三层接口设计初始化、亮度控制、显示内容一个好的驱动不应该裸调时序而是提供类似 tm1640_init、tm1640_set_brightness、tm1640_display_char 这样的接口。上层代码只需要关心“显示什么”不用关心“怎么发包”。初始化函数可以做三件事配置 GPIO 输出模式、发送 0x40 数据指令、清空显示 RAM。清空这一步很重要后面避坑章节会专门讲。下面是一段完整驱动代码/* 显示缓冲区负责映射TM1640的8个RAM地址 */ static uint8_t tm1640_buffer[8]; void tm1640_init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); /* 配置CLK和DIN为推挽输出 */ gpio.GPIO_Pin TM1640_CLK_PIN | TM1640_DIN_PIN; gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); /* 起始状态CLK和DIN都拉高 */ DIN_HIGH(); CLK_HIGH(); /* 清空缓冲区 */ memset(tm1640_buffer, 0x00, sizeof(tm1640_buffer)); /* 数据指令自动地址增加 */ tm1640_start(); tm1640_write_byte(0x40); tm1640_stop(); /* 地址指令从第一个RAM地址开始 */ tm1640_start(); tm1640_write_byte(0xC0); tm1640_stop(); /* 连续写16个0x00清空芯片内部RAM */ tm1640_start(); tm1640_write_byte(0xC0); for (uint8_t i 0; i 16; i) { tm1640_write_byte(0x00); } tm1640_stop(); /* 显示控制开显示亮度等级设为一级 */ tm1640_start(); tm1640_write_byte(0x89); tm1640_stop(); }这段代码里的 memset 是软件层的初始化写 16 个字节是硬件层的清屏。两者都要做因为 TM1640 上电时内部 RAM 内容不确定如果只清缓冲区芯片内部残留数据还是会在断电重置后显示随机内容。具体原因会在后面避坑章节展开。3.3 刷新策略一次全量发送比增量发送靠谱把数据显示到 TM1640 有两条路一条是每次显示内容变化时只更新变化的那个字节另一条是每次把 8 字节缓冲区全部刷一遍。我选全量刷新。全量刷新的好处是逻辑简单、同步性好。你只要保证 tm1640_buffer 里的数据是对的然后调用一次刷新函数芯片显示就一定和缓冲区一致。增量刷新省不了多少时间因为 TM1640 通讯速率不高波形的建立时间占大头省那几字节的数据量反而容易把状态搞乱。刷新函数如下void tm1640_refresh(void) { uint8_t i; /* 数据指令自动地址增加 */ tm1640_start(); tm1640_write_byte(0x40); tm1640_stop(); /* 起始地址 */ tm1640_start(); tm1640_write_byte(0xC0); tm1640_stop(); /* 写入8字节显示数据一次发完 */ tm1640_start(); for (i 0; i 8; i) { tm1640_write_byte(tm1640_buffer[i]); } tm1640_stop(); /* 重新发送显示控制指令保证亮度设置生效 */ tm1640_start(); tm1640_write_byte(0x8F); tm1640_stop(); }这个函数里最后一条 0x8F 指令值得多解释一句。有些芯片在连续写入显示数据之后显示控制寄存器的状态会被清掉表现为“亮度忽然变暗”或者“显示抖动”。重新发一次显示控制指令等于把状态再刷新一遍代价只有几十微秒换来的是稳定。如果你发现数码管偶尔亮度不一致优先检查这一行有没有写。3.4 显示单字符和数字上层接口怎么设计下面是驱动对外暴露的接口直接往缓冲区里放段码然后刷新void tm1644_display_digit(uint8_t pos, uint8_t num, uint8_t dot) { uint8_t code; if (pos 7) { pos 7; /* 越界保护最多8位 */ } if (num 15) { num 15; } code tm1640_seg_table[num]; if (dot) { code | 0x80; /* 点亮小数点dp位在最高位 */ } tm1640_buffer[pos] code; tm1640_refresh(); }pos 的取值范围是 0 到 7对应第一个到第八个数码管。dot 参数为 1 时点亮小数点这是通过把段码最高位置 1 实现的。这段代码把缓冲区操作和硬件刷新封装在一起上层应用只需要关心显示位置和内容完全不用碰时序。4. 移植到自己的工程引脚选择、延时调整、多片级联4.1 先想清楚一件事CLK 和 DIN 用推挽还是开漏TM1640 的 DIN 在部分应用场景里要用来读按键这时候 DIN 引脚需要支持输入功能。如果你的系统里只是纯显示输出直接把两个引脚配置成推挽输出就够了代码简单时序也稳定。但如果你打算用 TM1640 的按键扫描功能DIN 必须配置成开漏输出并且外部加上拉电阻。开漏输出的好处是可以切换到高阻态让芯片驱动 DIN 引脚MCU 再去读电平。很多人在移植时忽略了这个区别导致按键值读不到或者干扰显示。我一般会在原理图阶段就确认显示专用就用推挽带按键就用开漏加 10k 上拉。4.2 MCU 主频变了延时参数必须跟着变TM1640 的延时参数不是恒定的它取决于 MCU 的时钟频率。同一段延时函数在 8MHz 的芯片上和在 72MHz 的芯片上有完全不同的表现。延时太短芯片采样不到数据延时太长单个字节耗时可能到几十微秒写完整屏数据甚至超过一毫秒在高频刷新场景下会出现闪烁。我习惯把延时时间手动换算成 CPU 周期。比如 72MHz 主频下一个 NOP 指令大约 1 个周期约 14ns。一个 while 循环加 NOP 大概几十纳秒到几百纳秒所以用一个变量循环 30 次整体在 2us 到 5us 之间。8MHz 主频下同样的循环次数会跑到几十微秒明显偏慢。下面这张表是常见主频下的粗略参数参考MCU 主频典型延时循环次数单字节写入耗时8MHz4 ~ 8约 20us24MHz10 ~ 20约 10us72MHz25 ~ 40约 5us168MHz40 ~ 80约 3us这个表不是芯片手册参数是我在实际项目里测试出来的经验值。不同编译器优化等级会改变循环耗时建议在调通之后用逻辑分析仪抓一下 CLK 波形确认高电平脉宽在 1us 以上再固定这个参数。4.3 用宏抽象底层操作换平台不折腾如果你经常在不同 MCU 平台之间做迁移驱动里尽量少用 ST 库的 GPIO_WriteBit 这类函数因为这会把驱动绑死在特定芯片上。我一般用两层结构底层是极简的 GPIO 操作宏上层是纯逻辑的时序字节发送函数。例如在 STM32 上定义#define tm1640_clk_high() GPIO_WriteBit(GPIOB, GPIO_Pin_5, Bit_SET) #define tm1640_clk_low() GPIO_WriteBit(GPIOB, GPIO_Pin_5, Bit_RESET) #define tm1640_din_high() GPIO_WriteBit(GPIOB, GPIO_Pin_7, Bit_SET) #define tm1640_din_low() GPIO_WriteBit(GPIOB, GPIO_Pin_7, Bit_RESET)换到其他芯片时只需要改这四个宏或者改成直接操作寄存器位时序函数完全不用动。这也是一种值得推荐的驱动设计习惯把硬件差异限制在一个文件里其他地方全部复用。4.4 一片不够用多 TM1640 级联的思路有些应用场景显示位数超过 8 位例如做时钟加温度显示需要 12 位甚至 16 位。这时候有两种选择换更大驱动的芯片或者用两片 TM1640 级联。级联 TM1640 不需要额外片选线因为两片芯片共用 CLK 和 DIN只是地址指令不同。你需要做的第一件事是分别把两片芯片的地址映射到不同 RAM 区域然后刷新时先向第一片发 0xC0 地址写前 8 字节再向第二片发 0xC0 地址写后 8 字节。需要注意的是两片芯片共用数据线发送指令时不能像驱动单芯片那样简单连续发。常见做法是每片芯片都独立完成一次完整的“起始 指令 数据 停止”中间不要交叉。也就是说把刷新函数改成分段执行void tm1640_refresh_two_chip(void) { /* 刷新第一片地址0xC0写入buffer前8字节 */ tm1640_start(); tm1640_write_byte(0x40); tm1640_stop(); tm1640_start(); tm1640_write_byte(0xC0); tm1640_stop(); tm1640_start(); for (uint8_t i 0; i 8; i) { tm1640_write_byte(display_buffer[i]); } tm1640_stop(); /* 刷新第二片地址0xC0写入buffer后8字节 */ tm1640_start(); tm1640_write_byte(0x40); tm1640_stop(); tm1640_start(); tm1640_write_byte(0xC0); tm1640_stop(); tm1640_start(); for (uint8_t i 8; i 16; i) { tm1640_write_byte(display_buffer[i]); } tm1640_stop(); }这里有个隐藏坑第二片芯片和第一片完全共用 CLK 和 DIN 线当第一片在通讯时第二片也能看到同样的数据它怎么知道哪些数据是自己的这就是为什么每片都要发自己的起始地址指令。如果两片芯片的地址设置相同数据就会同时显示在两片上等于所有数码管显示一模一样的数字而且还会互相干扰。5. TM1640 驱动避坑五个最常见的玄学问题5.1 现象数码管显示乱码所有位数都在闪数字但看起来不固定原因DIN 数据建立时间不够。TM1640 在 CLK 上升沿采样 DIN但 DIN 的电平必须在上升沿之前一小段时间内保持稳定通常这个建立时间在手册上有标注。如果 MCU 在拉高 CLK 的同时或稍后才改变 DIN 状态芯片就会采到上一次的旧电平导致位移位、错乱。解决在发送每一位时先把 CLK 拉低设置 DIN延时一段时间再拉高 CLK延时再进入下一位。顺序绝不能反。很多人喜欢把CLK_LOW(); DIN_SET(); CLK_HIGH();三步连续写完不加延时看起来思路正确实际在高速 MCU 上这三条语句可能在几百纳秒内完成芯片根本来不及采样。5.2 现象初始化后数码管全灭但用万用表量 DIN 和 CLK 有波形原因漏发了显示控制指令。TM1640 上电默认状态不一定是开启显示如果你只发数据指令和地址指令芯片仍然处于关显示状态RAM 里有数据但不会输出到引脚。解决初始化函数最后必须发 0x89 或更高级别的指令。我习惯用 0x8F把亮度设到最高后面再通过接口调节。如果你把亮度调到最低 0x89在强光环境下可能仍然看不清这是正常现象不是驱动问题。5.3 现象程序跑起来一切正常断电再上电偶发乱码原因TM1640 内部 RAM 掉电后内容随机上电时如果直接从 RAM 读取并驱动数码管显示的就是上次残留的随机数据。另外MCU 复位时 GPIO 引脚短暂变成高阻态DIN 可能收到一个假的起始位导致芯片内部状态被扰乱。解决上电初始化先把 GPIO 配置成推挽输出并拉高再发送清 RAM 指令。清 RAM 的完整操作是发 0x40 数据指令、发 0xC0 地址指令、连续写 16 个 0x00。不能只清软件缓冲区必须让芯片内部 RAM 也清零。我一般还会在初始化前加 10ms 延时等电源稳定后再开始通讯。5.4 现象用硬件 I2C 外设配置 TM1640通讯失败或者显示不定原因TM1640 的协议只是长得像 I2C但严谨地说它不是标准 I2C。它没有 ACK 应答没有标准 I2C 的地址字节而且位序是 LSB first。MCU 的硬件 I2C 外设会自动插入开始/停止条件、检查应答、按 MSB first 发送这些行为都不符合 TM1640 的要求。解决不要用硬件 I2C 外设直接用普通 GPIO 软模拟时序。这也是 TM1640 驱动包通常以软件模拟方式实现的原因。如果项目里同时有 I2C 传感器并用了同一个 MCU 的 I2C 外设请把 TM1640 放在 GPIO 上避免两个外设互相打架。5.5 现象数字显示正常但小数点不亮或者小数点点亮后数字缺笔原因段码表里小数点位位置不对。TM1640 的 RAM 位和数码管的段引脚映射不是固定的如果硬件原理图上把 dp 接到了其他段位你的段码最高位置 1 就不一定能点亮小数点。解决用万用表逐段测量数码管引脚定义或者写一个测试函数依次点亮 8 个段void tm1640_seg_test(void) { uint8_t i; for (i 0; i 8; i) { tm1640_buffer[0] (1 i); /* 每次只点亮一个段 */ tm1640_refresh(); delay_ms(200); } }跑完这个测试你就能知道每个位的实际段位置再据此调整段码表。不要直接套用网上别人的段码表因为数码管厂商的封装定义可能完全不同这是最容易踩的坑。6. 验证驱动稳定性用逻辑分析仪把时序抓出来6.1 看波形指标重点检查建立时间和保持时间驱动写完不等于驱动正确。最直接的验证方式是用逻辑分析仪抓 CLK 和 DIN 波形把采到的数据按帧结构对照。抓波形的接线很简单逻辑分析仪的通道 0 接 CLK通道 1 接 DIN共地接好。触发方式设置为 DIN 下降沿触发采样率至少 2MHz不然抓不到细节。正常波形应该符合以下特征检查项合格标准起始位DIN 在 CLK 高电平时拉低且保持至少一个时钟脉冲宽度数据位DIN 电平在 CLK 上升沿前已稳定建立时间大于 1us停止位DIN 在 CLK 高电平时拉高且 CLK 和 DIN 都回到高电平数据顺序每个字节低位在前高位在后如果发现 DIN 变化和 CLK 上升沿几乎同时发生说明延时不够。如果发现波形有毛刺可能是 GPIO 边沿太陡或者线太长可以降低 GPIO 翻转速率串一个 220 欧姆电阻。6.2 一个自检函数让数码管滚动显示 0 到 F实际调板时我习惯先跑一个简单的滚动测试确认所有位都能正常显示void tm1640_roll_test(void) { uint8_t i, j; for (i 0; i 8; i) { for (j 0; j 16; j) { tm1640_buffer[i] tm1640_seg_table[j]; tm1640_refresh(); delay_ms(80); } } memset(tm1640_buffer, 0x00, sizeof(tm1640_buffer)); tm1640_refresh(); }这段代码会从第一个位到第八个位依次显示 0 到 F间隔 80ms。如果某个位跳过了数字说明该位的 RAM 地址可能不对或者硬件连线有问题。如果所有位都正常驱动基本可以交付了。6.3 我常用的排查习惯从那以后我每次新拿一个带 TM1640 的板子都会先在原理图上确认 CLK 和 DIN 的连接引脚再用万用表量一遍两个引脚对地电阻避免上电后才发现线序接反。接着用逻辑分析仪抓一段初始化时序确认起始位、停止位、数据字节能对上手册再跑滚动测试。虽然这套流程多了几分钟但省掉了很多“显示乱码”的排查时间。希望帮到你这套 TM1640 驱动源码包里的工程模板就是按这个思路整理的你拿过去改改引脚定义基本就能直接用。本文还有配套的精品资源点击获取