Based on TencentOS-tiny to realize formaldehyde sensor data analysis thought and realization (UK Dart WZ-S)

1. Formaldehyde sensor

This article uses the WZ-S formaldehyde detection sensor produced by DART Company in the United Kingdom.

WZ-S uses electrochemical principles to detect CH2O in the air, directly converts the content of formaldehyde gas in the air into a concentration value, and outputs it in a digital way, which is convenient to use.

1.1. Pin description

1.2. Technical indicators

1.3. Output data

After the sensor is powered on, the default state is active output, that is, the sensor actively sends serial data to the host with a time interval of 1s .

2. Use USB to serial port to view output data

2.1. Sensor active mode reporting

Use UBS to serial port to connect VCC, GND, TXD, RXD of the sensor directly, open the serial port assistant, the baud rate is 9600bps/s , you can see the data received periodically by the sensor: the

total length of the data received each time is 9 words Section , the meaning of each data is as follows:

2.2. Data Conversion

Among the data reported by the sensor:

① Convert the high level of gas concentration to decimal system, and convert the low level of gas concentration to decimal system;

② Gas concentration value = high gas concentration value * 256 + gas concentration status value (unit: ppb);

③ Unit conversion: 1ppb = 1000ppm;

④ Unit conversion: 1ppm = 1mg/m3;

2.3. Data verification

The last byte is the check value, and the sum check rule is adopted: take the sum of 1\2\3\4\5\6\7 of the receiving protocol, and then take the inverse +1.

For example, the data measured in the above figure:

FF 17 04 00 00 97 07 D0 77

First calculate the sum:

17 + 04+ 00 + 00 + 97 + 07+ D0 = 189

The calculation and negation are:

~189 = E76//~0001 1000 1001 = 1110 0111 0110

Add 1 to:

E76 + 1 = E77

Only output the value of one byte, which is 77, which is correct.

3. Use TencentOS-tiny operating system analysis

3.1. Analysis of ideas

The serial port receives byte by byte and buffers it in chr fifo --> the parsing task reads the buffered data for analysis and verification --> takes out the 2-byte payload and sends it to the mailbox --> the mailbox receives valid data and sends it through MQTT.

If pm2_5 appears in the picture, use ch20 instead.

3.2. Data structure abstraction

In the data flow shown in the figure above, there are three data in the whole block:
① The task control block, semaphore control block, and chr_fifo control block required by the entire parser can be encapsulated into one:

/* CH20 数据解析器控制块 */
typedef struct CH20_parser_control_st {
    
    
    k_task_t     parser_task;       //解析器任务控制块
    
    k_sem_t      parser_rx_sem;     //表示解析器从串口接收到数据
    k_chr_fifo_t parser_rx_fifo;    //存放解析器接收到的数据
} ch20_parser_ctrl_t;

Among them, the task-related size configuration and the size configuration of the chr_fifo buffer can be expressed by macro definitions for easy modification:

/* CH20 parser config */
#define CH20_PARSER_TASK_STACK_SIZE    512
#define CH20_PARSER_TASK_PRIO          5
#define CH20_PARSER_BUFFER_SIZE        32

② The raw sensor data read by the parser from the buffer can be encapsulated into a structure:

/**
 * @brief   解析出的CH20数据值
 * @note    可以作为邮件发送给其他任务进行进一步处理
 * @param   
 */
typedef struct ch20_data_st {
    
    
    uint16_t    data;
} ch20_data_t;

3.3. Byte by byte into the buffer

/**
 * @brief   向ch20解析器中送入一个字节数据
 * @param   data  送入的数据
 * @retval  none
 * @note    需要用户在串口中断函数中手动调用
*/
void ch20_parser_input_byte(uint8_t data)
{
    
    
    if (tos_chr_fifo_push(&ch20_parser_ctrl.parser_rx_fifo, data) == K_ERR_NONE) {
    
    
        /* 送入数据成功,释放信号量,计数 */
        tos_sem_post(&ch20_parser_ctrl.parser_rx_sem);
    }
}

Only need to receive one byte each time in the serial port interrupt processing function, and then call this function to send it to the buffer.

3.4. Parsing task realization

