
STM8S和TM1650这个组合说实话是我在做一个小型温控面板时临时抓来的方案。手里正好有一批STM8S103F3P6最小系统板显示部分又不想用74HC595这种老古董就翻了翻TM1650的数据手册结果越用越顺手。TM1650是一颗I2C接口的LED驱动芯片一个芯片能搞定4位数码管显示还自带键盘扫描接口在8位单片机项目里属于典型的“小芯片干大事”。这篇文章就把我调通的整套控制程序、硬件连接和踩坑记录完整放出来给正在折腾STM8S点数码管的朋友做个参考。1. TM1650与STM8S的适配分析1.1 为什么选择TM1650这颗驱动芯片数码管驱动方案其实有不少常见的有三极管位选扫描、74HC595串转并、TM1650这种专用驱动芯片还有带中文字库的LCD模块。TM1650在这几类方案里的定位很特别它只需要两根I2C信号线就能同时驱动4位数码管8段x4位还能反过来扫描按键矩阵。对STM8S这种资源紧张、引脚不算富裕的8位MCU来说TM1650解决了一个非常现实的痛点如果直接用IO口扫描驱动4位数码管至少需要8个段选加4个位选也就是12个IO口。STM8S103F3P6总共才20个引脚扣除电源、复位、下载口剩下的IO口本来就不多再被数码管占掉一大半传感器、按键、通信这些功能就别想了。而用TM1650之后显示部分只占PB4和PB5两个引脚剩下的IO口全部释放出来干正事。另外一个关键原因是软件开销。LED扫描刷新是需要CPU持续介入的如果纯靠GPIO模拟扫描主循环里要一直维持刷新频率稍不留神显示就会闪烁或者亮度不均。TM1650内部自己完成动态扫描主控只需在需要更新显示内容时通过I2C写一次数据之后就不用管了。这对STM8S这种主频只有16MHz的芯片来说非常友好CPU可以腾出来做按键处理、传感器采集、通信协议解析这些更重要的活。1.2 TM1650在用户控制程序中的典型角色很多“用户控制程序”本质上就两件事显示状态、响应按键。TM1650刚好把这两件事全包了。比如我做的温控面板主循环只负责读温度传感器、跑PID算法、根据状态决定显示什么数字和符号真正把数字变成数码管上亮灯的操作全部通过TM1650的寄存器写入完成。按键部分同样走TM1650的键盘扫描接口不需要占用额外的IO口做矩阵扫描。这个套路在很多场景都适用小型仪表盘实时显示电压、电流、温度、转速配合按键切换显示页面智能家居控制面板显示设定温度、湿度、定时时间按键调整参数简易计数器/定时器显示计数值按键启动停止广告灯/跑马灯虽然广告灯常用WS2812这类彩色灯珠但如果是数码管做滚动显示或左右移位TM1650也能胜任只是把写入的段码改成移位后的数据从开发角度看这套组合还有一个隐形的优点TM1650模块在电商平台上一抓一大把不但便宜而且绝大多数模块已经把数码管、限流电阻、按键接口都集成好了到手直接飞四根线就能开始写代码非常适合快速验证想法。2. TM1650芯片工作原理拆解2.1 显示控制核心寄存器地址与数据格式TM1650的数据手册在网上一搜就有但新手第一次看可能会被里面一堆表格绕晕。我按实际用到的功能梳理了一个精简版。TM1650的I2C从机地址是固定的7位地址为0x24对应8位写地址是0x48读地址是0x49。这个地址在绝大多数TM1650模块上都是一样的不需要像某些芯片那样通过外部引脚配置地址。如果你的模块用的TM1650芯片基本可以直接按这个地址操作但还是建议瞄一眼商家给的资料有些奇葩模块会翻新片或者兼容芯片地址可能存在差异。显示部分涉及两类寄存器显示模式寄存器地址0x48控制显示开关和亮度显存寄存器地址0x68-0x6F每个地址对应一位数码管的段码显示模式寄存器在写地址0x48也就是7位地址0x24加上写标志位之后紧跟一个数据字节。这个数据字节的低3位决定亮度和开关状态常见写法是0x01表示开启显示但亮度最低0x07表示开启显示且亮度最高。中间档位按1/16、2/16这样递增实际用下来觉得0x03到0x05之间的亮度比较舒服太亮了在暗环境下会刺眼。显存寄存器的操作逻辑是先发0x68作为寄存器起始地址后面连续写入的数据会自动递增地址分别对应DIG1到DIG4的段码。这里有个要注意的坑0x68之后其实还有0x6C、0x6D两个控制扩展寄存器部分数据手册会把它们列出来但说到底一般用不上只需要关心0x68到0x6B这4个显存地址即可。段码的编码方式是标准的共阴数码管段码从bit0到bit7分别对应A、B、C、D、E、F、G、DP这8个段。比如要显示数字1B段和C段亮段码就是0x06。如果你手里的模块是共阳数码管段码需要按位取反即显示1的段码从0x06变成0xF9。我一开始就在这上面翻过车显示出来的数字全是镜像的后来用逐位测试法才确认是段码极性弄反了。2.2 键盘扫描功能到底怎么用TM1650的键盘扫描是它区别于普通LED驱动芯片的一大卖点。它支持最多7x4的按键矩阵也就是7列4行共28个按键。扫描过程全由芯片内部完成主控只需要通过I2C读取按键寄存器即可。按键寄存器的读取方式和显示寄存器不同。显示是写数据到显存按键是读数据。具体操作是主控发起读操作读取一个字节的按键状态数据这个字节中的低7位反映了当前扫描到的按键编码。每次读取得到的是一个按键在当前扫描周期内的值连续读取可能会得到不同的值因为扫描的列在变化需要主控自己做一个简单的存储比对逻辑。这里分享一个我在项目里的做法不直接拿每次读取的原始值当按键状态而是在主循环里定时读取如果连续两次读到的按键编码相同且非0就认为有按键按下再结合一个简单的防抖计数器确认。这个做法的好处是可以过滤掉接触抖动也能排掉扫描周期带来的偶发毛刺。需要特别说明的是很多市售的TM1650四位数码管模块并没有把按键矩阵的线引出来只引出了数码管的段位。这种情况下芯片内部的键盘扫描电路实际上是悬空状态你去读按键寄存器可能会得到随机数或者全0属于正常现象。真正的按键扫描功能需要额外连接按键矩阵才能用。3. 硬件连接方案与细节3.1 STM8S与TM1650的引脚连接我用的STM8S103F3P6I2C引脚默认映射在PB4SCL和PB5SDA上。TM1650模块端一般已经引出了VCC、GND、SCL、SDA四个引脚直接飞线连接即可。实际接线表STM8S引脚TM1650模块说明3.3V或5VVCC供电电源模块上有稳压或限流则可用5VGNDGND共地PB4SCLI2C时钟线PB5SDAI2C数据线这里需要提一下的是供电电压。STM8S103可以工作在3.3V或5VTM1650的工作电压范围比较宽但最舒服的工作区间是3V到5.5V。如果你的STM8S板子稳压输出是3.3V那就用3.3V给模块供电I2C通信的电平自然匹配。如果板子上直接吃5V模块也支持5V供电但要注意I2C上拉电阻到哪个电源。如果用的是STM8S105或STM8S207这类更大封装的芯片I2C引脚可能映射到其他位置比如PB4/PB5在不同封装上可能有复用和重映射务必先查对应型号的数据手册确认引脚功能不要照搬我的接线。3.2 上拉电阻、供电与电平匹配的最佳实践I2C总线是开漏结构SCL和SDA两根线必须接上拉电阻到电源才能工作。很多STM8S开发板和TM1650模块上都已经默认装有上拉电阻这种情况下不需要额外再接。但如果你用的是自己画的板子或者模块上省掉了上拉电阻就需要在SCL和SDA上各接一个4.7kΩ到10kΩ的上拉电阻。上拉电阻选多大实际上取决于通信速率和总线上挂的设备数量。TM1650的通信速率不高标准模式100kHz就够用4.7kΩ是个很稳的默认值。如果总线拉得太长超过20cm或者线上干扰比较明显可以换到2.2kΩ增强上拉能力。一个容易被忽略的坑是STM8S内部虽然也有上拉电阻可配置但内部上拉阻值通常在30kΩ到50kΩ之间驱动能力明显不够不能替代外部上拉。我一开始偷懒没接外部上拉结果I2C通信时好时坏用示波器看波形的上升沿像滑坡一样慢慢爬就是因为上拉太弱。接上4.7kΩ外部上拉之后通信立刻稳定下来。另外如果STM8S和TM1650模块分别由不同的电源供电务必共地。I2C信号是以GND为参考的地不共会导致电平判断完全错乱轻则通信乱码重则烧毁芯片。4. STM8S控制程序完整实现4.1 I2C通信基础优先选择模拟I2CSTM8S103自带硬件I2C外设理论上直接调库函数就行。但我在实际开发中更推荐模拟I2C原因有三STM8S的硬件I2C库函数在不同版本的标准外设库和SDCC编译器之间存在兼容性问题光是寄存器名字和库函数签名就能劝退一批新手模拟I2C代码逻辑简单完全可控出了问题直接用调试器单步跑就能定位模拟I2C占用的资源极少在STM8S这种主频有限的芯片上100kHz的时序用延时函数就能精准模拟下面这段是模拟I2C的基础代码在STM8S上实测稳定运行#include iostm8s103f3p6.h #define SCL_H() PB_ODR | (1 4) #define SCL_L() PB_ODR ~(1 4) #define SDA_H() PB_ODR | (1 5) #define SDA_L() PB_ODR ~(1 5) #define SDA_READ() ((PB_IDR 5) 0x01) void I2C_Delay(void) { // 延时约5usSTM8S主频16MHz for (volatile uint8_t i 0; i 20; i); } void I2C_Start(void) { SDA_H(); SCL_H(); I2C_Delay(); SDA_L(); I2C_Delay(); SCL_L(); } void I2C_Stop(void) { SCL_L(); SDA_L(); I2C_Delay(); SCL_H(); I2C_Delay(); SDA_H(); } uint8_t I2C_WriteByte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { if (dat 0x80) SDA_H(); else SDA_L(); dat 1; I2C_Delay(); SCL_H(); I2C_Delay(); SCL_L(); } // 读取从机应答 SDA_H(); I2C_Delay(); SCL_H(); I2C_Delay(); if (SDA_READ()) { SCL_L(); return 1; // 无应答 } SCL_L(); return 0; } uint8_t I2C_ReadByte(void) { uint8_t i, dat 0; for (i 0; i 8; i) { SCL_H(); I2C_Delay(); dat 1; if (SDA_READ()) dat | 0x01; SCL_L(); I2C_Delay(); } return dat; }代码里的延时时间我标了5微秒左右换算下来通信速率大约100kHz正好落在TM1650标准模式范围内。如果你的芯片主频不是16MHz或者用的不是同一个编译器比如Cosmic和SDCC的优化策略不一样这个延时值可能需要微调。4.2 显示驱动函数从段码表到完整封装有了I2C底层驱动TM1650的显示函数其实很薄。核心就是把段码写进显存寄存器。段码表我直接放在代码里const uint8_t SEG_CODE[] { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F, // 9 0x77, // A 0x7C, // b 0x39, // C 0x5E, // d 0x79, // E 0x71, // F 0x00, // 全灭 0x80, // 小数点 };TM1650写显存的操作要注意一点它的地址是自动递增的所以初始化显示时可以一次性连写4个段码。每次要刷新的数据如果不满4位也要把剩余位写成0x00避免旧数据残留。下面这个函数一次写入4个显示位void TM1650_Display(uint8_t d1, uint8_t d2, uint8_t d3, uint8_t d4) { I2C_Start(); I2C_WriteByte(0x48); // 器件地址写标志 I2C_WriteByte(0x68); // 起始寄存器地址 I2C_WriteByte(d1); I2C_WriteByte(d2); I2C_WriteByte(d3); I2C_WriteByte(d4); I2C_Stop(); }调用的时候直接传段码TM1650_Display(SEG_CODE[1], SEG_CODE[2], SEG_CODE[3], SEG_CODE[4]);显示控制寄存器单独写用来设置亮度和开关void TM1650_SetBrightness(uint8_t brightness) { if (brightness 7) brightness 7; I2C_Start(); I2C_WriteByte(0x48); I2C_WriteByte(0x01 | (brightness 1)); // 最低位1表示开显示 I2C_Stop(); }这个函数里0x01是显示使能位亮度档位从bit1到bit3所以我把传入的0-7左移一位再和0x01拼起来。例如想开到最亮调用TM1650_SetBrightness(7)最终写进寄存器的值就是0x0F。初始化时记得先调用一次TM1650_SetBrightness(4)让芯片处于显示开启状态否则就算写了段码数码管也是一片黑。4.3 按键扫描读取实现如果你手上的模块引出了按键矩阵接口读取方式如下。TM1650的按键数据通过I2C读操作获取先发送器件地址和读标志0x49然后读回一个字节uint8_t TM1650_ReadKey(void) { uint8_t key 0; I2C_Start(); I2C_WriteByte(0x49); // 器件地址读标志 key I2C_ReadByte(); I2C_Stop(); return key; }读取到的键值怎么翻译成“第几列第几行”不同的TM1650模块定义不完全一样还真没有一个通用的转换表。我的建议是拿到模块后先做个自检程序把读到的原始值打印出来然后用镊子短接每一个按键触点记录对应的编码再把这个编码表硬编码到你的程序里。按键防抖逻辑放在主循环里做核心代码uint8_t key_scan(void) { static uint8_t last_key 0, stable_cnt 0; uint8_t cur_key TM1650_ReadKey(); if (cur_key last_key) { if (cur_key ! 0 stable_cnt 3) { stable_cnt; if (stable_cnt 3) { return cur_key; } } else { stable_cnt 0; } } else { last_key cur_key; stable_cnt 0; } return 0; }这段代码的逻辑很简单连续3次读到的键值相同才认为按键有效滤掉了接触抖动和扫描间隙的毛刺。如果TM1650模块上没有接按键矩阵读回来的数据通常一直会是0或者不稳定的值多半是引脚悬空造成的别在这个上面浪费太多时间。4.4 一个完整的示例工程显示、亮度和滚动把整个程序串起来一个完整的demo长这样#include iostm8s103f3p6.h #define TM1650_ADDR_W 0x48 #define TM1650_ADDR_R 0x49 const uint8_t SEG_CODE[] { /* ... */ }; // I2C底层函数省略见4.1节 void TM1650_Init(void) { TM1650_SetBrightness(4); TM1650_Display(0x00, 0x00, 0x00, 0x00); } void TM1650_DisplayNumber(uint16_t num) { uint8_t d4 num % 10; uint8_t d3 (num / 10) % 10; uint8_t d2 (num / 100) % 10; uint8_t d1 (num / 1000) % 10; TM1650_Display(SEG_CODE[d1], SEG_CODE[d2], SEG_CODE[d3], SEG_CODE[d4]); } void main(void) { uint16_t i 0; // 时钟和外设初始化略 TM1650_Init(); while (1) { TM1650_DisplayNumber(i); i; if (i 9999) i 0; for (volatile uint32_t t 0; t 50000; t); } }这个demo每完成一次计数就刷新一次显示实际跑起来不会有闪烁因为TM1650内部自己在维持扫描主控只在数值变化时介入。如果你想做滚动显示效果思路也很简单先定义一帧数据放到数组里每次刷新时把数组整体移一位并重新写入4个显存地址就实现了类似广告灯的左移效果。滚动显示的代码示意uint8_t scroll_buf[8] {SEG_CODE[1], SEG_CODE[2], SEG_CODE[3], SEG_CODE[4], SEG_CODE[5], SEG_CODE[6], SEG_CODE[7], SEG_CODE[8]}; int pos 0; void scroll_refresh(void) { TM1650_Display(scroll_buf[pos % 8], scroll_buf[(pos1) % 8], scroll_buf[(pos2) % 8], scroll_buf[(pos3) % 8]); pos; }每调用一次scroll_refresh画面就左移一个位置放到主循环里配一个延时就能看到滚动的数字串。5. 调试实录与踩坑总结5.1 高频问题速查表我把实际使用中碰到的问题整理成了表格这些问题基本覆盖了TM1650项目里90%的调试场景现象原因解决方法数码管完全没反应显示模式寄存器没配置默认可能是关显示初始化时调用TM1650_SetBrightness(4)数码管显示乱码段码极性不对共阴共阳搞反核对段码表共阳需要按位取反I2C通信不稳定上拉电阻缺失或阻值过大在SCL/SDA上外接4.7kΩ上拉电阻个别段不亮模块虚焊或者驱动电流不够检查数码管引脚焊接确认供电电压按键无反应模块没有引按键矩阵线确认模块型号悬空读取属正常亮度不均匀电源驱动能力不足加大电源供电或者降低亮度档位写入无应答器件地址不对用I2C扫描程序找出模块实际地址5.2 我在实际调试中的经验和建议最后聊几个实操层面的心得这些是光看数据手册很难学到的。第一件事永远是验证I2C地址。不管商家资料怎么说拿到模块第一件事写个最简单的I2C扫描程序把所有地址扫一遍谁应答谁就是对的。我遇到过一批号称TM1650的模块实际上用的是兼容芯片地址跟我预期完全不同导致我愣是调了两天。第二段码极性一定要单独验证。网上找的代码和段码表不一定适配你的模块。我习惯先点亮数码管的A段和B段看看这一个数字“1”显示得对不对确认极性对了再往后写。如果显示出来的数字是镜像的或者变成其他形状大概率是共阴共阳的问题。第三I2C时序的延时很重要。整个项目跑下来唯一一次让我怀疑芯片坏掉的情况就是延时太短导致通信速率超标。TM1650虽然标称支持400kHz快速模式但STM8S用模拟I2C很难保证那么精确的时序老老实实跑100kHz稳定性远高于跑高速。第四如果显示内容出现偶发乱码先怀疑主循环里的中断跟I2C时序打架了。我最早的程序里开了一个定时器中断做按键扫描结果中断一来正好打断了I2C的时钟翻转过程导致从机收的数据错位。解决方法是写数据期间关中断或者把I2C通信代码放到临界区里执行。TM1650加上STM8S这套方案我觉得在中小型项目里非常能打。两块芯片加起来成本不到几块钱功能却覆盖了显示和按键两大刚需。等后续有空我还打算把手头的温控面板程序整理成完整工程放出来把协议解析、菜单逻辑、数据存储这些部分也补上让有需要的朋友能直接拿去改。