学习资料下载
在本文学习中会用到如下的文档资料,没有的朋友先下载。
工程模板
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.c 、sys.c 、usart.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; 这个语句是什么意思呢? 与、或操作,就是将USART1 的CR3 这个寄存器的第7位置一。 在寄存器配置的时候是会非常多使用|= 和&= 操作的。刚开始使用肯定会用一些逻辑上的混乱,这个不着急,慢慢就会适应的。 这个语句怎么完成寄存器设置的,我就不详细说了,包括后面的逻辑操作也是。
6、USART1的DMA发送功能就打开了,接下来我们开始配置DMA的寄存器。 同样,DMA寄存器的描述也是在《手册》里面,除了NVIC配置的寄存器的描述不再这个这个文件,其他的都是这个手册。所以学习寄存器,这个手册非常重要。
7、我们先创建两个文件dma.c 和dma.h 。 dma.h 文件
void DMA_UsartTx_Init(void); //发送初始化
void DMA_UsartRX_Init(u32 Rx_Buff, u16 CNDTR); //接收初始化
在dma.c 文件编写void DMA_UsartTx_Init(void); 这个函数初始化DMA发送串口数据配置。 编写外设驱动的步骤一般是: (1)开启外设时钟 (2)配置外设寄存器 (3)设置中断优先级 好,我们先看第一个。 (1)开启外设时钟 1、STM32有三个时钟线,在三个寄存器中去配置:AHBENR 、APB1ENR 、APB2ENR 。寄存器具体都在那些时钟线下面呢?在《手册》的25页的图1 系统结构可以看到时钟线APB1 、APB2 所包含的外设,那除了这两个时钟线中所提到的外设,其他的外设都是在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 的完整代码:
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 完整代码:
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 代码:
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->AIRCR 和 NVIC->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] 设置为 组3 :100 ,那么 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、使用循环模式
总结
其实使用寄存器开发底层驱动有很多很好的特点:
- 代码简洁明了,当然需要我们注释好,不然比标准库更难看懂。
- 方便快捷,只需要一本《手册》就可以查看所有的寄存器,不需要像标准库那样记、找每个函数、每个参数的作用。
|