STM32模拟IIC驱动BMP280气压传感器OLED显示与Zigbee传输

发布时间:2026/9/16 14:03:31
STM32模拟IIC驱动BMP280气压传感器OLED显示与Zigbee传输 简介面向嵌入式、物联网与自动化方向的学习者及工程师这份资源提供了一整套基于标准库的STM32F103RCT6数据采集与无线传输参考方案采用模拟IIC驱动OLED屏实时显示并连接GYBMP280大气压传感器采集数据再通过串口3按自定义协议将气压浮点数据拆分为字节流由DL-20型ZigBee模块向外发送。工程按模块划分包含模拟IIC驱动代码、GYBMP280驱动、串口发送与协议封装程序编译通过可直接使用适合课程设计、毕业设计或工程原型参考。压缩包共194个文件以C源文件、头文件、Keil工程文件、编译生成的axf/hex固件及map/lst文档为主还有一段mp4演示视频整体大小10.7MB便于快速查看工程结构并烧录验证。已有443人学习或浏览。除完整工程源码外还包含编译中间文件与烧录固件可跳过环境配置直接对照实物验证模块化代码注释清晰适合深入学习STM32标准库、软件模拟IIC、气压传感采集及串口组包发送等关键环节。1. 从串口3发出气压数据先要跨过模拟IIC这道坎拿STM32F103RCT6单片机的标准库工程去驱动OLED屏和GYBMP280大气压传感器再用串口3把数据交给DL-20 Zigbee模块这条链路看上去很常规真正做起来坑全在模拟IIC的时序和浮点数据的拆包上。很多人做完OLED显示后一加BMP280就卡死或者气压读数总是0xFF。这个工程基于标准库开发把模拟IIC驱动、BMP280校准补偿、OLED实时显示、串口3自定义协议发送串成了一条完整链路浮点气压数据拆成字节后经DL-20透传出去。适合电子信息、物联网、自动化的同学拿来做课程设计或项目原型也适合想用标准库模块化组织传感器驱动的工程师。整个工程按驱动文件、应用文件、协议发送分开改引脚不用动协议层。2. 模拟IIC与BMP280驱动时序、校准和补偿逻辑2.1 BMP280 硬件连接与IIC地址选择BMP280 是 Bosch 的数字气压/温度传感器这个工程模块名写作 GYBMP280模块板上已经把去耦电容和上拉电阻做好了。板上的SDO引脚决定IIC地址SDO接地时地址是0x76接VCC时是0x77。工程默认使用0x76如果读出来的寄存器值全是0xFF先检查SDO和电源再检查IIC引脚是否和OLED屏共用导致总线被占用。引脚接STM32说明VCC3.3V不能接5VIIC上拉会被拉到3.3VGNDGND与STM32共地SCLPB6模拟IIC时钟线SDAPB7模拟IIC数据线开漏输出并外接上拉CSB3.3VIIC模式片选拉高SDOGND选择地址0x76这里的SCL/SDA引脚号取决于工程实际接线。常见设计是把OLED和BMP280并联在同一对IIC引脚也有的工程各用一组GPIO。标准库下每个GPIO引脚都要单独配置两个设备并联时要注意IIC地址不能相同BMP280是0x76或0x77OLED一般是0x3C或0x3D两者不冲突但总线电容会变大驱动能力不足时要把IIC速率降到100kHz以下或者把两个设备拆到独立引脚上。模拟IIC的好处就在这里GPIO重新分配一下驱动代码几乎不用改。2.2 模拟IIC的起始、停止和应答位实现模拟IIC不用芯片自带IIC外设直接用GPIO翻转电平。标准库下就是把SCL/SDA配置成开漏输出然后按IIC协议在SCL高电平期间切换SDA电平。起始条件是SCL高时SDA从高到低变化停止条件是SCL高时SDA从低到高变化。工程里常见的iic.c代码如下void IIC_Start(void) { SDA_H; // 数据线拉高 SCL_H; // 时钟线拉高 delay_us(4); SDA_L; // 数据线先低形成起始条件 delay_us(4); SCL_L; // 拉低时钟准备传数据 } void IIC_Stop(void) { SDA_L; SCL_H; delay_us(4); SDA_H; // 数据线拉高形成停止条件 delay_us(4); }这里的SDA_H、SCL_H是宏内部通常用GPIO_SetBits或直接操作ODR寄存器。延时用delay_us(4)对应IIC时钟约100kHz对BMP280和SSD1306都安全。不要用空循环做延时否则Keil优化等级一变时钟频率可能翻倍。起始和停止之间如果时序太短BMP280会不识别总线命令后续读出的寄存器值全是0xFF。模拟IIC还要检查应答位。读寄存器时从机收到地址后会拉低SDA表示ACK主机读完后要发NACK告诉从机停止发送。代码里通常这样写uint8_t IIC_WaitAck(void) { SDA_H; // 释放SDA等待从机拉低 delay_us(2); SCL_H; delay_us(4); uint8_t ack SDA_READ(); // 读SDA引脚电平0表示应答 SCL_L; delay_us(4); return ack; }如果从机没应答IIC_WaitAck返回1。上层驱动可以用返回值做错误处理而不是让程序死等。很多“加了OLED函数卡死”的场景表面看是OLED初始化失败实际上是SDA引脚被某个从设备拉低或者应答检查没有超时机制导致while循环出不来。这个函数是所有模拟IIC外设共用的关键点BMP280和OLED都要走同一套总线时序。2.3 BMP280寄存器读取和校准参数BMP280上电后要先读出厂校准参数。这些参数存放在0x88到0x9F寄存器区间包含dig_T1、dig_T2、dig_T3和dig_P1到dig_P9。温度和气压补偿都依赖这批参数不读校准值就计算出来的数据没有任何意义。标准库下通过模拟IIC读单个寄存器的函数如下uint8_t BMP280_ReadReg(uint8_t reg) { uint8_t value 0; IIC_Start(); IIC_SendByte((BMP280_ADDR 1) | 0x00); // 地址加写位 IIC_WaitAck(); IIC_SendByte(reg); // 要读的寄存器地址 IIC_WaitAck(); IIC_Start(); // 重复起始条件 IIC_SendByte((BMP280_ADDR 1) | 0x01); // 切换成读模式 IIC_WaitAck(); value IIC_ReadByte(); // 从机返回一字节 IIC_SendAck(0); // 主机发NACK IIC_Stop(); return value; }参数说明BMP280_ADDR是前面选好的IIC地址左移一位后是7位设备地址的8位表示形式reg是寄存器地址例如0xD0读ID0x88读校准参数。读取单个寄存器时先发写地址和寄存器地址再用重复起始条件切换为读模式。读完最后一字节后主机必须发NACK否则从机会继续发下一字节导致数据错位。工程中读取校准参数时会把多个相邻寄存器连续读出例如一次读24字节再按表拆分。注意dig_T2、dig_T3是有符号数直接用uint16_t强转会得到错误的负温度系数。2.4 温度补偿和气压补偿实现BMP280数据手册给出固定补偿公式官方驱动也是同一套。温度补偿的核心是计算中间变量t_fine工程代码int32_t t_fine; int32_t BMP280_Compensate_T(int32_t adc_T) { int32_t var1 ((((adc_T 3) - ((int32_t)dig_T1 1)) * (int32_t)dig_T2) 11); int32_t var2 (((((adc_T 4) - ((int32_t)dig_T1)) * ((adc_T 4) - ((int32_t)dig_T1))) 12) * (int32_t)dig_T3) 14; t_fine var1 var2; return (t_fine * 5 128) 8; }这里adc_T是从0xFA、0xFB、0xFC读到的20位原始温度值也就是三个字节拼成32位数据后右移4位。变量全部用32位有符号整数原因是为了避免8位乘法溢出。t_fine是全局变量因为气压补偿也要用到它。气压补偿公式更长工程里一般写成BMP280_Compensate_P同样用32位整数运算最后返回Pa为单位的气压值再除以1000变成kPa。最容易写错的是右移位数和变量符号建议对照官方头文件逐行检查。模拟IIC读取BMP280时建议把温度补偿和气压补偿分成两个函数避免每次只算压力时重复计算温度。工程内部可以在BMP280初始化结尾读一次芯片IDBMP280_ReadReg(0xD0)读出的值应当是0x58如果不是直接返回错误码。这样硬件接错时能在OLED上显示错误提示方便排查。如果你习惯用HAL库这个工程的模拟IIC时序代码可以原封不动搬到CubeMX生成的工程里只需要把GPIO操作宏换成HAL_GPIO_ReadPin和HAL_GPIO_WritePin。3. OLED显示与浮点转字节协议拆解与数据链路3.1 4针OLED屏的模拟IIC驱动要点常见的4针OLED屏接口就是VCC、GND、SCL、SDA控制芯片是SSD1306类模拟IIC直接驱动。工程初始化代码里包含标准的SSD1306初始化序列关闭显示、设置显示时钟、设置多路复用比、设置段重映射、设置COM扫描方向、开启电荷泵、设置显示模式、开启显示。初始化做完后定位到坐标直接写显存就能显示字符和数字。我一般不会在每次刷新时全屏清空而是直接定位到坐标替换数字否则气压每秒刷新几次时屏幕会明显闪。OLED显示字符和浮点数的函数在工程里是按模块封装的例如void OLED_ShowFloatNum(uint8_t x, uint8_t y, float val, uint8_t int_len, uint8_t dec_len) { int32_t int_part (int32_t)val; int32_t dec_part (int32_t)((val - int_part) * 10000); if (dec_part 0) dec_part -dec_part; OLED_ShowIntNum(x, y, int_part, int_len); OLED_ShowChar(x int_len * 8, y, .); OLED_ShowIntNum(x (int_len 1) * 8, y, dec_part, dec_len); }x、y是字符串在OLED上的起始像素坐标int_len是整数部分占的位数dec_len是小数部分宽度。dec_len传入2就表示保留两位小数。注意浮点数经过取整后dec_part是原始小数乘以10000再做类型转换如果dec_len设为2显示时会截断到两位这是速度较快的做法但会有四舍五入误差。对气压采集显示来说0.01kPa量级的误差可以接受。工程里气压单位显示为kPa所以屏幕上一段时间内会稳定显示101.3左右。3.2 浮点转字节的两种做法串口发送只能逐字节发送float在STM32F103上占4字节因此必须把气压浮点转成字节数组。工程里提供了两种思路。一种是指针强转float pressure 101.325f; uint8_t send_buf[4]; uint8_t *p (uint8_t *)pressure; for (int i 0; i 4; i) { send_buf[i] p[i]; }另一种是联合体typedef union { float fval; uint8_t bytes[4]; } float2bytes_t; float2bytes_t converter; converter.fval pressure; // converter.bytes[0] 到 converter.bytes[3] 对应内存中的四个字节指针方式代码直观但用memcpy更安全因为它不受编译器别名分析和优化影响。联合体方式在嵌入式编译器里普遍支持很多老工程师喜欢用因为内核里更容易管理。无论哪种方式接收端都必须知道大小端模式。STM32F103是小端float的bytes[0]是最低有效字节电脑端如果是x86同样是小端但如果接收端是ESP32或其他平台就要在协议里约定字节顺序。我一般会在协议格式说明里直接写明“气压字段为小端序”这样后续换平台不会猜。3.3 自定义通信协议的定义与发送流程工程对串口3发送的帧格式定义如下字节偏移字段长度说明0帧头11固定0xAA1帧头21固定0x552设备类型10x01表示大气压节点3数据长度1固定0x044~7气压值4float小端序单位kPa8校验值1前8字节累加和的低8位有了帧头就可以解决串口粘包、半包问题接收端用状态机找0xAA 0x55再按长度字段收取后续字节。校验值用累加和而不是CRC胜在代码短、执行快足够Zigbee透传场景使用。发送函数如下void Send_Pressure_Frame(float pressure) { uint8_t frame[9]; frame[0] 0xAA; frame[1] 0x55; frame[2] 0x01; frame[3] 4; memcpy(frame[4], pressure, 4); uint8_t checksum 0; for (int i 0; i 8; i) { checksum frame[i]; } frame[8] checksum; for (int i 0; i 9; i) { USART_SendData(USART3, frame[i]); while (USART_GetFlagStatus(USART3, USART_FLAG_TXE) RESET); } }这里需要注意memcpy的源地址是float变量地址目标地址是帧数组第4字节偏移正好跳过帧头、设备类型和长度字段。USART_FLAG_TXE表示发送数据寄存器为空等待它置位后才能写下一字节。如果是用中断方式发送要把这个循环放到USART3_IRQHandler里避免阻塞长时间占用CPU。OLED显示和发送的调度频率建议分开。BMP280在正常模式下测量一次大约几十毫秒OLED单次显示函数调用也需要几毫秒如果把读取、显示、发送都放在主循环里连续执行整个循环周期会不稳定。工程里通常用SysTick产生200ms和500ms两个标志200ms刷屏、500ms发一次帧。这样既能实时看到数据变化又不会把DL-20 Zigbee模块的发送队列撑满。4. 串口3与DL-20 Zigbee模块透明传输的参数匹配4.1 STM32F103RCT6串口3的初始化和引脚STM32F103RCT6的USART3挂在APB1总线上TX/RX默认引脚是PB10、PB11没有重映射时就是这个位置。标准库初始化需要同时打开GPIOB和USART3时钟PB10配置为复用推挽输出PB11配置为浮空输入波特率根据DL-20模块的默认配置设置。工程里通常配成9600bps、8N1和zigbee模块出厂默认一致。void USART3_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 9600; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Tx | USART_Mode_Rx; USART_Init(USART3, USART_InitStructure); USART_Cmd(USART3, ENABLE); }两个容易出错的点USART3时钟是APB1外设不能借USART1的RCC_APB2PeriphClockCmd写法TX引脚如果错配成通用推挽起始位和停止位可能翻转不干净表现为前几字节乱码。如果工程里还启用了串口接收要给USART3配中断或DMA否则主循环查询接收会丢字节。4.2 DL-20 Zigbee模块的工作机制DL-20是串口透传的Zigbee模块对STM32来说只看到一个串口设备。MCU把数据从TX发出去模块内部按照Zigbee协议栈打包通过无线发送给同一个网络内的另一块DL-20接收端再通过串口还原数据。应用层并不直接处理Zigbee帧只负责定义自己的数据协议因此上一章的自定义帧在链路上是透明传输的。模块的角色一般分成协调器、路由、终端三种。配合电脑使用的接收端通常是协调器STM32节点这边可以是终端或路由。组网时两块模块必须使用相同的PAN ID、相同的信道和相同的波特率。DL-20配置的方式一般是USB转TTL接模块的TXD、RXD、GND然后在串口助手里按模块手册发AT指令。常见流程是发送进入AT模式再设置PAN ID、信道、波特率保存并重启。不同固件版本的指令差异很大这里不抄具体命令以模块自带手册为准。我调试时会把两块模块都接到电脑上用两个串口助手分别打开先不接STM32直接通过USB转TTL往模块A发送一帧数据看模块B是否能收到。如果这步通了再把STM32的USART3接到模块A的RXD/TXD这样能快速定位是Zigbee组网问题还是MCU串口问题。注意模块A的RXD要接STM32的TX模块A的TXD要接STM32的RX两块模块的GND要连在一起否则信号参考地不一致。4.3 串口发送调试步骤开始整体联调时建议按下面的步骤推进步骤操作成功标准1STM32串口3直接接USB转串口不接DL-20串口助手能看到AA 55 01 04开头的帧2确认串口调试助手用HEX显示波特率严格匹配帧完整校验值符合累加和3STM32的TX/RX接DL-20模块A和模块B已组网第二台电脑上的模块B串口收到同样帧4检查模块B发送数据给STM32能否收到无要求时跳过单向透传只需前一步这里常见的问题是串口助手用ASCII模式看到乱码实际上数据没问题切换HEX模式立刻就能认出来。DL-20的CTS/RTS流控引脚在不同模块上处理不同如果模块上有RTS/CTS且默认使能必须按手册接好或通过AT指令关闭流控否则发送数据会卡在模块内部缓冲。另外DL-20模块在配置模式下不会透传数据调试时一定要确认模块已经退出AT模式。4.4 主循环里的采集-显示-发送节奏模块化工程里主循环一般写成时间片调度while (1) { if (flag_200ms) { flag_200ms 0; pressure BMP280_GetPressure(); OLED_ShowFloatNum(0, 0, pressure, 4, 2); } if (flag_500ms) { flag_500ms 0; Send_Pressure_Frame(pressure); } }这里flag_200ms和flag_500ms由SysTick中断或定时器中断置位。每次读取BMP280到显示结束期间不能有长时间关闭总线的操作。把发送周期放到500ms还有一层考虑DL-20模块在低功耗模式下会休眠间歇性发送能延长电池寿命但休眠后第一包数据可能需要一定时间唤醒接收端通常会丢掉前一两个字节。解决办法是在长周期发送前先发送一帧短数据作为唤醒包或者把DL-20配置成常发模式。工程是否需要这样做取决于现场对低功耗的要求。5. 验证链路与处理卡死OLED、BMP280和Zigbee的联合排查5.1 OLED加了函数就卡死怎么办常见现象是BMP280单独读正常加上OLED后程序跑飞。大部分原因在于模拟IIC的SDA引脚被某个设备持续拉低或两个设备的IIC地址冲突。可以先拔掉OLED单独读BMP280的ID寄存器再把OLED接上用示波器量SCL/SDA波形。如果SDA一直是低电平说明总线被占用。将OLED和BMP280分开配到两组GPIO各自初始化一套模拟IIC通常能解决。注意分开后GPIO时钟要分别使能不能只改引脚号否则引脚配置实际没有生效。5.2 用回环测试验证Zigbee链路为了确认数据是从STM32出去坏的还是Zigbee链路损伤可以在发送前把帧再通过USART1打印到调试串口与接收端收到的内容比对。比如printf(TX: ); for (int i 0; i 9; i) { printf(%02X , frame[i]); } printf(\r\n);如果接收端串口助手收到AA 55 01 04 ...而调试串口打印的是同一串说明数据原样被Zigbee传输了。如果前几个字节对后面数据错位先检查接收端的帧解析逻辑是不是按ASCII解析了HEX。Zigbee传输过程中出现单字节丢失的情况不多一旦出现基本是模块波特率不匹配或发送侧TXE等待超时。5.3 串口烧写失败先查模块是否占用TX接了DL-20模块后再用串口下载STM32程序经常出现连接失败。这不是代码逻辑问题而是DL-20的TXD/RXD与USB转串口工具同时接在USART3上几路信号互相打架。烧录前把DL-20的TXD、RXD从USART3上断开保留USB转串口下载线成功率立刻恢复。如果用的是CH340串口芯片安装驱动后仍提示连接失败检查BOOT0是否被拉高到3.3V进入系统存储器引导模式后再通过串口下载。OLED屏和BMP280挂在IIC引脚上不影响烧录但Zigbee模块挂在USART3引脚上会影响。本文还有配套的精品资源点击获取