STM32使用寄存器开发底层驱动学习(USART+DMA)

学习资料下载

在本文学习中会用到如下的文档资料,没有的朋友先下载。

工程模板
Cortex M3权威指南(中文)			:本文简称为《指南》
STM32F10x Cortex -M3编程手册  	:本文简称为《编程手册》
STM32F10x Cortex -M3编程手册(译文)
STM32中文参考手册_V10				:本文简称为《手册》

STM32F10X寄存器学习资料

任务

主要学习使用寄存器配置DMA和中断的配置,分为下面三个实验。

1、使用USART的DMA功能发送数据。
2、使用DMA非中断模式接收USART数据。
3、使用DMA中断模式接收USART数据。

USART的DMA功能发送数据

这里不讲解DMA功能的一些细节,没有了解的可以先了解一下DMA

1、模板使用了正点原子的寄存器工程模板,工程里的delay.csys.cusart.c已经配置好一些基本的功能,所以串口的配置我们不重新配置,但有一些我们需要修改。

2、在《手册》里有USART寄存器描述:控制寄存器 3(USART_CR3)。
在这里插入图片描述
这个寄存器CR3的第6、7位是使能DMA收发功能的。
在这里插入图片描述
在这个表格里面我们可以非常清楚的了解到每一个位的功能。

3、那么在编程的时候我们怎么去写呢?
usart.c文件中添加下面的语句:

	//使能DMA发送和接收
	USART1->CR3|=1<<7;
	USART1->CR3|=1<<6;

usart.c文件中需要做如下改动。添加DMA使能和注释掉串口1接收的中断配置。

在这里插入图片描述

配置外设寄存器的时候,我们只需要打出这个外设的名字加->就会弹出一个下拉框,这些下拉框就是这个外设的寄存器。
在这里插入图片描述
4、那么外设的名字在工程里面具体是怎么样的呢?
stm32f10x.h文件有所有外设的名字,包括后面DMA的各个通道名字。
在这里插入图片描述
5、USART1->CR3|=1<<7;这个语句是什么意思呢?
与、或操作,就是将USART1CR3这个寄存器的第7位置一。
在寄存器配置的时候是会非常多使用|=&=操作的。刚开始使用肯定会用一些逻辑上的混乱,这个不着急,慢慢就会适应的。
这个语句怎么完成寄存器设置的,我就不详细说了,包括后面的逻辑操作也是。

6、USART1的DMA发送功能就打开了,接下来我们开始配置DMA的寄存器。
同样,DMA寄存器的描述也是在《手册》里面,除了NVIC配置的寄存器的描述不再这个这个文件,其他的都是这个手册。所以学习寄存器,这个手册非常重要。

7、我们先创建两个文件dma.cdma.h
dma.h文件

#ifndef __DMA_H
#define __DMA_H

#include "sys.h"	//引用相关头文件

void DMA_UsartTx_Init(void);	//发送初始化
void DMA_UsartRX_Init(u32 Rx_Buff, u16 CNDTR);	//接收初始化

#endif

dma.c文件编写void DMA_UsartTx_Init(void);这个函数初始化DMA发送串口数据配置。
编写外设驱动的步骤一般是:
(1)开启外设时钟
(2)配置外设寄存器
(3)设置中断优先级
好,我们先看第一个。
(1)开启外设时钟
1、STM32有三个时钟线,在三个寄存器中去配置:AHBENRAPB1ENRAPB2ENR。寄存器具体都在那些时钟线下面呢?在《手册》的25页的图1 系统结构可以看到时钟线APB1APB2所包含的外设,那除了这两个时钟线中所提到的外设,其他的外设都是在AHB这个时钟线里面。所以我们想配置DMA的时钟,需要去AHBENR寄存器中配置。
在这里插入图片描述2、《手册》69页找到6.3.6 AHB外设时钟使能寄存器 (RCC_AHBENR)。这里有这个寄存器的描述。
在这里插入图片描述
这个寄存器的位0和位1是DMA1/2时钟使能的,那我们到底是使用那个寄存器呢?
3、在《手册》第147页10.3.7 DMA请求映像有详细的描述。串口1TX、RX的DMA请求分别是在DMA1的通道4和通道5。
在这里插入图片描述
4、那我们现在就知道串口1的DMA请求需要用到的是DMA1,而且是DMA1的通道4和5。

