ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32驱动JW01-CO2模块:串口通信与OLED显示实战

STM32驱动JW01-CO2模块:串口通信与OLED显示实战 1. 拿到JW01‑CO2‑V2.2模块后先搞清楚它到底怎么用JW01‑CO2‑V2.2这个模块在空气质量检测类项目里出镜率很高尤其是做毕业设计、环境监测终端、智能新风控制这类场景。它的核心是一颗NDIR非色散红外二氧化碳传感器测量范围通常覆盖400~5000ppm精度在±50ppm左右输出方式支持UART串口和PWM两种。很多人拿到手第一反应是“这不就是个串口模块吗接上RX/TX就能读数据”但实际调试时会发现——数据要么读不出来要么读出来全是乱码要么数值跳得离谱。这个模块的UART协议有几个关键点需要先弄清楚。默认波特率是9600数据位8停止位1无校验。它每隔一段时间主动往外吐一帧数据帧格式是固定的9个字节帧头0x42、0x4D然后是数据高字节、数据低字节、保留字节、保留字节、校验和、帧尾0x0D、0x0A。校验和的计算方式是从帧头开始到校验和之前所有字节的累加和取低8位。这个协议看起来简单但如果你用STM32的串口中断去接很容易因为帧头对齐问题导致数据错位。我在第一次用这个模块的时候用的是STM32F103C8T6最小系统板串口配置成9600‑8‑N‑1开了接收中断。结果发现串口助手能看到数据但STM32读出来的值一直在跳。后来用逻辑分析仪抓了一下波形才发现模块上电后前几秒输出的数据是不稳定的而且如果串口接收中断里没有做帧头检测很容易从帧中间开始接收导致整个解析全乱。所以第一步不是急着写代码而是先用USB转TTL模块把模块接到电脑上用串口助手确认模块本身工作正常记下它输出的原始十六进制数据这一步能帮你排除掉一半的硬件问题。还有一个容易被忽略的点模块的供电。JW01‑CO2‑V2.2标称工作电压是5V但它的UART电平是3.3V的。如果你直接把模块的TX接到STM32的RX上STM32的RX引脚是5V容忍的一般没问题但如果你用的是3.3V供电的STM32模块的RX接到STM32的TX时STM32输出的3.3V高电平模块能不能识别这个要看模块的输入阈值。实测下来大部分批次是可以的但如果你遇到通信不稳定可以在模块RX和STM32 TX之间加一个电平匹配电路或者直接用5V供电的STM32板子。提示模块上电后需要预热约3分钟才能输出稳定数据刚上电时的数值没有参考价值不要在初始化完成后立刻读取并显示。2. STM32串口外设配置USART2的初始化与中断接收2.1 为什么选USART2而不是USART1在STM32F103系列里USART1挂在APB2总线上时钟频率72MHzUSART2挂在APB1总线上时钟频率36MHz。两者都能满足9600波特率的需求但USART1的引脚是PA9/PA10通常会被用来做调试串口或者下载口。如果你用USART1接CO2模块那调试信息就没地方打印了。所以我一般把USART1留给调试串口USART2用来接JW01模块引脚用PA2TX和PA3RX。这样调试和业务数据分开排查问题的时候方便很多。USART2的初始化流程和USART1基本一致但要注意APB1的时钟使能是RCC_APB1PeriphClockCmd不是APB2。很多人复制USART1的代码改的时候忘了改这个结果串口死活不工作。下面是我常用的初始化代码void USART2_Init(u32 bound) { 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_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; 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 bound; 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_Mode_IN_FLOATING不是上拉输入。因为模块的TX引脚在空闲时输出高电平如果你配成上拉反而可能因为外部高电平和内部上拉冲突导致电平不稳定。浮空输入让模块的TX直接驱动STM32的RX实测最稳。2.2 接收中断里怎么做帧头对齐串口接收中断里最忌讳的就是“收到一个字节就处理一个字节”。JW01模块输出的是9字节的固定帧如果你在中断里逐字节判断很容易因为中断延迟或者数据错位导致解析失败。我的做法是在中断里只负责把数据存进一个环形缓冲区然后在主循环里做帧解析。这样中断服务函数尽量短不会阻塞其他中断。#define RX_BUF_SIZE 128 u8 rx_buf[RX_BUF_SIZE]; u16 rx_head 0; u16 rx_tail 0; void USART2_IRQHandler(void) { if(USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { u8 data USART_ReceiveData(USART2); u16 next (rx_head 1) % RX_BUF_SIZE; if(next ! rx_tail) { rx_buf[rx_head] data; rx_head next; } USART_ClearITPendingBit(USART2, USART_IT_RXNE); } }主循环里的解析函数负责从缓冲区里找帧头0x42 0x4D然后校验后续字节。这里有个经验不要只找一次帧头就认为对齐了最好连续找到两帧完整的、校验和正确的数据再确认通信稳定。因为模块上电初期可能会有半帧数据残留如果第一次找到帧头就开始解析很可能解析出一组错误值。2.3 校验和计算与数据提取校验和的计算逻辑很简单但写代码的时候容易把范围搞错。正确的做法是把帧头0x42、0x4D、数据高字节、数据低字节、保留字节1、保留字节2这6个字节累加取低8位和第7个字节校验和比较。如果相等说明这一帧有效。CO2浓度值的计算公式是浓度 数据高字节 × 256 数据低字节。u16 JW01_GetCO2(void) { u16 co2 0; u8 frame[9]; u16 i; while(rx_tail ! rx_head) { // 找帧头 if(rx_buf[rx_tail] 0x42) { u16 next (rx_tail 1) % RX_BUF_SIZE; if(rx_buf[next] 0x4D) { // 尝试读取9字节 u16 idx rx_tail; for(i 0; i 9; i) { frame[i] rx_buf[idx]; idx (idx 1) % RX_BUF_SIZE; } // 校验 u8 sum 0; for(i 0; i 6; i) sum frame[i]; if(sum frame[6] frame[7] 0x0D frame[8] 0x0A) { co2 frame[2] * 256 frame[3]; rx_tail (rx_tail 9) % RX_BUF_SIZE; return co2; } } } rx_tail (rx_tail 1) % RX_BUF_SIZE; } return 0; }这段代码里有个坑如果缓冲区里的数据不够9字节直接读取会读到无效数据。所以实际使用时要加一个判断确保从rx_tail到rx_head之间有至少9个字节。另外如果校验失败不要直接把rx_tail跳过9字节而是只跳过1字节继续找下一个帧头否则可能漏掉真正的帧。3. I2C‑OLED显示把CO2数值实时刷到屏幕上3.1 OLED选型和I2C地址确认I2C‑OLED我用得最多的是0.96寸的SSD1306分辨率128×64I2C地址通常是0x78写地址或0x3C7位地址。不同厂家的模块地址可能不一样有的背面会标注有的需要自己扫描。如果你不确定地址可以写一个简单的I2C扫描程序把总线上所有应答的地址打印出来。STM32的硬件I2C用起来比较麻烦尤其是F103系列的I2C外设有一些已知的时序问题所以我一般用软件模拟I2C引脚随便选两个普通GPIO就行比如PB6和PB7。软件I2C的延时函数很关键。延时太短OLED可能来不及响应延时太长刷新率上不去。我一般用SysTick做微秒级延时延时2微秒左右。下面是一个简单的软件I2C写字节函数void I2C_WriteByte(u8 data) { u8 i; for(i 0; i 8; i) { I2C_SCL_L(); if(data 0x80) I2C_SDA_H(); else I2C_SDA_L(); delay_us(2); I2C_SCL_H(); delay_us(2); data 1; } I2C_SCL_L(); I2C_SDA_H(); delay_us(2); I2C_SCL_H(); delay_us(2); I2C_SCL_L(); }OLED的初始化序列比较长但都是固定的直接照搬SSD1306的数据手册或者现成的驱动库就行。需要注意的是初始化之前要先给OLED上电延时至少100ms否则可能初始化失败。这个延时很多人会忽略结果OLED一直不亮查半天以为是I2C地址错了。3.2 显示刷新策略不要每读一次就刷全屏CO2模块大约每1秒输出一帧数据如果你每次读到数据就全屏刷新一次OLED屏幕会闪得厉害而且I2C总线占用率高可能影响串口接收。我的做法是只在数值变化超过一定阈值比如10ppm时才刷新显示区域而且只刷新数值那一行不清屏。SSD1306支持页地址模式可以只更新特定页的内容。具体实现上我把屏幕分成几行第一行显示“CO2:”第二行显示数值和单位“ppm”第三行显示一个简单的状态条。数值变化时只重绘第二行的数字部分。这样刷新率可以做到很快而且屏幕不闪。另外如果CO2值超过1000ppm我会把数值颜色反色显示黑底白字起到警示作用。SSD1306支持反色显示命令不需要重新取模。注意OLED的I2C通信和串口接收中断不要在同一个优先级上冲突。如果I2C写数据时被串口中断打断可能导致I2C时序错乱。我一般把串口中断优先级设得比SysTick高但I2C操作放在主循环里不在中断里调用。3.3 显示内容的布局设计128×64的屏幕虽然小但合理安排还是能显示不少信息。我通常这样布局区域内容字体大小第0~1页标题“CO2 Monitor”6×8第2~3页CO2数值 “ppm”16×16第4页状态条根据浓度变化6×8第5~7页预留可显示温度、湿度6×816×16的字体需要取模可以用PCtoLCD2002软件生成。取模方式选“阴码逐列式顺向”这样和SSD1306的显示RAM格式匹配。如果你不想取模也可以用8×16的字体虽然小一点但够用。4. 调试过程中最容易踩的五个坑4.1 串口接收到的数据全是0xFF这个问题我遇到过好几次每次原因都不一样。第一次是模块的TX和RX接反了模块的TX应该接STM32的RX结果我接成了TX对TX。第二次是波特率不对模块默认9600但我初始化的时候写成了115200。第三次是模块供电不足我用STM32板子上的3.3V给模块供电模块启动瞬间电流不够导致输出乱码。后来换成5V供电就正常了。排查顺序建议先确认接线TX→RXRX→TX再确认波特率最后用万用表量一下模块的VCC电压。如果电压低于4.5V模块可能工作不正常。4.2 校验和总是对不上校验和对不上通常有两种情况一是帧头找错了比如把数据里的0x42 0x4D当成了帧头二是接收过程中丢了字节。第一种情况可以通过连续校验两帧来排除如果两帧都校验失败说明帧头定位有问题。第二种情况一般是串口中断被其他高优先级中断打断了导致接收缓冲区溢出。解决办法是提高串口中断优先级或者增大环形缓冲区。还有一种可能是模块输出的帧格式和手册不一致。我遇到过一批模块帧尾不是0x0D 0x0A而是0x0A 0x0D。所以校验的时候不要只判断帧尾重点还是看校验和。4.3 OLED显示乱码或花屏OLED花屏一般是初始化序列不对或者I2C通信速率太快。软件I2C的延时可以适当加大比如从2微秒改成5微秒试试。另外OLED的供电也要注意有些模块标称3.3V但实际用3.3V驱动时对比度很低换成5V就正常了。如果花屏只出现在屏幕边缘可能是显示RAM的页地址设置错了检查一下页地址范围是不是0~7。4.4 模块数值跳动大CO2模块的数值本身会有±30ppm左右的波动这是正常的。但如果跳动超过100ppm可能是以下原因模块附近有人员呼吸CO2浓度会快速上升、模块没有预热够、或者模块的进气口被遮挡了。JW01模块底部有个小孔是进气口不要把它贴在PCB上或者用热熔胶封住。另外模块要远离发热元件温度变化会影响红外传感器的读数。4.5 程序跑一段时间后死机这个问题比较隐蔽通常是栈溢出或者中断优先级配置不当导致的。如果你在串口中断里调用了OLED显示函数而OLED函数里又有延时中断执行时间过长可能导致主循环里的其他任务得不到执行。我的建议是中断里只做数据搬运所有耗时操作都放到主循环里。另外检查一下NVIC的优先级分组设置确保串口中断的抢占优先级高于SysTick但不要高于硬件错误中断。5. 从能读到好用几个提升稳定性的实战技巧5.1 加一个简单的滑动平均滤波原始数据跳动是正常的但显示的时候如果数字一直跳看起来就不专业。我一般做一个长度为5的滑动窗口取平均值显示。这样数值变化平滑又不至于响应太慢。代码很简单#define FILTER_LEN 5 u16 co2_buf[FILTER_LEN] {0}; u8 co2_idx 0; u16 CO2_Filter(u16 new_val) { u32 sum 0; u8 i; co2_buf[co2_idx] new_val; co2_idx (co2_idx 1) % FILTER_LEN; for(i 0; i FILTER_LEN; i) sum co2_buf[i]; return sum / FILTER_LEN; }注意滤波后的值只用于显示不要用来做报警判断。报警判断应该用原始值否则可能因为滤波延迟导致报警不及时。5.2 模块预热期间显示“Warming up”模块上电后前3分钟数据不稳定如果直接显示数值用户会以为模块坏了。我一般在上电后的前180秒内OLED上显示“Warming up...”和一个倒计时倒计时结束后再显示实际数值。这样用户体验好很多也避免了误判。5.3 串口通信加超时重试虽然JW01模块是主动输出不需要STM32发送指令但如果你用的是需要请求才回复的版本就要加超时重试。即使是主动输出模式也建议在主循环里加一个超时检测如果超过3秒没有收到有效帧就在OLED上显示“Sensor Error”并尝试重新初始化串口。这个机制在长时间运行的项目里很有用能自动恢复偶发的通信故障。5.4 电源去耦和PCB布局如果你是自己画PCB模块的VCC引脚旁边一定要放一个100nF的陶瓷电容和一个10μF的钽电容。JW01模块内部有红外光源工作时电流会有脉冲波动如果电源去耦不好会影响STM32的ADC或者其他外设。另外模块的TX/RX走线尽量短远离晶振和电源开关电路。5.5 用Keil的逻辑分析仪功能辅助调试Keil MDK自带的逻辑分析仪可以实时查看变量变化不需要额外的硬件。你可以把CO2原始值、滤波后的值、串口接收缓冲区的头尾指针都加到逻辑分析仪里跑起来之后一眼就能看出数据流是否正常。这个功能在调试串口协议的时候特别有用比打印调试信息直观得多。6. 把CO2数据用起来从显示到控制的扩展思路6.1 阈值报警与继电器控制CO2浓度超过1000ppm时人就会感到闷超过2000ppm会明显不适。你可以设两个阈值1000ppm时OLED显示警告1500ppm时触发继电器打开新风系统或风扇。继电器模块一般用5V供电STM32的GPIO输出3.3V可能驱动不了需要加一个三极管或者用光耦隔离。我一般用S8050三极管做驱动基极串一个1k电阻继电器线圈两端反并联一个1N4148续流二极管。6.2 数据上传到上位机如果你想把数据传到电脑上画曲线可以用STM32的USART1做调试串口把CO2值以固定格式打印出来比如“CO2: 856 ppm\r\n”。上位机用串口助手或者自己写一个Python脚本解析。Python这边用pyserial库几行代码就能把数据存成CSV文件然后用matplotlib画图。这个方案比接屏幕更灵活适合做数据分析。6.3 结合RTC做定时记录STM32F103内部有RTC配一个32.768kHz的晶振就能走时。你可以每隔10分钟把CO2值和时间戳一起存到EEPROM或者SD卡里做成一个简易的空气质量记录仪。RTC的初始化要注意F103的RTC时钟源是LSE外部低速晶振或者LSI内部低速RCLSE更准但需要晶振起振LSI不需要外部元件但精度差一些。如果只是做记录LSI够用了。6.4 低功耗场景下的考虑如果你的项目是电池供电的JW01模块的功耗不低大概在20mA左右。STM32可以进Stop模式但模块不能断电否则每次上电都要重新预热。这种场景下建议用定时器唤醒STM32读一次数据就继续睡OLED也只在唤醒时刷新。不过说实话JW01模块本身就不适合低功耗场景如果对功耗有要求可以考虑其他类型的CO2传感器。7. 关于这个模块和STM32搭配的一些个人体会JW01‑CO2‑V2.2这个模块的性价比在百元以内的CO2传感器里算是很能打的NDIR原理决定了它比那些半导体式的CO2传感器靠谱得多。但它也有自己的脾气预热时间长、数据有波动、对电源质量敏感。STM32这边USART2加软件I2C OLED的组合是我用得最顺手的方案硬件资源占用少调试也方便。如果你刚开始做这个项目我的建议是先把模块用USB转TTL接到电脑上用串口助手确认数据格式然后再写STM32代码。不要一上来就焊板子、写程序那样出了问题你都不知道是硬件问题还是软件问题。另外Keil的软件仿真虽然能跑逻辑但串口和I2C这种外设还是要在真实硬件上调试仿真和实际差距很大。最后说一个细节JW01模块的UART输出是3.3V电平但如果你用5V的STM32板子STM32的TX输出5V电平到模块的RX长期看可能会影响模块寿命。稳妥的做法是在模块RX和STM32 TX之间串一个1k电阻再加一个3.3V的稳压二极管做钳位。这个电路成本不到一毛钱但能避免很多莫名其妙的通信故障。
返回列表