External Interface Design

Two pins have been reserved. The external interface has four pins: originally, I intended one for VCC, one for GND, and the other two for device control—one for fan speed and one for on/off. The idea was: when air quality is poor, the fan turns on. I planned to use a 12cm PC fan, combined with PM2.5 filter paper, to make an air purifier.

However, I realized I hadn’t considered the switch. Without one, the device would stay on until the battery died. Since I had already printed the case, I repurposed two pins as a switch by breaking the positive line of the battery. Once the pin header is inserted, the positive line is connected, acting as a switch.

 

Hardware

Dev Board: Ai-Thinker Ai-M61-32S Kit

 

Sensor: 21VOC 5-in-1 Air Quality Module (TVOC, CH₂O, CO₂, Temp, Humidity)

 

21VOC (TVOC, Formaldehyde, CO2, Temp & Humidity) Module Manual - V01.01.pdf

21VOC

Ai-M61-32S

GND

GND

3V3

3V3

RX

IO25

TX

IO26

 

Wiring Schemes

21VOC to Ai-M61-32S

  • GND → GND
  • 3V3 → 3V3
  • RX → IO25
  • TX → IO26
  • GND → GND
  • BUTTON → IO18
  • Pin1 → Bin+
  • Pin2 → Bout+
  • Pin3 → IO reserved
  • Pin4 → IO reserved

Display: 1.3” TFT (240x240)

Button to Ai-M61-32S

Vent Holes: Small perforated aluminum mesh (diamond or hexagonal holes).

 

External Interface (4-Pin) Mainly used to control external devices or serve as a switch.

 

UI Design (LVGL)

The detection device UI doesn’t need to be fancy, so I built a simple layout.

  • Create a new project
  • Choose LVGL v8.3.10
  • Select device template
  • Select application template

In project config: set panel type to Custom and name it. My screen is 240x240, so I set that resolution.

 

Code

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 serial port reading mov21

struct bflb_device_s *voc_uart;

// Total number of cached data, default mov21 data length is 12 bytes starting with 0x2C

int BUFFER_LEN = 12;

// Current array index

int voc_index = 0;

// Data reading status flag

int flag = 0;

// Data buffer array

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) {

    // Check if the highest bit is 1 (negative case)

    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);

 

            ...
Read more »