STM32 串口通信实战(包含串口、中断、DMA)

一、目的

  1. 掌握 STM32CubeMX 与 Keil 的协同开发流程,熟悉 HAL 库串口配置方法。
  2. 实现 STM32 向上位机连续发送数据,观察无延时发送时的数据传输情况。
  3. 通过中断方式实现上位机指令控制(“#”暂停、“*”继续),理解中断响应机制。
  4. 采用 DMA 方式完成高速串口发送,对比 CPU 占用率差异。
  5. 利用 Keil 软件仿真逻辑分析仪验证串口波特率时序正确性。

二、环境

类别 具体参数
硬件 STM32F103C8T6 核心板、USB 转TTL模块、杜邦线
软件 STM32CubeMX 、Keil MDK-ARM 5、串口调试助手
通信参数 波特率 115200bps、数据位 8 位、停止位 1 位、无校验位、无硬件流控

三、原理

1. 串口通信原理

串口通过异步通信方式传输数据,每帧数据包含 1 位起始位(低电平)、8 位数据位、1 位停止位(高电平),无校验位时共 10 位。波特率 115200bps 表示每秒传输 115200 位,单个位传输时间约 8.68μs。

2. 中断原理

开启 USART 接收中断后,当上位机发送数据时,STM32 会触发中断,进入中断服务函数处理指令(如“#”暂停发送、“*”继续发送),避免主循环阻塞,提升响应速度。

3. DMA 原理

DMA(直接存储器访问)可在内存与外设间直接传输数据,无需 CPU 干预。串口 DMA 发送时,CPU 仅需初始化 DMA 通道和缓冲区,后续数据传输由 DMA 自动完成,适合高速连续发送场景。

四、步骤与实现

1. 基础环境配置(STM32CubeMX)

1.1 芯片与时钟配置
  1. 打开 STM32CubeMX,选择芯片“STM32F103C8T6”。

  2. 配置时钟:RCC 选择“HSE 外部晶振”,系统时钟(SYSCLK)设为 72MHz,USART1 挂载于 APB2 总线,时钟频率 72MHz。
    在这里插入图片描述

1.2 USART1 配置
  1. 引脚配置:PA9 设为“USART1_TX”(复用推挽输出),PA10 设为“USART1_RX”(浮空输入)。

  2. 串口参数:模式选择“异步通信(Asynchronous)”,波特率 115200bps,数据位 8 位,停止位 1 位,校验位“无”,流控“无”。

在这里插入图片描述

1.3 中断与 DMA 补充配置
  • 中断配置:进入“NVIC Settings”,勾选“USART1 global interrupt”,抢占优先级 1,子优先级 1。

  • DMA 配置:进入“DMA Settings”,添加“USART1_TX”通道(DMA1_Channel4),方向“内存到外设”,模式“循环模式”,内存地址自增使能,数据宽度“字节”。

在这里插入图片描述

1.4 生成工程

选择“Keil MDK-ARM”作为 IDE,勾选“Generate peripheral initialization as a pair of .c/.h files per peripheral”,生成工程并打开。

2. 基础串口通信实现

无延时连续发送+上位机指令控制

代码实现(main.c)

#include "main.h"
#include <string.h>

UART_HandleTypeDef huart1;
uint8_t rx_data;       // 接收上位机指令的缓冲区(1字节)
uint8_t send_flag = 1; // 发送使能标志(1:发送,0:暂停)
uint8_t send_data[] = "hello windows!\r\n";

int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_USART1_UART_Init();

  while (1) {
    // 非阻塞接收上位机指令,超时时间 10ms(避免主循环阻塞)
    if (HAL_UART_Receive(&huart1, &rx_data, 1, 10) == HAL_OK) {
      if (rx_data == '#') {
        send_flag = 0; // 接收“#”,暂停发送
      } else if (rx_data == '*') {
        send_flag = 1; // 接收“*”,继续发送
      }
    }

    // 根据标志位控制发送
    if (send_flag) {
      UART_SendString(send_data);
      HAL_Delay(50); // 加短延时,避免发送过于密集
    }
  }
}

实验现象

  • 发送“*”:上位机连续接收“hello windows!”;

  • 发送“#”:上位机接收停止;再次发送“

在这里插入图片描述

3. 中断与 DMA 方式实现

3.1 :串口中断方式控制

代码实现(main.c + stm32f1xx_it.c)

#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stm32f1xx_hal_usart.h"
#include "stdio.h"
#include "string.h"
uint8_t rx_buf; // 存储接收的字符
uint8_t send_en = 1; // 发送使能标志,1为允许,0为暂停
char re_a = '#';
char send_buf[] = "hello windows! \r\n";

// 启动串口中断接收(单次接收1字节)
void UART_StartReceiveIT(void) {
  HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
UART_HandleTypeDef huart1;
// 串口接收中断回调函数(接收完成后自动调用)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart == &huart1)
  {
    // 根据接收的字符控制发送使能
   
		if (rx_buf == '#')
			{
			send_en = 0;  // 收到“#”,暂停发送
				// 发送单个字符'#'
			HAL_UART_Transmit(&huart1, (uint8_t*)&re_a, 1, 100);
			}
    
    else if (rx_buf == '*')
    {
      send_en = 1;  // 收到“*”,继续发送
    }
    
    // 重新启动接收中断,确保能持续接收数据
    // 使用do-while循环确保中断能重新启动
    do
    {
      if (HAL_UART_Receive_IT(&huart1, &rx_buf, 1) == HAL_OK)
      {
        break;
      }
    } while (1);
  }
}

int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_USART1_UART_Init();
  UART_StartReceiveIT(); // 初始化后启动中断接收

  while (1) {
    if (send_flag) {
      HAL_UART_Transmit(&huart1, send_data, strlen((char*)send_data), 0xFFFF);
      HAL_Delay(10);
    }
  }
}