void DMA_UsartTx_Init(void){
    
    

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

}

5、开始DMA寄存器的配置,在《手册》150页10.4.3 DMA通道x配置寄存器(DMA_CCRx)(x = 1…7)
在这里有DMA控制的配置寄存器,我们看看。
在这里插入图片描述
这个也很简单,一位一位看就行,第0位是通道开启,就是说将这位置1后,这个通道就开启工作了。所以像这些通道工作使能和中断使能这种一般我们都是最后才开启。我们要先配置其他的,最后才配置这一位。
第四位之前都是中断、工作使能这些,所以我们从第四位开始看。

从表里的描述也能很清楚的知道每一位的功能,我就简单说一下。

(位4)DIR:数据传输方向 (Data transfer direction)
如果我们想发送一段话,那肯定是先把想发送的内容存放到一个字符串变量里面,然后通过DMA将字符串变量里面的内容传送到串口1的发送缓冲区。在这一个过程中,存储器指的就是字符串变量,串口1的发送缓冲寄存器就是外设地址。明显我们需要从存储器读数据,所以位4需要置1。
	DMA1_Channel4->CCR=0x00000000;	//复位
	DMA1_Channel4->CCR|=1<<4;				//从存储器读	

为什么这个外设名字不是使用DMA1而是使用DMA1_Channel4呢?
我们回头看一下这个寄存器的名字是什么?
在这里插入图片描述
我们再看看DMA的中断标志清除寄存器。
在这里插入图片描述
这应该能看出区别吧,一个是需要具体到第几个通道的,一个是整个DMA1或者DMA2的配置。因为DMA是分好几个通道的,每个通道所管理的外设都不一样,那它们的寄存器肯定也是分开的。
前面我有提到在整个工程文件中各个外设的名字、包括DMA各个通道的名字。
在这里插入图片描述

(位5)CIRC:循环模式 (Circular mode)
这一位根据自己的需求来。我需要的是我每次使能DMA发送数据都是只发送一次。那我选择不执行循环操作。那么这一位置零。前面我们复位的时候我们将所有的位都置零了,那可以不写这位置零的语句了吗?当然可以,只是写上,并且注释好会更好,别人可以很轻松读懂你的程序。
	//如果你前面没有将所有位置零的话,不能使用下面的语句  需要使能这句	DMA1_Channel4->CCR&=~(1<<5);
	DMA1_Channel4->CCR|=0<<5;				//不执行循环操作
(位6)PINC:外设地址增量模式 (Peripheral increment mode)
外设地址肯定是不能增加的呀,串口1的发送缓冲区固定地址是 USART1->DR
DMA1_Channel4->CCR|=0<<6;				//不执行外设地址增量操作
(位7)MINC:存储器地址增量模式 (Memory increment mode)
存储器的地址是要增加的,字符串变量就是一个字符数组嘛,数据存放的地址是逐位增加的。
(位9:8)PSIZE[1:0]:外设数据宽度 (Peripheral size)、(位11:10)MSIZE[1:0]:存储器数据宽度 (Memory size)
字符数据格式是8位的。所以这两个都是相同的。
(位13:12)PL[1:0]:通道优先级 (Channel priority level)
这个是DMA1中各个通道中这个通道执行的优先级,这个根据需求。
(位14)MEM2MEM:存储器到存储器模式 (Memory to memory mode)
我们需要用到外设串口1。
	DMA1_Channel4->CCR|=1<<7;				//执行存储器地址增量操作
	DMA1_Channel4->CCR|=0<<8;				//8位
	DMA1_Channel4->CCR|=0<<10;				//8位
	DMA1_Channel4->CCR|=1<<12;				//优先级中
	DMA1_Channel4->CCR|=0<<14;				//非存储器到存储器模式;

5、在《手册》144页中,有DMA通道配置过程,一共有六步,我们刚刚完成了4、5。第6是最后的,我们看看1、2、3.
在这里插入图片描述

6、在《手册》第152页10.4.4 DMA通道x传输数量寄存器(DMA_CNDTRx)(x = 1…7)
在这里插入图片描述
7、在《手册》152页,有外设地址寄存器和存储器地址存储器的描述。
在这里插入图片描述
8、这个三个寄存器的使用都比较简单,就赋值就行。有个需要注意的就是地址的赋值,等下看代码的格式就行。最后还有一个就是使能通道就行了,因为我这个发送不需要中断,所以位1~3我们都不需要看。

	DMA1_Channel4->CNDTR = 14;			//数据长度14
	DMA1_Channel4->CMAR = (u32)Send_Buff;		//存储器地址
	DMA1_Channel4->CPAR = (u32)&USART1->DR;		//外设地址
	
	DMA1_Channel4->CCR|=1<<0;				//开启DMA1通道4

