教你手写DMA传输数据(看完这篇你就会手动写啦,保姆级讲解)---- 2020.3.31

关于DMA与串口原理方面的文章:

嵌入式stm32 复习(工作用)— USART(串口)通信原理知识 2020.3.23
添加链接描述

教你手写串口收发数据(看完这篇你就会手动写啦,保姆级讲解)---- 2020.3.28
添加链接描述

嵌入式stm32 复习(工作用)— DMA控制器知识 2020.3.30
添加链接描述

先上完整DMA串口收发部分代码!!!

#define CR1_OVER8_Set             ((u16)0x8000)  /* USART OVER8 mode Enable Mask */

void USART_DMA_Init(void) {

	//1.开启时钟
	RCC->AHBENR |= 1 << 0;//DMA1时钟

	//2.配置DMA
	DMA1_Channel5->CCR = 0;//先清零
	DMA1_Channel5->CCR |= 1 << 7;//执行存储器地址增量操作
	DMA1_Channel5->CCR |= 1 << 12;//中等优先级

	//设置DMA数据接收大小
	DMA1_Channel5->CNDTR = USART1_REC_MAX - 1;//允许最大接收数据缓存大小,注意,必须在DMA停止时设置
	//设置外设数据地址
	DMA1_Channel5->CPAR = (u32) &USART1->DR;
	//设置内存缓存器的地址
	DMA1_Channel5->CMAR = (u32) USART1_REC;

	//3.使能DMA
	DMA1_Channel5->CCR |= 1 << 0;

}

u16 USART_GetBound(USART_TypeDef* USARTx, int bound) {
	uint32_t integerdivider = 0x00, apbclock = 0x00;
	uint32_t tmpreg = 0x00;
	uint32_t fractionaldivider = 0x00;

	apbclock = 72000000; //如是USART1是72M,否则是36M

	/* Determine the integer part */
	if ((USARTx->CR1 & CR1_OVER8_Set) != 0) {
		/* Integer part computing in case Oversampling mode is 8 Samples */
		integerdivider = ((25 * apbclock) / (2 * bound));
	} else /* if ((USARTx->CR1 & CR1_OVER8_Set) == 0) */
	{
		/* Integer part computing in case Oversampling mode is 16 Samples */
		integerdivider = ((25 * apbclock) / (4 * bound));
	}
	tmpreg = (integerdivider / 100) << 4;

	/* Determine the fractional part */
	fractionaldivider = integerdivider - (100 * (tmpreg >> 4));

	/* Implement the fractional part in the register */
	if ((USARTx->CR1 & CR1_OVER8_Set) != 0) {
		tmpreg |= ((((fractionaldivider * 8) + 50) / 100)) & ((uint8_t) 0x07);
	} else /* if ((USARTx->CR1 & CR1_OVER8_Set) == 0) */
	{
		tmpreg |= ((((fractionaldivider * 16) + 50) / 100)) & ((uint8_t) 0x0F);
	}
	return tmpreg;

}

void USART1_Init(int bound) {

	//1.使能时钟
	RCC->APB2ENR |= 1 << 2;//GPIOA 时钟使能
	RCC->APB2ENR |= 1 << 14;//USART1 时钟使能

	//2.初始化GPIO
	GPIOA->CRH &= ~(0x0F << 4);
	GPIOA->CRH |= 0x0B << 4;//设置GPIOA.9 -> 50MHz,复用推免

	GPIOA->CRH &= ~(0x0F << 8);
	GPIOA->CRH |= 0x04 << 8;//设置GPIOA.10 -> 浮空输入模式

	//3.初始化USART1
	//3.1 设置波特率
	USART1->BRR = USART_GetBound(USART1, bound);// 0x1D4 << 4 | 0x4B;//设置波特率9600
	//3.2 设置校验位
	USART1->CR1 &= ~(1 << 10);//不使用校验位
	//3.3 数据位
	USART1->CR1 &= ~(1 << 12);//8位长度
	//3.4 停止位
	USART1->CR2 &= ~(0x02 << 12);//1个停止位
	//4.使能
	USART1->CR1 |= 1 << 3;//使能TX
	USART1->CR1 |= 1 << 2;//使能RX
	USART1->CR1 |= 1 << 13;//使能USART1

	USART1->CR3 |= 1 << 6;//开启USART1的RX DMA

	//配置数据接收时候,需要用到中断
	//5.配置NVIC
	//5.1 先分组
	NVIC_SetPriorityGrouping(5);
	NVIC_EnableIRQ(USART1_IRQn);//使能USART1的全局中断
	NVIC_SetPriority(USART1_IRQn, 10);//设置USART1的中断优先级是10

	//6 使能接受数据中断寄存器
//	USART1->CR1 |= 1 << 5;	,如果使用DMA方式,需要开启数据接收中断
	USART1->CR1 |= 1 << 4;//开启IDLE中断

	USART_DMA_Init();
}

