1. 项目背景与应用场景

大家好,今天我想和大家分享一个非常实用的嵌入式开发实战项目:如何使用STM32的通用IO口模拟SPI通信协议,实现对BMP280气压传感器的数据采集。在实际项目中,我们经常会遇到硬件资源受限的情况,比如某些STM32型号没有足够的硬件SPI外设,或者硬件SPI已经被其他设备占用。这时候,软件SPI就成了我们的救命稻草。

BMP280是博世公司推出的一款高精度、低功耗的气压和温度传感器,广泛应用于气象监测、无人机高度控制、室内导航等领域。它支持SPI和I2C两种通信协议,但SPI协议在传输速率和抗干扰能力上更有优势。通过软件模拟SPI,我们可以在任何具备通用IO口的STM32芯片上驱动BMP280,无需依赖硬件SPI外设。

这个项目的核心难点在于如何通过代码精确模拟SPI的时序,以及如何处理BMP280的校准数据。我会从最基础的SPI协议讲起,一步步带大家实现完整的驱动代码。如果你正在做一个需要测量气压或温度的项目,或者单纯想学习软件SPI的实现原理,这篇文章都会对你有所帮助。

2. BMP280传感器基础

2.1 传感器核心特性

BMP280是一款数字式气压传感器,它的精度可以达到±1 hPa(相当于海拔高度误差约±8米),温度测量精度为±1.0°C。虽然看起来参数不算特别惊艳,但对于大多数应用来说已经足够了。我最喜欢的是它的低功耗特性,在标准模式下电流消耗只有2.7μA,特别适合电池供电的便携设备。

传感器内部集成了ADC转换器和数字处理单元,可以直接输出经过温度补偿的气压值。它支持SPI和I2C两种通信方式,SPI模式最高支持10MHz的通信速率,比I2C快得多。在实际使用中,SPI接口的抗干扰能力也更强,特别适合在电机控制等噪声较大的环境中使用。

2.2 寄存器结构与功能

要正确使用BMP280,我们需要了解它的寄存器结构。传感器内部有一组控制寄存器和数据寄存器,还有一组很重要的校准参数寄存器。控制寄存器(地址0xF4)用来设置传感器的工作模式和过采样率。比如,我们可以选择让传感器进入睡眠模式、强制模式或正常模式,还可以设置气压和温度的采样精度。

数据寄存器(0xF7到0xFC)存储了最新的测量结果。这里要注意,BMP280的输出是原始数据,需要结合校准参数才能转换成真实的气压和温度值。校准参数存储在0x88到0xA1这26个寄存器中,包括温度补偿系数和气压补偿系数。这些系数每个传感器都不一样,需要在初始化时读取并保存起来。

3. SPI通信协议详解

3.1 硬件SPI与软件SPI对比

SPI是一种同步串行通信协议,通常需要4根线:SCK(时钟)、MOSI(主机发送)、MISO(主机接收)和CS(片选)。硬件SPI由STM32内部的专用外设实现,通信速率高且不占用CPU资源。但在某些情况下,我们不得不使用软件SPI:比如硬件SPI被其他设备占用,或者使用的STM32型号没有足够的SPI外设。

软件SPI是通过GPIO口模拟SPI时序实现的。虽然速度比不上硬件SPI(通常只能达到几百KHz),但优点是非常灵活。我们可以任意选择IO口,还可以根据需要调整时序。在实际项目中,我经常用软件SPI来驱动那些对速度要求不高的传感器,比如BMP280。

3.2 时序控制要点

SPI的时序控制有几个关键点需要注意。首先是时钟极性(CPOL)和时钟相位(CPHA)。CPOL决定时钟空闲时的电平,CPHA决定数据在时钟的哪个边沿采样。BMP280支持模式0(CPOL=0,CPHA=0)和模式3(CPOL=1,CPHA=1),我们通常选择模式0。

