基于TencentOS-tiny实现PM2.5传感器(攀藤PMSA003)数据解析思路及实现
❝说明:此文章提供了一种基于TencentOS-tiny的串口数据解析思路及实现,「感谢戴大神」最初写的源码,这种思路同样可以实现AT框架、基于串口的GPS数据解析等等。❞
1. PM2.5传感器
本文使用的是攀藤PMSA003 PM2.5传感器。
PMSA003 是一款「基于激光散射原理的数字式通用颗粒物传感器」, 可连续采集并计算单位体积内空气中不同粒径的悬浮颗粒物个数,即颗粒物浓度分布,进而换算成为质量浓度,并以通用数字接口形式输出。本传感器可嵌入各种与空气中悬浮颗粒物浓度相关的仪器仪表或环境改善设备,为其提供及时准确的浓度数据。
1.1. 测量原理
本传感器采用激光散射原理。即:
令激光照射在空气中的悬浮颗粒物上产生散射,同时在某一特定角度收集散射光,得到散射光强度随时间变化的曲线。
进而微处理器基于米氏(MIE)理论的算法,得出颗粒物的等效粒径及单位体积内不同粒径的颗粒物数量。
1.2. 技术指标
1.3. 引脚定义
❝
- 「PMSA003 需要 5V 供电,这是因为风机需要 5V 驱动。但其他数据通讯和控制管脚均需要 3.3V 作为高电平」。因此与之连接通讯的主板 MCU 应为 3.3V供电。如果主板 MCU 为 5V 供电,则在通讯线(TXD、 RXD)和控制线(SET、 RESET)上应当加入电平转换芯片或电路。
- SET 和 RESET 内部有上拉电阻,「如果不使用,则应悬空」。
- PIN6 和 PIN8 为程序内部调试用,「应用电路中应使其悬空」。
❞
1.4. 输出数据
主要输出结果为「单位体积内各浓度颗粒物质量以及个数」, 其中颗粒物个数的单位体积为 0.1L,质量浓度单位为:微克/立方米。
传感器上电后默认状态为主动输出,即「传感器主动向主机发送串行数据,时间间隔为 200~800ms」,空气中颗粒物浓度越高,时间间隔越短。
传感器还具备被动输出模式,如下:
其中指令字节和状态字节如下:
校验字为从特征字节开始所有字节累加和。
指令得到的应答为32个字节,和主动接收相同。
2. 使用USB转串口查看输出数据
2.1. 传感器为主动模式
直接使用UBS转串口连接传感器的VCC、GND、TXD、RXD,打开串口助手,波特率9600bps/s,即可看到传感器周期性收到的数据:
每次接收到的数据总长度为「32字节」,每个数据意义如下:
2.2. 传感器为被动模式
① 「进入待机模式的指令」如下:
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
3. 使用TencentOS-tiny操作系统解析
3.1. 解析思路
串口逐个字节接收,缓存到chr fifo中 --> 解析任务读取缓存的数据进行解析校验 --> 取出其中26字节载荷发到邮箱 --> 邮箱接收有效数据并通过MQTT发送。
3.2. 数据结构抽象
在上图所示的数据流中,整块的数据有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;
3.3. 逐个字节送入缓冲区
/**
* @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);
}
}
只需要在串口中断处理函数中每次接收一个字节,然后调用此函数送入缓冲区即可。
3.4. 解析任务实现
解析任务负责等待信号量,从缓冲区中不停的读取数据进行校验、解析。
首先是从缓冲区中等待读取一个字节的函数:
/**
* @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!rn");
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 = %drn", 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 = %drn", 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 = %drn", 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*)&pm2d5_value, &mail_size, TOS_TIME_FOREVER);
HAL_NVIC_DisableIRQ(USART3_4_IRQn);
//收到之后打印信息
printf("rnrnrn");
for (i = 0; i < 13; i++) {
printf("data[%d]:%d ug/m3rn", 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: %sn", payload);
if (tos_tf_module_mqtt_pub(pub_topic_name, QOS0, payload) != 0) {
printf("module mqtt pub failn");
//break;
} else {
printf("module mqtt pub successn");
}
//每隔5s收取一次邮件并向云端发布
tos_sleep_ms(5000);
}
① 因为PM2.5传感器的数据每隔800ms就主动向串口发送一次,所以在串口初始化完毕之后关闭该串口的中断,不然单片机一直跑去解析数据了。
② 在需要数据的时候,先将该串口中断打开,然后阻塞等待邮件;
③ 串口中断使能之后,解析器完成解析后会发送邮件,唤醒之前等待该邮件的任务;
④ 数据上报之后,继续将串口中断关闭,避免浪费CPU。
- springmvc框架开发常用的注解总结
- 详谈Struts2
- 持久层框架之MyBatis
- 总结hibernate框架的常用检索方式
- 互联网项目架构之基于服务的分布式架构
- 会优化,你真的会优化吗?其实你可能真的缺少一份理解【数据库篇】
- 用户登录安全框架shiro—用户的认证和授权(一)
- 第一道防线__SpringMVC配置拦截器
- Web层框架对网站中所有异常的统一处理
- Spring MVC__自定义日期类型转换器
- 解决在控制层springmvc框架发出的400状态的错误
- 解决springmvc在单纯返回一个字符串对象时所出现的乱码情况(极速版)
- MySQL日志文件之错误日志和慢查询日志详解
- 采用HTML5之"data-"机制自由提供数据
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- C++核心准则E.31:正确排列catch子句
- VBA解析复合文档07——Parse参数IReadWrite
- 再谈分布式服务架构
- VBA解析复合文档08——应用-解析Thumbs.db
- pyecharts 嵌入 PyQt5
- CS学习笔记 | 15、枚举的命令和方法
- WFD_RTSP交互包分析
- Linux阅码场 - Linux内核月报(2020年07月)
- WifiDisplay(Miracast)技术原理及实现
- Java常用设计模式--观察者模式(Observer Pattern)
- Java常用设计模式--适配器模式(Adapter Pattern)
- Java常用设计模式--装饰器模式(Decorator Pattern)
- Java常用设计模式-单例模式(Singleton Pattern)
- Java常用设计模式--三种工厂模式之简单工厂模式(Simple Factory)
- Java常用设计模式--三种工厂模式之工厂模式(Factory Pattern)