自从上一次完成小车工程后,好久都没有写博客了,因为这段时间都在忙着准备数学建模的比赛,而且这个学期课业任务很重,几乎没有怎么去进阶自己的单片机水平,总感觉自己被压的喘不过气来,没办法绩点不能不要哦。好在终于盼来了五一假期,可以松一口气,学习一下自己想学的东西了。
经过一段时间的学习,我的stm32也算是入门了,前段时间也自己做了一个智能小车的工程。可我也知道只仅仅才是入门的水平。之前一直是跟着正点原子的视频去学习库函数代码的编写,虽然看起来已经可以完成那么多实验,其实我已经感觉到仅仅是库函数版本的学习并无法深入到单片机的实质,无法去学习到芯片的架构。我的目标绝不仅仅是学习stm32,正如之前所说stm32仅仅是一种芯片,如果不能理解芯片的架构问题,一旦脱离了stm32就啥都干不了了,可能简单的8051都能把自己搞得一头雾水。所以我决定这次五一假期老老实实呆在宿舍,把之前做的实验用寄存器重新做一遍,同时好好啃啃中文参考手册。同时我决定把C语言重新学习一遍,C语言是嵌入式工程师的饭碗是最接近机器的语言,然而我的C语言水平自觉真不咋地。上一次做蓝牙串口的时候,字符和数都没搞清。所以无论目标多么远大都没什么软用,我得把基础搞好,才能走的更远。
然后就是关于未来的计划了。在进阶学习操作系统之前我决定先巩固一下基础,搞好基本功。首先是硬件方面的学习,焊板子是工程师的基本功,前段时间我练习了基本的电路焊接也焊废了不少东西,对于一些大的焊盘已经基本可以搞定,但太小的贴片电阻焊盘还是力不从心,所以还有更多的板子等着我去焊废。然后就是电路设计ALTIUMDISIGNER之前已经有过抄板的经历,但自己还是做不来PCB设计。所以这个暑假我会加紧学习一下。
给自己定一个小目标,在这个暑假结束前完成stm32和电路图设计的学习,并且再完成一个掌上游戏机的工程(本来是想做无人机的后来想想一方面不确定因素太多,另一方面成本也高)。
在暑假结束后大二,我估计自己已经可以进一步学习LINUX和安卓了,同时也具有参加ROBOCON和电子设计比赛的实力了。大学的学习我感觉终究还是得靠自己,课堂上的知识终究还是理论层面的,想要比旁人优秀自然要比旁人付出更多的努力。
更进一步的计划也没必要再提了,说多了都是虚的,先把技术搞好再谈其他的事。废话说完了现在可以聊正事了。
关于寄存器的一点看法,首先关于寄存器与库函数其实我内心更偏向于使用库函数,避免了查表,同时函数名即函数意义通俗易懂。但寄存器一方面运行会更快,另一方面有助于加深对内部结构的理解。所以代码依旧可以用库函数写,但是寄存器也要学。这里我对正点的寄存器开发指南进行精简,也方便今后自己的查找与使用。
时钟有HSE(高外),HSI(高内),PLL(对前两种做倍频),LSE(低外),LSI(低内)
SYSCLK(系统时钟),看门狗时钟,RTC时钟
相应的GPIO端口寄存器必须被配置为相应功能。以下8个时钟信号可被选作MCO时钟:
●SYSCLK
●HSI
●HSE
●除2的PLL时钟
●PLL2时钟
●PLL3时钟除以2
●XT1外部3~25MHz振荡器(用于以太网)
●PLL3时钟(用于以太网)
在MCO上输出的时钟必须小于50MHz(这是I/O端口的最大速度)。
时钟的选择由时钟配置寄存器(RCC_CFGR)中的MCO[3:0]位控制。
时钟控制寄存器(RCC_CR)
时钟配置寄存器(RCC_CFGR)
时钟中断寄存器(RCC_CIR)
外设时钟使能寄存器(RCC_APB2ENR)
外设时钟使能寄存器(RCC_APB1ENR)
STM32的每个IO端口都有7个寄存器来控制。他们分别是:配置模式的2个32位的端口配置寄存器CRL和CRH;2个32位的数据寄存器IDR和ODR;1个32位的置位/复位寄存器BSRR;一个16位的复位寄存器BRR;1个32位的锁存寄存器LCKR;这里我们仅介绍常用的
几个寄存器,我们常用的IO端口寄存器只有4个:CRL、CRH、IDR、ODR。
CRL,CRH:
配置IO模式
该寄存器的复位值为0X44444444,CRL低八位,CRH高八位。复位值其实就是配置端口为浮空输入模式。从上图还可以得出:STM32的CRL控制着每组IO端口(A~G)的低8位的模式。每个IO端口的位占用CRL的4个位,高两位为CNF,低两位为MODE。这里我们可以记住几个常用的配置,比如0X0表示模拟输入模式(ADC用)、0X3表示推挽输出模式(做输出口用,50M速率)、0X8表示上/下拉输入模式(做输入口用)、0XB表示复用输出(使用IO口的功能,50M速率)。
IDR:输入数据寄存器
直接反应IO状态,读操作即可。
ODR:
ODR是一个端口输出数据寄存器,也只用了低16位。该寄存器为可读写,从该寄存器读出来的数据可以用于判断当前IO口的输出状态。而向该寄存器写数据,则可以控制某个IO口的输出电平。
具体初始化操作步骤
时钟使能//APB2ENR
设置输出模式//CRL
写数据到ODR
//跑马灯有关初始化
#include"led.h"
//初始化PB5和PE5为输出口.并使能这两个口的时钟
//LEDIO初始化
voidLED_Init(void)
{
RCC->APB2ENR|=1<<3;//使能PORTB时钟
RCC->APB2ENR|=1<<6;//使能PORTE时钟
GPIOB->CRL&=0XFF0FFFFF;
GPIOB->CRL|=0X00300000;//PB.5推挽输出
GPIOB->ODR|=1<<5;//PB.5输出高
GPIOE->CRL&=0XFF0FFFFF;
GPIOE->CRL|=0X00300000;//PE.5推挽输出
GPIOE->ODR|=1<<5;//PE.5输出高
}
->表示包涵
|=按位取1
<<指定位
//按键输入有关初始化
voidKEY_Init(void)
{
RCC->APB2ENR|=1<<2;//使能PORTA时钟
RCC->APB2ENR|=1<<6;//使能PORTE时钟
GPIOA->CRL&=0XFFFFFFF0;//PA0设置成输入,默认下拉8=1000
GPIOA->CRL|=0X00000008;
GPIOE->CRL&=0XFFF00FFF;//PE3/4设置成输入
GPIOE->CRL|=0X00088000;
GPIOE->ODR|=3<<3;//PE3/4上拉因为3<<3 的结果是二进制(11000),
而(1<<3)|(1<<4)的结果也是二进制(11000)。
}
总结来说
IO口操作为
1.CRL,CRH设置模式,
2.ODR写操作,INR来读
APB2ENR
APB2RSTR复位寄存器串口1的复位设置位在APB2RSTR的第14位。通过向该位写1复位串口1,写0结束复位。
USART_BRR
串口波特率设置
USART_CR1~3
串口控制该寄存器的高18位没有用到,低14位用于串口的功能设置。UE为串口使能位,通过该位置1,以使能串口。M为字长选择位,当该位为0的时候设置串口为8个字长外加n个停止位,停止位的个数(n)是根据USART_CR2的[13:12]位设置来决定的,默认为0。PCE为校验使能位,设置为0,则禁止校验,否则使能校验。PS为校验位选择,设置为0则为偶校验,否则为奇校验。TXIE为发送缓冲区空中断使能位,设置该位为1,当USART_SR中的TXE位为1时,将产生串口中断。TCIE为发送完成中断使能位,设置该位为1,当USART_SR中的TC位为1时,将产生串口中断。RXNEIE为接收缓冲区非空中断使能,设置该位为1,当USART_SR中的ORE或者RXNE位为1时,将产生串口中断。TE为发送使能位,设置为1,将开启串口的发送功能。RE为接收使能位,用法同TE。
USART_DR数据发送与接收DR[8:0]为串口数据,包含了发送或接收的数据。由于它是由两个寄存器组成的,一个给发送用(TDR),一个给接收用(RDR),该寄存器兼具读和写的功能。TDR寄存器提供了内部总线和输出移位寄存器之间的并行接口。RDR寄存器提供了输入移位寄存器和内部总线之间的并行接口。当使能校验位(USART_CR1中PCE位被置位)进行发送时,写到MSB的值(根据数据的长度不同,MSB是第7位或者第8位)会被后来的校验位取代。当使能校验位进行接收时,读到的MSB位是接收到的校验位。
USART_SR
串口状态寄存器
RXNE(读数据寄存器非空),当该位被置1的时候,就是提示已经有数据被接收到了,并且可以读出来了。这时候我们要做的就是尽快去读取USART_DR,通过读USART_DR可以将该位清零,也可以向该位写0,直接清除。
TC(发送完成),当该位被置位的时候,表示USART_DR内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。该位也有两种清零方式:1)读USART_SR,写USART_DR。2)直接向该位写0。
//串口相关配置
void uart_init(u32pclk2,u32bound)
{
floattemp;
u16mantissa;
u16fraction;
temp=(float)(pclk2*1000000)/(bound*16);//得到USARTDIV
mantissa=temp;//得到整数部分
fraction=(temp-mantissa)*16;//得到小数部分
mantissa<<=4;
mantissa+=fraction;
RCC->APB2ENR|=1<<2;//使能PORTA口时钟
RCC->APB2ENR|=1<<14;//使能串口时钟
GPIOA->CRH&=0XFFFFF00F;//IO状态设置
GPIOA->CRH|=0X000008B0;//IO状态设置
RCC->APB2RSTR|=1<<14;//复位串口1
RCC->APB2RSTR&=~(1<<14);//停止复位
//波特率设置
USART1->BRR=mantissa;//波特率设置
USART1->CR1|=0X200C;//1位停止,无校验位.
#ifEN_USART1_RX//如果使能了接收
//使能接收中断
USART1->CR1|=1<<5;//接收缓冲区非空中断使能
MY_NVIC_Init(3,3,USART1_IRQn,2);//组2,最低优先级
#endif
}
总结一下所进行的操作大致为:
1.时钟使能,
2.IO口使能(串口IO设置为复用),
3. 复位串口,
4. 波特率设置BRR,
5. 串口基本属性CR设置,
6. 设置中断
中断步骤
1)初始化IO口为输入。这一步设置你要作为外部中断输入的IO口的状态,可以设置为上拉/下拉输入,也可以设置为浮空输入,但浮空的时候外部一定要带上拉,或者下拉电阻。否则可能导致中断不停的触发。在干扰较大的地方,就算使用了上拉/下拉,也建议使用外部上拉/下拉电阻,这样可以一定程度防止外部干扰带来的影响。
2)开启IO口复用时钟,设置IO口与中断线的映射关系。STM32的IO口与中断线的对应关系需要配置外部中断配置寄存器EXTICR,这样我们要先开启复用时钟,然后配置IO口与中断线的对应关系。才能把外部中断与中断线连接起来。
3)开启与该IO口相对的线上中断/事件,设置触发条件。这一步,我们要配置中断产生的条件,STM32可以配置成上升沿触发,下降沿触发,或者任意电平变化触发,但是不能配置成高电平触发和低电平触发。这里根据自己的实际情况来配置,同时要开启中断线上的中断。这里需要注意的是:如果使用外部中断,并设置该中断的EMR位的话,会引起软件仿真不能跳到中断,而硬件上是可以的。而不设置EMR,软件仿真就可以进入中断服务函数,并且硬件上也是可以的。建议不要配置EMR位。
4)配置中断分组(NVIC),并使能中断。
这一步,我们就是配置中断的分组,以及使能,对STM32的中断来说,只有配置了NVIC
的设置,并开启才能被执行,否则是不会执行到中断服务函数里面去的。
5)编写中断服务函数。
这是中断设置的最后一步,中断服务函数,是必不可少的,如果在代码里面开启了中断,
但是没编写中断服务函数,就可能引起硬件错误,从而导致程序崩溃!所以在开启了某个中断后,一定要记得为该中断编写服务函数。在中断服务函数里面编写你要执行的中断后的操作。
关于外部中断有关寄存器操作打包于Ex_NVIC_Config(u8GPIOx,u8BITx,u8TRIM),这个函数用于外部中断线的设置。
void Ex_NVIC_Config(u8GPIOx,u8BITx,u8TRIM)
{
u8EXTADDR;
u8EXTOFFSET;
EXTADDR=BITx/4;//得到中断寄存器组的编号
EXTOFFSET=(BITx%4)*4;
RCC->APB2ENR|=0x01;//使能io复用时钟
AFIO->EXTICR[EXTADDR]&=~(0x000F<<EXTOFFSET);//清除原来设置!!!
AFIO->EXTICR[EXTADDR]|=GPIOx<<EXTOFFSET;//EXTI.BITx映射到GPIOx.BITx
//自动设置
EXTI->IMR|=1<<BITx;//开启lineBITx上的中断
//EXTI->EMR|=1<
if(TRIM&0x01)EXTI->FTSR|=1<<BITx;//lineBITx上事件下降沿触发
if(TRIM&0x02)EXTI->RTSR|=1<<BITx;//lineBITx上事件上升降沿触发
}
这个太多了就不一一说了。
//就是隔一段时间干一次事
我对定时器存在的是这样理解的:首先定时器的时钟一般设置来源APB1,而系统时钟又来源于外高,默认一般为2倍频72M,一般只有定时器用这么高,IO口最高50M。时钟存在的意义在于可以将系统的时钟资源合理有序的分配。
STM32的通用定时器是一个通过可编程预分频器(PSC)驱动的16位自动装载计数器(CNT)
构成。STM32的通用定时器可以被用于:测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和PWM)等。使用定时器预分频器和RCC时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个毫秒间调整。STM32的每个通用定时器都是完全独立的,没有互相共享的任何资源。
STM3的通用TIMx(TIM2、TIM3、TIM4和TIM5)定时器功能包括:
1)16位向上、向下、向上/向下自动装载计数器(TIMx_CNT)。
2)16位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数为1~
65535之间的任意数值。
3)4个独立通道(TIMx_CH1~4),这些通道可以用来作为:
A.输入捕获
B.输出比较
C.PWM生成(边缘或中间对齐模式)
D.单脉冲模式输出
4)可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可以用1个定时器控制另外
一个定时器)的同步电路。
5)如下事件发生时产生中断/DMA:
A.更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
B.触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
C.输入捕获
D.输出比较
E.支持针对定位的增量(正交)编码器和霍尔传感器电路
F.触发输入作为外部时钟或者按周期的电流管理
计数器寄存器(TIMx_CNT)
预分频器寄存器(TIMx_PSC)对时钟源分频
自动装载寄存器(TIMx_ARR)
控制寄存器1(TIMx_CR1)最低位来使能定时器
DMA/中断使能寄存器(TIMx_DIER)最低位用于使能更新中断 TIMx_CNT寄存器计数器存储了当前定时器的计数值
自动重装载寄存器(TIMx_ARR)
TIMx_SR寄存器,我们同样只用到了最低位,当计数器CNT被重新初始化的时候,产生更新中断标记,通过这个中断标志位,就可以知道产生中断的类型。
1)TIM3时钟使能。
这里我们通过APB1ENR的第1位来设置TIM3的时钟,因为Stm32_Clock_Init函数里面
把APB1的分频设置为2了,所以我们的TIM3时钟就是APB1时钟的2倍,等于系统时钟(72M)。
2)设置TIM3_ARR和和TIM3_PSC的值。
通过这两个寄存器,我们来设置自动重装的值,以及分频系数。这两个参数加上时钟频率
就决定了定时器的溢出时间。
3)设置TIM3_DIER允许更新中断。
因为我们要使用TIM3的更新中断,所以设置DIER的UIE位为1,使能更新中断。
4)允许TIM3工作。
光配置好定时器还不行,没有开启定时器,照样不能用。我们在配置完后要开启定时器,
通过TIM3_CR1的CEN位来设置。
5)TIM3中断分组设置。
在定时器配置完了之后,因为要产生中断,必不可少的要设置NVIC相关寄存器,以使能TIM3中断。
6)编写中断服务函数
//通用定时器3中断初始化
//这里时钟选择为APB1的2倍,而APB1为36M
//arr:自动重装值。
//psc:时钟预分频数
//这里使用的是定时器3!
void TIM3_Int_Init(u16arr,u16psc)
{
RCC->APB1ENR|=1<<1;//TIM3时钟使能
TIM3->ARR=arr;//设定计数器自动重装值//刚好1ms
TIM3->PSC=psc;//预分频器7200,得到10Khz的计数时钟
TIM3->DIER|=1<<0;//允许更新中断
TIM3->CR1|=0x01;//使能定时器3
MY_NVIC_Init(1,3,TIM3_IRQn,2);//抢占1,子优先级3,组2
}
总结来说定时器中断可以为系统提供很多时间带,当然也可以作为时间长短的获取,可以于输入捕获取得类似的效果。
初始化步骤为时钟使能先设定装载值与预分频系数再使能更新中断与定时器最后设置中断
捕获/比较寄存器(TIMx_CCR1~4)
捕获/比较使能寄存器(TIMx_CCER)
捕获/比较模式寄存器(TIMx_CCMR1/2)
在输出模式下,该寄存器的值与CNT的值比较,根据比较结果产生相应动作。利用这点,我们通过修改这个寄存器的值,就可以控制PWM的输出脉宽了。本章,我们使用的是TIM3的通道2,所以我们需要修改TIM3_CCR2以实现脉宽控制DS0的亮度。我们要使用TIM3的CH2输出PWM来控制DS0的亮度,但是TIM3_CH2默认是接在PA7上面的,而我们的DS0接在PB5上面,如果普通MCU,可能就只能用飞线把PA7飞到PB5上来实现了,不过,我们用的是STM32,它比较高级,可以通过重映射功能,把TIM3_CH2映射到PB5上。
所以运用不同通道的CCR就可以在相同定时器的情况下生成不同PWM波。
1)开启TIM3时钟,配置PB5为复用输出。
要使用TIM3,我们必须先开启TIM3的时钟(通过APB1ENR设置),这点相信大家看了这
么多代码,应该明白了。这里我们还要配置PB5为复用输出,这是因为TIM3_CH2通道将重映射到PB5上,此时,PB5属于复用功能输出。
2)设置TIM3_CH2重映射到PB5上因为TIM3_CH2默认是接在PA7上的,所以我们需要设置TIM3_REMAP为部分重映射(通过AFIO_MAPR配置),让TIM3_CH2重映射到PB5上面。
3)设置TIM3的的ARR和和PSC。在开启了TIM3的时钟之后,我们要设置ARR和PSC两个寄存器的值来控制输出PWM的周期。当PWM周期太慢(低于50Hz)的时候,我们就会明显感觉到闪烁了。因此,PWM周期在这里不宜设置的太小。
4)设置TIM3_CH2的的PWM模式。
接下来,我们要设置TIM3_CH2为PWM模式(默认是冻结的),因为我们的DS0是低电
平亮,而我们希望当CCR2的值小的时候,DS0就暗,CCR2值大的时候,DS0就亮,所以我们要通过配置TIM3_CCMR1的相关位来控制TIM3_CH2的模式。
5)使能TIM3的的CH2输出,使能TIM3。
在完成以上设置了之后,我们需要开启TIM3的通道2输出以及TIM3。前者通过TIM3_CCER1来设置,是单个通道的开关,而后者则通过TIM3_CR1来设置,是整个TIM3的
总开关。只有设置了这两个寄存器,这样我们才能在TIM3的通道2上看到PWM波输出。
6)修改TIM3_CCR2控制占空比
/arr:自动重装值
//psc:时钟预分频数
voidTIM3_PWM_Init(u16arr,u16psc)
{
//此部分需手动修改IO口设置
RCC->APB1ENR|=1<<1;//TIM3时钟使能
RCC->APB2ENR|=1<<3;//使能PORTB时钟
GPIOB->CRL&=0XFF0FFFFF;//PB5输出
GPIOB->CRL|=0X00B00000;//复用功能输出
RCC->APB2ENR|=1<<0;//开启辅助时钟在配置 AFIO 相关寄存器的时候,必须先开启辅助功能时钟
AFIO->MAPR&=0XFFFFF3FF;//清除MAPR的[11:10]
AFIO->MAPR|=1<<11;//部分重映像,TIM3_CH2->PB5
TIM3->ARR=arr;//设定计数器自动重装值
TIM3->PSC=psc;//预分频器不分频
TIM3->CCMR1|=7<<12;//CH2PWM2模式
TIM3->CCMR1|=1<<11;//CH2预装载使能
TIM3->CCER|=1<<4;//OC2输出使能
TIM3->CR1=0x0080;//ARPE使能
TIM3->CR1|=0x01;//使能定时器3
}
总结来说初始化步骤为:
1.时钟使能,
2.IO口配置(复用),
3.配置AFIO以重映射,
4.定时器配置(ARR,PSC ,CCMR1,CCER,CR1)
在TIMx_CCMRx寄存器中的OCxM位写入’110’(PWM模式1)或’111’(PWM模式2),能够独立地设置每个OCx输出通道产生一路PWM。必须设置TIMx_CCMRx寄存器OCxPE位以使能相应的预装载寄存器,最后还要设置TIMx_CR1寄存器的ARPE位,(在向上计数或中心对称模式中)使能自动重装载的预装载寄存器。
当在输入捕获模式下使用的时候,对应图 15.1.2 的第二行描述,从图中可以看出,TIMx_CCMR1 明显是针对 2 个通道的配置,低八位[7:0]用于捕获/比较通道 1 的控制,而高八位[15:8]则用于捕获/比较通道 2 的控制,因为 TIMx 还有 CCMR2 这个寄存器,所以可以知道CCMR2 是用来控制通道 3 和通道 4
1 )开启 TIM5 时钟,配置 PA0 为下拉输入。要使用 TIM5,我们必须先开启 TIM5 的时钟(通过 APB1ENR 设置)。这里我们还要配置 PA0
为下拉输入,因为我们要捕获 TIM5_CH1 上面的高电平脉宽,而 TIM5_CH1 是连接在 PA0 上面的。
2 )设置 TIM5 的 的 ARR 和 和 PSC 。
在开启了 TIM5 的时钟之后,我们要设置 ARR 和 PSC 两个寄存器的值来设置输入捕获的自动重装载值和计数频率。
3 )设置 TIM5 的 的 CCMR1
TIM5_CCMR1 寄存器控制着输入捕获 1 和 2 的模式,包括映射关系,滤波和分频等。这里我们需要设置通道 1 为输入模式,且 IC1 映射到 TI1(通道 1)上面,并且不使用滤波(提高响应速度)器。
4)设置 TIM5 的 的 CCER ,开启输入捕获,并设置为上升沿捕获。 。
TIM5_CCER 寄存器是定时器的开关,并且可以设置输入捕获的边沿。只有 TIM5_CCER
寄存器使能了输入捕获,我们的外部信号,才能被 TIM5 捕获到,否则一切白搭。同时要设置好捕获边沿,才能得到正确的结果。
5 )设置 TIM5 的 的 DIER ,使能捕获和更新中断,并编写中断服务函数
因为我们要捕获的是高电平信号的脉宽,所以,第一次捕获是上升沿,第二次捕获时下降
沿,必须在捕获上升沿之后,设置捕获边沿为下降沿,同时,如果脉宽比较长,那么定时器就
会溢出,对溢出必须做处理,否则结果就不准了。这两件事,我们都在中断里面做,所以必须开启捕获中断和更新中断。设置了中断必须编写中断函数,否则可能导致死机。我们需要在中断函数里面完成数据处理和捕获设置等关键操作,从而实现高电平脉宽统计。
6 )设置 TIM5 的 的 CR1
//定时器 5 通道 1 输入捕获配置
//arr:自动重装值
//psc:时钟预分频数
void TIM5_Cap_Init(u16 arr,u16 psc)
{
RCC->APB1ENR|=1<<3; //TIM5 时钟使能
RCC->APB2ENR|=1<<2; //使能 PORTA 时钟
GPIOA->CRL&=0XFFFFFFF0; //PA0 清除之前设置
GPIOA->CRL|=0X00000008; //PA0 输入
GPIOA->ODR|=0<<0; //PA0 下拉
TIM5->ARR=arr; //设定计数器自动重装值
TIM5->PSC=psc; //预分频器
TIM5->CCMR1|=1<<0; //CC1S=01 选择输入端 IC1 映射到 TI1 上
TIM5->CCMR1|=0<<4; //IC1F=0000 配置输入滤波器 不滤波
TIM5->CCMR1|=0<<10; //IC2PS=00 配置输入分频,不分频
TIM5->CCER|=0<<1; //CC1P=0 上升沿捕获
TIM5->CCER|=1<<0; //CC1E=1 允许捕获计数器的值到捕获寄存器中
TIM5->DIER|=1<<1; //允许捕获中断
TIM5->DIER|=1<<0; //允许更新中断
TIM5->CR1|=0x01; //使能定时器 2
MY_NVIC_Init(2,0,TIM5_IRQn,2);//抢占 2,子优先级 0,组 2
}
//捕获状态
//[7]:0,没有成功的捕获;1,成功捕获到一次.
//[6]:0,还没捕获到高电平;1,已经捕获到高电平了.
//[5:0]:捕获高电平后溢出的次数
u8 TIM5CH1_CAPTURE_STA=0; //输入捕获状态
u16 TIM5CH1_CAPTURE_VAL; //输入捕获值
//定时器 5 中断服务程序
void TIM5_IRQHandler(void)
{
u16 tsr;
tsr=TIM5->SR;
if((TIM5CH1_CAPTURE_STA&0X80)==0)//还未成功捕获
{
if(tsr&0X01)//溢出
{
if(TIM5CH1_CAPTURE_STA&0X40)//已经捕获到高电平了
{
if((TIM5CH1_CAPTURE_STA&0X3F)==0X3F)//高电平太长了
{
TIM5CH1_CAPTURE_STA|=0X80;//标记成功捕获了一次
TIM5CH1_CAPTURE_VAL=0XFFFF;
}else TIM5CH1_CAPTURE_STA++;
}
}
if(tsr&0x02)//捕获 1 发生捕获事件
{
if(TIM5CH1_CAPTURE_STA&0X40) //捕获到一个下降沿
{
TIM5CH1_CAPTURE_STA|=0X80; //标记成功捕获到一次高电平脉宽
TIM5CH1_CAPTURE_VAL=TIM5->CCR1;//获取当前的捕获值.
TIM5->CCER&=~(1<<1); //CC1P=0 设置为上升沿捕获
}else //还未开始,第一次捕获上升沿
{
TIM5CH1_CAPTURE_STA=0; //清空
TIM5CH1_CAPTURE_VAL=0;
TIM5CH1_CAPTURE_STA|=0X40; //标记捕获到了上升沿
TIM5->CNT=0; //计数器清空
TIM5->CCER|=1<<1; //CC1P=1 设置为下降沿捕获
}
}
}
TIM5->SR=0;//清除中断标志位
}
STM32 的实时时钟(RTC)是一个独立的定时器。STM32 的 RTC 模块拥有一组连续计数
的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。
RTC_CR 寄存器 设置了相应的允许位,则在每个TR_CLK 周期中 RTC 产生一个中断(秒中断)
32 位的可编程计数器(RTC_CNT),可被初始化为当前的系统时间,一个 32 位的时钟计数器,按秒钟计算,可以记录 4294967296 秒,约合 136 年左右,作为一般应用,这已经是足够了的。
闹钟寄存器 RTC_ALR
2 个控制寄存器 RTC_CRH 和 RTC_CRL 该寄存器用来控制中断的,我们本章将要用到秒钟中断,所以在该寄存器必须设置最低位为 1,以允许秒钟中断
RTC 预分频装载寄存器,也有 2 个寄存器组成,RTC_PRLH 和RTC_PRLL。这两个寄存器用来配置 RTC 时钟的分频数的,那么我们要设置这两个寄存器的值为 32767,以得到一秒钟的计数频率
1 )使能电源时钟和备份区域时钟。
前面已经介绍了,我们要访问 RTC 和备份区域就必须先使能电源时钟和备份区域时钟。这
个通过 RCC_APB1ENR 寄存器来设置。
2 )取消备份区写保护。
要向备份区域写入数据,就要先取消备份区域写保护(写保护在每次硬复位之后被使能),
否则是无法向备份区域写入数据的。我们需要用到向备份区域写入一个字节,来标记时钟已经配置过了,这样避免每次复位之后重新配置时钟。
3 )复位备份区域,开启外部低速振荡器。
在取消备份区域写保护之后,我们可以先对这个区域复位,以清除前面的设置,当然这个
操作不要每次都执行,因为备份区域的复位将导致之前存在的数据丢失,所以要不要复位,要看情况而定。然后我们使能外部低速振荡器,注意这里一般要先判断 RCC_BDCR 的 LSERDY位来确定低速振荡器已经就绪了才开始下面的操作。
4 )选择 RTC 时钟,并使能。
这里我们将通过 RCC_BDCR 的 RTCSEL 来选择选择外部 LSI(32.768K 的外部晶振)作为
RTC 的时钟。然后通过 RTCEN 位使能 RTC 时钟。
5 )设置 RTC 的分频,以及配置 RTC 时钟。
在开启了 RTC 时钟之后,我们要做的就是设置 RTC 时钟的分频数,通过 RTC_PRLH 和
RTC_PRLL 来设置,然后等待 RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置
RTC 的允许配置位(RTC_CRH 的 CNF 位),设置时间(其实就是设置 RTC_CNTH 和 RTC_CNTL两个寄存器)或者设置闹钟(设置 RTC_ALRH 和 RTC_ALRL 两个寄存器)。
6 )更新配置,设置 RTC 中断。
在设置完时钟之后,我们将配置更新,这里还是通过 RTC_CRH 的 CNF 来实现。在这之后
我们在备份区域 BKP_DR1 中写入 0X5050 代表我们已经初始化过时钟了,下次开机(或复位)的时候,先读取 BKP_DR1 的值,然后判断是否是 0X5050 来决定是不是要配置,避免重复配置。接着我们配置 RTC 的秒钟中断,并进行分组。
7 )编写中断服务函数。
最后,我们要编写中断服务函数,在秒钟中断/闹钟中断产生的时候,读取当前的时间值,
并显示到 TFTLCD 模块上。
ADC 控制寄存器(ADC_CR1 和 ADC_CR2)
ADC_CR1 的 SCAN 位,该位用于设置扫描模式,由软件设置和清除,如果设置为 1,则
使用扫描模式,如果为 0,则关闭扫描模式。在扫描模式下,由 ADC_SQRx 或 ADC_JSQRx 寄
存器选中的通道被转换。如果设置了 EOCIE 或 JEOCIE,只在最后一个通道转换完毕后才会产生 EOC 或 JEOC 中断。ADC_CR1[19:16]用于设置 ADC 的操作模式
ADC_CR2,该寄存器我们也只针对性的介绍一些位:ADON 位用于开关 AD 转换器。而 CONT 位用于设置是否进行连续转换,我们使用单次转换,所以 CONT 位必须为 0。CAL 和 RSTCAL 用于AD 校准。ALIGN 用于设置数据对齐,我们使用右对齐,该位设置为 0。EXTSEL[2:0]用于选择启动规则转换组转换的外部事件
ADC 采样事件寄存器(ADC_SMPR1 和 ADC_SMPR2)这两个寄存器用于设置通道 0~17 的采样时间
ADC 规则序列寄存器(ADC_SQR1~3)L[3:0]用于存储规则序列的长度,我们这里只用了 1 个,所以设置这几个位的值为 0。其他的 SQ13~16 则存储了规则序列中第 13~16 个通道的编号(0~17)。另外两个规则序列寄存器同 ADC_SQR1 大同小异,我们这里就不再介绍了,要说明一点的是:我们选择的是单次转换,所以只有一个通道在规则序列里面,这个序列就是 SQ1,通过 ADC_SQR3 的最低 5 位(也就是 SQ1)设置。
ADC 规则数据寄存器(ADC_DR)规则序列中的 AD 转化结果都将被存在这个寄存器里面
1 )开启 PA 口时钟,设置 PA1 为模拟输入。
STM32F103ZET6 的 ADC 通道 1 在 PA1 上,所以,我们先要使能 PORTA 的时钟,然后设置 PA1 为模拟输入。
2 )使能 ADC1 时钟,并设置分频因子。
要使用 ADC1,第一步就是要使能 ADC1 的时钟,在使能完时钟之后,进行一次 ADC1 的
复位。接着我们就可以通过 RCC_CFGR 设置 ADC1 的分频因子。分频因子要确保 ADC1 的时钟(ADCCLK)不要超过 14Mhz。
3 )设置 ADC1 的工作模式。
在设置完分频因子之后,我们就可以开始 ADC1 的模式配置了,设置单次转换模式、触发
方式选择、数据对齐方式等都在这一步实现。
4 )设置 ADC1 规则序列的相关信息。
接下来我们要设置规则序列的相关信息,我们这里只有一个通道,并且是单次转换的,所
以设置规则序列中通道数为 1,然后设置通道 1 的采样周期。
5 )开启 AD 转换器,并校准。在设置完了以上信息后,我们就开启 AD 转换器,执行复位校准和 AD 校准,注意这两步是必须的!不校准将导致结果很不准确。
6 )读取 ADC 值。
在上面的校准完成之后,ADC 就算准备好了。接下来我们要做的就是设置规则序列 1 里面
的通道,然后启动 ADC 转换。在转换结束后,读取 ADC1_DR 里面的值就是了。
这里还需要说明一下 ADC 的参考电压,精英 STM32 开发板使用的是 STM32F103ZET6,
该芯片有外部参考电压:Vref-和 Vref+,其中 Vref-必须和 VSSA 连接在一起,而 Vref+的输入范围为:2.4~VDDA(VDDA 一般等于 3.3V,最大是 3.6V)。精英 STM32 V1 开发板通过 P5 端口,设置 Vref+,从而设置参考电压(Vref-直接接在 GND 上面了),默认 Vref+接到 VDDA,参考电压就是 3.3V。如果大家想自己设置其他参考电压,将你的参考电压接在 Vref+上就 OK了。本章我们的参考电压设置的是 3.3V。
通过以上几个步骤的设置,我们就能正常的使用 STM32F1 的 ADC1 来执行 AD 转换了。
//初始化 ADC1
//这里我们仅以规则通道为例
//我们默认仅开启通道 1
void Adc_Init(void)
{
//先初始化 IO 口
RCC->APB2ENR|=1<<2; //使能 PORTA 口时钟
GPIOA->CRL&=0XFFFFFF0F;//PA1 anolog 输入
RCC->APB2ENR|=1<<9; //ADC1 时钟使能
RCC->APB2RSTR|=1<<9; //ADC1 复位
RCC->APB2RSTR&=~(1<<9);//复位结束
RCC->CFGR&=~(3<<14); //分频因子清零
//SYSCLK/DIV2=12M ADC 时钟设置为 12M,ADC 最大时钟不能超过 14M!
//否则将导致 ADC 准确度下降!
RCC->CFGR|=2<<14;
ADC1->CR1&=0XF0FFFF; //工作模式清零
ADC1->CR1|=0<<16; //独立工作模式
ADC1->CR1&=~(1<<8); //非扫描模式
ADC1->CR2&=~(1<<1); //单次转换模式
ADC1->CR2&=~(7<<17);
ADC1->CR2|=7<<17; //软件控制转换
ADC1->CR2|=1<<20; //使用用外部触发(SWSTART)!!! 必须使用一个事件来触发
ADC1->CR2&=~(1<<11); //右对齐
ADC1->SQR1&=~(0XF<<20);
ADC1->SQR1|=0<<20; //1 个转换在规则序列中 也就是只转换规则序列 1
//设置通道 1 的采样时间
ADC1->SMPR2&=~(3*1); //通道 1 采样时间清空
ADC1->SMPR2|=7<<(3*1); //通道 1 239.5 周期,提高采样时间可以提高精确度
ADC1->CR2|=1<<0; //开启 AD 转换器
ADC1->CR2|=1<<3; //使能复位校准
while(ADC1->CR2&1<<3); //等待校准结束
//该位由软件设置并由硬件清除。在校准寄存器被初始化后该位将被清除。
ADC1->CR2|=1<<2; //开启 AD 校准
while(ADC1->CR2&1<<2); //等待校准结束
//该位由软件设置以开始校准,并在校准结束时由硬件清除
}
//获得 ADC1 某个通道的值
//ch:通道值 0~16
//返回值:转换结果
u16 Get_Adc(u8 ch)
{
//设置转换序列
ADC1->SQR3&=0XFFFFFFE0;//规则序列 1 通道 ch
ADC1->SQR3|=ch;
ADC1->CR2|=1<<22; //启动规则转换通道
while(!(ADC1->SR&1<<1)); //等待转换结束
return ADC1->DR; //返回 adc 值
}
总结来说我们需要做的配置是
IO时钟使能RCC,
GPIO口配置使能,
ADC时钟使能复位结束复位,
ADC分频,设置ADC工作模式CR1 CR2,
规则序列配置SQR,CR2开启校准,
通过CR2开启转换,读DR值。
好了有关STM32的基础大概就是这些,将之前学的东西再学一遍后,已经对内部的寄存器有了清晰的认识。相比于之前用库函数迷迷糊糊的写代码发现自己对其理解又更深一层,所以辛苦是没有白费的。
此外学习寄存器的过程明显比一开始的学习快很多,在了解了大致的原理过程后我需要完成的仅仅是从参考手册中找到对应的寄存器位以及设置方法。还有就是我发现寄存器的代码编写非常简洁不容易出错,而且可以在出BUG时非常容易找出错误所在,只要去观察寄存器状态在哪里出错,一次就可以找出错误所在。避免了一遍一遍的用串口刷机找错。
下面还有很多更高级的实验,就不再多说了,常用的寄存器大概就是这些。我接下来就准备用寄存器的编写方式去解决一下此前没有解决的小车测速部分的问题。