其次是时序的严格性。虽然BMP280对时序的要求不是特别苛刻,但我们还是要保证时钟频率在传感器支持的范围内(SPI模式最高10MHz)。在软件SPI中,我们需要通过延时来确保时序正确。这里有个小技巧:可以用NOP指令或者简单的循环来实现微秒级的延时。

4. 硬件连接与引脚配置

4.1 电路连接方案

我们以STM32F103C8T6(蓝莓派开发板)为例,讲解具体的硬件连接。BMP280的SPI接口有4个引脚:SCK、MOSI、MISO和CS。我们将SCK连接到PA5,MOSI连接到PA7,MISO连接到PA6,CS连接到PA4。VCC接3.3V,GND接地。

这里要特别注意电平匹配。BMP280是3.3V设备,而STM32F103的IO口可以容忍5V输入,但输出是3.3V电平,所以直接连接没有问题。如果你使用的是5V单片机,就需要添加电平转换电路,否则可能会损坏传感器。

4.2 GPIO初始化设置

在代码中,我们需要先初始化相关的GPIO口。SCK、MOSI和CS引脚配置为推挽输出模式,速度设置为50MHz。MISO引脚配置为浮空输入模式,因为我们需要从这个引脚读取数据。

#define SPI_SCK_PIN GPIO_Pin_5
#define SPI_MOSI_PIN GPIO_Pin_7
#define SPI_MISO_PIN GPIO_Pin_6
#define SPI_CS_PIN GPIO_Pin_4
#define SPI_GPIO_PORT GPIOA

void SPI_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    // 配置SCK、MOSI和CS为推挽输出
    GPIO_InitStructure.GPIO_Pin = SPI_SCK_PIN | SPI_MOSI_PIN | SPI_CS_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStructure);
    
    // 配置MISO为浮空输入
    GPIO_InitStructure.GPIO_Pin = SPI_MISO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStructure);
    
    // 初始状态CS拉高
    GPIO_SetBits(SPI_GPIO_PORT, SPI_CS_PIN);
}

初始化完成后,CS引脚应该保持高电平,当我们需要与传感器通信时,先将CS拉低,通信完成后再拉高。这个片选信号很重要,它告诉传感器什么时候开始和结束通信。

5. 软件SPI驱动实现

5.1 字节传输函数

软件SPI的核心是字节传输函数,它要模拟完整的SPI时序。我们以模式0(CPOL=0,CPHA=0)为例:时钟空闲时为低电平,数据在时钟上升沿采样,下降沿切换。

uint8_t SPI_SendByte(uint8_t byte)
{
    uint8_t i;
    uint8_t received_byte = 0;
    
    for (i = 0; i < 8; i++) {
        // 设置MOSI引脚输出数据位
        if (byte & 0x80) {
            GPIO_SetBits(SPI_GPIO_PORT, SPI_MOSI_PIN);
        } else {
            GPIO_ResetBits(SPI_GPIO_PORT, SPI_MOSI_PIN);
        }
        byte <<= 1;
        
        // 产生时钟上升沿
        GPIO_SetBits(SPI_GPIO_PORT, SPI_SCK_PIN);
        
        // 读取MISO数据
        received_byte <<= 1;
        if (GPIO_ReadInputDataBit(SPI_GPIO_PORT, SPI_MISO_PIN)) {
            received_byte |= 0x01;
        }
        
        // 产生时钟下降沿
        GPIO_ResetBits(SPI_GPIO_PORT, SPI_SCK_PIN);
    }
    
    return received_byte;
}

这个函数同时完成了发送和接收功能。在每一个时钟周期内,我们先设置MOSI引脚的电平,然后产生时钟上升沿,接着读取MISO引脚的电平,最后产生时钟下降沿。循环8次后,就完成了一个字节的传输。

5.2 片选控制与时序优化

片选信号的控制也很重要。在开始传输前,我们需要先将CS拉低,等待一小段时间后再开始发送数据。传输完成后,也要等待一段时间再拉高CS。这个等待时间虽然很短,但对某些传感器来说很关键。

