精华内容
下载资源
问答
  • 串口通信协议
    2021-11-03 20:48:55

    串口编程-通用串口通信协议


    如需转载请标明出处:http://blog.csdn.net/itas109
    QQ技术交流群:129518033

    前言

    串口通信协议是指规定了数据包的内容,内容包含了起始位、主体数据、校验位及停止位,双方需要约定一致的数据包格式才能正常收发数据的有关规范。

    本文将介绍一种通用的串口通信协议,用于提高串口通信的质量。

    串口通信约定

    波特率:9600
    数据位:8
    奇偶校验位:无
    停止位:1
    数据流控:无
    

    帧格式说明

    字节偏移字段长度(字节,byte)说明
    0帧头2固定不变 0xEB90
    2协议版本1用于协议升级扩展,初始版本为0x00
    3数据总长度2数据总长度N
    5数据N
    5 + N校验码2CRC-16/MODBUS(0x18005)

    默认情况下,超过一个字节的数据均采用大端模式(big endian,高位字节在前,低位字节在后)。

    默认情况下,超过一个字节的数据均采用大端模式(big endian,高位字节在前,低位字节在后)。


    帧格式示例

    EB90 00 0007 20211025000000 7F52
    
    帧头:0xEB90
    版本: 0x00
    数据长度:0x0007
    数据:0x20211025000000
    校验和:0x7F52
    

    License

    License under CC BY-NC-ND 4.0: 署名-非商业使用-禁止演绎

    如需转载请标明出处:http://blog.csdn.net/itas109
    QQ技术交流群:129518033


    Reference:

    1. NULL
    更多相关内容
  • 串口通信协议

    2016-04-26 15:26:55
    用c/c++开发的串口协议
  • 6轴机器人+滑台_上位机-串口通信协议_设计; 用于单片机与上位机通信交互! 通信格式:协议头+内容+校验+协议尾; 有需要设计下位机与上位机的兄弟可以参考
  • 本文主要为单片机串口通信协议代码,下面一起来学习一下
  • 本文主要介绍了一下关于STM32的几种串口通信协议,希望对你的学习有所帮助。
  • CH9329 是一款串口转标准设备(键盘、鼠标、自定义 HID)芯片,根据不同的...通过提供的上位机软件,用户也可自行配置芯片工作模式、串口通信模式、串口通信波特率、多种超时时间、VID、PID,以及各种 USB 字符串描述符。
  • stm32串口通信协议简单教程,stm32串口通信协议简单教程
  • 通信教程 | 自定义串口通信协议

    千次阅读 2022-02-26 00:21:18
    关注+星标公众号,不错过精彩内容作者 | strongerHuang微信公众号|strongerHuang有一些初学者总觉得通信协议是一个很复杂的知识,把它想的很高深,导致不知道该怎么...

    关注+星标公众,不错过精彩内容

    448eea02f0e00660d9c0386d5105aee8.gif

    作者 | strongerHuang

    微信公众号 | strongerHuang

    有一些初学者总觉得通信协议是一个很复杂的知识,把它想的很高深,导致不知道该怎么学。

    同时,偶尔有读者问关于串口自定义通信协议相关的问题,今天就来写写串口通信协议,并不是你想想中的那么难?

    1什么通信协议?

    通信协议不难理解,就是两个(或多个)设备之间进行通信,必须要遵循的一种协议。

    百度百科的解释:

    通信协议是指双方实体完成通信或服务所必须遵循的规则和约定。通过通信信道和设备互连起来的多个不同地理位置的数据通信系统,要使其能协同工作实现信息交换和资源共享,它们之间必须具有共同的语言。交流什么、怎样交流及何时交流,都必须遵循某种互相都能接受的规则。这个规则就是通信协议。

    相应该有很多读者都买过一些基于串口通信的模块,市面上很多基于串口通信的模块都是自定义通信协议,有的比较简单,有的相对复杂一点。

    举一个很简单的串口通信协议的例子:比如只传输一个温度值,只有三个字节的通信协议:

    帧头温度值帧尾
    5A一字节数值3B

    这种看起来是不是很简单?它也是一种通信协议。

    只是说这种通信协议应用的场合相对比较简单(一对一两个设备之间),同时,它存在很多弊端。

    2过于简单的通信协议引发的问题

    上面那种只有三个字节的通信协议,相信大家都看明白了。虽然它也能通信,也能传输数据,但它存在一系列的问题。

    比如:多个设备连接在一条总线(比如485)上,怎么判断传输给谁?(没有设备信息)

    还比如:处于一个干扰环境,你能保障传输数据正确吗?(没有校验信息)

    再比如:我想传输多个不确定长度的数据,该怎么办?(没有长度信息)。

    上面这一系列问题,相信做过自定义通信的朋友都了解。

    所以,在通信协议里面要约定更多的“协议信息”,这样才能保证通信的完整。

    3通信协议常见内容

    基于串口的通信协议通常不能太复杂,因为串口通信速率、抗干扰能力以及其他各方面原因,相对于TCP/IP这种通信协议,是一种很轻量级的通信协议。

    所以,基于串口的通信,除了一些通用的通信协议(比如:Modubs、MAVLink)之外,很多时候,工程师都会根据自己项目情况,自定义通信协议。

    下面简单描述下常见自定义通信协议的一些要点内容。

    84b7fc28933359d2108d95d2aa920046.png

    (这是一些常见的协议内容,可能不同情况,其协议内容不同)

    1.帧头

    帧头,就是一帧通信数据的开头。

    有的通信协议帧头只有一个,有的有两个,比如:5A、A5作为帧头。

    c8b3b4a3324b8bb78f4ce66b947d50cb.png

    2.设备地址/类型

    设备地址或者设备类型,通常是用于多种设备之间,为了方便区分不同设备。

    2222cfa37ada75ff2183645753b84b4d.png

    这种情况,需要在协议或者附录中要描述各种设备类型信息,方便开发者编码查询。

    当然,有些固定的两种设备之间通信,可能没有这个选项。

    3.命令/指令

    命令/指令比较常见,一般是不同的操作,用不同的命令来区分。

    0cd5a9412e769c30175a8b1955f91738.png

    举例:温度:0x01;湿度:0x02;

    4.命令类型/功能码

    这个选项对命令进一步补充。比如:读、写操作。

    a06dbd1d79f41b131fc0ac005936941a.png

    举例:读Flash:0x01; 写Flash:0x02;

    5.数据长度

    数据长度这个选项,可能有的协议会把该选项提到前面设备地址位置,把命令这些信息算在“长度”里面。

    这个主要是方便协议(接收)解析的时候,统计接收数据长度。

    731791ac84472428d9c3ca060e46d26e.png

    比如:有时候传输一个有效数据,有时候要传输多个有效数据,甚至传输一个数组的数据。这个时候,传输的一帧数据就是不定长数据,就必须要有数据长度来约束。

    有的长度是一个字节,其范围:0x01 ~ 0xFF,有的可能要求一次性传输更多,就用两个字节表示,其范围0x0001 ~ 0xFFFFF。

    当然,有的通信长度是固定的长度(比如固定只传输、温度、湿度这两个数据),其协议可能没有这个选项。

    6.数据

    数据就不用描述了,就是你传输的实实在在的数据,比如温度:25℃。

    7.帧尾

    有些协议可能没有帧尾,这个应该是可有可无的一个选项。

    8.校验码

    校验码是一个比较重要的内容,一般正规一点的通信协议都有这个选项,原因很简单,通信很容易受到干扰,或者其他原因,导致传输数据出错。

    如果有校验码,就能比较有效避免数据传输出错的的情况。

    74e5e06d9e6ab9209776f1d1f94fed57.png

    校验码的方式有很多,校验和、CRC校验算是比较常见的,用于自定义协议中的校验方式。

    还有一点,有的协议可能把校验码放在倒数第二,帧尾放在最后位置。

    4通信协议代码实现

    自定义通信协议,代码实现的方式有很多种,怎么说呢,“条条大路通罗马”你只需要按照你协议要写实现代码就行。

    当然,实现的同时,需要考虑你项目实际情况,比如通信数据比较多,要用消息队列(FIFO),还比如,如果协议复杂,最好封装结构体等。

    下面分享一些以前用到的代码,可能没有描述更多细节,但一些思想可以借鉴。

    1.消息数据发送

    a.通过串口直接发送每一个字节

    这种对于新手来说都能理解,这里分享一个之前DGUS串口屏的例子:

    #define DGUS_FRAME_HEAD1          0xA5                     //DGUS屏帧头1
    #define DGUS_FRAME_HEAD2          0x5A                     //DGUS屏帧头2
    
    
    #define DGUS_CMD_W_REG            0x80                     //DGUS写寄存器指令
    #define DGUS_CMD_R_REG            0x81                     //DGUS读寄存器指令
    #define DGUS_CMD_W_DATA           0x82                     //DGUS写数据指令
    #define DGUS_CMD_R_DATA           0x83                     //DGUS读数据指令
    #define DGUS_CMD_W_CURVE          0x85                     //DGUS写曲线指令
    
    
    /* DGUS寄存器地址 */
    #define DGUS_REG_VERSION          0x00                     //DGUS版本
    #define DGUS_REG_LED_NOW          0x01                     //LED背光亮度
    #define DGUS_REG_BZ_TIME          0x02                     //蜂鸣器时长
    #define DGUS_REG_PIC_ID           0x03                     //显示页面ID
    #define DGUS_REG_TP_FLAG          0x05                     //触摸坐标更新标志
    #define DGUS_REG_TP_STATUS        0x06                     //坐标状态
    #define DGUS_REG_TP_POSITION      0x07                     //坐标位置
    #define DGUS_REG_TPC_ENABLE       0x0B                     //触控使能
    #define DGUS_REG_RTC_NOW          0x20                     //当前RTCS
    
    
    //往DGDS屏指定寄存器写一字节数据
    void DGUS_REG_WriteWord(uint8_t RegAddr, uint16_t Data)
    {
      DGUS_SendByte(DGUS_FRAME_HEAD1);
      DGUS_SendByte(DGUS_FRAME_HEAD2);
      DGUS_SendByte(0x04);
    
    
      DGUS_SendByte(DGUS_CMD_W_REG);                 //指令
      DGUS_SendByte(RegAddr);                        //地址
    
    
      DGUS_SendByte((uint8_t)(Data>>8));             //数据
      DGUS_SendByte((uint8_t)(Data&0xFF));
    }
    
    
    //往DGDS屏指定地址写一字节数据
    void DGUS_DATA_WriteWord(uint16_t DataAddr, uint16_t Data)
    {
      DGUS_SendByte(DGUS_FRAME_HEAD1);
      DGUS_SendByte(DGUS_FRAME_HEAD2);
      DGUS_SendByte(0x05);
    
    
      DGUS_SendByte(DGUS_CMD_W_DATA);                //指令
    
    
      DGUS_SendByte((uint8_t)(DataAddr>>8));         //地址
      DGUS_SendByte((uint8_t)(DataAddr&0xFF));
    
    
      DGUS_SendByte((uint8_t)(Data>>8));             //数据
      DGUS_SendByte((uint8_t)(Data&0xFF));
    }

    b.通过消息队列发送

    在上面基础上,用一个buf装下消息,然后“打包”到消息队列,通过消息队列的方式(FIFO)发送出去。

    static uint8_t  sDGUS_SendBuf[DGUS_PACKAGE_LEN];
    
    
    //往DGDS屏指定寄存器写一字节数据
    void DGUS_REG_WriteWord(uint8_t RegAddr, uint16_t Data)
    {
      sDGUS_SendBuf[0] = DGUS_FRAME_HEAD1;           //帧头
      sDGUS_SendBuf[1] = DGUS_FRAME_HEAD2;
      sDGUS_SendBuf[2] = 0x06;                       //长度
      sDGUS_SendBuf[3] = DGUS_CMD_W_CTRL;            //指令
      sDGUS_SendBuf[4] = RegAddr;                    //地址
      sDGUS_SendBuf[5] = (uint8_t)(Data>>8);         //数据
      sDGUS_SendBuf[6] = (uint8_t)(Data&0xFF);
    
    
      DGUS_CRC16(&sDGUS_SendBuf[3], sDGUS_SendBuf[2] - 2, &sDGUS_CRC_H, &sDGUS_CRC_L);
      sDGUS_SendBuf[7] = sDGUS_CRC_H;                //校验
      sDGUS_SendBuf[8] = sDGUS_CRC_L;
    
    
      DGUSSend_Packet_ToQueue(sDGUS_SendBuf, sDGUS_SendBuf[2] + 3);
    }
    
    
    //往DGDS屏指定地址写一字节数据
    void DGUS_DATA_WriteWord(uint16_t DataAddr, uint16_t Data)
    {
      sDGUS_SendBuf[0] = DGUS_FRAME_HEAD1;           //帧头
      sDGUS_SendBuf[1] = DGUS_FRAME_HEAD2;
      sDGUS_SendBuf[2] = 0x07;                       //长度
      sDGUS_SendBuf[3] = DGUS_CMD_W_DATA;            //指令
      sDGUS_SendBuf[4] = (uint8_t)(DataAddr>>8);     //地址
      sDGUS_SendBuf[5] = (uint8_t)(DataAddr&0xFF);
      sDGUS_SendBuf[6] = (uint8_t)(Data>>8);         //数据
      sDGUS_SendBuf[7] = (uint8_t)(Data&0xFF);
    
    
      DGUS_CRC16(&sDGUS_SendBuf[3], sDGUS_SendBuf[2] - 2, &sDGUS_CRC_H, &sDGUS_CRC_L);
      sDGUS_SendBuf[8] = sDGUS_CRC_H;                //校验
      sDGUS_SendBuf[9] = sDGUS_CRC_L;
    
    
      DGUSSend_Packet_ToQueue(sDGUS_SendBuf, sDGUS_SendBuf[2] + 3);
    }

    c.用“结构体代替数组SendBuf”方式

    结构体对数组更方便引用,也方便管理,所以,结构体方式相比数组buf更高级,也更实用。(当然,如果成员比较多,如果用临时变量方式也会导致占用过多堆栈的情况)

    比如:

    typedef struct
    {
      uint8_t  Head1;                 //帧头1
      uint8_t  Head2;                 //帧头2
      uint8_t  Len;                   //长度
      uint8_t  Cmd;                   //命令
      uint8_t  Data[DGUS_DATA_LEN];   //数据
      uint16_t CRC16;                 //CRC校验
    }DGUS_PACKAGE_TypeDef;

    d.其他更多

    串口发送数据的方式有很多,比如用DMA的方式替代消息队列的方式。

    2.消息数据接收

    串口消息接收,通常串口中断接收的方式居多,当然,也有很少情况用轮询的方式接收数据。

    a.常规中断接收

    还是以DGUS串口屏为例,描述一种简单又常见的中断接收方式:

    void DGUS_ISRHandler(uint8_t Data)
    {
      static uint8_t sDgus_RxNum = 0;                //数量
      static uint8_t sDgus_RxBuf[DGUS_PACKAGE_LEN];
      static portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
    
    
      sDgus_RxBuf[gDGUS_RxCnt] = Data;
      gDGUS_RxCnt++;
    
    
      /* 判断帧头 */
      if(sDgus_RxBuf[0] != DGUS_FRAME_HEAD1)       //接收到帧头1
      {
        gDGUS_RxCnt = 0;
        return;
      }
      if((2 == gDGUS_RxCnt) && (sDgus_RxBuf[1] != DGUS_FRAME_HEAD2))
      {
        gDGUS_RxCnt = 0;
        return;
      }
    
    
      /* 确定一帧数据长度 */
      if(gDGUS_RxCnt == 3)
      {
        sDgus_RxNum = sDgus_RxBuf[2] + 3;
      }
    
    
      /* 接收完一帧数据 */
      if((6 <= gDGUS_RxCnt) && (sDgus_RxNum <= gDGUS_RxCnt))
      {
        gDGUS_RxCnt = 0;
    
    
        if(xDGUSRcvQueue != NULL)                    //解析成功, 加入队列
        {
          xQueueSendFromISR(xDGUSRcvQueue, &sDgus_RxBuf[0], &xHigherPriorityTaskWoken);
          portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
        }
      }
    }

    b.增加超时检测

    接收数据有可能存在接收了一半,中断因为某种原因中断了,这时候,超时检测也很有必要。

    比如:用多余的MCU定时器做一个超时计数的处理,接收到一个数据,开始计时,超过1ms没有接收到下一个数据,就丢掉这一包(前面接收的)数据。

    static void DGUS_TimingAndUpdate(uint16_t Nms)
    {
      sDGUSTiming_Nms_Num = Nms;
      TIM_SetCounter(DGUS_TIM, 0);                   //设置计数值为0
      TIM_Cmd(DGUS_TIM, ENABLE);                     //启动定时器
    }
    
    
    void DGUS_COM_IRQHandler(void)
    {
      if((DGUS_COM->SR & USART_FLAG_RXNE) == USART_FLAG_RXNE)
      {
        DGUS_TimingAndUpdate(5);                     //更新定时(防止超时)
        DGUS_ISRHandler((uint8_t)USART_ReceiveData(DGUS_COM));
      }
    }

    c.更多

    接收和发送一样,实现方法有很多种,比如接收同样也可以用结构体方式。但有一点,都需要结合你实际需求来编码。

    5最后

    以上自定义协议内容仅供参考,最终用哪些、占用几个字节都与你实际需求有关。

    基于串口的自定义通信协议,有千差万别,比如:MCU处理能力、设备多少、通信内容等都与你自定义协议有关。

    有的可能只需要很简单的通信协议就能满足要求。有的可能需要更复杂的协议才能满足。

    最后强调两点:

    1.以上举例并不是完整的代码(有些细节没有描述出来),主要是供大家学习这种编程思想,或者实现方式。

    2.一份好的通信协议代码,必定有一定容错处理,比如:发送完成检测、接收超时检测、数据出错检测等等。所以说,以上代码并不是完整的代码。

    ------------ END ------------

    关注公众号后台回复『通信教程』『单片机』阅读更多相关文章。

    回复“加群”按规则加入技术交流群,回复“1024”查看更多内容。

    6ff3365048da9963444f2671f061b335.png

    ef3b1976f1f276e6dfa97199c6ed28fc.png

    点击“阅读原文”查看更多分享

    展开全文
  • C8051F带通信协议串口通信通信程序,上位机发送0xAA,单片机收到后反馈0xBB,通信开始。通信结束后发送0xCC。
  • 主要介绍了Android 串口通信编程及串口协议分析的相关资料,这里对Android 串口通信进行详解,及简单实现步骤和协议进行分析,需要的朋友可以参考下
  • 串口通信协议简介—学习笔记

    千次阅读 2022-01-18 12:11:55
    串口通信协议简介—学习笔记 文章目录串口通信协议简介—学习笔记一、串口、COM口、UART口, TTL、RS-232、RS-485区别详解1、物理接口形式2、电平标准2.1 **TTL**2.2 **RS232**2.3 **RS485**2.4 TTL标准及RS-232标准...

    串口通信协议简介—学习笔记

    一、串口、COM口、UART口, TTL、RS-232、RS-485区别详解

    首先,串口、UART口、COM口、USB口是指的物理接口形式(硬件)。而TTL、RS-232、RS-485是指的电平标准(电信号)。

    1、物理接口形式

    • 串口:串口是一个泛称,UART、TTL、RS232、RS485都遵循类似的通信时序协议,因此都被通称为串口。
    • UART接口通用异步收发器(Universal Asynchronous Receiver/Transmitter),UART是串口收发的逻辑电路,这部分可以独立成芯片,也可以作为模块嵌入到其他芯片里,单片机、SOC、PC里都会有UART模块。
    • COM口:COM口即串行通讯端口,简称串口。这里区别于USB的“通用串行总线”和硬盘的“SATA”。

    一般我们见到的是两种物理标准:D型9针插头,和 4针杜邦头两种。

    • USB口:通用串行总线,和串口完全是两个概念。虽然也是串行方式通信,但由于USB的通信时序和信号电平都和串口完全不同,因此和串口没有任何关系。USB是高速的通信接口,用于PC连接各种外设,U盘、键鼠、移动硬盘、当然也包括“USB转串口”的模块。

    注:(USB转串口模块,就是USB接口的UART模块)

    2、电平标准

    2.1 TTL

    TTL指双极型三极管逻辑电路,市面上很多**“USB转TTL”模块,实际上是“USB转TTL电平的串口”**模块。这种信号0对应0V,1对应3.3V或者5V。

    与单片机、SOC的IO电平兼容。不过实际也不一定是TTL电平,因为现在大部分数字逻辑都是CMOS工艺做的,只是沿用了TTL的说法

    我们进行串口通信的时候 从单片机直接出来的基本是都是TTL 电平。

    TTL电平:全双工(逻辑1: 2.4V–5V 逻辑0: 0V–0.5V)

    TTL用于两个MCU间通信,硬件框图如下:
    在这里插入图片描述

    ‘0’和‘1’表示
    在这里插入图片描述

    2.2 RS232

    是电子工业协会(Electronic Industries Association,EIA) 制定的异步传输标准接口,同时对应着电平标准和通信协议(时序),其电平标准:+3V~+15V对应0,-3V~-15V对应1。

    RS232 的逻辑电平和TTL 不一样但是协议一样。

    RS-232电平:全双工(逻辑1:-15V–5V 逻辑0:+3V–+15V)

    RS-232用于MCU与PC机之间通信,硬件框图如下:
    在这里插入图片描述

    ‘0’和‘1’表示
    在这里插入图片描述

    2.3 RS485

    RS485是一种串口接口标准,为了长距离传输采用差分方式传输,传输的是差分信号,抗干扰能力比RS232强很多。

    两线压差为-(26)V表示1,两线压差为+(26)V表示0

    差分负逻辑

    RS-485:半双工、(逻辑0:+2V–+6V 逻辑1: -6V—2V)这里的电平指AB两线间的电压差。

    硬件框图如下:
    在这里插入图片描述

    ‘0’和‘1’表示:(差分信号)
    在这里插入图片描述

    2.4 TTL标准及RS-232标准

    我们知道常见的电子电路中常使用TTL的电平标准,理想状态下,使用5V表示二进制逻辑1,使用0V表示逻辑0;

    而为了增加串口通讯的远距离传输及抗干扰能力,它使用-15V表示逻辑1,+15V表示逻辑0。

    使用RS232与TTL电平校准表示同一个信号时的对比见图:
    在这里插入图片描述

    因为控制器一般使用TTL电平标准,所以常常会使用MA3232芯片对TTL及RS-232电平的信号进行互相转换
    在这里插入图片描述

    2.5 补充

    在旧式的台式计算机中一般会有RS-232标准的COM口(也称DB9接口)

    在目前的其它工业控制使用的串口通讯中,一般只使用RXD、TXD以及GND三条信号线,直接传输数据信号。而RTS、CTS、DSR、DTR及DCD信号都被裁剪掉了
    在这里插入图片描述

    3、几种常见、常用的硬件接口

    1. D型9针串口
    在这里插入图片描述

    一般只接出RXD TXD两针,外加GND。

    这种接口的协议只有两种:RS-232和RS-485。

    2. USB转TTL串口
    在这里插入图片描述

    CP2102芯片、PL2303芯片

    3. USB转RS-232串口
    在这里插入图片描述

    二、串口通信百度百科

    串口通信指串口按位(bit)发送和接收字节。尽管比特字节(byte)的串行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。串口通信协议是指规定了数据包的内容,内容包含了起始位、主体数据、校验位及停止位,双方需要约定一致的数据包格式才能正常收发数据的有关规范。

    在串口通信中,常用的协议包括RS-232、RS-422和RS-485

    三、协议层

    串口通讯的数据包由发送设备通过自身的TXD接口传输到接收设备的RXD接口。在串口通讯的协议层中,规定了数据包的内容,它由启始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据

    串口数据包的基本组成图如下:
    在这里插入图片描述

    1. 波特率

    主要讲解的是串口异步通讯,异步通讯中由于没有时钟信号(如前面讲解的DB9接口中是没有时钟信号的),所以两个通讯设备之间需要约定好波特率,即每个码元的长度,以便对信号进行解码,图 中用虚线分开的每一格就是代表一个码元。常见的波特率为4800、9600、115200等。

    2. 通讯的起始和停止信号

    串口通讯的一个数据包从起始信号开始,直到停止信号结束。数据包的起始信号由一个逻辑0的数据位表示,而数据包的停止信号可由0.5、1、1.5或2个逻辑1的数据位表示,只要双方约定一致即可。

    3. 有效数据

    在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效数据的长度常被约定为5、6、7或8位长。

    4. 数据校验

    在有效数据之后,有一个可选的数据校验位。由于数据通信相对更容易受到外部干扰导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。校验方法有奇校验(odd)、偶校验(even)、0校验(space)、1校验(mark)以及无校验(noparity),它们介绍如下:

    • 奇校验要求有效数据和校验位中"1"的个数为奇数,比如一个8位长的有效数据为:01101001,此时总共有4个"1",为达到奇校验效果,校验位为"1",最后传输的数据将是8位的有效数据加上1位的校验位总共9位。

    • 偶校验与奇校验要求刚好相反,要求帧数据和校验位中"1"的个数为偶数,比如数据帧:11001010,此时数据帧"1"的个数为4个,所以偶校验位为"0"。

    • 0校验是不管有效数据中的内容是什么,校验位总为"0",1校验是校验位总为"1"。

    • 在无校验的情况下,数据包中不包含校验位。
      求刚好相反,要求帧数据和校验位中"1"的个数为偶数,比如数据帧:11001010,此时数据帧"1"的个数为4个,所以偶校验位为"0"。

    • 0校验是不管有效数据中的内容是什么,校验位总为"0",1校验是校验位总为"1"。

    • 在无校验的情况下,数据包中不包含校验位。

    展开全文
  • 自定义串口通信协议,如何实现?

    千次阅读 2021-05-30 00:17:57
    关注+星标公众号,不错过精彩内容作者 | strongerHuang微信公众号|嵌入式专栏有一些初学者总觉得通信协议是一个很复杂的知识,把它想的很高深,导致不知道该怎么学。同时,偶尔有...

    关注+星标公众,不错过精彩内容

    作者 | strongerHuang

    微信公众号 | 嵌入式专栏

    有一些初学者总觉得通信协议是一个很复杂的知识,把它想的很高深,导致不知道该怎么学。

    同时,偶尔有读者问关于串口自定义通信协议相关的问题,今天就来写写串口通信协议,并不是你想想中的那么难?

    1什么通信协议?

    通信协议不难理解,就是两个(或多个)设备之间进行通信,必须要遵循的一种协议。

    百度百科的解释:

    通信协议是指双方实体完成通信或服务所必须遵循的规则和约定。通过通信信道和设备互连起来的多个不同地理位置的数据通信系统,要使其能协同工作实现信息交换和资源共享,它们之间必须具有共同的语言。交流什么、怎样交流及何时交流,都必须遵循某种互相都能接受的规则。这个规则就是通信协议。

    相应该有很多读者都买过一些基于串口通信的模块,市面上很多基于串口通信的模块都是自定义通信协议,有的比较简单,有的相对复杂一点。

    举一个很简单的串口通信协议的例子:比如只传输一个温度值,只有三个字节的通信协议:

    帧头温度值帧尾
    5A一字节数值3B

    这种看起来是不是很简单?它也是一种通信协议。

    只是说这种通信协议应用的场合相对比较简单(一对一两个设备之间),同时,它存在很多弊端。

    2过于简单的通信协议引发的问题

    上面那种只有三个字节的通信协议,相信大家都看明白了。虽然它也能通信,也能传输数据,但它存在一系列的问题。

    比如:多个设备连接在一条总线(比如485)上,怎么判断传输给谁?(没有设备信息)

    还比如:处于一个干扰环境,你能保障传输数据正确吗?(没有校验信息)

    再比如:我想传输多个不确定长度的数据,该怎么办?(没有长度信息)。

    上面这一系列问题,相信做过自定义通信的朋友都了解。

    所以,在通信协议里面要约定更多的“协议信息”,这样才能保证通信的完整。

    3通信协议常见内容

    基于串口的通信协议通常不能太复杂,因为串口通信速率、抗干扰能力以及其他各方面原因,相对于TCP/IP这种通信协议,是一种很轻量级的通信协议。

    所以,基于串口的通信,除了一些通用的通信协议(比如:Modubs、MAVLink)之外,很多时候,工程师都会根据自己项目情况,自定义通信协议。

    下面简单描述下常见自定义通信协议的一些要点内容。

    (这是一些常见的协议内容,可能不同情况,其协议内容不同)

    1.帧头

    帧头,就是一帧通信数据的开头。

    有的通信协议帧头只有一个,有的有两个,比如:5A、A5作为帧头。

    2.设备地址/类型

    设备地址或者设备类型,通常是用于多种设备之间,为了方便区分不同设备。

    这种情况,需要在协议或者附录中要描述各种设备类型信息,方便开发者编码查询。

    当然,有些固定的两种设备之间通信,可能没有这个选项。

    3.命令/指令

    命令/指令比较常见,一般是不同的操作,用不同的命令来区分。

    举例:温度:0x01;湿度:0x02;

    4.命令类型/功能码

    这个选项对命令进一步补充。比如:读、写操作。

    举例:读Flash:0x01; 写Flash:0x02;

    5.数据长度

    数据长度这个选项,可能有的协议会把该选项提到前面设备地址位置,把命令这些信息算在“长度”里面。

    这个主要是方便协议(接收)解析的时候,统计接收数据长度。

    比如:有时候传输一个有效数据,有时候要传输多个有效数据,甚至传输一个数组的数据。这个时候,传输的一帧数据就是不定长数据,就必须要有数据长度来约束。

    有的长度是一个字节,其范围:0x01 ~ 0xFF,有的可能要求一次性传输更多,就用两个字节表示,其范围0x0001 ~ 0xFFFFF。

    当然,有的通信长度是固定的长度(比如固定只传输、温度、湿度这两个数据),其协议可能没有这个选项。

    6.数据

    数据就不用描述了,就是你传输的实实在在的数据,比如温度:25℃。

    7.帧尾

    有些协议可能没有帧尾,这个应该是可有可无的一个选项。

    8.校验码

    校验码是一个比较重要的内容,一般正规一点的通信协议都有这个选项,原因很简单,通信很容易受到干扰,或者其他原因,导致传输数据出错。

    如果有校验码,就能比较有效避免数据传输出错的的情况。

    校验码的方式有很多,校验和、CRC校验算是比较常见的,用于自定义协议中的校验方式。

    还有一点,有的协议可能把校验码放在倒数第二,帧尾放在最后位置。

    4通信协议代码实现

    自定义通信协议,代码实现的方式有很多种,怎么说呢,“条条大路通罗马”你只需要按照你协议要写实现代码就行。

    当然,实现的同时,需要考虑你项目实际情况,比如通信数据比较多,要用消息队列(FIFO),还比如,如果协议复杂,最好封装结构体等。

    下面分享一些以前用到的代码,可能没有描述更多细节,但一些思想可以借鉴。

    1.消息数据发送

    a.通过串口直接发送每一个字节

    这种对于新手来说都能理解,这里分享一个之前DGUS串口屏的例子:

    #define DGUS_FRAME_HEAD1          0xA5                     //DGUS屏帧头1
    #define DGUS_FRAME_HEAD2          0x5A                     //DGUS屏帧头2
    
    
    #define DGUS_CMD_W_REG            0x80                     //DGUS写寄存器指令
    #define DGUS_CMD_R_REG            0x81                     //DGUS读寄存器指令
    #define DGUS_CMD_W_DATA           0x82                     //DGUS写数据指令
    #define DGUS_CMD_R_DATA           0x83                     //DGUS读数据指令
    #define DGUS_CMD_W_CURVE          0x85                     //DGUS写曲线指令
    
    
    /* DGUS寄存器地址 */
    #define DGUS_REG_VERSION          0x00                     //DGUS版本
    #define DGUS_REG_LED_NOW          0x01                     //LED背光亮度
    #define DGUS_REG_BZ_TIME          0x02                     //蜂鸣器时长
    #define DGUS_REG_PIC_ID           0x03                     //显示页面ID
    #define DGUS_REG_TP_FLAG          0x05                     //触摸坐标更新标志
    #define DGUS_REG_TP_STATUS        0x06                     //坐标状态
    #define DGUS_REG_TP_POSITION      0x07                     //坐标位置
    #define DGUS_REG_TPC_ENABLE       0x0B                     //触控使能
    #define DGUS_REG_RTC_NOW          0x20                     //当前RTCS
    
    
    //往DGDS屏指定寄存器写一字节数据
    void DGUS_REG_WriteWord(uint8_t RegAddr, uint16_t Data)
    {
      DGUS_SendByte(DGUS_FRAME_HEAD1);
      DGUS_SendByte(DGUS_FRAME_HEAD2);
      DGUS_SendByte(0x04);
    
    
      DGUS_SendByte(DGUS_CMD_W_REG);                 //指令
      DGUS_SendByte(RegAddr);                        //地址
    
    
      DGUS_SendByte((uint8_t)(Data>>8));             //数据
      DGUS_SendByte((uint8_t)(Data&0xFF));
    }
    
    
    //往DGDS屏指定地址写一字节数据
    void DGUS_DATA_WriteWord(uint16_t DataAddr, uint16_t Data)
    {
      DGUS_SendByte(DGUS_FRAME_HEAD1);
      DGUS_SendByte(DGUS_FRAME_HEAD2);
      DGUS_SendByte(0x05);
    
    
      DGUS_SendByte(DGUS_CMD_W_DATA);                //指令
    
    
      DGUS_SendByte((uint8_t)(DataAddr>>8));         //地址
      DGUS_SendByte((uint8_t)(DataAddr&0xFF));
    
    
      DGUS_SendByte((uint8_t)(Data>>8));             //数据
      DGUS_SendByte((uint8_t)(Data&0xFF));
    }
    

    b.通过消息队列发送

    在上面基础上,用一个buf装下消息,然后“打包”到消息队列,通过消息队列的方式(FIFO)发送出去。

    static uint8_t  sDGUS_SendBuf[DGUS_PACKAGE_LEN];
    
    
    //往DGDS屏指定寄存器写一字节数据
    void DGUS_REG_WriteWord(uint8_t RegAddr, uint16_t Data)
    {
      sDGUS_SendBuf[0] = DGUS_FRAME_HEAD1;           //帧头
      sDGUS_SendBuf[1] = DGUS_FRAME_HEAD2;
      sDGUS_SendBuf[2] = 0x06;                       //长度
      sDGUS_SendBuf[3] = DGUS_CMD_W_CTRL;            //指令
      sDGUS_SendBuf[4] = RegAddr;                    //地址
      sDGUS_SendBuf[5] = (uint8_t)(Data>>8);         //数据
      sDGUS_SendBuf[6] = (uint8_t)(Data&0xFF);
    
    
      DGUS_CRC16(&sDGUS_SendBuf[3], sDGUS_SendBuf[2] - 2, &sDGUS_CRC_H, &sDGUS_CRC_L);
      sDGUS_SendBuf[7] = sDGUS_CRC_H;                //校验
      sDGUS_SendBuf[8] = sDGUS_CRC_L;
    
    
      DGUSSend_Packet_ToQueue(sDGUS_SendBuf, sDGUS_SendBuf[2] + 3);
    }
    
    
    //往DGDS屏指定地址写一字节数据
    void DGUS_DATA_WriteWord(uint16_t DataAddr, uint16_t Data)
    {
      sDGUS_SendBuf[0] = DGUS_FRAME_HEAD1;           //帧头
      sDGUS_SendBuf[1] = DGUS_FRAME_HEAD2;
      sDGUS_SendBuf[2] = 0x07;                       //长度
      sDGUS_SendBuf[3] = DGUS_CMD_W_DATA;            //指令
      sDGUS_SendBuf[4] = (uint8_t)(DataAddr>>8);     //地址
      sDGUS_SendBuf[5] = (uint8_t)(DataAddr&0xFF);
      sDGUS_SendBuf[6] = (uint8_t)(Data>>8);         //数据
      sDGUS_SendBuf[7] = (uint8_t)(Data&0xFF);
    
    
      DGUS_CRC16(&sDGUS_SendBuf[3], sDGUS_SendBuf[2] - 2, &sDGUS_CRC_H, &sDGUS_CRC_L);
      sDGUS_SendBuf[8] = sDGUS_CRC_H;                //校验
      sDGUS_SendBuf[9] = sDGUS_CRC_L;
    
    
      DGUSSend_Packet_ToQueue(sDGUS_SendBuf, sDGUS_SendBuf[2] + 3);
    }
    

    c.用“结构体代替数组SendBuf”方式

    结构体对数组更方便引用,也方便管理,所以,结构体方式相比数组buf更高级,也更实用。(当然,如果成员比较多,如果用临时变量方式也会导致占用过多堆栈的情况)

    比如:

    typedef struct
    {
      uint8_t  Head1;                 //帧头1
      uint8_t  Head2;                 //帧头2
      uint8_t  Len;                   //长度
      uint8_t  Cmd;                   //命令
      uint8_t  Data[DGUS_DATA_LEN];   //数据
      uint16_t CRC16;                 //CRC校验
    }DGUS_PACKAGE_TypeDef;
    

    d.其他更多

    串口发送数据的方式有很多,比如用DMA的方式替代消息队列的方式。

    2.消息数据接收

    串口消息接收,通常串口中断接收的方式居多,当然,也有很少情况用轮询的方式接收数据。

    a.常规中断接收

    还是以DGUS串口屏为例,描述一种简单又常见的中断接收方式:

    void DGUS_ISRHandler(uint8_t Data)
    {
      static uint8_t sDgus_RxNum = 0;                //数量
      static uint8_t sDgus_RxBuf[DGUS_PACKAGE_LEN];
      static portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
    
    
      sDgus_RxBuf[gDGUS_RxCnt] = Data;
      gDGUS_RxCnt++;
    
    
      /* 判断帧头 */
      if(sDgus_RxBuf[0] != DGUS_FRAME_HEAD1)       //接收到帧头1
      {
        gDGUS_RxCnt = 0;
        return;
      }
      if((2 == gDGUS_RxCnt) && (sDgus_RxBuf[1] != DGUS_FRAME_HEAD2))
      {
        gDGUS_RxCnt = 0;
        return;
      }
    
    
      /* 确定一帧数据长度 */
      if(gDGUS_RxCnt == 3)
      {
        sDgus_RxNum = sDgus_RxBuf[2] + 3;
      }
    
    
      /* 接收完一帧数据 */
      if((6 <= gDGUS_RxCnt) && (sDgus_RxNum <= gDGUS_RxCnt))
      {
        gDGUS_RxCnt = 0;
    
    
        if(xDGUSRcvQueue != NULL)                    //解析成功, 加入队列
        {
          xQueueSendFromISR(xDGUSRcvQueue, &sDgus_RxBuf[0], &xHigherPriorityTaskWoken);
          portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
        }
      }
    }
    

    b.增加超时检测

    接收数据有可能存在接收了一半,中断因为某种原因中断了,这时候,超时检测也很有必要。

    比如:用多余的MCU定时器做一个超时计数的处理,接收到一个数据,开始计时,超过1ms没有接收到下一个数据,就丢掉这一包(前面接收的)数据。

    static void DGUS_TimingAndUpdate(uint16_t Nms)
    {
      sDGUSTiming_Nms_Num = Nms;
      TIM_SetCounter(DGUS_TIM, 0);                   //设置计数值为0
      TIM_Cmd(DGUS_TIM, ENABLE);                     //启动定时器
    }
    
    
    void DGUS_COM_IRQHandler(void)
    {
      if((DGUS_COM->SR & USART_FLAG_RXNE) == USART_FLAG_RXNE)
      {
        DGUS_TimingAndUpdate(5);                     //更新定时(防止超时)
        DGUS_ISRHandler((uint8_t)USART_ReceiveData(DGUS_COM));
      }
    }
    

    c.更多

    接收和发送一样,实现方法有很多种,比如接收同样也可以用结构体方式。但有一点,都需要结合你实际需求来编码。

    5最后

    以上自定义协议内容仅供参考,最终用哪些、占用几个字节都与你实际需求有关。

    基于串口的自定义通信协议,有千差万别,比如:MCU处理能力、设备多少、通信内容等都与你自定义协议有关。

    有的可能只需要很简单的通信协议就能满足要求。有的可能需要更复杂的协议才能满足。

    最后强调两点:

    1.以上举例并不是完整的代码(有些细节没有描述出来),主要是供大家学习这种编程思想,或者实现方式。

    2.一份好的通信协议代码,必定有一定容错处理,比如:发送完成检测、接收超时检测、数据出错检测等等。所以说,以上代码并不是完整的代码。

    ------------ END ------------

    后台回复『嵌入式软件设计与开发』『通信』阅读更多相关文章。

    欢迎关注我的公众号回复“加群”按规则加入技术交流群,回复“1024”查看更多内容。

    欢迎关注我的视频号:

    点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。

    展开全文
  • 为解决串口通信中的数据传输容易出错、可靠性差、安全性不高且容错能力低等问题,设计并实现了一种基于状态机的串口通信协议,并将此协议应用到称重仪表的上位机通信中。本文介绍了串口通信协议的数据包格式以及其...
  • 基于双机互联系统的SPCP串口通信协议编写的串口通信底层类源代码
  • nrf52_rv1126_串口通信协议V1.01
  • 自定义的串口通信协议

    千次阅读 2022-01-17 00:40:49
    自定义一主多从通讯协议
  • 串口通信协议源代码篇一:串口通信源代码#include#define uchar unsigned charuchar code table[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; uchar a,b,flag;.//定义...
  • UART异步串口通信协议,不错的例子可以学学
  • 什么是串口通信协议

    千次阅读 2020-10-10 10:25:23
    ARM体系结构-串口通信 一、什么是串口通信 1、串口通信属于基层基本性的通信规约,收发双方事先规定好通信参数。 2、它自己本身不会去协商通信参数,需要通信前通信双方事先约定好通信参数来进行通信。 3、因此,...
  • QT当中的串口通信你了解多少,平时接触的代码中的串口通信是不是让你手足无措,波特率、奇偶效验位、停止位、数据位怎么收发?射频识别的工作原理和代码编写,本章必须拿下
  • Seasky串口通信协议1

    2022-08-03 18:08:31
    1. 通信协议格式 2. 帧头详细定义 3. cmd_id 命令码 ID 说明(字节偏移 4,字节大小 2) 6. 使用待发送数据更新获取发送数据帧 7. 接收
  • 串口通信协议的制定方法,如何自己定制通信协议

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 47,478
精华内容 18,991
关键字:

串口通信协议

友情链接: narrowband.rar