
1. 从零开始为什么STM32的串口通信是嵌入式开发的“必修课”如果你刚开始接触STM32或者已经玩过点灯、按键那么串口通信UART绝对是你绕不开的第一个“硬骨头”。我见过太多新手包括当年的我自己在配置串口时被各种寄存器、波特率、中断、DMA搞得晕头转向最后代码跑起来要么收不到数据要么收到一堆乱码。但一旦你真正搞懂了它你会发现串口不仅仅是给电脑发个“Hello World”那么简单它是你调试程序最可靠的“眼睛”和“嘴巴”也是连接传感器、模块、甚至另一块MCU的“万能桥梁”。今天我就用最经典的STM32F1系列和它的标准外设库Standard Peripheral Library带你手把手、一行行代码地打通串口发送与接收的任督二脉。我们不玩虚的从原理到配置从代码到调试把每一个可能踩的坑都提前标出来。为什么强调标准库虽然现在HAL库和CubeMX很火图形化配置确实方便但对于理解底层原理、构建扎实的嵌入式功底标准库是无法替代的。它更接近硬件寄存器能让你清晰地看到“代码是如何驱动硬件的”。掌握了标准库下的串口你再去看HAL库会发现那只是一层封装核心思想完全一样。所以这篇内容既是实战指南也是一次底层原理的深度剖析。我们将聚焦于最常用的USART1实现阻塞式发送、中断接收并探讨更高效的DMA方式。准备好了吗我们开始。2. 核心原理速览串口通信到底在“通”什么在动手写代码之前我们必须花几分钟把串口通信的基本原理搞清楚。这就像学开车先要知道油门和刹车在哪否则直接上路就是灾难。串口通信本质上是两个设备通过两根线RX接收和TX发送按照事先约定好的规则一位一位地传输数据。2.1 关键概念拆解波特率、数据位、停止位与校验位想象一下两个人在用摩斯电码交流他们必须事先约定好敲多快算一个点波特率一个字母用几个点划表示数据位怎么表示一个字母发完了停止位以及怎么检查发错了没有校验位。串口通信的规则也是如此波特率 (Baud Rate)这是最重要的参数表示每秒传输的符号数。对于串口一个符号就是一个比特(bit)所以常被理解为每秒传输的位数(bps)。常见的波特率有9600 115200等。通信双方必须设置为相同的波特率否则接收方时钟对不上解出来的全是乱码。这里有个关键点STM32的USART时钟源通常来自APB总线和波特率寄存器值共同决定了实际波特率。计算不准通信必挂。数据位 (Data Bits)表示一个字符或者说一帧数据由多少位组成。通常是8位这也是最常用的因为它刚好对应一个字节byte。当然也可以是9位用于多机通信的地址帧识别。停止位 (Stop Bits)用于表示一帧数据的结束。可以是1位、1.5位或2位。绝大多数情况下用1位停止位。停止位提供了帧与帧之间的间隔让接收方有时钟恢复的余地。校验位 (Parity Bit)一种简单的错误检测机制。可以是奇校验、偶校验或无校验。奇校验保证数据位校验位中“1”的个数为奇数偶校验则为偶数。如果接收方计算出的奇偶性与约定不符就会报错。在要求不高的场合为了简单常用“无校验”。一个典型的8-N-1配置就是指8位数据位、无校验位、1位停止位。这是我们今天代码演示的基础配置。2.2 STM32的USART外设架构与工作流程STM32内部的USART通用同步异步收发器是一个相当复杂且强大的外设。对于基础异步通信UART模式我们主要关注以下几个部分发送器 (Transmitter)你把数据写入USART_DR数据寄存器硬件会在波特率时钟的控制下自动加上起始位、停止位和校验位然后通过TX引脚一位一位地移出去。发送完成后会有状态标志位如TC发送完成置起。接收器 (Receiver)RX引脚上的电平变化被硬件检测到起始位下降沿然后硬件按照波特率采样将收到的位流组合成数据存入USART_DR。接收完成后状态标志位如RXNE接收寄存器非空置起。波特率发生器 (Baud Rate Generator)这是核心。它根据你给USART_BRR寄存器写入的值对输入的APB时钟进行分频产生出精确的波特率时钟。波特率计算的准确性直接决定了通信成败。中断控制器 (NVIC)为了让CPU不用傻傻地等待轮询发送完成或数据到来我们可以使能USART的发送完成中断、接收中断等。当事件发生时硬件自动跳转到中断服务函数CPU再去处理这样效率高得多。理解了这些你就知道我们的代码要做什么初始化硬件配置GPIO、USART寄存器 - 设置通信规则波特率等 - 选择工作方式轮询还是中断 - 然后读写数据寄存器。3. 硬件连接与工程环境准备理论说再多不如接上线。我们以最普及的STM32F103C8T6蓝色药丸核心板为例它的USART1的TX是PA9RX是PA10。3.1 必不可少的硬件连接你需要准备STM32F103C8T6 开发板一块。USB转TTL串口模块一个比如CH340、CP2102等。这是连接电脑和STM32的桥梁。杜邦线若干。连接方式至关重要接错可能烧芯片STM32的TX (PA9)接USB转TTL模块的RX。STM32的RX (PA10)接USB转TTL模块的TX。STM32的GND接USB转TTL模块的GND。STM32的3.3V可以接USB转TTL模块的3.3V如果模块有且需要供电但强烈建议STM32单独供电比如通过开发板的MicroUSB口。绝对不要把USB转TTL模块的5V接到STM32的任何引脚除了某些支持5V容忍的I/O口但新手不建议冒险。注意TX接RXRX接TX这叫“交叉连接”。GND必须共地这是电压参考基准不共地通信绝对失败。检查三遍再上电3.2 软件工程搭建标准库的获取与配置获取标准库从ST官网或可靠的资源站下载STM32F10x_StdPeriph_Lib。解压后我们主要用到Libraries文件夹下的CMSIS内核相关和STM32F10x_StdPeriph_Driver外设驱动库以及Project下的模板工程。新建工程以Keil MDK为例新建一个工程选择你的芯片型号STM32F103C8。在工程中创建分组User放main.c等、StdPeriph放库文件、CMSIS放启动文件和系统文件。将库文件添加到对应分组。关键文件包括CMSIS分组core_cm3.csystem_stm32f10x.c 以及startup_stm32f10x_md.s启动文件对于C8是md中等密度。StdPeriph分组根据你需要的外设添加.c文件我们至少需要stm32f10x_gpio.cstm32f10x_usart.cstm32f10x_rcc.c时钟控制必须。在工程选项C/C的Define中添加全局宏定义USE_STDPERIPH_DRIVERSTM32F10X_MD。这是告诉编译器我们使用标准库和中等密度芯片。在Include Paths中添加头文件路径指向库的inc目录和你自己的User目录。完成这些一个最基础的标准库工程骨架就搭好了。接下来我们进入核心的代码部分。4. 代码实战USART1初始化与发送“Hello World”一切就绪我们从最简单的功能开始让STM32主动向电脑发送一串字符。我们采用轮询阻塞方式发送因为它最简单直观。4.1 GPIO与USART时钟使能所有外设在使用前必须先打开它的时钟。这是STM32低功耗设计的特点默认时钟都是关闭的。#include stm32f10x.h void USART1_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; /* 第一步开启时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // USART1挂在APB2总线上GPIOA也是所以一起开启4.2 配置GPIO引脚为复用推挽输出和浮空输入USART的TX引脚需要输出数据但它不是普通的GPIO输出而是由外设USART控制输出的所以模式要设置为复用推挽输出。RX引脚是输入设置为浮空输入或上拉输入均可。/* 第二步配置USART1 Tx (PA.9) 为复用推挽输出 */ 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); /* 配置USART1 Rx (PA.10) 为浮空输入 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, GPIO_InitStructure);4.3 配置USART通信参数并使能这是核心配置我们按照前面说的“8-N-1”来设置波特率设为经典的115200。/* 第三步配置USART1 工作参数 */ USART_InitStructure.USART_BaudRate 115200; // 波特率 USART_InitStructure.USART_WordLength USART_WordLength_8b; // 8位数据 USART_InitStructure.USART_StopBits USART_StopBits_1; // 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(USART1, USART_InitStructure); /* 第四步使能USART1 */ USART_Cmd(USART1, ENABLE); }4.4 实现阻塞式发送函数与主循环测试标准库提供了USART_SendData函数来发送一个字符实际是写DR寄存器但我们需要一个发送字符串的函数并且要等待上一个字符发送完成才能发下一个否则会覆盖。// 发送一个字符阻塞式等待发送完成 void USART1_SendByte(uint8_t byte) { USART_SendData(USART1, byte); // 将数据写入DR寄存器硬件开始发送 while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 循环等待发送完成标志TC置位 // 注意这里不能用RXNE接收标志也不能用TXE发送寄存器空TC是最稳妥的。 } // 发送一个字符串 void USART1_SendString(char *str) { while (*str) { USART1_SendByte(*str); } }现在在主函数中初始化后就可以发送“Hello World”了。int main(void) { USART1_Config(); // 初始化串口1 while (1) { USART1_SendString(Hello STM32 UART!\r\n); // \r\n是换行让串口助手能显示新行 Delay_ms(1000); // 假设有一个简单的延时函数延时1秒 } }将程序编译下载到板子用串口助手如XCOM、SSCOM选择正确的COM口你的USB转TTL模块对应的端口波特率设为1152008N1打开串口。如果一切正常你应该能看到每隔一秒收到一行“Hello STM32 UART!”。恭喜你发送功能通了踩坑点1波特率计算与时钟树。如果你的串口助手收到的是乱码99%是波特率不对。检查两点一、代码和串口助手设置的波特率是否一致二、你的系统时钟SystemCoreClock是否正确。标准库默认使用8MHz外部晶振HSE经过PLL倍频到72MHz。如果你的板子外部晶振不是8MHz比如是12MHz就需要修改system_stm32f10x.c文件中的宏定义和PLL配置否则系统时钟不对计算出的波特率自然不准。对于F103波特率计算公式是Tx/Rx波特率 fCK / (16 * USARTDIV)其中fCK是给USART的时钟PCLK2对于USART1。库函数USART_Init内部已经帮你计算并写入了USART_BRR寄存器。5. 进阶中断接收与数据处理只会发送是“哑巴”能接收才是“对话”。轮询接收不断查询RXNE标志会浪费大量CPU资源所以中断接收是更实际的选择。当USART收到一个字节后硬件会自动产生中断CPU跳转到中断服务函数取走数据。5.1 配置NVIC与使能接收中断首先我们需要配置嵌套向量中断控制器NVIC告诉CPU USART1的中断请求由谁处理以及优先级。void USART1_NVIC_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; /* 配置NVIC */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 设置优先级分组为组12位抢占2位响应 NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; // USART1全局中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; // 抢占优先级1 NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; // 子优先级1 NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }然后在之前的USART1_Config函数末尾加上NVIC配置和中断使能。/* 第五步配置并使能USART1接收中断 */ USART1_NVIC_Config(); // 配置NVIC USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收寄存器非空中断5.2 编写中断服务函数与数据缓冲中断服务函数ISR的名字是固定的在启动文件startup_stm32f10x_md.s里已经声明好了我们只需要定义它。它的核心任务是判断中断来源 - 读取数据 - 清除中断标志。但是中断服务函数里不宜做复杂处理比如解析字符串应该快进快出。通常的做法是将收到的字节存入一个环形缓冲区FIFO主循环再慢慢从缓冲区里取数据解析。我们先实现一个简单的全局数组作为缓冲区。#define RX_BUF_SIZE 128 uint8_t USART1_RxBuffer[RX_BUF_SIZE]; volatile uint16_t USART1_RxCounter 0; // volatile很重要防止编译器优化 void USART1_IRQHandler(void) { uint8_t received_data; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { // 判断是否是接收中断 received_data USART_ReceiveData(USART1); // 读取数据同时会清除RXNE标志 // 简单的缓冲区处理防止溢出 if (USART1_RxCounter RX_BUF_SIZE) { USART1_RxBuffer[USART1_RxCounter] received_data; } else { // 缓冲区溢出处理可以点亮一个错误LED // USART1_RxCounter 0; // 或者清空缓冲区重新开始 } // 通常不需要手动清除RXNE标志因为USART_ReceiveData()函数已经清除了 } // 还可以处理其他USART中断如发送完成、溢出错误等 // if (USART_GetITStatus(USART1, USART_IT_ORE) ! RESET) {...} }5.3 主循环中的数据处理逻辑现在我们修改主函数在发送“Hello World”的同时检查接收缓冲区是否有数据如果有就回显Echo回去。int main(void) { USART1_Config(); // 初始化串口1包含了中断配置 while (1) { // 1. 定时发送 static uint32_t last_send_time 0; if (HAL_GetTick() - last_send_time 1000) { // 使用系统滴答定时约1秒 last_send_time HAL_GetTick(); USART1_SendString(System Alive.\r\n); } // 2. 处理接收到的数据 if (USART1_RxCounter 0) { // 进入临界区防止中断打断简单处理可以关中断 __disable_irq(); uint16_t temp_counter USART1_RxCounter; USART1_RxCounter 0; __enable_irq(); // 回显数据 for (uint16_t i 0; i temp_counter; i) { USART1_SendByte(USART1_RxBuffer[i]); } USART1_SendString(\r\nEcho Done.\r\n); } } }重新编译下载。打开串口助手在发送区输入一些字符比如“test”点击发送。你应该能看到助手接收区立刻显示“test”和你添加的“Echo Done.”。这说明中断接收和回发功能完全正常。踩坑点2中断服务函数与标志位清除。中断服务函数里必须清除对应的中断标志位否则会不断触发中断导致程序卡死。对于RXNE中断USART_ReceiveData()函数读取USART_DR寄存器后硬件会自动清除RXNE标志。但对于其他中断如溢出错误ORE、发送完成TC等可能需要手动调用USART_ClearITPendingBit()来清除。务必仔细查阅参考手册确认每个中断标志的清除方式。踩坑点3缓冲区与临界区保护。上面的简单例子在主循环读取USART1_RxCounter和清空它时如果被中断打断可能导致数据错乱比如刚读了一半中断又写入新数据。我们用了__disable_irq()和__enable_irq()来临时关闭和打开全局中断这是一种简单粗暴的临界区保护。对于更复杂的系统建议使用真正的环形缓冲区并配合更精细的中断开关或信号量。6. 性能优化使用DMA进行高效数据搬运当你需要传输大量数据比如GPS模块持续输出、文件传输、图像数据时每个字节都进一次中断CPU开销会很大。这时就该DMA直接存储器访问出场了。DMA可以在外设如USART的DR寄存器和内存如你的数组之间直接搬运数据完全不需要CPU参与解放CPU去处理其他任务。6.1 DMA原理与USART接收配置对于USART接收我们可以配置DMA让它自动把USART1-DR寄存器里的数据搬运到我们指定的内存数组中。当搬运了指定数量的数据后DMA会产生一个传输完成中断通知CPU“数据包收齐了”。首先需要开启DMA时钟并配置DMA通道USART1_RX对应DMA1_Channel5。void USART1_DMA_Rx_Config(uint8_t *buffer, uint16_t buffer_size) { DMA_InitTypeDef DMA_InitStructure; /* 开启DMA时钟 */ RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); /* 配置DMA通道5 (USART1_RX) */ DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; // 外设地址USART数据寄存器 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)buffer; // 内存地址你的数组 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; // 传输方向从外设到内存 DMA_InitStructure.DMA_BufferSize buffer_size; // 传输数据量 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_Circular; // 循环模式收完指定数量后从头开始适合持续流数据 // DMA_InitStructure.DMA_Mode DMA_Mode_Normal; // 正常模式收完指定数量后停止适合定长数据包 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; // 不是内存到内存 DMA_Init(DMA1_Channel5, DMA_InitStructure); /* 使能USART1的DMA接收请求 */ USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); /* 使能DMA通道5 */ DMA_Cmd(DMA1_Channel5, ENABLE); }6.2 结合中断处理不定长数据DMA的循环模式很好但它不知道一帧数据什么时候结束。对于不定长数据比如以换行符\n结尾的字符串我们需要结合IDLE空闲线路中断。当USART的RX线空闲超过一个字符的时间没有新数据时会产生IDLE中断。在IDLE中断里我们可以计算出DMA接收了多少数据。首先使能USART的IDLE中断。// 在USART1_Config函数中使能接收中断后加上IDLE中断使能 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);然后修改中断服务函数处理IDLE中断。// 定义全局变量 #define DMA_RX_BUF_SIZE 256 uint8_t USART1_DMA_RxBuffer[DMA_RX_BUF_SIZE]; volatile uint16_t USART1_DMA_RxLen 0; // 本次接收到的数据长度 volatile uint8_t USART1_DMA_RxFlag 0; // 接收完成标志 void USART1_IRQHandler(void) { uint8_t received_data; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { // 如果使能了DMA接收RXNE中断可能不会产生或者我们不在这个中断里处理数据 // 这里可以留空或者用于非DMA的备用接收 USART_ReceiveData(USART1); // 读一下清标志 } if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { // 空闲中断检测到 USART_ReceiveData(USART1); // 读SR寄存器必须读SR再读DR才能清除IDLE标志 // 或者 USART1-SR; USART1-DR; 的组合 // 计算本次接收的数据长度 // DMA_CNDTRx寄存器存储剩余传输数据量 USART1_DMA_RxLen DMA_RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); // 设置接收完成标志 USART1_DMA_RxFlag 1; // 清除IDLE中断标志通过读SR和DR // 上面USART_ReceiveData(USART1)已经完成了清除操作 } }6.3 主循环处理与缓冲区重置在主循环中我们检查USART1_DMA_RxFlag当它为1时表示收到了一帧数据。int main(void) { // ... 初始化USART、DMA等 ... USART1_DMA_Rx_Config(USART1_DMA_RxBuffer, DMA_RX_BUF_SIZE); // 配置DMA接收 while (1) { if (USART1_DMA_RxFlag) { // 进入临界区 __disable_irq(); uint16_t temp_len USART1_DMA_RxLen; USART1_DMA_RxFlag 0; __enable_irq(); // 处理数据例如回显 for (uint16_t i 0; i temp_len; i) { USART1_SendByte(USART1_DMA_RxBuffer[i]); } USART1_SendString(\r\n[DMAIDLE] Echo Done.\r\n); // 重置DMA准备接收下一包数据 // 对于循环模式DMA会自动循环但我们需要重置CNDTR寄存器来“刷新”剩余计数 DMA_Cmd(DMA1_Channel5, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel5, DMA_RX_BUF_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } // ... 其他任务 ... } }这样我们就实现了一个高效的、基于DMAIDLE中断的不定长数据接收机制。CPU只在收到一整帧数据后才被唤醒处理大大提高了效率。踩坑点4DMA模式选择与缓冲区管理。DMA_Mode_Circular循环模式下DMA收满缓冲区后会自动回到开头覆盖旧数据。如果你处理数据的速度跟不上接收速度新数据就会覆盖未处理的数据造成丢失。你需要设计更复杂的双缓冲区或乒乓缓冲区机制。对于确定长度的数据包使用DMA_Mode_Normal正常模式更简单收满指定长度后产生传输完成中断你在中断里处理数据并重新启动DMA即可。踩坑点5IDLE中断标志清除。STM32的IDLE中断标志清除方式比较特殊必须先读USART_SR寄存器再读USART_DR寄存器。库函数USART_GetITStatus会读SR所以我们在其后调用USART_ReceiveData读DR来完成清除操作。这个顺序不能错否则IDLE中断会一直触发。7. 调试技巧与常见问题排查即使代码逻辑正确在实际硬件调试中也可能遇到各种问题。这里分享几个我踩过的坑和排查思路。问题1完全没数据TX引脚没波形。检查1电源和地线。确保板子和USB转TTL模块都已上电且共地。检查2线接反了。确认TX接RXRX接TX。检查3串口助手设置。端口号选对了吗波特率、数据位、停止位、校验位是否和代码一致串口打开了吗检查4代码初始化。时钟开启了吗GPIO模式配置对了吗TX是AF_PPUSART使能了吗如果用了中断/DMANVIC/DMA配置并开启了吗检查5硬件问题。换一个USB口、换一个USB转TTL模块试试。用示波器或逻辑分析仪测一下TX引脚看是否有波形输出。问题2收到乱码。检查1波特率。这是乱码的首要元凶。确认代码计算的波特率和串口助手设置的完全一致。检查系统时钟SystemCoreClock是否正确特别是外部晶振频率是否和代码中HSE_VALUE定义一致。检查2停止位/校验位。代码和串口助手设置必须一致8N1最常用。检查3电压电平。STM32是3.3V电平确保你的USB转TTL模块也是3.3V电平或者支持3.3V。5V电平模块接3.3V的RX可能可以但接TX可能不识别长期使用有风险。问题3只能发不能收或者收数据不全。检查1中断服务函数。中断函数名写对了吗USART1_IRQHandler。中断向量表配置正确吗标准库启动文件一般已配好。检查2中断标志清除。在中断函数里清除标志位了吗特别是RXNE和IDLE。检查3缓冲区溢出。你的接收数组够大吗中断里接收数据太快主循环处理太慢会导致数据被覆盖。考虑增大缓冲区或使用环形缓冲区。检查4流控制。如果使能了硬件流控RTS/CTS需要连接对应的引脚否则可能无法接收。我们通常禁用硬件流控USART_HardwareFlowControl_None。问题4使用DMA接收数据不对或进不了中断。检查1DMA通道。USART1_RX是DMA1_Channel5别配错了。检查2外设和内存地址。DMA_PeripheralBaseAddr必须是(uint32_t)USART1-DR。内存地址是你的数组首地址。检查3DMA传输数据量。DMA_BufferSize是你要传输的字节总数不是缓冲区大小。在循环模式下它通常等于缓冲区大小。检查4DMA使能顺序。应先配置USART再配置并使能DMA最后使能USART的DMA请求USART_DMACmd。检查5IDLE中断使能。对于不定长接收别忘了USART_ITConfig(USART1, USART_IT_IDLE, ENABLE)。一个实用的调试方法软件仿真。在Keil中即使没有硬件也可以使用软件仿真来测试串口逻辑。在Debug模式下打开View - Serial Windows - UART #1你可以看到程序“发送”到虚拟串口的数据。虽然不能模拟接收但对于验证发送逻辑和初始化代码非常有帮助。最后串口通信稳定后它将成为你最得力的调试工具。你可以通过串口打印变量值、程序状态、错误信息远比单步调试和点灯大法高效。花时间彻底掌握它绝对是嵌入式开发路上性价比最高的一笔投资。希望这篇超详细的指南能帮你扫清障碍顺利点亮STM32的串口技能。