USART(Universal Synchronous/Asynchronous Receiver/Transmitter),通用同步/异步串行收发,支持相互独立的收发数据操作,其核心功能涵盖同步/异步双模式运行。本文章聚焦于UART,即其中的异步通信。
在芯片中配置启用串口通信
以UART2为例:
- 在左侧选择Connectivity展开后选择USART2,将顶部的的Mode选项更改为Asynchronous即可启用异步通信串口功能;
- 在右侧的芯片引脚处,可以看见引脚PA2和PA3已经被启用,并且分别被标记为USART2_TX和USART2_RX;
- 在左侧的配置界面,我们可以看到更多的设置信息:
- Baud Rate(波特率):每秒发送的码元数量,默认地,TTL串口每传递1Byte(8bit)数据会添加起始位和终止位,也就是每传输1Byte信息实际上需要10bit,因此115200Bits/s的波特率实际的传输速度是11520Byte/s。通信的两个设备需要保持相同的波特率才能正常通信,通常保持默认的115200Bits/s即可
轮询模式(Polling)
在轮询模式下,接受/发送消息的过程处于阻塞,不能够执行其他指令,只有待接受/发送消息的指令结束后,才能继续执行其他指令。
发送信息
在main.c中/* USER CODE BEGIN 2 */和/* USER CODE END 2 */之间写入:
char *message="Hello world!"; HAL_UART_Transmit(&huart2, (uint8_t*)message, strlen(message), 100);//Hello world!其中:
&huart2:发送的UART端口地址,不要忘了取址符&;message:发送的信息,其数据类型应该是uint8_t*,我们在这里将其初始定义为字符串,发送时将其强制转化为uint8_t*类型;strlen(message):发送的信息的长度;100:最长等待时间(单位:毫秒),超过此时间后若还未发送完成则强制结束,设置为HAL_MAX_DELAY则无限等待。
将USB转TTL串口模块的TX与单片机上的RX相连,RX与TX相连,注意不要连反了!胡言乱语.jpg
下载程序后,打开串口调试应用,便可以看到单片机发来的Hello world!(记得把显示模式从HEX改为ASCII)
串口调试程序下载
接受信息
目的:发送长度为2的字符串,控制三色小灯亮灭,例如发送”R1”,使LED_Red亮起,并返回”LED_Red enabled.”
当我们编写代码涉及到添加更多功能时,若将代码全部写入main.c,多少显得有点臃肿,因此,将特定的功能独立出来,并写在额外的文件中,能大大提高代码的可读性和可维护性。
具体步骤如下:
-
在
\Core\Src下创建新文件serial.c,在\Core\Inc下创建新文件serial.h; -
在CLion中,在左侧的项目文件夹中按照
\Core\Src的路径找到新建的文件serial.c,右键并选择 “将’serial.c’添加到 CMake 项目” ;
在Keil和CubeIDE中也遵循类似的步骤,将新增文件添加到项目中。 -
在
main.c中/* USER CODE BEGIN Includes */和/* USER CODE END Includes */之间,写入:
/* USER CODE BEGIN Includes */#include "serial.h"/* USER CODE END Includes */- 在
serial.h(Header file)中,我们需要先对函数进行声明:
#ifndef __SERIAL_H//头文件保护,防止同一个头文件被重复包含,导致编译错误。#define __SERIAL_H
void serial_Polling(void);//声明函数
#endif- 在
serial.c(Source file)中,我们需要先include相关的.h文件:
#include "serial.h"//引入函数声明#include "string.h"//函数会使用到string.h中的strlen函数#include "usart.h"//串口接收,发送信息然后,写入相关的消息表:
static const struct { struct { const char *red_enabled; const char *red_disabled; const char *green_enabled; const char *green_disabled; const char *blue_enabled; const char *blue_disabled; } LED; const char *invalid;} message={ .LED={ .red_enabled = "LED_Red enabled.", .red_disabled = "LED_Red disabled.", .green_enabled = "LED_Green enabled.", .green_disabled = "LED_Green disabled.", .blue_enabled = "LED_Blue enabled.", .blue_disabled = "LED_Blue disabled." }, .invalid = "Invalid."};想要让上位机向单片机通过串口发送指令,需要用到以下函数:
HAL_UART_Receive(&huart2, receive_data, 2, HAL_MAX_DELAY);执行此函数后,单片机便进入阻塞接收状态,只有在接收足够长的信息后,才会退出此状态。
与HAL_UART_Transmit类似:
&huart2:用于接受的UART端口地址,不要忘了取址符&;receive_data:存储收到的信息;2:最多接受的信息的长度(单位:Byte);HAL_MAX_DELAY:最长等待时间(单位:毫秒),这里指无限等待。
最后,补全函数serial_Polling:
uint8_t receive_data[2];void serial_Polling(void) { HAL_UART_Receive(&huart2, receive_data, 2, HAL_MAX_DELAY); switch (receive_data[0]) { case 'R': { switch (receive_data[1]) { case '0': { HAL_GPIO_WritePin(LED_Red_GPIO_Port, LED_Red_Pin, GPIO_PIN_RESET); HAL_UART_Transmit(&huart2, (uint8_t*)message.LED.red_disabled, strlen(message.LED.red_disabled), HAL_MAX_DELAY); break; } case '1': { HAL_GPIO_WritePin(LED_Red_GPIO_Port, LED_Red_Pin, GPIO_PIN_SET); HAL_UART_Transmit(&huart2, (uint8_t*)message.LED.red_enabled, strlen(message.LED.red_enabled), HAL_MAX_DELAY); break; } } break; } case 'G': { switch (receive_data[1]) { case '0': { HAL_GPIO_WritePin(LED_Green_GPIO_Port, LED_Green_Pin, GPIO_PIN_RESET); HAL_UART_Transmit(&huart2, (uint8_t*)message.LED.green_disabled, strlen(message.LED.green_disabled), HAL_MAX_DELAY); break; } case '1': { HAL_GPIO_WritePin(LED_Green_GPIO_Port, LED_Green_Pin, GPIO_PIN_SET); HAL_UART_Transmit(&huart2, (uint8_t*)message.LED.green_enabled, strlen(message.LED.green_enabled), HAL_MAX_DELAY); break; } } break; } case 'B': { switch (receive_data[1]) { case '0': { HAL_GPIO_WritePin(LED_Blue_GPIO_Port, LED_Blue_Pin, GPIO_PIN_RESET); HAL_UART_Transmit(&huart2, (uint8_t*)message.LED.blue_disabled, strlen(message.LED.blue_disabled), HAL_MAX_DELAY); break; } case '1': { HAL_GPIO_WritePin(LED_Blue_GPIO_Port, LED_Blue_Pin, GPIO_PIN_SET); HAL_UART_Transmit(&huart2, (uint8_t*)message.LED.blue_enabled, strlen(message.LED.blue_enabled), HAL_MAX_DELAY); break; } } break; } default: { HAL_UART_Transmit(&huart2, (uint8_t*)message.invalid, strlen(message.invalid), HAL_MAX_DELAY); } } return;}- 完成功能外置后,我们回到
main.c,在while(1)中只需写入:
serial_Polling();就会执行期望功能:

