基于TencentOS-tiny实现甲醛传感器(英国达特 WZ-S)数据解析思路及实现

1. 甲醛传感器

本文使用的是英国达特DART公司生产的 WZ-S型 甲醛检测传感器。

WZ-S利用电化学原理对空气中存在的CH2O进行探测,直接将空气中的甲醛气体含量转换为浓度值,并使用数字方式输出,方便使用。

1.1. 引脚说明

1.2. 技术指标

1.3. 输出数据

传感器上电后默认状态为主动输出,即传感器主动向主机发送串行数据,时间间隔为 1s

2. 使用USB转串口查看输出数据

2.1. 传感器主动模式上报

直接使用UBS转串口连接传感器的VCC、GND、TXD、RXD,打开串口助手,波特率9600bps/s,即可看到传感器周期性收到的数据:

每次接收到的数据总长度为9字节,每个数据意义如下:

2.2. 数据转换

传感器上报的数据中:

① 将气体浓度高位转换为十进制,将气体浓度低位转换为10进制;

② 气体浓度值 = 气体浓度高位值 * 256 + 气体浓度地位值(单位:ppb);

③ 单位换算:1ppb = 1000ppm;

④ 单位换算:1ppm = 1mg/m3;

2.3. 数据校验

最后一个字节是校验值,采用求和校验规则:取接收协议的 1\2\3\4\5\6\7 的和,然后取反+1。

比如上图中测到的一次数据:

FF 17 04 00 00 97 07 D0 77

首先计算和:

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

计算和取反为:

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

加1为:

E76 + 1 = E77

只输出一个字节的值,为77,校验正确。

3. 使用TencentOS-tiny操作系统解析

3.1. 解析思路

串口逐个字节接收,缓存到chr fifo中 --> 解析任务读取缓存的数据进行解析校验 --> 取出其中2字节有效载荷发到邮箱 --> 邮箱接收有效数据并通过MQTT发送。

图片中出现pm2_5处,用ch20代替即可。

3.2. 数据结构抽象

在上图所示的数据流中,整块的数据有3个:
① 整个解析器所需要的任务控制块、信号量控制块、chr_fifo控制块可以封装为1个:

/* 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;

其中任务相关的大小配置、chr_fifo缓冲区的大小配置,可以用宏定义表示,方便修改:

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

② 解析器从缓冲区读取出的传感器原始数据,可以封装为一个结构体:

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

3.3. 逐个字节送入缓冲区

/**
 * @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);
    }
}

只需要在串口中断处理函数中每次接收一个字节,然后调用此函数送入缓冲区即可。

3.4. 解析任务实现

解析任务负责等待信号量,从缓冲区中不停的读取数据进行校验、解析。

首先是从缓冲区中等待读取一个字节的函数:

/**
 * @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;
}

基于此函数可以编写出在解析到包头和帧数据长度后,从缓冲区中提取整个数据的函数:

/**
 * @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;
}

接着创建一个任务task,循环读取缓冲区中数据,如果读到包头,则调用整个原始数据读取函数,一次性全部读出,并进行校验得到有效值,得到有效值之后通过邮箱队列发送:

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

最后编写创建解析器所需要的任务、信号量、chr_fifo的函数,此函数由外部用户调用

/**
 * @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使用邮件接收并发布到云服务器

mqtt task之前的一堆初始化代码省略,只要while(1)中的业务逻辑就够了:

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

① 因为CH20传感器的数据每隔 1s 就主动向串口发送一次,所以在串口初始化完毕之后关闭该串口的中断,不然单片机一直跑去解析数据了。

② 在需要数据的时候,先将该串口中断打开,然后阻塞等待邮件;

③ 串口中断使能之后,解析器完成解析后会发送邮件,唤醒之前等待该邮件的任务;

④ 数据上报之后,继续将串口中断关闭,避免浪费CPU。

接收更多精彩文章及资源推送,欢迎订阅我的微信公众号:『mculover666』。

猜你喜欢

转载自blog.csdn.net/Mculover666/article/details/108428209