一. 基础理论知识

1. 串口协议和RS-232标准

内容 串口协议(UART) RS-232 标准
全称 Universal Asynchronous Receiver/Transmitter(通用异步收发传输器) Recommended Standard 232(推荐标准232)
通信方式 异步串行通信(无需时钟线) 支持异步串行通信(物理层支持)
数据同步方式 发送端与接收端通过预先约定的波特率进行同步 依赖 UART 协议实现数据同步
典型参数配置 波特率(如 9600、115200 bps)
数据位:8 位
停止位:1 位
校验位:无 / 奇 / 偶
不定义具体参数,但常配合 UART 使用上述配置
电气特性 无固定电平标准,通常使用 TTL 电平(0V/3.3V 或 0V/5V) 定义明确电平:
逻辑“1”:-3V ~ -15V
逻辑“0”:+3V ~ +15V(负逻辑)
接口类型 无固定接口,常以引脚形式出现在电路板上 定义机械接口:常用 DB9DB25 连接器
传输距离 短距离(< 1 米,板级通信) 中短距离,最长约 15 米(受电缆质量影响)
主要用途 嵌入式系统内部通信、MCU 与外设通信 工业设备、老式计算机串口、调试接口等长距离通信场景
是否需要电平转换 直接连接 MCU,无需转换 必须通过 MAX232 等芯片将 TTL 转为 RS-232 电平才能连接

2.RS232电平与TTL电平的区别:

项目 TTL 电平 RS-232 电平
逻辑“0” 0 ~ 0.8V +3V ~ +15V
逻辑“1” 2.4 ~ 5V(或3.3V系统为1.8~3.3V) -3V ~ -15V
供电电压 3.3V / 5V ±12V(需专用电源)
传输距离 短(板级) 较长(可达15m)
接口芯片 直接连接MCU 需 MAX232/SP3232 等电平转换

关键点:TTL 是数字电路内部使用的电平;RS-232 是用于远距离通信的负逻辑高压电平。
补充说明

  • RS-232 是物理层标准,规定了电气特性;而 UART 是协议层,定义了数据帧格式。
  • TTL 电平不能直接连接到 RS-232 设备,必须通过 MAX232、SP3232 等电平转换芯片 进行转换。
  • 常见的“USB转串口模块”(如 CH340 + MAX232)正是实现 USB → TTL → RS-232 的桥梁。

3. USB/TTL转RS232 模块(以 CH340 模块为例)工作原理

“USB/TTL转232”模块(以CH340芯片为例)的核心作用是解决不同接口和电平的兼容性问题,让电脑的USB接口能与RS232设备(如老式仪表、打印机)或TTL设备(如单片机)通信。其工作原理可拆解为“协议转换”和“电平转换”两步,具体如下:

(1)模块核心组成

  • CH340芯片:USB转TTL的“协议转换器”,负责将USB信号转为TTL电平的UART串口信号(反之亦然)。
  • 电平转换电路(如MAX232芯片):负责将TTL电平与RS232电平互相转换(因两者电平规则完全不同,无法直接通信)。
  • 辅助电路:电源(从USB取5V电)、指示灯(显示工作状态)等。

(2)工作流程(以“电脑→RS232设备”为例)

  1. 电脑USB信号→CH340→TTL信号
    电脑通过USB线发送的是“USB协议差分信号”(电脑通用接口),进入模块后先被CH340芯片处理——CH340会将USB协议转换为单片机常用的“TTL电平UART信号”(即 TX_TTL(发送)、RX_TTL(接收)、GND(地线),电平规则:1对应3.3V/5V,0对应0V)。

  2. TTL信号→MAX232→RS232信号
    由于RS232设备只认“RS232电平”(1对应-3-15V,0对应+3+15V),TTL信号需经MAX232芯片转换:将TTL的3.3V/5V转为RS232的+3+15V(逻辑0),将TTL的0V转为RS232的-3-15V(逻辑1),最终输出符合RS232标准的信号给外部设备。

(3)反向流程(RS232设备→电脑)
RS232设备发送的-3-15V(逻辑1)和+3+15V(逻辑0)信号,先经MAX232转换为TTL电平(3.3V/5V和0V),再由CH340将TTL的UART信号转回USB协议信号,最终通过USB线传给电脑。

