DMA外设详解

一.DMA简介

直接存储器存取(DMA)(Direct Memory Access)也是一个挂载在AHB总线上的外设,用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。两个DMA控制器有12个通道(DMA1有7个通道,DMA2(只存在于大容量和互联网产品中)有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权

注:
大容量产品:是指闪存存储器容量在256K至512K字节之间的STM32F101xx和STM32F103xx微控制器。
互联型产品:是指STM32F105xx和STM32F107xx微控制器。

二.DMA 功能框图(重点)

在这里插入图片描述

  • DMA 请求

每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置DMA 具有 12 个独立可编程的通道,其中 DMA1 有 7 个通道,DMA2 有 5 个通道,每个通道对应不同的外设的 DMA 请求。虽然每个通道可以接收多个外设的请求,但是同一时间只能有一个有效,不能同时接收多个。

如果外设要想通过 DMA 来传输数据,必须先给 DMA 控制器发送 DMA 请求,DMA收到请求信号之后,控制器会给外设一个应答信号,当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。

不同的DMA控制器通道对应这不同外设的DMA请求:
在这里插入图片描述
在这里插入图片描述
我这里找了部分外设发送DMA请求的函数,想开启哪个外设的DMA请求直接去相应的外设头文件里面找就行了
在这里插入图片描述

  • 仲裁器
    仲裁器根据通道请求的优先级来启动外设/存储器的访问。
    优先权管理分2个阶段:

    ● 软件:每个通道的优先权可以在DMA_CCRx寄存器中设置,有4个等级:
    ─ 最高优先级
    ─ 高优先级
    ─ 中等优先级
    ─ 低优先级

    ● 硬件:如果2个请求有相同的软件优先级,则较低编号的通道比较高编号的通道有较高的优先权。举个例子,通道2优先于通道4

在大容量产品和互联产品中,DMA1控制器拥有高于DMA2控制器的优先级

在这里插入图片描述
先比较软件优先级,再比较通道编号

在这里插入图片描述

DMA控制器和Cortex™-M3核心共享系统数据总线,执行直接存储器数据传输。 当CPU和DMA同时访问相同的目标(RAM或外设)时,DMA请求会暂停CPU访问系统总线达若干个周期,总线仲裁器执行循环调度,以保证CPU至少可以得到一半的系统总线(存储器或外设)带宽。

根据上面的描述我们可以得出一个结论,DMA可以独立CPU之外进行数据的传输 (DMA可以通过数据总线访问存储器和外设的数据寄存器)

次过程不需要CPU的参与,CPU可以转而做其他事情,说白了DAM就是帮CPU打工的.

DMA 传输数据的方向有三个:从外设到存储器,从存储器到外设,从存储器到存储器。具体的方向 DMA_CCR 位 4 DIR 配置:0 表示从外设到存储器,1 表示从存储器到外设。这里面涉及到的外设地址由 DMA_CPAR 配置,存储器地址由 DMA_CMAR配置。

有人就会疑惑了,存储器有哪些呢,什么数据存放在存储器上,闪存FLASH、SRAM、就是所谓的存储器。

内部FLASH
简单介绍在flash存储内容:我们写好的程序编译之后都是一条条指令(二进制代码),存放在 FLASH 中,我们常量或常变量C 语言中的 const 关键字修饰也存放在FLASH.

内部SRAM
就是我们常说的电脑内存条,程序函数内部的局部变量和全局变量,堆(malloc分配)栈(局部变量)等的开销都是基于内部的SRAM。内核通过 DCode 总线来访问它

当然一个数组也是一个变量,当然是存放在SRAM这个存储器上咯,也就是说DMA访问这个数组也就是访问存储池器。

外设的话,一般外设都有一个数据寄存器,来暂存数据。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 外设到存储器

当我们使用从外设到存储器传输时,以 ADC 采集为例。DMA 外设寄存器的地址对应的就是 ADC 数据寄存器的地址,DMA 存储器的地址就是我们自定义的变量(用来接收存储 AD 采集的数据)的地址。方向我们设置外设为源地址。

  • 存储器到外设

当我们使用从存储器到外设传输时,以串口向电脑端发送数据为例。DMA 外设寄存器的地址对应的就是串口数据寄存器的地址,DMA 存储器的地址就是我们自定义的变量(一般是一个数组,相当于一个缓冲区,用来存储通过串口发送到电脑的数据)的地址。方向我们设置外设为目标地址。

  • 存储器到存储器

当我们使用从存储器到存储器传输时,以内部 FLASH 向内部 SRAM 复制数据为例。DMA 外设寄存器的地址对应的就是内部 FLASH(我们这里把内部 FALSH 当作一个外设来看)的地址,DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储来自内部 FLASH 的数据)的地址。方向我们设置外设(即内部 FLASH)为源地址。跟上面两个不一样的是,这里需要把 DMA_CCR 位 14:MEM2MEM:存储器到存储器模式配置为 1,启动 M2M 模式。

