[i=s] 本帖最后由 WT_0213 于 2025-9-9 23:26 编辑 [/i]
对之前版本做了一些优化,更新了以下功能:
粉尘PM2.5检测、甲醛检测、二氧化碳浓度检测
增加了 数值单位,如PM2.5与甲醛浓度都是ug/m³,二氧化碳浓度为ppm。
操作优化:
单击 显示下一个功能页面,双击显示上一个功能页面。
按钮操作代码优化,目前可稳定执行以上操作。
家里刚装修不就总是觉得害怕有甲醛什么的影响健康。市场上虽然有很多这样的检测设备,开始想买来着后来又觉得功能太过单一,就想着自己能不能 做一个呢。本着 DIY的精神与实用至上的原则并且有板子有模块那不是轻轻松松。

这个设备可以配合,桌面空气净化器使用。后面可能去完善这部分功能。
已经预留的两个引脚。 外部接口 这里有四个针脚,开始想的是,两个引脚一个引脚是VCC 一个GND, 另外两个是用来控制设备用的。一个 控制风扇转速,一个控制开关。当空气质量不好的时候就开启。想法是使用电脑的12cm风扇。配合PM2.5 滤纸 做个空气净化器。但是后来发现,哎?哎?哎? 没考虑开关问题。要么电池耗尽,否则设备永远开机,外壳都打印出来了,算了,使用 其中两个针脚做为开关使用吧,断开电池的正极。插入排针以后导通电池正极就成了开关了。
一、模块选择
小安派-Cam-D200
(一开始想着用这个,但是,但是不知道为啥,串口读取数据就会跑飞,然后就换了)

Ai-M61-32S-Kit
(最后用了这个)

21VOC五合一空气质量检测模块

文档:
附件:21VOC(TVOC,甲醛,CO2,温湿度)模块说明书-V01.01.pdf
附件:五合一协议[5字节].docx
接线方案:
| 21VOC |
Ai-M61-32S |
GND |
GND |
3V3 |
3V3 |
RX |
IO25 |
TX |
IO26 |
1.3 寸屏幕

接线方案:
| 1.3寸屏幕 |
Ai-M61-32S |
GND |
GND |
VCC |
3V3 |
SCK |
IO13 |
SDA |
IO15 |
RES |
IO12 |
DC |
IO14 |
BLK |
悬空 |
按钮
接线方案:
| 按钮 |
Ai-M61-32S |
GND |
GND |
BUTTON |
IO18 |
通气孔
小孔微孔铝网格网菱形六角形孔铝板

外部接口
主要用于控制外部设备和作为开关使用。
接线方案:
| 4Pin |
Ai-M61-32S |
Pin1 |
Bin+ |
Pin2 |
Bout+ |
Pin3 |
IO预留 |
Pin4 |
IO预留 |
由于没有七哥和各位大佬那样自己画板的能力,所有都是用现有开发板做的。唯一的坏处就是不能 控制设备的大小和功耗。还有一些组件也不能按照自己的意愿去调整。
二、页面设计
对于检测设备页面也没必要那么华丽,简单构建了一下布局。

创建新项目

选择lvgl版本 V8.3.10

选择设备模板

选择应用模板

项目配置信息
面板类型改成Custom,名字自己起个就行,屏幕的我是240x240 的所以就设置成这个。然后点击创建。

这里就是简单的拖拖拽拽。

生成C代码

导出代码