(4)信号流向(PC → 外部设备):

USB 协议⇌TTL 电平 UART(CH340 负责) → TTL 电平⇌RS232 电平(MAX232 负责)

功能作用

  • 实现现代无串口电脑与传统RS-232设备的连接。
  • 提供虚拟COM端口(在Windows中识别为COMx)。
  • 支持常见波特率(如9600, 115200等)。

二. 串口传输文件实验

串口传输文件的练习。将两台笔记本电脑,借助 usb转rs232 模块和杜邦线,建立起串口连接。然后用串口助手等工具软件(带文件传输功能,如:sscom)将一台笔记本上的一个大文件(图片、视频和压缩包软件)传输到另外一台电脑,预算文件大小、波特率和传输时间三者之间的关系,并对比实际传输时间。

1、硬件连接方式

使用两台笔记本电脑,通过以下方式建立串口连接:

在这里插入图片描述
在这里插入图片描述

2、下载sscom

sscom下载链接

3、串口设置

在这里插入图片描述

4、发送与接受

选择下面这张图片发送
在这里插入图片描述
在这里插入图片描述
接收方:
在这里插入图片描述
将收到的文件在画图中打开:
在这里插入图片描述

5、时间计算

通信参数
波特率 (Baud Rate) 115200 bps
数据位 (Data Bits) 8
停止位 (Stop Bits) 1
校验位 (Parity) None
起始位 (Start Bit) 1
每个字节实际占用 10 位(1 起始 + 8 数据 + 1 停止)

理论传输时间计算公式:

