> 技术文档 > STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280

STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280

随着MEMS微电子技术的进步,各种微型传感器由于其性能好,价格低等优点目前正越来越多得应用在我们的生活之中.从前只有在导弹,卫星等尖端领域所用到的例如陀螺仪,气压计,磁力计正在慢慢普及到千家万户.比如用在导弹上的惯性制导核心-陀螺仪,被用在了手机FPS游戏的瞄准之中,以前硕大无比性能还差的气压计如今升级成仅有几十毫克重量的微机电气压传感器被用在四轴无人机之中,做飞行定高…
气压传感器在各种场合都有其广阔的应用,此文用于记录笔者学习STM32过程中驱动BMP280的心路历程.
本文开发环境:STM32F103C8T6 HAL库+Keil5 AC6
首先看最终效果:
STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
可见,当前气压约101.6KPa,温度约7摄氏度,计算得海拔高度约-29.6m.

硬件连接

STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
硬件连接很简单,这里不在阐述,看图.

软件部分

数据手册

寄存器分布
STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
如上图所示,这是BMP280的寄存器map.看起来很唬人其实简单得很.
BMP280的温度,气压数据分别由三部分构成:MSB,LSB,XLSB.也就是高位,低位,次低位(小数位).
config和ctrl_meas寄存器顾名思义一个用来配置,一个用来控制测量,然后就是状态字,复位寄存器,chip_id寄存器,还有26个校准值(不可修改)用于读出和实际值做运算得到真实值.
一个器件,驱动的一般步骤是先初始化然后才能工作.
对于BMP280来说,初始化需要这些步骤:

  1. 读取校准寄存器26个数据
  2. 复位
  3. 配置
  4. 测量

而和BMP280通信还要先配置好I2C,这里使用STM32的硬件I2C外设,配置不再阐述,上个笔记有,值得注意的是I2C的频率设置的是10K,并非100K.
然后笔者遇到一个现象:在配置I2C时,如果先打开GPIO时钟再配置GPIO,然后配置I2C,这样会使I2C总线在初始化的时候有一个短暂的电平跳变,如图所示:
STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
这个电平跳变并不会影响I2C的数据传输,但是笔者觉得有Bug就修复以免以后给自己挖坑,就先配置GPIO然后再打开GPIO时钟,这样跳变果然消失了.皆大欢喜.
但是事实证明,万事都不能想当然.
\"修\"完这个Bug之后,笔者用这一套代码去驱动AT24C02就遇到问题了,问题有一定概率性,有时候I2C可以通信,有时挂b.当时以为是STM32最小系统的问题,毕竟那个最小系统板陪伴笔者已有数年,成色早已伊拉克.
换了个STM32问题好像得到解决了,不过笔者并没有重复试验,就丢一边去了,直到笔者驱动BMP280时,改用了中断的方式发送,结果,I2C死活不进中断,焦头烂额之际,笔者用STM32CubeMX配置却一点问题也没有.
于是乎,就检查自己的代码和CubeMX生成的有哪些不一样,翻来覆去,一行一行对比,最终发现了,CubeMX是先打开GPIO外设时钟,然后再配置GPIOInit,而我的正相反.
抱着死马当活马医的想法,就按他的来,结果问题都烟消云散了,STM32仿佛被打通任督二脉,飞一般的进I2C中断,漂漂亮亮一点儿都不脱泥带水的出中断,恢复现场.笔者甚感神奇,百度搜索关键词,发现了一些端倪.
原来,STM32的寄存器是一个个的D型触发器构成的集合.
STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
如图,必须要有时钟,输入的数据才会被锁存,否则数据是无效的.这也就解释了为什么要先开启时钟.
这也说明了学好数电对学习更高阶的应用(STM32)有很大的帮助,基础不牢,地动山摇啊.
奈何笔者本就不是读书的料,只是出于对技术的热爱才走上了嵌软这条路.要是笔者学STM32之前能打好数电的基础该多好,好在现在还有时间,笔者目前大一,大学时光还剩两年半,我希望用自顶而下的学习方式来反其道而行之,我相信这种方法应该比较适合我自己.
悟已往之不谏,知来者之可追.
回归正题.
我们配置好I2C之后就可以开始和传感器通信了,这也是本文重要部分.
我们先看BMP280时序图,这里只使用I2C通信方式.
STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280首先看I2C读时序,和众多I2C器件一样,都类似于先写\"数据指针\"然后再读,这里不再阐述.
值得注意的是,BMP280的时序可以用到一个叫重复起始条件的信号(见上图红框),也就是发完一帧不停止,再产生start.
一图胜千言:

STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
而我们使用这两个函数只能发送一帧之后就停下来:

STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
要实现上述功能,需要HAL库里的一个叫顺序读写的功能:
STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
这些函数就是用于顺序读写的,使用中断收发,所以要在I2C的MspInit里使能中断,这里不放代码了.
这些函数有个参数叫XferOptions,用于控制IO口行为,也就是选择发送完就stop啊还是发完一帧先等着.
STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
如图,拉低SCL就是I2C总线正忙,正在被占用,SCL为H了就说明总线空闲了.
对于XferOptions参数具体描述可参考HAL库使用手册第256页:Interrupt mode or DMA mode IO sequential operation .
代码有一些宏定义和自定义数据类型:
.h文件摘要:

//使用最大数据分辨率,也只实现了这一个#define sDRV_BMP280_USE_MAX_RESOLUTION//使用定点数做数据校准运算,注释掉将会使用浮点运算#define sDRV_BMP280_USE_FIXED_POINT_COMPE//为了方便使用指针,这里顺序有要求typedef struct{ uint8_t press_msb; //这些都是从寄存器里读到的值 uint8_t press_lsb; uint8_t press_xlsb; uint8_t temp_msb; uint8_t temp_lsb; uint8_t temp_xlsb; uint32_t temp; //最终温度数据 uint32_t press; //最终气压数据 uint8_t cal_val[26];//获取到的寄存器里的校准值 uint16_t dig_T1; //这里参考数据手册 int16_t dig_T2; int16_t dig_T3; uint16_t dig_P1; int16_t dig_P2; int16_t dig_P3; int16_t dig_P4; int16_t dig_P5; int16_t dig_P6; int16_t dig_P7; int16_t dig_P8; int16_t dig_P9;}sDrv_BMP280_t;

.c文件摘要:

//这是BMP280的I2C地址#define BMP280_ADDR  (0x76 << 1)//这些是BMP280的寄存器地址#define BMP280_REG_ID (0xD0)#define BMP280_REG_RESET (0xE0)#define BMP280_REG_STATUS (0xF3)#define BMP280_REG_CTRL_MEAS (0xF4)#define BMP280_REG_CONFIG (0xF5)#define BMP280_REG_PRESS_MSB (0xF7)#define BMP280_REG_PRESS_LSB (0xF8)#define BMP280_REG_PRESS_XLSB (0xF9)#define BMP280_REG_TEMP_MSB (0xFA)#define BMP280_REG_TEMP_LSB (0xFB)#define BMP280_REG_TEMP_XLSB (0xFC)#define BMP280_REG_CAL0 (0x88) //第0个校准值寄存器的地址//这些是一些值#define BMP280_VAL_RESET (0xB6)#define BMP280_VAL_CHIPID (0x58)//引用BSP的I2C句柄extern I2C_HandleTypeDef hi2c1;//用来存储BMP280的一些信息sDrv_BMP280_t bmp280;//这是算法的校准值,参考博世的数据手册代码int32_t t_fine;

初始化传感器这里直接贴代码,注释写的很详细了,有些下面再补充:

/*@brief BMP280初始化** @param 无** @return HAL_StatusTypeDef:如果初始化失败(通信异常)会返回HAL_ERROR,否则返回HAL_OK*/HAL_StatusTypeDef sDrv_BMP280_Init(){ //初始化I2C sBSP_I2C1_Init(); //读取BMP280里的chip_id寄存器与标准值比较,用于检查通信是否正常 uint8_t chip_id = 0; uint8_t chip_id_reg = BMP280_REG_ID; //发送一帧数据设置数据指针,但并不发送stop HAL_I2C_Master_Seq_Transmit_IT(&hi2c1,BMP280_ADDR,&chip_id_reg,1,I2C_FIRST_FRAME); Delay_ms(2); //接收数据指针指向的内容,发送stop HAL_I2C_Master_Seq_Receive_IT(&hi2c1,BMP280_ADDR,&chip_id,1,I2C_LAST_FRAME); Delay_ms(2); //如果通信异常就返回ERR if(chip_id != BMP280_VAL_CHIPID){ return HAL_ERROR; } //读取校准寄存器 uint8_t cal_reg[] = {BMP280_REG_CAL0}; HAL_I2C_Master_Seq_Transmit_IT(&hi2c1,BMP280_ADDR,cal_reg,1,I2C_FIRST_FRAME); Delay_ms(10); HAL_I2C_Master_Seq_Receive_IT(&hi2c1,BMP280_ADDR,bmp280.cal_val,26,I2C_LAST_FRAME); Delay_ms(100); //复位BMP280 uint8_t reset_reg[] = {BMP280_REG_RESET,BMP280_VAL_RESET}; HAL_I2C_Master_Transmit_IT(&hi2c1,BMP280_ADDR,reset_reg,2); Delay_ms(10); //设置为超高解析度 #ifdef sDRV_BMP280_USE_MAX_RESOLUTION //气压过采样:x16,温度过采样:x2,IIR滤波:16,Timing:0.5ms,Mode:Normal uint8_t conf_seq[] = {BMP280_REG_CTRL_MEAS,0x57,BMP280_REG_CONFIG,0x14}; HAL_I2C_Master_Transmit_IT(&hi2c1,BMP280_ADDR,conf_seq,4); Delay_ms(5); #endif// sHMI_Debug_Printf(\"calibration data:\");// for(uint8_t i = 0;i < 26;i++){// sHMI_Debug_Printf(\"0x%02X \",bmp280.cal_val[i]);// }// sHMI_Debug_Printf(\"\\n\\n\"); //把接收到的数据变成数据手册里的样子 //下面两块代码等价// for(uint8_t i = 0;i < 12;i++){// *((uint16_t*)(&(bmp280.dig_T1)) + (uint16_t)i) = \\// (((uint16_t)bmp280.cal_val[((i * 2)) + 1]) << 8) | \\// ((uint16_t)bmp280.cal_val[(i * 2)]);  // } //指针真好玩 for(uint8_t i=0;i<12;i++)*((uint16_t*)(&(bmp280.dig_T1))+(uint16_t)i)=(((uint16_t)bmp280.cal_val[((i*2))+1])<<8)|((uint16_t)bmp280.cal_val[(i*2)]); return HAL_OK;}

代码如上,我相信大部分代码都能理解,只是最后的这个for循环是什么鬼?哈哈.这就是指针的魔力.
查看数据手册,可以发现需要把从寄存器里读出的26的uint8_t数据变成12个16位数据:

STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
具体描述参考手册.
也就是我们要把:

STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
这26个校准值变成dig_T/Px的变量,存储起来用于算法校准
举个例子,如上图就是变成:
dig_T1: 0x6B0B
dig_T2: 0x632C
dig_T3: 0x0032

csdn一位朋友这样写的
STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
来源于:https://blog.csdn.net/bdjsm_hh/article/details/107623788

足足有几十行,笔者觉得这样是在不够优雅,于是乎:

for(uint8_t i = 0;i < 12;i++){ *((uint16_t*)(&(bmp280.dig_T1)) + (uint16_t)i) = \\ (((uint16_t)bmp280.cal_val[((i * 2)) + 1]) << 8) | \\ ((uint16_t)bmp280.cal_val[(i * 2)]);  }

如果再进一步变成一行就得到了:

//指针真好玩for(uint8_t i=0;i<12;i++)*((uint16_t*)(&(bmp280.dig_T1))+(uint16_t)i)=(((uint16_t)bmp280.cal_val[((i*2))+1])<<8)|((uint16_t)bmp280.cal_val[(i*2)]);

哈哈,是不是看到这玩意就头疼?其实原理很简单.
for循环遍历12次因为有12个最终数据嘛.

*((uint16_t*)(&(bmp280.dig_T1)) + (uint16_t)i) = 

这一句意思是取bmp280这个结构体里的dig_T1成员的地址然后加上for的偏移量i再解引用就得到了一个指向以dig_T1为首地址的一个12个uint16_t元素的数组,相当于:bmp280.dig_T1i.
注意一个uint16_t*如果加1则实际上偏移了2字节(因为是uint16_t的类型的指针).

(((uint16_t)bmp280.cal_val[((i * 2)) + 1]) << 8) | \\((uint16_t)bmp280.cal_val[(i * 2)]);

等号右边的也好理解,就是把读取到的原始数据,按照数据手册表里的样子把两个uint8_t融合成一个uint16_t.
这里不用考虑符号位.uint8_t和int8_t都只是标记而已,在内存里都是8个比特.
指针,妙不可言.
其实这样做也没带来多少时间或空间复杂度的降低,只是看起来优雅,但,这就够了.
再看这行代码编译之后的汇编代码吧:
STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
至此,我们已经成功的初始化了传感器,接下来我们可以读值了.

/*@brief BMP280获取测量值** @param 无** @return HAL_StatusTypeDef:如果传感器在忙会返回HAL_ERROR,否则返回HAL_OK*/HAL_StatusTypeDef sDrv_BMP280_GetMeasure(){ uint8_t status_reg_val = 0xFF; uint8_t status_reg = BMP280_REG_STATUS; //读取状态寄存器 HAL_I2C_Master_Seq_Transmit_IT(&hi2c1,BMP280_ADDR,&status_reg,1,I2C_FIRST_FRAME); Delay_ms(2); HAL_I2C_Master_Seq_Receive_IT(&hi2c1,BMP280_ADDR,&status_reg_val,1,I2C_LAST_FRAME); Delay_ms(2); //读取忙标志 if((status_reg_val & 0x08) == 1){ return HAL_ERROR; } //获取温度气压数据 uint8_t press_msb_reg = BMP280_REG_PRESS_MSB; HAL_I2C_Master_Seq_Transmit_IT(&hi2c1,BMP280_ADDR,&press_msb_reg,1,I2C_FIRST_FRAME); Delay_ms(2); HAL_I2C_Master_Seq_Receive_IT(&hi2c1,BMP280_ADDR,&(bmp280.press_msb),6,I2C_LAST_FRAME); Delay_ms(10); return HAL_OK;}

其实这里不读状态寄存器加以判断也是可以的,数据手册里写到,其实这里的寄存器也有一个影子结构,类似于STM31外设里的影子寄存器一样,直接读也不影响.
下面是获取温度和压力数据:

/*@brief BMP280获取压力数据** @param 无** @return double:气压数据,单位Pa*/double sDrv_BMP280_GetPress(){ //数据合并 int32_t press = ((uint32_t)(bmp280.press_msb) << 12) | ((uint32_t)(bmp280.press_lsb) << 4) | (((uint32_t)(bmp280.press_xlsb) >> 4)); //如果使用定点运算 #ifdef sDRV_BMP280_USE_FIXED_POINT_COMPE //这些是博世提供的算法 int64_t var1, var2, p;var1 = ((int64_t)t_fine) - 128000;var2 = var1 * var1 * (int64_t)bmp280.dig_P6;var2 = var2 + ((var1*(int64_t)bmp280.dig_P5)<<17);var2 = var2 + (((int64_t)bmp280.dig_P4)<<35);var1 = ((var1 * var1 * (int64_t)bmp280.dig_P3)>>8) + ((var1 * (int64_t)bmp280.dig_P2)<<12);var1 = (((((int64_t)1)<<47)+var1))*((int64_t)bmp280.dig_P1)>>33;if (var1 == 0){return 0; // avoid exception caused by division by zero}p = 1048576-press;p = (((p<<31)-var2)*3125)/var1;var1 = (((int64_t)bmp280.dig_P9) * (p>>13) * (p>>13)) >> 25;var2 = (((int64_t)bmp280.dig_P8) * p) >> 19;p = ((p + var1 + var2) >> 8) + (((int64_t)bmp280.dig_P7)<<4);return (double)p / 256.0; #else //使用浮点运算 //这些是博世提供的算法 double var1, var2, p;var1 = ((double)t_fine/2.0) - 64000.0;var2 = var1 * var1 * ((double)bmp280.dig_P6) / 32768.0;var2 = var2 + var1 * ((double)bmp280.dig_P5) * 2.0;var2 = (var2/4.0)+(((double)bmp280.dig_P4) * 65536.0);var1 = (((double)bmp280.dig_P3) * var1 * var1 / 524288.0 + ((double)bmp280.dig_P2) * var1) / 524288.0;var1 = (1.0 + var1 / 32768.0)*((double)bmp280.dig_P1);if (var1 == 0.0){return 0; // avoid exception caused by division by zero}p = 1048576.0 - (double)press;p = (p - (var2 / 4096.0)) * 6250.0 / var1;var1 = ((double)bmp280.dig_P9) * p * p / 2147483648.0;var2 = p * ((double)bmp280.dig_P8) / 32768.0;p = p + (var1 + var2 + ((double)bmp280.dig_P7)) / 16.0;return p; #endif}/*@brief BMP280获取温度数据** @param 无** @return double:温度数据,单位degC*/double sDrv_BMP280_GetTemp(){ //数据合并 int32_t temp = ((uint32_t)(bmp280.temp_msb) << 12) | ((uint32_t)(bmp280.temp_lsb) << 4) | (((uint32_t)(bmp280.press_xlsb) >> 4)); //如果使用定点运算 #ifdef sDRV_BMP280_USE_FIXED_POINT_COMPE int32_t var1, var2, T;var1 = ((((temp>>3) - ((int32_t)bmp280.dig_T1<<1))) * ((int32_t)bmp280.dig_T2)) >> 11;var2 = (((((temp>>4) - ((int32_t)bmp280.dig_T1)) * ((temp>>4) - ((int32_t)bmp280.dig_T1))) >> 12) * ((int32_t)bmp280.dig_T3)) >> 14;t_fine = var1 + var2;T = (t_fine * 5 + 128) >> 8;return (double)T / 100.0; #else //浮点运算 double var1, var2, T;var1 = (((double)temp)/16384.0 - ((double)bmp280.dig_T1)/1024.0) * ((double)bmp280.dig_T2);var2 = ((((double)temp)/131072.0 - ((double)bmp280.dig_T1)/8192.0) *(((double)temp)/131072.0 - ((double)bmp280.dig_T1)/8192.0)) * ((double)bmp280.dig_T3);t_fine = (int32_t)(var1 + var2);T = (var1 + var2) / 5120.0; return T; #endif}

有两种补偿算法可选,定点补偿和浮点补偿,笔者这里的F103没有FPU,所以用了定点补偿,速度相当快,完成一次运算笔者仿真计算得到只要约0.5us的时间,而使用浮点运算,特别这里还是双精度,则要0.11ms左右,使用定点补偿快了200多倍.但是得到的结果好像也没有太多不一样.
这个补偿算法都是博世提供的,在数据手册里有写,值得注意的是,t_fine这个变量存储着温度补偿值,对计算压力补偿是有用的,也就是温度算法得到的t_fine值要给压力补偿用.
然后我们如果要得到海拔信息,可以用一个函数进行近似计算:

double calculate_altitude(double pressure) { double pressure_sea_level = 101325; // 标准海平面大气压力  double altitude = 44330 * (1 - pow((pressure / pressure_sea_level), 1 / 5.255)); return altitude; } 

这是由ChatGPT提供的算法.
然后就能得到测量值了:

STM32笔记06-硬件I2C驱动BMP280读取气压,温度_stm32 bmp280
可见,气压值会随着温度和气候条件的变化而变化.甚至对传感器吹口气都会让气压值变化.

感谢你看到这里,我这个笔记其实本意是给自己看的,但是又希望我的文章对其他同学萌有所帮助,故发在知乎这个平台上.
如果对你有帮助,还请给我点个赞呀.
有错误还请各位前辈指正,笔者万分感谢!
-END-
-By Sightseer.
-2024.01.25 in home.