一个简单的基础通信协议的设计与实现

2023-05-16

一个简单的基础通信协议的设计与实现

    • 一种常见的通信协议格式
    • 搭建串口收发环境
      • 配置STM32CubeMX
      • 添加USART部分代码
    • 通信协议的实现

不同设备之间的通信,都需要设计自己的通信协议。为了保证设备与设备之间的数据的稳定传输,通信协议的设计需要考虑很多的问题。当然应对不同的应用场景,可以有针对性的设计不同的通信协议。

一种常见的通信协议格式

这是一种我们比较常见的通信协议格式

帧头地址位功能位帧序号数据长度数据内容校验位帧尾
1/2字节1字节1字节2字节2字节n字节1/2字节1/2字节

而为了应对不同的情况,可以依照情况做删改,例如减少帧头和帧尾,减少帧序号等等。

而本篇实现的通信协议如下,这里将几个部分都做了,实际中可能并不需要这么冗余的帧,可以按需求适当删改:

地址位功能位帧序号数据长度数据内容校验位
1字节1字节1字节1字节n字节1字节

本篇例程使用的开发板是STM32F103VET6,应用工具是MDK-ARM v5.33,STM32CubeMX V6.1.1
注:STM32CubeMX需要安装JAVA环境(JRE)。

搭建串口收发环境

参考:https://blog.csdn.net/u014470361/article/details/79206352#comments
使用串口1,DMA方式收发数据

注:DMA,全称为:Direct Memory Access,即直接存储器访问。DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。

配置STM32CubeMX

打开STM32CubeMX,File->New Project->Start Project

20210130162818-2021-01-30

RCC->打开外部时钟

20210130162940-2021-01-30

USART1->Asynchronous 异步通信

20210130163105-2021-01-30

下面NVIC Settings->Enabled 使能串口中断

20210130163235-2021-01-30

还是下面DMA Setthing->ADD->USART1_RX/USART_TX->Priority 使能DMA收发模式,高优先级

20210130163530-2021-01-30

SYS->Dubug-Serial Wire 启用调试引脚,因为我使用ST-Link进行调试,不使能调试引脚的话没法调试。

20210130163854-2021-01-30

上面的Clock Configuration时钟配置可以忽略,使用默认8MHz即可,然后是第三个选项Project Manager->Project Name设置工程名->Project Location设置工程路径,然后选择IDE->MDK-ARM
注意工程名和路径都不要出现中文字符

20210130164447-2021-01-30

最后点击GENERATE CODE生成工程文件,如果失败的话,可以尝试更换JAVA环境。

添加USART部分代码

在main.h宏定义一个最大接收字节数1024

#define UART_RX_LEN 1024

打开工程,并在main.c中添加部分代码

定义接收数组,接收数据长度以及标识。UART_RX_STA的0-14位存储数据长度,第15位表示接收状态。

  /* USER CODE BEGIN PV */
  uint8_t UART_RX_BUF[UART_RX_LEN];
  __IO uint16_t UART_RX_STA;
  /* USER CODE END PV */

注意位置DMA初始化需在MX_USART1_UART_Init();串口初始化之后。

  /* USER CODE BEGIN 2 */
  HAL_UART_Receive_DMA(&huart1, UART_RX_BUF, UART_RX_LEN);  // 启动DMA接收
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);              // 使能空闲中断
  /* USER CODE END 2 */

在while循环中添加DMA发送指令,将接收到的数据发送回去

if(UART_RX_STA & 0X8000)
{
  HAL_UART_Transmit_DMA(&huart1, UART_RX_BUF, UART_RX_STA & 0X7FFF);    // 将接收到的数据发送回去
  UART_RX_STA = 0;
}
/* USER CODE END WHILE */

打开stm32f1xx_it.c文件添加代码

/* USER CODE BEGIN PD */
extern uint8_t UART_RX_BUF[UART_RX_LEN];
extern __IO uint16_t UART_RX_STA;
/* USER CODE END PD */

