ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

STM32 HAL库驱动CH455数码管显示:I2C通信与避坑指南

STM32 HAL库驱动CH455数码管显示:I2C通信与避坑指南 简介面向STM32嵌入式开发者的CH455四位数码管驱动方案基于STM32Cube HAL库编写适用于需要快速实现数码管显示与按键扫描的仪表、控制面板等场景。压缩包共2个文件含1个C源文件和1个头文件采用IIC两线接口驱动CH455SDA、SCL引脚均可自定义在CubeMX中设置标签为TUF_SDA、TUF_SCL并同步修改头文件注释处引脚定义即可完成底层适配。使用时先在main.c中调用TUF_DPY_Write_Cmd(CH455_SYSON)开启键盘显示功能再调用TUF_DPY_SHOW_NUM(float num)直接显示浮点数适合输出传感器读数、计算结果等。CH455驱动可有效节省主控GPIO资源配合HAL库的IIC功能整套资源仅约2KB代码精简且易于移植到不同STM32型号。已有1682人学习下载适合具备基础HAL库知识、希望快速上手CH455驱动的开发者借鉴。1. 为什么这个标题值得花时间HAL 库 CH455 是低成本数码管显示的省心解STM32Cube HAL 库驱动 CH455 四位数码管这个组合解决的是嵌入式开发里最常见的一件事想显示数字但 GPIO 不够用。CH455 通过 I2C 把段选、位选都接管了CPU 只需要在内容变化时发几个字节之后芯片自己完成动态扫描不占主循环。相比 74HC595 需要自己写移位时序、相比 TM1637 需要读厂商私有协议CH455 用标准 I2C 通信工程上更干净排查起来也更直接。我最初接手一个带四位小数显的仪表板需求时第一反应是 GPIO 直接扫结果一算引脚差点不够后来换成 CH455 方案一版就点亮了。这篇笔记把 CubeMX 配置、HAL I2C 发送、段码拆帧、避坑清单一次讲透适合正在做实验箱、工业面板、充电桩显示模块的开发者。2. CH455 的通信机制为什么一个字符要拆成两个半字节发很多第一次用 CH455 的人会拿它当普通数码管驱动 IC 来理解以为像 TM1650 那样一个寄存器写一个完整段码。实际上 CH455 的帧结构设计很特别——它内部寄存器是 4 位宽的I2C 每个字节的高 4 位是寄存器地址低 4 位才是数据。这段话值得读两遍因为后面所有调试困惑都从这里来。2.1 段码 8 位、寄存器 4 位拆帧是硬性规定数码管一个字符包含 7 段加小数点正好 8 位可以对应一个 uint8_t。但 CH455 设计成每次只能写 4 位数据所以一位数码管的段码必须拆成低 4 位和高 4 位两次发送分别写到不同的寄存器地址去。用地址位来区分高半字节还是低半字节而不是像某些芯片那样用一个额外的标志位。我一般这样定义帧结构内部地址 0x00 到 0x03 对应第 1 到第 4 位数码管的低 4 段地址 0x04 到 0x07 对应第 1 到第 4 位数码管的高 4 段。所以发送时一个数据字节是(地址 4) | (半字节段码)。举个例子要让第 1 位数码管显示数字 0段码是 0x3F低 4 位是 0x0F高 4 位是 0x03分别组装成 0x0F 和 0x43 发送这才算写完整一个字符。这个设计与芯片的物理引脚排列有关。CH455 内部做动态扫描每时每刻只有一个位在被点亮位与位之间靠 CH455 自己轮流选通不需要 CPU 反复送位选信号。因此 CPU 要做的事情只剩两件组装帧、走 I2C 发出去。2.2 用 HAL 库的 I2C 外设还是 GPIO 模拟我为什么选前者CH455 是标准 I2C 从机理论上用两根 GPIO 模拟时序也能驱动。但我建议直接用 STM32Cube 生成的硬件 I2C 外设理由有三个首先硬件 I2C 的时序由外设保证起始条件、停止条件、应答位的电平跳变不用软件去卡延时代码更接近「发送数据」这个语义其次HAL 库已经封装好超时机制总线上出问题不会让程序死等最后后续如果要挂多个 I2C 设备硬件外设 地址区分能省很多接线。GPIO 模拟不是不能做它适合在引脚实在不够、或者想彻底绕开 I2C 外设 bug 的极端场景。但代价是时序代码里全是延时微调换一块板子、换一颗晶振可能就得重新调。用 HAL 库硬件 I2CCubeMX 里勾选一下就有可用的句柄 hi2c1发送函数一句话搞定。2.3 最小发送模型从 CubeMX 工程到第一个字节新一点的 CubeMX 生成 HAL 库工程后默认不会把 I2C 引脚映射到正确的位置所以第一步是在 Pinout 视图里把 SCL、SDA 所在的引脚功能选成 I2C1_SCL、I2C1_SDA。我通常把 PB8 配给 I2C1_SCLPB9 配给 I2C1_SDA这是最常见的一组复用脚具体按自己板子的丝印调整。时钟配置上I2C 外设挂在 APB1 总线上CubeMX 会自动根据系统时钟算出分频。为了减少变量我第一版会把 I2C Speed Mode 设成 Standard Mode也就是 100kHz。CH455 对时序要求不算苛刻但 100kHz 在排错时宽容度最高等整块板子稳定了再改 400kHz 也不迟。工程生成后验证通信是否通的一条最小代码是这样/* CH455 的 7 位从机地址HAL 内部会自动左移 1 位拼读写标志 */ #define CH455_ADDR 0x48 /* 发送一个字节高 4 位是内部寄存器地址低 4 位是段码半字节 */ uint8_t test_frame 0x0F; HAL_StatusTypeDef st HAL_I2C_Master_Transmit(hi2c1, CH455_ADDR, test_frame, 1, 100); if (st ! HAL_OK) { /* 返回非 OK先查 ACK、查接线、查地址别急着看显不显示 */ }第一次跑这段代码返回值是 HAL_OK 才算跨过了最基础的一关。HAL_I2C_Master_Transmit 的第三个参数是发送缓冲区第四个参数是长度第五个参数是超时毫秒数。这里地址参数填 0x48而不是 0x90因为 HAL 库要求的是 7 位地址库内部会自己做左移补位。这是新手最容易翻车的地方后面避坑章节还会单独展开。3. 四位共阴数码管与 CH455 的接线、CubeMX 参数配置硬件接线看着简单实际上段序、公共端、限流电阻这几个细节没处理好后面全是玄学问题。这一章把接线表和 CubeMX 参数一起列出来照着做基本能省掉三分之二的调试时间。3.1 段线、位线、电源一张接线表说清楚CH455 驱动的是共阴数码管这一点要先确认。四位共阴数码管有 12 根引脚8 根段脚a 到 g 加 dp和 4 根位脚第 1 位到第 4 位的公共阴极。CH455 的 SEG0 到 SEG7 依次接数码管的 a 到 dpDIG0 到 DIG3 接四个位脚。如果手里是共阳数码管不要硬接CH455 内部设计是按共阴来的共阳管需要额外加三极管反相已经失去了省引脚的意义。接线顺序本身不出问题容易出问题的是段序对应。有些数码管内部把 dp 放在段码最高位有些则把 g 放在最高位所以同一张段码表在不同厂家数码管上显示的结果可能完全不同。我一般第一版先用0x0F这种只有低 4 段亮的测试值逐位确认段脚和段码位的映射关系再固化段码表。下表是常见接法实际以你自己板子的丝印为准CH455 引脚接往对象备注SCLSTM32 PB8I2C1 时钟建议外部 4.7k 上拉SDASTM32 PB9I2C1 数据建议外部 4.7k 上拉VCC3.3V与 MCU 同电压域最省事GNDGND与 MCU 共地SEG0-SEG7数码管 a-dp段线上串限流电阻DIG0-DIG3数码管位 1-4共阴公共端不要串电阻限流电阻的取值要单独说。段线上的电阻直接决定亮度阻值太小电流大伤数码管阻值太大亮度不足。我常用 330 欧起步先看显示效果再往 470 欧或 220 欧调。位线上不要串电阻因为公共端电流是八段之和串了会造成不同位数亮度差异明显。3.2 CubeMX 配置项I2C 速度、上拉、时钟树的几个关键参数CubeMX 里 I2C 参数页主要有 Speed Mode、Own Address、Address Detection Mode、Clock No Stretch Mode 这几项。CH455 是从机MCU 是主机Own Address 填什么都无所谓只要不和总线上其他从机冲突。Address Detection Mode 选 7-bitClock No Stretch Mode 保持 Disable这些保持默认就行。真正要核对的是 Speed Mode 和时钟树。如果选 Fast ModeCubeMX 会要求把上升沿时间等参数填好不同板子布线质量不一样可能因为边沿太缓导致通信失败。我建议第一版老老实实选 Standard Mode100kHz。时钟树部分确认 APB1 时钟不为 0并且 I2C1 的时钟源已经挂上否则 HAL_I2C_Init 会返回失败。写到这里有个容易忽略的点CH455 的 SCL、SDA 是开漏结构必须靠上拉电阻才能输出高电平。STM32 的部分引脚可以启用内部上拉但内部上拉阻值偏大在长线上不够稳。我一般会在板上加两颗 4.7k 外部上拉电阻一个接到 SCL一个接到 SDA哪怕只做面包板原型也值得加上。别小看这一步很多「偶尔通信失败」在加了上拉之后就消失了。初始化完成后可以用 HAL_I2C_IsDeviceReady 来探测从机是否存在。这个函数会发送地址并等待应答返回 HAL_OK 说明 CH455 在总线上应答了uint32_t tick HAL_GetTick(); /* 对焦点发送带读标志的地址看 CH455 是否 ACK */ if (HAL_I2C_IsDeviceReady(hi2c1, CH455_ADDR, 10, 100) ! HAL_OK) { /* 超时还返回失败大概率是接线、上拉或地址问题 */ }如果这一步失败不要继续往下调显示逻辑先把电路查明白。I2C 是同步协议没有应答意味着物理层不通后面写再多显示代码都是白搭。硬件探测通过后再进入驱动函数封装才算真正开始写显示逻辑。4. 手写 CH455 驱动段码表、拆帧函数、四位刷新一次跑通到这一步I2C 通信已经通了接下来要做的就是把它封装成三个层次段码表、单字符写入、四位数字刷新。这里给出的代码是经过实际项目验证的常见写法直接用 HAL 库 API 实现不依赖任何私有协议库。4.1 共阴段码表先定义清楚后面少改段码表是显示逻辑的地基。对共阴数码管要点亮的段对应位写入 1段码就是这些位的组合。下面这张表覆盖了 0 到 9 和几个常用字母小数点位置单独留出来需要时可以和数字段码做或运算。/* 共阴数码管段码表数字 0-9位序为 a b c d e f g dp */ const uint8_t seg_table[10] { 0x3F, /* 0: a b c d e f 亮 */ 0x06, /* 1: b c 亮 */ 0x5B, /* 2: a b d e g 亮 */ 0x4F, /* 3: a b c d g 亮 */ 0x66, /* 4: b c f g 亮 */ 0x6D, /* 5: a c d f g 亮 */ 0x7D, /* 6: a c d e f g 亮 */ 0x07, /* 7: a b c 亮 */ 0x7F, /* 8: 全段 亮 */ 0x6F, /* 9: a b c d f g 亮 */ };这张表建立在段脚按 a、b、c、d、e、f、g、dp 的顺序接在 SEG0 到 SEG7 的前提下。如果你的板子段序不是这样比如把 dp 放在最前面表里的值就要整体按位重排。我建议先用一根杜邦线把每一位单独短接测试确认段序后再填表。4.2 单字符写入把段码拆成低 4 位和高 4 位核心函数就一个把位序号和段码组织成两个帧字节一次 I2C 传输发出去。这里要再次强调地址偏移第 0 到 3 位对应低 4 段地址第 4 到 7 位对应高 4 段地址所以高半字节的寄存器地址要加 4。#define CH455_ADDR 0x48 /* 向指定位写入段码bit_index 取值 0-3对应第 1 到第 4 位数码管 */ void ch455_set_segment(uint8_t bit_index, uint8_t seg) { /* 第一帧低 4 位段码寄存器地址 bit_index */ uint8_t low_frame (bit_index 4) | (seg 0x0F); /* 第二帧高 4 位段码寄存器地址 bit_index 4 */ uint8_t high_frame ((bit_index 4) 4) | ((seg 4) 0x0F); uint8_t buf[2] { low_frame, high_frame }; /* 一次传输两个字节HAL 内部会先发地址再发数据 */ HAL_I2C_Master_Transmit(hi2c1, CH455_ADDR, buf, 2, 100); }这个函数的关键在于bit_index不要超过 3否则高帧地址会越界到其他控制寄存器。发送长度是 2代表两个数据帧I2C 总线上会出现一次起始条件、一次地址字节、两个数据字节、一个停止条件。CH455 收到第一个字节后内部解析出地址和半段码收到第二个字节后解析出另一半合在一起更新对应位的显示缓存。有两点想提醒第一这个函数对每一位都做一次 I2C 传输刷新四位就要调用四次每次需要几百微秒对显示这种低频操作完全够用第二不要试图把四个位的帧合并成一个长 buffer 一次性发除非你确认寄存器地址是递增的而 CH455 的低 4 位地址和高 4 位地址是跳变的合并反而要先重新排序得不偿失。4.3 四位数字刷新一次性显示一个整数有了单字符函数显示整数就是把数字拆成个、十、百、千位逐位调用。注意显示的是明码数字还是 BCD 码这里按明码写最简单直接。void ch455_display_number(uint16_t num) { uint8_t digits[4]; /* 依次取出千、百、十、个位 */ digits[0] (num / 1000) % 10; digits[1] (num / 100) % 10; digits[2] (num / 10) % 10; digits[3] num % 10; for (uint8_t i 0; i 4; i) { ch455_set_segment(i, seg_table[digits[i]]); } }主循环里调用这个函数就够了不需要定时器去刷新因为动态扫描的工作全在 CH455 内部完成。CPU 只在数字变化时传一次数据中途即使主循环被别的任务卡住数码管也能保持显示内容不闪。这是 CH455 和 GPIO 直接扫最大的区别也是它适合做后台任务的工业面板的原因。如果想让某些位熄灭比如只显示三位数就给对应位写入 0x00。0x00 段码表示所有段都灭这一位看起来就是空白的。小数点的处理也简单把seg_table[digits[i]] | 0x80传入即可0x80 对应 dp 段。需要注意的是段码表里如果 dp 不在最高位那 0x80 可能需要换位置这又回到了段序核对的问题上。5. 避坑与排查CH455 驱动的 5 个高频翻车点CH455 本身不算复杂但因为它把地址和数据揉在一个字节里再加上 HAL 库对地址参数的处理方式实际调试时会遇到一些看起来「不科学」的故障。下面这五条是我自己踩过、也看别人踩过的坑按现象、原因、解决的顺序说清楚。5.1 I2C 应答失败地址填 0x90 是第一个拦路虎现象HAL_I2C_Master_Transmit 返回 HAL_ERROR用示波器看 SDA 上从机一直没有拉低应答位或者 HAL_I2C_IsDeviceReady 永远返回超时。原因多半是地址填错了。很多资料把 CH455 的写地址写成 0x90这是 8 位地址形态也就是 7 位地址 0x48 左移一位再补 0。但 HAL 库的接口要求传 7 位地址库内部会再左移一次。如果你填 0x90实际发送的地址变成 0x120超出 7 位范围从机根本不认。解决把地址定义成 0x48不要自己移位更不要从旧例程里照抄带 0x90 的写法。5.2 只有一半段亮段码拆帧时寄存器地址偏移搞错现象显示数字 8结果只有上面三横亮下面三横和中间一横完全不亮。原因是对第 0 位的高 4 段寄存器地址写成了bit_index 1而不是bit_index 4。CH455 的地址范围从 0 到 7前 4 个给低段后 4 个给高段加 4 才能跳到正确的高段寄存器。如果加 1实际写到的是后一位的低段寄存器把别的位的显示内容弄乱了。解决先固定只显示一个数字把两个帧字节打印出来对照手册确认地址高 4 位的值在 0 到 7 之间并且和自己的位对应关系一致。5.3 SDA 一直为低I2C 总线被从机锁死现象程序一走到 I2C 初始化SDA 就被拉低SCL 正常复位 MCU 也没用。原因可能是上电瞬间 CH455 检测到了不完整的起始条件进入了一个等待后续数据的状态而后续数据永远没来。解决在初始化 I2C 外设之前用 GPIO 手动把 SCL 拉高然后对 SDA 连续输出 9 个高低脉冲相当于给它一个假的时钟序列让它退出错误状态。这个过程也叫 I2C 总线复位是处理从机锁死的通用手段。/* 手动复位 I2C 总线9 个时钟脉冲释放被拉低的 SDA */ void i2c_bus_reset(void) { GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_9; /* SDA */ gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOB, gpio); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET); /* SCL 保持高 */ for (uint8_t i 0; i 9; i) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET); HAL_Delay(1); } /* 复位完成重新初始化 I2C 外设 */ }这个函数的实质是让 SDA 在 SCL 高电平期间反复变化从从机的视角看就是连续收到不算数的起始事件最终它会放弃等待释放总线。执行完再回到正常初始化流程一般就能恢复通信。我习惯在 main 函数最开始调用一次成本很低但能省掉很多不明原因的首次上电死锁。5.4 四位亮度不均匀限流电阻放错了位置现象数码管显示时有一个位明显比其余三个暗或者数字在切换时亮度跳动。原因很可能是在位线上串了限流电阻。四位共阴数码管的公共端流过的电流是八段之和一旦这里串了电阻每一位的公共端电压都会被抬高段电流随之下降。不同位的压降差异会造成亮度不一致。解决限流电阻只放在段线上位线直接接 CH455 的 DIG 脚。亮度统一后再微调段电阻阻值来匹配总体亮度。5.5 调试器连着正常重新上电就黑屏现象在线调试的时候数码管显示一切正常断开调试器、断电再上电数码管完全不亮但 MCU 程序明明在跑。原因通常是上电时 MCU 已经先跑起来了I2C 外设初始化完成但 CH455 的供电还没稳定芯片处于未上电或半上电状态丢失了第一次写入的数据。解决在 main 函数进入主循环前加一个 50 到 100ms 的延时让 CH455 稳定。如果还是不行那就在初始化 I2C 后重新调用一次全灭函数把所有位写 0x00把从机的内部状态拉回已知值。这个问题本质是上电时序问题不算驱动 bug但会让人误以为代码有问题值得记住。6. 往深走一步逻辑分析仪验证时序与按键扫描的扩展空间前面五章已经能把四位数码管正常点亮但工程上「能亮」和「可靠」还不是一回事。最后一章回到验证手段和一个常见扩展方向这两点对新老手都有价值。6.1 用逻辑分析仪看帧结构不用猜我强烈建议把逻辑分析仪接到 SCL 和 SDA 上抓一次 ch455_set_segment 的完整波形。重点看三件事地址字节是不是 0x90也就是 7 位地址 0x48 加写位 0数据字节是否按高低半帧排列ACK 位是否正常。眼不见为净但 I2C 这种时序问题靠猜效率太低抓一次波形所有关于地址和帧结构的疑问都会消失。逻辑分析仪不必高档采样率 20MHz 的入门款足够看 100kHz 的 I2C 波形。抓完波形你会发现显示 8 这个字符时第 0 位的数据字节是0x0F和0x43这正好对应低 4 段全亮和高 4 段中 g、dp 两段熄灭的状态。以后遇到显示异常先用波形确认数据对不对再看硬件问题就分成两半了排查范围小得多。6.2 从显示到输入一个 I2C 地址还能读按键CH455 本身带按键扫描功能这也是当初选它而不是单纯用移位寄存器方案的原因。同一个 I2C 地址写方向是发显示帧读方向就能读回按键状态。硬件上按键接在 SEG 和 DIG 之间形成交叉矩阵CH455 扫描到闭合后把键值放在内部寄存器MCU 通过 I2C 读回来。驱动写法和显示类似把 HAL_I2C_Master_Transmit 换成 HAL_I2C_Master_Receive 即可只是读回的字节每个位代表一个按键位置。这部分没有在标题要求的范围内但如果你的面板既要显示又要按键CH455 一颗芯片就能把两件事都干了等于省掉一个独立按键扫描芯片。我上次做某跨平台系统的控制面板时就是先实现显示再顺手把按键扫描加上整个输入输出模块只占用两条 I2C 线外围干干净净。回顾整个调试过程我最深的教训是遇到通信类芯片先花十分钟确认地址和帧格式再动手写显示逻辑。省下的时间远比想象中多。纸上谈兵说「应该没问题」的每一处最后都变成了一次示波器探头、一颗上拉电阻、或是一行地址左移的修正。希望这篇笔记能帮你少走这几步弯路。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表