总传输时间 = 文件大小(字节) × 10 波特率(bps) \text{总传输时间} = \frac{\text{文件大小(字节)} \times 10}{\text{波特率(bps)}} 总传输时间=波特率(bps文件大小(字节)×10

代入数据:

时间 = 1916270 × 10 115200 = 19162700 115200 ≈ 166.33   秒 \text{时间} = \frac{1916270 \times 10}{115200} = \frac{19162700}{115200} \approx 166.33\,\text{秒} 时间=1152001916270×10=11520019162700166.33

理论预估时间 ≈ 166.33 秒

实际传输时间(来自 SSCOM)
在这里插入图片描述

从截图中看到:
文件大小:1916270 字节
波特率:115200 bps
实际时间:大约 173.48500610352 秒

实际 vs 理论 173.49 - 166.33 = 7.16 秒 主要由于软件处理延迟和“分包延时”

SSCOM 中的“每发送256字节已插入1ms延时”
SSCOM 软件本身加入了人为延时!

计算额外延时:
文件大小:1,916,270 字节
每 256 字节加 1ms 延时
总共分包数:
⌊ 1916270 256 ⌋ = 7485  包 \left\lfloor \frac{1916270}{256} \right\rfloor = 7485 \text{ 包} 2561916270=7485 
每包加 1ms → 总额外延时:
7485 × 1   ms = 7.485   秒 7485 \times 1\,\text{ms} = 7.485\,\text{秒} 7485×1ms=7.485

👉 这与实际多出的 7.16 秒非常接近!

6、拓展

如果只接TX–RX, RX–TX 这样两根线,不接电源线或者不接GND地线,文件传输无法正常进行。
分析原因:
不接 GND(地线)时,两个设备没有共同的参考电压。
发送方说“高电平是 3.3V”,是相对于它的地;
接收方却用自己的地来判断,两者地电位可能不同。
结果:
接收方看不懂发送方的信号,导致通信失败或数据出错。
🔌 就像打电话,有声音线但没接地,声音就不清楚。
所以,TX、RX、GND 三根线必须都接,才能正常通信。

三. STM32CubeMX + Keil 使用 HAL 库配置 USART1

1、前期准备

开发环境搭建

  1. 安装 STM32CubeMX

  2. 安装 Keil MDK-ARM(推荐版本 5.x 或以上)

  3. 安装stm32的 HAL库支持包


硬件准备

  • 使用STM32F103C8T6开发板
  • 连接 CH340模块
  • 接线:
    • TX→PA10,RX→PA9
    • STM32 GND → USB-TTL GND

2、使用 STM32CubeMX 配置工程

步骤1:创建新项目
1 打开 STM32CubeMX,并点击

在这里插入图片描述
2. 选择芯片:STM32F103C8T6,收藏后点击右上角Start Project
在这里插入图片描述
3.在RCC中选择外部高速晶振(只有选择了这个之后才能在后续配置时钟树)
在这里插入图片描述
4. 在SYS中选择调试接口
在这里插入图片描述

注:如果不选择的话就只能下载一次代码,后续下载之后可能需要点击复位键,比较麻烦

为了增强大家对后续操作的理解,这里简单补充关于时钟树的相关知识
在这里插入图片描述
当然可以!以下是极简版 STM32 时钟树,用一张简单表格告诉你所有关键点,适合初学者快速理解:

名称 是什么 频率 用途
HSI 内部时钟(自带) 8 MHz 开机默认,不用外接
HSE 外部晶振(小水晶) 8 MHz 精准时钟,用于主系统
PLL 锁相环(倍频器) 72 MHz 把 8MHz 放大到 72MHz
SYSCLK 主时钟(系统心跳) 最高 72 MHz 决定 CPU 跑多快
HCLK CPU 时钟 通常 = SYSCLK(72MHz) 给 CPU 和内存用
PCLK1 低速外设时钟 通常 36 MHz 给 I2C、UART、TIM2/3/4 等
PCLK2 高速外设时钟 通常 72 MHz 给 GPIO、USART1、TIM1 等
LSE 外部低速晶振 32.768 kHz 给“闹钟”(RTC)用
LSI 内部低速时钟 ~40 kHz 给看门狗(IWDG)用

其中PLL为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16倍,但是其输出频率最大不得超过72MHz。 通过倍频之后作为系统时钟的时钟源。
所以我们后续选择HSE ,然后PLL进行倍频

步骤2:配置时钟(Clock Configuration)
1、进入 Clock Configuration 标签页
在这里插入图片描述

  • 设置 HSE 为 Crystal/Ceramic Resonator
  • 将系统时钟(SYSCLK)设置为 72MHz(典型值)
    • HCLK = 72MHz
    • PCLK1 = 36MHz, PCLK2 = 72MHz

在这里插入图片描述

步骤 3:配置 USART1
选择USART1进行传输,将串口设置为异步工作模式

  • Connectivity → USART1 中:
    在这里插入图片描述

步骤 4:启用中断

  • 在 NVIC Settings 标签页中:
    • ✅ USART1 global interrupt
      在这里插入图片描述

步骤 5:生成代码

  • Project Manager → 设置项目名、路径、工具链为 MDK-ARM V5
    在这里插入图片描述

  • Code Generator Options:

    • 勾选 Generate peripheral initialization as a pair of '.c/.h' files per peripheral
      在这里插入图片描述
  • 点击右上角 Generate Code生成代码
    在这里插入图片描述
    在这里插入图片描述


3、Keil 工程修改与代码实现

打开生成的工程,在 main.c 文件中进行如下修改:

✅ (1):连续发送“hello windows!”并测试是否丢包

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER INCLUDE END */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);

/* Global variables */
uint8_t send_data[] = "hello windows!\r\n";
uint8_t rx_data; // 用于接收中断
volatile uint8_t send_pause = 0; // 控制发送暂停标志

/**
  * @brief  The application entry point.
  */
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  /* Enable UART interrupt for RX */
  HAL_UART_Receive_IT(&huart1, &rx_data, 1);

  /* USER CODE BEGIN WHILE */
  while (1)
  {
    if (!send_pause)
    {
      HAL_UART_Transmit(&huart1, send_data, sizeof(send_data) - 1, HAL_MAX_DELAY);
      // 注意:这里没有加延时
    }
  }
}

结果如下:

串口无时延


➤仿真分析:
检测: USART1_SR 寄存器的 RXNE 位 的变化情况
在这里插入图片描述
在这里插入图片描述
分析:

第一次上升沿 RXNE = 1 发送了一个字节
第二次上升沿 RXNE = 1 再次发送一个字节
第三次上升沿 RXNE = 1 第三次发送