void SPI_CS_Enable(void)
{
    GPIO_ResetBits(SPI_GPIO_PORT, SPI_CS_PIN);
    delay_us(1);  // 短暂延时
}

void SPI_CS_Disable(void)
{
    delay_us(1);  // 短暂延时
    GPIO_SetBits(SPI_GPIO_PORT, SPI_CS_PIN);
}

在实际项目中,我发现添加微秒级的延时可以提高通信的稳定性。特别是在低速单片机中,IO口的状态变化需要一定时间,适当的延时可以确保传感器能够正确识别信号。

6. BMP280驱动开发

6.1 寄存器读写函数

有了基础的SPI函数,我们就可以编写BMP280的读写函数了。BMP280的寄存器读写有特定的格式:写操作时直接发送寄存器地址和数据;读操作时需要先发送寄存器地址(最高位置1),然后读取返回的数据。

void BMP280_WriteByte(uint8_t reg_addr, uint8_t data)
{
    SPI_CS_Enable();
    SPI_SendByte(reg_addr);
    SPI_SendByte(data);
    SPI_CS_Disable();
}

uint8_t BMP280_ReadByte(uint8_t reg_addr)
{
    uint8_t data;
    SPI_CS_Enable();
    SPI_SendByte(reg_addr | 0x80);  // 设置读标志
    data = SPI_SendByte(0x00);      // 发送 dummy byte 读取数据
    SPI_CS_Disable();
    return data;
}

写函数比较简单,直接发送寄存器地址和数据即可。读函数稍微复杂一些,需要先发送寄存器地址(最高位置1表示读操作),然后发送一个空字节来产生时钟信号,同时读取传感器返回的数据。

6.2 传感器初始化配置

初始化BMP280需要配置控制寄存器和配置寄存器。控制寄存器(0xF4)设置传感器的工作模式和过采样率,配置寄存器(0xF5)设置滤波器和采样间隔。

void BMP280_Init(void)
{
    SPI_GPIO_Init();
    
    // 软复位传感器
    BMP280_WriteByte(0xE0, 0xB6);
    delay_ms(10);
    
    // 读取校准参数
    BMP280_ReadCalibrationData();
    
    // 配置控制寄存器
    BMP280_WriteByte(0xF4, 0x27);  // 正常模式,气压x16,温度x2
    BMP280_WriteByte(0xF5, 0xA0);  // 滤波器系数16,采样间隔1000ms
}

在初始化过程中,我习惯先对传感器进行软复位,确保它处于已知状态。然后读取校准参数,这些参数每个传感器都不一样,需要保存起来用于后续的数据转换。最后配置控制寄存器,设置合适的工作模式和过采样率。

7. 数据采集与校准处理

7.1 原始数据读取

BMP280的测量数据存储在6个连续寄存器中(0xF7到0xFC),前3个字节是气压数据,后3个字节是温度数据。我们需要一次性读取这6个字节,然后组合成20位的原始数据。

void BMP280_ReadRawData(int32_t *raw_pressure, int32_t *raw_temperature)
{
    uint8_t data[6];
    
    // 读取6个数据字节
    for (int i = 0; i < 6; i++) {
        data[i] = BMP280_ReadByte(0xF7 + i);
    }
    
    // 组合气压数据
    *raw_pressure = ((uint32_t)data[0] << 12) | ((uint32_t)data[1] << 4) | (data[2] >> 4);
    
    // 组合温度数据
    *raw_temperature = ((uint32_t)data[3] << 12) | ((uint32_t)data[4] << 4) | (data[5] >> 4);
}

这里要注意数据的组合方式。每个测量值都是20位的,但寄存器只用了20位中的高18位或低18位,需要按照数据手册的说明进行移位和组合。

7.2 校准算法实现

原始数据需要经过复杂的校准计算才能转换成真实值。BMP280提供了大量的校准参数,包括温度补偿系数和气压补偿系数。校准算法比较复杂,但遵循固定的公式。