其实上面是我复制粘贴的,其实光看文字肯定理解不够深刻,上面提到的我后面有对应的三个实验,如果想深入理解一定要看到最后

通道配置过程
下面是配置DMA通道x的过程(x代表通道号):

  1. 在DMA_CPARx寄存器中设置外设寄存器的地址。发生外设数据传输请求时,这个地址将是数据传输的源或目标。

  2. 在DMA_CMARx寄存器中设置数据存储器的地址。发生外设数据传输请求时,传输的数据将从这个地址读出或写入这个地址。

  3. 在DMA_CNDTRx寄存器中设置要传输的数据量。在每个数据传输后,这个数值递减。
    在这里插入图片描述

  4. 在DMA_CCRx寄存器的PL[1:0]位中设置通道的优先级。
    在这里插入图片描述

  5. 在DMA_CCRx寄存器中设置数据传输的方向、循环模式、外设和存储器的增量模式、外设和存储器的数据宽度、传输一半产生中断或传输完成产生中断。
    传输方向:
    在这里插入图片描述
    循环模式:
    在这里插入图片描述
    外设和存储器的增量模式:
    在这里插入图片描述
    如果还不理解的话得先补习一下指针的知识—>《指针从入门到熟练掌握》
    外设和存储器的数据宽度:
    要想数据传输正确,源和目标地址存储的数据宽度还必须一致,串口数据寄存器是 8位的,所以我们定义的要发送的数据也必须是 8 位
    在这里插入图片描述
    如果两边的设置的数据宽度不一样会怎么样呢。
    在这里插入图片描述

  6. 设置DMA_CCRx寄存器的ENABLE位,启动该通道一旦启动了DMA通道,它即可响应连到该通道上的外设的DMA请求。

当传输一半的数据后,半传输标志(HTIF)被置1,当设置了允许半传输中断位(HTIE)时,将产生一个中断请求。在数据传输结束后,传输完成标志(TCIF)被置1,当设置了允许传输完成中断位(TCIE)时,将产生一个中断请求
在这里插入图片描述

当通道配置为非循环模式时,传输结束后(即传输计数变为0)将不再产生DMA操作。要开始新的DMA传输,需要在关闭DMA通道的情况下,在DMA_CNDTRx寄存器中重新写入传输数目。

要开始新的DMA传输必须先关闭DMA通道,在DMA_CNDTRx寄存器中重新写入传输数目,再打开DAM通道,这点非常重要。

中断
在这里插入图片描述

三.DMA初始化结构体

在这里插入图片描述
上面每个结构体成员变量要配置哪个寄存器,前面已经详述。

  1. DMA_PeripheralBaseAddr:外设地址,一般设置为外设的数据寄存器地址,如果是存储器到存储器模式则设置为其中一个存储器地址。

  2. DMA_Memory0BaseAddr:存储器(FLASH、SRAM)地址,一般设置为我们自定义存储区(一般为一个数组)的首地址(数组名)。

  3. DMA_DIR:传输方向选择,可选外设到存储器、存储器到外设。这里并没有存储器到存储器的方向选择,当使用存储器到存储器时,只需要把其中一个存储器当作外设使用即可。

  4. DMA_BufferSize:设定待传输数据数量,数量范围为0~65535

  5. DMA_PeripheralInc:如果配置为 DMA_PeripheralInc_Enable,使能外设地址自动递增功能,一般外设都是只有一个数据寄存器,所以一般不会使能该位。

  6. DMA_MemoryInc:如果配置为 DMA_MemoryInc_Enable,使能存储器地址自动递增功能,我们自定义的存储区一般都是存放多个数据的,所以要使能存储器地址自动递增功能。

  7. DMA_PeripheralDataSize:外设数据宽度,可选字节(8 位)、半字(16 位)和字(32位),

  8. DMA_MemoryDataSize:存储器数据宽度,可选字节(8 位)、半字(16 位)和字(32位),当外设和存储器之间传数据时,两边的数据宽度应该设置为一致大小。

  9. DMA_Mode:DMA 传输模式选择,可选一次传输或者循环传输,我们的 ADC 采集是持续循环进行的,所以使用循环传输模式。

  10. DMA_Priority:软件设置道的优先级,有 4 个可选优先级分别为非常高、高、中和低,DMA 通道优先级只有在多个 DMA 通道同时使用时才有意义,如果是单个通道,优先级可以随便设置。

  11. DMA_M2M :存储器到存储器模式 .

