资讯动态

STM32F103驱动TM1637数码管:两线制显示协议与GPIO模拟详解

发布时间:2026/10/5 5:21:35 来源:尧图企业网站定制
先交代一下背景最近刚好要做一个小项目需要一块能实时显示转速和计数值的屏幕翻了一圈手头的库存最后选了STM32F103最小系统板加TM1637数码管显示模块。这套组合在嵌入式圈子里非常常见发光醒目、接线简单、代码量又不大尤其适合做速度、温度、计数值这类数字型数据的直观展示。我这次把完整的驱动方法、接线细节和调试经验都整理出来给正准备用STM32F103和TM1637数码管做显示的朋友一个可以直接抄作业的参考。TM1637这个模块很多人第一反应是“Arduino用的”其实它在STM32上一样好使。它的本质是一个带键盘扫描接口的LED驱动控制芯片通过两根线CLK时钟、DIO数据就能驱动最多8位共阴数码管单片机只需要按协议把要显示的段码发过去剩下的动态扫描、刷新、限流都由芯片自己完成CPU负担极小。对STM32F103这种主频72MHz的芯片来说驱动TM1637简直绰绰有余而且用普通GPIO模拟时序即可不需要占用任何片上外设。这篇文章适合谁看一种是刚接触STM32、想做一个能真正跑起来的上手项目的初学者另一种是项目里需要显示模块但不想上OLED、LCD这类带缓存和字库的屏、只想要低成本高亮显示的开发者。我会从芯片工作原理讲到硬件接线再给出一套可直接编译运行的完整驱动最后把调试中容易踩的坑都列出来。跟着走一遍你基本能彻底搞清楚这类两线制显示驱动的套路。1. 项目整体设计与思路拆解1.1 为什么是STM32F103加TM1637先说选型逻辑。STM32F103这颗芯片在国产开发板和工业产品里出货量极大原因很简单便宜、耐造、资料全。手头这块最小系统板是STM32F103C8T672MHz主频20K RAM64K FlashGPIO支持推挽、开漏、复用等多种模式完全能满足驱动TM1637的需求。TM1637的选型则更看重它的几个特点这几点在你做方案对比时也值得记住两线制通信比直接用74HC595移位寄存器省线比并行8根数据线更是省了一大截在需要穿过转接板或者做小体积产品时非常友好。自带动态扫描逻辑。普通数码管需要单片机不断刷新每位占用CPU时间而TM1637内部有驱动电路和恒流源单片机只要把段码写入芯片内部的显示寄存器芯片就会自动扫描点亮数码管CPU完全不用管刷新。支持级联扩展。一个TM1637最多可以驱动4位或6位数码管取决于模块设计足够绝大多数数字显示场景。这一套组合在成本和效果上平衡得很好整片芯片在市场上的价格比一个普通四位共阴数码管还要便宜模块化产品也不过几块钱做出来效果又比单颗数码管好看得多。1.2 TM1637通讯协议和I2C的异同我第一次看TM1637数据手册的时候第一反应是“这和I2C差不多嘛”。确实TM1637的时序大量借鉴了I2C但它并不是标准的I2C协议这个差异直接决定了你不能用STM32的硬件I2C外设直接驱动必须用GPIO模拟。两者的区别主要在三点标准I2C有器件地址通信前要先发从机地址TM1637没有地址概念数据直接发给固定内部寄存器。I2C每传输一个字节后从机回ACK数据线拉低应答TM1637也有应答但应答发生在第9个时钟周期的下降沿而且它还不要求在停止信号前必须有NACK。I2C总线上可以挂多个器件靠地址区分TM1637是独占总线型一主一从一对一通信。因此动手之前脑子里必须有这个认知网上照搬别的I2C设备的驱动代码是行不通的必须按照TM1637自己的时序图来写。这也是为什么很多新手直接套I2C驱动、最后发现数码管毫无反应的原因。1.3 整体方案和资源占用预估本项目不占用USART、不占用定时器、不占用I2C外设只用了两个普通GPIO口我这里定义为PA1和PA2这样安排之后串口还能留着打调试信息定时器还能做测速输入捕获端口资源非常宽裕。数据刷新的时间开销也小得可以忽略不计TM1637的CLK频率一般建议控制在250kHz以下我们软件模拟时每bit延时10微秒左右一个字节8bit加上起始停止和应答总共大约120到150微秒显示四位数字两个字节也就300微秒上下对主循环而言这点时间根本感知不到。2. 硬件准备与接线细节2.1 需要的器材清单搭建这套系统需要的硬件极少具体如下器件型号/规格数量主控板STM32F103C8T6最小系统板1显示模块TM1637四位数码管模块带小数点1杜邦线母对母4根供电5V USB线给开发板供电即可1可选USB转TTL调试模块串口调试用1STM32F103C8T6的GPIO输出能力在3.3V逻辑下驱动TM1637完全没问题但模块本身为了驱动LED电源脚最好是接到5V。这是因为TM1637内部虽然有恒流驱动但模块上的数码管如果是共阴设计段电流还是从VCC经过内部寄存器流出去的供电电压太低会影响亮度一致性。如果你手上只有3.3V供电也能点亮但多位同时显示时亮度会明显不均匀尤其是百位和个位这种离电源远端的位置。2.2 具体接线对照表下面是我实际测试时使用的接线方案引脚号和模块丝印一一对应TM1637模块引脚连接到STM32F103说明VCC5V引脚或3V3见注意事项模块电源建议5VGNDGND必须共地CLKPA1时钟线开漏输出加上拉DIOPA2数据线开漏输出加上拉注意CLK和DIO可以接到任意带外部中断或普通输入功能的GPIO并不局限于PA1/PA2。我这里选PA1和PA2纯粹是这两脚在最小系统板上比较顺路方便杜邦线走线。你若想用PB口或者其他空闲引脚只要在代码里改两个宏定义即可。2.3 硬件上最容易踩的几个坑第一务必共地。这句话几乎每次都要强调但每次还是有人踩。开发板用USB供电模块用独立电源如果你不把两边的GND接在一起CLK和DIO上的高低电平参考点就不一致数据通信必然出错。表现为数码管时而乱码、时而闪烁、甚至完全没反应。第二5V还是3.3V要分清。我在测试中发现很多标注“3.3V-5V兼容”的TM1637模块其内部并没有做电平转换只是靠开漏加上拉电阻来适应不同电压。STM32F103的GPIO耐压是5V所以即使DIO被模块上拉到5V也不至于烧引脚但如果你用3.3V供电的模块配上5V上拉长时间运行有几个场景还是会有风险。稳妥方案是模块电源用5V但CLK和DIO用开漏输出内部上拉或者外部上拉到3.3V这样逻辑电平与STM32 GPIO完全一致。第三杜邦线接触不良问题比想象中高发。TM1637的CLK频率虽然不高但杜邦线插得松的时候每一次晃动都可能导致瞬间掉线芯片误判为停止信号显示随机跳变。排查的时候可以先用力按一下杜邦线两头如果显示恢复正常大概率就是接触问题。第四不要在没接限流电阻的情况下让所有段长时间全亮。模块本身设计有驱动电流一般每位数码管段电流在10mA左右可如果模块上用的是高亮共阴数码管且没有做限流处理长时间全亮会导致芯片发烫。正常显示数字没毛病但你写测试程序让所有位都显示“8.”的时候最好过几秒就停一下。3. 核心驱动逻辑与代码实现3.1 TM1637通信时序拆解TM1637的一次完整数据交互分为起始、数据传输、应答、停止四个阶段。每个阶段的时间都需要满足芯片时序要求我结合手册和实测给出下面的推荐参数阶段操作时间要求起始CLK拉高DIO从高拉低CLK再拉低建立时间约5us保持时间约5us数据写入每bit在CLK低电平期间改变DIOCLK高电平期间保持高低电平各约5到10us应答第9个CLK脉冲芯片将DIO拉低约10us停止CLK拉高DIO在CLK高电平期间从低拉高约5us数据字节都是低位在前发送这点非常关键如果你写成高位在前整个显示内容会完全不对而且在某些段码下可能表现为个别位乱码。TM1637的指令分三类都是单字节数据命令设置0x40表示写入显示寄存器地址自动加10x44表示固定地址写数据0x42表示读按键扫描。地址命令0xC0到0xC5对应显示寄存器的第0到第5地址。四位数字的话从0xC0开始连续写4个字节即可。显示控制命令0x88到0x8F。低三位是亮度等级0x88最暗0x8F最亮其中0x88到0x8B其实是占空比1/16到10/160x8C到0x8F是更高的亮度档。实际用下来0x8B到0x8F这段肉眼亮度差异不大但功耗差异明显。我的做法是显示前先发0x40数据命令再发0xC0地址命令然后连续发送N个段码字节最后发0x8F显示控制命令开显示并设置亮度。每一步之间首尾相连形成一次完整通信。3.2 段码表与显示数据处理TM1637模块上的数码管基本都是共阴结构段码对应关系是段a是最低bit段b次低bit一直到段g和段dp也就是bit7对应小数点。常用的0到9段码表如下字符段码无小数点段码带小数点00x3F0xBF10x060x8620x5B0xDB30x4F0xCF40x660xE650x6D0xED60x7D0xFD70x070x8780x7F0xFF90x6F0xEF除了数字TM1637还能显示A到F、H、L、P、U等字母做十六进制显示或者简单提示非常实用。冒号显示则需要看模块是否设计了第2位的dp段有些四位模块中间是带冒号的你只需要把对应位段码或上0x80即可。3.3 完整驱动代码实现下面给出我调试通过的完整驱动基于STM32标准外设库如果你用HAL库只需要把GPIO读写部分替换成HAL_GPIO_WritePin即可协议逻辑完全通用。先定义端口宏#define TM1637_CLK_PORT GPIOA #define TM1637_CLK_PIN GPIO_Pin_1 #define TM1637_DIO_PORT GPIOA #define TM1637_DIO_PIN GPIO_Pin_2 #define TM1637_CLK_HIGH() GPIO_SetBits(TM1637_CLK_PORT, TM1637_CLK_PIN) #define TM1637_CLK_LOW() GPIO_ResetBits(TM1637_CLK_PORT, TM1637_CLK_PIN) #define TM1637_DIO_HIGH() GPIO_SetBits(TM1637_DIO_PORT, TM1637_DIO_PIN) #define TM1637_DIO_LOW() GPIO_ResetBits(TM1637_DIO_PORT, TM1637_DIO_PIN)GPIO初始化这里有个非常关键的细节CLK和DIO都要配置为开漏输出并打开内部上拉。这样可以避免输出高低电平时的电流灌入问题也让DIO在读应答时可以直接读取引脚电平。void TM1637_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin TM1637_CLK_PIN | TM1637_DIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TM1637_CLK_HIGH(); TM1637_DIO_HIGH(); }延时函数用简单的循环即可不需要上定时器static void TM1637_Delay_us(uint32_t us) { uint32_t i; for (i 0; i us * 8; i) { __NOP(); } }这个延时是根据72MHz主频估算的8次空操作大约1微秒实际效果和编译器优化等级有点关系调试时若发现通信不稳定可以适当把系数调大。起始和停止信号static void TM1637_Start(void) { TM1637_CLK_HIGH(); TM1637_DIO_HIGH(); TM1637_Delay_us(5); TM1637_DIO_LOW(); TM1637_Delay_us(5); TM1637_CLK_LOW(); } static void TM1637_Stop(void) { TM1637_CLK_LOW(); TM1637_Delay_us(5); TM1637_DIO_LOW(); TM1637_Delay_us(5); TM1637_CLK_HIGH(); TM1637_Delay_us(5); TM1637_DIO_HIGH(); TM1637_Delay_us(5); }写一个字节并等待应答static uint8_t TM1637_WriteByte(uint8_t dat) { uint8_t i; uint8_t ack; for (i 0; i 8; i) { TM1637_CLK_LOW(); TM1637_Delay_us(2); if (dat 0x01) { TM1637_DIO_HIGH(); } else { TM1637_DIO_LOW(); } TM1637_Delay_us(2); TM1637_CLK_HIGH(); TM1637_Delay_us(2); dat 1; } TM1637_CLK_LOW(); TM1637_Delay_us(2); TM1637_DIO_HIGH(); // 释放DIO等待从机拉低应答 TM1637_Delay_us(2); TM1637_CLK_HIGH(); TM1637_Delay_us(2); ack GPIO_ReadInputDataBit(TM1637_DIO_PORT, TM1637_DIO_PIN); TM1637_Delay_us(2); TM1637_CLK_LOW(); return ack; }注意写入数据的循环里每一位数据必须在CLK为低电平时准备好DIO状态然后CLK拉高让芯片采样顺序不能反。这也是新手最容易写错的地方写反了会导致整个传输完全错位。显示控制接口void TM1637_Display(uint8_t *buf, uint8_t len, uint8_t brightness) { uint8_t i; TM1637_Start(); TM1637_WriteByte(0x40); // 数据命令写显示寄存器地址自动加1 TM1637_Stop(); TM1637_Start(); TM1637_WriteByte(0xC0); // 地址命令从0地址开始 for (i 0; i len; i) { TM1637_WriteByte(buf[i]); } TM1637_Stop(); TM1637_Start(); TM1637_WriteByte(0x88 | brightness); // 显示控制命令开显示并设置亮度 TM1637_Stop(); }主函数里的测试代码uint8_t seg_data[4]; uint16_t count 0; int main(void) { SystemInit(); TM1637_GPIO_Init(); while (1) { seg_data[0] SEG_TABLE[count / 1000]; seg_data[1] SEG_TABLE[(count / 100) % 10]; seg_data[2] SEG_TABLE[(count / 10) % 10]; seg_data[3] SEG_TABLE[count % 10]; TM1637_Display(seg_data, 4, 0x07); count; if (count 9999) { count 0; } TM1637_Delay_us(50000); } }段码表自己在头文件里定义一份数组就行把上面表格里的值填进去。测试下来从0000一直加到9999循环显示非常稳定。3.4 代码细节的几个为什么有人可能会问为什么GPIO不用推挽输出而要用开漏原因一是开漏输出时高电平靠上拉电阻实现DIO在读应答时不会因为外部拉低而和内部驱动打架二是模块上如果还有其他设备开漏更方便实现线与逻辑。哪怕你这个项目里只有一块TM1637也建议保持开漏这套代码以后复用到别的项目不需要改能避免不少隐藏问题。还有一点值得说的是延时参数。网上很多人写的TM1637延时用的是delay(5)实际上不同的STM32主频和优化等级下同样的循环次数产生的真实延时差别很大。我的经验是先把延时调大一点比如每步15微秒确认通信正常后再逐步减小找到你自己的板子能稳定工作的最小值。这样既保证了可靠性又能把刷新时间压到最短。4. 调试过程与常见问题排查实录4.1 从零开始点亮一块数码管的调试顺序很多新手一上来就把四位数全显示出了问题完全不知道从哪里查。我习惯从简单到复杂分步调第一步只显示一位。给地址0xC0发送一个0x3F如果显示“0”说明起始、停止、写字节、地址命令这条链路都是通的。如果这位都不亮优先检查电源和CLK/DIO接线。第二步设置亮度。把显示控制命令从0x88到0x8F挨个试一遍观察亮度变化。如果亮度完全没变化多半是显示控制命令没发出去或者模块的亮度调节引脚被硬件固定住了。第三步连续显示。用自动地址加1模式连续写两位确认地址自动递增功能正常然后扩展到四位。用这个顺序调通常半小时内能定位到问题。4.2 常见问题速查表现象可能原因排查方法数码管完全无显示供电没有、共地没接、CLK/DIO接反先用万用表测量模块VCC和GND之间是否有电压显示内容随机乱码段码表用错、高低位顺序不对、杜邦线接触不良显示固定值0x3F确认一位正常后再扩展亮度不均匀模块供电不足、上拉电阻阻值偏差改用5V供电并检查接线电阻时而显示时而不显示初始化和发送间隔过近、干扰造成通信错位上电后延时50ms以上再做第一次显示显示暗淡显示控制命令没有正常设置检查0x88到0x8F命令是否发送数字对但小数点错段码bit7处理错误确认module的dp段是否接在bit7还有一个特别隐蔽的坑我在实际项目中遇到过TM1637模块和主控板共用一个5V电源但电源是USB口直接供过来的当数码管全部点亮时电流突然增大USB口压降过大导致STM32F103复位。表现为接上数码管后开发板反复重启拔掉数码管就正常。解决方法是给数码管单独供电或者选择输出电流更大的USB口从根源上解决电源跌落的问题。4.3 和STM32F103其他外设协同排查的经验这里结合一下平时被问得比较多的几个场景。比如用STM32F103的串口1和串口3做调试信息输出时如果串口调试信息偶尔乱码而同时又在刷TM1637显示很可能是两个外设共用了一个中断优先级处理不当造成的。TM1637如果显示函数里正好被串口中断打断通信时序会被拉长虽然不一定会出错但如果你在中断服务函数里调用了显示函数那就很容易出问题。我的建议是显示函数不要放在中断里主循环每隔一段时间刷新即可。再比如有人喜欢用TM1637配合Modbus协议做远程显示。Modbus的帧接收通常用串口中断加定时器判断帧间隔如果显示刷新也放在主循环里完全没问题。但如果你在串口接收中断里处理完一帧后立刻调用TM1637显示就有可能导致主循环里的其他任务被延迟。正确做法是把接收到的数据存入缓冲区主循环解析之后再刷显示保持显示和通信解耦。4.4 用示波器验证时序调试嵌入式外设逻辑分析仪或者示波器几乎是必杀技。把CH1夹在CLKCH2夹在DIO观察通信波形起始信号应该是CLK高电平时DIO从高拉低停止信号是CLK高电平时DIO从低拉高。数据位则是CLK高电平期间DIO保持稳定CLK低电平期间DIO变化。如果波形里DIO的电平变化正好发生在CLK上升沿附近说明你的代码时序余量太小要么加大延时要么检查代码里CLK和DIO的操作顺序。很多网上搬运的代码在上述顺序上存在差异用示波器一眼就能看出来谁对谁错。5. 扩展玩法与应用场景提升5.1 用TM1637做时钟和秒表如果你觉得只显示数字太单调可以把TM1637模块和STM32F103的定时器结合起来做一个电子时钟。STM32F103内部的RTC或者定时器产生秒脉冲主循环里读取时间并刷新显示TM1637只需要处理显示秒表走时完全交给定时器。需要注意的是四位TM1637显示时间时分和分秒时需要处理分隔逻辑比如显示“12:30”时难点在于中间的冒号不是独立的显示单元而是第二位和第三位数字之间的小数点。具体实现时把第二位的dp段置1即可段码或上0x80。刷新的时机放在分钟变化时更新一次如果秒也在显示则每秒刷新一次也不会有什么负担。5.2 和按键组合实现参数设置TM1637还有很多模块型号自带按键扫描功能它的数据命令0x42是读按键扫描可以配合TM1637自身的键盘扫描接口实现按键输入。不过MCU直接接独立按键也很快不一定非得用芯片自带的键盘功能因为按键消抖、编码这些最终还是得自己写。我用这个方案做过一个简易的定时器两个按键分别调整设定值和启动停止数码管实时显示剩余秒数。整套系统跑下来非常稳定数码管的高亮特性在环境光很强的车间里也清晰可见这也是TM1637比LCD、OLED在很多工业场景下更实用的原因。5.3 显示数据和测速模块联动的参考方案如果你后面做小车测速或者电机转速测量STM32F103的定时器输入捕获模块采集霍尔编码器或者光电码盘脉冲主循环里换算成转速然后送往TM1637显示。这个方案性能上是完全的降维打击即使转速高达每秒几千转数码管也只需要每秒刷新20次CPU占用率不到1%。在做这类联动时记得给显示刷新留一个比较宽裕的间隔不要每几毫秒刷一次那样不仅数码管肉眼看起来没有差别还会让其他任务时不时被打断。显示刷新帧率有个20到30Hz就足够了人眼感知已经非常流畅。5.4 为什么不是OLED很多新手会问OLED显示模块也不贵为什么不直接上OLEDOLED的优势是显示内容灵活、支持汉字和图形缺点是成本略高、功耗略大、在阳光下可视性差。TM1637数码管的硬伤是不能显示复杂内容但优势恰恰是简单、抗干扰、发光效率高在工控表头、设备计数器这类场景数码管这种“土办法”反而是最可靠的选择。所以做方案选型的时候不要只看参数先问一句你到底需要这块屏幕显示什么数字多就数码管内容复杂再上OLED。6. 最后再分享两个实用技巧第一个技巧是模块上电后不要立即进行第一次显示操作。TM1637内部上电复位需要一点时间如果你在STM32F103初始化完后立刻调用显示函数芯片可能还没准备好导致第一次写入无效后续数据又全部错位。稳妥做法是在主函数开头加一个50到100毫秒的延时让模块稳定了再通信。别小看这个延时很多“时好时坏”的问题就是它引起的。第二个技巧是关于固件升级的如果你后续要芯片通过串口协议接收数据并显示建议给显示命令加一个简单的帧头校验。TM1637本身没有数据校验机制外部干扰可能让段码寄存器里的内容变成随机值你会看到数码管突然跳出个奇怪字符。虽然主循环持续刷新能自动恢复但如果刷新频率低错误可能停留好几秒。我在实际项目里是把显示刷新放在主循环的固定槽位并且每次刷新都用当前最新数据这样即使偶尔出错下一个刷新周期就会覆盖掉完全不影响观感。这套STM32F103加TM1637的小项目调试完之后给我最大的感受是数码管驱动这种活看起来简单但把时序吃透、把供电和重置时序这些细节处理好之后稳定性和可维护性会有质的提升。如果你按照这篇文章把驱动跑通后续再接触其他两线制LED驱动芯片比如类似协议的显示芯片你会发现很多套路都是通用的直接改改寄存器和命令字节就能上手。希望这篇记录能帮你少走点弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