STM32 HAL 库实现串口通信
XiaoMa 博士生

引脚定义

在 STM32F429IGT6 这块开发板中:

  • USART1_TX 与 PA9 复用,USART1_RX 与 PA10 复用。
  • USART2_TX 与 PA2 复用,USART2_RX 与 PA3 复用。
  • USART3_TX 与 PB10 复用,USART3_RX 与 PB11 复用。

HAL 库串口发送重要函数

  • 阻塞式发送函数新手推荐使用

    阻塞式发送函数是指在向设备发送数据时,函数会一直阻塞(即一直等待)直到数据发送完毕后才返回。在这种发送方式下,发送函数会一直等待直到发送缓冲区中的数据全部被发送出去,才会返回函数执行结果。

    1
    2
    3
    4
    HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, unit32_t Timeout);

    /* 参数解释 */
    句柄(哪个外设),指针,数据长度,超时时间
  • 非阻塞式发送函数不推荐使用

    非阻塞式发送函数是指在向设备发送数据时,不会一直等待数据全部发送完毕后才返回,而是在发送数据时,将数据放入发送缓冲区中,然后立即返回函数执行结果,继续执行后续代码。这种方式下,发送函数不会阻塞当前线程或任务,可以提高系统的实时性和响应能力。但是,需要在发送函数中添加相应的错误处理机制,以避免因为发送过程中出现错误导致数据未发送完成的问题。

    1
    2
    3
    HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

    /* 可以看到这个函数里面多了 IT(interrupt 中断),且没有了 Timeout(超时)参数 */

    发送完毕中断回调函数

    发送完毕中断回调函数指的是当使用 UART 或者其他通信方式向外部设备发送数据时,当数据全部发送完毕后,会产生一个发送完成中断(或称为发送完毕中断)。这个中断是外部设备向处理器发送的一种通知,用于告诉处理器数据已经全部发送完成,可以进行其他操作了。当发送完成中断触发时,可以通过调用对应的中断回调函数来处理这个中断事件。中断回调函数是在中断服务程序之后执行的一种特殊函数,它负责处理中断服务程序中未处理完的任务。

    1
    2
    void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
    void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);

举个🌰1

要求:使用(非)阻塞式的串口发送函数,将发送缓存数组 dat_Txd 中的前 5 个数据发送到 USART1,在数据发送完成后,翻转 PB1(LED0)引脚的输出电平。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 使用非阻塞式串口 1 发送函数 */
HAL_UART_Transmit_IT(&huart1, dat_Txd, 5); //因为 dat_Txd 是数组,前面不用加 &(取地址符号)
/* 发送完成后,使用中断回调函数来控制 LED0 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
{
if(huart->Instance == USART1)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1);
}
}
---------------------------------------------------------
/* 使用阻塞式串口 1 发送函数 */
HAL_UART_Transmit(&huart1, dat_Txd, 5, 10000);
/* 发送完成后,直接使用 Toggle 控制 LED0 */
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1);

HAL 库串口接收重要函数

  • 阻塞式接收函数不推荐使用

    阻塞式接收函数是指在从设备接收数据时,函数会一直阻塞(即一直等待)直到接收到完整的数据后才返回。在这种接收方式下,接收函数会一直等待直到接收缓冲区中的数据长度达到预定长度,或者接收超时时间到达后才返回函数执行结果。如果接收数据长度过短或者接收速率过慢,会导致阻塞时间较长,从而影响系统的实时性和响应性能。

    1
    2
    3
    4
    HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, unit32_t Timeout);

    /* 参数解释 */
    句柄(哪个外设),指针,数据长度,超时时间
  • 非阻塞式接收函数推荐使用

    非阻塞式接收函数是指在从设备接收数据时,不会一直等待数据接收完成后才返回,而是在接收数据时,将接收到的数据存入接收缓冲区中,然后立即返回函数执行结果,继续执行后续代码。这种方式下,接收函数不会阻塞当前线程或任务,可以提高系统的实时性和响应能力。但是,需要在接收函数中添加相应的错误处理机制,以避免因为接收过程中出现错误导致数据未接收完成的问题。

    1
    2
    3
    HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

    /* 可以看到这个函数里面多了 IT(interrupt 中断),且没有了 Timeout(超时)参数 */

    接收完毕中断回调函数

    接收完毕中断回调函数指的是当使用UART或者其他通信方式接收到完整的数据后,会产生一个接收完成中断(或称为接收完毕中断)。这个中断是外部设备向处理器发送的一种通知,用于告诉处理器数据已经接收完成,可以进行其他操作了。当接收完成中断触发时,可以通过调用对应的中断回调函数来处理这个中断事件。中断回调函数是在中断服务程序之后执行的一种特殊函数,它负责处理中断服务程序中未处理完的任务。

    1
    2
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
    void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);

