☰
TM1621驱动代码深度拆解:段码屏显示、时序与实战避坑
2026/10/16 5:00:13 网站建设 项目流程

简介:TM1621驱动程序是一份面向嵌入式开发者与电子爱好者的数码管显示控制源码,针对TM1621芯片提供完整的软件接口,适用于智能家居面板、仪器仪表和各类单片机项目。压缩包为RAR格式,仅含2个文件,分别是C源文件和配套头文件,整体大小约2KB,代码精简、结构清晰,便于快速集成到现有工程中。已有3123人学习下载,对于轻量级驱动库而言,具备不错的参考热度。源文件中的驱动程序已经过硬件测试,并配有逐段注释,涵盖芯片初始化、串行数据发送、数码管亮度设置和数字字符显示等核心功能;头文件则声明了函数接口、引脚定义和通信协议常量,方便调用与移植。借助这份带注释的源码,开发者无需从零查阅芯片手册,即可理解TM1621的工作时序和操作要点,有效降低开发门槛,缩短产品显示模块的调试周期。

1. 从一块显示黑屏开始:tm1621 驱动到底在驱动什么

如果你做过带段码液晶屏的仪表类产品,大概率见过 tm1621 这颗芯片。它干的活很简单:用 GPIO 模拟时序,把 MCU 想要显示的数字和符号,映射到 LCD 玻璃上的 COM 口和 SEG 口。我最初接触它是因为某型号的温控仪样机点不亮屏幕,查了一圈发现不是屏的问题,是驱动代码里命令字的位序写反了。从那以后我就把这类驱动代码当作一个独立的工程来对待,而不是顺手抄一段时序就完事。

tm1621 本身是一个 LCD 驱动器,内部有 DPRAM(显示数据存储区),外部通过 DIO、CLK、CS 三根线接收 MCU 发来的命令和数据。它支持 32 个 SEG 和 4 个 COM,最多可以驱动 128 个显示点。这个资源里的驱动程序,就是把这颗芯片的初始化、写命令、写显示数据、按键扫描全部封装好的一套代码。适合三类人看:一是正在调段码屏的嵌入式开发者,二是想搞懂 LCD 驱动时序的学生,三是准备把老产品从直驱屏改成驱动芯片方案的工程师。这篇文章会把代码结构、核心函数、移植要点和最容易翻车的几个细节完整拆开讲。

2. 整体架构与代码目录:一个驱动文件如何撑起整个显示模块

2.1 驱动代码的分层思路

拿到这份驱动源码,第一眼看上去文件数量不多,但它的分层是清晰的。整个工程围绕 tm1621.c 和 tm1621.h 两个核心文件展开,再加上一个 platform 层做 GPIO 适配。这种分层的直接好处是,换一块 MCU 的时候只需要改底层 IO 操作的实现,芯片驱动的逻辑一行都不用动。

驱动代码从功能上分为三层:

第一层是硬件抽象层,负责封装 GPIO 的输入输出模式和电平读写。通常是一组宏定义或者内联函数,比如tm1621_dio_write(level)、tm1621_clk_write(level)、tm1621_cs_write(level)。这一层直接面对寄存器操作,是整个驱动能够跑起来的前提。

第二层是时序层,负责严格按照 tm1621 数据手册上的时序图来产生 DIO 和 CLK 的配合关系。这层包括读写的核心位操作,一个 bit 一个 bit 地把命令字和数据字搬运进去。这里面最容易出问题的就是时钟极性:tm1621 要求 CLK 上升沿时 DIO 数据有效,写反了就是乱码。

第三层是应用层,封装出tm1621_set_display_data()、tm1621_display_on()、tm1621_read_key()这类面向业务的接口。调用者不需要关心芯片内部寄存器怎么操作,只需要把要显示的数据包传进去就行。

2.2 目录结构与关键文件职责

一个典型的 tm1621 驱动工程目录如下:

tm1621_driver/ ├── inc/ │ └── tm1621.h ├── src/ │ ├── tm1621.c │ └── platform_gpio.c ├── examples/ │ ├── main_tm1621_basic.c │ └── main_tm1621_key_scan.c └── docs/ └── tm1621_commands_table.md

