ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SPL06-007气压传感器接入STM32:SPI驱动、DMA时序与补偿算法详解

2026/9/19 6:51:42 拓冰建站 浏览量
SPL06-007气压传感器接入STM32:SPI驱动、DMA时序与补偿算法详解 做气压测高的时候我一开始用的是BMP280价格便宜资料也多但在室内环境下压力噪声一直压不下去换了好几种滤波方式都不理想。后来把传感器换成SPL06-007配合SPI接口和DMA传输气压数据的稳定度直接上了一个台阶。这块芯片的寄存器配置、校准系数解析和温度补偿算法比BMP280绕不少网上资料也比较散我啃数据手册加反复抓波形踩了不少坑。这篇文章把SPL06-007气压温度传感器接到STM32F103上的完整过程记录下来重点讲SPI驱动写法、DMA读取时容易忽略的时序细节以及补偿算法里最容易算错的校准系数拼接和缩放问题。如果你正在做气压计、高度计或者惯性导航里的气压辅助这篇应该能帮你少走很多弯路。1. 为什么选SPL06-007选型背景和这颗芯片的“脾气”1.1 数据手册里最容易被忽略的几个关键数字SPL06-007是歌尔出品的一颗压力温度传感器封装非常小适合做板载气压计。它的测量范围是300到1200 hPa温度范围在手册上标的是-40到85摄氏度这些大路参数大家都会看但有几个细节很容易被忽略。第一个是压力RMS噪声。手册里给出了不同过采样倍率下的RMS噪声曲线在64倍过采样下可以做到零点几个帕这比很多同价位传感器要安静。我的实测体感是它比BMP280在相同条件下的压力数据“干净”很多尤其是做静态高度判断时原来那种肉眼可见的跳跃明显变小了。第二个是SPI通信时钟的上限并不高。很多人拿STM32的SPI直接分频到18Mbit/s去读它结果读回来的数据偶尔跳变或者全错。我最后把SPI时钟压到了2.25Mbit/s左右非常稳。对于这类传感器真的没必要追求高速因为它本身转换时间就在毫秒级别SPI读一次才几十微秒瓶颈根本不在这里。第三个是状态读取问题。SPL06-007的数据就绪标志位在MEAS_CFG寄存器里很多人连续测量模式下不判断就绪位就直接读数据偶尔会读到转换中途的半新数据造成个别点明显跳变。严格来说是应该在读出前检查就绪标志或者干脆在连续模式下把读取频率压低到转换频率之下。1.2 为什么最终选了SPI而不是I2CSPL06-007同时支持I2C和SPI我在项目里最终选了SPI原因有两点。一是不用担心地址冲突。这颗芯片的I2C地址是0x76或者0x77和很多常见气压计、温度传感器都撞地址如果板上挂了多个器件还得去处理地址选择引脚和总线仲裁。SPI本身就是一从一主或者片选区分多从机没有这个问题。二是SPI更适合和DMA配合。I2C的读流程要处理ACK/NACK、起始停止位DMA编排起来相对麻烦。SPI就简单多了拉低片选发出命令字节后面跟若干个dummy字节同时把MISO上的数据收回来。一条DMA通道就能把整个收发过程跑完CPU几乎不用参与。当然I2C也有它的优势少一根MISO线省两个GPIO。如果你的主控I/O资源特别紧张I2C也能用。但就我在嵌入式项目里的习惯凡是传感器支持SPI我优先SPI。2. 硬件接线与CubeMX初始化软件片选为什么更省心2.1 接线要点与电平细节SPL06-007在SPI模式下需要接四根线SCLK、SDI对应MOSI、SDO对应MISO、CSB片选。再加上VDD和GND一共六根。如果板上INT引脚拉出来还可以用来做数据就绪中断后面扩展时再说。我的接法是SCLK - PA5SPI1_SCKSDI - PA7SPI1_MOSISDO - PA6SPI1_MISOCSB - PB12普通GPIO软件片选VDD - 3.3VGND - GND这里有个小细节SPL06-007的SDO引脚在I2C模式下同时承担地址选择功能SPI模式下不用关心地址但如果你在I2C和SPI之间切换过要注意SDO不要悬空我都是直接上拉到VDD省心。另一个容易被坑的点是片选引脚的初始电平。软件片选方式下MCU上电瞬间GPIO如果默认输出低电平传感器会以为SPI总线被选中了此时如果总线再有抖动可能会把寄存器写坏。所以我把CSB接了一个10k上拉电阻同时在CubeMX里把PB12初始状态设成High。这样MCU还没初始化时传感器也不会被误选中。2.2 CubeMX里SPI和DMA的完整配置项CubeMX配置STM32F103的SPI1有几个选项要特别注意。SPI Mode选择Full-Duplex Master时钟分频我选的是PCLK1/32或者PCLK2/32具体看你的SPI挂在哪条总线上。F103的SPI1挂APB272MHz主频下分频32就是2.25MHz这个速度实测很稳。时钟极性CPOL选High相位CPHA选2 Edge也就是SPI Mode 3。数据手册里的SPI时序图就是SCLK空闲为高、第二个边沿采样这个配置和大多数SPL06的例程是吻合的。帧格式8 bitMSB First这两项别动。NSS部分选择SoftwareNSS Signal Type选Software然后把PB12配成普通输出口做片选。这样整个SPI的时序完全由代码控制什么时候拉低片选、什么时候拉高都自己说了算。DMA部分也很关键。CubeMX里给SPI1加两个DMA请求一个RX一个TX方向分别是PeripheralToMemory和MemoryToPeripheralData Width都选ByteMode选NormalMemory Increment Address打开。这样SPI1_RX对应DMA1通道2SPI1_TX对应DMA1通道3CubeMX会自动分配好。生成代码之后记得在main函数里初始化DMAHAL_SPI_TransmitReceive_DMA这个接口在调用时会把DMA请求使能起来不需要手动去碰DMA寄存器。3. SPI读时序与DMA收发命令字节和dummy字节的关系3.1 读一个寄存器的本质主发dummy产生时钟很多刚接触SPI读传感器的朋友会在这卡住因为SPI和I2C不一样它没有专门的读时钟线。SPI是同步全双工主设备必须主动发出时钟从设备才能在MISO上把数据送出来。所以读操作的本质是主机持续发送字节来产生时钟从机在这期间把寄存器内容放到MISO线上。SPL06-007的命令字节是8位最高位bit7是读写标志1表示读0表示写低7位是寄存器地址。也就是说读0x28寄存器命令字节就是0x80 | 0x28 0xA8。之后如果你要读3个字节的数据主机必须继续发3个dummy字节在这3个时钟周期里传感器会依次把PSR_B2、PSR_B1、PSR_B0送上MISO。所以一次完整的读操作MOSI上会发送“1个命令字节 N个dummy字节”而MISO上会收到“1个无效字节 N个有效数据字节”。代码里要把有效数据从接收缓冲区的偏移1开始取。这个逻辑搞明白之后你就知道为什么很多人贴的代码要发一个len1长度的数组了。不是闲得慌是SPI协议本身就这么工作。3.2 基于HAL_SPI_TransmitReceive_DMA的读写封装基于这个原理我封装了一组读写函数。核心是用HAL_SPI_TransmitReceive_DMA把“命令dummy”一趟发出去同时把返回的数据收回来。代码长这样#define SPL06_CS_LOW() HAL_GPIO_WritePin(CSB_GPIO_Port, CSB_Pin, GPIO_PIN_RESET) #define SPL06_CS_HIGH() HAL_GPIO_WritePin(CSB_GPIO_Port, CSB_Pin, GPIO_PIN_SET) uint8_t spi_tx_buf[8]; uint8_t spi_rx_buf[8]; volatile uint8_t spi_txrx_done 0; void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) spi_txrx_done 1; } void SPL06_ReadBytes(uint8_t reg, uint8_t *data, uint8_t len) { uint8_t i; spi_tx_buf[0] 0x80 | reg; // 读命令 for (i 0; i len; i) spi_tx_buf[i 1] 0xFF; // dummy字节产生时钟 spi_txrx_done 0; SPL06_CS_LOW(); HAL_SPI_TransmitReceive_DMA(hspi1, spi_tx_buf, spi_rx_buf, len 1); while (!spi_txrx_done); // 等待DMA完成回调置标志 SPL06_CS_HIGH(); for (i 0; i len; i) data[i] spi_rx_buf[i 1]; // 跳过第一个无效字节 } void SPL06_WriteByte(uint8_t reg, uint8_t val) { spi_tx_buf[0] reg; // 写命令bit70 spi_tx_buf[1] val; spi_txrx_done 0; SPL06_CS_LOW(); HAL_SPI_Transmit_DMA(hspi1, spi_tx_buf, 2); while (!spi_txrx_done); SPL06_CS_HIGH(); }如果你用的是轮询方式也可以把HAL_SPI_TransmitReceive_DMA换成HAL_SPI_TransmitReceive效果一样只是会占用CPU等待。DMA的好处是释放了CPU可以把读传感器的操作扔到低优先级任务里等中断来了再处理数据。3.3 DMA完成回调里的CS竞争坑这个坑我调了一个晚上值得单独拿出来说。第一次写DMA版本时我在HAL_SPI_TxRxCpltCallback回调函数里直接做了CS拉高操作结果读出来的数据总是有一个字节不对。用逻辑分析仪抓波形发现CS上升沿来得太快最后一个bit在MISO上还没有被完全采到CS就释放了导致最后一个字节掉bit。原因是HAL库的SPI DMA完成回调触发的时机理论上是在SPI传输完成后但实际工程里由于DMA和SPI外设之间的配合回调触发和最后一个bit真正落定之间可能就差那么几十纳秒。在2.25MHz时钟下一个bit大约是440纳秒如果你的回调里还有别的操作很容易就把这个窗口错过。解决办法有两种。一种是在回调里先判断HAL_SPI_GetState(hspi1)是否回到READY再拉高CS。另一种更简单在等待spi_txrx_done标志置位之后加几个空指令延时再拉高片选。我最后用的是回调里置标志主函数里等待标志后拉高CS实测稳定。类似的问题也发生在读操作里。如果你把CS拉高动作放在DMA回调里一定要确认SPI状态机已经彻底空闲。否则你可能每隔一段时间就遇到一次偶发的错误字节很难复现也最难排查。4. 校准系数与补偿算法从原始值到可靠物理量的完整链路4.1 18字节校准系数的位域拼接SPL06-007出厂时会把校准系数烧录到OTP区域通过0x10开始的寄存器读出来。但这里有个比较绕的地方这些系数不是每个寄存器存一个完整的16位整数而是采用位域拼接的方式跨寄存器排列写代码时如果不仔细看手册的表格很容易拼错。我项目里实际用的拼接代码如下uint8_t coef[18]; SPL06_ReadBytes(0x10, coef, 18); int16_t c0 (int16_t)((coef[0] 8) | coef[1]); int16_t c1 (int16_t)((coef[2] 8) | coef[3]); int32_t c00 ((int32_t)coef[4] 12) | ((int32_t)coef[5] 4) | ((int32_t)coef[6] 4); int32_t c10 (((int32_t)coef[6] 0x0F) 16) | ((int32_t)coef[7] 8) | coef[8]; int16_t c20 (int16_t)((coef[9] 8) | coef[10]); int16_t c30 (int16_t)((coef[11] 8) | coef[12]); int16_t c01 (int16_t)((coef[13] 8) | coef[14]); int16_t c11 (int16_t)((coef[15] 8) | coef[16]); int8_t c21 (int8_t)coef[17];注意c00和c10这两个系数是跨字节位域拼接的c00取的是寄存器14、15的全部8位加上寄存器16的高4位一共20位c10取的是寄存器16的低4位加上寄存器17、18的全部位也是20位。拼接顺序错了压力值会偏到天上去。另外一个很容易翻车的地方是符号扩展。你从寄存器读进来的数是uint8_t左移到16位或24位之后符号位能不能被正确保留取决于编译器对char和有符号数的处理。我的建议是显式强转c0、c1、c20这类16位系数用int16_t像c21这种8位系数用int8_t不要依赖编译器默认的char符号性。这里必须强调一句不同批次的数据手册在校准系数位域的表格上可能有细微差异我上面贴的是我自己项目验证过的解析方法但你拿到新批次芯片或者换了一个封装型号时最好把读回来的18个字节和手册表格逐位对一遍别直接照抄网上代码。4.2 过采样倍率和scale factor的匹配关系校准系数解析完之后下一步是把原始24位整数转换成带小数的物理量这一步必须用到scale factor。SPL06-007的压力和温度原始值分别通过0x28和0x2B开始的3个字节读取。以压力为例读回来的24位有符号整数实际代表的物理量是“原始值 / scale factor”单位是kPa或者hPa具体取决于你参考的公式上下文。我的做法是统一先用Pa做单位。scale factor不是固定值它由PRS_CFG和TMP_CFG寄存器里的过采样倍率决定。手册里的对应关系大概是Oversampling倍率scale factor1x5242882x15728644x36700168x786432016x2539520032x5160960064x104038400128x208896000我实际选的是64x过采样scale factor对应104038400。这个数值看起来吓人其实只是初中除法。把24位原始值转成float除以这个数就得到了缩放后的原始物理量。这个表要跟你配置的过采样倍率一一对应不能乱配。我见过有人用64x过采样却拿1x的scale factor去算结果读出来的气压值在小数点后好几位乱跳还以为芯片坏了。实际上就是表没对上。这里顺便提一句过采样倍率越大转换时间越长分辨率越高。64x在大多数场景下是功耗和精度的平衡点。如果你做的是电池供电的低功耗设备可以考虑32x或者16x牺牲一点噪声换功耗。4.3 温度与气压补偿公式手把手推导拿到缩放后的压力和温度原始值再把校准系数都解析出来就可以套补偿公式了。温度补偿公式是这样的t_raw_scaled t_raw / temp_scale_factor temperature c0 * 0.5 c1 * t_raw_scaled注意这里的c0和c1并不是直接用寄存器里的整数而是需要按照各自的LSB权重换算。c0每个LSB对应0.5摄氏度c1每个LSB对应0.01摄氏度所以我在代码里是这样处理的float temp (float)c0 * 0.5f (float)c1 * 0.01f * t_raw_scaled;气压补偿公式稍微长一点但本质是一个多项式拟合p_raw_scaled p_raw / pressure_scale_factor pressure c00 p_raw_scaled * (c10 p_raw_scaled * (c20 p_raw_scaled * c30)) t_raw_scaled * (c01 p_raw_scaled * (c11 p_raw_scaled * c21))代码里写成float pressure (float)c00 * c00_scale p_raw_scaled * ((float)c10 * c10_scale p_raw_scaled * ((float)c20 p_raw_scaled * (float)c30)) t_raw_scaled * ((float)c01 p_raw_scaled * ((float)c11 p_raw_scaled * (float)c21));这些系数里c00、c10、c20这些项的LSB权重并不全是1具体数值同样要查手册。我最开始把c00直接当整数用结果气压整体偏了大概0.5%到1%后来逐一核对了每个系数的LSB权重才把一致性拉回来。这里要提醒的是补偿公式的核心思想是用温度对压力进行校正因为温度变化会引起压力传感器的灵敏度漂移。所以即使你只关心气压温度也是一定要读的这不是顺手多读一个值而是整个补偿链路的一部分。4.4 用PC端脚本交叉验证补偿结果补偿算法写完之后最怕的就是“以为算对了实际上偏了”。我的习惯是把原始数据和系数通过串口打印出来在PC端用Python重新算一遍对比两种结果是否一致。具体做法很简单MCU通过串口输出一行带有p_raw、t_raw以及18个校准系数的JSON字符串PC端接收后用Python脚本读取这些值套同样的公式计算温度和压力。如果MCU计算值和PC端计算结果偏差在浮点精度范围内说明算法移植没毛病如果有系统性的偏差十有八九是某个系数的符号扩展或者LSB权重处理不对。这个交叉验证方法帮我抓出过两次问题一次是c21的符号扩展写错了导致温度高了几摄氏度另一次是scale factor用错压力曲线和真实高度对应不上。在调试阶段我建议把串口输出做成“一行一个样本”的格式方便PC端逐行解析。到了正式版本再把这些调试输出关掉节省串口带宽和CPU开销。5. 实测数据、典型坑位和后续扩展5.1 静态稳定性和不同过采样率的表现算法跑通之后我把传感器放在桌面上静置做了两组实测。第一组是64x过采样连续测量模式下采集1000个样本气压标准差大概在0.2到0.4 Pa之间温度标准差在0.01到0.02摄氏度之间。这个水平对于室内测高已经完全够用1 Pa大约对应8到10厘米的高度差所以0.3 Pa的噪声换算成高度大概就是3厘米以内。第二组我把过采样降到16x噪声明显变大气压标准差跳到了1 Pa级别。这说明如果应用场景对高度测量精度不敏感倒是可以为了省功耗而降低过采样。反过来如果要做高精度测高直接拉到128x也没问题代价是转换时间变长。另外我试过在数据输出后面加一个10点的滑动平均气压噪声能再压下去一个级别但代价是响应变慢。做静态高度判断没问题如果设备在快速移动滑动窗口千万别太长否则高度变化会被明显滞后。5.2 我实际踩过的几个问题排查表把项目过程中遇到的问题整理成了一张表遇到类似的可以直接对号入座。现象根因解决办法读回来的寄存器全是0xFF或0x00SPI Mode不对或者CS时序错误确认CPOL1、CPHA1同一帧内发命令和dummy温度值明显离谱比如几十万校准系数拼接错误或符号扩展有问题对照手册位域表格逐位检查拼接显式强转int16_t/int8_t气压值趋势对但整体偏大/偏小scale factor和过采样倍率不匹配根据PRS_CFG/TMP_CFG配置换对应的scale factorDMA回调里CS拉高后偶尔丢字节CS释放太早最后一个bit未采完等SPI状态READY后再拉高CS或加几个空指令延时上电瞬间传感器乱码CSB初始电平为低CSB加10k上拉CubeMX里初始化为High数据有偶发跳变点没有判断数据就绪标志就读数读取前检查MEAS_CFG数据就绪位或保证读取速率低于转换速率排查过程中最重要的一件工具是逻辑分析仪。很多SPI问题光靠看代码很难定位把SCLK、MISO、CS三根线的波形抓下来一眼就能看出命令字节是否在同一帧内、CS释放时机对不对、数据线上的电平是否和寄存器值对应。有条件的话建议每个做嵌入式传感器驱动的人都准备一个支持SPI解码的逻辑分析仪几十块钱的就能用。5.3 以后可以继续做的方向SPL06-007还有一些功能我这次没有全部用上但后续项目可以扩展。第一个是INT引脚的数据就绪中断。现在我是轮询方式读数据对CPU占用不高但如果系统里还有大量其他任务可以配置INT引脚在传感器转换完成后拉高让MCU在中断里发起DMA读操作进一步减少无效轮询。第二个是内置FIFO。SPL06-007带了FIFO缓冲可以让传感器连续采集多个样本然后一次性通过SPI把一组数据读回来。这种批量读模式特别适合在低功耗设备中减少唤醒次数一次唤醒拿几十个样本然后继续睡觉。第三个是FreeRTOS环境下SPI中断优先级的设置。如果你的项目跑了RTOSSPI DMA完成中断的优先级不要设成和系统心跳同级。我一般把SPI DMA中断优先级设在比系统Tick低一到两级的位置这样既不会打断关键调度也不会让传感器数据等待太久。做低功耗气压测高的人最后通常都会走上传感器融合这条路。SPL06-007把气压噪声压下去之后再配合加速度计做高度融合静态漂移和动态响应都能得到很好的平衡。至少在我这个项目里气压数据稳定了后面所有跟高度相关的逻辑才真正算是能看了。