拉到底,找到USART1中断。修改如下

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET&&(UART_RX_STA&0x8000==0))  // 空闲中断标记被置位,并且拆包完成
    {
      __HAL_UART_CLEAR_IDLEFLAG(&huart1);  // 清楚中断标记
      HAL_UART_DMAStop(&huart1);           // 停止DMA接收
      UART_RX_STA = UART_RX_LEN - __HAL_DMA_GET_COUNTER(huart1.hdmarx);  // 总数据量减去未接收到的数据量为已经接收到的数据量
      UART_RX_BUF[UART_RX_STA] = 0;  // 添加结束符
      UART_RX_STA |= 0X8000;         // 标记接收结束
      HAL_UART_Receive_DMA(&huart1, UART_RX_BUF, UART_RX_LEN);  // 重新启动DMA接收
  }
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}

编译->生成->- 0 Error(s), 0 Warning(s).下载,烧录,打开串口调试助手->波特率115200->随便发送几个字节,查看接收
20210130171150-2021-01-30

到此,一个基本的串口DMA收发环境就搭建好了。下面就是通信协议的内容了。

通信协议的实现

新建一个protocol.h文件

#ifndef PROTOCOL_H
#define PROTOCOL_H
#include "main.h"

#define MY_ADDRESS 1    //本机地址

//校验方式宏定义
#define M_FRAME_CHECK_SUM    0        //校验和
#define M_FRAME_CHECK_XOR    1        //异或校验
#define M_FRAME_CHECK_CRC8    2        //CRC8校验
#define M_FRAME_CHECK_CRC16    3        //CRC16校验

//返回结果:错误类型定义
typedef enum
{
    MR_OK=0,                //正常
    MR_FRAME_FORMAT_ERR = 1,        //帧格式错误
    MR_FRAME_CHECK_ERR = 2,            //校验值错位
    MR_FUNC_ERR = 3,            //内存错误
    MR_TIMEOUT = 4,                //通信超时
}m_result;

//帧格式定义
__packed typedef struct
{
    //u8 head[2];                //帧头
    u8 address;                //设备地址:1~255
    u8 function;                //功能码,0~255
    u8 count;                    //帧编号
    u8 datalen;                //有效数据长度
    u8 data[UART_RX_LEN];                    //数据存储区
    u16 chkval;                //校验值
    //u8 tail;                //帧尾
}m_frame_typedef;

extern m_protocol_dev_typedef m_ctrl_dev;            //定义帧
extern u8 COUNT;                            //数据帧计数器

void my_packsend_frame(m_frame_typedef *fx);
m_result my_unpack_frame(m_frame_typedef *fx);
m_result my_deal_frame(m_frame_typedef *fx);

void My_Func_1(void);                     //功能码1

#endif

再建一个protocol.c文件

#include "main.h"

uint8_t COUNT; //数据帧计数器

extern UART_HandleTypeDef huart1;

uint8_t checkmode=M_FRAME_CHECK_SUM; //定义校验方式

extern uint8_t UART_RX_BUF[UART_RX_LEN];
extern __IO uint16_t UART_RX_STA;

//拆包一帧数据
//将数据分别解析到结构体中,
//这样在后面处理的时候就可以非常方便的解析各个有效数据
m_result my_unpack_frame(m_frame_typedef *fx)
{
    uint16_t rxchkval=0;                //接收到的校验值
    uint16_t calchkval=0;            //计算得到的校验值
    uint8_t datalen=0;                 //有效数据长度
    datalen=UART_RX_STA & 0X7FFF;
    
    if(datalen<5) //当帧长度小于5的时候(地址1位,功能码1位,帧序号1位,数据长度1位,校验码1位)认为这一帧数据有问题,直接返回帧格式错误。
    {
        UART_RX_STA=0;
        return MR_FRAME_FORMAT_ERR;  
    }
    switch(checkmode)
    {
        case M_FRAME_CHECK_SUM:                            //校验和
            calchkval=mc_check_sum(UART_RX_BUF,datalen-1);
            rxchkval=UART_RX_BUF[datalen-1];
            break;
        case M_FRAME_CHECK_XOR:                            //异或校验
            calchkval=mc_check_xor(UART_RX_BUF,datalen-1);
            rxchkval=UART_RX_BUF[datalen-1];
            break;
        case M_FRAME_CHECK_CRC8:                        //CRC8校验
            calchkval=mc_check_crc8(UART_RX_BUF,datalen-1);
            rxchkval=UART_RX_BUF[datalen-1];
            break;
        case M_FRAME_CHECK_CRC16:                        //CRC16校验
            calchkval=mc_check_crc16(UART_RX_BUF,datalen-2);
            rxchkval=((uint16_t)UART_RX_BUF[datalen-2]<<8)+UART_RX_BUF[datalen-1];
            break;
    }
    
    if(calchkval==rxchkval)        //校验正常
    {
        fx->address=UART_RX_BUF[0];
        fx->function=UART_RX_BUF[1];
        fx->count=UART_RX_BUF[2];
        fx->datalen=UART_RX_BUF[3];
        if(fx->datalen)
        {
            for(datalen=0;datalen<fx->datalen;datalen++)
            {
                fx->data[datalen]=UART_RX_BUF[4+datalen];        //拷贝数据
            }
        }
        fx->chkval=rxchkval;    //记录校验值
    }else 
    {
        UART_RX_STA=0;
        return MR_FRAME_CHECK_ERR;
    }
    UART_RX_STA=0;      //应当在拆包完成之后再清楚标志位
    return MR_OK;
}

