1.4 UART通信 - 实验篇

这个实验中我们要使用 UART 进行点灯

  • 让单片机通过 UART 给电脑发送消息
  • 在电脑上给单片机的 UART 串口发信号, 检测到是开灯信号的时候将小灯打开

Step 0 - 接线

我们用来进行烧录的 daplink 同样具有 TTL 通信的功能, 如果你不使用 daplink 而是用的 stlink 那么你需要一个 USB 转 TTL 模块 , 让你的电脑接受到串口信号

什么是 TTL ? - TTL 指的是 TTL 电平(晶体管-晶体管逻辑电平) - USB 转 TTL 模块它的主要作用是让电脑和单片机进行对话: 电脑的 USB 信号不能直接接到单片机的引脚上. 这个模块将 USB 信号转换成标准的 TTL 串口信号(通常包含 TXD 发送引脚和 RXD 接收引脚

如果你使用 daplink, 直接将 daplink 上的 Rx 与单片机的 Tx 相连, Tx 与 Rx 相连(单片机上的 Tx 和 Rx 在哪请看下文)

如果你使用 stlink, 将 USB 转 TTL 模块的Rx 与单片机的 Tx 相连, Tx 与 Rx 相连, GND 相连, 然后将它的另一端插到电脑上

串口助手: 这是一个你需要在电脑上下载的软件用于串口连接, 你随便在浏览器里面搜索串口助手然后下载一个就行 如果你是 MAC 用户, 我推荐使用CoolTrem, 当然他也有 Windows 版本的

Step 1 - CubeMX 配置

这里默认你前面已经好好学了, 我们新建一个工程, 因为我们要点灯所以使能一个 LED 灯的 GPIO 口. 再进行一些基础配置之后来到了这个实验的重头戏 – UART

  • 打开 Connectivity 选项, 选择USART1并且将它的模式选为 Asynchronous(异步)Pasted image 20260625213625

这里我们发现并没有一个 UART 设备, 而都是 USART 设备, 在原理篇中其实我们已经做了解释. USART 设备与 UART 的差别在于他不仅可以像UART 那样使用异步模式, 他还可以使用Synchronous(同步)模式, 那么这里我们将它的模式选择为异步

  • 在参数设置中配置 USART1 设备, 将比特率配置为 115200 bits/s(你也可以选别的数值), Word Length配置为 8(注意这个 Word Length包含校验位) 然后调整为 8N1 模式, 最后确定你选择的是 Receive and Transmit(即收又发), 过采样保持 16 就行Pasted image 20260625214412 配置好 USART1 这个外设之后你就会发现右侧的芯片图多使能了两个 GPIO口, 这个就是我们 daplink 要连接的地方

Step 2 - 写代码

配置完成之后点击生成代码, 在 Clion 中打开main.c 文件

  • 我们要在 PV(private variables) 中定义两个自己的数组用来储存发送或者接受的数据Pasted image 20260625215002

这个 uint_8 是什么意思?为什么不直接写 int?

因为 int 的大小是取决于编译器和硬件架构的, 他不控, 比如在你的 x86/ARM 电脑中 int 的大小是 4 字节. 但是在 8 位单片机中(51 单片机) int 的大小是 2 字节. stm32 是 32 位单片机int 的大小也是 4 字节, 但是为了规范我们在嵌入式开发中不会使用 int

uint_8 是指定他使用了 8 bit, 也就是 1 Byte uint_16 是指定他使用了 16 bit, 也就是 2 Byte

所以回过头来看uint_8 RxData[10] 是在说创建一个 RxData 的数组, 数组的每个元素的大小是一字节, 总共有 10 个元素

  • \r (0x0D):光标回到行首
  • \n (0x0A):光标移到下一行 那么 uint_8 TxData[10] = “hello”; 的内存分布就是Pasted image 20260625220437 与此同时我们要注意C 语言字符串在末尾会自动加上 \0 表示终止, 这个内存试图没有看出来我也不知道是怎么回事, 但是这个 \0 也就是在下标 7 的位置他是真正存在的

Pasted image 20260626063259 普通, 是指在接受数据的时候需要缓冲区接收满(超时) 后才能退出


Step 2.1 - 普通阻塞式

  • 来到while 循环, 我们使用HAL_UART_Transmit

  • 1
    HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout)
  • 来进行阻塞式的发送信息, Size 表示要发送多少个字节, Timeout 表示设定一个最长的等待时间去等待 UART 硬件状态标志位置位, MCU 操作寄存器的速度十分的快速, 然后 UART 外设需要将数据层寄存器中取出然后发送出去, 这个就是 MCU 在等待的时间, 也就是”阻塞”Pasted image 20260625224058 这里表示从 uart1 发出, 发送的数据是 TxData, 发送 7 个字节(7 字节就行, 因为后面的都是我们不需要的, 你也可以看看把它填成 8 会出现什么情况, 是不是会多一些点出来), 最长等待时间 1000 ms

  • 1
    HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
  • 这个接受数据HAL_UART_Receive() 也是同理

