ZZZ-15-串口通讯中断接收

第一步:STM32CubeIDE 配置 (.ioc)

我们要开启串口的中断“开关”

  1. 打开 Connectivity -> USART1
  2. 点击 NVIC Settings 选项卡。
  3. 勾选 USART1 global interrupt 后的 Enabled 框。
  4. 保存(Ctrl + S)生成代码。

第二步:编写接收逻辑 (main.c)

中断接收的逻辑是:你先告诉单片机“你听着,收到一个字节就叫我”,然后单片机去跑 while(1)。当电脑发来数据,单片机自动跳进“回调函数”执行任务。

1. 定义接收缓存

main.cUSER CODE BEGIN PV (Private Variables) 区域定义一个变量来存放收到的字符:

1
2
3
/* USER CODE BEGIN PV */
uint8_t rx_data; // 接收缓冲区,存放电脑发来的一个字节
/* USER CODE END PV */

2. 开启“听力”模式

main 函数的 while(1) 之前,调用一次中断接收函数:

1
2
3
4
/* USER CODE BEGIN 2 */
// 告诉单片机:开启中断接收,收满 1 个字节就存到 rx_data 变量里
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
/* USER CODE END 2 */

3. 编写“收到命令后做什么” (回调函数)

main.c 最下方的 USER CODE BEGIN 4 区域,重写回调函数。我们要实现:电脑发 ‘1’ 亮灯,发 ‘0’ 灭灯。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART1) // 确认是串口 1 触发的中断
    {
        if(rx_data == '1') // 电脑发来字符 '1'
        {
            HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 亮灯
            printf("LED ON!\r\n"); // 回复电脑
        }
        else if(rx_data == '0') // 电脑发来字符 '0'
        {
            HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);   // 灭灯
            printf("LED OFF!\r\n"); // 回复电脑
        }

        // 重要:中断是一次性的,处理完后必须再次开启,否则下次就收不到了
        HAL_UART_Receive_IT(&huart1, &rx_data, 1);
    }
}
/* USER CODE END 4 */

注意这里使用了 printf,需要查看 [[ZZZ-14-串口通讯UART]] 中关于 printf 的设置

第三步:原理图解 (中断流程)

graph TD A[1.程序启动:开启中断接收] --> B[2.CPU运行while循环] B --> C[3.电脑发送字符] C --> D[4.硬件触发UART中断] D --> E[5.进入Callback回调函数] E --> F[6.执行逻辑:如开关灯] F --> G[7.重新调用Receive_IT续约] G --> B

第四步:串口调试助手测试

  1. 烧录程序。
  2. 打开串口调试助手,波特率 115200。
  3. 在发送框输入字符 1(注意是字符模式,不是十六进制),点击发送。
    • 现象:板子上的 PC13 灯亮了,助手屏幕显示 LED ON!
  4. 在发送框输入字符 0,点击发送。
    • 现象:板子上的 PC13 灯灭了,助手屏幕显示 LED OFF!

避坑指南:为什么我的单片机只听一次话?

在代码里会看到反复强调最后一行 HAL_UART_Receive_IT

  • 原因:HAL 库的中断接收是“单次任务制”。当你收完指定的 1 个字节后,中断就会自动关闭。
  • 解决:必须在回调函数结束前,再次调用该函数,相当于给单片机续一份“听力合规”,它才能持续监听。

进阶挑战:如何接收一个“单词”

刚才实现的例子是“一个字节”触发一次中断。但现实中,我们经常需要接收一串字符,比如发送 "ON" 开启,发送 "OFF" 关闭。作为初学者,处理字符串接收有两种最常用的思路:

方案 A:字节拼接法(最基础,锻炼逻辑)

  • 原理:依然是 1 个字节触发一次中断。
  • 逻辑:在单片机里准备一个“小篮子”(数组)。每收到一个字节,就把它丢进篮子,直到收到了回车符 \n,说明一句话说完了。然后用 strcmp 函数比较这个篮子里装的是不是 "ON"

方案 B:空闲中断法(最优雅,专业用法)

这是处理不定长数据(比如一条指令有时长,有时短)最优雅、最工业级的方案。

  • 原理:利用 STM32 串口的一个高级特性——IDLE(空闲中断)
  • 逻辑:单片机会盯着串口线,如果发现超过 1 个字节的时间没有新数据进来了,它就认为“这串话发完了”,然后一次性把整个数组交给你处理。

下面开始方案 B 空闲中断法的学习