Send_Buff是我定义用于存放字符串变量。

文件dma.h的完整代码:

#include "dma.h"

u8 Send_Buff[14] = {
    
    "2022年10月21日"};

void DMA_UsartTx_Init(void){
    
    
	
	RCC->AHBENR|=1<<0;						//开启DMA1时钟
	DMA1_Channel4->CCR=0x00000000;			//复位
	DMA1_Channel4->CCR|=1<<4;				//从存储器读
	
	//如果你前面没有将所有为置零的话,不能使用下面的语句  需要使能这句DMA1_Channel4->CCR&=~(1<<5);
	DMA1_Channel4->CCR|=0<<5;				//不执行循环操作
	DMA1_Channel4->CCR|=0<<6;				//不执行外设地址增量操作
	DMA1_Channel4->CCR|=1<<7;				//执行存储器地址增量操作
	DMA1_Channel4->CCR|=0<<8;				//8位
	DMA1_Channel4->CCR|=0<<10;				//8位
	DMA1_Channel4->CCR|=1<<12;				//优先级中
	DMA1_Channel4->CCR|=0<<14;				//非存储器到存储器模式
	DMA1_Channel4->CNDTR = 14;				//数据长度14
	DMA1_Channel4->CMAR = (u32)Send_Buff;	//存储器地址
	DMA1_Channel4->CPAR = (u32)&USART1->DR;	//外设地址
	
	DMA1_Channel4->CCR|=1<<0;				//开启DMA1通道4
	
}

文件test.c完整代码:

#include "sys.h"
#include "usart.h"		
#include "delay.h"	 
#include "dma.h"

int main(void)
{
    
    				 

	Stm32_Clock_Init(9);		//时钟初始化
	uart_init(72, 115200);		//串口1初始化
	delay_init(72);				//延时函数初始化
	
	DMA_UsartTx_Init();			//使能一次DMA1发送串口1数据
	while(1){
    
    
		
		
		
	}
	
} 

9、下载运行
只发送一次数据。
在这里插入图片描述

DMA非中断模式接收USART数据。

后面的实验我就不将资料的位置都一一写出来了,学会自己找资料也很重要。

1、非中断接收数据的配置和发送的差不了多少。我先把代码放出来:

//Rx_Buff存储器地址
//CNDTR数据长度
void DMA_UsartRX_Init(u32 Rx_Buff, u16 CNDTR){
    
    
	
	RCC->AHBENR|=1<<0;							//开启DMA1时钟
	DMA1_Channel5->CCR=0x00000000;	//复位
	DMA1_Channel5->CCR|=0<<4;				//从外设读
	
	DMA1_Channel5->CCR|=0<<5;				//不执行循环操作
	DMA1_Channel5->CCR|=0<<6;				//不执行外设地址增量操作
	DMA1_Channel5->CCR|=1<<7;				//执行存储器地址增量操作
	DMA1_Channel5->CCR|=0<<8;				//8位
	DMA1_Channel5->CCR|=0<<10;				//8位
	DMA1_Channel5->CCR|=0<<12;				//优先级低
	DMA1_Channel5->CCR|=0<<14;				//非存储器到存储器模式
	DMA1_Channel5->CNDTR = CNDTR;			//数据长度
	DMA1_Channel5->CMAR = Rx_Buff;			//存储器地址
	DMA1_Channel5->CPAR = (u32)&USART1->DR;	//外设地址
	
	DMA1_Channel5->CCR|=1<<0;				//开启DMA1通道5
	
}

代码基本一样的,将所有的DMA1_Channel4改成DMA1_Channel5
2、然后不同的地方:
1、读数据的方向,位4;
2、。。。没了

优先改不改都没事。然后为了使用方便,我们将数据长度和存储器的地址作为传入参数。

文件test.c代码:

#include "sys.h"
#include "usart.h"		
#include "delay.h"	 
#include "dma.h"

u8 Rx_Buff[14];		//接收缓冲区