这个接受函数 Timeout 等待的是发送方. 比如你这里写你要接受 5 个字节, 但是发送方只发送了 4 个字节, 如果没有 Timeout, MCU 就会一直傻等知道发送方把最后一个字节给发送过来, 有 Timeout 的话超过了这个时间 MCU 就不会再等了, 超时后就带着那四个字节返回一个HAL_TIMEOUT来告诉你他超时了. 这些你都可以在 CLion 中跳转到这个函数的用例中看到

  • 这里的逻辑是我们接受的数据中如果第一位是 ‘1’ , 那么就把灯的电平反转, 然后再将第一位置为 0 . 所以只要我们在串口助手中输入 1 那么我们的灯就会点亮, 再发送一个 1 灯熄灭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Private variables ---------------------------------------------------------*/  

/* USER CODE BEGIN PV */
uint8_t RxData[10];
uint8_t TxData[10] = "hello\r\n";
/* USER CODE END PV */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_UART_Transmit(&huart1, TxData, 7, 1000);
HAL_UART_Receive(&huart1, RxData, 5, 1000);
if (RxData[0] == '1')
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
RxData[0] = '0';
}
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */}

现在插上线来看看你的阻塞式效果如何吧!

现在我们可以再试一试, 如果长按键盘上的 1, 也就是快速的一直发 1 会有什么反应, 你的 hello 是不是变快了?(如果不好观察的话将这个去掉)想想为什么, 和阻塞有关系吗?可以把回答写在评论区


Step 2.2 - 普通中断式

在这个实验中我们要实现 UART 随意的收发定长的数据

  • 我们首先要把 USART1 的中断开启, (中断由 NVIC 控制你还记得吗?), 来到 NVIC 设置, 将他选择为 EnablePasted image 20260625230141
  • 然后再重新生成代码, 用 Clion 打开

我们发现启用之后stm32f1xx_it.c, 中断向量层出现了这个ISR(中断服务函数)Pasted image 20260625231103

第 0 步, 写PV

在 PV(Private variables) 中定义两个数组, 用于存储要收发的数据

1
2
3
4
/* USER CODE BEGIN PV */  
uint8_t RxData[10];
uint8_t TxData[10] = "hello";
/* USER CODE END PV */

第 1 步, 开启一次中断式接受

我们要开启中断式接受(注意, 如果只写一个那么接受只能进行一次, 所以我们可以在这个回调函数中再次开启一次接受, 用来接受下一次信息), 在 while(1), 之前的 user code2 中Pasted image 20260625232344

1
2
3
/* USER CODE BEGIN 2 */  
HAL_UART_Receive_IT(&huart1, RxData, 5);
/* USER CODE END 2 */

这里就解释了为什么我们只能收发定长的数据, 因为这个函数的参数是 5, 这表明, 只有接受到 5 字节的数据之后, 他才会执行你接下来写的回调函数中断. 而它的非阻塞中断是体现在收到完整的一个字节才叫 CPU 来读, 而不是让 CPU 等着读(阻塞), 如果还不懂就回原理篇看看文末的图文

第 2 步, 实现这个中断式接受的回调函数

这个回调函数叫做HAL_UART_RxCpltCallback那么我们去实现它, 这里要说明, 之前我们实现回调函数在HAL_UART_RxCpltCallback这个文件, 这是便于初学者学习搞清楚代码结构, 但是更规范的做法是在 main.c 中实现, 以后的回调函数实现我也都会在 main.c 中

并且这个函数不要写在 main 函数里面, 应该写在 user code 4 这里比较合适

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* USER CODE BEGIN 4 */  
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1)
{
HAL_UART_Receive_IT(&huart1, RxData, 5); // 再使能一次中断式接受
if (RxData[0] == '1')
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
RxData[0] = '0';
}
}
}
/* USER CODE END 4 */

我们也可以追根溯源一下这个回调函数Pasted image 20260625234433 我们可以看到在 ISR 中, 他经过的是UART_Receive_IT这个中间层, 然后在UART_Receive_IT调用了HAL_UART_RxCpltCallback 这个函数Pasted image 20260625234747 所以调用路径是

层次 函数
1 USART1_IRQHandler
2 HAL_UART_IRQHandler
3 UART_Receive_IT
4 HAL_UART_RxCpltCallback