接下来就进入实战重点,深入理解原理,一共对应三个实验,存储器到外设、外设到存储器、存储器到存储器。

四.存储器到存储器

实验目的

我们先定义(const)一个静态的源数据,存放在内部 FLASH,然后使用 DMA 传输把源数据拷贝到目标地址上(内部 SRAM),最后对比源数据和目标地址的数据,看看是否传输准确,程序开始先亮一会红灯,等待数据传输完成然后对比数据,若数据传输正确,则亮绿灯,否则又亮红灯。

内部FLASH
简单介绍在flash存储内容:我们写好的程序编译之后都是一条条指令(二进制代码),存放在 FLASH 中,我们常量或常变量C 语言中的 const 关键字修饰也存放在FLASH.

内部SRAM
就是我们常说的电脑内存条,程序函数内部的局部变量和全局变量,堆(malloc分配)栈(局部变量)等的开销都是基于内部的SRAM。内核通过 DCode 总线来访问它

实验原理

在这里插入图片描述

上代码:

dma_mtm.h

#ifndef DMA_MTM_H
#define DMA_MTM_H
#include "stm32f10x.h"

// 当使用存储器到存储器模式时候,通道可以随便选,没有硬性的规定
#define DMA_CHANNEL     DMA1_Channel6
#define DMA_CLOCK       RCC_AHBPeriph_DMA1

// 传输完成标志
#define DMA_FLAG_TC     DMA1_FLAG_TC6

// 要发送的数据大小
#define BUFFER_SIZE     32

extern  const uint32_t aSRC_Const_Buffer[BUFFER_SIZE];
extern  uint32_t aDST_Buffer[BUFFER_SIZE];

void MtM_DMA_Config(void);
uint8_t  Buffer_cmp(const uint32_t * pBuffer,const uint32_t  *pBuffer1,uint32_t Bufferlength);
#endif /* DMA_MTM_H */



dma_mtm.c

#include "dma_mtm.h"



/* 定义aSRC_Const_Buffer数组作为DMA传输数据源
 * const关键字将aSRC_Const_Buffer数组变量定义为常量类型
 * 表示数据存储在内部的FLASH中
 */
const uint32_t aSRC_Const_Buffer[BUFFER_SIZE]= {
    
    
                                    0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
                                    0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
                                    0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
                                    0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
                                    0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
                                    0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
                                    0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
                                    0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80};

/* 定义DMA传输目标存储器
 * 存储在内部的SRAM中																		
 */
uint32_t aDST_Buffer[BUFFER_SIZE];
																																		
void MtM_DMA_Config(void)
{
    
    
	    DMA_InitTypeDef  DMA_InitStructure;
	    //开启DMA时钟(注意:DMA挂载在AHB总线上)
	    RCC_AHBPeriphClockCmd(DMA_CLOCK, ENABLE);
		// 源数据地址
        DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)aSRC_Const_Buffer;
		// 目标地址
		DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)aDST_Buffer;
		// 方向:外设到存储器(这里的外设是内部的FLASH)	
		DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
		// 传输大小	
		DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
		// 外设(内部的FLASH)地址递增	    
		DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
		// 内存地址递增
		DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
		// 外设数据单位	
		DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
		// 内存数据单位
		DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;	 
		// DMA模式,一次或者循环模式
		DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
		//DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;  
		// 优先级:高	
		DMA_InitStructure.DMA_Priority = DMA_Priority_High;
		// 使能内存到内存的传输
		DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;
		// 配置DMA通道		   
		DMA_Init(DMA_CHANNEL, &DMA_InitStructure);
        //清除DMA数据流传输完成标志位,为了后面检测数据是否完成
        DMA_ClearFlag(DMA_FLAG_TC);
		// 使能DMA
		DMA_Cmd(DMA_CHANNEL,ENABLE);
}

