
基于GD32VW553的BMP180气压传感器驱动移植与海拔测量实战最近在做一个环境监测的小项目需要用到气压传感器来测量海拔高度。手头正好有立创的GD32VW553开发板和一块BMP180气压传感器模块折腾了一番把驱动成功移植了上去。今天就把这个从硬件连接到代码实现的完整过程分享出来如果你也打算在GD32VW553上使用BMP180跟着这篇教程一步步来应该能少走不少弯路。BMP180是博世Bosch公司的一款经典气压传感器它不仅能测气压还能测温度通过气压值还能推算出海拔高度。它的精度不错体积小功耗也低关键是接口简单就一个I2C非常适合咱们嵌入式项目。这篇教程我会手把手带你完成硬件连接、I2C驱动编写、传感器数据读取和海拔计算最终在串口上看到实时的温度、气压和海拔数据。1. 准备工作认识你的硬件在动手写代码之前咱们先搞清楚手头有什么以及它们要怎么连起来。1.1 BMP180传感器模块我用的模块是市面上常见的GY-68 BMP180模块。它的核心参数如下参数规格工作电压1.8V ~ 3.6V (与GD32VW553的3.3V完美兼容)工作电流0.1uA ~ 1000uA (非常省电)温度测量范围0°C ~ 65°C温度精度±1.0°C气压测量范围300 hPa ~ 1100 hPa (相当于海拔-500米到9000米)气压精度±1.0 hPa通信接口I2C模块引脚3 Pin (VCC, GND, SCL, SDA)注意BMP180的I2C器件地址是固定的。写入数据时地址是0xEE读取数据时地址是0xEF。这个地址在代码里会用到。这个传感器有个特点它输出的原始数据不是直接可用的温度和气压值。每个传感器出厂时都在内部的E2PROM里存了11个独特的校准系数。我们需要先读出这些系数然后用它们和传感器读出的原始数据一起通过一套公式计算才能得到真实的物理值。听起来有点复杂别担心后面的代码已经把公式都封装好了。1.2 硬件连接连接非常简单只需要四根线。我用的是GD32VW553开发板连接关系如下表BMP180模块引脚GD32VW553开发板引脚说明VCC3V3电源正极接3.3VGNDGND电源地SCLPA2I2C时钟线SDAPA3I2C数据线把线对应接好就行。I2C通信线SCL和SDA通常需要上拉电阻好在GD32VW553的GPIO配置为开漏输出Open-Drain模式并内部上拉以及BMP180模块本身一般也带了上拉电阻所以咱们直接连接即可不需要额外加电阻。2. 软件工程搭建与I2C驱动编写硬件连好了接下来就是软件部分。因为GD32VW553的库函数可能没有提供完整的BMP180驱动或者我们想更深入地理解通信过程这里选择用GPIO模拟I2C时序的方式来驱动传感器。这种方式更底层但对理解I2C协议非常有帮助。2.1 创建驱动文件首先在你的工程里新建两个文件bsp_bmp180.c和bsp_bmp180.h。别忘了把存放头文件的路径添加到编译器的包含目录Include Paths里。这个操作如果忘了编译时会报错找不到头文件。2.2 编写头文件 (bsp_bmp180.h)头文件主要进行宏定义和函数声明。咱们先看看关键的引脚定义和类型重定义。#ifndef BSP_CODE_BSP_BMP180_H_ #define BSP_CODE_BSP_BMP180_H_ #include gd32vw55x.h #include systick.h // 为了方便定义一些简化的类型别名 #ifndef u8 #define u8 uint8_t #endif #ifndef u16 #define u16 uint16_t #endif #ifndef u32 #define u32 uint32_t #endif // 延时函数宏方便调用 #ifndef delay_ms #define delay_ms(x) delay_1ms(x) #endif #ifndef delay_us #define delay_us(x) delay_1us(x) #endif // 引脚和时钟使能宏定义 #define Module_RCU_ENABLE() \ rcu_periph_clock_enable(Module_SCL_RCU); \ rcu_periph_clock_enable(Module_SDA_RCU); // SCL引脚定义 (PA2) #define Module_SCL_RCU RCU_GPIOA #define Module_SCL_PORT GPIOA #define Module_SCL_PIN GPIO_PIN_2 // SDA引脚定义 (PA3) #define Module_SDA_RCU RCU_GPIOA #define Module_SDA_PORT GPIOA #define Module_SDA_PIN GPIO_PIN_3 // SDA引脚模式切换宏输入/输出 #define SDA_IN() \ gpio_mode_set(Module_SDA_PORT, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, Module_SDA_PIN); #define SDA_OUT() \ gpio_mode_set(Module_SDA_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, Module_SDA_PIN); \ gpio_output_options_set(Module_SDA_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_25MHZ, Module_SDA_PIN); // 引脚电平读写宏 #define SCL(BIT) gpio_bit_write(Module_SCL_PORT, Module_SCL_PIN, BIT) #define SDA(BIT) gpio_bit_write(Module_SDA_PORT, Module_SDA_PIN, BIT) #define SDA_GET() gpio_input_bit_get(Module_SDA_PORT, Module_SDA_PIN) // 函数声明 void BMP180_Init(void); float BMP180_Get_Temperature(void); float BMP180_Get_Pressure(void); void BMP180_Write_Cmd(uint8_t regaddr,uint8_t cmd); void BMP180_Get_param(void); float BMP180_Get_Altitude(float p); #endif /* BSP_CODE_BSP_BMP180_H_ */这里有几个关键点引脚定义明确指定SCL和SDA分别对应PA2和PA3和咱们的硬件连接一致。开漏输出GPIO_OTYPE_OD将引脚设置为开漏模式这是I2C总线要求的可以实现“线与”功能。内部上拉GPIO_PUPD_PULLUP启用了内部上拉电阻省去了外接电阻。2.3 编写I2C底层驱动 (bsp_bmp180.c 第一部分)我们用GPIO来模拟I2C的时序。I2C通信就像两个人主机和从机打电话有固定的“开场白”、“说话顺序”和“结束语”。下面这些函数就是实现这些规则的。/* IIC起始信号SCL高电平时SDA产生一个下降沿 */ void IIC_Start(void) { SDA_OUT(); SDA(1); delay_us(5); SCL(1); delay_us(5); SDA(0); // 在SCL高电平期间拉低SDA产生起始条件 delay_us(5); SCL(0); // 拉低SCL准备传输数据 delay_us(5); } /* IIC停止信号SCL高电平时SDA产生一个上升沿 */ void IIC_Stop(void) { SDA_OUT(); SCL(0); SDA(0); SCL(1); delay_us(5); SDA(1); // 在SCL高电平期间释放SDA产生停止条件 delay_us(5); } /* 主机发送应答(ACK)或非应答(NACK)信号 */ void IIC_Send_Ack(unsigned char ack) { SDA_OUT(); SCL(0); if(!ack) SDA(0); // ack0发送应答信号拉低SDA else SDA(1); // ack1发送非应答信号拉高SDA delay_us(5); SCL(1); delay_us(5); SCL(0); SDA(1); // 释放SDA线 } /* 主机等待从机BMP180的应答信号 */ unsigned char I2C_WaitAck(void) { char ack 0; unsigned char ack_flag 10; // 超时计数 SCL(0); SDA(1); SDA_IN(); // SDA切换为输入模式准备读取从机应答 delay_us(5); SCL(1); // 在第9个时钟周期从机应拉低SDA delay_us(5); // 循环检测SDA是否被拉低应答 while( (SDA_GET()1) ( ack_flag ) ) { ack_flag--; delay_us(5); } if( ack_flag 0 ) // 超时未收到应答 { IIC_Stop(); return 1; // 返回1表示错误 } else // 收到应答 { SCL(0); SDA_OUT(); // SDA切换回输出模式 } return 0; // 返回0表示成功 } /* 主机向从机发送一个字节数据 */ void Send_Byte(uint8_t dat) { int i 0; SDA_OUT(); SCL(0); // 拉低时钟准备数据 for( i 0; i 8; i ) // 从最高位(MSB)开始发送 { // 取出当前最高位并设置到SDA线上 SDA( (dat 0x80) 7 ); delay_us(1); SCL(1); // 拉高时钟从机在此时钟上升沿采样数据 delay_us(5); SCL(0); // 拉低时钟为下一位数据做准备 delay_us(5); dat 1; // 数据左移准备发送下一位 } } /* 主机从从机读取一个字节数据 */ 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); // 拉高时钟主机在此时钟高电平期间读取SDA delay_us(5); receive 1; // 左移为接收新位腾出空间 if( SDA_GET() ) // 读取SDA电平 { receive | 1; // 如果为高电平则该位为1 } delay_us(5); } SCL(0); return receive; }提示I2C时序对延时很敏感。这里的delay_us(5)是经验值保证了通信的稳定性。如果你的主频不同可能需要微调。3. BMP180传感器驱动实现底层通信搞定了现在就可以基于这些函数来跟BMP180“对话”了。对话主要分三步初始化并读取校准系数、读取原始数据、用校准系数计算真实值。3.1 定义校准数据结构与初始化首先我们需要一个结构体来存放从传感器E2PROM里读出的11个校准系数。// 在文件开头定义校准参数结构体 typedef struct _BMP180_STRUCT{ short AC1; short AC2; short AC3; uint16_t AC4; uint16_t AC5; uint16_t AC6; short B1; short B2; short MB; short MC; short MD; }_BMP180_PARAM_; _BMP180_PARAM_ param {0}; // 全局变量存储校准参数 long B5 0; // 计算温度时的一个中间变量计算气压时也会用到接下来是两个核心的读写函数它们封装了I2C的读写操作让我们可以方便地操作BMP180的寄存器。/* 向BMP180的指定寄存器写入一个命令 */ void BMP180_Write_Cmd(uint8_t regaddr, uint8_t cmd) { IIC_Start(); // 起始信号 Send_Byte(0xEE); // 发送器件地址写方向 (0xEE) if( I2C_WaitAck() 1 ) printf(Write_Cmd NACK -1\r\n); Send_Byte(regaddr); // 发送寄存器地址 if( I2C_WaitAck() 1 ) printf(Write_Cmd NACK -2\r\n); Send_Byte(cmd); // 发送要写入的数据命令 if( I2C_WaitAck() 1 ) printf(Write_Cmd NACK -3\r\n); IIC_Stop(); // 停止信号 } /* 从BMP180的指定地址开始读取指定长度的数据 */ uint16_t BMP180_Read16(uint16_t regaddr, uint8_t len) { int timeout 0; uint16_t dat[3] {0}; int i 0; for( i 0; i len; i ) { IIC_Start(); Send_Byte(0xEE); // 发送器件地址写方向 if( I2C_WaitAck() 1 ) printf(Read_Reg NACK -1\r\n); Send_Byte(regaddr i); // 发送要读取的寄存器地址 if( I2C_WaitAck() 1 ) printf(Read_Reg NACK -2\r\n); // 重新发送起始条件切换为读操作 do{ timeout; delay_ms(1); IIC_Start(); Send_Byte(0xEF); // 发送器件地址读方向 (0xEF) } while(I2C_WaitAck() 1 (timeout 5) ); // 等待从机应答 dat[i] Read_Byte(); // 读取一个字节数据 IIC_Send_Ack(1); // 发送非应答信号(NACK)表示读取结束 IIC_Stop(); delay_ms(1); } // 根据读取的长度组合数据 if( len 2 ) return ( ((uint32_t)dat[0] 8) | dat[1] ); // 16位数据 if( len 3 ) return (( ((uint32_t)dat[0] 16) | ((uint32_t)dat[1] 8) | (dat[2]) ) 8); // 24位数据右移处理 return 0; }有了这两个函数读取校准系数就非常简单了。每个校准系数都是16位2个字节存储在固定的地址。/* 读取BMP180的11个出厂校准系数 */ void BMP180_Get_param(void) { param.AC1 (short)BMP180_Read16(0xaa, 2); param.AC2 (short)BMP180_Read16(0xac, 2); param.AC3 (short)BMP180_Read16(0xae, 2); param.AC4 BMP180_Read16(0xb0, 2); param.AC5 BMP180_Read16(0xb2, 2); param.AC6 BMP180_Read16(0xb4, 2); param.B1 (short)BMP180_Read16(0xb6, 2); param.B2 (short)BMP180_Read16(0xb8, 2); param.MB (short)BMP180_Read16(0xba, 2); param.MC (short)BMP180_Read16(0xbc, 2); param.MD (short)BMP180_Read16(0xbe, 2); }最后完成初始化函数它负责配置GPIO引脚并读取校准系数。void BMP180_Init(void) { // 1. 使能GPIOA时钟 Module_RCU_ENABLE(); // 2. 初始化SCL引脚为开漏输出上拉速度25MHz gpio_mode_set(Module_SCL_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, Module_SCL_PIN); gpio_output_options_set(Module_SCL_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_25MHZ, Module_SCL_PIN); // 3. 初始化SDA引脚为开漏输出上拉速度25MHz gpio_mode_set(Module_SDA_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, Module_SDA_PIN); gpio_output_options_set(Module_SDA_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_25MHZ, Module_SDA_PIN); // 4. 将总线拉高进入空闲状态 SCL(1); SDA(1); delay_ms(100); // 等待传感器上电稳定 // 5. 读取校准系数这是后续计算准确数据的关键 BMP180_Get_param(); delay_ms(500); // 等待传感器稳定 }3.2 读取并计算温度值BMP180测量温度需要两步启动转换然后读取原始数据并进行计算。float BMP180_Get_Temperature(void) { long UT 0; // 未校准的温度原始值 long X1 0, X2 0; // 1. 启动温度转换向控制寄存器(0xF4)写入命令0x2E BMP180_Write_Cmd(0xF4, 0x2E); delay_ms(6); // 等待转换完成数据手册要求至少4.5ms // 2. 读取转换结果从数据寄存器(0xF6, 0xF7)读取2个字节 UT BMP180_Read16(0xf6, 2); // 3. 使用校准系数计算真实温度 (公式来自BMP180数据手册) X1 ((long)UT - param.AC6) * param.AC5 / 32768.0; X2 ((long)param.MC * 2048.0) / ( X1 param.MD ); B5 X1 X2; // B5是一个重要的中间变量计算气压时也会用到 // 4. 最终计算结果单位为0.1°C这里转换为°C return ((B5 8) / 16.0) * 0.1f; }3.3 读取并计算气压值气压的测量比温度稍复杂一点因为它有四种工作模式通过控制寄存器的bit7和bit6设置模式越高精度越高但转换时间也越长。我们这里使用默认的超低功耗模式oss0。float BMP180_Get_Pressure(void) { long UP 0; // 未校准的气压原始值 uint8_t oss 0; // 过采样设置0为超低功耗模式 long X1 0, X2 0; // 注意计算气压前必须先读取一次温度以更新B5值 BMP180_Get_Temperature(); // 1. 启动气压转换向控制寄存器写入命令 (0x34 (oss6)) BMP180_Write_Cmd(0xF4, (0x34 (oss 6))); delay_ms(10); // 等待转换完成超低功耗模式约需4.5ms这里给足余量 // 2. 读取转换结果从数据寄存器(0xF6,0xF7,0xF8)读取3个字节24位 UP BMP180_Read16(0xf6, 3); // 3. 使用校准系数和B5计算真实气压 (公式来自BMP180数据手册) int32_t B6 B5 - 4000; X1 (param.B2 * (B6 * B6 12)) 11; X2 param.AC2 * B6 11; int32_t X3 X1 X2; int32_t B3 (((param.AC1 * 4 X3) oss) 2) 2; // 注意原文公式此处有简化实际代码按数据手册实现 X1 param.AC3 * B6 13; X2 (param.B1 * (B6 * B6 12)) 16; X3 ((X1 X2) 2) 2; uint32_t B4 param.AC4 * (uint32_t)(X3 32768) 15; uint32_t B7 ((uint32_t)UP - B3) * (50000UL oss); int32_t p; if (B7 0x80000000) { p (B7 1) / B4; } else { p (B7 / B4) 1; } X1 (p 8) * (p 8); X1 (X1 * 3038) 16; X2 (-7357 * p) 16; p p ((X1 X2 3791) 4); return (float)p; // 返回气压值单位是帕斯卡(Pa) }注意气压计算公式看起来复杂但它是博世官方提供的校准算法直接使用即可。关键在于之前读取的11个校准系数param和温度计算中得到的B5变量。3.4 根据气压计算海拔高度有了准确的气压值就可以利用大气压随高度变化的公式来估算海拔了。这里用的是国际标准大气压公式。float BMP180_Get_Altitude(float p) { float altitude 0; // 标准海平面气压为101325 Pa // 公式altitude 44330 * [1 - (p / p0)^(1/5.255) ] altitude 44330 * (1 - pow((p) / 101325.0f, 1.0f / 5.255f)); return altitude; // 返回海拔高度单位米(m) }这个公式是一个近似值在标准大气条件下比较准确。实际的海拔还会受天气影响。4. 在主程序中测试驱动函数都写好了最后就是在主函数里调用它们了。下面是一个简单的测试例程它会循环读取并打印温度、气压和海拔。#include gd32vw55x.h #include systick.h #include stdio.h #include bsp_bmp180.h int main(void) { // 系统初始化时钟、延时等 systick_config(); // 串口初始化用于打印数据假设已实现 // gd_eval_com_init(EVAL_COM0); printf(GD32VW553 BMP180 Test Start...\r\n); // 初始化BMP180传感器 BMP180_Init(); printf(BMP180 Sensor Init Successful !!\r\n); while(1) { // 读取温度 float Temperature BMP180_Get_Temperature(); // 读取气压 float Pressure BMP180_Get_Pressure(); // 根据气压计算海拔 float Altitude BMP180_Get_Altitude(Pressure); // 通过串口打印结果 printf(\r\n); printf(Temperature %.2f *C\r\n, Temperature); printf(Pressure %.2f Pa\r\n, Pressure); printf(Altitude %.2f m\r\n, Altitude); delay_ms(1000); // 每秒读取一次 } }将代码编译下载到GD32VW553开发板打开串口助手波特率根据你的串口初始化设置应该就能看到每秒更新一次的传感器数据了。5. 可能遇到的问题与调试技巧读取数据全是0或65535首先检查硬件连接尤其是VCC和GND是否接反SCL和SDA是否接对。然后用逻辑分析仪或示波器抓一下I2C波形看起始信号、地址、应答信号是否正常。计算出的温度气压值异常务必确保成功读取了11个校准系数。可以在BMP180_Get_param()函数后通过串口打印出param结构体里的所有值看看是否都为0。如果为0说明I2C通信有问题没读到数据。海拔高度不准海拔计算依赖于一个参考海平面气压101325 Pa。这个值是标准值实际的海平面气压会随天气变化。要想获得更准确的海拔需要知道当前当地的海平面气压值来替换公式中的101325.0f。I2C通信不稳定可以适当调整delay_us()中的延时参数。如果总线上有多个设备要确保地址不冲突并且每个设备都有正确的上拉电阻。这个驱动我已经在实际项目中跑过了稳定性没问题。代码里最核心的就是那套校准计算公式虽然看起来复杂但它是传感器高精度的保证直接套用即可。希望这篇教程能帮你顺利在GD32VW553上驱动起BMP180。