基于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.youkuaiyun.com/qq_20553613/article/details/108367512
https://acuity.blog.youkuaiyun.com/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方式后,串口模块负责写,协议模块负责解析,测试无数据遗漏。

总结

采用此方法,串口可以解析变长数据,不用在中断中接收和解析数据,也能实现模块化开发,移植和可维护性强。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

boatarmy

1分,1毛都是对分享的鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值