STM32 HAL库 UART在循环DMA模式下接收大量不定字长数据并进行乒乓缓存
源码(Github)
详细Debug过程可见我的Blog
参考文献:
一个严谨的STM32串口DMA发送&接收(1.5Mbps波特率)机制
STM32 HAL 库实现乒乓缓存加空闲中断的串口 DMA 收发机制,轻松跑上 2M 波特率
DMA在循环模式下工作时,如果在大规模传输数据时仍旧空闲中断(或传输完成中断)会有风险,因为当DMA传输数据完成,CPU介入开始拷贝DMA通道缓冲区数据时,如果此时UART继续有数据进来,DMA继续搬运数据到缓冲区,就有可能将数据覆盖,因为DMA数据搬运是不受CPU控制的,即使你关闭了CPU中断。
因此严谨的做法需要建立双buffer,CPU和DMA各自使用一块内存交替访问,即乒乓缓存,处理流程为:
- DMA先将数据搬运到buf1,搬运完成通知CPU来拷贝buf1数据
- DMA将数据搬运到buf2,与CPU拷贝buf1数据不会冲突
- buf2数据搬运完成,通知CPU来拷贝buf2数据
- DMA继续开始拷贝新数据
STM32大多数型号不提供现成的双缓存机制,但提供“半满中断”,即数据搬运到buf大小的一半时,可以产生一个中断信号。基于这个机制,我们可以实现双缓存功能,只需将buf空间开辟大一点即可。
- DMA将数据搬运完成buf的前一半时,触发“半满中断”事件,Callback中通知CPU来拷贝buf前半部分数据
- DMA继续将数据搬运到buf的后半部分,与CPU拷贝buf前半部数据不会冲突
- buf后半部分数据搬运完成,触发“溢满中断”,Callback通知CPU来拷贝buf后半部分数据
- DMA循环拷贝新数据
基于上述描述机制,DMA方式接收串口数据,有三种中断场景需要CPU去将buf数据拷贝到final中,分别是:
HAL_UARTEx_RxEventCallback
)HAL_UART_RxHalfCpltCallback
)UART_FLAG_IDLE
)
也就是说,代码总共需要考虑以下几种情况:
- 数据量未达到半满,触发空闲中断
- 数据量达到半满,未达到满溢,先触发半满中断,后触发空闲中断
- 数据量刚好达到满溢,先触发半满中断,后触发满溢中断
- 数据量大于缓冲区长度,DMA循环覆盖溢出的字节
对于情况1:在空闲中断中拷贝全部数据
对于情况2:在半满中断中通知CPU拷贝一半的数据,DMA继续接收剩下的数据,最后在空闲中断中拷贝剩下的数据
对于情况3:在半满中断中通知CPU拷贝一半的数据,DMA继续接收剩下的数据,最后在满溢中断中拷贝剩下的一半数据
对于情况4:综合处理
不少教程会单独在三个中断回调函数中进行数据处理,可以发现代码量还是挺大的。尤其是这么写代码存在一个比较麻烦的逻辑:当DMA接收的数据量大于缓冲区大小RX_BUFFER_SIZE
时,由于DMA工作在循环模式,那么溢出的数据会被DMA重新放到缓冲区的开始部分,从而覆盖原有的数据。要处理这部分数据势必要引入比较复杂的判断机制,还要实时更新队首和队尾的指针,导致整个程序变得比较复杂。
好在HAL库除了普通的HAL_UART_Receive_DMA()
和HAL_UART_RxCpltCallback()
外,HAL库还提供了HAL_UARTEx_RxEventCallback
回调。
/**
* @brief Reception Event Callback (Rx event notification called after use of advanced reception service).
* @param huart UART handle
* @param Size Number of data available in application reception buffer (indicates a position in
* reception buffer until which, data are available)
* @retval None
*/
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
UNUSED(Size);
/* NOTE : This function should not be modified, when the callback is needed,
the HAL_UARTEx_RxEventCallback can be implemented in the user file.
*/
}
该回调函数会在“advanced reception service”事件发生后触发,这里的所谓高级接收服务就包括之前需要分开判断的半满中断、满溢中断和空闲中断。这三个中断触发后都会回调HAL_UARTEx_RxEventCallback()
函数。因此在拷贝数据时,无需再单独进行中断回调类型的判断。由于DMA工作不依赖CPU,因此在该函数内要做的就是将缓冲区内的数据拷贝至目标地址。形参Size
表示当前缓冲区内可用数据长度。
原先的三个中断中的代码可以合到一个中实现:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
static uint8_t rx_buf_head = 0;
static uint8_t rx_size; //待处理数据长度
rx_size = Size - rx_buf_head;
for (uint16_t i = 0; i < rx_size; i++)
{
RxFinal[final_index++] = RxBuf[(rx_buf_head + i) % RxBufSize]; // 环形缓冲处理
if (final_index >= RxFinalSize) final_index = 0; // 避免 RxFinal 溢出
}
rx_buf_head = rx_buf_head + rx_size;
if (rx_buf_head >= RxBufSize) rx_buf_head = 0;
}
}
作者:Akiyama_R1n