
简介STM32Cube HAL库环境下的CH455驱动四位数码管代码面向熟悉STM32基础、需要快速集成数码管显示功能的嵌入式开发者。压缩包内共2个文件分别是一个C源文件和一个H头文件C文件实现IIC读写时序和显示函数H文件则定义引脚宏、命令字等参数整体大小仅2KB结构非常精简。CH455通过IIC接口驱动数码管SDA和SCL引脚可在CubeMX中自定义只需将引脚标签改为TUF_SDA、TUF_SCL并同步修改头文件中注释指示的引脚宏即可完成配置适配不同STM32型号的灵活性较强。使用时在main.c中添加TUF_DPY_Write_Cmd(CH455_SYSON)开启键盘显示再调用TUF_DPY_SHOW_NUM(float num)即可直接显示数值代码封装了底层时序和控制逻辑开发者无需关心具体IIC协议细节也便于学习HAL库下IIC外设编程。目前已有1660人学习下载对于需要快速验证CH455驱动或希望复用这套轻量级驱动的项目而言能明显缩短调试周期。1. 项目概述与选型思路1.1 为什么选择CH455这颗芯片做单片机项目碰到数码管显示大多数人第一反应是直接用MCU的GPIO动态扫描。但如果你用的是STM32而且项目里已经跑着I2C、UART、ADC这些外设再让CPU去干数码管扫描这种脏活累活其实挺亏的。更麻烦的是动态扫描有严格的时序要求一旦中断响应不及时数码管亮度就会不均匀甚至出现肉眼可见的闪烁。CH455是沁恒推出的一颗I2C接口数码管驱动芯片单个芯片就能驱动4位共阴数码管还内置了键盘扫描功能。当初我选它理由很直接项目里已经有一路I2C总线闲着挂上CH455只需要占用两个引脚MCU侧只需按格式发送显示数据剩下的扫描工作全部由芯片内部完成。相比MAX7219这类SPI接口的驱动芯片CH455的优势是I2C总线可以挂多个从设备而且芯片和数码管之间不需要外接限流电阻它内部有恒流驱动。还有一个容易被忽略的点CH455的价格比MAX7219便宜不少批量采购时一颗就差好几毛钱做产品的人会懂这个差异意味着什么。而且它对共阴数码管做了专门的扫描电流优化亮度均匀性实测比GPIO扫描好一个档次。1.2 本文能帮你解决什么问题这篇文章适合两类人一是第一次接触CH455想快速跑通显示功能没有耐心翻那份有点绕的数据手册的二是已经在用STM32CubeMX和HAL库开发但不确定I2C驱动CH455时地址怎么填、显存格式怎么转换的。我会从CubeMX工程配置讲起给出完整的HAL库驱动代码再把段码转换和显示函数封装好最后把调试过程中遇到的坑和排查方法整理成清单。你照着做从零到数码管正常显示半小时内应该能搞定。2. CH455核心原理与显存格式详解2.1 芯片引脚和I2C地址CH455的封装是SOP16引脚不算多但有几个关键的要说清楚。SEG0~SEG7是段驱动输出对应数码管的a到g和小数点dpDIG0~DIG3是位选输出接4位数码管的公共端。对于共阴数码管来说DIG输出的是低电平选中SEG输出的是高电平点亮对应段方向感一定要先建立起来后面查问题能省不少时间。I2C地址是很多人第一道坎。CH455的7位从机地址由AD0、AD1引脚电平决定默认悬空或接地时是0x24。在HAL库的HAL_I2C_Master_Transmit函数里DevAddress参数填的是7位地址也就是直接填0x24HAL库内部会自动左移一位并拼上读写标志位。但网上很多老代码填的是0x48那是8位写地址因为在标准外设库的某些版本里需要填完整地址。如果你搬代码的时候没注意这一点会发现I2C始终回NACK。地址引脚7位地址HAL库DevAddressAD00, AD100x240x24AD01, AD100x250x25AD00, AD110x260x26AD01, AD110x270x272.2 系统命令与显示命令CH455的命令分成两大类通过写入的字节来区分。系统命令用来做全局控制比如显示使能、亮度调节、消隐、闪烁等命令码是0x01后面跟一个参数字节。显示命令用来加载8个字节的段数据命令码是0x02后面跟上8个字节的显存数据。系统命令参数字节的位定义要记一下bit0是显示使能写1打开显示写0关闭显示bit1是消隐控制bit2是闪烁控制bit3是测试模式bit5和bit4组合成两位亮度等级00最暗11最亮。上电之后CH455默认是显示关闭状态很多人写完代码发现数码管没反应其实不是驱动没写对而是忘了发这个打开显示的系统命令。发送方式是普通的I2C写操作先发地址0x24再发命令码再发参数。完整调用HAL库就是一次HAL_I2C_Master_Transmit把命令码和参数打包成一个数组发出去。2.3 段优先格式的显存排列这是CH455和很多其他驱动芯片最大的不同也是新手最容易绕晕的地方。CH455的显存是8个字节排列方式不是按数码管位来排的而是按段来排的。具体说显存第0字节对应段a第1字节对应段b一直到第7字节对应小数点dp每个字节的低4位分别对应4位数码管bit0是第1位最左边bit3是第4位最右边。我打个比方普通段码表是按“位”组织的每位数码管一个字节比特位代表段的亮灭CH455是反过来的按“段”组织每段一个字节比特位代表是哪一位数码管。所以你要显示的数字先用共阴段码表查出来再做一个矩阵转置把位优先的数据转换成段优先的数据才能正确写入CH455。这个转换逻辑用代码实现起来并不复杂关键是要把两层循环的对应关系理清楚外层循环遍历8个段内层循环遍历4个数码管位然后把每个位对应段码的那个bit取出来填到正确的字节和位上去。3. STM32CubeMX工程搭建3.1 基本配置步骤我用的是STM32F103C8T6这颗最经典的芯片你手里如果是F4系列或者G0系列操作路径基本一致。打开STM32CubeMX芯片型号选STM32F103C8Tx首先配置RCCHSE选择Crystal/Ceramic Resonator然后SYS选择Serial Wire这样保留SWD调试口接着配置I2C1模式选I2C速度默认100kHz可以工作但CH455支持到400kHz我建议直接选Fast Mode拿400kHz显示刷新更顺滑。时钟树不用太纠结如果外部晶振是8MHz直接让CubeMX自动生成72MHz主频配置就行。CH455的I2C通信对时钟精度没有苛刻要求PLL出来多少都能正常工作I2C外设自己会做分频和时序控制。最后Project Manager里选择生成工程Toolchain选MDK-ARM如果还没装STM32CubeIDE直接用Keil也完全没问题。如果你是刚接触STM32CubeIDE想改中文界面的话在安装目录里的STM32CubeIDE.ini文件加上-Duser.languagezh重启软件就是中文菜单了这个后面再说。3.2 硬件连接与上拉电阻连接关系很简单CH455的SCL接STM32的PB6I2C1_SCLSDA接PB7I2C1_SDAVCC接3.3V或5V都可以CH455的宽压范围是3V到5.5V但如果MCU是3.3V系统建议统一用3.3V供电避免I2C电平不匹配。GND共地。AD0和AD1直接接地拿到默认地址0x24。DIG0~DIG3分别接数码管的1到4位公共端SEG0~SEG7接数码管的a到g和dp这里要注意把段和数码管的引脚对应关系记好别接反了。I2C总线需要外接上拉电阻的事我得多说一句。STM32的I2C引脚内部有弱上拉但驱动强度不够多设备共总线或者线长了容易通信不稳定。我在面包板调试时就吃过这个亏SCL、SDA各接一个4.7k欧电阻到3.3V问题立刻消失。时序不稳定表现很随机有时候是偶尔显示错乱有时候是第一次上电能行复位后就不行了非常折腾人先把上拉电阻接上再说。4. HAL库驱动CH455完整实现4.1 驱动头文件与基础函数驱动代码不用写得很花哨核心就是几个函数初始化、写系统命令、写显存数据、显示数字、清屏。我直接把代码贴出来每个函数的注释都标注清楚。// ch455.h #ifndef __CH455_H #define __CH455_H #include main.h #define CH455_I2C_ADDR 0x24 // 7位I2C地址AD0/AD1接地 #define CH455_CMD_SYS_CTRL 0x01 // 系统控制命令 #define CH455_CMD_DISP_DATA 0x02 // 显存数据加载命令 // 系统控制参数位定义 #define CH455_DISP_ON 0x01 // 显示使能 #define CH455_DISP_OFF 0x00 // 显示关闭 #define CH455_BLANKING 0x02 // 消隐 #define CH455_FLICKER 0x04 // 闪烁 #define CH455_TEST_MODE 0x08 // 测试模式 #define CH455_BRIGHTNESS_0 0x00 // 亮度最低 #define CH455_BRIGHTNESS_1 0x10 #define CH455_BRIGHTNESS_2 0x20 #define CH455_BRIGHTNESS_3 0x30 // 亮度最高 void CH455_Init(void); void CH455_WriteSysCmd(uint8_t param); void CH455_WriteDisplay(uint8_t seg_data[8]); void CH455_ShowNumber(uint16_t value, uint8_t dot_mask); void CH455_Clear(void); #endif这里有个设计上的小考虑写系统命令时命令码固定是0x01所以我把命令码封装进到函数内部只对外暴露参数。调用方只需要关心要设置成什么状态不用每次记得命令码是多少。显示数据加载命令同理CH455_WriteDisplay函数内部会把命令码0x02和8字节数据拼成一个数组。4.2 初始化与显存写入// ch455.c #include ch455.h #include i2c.h // 共阴数码管段码表0~9, A~F static const uint8_t SEG_CODE[16] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71 }; static void CH455_WriteBuf(uint8_t *buf, uint8_t len) { HAL_I2C_Master_Transmit(hi2c1, CH455_I2C_ADDR, buf, len, 100); } static void CH455_WriteSysCmd(uint8_t param) { uint8_t buf[2]; buf[0] CH455_CMD_SYS_CTRL; buf[1] param; CH455_WriteBuf(buf, 2); } static void CH455_WriteDisplay(uint8_t seg_data[8]) { uint8_t buf[9]; buf[0] CH455_CMD_DISP_DATA; for (uint8_t i 0; i 8; i) { buf[i 1] seg_data[i]; } CH455_WriteBuf(buf, 9); } void CH455_Init(void) { // 打开显示设置亮度为最高档 CH455_WriteSysCmd(CH455_DISP_ON | CH455_BRIGHTNESS_3); } void CH455_Clear(void) { uint8_t zero[8] {0}; CH455_WriteDisplay(zero); }重点看CH455_WriteDisplay一次I2C传输把9个字节1个命令码加8个数据字节全发出去中间不需要Stop再Start。CH455的设计允许在单次传输中连续加载全部显存效率很高。HAL库的HAL_I2C_Master_Transmit天然支持这种多字节传输你不需要手动控制起始停止条件它内部会处理好。超时参数我填的是100毫秒对这个应用来说足够了。I2C总线400kHz下传9个字节大约只需要0.2毫秒100毫秒的超时余量非常充足就算总线被其他设备阻塞也能等一阵子才报错。我不建议为了显得精简把超时设成1或10万一总线上有其他设备临时拉低时钟超时太短容易返回错误码排查起来又得看代码。4.3 数字显示与段码转换显示数字是核心应用逻辑这里要把前面讲的段优先显存格式用代码实现出来。void CH455_ShowNumber(uint16_t value, uint8_t dot_mask) { uint8_t digit[4]; uint8_t seg_data[8] {0}; uint8_t code[4]; // 拆分为4位数字digit[0]是千位digit[3]是个位 digit[0] (value / 1000) % 10; digit[1] (value / 100) % 10; digit[2] (value / 10) % 10; digit[3] value % 10; // 查段码表 for (uint8_t i 0; i 4; i) { code[i] SEG_CODE[digit[i]]; } // 段优先转换seg_data[0]对应段abit0对应第1位 for (uint8_t seg 0; seg 8; seg) { for (uint8_t pos 0; pos 4; pos) { if (code[pos] (1 seg)) { seg_data[seg] | (1 pos); } } } // 处理小数点dot_mask的bit0对应第1位千位bit3对应第4位个位 for (uint8_t pos 0; pos 4; pos) { if (dot_mask (1 pos)) { seg_data[7] | (1 pos); } } CH455_WriteDisplay(seg_data); }我详细解释一下这个转换过程因为这里最容易出错。假设你要显示数字1234无小数点。先查段码表得到4个字节1的段码0x06、2的段码0x5B、3的段码0x4F、4的段码0x66。现在进入双重循环seg0的时候处理的是段a第1位数字1的段码0x06的bit0是0所以不置位第2位数字2的段码0x5B的bit0是1所以seg_data[0]的bit1置1第3位数字3的段码0x4F的bit0是1所以bit2置1第4位数字4的段码0x66的bit0是0不置位。这样seg_data[0] 0b00000110表示段a点亮的是第2位和第3位。这个结果和实际数字2、3的段a都需要点亮完全一致。小数点单独处理在seg_data[7]也就是dp段bit位置关系和数字位一一对应。dot_mask这个参数设计成位掩码的好处是调用起来直观比如想在千位显示小数点就传0x01想全部显示小数点就传0x0F。4.4 负数与特殊字符处理CH455_ShowNumber只处理了无符号整数实际项目中经常会遇到负数和带符号的数据。处理思路也不复杂先把负号映射成段码0x40也就是g段单独点亮然后根据数值大小决定显示格式。void CH455_ShowSigned(int16_t value, uint8_t dot_mask) { uint8_t digit[4]; uint8_t code[4] {0}; if (value 0) { // 千位显示负号其余位显示绝对值 digit[1] (abs(value) / 100) % 10; digit[2] (abs(value) / 10) % 10; digit[3] abs(value) % 10; code[0] 0x40; // 负号就是g段点亮 } else if (value 1000) { // 超过1000直接显示4位数字 digit[0] (value / 1000) % 10; digit[1] (value / 100) % 10; digit[2] (value / 10) % 10; digit[3] value % 10; } else { // 小于1000时高位消隐更美观 digit[0] 0; digit[1] (value / 100) % 10; digit[2] (value / 10) % 10; digit[3] value % 10; } for (uint8_t i 0; i 4; i) { if (digit[i] 10) { code[i] SEG_CODE[digit[i]]; } } uint8_t seg_data[8] {0}; for (uint8_t seg 0; seg 8; seg) { for (uint8_t pos 0; pos 4; pos) { if (code[pos] (1 seg)) { seg_data[seg] | (1 pos); } } } for (uint8_t pos 0; pos 4; pos) { if (dot_mask (1 pos)) { seg_data[7] | (1 pos); } } CH455_WriteDisplay(seg_data); }高位消隐这个细节比较实用。显示温度比如23度如果直接补0显示成0023观感上不够专业消隐掉前导零后只显示两位数看起来清爽多了。这里还有个约束负号占了千位所以负数的可显示范围是-999到-1绝对值超过999就溢出显示不下了适合的应该考虑调整单位比如显示成-1.23k这样的格式。5. 应用层实现与效果扩展5.1 温度监测显示完整案例驱动层写完后应用层用起来就非常简单了。我拿一个实际的温度监测项目举例STM32的ADC采集NTC热敏电阻分压值换算成温度后送给CH455显示同时当一个开关量触发时让数码管闪烁提示。ADC采集和温度换算不是本文重点直接给出调用部分// 假设adc_value已经通过HAL_ADC_GetValue获取 float temp NTC_GetTemperature(adc_value); // 返回实际温度比如26.3 int16_t temp_int (int16_t)(temp * 10); // 变成263 uint8_t dot 0x02; // 希望显示26.3小数点在十位 CH455_ShowSigned(temp_int, dot); // 需要闪烁告警时通过系统命令控制 if (over_temp_flag) { CH455_WriteSysCmd(CH455_DISP_ON | CH455_FLICKER | CH455_BRIGHTNESS_2); } else { CH455_WriteSysCmd(CH455_DISP_ON | CH455_BRIGHTNESS_3); }亮度和闪烁可以直接通过系统命令动态切换这个灵活性是GPIO扫描方案不好实现的。GPIO扫描想实现闪烁要么在定时器中断里翻转消隐标志要么在循环里延时都是占用CPU的做法。CH455只要一条I2C命令芯片自己就把闪烁周期控制好了。5.2 倒计时器与数码管轮询再举一个倒计时器的例子。用一个16位变量存剩余秒数定时器中断里每秒自减主循环里调用显示函数刷新。CH455的显存不会自动清零所以每次进入中断只更新显存数据显示结果一直保持CPU可以安心去处理其他任务不用担心数码管亮度闪烁。这和GPIO动态扫描比起来优势是质的飞跃。如果你需要显示的位数超过4位比如6位或8位可以把两个CH455挂到同一根I2C总线上AD0和AD1配成不同地址即可。分时刷新两片芯片不会有冲突HAL库的I2C调用天然保证了总线互斥。实测刷新频率在100Hz以上不会有视觉闪烁感应用上完全够用。5.3 驱动移植到其他STM32系列这套驱动代码的移植性做得比较干净和具体芯片相关的只有hi2c1这个句柄和I2C初始化。如果你的项目用的是I2C2把ch455.c里引用的hi2c1改成hi2c2然后在CubeMX里把I2C2配置好就行。如果你是PY32F003这类国产Cortex-M0芯片只要它的HAL库兼容ST的API风格驱动代码同样可以复用。我之前在PY32F003上移植过一次碰到的唯一问题是I2C时钟频率配置。PY32的I2C外设时钟源和STM32不完全一样CubeMX生成的初始化代码在RCC配置上略有差异但驱动层的调用方式完全一致。如果遇到I2C通信不稳定第一步检查时钟树第二步检查上拉电阻不要急着怀疑驱动代码。6. 排查实录与避坑指南6.1 I2C通信类问题I2C通信异常是最常见的我按崩溃概率排序整理了一张排查表。现象可能原因排查方法I2C总线报HAL_TIMEOUTAD0/AD1地址不对扫描0x24~0x27看哪个地址有ACK首次通信正常复位后失败SDA/SCL缺上拉电阻外接4.7k电阻到VCCSCL波形乱数据错位总线电容过大速率太高降到100kHz缩短杜邦线3.3V MCU挂5V供电的CH455电平不匹配统一3.3V供电或加电平转换排查I2C问题时我的做法是先写一个扫描函数把0x24到0x27全部发一遍地址字节看看哪个能收到ACK。这一步能快速排除地址错误问题。如果全部NACK再用示波器或逻辑分析仪看SCL、SDA波形确认起始条件、地址字节、ACK位是否正常。一个隐蔽的坑是芯片焊接问题特别是SOP16这种引脚间距不大的封装手工焊接时相邻引脚容易桥连。CH455的SCL和SDA如果被锡桥短路I2C怎么调都通不了用万用表蜂鸣档测引脚间阻值能快速发现桥连。6.2 显示异常和亮度问题显示全亮或者全灭先检查是不是系统命令没把显示使能打开。我在代码里CH455_Init做了这个操作但如果你把Init函数放在I2C还没初始化好的地方调用命令发不出去数码管自然不亮。调用顺序必须是CubeMX生成的MX_I2C1_Init()先执行然后才能调用CH455_Init()。显示乱码通常就是段码转换关系的bit错位。比如把共阴段码表误用了共阳的数字会显示得杂乱无章。有一个快速判断方法单独把seg_data[0]设为0x0F如果4位数字的a段全亮说明段对应关系正确如果亮的不是a段检查SEG0~SEG7的接线是否对应数码管的a到g。亮度不理想的时候先看系统命令的亮度位是不是设在最高档。CH455的亮度调节分成4级从0到3如果觉得最高档还是不够亮检查数码管是不是共阴的——这个芯片只能驱动共阴接共阳数码管只会得到一团糟或者完全不亮。另外CH455内部集成了恒流驱动不需要外部限流电阻如果你额外串了电阻反而会拖暗显示这也是个容易被忽视的操作误区。注意CH455只能驱动共阴数码管接共阳数码管无法正常工作。选型时务必确认数码管极性。6.3 开发环境的小坑和STM32开发环境相关的几个问题也顺便提醒一下。如果用的是STM32CubeIDE第一次烧录需要安装ST-Link驱动这个去ST官网下载STM32 ST-LINK Utility或者直接装CubeIDE自带的驱动包就行。很多人在Win10/Win11上发现设备管理器里ST-Link显示未知设备就是因为驱动没装好而不是调试器坏了。CH340或CP2102这类USB转串口驱动也经常出问题。CH340的驱动去沁恒官网下载CP2102去Silicon Labs官网下不要装那些来路不明的驱动管理软件。装完驱动后如果串口无法打开检查设备管理器的COM口号是否正确很多时候是COM号被系统重新分配了。串口调试和I2C调试共用一个工程时注意USART的波特率要和实际晶振匹配。如果用的不是标准8MHz晶振CubeMX自动计算的波特率可能存在误差串口打印乱码就从这里排查。7. 扩展思考CH455的键盘扫描能力CH455这颗芯片还有一个经常被忽略的功能内置4x4键盘扫描。它的读操作可以返回按键状态和显示共用I2C总线不需要额外引脚和扫描逻辑。如果你的项目里既要显示又要按键输入一颗CH455能同时搞定硬件设计可以简洁不少。读键盘的HAL库操作和写数据不同要用HAL_I2C_Master_Receive。CH455收到读命令后会返回按键状态字节具体位定义在数据手册里有详细说明。不过要注意CH455的键盘扫描和显示驱动不能同时进行读取键盘期间数码管会短暂停顿但因为扫描周期很短肉眼完全感觉不到。我实际测试过按键响应速度在毫秒级别对绝大多数HMI场景都够用。关键是你省掉了一个独立键盘扫描芯片或者一组GPIO尤其是在引脚紧张的封装上这个优势会放大。如果后续项目需要更复杂的人机交互可以考虑把CH455的键盘读取做成中断驱动按键触发时快速读取并清标志这样就不会占用主循环的轮询时间了。最后分享一个我在实际项目中养成的习惯驱动类代码尽量独立成模块不跟业务逻辑耦合。CH455驱动就管显示ADC就管采集按键就管输入每个模块暴露几个接口上层逻辑只调接口。这样做的好处是项目后期换MCU平台或者换显示方案驱动层重写一遍就行业务逻辑完全不用动。这个习惯帮我省掉的返工时间已经远远超出当初写驱动代码所花的时间了。本文还有配套的精品资源点击获取