目标: 在项目一的基础上,掌握使用DMA接收不定长串口数据,并构建一个“回显”系统。这是物联网、人机交互等应用的基础。别人对它“说”,它能“听到”,并且能“回应”。
核心概念:
DMA串口接收 (UART RX):配置DMA通道,让其在后台自动将串口接收到的数据存入内存缓冲区。
空闲线路检测 (IDLE Line Detection):这是DMA接收不定长数据的精髓。当串口数据流暂时停止(即总线空闲)时,触发一次中断,通知CPU“一帧数据已经接收完毕”。
中断服务程序 (ISR):在中断中处理接收到的数据,并启动下一次DMA发送任务。
实践步骤:
硬件不变:继续使用你的STM32开发板和USB转TTL模块。
配置DMA接收:配置一个新的DMA通道,数据方向设置为“从外设(串口数据寄存器)到内存”。建议使用循环模式(Circular Mode),这样DMA接收完缓冲区末尾后会自动回到开头,不会停止。
使能串口空闲中断:在串口初始化代码中,除了使能DMA接收,还要使能
IDLE
中断。编写中断服务函数:
在
IDLE
中断函数中,首先清除中断标志位。通过计算DMA剩余传输长度,得知本次接收了多少个字节的数据。
调用项目一中的
My_UART_Transmit_DMA
函数,将刚刚接收到的数据和长度作为参数传进去,实现“回显”。
测试:在PC串口助手中,你发送任何字符串(例如 "Hello, STM32!"),都会立刻在接收区看到完全相同的内容。
预期成果: 一个功能完备的串口回显服务器。这证明了你的系统不仅能通过DMA高效发送,还能高效接收,并能将二者联动,CPU的负担依然极小。
核心代码:
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "string.h"
// 定义接收缓冲区大小
#define BUFFER_SIZE 256
// 定义接收缓冲区
uint8_t Rx_buffer[BUFFER_SIZE];
// 定义发送缓冲区 (这就是我们要发送的“货物”)
uint8_t TxBuffer[BUFFER_SIZE];
uint16_t rx_len = 0;
void Serival_init()
{
//开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//GPIO初始化,把TX配置成复用推挽输出,RX配置成输入
GPIO_InitTypeDef GPIOA_InitStruct;
GPIOA_InitStruct.GPIO_Mode=GPIO_Mode_AF_PP;
GPIOA_InitStruct.GPIO_Pin=GPIO_Pin_9;
GPIOA_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIOA_InitStruct);
// 配置 PA10 (USART1_RX) 为浮空输入
GPIOA_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIOA_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIOA_InitStruct);
//配置USART(直接使用结构体进行配置即可)
USART_InitTypeDef USART1_InitStruct;
USART1_InitStruct.USART_BaudRate=115200; //需要什么波特率直接写即可
USART1_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
USART1_InitStruct.USART_Mode=USART_Mode_Rx | USART_Mode_Tx;
USART1_InitStruct.USART_Parity=USART_Parity_No;
USART1_InitStruct.USART_StopBits=USART_StopBits_1;
USART1_InitStruct.USART_WordLength=USART_WordLength_8b;
USART_Init(USART1,&USART1_InitStruct);
//发送或者接收(接收需要开启中断 进行ITconfig或NVIC配置(发送的话则不需要相关配置))
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能中断通道
NVIC_Init(&NVIC_InitStructure);
// 使能串口的IDLE中断(这个地方要注意,文章中会给出解释)
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
//开关控制(打开串口)
USART_Cmd(USART1,ENABLE);
}
void Uart1DMATX_init()
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置并启动DMA_Channel4 (USART1_TX) 进行发送
DMA_InitTypeDef DMA_InitStructure_TX;
DMA_DeInit(DMA1_Channel4); // 复位DMA发送通道
DMA_InitStructure_TX.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure_TX.DMA_MemoryBaseAddr = (uint32_t)TxBuffer; // 从发送缓冲区发送
DMA_InitStructure_TX.DMA_DIR = DMA_DIR_PeripheralDST; // 方向:内存到外设
DMA_InitStructure_TX.DMA_BufferSize = rx_len; // 发送实际接收到的长度
DMA_InitStructure_TX.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure_TX.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure_TX.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure_TX.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure_TX.DMA_Mode = DMA_Mode_Normal; // 模式:普通模式,发完即停
DMA_InitStructure_TX.DMA_Priority = DMA_Priority_Medium;
DMA_InitStructure_TX.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &DMA_InitStructure_TX);
// 使能串口的DMA发送请求
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
// 初始化完成后,保持通道关闭,等待发送函数来启动它
DMA_Cmd(DMA1_Channel4, DISABLE);
}
void Uart1DMARX_init()
{
DMA_InitTypeDef DMA_InitStructure;
// --- 配置DMA1_Channel5 (USART1_RX) ---
DMA_DeInit(DMA1_Channel5); // 复位DMA通道5
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; // 外设地址:USART1数据寄存器
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Rx_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_Priority = DMA_Priority_High; // 优先级:高
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 非内存到内存模式
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
// 使能串口的DMA接收请求
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
// 使能DMA1_Channel5
DMA_Cmd(DMA1_Channel5, ENABLE);
}
void Serival_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
}
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
{
Serival_SendByte(Array[i]);
}
}
void Serial_Sendstring(char *String)
{
uint8_t i;
for(i=0;String[i]!='\0';i++)
{
Serival_SendByte(String[i]);
}
}
uint32_t Serial_Pow(uint32_t X,uint32_t Y)
{
uint32_t Result=1;
while(Y--)
{
Result*=X;
}
return Result;
}
void Serial_SendNumber(uint32_t Number,uint8_t Length)
{
uint8_t i;
for(i=0;i<Length;i++)
{
Serival_SendByte(Number/Serial_Pow(10,Length-i-1)%10+'0');
}
}
int fputc(int ch,FILE *f)//printf()函数重定向输出信息到串口
{
Serival_SendByte(ch);
return ch;
}
void USART1_IRQHandler(void) {
// 1. 检查是否是IDLE(空闲线路)中断
if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) {
// 2. 清除IDLE中断标志位 (关键!)
// 清除方法是:先读SR寄存器,再读DR寄存器
(void)USART1->SR;
(void)USART1->DR;
// 3. 停止DMA接收,以安全地读取接收到的字节数
DMA_Cmd(DMA1_Channel5, DISABLE);
// 4. 计算接收到的数据长度
// 长度 = 缓冲区总大小 - DMA内待传输数据的数量
rx_len = BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);
// 增加长度判断,提高代码健壮性
if (rx_len > 0)
{
// 5. 准备回显数据
// 将接收缓冲区的数据复制到发送缓冲区
memcpy(TxBuffer, Rx_buffer, rx_len);
// 使能DMA发送通道
DMA_Cmd(DMA1_Channel4, DISABLE); // 先关
// main.c已经对发送的DMA通道进行了初始化你只需要修改长度并启动即可
DMA_SetCurrDataCounter(DMA1_Channel4, rx_len);
DMA_ClearFlag(DMA1_FLAG_TC4); // 清除上次传输完成标志
DMA_Cmd(DMA1_Channel4, ENABLE); // 再开
}
// --- 可以在这里等待发送完成,也可以不等待 ---
// while(DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET); // 等待DMA_Channel4传输完成
// USART_DMACmd(USART1, USART_DMAReq_Tx, DISABLE); // 等待后最好关闭
// 7. 重新使能DMA接收,为下一次接收做准备
DMA_Cmd(DMA1_Channel5, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel5, BUFFER_SIZE);// 将DMA接收通道的计数器手动重置为缓冲区大小
DMA_ClearFlag(DMA1_FLAG_TC5); // 清除上次接收完成标志
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}
⭐实践中需要特别注意的点(易踩坑)
1.IDLE中断的详解
USART_IT_IDLE
中断是 串口空闲中断(IDLE Interrupt),它的作用是:
当串口检测到 接收线路在一次数据接收完成后进入空闲状态(即一帧数据结束且没有新的数据进来)时,会触发这个中断。
📌 典型应用场景
在串口接收中,如果你无法提前知道数据长度(比如用 DMA 接收一段不定长数据),就可以用 IDLE 中断 来判断 一帧数据已经接收完成,在上述程序中我们就实现了使用DMA接收不定长数据,并且通过串口进行回显。
常见做法:
配置 DMA 连续接收(循环模式),不必每次都手动启动 DMA。
当数据发完且总线空闲时,IDLE 中断触发。
在 IDLE 中断里:
先读
USARTx->SR
再读USARTx->DR
清除 IDLE 标志(否则会一直进中断)。停止 DMA 接收,计算已接收的数据长度。
处理接收到的数据。
重新启动 DMA。
💡 清除IDLE中断标志位
在 IDLE 中断里清除中断标志位操作是
先读 USARTx->SR
再读 USARTx->DR
清除 IDLE 标志(否则会一直进中断)
// 清除方法是:先读SR寄存器,再读DR寄存器
(void)USART1->SR;
(void)USART1->DR;
在 STM32 的参考手册里(比如 STM32F1/F4 系列),IDLE
标志位的清除方式是特殊的:
要清除 IDLE 标志位,必须先读 USARTx->SR,然后再读 USARTx->DR。
这两个操作必须按顺序进行,且需要读取寄存器的值(哪怕不使用)。
USART_ClearITPendingBit()
只是写 USARTx->SR
的某些位来清除大多数中断标志(如 TC、RXNE 等),
但是 IDLE 标志并不能通过写 SR 来清除,所以标准库的这个函数对 IDLE 是无效的。
2.避免在通道开启时改寄存器:除了 NDTR
(数据计数器),一般也不要在 ENABLE
状态下去动 CMAR/CPAR/CCR(
修改DMA的传输数据和长度前一定要先关闭使能)
📌 原因
错误写法:
DMA_SetCurrDataCounter(DMA1_Channel4, rx_len); // 使能DMA发送通道
DMA_Cmd(DMA1_Channel4, DISABLE); // 先关
DMA_ClearFlag(DMA1_FLAG_TC4); // 清除上次传输完成标志
DMA_Cmd(DMA1_Channel4, ENABLE); // 再开
这里修改NDTR
(数据计数器),即DMA_SetCurrDataCounter(DMA1_Channel4, rx_len);操作放到了DMA_Cmd(DMA1_Channel4, DISABLE); ,应该注意的是NDTR
(数据计数器)是在通道关闭状态下才能写入的。
正确写法:
DMA_Cmd(DMA1_Channel4, DISABLE); // 先关
// main.c已经对发送的DMA通道进行了初始化你只需要修改长度并启动即可
DMA_SetCurrDataCounter(DMA1_Channel4, rx_len);
DMA_ClearFlag(DMA1_FLAG_TC4); // 清除上次传输完成标志
DMA_Cmd(DMA1_Channel4, ENABLE); // 再开
⭐总结:避免在通道开启时改寄存器:除了 NDTR
(数据计数器),一般也不要在 ENABLE
状态下去动 CMAR/CPAR/CCR
3.“状态要及时清理”,为下一次接收或发送做好准备
不论是接收或者发送都要及时的清除对应的状态位或者标志
DMA TX或者RX 发送完成后不清除 TC 标志,会导致 导致第二次启动 DMA 无效
DMA发送完成:当DMA通道4把所有数据(例如3个字节)从内存搬运到串口数据寄存器后,DMA硬件会自动:
停止DMA通道4(因为是
Normal
模式)。将
DMA1_FLAG_TC4
标志位置1,表示“我的搬运任务完成了”。
(如果不清除TC标志位会造成下面的现象)
第二次触发中断:您从PC发送第二批数据,IDLE中断再次正确触发。
启动发送失败:在中断程序中,您准备启动第二次回显。
代码执行到
DMA_Cmd(DMA1_Channel4, ENABLE);
。此时,DMA控制器检查通道4的状态,发现它的
TC4
标志位仍然是1(因为上次完成后没人清除它)。DMA控制器的内部逻辑是:“这个通道的任务已经标记为完成了,你现在又让我启动它?不行,除非你先把‘完成’标记给清掉。”
因此,
DMA_Cmd
命令被硬件忽略,第二次发送从未真正启动。 这就完美解释了“后续的发送都石沉大海”的现象。
✅ 正确操作
使用下面的这种规范的写法和思路,会保证你在进行下一次接收或发送时状态保证干净,保证传输效果
// 使能DMA发送通道
DMA_Cmd(DMA1_Channel4, DISABLE); // 先关
// main.c已经对发送的DMA通道进行了初始化你只需要修改长度并启动即可
DMA_SetCurrDataCounter(DMA1_Channel4, rx_len);
DMA_ClearFlag(DMA1_FLAG_TC4); // 清除上次传输完成标志
DMA_Cmd(DMA1_Channel4, ENABLE); // 再开
// 重新使能DMA接收,为下一次接收做准备
DMA_Cmd(DMA1_Channel5, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel5, BUFFER_SIZE);// 将DMA接收通道的计数器手动重置为缓冲区大小
DMA_ClearFlag(DMA1_FLAG_TC5); // 清除上次接收完成标志
DMA_Cmd(DMA1_Channel5, ENABLE);
完整代码到文章顶部下载获取,参阅本文章时对应着顶部的完整程序,效果更佳!