基于RT-Thread和STM32的模块化串口接收和解析不定长协议数据

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、创建用例
  • 二、测试用例
  • 三、加入协议测试
  • 协议格式
  • 协议测试
  • 三、加入fifo功能
  • 四、改造协议接口层
  • 四、测试结果
  • 总结

  • 前言

    物联网嵌入式软件开发时,经常需要处理外部的各种协议数据,比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

    物联沃分享整理
    物联沃-IOTWORD物联网 » 基于RT-Thread和STM32的模块化串口接收和解析不定长协议数据

    发表评论