这比我们之前学的外部中断和定时器中断多一层

第 3 步, 使能中断式发送

在 while(1)中我们启用中断式发送

1
2
3
4
5
6
7
/* Infinite loop */  
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_UART_Transmit_IT(&huart1, TxData, 5);
HAL_Delay(500);
/* USER CODE END WHILE */

想想看, 这里我们要不要实现这个发送的回调函数?

回答是不用的, 中断式发送, 它的目的就是要发送数据, 它中断的是原本要用来等待外设搬运数据的这个时间, 所以你写成这样你就可以实现中断式发送了

而这个中断式发送的回调函数, 是用于发送完数据之后, 如果你还要执行一些逻辑的时候才去使用它

第 4 步, 烧录和测试

连上你的单片机和串口, 在 clion 中启用 OpenOCD 程序进行烧录.

完成之后, 你会发现, 你只有每输入五个 1(完整的 5 个字节的数据)之后, 中断式接收回调函数才会被启用, 去判断你的数据的第一个字节的值

然后你再观察, 你输入的时候, 单片机的发送仍然在继续着, 这就是接收中断的体现; 如果没有这个接收中断, 单片机就会一直在等待你发送完 5 个数据, 期间做不了任何事情 然后你再观察, 你很快的输入的时候, 单片机的发送速度不会改变, 与上面的非中断式形成鲜明的对比, 这就是发送中断的体现

完成这个实验之后, 你还可以吧HAL_UART_Receive_IT这个函数设成接受一字节就行, 那么就会变成, 按一下 1, 灯立马就会亮起来


如果, 你确实是要在发送完数据之后完成一些逻辑, 那么我们就可以使用发送中断函数HAL_UART_TxCpltCallback, 和我们之前的中断一样的用法, 写在 main.c 的 user code 4 中比较合适

但是它的调用层次又有些许的不同, 我们现在来进行探究 在中断向量层的 ISR 函数HAL_UART_IRQHandler 内我们可以看到有一个UART_EndTransmit_IT函数Pasted image 20260626003027

我们继续进入它, 发现在这个函数中他才调用了我们的回调函数Pasted image 20260626003159

所以调用层次是这样的:

层次 函数
1 USART1_IRQHandler
2 HAL_UART_IRQHandler
3 UART_EndTransmit_IT
4 HAL_UART_TxCpltCallback

这里也是四层, 但是和 Rx 不一样的是, 第三层 Tx 是 EndTransmit 函数, 而 Rx 是Receive函数, 没有 EndReceive. 而 Tx 的 Transmit 函数有自己的作用, 可以课外去了解


Step 2.3 - 空闲(IDLE)中断式

这个实验我们要使用空闲式中断接收函数来完成点灯, 因为发送没有空闲这个概念, 所以我们直接说是空闲中断函数

根据我们原理篇的讲解, 空闲式中断函数的逻辑是:

  • 要么收满 5 个就回调
  • 要么对方发完一阵、线路空闲下来了(哪怕只发了 1 个、3 个),也立刻回调

第 1 步 - CubeMX 的配置

和之前的配置一样, 我们需要使能 USART1 的中断

第 2 步 - 写 PV

和之前一样写 PV

1
2
3
4
/* USER CODE BEGIN PV */  
uint8_t RxData[10];
uint8_t TxData[10] = "hello";
/* USER CODE END PV */

第 3 步 - 写空闲中断(接受)函数

我们在main 函数里面, 非 while(1)的地方开启一次空闲中断HAL_UARTEx_ReceiveToIdle_IT 与我们之前讲的接受中断函数同理, 他也是一个函数只能接收一次, 剩下的在中断里面再开启就行

1
2
3
/* USER CODE BEGIN 2 */  
HAL_UARTEx_ReceiveToIdle_IT(&huart1, RxData, 5);
/* USER CODE END 2 */

它的回调函数是HAL_UARTEx_RxEventCallback, 它在HAL_UART_IRQHandler直接被调用了, 相比前面的非空闲中断少了一层Pasted image 20260626020814

层次 函数
1 USART1_IRQHandler
2 HAL_UART_IRQHandler
3 HAL_UARTEx_RxEventCallback

所以我们同样的, 在 user code 4 中实现这个空闲中断回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* USER CODE BEGIN 4 */  
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == &huart1)
{
HAL_UARTEx_ReceiveToIdle_IT(&huart1, RxData, 5); // 不要忘了哦
if (RxData[0] == '1')
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
for (int i = 0; i < Size; i++) RxData[i] = 0;
}
}
}
/* USER CODE END 4 */

