mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2990 字
8 分钟
STM32-03:USART/TTL串口通信
2026-07-15
2026-07-19

USART(Universal Synchronous/Asynchronous Receiver/Transmitter),通用同步/异步串行收发,支持相互独立的收发数据操作,其核心功能涵盖同步/异步双模式运行。本文章聚焦于UART,即其中的异步通信。


在芯片中配置启用串口通信#

UART2为例:

  1. 在左侧选择Connectivity展开后选择USART2,将顶部的的Mode选项更改为Asynchronous即可启用异步通信串口功能;
  2. 在右侧的芯片引脚处,可以看见引脚PA2PA3已经被启用,并且分别被标记为USART2_TXUSART2_RX
  3. 在左侧的配置界面,我们可以看到更多的设置信息:
  • 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相连,RXTX相连,注意不要连反了!胡言乱语.jpg
    下载程序后,打开串口调试应用,便可以看到单片机发来的Hello world!(记得把显示模式从HEX改为ASCII
    串口调试程序下载

接受信息#

目的:发送长度为2的字符串,控制三色小灯亮灭,例如发送”R1”,使LED_Red亮起,并返回”LED_Red enabled.”
当我们编写代码涉及到添加更多功能时,若将代码全部写入main.c,多少显得有点臃肿,因此,将特定的功能独立出来,并写在额外的文件中,能大大提高代码的可读性和可维护性。
具体步骤如下:

  1. \Core\Src下创建新文件serial.c,在\Core\Inc下创建新文件serial.h

  2. CLion中,在左侧的项目文件夹中按照\Core\Src的路径找到新建的文件serial.c,右键并选择 “将’serial.c’添加到 CMake 项目”
    在Keil和CubeIDE中也遵循类似的步骤,将新增文件添加到项目中。

  3. main.c/* USER CODE BEGIN Includes */ /* USER CODE END Includes */之间,写入:

/* USER CODE BEGIN Includes */
#include "serial.h"
/* USER CODE END Includes */
  1. serial.h(Header file)中,我们需要先对函数进行声明:
#ifndef __SERIAL_H//头文件保护,防止同一个头文件被重复包含,导致编译错误。
#define __SERIAL_H
void serial_Polling(void);//声明函数
#endif
  1. 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;
}
  1. 完成功能外置后,我们回到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);

因此,我们要在此中断函数中对接收到的数据进行处理:

  1. serial.h文件中添加头文件并声明函数:
#include "usart.h"
void serial_IT(void);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
  1. 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;
}
  1. 当接收信息结束后,用中断函数HAL_UART_RxCpltCallback对接收到的信息进行处理,不过在处理之前,我们要先判断是否是&huart2发来的信息:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
if (huart==&huart2)
{
switch (receive_data[0]) {...}
}
return;
}
  1. 当然,在结尾处,我们不能忘了重新开启中断接收,以等待下一次的数据输入:
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与之前基本一致,而*RxlenSize的含义发生了改变:它表示的是一次能接受的最大数据长度
当接收完毕后,会调用以下回调函数:

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中。

寄存器#

  • 发送/接收数据寄存器
  • 发送/接收移位寄存器
STM32-03:USART/TTL串口通信
https://nf3eb.github.io/posts/stm32/stm32-03/
作者
NF3eb
发布于
2026-07-15
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录