USART - 串口通讯
更新于 2026年8月13日
本篇总结
使用 USART + DMA,实现串口数据收发回传。
本示例演示 W55MH32 的 USART1 外设配合 DMA(直接存储器访问)实现异步串口收发。与阻塞式收发不同,DMA 可在不占用 CPU 的情况下自动搬运数据:USART 收到数据时,DMA 自动将其从 USART_DR 寄存器搬到内存缓冲区(接收),反之亦然(发送)。
全程无需 CPU 逐字节轮询,大幅提升系统效率。
串口通讯协议简介
串口通讯(Serial Communication)是设备间常用的串行通讯方式,简单便捷,大部分电子设备都支持,电子工程师调试时也常用来输出调试信息。
通讯协议可分为两层:物理层规定机械、电子功能特性(如 RS-232 电平标准、DB9 接口),确保原始数据在物理媒体上传输;协议层规定通讯逻辑,统一收发双方的数据打包、解包标准(波特率、起始/停止位、数据帧格式、校验位等)。
本例程关键参数:异步通讯(无时钟线) / 波特率 115200 / 8 位数据 / 1 停止位 / 无校验(8N1)/ 全双工。USART1 使用 PA9(TX)、PA10(RX),通过板载 USB 转串口芯片与 PC 通讯。
前置工具准备
开始本例程前,请确认以下工具已安装:
- WIZ UartTool V1.0:串口收发测试工具:点击下载
硬件连接
USART1 的 TX(PA9) 和 RX(PA10) 通过板载 USB 转串口芯片连接到 DEBUG USB 口。仅需一根 Micro USB 线连接开发板 DEBUG USB 口到 PC 即可。
| 引脚 | 功能 | 方向 | 模式 |
|---|---|---|---|
| PA9 | USART1_TX | 开发板 → PC | 复用推挽输出 (AF_PP) |
| PA10 | USART1_RX | PC → 开发板 | 浮空输入 (IN_FLOATING) |
工程结构
工程位于 SDK 内 1.SDK\ModuleDemo\USART\USART_Asyn\,核心文件如下:
USART_Asyn/
├── USER/
│ ├── main.c ← 外设初始化 + DMA收发循环 + printf 重定向
│ ├── w55mh32_conf.h ← 外设头文件集中包含
│ ├── w55mh32_it.c/h ← 中断服务函数
│ ├── system_w55mh32.c/h← 系统初始化(时钟配置)
│ └── startup_w55mh32.s ← 启动文件
└── SYSTEM/
└── delay/
└── delay.c/h ← SysTick 延时函数
步骤1:头文件、宏定义与全局变量
文件:USER/main.c 顶部 — 引入标准库与 W55MH32 头文件,定义 USART_TEST 宏指向 USART1,声明三个配置函数原型,定义缓冲区大小 BUFFSIZE=8 与收发共用数组 Buff。
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "delay.h"
#include "w55mh32.h"
USART_TypeDef *USART_TEST = USART1;
void UART_Configuration(uint32_t bound);
void DMA_RecvConfiguration(void);
void DMA_SendConfiguration(void);
#define BUFFSIZE 8
uint8_t Buff[BUFFSIZE];步骤2:USART 初始化
文件:USER/main.c | 函数:UART_Configuration() — 使能 USART1 与 GPIOA 时钟,PA9 配置为复用推挽输出(TX),PA10 配置为浮空输入(RX),USART1 初始化为 115200 / 8N1 / 收发模式。
void UART_Configuration(uint32_t bound)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
/* 开启外设时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/* PA9: USART1_TX → 复用推挽输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* PA10: USART1_RX → 浮空输入 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* USART 参数:115200 / 8N1 / 全双工 */
USART_InitStructure.USART_BaudRate = bound;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART_TEST, &USART_InitStructure);
USART_Cmd(USART_TEST, ENABLE);
}四步完成:开时钟 → GPIO 引脚模式配置(TX 复用推挽,RX 浮空输入)→ USART 参数配置(115200 / 8N1)→ 使能外设。
如果你想自定义
- 改波特率 → 修改
UART_Configuration()的入参,如UART_Configuration(9600) - 换用 USART2/USART3 → 同步修改 GPIO 引脚和对应的时钟使能宏,具体引脚映射请参考数据手册
步骤3:DMA 接收配置(Channel5)
函数:DMA_RecvConfiguration() — 配置 DMA1 Channel5 将 USART1 RX 数据自动搬运到内存数组 Buff。外设地址为 USART1->DR,方向为外设→内存,缓冲区 8 字节。
void DMA_RecvConfiguration(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
/* USART1_RX DMA Config */
DMA_DeInit(DMA1_Channel5);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Buff;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设→内存
DMA_InitStructure.DMA_BufferSize = BUFFSIZE; // 搬 8 字节后停止
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 普通模式(非循环)
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel5, ENABLE);
}
关键两点:DMA_DIR_PeripheralSRC 表示方向为"外设→内存"(接收);DMA_Mode_Normal 表示搬运完 8 字节后自动停机,需软件重新配置。若需连续接收可改为 DMA_Mode_Circular。
如果你想自定义
- 改缓冲区大小 → 修改
BUFFSIZE宏定义,然后同步调整DMA_BufferSize - 改为循环模式(连续接收) → 将
DMA_Mode_Normal改为DMA_Mode_Circular,并在main()中移除每轮循环的手动重配逻辑
步骤4:DMA 发送配置(Channel4)
函数:DMA_SendConfiguration() — 与接收配置镜像对称,仅两处不同:方向改为"内存→外设"(DMA_DIR_PeripheralDST),使用 Channel4。
void DMA_SendConfiguration(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
/* USART1_TX DMA Config */
DMA_DeInit(DMA1_Channel4);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Buff;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 内存→外设
DMA_InitStructure.DMA_BufferSize = BUFFSIZE;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel4, ENABLE);
}步骤5:主函数逻辑
文件:USER/main.c | 函数:main() — 初始化延时与 UART(115200),打印时钟频率与 USART/DMA 配置参数(引脚、波特率、DMA 通道、缓冲区大小),提示主机发送 8 字节。主循环:打印等待输入提示 → 启动 DMA 接收 → 等待接收完成 → 以 HEX 和 CHAR 双格式打印 → 启动 DMA 发送回显 → 等待发送完成。
int main(void)
{
uint32_t i;
RCC_ClocksTypeDef clocks;
delay_init();
UART_Configuration(115200);
RCC_GetClocksFreq(&clocks);
printf("\n");
printf("SYSCLK: %3.1fMhz, HCLK: %3.1fMhz, PCLK1: %3.1fMhz, "
"PCLK2: %3.1fMhz, ADCCLK: %3.1fMhz\n",
(float)clocks.SYSCLK_Frequency / 1000000,
(float)clocks.HCLK_Frequency / 1000000,
(float)clocks.PCLK1_Frequency / 1000000,
(float)clocks.PCLK2_Frequency / 1000000,
(float)clocks.ADCCLK_Frequency / 1000000);
printf("USART Asyn DMA Test.\n");
printf("USART1: TX=PA9, RX=PA10, Baudrate=115200, 8N1\n");
printf("DMA1 Channel5(RX) / Channel4(TX), Buffer Size=%d bytes\n", BUFFSIZE);
printf("Please send %d bytes from the host serial tool, "
"the board will echo them back via DMA.\n", BUFFSIZE);
while (1)
{
printf("\n--- Waiting for %d bytes input via UART1 ---\n", BUFFSIZE);
/* ---- RX ---- */
DMA_Cmd(DMA1_Channel4, DISABLE);
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
DMA_RecvConfiguration();
while (DMA_GetFlagStatus(DMA1_FLAG_TC5) == RESET);
printf("DMA RX Complete (Channel5), received %d bytes:\n", BUFFSIZE);
printf(" HEX : ");
for (i = 0; i < BUFFSIZE; i++)
printf("%02X ", Buff[i]);
printf("\n CHAR: ");
for (i = 0; i < BUFFSIZE; i++)
printf("%c", Buff[i]);
printf("\n");
/* ---- TX ---- */
printf("DMA TX start (Channel4), echoing back %d bytes...\n", BUFFSIZE);
DMA_Cmd(DMA1_Channel5, DISABLE);
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
DMA_SendConfiguration();
while (DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET);
printf("DMA TX Complete (Channel4).\n");
}
}
RX 和 TX 分时复用,每轮循环先关闭对方 DMA 通道避免冲突。DMA 传输完成后用 DMA_GetFlagStatus() 轮询等待 TC(Transfer Complete)标志。整改后整个 DMA 收发链路的配置参数、等待状态、收发结果均清晰可见。
如果你想自定义
- 避免永久阻塞 → 在
while(DMA_GetFlagStatus(...))后加超时计数器,例如超时 1 秒则跳过本次收发 - 改为中断通知而非轮询 → 启用 DMA 的 TC 中断(
DMA_ITConfig(..., DMA_IT_TC, ENABLE)),在w55mh32_it.c的 DMA 中断中处理完成回调
步骤6:printf 重定向
SER_PutChar() 等待 USART1 的 TC(发送完成)标志置位后发送一个字节。fputc() 重定向标准输出到 USART1,遇到 \n 时先补发 \r,确保串口工具正确换行。
int SER_PutChar(int ch)
{
while (!USART_GetFlagStatus(USART_TEST, USART_FLAG_TC));
USART_SendData(USART_TEST, (uint8_t)ch);
return ch;
}
int fputc(int c, FILE *f)
{
/* Place your implementation of fputc here */
/* e.g. write a character to the USART */
if (c == '\n')
{
SER_PutChar('\r'); // 补回车,确保串口工具正确换行
}
return (SER_PutChar(c));
}编译与下载
Keil MDK 中打开 USART.uvprojx,F7 编译,确认 0 Error(s)。连接 DEBUG USB 口到 PC,F8 下载。下载成功后按 RESET 键启动新固件。
运行验证
打开 WIZ UartTool V1.0,选择开发板对应 COM 口,波特率 115200 / 8N1,打开串口。按 RESET 启动后,预期串口输出配置参数与等待提示:
SYSCLK: 72.0Mhz, HCLK: 72.0Mhz, PCLK1: 36.0Mhz, PCLK2: 72.0Mhz, ADCCLK: 36.0Mhz
USART Asyn DMA Test.
USART1: TX=PA9, RX=PA10, Baudrate=115200, 8N1
DMA1 Channel5(RX) / Channel4(TX), Buffer Size=8 bytes
Please send 8 bytes from the host serial tool, the board will echo them back via DMA.
--- Waiting for 8 bytes input via UART1 ---
在发送区输入 8 个字符(如 ABCDEFGH
)并发送。开发板通过 DMA 接收完成后,以 HEX 与 CHAR 双格式打印数据,随后通过 DMA 将数据原样回发:
DMA RX Complete (Channel5), received 8 bytes:
HEX : 41 42 43 44 45 46 47 48
CHAR: ABCDEFGH
DMA TX start (Channel4), echoing back 8 bytes...
DMA TX Complete (Channel4).
--- Waiting for 8 bytes input via UART1 ---
可看到 DMA 接收 8 字节成功,HEX 与 CHAR 双格式打印一致,DMA 回发完成后进入下一轮等待。整个 DMA 收发链路清晰可见。
常见问题
Q: 编译报错 "cannot open source file w55mh32.h"
检查 Project → Options → C/C++ → Include Paths,确保 SDK 的 Libraries 目录已添加。
Q: 下载成功但串口无输出
确认 COM 口号正确、波特率 115200,下载后需按 RESET 键启动。
Q: 程序卡在 DMA 等待处不动
确认 PC 端串口工具已发送至少 8 字节数据。可增加超时机制避免永久阻塞。
Q: 串口输出乱码
先看串口打印的时钟频率是否正常(SYSCLK 应为 72.0Mhz)。若频率异常,检查 system_w55mh32.c 中 HSE_VALUE 是否与实际晶振一致。
Q: DMA 接收数据错位或丢失
确认每轮循环先关闭对方 DMA 通道(DMA_Cmd(..., DISABLE))再重配本通道,避免残留数据。Normal 模式下每轮必须重新调用配置函数。
总结
本文基于 W55MH32 的 USART1 + DMA 实现了异步串口收发:
- USART1 配置为 115200 / 8N1 全双工模式,通过 PA9(TX) 和 PA10(RX) 与 PC 通信
- DMA1 Channel5 负责接收(外设→内存),Channel4 负责发送(内存→外设),Normal 模式下每轮循环手动重配
- 主循环打印配置参数与等待提示,接收完成后以 HEX/CHAR 双格式打印数据,再通过 DMA 原样回显
- 重写 fputc() 实现 printf 重定向,借助 WIZ UartTool V1.0 观察收发数据