//打包一帧数据,并发送
//fx:指向需要打包的帧
void my_packsend_frame(m_frame_typedef *fx)
{
    uint16_t i;
    uint16_t calchkval=0;            //计算得到的校验值
    uint16_t framelen=0;                //打包后的帧长度
    uint8_t sendbuf[UART_RX_LEN];                //发送缓冲区

    if(checkmode==M_FRAME_CHECK_CRC16)framelen=6+fx->datalen;
    else framelen=5+fx->datalen;
    sendbuf[0]=fx->address;
    sendbuf[1]=fx->function;
    sendbuf[2]=fx->count;
    sendbuf[3]=fx->datalen;
    for(i=0;i<fx->datalen;i++)
    {
        sendbuf[4+i]=fx->data[i];
    }
    switch(checkmode)
    {
        case M_FRAME_CHECK_SUM:                            //校验和
            calchkval=mc_check_sum(sendbuf,fx->datalen+4);
            break;
        case M_FRAME_CHECK_XOR:                            //异或校验
            calchkval=mc_check_xor(sendbuf,fx->datalen+4);
            break;
        case M_FRAME_CHECK_CRC8:                        //CRC8校验
            calchkval=mc_check_crc8(sendbuf,fx->datalen+4);
            break;
        case M_FRAME_CHECK_CRC16:                        //CRC16校验
            calchkval=mc_check_crc16(sendbuf,fx->datalen+4);
            break;
    }

    if(checkmode==M_FRAME_CHECK_CRC16)        //如果是CRC16,则有2个字节的CRC
    {
        sendbuf[4+fx->datalen]=(calchkval>>8)&0XFF;     //高字节在前
        sendbuf[5+fx->datalen]=calchkval&0XFF;            //低字节在后
    }else sendbuf[4+fx->datalen]=calchkval&0XFF;
    HAL_UART_Transmit_DMA(&huart1, sendbuf, framelen);     //DMA发送这一帧数据
}

m_result my_deal_frame(m_frame_typedef *fx)
{
    if(fx->address == MY_ADDRESS)
    {
        switch(fx->function)
        {
            case 1:
            {
                My_Func_1();
            }break;
            case 2:
            {
                //My_Func_2(fx);
            }break;
            case 3:
            {
                //My_Func_3(fx);
            }break;
            case 4:
            {
                //My_Func_4(fx);
            }break;
            case 5:
            {
                //My_Func_5(fx);
            }break;
            case 6:
            {
                //My_Func_6(fx);
            }break;
            case 7:
            {
                //My_Func_7(fx);
            }break;
            case 8:
            {
                //My_Func_8(fx);
            }break;
            default:
                return MR_FUNC_ERR;
        }
    }return MR_OK;
}

void My_Func_1(void)
{
    m_frame_typedef txbuff;
    txbuff.address=MY_ADDRESS;      //放入地址码
    txbuff.function=1;          //功能码返回
    txbuff.count=(COUNT++)%255; //帧序号加一
    txbuff.datalen=3;           //随便发送3位数据
    txbuff.data[0]=0x01;
    txbuff.data[1]=0x02;
    txbuff.data[2]=0x03;
    my_packsend_frame(&txbuff); //将数据打包并发送
}


