简介:基于STM32F103系列单片机的TM1628数码管驱动完整工程源码,面向嵌入式初学者与开发者,解决低成本环境下多位数码管显示与按键扫描的快速实现需求。源码核心为TM1628.c/TM1628.h,采用标准GPIO模拟时序完成通信,不依赖硬件SPI,兼容Keil MDK开发环境,并已通过实际硬件验证稳定运行。压缩包共6个文件,涵盖C源码、头文件及demo示例工程,整体大小约8KB,结构精简,便于直接移植到最小系统板或开发板。目前已有35人学习浏览。读者拿到后只需配置STB、CLK、DIO三根引脚,即可实现多位共阴/共阳数码管动态扫描显示数字、字母或自定义段码,同时读取连接在TM1628上的独立按键状态;该方案能直接应用于电子钟、工业仪表、简易人机界面等嵌入式交互项目,有效缩短开发周期。 拿到一套源码包,最怕的就是“能编译、烧进去、屏不亮”,然后对着数据手册和原理图干瞪眼。我前前后后帮人调过不少TM1628的板子,也见过太多人在STB时钟时序和段码映射上翻车。这篇就把我整理的一套基于STM32F103标准库的TM1628数码管驱动工程,从硬件选型、通信协议、代码框架到排坑思路,完整拆开讲清楚。不管你是刚接触单片机的小白,还是想快速给现有项目加个显示模块的老手,这套工程和思路都能让你少走弯路。
1. 项目整体设计与选型思路
1.1 为什么选STM32F103驱动TM1628
STM32F103这颗芯片在国产和开发板市场里的保有量极大,Cortex-M3内核,72MHz主频,GPIO翻转速度足够模拟出TM1628所需的通信时序。更关键的是,它的5V容忍引脚可以直接对接TM1628的逻辑电平,不需要额外做电平转换,这在小板子上省了不少事。
TM1628本身是一颗集成度很高的LED驱动芯片,内部集成了显示RAM、键扫电路和辉度调节。它最大的特点就是“省引脚”:用3根线(STB、CLK、DIO)就能驱动最多16个7段数码管,或者128个LED点。相比直接用74HC595级联,TM1628的优势在于自带键扫,做微波炉、电磁炉、热水器这类需要按键和显示同时存在的面板控制板,一颗芯片全搞定。
这套工程的适用面很广,从简单的温控仪表面板、倒计时器,到需要按键交互的小家电控制板,都可以直接套用。源码包里包含完整的标准库工程,用Keil MDK打开就能编译下载。
1.2 工程源码包的分层结构
这个工程我按照“驱动层-应用层”的思想拆成了四个模块,目的就是让显示逻辑和业务逻辑解耦。你可以直接拿来就用,也可以把TM1628驱动文件拖到你自己的工程里。
Project ├── User │ ├── main.c // 主函数,演示如何使用驱动接口 │ ├── stm32f10x_it.c // 中断服务函数(本工程未用中断) │ └── system_stm32f10x.c ├── Hardware │ ├── TM1628_drv.c/.h // TM1628底层驱动:时序模拟、命令封装 │ ├── TM1628_disp.c/.h // 显示应用层:段码表、数字/小数点显示封装 │ └── BSP_GPIO.c/.h // 引脚初始化 ├── Libraries │ └── (标准外设库) └── Project.uvprojx驱动层负责最底层的时序和控制字发送,应用层关心的是“显示什么内容”,两者之间通过接口函数对接。这样分层的好处很直接:以后你要是把TM1628换成TM1629或者别的驱动芯片,只改驱动层,应用层的显示逻辑不用动。
2. TM1628核心细节与通信协议解析
2.1 TM1628引脚功能与典型硬件电路
TM1628常见的封装是SOP32,引脚不算少,但真正需要单片机控制的只有3根线:STB(片选)、CLK(时钟)、DIO(数据)。剩下的都是驱动输出口和按键扫描口。
| 引脚名 | 功能说明 | 接法建议 |
|---|---|---|
| STB | 片选信号,低电平有效 | 接STM32任意GPIO(推挽输出) |
| CLK | 时钟信号 | 接STM32任意GPIO(推挽输出) |
| DIO | 数据输入/输出 | 接STM32 GPIO,注意设置成开漏或推挽 |
| VDD | 电源正极 | 5V或3.3V(取决于模块设计) |
| GND | 电源地 | 共地 |
| GRID1-GRID7 | 位选输出 | 接数码管公共端(共阴/共阳看模块) |
| SEG1-SEG13 | 段选输出 | 接数码管段引脚 |
硬件上有一个容易忽略的细节:TM1628的DIO是双向口,在读取按键状态时需要切换方向。如果用推挽输出模式直接干,读的时候容易出问题。我的做法是把它配置成开漏输出,外部加上拉电阻到VDD,这样既能拉低总线,又能释放总线读取按键电平。
还有一个常见坑是复位电路。TM1628要求上电时STB保持高电平,VDD上升到稳定后至少等待1ms再开始通信。很多人的板子一上电就立刻发数据,结果第一个字节经常丢失。这个时序在驱动代码里必须做处理,否则就会出现“第一次显示乱码,复位后正常”的怪现象。
2.2 通信协议:三线串行接口的时序拆解
TM1628的通信协议本质上是一种类SPI的串行协议,但比标准SPI要简单,没有时钟极性和相位的概念,也不涉及片选的自动控制,一切靠软件模拟。一次完整的通信过程是这样的:
- STB拉低,表示传输开始。
- 在CLK上升沿,DIO上的数据被移入芯片内部,先发低位(LSB first)。
- 一个字节发送完毕后,根据后续命令决定是继续发数据还是结束。
- 数据传输完成,STB拉高,结束本次通信。
时序上有个参数需要特别注意:CLK高电平和低电平的最短持续时间。TM1628数据手册上标注的是CLK周期不能小于2.5us(具体看手册版本,表格5.1中有tCLK参数)。我之前在F103主频72MHz下不加延时直接翻转GPIO,时钟周期大概只有0.2us,结果就是显示内容随机乱跳。后来在每次翻转CLK之后加了个短暂延时,问题就消失了。
代码中我用了一个辅助延时函数:
static void TM1628_DelayUs(volatile uint32_t us) { us *= 72; // 72MHz主频下,粗略延时1us while (us--) { __NOP(); } }这个延时函数只是简单的软件延时,在72MHz主频下,一个循环大概能撑住1us左右。实际测试时用示波器看波形微调一下循环次数即可,不需要精确到时钟周期。
2.3 三种命令类型与显示寄存器的关系
TM1628的命令分为三种:数据命令、显示控制命令、地址命令。每一种命令的格式不同,作用也不同,搞清楚它们之间的配合关系是驱动调试的关键。
| 命令类型 | 命令字格式 | 作用 |
|---|---|---|
| 数据命令 | 0x00~0x03 | 设置数据写入/读取的模式 |
| 显示控制命令 | 0x80~0x8F | 控制显示开关、辉度(亮度) |
| 地址命令 | 0xC0~0xC7 | 设置写入显示RAM的起始地址 |
数据命令的三种模式里,最常用的是“写数据到显示寄存器”模式,命令字为0x00。发送完数据命令后,紧接着发送地址命令(0xC0起始地址),然后连续发送要写入的数据字节。写入过程中地址会自动递增,不需要每次重新指定地址。
显示RAM总共16个字节(地址0x00-0x0F),但本工程只用到了前8个字节。每个字节对应一个GRID的8个SEG段。比如GRID1(地址0xC0)对应第1个数码管的段码,GRID2(地址0xC1)对应第2个数码管,以此类推。
显示控制命令就简单了,0x88是“开显示、辉度1/16”,0x89是“开显示、辉度2/16”,一直到0x8F是“开显示、辉度14/16”。0x80关闭显示。调试时建议先用最低辉度测试,电流小、发热低,也方便观察。
3. 硬件接线与CubeMX配置实操
3.1 引脚分配与接线对照表
我这次用的是STM32F103C8T6最小系统板,TM1628模块是某宝上最常见的数码管显示模块(8位数码管+8个按键那种)。GPIO分配如下:
| STM32引脚 | 功能 | 连接目标 |
|---|---|---|
| PA0 | STB | TM1628模块 STB |
| PA1 | CLK | TM1628模块 CLK |
| PA2 | DIO | TM1628模块 DIO |
| 3.3V | VCC | TM1628模块 VCC(部分模块需5V,请看模块说明) |
| GND | GND | TM1628模块 GND |
3.3V和5V的供电问题值得单独说一句。TM1628的VDD范围是3.0V-5.5V,3.3V供电完全没问题,但此时SEG/GRID输出的高电平也是3.3V,驱动红色数码管的亮度会稍微暗一点。想要高亮度,就用5V给模块供电,但STM32的GPIO输出高电平是3.3V,对TM1628来说逻辑高电平阈值(VIH)最高是0.7VDD,5V供电时大概是3.5V,3.3V电平就够不着了。
这时候有两个方案:一是用三极管/电平转换芯片,把单片机的3.3V信号升到5V再去驱动TM1628;二是直接3.3V供电凑合用。我的建议是,对亮度没有极致要求的话,统一3.3V供电最稳妥,不用纠结电平匹配问题。源码包里默认按3.3V供电设计。
3.2 标准库的GPIO初始化配置
GPIO初始化这部分看似简单,但角色得提前想清楚——STB和CLK是纯输出,DIO是双向口。DIO我用了开漏输出模式,外部接了4.7kΩ上拉电阻,这样在走读键扫流程时,可以直接把GPIO配置切成输入模式,不用重新初始化外设。
void BSP_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // STB、CLK、DIO 全部配置为推挽输出,速度50MHz GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始状态:STB和CLK都拉高,DIO也拉高 GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2); }DIO配置成推挽输出也能读写,但切换方向时要小心。如果输出寄存器里的值恰好是低电平,切到输入模式时,GPIO内部的上拉/下拉可能把总线钳位住,读回来全是0。开漏输出加外部上拉能完全避开这个问题,这是我踩过坑之后的深刻体会。
3.3 TM1628底层驱动的核心实现
底层驱动最关键的是把时序写对。我封装了三个静态函数:发送一个字节、接收一个字节、发送命令字。在此基础上再封装了写显存和读按键的API。
static void TM1628_WriteByte(uint8_t dat) { uint8_t i; for (i = 0; i < 8; i++) { if (dat & 0x01) { GPIO_SetBits(GPIOA, GPIO_Pin_2); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_2); } TM1628_DelayUs(1); GPIO_SetBits(GPIOA, GPIO_Pin_1); // CLK上升沿 TM1628_DelayUs(1); GPIO_ResetBits(GPIOA, GPIO_Pin_1); // CLK拉低 dat >>= 1; // LSB first } }注意这里的移位方向。TM1628是低位在前,很多从51单片机转过来的朋友习惯高位先出,一上来就写MSB first,结果整个显示全乱。这是协议层面最容易出错的一个点。
发送命令字的函数也很直观:
void TM1628_WriteCmd(uint8_t cmd) { GPIO_ResetBits(GPIOA, GPIO_Pin_0); // STB拉低,开始传输 TM1628_DelayUs(1); TM1628_WriteByte(cmd); GPIO_SetBits(GPIOA, GPIO_Pin_0); // STB拉高,结束传输 TM1628_DelayUs(1); }写显存时,先发数据命令0x00,再发地址命令0xC0,随后连续写入若干字节。一次写完所有8个GRID的段码,然后STB拉高结束。
void TM1628_SetDisplayBuffer(uint8_t *buf, uint8_t len) { uint8_t i; GPIO_ResetBits(GPIOA, GPIO_Pin_0); TM1628_WriteByte(0x00); // 数据命令:写显示寄存器 TM1628_WriteByte(0xC0); // 地址命令:从0x00地址开始 for (i = 0; i < len; i++) { TM1628_WriteByte(buf[i]); } GPIO_SetBits(GPIOA, GPIO_Pin_0); }写完之后调用TM1628_SetDisplayMode(0x8F)打开显示并把辉度调到最高档。这里有个细节:每次写显存后不需要重发显示控制命令,只要芯片不断电,显示模式不会自己变。但如果你写了显存却不显示,大概率就是忘了发0x8B这些控制字。
4. 数码管显示应用层封装与段码处理
4.1 共阴共阳与段码表的设计
数码管有共阴和共阳之分,但TM1628内部是恒流源结构,它不关心外面接的数码管是共阴还是共阳,关键是段码表要跟硬件匹配。你买的模块大概率是一体的,模块厂已经把数码管接好了,你需要做的只是把段码表测出来。
段码表其实很好理解:一个字节的bit0对应SEG1,bit1对应SEG2,以此类推。如果某个段亮了,对应的比特就置1。我工程里的段码表是我自己按“共阴”方式定义的,也就是段点亮时写1,熄灭时写0:
const uint8_t SEG_CODE[] = { 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 0x00, // 熄灭 0x80, // 小数点DP };这组码是标准的7段码,但如果你是照着网上的电路图自己画的板子,SEG引脚接的数码管段位可能跟我这张表不一样。遇到这种情况,最简单的办法就是写一个“扫段测试程序”,把0x01到0x80逐个写进去,肉眼记录每段对应的bit位,然后重新映射段码表。这个过程虽然原始,但一劳永逸,以后再也不用猜段码了。
4.2 动态显示与数据刷新策略
8位数码管如果直接并行驱动,需要很多引脚,TM1628的GRID是扫描方式,也就是“动态显示”。TM1628内部自动完成GRID扫描,不需要主控干预,主控只需要在显存里维护好每一GRID要显示的段码数据即可。
但是,主控往TM1628写显存的速度不能太慢。因为TM1628内部扫描一旦开始,它就会周期性地从RAM里取数据。如果你写RAM的速度太慢,中间会有一段肉眼可见的闪烁——尤其是在多位数码管都亮着的情况下。实测下来,我用72MHz主频、纯软件模拟时序,刷新一次8字节显存只需要不到20us,远小于人眼的闪烁感知,完全没问题。
应用层的显示接口我封装了三个常用函数:
TM1628_DisplayNumber(uint32_t num):显示一个整数,自动左对齐右对齐TM1628_DisplayFloat(float val, uint8_t decimal):显示带小数的数值TM1628_DisplayString(const char *str):显示字符串,支持0-9、A-F、一些符号
void TM1628_DisplayNumber(int32_t num) { uint8_t i, digit, buf[8]; // 从个位开始取数字 for (i = 8; i > 0; i--) { digit = num % 10; buf[i - 1] = SEG_CODE[digit]; num /= 10; if (num == 0) break; } // 剩余高位显示空格(熄灭) while (i > 1) { buf[i - 2] = SEG_CODE[16]; // 熄灭 i--; } TM1628_SetDisplayBuffer(buf, 8); }显示小数点时,注意不能把DP位的0x80直接加到段码上就完事,因为DP位在硬件上对应的是SEG13,而数字段码用的是SEG1-SEG7,两者需要按位或合并。我的段码表里最后一个0x80就是DP的段位码,实际显示时用SEG_CODE[num] | 0x80即可。
5. 常见问题排查与实测心得
5.1 显示乱码、缺段、闪烁的排查顺序
这些症状背后对应的原因往往不同,按以下顺序排查效率最高:
| 故障现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无显示 | 供电不对、STB一直拉低 | 万用表测量VDD和GND,检查STB电平静态值 |
| 显示乱码、内容错乱 | 通信时序太快、命令字发错 | 用示波器看CLK周期,降低通信速率或加长延时 |
| 闪烁 | 刷新太慢、VDD电压不稳 | 检查刷新频率,在VDD引脚旁边加大电容(104+10uF) |
| 某一位不亮 | GRID引脚虚焊或接触不良 | 用扫段测试程序检查该位,补焊或更换数码管 |
| 段缺笔画 | 段码表不匹配 | 重做扫段测试,修正段码表 |
遇到“第一次上电乱码”的情况,第一时间检查初始化代码里有没有留够上电稳定时间。TM1628在上电时STB必须保持高电平至少1ms,我用的是:
// 在 main() 开头 BSP_GPIO_Init(); delay_ms(5); // 确保上电稳定 TM1628_Init(); // 发送显示控制命令 0x8F 等这5ms的延时看起来不痛不痒,但在工业环境里如果电源本身纹波大,留多点时间更稳妥。有条件的可以改成检测VDD电压,但普通小板子软件延时够用了。
5.2 按键读取与DIO方向切换的注意事项
TM1628的按键扫描功能在这颗芯片里是免费的,不用白不用。读按键的流程是:先发数据命令0x42(读键扫数据),然后STB拉低,连续读入N个字节(取决于扫描组数),最后STB拉高。
关键在DIO方向切换。我一般在读之前把GPIO配置成输入模式,读完再切回开漏输出。标准库下切换要小心,GPIO_Init整个结构体重新初始化一遍最保险:
uint8_t TM1628_ReadKey(void) { uint8_t i, key_byte = 0; GPIO_InitTypeDef GPIO_InitStructure; // 发送读命令 GPIO_ResetBits(GPIOA, GPIO_Pin_0); TM1628_WriteByte(0x42); // DIO切为输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); for (i = 0; i < 8; i++) { if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2)) { key_byte |= (0x01 << i); } GPIO_SetBits(GPIOA, GPIO_Pin_1); // CLK上升沿 TM1628_DelayUs(1); GPIO_ResetBits(GPIOA, GPIO_Pin_1); TM1628_DelayUs(1); } // 切回开漏输出模式 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_2); GPIO_SetBits(GPIOA, GPIO_Pin_0); // STB拉高,结束 return key_byte; }还有一个细节:读键的时候,DIO在CLK下降沿时芯片输出数据,在CLK上升沿时主控去采样比较好。我在循环里先把引脚读到变量里,再拉高CLK,顺序就是这个考虑。不少参考代码是先拉CLK再读数据,结果读到的总是上一位的数据,按键值就会错位。
5.3 移植到其他工程的几个坑
最后说说把这套驱动往别的工程里搬会遇到的几个典型问题。
第一个是引脚定义。工程里直接把GPIO写死在BSP_GPIO.c里了,换芯片或者换引脚,别只改宏定义,要确保三处一起改:GPIO时钟使能、GPIO_Init的引脚配置、以及驱动函数里所有用到GPIO_SetBits/GPIO_ResetBits的地方。我的建议是学我在头文件里把端口、引脚、时钟都用宏集中定义:
#define TM1628_GPIO_PORT GPIOA #define TM1628_STB_PIN GPIO_Pin_0 #define TM1628_CLK_PIN GPIO_Pin_1 #define TM1628_DIO_PIN GPIO_Pin_2 #define TM1628_GPIO_CLK RCC_APB2Periph_GPIOA这样移植的时候只需要改头文件里的宏,驱动函数里的代码一个字都不用动。
第二个是延时函数。我这个工程用的软件延时,跟主频强相关。如果换到F407(168MHz)或者F103倍频到72MHz,延时函数的时间都会变,TM1628通信的CLK周期可能会超速。移植时用Systick或者定时器计时更通用,或者每次写完驱动后用示波器实测CLK频率,保证周期在2.5us以上。
第三个是5V电平问题。前面已经说过了,如果模块是5V供电,而MCU是3.3V,务必查一下TM1628的原理图,看模块上有没有电平转换。很多现成模块是兼容3.3V和5V的,但如果DIO线上有上拉电阻,上拉到5V的话,MCU的引脚会被钳位到5V,长期运行有烧引脚的风险。我一般看到这种情况,会串一个1kΩ电阻再接到MCU,既保护引脚又能正常通信。
6. 从源码包到项目的落地总结
这套工程我用在过两个实际项目上,一个是小家电的定时控制面板,一个是实验室的温控仪表。两个项目都遇到过不同的坑,但把底层时序、段码表、按键扫描这几块理顺之后,剩下的业务逻辑开发就顺畅多了。
如果你拿到这个源码包,我建议的先做一件事:不要急着改功能,先把“扫段测试”程序烧进去,确认每一段数码管都能按预期点亮。这一步确认了,后面的所有显示问题都不用再怀疑硬件。然后再烧一个显示“8.8.8.8.8.8.8.8”的程序,确认所有位都能正常显示。
后续扩展方向上,这套驱动配合TM1628的键扫能力,可以轻松实现“显示+按键”组合。我甚至在一款产品上同时用了两个TM1628,一个管显示,一个管按键矩阵,总共也就花了6个GPIO。如果你打算做低功耗设备,F103的停机模式加TM1628的显示关闭命令也能配合得很好,需要的时候唤醒刷新一下就行。
本文还有配套的精品资源,点击获取