3 个 RXNE 脉冲,以下原因如下:
接收中断触发条件
HAL_UART_Receive_IT() 设置的是 单字节接收中断。
每当收到一个字节,就会触发一次中断,并将 RXNE 标志置 1。
中断处理完成后,RXNE 自动清零(由硬件自动清除)。

(2):上位机控制 STM32 发送启停

功能描述

  • 上位机发 # → 停止发送
  • 上位机发 * → 继续发送

✅ 添加串口中断回调函数(处理接收命令)

main.c 中添加以下函数(放在 main() 后面即可):

/**
  * @brief UART 回调函数
  */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    if (rx_data == '#')
    {
      send_pause = 1; // 暂停发送
    }
    else if (rx_data == '*')
    {
      send_pause = 0; // 恢复发送
    }

    // 重新开启下一次中断接收
    HAL_UART_Receive_IT(&huart1, &rx_data, 1);
  }
}

效果如下:

串口阻塞


四. 使用中断和 DMA 方式实现串口通信

我们继续深入学习 STM32 的中断与 DMA 通信机制,使用 STM32CubeMX + HAL 库 + Keil 完成以下两个任务:

1、采用串口中断方式重做任务二(2)

目标:用 中断方式接收上位机命令# 暂停,* 继续),并用 中断方式发送数据 替代阻塞发送。

为什么使用中断?

  • 避免 HAL_UART_Transmit() 阻塞 CPU
  • 提高系统响应性和效率
  • 实现“后台发送 + 实时响应接收”

步骤 1:CubeMX 配置

确保已配置:

  • USART1 异步模式,115200, 8N1
  • ✅ 使能 USART1 全局中断(NVIC)
  • ✅ 初始化时已调用 HAL_UART_Receive_IT()HAL_UART_Transmit_IT()

CubeMX 中无需额外设置,只需在代码中启用中断传输。


步骤 2:完整代码(main.c)

/* Includes */
#include "main.h"
#include "usart.h"
#include "gpio.h"

/* Private variables */
extern UART_HandleTypeDef huart1;

uint8_t send_data[] = "hello windows!\r\n";
uint8_t rx_data;
volatile uint8_t send_pause = 0;
volatile uint8_t tx_complete = 1; // 标志:上一次发送完成

void SystemClock_Config(void);

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

  // 启动串口接收中断(单字节循环接收)
  HAL_UART_Receive_IT(&huart1, &rx_data, 1);

  while (1)
  {
    // 非阻塞发送控制
    if (!send_pause && tx_complete)
    {
      tx_complete = 0;
      HAL_UART_Transmit_IT(&huart1, send_data, sizeof(send_data) - 1);
      // 不加延时,靠中断触发下一次发送
    }
  }
}

/**
 * @brief UART 发送完成回调函数
 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    tx_complete = 1; // 发送完成,可触发下一次
  }
}

/**
 * @brief UART 接收完成回调函数
 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    if (rx_data == '#')
    {
      send_pause = 1;
    }
    else if (rx_data == '*')
    {
      send_pause = 0;
      // 若暂停后恢复,尝试立即触发一次发送
      if (tx_complete)
      {
        tx_complete = 0;
        HAL_UART_Transmit_IT(&huart1, send_data, sizeof(send_data) - 1);
      }
    }

    // 必须重新启动接收中断!!
    HAL_UART_Receive_IT(&huart1, &rx_data, 1);
  }
}

效果如下:

串口中断非阻塞


2、使用DMA 方式连续向上位机高速发送数据

目标:利用 DMA 实现 零 CPU 干预 的连续串口发送,支持 115200 或更高速率(如 921600、2Mbps)

为什么使用 DMA?

  • 发送大量数据时不占用 CPU
  • 支持高波特率下稳定传输
  • 适合音频、图像、日志等大数据流

步骤 1:CubeMX 配置 USART1 + DMA

  1. 打开 CubeMX,选择 USART1

  2. DMA Settings 标签页中点击 Add

    • Request: USART1_TX
    • Mode: Circular(循环发送)
    • Inc: Memory Increment Enable
    • Data Width: Byte
    • Direction: Memory to Peripheral
      在这里插入图片描述
  3. NVIC Settings:

    • ✅ USART1 global interrupt
  4. 生成代码

自动生成 MX_DMA_Init() 和 DMA 相关初始化


步骤 2: 完整代码(main.c)

#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"

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

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
// 发送缓冲区(可自定义内容和长度,最大65535字节)
#define TX_BUFFER_LEN  32
uint8_t tx_buffer[TX_BUFFER_LEN] = "Hello DMA\r\n";
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
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_DMA_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  // 启动DMA连续发送(循环模式下一次启动即可持续发送)
  if (HAL_UART_Transmit_DMA(&huart1, tx_buffer, TX_BUFFER_LEN) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    // 可添加其他业务逻辑(DMA自动循环发送,无需干预)
  }
  /* USER CODE END 3 */
}

