
简介基于STM32与MS5837水压传感器的完整测量例程面向嵌入式开发者以及水下测量、潜水设备、水文监测等应用场景。工程通过I2C总线读取MS5837的压力与温度数据完成压力到水深换算和上电校准适合用来掌握I2C通信时序、传感器复位与转换流程、压力温度标定方法以及STM32标准外设库的工程组织方式。压缩包共包含85个文件以C源码和头文件为主具体涵盖MS5837驱动、IIC读写、24CXX存储、定时器、LED、按键、USART等模块另有Keil工程文件、启动文件、HEX固件和工程清理脚本整体约330KB目录清晰便于直接打开编译和烧写验证。目前已有1300人学习下载。代码按HARDWARE、CORE、SYSTEM、USER及STM32F10x_FWLib分层组织不仅可作为学习I2C总线时序、上电校准算法与嵌入式C开发的参考资料还能帮助开发者在MS5837压力变送、水深测量等类似产品上快速移植复用。1. 为什么 STM32 工程里选 MS5837 而不是普通模拟水压传感器STM32 做水压测量时很多人会先想到模拟压力变送器4~20 mA 或 0.5~4.5 V 输出再接运放和 ADC。这个方案不是不行而是麻烦供电纹波、基准电压偏差、零点漂移都会直接进结果校准一次还要换环境重测。MS5837 这类数字水压传感器把压力敏感元件、24 位 ADC、温度传感器和出厂标定系数封装在 3.3×3.3 mm 的小体积里读出来的是 D1/D2 原始值再结合 PROM 里的 6 个系数换算成 mbar 压力绕开了整个模拟信号链。这里按“水压传感器32例程_stm32_MS5837_水压传感器_”这个参考工程的名字拆开讲从 I2C 命令时序、PROM 系数换算到换芯片后的移植和校准适合正在做水位监测、鱼缸水压、潜水平台或者拿 MS5837 做基于 STM32 毕业设计的读者。2. MS5837 的 I2C 通信协议与 STM32 驱动准备MS5837 是 TE Connectivity 的压阻式数字水压传感器常见型号有 02BA2 bar 量程和 30BA30 bar 量程两份驱动代码几乎完全一致只有换算公式里的常量和量程判断不同。写代码前要先理解它和普通 I2C 芯片的一大区别MS5837 没有寄存器地址空间通信完全由命令字驱动一次完整交互是“主发命令 → 主收数据”所以不能用往 AT24C32 那种HAL_I2C_Mem_Read的套路去读。下面把这套命令结构和驱动准备拆细。2.1 MS5837 的 PROM 数据结构与命令字速查MS5837 内部有一块 128 位的 PROM出厂时写入 8 个 16 位无符号整数C1 压力灵敏度、C2 压力零点偏移、C3 温度对灵敏度的修正、C4 温度对零点偏移的修正、C5 参考温度、C6 温度 ADC 对温度的修正外加一个 CRC 校验字和一个保留字。读取时 I2C 地址后跟 1 字节命令常用命令字集中在下面的表中。这里有个容易踩的坑PROM 字地址不是从 0 开始连续排列的而是 0xA0、0xA2、0xA4……每次递增 2如果照搬普通驱动里的“按寄存器地址读”读两次就会错位。操作命令字说明复位0x1E上电后必须执行至少等 2.8 ms读 PROM 字 00xA0包含 CRC 与工厂标记读 PROM 字 1~60xA2~0xAC对应 C1~C6读 PROM 字 70xAE保留字启动压力转换 D10x40 / 0x42 / 0x44 / 0x46 / 0x48OSR 256 到 4096启动温度转换 D20x50 / 0x52 / 0x54 / 0x56 / 0x58OSR 256 到 4096读 ADC 结果0x00返回 3 字节大端左对齐先说复位。PROM 在芯片上电初期处于未知状态提前读通常得到 0xFF 或者随机数例程里必须把 0x1E 复位命令放在整段初始化函数的开头并且等足 3 ms 才能继续。再说转换命令。D1 是压力转换D2 是温度转换OSR 越大转换时间越长、分辨率越高例程里如果对实时性没有特别要求直接上 4096 就好。2.2 STM32 HAL 库的 I2C 配置与上拉电阻选择在 CubeMX 里把 I2C 外设打开后速度选 Standard Mode100 kHz或 Fast Mode400 kHz。MS5837 数据手册标注 I2C 时钟上限 400 kHz例程默认按 400 kHz 跑没问题但 PCB 走线超过 5 cm 或上拉电阻选了 10 kΩ 时建议降回 100 kHz。上拉电阻一般取 2.2 kΩ 到 4.7 kΩ 比较稳3.3 V 供电时 4.7 kΩ 配 400 kHz 问题不大如果 SCL 波形上升沿老是不合格把上拉换成 2.2 kΩ 立竿见影。2.2.1 从机地址的 7 位与 8 位换算MS5837 的 7 位 I2C 从机地址由 CSB 引脚决定CSB 接 GND 时是 0x76接 VCC 时是 0x77。HAL 库的HAL_I2C_Master_Transmit第一个地址参数要传“左移 1 位后的 8 位地址”也就是0x76 1。常见错误是直接把 0x76 丢给库函数导致总线上实际发出错误的读写位组合传感器不 ACKHAL_I2C_IsDeviceReady一直返回超时。调 I2C 时如果用逻辑分析仪直接看从机地址字节就能判断问题出在这。2.2.2 例程移植到 GD32F103 与 APM32 的差异GD32F103 例程和 APM32 工程里直接拿 STM32 例程改第一反应是换芯片型号。实际上要注意外设时钟开启函数不同GD32 的 I2C0 对应 STM32 的 I2C1__HAL_RCC_I2C1_CLK_ENABLE()要换成 GD32 库里的对应时钟使能宏。APM32F103 因为外设基地址和 STM32F103 高度接近很多 HAL 工程只改芯片选择就能重编译但 APM32 的 IWDG、RTC 寄存器布局有差异例程如果不碰这些外设通常没问题。移植后先跑一个最简单的循环每秒读一次 PROM 并打印能过这一关再往下走。2.3 例程中 I2C 超时与 ACK 异常的处理策略I2C 总线卡死后现象通常是HAL_I2C_GetState停在HAL_I2C_STATE_BUSY后面的 Transmit/Receive 全部超时。例程里的处理分两步先HAL_I2C_DeInit再重新MX_I2C1_Init把外设彻底复位然后对 SCL 打 9 个时钟脉冲让总线上可能处于半通信状态的从机回到空闲这两步封装成ms5837_bus_reset()。如果连 PROM 都读不到先量 CSB 引脚电平确认地址确认 VDD 3.3 V 和 GND 没有虚焊最后再怀疑传感器本身。3. 跑通水压传感器例程的最小代码实现这一节给出一个可以在 STM32F103、F407、G030 上直接编译的 MS5837 驱动骨架依赖 HAL 库。代码按“初始化 → 读 PROM → 采样 → 换算”的顺序组织和网上常见例程的思路一致但把容易踩的字节序、延时和 64 位中间量问题全部按踩过的经验写进去。3.1 MS5837 初始化序列复位、读 PROM、CRC 校验上电后先发 0x1E 复位等 3 ms再依次读 8 个 PROM 字。读 PROM 时注意小端机器上不能把接收缓冲区强转成uint16_t*否则高低字节会反。正确写法下面这样。uint16_t ms5837_prom[8]; void ms5837_init(void) { uint8_t cmd 0x1E; uint8_t buf[2]; HAL_I2C_Master_Transmit(hi2c1, 0x76 1, cmd, 1, 100); HAL_Delay(3); for (uint8_t i 0; i 8; i) { cmd 0xA0 i * 2; HAL_I2C_Master_Transmit(hi2c1, 0x76 1, cmd, 1, 100); HAL_I2C_Master_Receive(hi2c1, 0x76 1, buf, 2, 100); ms5837_prom[i] (uint16_t)((buf[0] 8) | buf[1]); } }每发一个命令后HAL 库都会自动产生 Stop 条件MS5837 的 PROM 读取时序不要求重复 Start所以这样的分段读写没问题。0xA0 i * 2对应命令表中的地址i 从 0 到 7正好覆盖 0xA0 到 0xAE。超时参数 100 是 100 ms正常情况下每次传输几十微秒就完成如果频繁超时先把 I2C 速度降到 100 kHz再查硬件连接。PROM 读完后可以做一次 CRC4 校验防止 I2C 时序异常导致系数错乱。CRC4 算法按数据手册的 16 位移位版实现多项式为 0x3000计算结果的低 4 位要和 PROM[0] 低 4 位一致。这个校验函数会修改传入数组的第一个元素调用前先备份原值。uint8_t ms5837_crc4(uint16_t *prom) { uint16_t n_rem 0; uint8_t n_bit; uint16_t n_byte; prom[0] 0x3FFF; /* 去掉 CRC 位注意会修改原数组 */ for (uint8_t cnt 0; cnt 16; cnt) { if (cnt 1) n_byte prom[cnt 1] 0x00FF; else n_byte prom[cnt 1] 8; n_rem ^ n_byte; for (n_bit 0; n_bit 8; n_bit) { if (n_rem 0x8000) n_rem (n_rem 1) ^ 0x3000; else n_rem 1; } } return (uint8_t)((n_rem 12) 0x0F); }这段算法把 128 位 PROM 数据按字节输入逐位做模二除法最后 4 位就是 CRC 校验值。具体取值和芯片内部硬件电路对应不需要去理解每一行的数学意义但移植时系数不能乱改。调试阶段建议把 CRC 错误打印出来量产时可以直接删掉省去每次初始化的计算时间。3.2 温度与压力转换命令的正确顺序与等待时间MS5837 内部的 ADC 只有一个D1 和 D2 必须分时转换。例程里的推荐顺序是先发 D2 温度命令等转换完成读回再发 D1 压力命令再读回。这样做的原因是温度系数会用于修正压力温度采样时刻离压力采样时刻越近修正越准。如果你的应用里水温变化很快这个顺序就很重要如果是静态水压先读哪个差别不大。等待时间按 OSR 查表下面这张表是例程里的常用参数。OSR典型转换时间例程等待时间256约 1.1 ms2 ms1024约 2.2 ms5 ms4096约 9.0 ms15 ms等待时间宁多勿少。OSR4096 时如果只等 5 msADC 输出寄存器还在更新读回的高字节是旧值拼接出来就是错乱数据。例程里用HAL_Delay等处理好 SysTick 中断就不容易卡住。如果工程里同时用了 RTOS不要把HAL_Delay放在临界区里转换等待最好换成信号量或osDelay。uint32_t ms5837_read_adc(uint8_t cmd) { uint8_t buf[3]; uint32_t adc_raw; HAL_I2C_Master_Transmit(hi2c1, 0x76 1, cmd, 1, 100); HAL_Delay(15); cmd 0x00; HAL_I2C_Master_Transmit(hi2c1, 0x76 1, cmd, 1, 100); HAL_I2C_Master_Receive(hi2c1, 0x76 1, buf, 3, 100); adc_raw ((uint32_t)buf[0] 16) | ((uint32_t)buf[1] 8) | buf[2]; return adc_raw; }调用方式d2 ms5837_read_adc(0x58);再d1 ms5837_read_adc(0x48);。0x58 是温度转换 OSR40960x48 是压力转换 OSR4096。读 ADC 结果的命令 0x00 是一个单字节操作码不是寄存器地址所以先发一次 0x00再接收 3 字节数据。有的例程写成一条HAL_I2C_Mem_Read(addr, 0x00, ...)也能读但分两步写排错时更清楚。提示实际转换时间受供电电压和温度影响数据手册给出的是典型值例程里按最大值的两倍等待最稳妥代价只是采样率略微下降。3.3 把 D1/D2 原始值换算成压力与温度拿到 D1、D2 之后按数据手册一阶公式计算int32_t dT d2 - (int32_t)ms5837_prom[5] * 256; int32_t temp 2000 dT * (int32_t)ms5837_prom[6] / 8388608; int64_t off (int64_t)ms5837_prom[2] * 65536 (int64_t)ms5837_prom[4] * dT / 128; int64_t sens (int64_t)ms5837_prom[1] * 32768 (int64_t)ms5837_prom[3] * dT / 256;dT是实际温度 ADC 与参考温度的差值temp的单位是 0.01°C2000 就是 20.00°Coff和sens分别代表零点偏移和灵敏度它们都依赖dT把温度的影响拉进了压力计算。这里有个最常见的 bugd1 * sens很容易超过 32 位有符号整数的上限所以sens一定要声明成int64_t如果例程里写成int压力值会在量程中段莫名其妙跳变表现为一串 0xF 开头的负数。最终压力单位为 mbar温度单位为摄氏度的计算如下int32_t pressure ((int64_t)d1 * sens / 2097152 - off) / 32768; float temp_c temp / 100.0f;2097152是 2 的 21 次方32768是 2 的 15 次方这两个数来自芯片内部信号的增益配置不能随便改。算出来的pressure是绝对压力包含当前大气压后面做水深时要单独减去水面气压那个步骤放到第 5 章校准部分说。4. 例程移植到具体 STM32 型号时的常见问题网上能找到的 MS5837 例程大多是 F103 标准外设库而你手上的板子可能是 G030、H743或者是 GD32F103、APM32 这类兼容芯片。移植时最容易出问题的三个地方是芯片型号切换、延时函数卡死、Keil 工程配置不对。4.1 从 F103 换到 G030时钟源与 I2C 外设差异STM32G030 的 I2C 外设和 F103 不同F103 是传统 I2CG030 是带独立时序寄存器的新版 I2CHAL 函数名一样但初始化参数差异很大。从 F103 例程移植到 G030 时不要手工复制寄存器代码直接用 CubeMX 重新生成MX_I2C1_Init即可。G030 的 I2C 时钟源可以选 PCLK 或系统时钟CubeMX 默认配置在部分版本里会选到低速源导致 400 kHz 实际只有 200 kHz采样率看似正常但波形时序很怪。下面把 F103 和 G030 的主要差异列出来方便排查。项目F103G030I2C 外设传统 I2C新版 I2C带时序寄存器时钟源PCLK1可选 PCLK 或 SYSCLK从机地址长度7 位/10 位7 位配置更细HAL 初始化通用参数由 Timing 寄存器自动计算换型号之后建议先跑一次 I2C 扫描把所有连接到总线上的设备地址打印出来确认 MS5837 出现在 0x76 或 0x77再继续跑例程。4.2 例程卡死在 HAL_Delay 的排查方法如果你跑过江科大 STM32 教程里的软件定时器那一课对 HAL_Delay 在中断关闭时的表现应该有印象。MS5837 例程卡死在这个函数里原因通常是 SysTick 中断被关闭或优先级被调高到阻塞了比如在同一个工程里接入 DHT11、DS18B20 这类软件时序传感器它们会短暂关闭中断期间若有代码调用HAL_DelayuwTick不再累加看起来就像进了死循环。解决办法有两种一是把HAL_Delay换成基于 DWT 的延时函数不依赖中断二是在进入软件时序临界区之前调用HAL_SuspendTick()退出后再恢复但要注意 RTOS 环境下额外处理调度器。4.3 Keil5 芯片包安装与批量切换单片机型号新板子如果用的是 STM32G0 或 H7Keil5 会提示找不到器件这时要先装对应的 Device Family Pack。G0 系列的包名是 STM32G0xx_DFP在 Pack Installer 里勾选后等待安装下载慢就手动下载离线包安装。“keil5 兼容 c51 和 stm32 安装”这类问题指的是同一台机器上同时装 C51 和 ARM 两套编译环境两者互不干扰ARM 的例程只要装好 DFP 就能编译。若想在 CubeMX 里把例程从 F103 改成 G030只需要在工程设置里重新选择芯片型号重新生成代码后 HAL 库会自动切换但你自己手写的寄存器操作仍要参照新芯片的参考手册逐一核对。APM32 能不能直接用 STM32 程序取决于外设基地址是否一致多数 APM32F103 的工程在替换芯片型号后可以重编译但如果用到 IWDG、RTC 这类寄存器布局不同的外设还是要改代码。5. 提高水压测量精度的校准技巧与验证方法5.1 零点校准与水深换算MS5837 的出厂标定已经很准但用水位计场景时仍然建议做一次零点校准上电稳定 5 分钟让传感器在空气中连续读数 100 次取平均记为P0_mbar。之后的水深换算不再用 1013.25 mbar 作为基准而是用这个实测零点float depth_m (pressure_mbar - P0_mbar) / 98.0665f;98.0665 的由来是 1 m 水柱约等于 98.0665 mbar淡水、标准重力加速度。换成海水密度 1025 kg/m³ 时系数改为 100.515。如果pressure_mbar比P0_mbar小深度就是负值说明传感器露在水面以上属于正常情况。5.2 多点标定与最小二乘拟合如果手头有标准压力泵或者一个已知深度的水柱可以做更细的多点标定在量程的 0%、50%、100% 三个点记录“绝对压力读数”和“参考压力”然后做一次线性回归得到 slope 和 offset 烧进 STM32 的 Flash。量产时每片传感器按编号存一组校准参数统一公式pressure_cal raw * slope offset输出。标定时温度尽量稳定在 25°C 附近否则会把温漂混进零点偏差里。5.3 用 1 米水柱做重复性验证验证方法用一根 1 米透明水管就够传感器吊在管底注水到 1 m 标线静置 20 秒后串口以 1 Hz 打印压力换算值观察最后 10 次读数是否稳定在 ±2 mbar 以内。如果 1 m 处的深度计算值偏差超过 5 cm先检查温度是不是用 0.01°C 单位直接当成摄氏度用了这个错误会让温度补偿项差 100 倍压力值随之出现明显偏移确认无误后再考虑传感器量程选型和安装位置是否有气泡残留。本文还有配套的精品资源点击获取