1. 为什么它最“优雅”?

  • 传统中断:像“强迫症”,必须数够 10 个字节才告诉你。如果对方只发了 5 个,你就永远在死等。
  • 空闲中断:像“观察员”。它不管对方发了多少,它只盯着信号线。只要线上一段时间没有波动了(空闲了),它就认为“这串话发完了”,立刻叫醒 CPU。

最强组合:UART IDLE + DMA 通常我们会配合 DMA(直接存储器访问)使用。DMA 就像一个搬运工,数据一到就自动搬到内存里,不占用 CPU 资源。只有当一串话全说完了(触发 IDLE),CPU 才出来看一眼。

2、核心原理图

graph LR Sender[电脑/传感器] -- 发送数据流 --> RX_Pin[STM32 RX 引脚] RX_Pin -- 自动搬运 --> DMA[DMA 搬运工] DMA -- 存入 --> Buffer[内存缓冲区/数组] subgraph "检测机制" Line[信号线状态] -- 消失一段时间 --> IDLE_Flag[触发 IDLE 空闲中断] end IDLE_Flag -- 通知 --> CPU[CPU 处理整条数据]

3. STM32CubeIDE 配置步骤

我们要用到 HAL 库中一个非常强大的“新”函数:HAL_UARTEx_ReceiveToIdle_DMA(这里的 Ex 代表 Extended 扩展功能)。

  1. 开启 DMA
    • .ioc 文件中,点击 Connectivity -> USART1
    • 点击 DMA Settings 选项卡 -> 点击 Add
    • 选择 USART1_RX
    • Priority 选 Medium(中等)。
    • ModeNormal(普通模式,接收完处理后再重开)。
  2. 确认 NVIC
    • NVIC Settings 选项卡中,确保 USART1 global interrupt 已勾选(DMA 配合空闲中断也需要开启串口总中断)。
  3. 生成代码Ctrl + S

4. 编写代码 (main.c)

第一步:定义缓冲区

1
2
3
/* USER CODE BEGIN PV */
uint8_t rx_buf[100]; // 准备一个足够大的“篮子”
/* USER CODE END PV */

第二步:启动接收(在 main 函数循环前)

1
2
3
4
5
6
/* USER CODE BEGIN 2 */
// 告诉单片机:用 DMA 方式接收数据,如果触发了空闲中断或存满了 100 字节,就叫我
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, 100);
// 禁掉 DMA 的过半中断(可选,防止大数据量时频繁进中断)
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); 
/* USER CODE END 2 */

第三步:编写回调函数

注意!空闲中断的回调函数和之前的 RxCpltCallback 不同,它叫 RxEventCallback

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* USER CODE BEGIN 4 */
// Size 参数非常有用:它告诉你这次实际上收到了多少个字节
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART1)
    {
        // 1. 处理数据:比如把收到的单词原封不动发回给电脑
        HAL_UART_Transmit(&huart1, rx_buf, Size, 100);
        
        // 2. 可以在这里做字符串比较
        if (strncmp((char*)rx_buf, "ON", 2) == 0) {
            HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
        }
        else if(strncmp((char*)rx_buf, "OFF", 3) == 0) {
        	HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
        }
        // 3. 重要:处理完后,必须再次开启接收
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, 100);
        __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
    }
}
/* USER CODE END 4 */

5. 这种方案的“职业美感”体现在哪?

  1. Size 变量:这是神来之笔。如果你发 "Hello"Size 就是 5;如果你发 "How are you"Size 就是 11。你再也不用自己去写循环判断 \n 了。
  2. 极低 CPU 占用:在数据传输过程中,CPU 甚至可以去“睡觉”(进入低功耗模式)。只有整句话说完了,CPU 才醒过来处理一下。
  3. 高可靠性:不再会因为数据发得太快而导致丢字节(DMA 硬件级搬运)。

常见避坑

  • 重载函数名:一定要写对 HAL_UARTEx_RxEventCallback,拼错一个字母它都不会运行。
  • 缓冲区溢出:如果你定义的 rx_buf 是 100,但对方一次性发了 150 个字节,DMA 会在满 100 的时候强制触发一次中断。所以缓冲区要设得比最长的指令稍微大一点。

现在你可以试试发送 "Hello STM32"。你会发现,无论你发多长或多短,单片机都能精准地识别出长度并回复你。并且发送 ON,灯会亮、发送 OFF,灯会灭

Licensed under CC BY-NC-SA 4.0