拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TM1650数码管驱动芯片:I2C接口与按键扫描实战解析

TM1650数码管驱动芯片:I2C接口与按键扫描实战解析

1. TM1650到底是个啥:一颗芯片搞定数码管和按键

先把这个芯片的定位说清楚。TM1650是一颗I2C接口的LED数码管驱动芯片,市面上很多小家电、充电桩、仪表盘、温控器里都能看到它的影子。我最早接触它是在做一款小型计时器,当时MCU的GPIO非常紧张,但又要驱动4位数码管还要扫按键,最后就选了这颗芯片。用下来最大的感受是:它把两个最占I/O的活儿——段码刷新和按键扫描——全部收编成I2C通信,主控只需要两根线就能控制4位数码管外加7个按键。

从功能框图的角度看,TM1650内部集成了显示驱动电路和键盘扫描电路两部分。显示驱动部分支持4个位选(GRID1~GRID4)和8个段选(SEGA~SEGG、DP),也就是能直接驱动4位共阴数码管,每一位都能显示包括小数点在内的8个段。键盘扫描部分提供了7个按键检测通道(K1~K7),配合SGN引脚接地,可以在不增加主控负担的情况下实现矩阵按键的扫描。

适合什么人用?如果你正在做需要1~4位数码管显示的产品,同时又要处理少量按键输入,而且主控的GPIO或者定时资源比较紧张,这颗芯片就非常合适。STM32、AVR、PIC、Arduino,甚至是ESP32这类带I2C外设的芯片都能轻松驱动它;即使是纯软件模拟I2C,也只有两根线,随便找两个GPIO就能跑起来。我甚至见过有人用一颗8脚的MCU配合TM1650做了一整个电子钟,主控只负责算时间和处理按键逻辑,显示部分全部丢给TM1650,整体方案非常精简。

还有一个容易忽略的点:TM1650的显示驱动是恒流源式的,每个段的输出电流可以通过寄存器配置成8个档位,这就意味着它不需要像传统三极管驱动方案那样精心计算限流电阻。不同亮度需求、不同尺寸的数码管,只要改寄存器值就能适配。这一点在后面讲代码的时候会细说。

要注意的是,TM1650驱动的是共阴数码管,不是共阳的。买数码管的时候别买错了,共阳管子直接接上去会完全不亮或者显示错乱。这个坑我在帮朋友排查问题时遇到过,他拿着共阳的数码管焊上去,折腾半天以为芯片坏了,其实是管子和驱动方式不匹配。

2. 通信协议与寄存器:I2C从机是怎么工作的

2.1 I2C从机地址和读写方向

TM1650作为I2C从机,7位从机地址是0x24,换算成8位写地址就是0x48,读地址是0x49。这个地址是芯片出厂固定死的,没有地址引脚,所以一条I2C总线上只能挂一颗TM1650。如果你需要同时驱动两组数码管,只能另外再想办法,比如用两颗MCU,或者换成带地址选择脚的驱动芯片。这个限制我在做双面显示面板的时候踩过,当时想挂两颗TM1650共用总线的想法直接破产。

I2C通信的时序跟标准I2C完全一致,只是芯片内部没有复杂的寄存器映射表,所有操作都是通过"命令+数据"的流式写入来完成的。这一点跟EEPROM或者传感器那种"先写寄存器地址再写数据"的模式不太一样,刚上手时容易搞混。

2.2 显示寄存器:段码数据怎么填

写入显示数据的过程是这样的:主控先发送I2C起始信号,然后发送写地址0x48,紧接着发送显示位置命令,再发送该位置对应的段码数据。显示位置命令一共有4个:0x48表示第1位数码管,0x49表示第2位,0x4A表示第3位,0x4B表示第4位。注意这里0x48同时又是写地址,容易造成误解。实际流程是:先发0x48表示"我要写数据",再发一个0x48表示"我要写第1位",然后发段码字节。换句话说,整个写入流程是:START -> 0x48(从机地址+写方向) -> 0x48(位置命令) -> 段码数据 -> STOP。

段码数据对应关系是:bit0对应SEGA,bit1对应SEGB,以此类推,bit6对应SEGG,bit7对应DP。对共阴数码管来说,对应的段亮起来需要写1。所以数字0的段码就是0x3F,也就是bit0到bit5全部置1,g段不亮,dp不亮。这里有一个很小的细节:数据手册里有些地方用低电平有效来描述段选,容易让人误解。实际用下来,TM1650对共阴数码管,段码位写1就是点亮对应的段,跟74HC595直驱的逻辑是相同的。

