第十一课时:串口中断接收与回调函数实战解析

第十一课时:中断的具体使用

本课主题:中断的具体使用,串口中断接收与回调函数,这是用按键做复杂操作的基础。

课程视频:中断的具体使用(视频链接待补充)


一、第一步:先设计出不断闪烁的灯泡

先说调试的基本操作:先把代码优化调整为 level 0,然后点击右上角的放大镜加 d 进入调试,最后就能通过 View 选项卡使用各种变量监视功能。

先在私有变量区声明闪灯间隔:在 USER CODE BEGIN PV 和 USER CODE END PV 之间写 static uint32_t blinkInterval = 1000,也就是闪灯间隔。

然后把闪灯延时里的数字换成这个变量,改它的值就能改闪烁速度。

回到 CubeMX,在 USART 的 NVIC Settings 里把 USART1 global interrupt 勾上选中,通过打勾来使能这个中断。

那么中断的情况又要怎么去使用呢?

1.1 带 IT 的函数等于中断接收

和原来函数的区别在于,后面多了 IT,也就是 Interrupt,中断。

图里是 HAL_UART_Receive_IT 的原型,作用是使用中断方式接收一定数量的数据。参数 huart 填串口句柄的指针,pData 填接收数据缓冲区,Size 填要接收数据的数量。函数下方是回调函数 HAL_UART_RxCpltCallback,箭头旁写着一句话:所有 Size 个字节接收完成之后才调用。

中断以后就要有回调。这个回调函数是自己写的还是系统自带的呢?在 1.4 里说。

那么这个函数是怎么运行的呢?

while 函数一直执行,但每次收到数据的时候就会产生中断,然后调用中断响应函数,把数据存到声明的数组里,等收满之后再调用准备好的回调函数,在回调里把需要的数据输出出来。这里要注意,数据需要全部接收完才能够进行处理。

图上把时间轴画了出来:dataRcvd0 到 dataRcvd4 依次被填满,五个字节都收完之后,才回调一次。

但是这样做每次都只能接收固定数量的数据,那怎么接收不定长的数据呢?

HAL_UART_Receive_IT 的 Size 是固定的,所以每次接收固定数量的数据可以,接收不定长的数据就不行。

如果要连续接收,就在回调里再调用一次 HAL_UART_Receive_IT,把 Size 设成 1,这样每来一个字节就进一次回调,处理完再续上下一次接收。

这部分课上讲得比较模糊,容易分不清到底是执行一次就调用回调,还是执行五次再调用回调。实际上,每次接收到数据都会进中断,但回调是在收满 Size 个字节时才调一次,准确的时机在 1.4 里说。

1.2 具体代码

完整代码大概是:先声明接收缓冲区 uint8_t dataRcvd,在 while(1) 之前调用 HAL_UART_Receive_IT(&huart1, &dataRcvd, 1);while(1) 里放常规程序,比如闪灯;回调函数里处理数据,再调用一次 HAL_UART_Receive_IT 接收下一个字节。

图右边是根据指令改变闪灯间隔的例子:收到字符 1 就慢闪,blinkInterval 设成 1000;收到 2 正常,设成 300;收到 3 快闪,设成 50。

打开工程,driver 开头的就是驱动程序,也就是 stm32f1xx_hal_uart.c。翻到 2619 行,去那里复制需要的目标响应函数。

CubeMX 加 Keil5 里,在哪里声明变量、在哪里声明函数、在哪里创建函数,这些问题在 1.4 里统一说。

回调函数里记得判断句柄,if (huart == &huart1),确认传进来的是不是串口 1;处理完数据再续上接收。

1.3 名字拆解:HAL_UART_RxCpltCallback

把这个名字拆成几段就很好记了。HAL 是 Hardware Abstraction Layer,硬件抽象层,也就是 STM32 HAL 库的前缀;UART 是 Universal Asynchronous Receiver/Transmitter,通用异步收发器,也就是串口;Rx 是 Receive,接收,Tx 是 Transmit,发送;Cplt 是 Complete,完成;Callback 是回调函数。

连起来直译就是,HAL 库 UART 串口接收完成回调函数。

1.4 疑问解答

问 1:回调函数是自己写的还是系统自带的?

两者结合。系统提供了一个默认实现,在 stm32f1xx_hal_uart.c 里,带 __weak 前缀的 HAL_UART_RxCpltCallback,内容基本是空的;你在自己代码里写一个同名的函数,就把它覆盖掉了。

所以函数名是固定的,必须叫 HAL_UART_RxCpltCallback,你要做的只是在 USER CODE 区重新定义它,不用自己写声明。从 stm32f1xx_hal_uart.c 的 2619 行附近把函数原型复制出来,是为了保证名字、参数一模一样,这样才覆盖得掉。

