尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32驱动JW01-CO2与OLED:串口中断与I2C显示实战

STM32驱动JW01-CO2与OLED:串口中断与I2C显示实战 1. 项目缘起与整体方案设计JW01-CO2-V2.2 这颗模块在圈子里其实不算新面孔做空气质量检测类项目的人大概率都碰过它。它本质上是一颗基于 NDIR非色散红外原理的二氧化碳传感器出厂已经做好了标定串口直接吐数据省去了自己写补偿算法的麻烦。我这次拿它配合 STM32 做一套完整的采集与显示方案核心目标很明确STM32 通过 USART2 读取 CO2 浓度再把数据实时刷到 I2C 接口的 OLED 屏上形成一个可以独立运行的桌面监测终端。为什么选这个组合先说模块本身。JW01-CO2-V2.2 的通信协议非常友好默认波特率 9600主动上报模式下一秒钟推一次数据帧格式固定校验也简单。对于不想在传感器底层驱动上耗太多精力的开发者来说它几乎是“即插即用”的典型。而 STM32 这边我选的是最常见的 F103C8T6 最小系统板资源够用、资料多、成本低USART2 挂在 APB1 总线上配合 I2C1 驱动 OLED两个外设互不打架引脚分配也宽松。整体方案的设计思路是这样的USART2 配置为接收中断模式每收到一帧完整数据就置一个标志位主循环检测到标志位后解析出 CO2 浓度值再通过 I2C1 把数值和单位写到 OLED 上。这里没有用 DMA原因后面会细说。整个数据流是“中断收帧—主循环解析—I2C 刷屏”三段式逻辑清晰调试起来也方便。注意JW01-CO2-V2.2 有多个版本V2.2 的帧格式和早期版本略有差异拿到模块后先用 USB-TTL 接电脑确认一下实际输出别直接照搬网上的老代码。适合谁来参考这套方案如果你正在做基于 STM32 的空气质量检测开源项目或者毕业设计里需要环境监测环节又或者只是想找个练手项目把串口中断和 I2C 驱动串起来这套东西的复杂度刚好合适。它不涉及 RTOS不需要复杂的任务调度裸机跑起来就很稳。下面我把整个实现过程拆开讲包括选型考量、参数计算、代码结构和踩过的坑。2. 硬件选型与接线细节拆解2.1 为什么是 JW01-CO2-V2.2 而不是其他 CO2 模块市面上常见的 CO2 传感器大致分几类MG811 这类电化学的、CCS811 这类 VOC 兼测的、MH-Z19 系列 NDIR 的还有 JW01 这种 NDIR 串口模块。MG811 需要预热、输出模拟电压、还得做温度补偿对新手不友好CCS811 测 CO2 是推算值精度有限MH-Z19 精度不错但价格偏高而且有些版本默认是 PWM 输出要改串口模式得发命令。JW01-CO2-V2.2 的优势在于出厂标定、串口主动上报、价格适中、量程覆盖 400 到 5000 ppm日常室内监测完全够用。它的 NDIR 原理决定了它对 CO2 有较好的选择性不像 VOC 传感器那样容易被酒精、香水干扰。实测下来在密闭房间里点一根蜡烛数值几分钟内就能从 600 多爬到 1500 以上响应速度可以接受。2.2 STM32 最小系统板的引脚分配我用的是 F103C8T6 核心板48 引脚那种。USART2 的默认引脚是 PA2TX和 PA3RXI2C1 的默认引脚是 PB6SCL和 PB7SDA。接线的时候注意一点STM32 的 USART2_TX 要接模块的 RXUSART2_RX 要接模块的 TX交叉连接。模块供电是 5V逻辑电平也是 5V而 STM32 的 IO 是 3.3V 电平。这里有个关键问题模块的 TX 输出 5V 电平直接进 STM32 的 RX 引脚长期看是有风险的。我的处理方式是在模块 TX 和 STM32 PA3 之间串一个 1k 电阻再在 PA3 到地之间接一个 3.3V 稳压管做钳位。这样 5V 电平经过分压和钳位后进到 PA3 的电压大约在 3.3V 左右安全。如果你嫌麻烦也可以直接用电平转换模块但成本会高一点。模块的 RX 接 STM32 的 PA2 就没这个问题因为 STM32 输出 3.3V 高电平模块的 5V 输入阈值通常能识别为高实测通信正常。模块引脚STM32 引脚说明VCC5V模块供电必须 5VGNDGND共地TXPA3经 1k 电阻 3.3V 钳位RXPA2直接连接OLED 这边我用的是 0.96 寸 I2C 接口的 SSD13064 针那种。VCC 接 3.3VGND 共地SCL 接 PB6SDA 接 PB7。I2C 总线上拉了 4.7k 电阻到 3.3V模块自带的也有但并联后阻值偏小我一般只保留一组。2.3 电源部分的注意事项整个系统用一个 5V 电源适配器供电STM32 核心板上有 AMS1117-3.3 稳压。JW01 模块的功耗在峰值时能到 100mA 左右AMS1117 发热会有点明显。我试过把钽电容换成陶瓷电容对稳定性影响不大但输入输出电容的 ESR 会影响纹波建议还是保留原厂的钽电容或者用低 ESR 的电解电容。如果模块和 STM32 共用一路 5V最好在模块 VCC 附近并一个 100uF 电解加 0.1uF 陶瓷防止模块内部加热丝工作时把电源拉低导致 STM32 复位。3. USART2 接收中断的配置与数据帧解析3.1 串口参数计算与初始化JW01-CO2-V2.2 默认波特率 96008 位数据位1 位停止位无校验。STM32F103 的 USART2 挂在 APB1 总线上APB1 时钟默认是 36MHz。波特率寄存器 USARTDIV 的计算公式是USARTDIV fCK / (16 × 波特率)代入 36MHz 和 9600得到 USARTDIV 36000000 / (16 × 9600) 234.375。整数部分是 234小数部分 0.375 乘以 16 等于 6所以 BRR 寄存器的值就是 234 左移 4 位再加 6即 0xEA6。用标准库的USART_Init函数时库会自动算这个值但知道原理有助于排查波特率偏差问题。初始化代码大致如下void USART2_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // PA2 TX 复用推挽 GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // PA3 RX 浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, 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_Rx | USART_Mode_Tx; USART_Init(USART2, USART_InitStructure); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); NVIC_InitStructure.NVIC_IRQChannel USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); USART_Cmd(USART2, ENABLE); }3.2 中断服务函数里的收帧逻辑JW01-CO2-V2.2 的主动上报帧格式是固定的 9 个字节帧头 0xFF、0x86然后是 6 个数据字节最后是校验和。校验和的计算方式是从帧头开始累加前 8 个字节取低 8 位。数据字节里第 3 和第 4 个字节组合成 CO2 浓度值高字节在前。中断里我不做解析只负责把字节塞进缓冲区收满 9 个字节就置标志位。这样做的好处是中断执行时间短不会阻塞其他中断。具体实现#define FRAME_LEN 9 uint8_t rx_buf[FRAME_LEN]; uint8_t rx_cnt 0; volatile uint8_t frame_ready 0; void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART2); rx_buf[rx_cnt] data; if (rx_cnt FRAME_LEN) { rx_cnt 0; frame_ready 1; } USART_ClearITPendingBit(USART2, USART_IT_RXNE); } }这里有个细节如果模块上电后第一帧数据不完整缓冲区会错位。我的做法是在主循环解析时先检查帧头如果不是 0xFF 0x86就丢弃这一帧等下一帧。更稳妥的方式是在中断里做状态机但裸机项目里主循环检查帧头已经够用。3.3 校验与浓度值提取主循环里检测到frame_ready后先验帧头和校验和void parse_frame(void) { if (rx_buf[0] ! 0xFF || rx_buf[1] ! 0x86) return; uint8_t checksum 0; for (int i 0; i 8; i) checksum rx_buf[i]; if (checksum ! rx_buf[8]) return; uint16_t co2 (rx_buf[2] 8) | rx_buf[3]; // co2 即为浓度值单位 ppm }实测下来模块每秒上报一帧校验通过率接近 100%。偶尔因为电源波动丢一帧下一帧就恢复了不影响整体显示。4. I2C-OLED 驱动与显示刷新策略4.1 SSD1306 的 I2C 地址与初始化序列0.96 寸 SSD1306 的 I2C 地址通常是 0x78写地址也就是 7 位地址 0x3C 左移一位。有些模块背面电阻换一下会变成 0x7A拿到手先用扫描程序确认一下。初始化序列比较长核心是设置时钟分频、复用率、显示偏移、起始行、电荷泵、内存寻址模式、扫描方向、对比度这些。我一般把初始化序列做成一个数组逐条发送const uint8_t oled_init_cmds[] { 0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40, 0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0xCF, 0xD9, 0xF1, 0xDB, 0x40, 0xA4, 0xA6, 0xAF };每条命令前要加 0x00 控制字节表示后面跟的是命令写数据时控制字节是 0x40。I2C 发送函数用标准库的I2C_GenerateSTART、I2C_Send7bitAddress那一套或者用硬件 I2C 的 DMA但裸机项目里轮询发送就够了刷一屏 128×64 的显存也就几毫秒。4.2 显存管理与刷屏时机SSD1306 的显存是 128×64 位分成 8 页每页 128 字节。我在 STM32 里开一个uint8_t oled_gram[8][128]的数组做镜像所有绘图操作先改这个数组最后统一刷到屏幕。这样避免频繁 I2C 通信也方便做局部刷新。刷屏时机很关键。如果每收到一帧数据就全屏刷新I2C 占用时间虽然不长但屏幕会有轻微闪烁感。我的做法是每 500ms 刷一次中间只更新显存里的数值区域。具体来说CO2 数值显示在固定位置每次解析出新值后先把旧数值区域清掉再写新数值最后整屏刷新。实测下来500ms 的刷新周期肉眼看起来是连续的没有跳变感。提示OLED 显示中文需要取模我一般用 PCtoLCD2002 软件阴码、逐列式、顺向取模字体大小 16×16。如果只是显示数字和英文直接用 8×16 的 ASCII 字库就够了省事。4.3 显示内容的布局设计屏幕上我分了四行第一行显示 “CO2 Monitor”第二行显示浓度数值加单位 “ppm”第三行显示一个简单的状态条第四行留空或者显示 “STM32JW01”。状态条是根据浓度值映射到 0 到 128 像素的宽度直观反映当前空气质量。数值超过 1000 时状态条变长超过 2000 时可以在数值旁边加个感叹号提示。这种布局的好处是信息层次清晰一眼就能看到核心数值。如果你要做基于 STM32 的智能台灯或者鱼缸项目可以把 CO2 数值作为触发条件比如超过阈值就点亮警示灯或者启动风扇。5. 常见问题排查与实操避坑指南5.1 串口收不到数据或数据乱码这是最常见的问题排查顺序我一般是这样的先确认模块供电是不是 5V3.3V 供电模块不工作再用 USB-TTL 直接接模块看电脑端能不能收到正确数据排除模块本身的问题然后检查 STM32 的波特率是不是 9600APB1 时钟是不是 36MHz如果系统时钟配置改了APB1 分频变了波特率就会偏最后检查接线是不是 TX-RX 交叉了这个低级错误我犯过不止一次。还有一种情况是数据偶尔乱码大概率是电源干扰。模块内部有加热丝工作时电流突变会通过电源耦合到 STM32。我在模块 VCC 和 GND 之间并了一个 100uF 电解电容后乱码明显减少。5.2 OLED 不亮或显示花屏OLED 不亮先查 I2C 地址对不对用扫描程序扫一下总线看能不能找到设备。如果地址对但屏幕不亮检查初始化序列里电荷泵命令 0x8D 0x14 有没有发这个命令不开屏幕永远不亮。花屏通常是显存数据错位检查页地址和列地址的设置顺序SSD1306 要求先设页地址再设列地址顺序反了就会花。还有一个坑是 I2C 上拉电阻。有些 OLED 模块自带上拉STM32 核心板的 I2C 引脚上也有上拉两组并联后阻值可能降到 2k 以下导致上升沿太陡通信不稳定。我的做法是只保留一组 4.7k 上拉另一组拆掉。5.3 中断优先级冲突导致死机如果项目里还有定时器中断、外部中断USART2 的中断优先级要合理安排。我把 USART2 的抢占优先级设为 1定时器设为 2这样串口收帧不会被定时器打断。如果串口中断里执行时间过长比如在中断里做浮点运算或者刷 OLED会导致其他中断响应延迟严重时看门狗复位。记住一个原则中断里只做最紧急的事解析和显示都放到主循环。问题现象可能原因排查方法收不到数据供电不足、接线错误万用表测电压、检查 TX/RX数据乱码波特率偏差、电源干扰示波器看波形、加滤波电容OLED 不亮电荷泵未开、地址错误扫描 I2C 地址、检查初始化屏幕花屏页列地址顺序错、上拉过强调整设置顺序、拆上拉电阻系统死机中断优先级冲突、中断过长调整优先级、精简中断代码5.4 模块预热与数据稳定性JW01 模块上电后需要预热前几分钟数值会偏高然后慢慢回落到正常水平。我实测大概 3 到 5 分钟后数据才稳定。如果你做的是需要快速出结果的项目可以在上电后先显示 “预热中”等数值波动小于 50ppm 再正式显示。另外模块的 NDIR 腔体对气流敏感不要把它放在风扇直吹或者空调出风口附近否则数值会跳得厉害。6. 代码组织与工程结构建议6.1 文件划分与模块解耦裸机项目最容易写成一锅粥所有代码堆在 main.c 里。我的习惯是按功能拆成几个文件usart2.c管串口初始化和中断jw01.c管帧解析和浓度提取oled.c管 I2C 和显示驱动main.c只负责调度。这样改起来方便比如以后要换传感器只动 jw01.c 就行显示部分不用碰。头文件里用extern声明全局变量比如extern volatile uint8_t frame_ready;在 main.c 里检测这个标志。注意volatile不能省否则编译器优化后主循环可能读不到中断里的修改。6.2 主循环的调度逻辑主循环里我做了三件事检测帧标志、解析数据、定时刷屏。用一个软件计数器做 500ms 定时不用额外开定时器uint32_t last_tick 0; while (1) { if (frame_ready) { frame_ready 0; parse_frame(); } if (HAL_GetTick() - last_tick 500) { last_tick HAL_GetTick(); oled_refresh(); } }如果你用的是标准库没有HAL_GetTick可以用 SysTick 中断自己维护一个毫秒计数器。这个结构简单可靠实测连续跑 24 小时没有异常。6.3 后续扩展方向这套东西跑通之后扩展空间很大。比如加一个 SD 卡模块把 CO2 数据按时间戳存成 CSV方便做趋势分析或者加一个 ESP8266 模块把数据传到手机上看再或者加个继电器CO2 超标时自动启动新风系统。我之前做过一个版本用 STM32 的另一个串口接 LoRa 模块把数据传到几十米外的接收端做温控电路的时候顺便把 CO2 也带上了。代码层面可以把帧解析做成状态机支持多种传感器协议自动识别显示部分可以移植 LVGL做更复杂的界面。不过对于大多数场景现在这个裸机方案已经够用了稳定、简单、好维护。注意如果你要加 OTA 功能记得给 Bootloader 留出空间F103C8T6 只有 64KB Flash算好分区再动手。最后分享一个小技巧调试串口的时候我习惯在中断里翻转一个空闲的 GPIO用示波器看波形能直观判断收帧频率和中断响应时间。这个办法比打印日志快得多也不占用串口资源。
返回列表