其实在这个简单的实验里面清理首位就行, 甚至不清理下次传入的数据也会把之前的旧数据覆盖掉. 但是我这里使用 for 循环把所有的数据清理掉了是为了用上这个函数的 Size 参数, 这样显得更加优雅一点

第 4 步 - 写中断式发送函数

和之前一样, 写HAL_UART_Transmit_IT 函数

1
2
3
4
5
6
7
/* Infinite loop */  
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_UART_Transmit_IT(&huart1, TxData, 5);
HAL_Delay(500);
/* USER CODE END WHILE */

第 5 步 - 烧录并测试

现在你可以试试看每次发送一个 1, 灯就会打开或者关闭一次

然后你再试一试一次发六个1 或者100001 灯会有什么变化 你会发现灯短暂的亮了一下(很小很小)然后又迅速被关闭了, 这就是空闲中断的体现, 前面的 5 个 1 已经满足一次中断, 然后后面的一个 1 自己单独触发一次空闲中断

如果你和我一样使用的是 CoolTerm 那么在上面的Option(设置) 你可以开启发送字符串的选项, 但是如果你发送单个字符就直接在输出的窗口点就好了Pasted image 20260626023435


Step 2.4 - 普通 DMA 式

第 1 步: CubeMX 的配置

和之前的一样, 使能USART1 的中断, 完成相关的配置

接下来, 我们需要使能 USART1 的 DMA 点击 Add, 将 Rx 和 Tx 都加上Pasted image 20260626025741 加上了之后你就会在stm32f1xx_it.c中断向量层中看到两个有关 DMA 的 ISRPasted image 20260626064550

这里我将详细讲讲 DMA 中的具体的这些配置项都是什么意思Pasted image 20260626025927

  • Mode, 普通模式即可, 他支持定时搬运或代码主动调用. 循环模式会一直搬运下去
  • Data Width 选择 Byte, 因为 UART 以字节区分数据
  • Increment Address, 地址自增. 如果勾上 Peripheral(外设)意思就是:DMA 每搬一个数据,外设侧的访问地址也 +1,下一个数据就去 外设基址 + 1、再下一个 +2 对于 UART 来说它必须不能勾上, 因为UART 收发永远只用一个固定的寄存器 – 数据寄存器 DR。发的时候所有字节都往同一个 DR 写,收的时候所有字节都从同一个 DR 读。这个 DR 的地址是固定的,字节是靠移位寄存器一个个从这同一个口进出的(这与前面的原理篇的内容相呼应)

而 Memory 需要勾上, 内存侧是你的缓冲区 RxData[10](或 TxData),你当然希望第 1 个字节进 RxData[0]、第 2 个进 RxData[1]、第 3 个进 RxData[2]……一格一格往后填 所以内存地址要自增 如果不勾上 Memory 你的内存地址不会自增, 所有的数据都堆在同一位, 这就导致了后来的数据会把先前的数据覆盖掉, 最终的你数据只剩下最后一个在 RxData[0] 的位置

  • FIFO, First In First Out 先进先出, 就像队列一样(有些芯片会有这个选项) 当接受信息太多的时候 DMA 可能搬运不过来, FIFO可以先把来不及搬运的内容缓存下来排队, 再一个一个的让 DMA 进行搬运, 但代价是让信息的时效性降低

你需要考虑代价的对比, 信息丢失 vs 信息时效性差, 根据这个来决定是否启用 FIFO 和 FIFO 的长度, 在这个实验中我们不用进行勾选

现在你回头去看看原理篇结尾的内容吧!

第 2 步: 写代码

HAL_UART_Receive_DMA() 什么时候产生中断?

当调用 HAL_UART_Receive_DMA(&huart1, RxData, 10) 时,你设定了接收长度为 10

DMA 硬件只认数量,不认时间, 它只会在以下两个固定的数量节点产生中断:

  1. 过半中断(HT, Half Transfer):当收满一半数据(第 2 个字节, 向下取整)时触发
  2. 完成中断(TC, Transfer Complete):当不多不少正好收满 10 个字节时触发, 才此时会进入 HAL 库的回调函数 **__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){}**

如果对方只发了 5 个字节就停止了,因为没有收满 10 个字节,DMA 的完成中断永远不会触发HAL_UART_RxCpltCallback 也绝对不会执行, 数据就会卡在缓冲区里,这就是为什么不能单靠 HAL_UART_Receive_DMA() 来接收不定长数据

总体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* USER CODE BEGIN PV */  
uint8_t RxData[10];
uint8_t TxData[10] = "hello";
/* USER CODE END PV */

