基于RT-Thread和STM32的模块化串口接收和解析不定长协议数据
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
物联网嵌入式软件开发时,经常需要处理外部的各种协议数据,比modbus rtu数据,808协议数据,nmea协议数据,电子秤协议数据以及各种内部协议的数据等等。
按照传统方式,在串口中断中一个字节一个字节解析,虽然能实现,但不太优雅,维护也非常不方便。
如果一个项目中,需要系统支持多个协议,代码更难维护。
实现目标:
一、创建用例
参考rtthread官网,使用串口DMA方式,接收不定长数据的方法,做了一个demo,
系统配置如下:
rtthread:4.0.2版本:
STM32F427,串口6
可以不用cubemx,直接修改rtthread_setting和board.h的配置
创建串口模块:
#include <rtthread.h>
#include <rtdevice.h>
#include "sys_def.h"
#include "uart.h"
static rt_device_t uart_handle = RT_NULL;
/* 消息队列控制块 */
struct rt_messagequeue uart_rx_mq;
/**
* @brief 串口发送接口
* @param buff:待发送的数据
* len:待发送的数据长度
* @return 0:发送数据为空或者数据长度小于1,其他:实际发送的数据长度
*/
rt_int32_t uart_write(rt_uint8_t* data, rt_uint32_t len)
{
rt_size_t ret_size=0;
if ((data == NULL) || (len < 1))
{
return 0;
}
if (uart_handle == RT_NULL)
{
return -RT_ERROR;
}
// RS485_DIR_TX;
ret_size=rt_device_write(uart_handle, 0, data, len);
// if(ret_size>0)
// {
// RS485_DIR_RX;
// }
// else
// {
// rt_thread_mdelay(len+10);
// RS485_DIR_RX;
// }
return ret_size;
}
/**
* @brief 串口接收接口
* @param buff:存放读取的数据
* len:待读取的数据长度
* @return 0:发送数据为空或者数据长度小于1,其他:实际接收的数据长度
*/
rt_int32_t uart_read(rt_uint8_t* data, rt_uint32_t len)
{
rt_size_t ret_size=0;
if ((data == NULL) || (len < 1))
{
return 0;
}
if (uart_handle == RT_NULL)
{
return -RT_ERROR;
}
ret_size=rt_device_read(uart_handle, 0, data, len);
return ret_size;
}
/* 接收数据回调函数 */
static rt_err_t uart_recv_mq_callback(rt_device_t dev, rt_size_t size)
{
struct rx_msg msg;
rt_err_t result;
msg.dev = dev;
msg.size = size;
result = rt_mq_send(&uart_rx_mq, &msg, sizeof(msg));
if (result == -RT_EFULL)
{
/* 消息队列满 */
rt_kprintf("user com message queue full!\n");
}
return result;
}
/**
* @brief 初始化配置
* @param Baud:串口波特率
* @param parity:奇偶校验
* @return true:成功;false:失败
*/
rt_int32_t uart_init(rt_uint32_t baud,rt_uint8_t parity)
{
struct serial_configure port_arg = RT_SERIAL_CONFIG_DEFAULT;
if (uart_handle != RT_NULL)
{
uart_handle->open_flag &= ~RT_DEVICE_FLAG_INT_RX;
rt_device_close(uart_handle);
uart_handle = RT_NULL;
}
uart_handle = rt_device_find(uart_NAME);
if (uart_handle == RT_NULL)
{
rt_kprintf("uart_init == NULL\r\n");
return -RT_ERROR;
}
port_arg.baud_rate = baud; //default value
port_arg.parity = parity; //default value
if (port_arg.parity != PARITY_NONE)
{
port_arg.data_bits = DATA_BITS_9; //加了奇偶校验,数据位数要增加一位
}
else
{
port_arg.data_bits = DATA_BITS_8; //加了奇偶校验,数据位数要增加一位
}
port_arg.bufsz = 256; //一条消息最大长度
if (rt_device_control(uart_handle, RT_DEVICE_CTRL_CONFIG, &port_arg) != RT_EOK)
{
rt_kprintf("%s config fail\r\n", uart_NAME);
}
rt_err_t err_state = rt_device_open(uart_handle, RT_DEVICE_OFLAG_RDWR|RT_DEVICE_FLAG_DMA_RX);//配置为DMA_RX方式
if (err_state != RT_EOK)
{
rt_kprintf("%s open fail rt_err_t = %d\r\n", uart_NAME, err_state);
return -RT_ERROR;
}
/* 设置接收回调函数 */
rt_device_set_rx_indicate(uart_handle, uart_recv_mq_callback);
rt_kprintf("uart_init ok,baud=%d,parity=%d\r\n",baud,parity);
return RT_EOK;
}
static void uart_task(void* parameter)
{
struct rx_msg msg;
static char msg_pool[uart_BUFF_SIZE];
rt_err_t result;
rt_uint32_t rx_length;
rt_uint8_t* rx_buffer=RT_NULL;
rt_uint32_t baud=115200;
rt_uint8_t parity=0;
rt_kprintf("uart_task running\r\n");
//用户端串口初始化
uart_init(baud,parity);
/* 初始化消息队列 */
rt_mq_init(&uart_rx_mq, "rx_mq",
msg_pool, /* 存放消息的缓冲区 */
sizeof(struct rx_msg), /* 一条消息的最大长度 */
sizeof(msg_pool), /* 存放消息的缓冲区大小 */
RT_IPC_FLAG_FIFO); /* 如果有多个线程等待,按照先来先得到的方法分配消息 */
rx_buffer = (rt_uint8_t*)rt_malloc(uart_BUFF_SIZE);
while (1)
{
/* 从消息队列中读取消息 */
result = rt_mq_recv(&uart_rx_mq, &msg, sizeof(msg), RT_WAITING_FOREVER);
if (result == RT_EOK)
{
rt_memset(rx_buffer,0,uart_BUFF_SIZE);
rx_length=0;
if(msg.size<uart_BUFF_SIZE)
{
/* 从串口读取数据 */
rx_length = rt_device_read(msg.dev, 0, rx_buffer, msg.size);
rx_buffer[rx_length] = '\0';
uart_write(rx_buffer,rx_length);
}
}
}
}
void uart_task_start(void)
{
rt_thread_t tid = RT_NULL;
tid = rt_thread_create("uart",
uart_task,
RT_NULL,
uart_THREAD_STACK_SIZE,
uart_THREAD_PRIORITY,
uart_THREAD_TIMESLICE);
if (tid != RT_NULL)
{
rt_thread_startup(tid);
}
}
main中调用:
int main(void)
{
uart_task_start();
return RT_EOK;
}
二、测试用例
串口接收到数据后,立即发送回去,做回环测试,数据OK的,如下图所示:
三、加入协议测试
协议格式
随便找了一个协议,定义如下:
协议测试
协议适配层:
#include <rtthread.h>
#include <string.h>
#include "weight_pal.h"
#include "functionlib.h"
/**
* @brief 校验和计算
* @param
* buff:待计算的数据
* data_len:待计算的数据长度
* @return 计算得到的crc值
* @par 创建
*/
static rt_uint16_t checksum(rt_uint8_t* buff, rt_uint32_t data_len)
{
rt_uint32_t index = 0;
rt_uint16_t sum=0;
for (index = 0; index < data_len; index++)
{
sum=sum+buff[index];
}
return sum;
}
/**
* @brief 协议解析
* @param
in 数据输入
inlen 输入数据长度
out 数据输出,out->data 需指定存储空间
* @return 0:成功;其它:-1标识头不正确,2校验和不正确,3标识尾不正确,4解析的数据长度与输入数据长度不符
*/
rt_int32_t weight_decode(rt_uint8_t* in, rt_uint32_t inlen, void* out)
{
rt_uint16_t pos = 0;
rt_uint16_t checksum_cal =0;
s_weight* out_p = RT_NULL;
out_p = (s_weight* )out;
pos = 0;
if ((in == NULL) || (inlen < 5)||(in[pos]!=0xaa || in[pos+1]!=0x55))
{
rt_kprintf("weight_decode error -1 inlen=%d,in[pos]=%x\r\n",inlen,in[pos]);
return -1;
}
out_p->head = BufToU16(&in[pos]);
pos+=2;
out_p->cmd = in[pos];
pos += 1;
out_p->ctrl = in[pos];
pos += 1;
out_p->active = in[pos];
pos += 1;
rt_memcpy(out_p->data, &in[pos], 8);
pos += 8;
checksum_cal = checksum(&in[2], pos-2);
out_p->checksum = BufToU16(&in[pos]);
pos += 2;
if (out_p->checksum != checksum_cal)
{
rt_kprintf("weight checksum err\r\n");
return -3;
}
out_p->tail = BufToU16(&in[pos]);
pos += 2;
return 0; //成功
}
协议接口层:
rt_int32_t weight_recv(rt_uint8_t* in ,rt_uint32_t inlen)
{
s_weight* decodedata=RT_NULL;
rt_int32_t decode_ret=-1;
rt_int32_t retlen = 0;
static rt_uint32_t head_err_cnt=0;
static rt_uint32_t decode_err_cnt=0;
static rt_uint32_t decode_ok_cnt=0;
char temp[30]={0};
if(in[0]!=0xaa||in[1]!=0x55)
{
head_err_cnt++;
rt_sprintf(temp, "%s_%d\r\n","head_err",head_err_cnt);
user_com_write((rt_uint8_t*)temp,rt_strlen(temp));
return decode_ret;
}
decodedata = (s_weight*)rt_malloc(sizeof(s_weight));
if(decodedata!=RT_NULL)
{
rt_memset(decodedata, 0, sizeof(s_weight));
decode_ret=weight_decode(in,inlen,decodedata);
if(decode_ret==0)
{
decode_ok_cnt++;
rt_sprintf(temp, "%s_%d\r\n","decode_ok",decode_ok_cnt);
user_com_write((rt_uint8_t*)temp,rt_strlen(temp));
// weight_process(decodedata);
}
else
{
decode_err_cnt++;
rt_sprintf(temp, "%s_%d\r\n","decode_err",decode_err_cnt);
user_com_write((rt_uint8_t*)temp,rt_strlen(temp));
}
}
if(decodedata!=RT_NULL)
{
rt_free(decodedata);
decodedata=RT_NULL;
}
return decode_ret;
}
DMA方式,使用的是空闲中断,理论上会接收到完整的一帧数据,
若直接在uart_task的串口接收中加入协议接口层的接收接口处理数据,发现经常解析错误。
跟踪是数据包接收不完整。如下图所示:
17个字节数据,分成两次接收完整。所以有时候得不到完整数据,会解析出错。
怀疑是数据发送时,字节之间时间较差,超过了空闲中断检测时间。
三、加入fifo功能
参考了:
https://blog.csdn.net/qq_20553613/article/details/108367512
https://acuity.blog.csdn.net/article/details/78902689
两篇博文,改用fifo方式接收
fifo代码请参考第二篇博文
四、改造协议接口层
uart_task中负责写入,协议接口层单独建立任务,负责读和解析
#define WEIGHT_BUFF_SIZE 100
static rt_uint8_t weight_fifo_buf[512]={0};
static s_fifo_t weight_fifo={NULL};
rt_mutex_t weight_fifo_mutex=RT_NULL;
static rt_uint8_t weight_data[WEIGHT_BUFF_SIZE]={0};
void weight_data_fliter(void)
{
static rt_uint32_t offset=0;
static rt_uint32_t need_len=0;
rt_uint32_t read_len=0;
if(offset==0)
{
rt_memset(weight_data,0,sizeof(weight_data));
need_len=1;
}
read_len=fifo_read(&weight_fifo,&weight_data[offset],need_len);
if(read_len>0)
{
offset+=read_len;
if(weight_data[0]==0xaa)
{
need_len=1;
}
else
{
offset=0;
}
if(weight_data[1]!=0x00)
{
if(weight_data[1]==0x55)
{
need_len=17-offset;//固定长度协议,如果是变长的协议,可以在这里获取到len的位置,判断下一步需要多少字节
}
else
{
offset=0;
}
}
if(offset>=17)//已满足长度要求,处理协议
{
weight_recv(weight_data,offset);
offset=0;
}
}
}
void weight_fifo_write(rt_uint8_t* data, rt_size_t len)
{
fifo_write(&weight_fifo, (const uint8_t*)data, len);
}
static void weight_task(void* parameter)
{
weight_fifo_mutex=rt_mutex_create("w_fifo",RT_IPC_FLAG_FIFO);
if(weight_fifo_mutex!=RT_NULL)
{
fifo_register(&weight_fifo, weight_fifo_buf, sizeof(weight_fifo_buf),rt_mutex_take(weight_fifo_mutex, RT_WAITING_FOREVER),rt_mutex_release(weight_fifo_mutex));
}
while (1)
{
weight_data_fliter();
rt_thread_mdelay(1);
}
}
void weight_task_start(void)
{
rt_thread_t tid = RT_NULL;
tid = rt_thread_create("weight",
weight_task,
RT_NULL,
WEIGHT_THREAD_STACK_SIZE,
WEIGHT_THREAD_PRIORITY,
WEIGHT_THREAD_TIMESLICE);
if (tid != RT_NULL)
{
rt_thread_startup(tid);
}
}
其中weight_data_fliter()接口是根据协议格式,获取指定长度数据,直到找到len的位置。然后根据len获取后续数据
四、测试结果
采用fifo方式后,串口模块负责写,协议模块负责解析,测试无数据遗漏。
总结
采用此方法,串口可以解析变长数据,不用在中断中接收和解析数据,也能实现模块化开发,移植和可维护性强。
作者:boatarmy