问 2:这个函数是怎么运行的?

  1. 程序在 while(1) 里正常跑,比如闪灯;
  2. 串口每收到 1 个字节,触发 USART1 中断,进入中断服务函数 USART1_IRQHandler,这个 CubeMX 已经生成了;
  3. 中断服务函数里把字节存进你给的缓冲区,也就是 HAL_UART_Receive_IT 的第二个参数;
  4. 收满你指定的数量后,HAL 库调用接收完成回调 HAL_UART_RxCpltCallback,名字里的 Cplt 就是完成,你在这里处理数据;
  5. 处理完记得再调用一次 HAL_UART_Receive_IT,否则只会接收这一轮。

关于执行几次调回调:HAL_UART_Receive_IT(&huart1, buf, 5) 中间每来 1 个字节都会进中断,但回调是在收满 5 个字节时才调 1 次,完成才叫回调。Size 填几,就收满几个才调。

问 3:怎么接收不定长的数据?

常见三种做法,由易到难。一是每次只收 1 字节,HAL_UART_Receive_IT(&huart1, &rxByte, 1),回调里逐个字符判断结束符,比如 \n,攒够一帧再处理;二是空闲中断 IDLE,一帧发完、总线空闲时触发,新 HAL 用 HAL_UARTEx_ReceiveToIdle_IT,或者手动开 UART_IT_IDLE,最贴合不定长;三是 DMA 加空闲中断,串口数据量大时的主流做法,后面讲 DMA 时会用到。

记住思路:固定长度靠 Size,不定长靠结束符或空闲判断。

问 4:CubeMX 加 Keil5 里,变量、函数到底写哪儿?

main.c 里 CubeMX 生成了许多 /* USER CODE BEGIN X / 和 / USER CODE END X */ 区域,你只能写在 BEGIN 和 END 之间,这样重新生成代码时才不会被覆盖。

各区域分工如下:

要写的东西 写在哪个区域 说明
私有变量(本文件用) USER CODE BEGIN PV Private Variables,比如 uint8_t rxBuffer10;
外部变量(别的文件也要用) main.h 里的 USER CODE BEGIN EV 需要 extern 的那种
私有函数声明 USER CODE BEGIN PFP Private Function Prototypes,自己写的函数先在这里声明
函数实现或创建函数 USER CODE BEGIN 0 或 USER CODE BEGIN 4 0 区在文件前部,4 区在文件末尾,都可以
中断回调函数 USER CODE BEGIN 4(常用) 名字固定,直接重定义,不用声明
包含头文件或宏 对应的 USER CODE BEGIN Includes 或 PM 区

跨文件调用:想在别的 .c 文件里用某个变量,就在 main.h 的 USER CODE BEGIN EV 里声明 extern;想在别的 .c 文件里用某个函数,就在 main.h 的 USER CODE BEGIN EFP 里放函数原型。

注意 USER CODE 之外的地方不要改,CubeMX 重新生成会覆盖你的修改。

快捷记忆:PV 放变量,PFP 放函数声明,0 和 4 区写实现,回调也放 4 区,统统写在 BEGIN 和 END 之间。


复习速记

  • 带 IT 后缀就是以中断方式工作,比如 HAL_UART_Receive_IT;回调函数名固定,写个同名的就覆盖掉系统那个空的 __weak 版本。
  • 流程:每来 1 个字节进一次中断,ISR 把字节存进缓冲区,收满 Size 个才调 1 次回调,Cplt 就是完成。
  • 回调里处理完数据,记得再调用一次 HAL_UART_Receive_IT 续上,否则只收一轮。
  • CubeMX 加 Keil 的写法:PV 放变量,PFP 放函数声明,0 和 4 区写实现,回调放 4 区,只在 USER CODE BEGIN 和 END 之间写。
  • 找回调:右键 Go To Definition,或者在 HAL 源码里搜 __weak。
  • 不定长数据三种做法:每次收 1 字节加判断结束符;空闲中断 IDLE;DMA 加空闲中断。
相关推荐
不悔哥1 小时前
ESP32上跑 Linux: 乐鑫给 ESP32-S31开放了官方 BSP
linux·嵌入式硬件·开源
潜创微科技1 小时前
CH592:集成 BLE 的青稞 RISC-V 无线 MCU
c语言·单片机·risc-v·无线投屏·低延迟·沁恒
国科安芯1 小时前
星载微控制器MCU选型与系统设计的证据化方法
大数据·数据库·单片机·嵌入式硬件·低轨卫星·商业航天·抗辐射
番茄灭世神2 小时前
PN学堂GD32教程第16篇——Fatfs
单片机·嵌入式硬件
晚枫歌F2 小时前
dpdk实现tcpserver
单片机·嵌入式硬件
信奥卷王2 小时前
[GESP202406 六级] 二叉树(含视频)
数据结构·算法
学逆向的2 小时前
C 可变量参数
c语言·开发语言·网络安全
小莞尔2 小时前
【51单片机】【protues仿真】基于51单片机六路抢答器系统
c语言·单片机·嵌入式硬件·物联网·51单片机
2501_922639583 小时前
率能半导体 SS6622E直流双向马达驱动芯片:20V/2.4A,覆盖电子锁、玩具、无线充电、机器人四大应用场景
stm32·单片机·嵌入式硬件·硬件工程