首页 > 其他分享 >对串口接收时发送数据会导致被接收的数据错位乃至丢失的原理解读

对串口接收时发送数据会导致被接收的数据错位乃至丢失的原理解读

时间:2024-12-01 16:32:46浏览次数:7  
标签:HAL UART RX huart 串口 发送数据 接收

缘由:

在进行STM32与ESP8266通信时,我在接收中断中使用了HAL_UART_Transmit与printf函数,发现ESP8266返回来的数据有一定的丢失与错位现象,在对逻辑进行改写后发现问题与HAL库本身函数有关,这引起了我对其的兴趣。

原理:

我使用了串口空闲中断接收数据,而HAL_UART_Transmit在底层调用了一个接收中断打断函数,将丢失数据对应的接收中断位清除,使得该数据未被单片机接收,放在开发者眼中就是接收数据出现了错位与丢失。

实践过程:

起初,我在串口的空闲接收中断内将接收到的数据发送至调试串口,具体代码如下

可是这段代码却有一个巨大乃至无法忽视的BUG:使用该种方式接收的数据出现了字节错位乃至丢失这种堪称致命的BUG,具体情况如下图所示:

    

int HAL_uart_IDLE(UART_HandleTypeDef *huart, uint8_t *RX_Buffer, uint8_t *RX_GetData, uint8_t *RX_GetFlag, uint16_t RX_count)
{
  // static uint8_t RX_count = 0;
  // uint16_t i = 0;
  uint8_t ch = RX_Buffer[0];
  if (__HAL_UART_GET_IT_SOURCE(huart, UART_IT_IDLE) == RESET)
  {
    return 0;
  }

  __HAL_UART_CLEAR_IDLEFLAG(huart);
  __HAL_UART_DISABLE_IT(huart, UART_IT_IDLE);

  if (RX_GetFlag)
  {
    HAL_UART_Receive_IT(huart, RX_Buffer, 1);
    RX_GetFlag = RESET;
    goto move_RX_data;
  }

  move_RX_data:
  {
    if (Serial_RxFlag && ((RX_count<80)&&(RX_count>42)))
		{
			RX_GetData[RX_count-43] = ch;
		}
    //HAL_UART_Transmit(&huart1,&ch,1,1);
    //printf("%c",ch);
    if (ESP8266_Fram_Record_Struct.InfBit.FramLength < (RX_BUF_MAX_LEN - 1))
    {
      ESP8266_Fram_Record_Struct.Data_RX_BUF[ESP8266_Fram_Record_Struct.InfBit.FramLength++] = ch;
    }
    
    return 1;
  }
}//串口空闲中断接收处理函数

     图1 在接收中断中使用HAL_UART_Transmit向上位机发送调试数据

图2  在接收中断中使用printf向上位机发送调试数据

我们可以很直观的看到:图1与图2都有着不同程度的数据丢失现象,且图1 的严重程度远大于图2,在我的一番改良之后,我解决了数据的丢失问题,但是数据为什么会丢失呢?从以下几个图中,我似乎发现了这个现象的原因。这个现象在我当时看来是因为HAL_UART_Transmit与printf的运行时间不一样,我认为后者比前者要快,不然如何解释在调试串口波特率略大于数据接收串口的情况下,图4出现了数据丢失而图5却没有任何数据丢失呢?

图3  在调试串口波特率(115200)略大于接收串口波特率(100000)的情况下,使用HAL_UART_Transmit向上位机发送调试数据

图4  在图3的情况下,使用printf向上位机发送调试数据

图5  在图1与图2 的架构上进行的针对性升级,彻底解决了数据丢失现象

因为我根据以上结果得出了printf比HAL_UART_Transmit要快,所以我使用了仿真调试去尝试证明我的猜想,但是我得到的结果与我的猜想大相径庭,printf跟HAL_UART_Transmit相比,在同样持续运行5000次的结果下,前者仅比后者快上2微秒。具体结果如图所示:

void A_TEST()
{
  for (d = 0; d < 5000; d++)
  {
    printf("a");
  }
}
void A_TEST3()
{
  for (d = 0; d < 5000; d++)
  {
    HAL_UART_Transmit(&huart1,&ch,1,1);
    //HAL_UART_Transmit_IT(&huart1,&ch,1);
  }
}
void A_TEST1()
{
  for (d = 0; d < 1; d++)
  {
    printf("a");
  }
}
void A_TEST2()
{
  for (d = 0; d < 1; d++)
  {
    HAL_Delay(1000);
  }
}//测试代码


void main()
{
    A_TEST2();
    A_TEST1();
    A_TEST();
    A_TEST3();
}//测试函数具体调用顺序

图6  函数的执行时间,与上述代码的函数顺序一一对应

这个结果推翻我先前的所有假设:2微秒的时间对于一个5000字节的数据集合来说根本不会产生什么质的变化,更别提这个误差很有可能是因为仿真器的测试误差出现的,因此,我对该现象的研究进入了瓶颈。

