发帖
18 1 1

【电子DIY作品】甲醛颗粒物温湿度等多合一检测设备(更新)

WT_0213
论坛元老

130

主题

1913

回帖

1万

积分

论坛元老

勤劳的打工人

积分
17964
电子DIY 3143 18 2025-9-7 18:21:42
[i=s] 本帖最后由 WT_0213 于 2025-9-9 23:26 编辑 [/i]

对之前版本做了一些优化,更新了以下功能:

粉尘PM2.5检测、甲醛检测、二氧化碳浓度检测

增加了 数值单位,如PM2.5与甲醛浓度都是ug/m³,二氧化碳浓度为ppm。

操作优化:

单击 显示下一个功能页面,双击显示上一个功能页面。

按钮操作代码优化,目前可稳定执行以上操作。


家里刚装修不就总是觉得害怕有甲醛什么的影响健康。市场上虽然有很多这样的检测设备,开始想买来着后来又觉得功能太过单一,就想着自己能不能 做一个呢。本着 DIY的精神与实用至上的原则并且有板子有模块那不是轻轻松松。

cgi-bin_mmwebwx-bin_webwxgetmsgimg_&MsgID=3358910217666972767&skey=@crypt_3423.jpg

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


一、模块选择

小安派-Cam-D200

(一开始想着用这个,但是,但是不知道为啥,串口读取数据就会跑飞,然后就换了)

Snipaste_2025-09-07_17-28-04.png

Ai-M61-32S-Kit

(最后用了这个)

Snipaste_2025-09-07_17-32-03.png

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

Snipaste_2025-09-07_17-27-01.png

文档:
upload 附件:21VOC(TVOC,甲醛,CO2,温湿度)模块说明书-V01.01.pdf

upload 附件:五合一协议[5字节].docx

接线方案:

21VOC Ai-M61-32S
GND GND
3V3 3V3
RX IO25
TX IO26
1.3 寸屏幕

Snipaste_2025-09-07_17-34-24.png

接线方案:

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
通气孔

小孔微孔铝网格网菱形六角形孔铝板

Snipaste_2025-09-07_18-09-37.png

外部接口

主要用于控制外部设备和作为开关使用。

接线方案:

4Pin Ai-M61-32S
Pin1 Bin+
Pin2 Bout+
Pin3 IO预留
Pin4 IO预留

由于没有七哥和各位大佬那样自己画板的能力,所有都是用现有开发板做的。唯一的坏处就是不能 控制设备的大小和功耗。还有一些组件也不能按照自己的意愿去调整。


二、页面设计

对于检测设备页面也没必要那么华丽,简单构建了一下布局。

Snipaste_2025-09-07_17-03-53.png

创建新项目

Snipaste_2025-09-07_17-04-09.png

选择lvgl版本 V8.3.10

Snipaste_2025-09-07_17-04-50.png

选择设备模板

Snipaste_2025-09-07_17-05-18.png

选择应用模板

Snipaste_2025-09-07_17-06-39.png

项目配置信息

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

Snipaste_2025-09-07_17-03-30.png

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

Snipaste_2025-09-07_17-07-50.png

生成C代码

Snipaste_2025-09-07_17-08-09.png

导出代码

Snipaste_2025-09-07_17-08-22.png

页面到这里就设计好了。


三、代码

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 外壳设计

Snipaste_2025-09-07_17-46-02.png

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

Snipaste_2025-09-07_18-27-02.png

效果

有个圆圆的凸起,效果还是蛮不错的。


五、组装调试

屏幕接线

再次

cgi-bin_mmwebwx-bin_webwxgetmsgimg_&MsgID=6900917903122512534&skey=@crypt_3423.jpg

按钮\组装

最终效果/充电

3D模型外壳:

upload 附件:3D外壳模型.zip

固件:

upload 附件:voc_fan_bl616.zip

源码:

upload 附件:voc_fan.zip

视频效果:

操作

默认 显示空气颗粒物 ,单击按钮,显示下一页面,甲醛检测页面。
双击返回 粉尘检测页面。其实这个设备是我想做的桌面空气净化器的主控设备。可以单独拿出来用。

──── 1人觉得很赞 ────

使用道具 举报

2025-9-7 20:52:45
厉害
2025-9-7 22:37:55
哇。这个好玩😁
2025-9-8 08:15:35
我也是住的刚装修的,幸福哥寄来我测测准不准😁
2025-9-8 09:19:26
幸福哥一如既往支持工作~
2025-9-8 10:03:52
iiv 发表于 2025-9-7 22:37
哇。这个好玩😁

非常羡慕七哥自己可以画板,我这个电路比较糙。😂
2025-9-8 10:09:29
bzhou830 发表于 2025-9-8 08:15
我也是住的刚装修的,幸福哥寄来我测测准不准😁

来来来,我用油漆测了下,贴近油漆桶会爆表。平时屋子里面不知道时没有,还是检测不到。一直是0。
2025-9-8 10:10:30
爱笑 发表于 2025-9-8 09:19
幸福哥一如既往支持工作~

那必须的 😁
2025-9-8 10:12:34
WT_0213 发表于 2025-9-8 10:09
来来来,我用油漆测了下,贴近油漆桶会爆表。平时屋子里面不知道时没有,还是检测不到。一直是0。 ...

那应该可靠, 21VOC五合一空气质量检测模块推荐个
2025-9-8 10:12:58

😄 做着玩,顺便看看甲醛。这个模块说是寿命就3年。
您需要登录后才可以回帖 立即登录
高级模式
12下一页
统计信息
  • 会员数: 30101 个
  • 话题数: 44217 篇