本文使用的是英国达特DART公司生产的 WZ-S型 甲醛检测传感器。
WZ-S利用电化学原理对空气中存在的CH2O进行探测,直接将空气中的甲醛气体含量转换为浓度值,并使用数字方式输出,方便使用。
传感器上电后默认状态为主动输出,即传感器主动向主机发送串行数据,时间间隔为 1s。
直接使用UBS转串口连接传感器的VCC、GND、TXD、RXD,打开串口助手,波特率9600bps/s,即可看到传感器周期性收到的数据:
每次接收到的数据总长度为9字节,每个数据意义如下:
传感器上报的数据中:
① 将气体浓度高位转换为十进制,将气体浓度低位转换为10进制;
② 气体浓度值 = 气体浓度高位值 * 256 + 气体浓度地位值(单位:ppb);
③ 单位换算:1ppb = 1000ppm;
④ 单位换算:1ppm = 1mg/m3;
最后一个字节是校验值,采用求和校验规则:取接收协议的 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,校验正确。
串口逐个字节接收,缓存到chr fifo中 --> 解析任务读取缓存的数据进行解析校验 --> 取出其中2字节有效载荷发到邮箱 --> 邮箱接收有效数据并通过MQTT发送。
图片中出现pm2_5处,用ch20代替即可。
在上图所示的数据流中,整块的数据有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;
/**
* @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);
}
}
只需要在串口中断处理函数中每次接收一个字节,然后调用此函数送入缓冲区即可。
解析任务负责等待信号量,从缓冲区中不停的读取数据进行校验、解析。
首先是从缓冲区中等待读取一个字节的函数:
/**
* @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;
}
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』。