我不禁思索:如果不是因为时间,那是因为什么呢?

在这样的困扰下,我开始查看HAL库的底层代码,很幸运的是,我发现了数据丢失现象的根源。具体代码及分析放在下面。

 while (huart->TxXferCount > 0U)
    {
      if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
      {
        huart->gState = HAL_UART_STATE_READY;

        return HAL_TIMEOUT;
      }
      if (pdata8bits == NULL)
      {
        huart->Instance->DR = (uint16_t)(*pdata16bits & 0x01FFU);
        pdata16bits++;
      }
      else
      {
        huart->Instance->DR = (uint8_t)(*pdata8bits & 0xFFU);
        pdata8bits++;
      }
      huart->TxXferCount--;
    }

    if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TC, RESET, tickstart, Timeout) != HAL_OK)
    {
      huart->gState = HAL_UART_STATE_READY;

      return HAL_TIMEOUT;
    }
//HAL_UART_Transmit的核心代码

通过该图我们可以看到HAL_UART_Transmit的运行逻辑就是先判断传输数据是否超时,如果没有,就会传输一个字节同时数据指针自增。连续传输就是重复以上过程。

但是这里并没有我想要的内容,于是,我进入UART_WaitOnFlagUntilTimeout,

具体代码及分析放在下面。

static HAL_StatusTypeDef UART_WaitOnFlagUntilTimeout(UART_HandleTypeDef *huart, uint32_t Flag, FlagStatus Status,
                                                     uint32_t Tickstart, uint32_t Timeout)
{
  /* Wait until flag is set */
  while ((__HAL_UART_GET_FLAG(huart, Flag) ? SET : RESET) == Status)
  {
    /* Check for the Timeout */
    if (Timeout != HAL_MAX_DELAY)
    {
      if (((HAL_GetTick() - Tickstart) > Timeout) || (Timeout == 0U))
      {

        return HAL_TIMEOUT;
      }

      if ((READ_BIT(huart->Instance->CR1, USART_CR1_RE) != 0U) && (Flag != UART_FLAG_TXE) && (Flag != UART_FLAG_TC))
      {
        if (__HAL_UART_GET_FLAG(huart, UART_FLAG_ORE) == SET)
        {
          /* Clear Overrun Error flag*/
          __HAL_UART_CLEAR_OREFLAG(huart);

          /* Blocking error : transfer is aborted
          Set the UART state ready to be able to start again the process,
          Disable Rx Interrupts if ongoing */
          UART_EndRxTransfer(huart);

          huart->ErrorCode = HAL_UART_ERROR_ORE;

          /* Process Unlocked */
          __HAL_UNLOCK(huart);

          return HAL_ERROR;
        }
      }
    }
  }
  return HAL_OK;
}

想必你也看见了:Blocking error下的Disable Rx Interrupt if ongoing,这正是关于之前数据丢失现象的症结所在:传输函数每次发送一个字节,都要进行一次乃至更多的是否超时检查,而超时检查会失能接收中断。这本身没有问题,但是如果我们在接收中断中使用了传输函数就会使得我们本要接收的数据对应的那个中断标志位被清除,使得数据未被接收。UART_EndRxTransfer的具体代码及分析如下图所示。

/**
  * @brief  End ongoing Rx transfer on UART peripheral (following error detection or Reception completion).
  * @param  huart UART handle.
  * @retval None
  */
static void UART_EndRxTransfer(UART_HandleTypeDef *huart)
{
  /* Disable RXNE, PE and ERR (Frame error, noise error, overrun error) interrupts */
  ATOMIC_CLEAR_BIT(huart->Instance->CR1, (USART_CR1_RXNEIE | USART_CR1_PEIE));
  ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_EIE);

  /* In case of reception waiting for IDLE event, disable also the IDLE IE interrupt source */
  if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)
  {
    ATOMIC_CLEAR_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);
  }

  /* At end of Rx process, restore huart->RxState to Ready */
  huart->RxState = HAL_UART_STATE_READY;
  huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;
}

从图中我们可以很清晰的看到该函数关闭了数个中断,并且开启了对串口空闲中断的检查,如果空闲中断被开启,该函数就会清除空闲中断的标志位。如果我们的已置位空闲中断在我们传输时被清除,那单片机就不会接收到该次中断应该接收到的数据,也就发生了数据丢失现象。

那为什么printf函数与标准库的UART_SendData函数不会出现这样的情况呢?

二者的具体代码及分析如下图。

#if 1
#pragma import(__use_no_semihosting)
// 标准库需要的支持函数
struct __FILE
{
    int handle;
};

FILE __stdout;
// 定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
    x = x;
}
// 重定义fputc函数
int fputc(int ch, FILE *f)
{
    while ((USART1->SR & 0X40) == 0)
        ; 
    USART1->DR = (uint8_t)ch;
    return ch;
}
#endif
//本人使用的printf函数

