[已解决] RTT 串口DMA接受 开头丢失一整包的问题

起因

  项目需求需要设备通过串口的方式发送信息,STM32F4将串口的信息接收并保存到SD卡中;通过XCOM串口助手发送文件的方式进行功能测试,测试的时候发现总会丢弃一部分头;

  项目需要两路串口都保存到SD卡中,目前只调出来一路串口,因为写入串口的时候偶发会有一个很长的写入时间,正常4KB写入为15ms左右,偶发会有200+ms,在我双路波特率为460800的时候经常会爆缓存区。(尚未解决,如果哪位好心人知道的话可以在下面留言讨论)

  在调试过程中就觉得很奇怪,为什么会丢弃头?如果丢弃文件结尾的话可以理解,是因为当时接收到的包不足4KB,拼包线程还在等待数据拼成4KB再进行写入,但是丢弃头真的理解不了。开始的时候以为是线程优先级的问题,串口中断来了,拼包线程还没创建或者还没准备好,排除了这个问题之后,就开始检查中断的问题了。


过程

在drv_usart.c中

如果我们打开了DMA接收的话 ,比如我这里使用的STM32F405RGT6,打开了USART3 RX DMA,我们可以看到系统自动为我们打开了

#if defined(BSP_USING_UART3)
void USART3_IRQHandler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();

    uart_isr(&(uart_obj[UART3_INDEX].serial));

    /* leave interrupt */
    rt_interrupt_leave();
}
#if defined(RT_SERIAL_USING_DMA) && defined(BSP_UART3_RX_USING_DMA)
void UART3_DMA_RX_IRQHandler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();

    HAL_DMA_IRQHandler(&uart_obj[UART3_INDEX].dma_rx.handle);

    /* leave interrupt */
    rt_interrupt_leave();
}

  以上两个函数,分别是串口3中断函数跟DMA请求中断函数。

  其中中断函数调用了uart_isr(),这里比较重要,需要深究.

  DMA请求中断函数调用了HAL_DMA_IRQHandler(),内部主要是根据DMA->ISR寄存器,提示的标志位进行对应中断、错误的重置,并在收到UART_RX_DMA_IT_TC_FLAG/UART_RX_DMA_IT_HT_FLAG和错误标志的时候调用对应的处理函数,其处理函数如下所示:

/**
  * @brief  UART error callbacks
  * @param  huart: UART handle
  * @note   This example shows a simple way to report transfer error, and you can
  *         add your own implementation.
  * @retval None
  */
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    RT_ASSERT(huart != NULL);
    struct stm32_uart *uart = (struct stm32_uart *)huart;
    LOG_D("%s: %s %d\n", __FUNCTION__, uart->config->name, huart->ErrorCode);
    UNUSED(uart);
}

/**
  * @brief  Rx Transfer completed callback
  * @param  huart: UART handle
  * @note   This example shows a simple way to report end of DMA Rx transfer, and
  *         you can add your own implementation.
  * @retval None
  */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    struct stm32_uart *uart;
    RT_ASSERT(huart != NULL);
    uart = (struct stm32_uart *)huart;
    dma_recv_isr(&uart->serial, UART_RX_DMA_IT_TC_FLAG);
}

/**
  * @brief  Rx Half transfer completed callback
  * @param  huart: UART handle
  * @note   This example shows a simple way to report end of DMA Rx Half transfer,
  *         and you can add your own implementation.
  * @retval None
  */
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
    struct stm32_uart *uart;
    RT_ASSERT(huart != NULL);
    uart = (struct stm32_uart *)huart;
    dma_recv_isr(&uart->serial, UART_RX_DMA_IT_HT_FLAG);
}

  我们再来探究一下我们刚提到但是还没有看源码的uart_isr()函数

static void uart_isr(struct rt_serial_device *serial)
{
    struct stm32_uart *uart;

    RT_ASSERT(serial != RT_NULL);
    uart = rt_container_of(serial, struct stm32_uart, serial);

    /* UART in mode Receiver -------------------------------------------------*/
    if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET) &&
            (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_RXNE) != RESET))
    {
        rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_IND);
    }
#ifdef RT_SERIAL_USING_DMA
    else if ((uart->uart_dma_flag) && (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_IDLE) != RESET)
             && (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_IDLE) != RESET))
    {
        dma_recv_isr(serial, UART_RX_DMA_IT_IDLE_FLAG);
        __HAL_UART_CLEAR_IDLEFLAG(&uart->handle);
    }
    else if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) &&
            (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TC) != RESET))
    {
        if ((serial->parent.open_flag & RT_DEVICE_FLAG_DMA_TX) != 0)
        {
            HAL_UART_IRQHandler(&(uart->handle));
        }
        UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_TC);
    }