//数组比较函数
uint8_t  Buffer_cmp(const uint32_t * pBuffer,const uint32_t  *pBuffer1,uint32_t Bufferlength)
{
    
    
	while(Bufferlength--)
	{
    
    
		if(*pBuffer !=*pBuffer1)
			return 0;
		else
			pBuffer++;
		  pBuffer1++;
	}
	  return 1;
}
	
																		
					

main.c

#include "stm32f10x.h"
#include "led.h"
#include "dma_mtm.h"
#define SOFT_DELAY Delay(0x0FFFFF);

void Delay(__IO u32 nCount); 
int main(void)
{
    
    	
	/* LED 端口初始化 */
	LED_GPIO_Config();	
	//先亮一会红灯
      LED_G(OFF);
      LED_R(NO);
	    SOFT_DELAY;
	//DAM初始化
	MtM_DMA_Config();
	//等待DAM数据全部传输完成
	while ( DMA_GetFlagStatus( DMA_FLAG_TC)==RESET);
	 /* 比较源数据与传输后数据 */
	if(Buffer_cmp( aSRC_Const_Buffer,aDST_Buffer,BUFFER_SIZE))
	{
    
    
	  LED_G(NO);
      LED_R(OFF);
	}
	else
	{
    
    
	  LED_G(OFF);
      LED_R(NO);
	}
}

void Delay(__IO uint32_t nCount)	 //简单的延时函数
{
    
    
	for(; nCount != 0; nCount--);
}

这里要注意的一个点DMA是挂载在AHB总线,开启DAM时钟。

 RCC_AHBPeriphClockCmd(DMA_CLOCK, ENABLE);

在这里插入图片描述

实验效果

请添加图片描述

五.存储器到外设

在看该实验时若串口还不是很熟悉,请看STM32串口通信详解

实验目的

我们先定义一个数组变量,存于 SRAM 中,然后通过 DMA 的方式传输到串口的数据寄存器,然后通过串口把这些数据发送到电脑的上位机显示出来,传输的同时CPU表示很闲所以边传输的时候边让CPU点灯。

实验原理

利用DMA发送
使用DMA进行发送,可以通过设置USART_CR3寄存器上的DMAT位激活。当TXE位被置为’1’时,DMA就从指定的SRAM区传送数据到USART_DR寄存器

  1. 在DMA控制寄存器上将USART_DR寄存器地址配置成DMA传输的目的地址。在每个TXE事件后,数据将被传送到这个地址。
  2. 在DMA控制寄存器上将存储器地址配置成DMA传输的源地址。在每个TXE事件后,将从此存储器区读出数据并传送到USART_DR寄存器。

总结:DAM传输数据时到USATR时可以实现连续通信,原本如果我们要实现连续通信,传输一个一帧数据时我们需要等待TXE位置1才能,写入下一个数据,不然数据寄存器的数据会被覆盖,但DMA传输数据到串口数据寄存器时每传输一帧数据,DMA会自己等待TXE置1(数据寄存器为空,数据已经被转移到数据移位寄存器中),再传输下一帧数据,DMA与串口配合的天衣无缝,实现连续通信。

要想理解原理,下面两张图一定一定要看懂

在这里插入图片描述

在这里插入图片描述

这里解释一下,既然是存储器到外设,为啥串口发送的是串口发送(TX)的请求呢,DMA只是一个帮忙传输数据到串口与CPU作用一样,其实我们真正的目的是利用串口向另外一个设备(电脑)发送数据,所以串口向DMA发送串口发送请求。

在这里插入图片描述
在这里插入图片描述
上代码:
dma_mtp.h

#ifndef __DMA_MTP_H
#define __DMA_MTP_H

