资讯动态

CH32F103C8T6+BMP280+OLED:RISC-V环境监测方案

发布时间:2026/10/21 13:36:35 来源:尧图企业网站定制
简介CH32F103C8T6BMP2800.96OLED 是一套基于 RISC-V 内核 MCU 的传感器采集与显示工程面向嵌入式初学者、大学生及电子 DIY 爱好者解决在 CH32 平台上快速实现气压、海拔与芯片温度测量并屏显的需求。工程包含完整源码与 Keil 工程BMP280 通过 I2C 接 PB5/PB60.96 英寸 OLED 接 PB10/PB11代码中已配置好引脚和通信时序从 BMP280 寄存器配置、温度与气压补偿计算到 OLED 的字符和数值刷新均有对应实现同时演示了 I2C 读写流程与屏幕动态显示方式适合直接移植到同类工程。压缩包共 168 个文件以 C 源码、头文件、编译目标文件为主同时含有 Keil 工程配置、链接脚本、HEX 可烧录固件与 AXF 调试镜像等整体约 4.63MB目录结构清晰便于按模块阅读和二次开发。资源已服务 167 位学习者既可作为 RISC-V 单片机入门实验也可在此基础上扩展更多传感器或完善数据显示逻辑。1. 为什么用CH32F103C8T6跑BMP280和OLED一个RISC-V传感器显示方案怎么立住如果你打算交一份嵌入式课设或毕设最稳的做法通常是选STM32F103C8T6资料多、教程全、出问题一搜就有答案。但换成CH32F103C8T6就完全是另一套玩法了它基于RISC-V指令集架构引脚上和STM32F103C8T6兼容主频能到72MHz价格更低供应链也更自主。把它和BMP280大气压强传感器、0.96英寸OLED屏组合在一起刚好可以做一台桌面气象站或便携式环境监测节点。这套方案要解决的核心问题很明确用一块非ARM内核的MCU完整走通传感器采集、数据换算、屏幕显示这条链路。适合读这篇方案的人有两类。一类是做嵌入式课程设计或毕业设计的学生想找一个既能讲清楚原理、又不用堆复杂外设的题目另一类是工程师想快速验证RISC-V平台能不能替代手里原有的STM32方案把BMP280这类I2C传感器驱动迁移过来。下面从硬件选型、软件驱动、问题排查到答辩验收一步步把完整方案拆开。2. 硬件设计BMP280与0.96OLED如何接到CH32F103C8T6上2.1 器件核对BMP280的电气特性和I2C地址BMP280是一款数字气压传感器量程300到1100hPa典型绝对精度±1hPa在950到1050hPa这个区间表现最好正好覆盖日常大气压范围。它的供电电压范围是1.71V到3.6V所以必须接3.3V不能直接怼5V。通信接口支持I2C和SPI两种本方案用I2C因为只需要两根线而且OLED屏也是I2C接口可以共用同一根总线。BMP280的I2C地址由SDO引脚决定SDO接GND时地址是0x76SDO接VCC时地址是0x77。很多淘宝模块默认把SDO拉到GND也就是0x76但不同批次不一定一样。写驱动的时候最好做一个地址扫描或者直接用万用表量一下SDO的电平不要凭经验写死。0.96英寸OLED屏的驱动芯片是SSD1306分辨率128x64I2C地址固定为0x3C。它也有SPI版本但I2C版本只需要四根线VCC、GND、SCL、SDA接线最省事。显示屏工作电压同样是3.3V和BMP280共用一个电源轨逻辑电平完全匹配不需要电平转换。注意买OLED模块时确认驱动是SSD1306。市面上也有SH1106驱动的1.3寸屏初始化命令不完全相同不能直接套用。2.2 I2C总线设计上拉电阻和硬件接线表I2C总线是开漏结构SCL和SDA两根线必须通过上拉电阻拉到VCC阻值一般在4.7kΩ到10kΩ之间。大多数BMP280模块和OLED模块板载了上拉电阻但有些极简模块没有。如果通信不稳定不要急着怀疑代码先确认这两根线上有没有上拉。CH32F103C8T6的PB6和PB7对应I2C1的SCL和SDA但本方案用软件模拟I2C所以这两个引脚只是普通GPIO配置成开漏输出即可。整个系统的接线如下表。外设引脚CH32F103C8T6引脚说明BMP280 VCC3.3V模块供电BMP280 GNDGND共地BMP280 SCLPB6软件I2C时钟线BMP280 SDAPB7软件I2C数据线OLED VCC3.3V屏供电OLED GNDGND共地OLED SCLPB6与BMP280共用OLED SDAPB7与BMP280共用如果OLED模块有RST复位引脚可以接到CH32F103C8T6的PC13上初始化时拉低再拉高做一次硬件复位这样能避免上电时序不对导致的花屏。没有RST引脚的版本也可以工作软件初始化命令里已经包含了软件复位。供电方面BMP280正常工作时电流约2.7μA1Hz采样率OLED全亮时也就20mA左右两者加起来对USB供电或3.3V LDO毫无压力。不过要注意如果OLED用的是劣质升压模块上电瞬间可能会拉低3.3V电压导致BMP280读数异常。我的习惯是在电源入口放一个100μF电解电容和0.1μF瓷片电容并联。2.3 硬件检查清单上电前先做这三步拿到板子不要急着写代码先花两分钟做硬件自检。第一步用万用表量3.3V和GND之间是否有短路特别是OLED模块的电源端第二步确认SCL和SDA没有接反这一步是翻车重灾区两根线颜色一样插杜邦线时特别容易交叉第三步用万用表量SCL对地的电阻如果阻值在4.7kΩ到10kΩ之间说明上拉存在如果显示无穷大说明模块没有板载上拉需要自己补焊两个电阻。提示CH32F103C8T6有多个封装C8T6是LQFP48引脚间距小手工焊接时容易连锡。如果用的是核心板确认板子上的丝印和实物对应PB6、PB7在核心板排针上都有引出。3. 软件架构从软件I2C到BMP280补偿和OLED显示3.1 开发环境与工程结构官方Eclipse内核IDE和代码布局CH32F103C8T6基于RISC-V内核不能用Keil MDK直接编译需要安装沁恒官方提供的Eclipse内核IDEMounRiver Studio。这个IDE内置了riscv-none-embed-gcc工具链、调试器配置和标准外设库工程模板新建工程时选择CH32F103C8型号会生成一个包含system_ch32f10x.c、gpio、i2c、usart等外设驱动库的骨架。工程文件结构按模块拆分我的习惯是每个外设一个源文件加一个头文件project/ ├── core/ │ ├── main.c │ └── system_ch32f10x.c ├── driver/ │ ├── i2c_soft.c / i2c_soft.h │ ├── bmp280.c / bmp280.h │ └── oled.c / oled.h └── user/ └── delay.c / delay.hmain.c只做三件事初始化硬件外设、进入主循环、处理延时。具体每个传感器的逻辑全部封装在driver目录里这样答辩时讲代码结构也比较清晰。标准外设库的函数命名风格和STM32的SPL库很像如果你是从STM32转过来的看GPIO_InitTypeDef、USART_SendData这些函数不会太陌生但底层寄存器映射和中断控制器是RISC-V私有实现的不能直接把ARM的HAL代码搬过来。3.2 软件I2C驱动为什么不用硬件I2C以及时序实现CH32F103C8T6内部有硬件I2C外设但嵌入式社区都有个血泪经验这类芯片的硬件I2C状态机复杂总线忙标志位容易卡死排查起来像黑匣子。本方案只有两个从设备通信速率100kHz足够所以用软件模拟I2C代码完全透明出问题可以直接用逻辑分析仪逐bit查。软件I2C的核心是四个函数起始、停止、写字节、读字节。GPIO配置成开漏输出还需要允许输入这样在释放SDA时能读到从设备的ACK应答。基础时序代码如下。#define I2C_SCL_PIN GPIO_Pin_6 // PB6 #define I2C_SDA_PIN GPIO_Pin_7 // PB7 static void I2C_Delay(void) { // 72MHz主频下大约延时1us满足100kHz时序要求 for (volatile int i 0; i 40; i); } static void I2C_Start(void) { GPIO_SetBits(GPIOB, I2C_SDA_PIN); // SDA高 GPIO_SetBits(GPIOB, I2C_SCL_PIN); // SCL高 I2C_Delay(); GPIO_ResetBits(GPIOB, I2C_SDA_PIN); // SDA在SCL高电平期间拉低产生起始信号 I2C_Delay(); GPIO_ResetBits(GPIOB, I2C_SCL_PIN); // 拉低SCL准备传数据 } static uint8_t I2C_WriteByte(uint8_t data) { uint8_t ack; for (int i 0; i 8; i) { if (data 0x80) GPIO_SetBits(GPIOB, I2C_SDA_PIN); else GPIO_ResetBits(GPIOB, I2C_SDA_PIN); data 1; I2C_Delay(); GPIO_SetBits(GPIOB, I2C_SCL_PIN); // SCL高电平期间数据必须稳定 I2C_Delay(); GPIO_ResetBits(GPIOB, I2C_SCL_PIN); } GPIO_SetBits(GPIOB, I2C_SDA_PIN); // 释放SDA接收从机ACK I2C_Delay(); GPIO_SetBits(GPIOB, I2C_SCL_PIN); ack GPIO_ReadInputDataBit(GPIOB, I2C_SDA_PIN); // 低电平表示从机应答 GPIO_ResetBits(GPIOB, I2C_SCL_PIN); return ack; }代码里最关键的是起始信号必须在SCL高电平时拉低SDA结束信号相反在SCL高电平时拉高SDA。如果时序反了从设备会一直不响应。延时循环的具体次数不是精确的微秒级只要保证高低电平持续时间大于数据手册要求的最小值即可。实际跑的时候可以在调试器里量一下波形周期通常500ms刷一次屏幕I2C占用率很低。3.3 BMP280驱动寄存器配置、校准参数和补偿公式BMP280内部有一组OTP校准系数出厂时写入上电后要通过I2C读出来温度补偿和气压补偿计算都要用到这些系数。初始化顺序有讲究先读ID寄存器确认设备存在再连续读取校准系数最后配置测量控制寄存器。#define BMP280_ADDR 0x76 // 取决于SDO引脚电平 #define BMP280_REG_ID 0xD0 #define BMP280_REG_CTRL 0xF4 #define BMP280_REG_CONFIG 0xF5 typedef struct { uint16_t dig_T1; int16_t dig_T2, dig_T3; uint16_t dig_P1; int16_t dig_P2, dig_P3, dig_P4, dig_P5; int16_t dig_P6, dig_P7, dig_P8, dig_P9; int32_t t_fine; } BMP280_Calib; static BMP280_Calib calib; uint8_t BMP280_Init(void) { uint8_t id ReadRegByte(BMP280_ADDR, BMP280_REG_ID); if (id ! 0x58) { return 1; // ID不对检查接线或I2C地址 } // 从0x88开始连续读24字节校准系数 ReadRegs(BMP280_ADDR, 0x88, (uint8_t *)calib, 24); // ctrl_meas: 温度采样x1, 气压采样x1, normal模式 WriteRegByte(BMP280_ADDR, BMP280_REG_CTRL, 0x27); // config: standby 125ms, IIR滤波x4, SPI关闭 WriteRegByte(BMP280_ADDR, BMP280_REG_CONFIG, 0xA0); return 0; }读取原始数据的寄存器地址从0xF7开始连续6个字节顺序是气压MSB、气压LSB、气压XLSB、温度MSB、温度LSB、温度XLSB。因为BMP280是20位ADC必须把三个字节拼成32位整数再右移4位这步漏了的话数据会差16倍肉眼直接能看到气压值变成几万。温度补偿公式是整套驱动里最容易抄错的部分原厂手册的浮点版本已经比较精简可以直接实现。double BMP280_CompensateT(int32_t adc_T) { double var1, var2, T; adc_T 4; // 温度原始值20位有效 var1 (((double)adc_T) / 16384.0 - ((double)calib.dig_T1) / 1024.0) * ((double)calib.dig_T2); var2 ((((double)adc_T) / 131072.0 - ((double)calib.dig_T1) / 8192.0) * (((double)adc_T) / 131072.0 - ((double)calib.dig_T1) / 8192.0)) * ((double)calib.dig_T3); calib.t_fine (int32_t)(var1 var2); return (var1 var2) / 5120.0; }温度结果算出来后t_fine中间变量必须保存下来气压补偿公式里还要再引用它。气压补偿的计算分三段最终结果单位是Pa除以100就得到hPa。如果只显示大气压强注意单位换算日常气象数据习惯用hPa很多同学把BMP280输出的Pa直接当hPa显示结果气压变成十多万这是低级错误。3.4 OLED显示驱动SSD1306的初始化和显存刷新SSD1306的初始化靠一串命令完成核心步骤包括关闭显示、设置时钟分频、关闭电荷泵、设置内存寻址模式、打开显示。电荷泵必须打开否则屏幕只有微弱残影亮度极低。static const uint8_t OLED_INIT_CMDS[] { 0xAE, // 关闭显示 0xD5, 0x80, // 显示时钟分频 0xA8, 0x3F, // 多路复用比率64行 0xD3, 0x00, // 显示偏移为0 0x40, // 起始行0 0x8D, 0x14, // 开启电荷泵关键 0x20, 0x00, // 水平内存寻址模式 0xA1, // 段重映射 0xC8, // COM扫描方向 0xDA, 0x12, // COM引脚配置 0x81, 0xCF, // 对比度 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH选择 0xA4, // 全局显示开启 0xA6, // 正常显示非反色 0xAF // 开启显示 }; void OLED_Clear(void) { // 设置窗口为整屏列地址0-127页地址0-7 OLED_WriteCmd(0x21); OLED_WriteCmd(0x00); OLED_WriteCmd(0x7F); OLED_WriteCmd(0x22); OLED_WriteCmd(0x00); OLED_WriteCmd(0x07); // 连续写1024字节0x00SSD1306每个字节8个像素 for (int i 0; i 1024; i) { OLED_WriteData(0x00); } }SSD1306内部显存是128x64位按页组织每页8行总共8页。地址设置后写入的数据会自动填充显存不需要对每个像素单独操作。显示字符串时要先把ASCII码转换成对应的点阵数据。最省事的做法是定义一个8x16的ASCII字库数组每个字符16字节分别对应上半行8x8和下半行8x8。3.5 主循环采集、换算、显示一条龙主循环的框架很简单关键在于刷新率和延时控制。BMP280在normal模式下连续测量数据寄存器自动更新不需要每次启动转换这比单次模式省心很多。int main(void) { SystemInit(); GPIO_Config(); Delay_Init(); SoftI2C_Init(); BMP280_Init(); OLED_Init(); OLED_Clear(); while (1) { int32_t adc_T, adc_P; double temp, pressure, altitude; BMP280_ReadRaw(adc_P, adc_T); temp BMP280_CompensateT(adc_T); pressure BMP280_CompensateP(adc_P) / 100.0; // 转为hPa altitude 44330.0 * (1.0 - pow(pressure / 1013.25, 1.0 / 5.255)); OLED_ShowString(0, 0, Temp:); OLED_ShowNum(0, 1, temp, 1); OLED_ShowString(0, 8, Pres:); OLED_ShowNum(0, 9, pressure, 2); OLED_ShowString(0, 4, Alt:); OLED_ShowNum(0, 5, altitude, 1); Delay_Ms(500); } }显示位置计算有个小坑OLED的页地址是纵向的页0在屏幕顶部每页高度8像素。所以页0显示第一行字符页2显示第二行以此类推。很多初学者把行号当成页号传入结果四行字重叠在一起。如果显示的内容只有温度和气压可以只刷两页剩下的页保持空白这能缩短刷新时间。提示主循环里不要加太长的延时比如延时5秒否则操作按键或后续扩展串口时会有明显的卡顿感。500ms刷新间隔已经是人眼能感知的友好节奏。4. 常见问题排查I2C黑匣子、数据跳变和花屏4.1 现象I2C总线不通OLED黑屏、BMP280读不到ID这是最容易翻车的点而且症状掩盖了真正的原因。我见过有人卡在这个问题上两天换传感器模块、换核心板都没用最后发现是杜邦线插反了。原因有几个层次。第一是接线错误SDA和SCL交叉或者模块VCC和GND接反。第二是上拉电阻缺失总线处于悬浮状态通信时序完全不对。第三是I2C地址不对SDO引脚电平和代码里的地址不匹配。解决路径按顺序执行先量供电模块VCC对地电压必须在2.7V到3.6V之间然后量上拉SCL和SDA对地的电阻应该是几kΩ再确认PB6和PB7的GPIO配置是开漏输出且使能了输入模式最后在代码里加一个I2C地址扫描函数把0x3C和0x76、0x77都试一遍。如果还是不行用逻辑分析仪抓SCL和SDA波形看起始条件是否存在、ACK位是否被拉低。通常走到这一步问题已经定位了。4.2 现象BMP280气压数据在静止环境中上下乱跳传感器静止放在桌面气压值应该在±0.5hPa内稳定波动。如果看到数值上下跳了2到3hPa先怀疑自发热和滤波配置。自发热的原因是BMP280内部有一个热敏电阻长时间通电后芯片温度会比环境温度高两到三摄氏度气温每升高1摄氏度气压读数会有约0.3hPa的误差。解决方法是不要在传感器旁边放发热元件比如不要让MCU的LDO贴着BMP280或者把测量间隔加大到1秒让传感器有时间散热。另一个原因是寄存器配置中IIR滤波被关闭了。我在初始化代码里把config寄存器设为0xA0表示IIR滤波系数为4这本质上是一个低通滤波器能明显抑制数据抖动。如果对比度测试发现响应太迟钝可以把滤波系数改成2或1代价是短期波动变大。也可以直接在软件里对连续五次气压值取平均效果等效。4.3 现象OLED闪烁、花屏、对比度极低闪烁的根源是刷新方式不对。如果每次刷新都先清屏再写入新内容屏幕会在清屏瞬间闪一下。SSD1306支持按页写入正确的做法是保持全屏内容在显存里只更新变化的区域或者直接向窗口地址一次性写入完整帧数据不要先填0xFF再填数据。花屏则多是因为初始化序列不完整。特别是0x8D, 0x14这条开启电荷泵的命令很容易被遗漏。没有电荷泵屏幕功耗不足只能看到非常淡的残影这是SSD1306最经典的踩坑点。对比度低还可能是0x81对比度寄存器被设成了很小的值比如0x00调回0xCF即可。数据总线的ACK错误也可能导致花屏。OLED写入速度比BMP280慢如果I2C时钟频率太高或者延时函数过快OLED来不及接收数据就会丢字节。软件I2C的延时循环不要一味往小调100kHz已经够用追求速度没有意义。4.4 RISC-V工程调试寄存器不匹配和调试连接问题从ARM平台转过来的人最容易犯的错是把STM32的源代码直接拖进CH32工程。虽然标准外设库的函数命名相似但寄存器的位定义和中断控制器是两套体系RISC-V内核使用自定义的快速中断入口外部中断标志位也不一样。正确的做法是用官方库的例程作为起点只移植自己需要的部分。调试连接方面CH32F103C8T6使用RISC-V调试协议常见的ARM仿真器用不了需要官方调试器WCH-Link。连接后IDE里要选择RISC-V调试配置如果出现“cannot connect to target”报错先检查SWDIO、SWCLK两根线是否接对再确认板子供电正常。下载程序时如果提示Flash校验失败多半是芯片的读保护被打开用调试器执行解除读保护命令即可。5. 从显示到落地高度换算、串口输出和应用扩展5.1 气压换算海拔高度公式和标定方法气压和海拔不是线性关系标准大气模型下可以用简化公式计算altitude 44330.0 * (1.0 - pow(pressure / 1013.25, 1.0 / 5.255));这个公式的前提是基准气压等于海平面标准大气压1013.25hPa但实际上当地气象台发布的气压都是经过海拔修正的海平面气压。如果设备放在海拔50米的地方直接用当地实测气压代入计算出的“海拔”会偏大。更合理的做法是先用已知海拔的地点标定比如学校教学楼标高是20米把设备带到楼顶读取当前气压值反推基准气压把这作为公式里的1013.25替换值。高度精度方面BMP280的气压分辨率理论上可以做到0.18Pa对应约1.5厘米的高度变化但实际受温度漂移和气流影响稳定后的垂直分辨率大约在±1米左右。这意味着用这个方案做无人机定高不够可靠但做电梯楼层检测、爬楼高度计完全够用。5.2 串口输出printf重定向到USART1OLED屏幕空间有限不适合输出详细的调试日志。把温压数据通过串口发出去既能在上位机实时观察又能给后续的数据记录模块打基础。CH32F103C8T6的USART1是常用调试串口引脚是PA9TX和PA10RX配置成异步模式波特率1152008N1。printf重定向需要重写fputc函数把标准输出指向USART1int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }主循环里格式化输出一行数据用逗号分隔方便上位机直接按CSV解析printf(%lu,%.2f,%.2f,%.1f\r\n, (unsigned long)millis, temp, pressure, altitude);串口输出的意义在于答辩现场可以把数据接到电脑上用任意串口助手软件实时显示曲线这是OLED做不到的。如果时间充裕还可以在PC端用Python的pyserial库画气压变化曲线视觉效果比屏幕数字直观得多。5.3 完整应用场景桌面气象站或小型环境监测节点这套硬件组合天然适合做成桌面气象站。OLED固定显示当前温度和气压串口把数据送给电脑做记录BMP280负责感知环境变化。比如放在窗边天气系统过境时气压会快速下降这个现象在气压曲线上非常明显可以作为区分晴雨的有效参考。如果需要测量连续24小时的气压变化可以选择记录在一张SD卡上或通过串口让电脑端软件落盘。BMP280在正常模式下功耗极低用USB供电完全没问题如果换成电池供电可以把测量模式改成强制模式每10秒唤醒一次采样其余时间进入休眠整体平均功耗可以压到几十微安级别。硬件上还可以扩展一个DHT11或AHT20温湿度传感器和BMP280的温度互相校验。不过要注意DHT11的精度只有±2℃和BMP280自带的温度传感器数据会有差异不要试图把它们做交叉校准用途不同当作两路独立数据即可。6. 验收与答辩怎样证明这套方案真的能用6.1 验收标准每一项都有明确的测试方法方案拿出去之前我习惯列一张验收表每项都要实测打钩不凭感觉说“应该没问题”。以下是这个项目的验收清单。验收项测试方法通过标准上电显示USB供电观察OLED2秒内显示温压数据无花屏温度精度与标准温度计同环境放置10分钟偏差≤±1℃气压精度与当地气象站或手机天气App数据对比偏差≤±2hPa数据稳定性静置1小时记录每分钟数据气压峰峰值≤±1hPaI2C可靠性连续运行12小时不出现死机或无应答加压响应用密封袋包裹传感器并轻轻按压气压读数实时上升松开后回落加压响应测试是现场演示的最佳环节。用手隔着密封袋按压BMP280感受到的压强变化立刻反映在OLED上评委能直观看到传感器在工作。温度测试要提前准备让设备和温度计在同一环境下放置足够时间避免用手摸传感器导致局部温度偏差。6.2 答辩讲解的逻辑从选型到实现的三层思路答辩讲得好不好不在于代码背得熟不熟而在于能不能讲清楚“为什么这么做”。评委最常问的三类问题我在准备时都会列好答案。第一类是选型问题为什么不用STM32而用CH32F103C8T6答案要从三个维度组织成本更低、RISC-V指令集架构是发展方向、软件生态正在成熟。第二类是方案取舍问题为什么用软件I2C而不用硬件I2C回答时强调软件I2C的可调试性和代码透明性承认熟悉硬件I2C的工程师可以尝试后者但教学场景下可追溯性更重要。第三类是原理问题BMP280为什么需要补偿计算这里可以讲清楚校准系数来自OTP存储补偿公式是消除温度对气压传感器零点漂移的影响t_fine中间变量让温度和气压修正保持一致。演示环节有个小技巧不要只展示OLED上静止的数字提前在电脑上打开串口工具把气压数据画成曲线。让评委轻轻按压密封袋看着曲线同步上升这套演示比口头描述有说服力得多。那次做这个方案我在I2C上耗的时间比写BMP280补偿公式还多最后发现只是模块板载上拉电阻虚焊。从那以后我每次做传感器项目都会先把I2C地址、上拉电阻、供电电压这三样写在硬件检查表里再开始写驱动。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