还有一个值得注意的点:段码数据是每写一位就发一字节,写完后芯片内部会自动锁存。也就是说,你可以在任意时刻单独更新某一位的显示内容,其他位不受影响。这在做倒计时、秒表这类需要动态更新某一位数字的场景非常方便,不需要每次刷新都把4位全部重写一遍。

2.3 控制寄存器:亮度与开关

除了显示数据,TM1650还有一个控制寄存器,地址也是0x48,用不同的命令标识来区分。控制命令格式是这样:主控发送0x48(从机地址) -> 0x48(控制命令标识) -> 控制字节。控制字节的高位控制显示开关和亮度。具体来说,bit7是显示开关,1为开显示,0为关显示;bit6到bit4是亮度档位,000到111对应8级亮度,0最暗,7最亮;低4位可以全部置0。

这里有个常见的操作误区:有人初始化的时候只发控制命令,不发显示数据,发现数码管不亮就以为芯片坏了。实际上,TM1650上电后默认是显示关闭的状态,必须先写控制字节把bit7置1,然后写段码数据才能看到内容。我在第一次调试时就因为这个顺序问题折腾了半天,一度怀疑是焊接虚接了。

亮度的调节效果非常明显,0档和7档的亮度差异在视觉上大概有三到四倍的差距。如果数码管放在强光环境下,直接拉满7档;如果是室内弱光环境,3档左右就够了,还能降低整机功耗。对于那些用电池供电的小设备来说,这个亮度调节功能也是省电的利器,白天亮、夜间暗,代码里一条分支就能实现。

2.4 键盘扫描寄存器

键盘部分稍微特别一点。TM1650的键盘扫描结果不是通过写命令获取的,而是通过I2C读操作读出。主控发送读地址0x49之后,连续读取两个字节,第一个字节的低7位对应K1到K7,bit0对应K1,bit6对应K7;第二个字节通常保留,不需要关心。

需要说明的是,这个按键检测结果是电平状态,不是边沿触发。也就是说,你读回来的数据反映的是"此刻哪些按键被按下",低电平表示按下,高电平表示未按下。所以在软件里需要自己做消抖和边沿检测,才能在"按下"和"释放"两个动作上精确响应。这个后面在代码部分会有更详细的说明。

键盘扫描功能还有一个细节:按键引脚内部没有上拉,需要在硬件上给K1~K7接上拉电阻到VCC,这样按键另一端接SGN地,按下时才能把引脚拉低。有人直接用芯片内置的上拉,结果发现按键全部失灵,就是因为漏接了这颗上拉电阻。

3. 硬件设计要点:接线不复杂,但有几个坑要避开

3.1 典型接线方案

先给一个最简单的接线案例:4位共阴数码管+1个按键接到TM1650,主控用一片STM32。TM1650的SCL和SDA分别接到主控的I2C时钟和数据引脚,VCC接3.3V或者5V,GND共地。数码管的位选引脚(通常标为 DIG1~DIG4 或者 COM1~COM4)接到TM1650的GRID1~GRID4;数码管的段选引脚(A~G、DP)对应接到SEGA~SEGG和SGP引脚。按键的一端接K1,另一端接SGN,同时在K1上接一颗10kΩ上拉电阻到VCC。

市面上有一种集成好TM1650的数码管模块,4位一体,已经画好了PCB和限流电路,使用起来更省事。不过对于量产产品,还是建议自己把芯片和数码管布局在一块板上,成本能低不少,布线自由度也更大。我之前做的一款温控器,就是直接把TM1650放在数码管正下方,走线短,显示稳定,EMC表现也比较理想。

3.2 上拉电阻和电平匹配

I2C总线的SCL和SDA都需要上拉电阻,这个是一定不能省的。阻值选多少,取决于总线长度和通信速率。短距离板内走线,4.7kΩ是安全选择;如果总线长度超过20cm,或者并联了多个I2C从设备,可以换成2.2kΩ。有人图省事直接用MCU内部上拉,结果在快速模式(400kHz)下波形边沿变缓,偶尔出现通信错误。我的习惯是:无论是I2C还是其他总线,该用外部上拉就用外部上拉,内部上拉只是应急方案。

电源电压方面,TM1650的工作电压范围大概在2.0V到5.5V之间,3.3V和5V系统都能用。如果你的主控是5V,TM1650也接5V,I2C电平都是5V,自然兼容;如果主控是3.3V,TM1650也接3.3V,同样没问题。怕的是主控3.3V而TM1650接5V,这时候SDA和SCL的电平就不匹配了,需要加电平转换电路或者用开漏配合外部上拉到3.3V的方式处理。我一般避免这种不对称供电,因为省事永远是第一位的。

