简介:面向STM32F103的TM1628数码管驱动代码包,专为需要在嵌入式项目中快速实现多位数码管或LED点阵显示的开发者和学习者设计。程序通过GPIO或SPI模拟时序完成STM32与TM1628之间的数据交互,覆盖初始化配置、数据写入、扫描控制、显示刷新等核心步骤,代码结构清晰,可直接移植或按需二次开发。资源包内共2个文件,包含1个C源文件和1个头文件:C文件实现底层时序与显示驱动函数,头文件定义寄存器地址、常用宏及接口声明;整体压缩后仅1KB,轻量易读,便于快速上手。截至目前已有1324人学习浏览。通过研读源码,能深入理解TM1628的数据帧格式、锁存器与动态扫描机制,掌握在STM32平台驱动外部显示芯片的完整流程;同时头文件中的模块化声明有助于工程集成,并结合中断或定时器实现动态数字更新,对排查硬件连接和显示异常也有实际参考价值。
1. TM1628 为什么出现在 STM32F103 项目里
一块 STM32F103 开发板要同时伺候数码管显示和十几个按键时,最省事的办法不是拿 GPIO 硬拼,而是挂一片 TM1628。这颗芯片用 STB、CLK、DIO 三根线接管 LED 段位驱动、位选扫描和按键矩阵扫描,MCU 这边只要按帧写显存、读键值,剩下的点亮、消隐、轮询都由芯片自己完成。网上一搜就能找到的 tm1628 程序,大多是 rar 压缩包里抠出来的单个 c 文件,能跑,但命令字节和延时往往写死在一块,换个引脚、换种数码管接法就水土不服。这里不引什么官方源码,只按我在 stm32 项目里调这颗芯片的通用做法,把 TM1628 的时序、驱动函数、按键扫描和显示刷新完整走一遍:新手照着接线能跑起来,老手可以直接把框架收进自己的工程再改引脚。
2. 看懂 TM1628 的时序和命令,再动手写驱动
2.1 STB、CLK、DIO 三根线怎么接到 STM32F103
TM1628 不是标准 SPI/I2C,是一种带锁存的三线串行协议,跟移位寄存器类似。STB 负责整帧开始和结束,CLK 提供移位时钟,DIO 是双向数据线:写显存时由 STM32F103 输出,读按键时要切回输入。典型接线见下表,引脚号按你自己板子的连接改:
| TM1628 引脚 | 接 STM32F103 | 作用 |
|---|---|---|
| VCC | 3.3V 或 5V | 按模块使用的电平供电 |
| GND | GND | 与 F103 共地 |
| STB | PB6 | 帧开始拉低,所有字节传完拉高 |
| CLK | PB7 | 上升沿采样 DIO |
| DIO | PB8 | 双向数据,外部上拉 10kΩ 到 VCC |
DIO 的上拉电阻不是可有可无。读按键时 TM1628 通过开漏方式把按键状态放到 DIO 上,没有上拉,总线在芯片不驱动的间隙就是浮空,读回的数据会忽 0 忽 1。STM32F103 的 GPIO 内部也有上拉,但内部上拉电阻偏大,在长线、多芯片并联时容易压不住噪声,我会在模块侧外部放一只 10kΩ 到 VCC。
GPIO 速度建议直接配 50MHz。F103 的 IO 驱动能力在这里不是关键指标,TM1628 输入阻抗很高,真正要紧的是 CLK 边沿越陡,采样点越稳定。如果整个 stm32 工程对功耗敏感,可以在 TM1628 空闲时把 CLK/STB 输出固定为低电平,避免引脚悬空导致芯片漏电。
2.2 一帧数据:STB 低电平开始,CLK 上升沿移入,STB 高电平锁存
TM1628 的时序可以从帧、字节、位三个层级来看。帧层:一切操作先拉低 STB,传完需要的所有字节后再拉高 STB,STB 上升沿那一刻芯片才响应本帧内容;STB 低电平期间中途拉高,本帧作废。字节层:每字节 8 位,先传最低位,也就是 LSB first,和很多 SPI 外设默认的 MSB first 正好相反。位层:CLK 低电平时 DIO 可以变化,CLK 上升沿芯片采样 DIO。
读按键和写显存方向相反。写时 MCU 一直驱动 DIO;读时 MCU 先把读命令字节发给芯片,然后把 DIO 切输入,等待芯片在后续时钟沿上把键值一位一位放出来。最稳的读法是 CLK 拉高后再延时一小段,然后采样 DIO,因为芯片的数据输出有建立时间,CLK 刚跳变就采样容易抓到上一拍的电平:
// 读一个字节,LSB first,CLK 高电平期间采样 uint8_t tm1628_rd_byte(void) { uint8_t val = 0; for (int i = 0; i < 8; i++) { TM1628_CLK_L(); // 拉低,为上升沿做准备 delay_us(1); TM1628_CLK_H(); // 上升沿:芯片把一位数据放到 DIO delay_us(1); // 等待数据建立 if (TM1628_DIO_READ()) // 此时采样最稳 val |= (1 << i); // 第 0 位先到,放 bit0 } TM1628_CLK_L(); // 字节结束后拉低,保持 CLK 空闲低电平 return val; }读键盘帧还有一层要求:完整一帧往往由多个字节组成,读完第一个字节后 CLK 继续给,芯片会接着输出下一个字节;如果提前拉高 STB,未读完的按键寄存器就丢了。所以读按键时要把需要的字节一次读完,再拉高 STB。
2.3 命令帧:写显示、读按键、显示控制各自的格式
TM1628 的指令从功能上分成数据命令、显示命令、地址命令三类。命令字节的编码在兼容芯片里有两套常见写法,驱动里所有命令都要收成宏,换芯片时只改头文件。下表左边是功能,中间是工程里常见的两系编码:
| 功能 | 命令字(常见两系) | 说明 |
|---|---|---|
| 写显存,地址自增 | 0x40 / 0x00 | 批量更新显存时最常用 |
| 写显存,固定地址 | 0x44 / 0x01 | 只改某一位时用 |
| 读按键值 | 0x42 / 0x02 | 读按键矩阵扫描结果 |
| 显存起始地址 | 0xC0 | 地址命令,两系基本一致 |
| 开显示 | 0x88~0x8F | 低三位控制亮度,0x8F 最亮 |
| 关显示 | 0x80 | 关显示,不清空显存数据 |
命令发送本身只有一帧、一个字节:STB 拉低,送命令,STB 拉高。写显存则是两帧配合:先单独发一帧写显存命令让芯片进入“写显存、地址自动递增”模式,再发第二帧,帧内先送起始地址 0xC0,随后每个字节写进一个显示寄存器,写完后拉高 STB。把写命令和 0xC0 拼在一帧里发是常见错误,芯片会把写命令当作显存地址偏移,显示数据整体错位:
void tm1628_send_cmd(uint8_t cmd) { TM1628_STB_L(); // 帧开始 tm1628_wr_byte(cmd); // 一个字节的命令 TM1628_STB_H(); // 帧结束,命令生效 }如果只需要更新某一位数码管,用固定地址模式可以只写一个寄存器,但要注意固定地址模式不会自动递增,连续写多个字节时它们会都覆盖同一个地址。批量刷新整个显存用地址自增模式加 0xC0 起始地址更省事,这也是后续 main 循环里推荐的写法。
3. 手写 STM32F103 的 TM1628 最小驱动
3.1 先建好 stm32 开发环境,把 GPIO 配成能读又能写
标准库或者 HAL 库写法都行,这里用标准库,逻辑拆开后在 HAL 里几乎一样。不动手脚的关键是 DIO 的方向切换:写时输出,读时输入,切换错了第一个字节就是错的。GPIO 初始化:
// 引脚宏集中定义,换板子只改这里 #define TM1628_STB_PIN GPIO_Pin_6 #define TM1628_CLK_PIN GPIO_Pin_7 #define TM1628_DIO_PIN GPIO_Pin_8 #define TM1628_PORT GPIOB // STM32F103 标准库 GPIO 初始化 void tm1628_gpio_init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin = TM1628_STB_PIN | TM1628_CLK_PIN; gpio.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(TM1628_PORT, &gpio); gpio.GPIO_Pin = TM1628_DIO_PIN; gpio.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏,靠上拉拉高 gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(TM1628_PORT, &gpio); }DIO 用开漏是刻意为之。开漏输出写 1 时引脚不驱动,靠外部 10kΩ 上拉拉高;写 0 时拉低。这样从写转读只需要把引脚模式从输出改成输入,不用先写一个 1 再切方向。如果板子上没有外部上拉,用推挽输出也能工作,读之前先GPIO_SetBits(DIO)再切GPIO_Mode_IPU,效果差不多,只是多一步依赖内部弱上拉,抗干扰差些。
另外需要两个方向切换的小函数,读按键前后各调一次:
void tm1628_dio_to_in(void) // 写模式切读模式 { GPIO_InitTypeDef gpio; gpio.GPIO_Pin = TM1628_DIO_PIN; gpio.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(TM1628_PORT, &gpio); } void tm1628_dio_to_out(void) // 读模式切写模式 { GPIO_InitTypeDef gpio; gpio.GPIO_Pin = TM1628_DIO_PIN; gpio.GPIO_Mode = GPIO_Mode_Out_OD; // 回到开漏输出 gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(TM1628_PORT, &gpio); }两个函数看起来冗余,但比在每次字节读写里手动改 CRL 寄存器的位可读性强很多;等驱动跑起来,你可以再把它优化成只操作 GPIOx->CRL 的位域,那是对时序有洁癖的人最后会做的事。
3.2 底层时序:CLK 低电平改数据,上升沿采样
写字节的底层时序和读字节对称。STM32F103 主频 72MHz,一条 GPIO 操作是几个时钟周期,所以微秒级延时主要靠软件空转,但延时不能用得太短。数据手册一般要求时钟高/低电平不小于几百纳秒,实际环境里线长、上拉电阻、芯片批次都会影响,2us 是比较稳妥的起步值:
void tm1628_wr_byte(uint8_t dat) { for (int i = 0; i < 8; i++) { TM1628_CLK_L(); // 先拉低,再改数据 if (dat & (1 << i)) // LSB first,第 0 位先发 TM1628_DIO_H(); else TM1628_DIO_L(); delay_us(2); // 数据建立时间 TM1628_CLK_H(); // 上升沿,芯片采样 delay_us(2); // 高电平持续时间 } TM1628_CLK_L(); // 字结束拉低,为下个字节做准备 }写完后把 CLK 停在低电平是好习惯。有些 TM16xx 系列芯片在 CLK 高电平空闲时会进入不确定状态,停在低电平则无论下一帧是命令还是数据都从上升沿重新同步。把delay_us(2)提到 5us 通常不会出问题,反而更稳;但不要把这个延时和主循环里 10ms 的节拍混淆,那是两个层级的定时。时序参数参考如下:
| 阶段 | 建议延时 | 说明 |
|---|---|---|
| DIO 建立时间 | ≥2us | CLK 上升沿前数据需稳定 |
| CLK 高电平 | ≥2us | 采样窗口 |
| CLK 低电平 | ≥2us | 数据位之间切换时间 |
3.3 封装写显存和读按键,注意 STB 的边界
先把命令值收成宏,放在驱动头文件里。换芯片批次时,这里是唯一需要动的地方:
#define TM1628_WR_INC 0x40 // 写显存,地址自增;部分芯片为 0x00 #define TM1628_RD_KEY 0x42 // 读按键;部分芯片为 0x02 #define TM1628_ADDR_0 0xC0 // 显存起始地址 #define TM1628_DISP_ON 0x8F // 开显示,最高亮度写显存函数和前面 2.3 的命令帧对应。代码里把“设置写模式”和“送地址+数据”分成两段,串口调试时也能直接从波形上区分:
void tm1628_write_display(uint8_t *buf, uint8_t len) { uint8_t i; tm1628_send_cmd(TM1628_WR_INC); // 第一帧:写显存,地址自增 TM1628_STB_L(); // 第二帧开始 tm1628_wr_byte(TM1628_ADDR_0); // 起始地址 for (i = 0; i < len; i++) tm1628_wr_byte(buf[i]); // 连续写 len 个寄存器 TM1628_STB_H(); // 第二帧结束,显存更新 }读按键和写显存方向相反,所以读完后必须把 DIO 切回输出。这里见到的 bug 最多:读回键值正常,但切回输出时忘了重新初始化 GPIO,下一次写显存时 DIO 还是输入状态,第一个数据字节被当成悬空电平,整段显示右移一位:
#define TM1628_KEY_BYTES 2 // 一个 TM1628 的按键寄存器数量 uint16_t tm1628_read_key(void) { uint8_t k[TM1628_KEY_BYTES] = {0}; int i; tm1628_send_cmd(TM1628_RD_KEY); // 读按键命令 TM1628_STB_L(); for (i = 0; i < TM1628_KEY_BYTES; i++) k[i] = tm1628_rd_byte(); // 连续读完所有字节 TM1628_STB_H(); tm1628_dio_to_out(); // 关键:恢复 DIO 输出 return (uint16_t)(k[0] | (k[1] << 8)); }注意:读按键命令是一帧,字节发送后 STB 在整个读字节期间保持低。如果程序里把读命令也包进第二个 STB 低窗口,芯片会把它当作显存地址,读回来全是 0xFF。
4. 按键扫描和显示刷新:一个 main 让它跑起来
4.1 段码表和显示缓存的对应关系
TM1628 的显示寄存器每一位对应一个段,8 位寄存器正好驱动一位 7 段数码管加小数点。共阴接法直接把段码写进寄存器就亮,共阳要取反。常用 0~9 和 A~F 段码表:
// 共阴数码管段码,dp 位是 0x80 static const uint8_t seg_tab[16] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71 };段码表只是第一步,位置映射才是显示不乱的保证。TM1628 的寄存器地址从 0xC0 开始排,但板子上的位选不一定从左到右对应地址 0、1、2。比如第二位数码管可能接到地址 3,你要在软件里建一张位置映射表,而不是在每个业务逻辑里调换索引:
// 板子走线不按寄存器顺序时,用映射表调整,示例第 2 位在地址 3 const uint8_t pos_to_addr[8] = {0, 3, 1, 2, 4, 5, 6, 7};4.2 按键状态机:连续两次相同读数才算稳定
TM1628 读回的按键原始位图,每一位对应矩阵里一个交叉点,按下为 1,释放为 0。机械按键按下和松开的瞬间会有抖动,读数在 1/0 之间跳变,不能直接用。按键扫描的电路设计上,TM1628 内部已经处理了扫描时序,外部需要解决的就是抖动和重复触发。用一个 10ms 周期调用的状态机:
// 连续两次读数一致,才把稳定值更新出去 uint16_t tm1628_key_scan(void) { static uint16_t prev_raw = 0xFFFF; // 初始值保证第一次不触发 static uint16_t stable_key = 0; uint16_t raw = tm1628_read_key(); if (raw == prev_raw) stable_key = raw; // 与上一次一致,认为消抖完成 prev_raw = raw; return stable_key; }这个状态机第一次读到新键值时不会立即返回,要等第二次一致才更新稳定值,等效于一次 10ms 的软件防抖。稳定的键值再交给业务层做边沿检测,按下一次只处理一次:
void tm1628_key_task(void) { uint16_t stable = tm1628_key_scan(); static uint16_t last = 0; uint16_t pressed = stable & ~last; // 从 0 变 1 的位 uint16_t released = ~stable & last; // 从 1 变 0 的位 last = stable; if (pressed & KEY_UP) { /* 上键按下 */ } if (pressed & KEY_DOWN) { /* 下键按下 */ } if (released & KEY_OK) { /* 确认键抬起 */ } }按键状态机要在固定的时间基准上运行,最好挂到 SysTick 里维护一个 10ms 的软件定时器。如果直接在 main 里 delay_ms(10) 后调用,主循环被其他任务阻塞时按键会漏扫;反过来,在定时器中断里调用 tm1628_read_key 又可能和主循环的显示刷新抢 DIO 总线。驱动层建议加一个简单的互斥占用标志,读按键和写显存同一时刻只允许一个操作。几个关键参数参考:
| 参数 | 取值 | 作用 |
|---|---|---|
| 扫描周期 | 10ms | 决定消抖窗口和显示刷新率 |
| 稳定确认次数 | 2 | 连续两次读数一致才更新 |
| 按键位宽 | 16 | 由 TM1628_KEY_BYTES 决定 |
4.3 主循环:共享显示缓存,整帧刷新
显示刷新推荐用“软件缓存 + 整帧提交”的方式。业务逻辑只改缓存数组,主循环每隔 10ms 把缓存整体写给 TM1628,这样不会出现某一位刚更新、另一位还是旧数据的花屏中间态:
int main(void) { uint8_t disp[8] = {0}; uint16_t key; tm1628_gpio_init(); tm1628_send_cmd(TM1628_DISP_ON); // 开显示,最高亮度 while (1) { key = tm1628_key_scan(); // 10ms 一次扫描 // 把键值低 4 位显示在第 0 位,高位显示在第 1 位 disp[0] = seg_tab[key & 0x0F]; disp[1] = seg_tab[(key >> 4) & 0x0F]; tm1628_write_display(disp, 8); // 整帧刷新 delay_ms(10); // 主循环节拍 } }10ms 周期既是按键消抖的基准,也是显示刷新的节拍,对数码管来说 100Hz 完全够用,肉眼看不到闪烁。TM1628 只在 STB 拉低的几十微秒内占用 STM32F103 的 GPIO,剩余时间整颗芯片自己完成扫描,CPU 基本是空闲的。如果你要显示系统运行时间或动态小数点的位置,只需改动 disp 缓存,再调一次整帧提交,驱动层不需要任何修改。
5. 调不通时先查这四样:延时、时序、命令字节、电源
5.1 延时函数别抱死 SysTick
很多 stm32 标准库例程里自带的 Delay 用 SysTick 死等,放主循环没问题,放进中断或者和 FreeRTOS 共用时会互相干扰,严重时 delay_us 永远等不到计数,程序卡死在驱动里。这里的时序延时我一般用 DWT->CYCCNT 实现微秒级延时,SysTick 只做 10ms 节拍,两者分开,CLK 波形就不会被节拍中断拉乱。
5.2 一帧波形的两个易错点
逻辑分析仪抓 STB/CLK/DIO 三根线,先数上升沿个数,应为字节数乘以 8。最容易错的是 STB 拉高太早:整帧最后一个字节的最后一个时钟下降沿还没走完,STB 就跳高,芯片认为命令被截断,之前发的一整帧全部无效。另一个是读按键时 DIO 方向切换瞬间恰好遇到 CLK 跳变,会多采一个毛刺,键值里出现一个说不清来源的 1;切换方向时先把 CLK 拉低,再切换 DIO。
5.3 命令字节和上拉电阻优先检查
显示乱码先核对命令字是不是把写显存命令和 0xC0 合帧发送了;按键读回全 0xFF 或全 0x00,先量 DIO 对地电压,外部上拉缺失时这是头号原因。再留一个实用技巧:把 tm1628_read_key() 的原始值格式化后从串口打出来,手动依次按每个按键记录键值,做成一份“丝印→键值”映射表,之后业务代码只查表,不用再去翻位运算。TM1628 驱动真正跑稳的标志不是一遍编译通过,而是手上有这份映射表,还有一张能看清 STB 边界的逻辑分析仪波形图。
本文还有配套的精品资源,点击获取