❝说明:此文章提供了一种基于TencentOS-tiny的串口数据解析思路及实现,「感谢戴大神」最初写的源码,这种思路同样可以实现AT框架、基于串口的GPS数据解析等等。❞
本文使用的是攀藤PMSA003 PM2.5传感器。
PMSA003 是一款「基于激光散射原理的数字式通用颗粒物传感器」, 可连续采集并计算单位体积内空气中不同粒径的悬浮颗粒物个数,即颗粒物浓度分布,进而换算成为质量浓度,并以通用数字接口形式输出。本传感器可嵌入各种与空气中悬浮颗粒物浓度相关的仪器仪表或环境改善设备,为其提供及时准确的浓度数据。
本传感器采用激光散射原理。即:
令激光照射在空气中的悬浮颗粒物上产生散射,同时在某一特定角度收集散射光,得到散射光强度随时间变化的曲线。
进而微处理器基于米氏(MIE)理论的算法,得出颗粒物的等效粒径及单位体积内不同粒径的颗粒物数量。
❝
❞
主要输出结果为「单位体积内各浓度颗粒物质量以及个数」, 其中颗粒物个数的单位体积为 0.1L,质量浓度单位为:微克/立方米。
传感器上电后默认状态为主动输出,即「传感器主动向主机发送串行数据,时间间隔为 200~800ms」,空气中颗粒物浓度越高,时间间隔越短。
传感器还具备被动输出模式,如下:
其中指令字节和状态字节如下:
校验字为从特征字节开始所有字节累加和。
指令得到的应答为32个字节,和主动接收相同。
直接使用UBS转串口连接传感器的VCC、GND、TXD、RXD,打开串口助手,波特率9600bps/s,即可看到传感器周期性收到的数据:
每次接收到的数据总长度为「32字节」,每个数据意义如下:
① 「进入待机模式的指令」如下:
42 4D E4 00 00 01 73
执行之后传感器进入待机模式,风扇停止转动。
② 恢复正常模式的指令如下:
42 4D E4 00 01 01 74
执行之后传感器恢复正常工作。
③ 切换被动读数模式:
42 4D E1 00 00 01 70
收到的返回数据为:
42 4D 00 04 E1 00 01 74
执行之后传感器依然在正常工作,风扇正常转动,但是不会主动上报数据,需要手动读取。
④ 读取一次数据:
42 4D E2 00 00 01 71
收到传感器返回的32字节数据:
42 4D 00 1C 00 02 00 03 00 03 00 02 00 03 00 03 02 10 00 A1 00 17 00 00 00 00 00 00 97 00 02 1C
⑤ 切换回主动上报模式:
42 4D E1 00 01 01 71
串口逐个字节接收,缓存到chr fifo中 --> 解析任务读取缓存的数据进行解析校验 --> 取出其中26字节载荷发到邮箱 --> 邮箱接收有效数据并通过MQTT发送。
在上图所示的数据流中,整块的数据有3个:① 整个解析器所需要的任务控制块、信号量控制块、chr_fifo控制块可以封装为1个:
/* PM2.5 数据解析器控制块 */
typedef struct pm2d5_parser_control_st {
k_task_t parser_task; //解析器任务控制块
k_sem_t parser_rx_sem; //表示解析器从串口接收到数据
k_chr_fifo_t parser_rx_fifo; //存放解析器接收到的数
} pm2d5_parser_ctrl_t;
其中任务相关的大小配置、chr_fifo缓冲区的大小配置,可以用宏定义表示,方便修改:
/* pm2d5 parser config */
#define PM2D5_PARSER_TASK_STACK_SIZE 512
#define PM2D5_PARSER_TASK_PRIO 5
#define PM2D5_PARSER_BUFFER_SIZE 64
② 解析器从缓冲区读取出的传感器原始数据,可以封装为一个结构体,union是为了后续实现结构体无差异遍历:
/**
* @brief 解析出的PM2D5数据值
* @note 可以作为邮件发送给其他任务进行进一步处理
*/
typedef struct pm2d5_data_st {
uint16_t data1;
uint16_t data2;
uint16_t data3;
uint16_t data4;
uint16_t data5;
uint16_t data6;
uint16_t data7;
uint16_t data8;
uint16_t data9;
uint16_t data10;
uint16_t data11;
uint16_t data12;
uint16_t data13;
}pm2d5_data_t;
typedef union pm2d5_data_un {
uint16_t data[13];
pm2d5_data_t pm2d5_data;
} pm2d5_data_u;
③ 解析器从原始数据中解析出的数据,需要使用邮箱发送,也可以封装为一个结构体:
/**
* @brief 解析出的PM2D5数据值
* @note 可以作为邮件发送给其他任务进行进一步处理
*/
typedef struct pm2d5_data_st {
uint16_t data1;
uint16_t data2;
uint16_t data3;
uint16_t data4;
uint16_t data5;
uint16_t data6;
uint16_t data7;
uint16_t data8;
uint16_t data9;
uint16_t data10;
uint16_t data11;
uint16_t data12;
uint16_t data13;
}pm2d5_data_t;
typedef union pm2d5_data_un {
uint16_t data[13];
pm2d5_data_t pm2d5_data;
} pm2d5_data_u;
/**
* @brief 向PM2D5解析器中送入一个字节数据
* @param data 送入的数据
* @retval none
* @note 需要用户在串口中断函数中手动调用
*/
void pm2d5_parser_input_byte(uint8_t data)
{
if (tos_chr_fifo_push(&pm2d5_parser_ctrl.parser_rx_fifo, data) == K_ERR_NONE) {
/* 送入数据成功,释放信号量,计数 */
tos_sem_post(&pm2d5_parser_ctrl.parser_rx_sem);
}
}
只需要在串口中断处理函数中每次接收一个字节,然后调用此函数送入缓冲区即可。
解析任务负责等待信号量,从缓冲区中不停的读取数据进行校验、解析。
首先是从缓冲区中等待读取一个字节的函数:
/**
* @brief PM2D5解析器从chr fifo中取出一个字节数据
* @param none
* @retval 正常返回读取数据,错误返回-1
*/
static int pm2d5_parser_getchar(void)
{
uint8_t chr;
k_err_t err;
/* 永久等待信号量,信号量为空表示chr fifo中无数据 */
if (tos_sem_pend(&pm2d5_parser_ctrl.parser_rx_sem, TOS_TIME_FOREVER) != K_ERR_NONE) {
return -1;
}
/* 从chr fifo中取出数据 */
err = tos_chr_fifo_pop(&pm2d5_parser_ctrl.parser_rx_fifo, &chr);
return err == K_ERR_NONE ? chr : -1;
}
基于此函数可以编写出在解析到包头和帧数据长度后,从缓冲区中提取整个数据的函数:
/**
* @brief PM2D5读取传感器原始数据并解析
* @param void
* @retval 解析成功返回0,解析失败返回-1
*/
static int pm2d5_parser_read_raw_data(pm2d5_raw_data_u *pm2d5_raw_data, pm2d5_data_u *pm2d5_data)
{
int i;
uint8_t len_h,len_l;
uint16_t len;
uint16_t check_sum;
uint16_t check_sum_cal = 0x42 + 0x4d;
/* 读取并计算帧长度 */
len_h = pm2d5_parser_getchar();
len_l = pm2d5_parser_getchar();
len = (len_h << 8) | len_l;
if ( len != 0x001C) {
//非传感器值数据,清空缓存
for (i = 0; i < len; i++) {
pm2d5_parser_getchar();
}
return -1;
}
/* 读取传感器原始数据 */
for (i = 0; i < len; i++) {
pm2d5_raw_data->data[i] = pm2d5_parser_getchar();
}
/* 和校验 */
//通过数据计算和校验
check_sum_cal = check_sum_cal + len_h + len_l;
for (i = 0; i < len -2; i++) {
check_sum_cal += pm2d5_raw_data->data[i];
}
//协议中给出的和校验值
check_sum = (pm2d5_raw_data->pm2d5_raw_data.chk_sum_h << 8) + pm2d5_raw_data->pm2d5_raw_data.chk_sum_l;
if (check_sum_cal != check_sum) {
return -1;
}
/* 存储传感器值 */
for (i = 0; i < sizeof(pm2d5_data_t); i++) {
pm2d5_data->data[i] = pm2d5_raw_data->data[i];
}
return 0;
}
接着创建一个任务task,循环读取缓冲区中数据,如果读到包头,则调用整个原始数据读取函数,一次性全部读出,并进行校验得到有效值,得到有效值之后通过邮箱队列发送:
/**
* @brief PM2D5解析器任务
*/
static void pm2d5_parser_task_entry(void *arg)
{
int chr, last_chr = 0;
while (1) {
chr = pm2d5_parser_getchar();
if (chr < 0) {
printf("parser task get char fail!\r\n");
continue;
}
if (chr == 0x4d && last_chr == 0x42) {
/* 解析到包头 */
if (0 == pm2d5_parser_read_raw_data(&pm2d5_raw_data, &pm2d5_data)) {
/* 正常解析之后通过邮箱发送 */
tos_mail_q_post(&mail_q, &pm2d5_data, sizeof(pm2d5_data_t));
}
}
last_chr = chr;
}
}
最后编写创建解析器所需要的任务、信号量、chr_fifo的函数,「此函数由外部用户调用」:
/**
* @brief 初始化PM2D5解析器
* @param none
* @retval 全部创建成功返回0,任何一个创建失败则返回-1
*/
int pm2d5_parser_init(void)
{
k_err_t ret;
memset((pm2d5_parser_ctrl_t*)&pm2d5_parser_ctrl, 0, sizeof(pm2d5_parser_ctrl));
/* 创建 chr fifo */
ret = tos_chr_fifo_create(&pm2d5_parser_ctrl.parser_rx_fifo, pm2d5_parser_buffer, sizeof(pm2d5_parser_buffer));
if (ret != K_ERR_NONE) {
printf("pm2d5 parser chr fifo create fail, ret = %d\r\n", ret);
return -1;
}
/* 创建信号量 */
ret = tos_sem_create(&pm2d5_parser_ctrl.parser_rx_sem, 0);
if (ret != K_ERR_NONE) {
printf("pm2d5 parser_rx_sem create fail, ret = %d\r\n", ret);
return -1;
}
/* 创建线程 */
ret = tos_task_create(&pm2d5_parser_ctrl.parser_task, "pm2d5_parser_task",
pm2d5_parser_task_entry, NULL, PM2D5_PARSER_TASK_PRIO,
pm2d5_parser_task_stack,PM2D5_PARSER_TASK_STACK_SIZE,0);
if (ret != K_ERR_NONE) {
printf("pm2d5 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*)&pm2d5_value, &mail_size, TOS_TIME_FOREVER);
HAL_NVIC_DisableIRQ(USART3_4_IRQn);
//收到之后打印信息
printf("\r\n\r\n\r\n");
for (i = 0; i < 13; i++) {
printf("data[%d]:%d ug/m3\r\n", i+1, pm2d5_value.data[i]);
}
//数据上云
memset(payload, 0, 256);
snprintf(payload, sizeof(payload), "{\\\"method\\\":\\\"report\\\"\\,\\\"clientToken\\\":\\\"clientToken-145023f5-bc9b-4174-ba3b-430ba5956e5c\\\"\\,\\\"params\\\":{\\\"Pm2d5Value\\\":%d}}", pm2d5_value.pm2d5_data.data2);
printf("message publish: %s\n", payload);
if (tos_tf_module_mqtt_pub(pub_topic_name, QOS0, payload) != 0) {
printf("module mqtt pub fail\n");
//break;
} else {
printf("module mqtt pub success\n");
}
//每隔5s收取一次邮件并向云端发布
tos_sleep_ms(5000);
}
① 因为PM2.5传感器的数据每隔800ms就主动向串口发送一次,所以在串口初始化完毕之后关闭该串口的中断,不然单片机一直跑去解析数据了。
② 在需要数据的时候,先将该串口中断打开,然后阻塞等待邮件;
③ 串口中断使能之后,解析器完成解析后会发送邮件,唤醒之前等待该邮件的任务;
④ 数据上报之后,继续将串口中断关闭,避免浪费CPU。