简介这是一款面向嵌入式开发者的TM1621数码管控制器C语言驱动可直接用于智能家居面板、便携仪表、工业设备等需要七段数码管或点阵LED显示的场景解决开发者通过软件控制显示数字、字符与符号的核心需求。整套资源以RAR格式压缩大小仅2KB包含2个文件一个C源文件和一个配套H头文件。源文件完成与TM1621芯片的底层交互包括通信时序、初始化、亮度调节及指定位置显示等常用API头文件则集中定义寄存器常量、引脚配置和函数原型便于在其他工程中安全引用与快速移植。代码已经过实际硬件测试每一段关键逻辑均附注释即使不熟悉TM1621时序协议也能依据注释理清调用流程缩短调试时间。资源上传以来已有3123人学习适合正在入门显示驱动或需要快速集成TM1621的开发者参考。1. TM1621 驱动到底在调什么一颗段码屏芯片的三线串口与显存操作做小家电和仪表的人对 TM1621 应该不陌生这类芯片常出现在温控器、咖啡机、电量表之类的段码屏上。它本质上是一颗 4 COM × 32 SEG 的 LCD 驱动器内置 128 bit 显存通过 CS、WR、DATA 三根线跟 MCU 通信还能顺带扫按键。所谓「写 TM1621 驱动」核心就三件事搞清帧格式和命令字、给出可靠的初始化序列、把显存写入和按键读取两条路径理顺。适合谁看手上有一块 TM1621 屏幕但手册读得头疼的人以及准备把驱动从某平台移植到另一平台、想少走弯路的人。这篇文章不贴完整源码包但把每个代码块的逻辑、参数边界和踩坑点都拆出来你照着抄一遍基本能亮屏。2. TM1621 的寄存器与命令体系地址映射、帧结构与初始化顺序2.1 芯片功能分块与引脚角色TM1621 内部可以理解成三个功能区显存区、控制寄存器区、按键扫描区。显存区就是那 128 bit 的 RAM4 个 COM 对应 4 个位平面32 个 SEG 对应 32 个地址一个地址存 4 bitbit0~bit3 分别对应 COM0~COM3。你在程序里往某个地址写一个值屏幕上的对应段就会亮。这种映射关系跟数码管的「段码表」逻辑一样只是多了一个 COM 维度。引脚上最需要留意的是 DATA 这根线它是双向的。写显存、写控制寄存器的时候DATA 是输出读按键状态的时候同样的引脚要切回输入。很多人第一次用的时候把它当成纯输出的 SPI结果按键扫描永远读不到数据。CS 低电平有效整帧传输期间都要保持低帧结束拉高相当于一个锁存动作。WR 是写时钟常见做法是让它上升沿采样 DATA 线上的电平一帧数据移完、CS 拉高之后命令或者数据才真正落进芯片内部。TM1621 的电源域也要注意VDD 是逻辑电源VLCD 是 LCD 驱动电压。有些模块直接把 VLCD 接 VDD有些通过外部电阻分压或者电荷泵单独提供。VLCD 的高低直接决定屏幕对比度这一块留到第 4 章专门讲先记住它跟逻辑电源不是一个概念就够了。2.2 命令帧结构与地址映射我按手头手册整理的帧格式是这样每帧固定 13 bit从低位开始发。前 3 bit 是帧头固定为101相当于告诉芯片「我要开始读写了」中间 6 bit 是命令字段写显存的时候它就是显存地址最后 4 bit 是数据字段写显存的时候它就是 COM0~COM3 对应的 4 个位。控制类命令也走同一个帧结构只是命令字段换成具体的寄存器编号数据字段换成参数。这里必须提醒一句TM1621 不同批次、不同封装后缀的手册对命令字段的位宽和偏压位的定义不完全一致网上抄的初始化序列翻车大多栽在这。我下面给的值是常见做法你动手前先对着自己的手册核一遍高位标志别直接抄。功能命令字段(6bit)数据字段(4bit)说明系统关闭 SYS DIS0x000x0关内部 RC 振荡器低功耗系统使能 SYS EN0x000x1启动内部振荡器LCD 关闭0x020x0停止输出显示波形显存内容不清LCD 开启0x020x1正常输出显示波形偏压 1/2 bias0x050x0对比度较低、功耗低偏压 1/3 bias0x050x1对比度较高、功耗略高写显存0x00~0x1F任意 4bit地址对应 SEG数据对应 COM写显存的时候有一个很实用的特性地址自增。如果你连续写多个地址不需要每帧都重新指定地址。但要留意一点有的芯片只有在同一帧连续写时才自增有的芯片只要内部指针在写操作后自动加一这个差异会影响你代码里是否需要显式地逐地址写入。我的习惯是底层帧函数封装好上层不管自增不自增逐个地址写最稳。初始化顺序我建议固定成系统关闭 → 设置偏压 → 清屏 → 系统使能 → LCD 开启。顺序不要乱。先开系统再开显示可以避免屏幕一上电就出现随机段全亮的「花屏」先清屏再开显示是为了防止显存里残留上电噪声数据直接显示出来。3. 从零写 TM1621 驱动时序模拟、初始化序列与显示刷新3.1 三线时序的软件模拟实现TM1621 的时序用 GPIO 模拟是最通用的做法毕竟这种芯片的场景基本是低成本小家电MCU 资源有限硬件 SPI 不一定有空闲外设就算有三线时序也得拿 GPIO 搭。先定义一组 HAL 宏把 IO 操作隔离出来后面做平台移植的时候只改这一块。#define TM1621_CS_LOW() gpio_write(CS_PORT, CS_PIN, 0) #define TM1621_CS_HIGH() gpio_write(CS_PORT, CS_PIN, 1) #define TM1621_WR_LOW() gpio_write(WR_PORT, WR_PIN, 0) #define TM1621_WR_HIGH() gpio_write(WR_PORT, WR_PIN, 1) #define TM1621_DATA_OUT(v) gpio_write(DATA_PORT, DATA_PIN, v) #define TM1621_DATA_IN() gpio_read(DATA_PORT, DATA_PIN)这里gpio_write、gpio_read是平台相关的函数实际工程里换成你手头 SDK 的接口就行。接着是发送单 bit 和发送整帧的函数。我这边按 WR 上升沿采样来写如果你的手册写的是下降沿采样把数据写入和 WR 拉高的顺序对调一下即可。static void tm1621_send_bit(uint8_t bit) { TM1621_DATA_OUT(bit ? 1 : 0); delay_us(1); /* 数据建立时间别省 */ TM1621_WR_HIGH(); delay_us(1); /* 高电平保持时间 */ TM1621_WR_LOW(); } void tm1621_send_frame(uint8_t cmd, uint8_t data) { uint8_t i; TM1621_CS_LOW(); /* 整帧传输期间 CS 保持低 */ /* 帧头 101低位先发所以先发 1再发 0再发 1 */ tm1621_send_bit(1); tm1621_send_bit(0); tm1621_send_bit(1); /* 命令/地址字段6 bit */ for (i 0; i 6; i) { tm1621_send_bit((cmd i) 0x01); } /* 数据字段4 bit */ for (i 0; i 4; i) { tm1621_send_bit((data i) 0x01); } TM1621_CS_HIGH(); /* 帧结束数据锁存 */ delay_us(1); }这段代码里有几个地方是容易出问题的。第一delay_us在 1us 级别实际上很多 MCU 的 GPIO 翻转本身就几十 ns不加延时也能工作但 TM1621 这类芯片在高温、低电压下对建立时间更敏感留 1us 余量是血泪经验。第二帧头 101 不能发错很多人抄代码时把位序从高位开始发结果整帧就反了。第三TM1621_CS_HIGH()之后最好加一点延时再开始下一帧给芯片内部锁存留时间。3.2 初始化序列与显示缓冲写入初始化函数建议做成幂等的也就是无论什么时候调用都能把芯片恢复到确定状态。上电后先延时 50ms 左右等 LCD 供电稳定尤其是电池供电的设备MCU 起来得比 LCD 电源快不延时就会出现首帧花屏。void tm1621_init(void) { delay_ms(50); /* 等电源稳定 */ tm1621_send_frame(0x00, 0x0); /* SYS DIS 先关系统 */ tm1621_send_frame(0x05, 0x1); /* 设置 1/3 bias */ tm1621_clear(); /* 上电先清屏 */ tm1621_send_frame(0x00, 0x1); /* SYS EN 使能系统 */ tm1621_send_frame(0x02, 0x1); /* LCD ON 开显示 */ }清屏和写显存是同一个底层函数区别只在地址和数据。tm1621_clear()就是把 0x00 到 0x1F 全部写 0。注意写显存函数里的地址边界TM1621 的地址范围是 5 bit最大 0x1F超了就会回卷或者写到别的寄存器上不同批次行为不同所以防御性判断不能少。void tm1621_clear(void) { uint8_t addr; for (addr 0; addr 0x20; addr) { tm1621_send_frame(addr, 0x00); } } void tm1621_write_seg(uint8_t addr, uint8_t com_data) { if (addr 0x1F) { return; } tm1621_send_frame(addr, com_data 0x0F); }com_data先跟 0x0F 做掩码再发送是因为这个芯片每个地址只有 4 bit 有效位高 4 位传了也会被忽略但万一以后换芯片这个掩码能帮你挡掉不少意外。真正做显示内容的时候我一般会在上层维护一个 32 字节的显存 buffer业务代码只改 buffer再由一个刷新函数把整块 buffer 写进芯片这样避免频繁单点写入造成显示闪烁。3.3 显示刷新与按键扫描的调度很多人写显示刷新喜欢放在定时中断里每毫秒刷一次对 TM1621 其实没必要它的显存是静态 RAM写入一次会一直保持。刷新频率只需要满足「人眼看不到变化过程」即可典型做法是业务数据改变时立刻刷或者 10~20ms 周期刷一次。刷得越频繁跟按键扫描抢 DATA 线的概率越大。按键扫描要做的事是发一个读命令帧头然后把 DATA 方向切到输入在 WR 上升沿把 4 bit 数据读回来。关键是读完必须立刻把 DATA 切回输出否则下一次写显存时数据线方向不对显示就会出雪花一样的乱码。uint8_t tm1621_read_key(void) { uint8_t val 0; uint8_t i; TM1621_CS_LOW(); /* 帧头 101 */ tm1621_send_bit(1); tm1621_send_bit(0); tm1621_send_bit(1); /* 读命令 地址这里以 0x00 为例 */ for (i 0; i 6; i) { tm1621_send_bit((0x00 i) 0x01); } tm1621_data_dir_input(); /* DATA 切到输入 */ delay_us(2); /* 方向切换建立时间 */ for (i 0; i 4; i) { TM1621_WR_HIGH(); delay_us(1); if (TM1621_DATA_IN()) { val | (1 i); } TM1621_WR_LOW(); } tm1621_data_dir_output(); /* 立刻切回输出 */ TM1621_CS_HIGH(); return val; }按键扫描和显存写入不要在同一个时刻并发。最简单的做法是让它们在同一个循环里顺序执行按键扫描放在主循环的前半段显存刷新放在后半段如果用了 RTOS就给这两类操作加一个互斥锁或者干脆把按键扫描做成只在无刷新请求时进行。方向切换那 2us 延时看着短但缺了它数据线电平在切换后还没稳定读回来的按键值会随机跳。4. 移植与参数配置HAL 抽象、偏压选择与低功耗恢复4.1 跨平台移植的 HAL 抽象层第 3 章的代码直接在宏层面操作 GPIO好处是简单坏处是换平台要改一堆宏。我一般在项目稍微规整一点的时候就把硬件操作抽象成一个结构体驱动代码只跟这个结构体打交道。typedef struct { void (*cs)(uint8_t level); void (*wr)(uint8_t level); void (*data_out)(uint8_t level); uint8_t (*data_in)(void); void (*data_dir)(uint8_t is_output); } tm1621_hal_t; static void bsp_cs(uint8_t level) { gpio_write(CS_PORT, CS_PIN, level); } static void bsp_wr(uint8_t level) { gpio_write(WR_PORT, WR_PIN, level); } static void bsp_data_out(uint8_t lv) { gpio_write(DATA_PORT, DATA_PIN, lv); } static uint8_t bsp_data_in(void) { return gpio_read(DATA_PORT, DATA_PIN); } static void bsp_data_dir(uint8_t out){ gpio_set_dir(DATA_PORT, DATA_PIN, out); } const tm1621_hal_t tm1621_hal { .cs bsp_cs, .wr bsp_wr, .data_out bsp_data_out, .data_in bsp_data_in, .data_dir bsp_data_dir, };这样上层tm1621_send_frame全部改成调hal.cs(0)、hal.wr(1)这类接口实际的引脚、端口、寄存器操作全部留在 BSP 文件里。平台移植的时候只改这个结构体里的函数实现驱动逻辑一行不动。这个分层还能带来一个额外好处你可以在 PC 上写一个模拟 hal把帧数据打出来调试不用每次都在板子上点灯。要注意的是data_dir这个接口在 HAL 里很容易被忽略但实际上它是 TM1621 和普通 SPI 从机最大的区别。普通 SPI 从机的数据线方向是固定的TM1621 要读写切换。如果你用的 MCU 引脚支持矩阵重映射务必确认 DATA 引脚在切方向后还能被正确读取别把data_dir实现成只改方向不改复用功能。4.2 偏压、占空比与 VLCD 的配置选择TM1621 是 4 COM 输出占空比固定 1/4能调的只有偏压。偏压的选择直接决定液晶两端能分到多少有效电压跟 VLCD 是配合关系。1/2 bias 下LCD 两端有效电压约等于 VLCD/21/3 bias 下约等于 2×VLCD/3。同样是 3.3V 的 VLCD1/3 bias 的对比度明显更高但功耗也高一些。偏压有效电压占比对比度静态功耗适用场景1/2 biasVLCD/2一般较低电池供电、显示内容少1/3 bias2×VLCD/3高略高插电设备、显示密度大我的习惯是3V 纽扣电池供电的设备优先选 1/2 bias省电优先5V 供电或者显示内容多的设备选 1/3 bias保证视角和对比度。VLCD 这个引脚上的电容不能省至少 100nF靠近芯片引脚放置。没有这个电容VLCD 波形上有毛刺你调命令怎么调对比度都是花的。还有一类特殊情况模块上 VLCD 直接跟 VDD 短接这时候如果供电电压偏低比如锂电池放电到 3.0V1/3 bias 的有效电压只有约 2V屏幕会明显变淡。解决方法是改电路让 VLCD 经过一个电荷泵升压或者接受「电量越低屏幕越淡」的现实把低电量报警做在 UI 里而不是物理电路里。这也是很多仪表项目常见的取舍。4.3 低功耗模式关显示、关系统与保留显存低功耗处理是 TM1621 驱动里最容易被写错的部分。SYS DIS 是关系统内部振荡器停振LCD OFF 是关显示波形输出系统还在跑。两者不是一回事。进入低功耗的正确顺序是先 LCD OFF再 SYS DIS。如果直接 SYS DIS显示波形输出会在振荡器停振的瞬间产生一个不完整的波形屏幕上可能出现残影。void tm1621_enter_sleep(void) { tm1621_send_frame(0x02, 0x0); /* LCD OFF */ tm1621_send_frame(0x00, 0x0); /* SYS DIS */ }唤醒流程有个隐蔽坑有些批次的 TM1621 在 SYS DIS 期间显存内容能保持有些会掉。你不能赌它不掉唤醒后必须把上层维护的显存 buffer 重新刷一遍。void tm1621_wakeup(void) { tm1621_send_frame(0x05, 0x1); /* 重设偏压 */ tm1621_send_frame(0x00, 0x1); /* SYS EN */ tm1621_clear(); /* 先清屏 */ tm1621_send_frame(0x02, 0x1); /* LCD ON */ tm1621_write_buf(disp_buf, 32); /* 重刷显存 buffer */ }这里tm1621_write_buf就是把上层 buffer 的 32 字节逐个地址写进去。唤醒流程跟初始化流程基本一致区别是唤醒不需要再延时 50ms因为电源已经在位。如果你发现唤醒后屏幕偶尔不亮大概率是漏了清屏这一步——不清屏残留数据和正常数据叠加就会出现「看起来没亮其实亮了乱码」的诡异现象。5. TM1621 调试点排查五条实测踩坑记录与对策5.1 上电花屏与初始化乱码现象设备一上电屏幕全段点亮亮几秒后恢复正常或者初始化完成后某些不该亮的段一直亮着。原因MCU 的 GPIO 在复位期间是高阻态CS 脚如果悬空噪声会把 CS 拉低TM1621 就会把 DATA 线上的随机电平当成有效数据写进显存。这时候显存内容是垃圾数据等你初始化清屏之前它就被显示出来了。解决硬件上给 CS 加一个 10k 上拉电阻到 VDD确保 MCU 复位期间 CS 保持高电平固件上初始化之前先延时 50ms然后在 SYS EN 和 LCD ON 之前强制清屏。这两个措施同时做基本能根除花屏。5.2 段码错位和 COM 映射反了现象显示数字 1实际亮的是第 3 段或者往地址 0x00 写数据屏幕最右边的段亮了而不是最左边。原因位序发反。TM1621 是 LSB 先发如果你循环从cmd 5开始发整帧就变成 MSB 先发地址和数据全部镜像。另一个常见原因是 PCB 上 SEG 走线没有按顺序排逻辑地址和物理段不是一一对应。解决先用点亮测试法确认映射方法在第 6 章详细写。位序问题用 0x0F 和 0xF0 分别写同一个地址看屏幕亮的位置是不是镜像就能判断是不是遍历方向反了。改send_frame里的循环方向即可不用改业务代码。5.3 按键扫描后显示出现雪花乱码现象按键按一下显示内容出现局部乱码过一会又自己恢复或者乱码会粘住不变。原因按键扫描把 DATA 切成输入后没有切回输出或者切回输出后没等建立时间就开始写显存。此时 DATA 线电平被外部电路或者按键走线上的噪声干扰写入显存的数据就是错的。解决读按键函数末尾强制加一句tm1621_data_dir_output()并且在切回输出后延时 2~5us 再允许其他模块调用写显存函数。如果项目里用中断触发按键扫描记得在tm1621_read_key整个函数前后加临界保护防止扫描中途被高优先级中断打断。5.4 低功耗唤醒后屏幕不亮但程序在跑现象设备从休眠唤醒后功能正常就是屏幕一片空白重新上电又好了。原因唤醒流程只发了 SYS EN漏了 LCD ON或者发了 LCD ON但显存内容在 SYS DIS 期间已经丢了没重刷。还有一个隐蔽情况唤醒后偏压寄存器回到默认值显示波形电压不对屏幕没有任何显示内容。解决唤醒流程严格按第 4 章的顺序执行重设偏压 → SYS EN → 清屏 → LCD ON → 重刷显存 buffer。不要自己精简步骤尤其是「清屏」这一步看起来多余实际上能保证显存里没有残留垃圾。从那以后我每次写低功耗都强制跑一遍完整唤醒流程再也没遇到过「休眠后黑屏」的问题。5.5 显示均匀偏淡或周期性闪烁现象屏幕能显示但对比度明显偏低或者整个屏幕以某个固定频率闪烁。原因VLCD 电压不够或者 VLCD 引脚上的滤波电容太小。周期闪烁大概率是偏压切换频率和数据刷新频率互相干扰TM1621 的显示刷新是芯片内部自己做的外部频繁写显存会导致 COM 波形畸变。解决先量 VLCD 引脚电压确认它跟偏压选择的匹配关系然后在 VLCD 引脚上补 100nF~1uF 电容。固件层面把显存写入频率降下来只在数据变化时写不要在定时器里无脑每毫秒刷。改完这两个地方偏淡和闪烁基本都能解决。6. 一个通用的验证技巧点亮固定 pattern 反推段码映射段码屏调试最花时间的不是把驱动写出来而是确认「逻辑地址 → 屏幕实际段」的映射关系。芯片手册给的是 COM0~COM3 和 SEG0~SEG31 的编号但 PCB 走线经常不按顺序厂家给的原理图还可能标错。我有个固定做法跳过手册直接点亮测试。初始化完成后先不要清屏往地址 0x00 写 0x0F看哪些点亮了。0x0F 意味着 COM0~COM3 全亮对应的就是 SEG0 那 4 个位置。如果预想的位置没亮而别处亮了说明这 4 个段在 PCB 上被接到了别的 SEG 编号。逐个地址写 0x01、0x02、0x04、0x08把亮的位置记录下来就能建出一张完整的映射表。static const uint8_t seg_map[32] { /* 这是点亮测试后的实际结果每个元素 实际物理 SEG 号 */ 0x00, 0x0D, 0x02, 0x03, /* 0~30 号地址对应物理 SEG01 号对应 SEG13 ... */ 0x04, 0x05, 0x06, 0x07, /* 剩余按实测填充 */ }; uint8_t tm1621_logic_to_phys(uint8_t addr) { if (addr 0x1F) { return 0; } return seg_map[addr]; }建立这张表之后上层写字符就只认逻辑段号物理走线再怎么乱都不影响业务代码。位序对不对也可以在这一步验出来往地址 0x00 写 0x01如果亮的是 COM1 的点而不是 COM0说明你的send_frame数据位遍历顺序反了。改遍历方向而不是改业务代码。我最早调这类段码屏的时候吃了不少手册和实物对不上的亏后来学乖了每次接新屏不管手册写得多详细都强制先跑一遍点亮测试把映射表填好再写上层。这套流程看起来多花了半小时但省掉的是后面整机联调时几天的排查时间。希望帮到你。本文还有配套的精品资源点击获取