然后和校验、或校验、CRC8和CRC16校验的代码就不贴了,可以点击本文末尾的链接查看。

最后打开main.c将void main函数修改如下:

int main(void)
{
  /* USER CODE BEGIN 1 */
    m_frame_typedef fx;
    m_result res;
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  HAL_UART_Receive_DMA(&huart1, UART_RX_BUF, UART_RX_LEN);  // 启动DMA接收
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);              // 使能空闲中断
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    if(UART_RX_STA & 0X8000)      //是否接收到数据
    {
        res=my_unpack_frame(&fx);   //拆解一帧数据
        if(res==MR_OK)  //拆解完成
        {
            my_deal_frame(&fx); //解析这帧数据
        }
        UART_RX_STA = 0;    //清空标志和长度
    }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

可以编译运行一下,如果有错误可以查看一下头文件是否完整,左侧是否将你的新文件添加进来了。还有记得在main.h的适当位置include你的protocol.h和check.h文件。

最后的运行结果就是这个样子的->(勾选16进制发送与接收)
再编辑一条0x01的功能码命令,例如

地址位功能位帧序号数据长度数据内容校验位
010101010004

20210131231459-2021-01-31

一个简易的通信协议的设计就完成了,一般需要注意以下几个点,就是一般接收到一帧数据之后,将数据的各个部分都分别拆解到结构体中,这样可以非常方便的做后面的处理,同样打包一帧数据也是如此,只需要将结构体的各个部分写好,然后将数据帧结构体提交给发送函数就可以了,基本的思路就是这样的。
github:https://github.com/luodeb/USART1

如果有啥错误或不合理之处,请在评论区指出。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一个简单的基础通信协议的设计与实现 的相关文章

