STM32串口通信IDLE
引入
最近在做UART与上位机通信的时候,注意到STM32的UART居然支持硬件IDLE。
于是就尝试了一把 IDLE+DMA
的模式,使得设备可以自动在接收空闲时唤起中断,DMA直接搬运数据到内存,实现任意长度的命令或是数据都能自动完成接收和回调。
IDLE模式
首先来讲讲什么是IDLE模式?
所谓的IDLE就是not working or being used,翻译成中文就是闲置状态。当UART的Rx总线收到持续信息时,会在最后一帧数据结束后进行检查,当出现了至少一帧的闲置电平,就会唤醒相关的回调函数管理。也就实现了无论数据长度有多长,都会自动停止接收并进入中断处理,非常方便。
网上某些过时的教程会告诉你,UART的IDLE模式要通过以下这样的语句开启:__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE)
其实HAL库目前已经将IDLE的相关功能封装成了正式函数,并且已经包含在下列库文件中。
- stm32g4xx_hal_uart_ex.c
- stm32g4xx_hal_uart_ex.h
现在打开IDLE模式的正确姿势应该是:HAL_UARTEx_ReceiveToIdle
HAL_UARTEx_ReceiveToIdle_IT
HAL_UARTEx_ReceiveToIdle_DMA
这些函数会自动开启IDLE功能,并启用相关的中断。IDLE接收本身就很方便,但如果再加上DMA辅助搬运数据,就更能为CPU分担压力,节省计算资源。
然而如果我们真的想写一个足够健壮且高效的串口处理程序,不用的时候就安静的待在一边,数据来了的时候就自动接收、自动搬运、自动停止、自动通知CPU处理。
那可能还真没那么好写,因为一旦牵扯到DMA,问题就会开始变得复杂。
当一组数据被传递过来,而CPU还在运算其他优先级更高的任务,来不及处理这批数据。紧接着又来了一批数据把上一批数据全部覆盖掉,此时的CPU就浑然不知,只做了一批数据的回调处理就继续干别的去了,这样就导致了前一批数据丢失。
DMA中断模型
其实数据被反复覆盖的问题在HAL库设计之初就有被考虑到过,HAL库也给出了相应的解决方案:HAL_UART_RxHalfCpltCallback
HAL_UART_RxCpltCallback
现在我们考虑如下的情形:
首先使能 DMA
的 Circular
模式,开启数据的循环接收。随后创建一个 Buffer[20]
用来接收数据,并用 R
和 W
来标记当前缓冲区内读指针和写指针。且当 Buffer[20]
被填写到一半或者填满时,就触发对应的回调函数 HT
和 TC
;每当本次数据接收完毕,I
就会被触发,也就是空闲中断回调。
此时可以将所有的内存写入问题归纳为如下五种模型(图源):
情况 A:
收到10字节,触发HT
,随后处理已接收的数据。情况 B:
收到10字节,触发TC
,处理现在从最后已知位置R
开始,直到内存结束。DMA 处于循环模式,因此它会从缓冲区的开头继续传输数据,覆盖原有数据。情况 C:
收到10字节,当接收到前6字节时,触发了HT
。但在接下来4字节传输后,将再次触发I
。情况 D:
收到10字节,前4字节会触发TC
,随后缓冲区溢出,回到顶点重新覆盖旧数据。接下来6字节成功传输到内存后,触发I
。情况 E:
收到30字节,超出缓冲区能处理的范围,有10字节会被直接覆盖。这时HT
和TC
就起到了作用,当刚好发到20个字节时,HT
和TC
会被分别触发一次。而若这两次都已经被触发过了,还在有新的数据进入,并且一直覆盖过了原先的R
指针,那么就说明发生了错误,需要及时进行相应的错误处理。
由上述模型可知,HAL库之所以只提供 HT
和 TC
这两个节点的回调,正是因为这两点能够满足最基本的数据溢出检查。
即便数据溢出到能覆盖两轮以上(对于上述的模型也就是在20字节缓冲区里发送40字节及以上),我们也可以凭中断被多次触发,而数据未被CPU及时处理,来判断UART接收出现了错误。
Callback问题
虽然上述想法很好,但是实践起来还是会有一些问题。
首先IDLE中断会触发下列函数的回调:HAL_UART_RxCpltCallback
HAL_UART_RxHalfCpltCallback
HAL_UARTEx_RxEventCallback
那么考虑以下的情况,当数据恰好被填到一半的时候,理应有两次中断被调用:
- 当数据被填充到一半时唤起“HAL_UART_RxHalfCpltCallback”
- 随后Rx总线空闲,唤起“HAL_UARTEx_RxEventCallback”
同理,被恰好填充到满时也应该有两次调用,对吧?
但是事实并非如此,被恰好填充到满时,数据缓冲区会被重置,并从缓冲区顶重新写入,此时IDLE中断并不会被唤起。
这就给程序的判断造成了难题,是否能在这里截断?如果数据还没完,就直接将现在接收到的数据交给CPU,那得到的结果肯定是不正确的。
另一个问题是,两次中断之间是肯定有时间间隙的。像H7这样的芯片,在满主频且串口压力较小的情况下,是真的可以做到在两次中断之间把现有截断的数据处理完成,进而导致结果错误的。
这些问题的出现既有可能是HAL库设计不周,也有可能 UART IDLE 根本不是这样设计来用的。笔者本人水平一般,暂时没有得出有说服力的结论。之后关于这个问题若是有什么进展,会及时在这篇博客更新。
倒是有一些比较偷懒的办法解决上述冲突,虽然不太优雅就是了。
1 |
|
用上述两个函数可以禁用 HT
和 TC
中断,此时就只有 UART IDLE 的中断会被调用。虽然此时缓冲区被恰好填充完整时,依然不会有任何中断会被调用。但上位机可以任意发送一位信息,唤起IDLE中断让其进行处理。