告别点灯!用STM8和TM1628驱动4位数码管制作一个简易计数器(附工程源码)

发布时间:2026/6/7 3:17:33
告别点灯!用STM8和TM1628驱动4位数码管制作一个简易计数器(附工程源码)
从零构建STM8计数器TM1628驱动设计与模块化实践在嵌入式开发领域显示驱动往往是项目中最基础却最容易出错的环节之一。当我们需要在STM8这样的资源受限单片机上实现稳定可靠的数码管显示时TM1628这类专用驱动芯片就成了性价比极高的选择。本文将带您从零开始构建一个完整的4位数码管计数器项目不仅实现基础显示功能更着重探讨如何编写可复用的驱动模块以及如何将显示逻辑与业务逻辑优雅分离。1. 硬件架构设计1.1 核心器件选型本项目采用STM8S105系列作为主控芯片搭配TM1628作为显示驱动。这种组合在成本敏感型应用中表现出色STM8S105S48位MCU16MHz主频8KB Flash1KB RAM丰富的GPIO和外设TM1628LED驱动控制专用电路支持最大10段×8位显示内置键扫功能四位共阴数码管显示界面每个数码管包含7段LED和1个小数点硬件连接示意图如下STM8引脚TM1628引脚功能说明PE5STB片选/使能信号PC2CLK时钟信号PC3DIO双向数据线-VDD3.3V-5V电源-GND共同地线1.2 电路设计要点实际布线时需注意在STM8与TM1628之间串联100Ω电阻保护IO口VDD引脚就近放置0.1μF去耦电容数码管公共端需根据电流大小设计合适的三极管驱动电路保留SWIM接口用于程序下载调试提示使用万用表确认数码管引脚定义不同厂家产品段码顺序可能不同2. 底层驱动实现2.1 通信协议解析TM1628采用类SPI的三线通信协议但有以下特殊之处数据有效性在CLK上升沿采样数据命令结构包含显示模式设置、地址模式选择等时序要求STB下降沿标志传输开始上升沿标志结束典型命令帧格式// 显示模式设置命令 #define CMD_DISPLAY_MODE 0x03 // 7段10位显示模式 // 数据写入命令 #define CMD_DATA_WRITE 0x44 // 固定地址写入模式 // 亮度控制命令 #define CMD_BRIGHTNESS 0x8F // 最大亮度2.2 驱动函数封装我们采用分层设计思想将底层通信封装为独立模块// tm1628_driver.h #ifndef TM1628_DRIVER_H #define TM1628_DRIVER_H #include stdint.h void TM1628_Init(void); void TM1628_SendCommand(uint8_t cmd); void TM1628_WriteData(uint8_t addr, uint8_t data); void TM1628_SetBrightness(uint8_t level); #endif对应实现文件包含详细的时序控制// tm1628_driver.c #include tm1628_driver.h #include gpio.h // 硬件抽象层 static void delay_us(uint16_t us) { while(us--) { __asm__(nop); } } void TM1628_SendByte(uint8_t byte) { for(uint8_t i0; i8; i) { GPIO_CLK_LOW(); delay_us(2); if(byte 0x01) { GPIO_DIO_HIGH(); } else { GPIO_DIO_LOW(); } delay_us(2); GPIO_CLK_HIGH(); delay_us(4); byte 1; } } void TM1628_WriteData(uint8_t addr, uint8_t data) { GPIO_STB_LOW(); TM1628_SendByte(addr); TM1628_SendByte(data); GPIO_STB_HIGH(); }3. 显示逻辑抽象3.1 数码管编码转换建立显示内容与段码的映射关系const uint8_t SEGMENT_MAP[] { // 0-9的段码共阴数码管 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F // 9 }; const uint8_t POSITION_ADDR[] { 0xC0, // 第一位 0xC2, // 第二位 0xC4, // 第三位 0xC6 // 第四位 };3.2 显示缓冲区设计采用双缓冲机制避免显示闪烁typedef struct { uint8_t current[4]; uint8_t pending[4]; bool update_flag; } DisplayBuffer; void Display_UpdateNumber(uint16_t num) { display.pending[0] SEGMENT_MAP[num % 10]; display.pending[1] SEGMENT_MAP[(num/10) % 10]; display.pending[2] SEGMENT_MAP[(num/100) % 10]; display.pending[3] SEGMENT_MAP[num/1000]; display.update_flag true; } void Display_Refresh(void) { if(display.update_flag) { for(uint8_t i0; i4; i) { TM1628_WriteData(POSITION_ADDR[i], display.current[i]); } memcpy(display.current, display.pending, 4); display.update_flag false; } }4. 计数器功能实现4.1 主程序架构采用状态机模式组织程序逻辑// main.c #include system.h #include tm1628_driver.h #include display.h typedef enum { COUNTER_IDLE, COUNTER_RUNNING, COUNTER_PAUSED, COUNTER_RESET } CounterState; int main(void) { System_Init(); TM1628_Init(); CounterState state COUNTER_IDLE; uint16_t count 0; uint32_t last_tick 0; while(1) { uint32_t current_tick System_GetTick(); switch(state) { case COUNTER_IDLE: if(Button_Pressed(START_BUTTON)) { state COUNTER_RUNNING; } break; case COUNTER_RUNNING: if(current_tick - last_tick 1000) { count; Display_UpdateNumber(count); last_tick current_tick; } if(Button_Pressed(PAUSE_BUTTON)) { state COUNTER_PAUSED; } break; // 其他状态处理... } Display_Refresh(); System_Sleep(10); } }4.2 按键扫描集成TM1628内置键扫功能可通过以下方式读取uint8_t TM1628_ReadKeys(void) { uint8_t keys 0; GPIO_STB_LOW(); TM1628_SendByte(0x42); // 读键扫数据命令 GPIO_DIO_INPUT(); for(uint8_t i0; i8; i) { GPIO_CLK_HIGH(); delay_us(2); if(GPIO_DIO_READ()) { keys | (1 i); } GPIO_CLK_LOW(); delay_us(2); } GPIO_STB_HIGH(); GPIO_DIO_OUTPUT(); return keys; }5. 工程优化技巧5.1 低功耗设计通过合理配置TM1628工作模式降低功耗在无显示更新时关闭显示void Display_Sleep(void) { TM1628_SendCommand(0x80); // 关闭显示 }使用自动亮度调节void Display_AutoBrightness(uint8_t ambient_light) { uint8_t level ambient_light / 32; // 0-7 TM1628_SendCommand(0x88 | (level 0x07)); }5.2 防闪烁处理采用分时刷新策略void Display_Refresh_Partial(void) { static uint8_t pos 0; TM1628_WriteData(POSITION_ADDR[pos], display.current[pos]); pos (pos 1) % 4; }5.3 模块测试方案编写测试用例验证驱动稳定性void Test_DisplayAllSegments(void) { for(uint8_t i0; i4; i) { TM1628_WriteData(POSITION_ADDR[i], 0xFF); // 全段点亮 Delay_ms(500); TM1628_WriteData(POSITION_ADDR[i], 0x00); } } void Test_Counter(void) { for(uint16_t i0; i9999; i) { Display_UpdateNumber(i); Display_Refresh(); Delay_ms(50); } }6. 进阶扩展方向6.1 多级菜单实现基于状态机设计简单菜单系统typedef enum { MENU_MAIN, MENU_SET_TIME, MENU_SET_ALARM, MENU_SETTINGS } MenuState; void Menu_Handler(uint8_t key_event) { static MenuState state MENU_MAIN; switch(state) { case MENU_MAIN: if(key_event KEY_UP) { Display_Show(TIME); state MENU_SET_TIME; } // 其他处理... break; // 其他状态... } }6.2 数据持久化存储利用STM8的EEPROM保存配置#define EEPROM_COUNT_ADDR 0x4000 void Save_Count(uint16_t count) { FLASH_Unlock(FLASH_MEMTYPE_DATA); FLASH_ProgramByte(EEPROM_COUNT_ADDR, count 0xFF); FLASH_ProgramByte(EEPROM_COUNT_ADDR1, (count 8) 0xFF); FLASH_Lock(FLASH_MEMTYPE_DATA); } uint16_t Load_Count(void) { uint16_t count FLASH_ReadByte(EEPROM_COUNT_ADDR); count | (FLASH_ReadByte(EEPROM_COUNT_ADDR1) 8); return count; }6.3 上位机通信接口通过UART实现与PC的数据交互void UART_SendCount(uint16_t count) { printf(COUNT:%04d\r\n, count); } void UART_ProcessCommand(char* cmd) { if(strncmp(cmd, SET , 4) 0) { uint16_t value atoi(cmd4); Display_UpdateNumber(value); } // 其他命令处理... }在项目开发过程中最耗时的往往是调试显示异常问题。建议在初期就建立完善的测试用例特别是边界值测试如全0、全8、快速变化等情况。实际使用中发现TM1628对时序要求较为严格适当增加信号线延迟可提高稳定性。