/* USER CODE BEGIN 2 */
HAL_UART_Receive_DMA(&huart1, RxData, 5);
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_UART_Transmit_DMA(&huart1, TxData, 5);
HAL_Delay(500);
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */}
/* USER CODE END 3 */

/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1)
{
HAL_UART_Receive_DMA(&huart1, RxData, 5);
if (RxData[0] == '1')
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
RxData[0] = 0;
}
}
}
/* USER CODE END 4 */

这个使用 DMA 的中断函数和我们之前那个Step 2.2 - 中断式 的回调函数是一样的

现象: 你发现他和 Step 2.2 的实验现象没什么两样, 也确实, 因为他本质就是用 DMA 代劳了数据的搬运, 只会在长任务体现出效率的差别, 这个小实验看不出啥


Step 2.5 - 空闲 DMA 式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* USER CODE BEGIN PV */  
uint8_t RxData[10];
uint8_t TxData[10] = "hello";
/* USER CODE END PV */

/* USER CODE BEGIN 2 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, RxData, 5);
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */ while (1)
{
HAL_UART_Transmit_DMA(&huart1, TxData, 5);
HAL_Delay(500);
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */ }
/* USER CODE END 3 */
}

/* USER CODE BEGIN 4 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == &huart1)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, RxData, 5);
if (RxData[0] == '1')
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
for (int i = 0; i < Size; i++) RxData[i] = 0;
}
}
}
/* USER CODE END 4 */

总结一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*阻塞收发函数*/

// ---------- 第一种: 普通阻塞式 -------------
// 以阻塞的方式发送,库函数在前台处理发送,缓冲区发送完 或 超时后退出
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// 以阻塞的方式接收,库函数在前台处理接受,缓冲区接收满 或 超时后退出
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// 回调: 对于接收完毕回调函数,如果连续接收要在此重新开始接收
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){}
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart){}


// ---------- 第二种: 空闲阻塞式 -------------
// - 如果 Timeout 的时间少于一帧, 在检测到空闲之前就返回了, 这就是空闲阻塞, 它本质上还是阻塞, 因为假如预期等待 5 个字节, 在接受完 4 个字节后 CPU 还在等待第 5 个字节, 这个时候在检测到空闲之前, 它超时了, 返回

// 无发送, 请使用普通阻塞式的发送

// 以空闲阻塞的方式接收,库函数在前台处理接收,空闲 或 缓冲区接收满 或 超时后退出
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint16_t *RxLen, uint32_t Timeout);

// 回调函数: 如果连续接收要在此重新开始接收,具体什么事件会调用请参考HAL手册
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*中断收发函数*/

// ---------- 第一种: 普通中断式 -------------
// 以中断的方式发送,每发送一个字节就进入CT中断
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)

// 以中断的方式接收,每收到一个字节就进入RXNE中断,库函数在后台自动处理中断,缓冲区收满 自动调用接收完毕回调函数
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

// 回调: 对于接收完毕回调函数,如果连续接收要在此重新开始接收
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){}
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart){}


// ---------- 第二种: 空闲中断式 -------------
// - 在空闲的时候触发中断事件

// 无发送

// 以空闲中断的方式接收,每收到一个字节就进入RXNE中断,库函数在后台自动处理中断,空闲 或 缓冲区收满 等事件自动调用UARTEx事件回调函数
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

// 回调函数: 如果连续接收要在此重新开始接收,具体什么事件会调用请参考HAL手册
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* DMA 中断 - 非阻塞*/
// - 他是非阻塞的(因为他没让 CPU 在傻等着接收 RDR 的数据)


// ---------- 第一种: 普通 DMA 式 -------------
// 普通 DMA 中断在接受数据的时候, 不是像空闲中断那样等待一帧的时间, 他是靠计数 size (接受数据的数量)直到接受完了 size 个之后才返回回调函数

// 发送:
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)

// 接受:
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

// 回调: 对于接收完毕回调函数,如果连续接收要在此重新开始接收
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){}
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart){}

// ---------- 第二种: 空闲 DMA 式 -------------
// 无发送, 请使用普通 DMA 的发送

// 接受:
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

// 回调: 对于接收完毕回调函数,如果连续接收要在此重新开始接收
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){}

我们发现, 只有空闲阻塞或者空闲中断的回调函数发生了改变, 而其他的都是void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){}

因为空闲是 UARTEx事件, 所以我们要使用 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){}回调函数, 我们还可以通过如下函数查询是什么事件触发了UARTEx回调函数

1
2
// 查询是什么事件触发了UARTEx回调函数
HAL_UART_RxEventTypeTypeDef HAL_UARTEx_GetRxEventType (UART_HandleTypeDef * huart);