1. 项目概述
作为一名嵌入式开发工程师,我经常需要在STM32平台上实现高效的串口通信。传统的串口轮询方式会占用大量CPU资源,而中断方式虽然有所改善,但在大数据量传输时仍不够理想。DMA(直接内存访问)技术能够在不占用CPU的情况下完成数据传输,是提升串口通信效率的最佳选择。
本教程将详细讲解如何在STM32F103C8T6平台上,通过STM32CubeMX工具配置USART串口通信,并结合DMA实现高效的数据收发。这个方案特别适合需要长时间稳定传输数据的应用场景,比如工业控制、数据采集等。
1.1 核心知识点
这个项目涉及以下几个关键技术点:
-
STM32CubeMX工程配置:这是STM32开发的起点,正确的工程配置能避免很多底层问题。我们将重点讲解时钟树配置、调试接口选择等关键设置。
-
USART串口通信原理:理解异步串行通信的基本原理,包括波特率、数据位、停止位等参数的设置意义。
-
DMA控制器工作机制:DMA如何在不占用CPU的情况下完成数据传输,以及如何与USART外设协同工作。
-
中断处理机制:如何通过中断及时响应DMA传输完成事件,以及错误处理的最佳实践。
-
性能优化技巧:包括内存对齐、传输优先级设置等提升DMA传输效率的方法。
1.2 硬件与软件环境
为了确保教程的可复现性,我选择了最常用的STM32F103C8T6(蓝桥杯开发板常用芯片)作为硬件平台。以下是详细的软硬件配置清单:
| 类型 | 具体配置 | 备注 |
|---|---|---|
| 主控芯片 | STM32F103C8T6 | 72MHz主频,64KB Flash,20KB RAM |
| 开发环境 | STM32CubeMX 6.9.0 + MDK-ARM 5.38 | CubeMX用于工程配置,MDK用于代码编写 |
| 调试工具 | USB转TTL模块(CH340) | 价格便宜,稳定性好 |
| 串口调试软件 | SSCOM 5.13.1 | 或其他任意串口助手 |
| 硬件连接 | PA9(TX)接USB-TTL的RX PA10(RX)接USB-TTL的TX GND互联 |
注意交叉连接 |
提示:在实际项目中,建议使用带隔离的USB转串口模块,可以提高系统的抗干扰能力,特别是在工业环境中。
2. STM32CubeMX工程配置
2.1 新建工程
首先打开STM32CubeMX,点击"New Project"按钮。在芯片选择界面输入"STM32F103C8T6",选中后点击"Start Project"。这里有几个细节需要注意:
- 确保选择的芯片型号完全匹配,不同封装的引脚定义可能不同。
- 首次使用某款芯片时,CubeMX会自动下载对应的芯片支持包,需要保持网络连接。
- 工程保存路径最好不要包含中文或特殊字符,避免潜在的兼容性问题。
2.2 基础系统配置
2.2.1 时钟配置(RCC)
时钟是STM32系统的核心,正确的时钟配置直接影响串口通信的稳定性。按照以下步骤配置:
- 在"Pinout & Configuration"标签页,找到"System Core"→"RCC"。
- 在"High Speed Clock (HSE)"选项中选择"Crystal/Ceramic Resonator",启用外部8MHz晶振。
- 切换到"Clock Configuration"标签页,进行如下设置:
- 选择HSE作为系统时钟源
- 设置PLL倍频系数为×9(8MHz×9=72MHz)
- 确认HCLK为72MHz
- APB1 Prescaler设置为/2(APB1时钟=36MHz)
- APB2 Prescaler保持/1(APB2时钟=72MHz)
注意:USART1挂在APB2总线上,因此其时钟为72MHz,这决定了我们可以设置的波特率范围。
2.2.2 SYS配置(调试接口)
调试接口的选择会影响部分GPIO引脚的使用:
- 进入"System Core"→"SYS"。
- 在"Debug"选项中选择"Serial Wire"(SWD)。这是最常用的调试方式,只需要占用PA13(SWDIO)和PA14(SWCLK)两个引脚。
- 其他选项保持默认,特别是"Trace Asynchronous Sw"保持禁用,除非你使用更高级的调试功能。
2.2.3 时钟树配置
时钟树配置已经在RCC部分完成,这里需要特别检查以下几点:
- 确认系统时钟(SYSCLK)显示为72MHz(绿色)。
- 确认APB1总线时钟为36MHz,APB2为72MHz。
- 检查各个外设的时钟源是否正确,特别是USART1应该来自APB2总线。
2.3 串口(USART1)配置
USART1的配置是整个项目的核心之一:
- 在"Connectivity"中选择"USART1"。
- 模式选择"Asynchronous"(异步通信模式)。
- 参数配置如下:
- Baud Rate: 115200 Bits/s
- Word Length: 8 Bits
- Stop Bits: 1
- Parity: None
- Hardware Flow Control: None
- 确认引脚分配:
- PA9被自动分配为USART1_TX
- PA10被自动分配为USART1_RX
经验分享:在工业环境中,建议启用奇偶校验(Parity)以提高通信可靠性,虽然会增加少量开销,但能有效检测传输错误。
2.4 DMA配置(绑定USART1)
2.4.1 启用DMA控制器
DMA配置是提升串口性能的关键:
- 进入"System Core"→"DMA"。
- 点击"Add"按钮添加DMA通道:
- 选择"USART1_TX":对应DMA1 Channel4
- 选择"USART1_RX":对应DMA1 Channel5
2.4.2 DMA通道参数配置
对TX和RX通道分别配置:
USART1_TX (DMA1 Channel4)
- Direction: Memory to Peripheral
- Priority: Medium
- Mode: Normal
- Increment Address: Memory→Enabled, Peripheral→Disabled
- Data Width: Byte
USART1_RX (DMA1 Channel5)
- Direction: Peripheral to Memory
- Priority: Medium
- Mode: Circular
- Increment Address: Memory→Enabled, Peripheral→Disabled
- Data Width: Byte
关键点:RX通道必须设置为Circular模式,这样才能实现持续接收数据而不需要CPU干预。TX通道通常用Normal模式,因为发送通常是主动触发的。
2.5 中断配置
为了及时响应DMA传输完成事件和错误处理,需要配置相关中断:
- 进入"System Core"→"NVIC"。
- 启用以下中断:
- DMA1 channel4 global interrupt(USART1_TX)
- DMA1 channel5 global interrupt(USART1_RX)
- USART1 global interrupt
- 设置中断优先级:
- Preemption Priority: 1
- Sub Priority: 0
调试技巧:在实际项目中,可以根据系统复杂度调整中断优先级。如果系统中有更高优先级的中断,可以适当降低DMA中断优先级,但不要低于USART中断。
2.6 工程生成配置
最后一步是生成工程代码:
- 切换到"Project Manager"标签页。
- 配置工程信息:
- Project Name: STM32_USART_DMA
- Toolchain/IDE: MDK-ARM V5
- 在"Code Generator"标签页:
- 勾选"Generate peripheral initialization as a pair of '.c/.h' files per peripheral"
- 勾选"Copy all used libraries into the project folder"
- 点击"Generate Code"生成工程,完成后点击"Open Project"启动MDK-ARM。
建议:勾选"Generate peripheral initialization..."选项可以让代码结构更清晰,每个外设的配置代码单独成文件,便于维护。
3. 代码编写与优化
3.1 代码文件结构说明
CubeMX生成的工程包含以下主要文件:
main.c:主程序入口,包含main()函数和系统时钟配置usart.c/h:USART1的初始化配置代码dma.c/h:DMA控制器的初始化配置代码gpio.c/h:GPIO引脚配置代码
我们需要新增两个文件来封装DMA收发功能:
usart_dma.c/h:实现DMA收发功能的封装函数
这种结构设计遵循了模块化编程原则,将DMA相关的功能独立封装,提高代码的可重用性和可维护性。
3.2 新增文件:usart_dma.h
头文件主要定义接口和缓冲区:
c复制#ifndef __USART_DMA_H
#define __USART_DMA_H
#include "stm32f1xx_hal.h"
/* 缓冲区大小定义 - 根据实际需求调整 */
#define USART_DMA_RX_BUF_SIZE 128 // 接收缓冲区大小
#define USART_DMA_TX_BUF_SIZE 128 // 发送缓冲区大小
/* 全局变量声明 */
extern uint8_t g_usart1_rx_buf[USART_DMA_RX_BUF_SIZE]; // 接收缓冲区
extern uint8_t g_usart1_tx_buf[USART_DMA_TX_BUF_SIZE]; // 发送缓冲区
extern uint16_t g_usart1_rx_len; // 实际接收字节数
/* 函数声明 */
void USART_DMA_Init(UART_HandleTypeDef *huart);
HAL_StatusTypeDef USART_DMA_SendData(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
void USART_DMA_ReceiveData(UART_HandleTypeDef *huart);
uint16_t USART_DMA_GetRxLen(UART_HandleTypeDef *huart);
void USART_DMA_ClearRxBuf(UART_HandleTypeDef *huart);
#endif /* __USART_DMA_H */
设计要点:缓冲区大小的设置需要权衡内存占用和实际需求。对于大多数应用,128字节已经足够,但如果需要接收大量数据,可以适当增大,但要注意不要耗尽STM32有限的RAM资源。
3.3 新增文件:usart_dma.c
实现文件包含DMA收发功能的具体实现:
c复制#include "usart_dma.h"
#include <string.h>
/* 全局变量定义 - 4字节对齐优化DMA传输 */
uint8_t g_usart1_rx_buf[USART_DMA_RX_BUF_SIZE] __attribute__((aligned(4))) = {0};
uint8_t g_usart1_tx_buf[USART_DMA_TX_BUF_SIZE] __attribute__((aligned(4))) = {0};
uint16_t g_usart1_rx_len = 0;
/* DMA初始化函数 */
void USART_DMA_Init(UART_HandleTypeDef *huart)
{
if(huart == NULL) return;
/* 启动DMA接收(循环模式) */
HAL_UART_Receive_DMA(huart, g_usart1_rx_buf, USART_DMA_RX_BUF_SIZE);
}
/* DMA发送函数(阻塞式) */
HAL_StatusTypeDef USART_DMA_SendData(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
HAL_StatusTypeDef status = HAL_ERROR;
/* 参数校验 */
if(huart == NULL || pData == NULL || Size == 0 || Size > USART_DMA_TX_BUF_SIZE)
return HAL_ERROR;
/* 等待前一次DMA传输完成 */
while(HAL_DMA_GetState(huart->hdmatx) != HAL_DMA_STATE_READY);
/* 拷贝数据到发送缓冲区 */
memcpy(g_usart1_tx_buf, pData, Size);
/* 启动DMA发送 */
status = HAL_UART_Transmit_DMA(huart, g_usart1_tx_buf, Size);
/* 等待发送完成 */
while(HAL_UART_GetState(huart) != HAL_UART_STATE_READY);
return status;
}
/* 重启DMA接收 */
void USART_DMA_ReceiveData(UART_HandleTypeDef *huart)
{
if(huart == NULL) return;
if(HAL_UART_GetState(huart) & HAL_UART_STATE_DMA_RX)
HAL_UART_AbortReceive_DMA(huart);
HAL_UART_Receive_DMA(huart, g_usart1_rx_buf, USART_DMA_RX_BUF_SIZE);
}
/* 获取接收数据长度 */
uint16_t USART_DMA_GetRxLen(UART_HandleTypeDef *huart)
{
if(huart == NULL) return 0;
g_usart1_rx_len = USART_DMA_RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
return g_usart1_rx_len;
}
/* 清空接收缓冲区 */
void USART_DMA_ClearRxBuf(UART_HandleTypeDef *huart)
{
if(huart == NULL) return;
memset(g_usart1_rx_buf, 0, USART_DMA_RX_BUF_SIZE);
g_usart1_rx_len = 0;
USART_DMA_ReceiveData(huart);
}
/* DMA接收完成回调函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
g_usart1_rx_len = USART_DMA_RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
USART_DMA_ReceiveData(huart); // 重启接收
}
}
/* DMA发送完成回调函数 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
/* 可以在这里添加发送完成后的处理逻辑 */
}
}
/* 错误回调函数 */
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
/* 错误处理:清除标志并重启接收 */
__HAL_UART_CLEAR_FLAG(huart, UART_FLAG_PE | UART_FLAG_FE | UART_FLAG_NE | UART_FLAG_ORE);
USART_DMA_ReceiveData(huart);
}
}
代码解析:这里有几个关键点需要注意:
- 使用
__attribute__((aligned(4)))确保缓冲区4字节对齐,提升DMA传输效率- DMA接收使用循环模式,可以持续接收数据而不需要CPU干预
- 发送函数采用阻塞式设计,确保数据完整发送
- 错误回调中清除所有可能的错误标志,保证通信可靠性
3.4 修改main.c文件
主程序主要实现初始化逻辑和简单的回显功能:
c复制/* Includes */
#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "dma.h"
#include "usart_dma.h"
#include <string.h>
/* 主函数 */
int main(void)
{
/* HAL库初始化 */
HAL_Init();
/* 系统时钟配置 */
SystemClock_Config();
/* 外设初始化 */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* 初始化串口DMA */
USART_DMA_Init(&huart1);
/* 发送测试数据 */
uint8_t test_data[] = "STM32 USART DMA Test: Hello World!\r\n";
USART_DMA_SendData(&huart1, test_data, strlen((char*)test_data));
/* 主循环 */
while (1)
{
/* 检查接收数据 */
uint16_t rx_len = USART_DMA_GetRxLen(&huart1);
if(rx_len > 0)
{
/* 回显接收到的数据 */
USART_DMA_SendData(&huart1, g_usart1_rx_buf, rx_len);
/* 清空缓冲区 */
USART_DMA_ClearRxBuf(&huart1);
}
/* 延时降低CPU占用 */
HAL_Delay(10);
}
}
应用逻辑:这个简单的回显程序演示了DMA收发的基本用法。在实际项目中,可以根据需要修改主循环中的处理逻辑,比如解析接收到的数据、执行相应操作等。
4. 代码编译与调试
4.1 编译代码
- 在MDK-ARM中,点击"Rebuild"按钮(或按F7)编译整个工程。
- 检查编译输出窗口,确保没有错误和警告。
- 如果出现头文件找不到的错误,检查:
usart_dma.h文件是否在包含路径中- 在工程中添加了
usart_dma.c文件
编译技巧:建议在MDK的"Options for Target"→"C/C++"中设置警告级别为"AC5-like Warnings",这样可以发现更多潜在问题。
4.2 硬件接线
正确的硬件连接是通信成功的前提:
| STM32引脚 | USB-TTL模块 | 说明 |
|---|---|---|
| PA9 (TX) | RX | 交叉连接 |
| PA10 (RX) | TX | 交叉连接 |
| GND | GND | 共地 |
| 3.3V | 3.3V | 可选,为模块供电 |
安全提示:连接前务必确认USB-TTL模块的工作电压是3.3V,如果是5V模块,需要通过电平转换电路连接,否则可能损坏STM32芯片。
4.3 下载程序
- 连接ST-Link或J-Link调试器到STM32的SWD接口(SWDIO和SWCLK)。
- 在MDK中点击"Load"按钮(或按F8)下载程序。
- 下载完成后,按复位键重启芯片。
下载问题排查:如果下载失败,检查:
- 调试器驱动是否安装正确
- 芯片供电是否正常
- BOOT0引脚是否接地(正常运行时必须接地)
4.4 调试验证
- 打开串口助手软件(如SSCOM)。
- 配置串口参数:
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验位:无
- 打开串口,应该会收到STM32发送的测试字符串。
- 发送任意数据,STM32会将其回显。
调试技巧:如果通信不正常,可以尝试以下排查步骤:
- 检查硬件连接是否正确
- 确认波特率等参数一致
- 用逻辑分析仪或示波器检查TX引脚是否有信号输出
- 检查CubeMX中的USART和DMA配置
5. DMA传输优化技巧
5.1 传输优先级优化
在复杂的系统中,可能有多个DMA通道同时工作。通过调整优先级可以确保关键数据传输不被延迟:
c复制/* 在DMA初始化代码中设置优先级 */
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
设计原则:根据数据的重要性和实时性要求分配DMA优先级。通常,接收通道的优先级应高于发送通道,因为数据丢失的后果更严重。
5.2 内存对齐优化
正确的内存对齐可以显著提升DMA传输效率:
c复制/* 定义4字节对齐的缓冲区 */
__attribute__((aligned(4))) uint8_t g_usart1_rx_buf[USART_DMA_RX_BUF_SIZE];
__attribute__((aligned(4))) uint8_t g_usart1_tx_buf[USART_DMA_TX_BUF_SIZE];
性能分析:4字节对齐后,DMA可以以32位为单位传输数据,效率是字节传输的4倍。这在大数据量传输时效果尤为明显。
5.3 错误处理优化
健壮的错误处理机制能提高系统稳定性:
c复制void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
uint32_t errors = HAL_UART_GetError(huart);
if(errors & HAL_UART_ERROR_PE)
__HAL_UART_CLEAR_FLAG(huart, UART_FLAG_PE);
if(errors & HAL_UART_ERROR_FE)
__HAL_UART_CLEAR_FLAG(huart, UART_FLAG_FE);
if(errors & HAL_UART_ERROR_ORE)
__HAL_UART_CLEAR_FLAG(huart, UART_FLAG_ORE);
USART_DMA_ReceiveData(huart); // 重启接收
}
}
经验之谈:在实际项目中,除了清除错误标志,还应该记录错误发生的次数和类型,便于后期分析和改进。
5.4 非阻塞式发送优化
将发送改为非阻塞式可以释放CPU资源:
c复制/* 新增全局标志 */
volatile uint8_t g_tx_complete = 1;
/* 非阻塞发送函数 */
HAL_StatusTypeDef USART_DMA_Send_NonBlocking(UART_HandleTypeDef *huart, uint8_t *data, uint16_t size)
{
if(!g_tx_complete) return HAL_BUSY;
g_tx_complete = 0;
memcpy(g_usart1_tx_buf, data, size);
return HAL_UART_Transmit_DMA(huart, g_usart1_tx_buf, size);
}
/* 发送完成回调 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
g_tx_complete = 1;
}
性能考量:非阻塞式发送允许CPU在数据传输期间执行其他任务,提高了系统整体效率。但需要应用程序处理发送忙状态。
6. 常见问题与解决方法
6.1 串口无数据输出
可能原因及解决方案:
-
硬件连接错误:
- 检查TX/RX是否交叉连接
- 确认GND已连接
- 检查USB-TTL模块是否正常工作
-
波特率不匹配:
- 确认STM32和PC端串口助手使用相同的波特率
- 检查系统时钟配置是否正确(特别是APB2时钟)
-
DMA配置错误:
- 确认DMA通道正确绑定到USART
- 检查DMA传输方向设置是否正确
6.2 DMA接收数据乱码
可能原因及解决方案:
-
时钟配置错误:
- 重新检查时钟树配置
- 确保USART1时钟源正确(APB2)
-
缓冲区溢出:
- 增大接收缓冲区大小
- 提高数据处理速度,避免缓冲区满
-
电气干扰:
- 检查接线是否过长
- 考虑使用屏蔽线或双绞线
- 在TX/RX线上添加适当的上拉电阻
6.3 DMA传输完成后无回调
可能原因及解决方案:
-
中断未启用:
- 检查CubeMX中是否启用了DMA和USART中断
- 确认NVIC中断优先级设置正确
-
回调函数未实现:
- 确保在main.c或其他文件中实现了回调函数
- 检查函数名拼写是否正确
-
中断优先级过低:
- 提高DMA中断的优先级
- 检查是否有其他中断长时间占用CPU
7. 项目总结与扩展建议
通过这个项目,我们实现了基于DMA的高效串口通信,相比传统方式具有以下优势:
- 低CPU占用:数据传输由DMA控制器完成,CPU只需在传输完成时处理数据
- 高可靠性:循环接收模式确保不会丢失数据
- 高灵活性:可根据实际需求调整缓冲区大小和优先级
对于需要进一步扩展的项目,可以考虑:
- 增加协议解析:在接收回调中实现协议解析,如Modbus、自定义协议等
- 多串口管理:扩展支持多个USART接口,统一管理
- 动态缓冲区:实现动态内存分配,适应不同大小的数据包
- 流量控制:添加硬件或软件流控,防止数据丢失
在实际应用中,我已经使用这种DMA串口方案成功开发了多个工业设备通信模块,稳定运行在各种复杂环境中。关键在于正确配置DMA参数和设计健壮的错误处理机制。希望这个教程能帮助你快速掌握这项实用技术。