#include "stm32f10x.h"

// 要发送的数据大小
#define SENDBUFFER_SIZE                   32

extern uint8_t aSRC_Buffer[SENDBUFFER_SIZE];

#define  DEBUG_USARTx                   USART1
#define  DEBUG_USART_CLK                RCC_APB2Periph_USART1
#define  DEBUG_USART_APBxClkCmd         RCC_APB2PeriphClockCmd
#define  DEBUG_USART_BAUDRATE           115200

// USART GPIO 引脚宏定义
#define  DEBUG_USART_GPIO_CLK           (RCC_APB2Periph_GPIOA)
#define  DEBUG_USART_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd
    
#define  DEBUG_USART_TX_GPIO_PORT       GPIOA   
#define  DEBUG_USART_TX_GPIO_PIN        GPIO_Pin_9
#define  DEBUG_USART_RX_GPIO_PORT       GPIOA
#define  DEBUG_USART_RX_GPIO_PIN        GPIO_Pin_10

#define DMA1_MtP_FLAG_TC                 DMA1_FLAG_TC4
#define DMA1_MtP_CLK                     RCC_AHBPeriph_DMA1
#define DMA1_MtP_Channel                 DMA1_Channel4
#define USART_DR_BASE                    (USART1_BASE+0X04)
void DMA_MtoP_Config(void);
uint16_t Buffer_cmp(const uint32_t * aSRC_Buffer,uint32_t * aDST_Buffer,uint16_t length);
void Usart_GPIO_Config(void);
#endif /*__DMA_MTP_H */


dma_mtp.c

#include "dma_Mtp.h"


uint8_t aSRC_Buffer[SENDBUFFER_SIZE];

void Usart_GPIO_Config(void)
{
    
    
	GPIO_InitTypeDef    GPIO_InitStuctrue;
	USART_InitTypeDef   USART_InitStuctrue;
	//开启GPIO外设时钟
	DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK,ENABLE);
	//开启USART外设时钟
	DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK,ENABLE);
	
	GPIO_InitStuctrue.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
	GPIO_InitStuctrue.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStuctrue.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(DEBUG_USART_TX_GPIO_PORT,&GPIO_InitStuctrue);
	
	GPIO_InitStuctrue.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
	GPIO_InitStuctrue.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(DEBUG_USART_RX_GPIO_PORT,&GPIO_InitStuctrue);

	USART_InitStuctrue.USART_BaudRate = DEBUG_USART_BAUDRATE;
	USART_InitStuctrue.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	USART_InitStuctrue.USART_Parity = USART_Parity_No;
	USART_InitStuctrue.USART_StopBits = USART_StopBits_1;
	USART_InitStuctrue.USART_WordLength = USART_WordLength_8b;
 	USART_InitStuctrue.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_Init(DEBUG_USARTx,&USART_InitStuctrue);
	
	USART_Cmd(DEBUG_USARTx,ENABLE);

}
																		
void DMA_MtoP_Config(void)
{
    
    

	DMA_InitTypeDef  DMA_InitStructure;
	RCC_AHBPeriphClockCmd(DMA1_MtP_CLK, ENABLE);

	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART_DR_BASE;
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)aSRC_Buffer;
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;

	DMA_InitStructure.DMA_BufferSize = SENDBUFFER_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的循环传输
	DMA_InitStructure.DMA_Mode =DMA_Mode_Circular;
	DMA_InitStructure.DMA_Priority =DMA_Priority_High;
	DMA_InitStructure.DMA_M2M =DMA_M2M_Disable;
	DMA_Init(DMA1_MtP_Channel,&DMA_InitStructure);
	
	DMA_ClearFlag(DMA1_MtP_FLAG_TC);
	DMA_Cmd(DMA1_MtP_Channel,ENABLE);
	
}	

main.c

#include "stm32f10x.h"
#include "led.h"
#include "delay.h"
#include "dma_Mtp.h"
#define SOFT_DELAY Delay(0x0FFFFF);

void Delay(__IO u32 nCount); 