The parsing task is responsible for waiting for the semaphore, and continuously reading data from the buffer for verification and analysis.

The first is a function that waits to read a byte from the buffer:

/**
 * @brief   ch20解析器从chr fifo中取出一个字节数据
 * @param   none
 * @retval  正常返回读取数据,错误返回-1
*/
static int ch20_parser_getchar(void)
{
    
    
    uint8_t chr;
    k_err_t err;
    
    /* 永久等待信号量,信号量为空表示chr fifo中无数据 */
    if (tos_sem_pend(&ch20_parser_ctrl.parser_rx_sem, TOS_TIME_FOREVER) != K_ERR_NONE) {
    
    
        return -1;
    }
    
    /* 从chr fifo中取出数据 */
    err = tos_chr_fifo_pop(&ch20_parser_ctrl.parser_rx_fifo, &chr);

    return err == K_ERR_NONE ? chr : -1;
}

Based on this function, you can write a function that extracts the entire data from the buffer after parsing the packet header and frame data length:

/**
 * @brief   ch20读取传感器原始数据并解析
 * @param   void
 * @retval  解析成功返回0,解析失败返回-1
*/
static int ch20_parser_read_raw_data(ch20_data_t *ch20_data)
{
    
    
    uint8_t data;
    uint8_t data_h, data_l;
    uint8_t check_sum_cal = 0x17;
   
    /* 读取气体浓度单位 */
    data = ch20_parser_getchar();
    if (data != 0x04) {
    
    
        return -1;
    }
    CH20_DEBUG_LOG("--->[%#02x]\r\n", data);
    check_sum_cal += data;
    
    /* 读取小数位数 */
    data = ch20_parser_getchar();
    if (data != 0x00) {
    
    
        return -1;
    }
    CH20_DEBUG_LOG("--->[%#02x]\r\n", data);
    check_sum_cal += data;    
    
    /* 读取气体浓度高位 */
    data = ch20_parser_getchar();
    if (data == 0xFF) {
    
    
        return -1;
    }
    CH20_DEBUG_LOG("--->[%#02x]\r\n", data);
    data_h = data;
    check_sum_cal += data;
    
    /* 读取气体浓度低位 */
    data = ch20_parser_getchar();
    if (data == 0xFF) {
    
    
        return -1;
    }
    CH20_DEBUG_LOG("--->[%#02x]\r\n", data);
    data_l = data;
    check_sum_cal += data;
    
    /* 读取满量程高位 */
    data = ch20_parser_getchar();
    if (data != 0x07) {
    
    
        return -1;
    }
    CH20_DEBUG_LOG("--->[%#02x]\r\n", data);
    check_sum_cal += data;
    
    /* 读取满量程低位 */
    data = ch20_parser_getchar();
    if (data != 0xD0) {
    
    
        return -1;
    }
    CH20_DEBUG_LOG("--->[%#02x]\r\n", data);
    check_sum_cal += data;

    /* 和校验 */
    data = ch20_parser_getchar();
    CH20_DEBUG_LOG("--->[%#02x]\r\n", data);
    check_sum_cal = ~(check_sum_cal) + 1;
    CH20_DEBUG_LOG("check_sum_cal is 0x%02x \r\n", check_sum_cal);
    if (check_sum_cal != data) {
    
    
        return -1;
    }
    
    /* 存储数据 */
    ch20_data->data = (data_h << 8) + data_l;
    CH20_DEBUG_LOG("ch20_data->data is 0x%04x\r\n", ch20_data->data);
    
    return 0;
}

Then create a task to read the data in the buffer cyclically. If the header of the packet is read, the entire original data reading function is called, read all at once, and verify the valid value, and then send it through the mailbox queue after the valid value is obtained :

extern k_mail_q_t mail_q;
ch20_data_t     ch20_data;

