简介:TM1621数码管控制器驱动程序,面向嵌入式开发与电子爱好者,解决通过编程控制芯片驱动7段数码管或点阵LED显示数字与字符的问题,适用于智能家居设备、仪器仪表和嵌入式系统等人机交互显示场景。压缩包内共2个文件,包含一个C源文件与一个头文件:c文件实现初始化、发送数据和显示数字等接口函数,h文件提供引脚定义、通信常量及函数原型,包体仅2KB,轻量易用。已有3123人学习下载,适合需要快速集成TM1621显示功能的开发者参考。代码经过实际硬件测试并附有详细注释,可帮助理解底层协议与驱动流程;头文件中的数据结构与声明便于跨文件调用,示例函数如init_TM1621和display_digit可直接复用,显著降低调试门槛并提升开发效率。
1. TM1621驱动入门:段码LCD缺笔画,先查这三处
家电面板上的段码LCD数字跳着缺笔画,电表走字偶尔闪一下,换新屏还是老样子,最后查出来是驱动芯片TM1621的初始化时序没走对。TM1621是一颗段码液晶驱动芯片,用3线或4线串行接口控制,常见规格可以带32×4段的显示量。这类芯片在仪表盘、热水器面板、血压计上很常见,单片成本低、静态电流小,MCU用几个GPIO就能拉起来。真正出问题的往往不是芯片本身,而是偏压配置、写显存的地址顺序、以及取模方向这三个环节。这篇文章把这颗芯片从时序到映射捋一遍,新手能照步骤调通,熟手可以绕开我踩过的几个坑。
2. 芯片选型与引脚理解:128段显示背后的资源边界
2.1 段码屏为什么还要专用驱动芯片
段码LCD和点阵屏不一样,它每一个笔段是一块独立的电极,公共端COM和段引脚SEG交叉形成一个个“显示点”。直接拿MCU引脚去驱动段码屏,一段就要占一个引脚,8位数码管加符号位就要三四十个引脚,完全不可行。TM1621这类芯片的作用就是把显存里的数据映射到COM×SEG的交叉电极上,MCU只需要通过串行口把显示数据写进它的内部RAM,电压切换和频率产生都由芯片完成。所以选它的理由很直接:占用MCU引脚少、自带振荡器、宽电压范围,整机待机电流能做到很低。
从资源边界看,TM1621的段容量常见是32×4,也就是128段。很多项目用不满,比如只驱动6位7段数码管加几个状态图标,大约50段,剩下的位可以做闪烁标志或者电池电量符号。选型时如果段数刚好在120左右,建议往上留一档,因为段码表取模后往往会有一些隐藏的公共段和特殊符号,数着数着就超了。
2.2 引脚功能与串行接口模式
TM1621的引脚大致分这么几组:电源(VDD、VLCD、VSS)、串行接口(CS、WR、DATA,四线模式加一个RD)、显示输出(COM0~COM3、SEG0~SEG31)、OSC振荡引脚。VLCD引脚用于调节LCD驱动电压,直接影响显示对比度,接法上常见做法是串一个几十千欧的电阻到VDD,或者根据LCD屏的规格直接短接,具体看屏的阈值电压。
| 引脚 | 方向 | 作用 | 连接建议 |
|---|---|---|---|
| CS | 输入 | 片选,低有效 | 接MCU GPIO,平时拉高 |
| WR | 输入 | 写时钟,上升沿锁存 | 接MCU GPIO |
| DATA | 双向 | 串行数据输入/输出 | 三线模式只接MCU输出 |
| RD | 输入 | 读时钟 | 四线模式或读回显存时用 |
| VLCD | 模拟 | LCD偏压输入 | 串电阻到VDD,按屏规格调 |
| COM0~COM3 | 输出 | 公共端 | 直接接LCD屏COM脚 |
| SEG0~SEG31 | 输出 | 段输出 | 直接接LCD屏SEG脚 |
三线模式和四线模式的区别只在有没有RD脚。三线模式用CS、WR、DATA三条线,写数据时DATA方向是MCU到芯片;四线模式多一条RD,可以单独读回显存内容做校验。实际项目里三线模式用得最多,因为省引脚,读回功能大多数时候用不上,后续需要调试时再切四线也行。
2.3 兼容型号与移植陷阱
这颗芯片和市面上常见的HT1621在引脚和命令格式上高度接近,很多代码可以直接改引脚宏定义就运行。但有个隐藏差异:部分型号的OSC频率校准寄存器和偏压配置寄存器地址不一样,如果直接用另一型号的初始化数组,可能屏幕能亮但刷新率偏低,肉眼看到闪烁。我一般会先在数据手册里翻寄存器表,对着初始化数组逐条核一遍A0/A1位和命令字长度,再上电跑显示。这个习惯帮我避开了至少两次“芯片型号换了一版,程序莫名其妙闪屏”的问题。
3. 三线时序驱动实战:命令字、数据位与GPIO模拟
3.1 写字节的底层函数
GPIO模拟的时序核心就是把CS拉低,然后按位把数据送到DATA线,在WR上升沿把当前位锁存进芯片。下面这段是基础函数,任何模块都能用:
#define TM1621_CS_H() GPIO_SetBits(GPIOB, GPIO_Pin_12) #define TM1621_CS_L() GPIO_ResetBits(GPIOB, GPIO_Pin_12) #define TM1621_WR_H() GPIO_SetBits(GPIOB, GPIO_Pin_13) #define TM1621_WR_L() GPIO_ResetBits(GPIOB, GPIO_Pin_13) #define TM1621_DATA_H() GPIO_SetBits(GPIOB, GPIO_Pin_14) #define TM1621_DATA_L() GPIO_ResetBits(GPIOB, GPIO_Pin_14) // 发送一个字节,高位在前 // data: 待发送字节 // cnt: 实际发送位数,范围1~8 void TM1621_WriteByte(unsigned char data, unsigned char cnt) { unsigned char i; for (i = 0; i < cnt; i++) { if (data & 0x80) // 取当前最高位 TM1621_DATA_H(); else TM1621_DATA_L(); TM1621_WR_H(); // WR上升沿,芯片锁存这一位 TM1621_DATA_L(); // 释放数据线,避免毛刺 TM1621_WR_L(); // WR拉低,准备下一位 data <<= 1; // 左移,取下一位 } }这段代码的关键在于“高位在前”这个约定。TM1621的命令格式是从最高位开始接收的,所以发送前必须把数据左对齐,cnt参数控制实际发送位数,因为命令字段有时是9位(3位识别码+6位数据),显示数据是8位。很多翻车现场就是把命令当8位发,低一位导致整个命令字错位。最后一行data <<= 1逐位往后递进,对应芯片的移位寄存器逻辑,顺序不能反过来。
3.2 命令发送与数据写入的完整流程
串行通信至少要分成两个层次:底层发位,上层发命令和数据。先给出一个标准的初始化+显示流程:
// 发送命令字,tm1621命令由3位识别码+6位命令组成 // cmd: 6位命令字 void TM1621_SendCmd(unsigned char cmd) { TM1621_CS_L(); // 片选拉低,开始通信 TM1621_WriteByte(0x80, 3); // 发送识别码100,固定不变 TM1621_WriteByte(cmd, 6); // 发送6位命令 TM1621_CS_H(); // 结束通信 } // 向指定显存地址写一个字节 // addr: 5位显存地址,范围0~31 // dat: 8位显示数据 void TM1621_WriteData(unsigned char addr, unsigned char dat) { TM1621_CS_L(); TM1621_WriteByte(0xA0, 3); // 发送识别码101,表示写数据 TM1621_WriteByte(addr << 2, 6); // 5位地址前移到高5位 TM1621_WriteByte(dat, 8); // 8位显示数据 TM1621_CS_H(); } // 初始化并让屏幕工作 void TM1621_Init(void) { TM1621_SendCmd(0x01); // 系统使能 TM1621_SendCmd(0x02); // 打开RC振荡器 TM1621_SendCmd(0x28); // 偏压选择:1/3偏压 TM1621_SendCmd(0x20); // 占空比:1/4,4个COM口 TM1621_SendCmd(0x00); // 关闭WDT,避免意外复位 TM1621_SendCmd(0x08); // LCD关,先清屏再开 TM1621_SendCmd(0x03); // LCD开 }识别码是芯片区分命令和数据的固定头,命令写100、数据写101,这是兼容HT1621系列芯片的通用格式。初始化顺序有讲究:先开系统电源和振荡器,再选偏压和占空比,最后再开LCD显示。如果先开LCD再配偏压,上电瞬间屏幕会闪一下,虽然不影响最终显示,但在有电池供电的产品里会多消耗一次冲击电流。addr << 2这个移位是为了把5位地址放到6位地址字段的高位,芯片内部取前5位,低1位自动补零,这是手册里规定的格式,别省这一步。
3.3 连续写显存与CS时序的细节
写多字节数据时,一个常见优化是连续写,CS保持低电平,中间不拉高:
// 连续向地址0~n写入显示数据 // startAddr: 起始地址 // len: 写入长度 // *buf: 显存缓冲 void TM1621_WriteContinuous(unsigned char startAddr, unsigned char len, unsigned char *buf) { unsigned char i; TM1621_CS_L(); TM1621_WriteByte(0xA0, 3); // 写数据识别码 TM1621_WriteByte(startAddr << 2, 6); for (i = 0; i < len; i++) { TM1621_WriteByte(buf[i], 8); } TM1621_CS_H(); }连续模式的好处是少了多次片选拉高拉低的时间,在刷新频率要求高的场景下能明显减少MCU占用。这里有一个隐含规则:芯片内部地址在写完一个字节后自动加1,所以起始地址设好后,后续字节会按顺序落到相邻地址空间。实际使用中我一般做一个全局显存数组,先全部改到位再一次性刷进芯片,比单笔写入减少闪烁。如果CS提前拉高,芯片会把半截数据当作命令处理,屏幕会出一段乱码,我遇到过一次,排查时先怀疑数据线,后来用示波器看CS和WR的相对时间才定位到是中断把时序切断了。
4. 寄存器配置与段码映射:从显存到位码的转换
4.1 命令字与寄存器对照参考
TM1621的6位命令字在不同取值下控制不同功能,不用全背,但几个关键的要能对应上:
| 命令值 | 功能 | 说明 |
|---|---|---|
| 0x01 | 系统使能 | 打开内部系统电路 |
| 0x02 | 振荡器开 | 启用内部RC振荡 |
| 0x20~0x2E | 占空比/偏压选择 | 低2位是偏压,高3位是占空比 |
| 0x08 | LCD OFF | 关显示,不清显存 |
| 0x03 | LCD ON | 开显示 |
| 0x00 | WDT禁用 | 防止看门狗误复位 |
占空比和偏压这两项必须和LCD屏的参数匹配。4个COM口对应的就是1/4占空比,偏压看屏的阈值电压,常见的是1/3偏压。选错偏压的典型现象是显示对比度很低,所有段都是浅浅的一层,不是缺笔画那种。如果屏是定制的,最好问屏厂要一下工作电压和偏压建议,他们给的参数会比手册里的通用值更准。
4.2 显存地址与COM/SEG的物理映射
TM1621的显存地址和屏幕段码不是一一对应的,它按COM组来划分内存地址空间。每个地址存8位,对应8个SEG引脚,而不同的地址对应不同的COM组合。这个映射关系决定了取模脚本怎么设计。
// 显示缓冲区:按地址0~29排列 // 每个地址的高4位和低4位分别对应不同COM // 段码表示例:数字0~9在某个特定屏上的编码 const unsigned char seg_table[] = { 0xFC, // 0: a b c d e f 0x60, // 1: b c 0xDA, // 2: a b g e d 0xF2, // 3: a b g c d 0x66, // 4: f g b c 0xB6, // 5: a f g c d 0xBE, // 6: a f e d c g 0xE0, // 7: a b c 0xFE, // 8: 全部段 0xF6 // 9: a b c d f g };这条数组看起来简单,实际上是从实际屏的引脚定义里反推出来的。不同厂商的段码屏,COM和SEG的排列可能不一样,同样显示数字“1”,有的屏编码是0x60,有的屏是0x06,差别就在于取模方向。遇到显示数字对但符号位错乱的情况,先把屏的规格书里COM/SEG对照表找出来,列一个Excel表,把每个数字的段亮灭标好,再回来改数组,直接肉眼猜很容易翻车。
4.3 段码取模脚本:减少人工算码的失误
人工算段码太容易错,我一般会写一个简单的Python脚本,从屏的引脚定义文件直接生成段码表:
# seg_map.py # 读取屏定义文件,生成段码表 # 输入格式:每行 "数字, a, b, c, d, e, f, g" # 输出:C语言数组 import sys def calc_code(segments): # 假设COM0~COM3对应位7~4,SEG映射到显存位 code = 0 for seg in segments: if seg == 'a': code |= 0x80 elif seg == 'b': code |= 0x40 elif seg == 'c': code |= 0x20 elif seg == 'd': code |= 0x10 # 按实际COM和SEG映射关系继续补 return code lines = sys.stdin.readlines() for line in lines: parts = line.strip().split(',') num = parts[0] segs = parts[1:] code = calc_code(segs) print(f"0x{code:02X}, // {num}")脚本的价值在于把“引脚定义→段码”这个过程自动化。显示屏数据手册里通常会给每个数字对应的段位组合,手工转码在段数少的时候还好,段数超过50个就很容易漏段或者多段。脚本配合Excel表格,改一处引脚定义重新跑一遍就能得到新的段码表,省去逐行核对的时间。
5. 避坑排查:TM1621显示异常的四个常见事故记录
5.1 屏幕全黑但电流正常
现象:初始化跑了,指令也发了,LCD就是没内容,背光正常,电流和预期一致。
原因排查下来是LCD OFF命令没有先发一次。某些型号芯片上电默认状态是LCD关闭,显存里有数据也不会显示。我那次就是初始化数组里跳过了0x08(LCD OFF)和0x03(LCD ON)的切换,直接写了数据,结果芯片认为显示处于关闭态。
解决:初始化流程固定为“系统使能→振荡器开→偏压/占空比设置→LCD OFF→写显存→LCD ON”。这个顺序不要乱,预先关LCD能让后续写显存的过程中不出现画面撕裂。
5.2 缺笔画但其他段正常
现象:某个数字的中间一横总是不亮,换屏也一样,程序在本地跑正常,同事的板子就有问题。
问题出在段码表的方向上。同一型号芯片可以接不同厂家的屏,COM和SEG的排列有差异,A屏用地址0~15,B屏可能把COM0和COM2对调。代码里的段码表是从A屏的规格书推的,B屏硬件上段位对不上,自然缺笔。
解决:先对所有段做一次全亮的自检代码,在0~15地址全部写入0xFF,看屏幕上哪些段亮哪些不亮,和设计稿对比,把差异记下来改段码表。这个排查方法比一个数字一个数字试快得多。
5.3 显示暗淡、对比度很低
现象:段能亮,但颜色很浅,像是用了很久电池没电的感觉,电压测量正常。
原因通常是偏压配置错误,芯片输出到LCD的驱动电压低于屏的阈值。TM1621有不同偏压选择,如果屏需要1/2偏压而程序里配置成1/3,显示就会偏淡;反过来会偏深甚至出现“串扰”现象,不该亮的段轻微显示。
解决:查看LCD屏规格书里“工作电压”和“偏压”两栏,把命令字调整到匹配的值。偏压是硬件特性,靠软件加大驱动电压是治标不治本,用示波器测VLCD引脚波形和屏规格对比,偏差超过5%就要查电阻配置。
5.4 高低温环境下数字闪屏
现象:板子在常温无异常,放在高温箱里老化一段时间后,个别段开始闪烁,温度降回来又好了。
原因是内部RC振荡器的频率随温度漂移,TM1621在宽温范围内的振荡频率变化比标称值大不少,刷新率掉到人眼可见范围就表现为闪。命令寄存器里有OSC调校选项,但很多兼容型号没把这个功能完整实现。
解决:项目只做常温场景的话忽略这个坑;如果产品要过高温测试,在硬件上优先考虑用外部振荡源,或者换成温漂指标更明确的型号,不要在软件上强行补偿。调试时先确认是全部段闪还是个别段闪,全部段闪基本就是振荡的问题。
6. 读回验证与显示走查:驱动移植的收尾习惯
驱动代码跑通后,不要急着接应用层,先强制自己走一遍显示走查。用四线模式把RD引脚接上,写一个读回函数,把显存内容读出来和写入值比对:
// 四线模式读回显存 // addr: 5位地址 // 返回: 8位显示数据 // 使用READ引脚,时序需要单独配置 unsigned char TM1621_ReadData(unsigned char addr) { unsigned char i, dat = 0; TM1621_CS_L(); TM1621_WriteByte(0x60, 3); // 识别码:110表示读 TM1621_WriteByte(addr << 2, 6); // 切换DATA方向为输入,RD拉低再拉高逐位读取 TM1621_RD_H(); for (i = 0; i < 8; i++) { dat <<= 1; TM1621_RD_L(); if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == SET) dat |= 0x01; TM1621_RD_H(); } TM1621_CS_H(); return dat; }这个函数的用途是验证芯片侧的实际状态,排除MCU侧把数据发丢的情况。实际开发中,我写完驱动会做一张显示走查表:地址0~29逐个填入0xAA和0x55,肉眼确认屏幕上的亮灭规律和表上一致。走查表的模式特意选用交替亮灭,一眼就能看出地址顺序有没有错位。从那以后,每次驱动移植我都在上电后先跑一轮走查,确认段码对应关系再交付给应用同事写界面逻辑。这个习惯帮我挡掉过不少“屏亮但数据对不上”的返工问题,希望对你有用。
本文还有配套的精品资源,点击获取