STM32F103C8T6学习——直接存储器访问(DMA)标准库实战2(DMA串口接收与回显服务器 (DMA-RX))

发布于:2025-08-16 ⋅ 阅读:(20) ⋅ 点赞:(0)

目标: 在项目一的基础上,掌握使用DMA接收不定长串口数据,并构建一个“回显”系统。这是物联网、人机交互等应用的基础。别人对它“说”,它能“听到”,并且能“回应”。

核心概念:

  • DMA串口接收 (UART RX):配置DMA通道,让其在后台自动将串口接收到的数据存入内存缓冲区。

  • 空闲线路检测 (IDLE Line Detection):这是DMA接收不定长数据的精髓。当串口数据流暂时停止(即总线空闲)时,触发一次中断,通知CPU“一帧数据已经接收完毕”。

  • 中断服务程序 (ISR):在中断中处理接收到的数据,并启动下一次DMA发送任务。

实践步骤:

  1. 硬件不变:继续使用你的STM32开发板和USB转TTL模块。

  2. 配置DMA接收:配置一个新的DMA通道,数据方向设置为“从外设(串口数据寄存器)到内存”。建议使用循环模式(Circular Mode),这样DMA接收完缓冲区末尾后会自动回到开头,不会停止。

  3. 使能串口空闲中断:在串口初始化代码中,除了使能DMA接收,还要使能 IDLE 中断。

  4. 编写中断服务函数

    • IDLE 中断函数中,首先清除中断标志位。

    • 通过计算DMA剩余传输长度,得知本次接收了多少个字节的数据。

    • 调用项目一中的 My_UART_Transmit_DMA 函数,将刚刚接收到的数据长度作为参数传进去,实现“回显”。

  5. 测试:在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接收不定长数据,并且通过串口进行回显。

常见做法:

  1. 配置 DMA 连续接收(循环模式),不必每次都手动启动 DMA。

  2. 当数据发完且总线空闲时,IDLE 中断触发。

  3. 在 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);


完整代码到文章顶部下载获取,参阅本文章时对应着顶部的完整程序,效果更佳!


网站公告

今日签到

点亮在社区的每一天
去签到