/**
 * @brief   ch20解析器任务
*/
static void ch20_parser_task_entry(void *arg)
{
    
    
    int chr, last_chr = 0;
    
    while (1) {
    
    
       
        chr = ch20_parser_getchar();
        if (chr < 0) {
    
    
            printf("parser task get char fail!\r\n");
            continue;
        }
        
        if (chr == 0x17 && last_chr == 0xFF) {
    
    
            /* 解析到包头 */
            if (0 ==  ch20_parser_read_raw_data(&ch20_data)) {
    
    
                /* 正常解析之后通过邮箱发送 */
                tos_mail_q_post(&mail_q, &ch20_data, sizeof(ch20_data_t));
            }
        }
        
        last_chr = chr;
    }
}

Finally, write the tasks, semaphores, and chr_fifo functions needed to create the parser. This function is called by external users :

/**
 * @brief   初始化ch20解析器
 * @param   none
 * @retval  全部创建成功返回0,任何一个创建失败则返回-1
*/
int ch20_parser_init(void)
{
    
    
    k_err_t ret;
    
    memset((ch20_parser_ctrl_t*)&ch20_parser_ctrl, 0, sizeof(ch20_parser_ctrl));
    
    /* 创建 chr fifo */
    ret = tos_chr_fifo_create(&ch20_parser_ctrl.parser_rx_fifo, ch20_parser_buffer, sizeof(ch20_parser_buffer));
    if (ret != K_ERR_NONE) {
    
    
        printf("ch20 parser chr fifo create fail, ret = %d\r\n", ret);
        return -1;
    }
    
    /* 创建信号量 */
    ret = tos_sem_create(&ch20_parser_ctrl.parser_rx_sem, 0);
    if (ret != K_ERR_NONE) {
    
    
        printf("ch20 parser_rx_sem create fail, ret = %d\r\n", ret);
        return -1;
    }
    
    /* 创建线程 */
    ret = tos_task_create(&ch20_parser_ctrl.parser_task, "ch20_parser_task", 
                          ch20_parser_task_entry, NULL, CH20_PARSER_TASK_PRIO,
                          ch20_parser_task_stack,CH20_PARSER_TASK_STACK_SIZE,0);
    if (ret != K_ERR_NONE) {
    
    
        printf("ch20 parser task create fail, ret = %d\r\n", ret);
        return -1;
    }

    return 0;
}

3.5. MQTT uses mail to receive and publish to cloud server

A bunch of initialization codes before mqtt task are omitted, as long as the business logic in while(1) is enough:

 while (1) {
    
    
        /* 通过接收邮件来读取数据 */
        HAL_NVIC_EnableIRQ(USART3_4_IRQn);
        tos_mail_q_pend(&mail_q, (uint8_t*)&ch20_value, &mail_size, TOS_TIME_FOREVER);
        HAL_NVIC_DisableIRQ(USART3_4_IRQn);
        
        /* 接收到之后打印信息 */
        ch20_ppm_value = ch20_value.data / 1000.0;
        printf("ch20 value: %.3f\r\n", ch20_ppm_value);
        
        /* OLED显示值 */
        sprintf(ch20_ppm_str, "%.3f ppm(mg/m3)", ch20_ppm_value);
        OLED_ShowString(0, 2, (uint8_t*)ch20_ppm_str, 16);
        
        /* 上报值 */
        memset(payload, 0, sizeof(payload));
        snprintf(payload, sizeof(payload), REPORT_DATA_TEMPLATE, ch20_ppm_value);
        
        if (lightness > 100) {
    
    
            lightness = 0;
        }
        
        if (tos_tf_module_mqtt_pub(report_topic_name, QOS0, payload) != 0) {
    
    
            printf("module mqtt pub fail\n");
            break;
        } else {
    
    
            printf("module mqtt pub success\n");
        }
        
        tos_sleep_ms(5000);
    }

① Because the data of the CH20 sensor is actively sent to the serial port every 1s, the interrupt of the serial port is turned off after the serial port is initialized, or the MCU keeps running to parse the data.

② When you need data, first open the serial port interrupt, and then block waiting for mail;

③ After the serial port interrupt is enabled, the parser will send an email after the analysis is completed, and the task of waiting for the email before waking up;

④ After the data is reported, continue to close the serial port interrupt to avoid wasting the CPU.

To receive more exciting articles and resource pushes, please subscribe to my WeChat public account: "mculover666".

Guess you like

Origin blog.csdn.net/Mculover666/article/details/108428209
Recommended