电控 | 串口通信基础——串口中断与DMA
串口中断与DMA
在前一个章节中,我们说到了串口通信的轮询模式
。而且,这个模式具有以下的几个缺点:第一,阻塞主程序运行。第二,只能接收固定长度。
而这几个缺点是由其工作原理决定的。要解决这些问题,就要升级其工作原理。因此,我们需要从轮询模式
的工作原理开始说起。
串口轮询模式
在STM32中的每个串口的内部,都有两个寄存器:
发送数据寄存器 TDR
发送移位寄存器
当调用HAL_UART_Transmit(&huart1, data, 5, 100)
的时候,STM32的CPU,会将这个要发送的data
一个一个的放置到寄存器中。其放置的方法如下:
发送移位寄存器中的数据,将会按照设定的比特率,转化成高低电平,从TX引脚输出。
当前一个数据被发送出去后,发送数据寄存器TDR中的数据,会被传到发送移位寄存器。这个时候,CPU就会不断的查询TDR中是否为空,若为空,则将后一个数据传入TDR中。
如果TDR为空,CPU就会不断查询,直到所有的数据都传完(传输了固定大小5
),或者超过了设置的等待时间100
。
使用接收函数HAL_UART_Receive(&huart1, receiveData, 5, 100);
的时候,也是同理。CPU会不断查询接收数据寄存器(RDR)中是否为空。
由此可见,弊端就暴露出来了:
在轮询模式下,无论是发送还是接收,CPU会一直忙碌,无法处理其他进程。
我们将一直等待使得程序暂时无法向下运行的状态为:堵塞。
那么,如何解决“堵塞”问题呢?STM32提供了串口中断模式。
串口中断模式
原理
当CPU将数据塞入发送数据寄存器TDR的时候,CPU就可以去处理其他的进程。
而当数据发出,发送数据寄存器TDR位空时,触发 发送寄存器空中断,CPU被唤回来,将后面的数据塞入发送数据寄存器TDR。
而当数据再进行接收的时候,每当接收数据寄存器(RDR)中有数据的时候,就会触发 接收寄存器非空中断,CPU被唤回,将寄存器中的 数据传入预先设置好的保存数据的变量空间。
代码实践
串口发送中断
这一原理不需要我们去手动编写。打开CubeMX,在System Core中找到NVIC,打开所选串口的中断:USART global interrupt
。
保存并生成代码,就可以了。
使用中断的方式实现串口数据发送,和轮询方式很类似,只是函数名略有不同。使用的函数是HAL_UART_Transmit_IT
。
1 | int main(void) |
上面的代码,是根据Robomaster官方的串口通信教程进行一些修改,对比了串口轮询模式和串口中断模式的发送方法。
可以看到,轮询发送函数和中断发送函数有一个区别,就是中断发送函数没有了超时时间。这是因为中断发送不需要长期占用CPU 使程序堵塞,所以也就不再需要设置超时时间了。
串口接收中断
我们接收到了串口的数据,就要对数据进行处理。那么我们该如何处理接收到的数据呢?
打开stm32f4xx_it.c
文件,可以找到这样一个中断处理函数。
1 | void USART1_IRQHandler(void) |
这就是USART1的中断向量对应的中断处理函数。我们希望把接收到的数据在函数中进行处理。但是,我们不能把代码写在USART1_IRQHandler(void)
中了,因为USART的各种中断事件都被连接到同一个中断向量中。我们只希望普通接收中断后处理数据,而不希望别的中断也处理这些数据,否则会出错。
这个中断向量就叫做HAL_UART_IRQHandler()
,里面还包括了刚才所说的发送寄存器空中断,还有线路空闲中断等等。而我们要找的,是接收寄存器非空中断。
因此,我们来到HAL_UART_IRQHandler()
函数,跨过这个函数,找到了如下函数:
1 | /** |
这里的Cplt是Complete的缩写,表示完成;Callback的意思是回调。接收完成回调函数,我们通常叫它:接收中断回调函数。
顾名思义,这个函数在接收完成的时候执行。
根据原理,接收过程当中,每次接收移位寄存器每完成一次储存,就会触发一次接收中断,但是并不是每次中断都需要处理。这个函数做了优化,只有当所有数据都被接收之后的中断,才会将数据进行处理。
而我们要写的处理代码,也将写在这个函数里。
注意到,这是一个弱定义。一般而言,要把函数放到一个专门的文件中进行定义。但是这一次,我们直接把它放main.c中,看起来更为友善一些。
1 | /* Private function prototypes -----------------------------------------------*/ |
这样就可以完成串口中断接收了。
但是我们发现,虽然利用普通的串口中断处理可以解决堵塞问题,但是我们依旧是无法解决发送接收数据必须是定长的问题。那么接下来,就该了解串口的DMA模式了。
串口DMA模式
节省CPU
如果使用串口中断模式,CPU就会疲于不断的切换工作任务。那么是否能够给CPU找一个代工呢?我们使用直接内存访问。
DMA (Direct Memory Access),即直接内存访问。我们只需要给DMA一个带有起点和终点的通道,DMA就可以在合适的时机帮我们搬运内存。等全部搬运完成了,再通过中断提醒。
简单而言,就是创建这样两条通道:
DMA通道的配置依然可以通过CubeMX。
打开串口DMA设置,选择Add,可以添加TX和RX通道。剩下的,CubeMX已经帮我们配置好了,不需要修改。
如图所示的TX通道,设置方向位从内存向外设搬运(Memory To Peripheral),即从内存变量,向发送数据寄存器搬运。
而代码方面,只需要把串口中断的_IT
改为_DMA
就可以,同样需要中断回调函数。
接收不定长数据
接收不定长数据的解决根源,在于改变中断方式——使用串口空闲中断(idle)。其开启中断的方式是,只有当串口接收端从忙碌变为空闲的时候才会触发。因此可以认为串口空闲中断发生时,就是一帧的数据包接收完成了。
我们使用HAL_UARTx_ReceiveToIdle_DMA(huart, receiveData, max_size)
其中max_size
一般用sizeof(receiveData)
取代。
这个函数的回调函数,就不是前面的RxCpltCallback了,而是如下的回调函数:
1 | /** |
这里多了一个Size变量。因为原先的回调函数是定长的,已知变量。而现在是不定长的,故此需要传入长度。
注:当数据接收到一半的时候,也会触发一次RxEventCallback,对于一般场景很烦人。使用__HAL_DMA_DISABLE_IT(&hdma_usart1_tx, DMA_IT_HT)来取消这一机制。
代码实现
接下来,我们就看看Robomaster的官方代码是如何利用DMA的。
比较特别的是,Robomaster官方代码喜欢将回调函数直接写在IRQHandler
里面,这样看起来确实是更简洁的。
等日后续上