起因
项目需求需要设备通过串口的方式发送信息,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和错误标志的时候调用对应的处理函数,其处理函数如下所示:
/**
[url=home.php?mod=space&uid=2666770]@Brief[/url] UART error callbacks
[url=home.php?mod=space&uid=3142012]@param[/url] huart: UART handle
[url=home.php?mod=space&uid=1902110]@NOTE[/url] 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_RXNE和UART_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 的固件包,如果其他固件发现也有问题,可以参考这里的做法进行修改。
原作者:ItsGettingWorse