u8 USART1_REC[USART1_REC_MAX] = { 0 };
//USART1_STA[15] 标识是否接收完成状态位,1:标识接收完成;0:未接收完成;
//USART1_STA[14] 标识是否为第一个数据,1:非第一个数据;0:表示第一个数据;
//USART1_STA[13:0] 标识有效的数据接收长度
u16 USART1_STA = 0;

void USART1_IRQHandler(void) {

	if (USART1->SR & 0x10) {	//总线空闲
		USART1_STA = (USART1_REC_MAX - 1) - DMA1_Channel5->CNDTR;//实际接收的数据大小

		USART1_REC[USART1_STA & 0x3FFF] = '\0';//保证有效字符串

		USART1_STA |= 0x8000;//标识接收成功

		USART1->DR;//清空状态

		DMA1_Channel5->CCR &= ~(1 << 0);//先关闭DMA
		DMA1_Channel5->CNDTR = USART1_REC_MAX - 1;//还原允许的数据缓冲区的大小
		DMA1_Channel5->CCR |= (1 << 0);//启动DMA
	}

}

void USART1_SendData(u8 *data, u8 len) {
	u8 i = 0;

	for (i = 0; i < len; i++) {
		if (*(data + i) == '\0')	//空白符号无需发送
			return;
		//判断是否允许发送数据
		while ((USART1->SR & 0x40) == 0)
			;
		USART1->DR = *(data + i);	//等效于下面的函数
//		USART_SendData(USART1, *(data + i));
	}
}

int fputc(int ch, FILE* f) {
	while ((USART1->SR & 0x40) == 0)
		;
	USART1->DR = (u8) ch;	//等效于下面的函数
	return ch;
}

好!按照老样子,接下来开始详细讲解每行代码的用处,以及为什么这样写!

串口DMA初始化部分

//1.开启时钟
RCC->AHBENR |= 1 << 0;//DMA1时钟

//
在这里插入图片描述//由上图可知DMA是在AHB总线下的,所以需要开启AHB时钟,并且我们这里使用的是DMA1。
//
在这里插入图片描述//
在这里插入图片描述

//2.配置DMA
DMA1_Channel5->CCR = 0;//先清零
DMA1_Channel5->CCR |= 1 << 7;//执行存储器地址增量操作
DMA1_Channel5->CCR |= 1 << 12;//中等优先级

//
在这里插入图片描述//咦?这里为啥是反的?大家凑合看吧~
在这里插入图片描述
//
在这里插入图片描述//因为这里使用过的就是USART1的读取,因为我们今天使用的过程就是通过RX读取外设上的数据,然后再通过DMA通道5来发送数据到存储器中。

//在这里插入图片描述
在这里插入图片描述//所以这里我们先清零,然后才能将我们需要写入的数据放到该寄存器中。其实另一方面也相当于设置成我们想要的配置了,大家往下看就知道了。
//
在这里插入图片描述//因为我们这里是外设到存储器,所以该位为0。

