一个成功的网站必须具备哪几个要素,小程序在线制作平台,织梦手机网站模板下载,建设企业银行u盾如何使用MS5611气压传感器在天空星STM32F407上的I2C驱动移植与数据读取实战 最近在做一个无人机项目#xff0c;需要精确测量飞行高度#xff0c;选来选去#xff0c;MS5611这款高精度气压传感器成了我的首选。它精度高、功耗低#xff0c;而且价格也合适。不过#xff0c;当我拿到…MS5611气压传感器在天空星STM32F407上的I2C驱动移植与数据读取实战最近在做一个无人机项目需要精确测量飞行高度选来选去MS5611这款高精度气压传感器成了我的首选。它精度高、功耗低而且价格也合适。不过当我拿到模块准备在天空星STM32F407开发板上用起来时发现网上的代码要么不完整要么和我的硬件对不上调试起来挺费劲的。所以我花时间把整个驱动从零开始移植、调试了一遍把过程中的关键步骤和容易踩的坑都整理了出来。如果你也在用天空星或者类似的STM32F4板子想驱动MS5611来测气压和高度那这篇教程就是为你准备的。我会手把手带你完成硬件连接、软件模拟I2C、读取数据到最终换算的全过程代码都是可以直接用的。1. 认识MS5611你的高精度“气压计”在动手写代码之前咱们先花几分钟了解一下MS5611这个小模块。它本质上是一个集成了压力传感器和温度传感器的芯片通过测量大气压来反推海拔高度精度能达到±1.5米巴换算成高度大概±1米左右对于很多需要高度信息的项目来说完全够用了。我用的模块是GY-63非常常见。它的核心参数如下参数数值说明工作电压1.8V ~ 3.6V直接用3.3V供电就行工作电流0.25uA ~ 23uA非常省电温度测量范围-40℃ ~ 85℃温度精度±0.8℃气压测量范围10 ~ 1200 mbar覆盖地面到约9000米高空气压精度±1.5 mbar通信接口I2C 或 SPI通过PS引脚选择注意我用的这个模块原理图上PS引脚通过一个上拉电阻接到了高电平所以它默认工作在I2C模式。这也是我们本篇教程采用的方式。模块的I2C器件地址由CSB引脚决定CSB接高电平时器件地址为0xEE写 /0xEF读。CSB接低电平时器件地址为0xEC写 /0xED读。我的模块上CSB也是接高电平的所以后续代码中我们使用的写地址就是0xEE。2. 硬件连接把模块和开发板“牵上线”硬件连接很简单MS5611模块就三个引脚VCC, GND, SDA, SCL我们需要把它和天空星开发板的I/O口连起来。这里有个关键点我选择使用软件模拟I2C而不是STM32硬件I2C。原因很简单软件模拟更灵活时序完全可控避开了硬件I2C那些可能出现的兼容性和稳定性问题在项目初期调试起来更省心。我选择了STM32F407的PA1和PA2这两个引脚分别作为SCL和SDA。你可以根据自己板子的情况选择其他空闲的GPIO只需要在代码里改一下宏定义就行。连接方式如下表所示MS5611模块引脚天空星STM32F407引脚连接说明VCC3.3V电源正极GNDGND电源地SCLPA1I2C时钟线SDAPA2I2C数据线接好线硬件部分就搞定了。接下来就是重头戏——软件驱动。3. 软件驱动移植手把手编写I2C底层驱动代码主要分为两个文件bsp_ms5611.c函数实现和bsp_ms5611.h引脚和函数声明。我们一步步来。3.1 头文件定义与引脚配置首先在bsp_ms5611.h中我们要定义好使用的GPIO端口和引脚以及一些操作宏。#ifndef _BSP_MS5611_H_ #define _BSP_MS5611_H_ #include stm32f4xx.h // 端口移植 - 这里根据你的连接修改 #define RCC_MS5611 RCC_AHB1Periph_GPIOA #define PORT_MS5611 GPIOA #define GPIO_SCL GPIO_Pin_1 // 时钟线接PA1 #define GPIO_SDA GPIO_Pin_2 // 数据线接PA2 // 设置SDA为输出模式用于发送数据 #define SDA_OUT() { \ GPIO_InitTypeDef GPIO_InitStructure; \ GPIO_InitStructure.GPIO_Pin GPIO_SDA; \ GPIO_InitStructure.GPIO_Mode GPIO_Mode_OUT; \ GPIO_InitStructure.GPIO_OType GPIO_OType_OD; // 开漏输出 \ GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; \ GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; // 内部上拉 \ GPIO_Init(PORT_MS5611, GPIO_InitStructure); \ } // 设置SDA为输入模式用于接收数据 #define SDA_IN() { \ GPIO_InitTypeDef GPIO_InitStructure; \ GPIO_InitStructure.GPIO_Pin GPIO_SDA; \ GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN; \ GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; // 内部上拉 \ GPIO_Init(PORT_MS5611, GPIO_InitStructure); \ } // 读取SDA引脚电平 #define SDA_GET() GPIO_ReadInputDataBit(PORT_MS5611, GPIO_SDA) // 控制SDA和SCL引脚输出高低电平 #define SDA(x) GPIO_WriteBit(PORT_MS5611, GPIO_SDA, (x?Bit_SET:Bit_RESET) ) #define SCL(x) GPIO_WriteBit(PORT_MS5611, GPIO_SCL, (x?Bit_SET:Bit_RESET) ) // 函数声明 void MS5611_GPIO_Init(void); char MS5611_Reset(void); void MS5611_Read_PROM(void); float Get_TEMP(void); float Get_pressure(void); #endif这里有几个细节需要注意GPIO_OType_OD 我们配置为开漏输出模式。这是I2C总线标准要求可以方便地实现“线与”功能避免总线冲突。GPIO_PuPd_UP 启用了内部上拉电阻。虽然I2C总线规范要求外部上拉但STM32的内部上拉电阻约40kΩ在低速模拟I2C时勉强可用。如果通信不稳定强烈建议你在SDA和SCL线上各加一个4.7kΩ的外部上拉电阻到3.3V这是最稳妥的做法。如果你换用了其他GPIO引脚只需要修改RCC_MS5611,PORT_MS5611,GPIO_SCL,GPIO_SDA这四个宏定义即可。3.2 GPIO初始化与I2C时序模拟接下来在bsp_ms5611.c中我们先实现最基础的GPIO初始化和I2C通信时序。I2C通信就像两个人对话有固定的“开场白”起始信号、“结束语”停止信号和“应答机制”。首先是引脚初始化函数它负责打开GPIO时钟并把我们用的两个引脚配置好。#include bsp_ms5611.h #include stdio.h #include board.h // 这个头文件包含了delay_ms等函数你需要根据你的工程替换 // 存储从传感器读出的6个出厂校准值 uint16_t Cal_C1_6[8]; void MS5611_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能GPIOA的时钟 RCC_AHB1PeriphClockCmd(RCC_MS5611, ENABLE); // 初始化SCL和SDA为开漏输出并上拉 GPIO_InitStructure.GPIO_Pin GPIO_SDA | GPIO_SCL; GPIO_InitStructure.GPIO_Mode GPIO_Mode_OUT; GPIO_InitStructure.GPIO_OType GPIO_OType_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(PORT_MS5611, GPIO_InitStructure); // 先将两条线拉高总线处于空闲状态 GPIO_SetBits(PORT_MS5611, GPIO_SDA | GPIO_SCL); }然后是I2C的起始信号和停止信号。你可以把SCL看作时钟拍子SDA看作数据。起始信号就是在SCL高电平时SDA来一个下降沿停止信号则是在SCL高电平时SDA来一个上升沿。void IIC_Start(void) { SDA_OUT(); // 确保SDA是输出模式 SDA(1); delay_us(5); SCL(1); delay_us(5); SDA(0); // 在SCL高电平期间SDA产生下降沿 - 起始信号 delay_us(5); SCL(0); // 拉低SCL准备后续数据传输 delay_us(5); } void IIC_Stop(void) { SDA_OUT(); SCL(0); SDA(0); delay_us(5); SCL(1); delay_us(5); SDA(1); // 在SCL高电平期间SDA产生上升沿 - 停止信号 delay_us(5); }发送一个字节的数据是从最高位(MSB)开始一位一位地放到SDA线上每放一位就用SCL产生一个上升沿“告诉”从机来读取。void Send_Byte(uint8_t dat) { int i 0; SDA_OUT(); SCL(0); // 拉低时钟开始数据传输 for(i 0; i 8; i) { // 取出最高位右移7位后结果不是1就是0 SDA( (dat 0x80) 7 ); delay_us(1); SCL(1); // 时钟拉高从机在此刻采样SDA数据 delay_us(5); SCL(0); // 时钟拉低为下一位数据准备 delay_us(5); dat 1; // 左移准备发送下一位 } }读取一个字节则相反主机先把SDA线释放设为输入然后在SCL的每个上升沿去读取SDA线上的电平。unsigned char Read_Byte(void) { unsigned char i, receive 0; SDA_IN(); // SDA设置为输入释放总线 for(i 0; i 8; i) { SCL(0); delay_us(5); SCL(1); // 时钟拉高 delay_us(5); receive 1; // 左移为接收新位腾出空间 if(SDA_GET()) // 读取SDA电平 { receive | 1; // 如果为高则最低位置1 } delay_us(5); } SCL(0); return receive; }最后是应答机制。主机每发送完8位数据需要释放SDA线并产生一个时钟脉冲SCL1在这个脉冲期间从机会把SDA拉低表示“收到”ACK。主机读取数据后也需要在最后一个字节后发送一个“非应答”NACK告诉从机“不用再发了”。void IIC_Send_Ack(unsigned char ack) { SDA_OUT(); SCL(0); SDA(0); // 先拉低准备产生应答或非应答信号 delay_us(5); if(!ack) SDA(0); // ack0发送应答(低电平) else SDA(1); // ack1发送非应答(高电平) SCL(1); delay_us(5); SCL(0); SDA(1); // 释放SDA线 } unsigned char I2C_WaitAck(void) { char ack 0; unsigned char ack_flag 10; // 超时计数 SCL(0); SDA(1); // 主机释放SDA线 SDA_IN(); // 切换为输入等待从机拉低 delay_us(5); SCL(1); // 产生第9个时钟脉冲 delay_us(5); // 等待SDA被从机拉低同时做超时判断 while((SDA_GET() 1) (ack_flag)) { ack_flag--; delay_us(5); } if(ack_flag 0) // 超时没有收到应答 { IIC_Stop(); return 1; } else // 收到应答 { SCL(0); SDA_OUT(); // 切换回输出模式准备后续操作 } return ack; }提示delay_us和delay_ms是你需要自己实现的微秒和毫秒延时函数。如果你的工程里没有可以用STM32的SysTick定时器简单实现一个。4. MS5611驱动核心读取校准值与原始数据底层I2C通信搞定后我们就可以和MS5611“对话”了。MS5611内部有6个非常重要的出厂校准参数C1-C6每次测量都需要用它们来计算最终的温度和气压值。所以第一步就是把这些参数读出来。4.1 传感器复位与读取校准值首先给传感器发个复位命令让它回到初始状态。char MS5611_Reset(void) { IIC_Start(); // 起始信号 Send_Byte(0xee | 0); // 发送器件写地址 (0xEE) if(I2C_WaitAck() 1) return 1; // 等待应答失败返回1 Send_Byte(0x1e); // 发送复位命令 if(I2C_WaitAck() 1) return 2; // 等待应答失败返回2 IIC_Stop(); return 0; // 复位成功 }复位后需要等待一小段时间比如300ms让传感器稳定。然后我们就可以去读取那6个校准值了。它们存储在PROM可编程只读存储器的特定地址里。void MS5611_Read_PROM(void) { uint8_t data_H 0, data_L 0; uint8_t i 0; for(i 0; i 8; i) // 注意循环8次因为前7个地址有数据0是厂商信息1-6是C1-C67是CRC { IIC_Start(); Send_Byte(0xee | 0); // 写地址 I2C_WaitAck(); Send_Byte(0xA0 i * 2); // 发送PROM地址命令每个校准值占2个字节 I2C_WaitAck(); IIC_Stop(); delay_us(200); // 命令间需要短暂延时 IIC_Start(); Send_Byte(0xee | 1); // 读地址 I2C_WaitAck(); data_H Read_Byte(); // 读取高8位 IIC_Send_Ack(0); // 发送应答继续读 data_L Read_Byte(); // 读取低8位 IIC_Send_Ack(1); // 发送非应答读取结束 IIC_Stop(); // 将两个8位数据合并成一个16位校准值 Cal_C1_6[i] (data_H 8) | data_L; } }读出来的Cal_C1_6[1]到Cal_C1_6[6]就对应着C1到C6这6个校准系数后面计算要用到。4.2 启动转换与读取原始数据MS5611测量需要两步先启动一次气压D1或温度D2的AD转换等待转换完成再读取转换结果。uint32_t MS5611_Read_D1_D2(uint8_t regaddr) { uint32_t dat 0; uint8_t buff[3] {0}; // 第一步发送转换命令 IIC_Start(); Send_Byte(0xee | 0); // 写地址 if(I2C_WaitAck() 1) printf(D1 NACK -1\r\n); Send_Byte(regaddr); // regaddr: 0x48 启动气压转换0x58 启动温度转换 if(I2C_WaitAck() 1) printf(D1 NACK -2\r\n); IIC_Stop(); delay_ms(10); // 等待AD转换完成OSR4096时最大需要9ms // 第二步发送读ADC命令并读取24位结果 IIC_Start(); Send_Byte(0xee | 0); // 写地址 if(I2C_WaitAck() 1) printf(D1 NACK -3\r\n); Send_Byte(0X00); // ADC读命令 if(I2C_WaitAck() 1) printf(D1 NACK -4\r\n); IIC_Stop(); delay_ms(10); // 第三步读取转换结果3个字节 IIC_Start(); Send_Byte(0xee | 1); // 读地址 if(I2C_WaitAck() 1) printf(D1 NACK -5\r\n); buff[0] Read_Byte(); // 读取最高字节 IIC_Send_Ack(0); // 应答继续读 buff[1] Read_Byte(); // 读取中间字节 IIC_Send_Ack(0); // 应答继续读 buff[2] Read_Byte(); // 读取最低字节 IIC_Send_Ack(1); // 非应答读取结束 IIC_Stop(); // 将3个字节合并成一个24位数据 dat (((buff[0] 16) | (buff[1] 8)) | buff[2]); return dat; }5. 数据换算从原始值到温度气压拿到了原始数据D1气压和D2温度以及之前读出的校准系数C1-C6就可以按照数据手册给出的公式进行计算了。这个过程有点复杂但代码已经帮你封装好了。uint32_t D1 0, D2 0, dT 0; float Get_TEMP(void) { float dat 0; long long TEMP 0; // 1. 读取温度和气压的原始数据 D1 MS5611_Read_D1_D2(0x48); // 读取气压原始值D1 delay_ms(10); D2 MS5611_Read_D1_D2(0x58); // 读取温度原始值D2 delay_ms(10); // 2. 计算dT温度差值 dT D2 - (Cal_C1_6[5] * 256.0); // C5对应PROM地址5的值 // 3. 计算实际温度单位0.01°C TEMP 2000 (((float)dT * Cal_C1_6[6]) / 8388608.0); // C6对应PROM地址6的值 // 4. 转换为摄氏度这里去掉了小数可根据需要调整 dat (((TEMP / 1000) * 10) (TEMP / 100 % 10)); return dat; // 返回如“25.0”格式的温度 } float Get_pressure(void) { long long SENS 0; long long P 0; long long OFF 0; // 先获取温度因为气压计算需要用到dT Get_TEMP(); // 计算OFF和SENS补偿值 OFF Cal_C1_6[2] * 65536.0 Cal_C1_6[4] * dT / 128; SENS (Cal_C1_6[1] * 32768.0) ((Cal_C1_6[3] * dT) / 256.0); // 计算最终气压值单位Pa P (D1 * SENS / 2097152.0 - OFF) / 32768.0; return (P / 100.0); // 转换为百帕HPa1 HPa 100 Pa }注意上面的计算是简化的一阶补偿算法适用于大部分场合。MS5611数据手册还提供了更精确的二阶温度补偿公式如果你的应用对低温20°C或高温45°C下的精度要求极高建议查阅手册实现完整的二阶补偿。6. 实战应用在主函数中调用所有驱动函数都准备好了现在我们在主函数里把它们用起来实现每秒读取一次温度和气压并打印出来。#include board.h #include bsp_uart.h #include stdio.h #include bsp_ms5611.h int main(void) { board_init(); // 系统时钟、外设初始化 uart1_init(115200U); // 初始化串口1用于打印数据 MS5611_GPIO_Init(); // 初始化MS5611的GPIO MS5611_Reset(); // 传感器复位 delay_ms(300); // 等待复位完成 MS5611_Read_PROM(); // 读取校准系数 printf(MS5611 Sensor Start!\r\n); while(1) { // 读取并打印温度摄氏度 printf(温度 %.0f℃\r\n, Get_TEMP()); // 读取并打印气压百帕 HPa printf(气压 %.2f HPa\r\n, Get_pressure()); printf(\n); delay_ms(1000); // 每秒读取一次 } }将代码编译下载到天空星开发板打开串口助手波特率115200你应该能看到类似下面的输出MS5611 Sensor Start! 温度 25℃ 气压 1013.25 HPa 温度 25℃ 气压 1013.24 HPa ...恭喜你到这里你已经成功在天空星STM32F407上驱动了MS5611气压传感器。整个过程的关键在于理解I2C的时序以及耐心调试。如果一开始没有数据别着急先用逻辑分析仪或者示波器抓一下SDA和SCL的波形看看起始信号、地址、数据位对不对这是排查I2C问题最有效的方法。