/**
  * @brief  Transmits single data through the USARTx peripheral.
  * @param  USARTx: Select the USART or the UART peripheral. 
  *   This parameter can be one of the following values:
  *   USART1, USART2, USART3, UART4 or UART5.
  * @param  Data: the data to transmit.
  * @retval None
  */
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
{
  /* Check the parameters */
  assert_param(IS_USART_ALL_PERIPH(USARTx));
  assert_param(IS_USART_DATA(Data)); 
    
  /* Transmit Data */
  USARTx->DR = (Data & (uint16_t)0x01FF);
}
//标准库提供的串口发送函数

从图片中我们可以知道,printf函数没有涉及HAL库的中断关闭,而标准库的发送函数则仅有对相应外设的宏参数检查,只要输入的参数合法,就可以直接调用串口底层寄存器发送数据,也不用涉及串口中断的关闭,所以原代码的接收中断不会出现数据丢失现象。

标签:HAL,UART,RX,huart,串口,发送数据,接收
From: https://blog.csdn.net/2301_80285967/article/details/144153124

相关文章

  • 【纸飞机串口调试工具】数值显示器及四则运算
    目录纸飞机串口工具介绍软件下载适用场合功能介绍纸飞机串口工具介绍纸飞机一款性能强劲且专业的串口/网络/HID调试助手,具有多窗口绘图、关键字高亮、数据分窗和数据过滤等众多功能,可以极大的方便嵌入式开发人员的调试过程。本文介绍数值显示器的四则运算。软件下载......
  • Redis设计与实现第16章 -- Sentinel 总结1(初始化、主从服务器获取信息、发送信息、接
    Sentinel是Redis的高可用解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器替代已下线的主服务器......
  • SpringBoot-springboot项目接收解析HL7 V2.3.1协议报文
    背景本协议基于HL7v2.3.1来定义接收消息类型为:ORU^R01(样本结果)每收到一条样本结果,需要回应一条样本应答消息(ACK)7Edit工具模拟HL7协议请求工具安装包地址:通过百度网盘分享的文件:7Edit链接:https://pan.baidu.com/s/12P-8RdsMyYBAaR7R6r8jGg提取码:sky1File-new-选择......
  • 蓝牙接收文件后找不到存储路径的解决方案
        在数字时代,我们每天都在通过蓝牙技术无缝地交换信息和文件。然而,就像在浩瀚的宇宙中迷失方向的宇航员,我们有时会发现自己在接收文件后,面对着一个令人困惑的问题:文件究竟存储在了哪里?这个问题,虽然看似微不足道,却可能严重影响我们的工作效率和日常生活的流畅性。 ......
  • Arduino mega2560硬件串口2控制4台张大头闭环步进电机运动
    //程序为mega2560通过Serial2控制4台编号为1-4的张大头闭环步进电机,电机供电为4S电池,开发板为5V供电,开发板与4台步进电机共地,步进与Arduino接线如下://Arduino42bujin142bujin242bujin342bujin4//Rx2(pin17)------Tx-----------Tx----------Tx-------......
  • STM32cubeMX配置FreeRTOS生成代码--完成一个简单测试(Led闪烁和向串口发送“hello!world
    一、STM32cubeMX中相关配置(首先我用的STM32板子是STM32F103VBT6,板子不同,配置会略有不同,仅作参考!)打开STM32cubeMX,新建工程,选择对应板子型号:1.配置微控制器的时钟系统HighSpeedClock(HSE):高速时钟源,这里选择的是“Crystal/CeramicResonator”,意味着使用外部晶体......
  • STM32之串口232通讯
    STM32F407系列文章-RS232通讯(六)文章目录前言一、串口(UART)二、RS23-硬件特性三、RS232-程序实现1.函数rs232_init()2.函数USART_UX_IRQHandler()3.函数rs232_send_data()4.函数rs232_receive_data()5.函数rs232_receive_data()总结前言一般STM32F407芯片都会......
  • SpringMVC接收请求参数
    (5)请求参数==》五种普通参数1.普通参数代码块@RequestMapping("/commonParam")@ResponseBodypublicStringcommonParam(Stringname,intage){ System.out.println("普通参数传递name==>"+name); System.out.println("普通参数传递age==>"+age);......
  • 深入剖析射频工程指标:从接收灵敏度到邻道泄漏,5G时代的关键技术与优化策略
    深入剖析射频工程指标:从接收灵敏度到邻道泄漏,5G时代的关键技术与优化策略在无线通信中,射频工程师需要面对多种复杂的指标和参数,来保证系统的性能、覆盖范围和信号质量。随着5G的到来,射频性能的要求比4G时代更加严格和复杂,这些参数不仅决定了通信质量,也直接影响到网络的容......
  • rabbitmq MessageConverter消息接收异常 一直unacked 解决
    rabbitmqMessageConverter消息接收异常一直unacked解决rabbitmq:host:127.0.0.1port:5672username:guestpassword:guestvirtual-host:/listener:simple:concurrency:1#Minimumnumberofconsumers.max-conc......