int main(void)
{
    
    				 

	Stm32_Clock_Init(9);		//时钟初始化
	uart_init(72, 115200);		//串口1初始化
	delay_init(72);				//延时函数初始化
	
//	DMA_UsartTx_Init();			//使能一次DMA1发送串口1数据
	DMA_UsartRX_Init((u32)Rx_Buff, 14);		//初始化DMA接收
	while(DMA1_Channel5->CNDTR != 0);		//等待接收完成
	printf("%s\r\n", Rx_Buff);				//输出接收内容
	while(1){
    
    
		
		
	}
	
} 

3、下载运行:
在这里插入图片描述

DMA中断模式接收USART数据

1、中断模式和非中断模式的区别就是打开中断和配置NVIC相关的寄存器。DMA接收打开中断倒是十分简单,而难的的NVIC相关寄存器的配置。

2、关于NVIC的知识,推荐这篇文章【STM32】NVIC中断优先级管理(中断向量表)

3、学习完上面的文章后相信你对NVIC的配置会有很多认识!我这里将会和大家一起学习关于NVIC相关寄存器的配置。

4、中断优先级分组:
分组配置是由SCB->AIRCR寄存器的(位10:8)来定义的。在《指南》第285页表 D.13 应用程序中断及复位控制寄存器(AIRCR) 0xE000_ED0C有这个寄存器的详细描述。
在这个寄存器中我们只要关注(位31:16)和(位10:8)。
位31:16)是访问钥匙,在后面的代码会体现出来。
位10:8)是优先级分组,这个会和另外一个寄存器(NVIC_IP)配合使用的。所以等我们了解寄存器(NVIC_IP)后再一起配置。

5、在《编程手册》(译文有翻译错误的地方,可以对照英文版本查看使用)第127页4.3.1 Cortex®-M3 NVIC寄存器的CMSIS映射NVIC寄存器的描述。

core_cm3.h文件中有描述用于配置NVIC的结构体NVIC_Type
在这里插入图片描述

中断使能寄存器(NVIC_ISERx)
ISER[0]和ISER[1]每个寄存器是32位的,每一位控制着对应的中断源的中断使能。
stm32f10x.h文件有枚举了各个中断源的位置,在这个列表中我们可以找到每个中断源的名字,后面需要使用到。
在这里插入图片描述
中断优先级寄存器(NVIC_IPRx)
在NVIC_Type结构体中并没有描述这个寄存器呀?只有 IP[240],那么这两个寄存器是什么关系呢?
在《编程手册》中关于这两个寄存器的描述是这样说的: IPR0-IPR16寄存器为每个中断提供4位优先级字段。这些寄存器是字节可访问的。每个寄存器都有四个优先级字段,它们映射到CMSIS中断优先级数组IP[0]到IP[67]中的四个元素。
在这里插入图片描述
简单的说就是将32位的 NVIC_IPR[17]映射到8位的IP[68]
而且8位的IP[x]只用到(位7:4)。

6、到这里就了解完关于stm32中断优先级分组每个中断源中断优先级的配置寄存器了。

SCB->AIRCRNVIC->IP[x] 是怎么组合设置优先级的呢?

AIRCR中断分组设置表:

AIRCR[10:8] IP bit[7:4]分配情况 分配结果
0 111 0:4 0位抢占优先级,4位响应优先级
1 110 1:3 1位抢占优先级,3位响应优先级
2 101 2:2 2位抢占优先级,2位响应优先级
3 100 3:1 3位抢占优先级,1位响应优先级
4 011 4:0 4位抢占优先级,0位响应优先级

这里又有两个知识点:抢占优先级、响应优先级。不清楚的可以看看抢占优先级和响应优先级
然后就是关于 AIRCR[10:8] 内容的写入需要注意的,当我们需要设置中断分组为0组的时候, AIRCR[10:8] 需要写入111,而不是000,好像进行了一个取反的操作,其他组的设置也是。
关于 IP bit[7:4] 的分配情况:比如我们将 AIRCR[10:8] 设置为 组3100,那么 IP bit[7:4] 将会被分成两部分来分 IP bit[7:5]IP bit[4] 这两部分, IP bit[7:5] 会被用来设置抢占优先级,而剩下的一位 IP bit[4] 会被用来设置响应优先级。