int32_t BMP280_CompensateTemperature(int32_t adc_T)
{
    int32_t var1, var2, T;
    
    var1 = ((((adc_T >> 3) - ((int32_t)calib.dig_T1 << 1))) * ((int32_t)calib.dig_T2)) >> 11;
    var2 = (((((adc_T >> 4) - ((int32_t)calib.dig_T1)) * ((adc_T >> 4) - ((int32_t)calib.dig_T1))) >> 12) * ((int32_t)calib.dig_T3)) >> 14;
    
    t_fine = var1 + var2;
    T = (t_fine * 5 + 128) >> 8;
    
    return T;
}

int32_t BMP280_CompensatePressure(int32_t adc_P)
{
    int64_t var1, var2, p;
    
    var1 = ((int64_t)t_fine) - 128000;
    var2 = var1 * var1 * (int64_t)calib.dig_P6;
    var2 = var2 + ((var1 * (int64_t)calib.dig_P5) << 17);
    var2 = var2 + (((int64_t)calib.dig_P4) << 35);
    var1 = ((var1 * var1 * (int64_t)calib.dig_P3) >> 8) + ((var1 * (int64_t)calib.dig_P2) << 12);
    var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)calib.dig_P1) >> 33;
    
    if (var1 == 0) {
        return 0;
    }
    
    p = 1048576 - adc_P;
    p = (((p << 31) - var2) * 3125) / var1;
    var1 = (((int64_t)calib.dig_P9) * (p >> 13) * (p >> 13)) >> 25;
    var2 = (((int64_t)calib.dig_P8) * p) >> 19;
    
    p = ((p + var1 + var2) >> 8) + (((int64_t)calib.dig_P7) << 4);
    
    return (int32_t)p;
}

校准算法看起来复杂,但其实都是按照数据手册提供的公式实现的。温度校准相对简单,气压校准涉及到64位运算,需要注意数据溢出问题。校准后的温度单位为0.01°C,气压单位为Pa。

8. 实际应用与调试技巧

8.1 常见问题排查

在实际使用中,可能会遇到各种问题。最常见的是通信失败,读取的数据全是0或0xFF。这时候可以先检查硬件连接是否正确,特别是CS引脚是否正常控制。然后用逻辑分析仪或示波器观察SPI时序,看时钟和数据信号是否符合预期。

如果通信正常但数据不准,可能是校准参数读取有误。建议在初始化时打印出所有的校准参数,与数据手册中的示例值进行对比。还有可能是校准算法实现有误,特别是64位运算容易出错。

8.2 性能优化建议

软件SPI的速度受限于GPIO操作和延时函数。如果对速度有要求,可以尝试以下优化:使用寄存器直接操作代替库函数、减少不必要的延时、使用编译器优化等。在我的测试中,STM32F103的软件SPI最高可以达到约1MHz的速率,对于BMP280来说绰绰有余。

对于功耗敏感的应用,可以在不需要测量时让传感器进入睡眠模式。BMP280在睡眠模式下的电流只有0.1μA,非常适合电池供电设备。需要测量时再切换到强制模式,测量完成后自动返回睡眠模式。

9. 完整代码实现与测试

9.1 工程文件结构

一个完整的BMP280驱动应该包含以下几个文件:bmp280.h(头文件)、bmp280.c(驱动实现)、main.c(示例代码)。头文件中定义寄存器地址、校准参数结构和函数原型。

在bmp280.c中实现所有的驱动函数,包括SPI初始化、寄存器读写、校准参数读取、数据读取和校准计算。main.c中展示如何使用这些函数,包括初始化、读取数据和打印结果。

9.2 测试与验证

测试时可以先读取传感器的ID寄存器(0xD0),BMP280的ID应该是0x58。如果能够正确读取ID,说明基本通信没有问题。然后读取校准参数,检查是否在合理范围内。

最后进行实际测量,与已知准确的气压计和温度计进行对比。在我的测试中,软件SPI驱动的BMP280能够达到与硬件SPI相同的精度,温度误差在±0.5°C以内,气压误差在±1 hPa以内。

记得在代码中添加必要的延时,特别是在初始化

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