int main(void)
{
    
    	
  uint32_t i =0;
  delay_init();
	/* LED 端口初始化 */
	LED_GPIO_Config();
  //串口初始化	
  Usart_GPIO_Config();
	//初始化数组
	for(i=0;i<SENDBUFFER_SIZE;i++)
	{
    
    
		aSRC_Buffer[i]='k';
	}
	//DMA配置
	DMA_MtoP_Config();
	//向DMA发送串口TX请求
	USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
	//CPU同时点灯
	while(1)
	{
    
    
		LED_G(NO);
		LED_R(OFF);
		delay_ms(300);
		LED_R(NO);
		LED_G(OFF);
		delay_ms(300);
	}
}

void Delay(__IO uint32_t nCount)	 //简单的延时函数
{
    
    
	for(; nCount != 0; nCount--);
}

原谅我不想写注释,不过这里跟上面存储器到存储器差不多,不写注释也看的懂的。

注意:这里串口DMA发送请求一定要是DAM1通道4
在这里插入图片描述

实验效果

请添加图片描述

六.外设到存储器(重点理解)

实验目的

我们先定义一个数组变量,存于 SRAM 中,用来存放串口接收的数据,实验流程就是,电脑向串口接收数据寄存器发送数据(多帧),然后通过DMA从串口接收数据寄存器读取数据传输到存储在SRAM的数组中,然后通过串口又将数组接收到的数据发送回电脑端。

实验原理

利用DMA接收
可以通过设置USART_CR3寄存器的DMAR位激活使用DMA进行接收,每次接收到一个字节,DMA控制器就就把数据从USART_DR寄存器传送到指定的SRAM区(参考DMA相关说明)。

  1. 通过DMA控制寄存器把USART_DR寄存器地址配置成传输的源地址。在每个RXNE事件后,将从此地址读出数据并传输到存储器
  2. 通过DMA控制寄存器把存储器地址配置成传输的目的地址。在每个RXNE事件后,数据将从USART_DR传输到此存储器区

总结:DAM接收数据时RXNE位置1(接收数据寄存器不为空)时,来向读取接收数据寄存器读取数据传输到存储器SRAM(数组中)。

在这里插入图片描述

在这里插入图片描述

我们还有一个很大的问题没有解决,我们肯定要实现这样一个效果,电脑端发送几个数据帧(8位),串口就返回电脑端几个数据帧(8位),但有一个问题,DMA要配置数据帧传输数量,如果我们采用DMA的传输完成的中断,则我们电脑端发送的数据一定得等于DMA配置的数据帧传输数量,这样一来就达不到我们发送几个数据帧就返回几个数据帧的目的了,因为必须接收到DMA配置数据帧传输数量数据帧,才会触发中断。

所以我们换一个思路:
利用串口的空闲中断

在这里插入图片描述

总结:

清除串口空闲中断的方法为:

USART1->DR
USART1->SR

分别读两个寄存器

触发串口空闲中断的方式:
接收数据之后(RXNE位置1),若出现一个字节的高电平(空闲)状态,在串口不发送也不接受的情况下,就是处于空闲状态,但是要注意的是如果你发送了数据然后不发送了,然后就会触发空闲中断,但是记住后面如果你没有发数据了不会再触发空闲中断了,只有接收到下一组数据之后,才会检测空闲中断,若此时总线空闲则触发下一次空闲中断。

注意:
其实发送的两个数据帧(一个字节)之间间隔非常短,所以在两个数据帧之间不叫空闲。空闲中断是检测到有数据被接收后,总线上在一个字节的时间内没有再接收到数据的时候发生的,也就是数据(多个数据)全部发送完毕,才会检测到空闲中断。

直接上代码讲解:

dma_usart_rx.h

#ifndef __DMA_USART_RX_H
#define __DMA_USART_RX_H

#include "stm32f10x.h"


#include <stdio.h>

// 串口工作参数宏定义
#define  DEBUG_USARTx                   USART1
#define  DEBUG_USART_CLK                RCC_APB2Periph_USART1
#define  DEBUG_USART_APBxClkCmd         RCC_APB2PeriphClockCmd
#define  DEBUG_USART_BAUDRATE           115200

// USART GPIO 引脚宏定义
#define  DEBUG_USART_GPIO_CLK           (RCC_APB2Periph_GPIOA)
#define  DEBUG_USART_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd
    