7、了解了这么多,其实代码就四五句就能搞定。
在函数void DMA_UsartRX_Init(u32 Rx_Buff, u16 CNDTR)末尾添加如下代码

	
	DMA1_Channel5->CCR|=1<<1;		//打开完成传输中断
	temp = SCB->AIRCR;				//读SCB->AIRCR数据
	temp&=0x0000F8FF;				//将[31:16][10:8]置零,其他位我们不改动
	temp|=0x05FA0400;				//写入钥匙和设置分组为3
	SCB->AIRCR = temp;				//数据写入
	// 注意:上面这段代码在整个工程中只能出现一次,因为改变分组后可能导致之前设置的优先级改变,可能造成不可预测的错误!!!
	
	//DMA1_Channel5_IRQn/32 先找出该中断是在寄存器ISER[0]还是ISER[1]
	//|=(1<<DMA1_Channel5_IRQn%32) 将对应位置一,使能中断
	NVIC->ISER[DMA1_Channel5_IRQn/32]|=(1<<DMA1_Channel5_IRQn%32);

	//写入0101 抢占优先级2  响应优先级1
	NVIC->IP[DMA1_Channel5_IRQn]|=5<<4; 
	
	//最后开启通道
	DMA1_Channel5->CCR|=1<<0;				//开启DMA1通道5
	

编写DMA1通道5的中断服务函数void DMA1_Channel5_IRQHandler(){}

在启动文件startup_stm32f10x_hd.s中有各个中断源的中断服务函数名字。
在这里插入图片描述
dma.c文件添加如下代码:

u8 flag;
void DMA1_Channel5_IRQHandler(){
    
    
	
	flag = 1;			//信号量置一
	
	DMA1->IFCR|=1<<17;	//清除中断标志位
	//在《手册》150页有DMA中断标志清除寄存器(DMA_IFCR)的描述
	
}

信号量flag是全局变量,我们需要在头文件dma.h中声明一下。使用关键词extern
在这里插入图片描述
main主函数代码:

int main(void)
{
    
    				 

	Stm32_Clock_Init(9);		//时钟初始化
	uart_init(72, 115200);		//串口1初始化
	delay_init(72);				//延时函数初始化
	
//	DMA_UsartTx_Init();			//使能一次DMA1发送串口1数据
	
	DMA_UsartRX_Init((u32)Rx_Buff, 14);		//初始化DMA接收
	
//	while(DMA1_Channel5->CNDTR != 0);		//等待接收完成
//	printf("%s\r\n", Rx_Buff);				//输出接收内容
	
	while(1){
    
    
		
		//如果完成一次数据传输
		if(flag){
    
    	
			
			flag = 0;					//清零
			printf("%s\r\n", Rx_Buff);	//打印信息
			
		}
		
	}
	
} 

8、下载运行:
在这里插入图片描述
实验结果:发送一次后,再次点击发送没有反应了。
这是为什么呢?
因为我们没有开启循环发送。DMA接收完14位数据后,(DMA_CNDTRx)寄存器计数变为0了,如果我们不使用循环模式,也不将(DMA_CNDTRx)寄存器计数置位回14的话,DMA将不会继续传输数据。
(DMA_CNDTRx) 寄存器中明确说明这一点: 当寄存器的内容为0时,无论通道是否开启,都不会发生任何数据传输。
在这里插入图片描述
所以,如果想继续接收数据,我们有两种办法:
1、关闭通道工作使能(因为通道开启后(DMA_CNDTRx)寄存器为只读状态,我们无法写入数据),然后写入需要接收的数据,最后开启通道

	while(1){
    
    
		
		//如果完成一次数据传输
		if(flag){
    
    	
			
			flag = 0;					//清零
			
			DMA1_Channel5->CCR&=~(1<<0);	//关闭通道
			DMA1_Channel5->CNDTR = 14;		//写入计数
			DMA1_Channel5->CCR|=1<<0;		//开启通道
		
			printf("%s\r\n", Rx_Buff);	//打印信息
			
		}
		
	}

2、使用循环模式
在这里插入图片描述

总结

其实使用寄存器开发底层驱动有很多很好的特点:

  • 代码简洁明了,当然需要我们注释好,不然比标准库更难看懂。
  • 方便快捷,只需要一本《手册》就可以查看所有的寄存器,不需要像标准库那样记、找每个函数、每个参数的作用。

猜你喜欢

转载自blog.csdn.net/weixin_45915259/article/details/127342431