3.3 电源和去耦

TM1650的峰值电流不容小觑。4位8段全亮时,每位电流按20mA算,总共需要80mA以上,这还不包括点亮瞬间的浪涌。如果主控板上3.3V的LDO本来余量就不大,加入TM1650之后很可能出现电压跌落,导致MCU复位或者数码管亮度不均匀。建议在芯片电源引脚附近放一颗10μF电解电容和一颗0.1μF陶瓷电容并联去耦,并且从电源入口单独拉一条粗一点的走线给数码管供电。

还有一个在批量生产中很关键的点:数码管的段和位之间有一个最大耐压的限制,不要超过手册给的绝对最大值。如果是12V供电的系统,数码管驱动部分需要做额外的电平转换,不能直接把TM1650悬在12V上。绝大多数情况下TM1650都是低压系统里的器件,涉及高压就要换思路了。

4. 驱动代码实现:从初始化到动态显示

4.1 初始化流程

初始化TM1650的流程非常简单,无非就是往控制寄存器写一个字节。以STM32的HAL库为例:

uint8_t tm1650_ctrl = 0x81; // 显示开,亮度1档 uint8_t set_addr = 0x48; HAL_I2C_Master_Transmit(&hi2c1, 0x48, &set_addr, 1, 100); HAL_I2C_Master_Transmit(&hi2c1, 0x48, &tm1650_ctrl, 1, 100);

第一句先发送地址字节0x48表示要操作控制寄存器,第二句发送控制字0x81表示开显示、亮度1档。注意这里两次都用了0x48作为从机地址,因为HAL库的地址参数需要包含写标志位,而0x48本身就是"0x24+写位"的结果。

如果你用的是Arduino,代码会更简洁:

#include <Wire.h> void setup() { Wire.begin(); Wire.beginTransmission(0x48); // 写地址0x48 Wire.write(0x48); // 控制命令 Wire.write(0x81); // 开显示,亮度1 Wire.endTransmission(); }

如果连初始化这一步都省了,直接写显示数据,大概率会看到数码管毫无反应。所以无论用什么平台,记得先把显示开关打开。

4.2 段码表的建立

段码表是驱动数码管绕不开的东西。我一般建一个const数组,把所有需要显示的字符的段码放进去。下面是常见的共阴数码管段码表:

字符段码(共阴)备注
00x3F全部点亮a~f
10x06b、c点亮
20x5Ba、b、d、e、g
30x4Fa、b、c、d、g
40x66b、c、f、g
50x6Da、c、d、f、g
60x7Da、c、d、e、f、g
70x07a、b、c
80x7F全部点亮
90x6Fa、b、c、d、f、g
A0x77a、b、c、e、f、g
b0x7Cc、d、e、f、g
C0x39a、d、e、f
d0x5Eb、c、d、e、g
E0x79a、d、e、f、g
F0x71a、e、f、g
全灭0x00所有段熄灭
横杠0x40g段点亮

如果你不清楚某个字符长什么样,最好的办法是照着数码管的段布局图慢慢推。我还习惯把带小数点的段码做一份,比如"0."就是0x3F | 0x80 = 0xBF,这样在显示温度小数位的时候就不用单独处理小数点标志位了。

4.3 显示一个数字的完整过程

假设要在4位数码管上显示"12.34",拆解出来的4个字符分别是'1'、'2'、'3'、'4',其中第二位后面带小数点。显示过程如下:

