简介:这是一份基于沁恒CH32F103C8T6(RISC-V内核)的BMP280大气压强/温度采集与0.96英寸OLED显示完整工程,适合嵌入式入门开发者参考I2C总线配置、传感器读取和屏幕驱动。工程将CH32作为主控,BMP280采集海拔、气压与芯片温度,OLED实时显示;硬件接线已给出SCL/SDA对应引脚,便于按图搭建。包内共168个文件,以C源码、H头文件、编译生成的o/axf/hex及Keil工程文件为主,另含少量链接脚本与调试配置,可直接打开工程查看代码流程并烧录验证,压缩包约4.63MB。已有167人学习下载,适合想快速上手RISC-V MCU与常用传感器组合开发的读者,从引脚定义到外设驱动均有清晰的工程结构可循。
1. 基于 CH32F103C8T6 的大气压强监测终端:RISC-V 主控、BMP280 数据采集与 OLED 上屏
做气象采集类课设或毕设时,最常见的选择是 STM32F103C8T6 加 BMP280 加 0.96 寸 OLED。但这套方案反过来:主控换成同封装的国产 RISC-V 芯片 CH32F103C8T6,引脚兼容、外设布局几乎一致,价格更低,能讲的内容却多了一层“新架构落地”的加分项。方案要解决的问题很具体——用 CH32F103C8T6 通过 I2C 读取 BMP280 的大气压强和温度,经过校准补偿与滤波后,在 OLED 上实时显示,并支持简单的海拔高度换算。适合正在做嵌入式课设、毕业设计的学生,也适合想快速验证 RISC-V 内核 MCU 开发流程的工程师:一套硬件,两条产出线。
2. 硬件方案设计:主控选型、引脚分配与整体接线
2.1 为什么选 CH32F103C8T6 而不是 STM32F103C8T6
CH32F103C8T6 最吸引人的一点是它与 STM32F103C8T6 的引脚兼容关系。同样是 LQFP48 封装,同样是 64KB Flash、20KB SRAM,72MHz 主频,连 GPIO、USART、I2C、SPI、ADC 这些外设的寄存器映射都做了对齐。这意味着什么?意味着如果之前有 STM32F103 的基础,迁移到 RISC-V 平台时,硬件原理图基本不用改,软件上也能复用大部分标准外设库的 API 风格。
从项目选题角度看,选 CH32F103C8T6 比选 STM32F103C8T6 更有“设计感”。它用的是 RISC-V 内核(RV32IMAC 指令集),这正好踩在近几年嵌入式课程和招聘关注的热点上。答辩的时候,你能讲清楚“RISC-V 与 ARM Cortex-M 在指令集、工具链、生态上的差异”,比单纯说“我用 STM32 做了个传感器采集”要有说服力得多。
| 对比项 | CH32F103C8T6 | STM32F103C8T6 |
|---|---|---|
| 内核 | RISC-V RV32IMAC | ARM Cortex-M3 |
| 主频 | 72MHz | 72MHz |
| Flash / SRAM | 64KB / 20KB | 64KB / 20KB |
| 调试接口 | SWD | SWD |
| 引脚封装 | LQFP48 | LQFP48 |
| 成本 | 通常更低 | 较高 |
做成表格方便答辩时直接展示。注意一个细节:CH32F103C8T6 虽然引脚兼容,但芯片内部毕竟不是 ARM 核,不能直接把 STM32 的 HAL 库工程搬过来编译。后面第四章会专门讲这个坑。
2.2 硬件连接:BMP280 与 0.96 OLED 的引脚分配
整个系统的硬件连线不复杂,核心是一条 I2C 总线挂两个设备:BMP280 气压传感器和 SSD1306 驱动的 OLED 显示屏。CH32F103C8T6 的 I2C1 外设默认映射在 PB6(SCL)和 PB7(SDA),我把两个设备都挂在这条总线上。
| 设备 | 引脚 | 连接目标 | 说明 |
|---|---|---|---|
| CH32F103C8T6 | PB6 | BMP280 SCL、OLED SCL | I2C 时钟线 |
| CH32F103C8T6 | PB7 | BMP280 SDA、OLED SDA | I2C 数据线 |
| BMP280 | SDO | GND | 拉低后 I2C 地址为 0x76 |
| BMP280 | CSB | VCC(3.3V) | 拉高,选择 I2C 模式而非 SPI |
| OLED | SA0 | GND | 拉低后 I2C 地址为 0x3C |
| 公共 | VCC | 3.3V | 两个模块均用 3.3V 供电 |
| 公共 | GND | GND | 共地 |
BMP280 的 SDO 和 OLED 的 SA0 这两个引脚很容易被忽略。SDO 拉低是 0x76,拉高是 0x77;SA0 拉低是 0x3C,拉高是 0x3D。如果模块出厂默认状态和你代码里写的地址不一致,I2C 通信就会失败。稳妥的做法是上电后先写一个 I2C 地址扫描函数,把总线上所有能响应的地址打印出来,确认 0x76 和 0x3C 都出现了再继续往下写业务代码。
VCC 供电这里单独说一句:BMP280 工作电压是 1.71V 到 3.6V,SSD1306 模块虽然很多板载了稳压和电平转换,但为了保险,整个系统统一用 3.3V 供电。不要图省事直接怼 5V,一旦模块上没有稳压芯片,传感器直接烧掉。开发板一般自带 3.3V LDO,直接从 3.3V 引脚取电即可。
2.3 电源、上拉电阻与布局的实践经验
I2C 总线需要上拉电阻,这是协议本身的要求。很多模块板上已经贴了 4.7k 或 10k 的上拉电阻,如果模块自带,就不用在主控板上重复加;如果用的是裸传感器和裸屏,建议在 SCL 和 SDA 上各加一个 4.7k 电阻到 3.3V。上拉电阻太小(比如 1k)会让总线拉低困难,信号边沿变差;太大会让上升沿变缓,高速通信时出错。
电源布局上有一个容易翻车的点:BMP280 对温度很敏感,而 CMOS 传感器、LDO 稳压芯片、甚至 OLED 背光都会发热。如果把传感器紧贴着稳压芯片或主控芯片放,气压读数受温度影响会产生明显漂移。我一般会把 BMP280 放在板子边缘,离发热源至少 1 到 2 厘米,必要时在传感器下面铺一块孤立的地铜皮。
连线走线方面,I2C 是低速总线,短距离内对线序和等长要求不高,但杜邦线接触不良是最隐蔽的问题。用万用表量每根线两端是通的,插上板子却读不到数据,这种情况我遇到不止一次。后来学乖了:所有 I2C 模块的接线都按压线端子或者直接焊接,尽量少用杜邦线,排查问题的成本会低很多。
3. 软件实现:BMP280 数据读取、滤波处理与 OLED 上屏
3.1 开发环境与软件框架:官方 IDE、标准外设库与模拟 I2C
软件部分的第一步是搭工程。CH32F103C8T6 的开发环境通常有两条路线:一条是用芯片厂商提供的 IDE 和标准外设库,底层寄存器操作清晰,适合毕设展示代码细节;另一条是 Arduino 框架,适合快速出原型,但 RISC-V 相关的移植层会屏蔽掉很多底层细节。这里以标准外设库为主线,因为它能直接把 GPIO、I2C、延时这些寄存器操作写到代码里,答辩时可以逐行解释。
I2C 通信实现上,我推荐用 GPIO 模拟而不是芯片硬件 I2C。原因很实际:硬件 I2C 外设的状态机一旦出错,排查问题比较费劲;而 GPIO 模拟 I2C 时序完全由自己控制,出问题可以用示波器或逻辑分析仪直接对比时序图。CH32F103C8T6 的 I2C 外设本身没有大问题,但模拟 I2C 对搞课设和毕设的人来说,心理负担小得多,代码量也就多几十行。
### 3.2 BMP280 读取:ID 校验、校准系数、寄存器配置与温度气压计算BMP280 上电后默认处于 sleep 模式,必须先配置测量控制寄存器才会开始转换,这是很多人第一次读数据失败的根本原因。整体读取流程按下面几步走。
先通过 I2C 读取芯片 ID 寄存器 0xD0,正常值应该是 0x58。如果不匹配,不要继续往下读,先检查接线和 I2C 地址;这一步能拦下一大半硬件问题。
再向控制寄存器 0xF4 写入配置。常用值是 0x27,二进制是 001_001_11,含义是温度过采样 ×1、气压过采样 ×1、normal 模式。如果希望功耗更低,可以写 0x25,进入 forced 模式,每次读数据前触发一次单次转换。
接着读取校准系数。BMP280 在生产时每颗芯片的校准参数都不同,存放在 0x88 开始的 24 个字节里,包含无符号的 dig_T1、dig_P1,以及有符号的 dig_T2、dig_T3、dig_P2 到 dig_P9。校准数据要一次性连续读完,然后按小端方式组合成 16 位整数。
typedef struct { uint16_t dig_T1; // 无符号 16 位 int16_t dig_T2; // 有符号 16 位 int16_t 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; } bmp280_calib_t; void BMP280_ReadCalib(bmp280_calib_t *c) { uint8_t buf[24]; BMP280_ReadRegs(0x88, buf, 24); // 连续读 0x88 起 24 字节 c->dig_T1 = buf[0] | (buf[1] << 8); c->dig_T2 = (int16_t)(buf[2] | (buf[3] << 8)); c->dig_T3 = (int16_t)(buf[4] | (buf[5] << 8)); c->dig_P1 = buf[6] | (buf[7] << 8); // P2~P9 同理,注意转成 int16_t }温度数据寄存器是 0xFA、0xFB、0xFC,气压数据寄存器是 0xF7、0xF8、0xF9。每个参数都是 20 位有效数据,横跨三个字节。读取后要右移 4 位得到真正的 20 位 ADC 值。温度补偿公式按数据手册给出的方式计算,先把原始 ADC 值代入,得到实际温度。
float BMP280_CompensateTemp(int32_t adc_T, bmp280_calib_t *c) { float var1, var2, T; var1 = ((float)adc_T / 16384.0f - (float)c->dig_T1 / 1024.0f) * (float)c->dig_T2; var2 = (((float)adc_T / 131072.0f - (float)c->dig_T1 / 8192.0f) * ((float)adc_T / 131072.0f - (float)c->dig_T1 / 8192.0f)) * (float)c->dig_T3; T = (var1 + var2) / 5120.0f; return T; }气压补偿公式更长,分 var1、var2、var3 三个中间变量,再代入 dig_P5 到 dig_P9,最后得到 hPa 单位的气压值。我习惯先把校准系数全部读出来打印一遍,确认每个系数的正负号和小端组合没有搞反,再跑完整公式。原始 ADC 值、补偿温度和最终气压这三个量,调试阶段全部通过串口打印出来,看数据是否线性、量级是否正确。
3.3 SSD1306 驱动的初始化序列与显存写入
0.96 寸 OLED 的驱动芯片是 SSD1306,128×64 分辨率,I2C 地址是 0x3C。I2C 传输时有一个细节:控制字节 0x00 表示后续是命令,0x40 表示后续是显存数据。很多教程把初始化序列封装成一个数组,这里也这么做,关键是几个寄存器不能漏。
static const uint8_t ssd1306_init_cmds[] = { 0xAE, /* 关闭显示 */ 0xD5, 0x80, /* 时钟分频 */ 0xA8, 0x3F, /* 复用率 1/64 */ 0xD3, 0x00, /* 显示偏移 */ 0x40, /* 起始行 */ 0x8D, 0x14, /* 开启内部电荷泵,必写 */ 0x20, 0x00, /* 水平寻址模式 */ 0xA1, /* 列地址重映射 */ 0xC8, /* COM 扫描方向 */ 0xDA, 0x12, /* COM 引脚配置 */ 0x81, 0xCF, /* 对比度 */ 0xD9, 0xF1, /* 预充电周期 */ 0xDB, 0x40, /* VCOMH 电平 */ 0xA4, /* 恢复显示 */ 0xA6, /* 正常显示,非反显 */ 0xAF /* 打开显示 */ };这里最容易翻车的两个命令是 0x8D 0x14(电荷泵)和 0xAF(开显示)。电荷泵不开,屏永远黑着,但 I2C 通信是正常的,很多人在这里耗尽一下午。初始化完成后,SSD1306 内部有 1KB 显存,对应 8 页 × 128 字节,每页 8 个像素点。写入数据前要先设置页地址和列地址,然后连续写入显示数据。
显示字符串需要字库。0.96 寸 OLED 上常用 6×8 或 8×16 的 ASCII 字库,用取模软件生成字节数组。显存是纵向排列的,取模时选择“纵向取模、字节倒序”,否则显示出来的字符是反的。这一条属于不做不知道、做了忘不掉的经验。
3.4 数据融合:滑动平均滤波、高度换算与显示刷新策略
BMP280 的原始气压数据即使经过补偿,也会有小幅跳动,尤其是开了空调或者有人走动的房间里。解决方法是加一个滑动平均滤波,保留最近 10 次采样值,每次取平均,代码量很小,效果明显。
#define FILTER_N 10 float filter_buf[FILTER_N]; uint8_t filter_idx = 0; float Pressure_Filter(float new_val) { float sum = 0.0f; filter_buf[filter_idx] = new_val; filter_idx = (filter_idx + 1) % FILTER_N; for (uint8_t i = 0; i < FILTER_N; i++) { sum += filter_buf[i]; } return sum / FILTER_N; }滤波窗口大小不是越大越好。取 10 次能明显压住随机抖动,取 50 次以上会掩盖气压的快速变化,比如拿着传感器上下电梯时,气压变化趋势会变得迟钝。这个项目里 10 次是比较合适的平衡点。
显示内容的组织上,我分了四行:第一行显示标题,第二行显示大气压,单位 hPa,第三行显示温度,第四行显示根据气压推算的海拔高度。高度换算用标准的压高公式:h = (1 - (p / 101325) 的 5.255 次方根) × 44330,其中 p 是实测气压。这个公式在低海拔区域误差不大,能看出高度变化趋势。
刷新策略要克制。I2C 写入 OLED 显存 1KB 数据在 400kHz 下耗时很短,但没必要高频刷新。我设置的刷新周期是 1 秒,显示稳定不闪烁,人眼看起来也舒服。如果刷新太快,数字跳动会很明显,反而显得数据不可靠。
4. 常见问题排查:数据异常、花屏与编译踩坑实录
4.1 气压读出来全是 0 或者 0xFF
现象是串口打印的气压和温度值要么全 0,要么全 255,偶尔中间夹几个乱码。这个坑我帮别人排查过很多次,原因基本集中在这几个方向,按概率排序。
原因是第一,BMP280 还停留在 sleep 模式,没写 0xF4 配置寄存器。第二,I2C 地址写错了,设备根本没应答。第三,接线有断路或接触不良,尤其杜邦线松动。第四,芯片是坏的或者模块虚焊。
解决的步骤很有规律:先用万用表量 3.3V 和 GND 是否正常,再读 0xD0 寄存器确认 ID 是否为 0x58,然后检查 0xF4 是否成功写入,最后用逻辑分析仪抓 SDA 和 SCL 波形。ID 校验能通过,说明底层 I2C 通信没问题,剩下的就是配置寄存器的事。
4.2 I2C 地址记混:BMP280 的 0x76 与 OLED 的 0x3C
有人会觉得两个器件挂在同一条 I2C 总线上会“地址冲突”,其实 0x76 和 0x3C 差得很远,完全不冲突。真正的现象是两个设备只有一个能正常工作,另一个读写无响应。
原因是 BMP280 的 SDO 引脚被拉高变成了 0x77,或者 OLED 的 SA0 被拉高变成了 0x3D,代码里写的地址和硬件实际地址不匹配。还有一种情况是模块上丝印标注的地址和实际不符,这是一个比较隐蔽的坑。
解决的办法是写一个 I2C 地址扫描函数,把 0x00 到 0x7F 所有地址都发一遍读请求,能收到 ACK 的地址打印出来。这个方法对任何 I2C 项目都通用,第一次调试就能把总线上挂了几个设备、各自什么地址摸清楚。
4.3 OLED 花屏、闪烁与残影
现象是初始化后屏幕显示乱码、部分区域闪烁,或者切换页面时有残影。
花屏的第一个原因是初始化序列里电荷泵命令 0x8D 0x14 没写或写错,屏幕供电不足,显示异常。第二个原因是 I2C 速率过高,SSD1306 跟不上或者信号质量差,把速率降到 400kHz 以下能解决一大部分花屏问题。残影问题通常是刷新页面时没有先清整屏显存,只覆盖了局部区域,老数据显示残留在显存里。
解决措施是初始化后先全屏清一次显存,页面切换时先清屏再写入新数据。如果花屏是偶发的,重点查排线接触和电源纹波,OLED 模块对供电质量比较敏感。
4.4 RISC-V 工具链环境下代码移植的编译问题
现象很典型:把 STM32F103 的一个 HAL 库工程下载下来,改芯片型号为 CH32F103C8T6,编译直接报几百个错误,然后开始怀疑人生。
原因在于 CH32F103C8T6 虽然是引脚兼容,但内部不是 ARM Cortex-M3 核,HAL 库和 SPL 库的底层寄存器操作根本跑在 RISC-V 上。引脚和外设寄存器地址兼容,不代表库代码兼容,这是 RISC-V 移植时最简单也最容易忽略的一点。
解决的办法是使用芯片厂商提供的标准外设库或者适配过的 SDK。在官方 IDE 中新建工程时直接选对应芯片型号,模板工程里包含完整的外设驱动。另一个思路是用 Arduino 框架,在板卡管理器中安装对应核心,然后选择 CH32F103 系列,BMP280 和 SSD1306 都有现成库可以直接调,适合赶时间出原型。但要注意,Arduino 封装屏蔽了大量寄存器细节,毕设答辩时如果老师问到底层 I2C 时序怎么实现的,答不上来就会比较尴尬。我的建议是工程上用标准外设库,调试时用 Arduino 做交叉验证。
5. 验收与答辩:误差测试、连续运行验证与讲解路径
5.1 验收标准:数据可信、趋势正确、24 小时稳定
验收不能只看“屏幕亮了”,要拿出一套可复现的测试方法。我通常按下面这个表格逐项过,每一项都记录测试结果,作为答辩时的佐证材料。
| 验收项 | 验收标准 | 测试方法 |
|---|---|---|
| 传感器 ID 识别 | 读取 0xD0 返回 0x58 | 串口打印 |
| 温度准确度 | 与标准温度计相差 ±1℃ | 恒温环境静置 10 分钟比对 |
| 气压准确度 | 与本地气象站数据相差 ±2 hPa | 查询当地气象站实时气压 |
| 高度趋势 | 上升 3 米显示高度增加约 3 米 | 拿着设备在楼层间移动 |
| 显示稳定性 | 1 分钟无花屏、无闪烁 | 肉眼观察 |
| 连续运行 | 24 小时不死机、数据不中断 | 跑批处理记录串口日志 |
气压对比时注意一个细节:气象站发布的气压通常是修正到海平面的气压,而 BMP280 实测的是当地大气压,两者差一个海拔修正量。对比前先确认同一口径,或者直接用同一时刻的海平面气压做参考,否则会得出“误差很大”的错误结论。
5.2 答辩讲解路径:从需求到验证的一条线
答辩时不要按代码顺序讲,而是按“为什么做、怎么做、怎么证明”的逻辑讲。先讲为什么选 RISC-V 芯片做传感器采集,点出 CH32F103C8T6 和 STM32F103C8T6 的兼容关系与成本差异;再讲 BMP280 的原理,重点带过校准系数和温度补偿的必要性;接着讲 I2C 总线上两个设备的地址规划和通信流程;最后拿验收记录证明系统是可信的。
有一个实用的答辩技巧:把调试过程中遇到的两个典型问题(比如电荷泵没开导致 OLED 黑屏、BMP280 没退出 sleep 导致读数为 0)写进 PPT,展示“排查过程”而不是只展示“最终结果”。老师更看重分析问题的思路,传感器项目的技术难点不多,能讲清楚踩坑和排查,比罗列功能有价值得多。
从那以后我每次做传感器类项目,都强制自己先过一遍“ID 校验、寄存器手册核对、上电波形”这三步,再开始写业务代码。这套流程帮我省掉了无数黑匣子式的调试时间,希望也能帮到你。
本文还有配套的精品资源,点击获取