精华内容
下载资源
问答
  • 自定义串口通信协议,如何实现?

    千次阅读 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”查看更多内容。

    欢迎关注我的视频号:

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

    展开全文
  • 有一些初学者总觉得通信协议是一个很复杂的知识,把它想的很高深,导致不知道该怎么学。同时,偶尔有读者问关于串口自定义通信协议相关的问题,今天就来写写串口通信协议,并不是你想想中的那么难?1什...

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

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

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

    1.用它,只需几个简单指令就能开发俄罗斯方块游戏!

    2.芯片公司变成钱多多,制造企业真能买得起单?

    3.为什么说i.MXRT不走传统MCU套路?从外设寄存器访问设计谈起

    4.该不该用RTOS?

    5.移植mbedtls库到MCU裸机的两种方法

    6.低耦合高内聚的MCU实用软件框架~

    展开全文
  • 上位机 数据采集 工业控制 上位机软件 工控软件 数据采集处理 分析 存储 显示 导出 报表打印 标准modbus 协议通信 自定义协议通信 plc通信 手机端APP定制 机器视觉opencv halcon等软件定制。

    上位机 数据采集 工业控制 上位机软件 工控软件
    数据采集处理 分析 存储 显示 导出 报表打印
    标准modbus 协议通信 自定义协议通信 plc通信
    手机端APP定制
    机器视觉opencv halcon等软件定制。

    请添加图片描述
    请添加图片描述
    请添加图片描述
    请添加图片描述
    请添加图片描述
    请添加图片描述
    请添加图片描述

    展开全文
  • STM32串口自定义数据接收协议

    千次阅读 2021-09-10 16:20:16
    单片机和4G通过串口通信,当4G与平台连接之后,在保证数据在平台与4G模块之间能正常流转的情况下,可视为单片机使用串口直接与平台进行通信。而原子亦给出了串口通信的相关例程,其中包含串口收发实验,可做参考。 1...

    写在前面

    最近使用STM32做串口数据收发,遇到了一些问题。折腾了一番,在此记录一下。

    0 需求

    1. 云平台通过“发布消息”,下行指令。
    2. 4G模块接收平台下行指令并转发到单片机,单片机通过串口(UART3)做数据接收与分析。
      总的来说,比较简单。单片机和4G通过串口通信,当4G与平台连接之后,在保证数据在平台与4G模块之间能正常流转的情况下,可视为单片机使用串口直接与平台进行通信。而原子亦给出了串口通信的相关例程,其中包含串口收发实验,可做参考。

    1 问题产生

    为了实现需求,先进行两个小实验
    3. 模块+上位机实验 : 验证4G模块与平台之间数据收发正常。
    4. 电路板串口数据接收实验 :排除电路板硬件异常问题。

    1.1 模块+上位机实验

    平台发布消息,模块TX引脚输出。通过CH340与串口调试助手相连,接收并显示数据。验证模块能否正常收发数据。
    在这里插入图片描述
    ---------------------补一张串口接收数据图--------------

    可以正常收发数据,排除模块问题。
    ASCII编码对照表_911查询

    1.2 电路板串口数据接收实验

    实验硬件:UART1(COM3) , UART3(COM1)实验。
    实验现象:UART1 接收到的消息通过UART1打印到串口;UART3 接收到的消息亦通过UART1打印到串口。(UART1做串口调试使用)
    实验结论: 排除电路板硬件异常原因,UART1 & UART3 可以正常收发数据。
    示例代码:

    while(1)
    {
    		
       if(USART_RX_STA&0x8000)
    	{		
    		LED0 = 0;			 //点亮LED显示接收到消息	
    		len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度
    		printf("\r\nUART1发送的消息为:\r\n");
    		HAL_UART_Transmit(&UART1_Handler,(uint8_t*)USART_RX_BUF,len,1000);	//发送接收到的数据
    		while(__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_TC)!=SET);		//等待发送结束
    		printf("\r\n\r\n");//插入换行
    		USART_RX_STA=0;
    	}
    	else if(USART3_RX_STA&0x8000)
    	{
    		LED0 = 1;			 //熄灭LED显示接收到消息	
    		len=USART3_RX_STA&0x3fff;//得到此次接收到的数据长度
    		printf("\r\nUART3发送的消息为:\r\n");
    		HAL_UART_Transmit(&UART1_Handler,(uint8_t*)USART3_RX_BUF,len,1000);	//发送接收到的数据
    		while(__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_TC)!=SET);		//等待发送结束
    		printf("\r\n\r\n");//插入换行
    		USART3_RX_STA=0;
    	} 
    }	
    

    在这里插入图片描述

    1.3 问题来了!

    当以上两个小测试完成时,可以确定整个系统软件与硬件无误。也就是说,当电路板上插上4G模块时,即可实现单片机与平台通信的功能!
    然而,当平台发布数据时,单片机未能接收到数据。通过软件调试等操作发现,UART3并未进入到接收到数据中断。用示波器测量模块TX引脚,平台发布消息时确实有信号输出!迷惑行为,,,,,,

    2 开始分析

    2.1 串口数据格式

    如下图,可以看出

    1. 无数据时,电平始终为‘1’
    2. 起始位为1位‘0’,停止位为1位‘1’
      在这里插入图片描述
      串口助手配置为:
      bound :115200 ;停止位 :1 ;数据位 :8 ; 检验位 : None
      在这里插入图片描述

    2.2 测一波波形

    以上两个小测试确实可以验证软件与硬件无误。那么为什么单片机可以接收上位机传来的数据,而不能接收4G模块转发来的数据呢?二者数据有何区别?
    验证方法 : 上位机和平台同时发送信息,测其输出信号。测试数据 11(0011_0001 0011_0001)

    • 注 :串口助手及平台发布的数据为字符(ASCII),助手可选择hex发送及显示。
      1. 平台发布数据 11(0011_0001 0011_0001) ,4G转发数据波形如图
      在这里插入图片描述
      **波形分析:**非常漂亮的波形,可以读出数据为 0011_0001 0011_0001(起始位为1位‘0’,停止位为1位‘1’)
      2. 串口助手通过CH340发送数据 11(0011_0001 0011_0001)
      在这里插入图片描述
      CH340 的 TX 引脚输出波形如下:X10_1000_1100_10_1000_1100_10_1011_0000_10_0101_0000_1X
      在这里插入图片描述
      参考串口数据格式,可知:串口发送的数据为:0011_0001 , 0011_0001 , 0000_1101 , 0000_1010

    **波形分析:**前两个数据值为32,对应的字符为 ‘1’,‘1’,与发送数据一致。多了后两个值13 10,查询ASCII码为 0D 0A。对应换行键和归位键,嗯?
    在这里插入图片描述
    查看串口助手,果然!默认勾选了“发送新行”
    在这里插入图片描述
    再次测试,勾选了“发送新行”数据可以被接收;不勾选了“发送新行”,数据不被接收!可以看出“发送新行”即为单片机识别数据的“校验”格式。那么程序中一定有与“校验”相关的代码,那就找到他!

    • 测波形时遇到一个现象,在这记录一下。

    当直接接CH340输出时,图形在上。可以看到高电平只有1,5V左右,电压驱动并不强;当CH340与单片机相连时,发送波形测得的波形如下。当时怀疑过是高电平的问题,在单片机上接了个上拉电阻,波形是好看了,但依旧没有解决问题。因为串口低电平有效,并不要求严格的高电平。
    在这里插入图片描述

    3 代码分析

    关于串口接收的代码如下,一眼就可看到关于0x0a,0x0d的判断,确认是结尾校验无误了。若要修改为 ‘**’ 校验,改为0x2a,0x2a即可。修改后测试成功,没图。

    if(huart->Instance==USART3)//如果是串口1
    {
    	if((USART3_RX_STA&0x8000)==0)//接收未完成
    	{
    		if(USART3_RX_STA&0x4000)//接收到了0x0d
    		{
    			if(aRx3Buffer[0]!=0x0a)USART3_RX_STA=0;//接收错误,重新开始
    			else USART3_RX_STA|=0x8000;	//接收完成了 
    		}
    		else //还没收到0X0D
    		{	
    			if(aRx3Buffer[0]==0x0d)USART3_RX_STA|=0x4000;
    			else
    			{
    				USART3_RX_BUF[USART3_RX_STA&0X3FFF]=aRx3Buffer[0] ;
    				USART3_RX_STA++;
    				if(USART3_RX_STA>(USART_REC_LEN-1))USART3_RX_STA=0;//接收数据错误,重新开始接收	  
    			}		 
    		}
    	}
    }
    
    • 注 :关于0x0a,0x0d 网上也有不少资料参考。原子的串口助手也有提示正确格式。由于很少使用16进制发送,一致没有注意到!
      在这里插入图片描述

    4 新的问题:串口数据累加

    新的问题收测试时出现寄存器数据累加情况,具体表现为:

    1. 当串口UART3接收到的数据未加结束校验“**”,单片机未能判断数据接收完毕。
    2. 当下次数据来临(带校验),单片机判断数据发送完毕。通过串口1将数据输出。
      在这里插入图片描述
      再使用例程时,取消"发送新行",会出现同样的问题,由此可以判断是底层代码问题。
      在这里插入图片描述
      猜测:串口在接收未加校验的数据时,已经将数据存入串口接收缓冲buff中。当之后数据(带校验)来临时,继续将数据存入buff,并判断数据接收完毕。此时,多次发送的数据集中在同一buff中,数据为及时清空,由此导致数据累加的情况。
      再来分析这段代码:
      其中,USART3_RX_STA 是串口接收状态标记。定义为: u16 USART_RX_STA=0; 功能如下
      | bit15 | bit14 | bit13~0 |
      | ---------------- | -------------- | -------------------- |
      | 接收完成标志0x0a | 接收到0X0d标志 | 接收到的有效数据个数 |
    if(huart->Instance==USART3)	//如果是串口3
    {		
    	if((USART3_RX_STA&0x8000)==0)		//接收未完成  USART3_RX_STA最高位判断
    	{
    		if(USART3_RX_STA&0x4000)		//接收到了第一个0x2a  USART3_RX_STA次高位判断
    		{
    			if(aRx3Buffer[0]!=0x0a)
    			{
    				USART3_RX_STA=0;		//接收错误,重新开始
    			}
    			else USART3_RX_STA|=0x8000;	//接收完成了 
    		}
    		else 							//还没接收到第一个0x2a
    		{	
    			if(aRx3Buffer[0]==0x0d)
    			{
    				USART3_RX_STA|=0x4000;
    			}
    			else
    			{
    				USART3_RX_BUF[USART3_RX_STA&0X3FFF]=aRx3Buffer[0] ;  //0X3FFF  USART3_RX_STA 低14位是数据
    				USART3_RX_STA++;
    				if(USART3_RX_STA>(USART_REC_LEN-1))
    				{
    					USART3_RX_STA=0;//接收数据错误,重新开始接收
    				}	  
    			}		 
    		}
    	}
    }
    

    大致画了下流程图,时间关系。不再文字分析了,几个判断嵌套。串口通信实验讲解里关于USART_RX_STA的问题与思考这篇博客文字分析比较详细,推荐一波。
    在这里插入图片描述
    可以看出,与猜测一致。串口接收缓冲buff并没有进行数据清除。当数据(带校验)未临时,之前的数据会一直寄存在buff中,直到最终发送完毕。(当然数据累加有一定上限,USART_REC_LEN)

    若要消除数据累加的情况,就必须在接收完一次不带校验的数据后,及时清除缓冲buff。实际使用中,两次接收数据之间有一定间隔,若能在间隔之中清除buff,即可规避。故加入以下代码,测试一下。

    	/* 间隔一定时间清空串口缓冲BUF USART3_RX_BUF */
    		if (time_10_ms)		 //定时器 10ms
    		{ 
    			time_10_ms = 0 ;
    			memset(USART3_RX_BUF, 0, sizeof USART3_RX_BUF);  
    		}
    

    在这里插入图片描述
    测试结果:不加校验的数据串口不识别,定时清空处理,无数据累加情况。测试ok!

    总结

    没啥写的,强迫症凑一凑。

    展开全文
  • 安卓app客户端与服务器通信协议 内容精选换一换WebSocket简介传统的客户端和服务器通信协议是HTTP:客户端发起请求,服务端进行响应,服务端从不主动勾搭客户端。这种模式有个明显软肋,就是同步状态。而实际应用中...
  • 西门子PLC300串口通信协议比较与分析通信方式一:CP341通信模块的通信方式CP341有三种不同的接口模式,分别为:CP341-RS232C,CP341-20mATTYandCP341-RS422/485,CP341本身支持三种不同的双向通讯协议:RK512通信...
  • BLE通信协议

    2021-06-07 16:38:00
    } // 校验字节从“命令”字节开始到“数据”结束 // 可以用格西CRC计算器1.0 软件验证 附: 需要增加“开启/关闭闹钟命令”,及回复命令;获取计步数据的命令的数据标示位没有,是否需要增加。
  • 充电桩与云服务器通信协议 内容精选换一换华为云弹性负载均衡( Elastic Load Balance)将访问流量自动分发到多台云服务器,扩展应用系统对外的服务能力,实现更高水平的应用容错。华为云ELB包含独享性实例和共享型...
  • 文档介绍:文档名称WIFI模块串口通信协议文档文件编号版本号编制:审核:批准:编制时间:审核时间:批准时间:WIFI模块串口通信协议版本历史版本号修订日期修订人参与者修订内容备注目录WIFI模块串口通信协议1版本历史2...
  • python网络编程之socket

    2021-02-03 07:47:04
    一、socket是什么在TCP/IP五层协议中,工作在应用层的软件程序要想把它的数据发送给网络另一端的计算机并让那台计算机能把接收到的数据正常解析出来传递给对应的程序就需要按照互联网协议在数据的前面依次加上每一层...
  • 数据通信协议规程.doc

    2021-08-14 01:24:57
    数据通信协议规程PAGEPAGE 19数据通信协议规程篇一:多机通信协议规范波特率受到限制,所以,一般的串行打印机工作在110波特率,点针式打印机由于其内部有较大的行缓冲区,所以可以按高达2400波特的速度接收打印信息...
  • 从websocket协议到tcp自定义协议,tcp分包与粘包,明文传输 徒手实现网络协议栈,请准备好环境,一起来写代码 服务器开发岗位面试必问的UDP技术点 WebSocket和Socket区别 可以把WebSocket想象成HTTP(应用层),HTTP和...
  • 软件部分由测试设计软件模块、测试执行服务软件模块、测试执行客户端软件模块、设备资源管理软件模块等主要软件模块以及曲线数据生成、CRC插件生成与诊断、测试数据记录与查看、应用协议生成工具、应用协议模板管理...
  • 即便如此,我们也只是实现了客户端一侧的进程间通信,而要实现与完整聊天系统中另一端的角色——服务端的通信,则需依靠「网络通信协议」来协助完成,在此我们选用的是WebSocket协议。 什么是WebSocket? WebSocket...
  • (一)串口通信协议简介 (二)物理层 2.1、电平标准 2.2、RS-232 信号线 (三)协议层 3.1、波特率 3.2、通讯的起始和停止信号 3.3、有效数据 3.4、有效检验 (四) STM32 的 USART 简介 4.1、USART 功能...
  • http客户端和服务器通信协议 内容精选换一换TCP :传输控制协议(TCP),是互联网协议组的主要协议之一。它起源于最初的网络实施,在网络实施中,它补充了互联网协议。UDP: 用户数据报协议(User Datagram Protocol)是...
  • 的特征值 charX基础上,添加一个用户自定义通信协议,并用安卓手机蓝牙app (这里用的是BLE调试宝软件)发送指令实现 LED 点灯,并且有状态值通知返回到调试软件上。 实验平台 CC2640R2F 平台 ①协议栈版本:...
  • OPC UA通信协议说明

    2020-12-19 10:20:56
    OPC现已成为工业界系统互联的缺省方案,为工业监控编程带来了便利,用户不用为通讯协议的难题而苦恼。任何一家自动化软件解决方案的提供者,如果它不能全方位地支持OPC,则必将被历史所淘汰。 1、在控制领域中,系统...
  • 客户端界面: 服务器界面: 协议设计:
  • 水表下行通信规约 188 协议V1.5 标准版(集中器/系统终端/掌上电脑与智能总线表计通讯协议)第 1 章 概述本规范是集中抄表系统下行接口通讯协议(除少部分自定义部分外,均参照 CJ/T 188-2004 中华人民共和国城镇建设...
  • ROS下注意事项package.xml自定义通信数据接口话题通信服务通信 CMakeLists.txt 什么是Cmake? CMake 是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的 ...
  • 软件需求:力控组态软件 力控 ForceControl V7.0 。优势:无需虚拟串口软件做转发2:...4:串口服务器的设置,进入网页,设置模块的波特率和通信方式。4:组态软件的设置.(1)新建工程。(2)IO 设备组态。建立一个MODB...
  • 前言上篇文章讲了网络通信的基本知识,这篇文章讲一下如何用udp协议进行网络通信。废话不多说,看下面教程吧!socket想要进行网络通信,首先要...udp协议udp协议是众多网络通信协议中的其中一种,全名叫User Datagra...
  • OBD-II故障码分类 搞定了通讯协议,再加上有了软件和硬件作为基础,诊断接口作为信息交互端口。 接下来我们就可以让它开启工作模式,比如进行故障码的读取,这也是我们在故障维修中的必做项。 一般情况下,故障码...
  • 系统间通信【MQ-消息协议】 1、概述 从本文开始,我们介绍另一类型的系统间通讯及输:MQ消息队列。首先我们将讨论几种常用消息队列协议的基本原理和工作方式,包括MQTT、XMPP、Stomp、AMQP、OpenWire等。然后在...
  • 优势:无需虚拟串口软件即可通信,2:串口设备采用MODBUS协议的温湿度变送器。MODBUS RTU 产品采用温湿度传感器读取温湿度指令是 11 04 00 00 00 02 73 5B3:将温湿度传感器串口USR-410串口服务器的串口接口上。4:...
  •  前面博客大部分介绍了基于EMQ中间件,通信协议使用的是MQTT,而传输的数据为纯文本数据,采用JSON格式。这种方式,大部分一看就知道是熟悉Web开发、软件开发的人喜欢用的方式。由于我也是做web软件开发的,也是...
  • 如何从零设计一种物联网组网协议

    千次阅读 多人点赞 2021-05-24 15:18:35
    不看这几篇好文,就别说自己了解物联网 ...本文将从物联网通信协议现状分析入手,讲述如何从零设计一种物联网组网协议。 1、物联网通信技术分类 物联网的通信技术有很多种,首先从大类上分为...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 61,273
精华内容 24,509
关键字:

自定义软件通信协议