ARTICLE DETAIL

资讯详情

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

LPS28DFW状态轮询驱动:STM32 I2C读取压力与温度

LPS28DFW状态轮询驱动:STM32 I2C读取压力与温度 简介面向嵌入式开发者的LPS28DFW气压计开发工程基于STM32H503CB主控完整演示轮询获取气压与温度数据的实现思路。工程压缩包共190个文件大小3.67MB以C源码和头文件为主体配合编译生成的库文件、uvprojx/ioc工程配置以及PDF数据手册便于查看依赖关系与二次开发。代码从初始化I2C接口开始包含传感器设备ID校验、输出数据率与低通滤波器配置并在主循环中通过轮询方式读取气压和温度将原始数据转换为百帕和摄氏度后经串口输出。资源还保留了驱动分层与寄存器操作细节说明如何判断传感器就绪状态适合正在调试LPS28DFW或同类MEMS气压计的开发者可参考工程结构、接口封装和排错经验快速移植到自己的项目。无论是学习MEMS驱动开发还是完成产品原型验证都能从中获得直接帮助。已有79人学习下载。1. 为什么先做 LPS28DFW 的状态轮询而不是直接读中断在 STM32 这类小板上调试 LPS28DFW很多人第一个动作就是挂中断却忽略了一个事实10 Hz 的 ODR 下中断每秒最多触发 10 次和主循环里几十毫秒一次的轮询差别不大。轮询反而能省下一个 EXTI 通道也避开了中断服务函数里操作 I2C 时序不稳定的问题。这里不讲复杂理论直接给一套可复现的最小方案先认识 LPS28DFW 的寄存器分布再写一个可移植的 I2C 状态轮询驱动把压力、温度换算成可打印的物理量。新手可按步骤改到自己的板子老手可以直接摘走驱动骨架。后续要换中断方式或者接 SPI改动的只是最底层读写函数核心的状态判断逻辑不用动。2. LPS28DFW 寄存器地图轮询读取要认识的三个寄存器组2.1 WHO_AM_I 芯片识别所有移植的第一步拿到 LPS28DFW 小板第一步不是配 ODR而是读 WHO_AM_I。这个寄存器在地址 0x0F复位值固定为 0xB4。I2C 7-bit 地址常见是 0x5D具体取决于 SDO 引脚电平所以在很多小板上看到地址表和代码里不一样。用 I2C 读一下返回 0xB4 才说明总线上的设备确实是 LPS28DFW也验证了地址是对的。读 WHO_AM_I 的代码通常这样写uint8_t id 0; if (i2c_read_regs(0x5D, 0x0F, id, 1) ! 0) { printf(I2C transfer failed\n); } if (id ! 0xB4) { printf(Unexpected WHO_AM_I: 0x%02X\n, id); }这段代码演示了原理实际工程中会封装成带函数指针的驱动接口具体见下一章。WHO_AM_I 失败时读到 0xFF 一般是 SDA/SCL 接线错误或上拉电阻问题读到其他值像 0xB3 或 0xBA多半是 SDO 电平设错把另一个地址的设备当成 LPS28DFW 了。2.2 CTRL_REG1 与 ODR轮询频率的源头LPS28DFW 的输出速率由 CTRL_REG1 的 ODR 位决定。虽然寄存器里还有滤波和其他控制位写轮询程序时最需要的就是 ODR 这一组。常见的 ODR 映射如下低四位写 0x00 就是 1 Hz写 0x01 变成 10 Hz。这里不展开全部组合只要记住 10 Hz 是气压计小板最常用的起步值。ODR 配置值CTRL_REG1 低4位输出速率数据周期典型场景0x001 Hz1 s气象站、休眠模式0x0110 Hz100 ms高度计、手势识别0x0225 Hz40 ms快速姿态融合0x0350 Hz20 ms跌落检测0x0475 Hz~13.3 ms工业监测0x05100 Hz10 ms高采样率测试配置方法就是向 0x10 寄存器写入 0x01但要注意如果之前固件配置过滤波最好先读回 CTRL_REG1再修改低四位避免把其他位覆盖掉。我一般直接写因为出厂默认其他位是 0读回再改更保险uint8_t ctrl1; bus.read_regs(addr, 0x10, ctrl1, 1); ctrl1 (ctrl1 0xF0) | 0x01; // 保留高四位ODR设为10Hz bus.write_reg(addr, 0x10, ctrl1);高四位里包含 AVG 设置跟低功耗平均次数有关轮询场景下保持默认就行。ODR 值决定传感器内部什么时候更新数据寄存器也就决定轮询时状态位多长时间置一次 1。注意 LPS28DFW 默认上电后并不一定立即开始转换需要确认 CTRL_REG1 的最高位没有处于 Power-down 模式。大多数板子默认是连续模式所以写 ODR 后等一个周期就能读到数。2.3 STATUS 寄存器数据可用的唯一信号轮询和中断的区别就在这中断由 INT 引脚主动告诉 CPU轮询则是 CPU 主动看 STATUS 寄存器。LPS28DFW 的 STATUS 寄存器地址 0x7D里面 P_DA 位bit2表示压力数据已完成更新T_DA 位bit3表示温度已更新。只要这两个位是 1就能读对应的数据寄存器。读取数据寄存器之后状态位自动清零。轮询里不能只读压力不读状态。如果跳过了 STATUS 判断直接在每次循环里读 PRESSURE_OUT很可能读到上一条数据尤其在 ODR 还没稳定的时候会出现一条数据被重复读两次下一条数据被漏掉。判断 P_DA 的过程本质上是和传感器内部的新数据事件做同步。这里再给一个状态寄存器的位布局示例位名字含义3T_DA温度数据可用2P_DA压力数据可用1-0保留固定读 0状态位读取不会清除它只有读取对应的数据寄存器才清除。所以先读 STATUS 再读数据顺序不能反。在 I2C 上如果连续读多个寄存器要注意压力寄存器 0x28 和温度寄存器 0x2D 之间隔着保留地址不能把两个数据块放到一次连续读里否则读到的是保留值拼接出来的压力、温度全错。这个小细节在第 4 章写驱动时会再演示。3. 先写可移植 I2C 抽象层再初始化 LPS28DFW3.1 用函数指针封装的 I2C 读写接口在不同开发板上迁移驱动是嵌入式开发最常见的需求之一。STM32 用 HALESP32 用 ESP-IDFLinux 小板上可能是 ioctl。为了让 LPS28DFW 驱动在板子之间复用我一般把底层 I2C 操作用函数指针包一层typedef struct { int (*read_regs)(uint8_t i2c_addr, uint8_t reg, uint8_t *buf, uint16_t len); int (*write_reg)(uint8_t i2c_addr, uint8_t reg, uint8_t val); } lps28dfw_bus_t;参数说明read_regs从 i2c_addr 这个设备地址读取从 reg 开始的 len 个字节结果放到 buf。write_reg写单个寄存器适用于控制寄存器配置。返回值统一为 0 表示成功非 0 表示失败。有了这个抽象LPS28DFW 驱动本身不关心底层是模拟 I2C 还是硬件 I2C。在 STM32 上实现这个接口可能是这样static int stm32_read_regs(uint8_t addr, uint8_t reg, uint8_t *buf, uint16_t len) { return HAL_I2C_Mem_Read(hi2c1, addr 1, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 100); }注意这里把 7 位地址左移一位转成 8 位地址HAL 函数内部用它做设备寻址。错误码非零通常表示 NACK 或超时。在 Linux 下用 ioctl 的话read_regs 里就是打开 /dev/i2c-X 后调用 I2C_RDWR逻辑完全一样。3.2 最小初始化代码配置 ODR、准备数据输出初始化流程只需要四步确认设备、等待上电、软复位、配置 ODR。LPS28DFW 的量程固定为 260~1260 hPa没有量程选择寄存器所以不需要像加速度计那样配 2g/4g。软复位不是必须的但如果设备被之前代码配置到奇奇怪怪的状态软复位能让寄存器回到默认值。#define LPS28DFW_ADDR 0x5D #define LPS28DFW_WHO_AM_I 0x0F #define LPS28DFW_WHO_AM_I_VAL 0xB4 #define LPS28DFW_CTRL_REG1 0x10 #define LPS28DFW_CTRL_REG2 0x11 int lps28dfw_init(lps28dfw_bus_t *bus) { uint8_t id; if (bus-read_regs(LPS28DFW_ADDR, LPS28DFW_WHO_AM_I, id, 1) ! 0) { return -1; // I2C 通信失败 } if (id ! LPS28DFW_WHO_AM_I_VAL) { return -2; // 芯片识别异常 } // 软复位CTRL_REG2 bit0 写 1之后自动清零 bus-write_reg(LPS28DFW_ADDR, LPS28DFW_CTRL_REG2, 0x01); delay_ms(10); // ODR 10HzAVG 等保持默认 bus-write_reg(LPS28DFW_ADDR, LPS28DFW_CTRL_REG1, 0x01); return 0; }这段代码的返回值含义0初始化完成可以开始轮询。-1I2C 读写层面出错先查上拉电阻和引脚复用。-2芯片地址或类型不对检查 SDO 引脚和硬件焊接。软复位位用 bit0 是这代压力传感器常见的定义如果你的子板手册上不一致可以跳过这行不影响后续轮询。初始化里注意一点软复位之后立刻写 ODR可能因为芯片还没完成复位而丢寄存器值。所以 delay 10~20ms 再写。如果你的固件跑得很紧建议在等待后回读一次 CTRL_REG1确认写入成功。3.3 初始化失败时的诊断步骤上电以后遇到读不到 ID不用急着查原理图。按下面的顺序做一次预检查用 I2C 扫描工具列出总线上所有地址看 LPS28DFW 到底在哪个地址。如果扫描得到 0x5D 和 0x5C 两个地址中的一个说明 SDO 定义和预期不一样改代码里的 LPS28DFW_ADDR 即可。如果扫描不到用万用表量 SDA/SCL 空闲电平低于 2.5V 说明上拉电阻没装或没接 VDD。确认供电电压和 I/O 电平匹配电平不匹配会导致 NACK。这一套做完90% 的初始化失败都能定位。剩下 10% 是芯片本身过热或者引脚虚焊只能换板。4. 轮询循环里读压力与温度状态判断、拼接和换算4.1 用 P_DA 位判断本次数据是否有效初始化完成后系统跑一个主循环。每次循环先读 STATUS 寄存器检查 P_DA 位#define LPS28DFW_STATUS 0x7D #define LPS28DFW_PRESS_OUT_XL 0x28 #define LPS28DFW_TEMP_OUT_L 0x2D #define LPS28DFW_P_DA_MASK 0x04 int lps28dfw_poll_once(lps28dfw_bus_t *bus, lps28dfw_data_t *out) { uint8_t status; if (bus-read_regs(LPS28DFW_ADDR, LPS28DFW_STATUS, status, 1) ! 0) { return -1; // I2C 错误 } if (!(status LPS28DFW_P_DA_MASK)) { return 1; // 数据还没准备好本次轮询无新数据 } // 下面继续读压力温度 }这段逻辑是状态轮询的核心返回 1 不代表失败只代表传感器还没更新。调用方可以选择忙等也可以隔几毫秒再调用。注意 STATUS 里的 P_DA 位是“新数据就绪”信号读一次数据后自动清 0。如果你在 ODR10Hz 下以 5ms 周期轮询第一次读到 P_DA1 并读取数据那么接下来 95ms 内 P_DA 都是 0直到下一个新数据产生。4.2 24 位压力寄存器拼接与 hPa 换算LPS28DFW 的压力原始值分布在三个连续寄存器 PRESS_OUT_XL/L/H 中地址从 0x28 开始。这三个字节组成一个 24 位的二进制补码。但芯片内部有效数据是 20 位放到 24 位寄存器时低 4 位固定为 0所以实际换算要除以 4096。int32_t raw_p; uint8_t p[3]; if (bus-read_regs(LPS28DFW_ADDR, LPS28DFW_PRESS_OUT_XL, p, 3) ! 0) { return -2; } raw_p (int32_t)((uint32_t)p[0] | ((uint32_t)p[1] 8) | ((uint32_t)p[2] 16)); if (raw_p 0x800000) { raw_p | 0xFF000000; // 扩展符号位 } out-pressure_hpa raw_p / 4096.0f;说明p[0] 是最低有效字节p[2] 是最高有效字节顺序不能换。24 位补码需要手动做符号扩展否则负压值会被解释成 8 百多万的大正数。除以 4096 是因为低 4 位没有信息量。有些例程除以 4096 得到 hPa也有除以 16 得到 LSb/hPa看手册定义这里用小数值输出更直观。为什么强调不能把压力温度放在一次连续读里因为 0x28 后面到 0x2D 之间有空寄存器如果一次读 5 个字节第 3、4 字节来自保留区拼出来的温度将会是乱码。所以驱动里分成两次读中间即使发生一点时间差也因为没有使用 BDU 而导致温度与压力来自不同采样周期。对于轮询场景压力温度都是毫秒级变化这种错位可以接受。4.3 温度读出与打印格式设计温度寄存器地址 0x2D 起连续 2 字节低字节在前。换算公式是原始 16 位补码除以 100得到摄氏度。int16_t raw_t; uint8_t t[2]; if (bus-read_regs(LPS28DFW_ADDR, LPS28DFW_TEMP_OUT_L, t, 2) ! 0) { return -3; } raw_t (int16_t)((uint16_t)t[0] | ((uint16_t)t[1] 8)); out-temperature_c raw_t / 100.0f;温度数据的分辨率是 0.01°C所以 25.00°C 在寄存器里是 2500。打印时用%.2f保留两位小数。压力也用%.2f单位 hPa1 hPa 对应 1 mbar。主调用处的完整轮询循环while (1) { int ret lps28dfw_poll_once(bus, data); if (ret 0) { printf(P%.2f hPa, T%.2f C\n, data.pressure_hpa, data.temperature_c); } else if (ret 1) { // 没有新数据做别的事或短延时后再轮询 } else { // 处理 I2C 错误 } delay_ms(10); }这里用 delay_ms(10) 控制轮询率。在 10Hz ODR 下数据周期 100ms10ms 轮询一次相当于每周期做 10 次状态查询CPU 占用不大也够实时响应。5. 轮询周期与 ODR 的匹配高轮询率下的数据更新错位问题5.1 轮询率测试为什么 ODR10Hz 时轮询周期要大于 100ms有人把轮询率调到 1ms以为读得越快数据越新。实际不是这样。LPS28DFW 内部数据更新时间由 ODR 决定ODR10Hz 时每 100ms 才能产生一个新压力样本。轮询周期是 1ms意味着同一份数据会被读 100 次而状态位只在新的采样完成后翻转。如果代码不判断 P_DA输出会看起来“没更新”还会白白占满 I2C 总线影响其他传感器。更好的办法是让轮询周期略小于数据周期比如 ODR10Hz 用 10ms 轮询。这样每个数据周期内平均会查询 10 次第一次发现 P_DA1 就取走数据剩下的查询会看到 P_DA0直到下一个周期开始。下表给出实际调试中常用的搭配LPS28DFW ODR数据周期推荐轮询周期说明1 Hz1000 ms100~200 ms省电场景10 Hz100 ms10~50 ms高度计默认25 Hz40 ms5~10 ms需要快速响应50 Hz20 ms2~5 ms注意 I2C 速率100 Hz10 ms1~2 ms建议使用 FIFO轮询周期太短副作用明显状态位读取频繁I2C 上产生大量读 STATUS 的 ACKCPU 占用率反而更高。关键指标是“新数据被取走的平均延迟”不是查询次数。轮询率为 10ms 时平均最大延迟约 10ms轮询率为 1ms 时平均延迟约 0.5ms但代价是 10 倍的 I2C 流量。多数气压计应用对 10ms 延迟完全不敏感所以不必追求 1ms 轮询。5.2 三种常见轮询方案及其适用场景第一种是查询后忙等while (1) { while (lps28dfw_poll_once(bus, data) 1) { delay_ms(2); } process(data); }这个方案简单但会在等待状态位时阻塞主循环适合单传感器任务。第二种是固定节拍查询主循环以固定周期调用 poll_once返回值是 0 才处理适合和 RTOS 的 tick 任务结合。第三种是注册到定时器回调里做一个“软中断”查询节拍由定时器保证适合轮询周期要求严格的应用。我一般选择第二种因为代码结构清晰也能和其他传感器轮询共用同一个主循环。轮询本身并不一定比中断差真正决定实时性的是“查询到数据到处理数据”之间的路径长度。如果主循环里还有其他耗时操作比如打印调试日志至少不要让 printf 出现在状态判断之前否则每次都会拖长查询节拍。5.3 排查读回全 0xFF、压力不变、数据跳变轮询调通后遇到最多的问题有三个。读回全 0xFF大概率是 I2C 总线上拉了。如果 STATUS 也读到 0xFF多半是 SDA 保持高电平导致读不到设备。可以在每次读取后加一个短延时或者检查 SCL/SDA 是否有 4.7kΩ 上拉到 VDD。压力不变先看 P_DA 是否一直为 0如果是确认 CTRL_REG1 有没有被误写成 0x00。0x00 对应 1Hz不是关闭但如果芯片进入了 power-down 模式状态位永远不会置位。再检查是不是软复位后没等 10ms 就写了 ODR。数据跳变看符号扩展。raw_p是 int32_t 时如果只做位运算没做符号扩展负压会显示成超大正值。另一个原因是压力温度分两次读期间没有 BDU恰好在读压力高字节时内部更新了数据造成高低字节来自不同时刻这就是数据跳变。解决方法是开启 BDU如果手册支持或者在 P_DA1 后尽快读完三字节并在下一次读取时用两次读数做差值校验。6. 轮询读取的压力稳定性优化滑动平均滤波与连续读技巧6.1 在轮询线程里直接做滑动平均气压计原始数据在 10Hz 下往往有 0.1~0.3 hPa 的随机跳动这对高度测量来说相当于 1~3 米误差。直接打印原始值看起来“不稳定”但这不是芯片坏而是空气本身的湍流和传感器噪声。用一个宽度 8 的滑动平均能把峰值跳动压到一半以下。#define FILTER_LEN 8 static float pressure_fifo[FILTER_LEN]; static uint8_t pressure_idx 0; float lps28dfw_pressure_filter(float new_value) { pressure_fifo[pressure_idx] new_value; pressure_idx (pressure_idx 1) % FILTER_LEN; float sum 0.0f; for (uint8_t i 0; i FILTER_LEN; i) { sum pressure_fifo[i]; } return sum / FILTER_LEN; }使用时在 lps28dfw_poll_once 返回 0 后把 pressure_hpa 传入该函数用返回值显示。FILTER_LEN 是过滤深度8 次在 10Hz 下对应 0.8 秒窗口窗口越大越平滑但对上升沿的响应越迟钝。如果做飞行高度计不要超过 16否则拉杆时高度变化会滞后明显。6.2 用连续读减少重复进入临界区的开销轮询驱动里每次调用 read_regs 都可能触发一次 I2C 启动和停止条件。LPS28DFW 在 I2C 上支持多字节读压力三个字节可以一次读完。如果总线上还有别的传感器共享同一条 I2C可以考虑把压力和温度合并成一次事务。前面说过 I2C 地址不连续不能直接合并但改用 SPI 后地址可以自由递增0x28 到 0x2D 段里的保留地址直接跳过即可。在 I2C 模式下我一般保持两次读不做多余优化。合并要维护一个映射表带来的代码复杂度比省下的几微秒更高。真正值得优化的是轮询率和滤波而不是寄存器读取次数。验证优化效果时把代码放在串口上打印滤波前、滤波后的数值连续观察 20 秒看峰峰值是否从 0.4 hPa 降到 0.1 hPa。如果变化不大优先检查电源纹波和传感器是否靠近热源。本文还有配套的精品资源点击获取
返回列表