我们已经学习了使用串口的轮询模式,实现了使用串口来控制开发板上的彩色LED,不过同时我们也知道了轮询模式的一些弊端,比如程序必须等待发送或者接收结束才能接着执行,再比如只能接收确定长度的数据,那么,怎么解决这些问题呢?
本次学习,我们将会学习如何使用中断模式解决程序的等待问题,正式介绍串口的中断模式之前,我们不妨先来看看轮询模式的底层机制是怎样的。
在STM32每个串口的内部都有这样两个寄存器:发送数据寄存器(TDR)和发送移位寄存器。当我们调用HAL_UART_Transmit发送一段数据时,STM32的CPU会依次将数据移到寄存器中,发送移位寄存器中的数据会按照我们设定的比特率,转换成高低电平从TX引脚引出。



发送数据寄存器中的数据会在发送移位寄存器发送完成后,被移到发送移位寄存器进行下一次发送,而在此过程中,CPU需要不断的去查询,发送数据寄存器中的数据是否已经移送到发送移动寄存器,移了的话,就赶紧把下一个数据塞进来,如果还没有移,那就再接着不停地查询,直到把本次要发送的数据全部发完,或者用时超过我们设定的超时时间。
轮询模式下的串口接收也是类似,调用HAL_UART_Receive函数后,从RX引脚接收的高低电平信号依次转换后存入接收移位寄存器。


接收移位寄存器每接收完一帧,就将数据移到接收数据寄存器,CPU会一直查询接收数据寄存器中是否有新数据可以读,一旦检测到,就马上把数据从寄存器移到我们用来接收数据的变量中,直到接受完我们希望接收的字节数,或者时间超时。

不管是发送还是接收,CPU一直处于忙碌状态,一轮一轮地去查询寄存器是否可用,无暇顾及其他的任务,我们一般称这种一直等待使程序暂时无法向下执行的状态为"堵塞"。那如何解决这种长期占用CPU的堵塞问题呢?

STM32为我们提供了串口的中断模式,使用中断模式的串口发送时,CPU将数据塞入寄存器后就可以继续进行其他任务了,当发送移位寄存器中的数据发送出去后,则会触发"发送数据寄存器空"中断把CPU叫回来,CPU再中断处理函数中将数据塞入发送数据寄存器后,就又可以去处理其他代码了,如此反复直到全部发送完成。当然,这一过程并不需要我们自己去实现。

接着上次的工程,回到Cube MX,打开我们的老朋友中断控制器NVIC,开启USART2的中断功能,然后保存并生成代码

使用中断发送数据的函数与轮询发送十分类似,HAL_UART_Transmit_IT,也就是只加了_IT后缀,它的参数也与轮询发送的前三个相同。第一个参数还是用来发送的串口,也就是huart2的指针。第二个参数依旧是要发送的数据的指针。第三个参数也是要发送的数据的长度,由于中断发送不需要长期占用CPU使程序堵塞,也就不需要第四个参数设置超时时间了。
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
参数:
huart:指向 UART 句柄结构体的指针,用于指定要使用的 UART 外设。
pData:指向要发送数据缓冲区的指针,可以是 uint8_t 类型或 uint16_t 类型的数据。
Size:要发送的数据大小,以数据元素(uint8_t 或 uint16_t)的数量表示。
返回值:
HAL_StatusTypeDef 类型的返回值,表示函数的执行状态。可能的返回值包括:
HAL_OK:发送操作已成功启动。
HAL_BUSY:当前有正在进行的发送操作。
HAL_ERROR:传入的参数不合法。
将轮询发送注释掉,我们编译下载试一试,可以看到效果依旧,说明中断模式的串口发送可以正常将数据发出了,虽然宏观上我们并没有看出效果上有什么区别,但在比较复杂的项目中,节省出的CPU时间可以做许多事情。
再来看一下中断模式下的串口接收,开启串口中断接收函数也是一样,在HAL_UART_Receive后加上_IT即可。其参数也是一样,只是没有超时时间的设定。不过要注意的是,由于其不会阻塞程序的执行,也就是还没等接收到数据,就会接着向下执行下去,这就会造成当执行到下次循环时,可能是上次的数据还没有接收完成,就又执行开启串口中断接收了,所以我们将其放在循环之前,只执行一次。另外也注释掉轮询模式的接收函数,在轮询模式下HAL_UART_Receive函数是堵塞执行的,当他执行完时,我们就知道数据已经接收完成,放到用于接收的变量里面了。
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
参数:
huart:指向 UART 句柄结构体的指针,用于指定要使用的 UART 外设。
pData:指向接收数据缓冲区的指针,可以是 uint8_t 类型的数据。
Size:要接收的数据大小,以数据元素(uint8_t)的数量表示。
返回值:
HAL_StatusTypeDef 类型的返回值,表示函数的执行状态。可能的返回值包括:
HAL_OK:接收操作已成功启动。
HAL_BUSY:当前有正在进行的接收操作。
HAL_ERROR:传入的参数不合法。
接下来就可以进行分析,但中断模式的HAL_UART_Receive_IT并不会堵塞,它开启中断接收后就继续向下执行了,所以不能直接对其运行后就立即对数据进行分析,因为此时数据很可能还没有接收完成。
那我们要怎样知道何时数据接收完成,对接收到的数据进行分析处理呢?当然是用中断处理函数啦,当我们使用HAL_UART_Receive_IT进行中断接收后,每当接收移位寄存器将一帧数据移入接收数据寄存器,就会触发一次"接收数据寄存器非空"中断,把正在处理其他事情的CPU叫回来,把数据读入变量中,然后CPU再去处理其他事情,直到接收到了我们设定的数据长度,完成本次接收。