页面到这里就设计好了。
三、代码
voc.h
#ifndef VOC_H
#define VOC_H
typedef enum {
SINGLE_CLICK,
DOUBLE_CLICK,
LONG_CLICK,
NONE_CLICK,
} click_t;
void voc_init(void);
float convert_temperature(float temperature);
void voc21Task (void *pvParameters);
void send_sensor_data(int voc, int ch2o, int eco2, int temperature, int humidity);
#endif
voc.c
#include "bflb_mtimer.h"
#include "board.h"
#include "bflb_uart.h"
#include "bflb_gpio.h"
#include "FreeRTOS.h"
#include "task.h"
#include "cJSON.h"
#include "math.h"
#include <FreeRTOS.h>
#define DBG_TAG "MAIN"
#include "log.h"
#include <task.h>
#include <queue.h>
#include "custom.h"
#define BUFFER_SIZE 1024*2
// uart串口读取 mov21
struct bflb_device_s *voc_uart;
// 缓存数据总数,默认mov21 数据,是12位码头0x2C
int BUFFER_LEN = 12;
// 当前数组下标
int voc_index = 0;
// 读取数据状态标记
int flag = 0;
// 换出数据缓存数组
uint8_t UART_RECEIVE_BUFFER[12];
custom_event_t custom_event = CUSTOM_EVENT_GET_PM25_DATA;
extern QueueHandle_t queue;
float convert_temperature(uint16_t raw) {
// 检查最高位是否为1(负数情况)
if (raw & 0x8000) {
return -(0xFFFF - raw) * 0.1f;
}
return raw * 0.1f;
}
static void uart_isr(int irq, void* arg)
{
uint32_t intstatus = bflb_uart_get_intstatus(voc_uart);
uint32_t rx_data_len = 0;
char* queue_buff = pvPortMalloc(64);
if (intstatus & UART_INTSTS_RX_FIFO) {
LOG_I("rx fifo\r\n");
while (bflb_uart_rxavailable(voc_uart)) {
int ch = bflb_uart_getchar(voc_uart);
if(voc_index < BUFFER_LEN){
// 防止 数据读取完成不需要再读取,同时防止index越界
if(ch!=-1 && voc_index < BUFFER_LEN){
if(flag == 1){
// 缓存数据
UART_RECEIVE_BUFFER[voc_index++] = ch;
}else{
// 读取到第二个 AA 改变标记 2 准备读取数据
if(ch == 0x2C){
flag = 1;
memset(UART_RECEIVE_BUFFER,0 , sizeof(UART_RECEIVE_BUFFER));
UART_RECEIVE_BUFFER[0] = ch;
voc_index = 1;
}else{
flag = 0;
}
}
}
}else{
LOG_I("0x%02x\r\n",ch);
}
}
}
if (intstatus & UART_INTSTS_RTO) {
LOG_I("rto");
bflb_uart_int_clear(voc_uart, UART_INTCLR_RTO);
LOG_I("uart int clear");
}
if (intstatus & UART_INTSTS_TX_FIFO) {
LOG_I("tx fifo\r\n");
for (uint8_t i = 0; i < 27; i++) {
bflb_uart_putchar(voc_uart, UART_RECEIVE_BUFFER[i]);
}
bflb_uart_txint_mask(voc_uart, true);
}
vPortFree(queue_buff);
}
void init_voc(void){
// 初始化uart
voc_uart = bflb_device_get_by_name("uart1");
struct bflb_device_s* gpio;
// 初始化uart配置参数
struct bflb_uart_config_s conf = {
.baudrate = 9600,
.data_bits = UART_DATA_BITS_8,
.stop_bits = UART_STOP_BITS_1,
.parity = UART_PARITY_NONE,
.flow_ctrl = UART_FLOWCTRL_NONE,
.rx_fifo_threshold = 7,
.tx_fifo_threshold = 7
};
gpio = bflb_device_get_by_name("gpio");
bflb_gpio_uart_init(gpio, GPIO_PIN_25, GPIO_UART_FUNC_UART1_TX);
bflb_gpio_uart_init(gpio, GPIO_PIN_26, GPIO_UART_FUNC_UART1_RX);
bflb_uart_init(voc_uart, &conf);
bflb_uart_txint_mask(voc_uart, false);
bflb_uart_rxint_mask(voc_uart, false);
bflb_irq_attach(voc_uart->irq_num, uart_isr, NULL);
bflb_irq_enable(voc_uart->irq_num);
}
void send_sensor_data(int voc, int ch2o, int eco2, int temperature, int humidity) {
char json_str[BUFFER_SIZE]; // 确保缓冲区足够大
memset(json_str, 0, sizeof(json_str));
printf(custom_event == CUSTOM_EVENT_GET_PM25_DATA ? "PM2.5" : "CH2O");
if(custom_event == CUSTOM_EVENT_GET_PM25_DATA){
snprintf(json_str, sizeof(json_str),
"{\"pm25\":{"
"\"pm25\":%d,"
"\"ch2o\":%d,"
"\"eco2\":%d,"
"\"temperature\":%.2f,"
"\"humidity\":%.2f"
"}}",
voc, ch2o, eco2, convert_temperature(temperature), humidity * 0.1f
);
}else if(custom_event == CUSTOM_EVENT_GET_CH2O_DATA){
snprintf(json_str, sizeof(json_str),
"{\"ch2o\":{"
"\"pm25\":%d,"
"\"ch2o\":%d,"
"\"eco2\":%d,"
"\"temperature\":%.2f,"
"\"humidity\":%.2f"
"}}",
voc, ch2o, eco2, convert_temperature(temperature), humidity * 0.1f
);
}
// 发送队列数据(确保 queue 已初始化)
if (queue != NULL) {
xQueueSend(queue, json_str, portMAX_DELAY);
printf("[DEBUG] Sending to queue: %s\n", json_str);
}
}
void voc21Task(void* pvParameters){
while (1)
{
// 读取到结束标记 处理数据
if(voc_index == BUFFER_LEN){
// 打印看下读取的数据
for (size_t i = 0; i < sizeof(UART_RECEIVE_BUFFER); i++) {
printf("0x%02x ", UART_RECEIVE_BUFFER[i]);
}
printf("\r\n");
uint32_t voc = UART_RECEIVE_BUFFER[1] <<8 | UART_RECEIVE_BUFFER[2];
printf("VOC空气质量: %d ug/m3", voc);
printf(" 、");
uint32_t ch2o = UART_RECEIVE_BUFFER[3] <<8 | UART_RECEIVE_BUFFER[4];
printf("甲醛:%d ug/m3", ch2o);
printf(" 、");
uint32_t eco2 = UART_RECEIVE_BUFFER[5] <<8 | UART_RECEIVE_BUFFER[6];
printf("eCO2(PPM): %d ppm", eco2);
printf(" 、");
uint16_t temperature = UART_RECEIVE_BUFFER[7] <<8 | UART_RECEIVE_BUFFER[8];
printf("温度:%.1f °C", convert_temperature(temperature));
printf(" 、");
uint32_t humidity = UART_RECEIVE_BUFFER[9] <<8 | UART_RECEIVE_BUFFER[10];
printf("湿度 %.1f %% RH", humidity*0.1f);
printf("\r\n");
send_sensor_data(voc, ch2o, eco2, temperature, humidity);
memset(UART_RECEIVE_BUFFER, 0, BUFFER_LEN);
// 处理完数据 恢复状态
flag = 0;
voc_index = 0;
}
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
四、FreeCAD 外壳设计

这个是通气孔那个铝网的磨具。手工裁剪的时候把它平的面放在铝网上,转着圈剪下来。然后再将铝网放在曲面这面,按压。这样就可以得到一个拱形的罩子了 O(∩_∩)O哈哈~。

效果
有个圆圆的凸起,效果还是蛮不错的。
五、组装调试
屏幕接线
再次

按钮\组装
最终效果/充电
3D模型外壳:
附件:3D外壳模型.zip
固件:
附件:voc_fan_bl616.zip
源码:
附件:voc_fan.zip
视频效果:
操作
默认 显示空气颗粒物 ,单击按钮,显示下一页面,甲醛检测页面。
双击返回 粉尘检测页面。其实这个设备是我想做的桌面空气净化器的主控设备。可以单独拿出来用。