tm1621.h 里定义了芯片的寄存器地址、命令字和对外接口函数声明。tm1621.c 实现核心时序与数据写入逻辑,包括tm1621_init、tm1621_write_command、tm1621_write_data。platform_gpio.c 把实际的 GPIO 操作从芯片逻辑里隔离出来,里面的函数体是根据具体 MCU 的寄存器来写的,stm32 用 HAL 库、51 用 sbit 直接操作、其他平台用寄存器地址赋值都可以。

exmaples 目录里提供了两个示例:基础显示示例和按键扫描示例。docs 目录下的命令表文档是排查问题时的重要参考,里面列出了系统使能、偏压配置、刷新频率设置这些常用命令字的值。

2.3 对外接口说明

从使用者的角度,只需要关心这几个接口:

void tm1621_init(void); void tm1621_display_on(void); void tm1621_display_off(void); void tm1621_write_data(uint8_t com, uint8_t seg, uint8_t bit_value); void tm1621_set_all_display(uint8_t segment_data[4][8]); uint8_t tm1621_read_key(void);

tm1621_init完成芯片的上电复位和初始配置,包括选择 1/4 占空比(4 个 COM 口全部使用)和 1/3 偏压。tm1621_write_data是单点写入函数,com 和 seg 指定显示位置,bit_value 是 0 或 1,用来点亮或熄灭某个段。tm1621_set_all_display是一次性写入整个显示缓冲区的高效版本,适合刷新整个屏幕的场景。tm1621_read_key用来读取 tm1621 的按键扫描结果,这个功能依赖芯片的 KEY 引脚和内置扫描逻辑。

提示:如果你的屏只有 3 个 COM 口,不需要把 init 函数里的占空比参数改成 1/3,那是标准 4 COM 模式。强行改占空比可能让显示内容偏移。

3. 核心模块拆解:一个字节从 MCU 到 LCD 玻璃的完整旅程

3.1 初始化流程:读命令表远比背代码重要

tm1621 的初始化说到底是往芯片里写一串命令字,但这个流程有先后依赖。代码里的tm1621_init函数一般按这个顺序执行:

void tm1621_init(void) { tm1621_cs_write(1); tm1621_clk_write(0); tm1621_dio_write(0); tm1621_delay_us(10); tm1621_cs_write(0); // 片选拉低,开始命令传输 tm1621_write_command(0x01); // SYS_EN,打开系统振荡器 tm1621_write_command(0x02); // LCD_ON,打开 LCD 偏压发生器 tm1621_write_command(0x28); // 选择 1/4 占空比,4 个 COM 口 tm1621_write_command(0x20); // 选择 1/3 偏压 tm1621_write_command(0x03); // 设置刷新频率为中档 tm1621_cs_write(1); }

第一个命令是打开系统振荡器,这一步没做的话后面的命令全部无效。然后是打开 LCD 偏压发生器,这相当于给液晶玻璃“上电”。占空比和偏压参数必须与实际屏的规格一致。最后设置刷新频率,一般选中等频率,太低会看到闪烁,太高会增加功耗。

写命令之前,片选必须拉低。有些驱动代码把片选当成可有可无的操作,只在初始化时拉一次,后面读写数据就不管了,这在 tm1621 上是不成立的:每次传输命令或数据之前,CS 都要有一个完整的拉低、拉高的脉冲,芯片才认为是一次新的传输开始。如果你把 CS 一直拉低,芯片会把连续的数据误读成一条长命令。

3.2 读写时序的底层实现:位操作是全部玄学所在

tm1621 通过 3 线串行接口通信,没有时钟自动产生机制,完全靠 MCU 用 GPIO 模拟。核心的写字节函数长这样:

void tm1621_write_byte(uint8_t data, uint8_t is_command) { uint8_t i; if (is_command) { tm1621_dio_write(1); // ID 位:命令模式 } else { tm1621_dio_write(0); // ID 位:数据模式 } tm1621_dio_set_output(); tm1621_clk_write(0); tm1621_delay_us(1); tm1621_clk_write(1); // 产生第一个上升沿,锁存 ID 位 tm1621_delay_us(1); for (i = 0; i < 8; i++) { tm1621_clk_write(0); if (data & 0x80) { tm1621_dio_write(1); } else { tm1621_dio_write(0); } tm1621_delay_us(1); tm1621_clk_write(1); // 上升沿,DIO 数据被锁存进芯片 tm1621_delay_us(1); data <<= 1; } tm1621_clk_write(0); tm1621_delay_us(5); }

这段代码把电气层面的时序要求全部落地了。第一个 CLK 上升沿用来锁存 ID 位,告诉芯片接下来是命令还是数据。后面的 8 个 CLK 上升沿锁存一个字节的数据,高位在前。DIO 的电平必须在 CLK 拉低期间准备好,在 CLK 拉高时芯片采样。

几个参数值得关注:

  • tm1621_delay_us(1)是时序裕量,要求 CLK 高电平和低电平各自保持至少 1 微秒。如果 MCU 主频很高,GPIO 翻转本身很快,这个延时就是硬性要求,去掉会偶发显示乱码。
  • tm1621_dio_set_output()必须显式调用,因为 DIO 在按键扫描模式下是输入方向,读写切换不做会出问题。
  • 命令和数据都走这个写字节函数,区别只在第一个 ID 位。

3.3 DPRAM 映射与显存操作逻辑

tm1621 内部有 128 个 bit 的 DPRAM,分成 4 个 COM 组,每组 32 个 bit,对应 32 个 SEG。显示数据按“COM + SEG”的方式寻址,但实际写入时是以 4 bit 为单位,一次写一个 COM 的一个 nibble(4 bit),正好对应 4 个 SEG 在同一 COM 下的状态。

驱动代码通常会维护一个本地的显示缓冲区,结构如下:

uint8_t tm1621_display_buffer[4][8]; // [com][seg_byte] // com: 0~3,对应 COM0~COM3 // seg_byte: 0~7,每个字节管理 4 个 SEG

seg_byte的高 4 位对应 SEG0~SEG3,低 4 位对应 SEG4~SEG7。每次写一个 nibble 时,芯片会根据命令字里的地址位自动把数据放到对应 COM 的对应 SEG 组里。

这段映射关系不画图很难理解,我踩坑时直接把 datasheet 上的地址表打印出来贴在显示器边上。tm1621 的 DPRAM 地址本质上是按 SEG 顺序排列的,但每个地址里存的是 4 个 COM 的数据。这一点和很多其他 LCD 驱动芯片正好相反,那些芯片是按 COM 排地址、每个地址里存多个 SEG。写驱动代码时如果拿旧逻辑套,显示结果就会错位。

tm1621_write_data函数的实现思路是:

void tm1621_write_data(uint8_t com, uint8_t seg, uint8_t bit_value) { uint8_t addr; uint8_t data_nibble; uint8_t mask; uint8_t idx; addr = seg / 4; // 计算地址在哪个字节 idx = seg % 4; // 在这个字节里的第几个 nibble if (bit_value) { tm1621_display_buffer[com][addr] |= (0x01 << idx); } else { tm1621_display_buffer[com][addr] &= ~(0x01 << idx); } data_nibble = (tm1621_display_buffer[com][addr] >> 4) & 0x0F; mask = 0x0F << 4; tm1621_cs_write(0); tm1621_write_byte(addr * 4, 0); // 数据地址,注意芯片内部地址递增 tm1621_write_byte((data_nibble << 4), 0); tm1621_cs_write(1); // 把另一个 nibble 也写上去 data_nibble = tm1621_display_buffer[com][addr] & 0x0F; tm1621_cs_write(0); tm1621_write_byte(addr * 4 + 1, 0); tm1621_write_byte((data_nibble << 4), 0); tm1621_cs_write(1); }

这个函数的核心问题是地址计算。tm1621 的地址是按 nibble 递增的,addr * 4和addr * 4 + 1这两个地址分别对应同一个存储单元的高 4 位和低 4 位。写入的时候必须把数据放到低 4 位的位置再左移 4 位,因为芯片的数据位 D3~D0 对应低 4 位,但发送格式要求数据在字节的高 4 位。很多开发者在这里直接传data_nibble不左移,结果显示一塌糊涂。

3.4 按键扫描实现:复用 COM 口的巧妙设计

tm1621 除了驱动 LCD,还能扫按键。它的原理是利用 COM 口和 KEY 引脚之间的电平变化,当某个按键按下时,对应 COM 和 KEY 之间短路,芯片内部的扫描逻辑会检测到这个变化。驱动代码里的tm1621_read_key函数在这种场景下用:

uint8_t tm1621_read_key(void) { uint8_t key_value = 0; uint8_t i; tm1621_dio_set_input(); // 切换 DIO 为输入,读取 KEY 引脚 tm1621_cs_write(0); tm1621_write_byte(0x40, 1); // 命令:读取按键值 for (i = 0; i < 8; i++) { tm1621_clk_write(0); tm1621_delay_us(1); key_value <<= 1; if (tm1621_dio_read()) { key_value |= 0x01; } tm1621_clk_write(1); tm1621_delay_us(1); } tm1621_cs_write(1); tm1621_dio_set_output(); // 切回输出模式,准备下次显示 return key_value; }

按键扫描期间,DIO 完全变成了输入引脚。tm1621_dio_set_input()这一步是针对特定 MCU 的 GPIO 模式切换,如果忘了切换,读到的按键值永远是 0xFF。读取完成后必须切回输出模式,否则下一次写显示数据时 DIO 拉不高低,所有段都会变成点亮状态,表现为屏幕全亮。

一般读键和显示刷新放在同一个循环里交替执行,但要注意时序上的配合。按键扫描时 DPRAM 的内容不会丢,所以不用担心切换方向会影响显示。我遇到过一次异常是按键扫描函数里没有延时,导致 CLK 翻转太快超出了芯片极限,某些按键的码值偶尔会错位。在每一次 CLK 翻转之间加一个小的空操作延时,问题就不再出现了。

4. 运行与移植:从接线到点亮一块真实屏幕的完整路径

4.1 硬件连接与平台适配

把这份驱动跑起来的硬件前提是:MCU 至少空出 3 个 GPIO,分别接 tm1621 的 DIO、CLK、CS。还有一条线是 VDD 和 GND,tm1621 的工作电压在 2.4V 到 5.2V 之间,大多数 3.3V 和 5V 系统都能直接供电。如果 MCU 的 IO 电平是 1.8V 而 tm1621 跑在 3.3V,中间需要加电平转换。

platform_gpio.c 文件是所有移植工作的核心。原始代码可能写的是某个特定平台的寄存器操作,你在自己的板子上要用对应平台的实现替换。以 STM32 的 HAL 库为例:

void tm1621_dio_write(level) { HAL_GPIO_WritePin(TM1621_DIO_PORT, TM1621_DIO_PIN, level ? GPIO_PIN_SET : GPIO_PIN_RESET); } void tm1621_clk_write(level) { HAL_GPIO_WritePin(TM1621_CLK_PORT, TM1621_CLK_PIN, level ? GPIO_PIN_SET : GPIO_PIN_RESET); } void tm1621_cs_write(level) { HAL_GPIO_WritePin(TM1621_CS_PORT, TM1621_CS_PIN, level ? GPIO_PIN_SET : GPIO_PIN_RESET); }

如果你用的是 51 单片机,那更简单,直接用 sbit 定义:

sbit TM1621_DIO = P1^0; sbit TM1621_CLK = P1^1; sbit TM1621_CS = P1^2;

然后写电平就是直接赋值。gpio 模式切换在 51 上不太需要,因为 51 的引脚是准双向口,读的时候直接读引脚电平。但在 STM32 上必须用GPIO_MODE_OUTPUT_PP和GPIO_MODE_INPUT两种模式来回切换。

4.2 初始化调用与显示验证

在 main 函数里的标准流程是:

int main(void) { // 初始化 MCU 时钟、GPIO 外设等 platform_gpio_init(); // 初始化 tm1621 tm1621_init(); // 点亮所有段,做显示自检 uint8_t test_pattern[4][8]; memset(test_pattern, 0xFF, sizeof(test_pattern)); tm1621_set_all_display(test_pattern); // 显示数字 1234 uint8_t seg_data[4][8] = {0}; // 这里按你的屏段码表填数据 disp_number_on_lcd(1234, seg_data); tm1621_set_all_display(seg_data); while (1) { // 主循环:刷新显示 + 扫描按键 uint8_t key = tm1621_read_key(); if (key != 0) { handle_key(key); } delay_ms(5); } }

显示自检很重要。你先让所有段全部点亮,确认每个点都亮、没有乱码。如果全亮时某些段不亮,基本可以排除驱动代码问题,检查 LCD 玻璃的引脚和电路板的焊接。如果全亮时出现本来不该亮的段,那就是 SEG 或 COM 的对应关系搞错了,需要去查你的屏的引脚定义图。

数字显示函数disp_number_on_lcd通常是开发者自己写的,因为段码屏的段和数字的对应关系每块屏都不一样。驱动代码只负责把你的显示缓冲区内容搬到芯片上,也不知道你要显示数字 3 需要哪些段,这些知识完全在屏的规格书里。

4.3 常见坑之外的三个低频故障

这里提三个不常见但真实的故障。第一个是 CLK 引脚被复用。有些 MCU 的 CLK 引脚同时也接了调试器或者外设时钟线,导致驱动时序被其他信号干扰。解决方案是换引脚或者检查复用配置。

第二个是片选线上的下拉电阻。tm1621 的 CS 引脚内部没有上拉,外部如果接了一个大下拉电阻,可能导致 CS 无法在短时间内拉高到有效电平,芯片接收的数据会丢后半部分。去掉下拉电阻或者换成 10k 以内的上拉可以解决。

第三个是延时函数的精度问题。如果在 RTOS 环境里用了osDelay而不是忙等延时,任务被抢占一次就会让 CLK 的高电平时间拉长到不可控,低电平时间倒是无所谓,但高电平拉长可能让芯片认为这是命令间隙。维护代码里这种地方一律用忙等延时,或者关中断保护读写过程。

注意:如果你用 systick 中断来做延时,那么 DIO 和 CLK 的翻转之间随时可能被插入一个中断响应。中断函数只要超过 10 微秒,就可能破坏时序。建议把读写函数整体放进关中断区,或者用纯循环做 1 微秒延时。

5. 避坑实录:tm1621 调试中反复遇到的 5 个问题

5.1 问题一:屏幕显示乱码,段位完全对不上

现象:写完数字 1,屏幕上显示的是 8,或者数字“123”变成了“321”的方向反过来。

原因:这是 SEG 口的位序反了。tm1621 的地址是按 SEG0 到 SEG31 排列的,但你的屏可能把 SEG0 接到了面板的最右侧。驱动代码里的 seg_byte 数组下标按从小到大的顺序写入时,对应到屏幕上是反的。

解决:查屏规格书的引脚图,确认 SEG 编号和玻璃上的实际位置的对应关系。然后在驱动里加一层映射:定义一个数组 seg_remap_table,把逻辑 seg 编号映射到物理 seg 编号。写入缓冲区前先查表。

5.2 问题二:命令写不进去,芯片无响应

现象:初始化后屏幕全黑,示波器量 CLK 和 DIO 都有波形,CS 也有电平跳变,但芯片就是没反应。

原因:CS 的片选时序不对。tm1621 要求 CS 每次传输一个完整的下降沿和上升沿,且 CS 为低电平时才能接收命令。有些代码把 CS 一直拉低,以为省事,结果后续的每次传输芯片都识别不到边界。另一个原因是 CS 拉低之前,DIO 还没有稳定下来,芯片上电瞬间收到一个垃圾命令字。

解决:严格按“CS 拉低,延时,发命令,CS 拉高”顺序来。上电后先加 50ms 延时再执行初始化,确保芯片复位完成。

5.3 问题三:按键扫描值不稳定

现象:同一个键按下去,有时读到 0x01,有时读到 0x41,还出现过几次全 0xFF。

原因:DIO 引脚模式切换时序不对。按键扫描要先切输入再读,读完后切回输出。如果切换到输入之后没有等 2 到 3 个微秒就让 CLK 开始跳动,引脚电平还没稳定,读到的数据是随机值。切回输出之后要立即给 DIO 赋一个确定电平,否则输出状态是上一次读到的电平。

解决:在tm1621_dio_set_input()之后加tm1621_delay_us(5),在读完后先tm1621_dio_write(0)再切输出模式。

5.4 问题四:刷新一段时间后屏幕偶尔闪烁

现象:程序连续跑几小时后,屏幕每隔几十秒闪一下,时间很短,捕捉不到。

原因:这个问题的根源往往不在 tm1621,而是主循环里的其他中断把显示刷新函数卡住了。tm1621 是动态扫描驱动的芯片,刷新频率由芯片内部决定,但如果 MCU 传输数据之后没有跟上足够的刷新脉冲,LCD 的电容会慢慢漏电,表现出来就是闪烁。

解决:确认显示数据写入函数执行频率不低于 30Hz,最稳妥的做法是在主循环里每 10ms 刷新一次。如果闪烁仍然存在,检查初始化里的刷新频率设置,把命令字改成更高一档。

5.5 问题五:低功耗模式下屏幕有残影

现象:进入睡眠模式前调用tm1621_display_off(),但屏幕上的内容还会浅浅地显示一段时间,尤其是温度较高的环境。

原因:display_off只是关闭了偏压发生器,液晶分子已经极化,放电需要时间。芯片本身没有放电电路。

解决:在tm1621_display_off()之前,先向 DPRAM 全写 0,再关偏压,最后关振荡器。顺序反过来也行,但关掉偏压后 DPRAM 写不进去。这个顺序写进驱动代码里,做成一个统一的tm1621_enter_sleep_mode()函数,避免每次调用的人记错顺顺序。

6. 二次开发实战:把单屏驱动扩展成一个多屏实例的完整改法

拿到这份 tm1621 驱动源码并跑通之后,最常见的改造需求是同一颗 MCU 上挂两块屏幕。一种情况是两块屏共用一组 DIO 和 CLK,但各用各的 CS;另一种情况是两块屏完全独立,6 根线全部单独接。第二种改起来最容易,只要把 tm1621.c 里的所有静态函数都加一个结构体参数,保留一路实例就行。但第一种更省引脚,也更能看出这份驱动代码的分层好不好。

我建议把驱动改造成多实例支持的方式。先定义一个实例结构体:

typedef struct { gpio_pin_t dio; gpio_pin_t clk; gpio_pin_t cs; uint8_t display_buffer[4][8]; } tm1621_instance_t;

然后把原来直接操作全局 GPIO 的函数,全部改成接受tm1621_instance_t *指针的形式。原来用到的tm1621_dio_write(level)变成tm1621_instance_dio_write(inst, level),其中 inst 里保存了这一个实例的 GPIO 编号。platform_gpio.c 里只要写一个根据 pin 号操作电平的通用函数即可:

void tm1621_instance_dio_write(tm1621_instance_t *inst, uint8_t level) { gpio_write_pin(inst->dio, level); }

这样改完之后,初始化两块屏的代码变成:

tm1621_instance_t lcd_main; tm1621_instance_t lcd_sub; void system_display_init(void) { lcd_main.dio = GPIO_PIN_1; lcd_main.clk = GPIO_PIN_2; lcd_main.cs = GPIO_PIN_3; tm1621_instance_init(&lcd_main); lcd_sub.dio = GPIO_PIN_1; // 与主屏共用 DIO lcd_sub.clk = GPIO_PIN_2; // 共用 CLK lcd_sub.cs = GPIO_PIN_4; // 独立 CS tm1621_instance_init(&lcd_sub); }

写显示数据时分别调用tm1621_instance_set_all_display(&lcd_main, buf)和tm1621_instance_set_all_display(&lcd_sub, buf)。由于两块屏共用时钟和数据线,写一块屏的时候另一块屏的 CS 必须保持高电平,否则共享数据会被同时写进两块屏。这个细节在改造时要格外留意。

按键扫描的改造同理,把读键函数改成带实例参数的版本。两块屏如果都要做按键扫描,要注意 DIO 在同一时刻只能有一个方向,不能同时读两块屏的键值。常见做法是两块屏的按键扫描错开时间片,比如每 10ms 换一块屏扫。

从这段改造里我学到的一个习惯是:驱动芯片的代码从一开始就不要直接引用某个平台的具体 GPIO 定义,而是全部通过一个很小的适配层间接操作。那以后我每次拿一个芯片写驱动,都强制走一遍“先抽象实例结构体,再写平台适配”的流程,后面接新屏幕、换 MCU 都会轻松很多。希望这篇拆解能帮你在 tm1621 调试和二次开发上少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询