效果如下:

DMA


3、提高波特率的方法(支持更高速率)

CubeMX 中修改 USART1 波特率

  • 设置为:9216001000000(1Mbps)甚至 2000000
  • 注意:PC 端串口芯片(如 CH340、CP2102)必须支持该速率
    • ✅ CP2102 支持最高 2Mbps
    • ❌ CH340 最高通常 2Mbps,但部分驱动支持有限

推荐使用 CP2102 模块测试高速传输


五、 总结

从实验中可以总结出串口DMA的核心作用,二者在数据传输中分工不同但协同工作,共同保障通信的可靠性与效率:

1、串口(UART/USART)的作用

串口是设备间数据交换的“物理链路与协议规范”,核心作用是实现不同设备(如STM32与上位机、电脑与电脑)之间的异步数据传输,具体体现为:

  1. 提供标准化的通信协议
    定义数据传输的“规则”:包括波特率(数据传输速率,如115200bps)、数据帧格式(1位起始位、8位数据位、1位停止位、无校验位等),确保发送方与接收方能“听懂”彼此的信号(如实验中STM32与上位机需一致配置波特率才能正确解析“hello windows!”)。

  2. 实现电平信号的转换与传输
    作为物理接口,串口通过TX(发送)和RX(接收)引脚传输电信号(TTL电平或RS232电平),将二进制数据转换为符合协议的高低电平波形(如实验中STM32的PA9引脚输出的串口波形),完成数据的物理层传输(如文件传输实验中电脑间的二进制文件数据通过串口线传递)。

  3. 支持双向通信与指令交互
    串口可同时实现发送(TX)和接收(RX),支持设备间的双向数据交换。例如实验中“#暂停、*继续”功能:上位机通过串口发送控制指令,STM32通过串口接收并响应,体现了串口作为“指令交互通道”的作用。

2、DMA(直接存储器访问)的作用

DMA是解放CPU、提升数据传输效率的“硬件搬运工”,核心作用是在无需CPU干预的情况下,直接实现内存与外设(如串口)之间的数据传输,具体体现为:

  1. 减少CPU负担,提高系统效率
    传统串口发送(轮询或中断方式)中,CPU需频繁参与数据搬运(如轮询等待发送完成,或中断响应处理),占用大量CPU资源。而DMA可独立完成“内存缓冲区→串口外设”的数据传输(如实验中DMA循环发送“hello windows!”时,CPU可空闲处理其他任务)。

  2. 支持高速率、连续数据传输
    由于DMA由硬件直接控制传输,速度远快于CPU软件操作,适合高波特率(如115200bps及以上)或大量数据的连续传输。实验中DMA循环发送时,数据无间断输出,且不会因CPU忙碌导致传输延迟或丢失,保障了高速通信的稳定性。

  3. 简化连续传输的软件逻辑
    通过配置DMA为“循环模式”,可实现数据的自动重复发送(如实验中发送缓冲区的数据发送完成后,DMA自动从头开始传输),无需CPU在主循环中重复调用发送函数,简化了连续传输的代码设计。

二者结合,既保证了通信的可靠性(串口协议),又提升了系统的效率(DMA硬件加速),是嵌入式系统中高效数据传输的经典方案。

📌 如本文对你有帮助,欢迎点赞 ✅、收藏 ⭐、关注 💡,你的支持是我持续创作的最大动力!

💬 有任何问题或建议,欢迎在评论区留言,我会一一回复!

3、参考文献

江科大自化协
串口简单数据收发

Logo

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

更多推荐