#endif
    else
    {
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_ORE) != RESET)
        {
            __HAL_UART_CLEAR_OREFLAG(&uart->handle);
        }
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_NE) != RESET)
        {
            __HAL_UART_CLEAR_NEFLAG(&uart->handle);
        }
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_FE) != RESET)
        {
            __HAL_UART_CLEAR_FEFLAG(&uart->handle);
        }
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_PE) != RESET)
        {
            __HAL_UART_CLEAR_PEFLAG(&uart->handle);
        }
#if !defined(SOC_SERIES_STM32L4) && !defined(SOC_SERIES_STM32WL) && !defined(SOC_SERIES_STM32F7) && !defined(SOC_SERIES_STM32F0) \
    && !defined(SOC_SERIES_STM32L0) && !defined(SOC_SERIES_STM32G0) && !defined(SOC_SERIES_STM32H7) \
    && !defined(SOC_SERIES_STM32G4) && !defined(SOC_SERIES_STM32MP1) && !defined(SOC_SERIES_STM32WB) \
    && !defined(SOC_SERIES_STM32L5) && !defined(SOC_SERIES_STM32U5)
#ifdef SOC_SERIES_STM32F3
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_LBDF) != RESET)
        {
            UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_LBDF);
        }
#else
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_LBD) != RESET)
        {
            UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_LBD);
        }
#endif
#endif
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_CTS) != RESET)
        {
            UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_CTS);
        }
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TXE) != RESET)
        {
            UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_TXE);
        }
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET)
        {
            UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_RXNE);
        }
    }
}

  可以观察到,在上述函数中主要处理了UART_IT_RXNEUART_IT_IDLE;

  对RXNE不做细究, 我们直接看IDLE空闲中断中会做什么处理。

  IDLE函数调用了dma_recv_isr(serial, UART_RX_DMA_IT_IDLE_FLAG);

该函数是我们需要仔细研究的地方… 问题就出现在这里

  如果看到这里,你不是很愚钝的话,应该已经发现了这几个函数的共性。

  他们最终的处理函数都是dma_recv_isr(),只不过他们的输入参数不同罢了.

输入参数分析

  那他们的输入参数分别是什么呢? 我们简单的列出他们的输入参数

  • DMA传输过半中断

    dma_recv_isr(&uart->serial, UART_RX_DMA_IT_HT_FLAG);

  • DMA传输完成中断

    dma_recv_isr(&uart->serial, UART_RX_DMA_IT_TC_FLAG);

  • 串口空闲中断

    dma_recv_isr(serial, UART_RX_DMA_IT_IDLE_FLAG);


最终归宿

  那**活着(问题)**的最终归宿是什么呢,很明显,是dma_recv_isr()

#ifdef RT_SERIAL_USING_DMA
static void dma_recv_isr(struct rt_serial_device *serial, rt_uint8_t isr_flag)
{
    struct stm32_uart *uart;
    rt_base_t level;
    rt_size_t recv_len, counter;

    RT_ASSERT(serial != RT_NULL);
    uart = rt_container_of(serial, struct stm32_uart, serial);

    level = rt_hw_interrupt_disable();
    recv_len = 0;
    counter = __HAL_DMA_GET_COUNTER(&(uart->dma_rx.handle));

    switch (isr_flag)
    {
    case UART_RX_DMA_IT_IDLE_FLAG:
        if (counter <= uart->dma_rx.remaining_cnt)
            recv_len = uart->dma_rx.remaining_cnt - counter;
        else
            recv_len = serial->config.bufsz + uart->dma_rx.remaining_cnt - counter;
        break;

    case UART_RX_DMA_IT_HT_FLAG:
        if (counter < uart->dma_rx.remaining_cnt)
            recv_len = uart->dma_rx.remaining_cnt - counter;
        break;

    case UART_RX_DMA_IT_TC_FLAG:
        if(counter >= uart->dma_rx.remaining_cnt)
            recv_len = serial->config.bufsz + uart->dma_rx.remaining_cnt - counter;

    default:
        break;
    }

    if (recv_len)
    {
        uart->dma_rx.remaining_cnt = counter;
        rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_DMADONE | (recv_len << 8));
    }
    rt_hw_interrupt_enable(level);

}