uint8_t display(uint8_t pos, uint8_t seg_data) { uint8_t addr = 0x48 + pos; // pos从0开始,对应0x48~0x4B HAL_I2C_Master_Transmit(&hi2c1, 0x48, &addr, 1, 100); HAL_I2C_Master_Transmit(&hi2c1, 0x48, &seg_data, 1, 100); return 0; } // 显示"12.34" display(0, code_1); // 第1位显示1 display(1, code_2 | 0x80); // 第2位显示2,带小数点 display(2, code_3); // 第3位显示3 display(3, code_4); // 第4位显示4

整个过程中,TM1650内部会自动完成位选和段选的控制,主控不需要做动态扫描。这意味着CPU的负担极轻,刷新几百次都不会有闪烁感,因为每次刷新都是直接把段码数据锁存到芯片内部的显示寄存器里,由芯片硬件驱动数码管持续点亮。

4.4 键盘读取与消抖

读键盘的代码也不复杂,只是需要连续读两个字节。以HAL库为例:

uint8_t keys[2]; HAL_I2C_Master_Receive(&hi2c1, 0x49, keys, 2, 100); uint8_t key_state = ~keys[0] & 0x7F; // 取低7位,高电平表示按下

取反是为了把逻辑统一成"bit为1表示按下",方便后续处理。如果你在硬件上已经做了取反逻辑,这里就可以省掉取反操作,具体看个人习惯。

消抖我一般放在定时器中断里做,每5ms读一次,连续读到两次相同状态才算数。边沿检测则用"上次状态"和"本次状态"比较实现,这样既能识别按下事件,也能识别释放事件。一个简单有效的实现思路:把key_state和old_state做异或,结果中为1的位就是状态发生变化的引脚;再和当前状态做与操作,就能得到"刚刚按下"的引脚掩码。

如果你不想占用定时器资源,也可以把按键扫描放在主循环里用软件延时做消抖。但对于多任务系统,我强烈建议用定时器扫描,否则很容易出现按键响应卡顿。

5. 常见问题排查:显示乱码、不亮、按键误触怎么办

5.1 数码管完全不亮

数码管完全不亮,首先检查的是控制寄存器是否写对了。很多芯片上电默认是显示关闭状态,TM1650也一样,必须把控制字节的bit7置1。如果你初始化时写的是0x01而不是0x81,那就是只开了亮度没开显示。这类问题在代码审查时最容易被忽略,因为编译器不会报错,逻辑上也说得通。

其次检查I2C地址。注意区分0x24和0x48:0x24是7位地址,0x48是带写标志的8位地址。在不同厂商的库函数接口中,两种写法都有可能出现。如果你在STM32的HAL接口里写0x24,实际发送的是0x48,从机地址会完全错位。用示波器或者逻辑分析仪看I2C波形,一抓就能确认。

最后检查硬件连线。TM1650的GRID接到数码管的位选,SEG接到段选,接反了会导致亮灯位置错乱,但不会完全不亮。完全不亮大概率是电源问题,用万用表量一下芯片VCC引脚有没有电压,排除焊接短路和虚焊。

5.2 显示乱码或者段错位

如果数码管亮了但显示的是乱码,基本可以锁定是段码表的问题。比如把共阴数码管当成共阳来编码,显示0就变成了全灭,显示1变成了除了b、c之外全亮,看起来很像是乱码。这时候对照段码表逐位验证,不要瞎猜。还有一种情况是段选引脚和数码管引脚顺序不对应。不同厂家的数码管段排列顺序可能不同,A段不一定在物理引脚1上。仔细看数码管的数据手册,一一对应好再焊板子。

动态刷新时序问题也会导致显示异常,比如在写段码数据时被中断打断,导致数据字节错位。不过TM1650是I2C接口,每次传输都有起始位和停止位保护,只要协议上完整,被中断打断的概率比 GPIO 模拟扫描的74HC595方案低得多。真的遇到传输被打断的情况,可以试试用硬件I2C而不是模拟I2C,或者把I2C中断优先级调低,避免关键传输被打断。

5.3 按键误触或者无响应

按键误触大概率是上拉电阻没接或者接得太大。TM1650的K1~K7引脚内部没有上拉,如果不外部接上拉,引脚会浮空,按键按下和释放的状态都会被噪声干扰,表现出来就是按键乱跳。我一般用10kΩ上拉,最大不要超过47kΩ,否则引脚上的电容效应会让电平跳变变慢,消抖时间得加长。

无响应的问题除了上拉电阻,还有一个容易被忽略的点:SGN引脚必须接到系统地。SGN是按键扫描的参考地,如果不接地,按键回路就不完整,自然读不到状态。有些模块把SGN引出来了,需要自己在主板上接GND,别漏了。

软件上,如果按键读回来的值总是0xFF,大概率是I2C读时序不对。读操作时,主控发完从机地址和读位后,需要连续读两个字节,第一个字节才是有效按键状态。有些参考代码只读一个字节就停止了,这在某些芯片上可以工作,但在TM1650上就会导致数据错位。

5.4 和TM1637、MAX7219怎么选

很多人会把TM1650和TM1637搞混。TM1637也是I2C接口的数码管驱动芯片,能驱动4位8段数码管,也带按键扫描,但它的I2C是"类I2C",并不是标准的I2C时序,在某些平台上模拟时序需要注意。TM1650使用的是标准I2C协议,接口更通用。功能上两家的显示驱动能力接近,日常使用差异不大,选谁主要看你手里模块的货源和价格。

MAX7219则是SPI接口,驱动位数更多(最多8位),而且支持级联,适合做多位大屏显示。但MAX7219的段电流控制是固定的,亮度调节不如TM1650灵活,价格也高不少。我的选型建议很实在:做4位以内小尺寸显示,TM1650性价比最高;需要级联驱动多位大屏,就上MAX7219;如果要完整的标准I2C同时要兼顾按键,TM1650依然是最优选。

5.5 一个关于I2C时钟速率的提醒

TM1650虽然支持到400kHz的快速模式,但我在实测中建议用100kHz标准模式跑。原因是TM1650内部没有复杂的FIFO缓冲,主控发送数据的间隔如果太短,芯片内部的锁存逻辑偶尔跟不上,会出现个别位显示刷新不及时的问题。把速率降到100kHz之后,这类问题几乎绝迹。反正显示数据本身量很小,每次只发几个字节,100kHz的带宽完全够用,没必要为了这点速度去挑战稳定性。

5.6 常见问题速查表

现象可能原因排查方法
完全不亮控制寄存器未开显示检查Ctrl字节bit7是否为1
完全不亮I2C地址错误确认0x24/0x48的区别
乱码段码表不符合共阴共阳逐段验证段码
乱码SEG顺序接错对照数码管数据手册
亮度太暗亮度档位太低尝试把亮度档提到7
亮度不均匀某位限流异常测量每位电流
按键乱跳缺少上拉电阻加上拉10kΩ
按键无响应SGN未接地将SGN接GND
按键误触发消抖不充分增加消抖时间或滤波

6. 几个实战中的小技巧

最后聊几个我在实际项目中总结的细节,这些在数据手册里很难找到,但遇到了确实能省不少时间。

第一个技巧是批量显示前先做一个"全亮自检"。在初始化之后,把所有位的段码都写成0xFF,让数码管全亮3秒钟。这一步看起来简单,但在硬件调试时特别有用:能快速确认每颗数码管的段和位是否都接触良好,也能直观看到亮度档位是否符合预期。如果全亮时某一段不亮,直接就能定位是硬件问题还是驱动问题,不用对着逻辑分析仪猜半天。

第二个技巧是关于小数点处理的。如果产品需要显示小数,而你又不想每次手动给段码加0x80,可以在封装显示函数的时候把小数点和数字分离开。比如函数原型定义为display_digit(pos, digit, int dot),内部自动完成带小数点段码的计算。这样调用层只关心业务逻辑,不用关心段码细节,代码可读性提升非常多。

第三个技巧是用亮度变化替代闪烁来做报警指示。很多设计会用数码管闪烁来提示状态,但MCU闪烁数码管要么需要定时刷新,要么需要额外控制逻辑。TM1650支持随时修改控制寄存器,可以每隔一段时间把显示开关切一次,实现闪烁效果。这种方式的好处是段码数据不需要重写,闪烁的稳定性也好,主控的负担基本可以忽略。甚至可以让不同位在不同时间点亮,做出位扫式的装饰效果,花不了多少资源。

第四个技巧是键盘扫描结果和显示数据之间的时序关联。TM1650的键盘扫描是独立的,和显示更新互不干扰,所以在设计软件架构时可以把显示刷新和按键检测分成两个独立的模块,分别驱动。如果放在同一个任务里,注意不要在一次I2C操作中间穿插另一个I2C操作,否则总线时序会乱。我给每个I2C操作加一个互斥锁或者简单的"忙标志"就能完全避免这个问题。

7. 写在最后,还是老规矩

做了这么多年嵌入式,各种显示驱动芯片也用了几十种,TM1650属于那种"朴实无华但真管用"的器件。它的技术指标不算惊艳,功能也不算花哨,但胜在把数码管和按键两个最常用的外围整合到了一颗芯片里,通信又用的是标准I2C,几乎任何主控都能轻松驱动。对于小尺寸显示产品来说,这套方案成本低、稳定、开发周期短,是我个人非常偏爱的一种组合。

如果你正在做一个需要数码管显示的小项目,我建议直接拿TM1650起步,先把点亮和显示跑通,再逐步加按键、调亮度、做动画,整个过程会非常顺畅。踩过几个坑之后你就会发现,这颗芯片其实没有那么多隐藏的雷,大部分问题都出在基础细节上——地址有没有写对、共阴还是共阳、上拉有没有接。把这些基本功打牢,TM1650能陪你走很远。

返回列表