举个🌰2

要求:使用非阻塞式的串口接收函数,接收USART1中的一个字节,将其保存在 dat_Rxd 变量中,在数据发送完成后,若该字节为 0x5A,则翻转 PB0(LED1) 引脚的输出电平。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 使用非阻塞式串口 1 发送函数 */
HAL_UART_Receive_IT(&huart1, &dat_Rxd, 1);//因为 dat_Rxd 是地址,前面需要加 &(取地址符号)
/* 接收完成后,使用中断回调函数来控制 LED1 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
{
if(huart->Instance == USART1)
{
if(dat_Rxd == 0x5A)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
}
}
}

例程 1:通过串口通信开关灯

要求:在 STM32F429IGT6 中进行 STM32 应用开发,完成以下功能。

  • 开机后,向串口 1 发送”Hello World!”。
  • 串口 1 收到字节指令”0xA1”,打开 LED0(PB1),发送”LED0 Opened!”。
  • 串口 1 收到字节指令”0xA2”,关闭 LED0(PB0),发送”LED0 Closed!”。
  • 在串口发送过程中,打开 LED1 作为发送数据指示灯。

步骤

  1. 打开 STM32CubeMX 软件,按照 STM32CubeMX 通用配置[1]配置完成后。将 PB0、PB1 引脚分别设置为 GPIO_Out。接着配置 USART1 的模式、参数及使能中断,如图所示:
image-20230410152306321
图 4.1 串口 USART1 配置
image-20230410152508181
图 4.2 使能 USART1 中断
  1. 进行 STM32CubeMX 通用配置 [1]的第 5 步:生成代码。

  2. 打开工程文件。按照 Keil5 MDK 通用配置 [1]完成后开始编写代码。

    打开 usart1.c 文件,可以看到 huart1 的配置。

    image-20230410153414008
    图 4.3 USART1 的代码初始化

    打开 main.c 文件。

    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    -----------------------
    ↓↓↓注:main() 函数外↓↓↓
    -----------------------
    /* USER CODE BEGIN 0 */

    /* 宏定义 LED0、LED1 亮灭 */
    #define LED0_ON() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET);
    #define LED0_OFF() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);
    #define LED1_ON() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
    #define LED1_OFF() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);

    /* 首先定义了无符号 8 位整数型(unsigned 8-bit integer)数组的语句,常用于表示一串 ASCII 字符。其中,Tx_str1 是数组的名称,方括号中没有指定数组长度,因此该数组长度将根据初始化时赋值的元素个数进行确定 */
    uint8_t Tx_str1[] = "Hello World!\r\n"; // \r:回车,\n:换行
    uint8_t Tx_str2[] = "LED1 Opened!\r\n";
    uint8_t Tx_str3[] = "LED1 Closed!\r\n";

    /* 定义了一个无符号8位整数型(unsigned 8-bit integer)变量,通常用于存储通过UART接收到的单个字节数据,每次接收完成后将数据存储到该变量中 */
    uint8_t Rx_dat = 0;
    /* 中断回调函数, 在这里面实现接收到指令后的功能*/
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
    {
    if(huart->Instance == USART1)
    {
    if(Rx_dat == 0xa1)
    {
    LED0_ON();

    /* LED1 作为指示灯,先设置高电平灭掉,然后发送数据,再点亮 */
    LED1_OFF();
    HAL_Delay(200);
    HAL_UART_Transmit(&huart1, Tx_str2, sizeof(Tx_str2), 10000);
    HAL_Delay(200);
    LED1_ON();

    HAL_UART_Receive_IT(&huart1, &Rx_dat, 1);
    }
    else if(Rx_dat == 0xa2)
    {
    LED0_OFF();

    /* LED1 作为指示灯,先设置高电平灭掉,然后发送数据,再点亮 */
    LED1_OFF();
    HAL_Delay(200);
    HAL_UART_Transmit(&huart1, Tx_str3, sizeof(Tx_str3), 10000);
    HAL_Delay(200);
    LED1_ON();

    HAL_UART_Receive_IT(&huart1, &Rx_dat, 1);
    }
    }
    }

    /* USER CODE END 0 */

    -----------------------
    ↓↓↓注:main() 函数内↓↓↓
    -----------------------
    /* USER CODE BEGIN 2 */

    /* 功能 1:开机后向串口 1 发送 “Hello World!” */
    /* LED1 作为指示灯,先设置高电平灭掉,然后发送数据,再点亮 */
    LED1_OFF();
    HAL_Delay(200); //加入延时函数,以便肉眼观察得到
    HAL_UART_Transmit(&huart1, Tx_str1, sizeof(Tx_str1), 10000);
    HAL_Delay(200); //加入延时函数,以便肉眼观察得到
    LED1_ON();

    /* 功能 2、3:接收串口发来的数据” */
    /* &huart1 表示指向 UART1 外设的指针,&Rx_dat 表示指向存储接收数据的缓冲区的指针, 1 表示要接收的数据长度 */
    HAL_UART_Receive_IT(&huart1, &Rx_dat, 1);

    /* USER CODE END 2 */

    串口调试助手操作要点

    1. MicroUSB 接口要接到板子的左下角第二个口,写着 USB_232。才会显示 COM4:USB-SERIALCOM3 不能用于串口调试。
    2. 波特率要和 CubeMX 中设置的一致。
    3. 在发送框中输入程序中所写的字符串,然后选择 16 进制发送