如果从第二次起,一直返回”Invalid”,则有可能是串口调试软件在发送的信息尾部自动追加了\n或\r,更改为无追加即可。
中断模式
中断模式下,处理器通过频繁的触发中断,实现在接收/发送信息的同时也不会阻塞其他指令。
芯片配置
在CubeMX中,在左侧选择Connectivity展开后选择USART2,进入下方的NVIC Settings栏目,找到其中的USART2 global interrupt,这一条就是USART2的中断路线,将其启用,并生成代码,便可以使用USART2串口通信的中断模式。

接收信息
使用以下函数,可以开启中断接收模式:
HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);开启中断接收后,处理器会继续处理其他指令,直到接受到指定长度的数据,然后触发以下中断函数:
HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);因此,我们要在此中断函数中对接收到的数据进行处理:
- 在
serial.h文件中添加头文件并声明函数:
#include "usart.h"void serial_IT(void);void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);- 在
serial.c中写入函数serial_IT()用于启用中断接收模式:
int is_receiving=0;void serial_IT(void){ if(is_receiving==0){ HAL_UART_Receive_IT(&huart2,receive_data, 2); is_receiving=1; } return;}- 当接收信息结束后,用中断函数
HAL_UART_RxCpltCallback对接收到的信息进行处理,不过在处理之前,我们要先判断是否是&huart2发来的信息:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){ if (huart==&huart2) { switch (receive_data[0]) {...} } return;}- 当然,在结尾处,我们不能忘了重新开启中断接收,以等待下一次的数据输入:
is_receiving=0;发送信息
与接收信息类似,发送信息也有自己的中断模式:
HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);同样地,也有自己的中断函数:
HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);他们与接收信息的函数基本一致,在此不再赘述。
DMA模式
DMA(Direct Memory Access),直接内存访问,可以协助处理器在寄存器与内存之间搬运数据。
芯片配置
在左侧选择Connectivity展开后选择USART2,进入下方的DMA Settings栏目,点击最下方的Add,为其添加DMA通道。

