
简介这份资源面向嵌入式开发初学者与STM32入门工程师围绕STM32F103C8T6单片机通过TM1637芯片驱动4位数码管这一典型场景提供可直接编译运行的完整工程。压缩包共79个文件约322KB以31个c源文件与32个h头文件为主体涵盖TM1637驱动、LED与SysTick等外设模块另含8个s启动文件、hex烧录文件及Keil工程配置并附功能说明文档目录按User、Libraries、Doc、Listing等模块划分结构清晰。资源重点演示GPIO模拟时序、启动与结束信号、段码位码逐位传输及循环显示等关键环节读者可据此掌握TM1637通信协议与STM32底层驱动编写方法并在此基础上扩展亮度调节、滚动与闪烁显示等功能。目前已有6050人学习下载适合作为数码管驱动开发的实践参考。1. 从两根线点亮四位数码管说起STM32F103 驱动 TM1637 的完整落地路径如果你手头有一块 STM32F103 最小系统板又刚好翻出一块四位数码管模块大概率会遇到一个尴尬数码管是 12 个引脚STM32 的 IO 虽然够用但接线能把人逼疯。TM1637 这颗芯片解决的正是这个问题——它把段选、位选、扫描、限流全部塞进一颗 SOP16 封装里对外只留两根线CLK 和 DIO。你只需要两个普通 GPIO就能驱动四位共阳数码管还能读按键、调亮度。这套方案在工业仪表、小型计时器、温控面板里非常常见成本低、代码量小、稳定性够。本文面向的是手里有 STM32F103 和 TM1637 模块、想快速跑通并理解时序细节的开发者从协议原理到 HAL 库实现再到实际调试中那些让人抓狂的坑一步步拆开讲。2. TM1637 协议拆解为什么它只需要两根线2.1 起始条件、数据位与 ACKI2C 的影子协议TM1637 的通信协议不是标准 I2C但长得像。它没有从机地址没有寄存器地址概念所有操作靠命令字区分。通信由 START 开始STOP 结束数据在 CLK 上升沿被采样。和 I2C 最大的区别在于TM1637 的 ACK 是主动拉低 DIO 一个时钟周期而不是像 I2C 那样由从机在第 9 个时钟拉低。如果你用硬件 I2C 外设去驱动它大概率会翻车因为时序细节对不上。常见做法是用 GPIO 模拟时序这也是最稳妥的方案。先看起始和停止条件的代码实现// 起始条件CLK 高电平时DIO 从高拉低 void TM1637_Start(void) { TM1637_DIO_H(); // DIO 拉高 TM1637_CLK_H(); // CLK 拉高 delay_us(2); // 保持时间TM1637 对时序不苛刻2us 足够 TM1637_DIO_L(); // DIO 在 CLK 高时拉低形成起始 delay_us(2); } // 停止条件CLK 高电平时DIO 从低拉高 void TM1637_Stop(void) { TM1637_CLK_L(); delay_us(2); TM1637_DIO_L(); delay_us(2); TM1637_CLK_H(); // CLK 先拉高 delay_us(2); TM1637_DIO_H(); // DIO 在 CLK 高时拉高形成停止 delay_us(2); }逻辑说明起始和停止条件都依赖“CLK 为高时 DIO 跳变”这个规则。delay_us(2) 是保守值实际 TM1637 手册要求的最小脉宽是 400ns 左右但 STM32F103 在 72MHz 下 GPIO 翻转速度很快不加延时容易导致时序错乱。参数上如果你把 delay_us 改成 delay_ns 级别的空操作可能会因为编译器优化导致波形畸变建议用 volatile 变量或 __NOP() 做短延时。2.2 写一个字节LSB 优先与 ACK 等待TM1637 的数据传输是 LSB 优先也就是最低位先发。每发完一个字节主机要释放 DIO等待从机拉低表示 ACK。如果从机没拉低说明通信失败通常是接线或上拉电阻问题。// 写一个字节LSB 优先 uint8_t TM1637_WriteByte(uint8_t data) { uint8_t i; for (i 0; i 8; i) { TM1637_CLK_L(); // 拉低时钟准备数据 delay_us(2); if (data 0x01) // 先发最低位 TM1637_DIO_H(); else TM1637_DIO_L(); delay_us(2); TM1637_CLK_H(); // 上升沿从机采样 delay_us(2); data 1; // 右移准备下一位 } // ACK 阶段主机释放 DIO检测从机是否拉低 TM1637_CLK_L(); TM1637_DIO_H(); // 释放 DIO如果外部有上拉这里就是高 delay_us(2); TM1637_CLK_H(); delay_us(2); // 读 DIO 电平低表示 ACK 成功 if (TM1637_DIO_READ() 0) { TM1637_CLK_L(); return 0; // 成功 } TM1637_CLK_L(); return 1; // 失败 }逻辑说明循环 8 次每次先拉低 CLK再设置 DIO然后拉高 CLK 让从机采样。注意这里没有在 CLK 高电平期间改变 DIO符合协议要求。ACK 阶段主机必须释放 DIO否则从机拉不低。参数上data 是要发送的命令字或显示数据返回值 0 表示 ACK 正常1 表示异常。实际调试时如果一直返回 1先检查 DIO 和 CLK 是否接反再检查模块供电是否正常。2.3 命令字体系显示寄存器、地址自动递增与亮度控制TM1637 的操作靠命令字区分常用命令有三类命令类型命令字十六进制作用数据命令0x40写显示寄存器地址自动递增数据命令0x44写显示寄存器固定地址地址命令0xC0 ~ 0xC3设置起始地址为第 1~4 位显示控制0x88 ~ 0x8F开启显示亮度 0~7 级典型写显示流程是发 0x40 命令 → 发 0xC0 地址 → 连续发 4 个字节的段码 → 发 0x8F 开显示并设最大亮度。这里有个细节0x40 命令下地址会自动递增所以你只需要发一次起始地址后面连续写 4 个字节就行。如果你用 0x44 命令每次写一个字节都要重新发地址代码更繁琐一般不用。// 向 TM1637 写入 4 位显示数据 void TM1637_Display(uint8_t *seg_data) { TM1637_Start(); TM1637_WriteByte(0x40); // 数据命令自动地址递增 TM1637_Stop(); TM1637_Start(); TM1637_WriteByte(0xC0); // 起始地址第 1 位 TM1637_WriteByte(seg_data[0]); TM1637_WriteByte(seg_data[1]); TM1637_WriteByte(seg_data[2]); TM1637_WriteByte(seg_data[3]); TM1637_Stop(); TM1637_Start(); TM1637_WriteByte(0x8F); // 显示控制开显示亮度 7 TM1637_Stop(); }逻辑说明seg_data 是 4 个字节的段码数组每个字节对应一位数码管的 7 段加小数点。0x8F 中的低 3 位是亮度0 最暗7 最亮。如果你觉得太刺眼改成 0x88 到 0x8E 之间即可。注意每次 Start 和 Stop 之间是一个完整命令不能把多个命令混在一个 Start-Stop 里否则 TM1637 会解析错误。3. STM32F103 上的 GPIO 模拟与 HAL 库适配3.1 硬件连接与上拉电阻的选择TM1637 模块通常有 4 个引脚VCC、GND、DIO、CLK。VCC 接 3.3V 或 5V 都可以但 STM32F103 的 GPIO 是 3.3V 电平如果模块供电 5VDIO 和 CLK 的上拉电阻会拉到 5V长期可能损伤 IO。稳妥做法是模块供 3.3V或者加电平转换。上拉电阻方面TM1637 模块板上一般已经带了 10kΩ 上拉如果你用杜邦线连接且线长超过 20cm建议在 STM32 端再并一个 4.7kΩ 上拉否则 ACK 可能不稳定。我一般这样接线STM32F103 引脚TM1637 模块备注PA5CLK普通 GPIO 推挽输出PA6DIO开漏输出外部上拉3.3VVCC不建议用 5VGNDGND共地DIO 配置成开漏输出是关键因为 ACK 阶段需要从机拉低推挽输出会打架。CLK 可以推挽因为始终由主机驱动。3.2 GPIO 初始化与微秒延时实现用 HAL 库初始化 GPIO 很简单但要注意 DIO 的模式切换写数据时是输出读 ACK 时要切成输入。很多教程用推挽输出加读输入寄存器的方式虽然能跑但不规范。正确做法是开漏输出读的时候直接读输入寄存器因为开漏模式下输出寄存器写 1 就是释放总线。// TM1637 GPIO 初始化 void TM1637_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); // CLK推挽输出 GPIO_InitStruct.Pin GPIO_PIN_5; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // DIO开漏输出 GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // 初始状态CLK 和 DIO 都拉高 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_SET); } // 微秒级延时基于 SysTick 或 DWT void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t cycles us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) cycles); }逻辑说明DWT 是 Cortex-M3 内核自带的数据观察点单元用它做延时比 SysTick 更准也不占用中断。使用前需要先使能 DWT在 main 函数开头加CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; DWT-CYCCNT 0;。参数上SystemCoreClock 通常是 72000000所以 cycles 就是 us 乘以 72。如果你用 HAL_Delay 做微秒延时精度完全不够因为 HAL_Delay 最小单位是毫秒。3.3 段码表与显示刷新逻辑四位数码管的段码表取决于模块是共阳还是共阴。TM1637 驱动的是共阳数码管段码 0 表示亮1 表示灭。常见 0~9 的段码如下// 共阳数码管段码表0~9不带小数点 const uint8_t seg_table[] { 0xC0, // 0 0xF9, // 1 0xA4, // 2 0xB0, // 3 0x99, // 4 0x92, // 5 0x82, // 6 0xF8, // 7 0x80, // 8 0x90 // 9 }; // 显示一个整数范围 0~9999 void TM1637_ShowNumber(uint16_t num) { uint8_t seg_data[4]; seg_data[0] seg_table[num / 1000]; seg_data[1] seg_table[(num / 100) % 10]; seg_data[2] seg_table[(num / 10) % 10]; seg_data[3] seg_table[num % 10]; TM1637_Display(seg_data); }逻辑说明seg_table 里的值是按 a~g 段对应 bit0~bit6 排列的bit7 是小数点。如果你要显示小数点把对应字节和 0x7F 做与运算即可。比如显示“12.34”第三位需要加小数点seg_data[1] 0x7F;。参数上num 超过 9999 会溢出实际使用时要加范围判断。刷新频率建议不低于 50Hz否则人眼会看到闪烁但 TM1637 内部有扫描电路你只需要在数据变化时调用 TM1637_ShowNumber不需要定时刷新。4. 避坑与排查那些让数码管不亮的玄学问题4.1 现象上电后数码管全亮或全灭写数据无反应原因最常见的是 DIO 和 CLK 接反或者模块供电不足。TM1637 的工作电压是 3.3V~5.5V如果你从 STM32 的 3.3V 取电而模块板上还有 LED 限流电阻电流可能不够。另一个原因是 GPIO 初始化顺序不对先初始化了 DIO 再初始化 CLK导致起始条件不满足。解决先用万用表量 VCC 和 GND 之间电压确保在 3.3V 以上。然后交换 DIO 和 CLK 试试。如果还不行用逻辑分析仪抓波形看起始条件是否满足“CLK 高时 DIO 拉低”。没有逻辑分析仪的话把 delay_us 的参数从 2 改成 10降低时序速度很多时候能绕过时序问题。4.2 现象显示的数字缺段或错位原因段码表搞错了共阳共阴或者地址命令用错。TM1637 的地址 0xC0 对应第一位0xC1 对应第二位如果你用 0xC0 但发送顺序反了显示就会左右颠倒。另外如果你用 0x44 命令固定地址写每次都要重新发地址漏发就会导致数据写到错误位置。解决确认段码表是共阳还是共阴。共阳段码 0 亮共阴段码 1 亮。TM1637 手册明确是共阳驱动所以用本文的段码表。地址顺序上0xC0 是最左边第一位如果你发现左右颠倒把 seg_data 数组倒序发送即可。4.3 现象ACK 一直失败WriteByte 返回 1原因DIO 没有配置成开漏或者外部上拉电阻缺失。推挽输出时主机拉高 DIO 的驱动能力太强从机拉不低ACK 自然失败。另一个原因是 DIO 在 ACK 阶段没有释放代码里忘了写TM1637_DIO_H()。解决检查 GPIO 初始化DIO 必须是GPIO_MODE_OUTPUT_OD。如果模块板上没有上拉电阻在 STM32 端加一个 4.7kΩ 到 3.3V。代码里 ACK 阶段先拉低 CLK再释放 DIO然后拉高 CLK最后读 DIO 电平。顺序不能乱。4.4 现象亮度调节无效始终最亮或最暗原因显示控制命令 0x8F 中的低 3 位是亮度但如果你在发完显示数据后没有重新发显示控制命令亮度不会更新。另外有些模块的亮度调节范围不是 0~7而是 0~3具体看芯片版本。解决每次修改亮度都要重新发一次显示控制命令。如果你用 0x88 发现太暗用 0x8F 发现太亮可以试 0x8A 到 0x8C 之间的值。注意显示控制命令必须在数据写入完成后发送否则会被后续数据覆盖。4.5 现象数码管闪烁或亮度不均原因电源纹波太大或者 DIO/CLK 走线太长受到干扰。TM1637 内部扫描频率是固定的但如果电源不稳LED 亮度会波动。另外如果你在中断里频繁调用显示函数可能导致时序被打断。解决在 VCC 和 GND 之间并一个 100uF 电解电容和一个 0.1uF 陶瓷电容。DIO 和 CLK 的走线尽量短远离电机或继电器。显示函数不要在中断里调用放在主循环里用标志位触发。5. 进阶技巧用 TM1637 读按键与多模块级联TM1637 除了驱动数码管还能读按键。模块上通常有 2 个按键输入引脚对应命令字 0x42。读按键的流程是发 0x42 命令然后读一个字节bit0 和 bit1 分别对应两个按键。注意读之前要先把 DIO 切成输入模式读完后切回输出。// 读取 TM1637 按键状态 uint8_t TM1637_ReadKeys(void) { uint8_t keys 0; TM1637_Start(); TM1637_WriteByte(0x42); // 读按键命令 // 释放 DIO准备读取 TM1637_DIO_H(); for (int i 0; i 8; i) { TM1637_CLK_L(); delay_us(2); TM1637_CLK_H(); delay_us(2); keys 1; if (TM1637_DIO_READ()) keys | 0x80; } TM1637_Stop(); return keys; }逻辑说明读按键时TM1637 在 CLK 下降沿输出数据主机在上升沿采样。keys 的 bit0 和 bit1 对应按键具体哪个键看模块原理图。参数上返回值 0 表示无按键1 或 2 表示对应按键按下。注意读按键和写显示不能同时进行否则时序会冲突。多模块级联方面TM1637 没有地址引脚不能像 I2C 那样挂多个设备。如果你需要驱动 8 位数码管常见做法是用两颗 TM1637分别用两组 GPIO或者用一颗 TM1637 加一颗移位寄存器扩展。我一般会选前者代码简单互不干扰。验证方法上写一个简单的测试程序上电后显示“1234”然后每隔一秒加一同时读按键按下时在串口打印键值。如果显示正常且按键响应灵敏说明时序和硬件都没问题。从那以后我每次调 TM1637都会先跑这个测试程序确认基础通信正常后再加业务逻辑。希望帮到你。本文还有配套的精品资源点击获取