我们来分析一下这个函数的主要逻辑

  每次进函数的时候都会初始化当前接收的recv_len=0,并且去获取对应的NDTR寄存器的值(counter = __HAL_DMA_GET_COUNTER(&(uart->dma_rx.handle)).

  我们需要分成两种情况来讨论接收和处理.

  • 上位机发送字符数量小于我缓冲区大小(一次能收完)

    这种情况非常简单,通过一开始写入NDTR的值-当前获得的,我们能获取当次串口接受到的字符数量,也就是recv_len。

  • 上位机发送字符数量大于我缓冲区大小(缓冲区需要重载)

    这种情况相对于第一种情况就略显复杂了, 我们需要统计两次,因为DMA设置了循环模式,所以他会自动重载.我们需要读取上一次进中断时NDRT的值; 先计算[上一次进中断时NDRT-0],这是重载发生前接收的字符数量;还需要计算[重载值(buffsize)-当前NDRT的值],这是重载发生后接收的字符数量.

  如果理解了上述的两种情况,那么我们对下面这段代码就非常好理解了.

        if (counter <= uart->dma_rx.remaining_cnt)
            recv_len = uart->dma_rx.remaining_cnt - counter; //未发生重载
        else
            // 发生重载
            recv_len = serial->config.bufsz + uart->dma_rx.remaining_cnt - counter;
        break;

  那么问题出现在哪里呢?
问题就出现在发生第一次重载的时候, 计算[重载值(buffsize)-当前NDRT的值]上.

RTT初始化uart->dma_rx.remaining_cnt为0, 当我们第一次接收到大于重载值(buffsize)的包的时候,我们会先进入DMA传输半中断,但是此时在DMA传输半中断中什么都没有做,因为dma_rx.remaining_cnt=0!!!

    case UART_RX_DMA_IT_HT_FLAG:
        if (counter < uart->dma_rx.remaining_cnt)
            recv_len = uart->dma_rx.remaining_cnt - counter;
        break;

    case UART_RX_DMA_IT_TC_FLAG:
        if(counter >= uart->dma_rx.remaining_cnt)
            recv_len = serial->config.bufsz + uart->dma_rx.remaining_cnt - counter;

  观察上面两个处理方式,在半中断的时候其实已经计算了一次recv_len,并在后序调用用户callback函数告知用户需要取走数据;但由于dma_rx.remaining_cnt=0,第一次进入半中断其实啥也没干,recv_len也是默认值0,用户也取不到数据;等到发生DMA接受中断的时候,因为DMA是循环模式,数据已经被覆盖掉了,所以丢失掉了完整的一个包,包长度为(buffsize).


解决方案

设置uart->dma_rx.remaining_cnt为buffsize就好了
给出修改后的代码, 稍后会提交PR.

static rt_err_t stm32_configure(struct rt_serial_device *serial, struct serial_configure *cfg)
{
    struct stm32_uart *uart;
    RT_ASSERT(serial != RT_NULL);
    RT_ASSERT(cfg != RT_NULL);

    uart = rt_container_of(serial, struct stm32_uart, serial);
    uart->handle.Instance          = uart->config->Instance;
    uart->handle.Init.BaudRate     = cfg->baud_rate;
    uart->handle.Init.Mode         = UART_MODE_TX_RX;
    uart->handle.Init.OverSampling = UART_OVERSAMPLING_16;

    switch (cfg->flowcontrol)
    {
    case RT_SERIAL_FLOWCONTROL_NONE:
        uart->handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
        break;
    case RT_SERIAL_FLOWCONTROL_CTSRTS:
        uart->handle.Init.HwFlowCtl = UART_HWCONTROL_RTS_CTS;
        break;
    default:
        uart->handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
        break;
    }

    switch (cfg->data_bits)
    {
    case DATA_BITS_8:
        if (cfg->parity == PARITY_ODD || cfg->parity == PARITY_EVEN)
            uart->handle.Init.WordLength = UART_WORDLENGTH_9B;
        else
            uart->handle.Init.WordLength = UART_WORDLENGTH_8B;
        break;
    case DATA_BITS_9:
        uart->handle.Init.WordLength = UART_WORDLENGTH_9B;
        break;
    default:
        uart->handle.Init.WordLength = UART_WORDLENGTH_8B;
        break;
    }

    switch (cfg->stop_bits)
    {
    case STOP_BITS_1:
        uart->handle.Init.StopBits   = UART_STOPBITS_1;
        break;
    case STOP_BITS_2:
        uart->handle.Init.StopBits   = UART_STOPBITS_2;
        break;
    default:
        uart->handle.Init.StopBits   = UART_STOPBITS_1;
        break;
    }

    switch (cfg->parity)
    {
    case PARITY_NONE:
        uart->handle.Init.Parity     = UART_PARITY_NONE;
        break;
    case PARITY_ODD:
        uart->handle.Init.Parity     = UART_PARITY_ODD;
        break;
    case PARITY_EVEN:
        uart->handle.Init.Parity     = UART_PARITY_EVEN;
        break;
    default:
        uart->handle.Init.Parity     = UART_PARITY_NONE;
        break;
    }

#ifdef RT_SERIAL_USING_DMA
    if (!(serial->parent.open_flag & RT_DEVICE_OFLAG_OPEN)) {
//        uart->dma_rx.remaining_cnt = 0;
        uart->dma_rx.remaining_cnt = cfg->bufsz;
    }
#endif

    if (HAL_UART_Init(&uart->handle) != HAL_OK)
    {
        return -RT_ERROR;
    }

    return RT_EOK;
}

因为我只修改了STM32 的固件包,如果其他固件发现也有问题,可以参考这里的做法进行修改.

已提交PR -> PR 6547


特别致谢

贡献者:

fizecoding

ItsGettingWorse

你可能感兴趣的:(STM32爬坑,stm32,1024程序员节)