//在这里插入图片描述//串口传输的长度是8位,所以11:10位也为0。
//
在这里插入图片描述//这里我们外设地址不加一,所以该位为0。
//
在这里插入图片描述
//这里我们不执行循环操作,因为如果是循环操作的话,那么DMA写数据到存储器中,是不会管存储器缓存空间是否溢出,所以我们等等需要设置的就是得自己认为关闭掉DMA,然后再清空缓存区里面的数据,最后再开启,相当于是人为进行循环了。

//在这里插入图片描述//因为我们这里就是从外设读数据到存储器中,所以该位为0。

//由上图我们可以得知,存储器地址是加一的,外设地址是不加一的。
//
在这里插入图片描述//在这里插入图片描述//这里我们设置为中等优先级,如果我们在用DMA传输的数据比较重要的话,在这里可以改成高或者最高优先级。

//设置DMA数据接收大小
DMA1_Channel5->CNDTR = USART1_REC_MAX - 1;//允许最大接收数据缓存大小,
//注意,必须在DMA停止时设置

//
在这里插入图片描述//在前几篇关于串口收发数据的文章中我们可以得知USART1_REC_MAX是可以接收的最大数据长度,但是由于最后我们要表示该数据为有效数据,往往在最后加一个“/0”停止符。所以我们这里还是减一,留出一个空间来存放“/0”。

扫描二维码关注公众号,回复: 10344973 查看本文章
//设置外设数据地址
DMA1_Channel5->CPAR = (u32) &USART1->DR;

//在这里插入图片描述//这里就是将串口的数据赋值给这个寄存器,然后DMA再进行传输。

//设置内存缓存器的地址
DMA1_Channel5->CMAR = (u32) USART1_REC;

//
在这里插入图片描述//最后存储器中的缓冲区为USART1_REC。

//3.使能DMA
DMA1_Channel5->CCR |= 1 << 0;

//在这里插入图片描述//这里就是开启DMA对应的通道,也相当于是使能。

串口DMA读取数据中断服务函数部分

if (USART1->SR & 0x10) {	//总线空闲

//
在这里插入图片描述//当检测到串口总线空闲,那么就代表数据传输完成。
//
在这里插入图片描述

USART1_STA = (USART1_REC_MAX - 1) - DMA1_Channel5->CNDTR;//实际接收的数据大小

//
在这里插入图片描述//由上图我们可以得知,该寄存器代表的是现在的缓存区中还有多少个空余的缓存区。
那么我们想知道具体到底传输了多少个数据,那就可以用我们自己设置的最大可接受数据长度减掉这个寄存器值。

USART1_REC[USART1_STA & 0x3FFF] = '\0';//保证有效字符串

//由于上一篇文章中也讲了,所以这里就不再赘述了。

USART1_STA |= 0x8000;//标识接收成功
USART1->DR;//清空状态

//同理不再赘述。

DMA1_Channel5->CCR &= ~(1 << 0);//先关闭DMA
DMA1_Channel5->CNDTR = USART1_REC_MAX - 1;//还原允许的数据缓冲区的大小
DMA1_Channel5->CCR |= (1 << 0);//启动DMA

//在前面我们也说过我们设置的不是循环模式,所以如果想要源源不断的传输数据并且保证最终的存储区不溢出,那么我们就需要先关闭DMA,然后还原允许的数据缓冲区的大小,最后再启动DMA即可。

结束语

个人认为大家如果细心看完这篇文章,并且结合上一篇文章一起看(在文章的刚开始会将前几篇关于USART和DMA原理部分的文章链接发出来),我相信大家会彻底掌握DMA了!!!如果觉得这篇文章还不错的话,记得点赞 ,支持下!!!

以后我会继续推出关于嵌入式(stm32)的协议方面的讲解,下一讲会推出PWM部分的文章!敬请期待!!!

**我先休息去了~~╭(╯^╰)╮

发布了35 篇原创文章 · 获赞 66 · 访问量 7393

猜你喜欢

转载自blog.csdn.net/qq_40544107/article/details/105186669