#define  DEBUG_USART_TX_GPIO_PORT       GPIOA   
#define  DEBUG_USART_TX_GPIO_PIN        GPIO_Pin_9
#define  DEBUG_USART_RX_GPIO_PORT       GPIOA
#define  DEBUG_USART_RX_GPIO_PIN        GPIO_Pin_10

#define  DEBUG_USART_IRQ                USART1_IRQn
#define  DEBUG_USART_IRQHandler         USART1_IRQHandler

// 串口对应的DMA请求通道
#define  USART_TX_DMA_CHANNEL     DMA1_Channel5
// 外设寄存器地址
#define  USART_DR_ADDRESS        (USART1_BASE+0x04)
// 一次发送的数据量
#define  RECEIVEBUFF_SIZE            5000

void USART_Config(void);
void DMA_USART_RX_Config(void);
void Usart_SendArray( USART_TypeDef * pUSARTx, uint8_t *array, uint16_t num);
#endif /* __DMA_USART_RX_H */


dma_usart_rx.c

#include "dma_usart_rx.h"

//用来接收数据的数组
uint8_t ReceiveBuff[RECEIVEBUFF_SIZE];

static void NVIC_DMA_USART(void)
{
    
    
	NVIC_InitTypeDef  NVIC_InitStructure;
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	//串口1中断
	NVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
	NVIC_Init(&NVIC_InitStructure);
}

void USART_Config()
{
    
    
	
	USART_InitTypeDef  USART_InitStructure;
	GPIO_InitTypeDef  GPIO_InitStructure;
    //打开串口GPIO的时钟
	DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK,ENABLE);
	// 打开串口外设的时钟
	DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK,ENABLE);
	// 将USART Tx的GPIO配置为推挽复用模式
    GPIO_InitStructure.GPIO_Mode =GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin =DEBUG_USART_TX_GPIO_PIN;
	GPIO_InitStructure.GPIO_Speed =GPIO_Speed_50MHz;
	GPIO_Init(DEBUG_USART_TX_GPIO_PORT,&GPIO_InitStructure);
	// 将USART Rx的GPIO配置为浮空输入模式
	GPIO_InitStructure.GPIO_Mode =GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin =DEBUG_USART_RX_GPIO_PIN;
	GPIO_Init(DEBUG_USART_RX_GPIO_PORT,&GPIO_InitStructure);
	//配置中断
	NVIC_DMA_USART();
	
	// 配置串口的工作参数
	// 配置波特率
	USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
	// 配置 针数据字长
	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(DEBUG_USARTx, &USART_InitStructure);	
	//使能空闲中断
	USART_ITConfig(DEBUG_USARTx,USART_IT_IDLE,ENABLE);
	// 使能串口
	USART_Cmd(DEBUG_USARTx, ENABLE);	  
	
}

void DMA_USART_RX_Config()
{
    
    
   DMA_InitTypeDef DMA_InitStructure;
	 //开启DMA时钟(注意:DMA挂载在AHB总线上)
	 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
		// 设置DMA源地址:串口数据寄存器地址*/
    DMA_InitStructure.DMA_PeripheralBaseAddr = USART_DR_ADDRESS;
		// 内存地址(要传输的变量的指针)
		DMA_InitStructure.DMA_MemoryBaseAddr =(uint32_t)ReceiveBuff;
		// 方向:从外设到内存	
		DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
		// 传输大小	
		DMA_InitStructure.DMA_BufferSize = RECEIVEBUFF_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模式,一次或者循环模式
    //DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
		DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;	
		// 优先级:中	
		DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; 
		// 禁止内存到内存的传输
		DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
		// 配置DMA通道		   
		DMA_Init(USART_TX_DMA_CHANNEL, &DMA_InitStructure);		
		// 使能DMA
		DMA_Cmd (USART_TX_DMA_CHANNEL,ENABLE);
	
}



/*****************  发送一个字节 **********************/
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
{
    
    
	/* 发送一个字节数据到USART */
	USART_SendData(pUSARTx,ch);
		
	/* 等待发送数据寄存器为空 */
	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);	
}

/****************** 发送8位的数组 ************************/
void Usart_SendArray( USART_TypeDef * pUSARTx, uint8_t *array, uint16_t num)
{
    
    
  uint8_t i;
	
	for(i=0; i<num; i++)
  {
    
    
	    /* 发送一个字节数据到USART */
	    Usart_SendByte(pUSARTx,array[i]);	
  
  }
	/* 等待发送完成 */
	while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET);
}