代码配置
启用DMA接收模式,需要用到以下函数:
HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);设置DMA模式后,发送和接收的过程由DMA协助完成,处理器能够去处理其他指令,因此,和中断模式类似,DMA模式也是非阻塞的。
和中断模式类似,在发送/接收结束时,也会触发函数:
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);因此,同样地,我们需要在此函数对接收到的信息进行处理,在此不再赘述。
不定长数据的接收
借助串口空闲中断,可以实现对不定长数据的接收。
同样地,它也拥有阻塞,中断,DMA三种模式:
HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint16_t *RxLen, uint32_t Timeout);HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);其中*huart,*pData与之前基本一致,而*Rxlen和Size的含义发生了改变:它表示的是一次能接受的最大数据长度。
当接收完毕后,会调用以下回调函数:
HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);与HAL_UART_RxCpltCallback不同的是,HAL_UARTEx_RxEventCallback多了一个入参Size,它代表着此次实际上接收了多少字节的数据。
在serial.h中添加函数声明:
void serial_idle(void);void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);在serial.c中写入函数:
uint8_t receive_data_2[50];void serial_idle(void){ if(is_receiving==0){ HAL_UARTEx_ReceiveToIdle_DMA(&huart2, receive_data_2, sizeof(receive_data_2)); is_receiving=1; } return;}void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){ if(huart==&huart2){ HAL_UART_Transmit_DMA(&huart2, receive_data_2, Size); } is_receiving=0; return;}这样,我们从上位机输入任意长度的字符串时,单片机也会返回相同长度的字符串。

NOTE注意,在DMA模式下,DMA传输过半中断也会触发
HAL_UARTEx_RxEventCallback回调函数,即接收的数据量达到设定最大值的一半时,也会触发此回调函数。
因此,我们需要添加以下函数,以关闭DMA传输过半中断:__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT);在
usart.h中添加extern语句:extern DMA_HandleTypeDef hdma_usart2_rx;修改后的
serial_idle函数:void serial_idle(void){if(is_receiving==0){HAL_UARTEx_ReceiveToIdle_DMA(&huart2, receive_data_2, sizeof(receive_data_2));is_receiving=1;}__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT);return;}别忘了也需要在程序开始时添加此函数。
深入串口通信
TTL串口通信中,仅需两根数据线即可完成两个设备之间的双向通信,两设备分别有RX(接收端)和TX(发送端)两个接口,要注意A设备的TX应该和B设备的RX相接,A设备的RX应该和B设备的TX相接。
在STM32每个串口的内部都有两个寄存器:发送/接收数据寄存器和发送/接收移位寄存器。
在轮询模式下,当我们调用函数HAL_UART_Transmit(&huart2, (uint8_t*)message, strlen(message), 100);时,处理器会依次将数据移动到寄存器中:
- 发送移位寄存器会将字符转化为高低电平信号,从TX引脚发送回上位机;
- 发送完当前字节后,发送数据寄存器会将自身存储的数据移动到发送移位寄存器中,然后处理器将下一个数据写入发送数据寄存器中;
这样说可能有些难懂,我们可以进行以下的类比:
- 发送移位寄存器:在枪管中正在被发射的子弹;
- 发送数据寄存器:在枪膛中待击发的子弹,待枪管中的子弹被打出后(发送移位寄存器空),将其击发(送入发送移位寄存器),然后机械结构将弹匣中下一发子弹移入枪膛(处理器将下一位数据移入发送数据寄存器);
- 待发送的数据:弹匣中的子弹;
- 处理器:枪械中的机械结构。
串口接收也是类似,接收到的信息先进入接收移位寄存器,然后进入接收数据寄存器,处理器会一直查询接收数据寄存器是否为空,然后将接收数据寄存器中的数据移入用于接收数据的变量中。
在此过程中,处理器会不停的检测发送/接收数据寄存器是否为空,如果为空,则将下一个数据移入发送/接收数据寄存器,直到把所有数据发送/接收完毕或达到设定的最大超时时间。因此,在轮询模式下,属于阻塞状态。

而在中断模式下,处理器将数据移入寄存器后,便去执行其他指令,而当发送数据寄存器的数据被移入发送移位寄存器后,触发发送数据寄存器空 中断,使处理器将下一位数据移入发送数据寄存器,然后处理器继续处理其他指令,如此反复直到全部数据发送完毕,并触发HAL_UART_TxCpltCallback回调函数。
同样地,接收数据时,接收移位寄存器将数据移入接收数据寄存器,触发接收数据寄存器非空 中断,使处理器将接收数据寄存器中的数据移入存储数据的变量中,然后处理器继续处理其他指令,直至接受到最大长度的数据,然后触发HAL_UART_RxCpltCallback回调函数。
在DMA模式下, DMA会协助在发送/接收数据寄存器和内存变量中搬运数据,无需处理器搬运,因而DMA模式也是非阻塞的。
总结
相关函数
HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);//阻塞发送HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);//阻塞接收
HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);//中断发送HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);//中断接收
HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);//DMA模式发送HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);//DMA模式接收
HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//发送结束回调函数HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//接收结束回调函数
HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint16_t *RxLen,uint32_t Timeout);//阻塞不定长数据接收HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);//中断不定长数据接收HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);//DMA模式不定长数据接收HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);//接收事件回调函数多文件编程
.h(Header file)文件用于声明函数,需要加头文件保护,防止同一个头文件被重复包含,导致编译错误;.c(Source file)文件用于写入函数源码;- 记得将
.c文件加入到CMake中。
寄存器
- 发送/接收数据寄存器
- 发送/接收移位寄存器
部分信息可能已经过时





