简介:TM1621驱动程序是一份面向嵌入式开发者的数码管控制源码,主要解决通过编程驱动TM1621芯片显示数字或字符的问题。资源可直接应用于智能家居、仪器仪表及小型嵌入式系统的显示模块开发,适合有一定单片机基础的开发者借鉴。压缩包内共2个文件,包含C源码文件与对应头文件;C文件实现了芯片初始化、数据发送、亮度控制及数字/字符显示等核心接口,头文件则声明了引脚定义、通信协议常量与函数原型,整体结构清晰,便于直接移植或二次修改。包体约2KB,代码精简。已有3123人学习下载。源码经过实际硬件测试并通过,关键代码附有中文注释,可辅助开发者快速理解初始化时序、数据帧格式和显示调用逻辑,从而缩短TM1621驱动调试与集成周期,是学习数码管驱动编写和快速上手该芯片的实用参考。
1. TM1621 驱动程序:段码屏驱动的真正复杂度
TM1621 是段码液晶驱动芯片里非常普及的一颗,很多家电面板、遥控器、仪表上的数字和图标段码都靠它点亮。表面上只要接三根线——片选、时钟、数据——就能把段码显示出来,可真轮到自己写驱动时,片选时序、命令包的读写位、显存映射和上电顺序这些细节能把一个调试经验丰富的工程师按在逻辑分析仪前磨一下午。下面这份记录想把拆 TM1621 驱动时踩过的坑、验证过可行的时序和一组能直接改来用的代码框架写清楚,适合刚接手段码屏项目的新手,也适合想快速移植到新平台、怕在初始化顺序上翻车的熟手。
2. 认识 TM1621:接口时序、命令集与显存映射关系
2.1 三段式接口:片选、时钟和数据的三方配合
TM1621 的命令和数据都走同一根 DAT 线,靠 CS 和 CLK 划定边界,所以第一次接触这个芯片的人最容易把注意力放在“命令码是多少”上,忽略了三个引脚配合的先后顺序。通信开始前,CS 必须先拉低并保持住,CLK 和 DAT 上的电平变化都只在这个低电平窗口内有效;通信结束再把 CS 拉高。只要中途任何一帧这个片选被意外打断,芯片就会把后续字节当成另一条命令,表现出来就是屏幕的段码乱跳。
每个 bit 的写入顺序也很关键。数据线在 CLK 下降沿附近改变电平,在 CLK 上升沿被锁存,所以单 bit 的驱动动作永远是“CLK 拉低 -> DAT 设值 -> CLK 拉高”,这个顺序反过来就成了 CLK 上升沿先到、数据线后变化,采样点全是毛刺。命令模式下是 9 位一个包,高 8 位是命令码,最低位是读写标志,写操作补 0;数据模式下则把地址和连续数据都塞在同一个 CS 低电平窗口内,发完命令后地址自动递增,后面跟着的每个字节都按顺序落进显存。三线接法没有独立的 WR 引脚,读写方向就靠最低这一位区分,四线接法才单独引出一根 WR 控制线,多数小项目用三线就够。
这里有个值得养成的习惯:不管哪种接法,CS 必须在第一个 bit 之前先稳定下来,至少提前一个建立时间。很多人在初始化写完屏幕没反应时反复查命令表,其实问题只是单片机复位瞬间 CS 电平没被拉死,第一个命令被吞了半个字节。
2.2 命令集速读:0xA0、0x81、0xA5 这些命令背后在做什么
TM1621 的寄存器不像通用 MCU 那样可以直接读写某个地址,所有配置都靠命令码触发。不用把整张命令表背下来,但下面这几个必须记清楚,因为上电顺序就是围绕它们搭的:
| 命令码 | 含义 | 典型使用场景 |
|---|---|---|
| 0x80 | 系统关闭,内部振荡器和 LCD 偏压电路全部停止 | 复位默认态、进入低功耗 |
| 0x81 | 系统使能,启动内部电源生成电路 | 初始化中第一个开启项 |
| 0x82 | LCD 显示关闭,只关输出不关系统 | 改显存前防止闪屏 |
| 0x83 | LCD 显示开启,把显存内容映射到 COM/SEG 输出 | 初始化流程最后一步 |
| 0xA4 / 0xA5 | 偏压设置,1/2 偏压或 1/3 偏压 | 根据液晶面板电容与电压选 |
| 0xA0 | 写显存命令,后面跟地址再加数据 | 每次刷新显示内容 |
| 0xB0 附近 | 占空比设置,对应 1/2、1/3、1/4 周期 | 4 COM 屏固定用 1/4 占空比 |
命令背后的设计逻辑其实是分层的:0x80 和 0x81 管内部电路是否工作,0x82 和 0x83 管显示输出是否使能,0xA4/0xA5 和占空比管 COM/SEG 驱动波形的电压分配。偏压选 1/3 比 1/2 在低电压面板上更常见,选错不会让芯片冒烟,但显示对比度会差一大截,段码淡得像水印。占空比则必须和屏的 COM 数对齐,4 COM 的屏就配 1/4 占空比,配成 1/2 会看到重影叠影,这是新手最容易当成“芯片坏了”的坑。
顺序上,我一般先发 0x80 让系统进入默认关闭态,接着设偏压和占空比,再发 0x82 关显示,清完显存后发 0x81 使能系统,最后才发 0x83 打开显示。0x83 一定放在最后,因为显示输出需要内部电源已经稳定,先开显示后开系统的后果就是上电瞬间那几百毫秒里液晶被加上不完整的驱动波形,对比度忽高忽低。
2.3 显存映射:128 位是怎么排布成亮与灭的
TM1621 内部显存一共 128 位,对应 4 个 COM 和 32 个 SEG 的交叉点,16 个字节刚好放下。地址从 0x00 连续排到 0x0F,写显存时先发 0xA0 命令,再发起始地址,之后连续写入的字节会按地址自动递增填进去,CS 拉高结束。128 位显存里每一位都直接控制一个显示点,写 1 那段就亮,写 0 那段就灭,没有灰度也没有属性位,亮灭关系是一一对应的。
具体每一位对应哪个 COM 和哪个 SEG,取决于芯片内部硬连线和屏幕厂家的走线,软件在初始化阶段改不动。同系列芯片常见做法是一个地址的 8 个 bit 对应同一 COM 下的连续 8 个 SEG,不同 COM 则落在相邻地址区域,但不同封装里这个分布可能差一个 bit 位。所以真正落地时不要凭记忆画映射表,直接拿屏厂给的规格书对照地址与段码编号,屏幕上标着“第 3 段”不代表 RAM 里就是 bit3,真有厂商把段号和位号错开接。调试时如果出现数字显示对但位置错,先查映射表而不是改代码,这一步错了后面所有刷新逻辑都会连锁翻车。
还有一点:显存内容在上电瞬间是不确定的,芯片不会帮你清零。所以初始化序列里必须先向 0x00 到 0x0F 写一遍全 0,否则一开显示屏幕就是花屏乱码,那种乱码每个项目长得还不一样,看起来特别像时序问题,实际上就是显存垃圾数据被直接映射到了段码上。
3. 手写驱动程序:从底层 GPIO 到显存刷新
3.1 硬件抽象:先约定引脚再写逻辑
驱动代码不复杂,但移植成本高低全看引脚抽象怎么设计。如果把 GPIO 操作直接写死在每个函数里,换个板子就要满文件找引脚改,所以我习惯先剥一层宏,驱动逻辑里只出现 CS、CLK、DAT,不出现具体寄存器地址。下面这个头文件是整套驱动的地基:
/* tm1621.h - TM1621 段码屏驱动头文件 */ #ifndef TM1621_H #define TM1621_H #include <stdint.h> /* 引脚电平宏,移植到新平台时只改这四个宏 */ #define TM1621_CS_LOW() gpio_write(CS_PIN, 0) #define TM1621_CS_HIGH() gpio_write(CS_PIN, 1) #define TM1621_CLK_LOW() gpio_write(CLK_PIN, 0) #define TM1621_CLK_HIGH() gpio_write(CLK_PIN, 1) #define TM1621_DAT_LOW() gpio_write(DAT_PIN, 0) #define TM1621_DAT_HIGH() gpio_write(DAT_PIN, 1) /* 常用命令码 */ #define TM1621_SYS_DIS 0x80 #define TM1621_SYS_EN 0x81 #define TM1621_LCD_OFF 0x82 #define TM1621_LCD_ON 0x83 #define TM1621_BIAS_1_3 0xA5 #define TM1621_DUTY 0xB2 #define TM1621_WRITE_RAM 0xA0 #define TM1621_RAM_BYTES 16 void tm1621_init(void); void tm1621_write_buffer(const uint8_t *buf, uint8_t len); #endif这里的 gpio_write 在移植时需要映射成实际单片机平台的驱动函数,CS_PIN、CLK_PIN、DAT_PIN 同理。不要把引脚写死在宏展开里,因为段码屏项目经常遇到引脚复用冲突,到最后留给 CS 的不一定是默认引脚,宏抽象能让你在改板时只动这一处。DAT 引脚还需要在需要读回显存时切换输入方向,这个在头文件里留一个方向切换宏的注释位就行,暂时用不到读功能可以不写。
3.2 命令发送的核心原语:9 位时序怎么写才稳
命令发送的核心是一个按位搬运的函数。注意这里没有把 CS 拉低写进去,片选由上层函数控制,因为写显存时要连续发命令、地址、数据一整段,片选必须保持连贯,不能在中间被命令发送函数重新拉低。
/* 发送 bits 位数据,MSB 先行,调用前 CS 必须已经拉低 */ static void tm1621_send_bits(uint16_t data, uint8_t bits) { uint8_t i; uint16_t mask = 1u << (bits - 1); for (i = 0; i < bits; i++) { TM1621_CLK_LOW(); /* 时钟低电平,数据允许变化 */ if (data & mask) { TM1621_DAT_HIGH(); } else { TM1621_DAT_LOW(); } TM1621_CLK_HIGH(); /* 时钟上升沿,数据被锁存 */ delay_us(1); /* 高电平保持时间,按主频调整 */ mask >>= 1; } } /* 发送 9 位命令包:8 位命令 + 1 位写标志 */ static void tm1621_send_cmd(uint8_t cmd) { TM1621_CS_LOW(); tm1621_send_bits((uint16_t)cmd << 1, 9); TM1621_CS_HIGH(); }这段代码里最容易被看走眼的是(uint16_t)cmd << 1。TM1621 命令包是 9 位,高位是 8 位命令码,最低位是写标志。把命令码左移一位,最低位自动补 0,正好就是“写”标志位。mask初始值取0x100,从第 9 位开始逐位下移,这样命令码的最高位最先出现在 DAT 线上。delay_us(1) 是时钟高电平保持的固定延时,如果平台主频很高或者 IO 翻转很快,建议改成高电平时长 2us、低电平时长 2us,宁可慢一点也别省略,太快的时钟会让示波器上看起来全是毛刺。
3.3 初始化序列:上电后把寄存器推进到正确状态
初始化函数按之前讲的顺序执行:关闭系统、设置偏压和占空比、关闭显示、清显存、使能系统、打开显示。清显存如果只写 RAM 副本还不够,必须把整个缓冲区的 16 个字节实际写进芯片。
/* 内部显存副本,用于刷新比较 */ static uint8_t ram_buf[TM1621_RAM_BYTES]; /* 写显存:CS 保持低,命令 + 地址 + 连续数据 */ void tm1621_write_buffer(const uint8_t *buf, uint8_t len) { uint8_t i; TM1621_CS_LOW(); tm1621_send_bits((uint16_t)TM1621_WRITE_RAM << 1, 9); /* 0xA0 写命令 */ tm1621_send_bits(0x00, 8); /* 起始地址 0x00 */ for (i = 0; i < len; i++) { tm1621_send_bits(buf[i], 8); } TM1621_CS_HIGH(); } void tm1621_init(void) { tm1621_send_cmd(TM1621_SYS_DIS); tm1621_send_cmd(0xA5); /* 1/3 偏压,按屏选 */ tm1621_send_cmd(TM1621_DUTY); /* 1/4 占空比,固定 4 COM */ tm1621_send_cmd(TM1621_LCD_OFF); memset(ram_buf, 0, TM1621_RAM_BYTES); tm1621_write_buffer(ram_buf, TM1621_RAM_BYTES); tm1621_send_cmd(TM1621_SYS_EN); tm1621_send_cmd(TM1621_LCD_ON); }注意:写缓冲区必须带起始地址 0x00,而且地址是 8 位,不是 6 位。TM1621 地址范围只有 64 字节,写 0x00 之后地址自动递增,16 个字节写完刚好到 0x0F。如果在循环前先调用一次 send_cmd(0xA0),那 CS 会被拉高再拉低一次,整个数据包就被拆成两段,显存内容会错位成垃圾。缓冲区里的ram_buf是软件侧的映象,初始化时写 0,之后所有显示操作先改这个副本再整帧刷进芯片,这样任何时候都能知道屏幕上当前是什么内容。
3.4 刷新函数:把缓冲区变成看得见的数字
显示内容变化时不需要重走完整初始化流程,只需把更新后的 16 字节显存数据重新送入芯片。刷新函数和写缓冲函数其实是同一个底层,所以我只暴露一个入口,内部统一走 0xA0 写显存路径:
void tm1621_refresh(const uint8_t *buf, uint8_t len) { tm1621_write_buffer(buf, len); memcpy(ram_buf, buf, len); }这个函数是否每 20ms 刷一次,还是只在数据变化时刷一次,取决于项目对刷新率和低功耗的要求。段码屏没有扫描刷新的概念,显存内容写了就保持,所以理论上不需要周期刷新。但工业现场电磁干扰强的场合,显存内容偶尔会被噪声打翻,我习惯加一个 20ms 到 50ms 的整帧周期刷新兜底,代价只是每帧 16 字节的数据量,对主控几乎无感。memcpy 同步副本这步不能省,否则后续做增量刷新时软件侧不知道芯片里当前是什么,只能每次全量写。
4. 驱动调试:逻辑分析仪从黑匣子里挖出信号真相
4.1 先抓时序再改代码:一帧波形能看出九成问题
段码屏驱动出错时,最忌讳的就是对着代码猜。TM1621 的时序对逻辑分析仪而言非常友好,三根线全是低速数字信号,抓取门槛很低。把 CS、CLK、DAT 三路分别接到通道上,采样率设成 8MHz 左右,触发放到 CS 下降沿,然后执行一次初始化,就能看到完整的一帧命令。重点核对三件事:第一,CS 低电平窗口内是否完整包含了每一个 bit,有没有中途抬起;第二,CLK 上升沿时刻 DAT 线是否已经稳定,如果采样点刚好落在 DAT 跳变沿上,大概率是延时不够;第三,命令包的 9 位结构是否完整,特别是最低位写标志有没有和下一段的地址字节粘连。
抓到波形后先从第一帧命令开始数,用光标量出 9 个 CLK 周期,比对命令码从最高位开始是否和发送值一致。这一步能过滤掉一大半方向性问题:命令发错、顺序颠倒、片选断裂,全都能在波形上直接看出来,根本不用改代码反复试。很多被当成“芯片体质不行”的怪问题,最后都是这里发现 CLK 拉高后只保持了 500ns,芯片来不及锁存。
读回方向上的检查要麻烦一点,因为 DAT 在命令阶段是输出,在数据阶段要切输入。抓波形时如果在读命令的最后几位看到 DAT 高阻态被下拉电阻拉低,那是正常的;看到 DAT 始终被推高或拉低不动,检查方向切换时机是否比命令结束早了或晚了半拍。这类问题只有逻辑分析仪能一眼定案,调了半天代码发现是 IO 方向脚配错的情况很常见。
4.2 排查矩阵:屏幕不亮先查这六项
| 现象 | 可能原因 | 先查什么 |
|---|---|---|
| 全屏不亮,无任何段码 | SYS_EN 或 LCD_ON 未发出 | 初始化最后两步是否执行 |
| 上电乱码,图形无规律 | 显存未清零 | 0x00 到 0x0F 是否写了全 0 |
| 段码显示位置错乱 | 显存映射或 SEG/COM 接线不符 | 对照规格书核对 RAM 位号 |
| 对比度很淡,像水印 | 偏压选错或 VDD 偏低 | 1/2 还是 1/3 偏压匹配屏 |
| 出现重影、叠影 | 占空比设置与 COM 数不匹配 | 4 COM 是否用了 1/4 占空比 |
| 数字显示正常,偶尔花一帧 | CLK 高电平太短或刷新被打断 | 抓波形检查 CLK 占空比 |
这张表基本覆盖了我见过的九成段码屏驱动问题。前两条是新手区,后四条是移植老手也容易翻车的区域。尤其最后一条“偶尔花一帧”最难定位,因为它可能几天才出现一次,现象又不像命令错误那样稳定,最后往往要靠长时逻辑分析仪录制才能抓到刷新时刻的时序毛刺。
4.3 关键参数速查:时钟、延时、上下拉建议
调试时最常问的一组参数其实是时序边界。TM1621 不像 SPI Flash 有那种大余量,时钟高低电平太短会直接丢 bit,但手册给的极限参数往往贴着最小值,工程上不适合卡在边界跑。下面这组值是项目里实际验证过偏保守的设定,稳定优先,不追求极限。
| 参数 | 建议值 | 说明 |
|---|---|---|
| CLK 高电平时间 | 1 到 2us | 太短会导致锁存不稳 |
| CLK 低电平时间 | 1 到 2us | 给 DAT 留足变化窗口 |
| DAT 建立时间 | 0.5us 以上 | 相对 CLK 上升沿提前稳定 |
| CS 提前建立时间 | 1us 以上 | 第一个 CLK 到来前 CS 必须为低 |
| DAT 线上拉 | 10k 到 100k | 防止空闲时 DAT 浮空误触发 |
| 写周期 | 5us 以上 | 从 CS 拉低到拉高的最小时间 |
还有个细节容易被忽略:DAT 线上拉不是必须的,但建议加。三线模式下 DAT 是半双工,输出阶段结束后引脚会回到输入态,如果线上没有弱上拉,读回时高电平会读成不确定值。加一个 100k 上拉不影响写时序,却能省掉读回时电平漂移的坑。至于下拉,一般不需要,因为 TM1621 的命令和地址都是高电平有效的 bit,低电平噪声在空闲态不会误触发。
5. 避坑实例:五条踩过就忘不掉的记录
5.1 坑一:命令包片选被打断,显示顺序随机跳
现象:初始化后屏幕能亮,但显示的数字和写入内容对不上,而且每次上电错的段码都不同。换一片芯片也一样,排除了芯片体质问题。
原因:逻辑分析仪抓波形发现,0xA0 写显存命令只完成了 9 位,CS 就抬高了 1us,然后又拉低发地址。问题出在写缓冲函数里用了两次独立的命令发送封装,第一次发完命令后函数把 CS 拉高,第二次再拉低发地址,中间这段空隙被芯片理解为一次总线释放,后面的地址和数据全乱套。
解决:重写底层,把“命令 + 地址 + 数据”放进同一个 CS 低电平窗口内,所有位序列由单一函数管理。从那以后我的驱动里再没有“发完命令自动拉高 CS”的封装,所有多字节序列都是显式控制片选,一次拉低到底。
5.2 坑二:上电后先开系统还是先开显示,顺序错了对比度像水印
现象:屏幕能出内容,但对比度明显偏淡,把偏压从 1/3 调到 1/2 也没有改善,屏在强光下几乎看不清。
原因:初始化序列被写成 0x83 LCD_ON 在前、0x81 SYS_EN 在后。芯片的显示输出依赖内部偏压电路产生 COM 驱动波形,系统还没使能就开显示,相当于让显示输出电路空跑,偏压没有建立,液晶得到的驱动电压不足。再调偏压命令也没用,因为偏压电路本身没启动。
解决:调整顺序为 0x80 -> 偏压 -> 占空比 -> 0x82 -> 清显存 -> 0x81 -> 0x83,0x81 必须早于 0x83,但可以晚于偏压设置。改完对比度立刻正常。这个坑的隐蔽之处在于“系统使能”和“显示使能”是两个独立寄存器位,先开显示不会报错,只是输出波形不对。
5.3 坑三:显存不清零,开屏全是乱码
现象:首次上电屏幕显示一片乱码,段码毫无规律;手动复位后乱码消失,显示正常。多次上电复现概率不固定,有些板子稳定复现,有些板子偶尔出现。
原因:TM1621 的 RAM 在上电时不保证为全 0,是随机状态。每次复位如果芯片供电没完全放干净,RAM 内容就会保留部分上次状态,下次上电直接映射成乱码,看起来像时序问题,实际是初始化序列里少了清显存步骤。
解决:初始化里先 memset 软件缓冲区,再调用写显存函数把 16 字节全 0 写入 0x00 到 0x0F,之后才开显示。注意软件缓冲区清零不等于芯片显存清零,必须实际写一轮,否则芯片内部 RAM 仍保留着随机值。
5.4 坑四:刷新函数被中断抢占,数字显示花屏
现象:显示内容和串口通信同时工作时,偶尔出现某个数字的段码瞬时错乱,持续几十毫秒又恢复正常。单独测试显示功能则完全正常,加了中断后才出现。
原因:刷新函数执行到一半时被高优先级中断打断,CS 保持低电平的时间被拉长到几十微秒。TM1621 的串行协议对片选低电平时间没有硬性上限,但中断里如果恰好操作了同一条 DAT 线的 GPIO,数据位就会被改写,芯片锁存到的 bit 和发送端不一致。
解决:刷新期间临时关中断,或者把刷新函数放进临界区保护,保证一帧 16 字节的写入不被任何中断打断。中断里也绝对不要碰 CS、CLK、DAT 三根引脚,哪怕只是读一个电平,都可能破坏时序。这个坑在逻辑分析仪上很难一次性抓到,因为一次花屏持续时间太短,触发了中断抢占才能重现。
5.5 坑五:占空比配错,重影叠影看不清
现象:屏幕能显示内容,但数字边缘有淡淡的重复残影,像是每个段码旁边多了一个半透明的副本。调节偏压无效,换屏也无效。
原因:屏厂给出的模组是 4 COM 结构,驱动配置里却用了 1/2 占空比。COM 扫描周期和驱动波形不匹配,非选通帧的电压没有完全消除,液晶分子在残压下来回微动,产生重影。TM1621 的占空比命令要根据屏的实际 COM 数设置,不是选一个“看起来通用”的值。
解决:改成 1/4 占空比配置,重影消失。这个坑常见的变体是:屏是 3 COM 却配了 1/4 占空比,一样会出现一个多余的残段。凡是出现“每条线旁边多一条线”,先怀疑占空比和 COM 数不匹配,而不是怀疑偏压。偏压错了是整个屏幕一起淡,占空比错了是图形边缘出现叠影,这两者肉眼可以区分。
6. 进阶技巧:让段码屏刷新更稳的一组代码习惯
把基础驱动跑通之后,有一组习惯能让整体稳定性上一个台阶,尤其适合产品化阶段的调试。第一是显存刷新节拍化。不要每次改一个数字就立刻整帧写入,而是在定时器里固定 20ms 周期做一次刷新,刷新前比较软件副本和要写入的内容,只有变化了才真正调用写显存函数。段码屏项目里显示内容通常变化不频繁,20ms 的刷新节拍人眼看不出延迟,却能把刷新函数和主循环里的其他时序完全解耦。
第二是初始化加看门狗重试。段码屏驱动不太可能每次都失败,但上电瞬间电源毛刺偶尔会让初始化序列丢一帧命令。我会在初始化完成后读回一个已知地址的数据,校验失败就重试整个初始化流程三次,三次失败才上报错误。这个“读回校验 + 重试”的组合能把现场偶发的不亮屏问题从“玄学”变成“基本不发生”,代价只是初始化时间多花几毫秒。
第三是低功耗唤醒恢复。进入休眠时不要直接关闭 SYS_EN,只发 0x82 LCD_OFF 关显示,并把时钟停下,RAM 内容保留。唤醒时只需要重新使能系统、重新打开显示,显存数据还在,不用重新整帧刷新。这个习惯在电池供电产品里很关键,LCD_OFF 到 LCD_ON 的恢复时间远小于完全初始化到重新显示的时间,用户感觉不到闪烁。
从那以后我每次调段码屏驱动都强制走一遍固定流程:先拿逻辑分析仪抓一帧波形确认时序和命令结构,再按“初始化顺序 -> 显存清零 -> 偏压占空比 -> 刷新被抢占”这四个方向逐一排查,最后再考虑参数微调。这个顺序帮我少走了很多弯路,遇到屏幕不亮时不再拆芯片换屏,而是先问一句:你的初始化顺序和我这份笔记里的顺序一致吗?希望帮到你。
本文还有配套的精品资源,点击获取