ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

GD32H759实战:RT-Thread下I2C与RTC驱动开发详解

GD32H759实战:RT-Thread下I2C与RTC驱动开发详解 GD32H759 这套工控项目写到第 5 篇了前面几篇把串口、GPIO、定时器这些基础外设都打通了跑 RT-Thread 的系统框架也稳定下来。这篇要解决的是两个现场设备绕不开的问题一个是跟外部传感器、存储芯片通信用的 I2C一个是设备断电之后还能继续走时的 RTC。GD32H759 在这两块上的硬件底子很好RT-Thread 也提供了现成的设备驱动框架但真正接进项目里还是会踩到不少坑这篇把 I2C 和 RTC 从硬件特性、驱动适配到调试方法完整过一遍适合正在做 GD32H7 系列工控板、或者想搞明白 RT-Thread 设备驱动怎么写的人参考。1. I2C 和 RTC 在工控实战里的定位1.1 为什么 I2C 在工控板里这么常见工控板上的外设五花八门温度传感器、湿度传感器、EEPROM、触摸屏控制器、数字电位器、电源管理芯片很多都选择了 I2C 接口。原因很简单它只占两根线SCL 和 SDA总线上可以挂几十个设备靠地址区分不需要像 SPI 那样每个设备都要单独拉一根片选线。对于 PCB 面积紧张、走线受限的工控板来说这个优势太实用了。I2C 的速度虽然不如 SPI 和并行总线但工控场景里大部分传感器和存储芯片的数据量都不大几十上百字节的读写400kHz 的快速模式跑起来完全够用。GD32H759 自带的硬件 I2C 控制器支持主机模式、从机模式、多主机模式还能配合 DMA 和中断工作省掉的空闲等待时间在实时应用里很关键。我目前的项目里用 I2C 接了 EEPROM 存配置参数还接了电容触摸屏控制器总线上挂两个设备实测下来很稳。这里顺便提一下 PMBus它是基于 I2C 物理层定义的一套电源管理协议在数字电源模块里很常见。如果你的板子上有 PMBus 接口的电源芯片底层通信还是走 I2C只是地址分配、命令格式和错误检测有专门规范。搞懂了 I2C 时序后面再看 PMBus 就是水到渠成的事。1.2 RTC 不只是“一个会走的表”把 RTC 单独拿出来写一篇是因为它在工控项目里的作用被很多人低估了。设备的运行日志要时间戳故障记录要时间戳定时维护和固件升级窗口要时间故障黑匣子分析更要时间。如果控制器重启之后时间归零那这些数据全都失去意义。GD32H759 的 RTC 是完整的日历型 RTC和我们以前在 GD32F103 上常用的简单秒计数器完全不是一回事。它内部有专门的电源域由 VBAT 引脚供电主电源 VDD 断掉之后只要 VBAT 还接着电池或者超级电容RTC 就会继续运行功耗非常低整个 VBAT 域处于极低功耗状态包括了 RTC 和 LSE 振荡器都在这个域里。这一点对现场设备特别重要因为很多工控设备本质上是“断电关机的”但每次开机时间都从 1970 年开始这谁能忍。除此之外RTC 还带闹钟、周期性唤醒、时间戳捕获、数字校准等功能。工控里最常见的用法是闹钟唤醒低功耗模式比如设备每一分钟醒来一次采集数据再睡回去。这些功能的实现和普通计时不同寄存器配置也复杂一些但用熟之后能省下不少系统功耗。这篇主要讲日历时间和掉电保持重点把“让时间在断电后继续走”这件事彻底做明白。2. GD32H759 I2C 总线实现与 RT-Thread 框架接入2.1 I2C 协议本质和硬件侧要点I2C 的物理层是开漏结构SCL 和 SDA 两根线都需要外部上拉电阻。因为开漏任何设备都可以把线拉低协议里起始条件、停止条件、应答信号都是靠拉低电平实现的。也正因为开漏主机和从机之间不存在电平冲突一根线上可以安全挂多个设备。先看一张简单的对比表能更清楚 I2C 在总线家族里的定位总线信号线数量寻址方式典型速率适用场景UART2TX/RX无寻址点对点115200bps~几Mbps串口屏、调试、模组通信SPI3NMOSI/MISO/SCK/CS片选线区分设备几MHz~几十MHzFlash、ADC、高速传感器I2C2SCL/SDA7位或10位地址100kHz/400kHz/1MHz传感器、EEPROM、电源管理上拉电阻的取值是第一个容易被忽略的坑。电阻太大信号上升沿被电容拉长总线频率一高波形就变圆通信容易误码电阻太小灌电流变大从机可能拉不动或者功耗超标。常规做法是 4.7kΩ 起步总线设备多、线长的时候改用 2.2kΩ 或者 1kΩ。我的经验是工控板上走线往往比较长建议先按 2.2kΩ 设计实测波形再调整。I2C 标准规定总线电容一般不能超过 400pF在强干扰的现场可以适当增大上拉强度来改善抗干扰能力但不要低于 1kΩ。GD32H759 的硬件 I2C 控制器提供了完善的错误检测机制包括总线错误、仲裁丢失、ACK 失败、溢出和超时检测。启用超时检测很重要如果总线上某个从机卡死把 SDA 拉低了没有超时机制的话主机会一直死等下去整个任务就挂了。2.2 RT-Thread I2C 设备驱动框架拆解RT-Thread 的设备框架把 I2C 抽象成了一个标准设备核心数据结构是rt_i2c_bus_device对外提供rt_i2c_transfer()、rt_i2c_master_send()、rt_i2c_master_recv()这些接口。你只需要找到一个已经注册好的 I2C 总线设备然后向它发送消息即可不需要直接操作寄存器。消息结构rt_i2c_msg是理解这个框架的关键一个 msg 包含从机地址、读写标志、数据缓冲和长度。一次完整的通信可以是一个 msg也可以是多个 msg 组合成的“事务”比如读 EEPROM 时要先写寄存器地址再读数据就可以用两个 msg 组合框架会在连续传输时不产生停止条件保证从机端看成一次完整的访问。struct rt_i2c_msg { rt_uint16_t addr; /* 从机地址7位 */ rt_uint16_t flags; /* 读写标志 RT_I2C_WR / RT_I2C_RD */ rt_uint16_t len; /* 数据长度 */ rt_uint8_t *buf; /* 数据缓冲 */ };驱动层的核心是 ops 结构里的master_xfer函数所有 I2C 消息最终都会走到这个函数里。底层的 GD32 I2C 驱动要做的事情就是遍历消息数组逐条按硬件时序发送起始条件、地址、数据和停止条件。RT-Thread 的 I2C 驱动框架本身是一个中间层不关心底层是什么芯片所以只要把 GD32H759 的硬件 I2C 适配好上层应用就能用统一 API 访问。2.3 工程配置、GPIO 复用和总线注册我用的 IDE 是 RT-Thread Studio工程配置里需要打开 I2C 设备驱动框架。在 RT-Thread Settings 的组件配置中进入 Device Drivers勾选 I2C device drivers然后重新构建工程。这一步只是把框架代码编进去真正的硬件初始化还要自己写。GD32H759 的引脚都有复用功能同一个外设可以映射到不同的引脚组。具体该选哪组要查芯片数据手册里的复用功能表不同封装引脚的可用性还不一样。我的建议是拿到板子原理图之后先锁定电源、晶振、调试串口占用了哪些引脚再为 I2C 选一组不冲突的复用引脚。初始化代码大致是这样#include gd32h7xx.h static void i2c_gpio_init(void) { /* 使能 GPIO 和 I2C 外设时钟 */ rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_I2C0); /* 引脚复用为 I2C0具体 AF 号以手册为准 */ gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_8 | GPIO_PIN_9); gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_8 | GPIO_PIN_9); gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_8 | GPIO_PIN_9); } static void i2c_periph_init(void) { /* 使用外部上拉所以内部上拉可以不使能也可以同时使能增强波形 */ i2c_clock_config(I2C0, 400000, I2C_DTCY_2); i2c_enable(I2C0); }注意 GPIO 模式必须配成开漏复用代码里的GPIO_OTYPE_OD就是开漏输出。如果误配成推挽输出外部上拉会直接影响输出高电平而且多设备共线时会出问题这是新手必踩的坑。底层驱动做好了之后还要把总线设备注册到 RT-Thread 里struct rt_i2c_bus_device_private i2c0_priv; static const struct rt_i2c_bus_device_ops i2c0_ops { rt_hw_i2c_master_xfer, RT_NULL, RT_NULL }; int rt_hw_i2c_init(void) { i2c_gpio_init(); i2c_periph_init(); i2c0_priv.ops i2c0_ops; i2c0_priv.intr_flags 0; rt_i2c_bus_device_register(i2c0_priv.bus, i2c0); return RT_EOK; } INIT_BOARD_EXPORT(rt_hw_i2c_init);注册完成后应用层就可以用rt_device_find(i2c0)找到这个总线设备了。2.4 例程用 I2C 读写 AT24C02拿 AT24C02 来实操最合适它几乎是 I2C 外设里的“Hello World”地址简单、时序直观而且非常适合存配置参数。它的器件地址是 0xA08 位模式换算成 I2C 7 位地址就是 0x50要和 EEPROM 的 A0/A1/A2 引脚电平对应。这个地址换算是第一处容易出问题的地方后面排查章节再细说。读写 EEPROM 的时序是 I2C 综合应用的典型例子。写一个字节要先发设备地址加写标志再发目标字节地址最后发数据。读数据要先发一个“伪写”序列指定字节地址然后重新发起起始条件再发设备地址加读标志之后从机才会把数据放到总线上。用 RT-Thread 的接口实现随机读static rt_err_t at24c02_read_byte(struct rt_i2c_bus_device *bus, rt_uint8_t dev_addr, rt_uint16_t mem_addr, rt_uint8_t *data) { struct rt_i2c_msg msgs[2]; rt_uint8_t addr_buf[2]; addr_buf[0] (rt_uint8_t)(mem_addr 8); addr_buf[1] (rt_uint8_t)(mem_addr 0xFF); msgs[0].addr dev_addr; msgs[0].flags RT_I2C_WR; msgs[0].buf addr_buf; msgs[0].len 2; msgs[1].addr dev_addr; msgs[1].flags RT_I2C_RD; msgs[1].buf data; msgs[1].len 1; return rt_i2c_transfer(bus, msgs, 2); }这里两个 msgs 组成一个事务中间不会插入停止条件对 EEPROM 来说这就是一次“组合传输”。如果两个 msg 之间被拆开中间出现 STOP 条件AT24C02 就认为前面是写入操作后面的读操作会读到错误的位置。RT-Thread 框架保证一次rt_i2c_transfer调用内的多个 msg 连续执行这也是推荐用组合消息而不是分别调两次 send/recv 的原因。写完之后要注意 EEPROM 的内部写周期AT24C02 大概要等 5ms期间不响应任何命令。简单粗暴的方案是每次写完延时 5ms更高效的做法是写完轮询设备 ACKAT24C02 在内部写周期时不会应答连续发送地址直到收到 ACK 就代表写完了。调试的时候如果感觉写入偶尔失败大概率就是没有等写周期。3. RT-Thread 下的 RTC 驱动与掉电保持设计3.1 VBAT 电源域与 RTC 电路设计前面说过GD32H759 的 RTC 和 LSE 振荡器位于独立的 VBAT 电源域主电源 VDD 掉了之后这一小片电路由 VBAT 引脚继续供电。这个域消耗的能量极低包含 RTC 和 LSE 振荡器的整体电流通常在微安级别一颗 CR2032 纽扣电池能让它跑几年。硬件设计上首先要把 VBAT 引脚接好不能悬空。最稳妥的方案是 VBAT 接一个 3V 纽扣电池或者可充电的超级电容。如果项目里本来就有常备电源也可以用二极管隔离把 VBAT 和 VDD 接在一起但要考虑压降常规 1N4148 二极管压降约 0.6V3.3V 电源经过二极管后只有 2.7V 左右够 GD32H759 的 VBAT 最低工作电压吗设计时务必查手册确认。更保险的做法是用理想的 OR-ing 电路或者低漏电的开关方案防止电池被主电源反充也会防止主电源掉电时电池电流倒灌到其他电路。LSE 晶振是 RTC 的时钟源工控板上要选 32.768kHz、负载电容和 ESR 参数合适的晶振并且在 PCB 布局时尽量靠近芯片 VBAT 域引脚。PCB 上 LSE 的两根走线要避免与高速信号、PWM 走线平行靠近不然现场强干扰环境下晶振很容易停振。晶振两端的匹配电容取值要看晶振手册通常 6pF~12.5pF 之间这里有一个普遍规律匹配电容偏小会导致频率偏高时间跑快匹配电容偏大时间就跑慢。实际项目中如果发现一天快慢几秒先用示波器测量晶振频率再调整匹配电容。3.2 RT-Thread RTC 设备接入和时间维护RT-Thread 的 RTC 设备框架用起来很清爽底层驱动注册好之后应用层只需要用标准设备接口访问。设备名通常是rtc核心操作就是把时间戳读出来或者写进去。rt_device_t rtc_dev rt_device_find(rtc); if (rtc_dev ! RT_NULL) { time_t now 1700000000; rt_device_control(rtc_dev, RT_DEVICE_CTRL_RTC_SET_TIME, now); time_t read_back 0; rt_device_control(rtc_dev, RT_DEVICE_CTRL_RTC_GET_TIME, read_back); }底层驱动要做的事是把time_t换算成 RTC 寄存器能表达的年月日时分秒。GD32H759 的 RTC 寄存器存储的是 BCD 格式的日历信息而 RT-Thread 传递的是 Unix 时间戳也就是从 1970 年 1 月 1 日开始的秒数所以驱动里需要来回转换。转换逻辑如果自己写要注意闰年判断、月份天数、BCD 码转换这些细节不建议手写最好复用标准 C 库的gmtime()和mktime()或者直接参考 ST/兆易官方例程里的转换函数。调试阶段在控制台直接输入date命令就能看当前时间设置时间用date -s加一个时间字符串前提是系统里挂载了 RTC 设备驱动。我第一次调这个的时候以为设置完时间后立刻读出来就完事了结果发现date命令显示的其实是软件time()函数返回的系统时间和 RTC 硬件时间是两个层面的东西。RTC 驱动初始化时要同步一次系统时间系统运行中会靠软件定时器更新掉电重启后系统时间才从 RTC 恢复。这里要注意时区问题。RT-Thread 内部通常按 UTC 时间戳存储而我们在东八区硬件 RTC 里存的是本地时间还是 UTC 时间项目里必须有一套约定。我的做法是 RTC 硬件保存 UTC开机时由应用层根据本地时区偏移换算。如果只是国内设备直接在 RTC 驱动里存东八区本地时间也能跑但一旦要考虑多时区部署就会埋雷。另一个边界是 32 位时间戳的 2038 年问题GD32H759 的 RTC 寄存器是 32 位秒计数的话迟早会撞上工控设备生命周期动辄十年起规划时要心里有数。3.3 备份寄存器、复位标志和首次上电判断RTC 这个电源域里还有一小块备份寄存器GD32H759 同样支持用于在主电源掉电后保存少量关键数据。最典型的应用场景是判断“RTC 是否已经被初始化过”。每次设备上电软件都要检查 RTC 是否有效。如果 RTC 已经走起来了就直接读取时间跳过初始化如果发现 VBAT 域发生了复位比如电池耗尽后重新换电池或者备份寄存器里的标志位不对那就必须执行完整的 RTC 初始化和时间设置流程。否则每次上电都会把 RTC 重新初始化一遍之前调好的时间全被重置。我一般会在备份寄存器里固定存一个标志值#define RTC_INIT_FLAG 0xA5A5 if (bkp_flag_read() ! RTC_INIT_FLAG) { /* 首次上电或备份域复位需要初始化 RTC 并设置时间 */ rtc_init(); set_default_time(); bkp_flag_write(RTC_INIT_FLAG); } else { /* RTC 已经在走直接读取当前时间 */ rtc_get_time(current_time); }判断备份域是否复位还可以通过读 RTC 复位标志寄存器来实现GD32 系列不同型号的寄存器命名有差异以芯片参考手册为准。这里要提醒一点备份域寄存器的访问必须保证 VBAT 域时钟和备份域接口使能在低功耗模式下尤其要小心否则读回来的全是默认值会误判成“从未初始化”。3.4 RTC 时间跳变和校准技巧RTC 跑久了出现时间偏移几乎是所有工控项目都会遇到的问题。32.768kHz 晶振本身有初始误差温度变化还会引起漂移ppm 级别的误差一天下来就是零点几秒到几秒不等。GD32H759 的 RTC 提供了数字校准功能原理是在一定时间窗口内对时钟进行加减脉冲从而微调走时快慢。校准目标可以用普通秒表去对也可以用一个独立的高精度时间基准比如 GPS 授时模块或者 NTP 服务器来对比。具体做法是先让 RTC 跑上 24 小时记录偏差秒数再换算成 ppm最后把校准参数写进 RTC 的校准寄存器。更工程化的方案是在系统层面做时间同步。设备在联网状态下每天定时向 NTP 服务器同步一次就像给 RTC 每天对一次表。工控现场如果内网有授时服务器这是成本最低的方案。如果没有网络就用外部高精度 RTC 芯片比如 DS3231内部带温度补偿晶振精度能做到一年只差几分钟I2C 接口正好和本文前半部分的 I2C 章节呼应上了。选型的时候看设备是“能接受一天几秒误差”还是“必须一月几秒误差”再决定用内部 RTC 加校准还是直接上外部补偿芯片。4. 调试实录常见问题与排查速查表4.1 I2C 总线死锁和 ACK 异常调试 I2C 遇到的第一个问题往往是总线死锁现象是rt_i2c_transfer返回值一直不对逻辑分析仪抓不到任何波形或者 SDA 始终是低电平。排查方向按顺序来先测静态电平确认 SCL 和 SDA 空闲时都是高电平如果不是高说明有设备把线拉住了挨个断开从机寻找“肇事者”。第二个常见问题是设备有响应但传输中途出错比如读回来的数据全是 0xFF。这种情况多半是地址配错了。AT24C02 的 8 位地址是 0xA0I2C 主机层如果要填 7 位地址就是 0x50有些驱动里直接把 0xA0 直接减去 1 就当 7 位地址用数学上没问题但新手容易在左移右移上绕晕。RT-Thread 的rt_i2c_msg.addr明确是 7 位地址写 0x50 而不是 0xA0这个坑我踩过不止一次。第三个问题是从机无响应或 ACK 失败原因通常是从机地址错误、上拉电阻过强或过弱、通信速率过快从机跟不上。排查时先用 100kHz 标准模式试通信把速率降下来能通信了再逐步提高。有些从机芯片对上升沿时间有要求100kHz 和 400kHz 模式下的时序参数不同不能用一套配置通吃所有设备。还有一个容易被忽略的点是总线上多个设备速率不同混合挂载时要按最慢的那个设备来配置总线速率。4.2 逻辑分析仪看 I2C 波形的方法没有逻辑分析仪调 I2C就像蒙着眼睛绣花。市场上几十块钱的 8 通道逻辑分析仪就够用关键是会看波形。抓数据时要确认触发条件设置为 I2C 起始条件这样每次通信开始都会捕获整帧数据。看 I2C 波形主要盯几个点起始条件必须是 SCL 高电平时 SDA 由高变低停止条件则是 SCL 高电平时 SDA 由低变高。地址字节是 7 位地址加 1 位读写位读写位是低表示写高表示读。第九个时钟周期是从机的 ACK 位正常应该是低电平如果这个位置一直保持高说明从机没有响应。我调试 GT911 触摸屏时遇到过一次很典型的问题设备地址明明是对的SCL 和 SDA 静态电平和时序图都正常但就是读取失败。用逻辑分析仪抓完之后发现从机 ACK 之后主机这边还在继续发时钟而从机已经释放总线了原因是代码里读操作的最末尾多发了一个 ACK 和 STOP。这种问题看代码很难发现拿着逻辑分析仪对照时序图一眼就能定位。4.3 RTC 时间丢失和晶振停振RTC 最让人头疼的问题是“明明调好了时间断电重启后又回到默认值”。排查的第一步是量 VBAT 引脚的电压。常见故障包括VBAT 引脚悬空、电池已经耗尽、二极管压降导致电压低于 RTC 工作范围。用万用表量一下 VBAT 对地电压就能排除一大半问题。第二步是确认 LSE 晶振是否起振。用示波器探头测 OSC32_IN 引脚应该能看到 32.768kHz 的正弦波或方波。这里要注意示波器探头的负载电容会影响晶振频率最好用 x1 档有源探头或者低电容探头普通 x10 探头也能看个大概。如果晶振完全没有波形检查匹配电容是否选择正确、晶振是否虚焊、晶振引脚对地是否存在漏电。有时候 PCB 清洗不干净助焊剂残留会让晶振停振这种故障很隐蔽。第三步是检查代码里的备份域复位标志。GD32H759 的 RTC 在上电瞬间可能产生备份域复位如果软件没有正确处理会导致 RTC 时间被清零。正确做法是用备份寄存器标志位判断初始化状态配合读取复位标志寄存器区分“真正的首次上电”和“意外复位”特别要排除看门狗复位、软件复位对备份域造成的干扰。4.4 排查速查表下面这张表是我实际维护项目时总结的速查表遇到问题按照表格逐项检查能省下不少时间现象可能原因排查方法解决建议I2C 无波形SDA/SCL 为低从机拉死总线、无上拉断开从机测静态电平去掉问题设备检查上拉电阻I2C 波形正常但无 ACK地址错误、从机未上电、速率过快逻辑分析仪看第九个时钟核对 7 位地址降速到 100kHz读 EEPROM 返回 0xFF器件地址错误、写周期未等待检查地址位轮询 ACK用 7 位地址 0x50等待 5msI2C 随机丢失数据干扰、上升沿过缓看波形边沿检查线长减小上拉电阻加滤波器RTC 掉电时间丢失VBAT 无电、备份域复位测 VBAT 电压查复位标志接电池写备份标志RTC 走时不准晶振误差、匹配电容不对测 32.768kHz 频率调整匹配电容做数字校准RT-Thread 读取 RTC 失败驱动未注册、设备名不对list_device查看设备确认注册的 rtc 设备存在date 命令时间不更新系统时间与 RTC 未同步检查驱动同步逻辑运行date命令手动同步4.5 调试工具箱推荐最后分享一个调试工具箱是我这几年做嵌入式养成的工作习惯一个 8 通道逻辑分析仪软件带 I2C 协议解析抓波形之后直接显示数据内容是排查 I2C 问题的主力工具一个带频率测量功能的示波器用于测 LSE 晶振频率和看信号质量一盒不同阻值的贴片电阻从 1kΩ 到 10kΩ 都备上现场调上拉电阻非常必要一台可调直流电源用来模拟 VBAT 掉电场景方便反复验证 RTC 掉电保持逻辑还有万用表测静态电平、VBAT 电压、电池静态电流都离不开它。我个人在实际操作中的一个体会是I2C 和 RTC 这类问题不要一上来就埋头改代码先把示波器和逻辑分析仪架起来看波形数据会告诉你真相。示波器看 RTC 晶振时尽量用 x10 探头减少负载对频率的影响逻辑分析仪采样率设置高一点I2C 协议解析出来的数据才可靠。这套设备做 I2C 和 RTC 的基础已经牢固了接下来我打算把以太网和现场总线接口也整理了到时候可以把时间同步和远程故障上报串起来那才是一个完整工控节点的样子。
返回列表