本文摘自:
https://blog.csdn.net/xiashiwendao/article/details/122292404
概述
点亮LED表面看起来貌似很简单,但是如何想要搞清楚其背后牵涉的每一行代码的具体含义,还是需要花费一些功夫的,而且,只有把LED的背后只是搞清楚了,才算嵌入式开发的基础入门。
今天我们就来研究一下LED的重头戏,RCC_Init;什么是RCC?上手册:
RCC
RCC,Reset and Clock Control,重置以及时钟控制;STM32手册使用了两个章节来对其进行描述,可见它的重要性;对于RCC的初始化也是比较复杂,里面包含了STM32对于时钟的相关机制,
代码总览
void RCC_init(uint16_t PLL)
{
uint32_t temp=0;
*((uint32_t *)RCC_CR) |= 0x00010000;
while(!( *((uint32_t *)RCC_CR) >>17));
*((uint32_t *)RCC_CFGR) = 0X00000400;
PLL -= 2;
*((uint32_t *)RCC_CFGR) |= PLL<<18;
*((uint32_t *)RCC_CFGR) |= 1<<16;
*((uint32_t *)FLASH_ACR)|=0x2;
*((uint32_t *)RCC_CR) |= 0x01000000;
while(!(*((uint32_t *)RCC_CR) >> 25));
*((uint32_t *)RCC_CFGR) |= 0x00000002;
while(temp != 0x02)
{
temp = *((uint32_t *)RCC_CFGR) >> 2;
temp &= 0x03;
}
}
使能HSE
第一行有效代码,是熟悉的味道,前一节我们说过,或运算一般用于设定指定位(而且还不影响其他位)
*((uint32_t *)RCC_CR) |= 0x00010000;
看到了RCC_CR,首先就是翻手册,RCC的章节的RCC_CR章节:
然后查看寄存器内部32位的定义:
0x00010000转换为32bit的二进制:0000 0000 0000 0001 0000 0000 0000 0000;这里有一个小技巧,就是学会分割来看,手册里面的寄存器的定义一般定义是分为上下两行的;从16进制来看,从左往右,前面4为是负责上面RCC_CFGR的高位16bit,后面4位是对应(二进制)低位16bit,即下面的16bit;对此次而言,低4位都是0,可以不需要care,直接关注高4位即可,其中高4为只有最后一位是有效设置,1对应的四位二进制是0001,即16位设置为1,查看寄存器定义的16位对应的是HSEON位,即使能HSEON。
HSE和时钟
什么是HSEON?需要拆开来看:HSE ON,HSE是High Speed External,外部高速时钟,所以HSEON就是使能外部高速时钟;
为什么要配置HSE呢?因为在硬件系统中,各个部件的工作、协调,都是基于系统时钟的,比如我们通常讲的CPU的速度快慢,就是指CPU的时钟频率,即每秒钟能够工作多少个时钟;
小贴士:
在win10系统中可以看到有两个频率,如下图,分别是1.9GHz以及2.11GHz;其中第一个是Intel提供的标准频率,其实是CPU名字的一部分,第二个频率是win10系统自己计算出来的,很多时候和Intel提供的频率值并不相等;不过两者相差也不会太大。
系统时钟有三个来源,分别是HSE(High Spped External),HSI(High Speed Internal)以及PLLCLK,引入了PLL就是因为很多时候,需要基于系统时钟进行分频倍频,比如有的设备(RAM,DMA)需要比系统时钟快,于是需要通过PLL来进行加倍频率,还有的低俗的设备需要低于系统频率工作,也是通过PLL来进行减速,这样只需要一个系统时钟就可以同时满足高速和低俗设备。
所以PLL的时钟源还是HSE,HSI,不过经过PLL倍频调节输出的时钟称之为PLLCLK;
再回到我们的代码里面,这里配置时钟源就是HSEON(如果需要使用PLL则还需要将PLLx位配置为1);
确认HSE使能生效
完成了时钟源的配置,下面一步是等待HSEON的配置的生效:
while(!( *((uint32_t *)RCC_CR) >>17));
即第等待第18位(编号17)HSERDY的值变成1,当且仅当配置HSEON生效之后,该位置才会由硬件设置为1,注意在寄存器定义里面HSERDY配置为“r”,这个代表软件层面是无法改变这个bit的值,只能够读取:
然后再查看寄存器定义里面对于这一位的解释,0就是HSEON位设置并未生效,1就是设置已经生效:
等待的过程是使用位运算里面右移“>>"实现的,右移的运算规则里面是低位直接丢弃,例如11111右移4bit,最后结果就是1;这行代码的逻辑意义就是RCC_CR右边17位直接丢弃,即016为抛弃,只是保留了3118位(注意数字方向,是从左向右逐次减小的,和寄存器定义一致),共计15bit;
又因为上一步骤中通过和0x00010000进行与运算将除了HSEON位之外的位置全部置为“0”了,所以之类RCC_CR右移17位之后,包括HSERDY在内的位全部都是0,只有当HSEON生效之后,HSERDY才是1,即*((uint32_t *)RCC_CR) >>17 = 1,于是此时退出while循环。
配置其他时钟
CR位配置解决了,下面我们看一下CFGR的配置:
*((uint32_t *)RCC_CFGR) = 0X00000400;
看到这里,我觉得我们可以总结一下写寄存器的基本套路:
-
查看手册相应章节,并了解缩写意义,比如CFGR,全称Clock Configuration Register,时钟配置寄存器:
-
查看寄存器定义:
-
查看具体的某一位的定义,比如我们这里设置为0X00000400,4是在后四位,所以重点关注低16bit,即从15 ~ 0bit:0000 0100 0000 0000,发现正好配置的PPRE1,值为100,手册介绍如下:
用来配置APB1的从HCLK中获得时钟的分频系数,二进制100对应的分频系数是2;HCLK是AHB总线的时钟;然后AHB经过AHB-APB桥接将时钟分配到APB1和APB2总线,APB1的总线对应PCLK1,APB2总线对应的PCLK2;这里配置的就是APB1时钟频率的分频值,为HCLK/2;
这里有一个坑,虽然显式的为PCLK1赋值了,但是其实隐式的同时将PCLK2(对应APB2),AHB都赋值了;只不过配置的是0;
比如PPRE2配置为000,对应的就是不分频,或者说分频数为1:
还有HPRE位,配置也是000,对应的不分频,或者说频数为1:
所以CFGR的配置重要的指定了AHB总线以及APB1和APB2总线的分频/倍频数;
设置PLL倍频数
继续看后面的代码,PLL是参数,即倍频数,PLL上面已经介绍了,专门用于接入时钟源然后后对其进行分频倍频再分配给各个总线(下面挂载的设备):
PLL -= 2;
*((uint32_t *)RCC_CFGR) |= PLL<<18;
那么这里为什么要-2呢?我们先存疑,理解了下一行代码谜底就自然打开了;
PLL值左移18位,左移是位运算,左移+或运算 = 赋值操作,即将PLL值赋给18位起始的后面四位,通过查看寄存器定义可以看到CFGR的18位起始到后面四位是PLLMUL,再查看手册对于PLLMUL的解释:PLL的倍频;不过看一下赋值情况0010(十进制2)对应是4倍频,0011(十进制3)对应的是5倍频,以此类推,就是寄存器的只是和真实的倍频差2,看到这里你就明白了为什么在PLL -=2了:
紧随其后的,可以知道是要给CFGR的第16bit赋值为1:
*((uint32_t *)RCC_CFGR) |= 1<<16;
查看手册,是描述PLL时钟源,1对应是PLL的时钟原始PREDIV1:
时钟树
关于PLLSRC以及PREDIV1他们之间关系在时钟树(Clock Tree)上面有比较明确的关系说明,从下图可以看到作为时钟源最开始是HSE,然后通过时钟被分频后成为了PREDIV1,然后再除以2,获得了PLLSRC:
所以如果PLLMUL的值设置为1,即PLL的时钟源采用内部高速时钟(HSI),最终输出时钟信号在HSI/2即可;
如果值设置为1即采用外部高速时钟(HSE),在经历了分频后输出为PREDIV1(时钟)信号,还需要再分频一下,分频系数为2;
FLASH ARC配置
再看下面的代码就没有那么怕了,直接明白,目的就是要给FLASH的ACR寄存器赋值:
*((uint32_t *)FLASH_ACR)|=0x2;
不过这次要找FLash的ACR似乎并不那么顺利,很难直接从目录中查找到,需要全局搜一下ACR,不过还好,没有让我们搜索太多时间,在3.3.3章节中找到了:
寄存器的定义如下:
关于16进制转2进制补0
这里其实有一个补0问题,就是对于32bit,0x2究竟是0x0000 0002,还是0x2000 0000呢?回归本源,这两种表达形式哪一个是2呢?毫无疑问,是第一个;
所以这里的0x2,切换到32bit二进制表示就是:0000 0000 0000 0000 0000 0000 0000 0010;可以看到其实有效赋值是LATENCY,位的值是010,查看寄存器定义:
代表了比例,系统时钟比FLASH存取周期的值,010对应的2,即系统时间是Flash的两倍,所以同步的时候,需要做两个周期的延迟用以同步CPU和Flash之间时钟;
为什么需要做此配置呢?首先程序都是存放在FLASH里面,CPU需要从FLASH里面取出执行指令,所以通信之前需要首先同步时钟;那么就需要知道Flash的时钟和系统时钟(CPU时钟)之间的差别;又因为我们配置时钟是72MHz,这里010的时钟范围包含了72MHz;
使能PLL
下面是对于RCC_CR寄存器最后的配置:
*((uint32_t *)RCC_CR) |= 0x01000000;
while(!(*((uint32_t *)RCC_CR) >> 25));
还是套路:拆解0x0100 0000,只有高16位有有效值,低16位全零;高16位转换为2进制之后值为:0000 0001 0000 0000,所以我们看到对应操作的是PLLON位,PLLON位说明如下:
写入1则代表打开PLL,上面我们做了关于PLL的倍频的配置,PLL时钟源的配置,都是需要在PLL使能之后才会生效,在嵌入式开发中,所有的配置都是基于设备/ 组件使能的前提下才会配置生效;
至于while语句和上面配置HSE使能的语句的等待READY的功效是一致的;可以通过查看寄存器定义,25位是PLLRDY,while循环就是等待这一位置为1,是否需要担心其他位有1从而影响循环判断?大可不必,因为31~26都是Reserved,必然为0,唯一的有效位就是PLLRDY位。
可以看到在RCC初始化的代码中,每次配置一个使能都是需要通过while循环来确认配置成功了;
配置PLLCLK为系统时钟
最后一部分代码,让RCC_CFGR与0x00000002做或运算:
*((uint32_t *)RCC_CFGR) |= 0x00000002;
while(temp != 0x02)
{
temp = *((uint32_t *)RCC_CFGR) >> 2;
temp &= 0x03;
}
有效为发生在低16位,0000 0000 0000 0010,有效配置位是最后两位,即SW位(Switch,切换系统时钟之意),解释如下,可以看到10代表使用PLL的输出(PLLCLK)作为系统时钟:
之后的while语句则是轮训查看使能配置是否生效;注意这里的使能生效并没有采用上面单句while循环的方式,而是采用赋值方式来进行的,就是因为会有其他位配置会影响判断;
首先看一下SWS位说明:
赋值判断的方式也是非常巧妙,首先是RCC_CFGR右移(>>)2位,挤掉了SW位,现在SWS(Switch Status)位在最后面,然后将其和0x03进行与运算,与运算的目的就是:0位清零,1位保留原值(或运算目的:0位维持原值不变,1位用于置1)。
所以,和temp进行与运算0x03的其实是30bit数(最后两位已经通过右移2位挤掉了):0000 0000 0000 0000 0000 0000 0000 11,所以就是要取用SWS的位的值,当返回值为“10”的时候,即0x02,代表PLL已经被设置为系统时钟,说明RCC_CFGR的配置已经生效。