来到我们之气在学习外部中断时就打开过的stm32f1xx_it.c文件,在最底部就可以找到USART2_IRQHandler函数,是的,他就是USART2的中断向量对应的中断处理函数,就好像我们之前学习外部中断时用到的EXTI15_10_IRQHandler一样,不过这次,我们的逻辑代码不能再写在IRQHandler里了,这是因为每个USART只有一个中断向量,除了我们要用的"接收数据寄存器非空"中断,还有刚刚我们提到的"发送数据寄存器空"中断"线路空闲"中断等好几个中断,也共用了此中断处理函数,所以就需要一点点简单判断才能确定当前是因何原因触发的中断。

当然,这点简单的判断代码Cube MX与HAL库早就帮我们处理好了,按住Ctrl点击进入到HAL_UART_IRQHandler函数就可以看到。那怎样在这简单的串口中断处理逻辑中执行我们自己的代码呢?HAL库为我们提供了Callback也就是回调函数机制。所谓回调函数,就是指当有什么事情发生时,就会调用这个函数。我们跨过这些"简单"的逻辑,在下面可以找到一个HAL_UART_RxCpltCallback的代码定义,这就是一个回调函数,其中Cplt是complete也就是完成的意思,顾名思义,这个函数会在接收完成时执行。
具体来说,虽然接收移位寄存器每向接受数据寄存器转移一帧数据都会触发一次中断,但HAL库的中断处理流程(也就是刚刚那点简单的代码)为我们做了优化,只有当接收到我们想要的字节数,也就是接收完成时,才会调用HAL_UART_RxCpltCallback回调函数,所以我们将代码写到HAL_UART_RxCpltCallback中,也就能在串口接收完成的第一时刻对收到的数据进行分析处理了。
不过在这个HAL库文件中写我们自己的用户代码可不是好办法,所以HAL库为这个函数定义添加了__weak前缀,代表此处是一个弱定义,我们可以在其他地方重新定义一个此函数。工程中我们通常会开一个专门的文件对此类函数进行定义,不过为了专注于本次的知识点,我们就将其在main.c进行实现吧,复制这个函数定义,然后回到main.c,在USER CODE 0注释对中进行粘贴,重新定义此函数,其实将之前的代码逻辑剪切过来就好了,因为都是在接收到数据后的处理逻辑。

但是我们靠中断好不容易省出来的CPU资源,在while循环中却什么都不干,确实有点说不过去。为了理解起来更加方便,我们还是给加上一些注释,假装这里有一些复杂代码。另外还有一件事,不仅main函数,RxCpltCallback函数也用到了receiveData,所以我们将它提升为全局变量。将他剪切,然后为了规范,放到USER CODE PV注释对中。PV就是Private variables 私有变量的意思。

编译下载看看效果,通过串口助手发送R1,可以看到红灯成功亮起,而且指令也返回来了,看来中断接收也确实起了作用,正常执行了。那我们在发送B1试试,可以看到蓝色小灯没有亮起,指令也没有返回来,这是什么情况?这其实是因为我们只在程序刚开始的时候,启动了一次串口中断接收,接收完一次后就不在接收了。那怎么办?

我们刚刚说了不要在while循环中不断去开启串口中断接收,其实解决办法也很简单,只需要在处理完成一次串口接受回调后,为下次的接收启动串口中断接收就好了。

编译下载再来试试,好了,这下就没问题了。
底层的东西,刚入门的大家了解一下就好,真正要使用串口的中断模式,只需要勾选开启相应串口的中断

在发送函数和接收函数后加_IT后缀



以及将处理函数逻辑移入到HAL_UART_RxCpltCallback即可。
通过中断,我们成功解决了串口操作一直占用CPU的问题。可要如何解决接收不确定长度数据的问题呢?有没有更好的方式进一步减少CPU的占用呢?