/*****************  发送字符串 **********************/
void Usart_SendString( USART_TypeDef * pUSARTx, char *str)
{
    
    
	unsigned int k=0;
  do 
  {
    
    
      Usart_SendByte( pUSARTx, *(str + k) );
      k++;
  } while(*(str + k)!='\0');
  
  /* 等待发送完成 */
  while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET)
  {
    
    }
}

/*****************  发送一个16位数 **********************/
void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch)
{
    
    
	uint8_t temp_h, temp_l;
	
	/* 取出高八位 */
	temp_h = (ch&0XFF00)>>8;
	/* 取出低八位 */
	temp_l = ch&0XFF;
	
	/* 发送高八位 */
	USART_SendData(pUSARTx,temp_h);	
	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
	
	/* 发送低八位 */
	USART_SendData(pUSARTx,temp_l);	
	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);	
}

///重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch, FILE *f)
{
    
    
		/* 发送一个字节数据到串口 */
		USART_SendData(DEBUG_USARTx, (uint8_t) ch);
		
		/* 等待发送完毕 */
		while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);		
	
		return (ch);
}

///重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{
    
    
		/* 等待串口输入数据 */
		while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == RESET);

		return (int)USART_ReceiveData(DEBUG_USARTx);
}

void USART1_IRQHandler(void)
{
    
    
	 uint16_t  tmp;
	 //检测串口空闲中断是否发生
	if( USART_GetITStatus(DEBUG_USARTx,USART_IT_IDLE)==SET )
	{
    
     
		//关闭DAM传输
		DMA_Cmd(USART_TX_DMA_CHANNEL,DISABLE);
		//获取DMA未接收数据数量
		tmp=DMA_GetCurrDataCounter(USART_TX_DMA_CHANNEL);
	 //向电脑返回数据(接收数据数量 = SENDBUFF_SIZE - 剩余未传输的数据数量)
		Usart_SendArray(DEBUG_USARTx,ReceiveBuff,RECEIVEBUFF_SIZE-tmp);
    printf("\r\n");
		
		//重新设置传输的数据数量
		DMA_SetCurrDataCounter(USART_TX_DMA_CHANNEL,RECEIVEBUFF_SIZE);
		//开启DMA传输
		DMA_Cmd(USART_TX_DMA_CHANNEL,ENABLE);
		
    //清除空闲中断标志位
		USART1->SR;
    USART1->DR;
    //USART_ClearFlag(DEBUG_USARTx,USART_FLAG_IDLE); 
	}

}



main.c

#include "stm32f10x.h"
#include "led.h"
#include "dma_usart_rx.h"

#define SOFT_DELAY Delay(0x0FFFFF);

void Delay(__IO u32 nCount); 


int main(void)
{
    
    	

	// LED 端口初始化
	LED_GPIO_Config();

	//初始化串口
  USART_Config();
	
	//配置DMA模式
  DMA_USART_RX_Config();
  
  //串口向DAM发送RX请求	
	USART_DMACmd(DEBUG_USARTx,USART_DMAReq_Rx, ENABLE);
	LED_R(OFF);
	LED_G(OFF);
	
	printf("\r\nDMA外设到存储器模式,用电脑向开发板串口发送数据,数据会返回到电脑。\r\n");
	while(1)
	{
    
    
    LED_G(NO);
		SOFT_DELAY
		LED_G(OFF);
		SOFT_DELAY
	}

}

void Delay(__IO uint32_t nCount)	 //简单的延时函数
{
    
    
	for(; nCount != 0; nCount--);
}


中断服务函数一定要搞懂,主要是空闲中断的作用。
在这里插入图片描述

实验效果

请添加图片描述

总结

就算是库函数开发,但寄存器的每个位的作用一定一定要搞明白,不然虽然你实现了功能,但原理还是模模糊糊的,不利于打好基础,如果对DMA操作还有疑问的欢迎在评论区讨论!!!

猜你喜欢

转载自blog.csdn.net/k666499436/article/details/124492786
DMA
今日推荐