目录

1.  简介

2.  物理层

3.  协议层

4.  工程配置

5.  发送一个字节

6.  发送一个字符串

7.  printf()重定向


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 () 可能无法正常使用:

        随便在主函数打一句话:

完整工程:

基于STM32实现串口(USART)发送数据(HAL库版本)资源-CSDN下载

HAL库学习笔记_时光の尘的博客-CSDN博客

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