image-20230410181007350
图 4.4 功能实现

例程 2:定时器与串口综合训练

要求:在 STM32F429IGT6 中进行 STM32 应用开发,完成以下的功能。

  1. 开机后,LED0 与 LED1 依次点亮,然后熄灭,进行灯光检测。

  2. 系统通过串口 1 向上位机发送一个字符串”STM32F429 欢迎您!”。

  3. LED0 作为一个秒闪灯,系统向上位机发送完字符串后,开始亮 0.5 秒,灭 0.5 秒……循环闪烁,并开始启动系统运行时间的记录,其时分秒格式为”XX:XX:XX”。

  4. 上位机通过一个由 3 个字节组成的命令帧控制 LED1 灯的开关。该命令帧的格式为”0xBF 控制字 OxFB”。0xBF 为帧头,0xFB 为帧尾,控制字的定义如下:

    0xA1:打开 LED1,返回信息”XX:XX:XX LED1 打开”。

    0xA2:关闭 LED1,返回信息”XX:XX:XX LED1 关闭”。

    其他:返回信息”XX:XX:XX 这是一个错误指令!”。

步骤

  1. 打开 STM32CubeMX 软件,按照 STM32CubeMX 通用配置[1]配置完成后。将 PB0、PB1 引脚分别设置为 GPIO_Out。接着配置定时器 TIM2、串口 USART1,最后中断使能,如图所示:
image-20230410224921634
图 5.1 LED 灯引脚设置
image-20230410224315250
图 5.2 定时器 TIM2 配置
image-20230410224531718
图 5.3 串口 USART1 配置
image-20230410224713093
图 5.4 中断使能
  1. 进行 STM32CubeMX 通用配置 [1]的第 5 步:生成代码。

  2. 打开工程文件。按照 Keil5 MDK 通用配置 [1]完成后开始编写代码。

    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    -----------------------
    ↓↓↓注:main() 函数外↓↓↓
    -----------------------
    /* USER CODE BEGIN Includes */
    #include "stdio.h"
    /* USER CODE END Includes */

    /* USER CODE BEGIN 0 */

    /* 第一步:宏定义 LED 灯,方便后续代码易读 */
    #define LED0_ON() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET)
    #define LED0_OFF() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET)
    #define LED1_ON() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET)
    #define LED1_OFF() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET)
    #define LED0_TOG() HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1)
    #define LED1_TOG() HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0)

    /* 第二步:定义所要用到的字符串 */
    uint8_t str1[] = "= = = = = = = Welcome to Xiaoma's codes = = = = = = =\r\n"; //开机显示
    uint8_t hh = 0, mm = 0, ss = 0, ss05 = 0; //定义时分秒,以及 0.5s
    uint8_t str_buff[64]; //定义一个字符串的缓冲数组,64 个字节
    uint8_t Rx_dat[16]; //定义一个串口接收的数组

    /* 第三步-1:功能函数:灯光检测 */
    /* 跑马灯,LED0、LED1 轮流灭亮 */
    void Check_LED()
    {
    HAL_Delay(1000);

    LED0_OFF();
    HAL_Delay(500);
    LED1_OFF();
    HAL_Delay(500);

    LED0_ON();
    HAL_Delay(500);
    LED1_ON();
    HAL_Delay(500);
    }

    /* 第五步-2:重写定时器 TIM2 的中断回调函数,使 LED0 按 0.5s 间隔闪烁,0.5 是在 CubeMX 中设置好的 500ms 产生一次中断 */
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
    LED0_TOG();

    /* 把时间变化记录到字符串时分秒中 */
    /* 逻辑:当记录两次 ss05,则 ss05 清零,记录 1s;当记录 60 次 ss,则 ss 清零,记录 1min;当记录 60 次 1min,则 mm 清零,记录 1h。依此循环*/
    ss05++;
    if(ss05 == 2)
    {
    ss05 = 0;
    ss++;
    if(ss == 60)
    {
    ss = 0;
    mm++;
    if(mm == 60)
    {
    mm = 0;
    hh++;
    }
    }
    }
    }

    /* 第六步-2:重写非阻塞式接收字符串的中断回调函数 */
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
    {
    /* 判断串口是否为 USART1 */
    if(huart->Instance == USART1)
    {
    /* 判断第一个字符是否为 BF 并且第三个字符是否为 FB */
    if(Rx_dat[0] == 0xBF && Rx_dat[2] == 0xFB)
    {
    /* 使用 switch:case/break 来判断第二个字符是什么,共有三组判断 */
    /* 此处也可以使用 if/else 来判断,但使用 switch 使代码更整洁 */
    switch(Rx_dat[1])
    {
    /* 接收到 a1 时,则 LED1 关闭 */
    case 0xa1:
    LED1_OFF();
    /* 要想使用 sprintf() 函数,需引入头文件 #include "stdio.h" */
    /* %d 是占位符,在双引号的后面写对应的参数 */
    /* 开头定义 str_buff 为无符号 8 位整型(uint8_t),此处使用 (char *) 转化为字符型指针,因为 sprintf() 函数需要的参数是字符型指针*/
    sprintf((char *)str_buff, "%d:%d:%d LED1 关闭!\r\n", hh, mm, ss);
    break;

    /* 接收到 a1 时,则 LED1 关闭 */
    case 0xa2:
    LED1_ON();
    sprintf((char *)str_buff, "%d:%d:%d LED2 打开!\r\n", hh, mm, ss);
    break;

    default:
    sprintf((char *)str_buff, "%d:%d:%d 这是一个错误的命令!\r\n", hh, mm, ss);
    break;
    }
    /* 向串口发送缓冲区 str_buff 字符串 */
    HAL_UART_Transmit(&huart1, str_buff, sizeof(str_buff), 10000);
    /* 补一个接收中断函数,因为还要继续接收串口调试助手发来的字符串,同例程 1*/
    HAL_UART_Receive_IT(&huart1, Rx_dat, 3);
    }
    }
    }

    /* USER CODE END 0 */

    -----------------------
    ↓↓↓注:main() 函数外↓↓↓
    -----------------------
    /* USER CODE BEGIN 2 */

    /* 第三步-2:灯光检测功能运行 */
    Check_LED();

    /* 第四步:发送 str1 到串口调试助手*/
    HAL_UART_Transmit(&huart1, str1, sizeof(str1), 10000); //阻塞式发送

    /* 第五步-1:启动定时器 TIM2 中断 */
    /* 此函数在 main.c 关联的 stm32f4xx_hal_tim.h 文件的最下面可以找到 */
    HAL_TIM_Base_Start_IT(&htim2);

    /* 第六步-1:采用非阻塞式接收字符串 */
    /* 接收到的字节放到 Rx_dat,当接收到完整的三个字节后,进入串口接收完成中断,然后调用它的回调函数 */
    HAL_UART_Receive_IT(&huart1, Rx_dat, 3);

    /* USER CODE END 2 */
image-20230411000329095
图 5.5 串口调试助手功能实现

参考

[1] 成为点灯大师 LED Master | Story Begins……