// stm32f1xx_it.c 部分(中断服务函数)
#include "usart.h"
void USART1_IRQHandler(void) {
  // 调用 HAL 库中断处理函数,间接触发回调
  HAL_UART_IRQHandler(&huart1);
}

实验现象
与任务二(2)功能一致,但中断方式响应速度更快(上位机发送指令后,STM32 可立即响应)。
在这里插入图片描述

3.2 :串口 DMA 连续发送

代码实现(main.c)

#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <string.h>

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;

// 定义要发送的字符串
#define SEND_STR "hello world\r\n"
// 字符串长度(不含结束符)
#define STR_LEN  (sizeof(SEND_STR) - 1)
// 发送缓冲区大小(建议为字符串长度的整数倍,此处用4倍避免频繁中断)
#define TX_BUFFER_SIZE  (STR_LEN * 4)
uint8_t tx_buffer[TX_BUFFER_SIZE];

int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_DMA_Init();        // DMA 初始化(CubeMX 自动生成)
  MX_USART1_UART_Init();// USART1 初始化

  // 填充 DMA 发送缓冲区(循环复制字符串)
  for (uint16_t i = 0; i < sizeof(tx_buffer); i++) {
    tx_buffer[i] = SEND_STR[i % TX_LEN];
  }

  // 启动 DMA 循环发送(无需 CPU 干预)
  HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer));

  while (1) {
    // 主循环空闲,DMA 自动完成数据传输
  }
}

// DMA 发送完成回调函数(可选,循环模式下自动重启)
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
  if (huart == &huart1) {
    // 可在此处更新 tx_buffer 数据(循环模式下发送不中断)
  }
}

实验现象
上位机连续、无丢失接收“hello world”,即使波特率提升至 1Mbps 仍稳定传输。
在这里插入图片描述

4. 任务四:Keil 软件仿真分析波特率

4.1 仿真配置步骤
  1. 打开 Keil 工程,点击 Debug → Start/Stop Debug Session 进入仿真模式。

  2. 打开逻辑分析仪:View → Analysis Windows → Logic Analyzer

  3. 添加串口 TX 引脚:点击逻辑分析仪界面的 Setup,在“Signal”栏输入 PORTA.9(USART1_TX 引脚),“Display Type”选择“Bit”,点击“Add”。

  4. 启动仿真:点击 Run 按钮,等待 2~3 秒后点击 Stop,观察时序波形。

  5. 其他keil配置:

    在这里插入图片描述

4.2 波特率计算
  • 波形分析:1 个字符(如“h”,ASCII 码 0x68)包含 1 位起始位(低电平)、8 位数据位、1 位停止位,共 10 位。
  • 时间测量:在逻辑分析仪波形上,测量 1 个字符的总传输时间 T(10 位 / 115200bps ≈ 86.8μs)。
  • 计算实际波特率:波特率 = 10 / T(实测值与理论值误差 <1%)。

仿真结果
实测 1 个字符传输时间约 87μs,计算波特率 ≈ 114943bps,误差 <0.2%,符合设计要求。

五、总结

  1. 数据丢失原因:无延时连续发送时,STM32 代码执行速度远高于串口传输速度,导致发送缓冲区溢出,需通过 HAL_Delay() 或 DMA 避免。
  2. 中断与 DMA 对比:中断适合响应上位机指令(低延迟),DMA 适合高速连续传输(零 CPU 占用)。
  3. 仿真验证价值:Keil 软件仿真可无需示波器,直接观察串口时序,验证波特率正确性,降低硬件调试成本。

六、附录

  1. 硬件接线图: STM32 PA9(TX)→ USB 转串口 RX、PA10(RX)→ USB 转串口 TX、共地引脚。
    在这里插入图片描述

  2. 串口助手配置图:标注端口号、波特率、数据位/停止位/校验位设置。

在这里插入图片描述

仿真验证价值*:Keil 软件仿真可无需示波器,直接观察串口时序,验证波特率正确性,降低硬件调试成本。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