STM32学习·HAL库速通篇(八)·串口发送运用(USART)

目录
1. 简介
串口通讯 (Serial Communication) 是一种设备间非常常用的串行通讯方式,因为它简单便捷,因此大部分电子设备都支持该通讯方式,电子工程师在调试设备时也经常使用该通讯方式输出调试信息。
在计算机科学里,大部分复杂的问题都可以通过分层来简化。如芯片被分为内核层和片上外设;STM32 标准库则是在寄存器与用户代码之间的软件层。对于通讯协议,我们也以分层的方式来理解,最基本的是把它分为物理层和协议层。物理层规定通讯系统中具有机械、电子功能部分的特性,确保原始数据在物理媒体的传输。协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准。简单来说物理层规定我们用嘴巴还是用肢体来交流,协议层则规定我们用中文还是英文来交流。
2. 物理层
根据通讯使用的电平标准不同,串口通讯可分为 TTL 标准及 RS-232 标准,见表 TTL 电平标准与 RS-232 电平标准。
| 标准 | 逻辑 1(高电平) | 逻辑 0(低电平) | 典型应用场景 | 传输距离 |
| TTL | +3.3V ~ +5V | 0V | 单片机内部、板级通信 | 短(厘米级) |
| RS-232 | -3V ~ -15V | +3V ~ +15V | 工业控制、老式外设(如调制解调器) | 较长(可达 15 米) |

因为控制器一般采用 TTL 电平标准,因此需借助MAX3232 芯片实现TTL 电平与 RS-232 电平的双向转换,这是串口通讯中实现控制器与 RS-232 设备互联的关键环节。
3. 协议层
串口通讯的数据包由发送设备通过自身的TXD 接口传输到接收设备的RXD 接口。在协议层,数据包由起始位、主体数据、校验位和停止位组成,通讯双方必须约定一致的数据包格式,才能正常收发数据。

| 组成部分 | 作用 | 典型配置 |
| 起始位 | 标志一个数据包的开始,使接收方同步接收数据 | 固定为 1 位,逻辑电平为低电平 |
| 主体数据 | 实际传输的有效信息,长度可变 | 常见为 8 位(1 字节) |
| 校验位 | 用于检测传输过程中是否发生错误,可选奇校验、偶校验或无校验 | 1 位或无 |
| 停止位 | 标志一个数据包的结束,提供接收方的缓冲时间 | 常见为 1 位或 2 位,逻辑电平为高电平 |
4. 工程配置
还是一样,重复的配置,这里不再一步一步演示步骤,详细可以参考:
STM32学习·HAL库速通篇(三)·如何在STM32CubeMX新建工程_stm32cubemx 配置cmsis库-CSDN博客
只进行一些上面没有的配置,现在我们想要配置串口1,找到图示位置进行配置:

- Disable:禁用
- Asynchronous:异步模式
- Synchronous:同步模式
- Single Wire (Half-Duplex):单线(半双工)模式
- Multiprocessor Communication:多处理器通讯模式
- IrDA:红外数据协会(红外通讯)模式
- LIN:本地互连网络(LIN 总线)模式
- SmartCard:智能卡模式
然后配置数据通讯协议相关参数,这里直接使用默认参数:

波特率指数据信号对载波的调制速率,它用单位时间内载波调制状态改变次数来表示,单位为波特。比特率指单位时间内传输的比特数,单位 bit/s (bps)。对于 USART 波特率与比特率相等,以后不区分这两个概念。波特率越大,传输速率越快。
USART 的发送器和接收器使用相同的波特率。计算公式如下:
其中,fPLCK 为 USART 时钟,USARTDIV 是一个存放在波特率寄存器 (USART_BRR) 的一个无符号定点数。其中 DIV_Mantissa [11:0] 位定义 USARTDIV 的整数部分,DIV_Fraction [3:0] 位定义 USARTDIV 的小数部分。
然后就是对项目名称等的设置(参考上方链接),生成完可以看到,串口初始化成功:
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file usart.c
* @brief This file provides code for the configuration
* of the USART instances.
******************************************************************************
* @attention
*
* Copyright (c) 2026 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "usart.h"
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
UART_HandleTypeDef huart1;
/* USART1 init function */
void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART1_Init 2 */
/* USER CODE END USART1_Init 2 */
}
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE END USART1_MspInit 0 */
/* USART1 clock enable */
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USER CODE BEGIN USART1_MspInit 1 */
/* USER CODE END USART1_MspInit 1 */
}
}
void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
{
if(uartHandle->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspDeInit 0 */
/* USER CODE END USART1_MspDeInit 0 */
/* Peripheral clock disable */
__HAL_RCC_USART1_CLK_DISABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9|GPIO_PIN_10);
/* USER CODE BEGIN USART1_MspDeInit 1 */
/* USER CODE END USART1_MspDeInit 1 */
}
}
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
5. 发送一个字节
这里主要用到 HAL_UART_Transmit() 函数:

简单翻译一下:
/**
* @brief 以阻塞模式发送指定长度的数据。
* @note 当UART校验位未使能(PCE = 0)且字长配置为9位(M1-M0 = 01)时,
* 发送的数据会被当作一组u16类型数据处理。这种情况下,Size参数必须指明
* 通过pData传入的u16数据个数。
* @param huart 指向UART_HandleTypeDef结构体的指针,该结构体包含指定UART模块的
* 配置信息。
* @param pData 指向数据缓冲区的指针(数据元素类型为u8或u16)。
* @param Size 要发送的数据元素个数(u8或u16类型)
* @param Timeout 超时持续时间(单位:ms)
* @retval HAL状态码(HAL_OK/HAL_ERROR/HAL_TIMEOUT/HAL_BUSY)
*/
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
const uint8_t *pdata8bits; // 指向8位数据的指针
const uint16_t *pdata16bits; // 指向16位数据的指针(用于9位无校验场景)
uint32_t tickstart = 0U; // 记录函数开始执行的系统时间,用于超时判断
/* 检查当前是否有其他Tx(发送)过程正在进行 */
if (huart->gState == HAL_UART_STATE_READY)
{
/* 校验入参合法性:数据指针为空 或 发送长度为0,直接返回错误 */
if ((pData == NULL) || (Size == 0U))
{
return HAL_ERROR;
}
// 初始化UART错误码为“无错误”
huart->ErrorCode = HAL_UART_ERROR_NONE;
// 将UART状态标记为“忙(发送中)”,防止多线程重复调用
huart->gState = HAL_UART_STATE_BUSY_TX;
/* 记录当前系统滴答定时器值,作为超时判断的起始时间 */
tickstart = HAL_GetTick();
// 保存要发送的总数据长度
huart->TxXferSize = Size;
// 初始化剩余发送数据计数(初始值等于总长度)
huart->TxXferCount = Size;
/* 特殊场景处理:9位字长 + 无校验位时,pData需当作uint16_t指针处理
(因为9位数据无法用8位存储,需占用16位变量的低9位) */
if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
{
pdata8bits = NULL; // 8位指针置空
pdata16bits = (const uint16_t *) pData; // 转换为16位数据指针
}
else
{
// 常规场景(8位/带校验的9位):使用8位数据指针
pdata8bits = pData;
pdata16bits = NULL; // 16位指针置空
}
/* 循环发送数据,直到剩余发送计数为0 */
while (huart->TxXferCount > 0U)
{
/* 等待TXE标志(发送数据寄存器空)置位,直到超时
TXE=1表示DR寄存器已空,可以写入下一个数据 */
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
{
huart->gState = HAL_UART_STATE_READY; // 恢复UART为就绪状态
return HAL_TIMEOUT; // 返回超时错误
}
// 根据数据类型,将数据写入UART数据寄存器(DR)
if (pdata8bits == NULL)
{
// 9位无校验场景:取低9位数据写入DR(0x01FF是9位掩码)
huart->Instance->DR = (uint16_t)(*pdata16bits & 0x01FFU);
pdata16bits++; // 指针后移,指向下一个16位数据
}
else
{
// 常规场景:取低8位数据写入DR(0xFF是8位掩码)
huart->Instance->DR = (uint8_t)(*pdata8bits & 0xFFU);
pdata8bits++; // 指针后移,指向下一个8位数据
}
huart->TxXferCount--; // 剩余发送计数减1
}
/* 等待TC标志(发送完成)置位,直到超时
TC=1表示整个帧(包括停止位)已发送完成,防止提前退出导致数据未发完 */
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TC, RESET, tickstart, Timeout) != HAL_OK)
{
huart->gState = HAL_UART_STATE_READY; // 恢复UART为就绪状态
return HAL_TIMEOUT; // 返回超时错误
}
/* 发送过程结束,恢复UART状态为就绪 */
huart->gState = HAL_UART_STATE_READY;
// 返回发送成功
return HAL_OK;
}
else
{
// UART正忙(已有其他发送/接收过程),返回忙状态
return HAL_BUSY;
}
}
主要是通过while循环逐字节写入 DR 寄存器,等待TXE(寄存器空)和TC(发送完成)标志,确保数据可靠发送。
我们来调用一下看看,来到主函数,首先随便一个字节数据:
uint8_t ch = 'A';//要发送的字节
然后通过调用 HAL_UART_Transmit() 函数将数据发送出去:
HAL_UART_Transmit(&huart1, &ch, 1, HAL_MAX_DELAY);//发送一个字节数据
- &huart1:串口句柄指针,指定使用USART1进行数据发送;
- &ch:要发送的数据缓冲区指针;
- 1:发送的数据长度,值为 1 表示仅发送1 个字节;
- HAL_MAX_DELAY:超时时间,宏定义值为0xFFFFFFFFU(无符号 32 位最大值),表示无限等待,直到数据发送完成(不会触发超时错误)。
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
uint8_t ch = 'A';//要发送的字节
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_UART_Transmit(&huart1, &ch, 1, HAL_MAX_DELAY);//发送一个字节数据
HAL_Delay(1000);//加个延时方便观察
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
然后下载程序,打开串口调试助手,我们可以看到数据正常发送:

6. 发送一个字符串
这个和发送字节没有多少差别,首先定义一个字符串:
char msg[] = "Hello,USART!!!\r\n";//要发送的字符串
然后通过调用 HAL_UART_Transmit() 函数将数据发送出去:
HAL_UART_Transmit(&huart1, (uint8_t *)msg, strlen(msg), HAL_MAX_DELAY);//发送字符串数据
- &huart1:串口句柄指针,指定使用USART1进行数据发送;
- (uint8_t *)msg:要发送的数据缓冲区;
- strlen(msg):计算发送的数据长度,注意头文件需要包含#include <string.h>;
- HAL_MAX_DELAY:超时时间,宏定义值为0xFFFFFFFFU(无符号 32 位最大值),表示无限等待,直到数据发送完成(不会触发超时错误)。
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
// uint8_t ch = 'A';//要发送的字节
char msg[] = "Hello,USART!!!\r\n";//要发送的字符串
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
// HAL_UART_Transmit(&huart1, &ch, 1, HAL_MAX_DELAY);//发送一个字节数据
// HAL_Delay(1000);//加个延时方便观察
HAL_UART_Transmit(&huart1, (uint8_t *)msg, strlen(msg), HAL_MAX_DELAY);//发送字符串数据
HAL_Delay(1000);//加个延时方便观察
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
然后下载程序,打开串口调试助手,我们可以看到数据正常发送:

7. printf()重定向
C 语言中的 printf () 默认输出到显示器,在嵌入式中没有显示器,需要重定向到串口
通过重写 fputc () 函数,把 printf () 输出的字符通过串口发出去:
#include <stdio.h>
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
直接将代码复制进去即可:

然后找到图示位置勾选上,否则 printf () 可能无法正常使用:

随便在主函数打一句话:

完整工程:


更多推荐

所有评论(0)