随机推荐

  • 电源管理与驱动设计笔记

    关注同名微信公众号 混沌无形 xff0c 阅读更多有趣好文 xff01 1 电源管理的功能 xff1a 具备电压过高保护 电流过大保护 电量监测 过放保护 等功能 gt 自主充电 2 一款清洁机器人的运动控制系统方案设计示意图 2 1电源充
  • (4)(4.3) 将固件加载到已有ArduPilot固件的主板上

    文章目录 前言 1 将自动驾驶仪连接到电脑 2 选择COM端口 3 安装固件 4 使用测试版和开发版 4 1 测试版 4 2 最新开发版本 4 3 自定义固件构建服务器 5 测试 前言 这些说明将告诉你如何将最新的固件下载到已经安装了 Ar
  • 【MDK KEIL】keil添加文件夹目录结构(批量添加.c和.h文件到工程中)(keil添加头文件路径)

    第一步打开创建文件夹选项 xff1a 第二步 xff1a 直接创建删除 或者上移下移 就好了 3 添加文件夹的同时别忘记导入头文件 xff1a
  • 单片机IO详解(上拉 下拉 准双向 输入 输出 推挽 开漏)

    目录 上拉 xff1a 下拉 输入 xff1a 上拉输入 下拉输入 输入浮空 模拟 施密特输入 xff1a 三态输入 xff1a 输出 复用推挽和推挽输出区别 推挽输出特点 开漏输出 xff1a 准双向口 一般单片机都会提供上拉和下拉功能
  • 局部路径规划算法——实现DWA(dynamic window approach)控制空间采样

    DWA算法是局部路径规划算法 xff0c 在全局路径规划算法完成后 xff0c DWA算法能够根据当前小车 xff08 机器人 xff09 位置 障碍物 终点的位置进行控制空间 xff08 速度 角速度 xff09 的采用 xff0c 从而
  • make和cmake简要介绍

    GCC GCC xff08 GNU Compiler Collection xff0c GNU编译器套件 xff09 是由GNU开发的编程语言译器 GNU编译器套件包括C C 43 43 Objective C Fortran Java A
  • 什么是小端模式,什么是大端模式

    字节序 前言 内存在读写数据的时候 xff0c 都是以字节为单位进行读写的 xff0c 其最小的读写单位也是字节 一个字节占8位 xff0c 如果暂且只考虑无符号数 xff0c 那么其能表示的范围只有0 255这256个整数数值 如果将一个
  • 多旋翼飞行器螺旋桨动力学模型

    1 多旋翼螺旋桨动力学模型 1 1 螺旋桨几何位置基本描述1 2 螺旋桨拉力及拉力力矩1 3 螺旋桨反扭力矩1 4 螺旋桨陀螺力矩1 5 螺旋桨动力学方程 1 多旋翼螺旋桨动力学模型 1 1 螺旋桨几何位置基本描述 坐标系分为多旋翼机体重心
  • 雷达天线孔径与分辨率

    我们现在讲一下真实孔径的问题 xff0c 我们先有一个 最小分辨角 的概念 xff0c 根据瑞丽 xff08 Rayleigh xff09 判据 xff0c 几何光学 xff0c 物体上的一个发光点经透镜成像后得到的应是一个几何像点 而由于
  • 【新手】关于ros包安装时,提示找不到相应的ros包(unable to locate package ros- **** )

    我是在学习这个大佬的教程 xff1a 从零开始的ROS学习之仿真 43 SLAM https blog csdn net u011612364 article details 122147741 xff08 引用 xff09 时发现我自己出
  • c++实现守护进程

    概述 Linux Daemon xff08 守护进程 xff09 是运行在后台的一种特殊进程 它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件 约束 守护进程一般在系统启动时开始运行 xff0c 除非强行终止 xff0c 否
  • linux C++ 环境下的ActiveMQ学习

    ActiveMQ 1 概述 ActiveMQ 是Apache出品 xff0c 最流行的 功能强大的即时通讯和集成模式的开源服务器 ActiveMQ 是一个完全支持JMS1 1和J2EE 1 4规范的 JMS Provider实现 提供客户端
  • Django与mysql建立长连接

    问题 xff1a 每次请求操作mysql数据库都会从新创建一个connect实例 xff0c 大大降低了数据处理的效率 还是django不够熟悉 解决办法 xff1a 自Django1 6版本后 xff0c 官方提供支持长连接 xff0c
  • Python ActiveMQ 消费者实现

    encoding utf 8 import stomp import sys import thread Create your tests here topic name 61 39 topic your topic name 39 br
  • Django 服务启动2次问题

    首先 xff0c 我们的操作如下 xff1a 一 命令行的启动服务 xff1a python manage py runserver 0 0 0 0 8000 二 服务启动行为 try os environ setdefault 34 DJ
  • Django settings.py配置总结

    1 版本 xff1a 1 8 9 2 解决数据库时间和当前系统时间不一致问题 设置USE TZ 61 False 3 时区配置 xff1a TIME ZONE 61 39 Asia Shanghai 39 4 静态文件js css等的引用路
  • Extern关键字在vscode中不生效的解决办法

    今天回顾extern关键字的时候遇到一个问题 a c里的代码 b c里的代码 这么简单的几个代码 vscode竟然给我报错 为什么 抱着这个问题去逛了下csdn 终于让我找到了问题所在 原来vscode默认只能对单文件编译 即使俩文件在同一
  • 头文件、源文件 及extern的使用

    头文件与源文件的作用 1 编译器的工作过程 简单的说其实要理解C文件与头文件 xff08 即 h xff09 有什么不同之处 xff0c 首先需要弄明白编译器的工作过程 xff0c 一般说来编译器会做以下几个过程 xff1a 1 预处理阶段
  • 从零开始的三维激光雷达SLAM教程第二讲(搭建Gazebo仿真环境,并添加动态障碍物)

    文章目录 引言第二节 xff1a 搭建仿真环境下载velodyne激光模拟功能包安装turtlebot3模拟器安装三维激光雷达到turtlebot3上构建Gazebo地图添加动态障碍物操作机器人建图最终效果 引言 毕业设计打算做三维激光SL
  • 一个简单的基础通信协议的设计与实现

    一个简单的基础通信协议的设计与实现 一种常见的通信协议格式搭建串口收发环境配置STM32CubeMX添加USART部分代码 通信协议的实现 不同设备之间的通信 xff0c 都需要设计自己的通信协议 为了保证设备与设备之间的数据的稳定传输 x