ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32驱动JW01-CO2-V2.2:串口通信、数据解析与OLED显示实战

STM32驱动JW01-CO2-V2.2:串口通信、数据解析与OLED显示实战 1. 拿到JW01‑CO2‑V2.2模块后先搞清楚它到底怎么用JW01‑CO2‑V2.2这个模块在圈子里出现频率不低尤其是做空气质量检测、智能通风、农业大棚监控这类项目的时候很多人第一反应就是找它。原因也简单它把红外二氧化碳传感器的信号调理、标定、串口输出都集成在一块小板子上了你不需要自己去搞光学腔体的驱动电路也不用研究NDIR传感器的原始信号怎么滤波。模块上电就能通过串口往外吐数据对STM32开发者来说门槛确实低了不少。但低门槛不等于没坑。我见过太多人拿到模块接上STM32的USART2打开串口助手一看——要么全是乱码要么数据不动要么读出来的CO2浓度一直是5000ppm顶格。这些问题十有八九不是模块坏了而是几个关键细节没对上。这篇文章就围绕STM32驱动JW01‑CO2‑V2.2这个具体场景把从硬件接线、串口配置、协议解析到OLED显示的完整链路拆开讲清楚同时把那些文档里不会写、但实际调试中一定会遇到的坑一并交代。适合谁看如果你手头正好有这块模块和一块STM32最小系统板想快速把CO2数据读出来并显示在OLED上那这篇内容可以直接照着做。如果你还没买模块也可以先看看它的通信逻辑和注意事项判断它是否适合你的项目需求。2. JW01‑CO2‑V2.2的通信协议与数据帧拆解2.1 模块默认的串口参数和输出节奏JW01‑CO2‑V2.2出厂默认走的是UART通信波特率9600数据位8停止位1无校验。这个参数和STM32标准库下USART2的常用配置完全一致所以硬件上只需要把模块的TX接到STM32的RX模块的RX接到STM32的TX共地就能建立通信。模块内部会周期性地往外发送数据帧不需要你主动发指令去问。这个“主动上报”的模式对初学者很友好因为省去了请求-响应的交互逻辑。但这里有个细节模块的上报周期并不是固定不变的它会根据内部状态和CO2浓度的变化速率做动态调整。浓度稳定时可能几秒才发一帧浓度快速上升时可能一秒发好几帧。所以你在STM32端做数据解析时不能假设“每隔固定时间一定会来一帧”而应该用“收到完整帧就解析”的思路去处理。这一点在后面讲串口中断接收的时候会具体展开。2.2 数据帧的字节结构和校验方式模块输出的数据帧长度是固定的一共9个字节。我直接把帧结构列出来方便你对照字节位置含义说明Byte0帧头固定为0xFFByte1命令字固定为0x01Byte2数据高字节CO2浓度的高8位Byte3数据低字节CO2浓度的低8位Byte4保留通常为0x00Byte5保留通常为0x00Byte6保留通常为0x00Byte7校验和前面7个字节的累加和取低8位Byte8帧尾固定为0xFECO2浓度的计算方式是浓度 (Byte2 8) | Byte3。比如Byte2是0x01Byte3是0x8A那浓度就是0x018A十进制394ppm。这个值就是当前环境下的二氧化碳浓度单位是ppm。校验和的计算很多人会搞错。它不是异或也不是CRC而是简单的累加和。具体来说把Byte0到Byte6这7个字节相加取结果的低8位就是Byte7的值。你在STM32端解析时一定要先验证校验和再取浓度值。否则一旦串口受到干扰某个字节跳变你算出来的浓度可能就是错的而且错得离谱。2.3 为什么校验和验证不能省我刚开始用这个模块的时候觉得串口通信挺稳定的校验和验证就懒得加。结果有一次在电机旁边做测试串口线上感应到了干扰Byte3偶尔会跳变。因为CO2浓度的高字节和低字节权重差256倍低字节跳一下可能只差几十ppm但高字节跳一下直接差几千ppm。更麻烦的是这种跳变是偶发的你盯着屏幕看半天可能才出现一次很容易被忽略。后来加上校验和验证之后凡是校验不通过的帧直接丢弃数据就干净多了。所以我的建议很明确校验和验证这一步必须做而且要在解析浓度之前做。校验不通过的帧直接丢掉不要试图去“修正”它。串口数据帧本来就是周期上报的丢一帧无所谓下一帧很快就来了。3. STM32端USART2的配置与中断接收实现3.1 为什么选USART2而不是USART1STM32的USART1和USART2在功能上都能满足这个模块的需求但实际项目里我更倾向于用USART2。原因有几个第一USART1的引脚通常是PA9和PA10这两个引脚在很多最小系统板上已经默认接到了CH340之类的USB转串口芯片上用来做下载和调试打印。如果你把CO2模块也接到USART1就会和调试串口冲突。第二USART2的引脚是PA2和PA3这两个脚在大多数板子上是空闲的接线方便。第三如果你后面还要加别的串口设备USART1留给调试口USART2接CO2模块USART3接其他模块分工清晰不会打架。当然如果你用的是STM32F103C8T6这类引脚比较少的最小系统板PA2和PA3可能被其他功能占用了那就需要根据实际引脚分配来调整。核心原则是CO2模块独占一个USART不要和调试串口共用。3.2 标准库下的USART2初始化代码下面这段代码是基于STM32标准库的USART2初始化波特率96008位数据1位停止无校验开启接收中断。我直接把关键配置列出来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); }这段代码里有一个地方容易出问题PA3的GPIO模式。很多人习惯性地把RX引脚也配成复用推挽输出结果就是串口收不到数据。RX引脚必须配成浮空输入或者上拉输入因为它是接收端STM32要读外部电平。TX引脚才是复用推挽输出。这个细节在标准库的例程里有时候写得不够醒目但实际配置错了就是收不到数据。3.3 中断接收的状态机设计串口中断里不能做太多事情尤其是不能做耗时操作。我的做法是在中断里只做两件事把收到的字节存入一个缓冲区然后置一个标志位。主循环里再去检查标志位解析数据帧。但这里有个问题模块发过来的数据帧是9个字节连续的如果中断里只收一个字节就退出主循环还没来得及处理下一个字节就来了很容易丢字节。所以更稳妥的做法是在中断里维护一个接收状态机按帧头、数据、校验、帧尾的顺序逐字节接收收满一帧再置标志位。具体实现思路是这样的定义一个长度为9的数组作为帧缓冲区再定义一个索引变量。中断里每收到一个字节先判断当前索引位置应该收什么。如果索引是0判断是不是0xFF是就存入并索引加1不是就丢弃并保持索引为0。如果索引在1到7之间直接存入并索引加1。如果索引是8判断是不是0xFE是就存入、索引归零、置帧接收完成标志不是就索引归零重新开始。这样一套逻辑下来即使中间丢了几个字节状态机也能自动重新同步到帧头。注意中断里不要做校验和计算也不要做浮点运算。这些都在主循环里做。中断服务函数的执行时间越短越好。4. 数据解析、校验与浓度换算的完整实现4.1 主循环里的帧解析流程主循环里检测到帧接收完成标志后先不要急着清标志而是先把帧缓冲区的内容拷贝出来再清标志。这样做的目的是避免在解析过程中中断又收到新帧覆盖了缓冲区。拷贝出来之后按照前面说的校验和规则验证Byte0到Byte6的累加和低8位是否等于Byte7。如果相等再计算浓度值如果不相等直接丢弃。浓度计算就是简单的位移和或运算co2 (frame[2] 8) | frame[3]。算出来的值可以直接用不需要再做其他换算。模块输出的就是ppm值量程一般是400到5000ppm。如果你读出来的值长期在400左右说明环境空气正常如果到了1000以上说明通风不良如果顶到5000要么是模块需要标定要么是环境里确实有高浓度CO2源。4.2 校验和计算的代码实现校验和计算我单独写了一个函数方便复用uint8_t JW01_CheckSum(uint8_t *data, uint8_t len) { uint8_t sum 0; for (uint8_t i 0; i len; i) { sum data[i]; } return sum; }调用的时候传入帧缓冲区的首地址和7返回值跟frame[7]比较。这里注意len传7不是8也不是9。因为校验和是前7个字节的累加不包括校验和本身也不包括帧尾。4.3 数据滤波让读数不再跳来跳去模块原始输出的浓度值会有小幅波动这是NDIR传感器的正常特性。如果你直接把这个值显示在OLED上会看到数字一直在跳比如398、402、395、401这样。对于显示来说这种跳动很影响观感。我的做法是加一个简单的滑动平均滤波取最近5次有效读数的平均值作为显示值。滑动平均的实现很简单定义一个长度为5的数组和一个索引每次收到有效帧就把新值存入数组索引循环递增然后计算数组里所有有效值的平均。如果还没收满5个值就只对已收到的值求平均。这样处理之后显示值会平滑很多但响应速度也不会太慢因为窗口只有5个样本。提示如果你做的是需要快速响应的项目比如CO2浓度超标立刻触发报警那滤波窗口不要设太大3到5个样本就够了。窗口太大虽然曲线好看但报警会延迟。5. I2C‑OLED显示CO2浓度的接线与驱动要点5.1 OLED的I2C地址和引脚分配I2C‑OLED模块常见的是0.96寸和1.3寸两种驱动芯片大多是SSD1306或SH1106。I2C地址通常是0x78或0x7A具体取决于模块上的地址选择电阻。大部分模块默认是0x78也就是7位地址0x3C左移一位。如果你用硬件I2CSTM32的I2C1引脚是PB6和PB7I2C2是PB10和PB11。我一般用I2C1因为PB6和PB7在大多数板子上都引出来了。接线的时候注意OLED的VCC接3.3VGND共地SCL接PB6SDA接PB7。有些OLED模块标的是GND、VCC、SCL、SDA的顺序有些是VCC、GND、SCL、SDA接线前一定要看清楚模块上的丝印接反了可能烧模块。5.2 硬件I2C和软件模拟I2C的选择STM32的硬件I2C在F103系列上有个历史遗留问题在某些情况下会卡死在总线忙状态尤其是总线上有干扰或者从设备没有正确响应的时候。虽然ST后来在库里面做了一些修补但实际用起来还是不如软件模拟I2C稳。所以对于OLED这种低速外设我通常直接用软件模拟I2C用两个普通GPIO口通过延时控制时序。这样虽然占用一点CPU时间但稳定性好很多而且移植方便换到任何一款STM32上都能用。软件模拟I2C的延时可以用简单的for循环实现不需要精确的微秒级延时。OLED的I2C速率一般在400kHz以下STM32跑72MHz的时候几个空循环就能满足时序要求。如果你用SysTick做延时注意不要在中断里调用否则会卡死。5.3 OLED显示内容的布局设计显示内容我一般分三行第一行显示“CO2:”第二行显示浓度数值和单位“ppm”第三行显示一个简单的状态条或者报警提示。字体用6x8或者8x16都可以8x16看起来更清楚但一行只能显示16个字符。浓度数值我习惯用8x16字体因为数字大一点远处也能看清。刷新策略上不要每次收到新数据就全屏刷新那样屏幕会闪。我的做法是只刷新浓度数值所在的区域其他区域不动。SSD1306支持页地址和列地址设置你可以精确控制刷新区域。如果嫌麻烦也可以用一个标志位控制刷新频率比如每500ms刷新一次这样即使数据更新很快屏幕也不会闪。6. 调试过程中最容易卡住的几个问题6.1 串口收不到任何数据这是最常见的问题。排查顺序是这样的先用万用表量模块的VCC和GND确认供电正常。JW01‑CO2‑V2.2的工作电压一般是5V虽然有些版本标称3.3V到5V宽压但实测5V更稳定。然后检查TX和RX有没有接反。模块的TX要接STM32的RX模块的RX要接STM32的TX。很多人在这里接反了然后一直等不到数据。如果接线没问题用示波器或者逻辑分析仪看一下模块的TX引脚有没有波形。如果没有波形可能是模块本身没启动。模块上电后一般需要几十秒的预热时间预热期间可能不输出数据或者输出固定值。等一两分钟再看。如果模块TX有波形但STM32收不到那就检查USART2的配置。重点看波特率是不是9600GPIO模式对不对中断有没有使能NVIC有没有配置。这几个地方任何一个错了都会导致收不到数据。6.2 数据全是乱码乱码通常意味着波特率不匹配。检查STM32的USART2波特率是不是9600模块的波特率是不是9600。有些模块可以通过指令修改波特率如果你之前改过可能现在不是9600了。另外如果你用的是外部晶振检查晶振频率和系统时钟配置对不对。STM32F103标准库默认用的是8MHz外部晶振如果你板子上是12MHz或者别的频率系统时钟就不对波特率也会跟着错。还有一种可能是数据位、停止位、校验位配置不匹配。模块默认是8N1你的USART2也必须是8N1。如果配成了8E1或者7N1收到的数据就是乱的。6.3 浓度值一直不变或者顶格浓度值一直不变先看模块是不是在预热。预热期间输出固定值是正常的。如果预热了很久还是不变可能是模块的传感器腔体被堵住了或者模块需要重新标定。JW01‑CO2‑V2.2支持手动标定具体方法是在干净空气环境下室外或者通风良好的室内给模块发标定指令把当前浓度校准到400ppm。标定指令的具体格式参考模块手册不同批次的固件可能略有差异。浓度值顶格5000ppm最常见的原因是模块刚上电还没预热完或者环境里确实有高浓度CO2。如果你对着模块呼气浓度会快速上升这是正常的。如果放在那里不动也顶格那可能是传感器坏了或者需要标定。6.4 OLED不亮或者显示异常OLED不亮先检查供电。3.3V有没有GND有没有共地。然后检查I2C地址对不对。用逻辑分析仪抓一下I2C总线看STM32有没有发出起始信号和地址字节。如果地址字节发出去了但没有应答说明地址错了或者OLED没焊好。显示异常比如花屏、部分区域不显示通常是初始化序列不对或者显存刷新逻辑有问题。SSD1306的初始化序列比较长建议直接用一个经过验证的初始化数组不要自己一条一条命令去试。显存刷新的时候注意页地址范围是0到7列地址范围是0到127超出范围的操作会被忽略或者产生奇怪的效果。7. 从能读到能用几个提升项目完成度的细节7.1 电源处理对CO2读数的影响JW01‑CO2‑V2.2内部的红外光源在工作时会有脉冲电流如果供电不稳光源的驱动电流就会波动进而影响测量精度。我在实际项目里发现用USB直接供电的时候CO2读数偶尔会跳变换成LDO稳压供电之后读数就稳定多了。所以如果你的项目对精度有要求建议给模块单独加一个LDO比如AMS1117‑5.0输入输出都加滤波电容。模块的VCC和GND之间再并一个100uF的电解电容和一个0.1uF的陶瓷电容效果会更好。7.2 模块预热时间的合理处理模块上电后需要预热预热时间跟环境温度有关一般一到三分钟。预热期间输出的浓度值不可信我的做法是在STM32端加一个计时器上电后前120秒收到的数据只用于显示“预热中”不参与报警判断也不参与滤波。120秒之后的数据才认为是有效数据。这样避免了开机就误报警的尴尬。7.3 串口数据帧的容错处理前面说了校验和验证但还有一种情况模块偶尔会输出一些不完整的帧比如只发了前几个字节就断了。这种帧如果被状态机收进去会导致后续的字节错位。我的处理方式是在状态机里加一个超时机制如果超过一定时间没有收到新的字节就把索引归零重新等待帧头。这个超时时间可以设为100ms左右因为模块正常发帧的时候9个字节是连续发出来的字节间隔不会超过几毫秒。7.4 把CO2数据接入更上层的逻辑读到了CO2浓度显示在OLED上这只是第一步。如果你做的是智能通风项目下一步就是根据浓度值控制风扇或者新风系统。我的建议是设置两个阈值一个高阈值比如1000ppm超过就开风扇一个低阈值比如600ppm低于就关风扇。中间留一个回差避免风扇频繁启停。这个逻辑用简单的if-else就能实现但阈值的选择要根据实际场景来调。办公室和卧室的舒适区间不一样大棚和冷库的要求也不一样没有万能值。8. 关于这块模块和STM32搭配的一些个人体会JW01‑CO2‑V2.2在国产CO2模块里算是性价比比较高的选择串口输出简单直接不需要额外的驱动电路对STM32开发者来说上手很快。但它的精度和长期稳定性跟工业级的NDIR传感器比还是有差距如果你做的是需要高精度、长期无人值守的项目可能需要考虑更高阶的模块或者定期标定。STM32这边USART2加中断接收加状态机解析这套组合基本上可以应对大多数串口传感器的数据读取需求。把这套逻辑吃透之后换成其他串口输出的传感器比如PM2.5、温湿度、甲醛模块代码结构几乎不用大改只需要改帧格式和校验方式就行。所以这个项目虽然简单但作为一个串口通信和传感器数据处理的练手项目价值还是挺高的。最后说一个我踩过的坑有一次我把模块的TX直接接到了STM32的PA3但没有共地。结果串口助手能看到数据STM32就是收不到。查了半天才发现是地线没接。串口通信也好I2C也好共地是底线任何时候都不能省。
返回列表