• 串口数据包收发


    数据包

    把属于同一批的数据进行打包和分割,方便接收方进行识别

    HEX数据包

    思路:一个数据规定四个字节,以0xFF为包头,0xFE为包尾,当检测到0xFF时,接下来四个数据就是数据,接收到0xFE时,置一个接收完毕标志位

     这样存在几个问题需要解决:

    问题1:包头包尾和数据载荷重复的问题

    解决方法:

    一、限制载荷数据的范围,不超过包头包尾

    二、严格限制数据包的长度

    三、增加包头包尾的数量,且组合方式为载荷数据不会出现的情况

    问题2:包头包尾并不是全都需要的,可以只要包头不要包尾(只能用于固定包长的情况)

    问题3:各种数据转换为字节流的问题,这里的数据包都是一个个字节组成的,如果想发送16位的整形数据,32位的整形数据,float,double,甚至是结构体,其实都没问题,因为他们内部也都是由一个个字节组成的,只需要用uint8_t的指针指向它,把它们当做一个字节数组发送就行了

    文本数据包

    两者优缺点 

    HEX数据包:传输最直接,解析数据非常简单,比较适合一些模块发送原始的数据,比如一些使用串口通信的陀螺仪、温湿度传感器,缺点是灵活性不足、载荷容易和包头包尾重复;

    文本数据包:数据直观,易理解,非常灵活,比较适合一些输入指令进行人机交互的场合,比如蓝牙模块常用的AT指令,CNC和3D打印机常用的G代码,缺点是解析效率低

    数据包的发送

    HEX数据包的发送:定义一个数组,填充数据,然后用Send函数一发即可

    文本数据包的发送:定义一个字符串.......

    数据包的接收

    如何接收固定包长的HEX数据包

     在之前的代码中,串口每接收到一个数据,程序都会进入一个中断,在中断中获取到这一个字节,在这之后会退出中断,所以每拿到一个数据,都是一个独立的过程,对于数据包来说,很明显它具有前后关联性——包头之后是数据,数据之后是包尾,对应包头,数据和包尾这三种状态,我们都需要有不同的处理逻辑,在程序中我们需要设计一个能记住不同状态的机制,在不同状态执行不同的操作,同时还要进行状态的合理转移,这种程序思维被称为“状态机”。

    如图是状态转移图,我们设定三种状态——1、等待包头,2、接收数据,3、等待包尾

    等待包头状态下,S=0,直到接收到0xFF时,把S置1,然后进入接收数据状态,再然后直到收集满4个数据,并把数据存储到数组中后,把S置2,然后进入等待包尾状态,直到接收到包尾0xFE后,把S置0,进入等待包头状态。

    不固定包长的文本数据包接收

    代码实操 

    串口收发HEX数据包

    发:

    1. //发送数据包
    2. void Serial_SendPacket(void)
    3. {
    4. Serial_SendByte(0xFF);
    5. Serial_SendArray(Serial_TXPacket, 4);
    6. Serial_SendByte(0xFE);
    7. }
    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "Serial.h"
    5. uint16_t Data;
    6. int main(void)
    7. {
    8. OLED_Init();
    9. Serial_Init();
    10. Serial_TXPacket[0]=0x01;
    11. Serial_TXPacket[1]=0x02;
    12. Serial_TXPacket[2]=0x03;
    13. Serial_TXPacket[3]=0x04;
    14. Serial_SendPacket();
    15. while(1)
    16. {
    17. }
    18. }

    收:

    1. uint8_t Serial_RXFlag;
    2. uint16_t Serial_TXPacket[4];
    3. uint16_t Serial_RXPacket[4];
    4. //用于获取自建的标志位
    5. uint8_t Serial_GetRXFlag(void)
    6. {
    7. if (Serial_RXFlag == 1)
    8. {
    9. //检测标志位位1后立马清零
    10. //以便下次获取串口接收值
    11. Serial_RXFlag = 0;
    12. return 1;
    13. }
    14. return 0;
    15. }
    16. //中断函数
    17. void USART1_IRQHandler(void)
    18. {
    19. static uint8_t RXState = 0;
    20. static uint8_t pRXPacket = 0;
    21. if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
    22. {
    23. uint8_t RXData = USART_ReceiveData(USART1);
    24. if (RXState == 0)
    25. {
    26. if (RXData == 0xFF)
    27. {
    28. RXState = 1;
    29. //给接收数据的数组下标清零
    30. //在这清零能保证每次接收数据时下标正确
    31. pRXPacket = 0;
    32. }
    33. }
    34. else if (RXState == 1)
    35. {
    36. //把接收到的数据存入数组中
    37. Serial_RXPacket[pRXPacket] = RXData;
    38. pRXPacket ++;
    39. if (pRXPacket >= 4)
    40. {
    41. RXState = 2;
    42. }
    43. }
    44. else if (RXState == 2)
    45. {
    46. if (RXData == 0xFE)
    47. {
    48. RXState = 0;
    49. Serial_RXFlag = 1;
    50. }
    51. }
    52. USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    53. }
    54. }

    两个数组还要在.h文件中加上extern,以便在主函数中直接修改或者调用其值

     

    即使载荷数据和包头包尾重复都没有影响

    隐藏的问题

    RXPacket是一个同时被写入又同时被读出的数组,在中断函数中,我们会依次写入它,在主函数中,我们又依次读出它,这会造成数据包之间的数据混在一起,比如读出的过程太慢了,可能会造成前面两个数据是新的,后面两个数据是之前的数据,即我们读出的数据可能一部分属于上一个数据包,解决办法:在接收部分加入判断,在每个数据包读取处理完毕后,再接收下一个数据包(线程安全),但其实这个问题也是相对实际情况而言的,可以不处理,也可能必须要处理。

    再添加一个功能——按下按键TXPacket中的数据都+1,用于检测发出数据包程序是否正确运行

    在主函数中修改即可,顺便优化代码

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "Serial.h"
    5. #include "Key.h"
    6. uint16_t Data;
    7. int main(void)
    8. {
    9. OLED_Init();
    10. Key_Init();
    11. Serial_Init();
    12. OLED_ShowString(1, 1, "TXPacket");
    13. OLED_ShowString(3, 1, "RXPacket");
    14. Serial_TXPacket[0]=0x01;
    15. Serial_TXPacket[1]=0x02;
    16. Serial_TXPacket[2]=0x03;
    17. Serial_TXPacket[3]=0x04;
    18. Serial_SendPacket();
    19. while(1)
    20. {
    21. if (Key_GetNum()==1)
    22. {
    23. Serial_TXPacket[0]++;
    24. Serial_TXPacket[1]++;
    25. Serial_TXPacket[2]++;
    26. Serial_TXPacket[3]++;
    27. Serial_SendPacket();
    28. OLED_ShowHexNum(2, 1, Serial_TXPacket[0], 2);
    29. OLED_ShowHexNum(2, 4, Serial_TXPacket[1], 2);
    30. OLED_ShowHexNum(2, 7, Serial_TXPacket[2], 2);
    31. OLED_ShowHexNum(2, 10, Serial_TXPacket[3], 2);
    32. }
    33. if (Serial_GetRXFlag() == 1)
    34. {
    35. OLED_ShowHexNum(4, 1, Serial_RXPacket[0], 2);
    36. OLED_ShowHexNum(4, 4, Serial_RXPacket[1], 2);
    37. OLED_ShowHexNum(4, 7, Serial_RXPacket[2], 2);
    38. OLED_ShowHexNum(4, 10, Serial_RXPacket[3], 2);
    39. }
    40. }
    41. }

    就可以实现按一下按钮,串口就输出一组数据包,且这组数据包每个数据逐渐递增

    然后在发送区发送的数据在OLED中显示

    串口收发文本数据包

    接收数据包直接使用SendString即可,所以可以把TXPacket相关函数删去

    然后修改一下中断函数的判断条件

     

    1. //以下皆用于接收数据包
    2. uint8_t Serial_RXFlag;
    3. char Serial_RXPacket[100];
    4. //用于获取自建的标志位
    5. uint8_t Serial_GetRXFlag(void)
    6. {
    7. if (Serial_RXFlag == 1)
    8. {
    9. //检测标志位位1后立马清零
    10. //以便下次获取串口接收值
    11. Serial_RXFlag = 0;
    12. return 1;
    13. }
    14. return 0;
    15. }
    16. //中断函数
    17. void USART1_IRQHandler(void)
    18. {
    19. static uint8_t RXState = 0;
    20. static uint8_t pRXPacket = 0;
    21. if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
    22. {
    23. char RXData = USART_ReceiveData(USART1);
    24. if (RXState == 0)
    25. {
    26. if (RXData == '@')
    27. {
    28. RXState = 1;
    29. //给接收数据的数组下标清零
    30. //在这清零能保证每次接收数据时下标正确
    31. pRXPacket = 0;
    32. }
    33. }
    34. else if (RXState == 1)
    35. {
    36. if (RXData == '\r')
    37. {
    38. RXState = 2;
    39. }
    40. else
    41. {
    42. //把接收到的数据存入数组中
    43. Serial_RXPacket[pRXPacket] = RXData;
    44. pRXPacket ++;
    45. }
    46. }
    47. else if (RXState == 2)
    48. {
    49. if (RXData == '\n')
    50. {
    51. RXState = 0;
    52. //标志位'\0'标志着字符串的结束
    53. Serial_RXPacket[pRXPacket] = '\0';
    54. Serial_RXFlag = 1;
    55. }
    56. }
    57. USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    58. }
    59. }

    主函数中实验一下

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "Serial.h"
    5. #include "LED.h"
    6. #include
    7. int main(void)
    8. {
    9. OLED_Init();
    10. LED_Init();
    11. Serial_Init();
    12. OLED_ShowString(1, 1, "TXPacket");
    13. OLED_ShowString(3, 1, "RXPacket");
    14. while(1)
    15. {
    16. if (Serial_GetRXFlag() == 1)
    17. {
    18. OLED_ShowString(4, 1, " ");
    19. OLED_ShowString(4, 1, Serial_RXPacket);
    20. }
    21. }
    22. }

    接下来就应该实现通过串口输入文本来控制LED的亮灭,并通过OLED上有所反映

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "Serial.h"
    5. #include "LED.h"
    6. #include
    7. int main(void)
    8. {
    9. OLED_Init();
    10. LED_Init();
    11. Serial_Init();
    12. OLED_ShowString(1, 1, "TXPacket");
    13. OLED_ShowString(3, 1, "RXPacket");
    14. while(1)
    15. {
    16. if (Serial_GetRXFlag() == 1)
    17. {
    18. OLED_ShowString(4, 1, " ");
    19. OLED_ShowString(4, 1, Serial_RXPacket);
    20. if(strcmp(Serial_RXPacket, "LED_ON") == 0)
    21. {
    22. LED_On();
    23. OLED_ShowString(2, 1, " ");
    24. OLED_ShowString(2, 1, "LED_ON_OK");
    25. Serial_SendString("LED_ON_OK\r\n");
    26. }
    27. else if (strcmp(Serial_RXPacket, "LED_OFF") == 0)
    28. {
    29. LED_Off();
    30. OLED_ShowString(2, 1, " ");
    31. OLED_ShowString(2, 1, "LED_OFF_OK");
    32. Serial_SendString("LED_OFF_OK\r\n");
    33. }
    34. else
    35. {
    36. OLED_ShowString(2, 1, " ");
    37. OLED_ShowString(2, 1, "Error");
    38. Serial_SendString("Error\r\n");
    39. }
    40. }
    41. }
    42. }

    这样就可以实现目标了

    但是还会有个问题,如果连续发送数据包,程序处理不及时,可能会导致数据包错位,这时候我们就需要添加一个程序使其在上一个数据包未处理完成,就不接受新的数据包的功能。

    只需把Serial_GetRXFlag(void)函数删去,在中断函数的第一个判断语句中修改为

    1. if (RXData == '@' && Serial_RXFlag == 0)
    2. {
    3. RXState = 1;
    4. //给接收数据的数组下标清零
    5. //在这清零能保证每次接收数据时下标正确
    6. pRXPacket = 0;
    7. }

    然后再在主函数if条件修改为

    if (Serial_RXFlag == 1)

    然后再在其最后添加

    Serial_RXFlag = 0;

    即可

    或者可以再定义一个指令缓存区,把接收好的字符串放在这个指令缓存区里排队,这样处理起来更有条理

  • 相关阅读:
    华纳云服务器怎么清理cdn缓存?
    【Docker】Docker File
    Pandas数据预处理python 数据分析之4——pandas 预处理在线闯关_头歌实践教学平台
    arcgis插件 批量出图 按地块批量出图工具
    vue实现全局消息提醒功能(vue-extend)
    JavaWeb基础(一)——Tomcat介绍
    Spring IoC、容器初始化、对象
    【Python】使用Python库中的pymysql执行SQL
    linux命令:netstat ---net-tools安装
    Tomcat部署与优化
  • 原文地址:https://blog.csdn.net/m0_74460550/article/details/133501483