STM32F103实战:用HAL库+软件模拟I2C驱动SPL06气压传感器(附完整代码)
STM32F103实战:用HAL库+软件模拟I2C驱动SPL06气压传感器(附完整代码)
在无人机定高、智能车坡道检测或者室内导航项目中,精准的高度信息往往是决定成败的关键。硬件I2C外设虽然方便,但在资源紧张的STM32F103这类Cortex-M3内核MCU上,有时会面临引脚冲突、时序不稳定或者DMA配置复杂等问题。这时,回归底层,用几根普通的GPIO口“手搓”一个软件模拟I2C协议,反而成了一种高效可靠的解决方案。本文将带你深入实战,基于STM32 HAL库,从零构建一个驱动SPL06高精度气压传感器的完整工程,不仅会详细拆解软件I2C的每一个时序细节,更会分享如何利用数据结构优化校准系数管理,并实现一套高效的高度滤波算法,让你的项目飞得更稳、跑得更准。
1. 项目环境搭建与工程配置
在开始敲代码之前,一个清晰、模块化的工程结构能避免后续无数麻烦。我习惯为每个重要的外设或传感器创建独立的.c/.h文件对,这样主程序逻辑干净,调试和移植也方便。
1.1 硬件连接与CubeMX基础配置
首先,确保你的硬件连接正确。SPL06传感器通常支持I2C和SPI,我们选择I2C模式。其I2C地址由SDO引脚电平决定:SDO接高电平(VDDIO)时地址为0x77,接低电平(GND)时为0x76。这里我们以0x77为例。
硬件连接表:
| STM32F103引脚 | SPL06引脚 | 说明 |
|---|---|---|
| 3.3V | VDD | 电源 |
| GND | GND | 地 |
| 任意GPIO (如PB6) | SCL | 时钟线 |
| 任意GPIO (如PB7) | SDA | 数据线 |
| 3.3V 或 GND | SDO | 地址选择 |
注意:用于模拟I2C的GPIO口必须配置为**开漏输出(Open-Drain)**模式,并且外部需要接上拉电阻(通常4.7KΩ)。这是I2C总线标准的硬件要求,确保总线能够实现“线与”功能。
使用STM32CubeMX初始化项目:
- 选择你的STM32F103型号。
- 在
System Core->SYS中,将Debug选为Serial Wire(如果要用SWD下载调试)。 - 配置时钟树(RCC),根据你的外部晶振选择HSE,并设置系统时钟为72MHz(F103的典型值)。
- 关键一步:为我们用于模拟I2C的两个GPIO(例如PB6和PB7)配置模式。在
Pinout & Configuration视图的芯片图上,找到PB6和PB7,将其模式设置为GPIO_Output。然后,在左侧System Core->GPIO中,分别点击PB6和PB7,将其GPIO output level初始化为高电平(High),GPIO mode设置为Open Drain,GPIO Pull-up/Pull-down设置为No pull-up and no pull-down(因为我们在外部接了上拉电阻)。 - 生成代码(Generate Code),选择MDK-ARM(Keil)或你使用的IDE。
1.2 软件I2C底层驱动实现
软件I2C的核心就是通过精确控制GPIO电平的变化来模拟I2C协议的时序。我们创建一个MyI2C.c和MyI2C.h文件。代码的健壮性在于对每一个时序细节的把握,比如启动、停止、应答信号。
// MyI2C.h
#ifndef __MYI2C_H
#define __MYI2C_H
#include "main.h"
// 根据你的实际连接修改引脚定义
#define BARO_SCL_GPIO_Port GPIOB
#define BARO_SCL_Pin GPIO_PIN_6
#define BARO_SDA_GPIO_Port GPIOB
#define BARO_SDA_Pin GPIO_PIN_7
// 基础时序控制函数
void MyI2C_W_SCL(uint8_t BitValue);
void MyI2C_W_SDA(uint8_t BitValue);
uint8_t MyI2C_R_SDA(void);
// I2C协议层函数
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);
// 应用层封装函数(针对SPL06)
uint8_t MyI2C_WriteRegister(uint8_t RegAddr, uint8_t byte);
uint8_t MyI2C_ReadRegister(uint8_t RegAddr);
uint8_t MyI2C_ReadMultiRegister(uint8_t I2CAddr, uint8_t RegAddr, uint8_t length, uint8_t* buffer);
uint8_t MyI2C_WriteMultiRegister(uint8_t I2CAddr, uint8_t RegAddr, uint8_t length, uint8_t* buffer);
#endif
在.c文件中,实现最关键的几个函数。这里有一个我调试时遇到的坑:SCL高电平期间,SDA必须保持稳定。这意味着在MyI2C_SendByte和MyI2C_ReceiveByte函数中,改变SDA电平的操作必须在SCL为低时进行。
// MyI2C.c 部分核心代码
void MyI2C_SendByte(uint8_t Byte) {
uint8_t i;
for (i = 0; i < 8; i++) {
// 先设置SDA数据位(SCL为低)
MyI2C_W_SDA(Byte & (0x80 >> i)); // 取出最高位依次发送
Delay_us(5); // 短暂延时,保证数据稳定
// 拉高SCL,产生时钟脉冲,从机在此时采样SDA
MyI2C_W_SCL(1);
Delay_us(10); // 保持高电平时间,满足传感器时序要求
// 拉低SCL,为下一个数据位做准备
MyI2C_W_SCL(0);
Delay_us(5);
}
}
uint8_t MyI2C_ReceiveByte(void) {
uint8_t i, Byte = 0x00;
MyI2C_W_SDA(1); // 释放SDA线,设置为输入模式(靠外部上拉)
for (i = 0; i < 8; i++) {
MyI2C_W_SCL(1); // 拉高SCL
Delay_us(10);
if (MyI2C_R_SDA()) { // 读取SDA电平
Byte |= (0x80 >> i); // 从最高位开始组装数据
}
MyI2C_W_SCL(0); // 拉低SCL
Delay_us(5);
}
return Byte;
}
Delay_us函数需要一个微秒级延时,你可以用HAL库的HAL_Delay(毫秒级)配合循环实现,或者使用SysTick定时器实现更精确的延时。这是软件模拟时序的精度保障。
2. SPL06传感器驱动深度解析
SPL06是一款性能出色的MEMS气压传感器,其精度和温度补偿算法是亮点。驱动它不仅仅是读写寄存器,更要理解其内部工作机制和校准流程。
2.1 寄存器配置与初始化流程
SPL06的初始化是一个有严格顺序的过程,乱序可能导致传感器工作异常。完整的初始化函数SPL06_Init应该包含以下步骤:
- I2C通信测试:向传感器写入再读出,验证物理连接和基本通信是否正常。这是排查硬件问题的第一道关卡。
- 软复位:向
RESET寄存器写入特定序列0x89,让传感器恢复到上电默认状态,确保配置起点一致。 - 配置测量模式:通过
MEAS_CFG寄存器选择连续测量模式(例如同时连续测量气压和温度)。 - 配置气压和温度测量参数:分别设置
PRS_CFG和TMP_CFG寄存器,关键参数是测量速率(Measure Rate)和过采样率(Oversampling Rate)。过采样率越高,单次测量时间越长,但能有效抑制噪声,提高分辨率。需要根据应用在速度和精度间权衡。 - 启用P-Shift:当气压过采样率设置大于8倍时,必须将
CFG_REG寄存器的P_SHIFT位置1,以避免数据溢出。 - 读取校准系数:这是获得精确数据的核心。传感器出厂时,会将一组独特的补偿系数(c00, c10, c01, c11, c20, c21, c30, c0, c1)写入OTP存储器。必须在测量前读取并妥善处理。
这里提供一个关键配置的示例代码片段:
// 配置连续测量气压和温度
uint8_t SPL_OperatingModeInit(MeasureModeConfig config) {
if(MyI2C_WriteRegister(MEAS_CFG, config)) {
return 1; // 写入失败
}
return 0;
}
// 在主初始化函数中调用
if(SPL_OperatingModeInit(STRAT_CONTINUOUS_PSR_TEMP)) {
// 处理错误
}
2.2 校准系数的读取与处理
校准系数是24位有符号补码形式存储的,直接使用会导致计算错误。GetCoefVal函数完成了读取和转换。这里面的难点在于补码到原码的转换,以及不同系数位宽(12位、16位、20位)的处理。
// 以20位系数c00为例,演示补码转换逻辑
#define Total_Number_20 1048576.0 // 2^20
if(COEF->raw_C00 & 0x80000) { // 检查最高位(符号位)是否为1
// 如果是负数,补码转原码:原码 = 补码 - 2^n
COEF->C00 = COEF->raw_C00 - Total_Number_20;
} else {
COEF->C00 = COEF->raw_C00; // 正数,补码即原码
}
所有系数转换后,会存储在一个结构体中,供后续补偿计算使用。务必确保在传感器上电并完成初始测量后(COEF_RDY标志位置位)再读取系数,否则读到的可能是默认值。
3. 数据结构优化:链表管理比例因子
SPL06的原始气压和温度数据(Praw和Traw)需要根据设置的过采样率除以一个对应的**比例因子(Scale Factor)**才能得到用于补偿计算的Praw_sc和Traw_sc。这个比例因子与过采样率是一一对应的固定关系。
一种简单的实现是用switch-case或查找表。但这里我介绍一种更优雅、易于维护和扩展的方法:使用单向链表。当未来需要支持更多过采样率或类似传感器时,只需在链表中添加节点,而无需修改查找逻辑。
// 定义链表节点结构
typedef struct node {
uint8_t OverSamplingRate; // 过采样率配置值(寄存器值)
uint32_t FACTOR; // 对应的比例因子
struct node* next;
} Factor_List;
// 全局链表头指针
Factor_List* b_list;
// 初始化链表并插入所有已知的过采样率-比例因子对
void Init_FactorList(void) {
b_list = initList(); // 创建头节点,存储单次过采样的数据
insertTail(b_list, _2TIMES_OVERSAMPLING, _2TIMES_SCALE_FACTOR);
insertTail(b_list, _4TIMES_OVERSAMPLING, _4TIMES_SCALE_FACTOR);
// ... 插入其他节点
insertTail(b_list, _128TIMES_OVERSAMPLING, _128TIMES_SCALE_FACTOR);
}
// 根据过采样率查找比例因子
uint32_t FindFactor(Factor_List* l, uint8_t val) {
while(l != NULL) {
if(l->OverSamplingRate == val) {
return l->FACTOR;
}
l = l->next;
}
return 0; // 未找到,返回0或错误码
}
在GetCompensatedVal函数中,只需调用FindFactor(b_list, PSR_InitStructure.OVER_SAMPLING_TIMES)即可获得正确的KP值。这种设计将数据与算法解耦,提高了代码的清晰度和可复用性。
4. 从气压到高度:计算与滤波实战
得到校准后的气压值(单位:Pa)后,就可以将其转换为高度了。同时,由于气压信号本身存在噪声,直接计算的高度值会跳动,必须进行滤波。
4.1 气压-高度转换公式
国际标准大气压模型给出了气压与海拔高度的近似关系。我们使用最常用的公式:
[ \text{Altitude} = 44330 \times \left( 1 - \left( \frac{P}{P_0} \right)^{\frac{1}{5.255}} \right) ]
其中:
Altitude是海拔高度(米)。P是测量点的绝对气压(帕斯卡,Pa)。P0是海平面标准大气压,通常取 101325 Pa。
#define P0 101325.0f
float getAltitude(float P) { // 输入P单位为Pa
float Altitude;
// 增加合理性检查,避免无效数据
if(P > 30000.0f && P < 120000.0f) {
Altitude = 44330.0f * (1.0f - powf(P / P0, 1.0f/5.255f));
} else {
return -9999.0f; // 返回一个明显错误值
}
return Altitude;
}
提示:这个公式在低空(<11km)精度较高。
P0也可以根据当地气象站数据实时校准,以消除天气变化带来的基准误差,这对于高精度定高应用尤为重要。
4.2 高度数据滤波算法:快速排序结合中值均值滤波
原始高度数据往往毛刺较多。简单的移动平均滤波在数据出现突发跳变(野值)时效果很差。我推荐一种结合了中值滤波抗野值和均值滤波平滑特性的混合算法:“掐头去尾”中值均值滤波。
其思路是:
- 维护一个固定长度(例如32个点)的滑动窗口,存储最近的高度原始值。
- 每次得到新数据时,更新这个窗口。
- 对窗口内的数据进行快速排序。
- 排序后,丢弃最高和最低的各三分之一数据(即去除可能存在的野值)。
- 对剩余的中间三分之一数据求平均值,作为本次滤波输出。
#define FILTER_WINDOW_SIZE 32
float AltitudeFilter(float newAltitude) {
static float dataWindow[FILTER_WINDOW_SIZE] = {0};
static int index = 0;
float sortedWindow[FILTER_WINDOW_SIZE];
float sum = 0.0f;
// 1. 更新滑动窗口
dataWindow[index] = newAltitude;
index = (index + 1) % FILTER_WINDOW_SIZE;
// 2. 复制到临时数组用于排序(不破坏原始顺序)
memcpy(sortedWindow, dataWindow, sizeof(dataWindow));
// 3. 快速排序
quickSort(sortedWindow, 0, FILTER_WINDOW_SIZE - 1);
// 4. & 5. 掐头去尾,取中间值平均
int startIdx = FILTER_WINDOW_SIZE / 3;
int endIdx = FILTER_WINDOW_SIZE * 2 / 3;
int validCount = endIdx - startIdx;
for(int i = startIdx; i < endIdx; i++) {
sum += sortedWindow[i];
}
return sum / validCount;
}
这个quickSort函数需要自己实现一个针对浮点数的快速排序算法。这种滤波方法在无人机项目中实测效果显著,既能滤除高频噪声,又能有效抵抗偶尔的跳变干扰,输出曲线非常平滑。
5. 系统集成与性能调优
将各个模块组合起来,并在主循环中调用,就完成了整个驱动。但要让它在实际项目中稳定运行,还需要关注一些细节。
5.1 主程序逻辑与任务调度
在main.c中,逻辑应该清晰简洁。初始化后,进入主循环,以固定的周期读取、计算并滤波高度值。
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
float baro_hPa, temp_C, altitude_m, filtered_altitude_m;
uint8_t init_status;
// 初始化SPL06
init_status = SPL06_Init();
if(init_status != 0) {
// 初始化失败,根据状态码处理错误,例如通过LED闪烁或串口打印
Error_Handler(init_status);
}
while (1) {
// 1. 获取校准后的气压和温度
if(GetCompensatedVal(&baro_hPa, &temp_C, &Coefficient) == 0) {
// 2. 转换为高度(注意单位转换,GetCompensatedVal输出hPa,getAltitude输入Pa)
altitude_m = getAltitude(baro_hPa * 100.0f);
// 3. 滤波
filtered_altitude_m = AltitudeFilter(altitude_m);
// 4. 使用滤波后的高度值(例如:发送到飞控、显示、记录)
// UseFilteredAltitude(filtered_altitude_m);
} else {
// 读取数据失败,进行错误处理
}
// 控制读取频率,例如20Hz (50ms)
HAL_Delay(50);
}
}
5.2 常见问题排查与优化建议
在实际部署中,你可能会遇到以下问题:
- I2C通信失败:首先用逻辑分析仪或示波器抓取SCL和SDA波形,检查时序是否符合标准(启动、停止、数据建立/保持时间)。检查上拉电阻是否接好,GPIO模式是否为开漏。
- 高度数据跳变大:
- 检查电源:确保给传感器的3.3V电源干净、稳定。电机等大功率设备可能引起电源噪声。
- 调整过采样率:提高气压测量的过采样率(如从
_8TIMES提高到_64TIMES)可以大幅降低噪声,但会降低输出速率。 - 优化滤波参数:调整
FILTER_WINDOW_SIZE。窗口越大,越平滑,但延迟也越大。对于高速运动的无人机,需要权衡。
- 高度值存在固定偏差:
- 校准P0:在已知海拔高度的地方(如地面),读取稳定的气压值,反推出当地的
P0基准值,替换公式中的101325。 - 温度补偿:确保温度测量和补偿流程正确。SPL06的温度补偿算法已经比较完善,但极端环境仍需验证。
- 校准P0:在已知海拔高度的地方(如地面),读取稳定的气压值,反推出当地的
最后,将完整的驱动代码模块化封装好,你会发现,这套基于软件I2C的SPL06驱动,其稳定性和可移植性甚至优于某些硬件I2C的驱动。它不依赖于特定的硬件外设,只要有两个空闲的GPIO,就能在任何一款STM32甚至其他ARM MCU上快速跑起来,这为你在不同项目间复用代码、快速原型开发提供了极大的便利。
更多推荐


所有评论(0)