2.2 STM32单片机的时钟配置
一般而言,嵌入式系统在正式工作前,都要进行一些初始化工作,我们常把这个阶段写成一个子函数的形式,叫BSP_Init函数(Board Support Package,BSP,板级支持包)。
开发板初始化函数BSP_Init会调用3个函数:RCC_Configuration(复位和时钟设置),GPIO_Configuration(IO口设置),NVIC_Configuration(中断设置)。这里先介绍了前两个函数,第3个函数在后面讲解中断时再介绍。
void BSP_Init() { RCC_Configuration(); /*Configure the system clocks:系统时钟设置*/ GPIO_Configuration(); /*GPIO Configuration:设置I/O */ NVIC_Configuration(); /*NVIC Configuration:设置中断*/ }
我们先认识一下开发板初始化函数中的复位和时钟配置函数RCC_Configuration(Reset and Clock Configuration,RCC),它与STM32系列微控制器中的时钟有关。
STM32系列微控制器中的五个时钟源:HSI、HSE、LSI、LSE、PLL
① HSI(High Speed Internal)是高速内部时钟,RC振荡器,频率为8MHz(精度较差)。
② HSE(High Speed External)是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4~16MHz(精度高)。
③ LSI(Low Speed Internal)是低速内部时钟,RC振荡器,频率为30~60kHz。
④ LSE(Low Speed External)是低速外部时钟,接频率为32.768kHz的石英晶体,供实时时钟RTC使用。电路如图2.3(b)所示。
⑤ PLL(Phase Lock Loop)为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16倍,但是其输出频率最大不得超过72MHz。
锁相环的基本组成
PLL(Phase Locked Loop):锁相回路或叫锁相环。PLL用于振荡器中的反馈技术。许多电子设备要正常工作,通常需要外部的输入信号与内部的振荡信号同步,利用锁相环路就可以实现这个目的。锁相环是一种反馈控制电路,特点是:利用外部输入的参考信号控制环路内部振荡信号的频率和相位。因锁相环可以实现输出信号频率对输入信号频率的自动跟踪,所以锁相环通常用于闭环跟踪电路。
锁相环在工作的过程中,当输出信号的频率与输入信号的频率相等时,输出电压与输入电压保持固定的相位差值,即输出电压与输入电压的相位被锁住,这就是锁相环名称的由来。锁相环通常由鉴相器(Phase Detector,PD)、环路滤波器(Loop Filter,LF)和压控振荡器(Voltage Controlled Oscillator,VCO)三部分组成,锁相环组成的原理框图如图2.4所示。图中的鉴相器又称为相位比较器,它的作用是检测输入信号和输出信号的相位差,并将检测出的相位差信号转换成uD(t)电压信号输出,该信号经低通滤波器滤波后形成压控振荡器的控制电压uC(t),对振荡器输出信号的频率实施控制。输出频率fout与输入参考频率fr的关系为:fout = M*fr。
图2.4 锁相环组成的原理框图
STM32单片机的将时钟信号(常是HSE)经过分频或倍频(PLL)后,得到系统时钟,系统时钟经过分频,产生外设所使用的时钟。图2.5是STM32时钟系统结构图。
图2.5 STM32时钟系统结构图
其中,40kHz(典型值)的LSI供独立看门狗IWDG使用,另外它还可以被选择为实时时钟RTC的时钟源。实时时钟RTC的时钟源也可以选择LSE,或者是HSE的128分频。RTC的时钟源通过备份域控制寄存器(RCC_BDCR)的RTCSEL[1:0]来选择。
STM32中有一个全速功能的USB模块,其串行接口引擎需要一个频率为48MHz的时钟源。该时钟源只能从PLL输出端获取,可以选择为1.5分频或者1分频,也就是,当需要使用USB模块时,PLL必须使能,并且时钟频率配置为48MHz或72MHz。
另外,STM32还可以选择一个时钟信号输出到MCO脚(PA8)上,可以选择为PLL输出的2分频、HSI、HSE、或者系统时钟。
系统时钟SYSCLK,它是供STM32中绝大部分部件工作的时钟源。系统时钟可选择为PLL输出、HSI或者HSE。系统时钟最大频率为72MHz,它通过AHB分频器分频后送给各模块使用,AHB分频器可选择1、2、4、8、16、64、128、256、512分频。其中AHB分频器输出的时钟送给8大模块使用。
① 送给SDIO使用的SDIOCLK时钟。
② 送给FSMC使用的FSMCCLK时钟。
③ 送给AHB总线、内核、内存和DMA使用的HCLK时钟。
④ 通过8分频后送给Cortex的系统定时器时钟(SysTick)。
⑤ 直接送给Cortex的空闲运行时钟FCLK。
⑥ 送给APB1分频器。APB1分频器可选择1、2、4、8、16分频,其输出一路供APB1外设使用(PCLK1,最大频率36MHz),另一路送给定时器2、3、4倍频器使用。该倍频器可选择1或者2倍频,时钟输出供定时器2、3、4使用。
⑦ 送给APB2分频器。APB2分频器可选择1、2、4、8、16分频,其输出一路供APB2外设使用(PCLK2,最大频率72MHz),另一路送给定时器1倍频器使用。该倍频器可选择1或者2倍频,时钟输出供定时器1使用。另外,APB2分频器还有一路输出供ADC分频器使用(可选择为2、4、6、8分频),分频后得到ADCCLK时钟,送给ADC模块使用。
⑧ 2分频后送给SDIO AHB接口使用(HCLK/2)。
AMBA片上总线
片上总线标准种类繁多,而由ARM公司推出的AMBA片上总线受到了广大IP开发商和SoC(System on Chip)片上系统集成者的青睐,已成为一种流行的工业标准片上结构。AMBA规范主要包括了AHB(Advanced High performance Bus)系统总线和APB(Advanced Peripheral Bus)外设总线。二者分别适用于高速与相对低速设备的连接。
时钟输出的使能控制
在以上的时钟输出中,有很多是带使能控制的,如AHB总线时钟、内核时钟、各种APB1外设、APB2外设等。当需要使用某个外设模块时,记得一定要先使能对应的时钟,否则这个外设不能工作。因此,使用任何一个外设都必须打开相应的时钟。这样的好处就是,如果不使用一个外设时,就把它的时钟关掉,从而可以降低系统的功耗,达到节能,实现低功耗的效果。当STM32单片机系统时钟为72MHz时,在运行模式下,打开全部外设时的功耗电流为36mA,关闭全部外设时的功耗电流为27mA。
需要注意的是定时器2、3、4的倍频器,当APB1的分频为1时,它的倍频值为1(且只能为1,因为不能高于AHB频率),此时,定时器的时钟频率等于APB1的频率;当APB1的预分频系数为其他数值(即预分频系数为2、4、8或16)时,它的倍频值就为2。连接在APB1(低速外设)上的设备有电源接口、备份接口、CAN、USB、I2C1、I2C2、UART2、UART3、SPI2、窗口看门狗、Timer2、Timer3、Timer4。注意USB模块虽然需要一个单独的48MHz时钟信号,但它应该不是供USB模块工作的时钟,而只是提供给串行接口引擎(SIE)使用的时钟。USB模块工作的时钟是由APB1提供的。连接在APB2(高速外设)上的设备有:UART1、SPI1、Timer1、ADC1、ADC2、所有普通I/O口(PA~PE)、第二功能I/O口。
为什么ARM时钟这么复杂?
大家可能看到了,基于ARM Cortex-M3的STM32单片机时钟很复杂,与51单片机相差太大。标准51单片机很简单,外部晶振12分频就是机器频率,即51单片机工作的基准频率,增强型51(如C8051)也不过是外部晶振频率直接是机器频率而已。
其实,随着芯片工艺的发展,台式机和嵌入式系统的处理器频率越来越快,而处理器除了中央处理单元(CPU)外,还有一些外设接口,如串口,它们的时钟并没有那么快。如果中央处理单元与外设接口公用一样的时钟,那么中央处理单元在同一时间内要做很多事情,外设接口才能做一件事情,如数据存取。设想一下,当中央处理单元等待外设接口传来一个数据时,岂不是要等到很久。这样中央处理单元的性能就不能发挥出来,而且外设接口也没必要提供太高的时钟。另一个原因就是时钟分开有助于实现低功耗。
因此,现在的嵌入式系统处理器常常将时钟分开,有供中央处理单元使用的,也有供外设接口使用的。不仅仅是基于ARM内核的芯片,很多其他32位嵌入式处理器也是这样。
复位和时钟配置函数RCC_Configuration
时钟的具体配置是从RCC配置寄存器开始。定义RCC配置寄存器的是结构体RCC_TypeDef,在文件“stm32f10x_map.h”中定义如下:
typedef struct { vu32 CR; //时钟控制寄存器:Clock control register vu32 CFGR; //时钟配置寄存器:Clock configuration register vu32 CIR; //时钟中断寄存器:Clock interrupt register vu32 APB2RSTR; //APB2外设复位寄存器:APB2 Peripheral reset register vu32 APB1RSTR; //APB1外设复位寄存器:APB1 Peripheral reset register vu32 AHBENR; //AHB外设时钟使能寄存器:AHB Peripheral Clock enable register vu32 APB2ENR; //APB2外设时钟使能寄存器:APB2 Peripheral Clock enable register vu32 APB1ENR; //APB1外设时钟使能寄存器:APB1 Peripheral Clock enable register vu32 BDCR; //备份域控制寄存器:Backup domain control register vu32 CSR; //控制/状态寄存器:Control/status register } RCC_TypeDef; … #define PERIPH_BASE ((u32)0x40000000) … #define AHBPERIPH_BASE (PERIPH_BASE+0x20000) … #define RCC_BASE (AHBPERIPH_BASE+0x1000) … #ifdef _RCC #define RCC ((RCC_TypeDef*)RCC_BASE) #endif
其中,vu32代表一个32位的无符号长整形数,在文件“stm32f10x_type.h”中定义了:
typedef signed long s32; typedef signed short s16; typedef signed char s8; typedef volatile signed long vs32; typedef volatile signed short vs16; typedef volatile signed char vs8; typedef unsigned long u32; typedef unsigned short u16; typedef unsigned char u8; typedef unsigned long const uc32; /*Read Only*/ typedef unsigned short const uc16; /*Read Only*/ typedef unsigned char const uc8; /*Read Only*/ typedef volatile unsigned long vu32; typedef volatile unsigned short vu16; typedef volatile unsigned char vu8; typedef volatile unsigned long const vuc32; /*Read Only*/ typedef volatile unsigned short const vuc16; /*Read Only*/ typedef volatile unsigned char const vuc8; /*Read Only*/ typedef enum {FALSE = 0, TRUE = !FALSE} bool; typedef enum {RESET = 0, SET = !RESET} FlagStatus, ITStatus; typedef enum {DISABLE = 0, ENABLE = !DISABLE} FunctionalState; #define IS_FUNCTIONAL_STATE(STATE) ((STATE == DISABLE) || (STATE == ENABLE)) typedef enum {ERROR = 0, SUCCESS = !ERROR} ErrorStatus;
从上面的几个宏定义可以看出,在程序中所有写RCC的地方,编译器的预处理程序将它替换成((RCC_TypeDef *) 0x40021000)。其实,这个地址是RCC寄存器组的首地址,RCC寄存器映像和复位值如表2.1 所示,这些寄存器的具体定义和使用方式参见芯片数据手册。关于STM32处理器的存储映射参见附录,这里不再赘述。
表2.1 RCC寄存器映像和复位值表
volatile关键字的含义
一个定义为volatile的变量是说这变量是易变的,可能会被意想不到地改变,它们的值可能由于程序控制之外的事件而被潜在改变。这样,编译器就不会去假设这个变量的值了。准确地说就是,编译器优化时,在用到这个变量时必须每次都重新读取这个变量的值,即每次读写都必须访问实际地址存储器的内容,而不是使用保存在寄存器中的副本。
在嵌入式系统中,volatile大量地用来描述一个对应于内存映射的输入/输出端口,或者硬件寄存器(如状态寄存器)。
进一步讲解volatile
那为什么编译器会将没有被volatile修饰的变量在寄存器里保存个备份呢?这往往是基于程序运行效率的考虑,因为从寄存器里取数据要更快些,寄存器是在嵌入式处理器内核中,而从实际的存储器地址(往往是外设)访问会慢些。
这个问题往往是区分C程序员和嵌入式系统程序员的最基本问题。嵌入式工程师经常同硬件、中断、RTOS等打交道,所有这些都要求用到volatile变量。不懂得volatile的含义就将会给嵌入式系统软件带来缺陷,发生不可预料甚至灾难性的后果。
其次,中断服务例程中使用的非自动变量或者多线程应用程序中多个任务共享的变量也必须使用volatile进行限定。例如代码:
int flag=0; void f(){ while(1){ if(flag) some_action(); } } void isr_f(){ flag=1; }
如果没有使用volatile限定flag变量,编译器看到在f()函数中并没有修改flag,可能只执行一次flag读操作并将flag的值缓存在寄存器中,以后每次访问flag(读操作)都使用寄存器中的缓存值而不进行存储器绝对地址访问,导致some_action函数永远无法执行,即使中断函数isr_f()执行了将flag置1。
下面,我们看看复位和时钟配置函数RCC_Configuration。
ErrorStatus HSEStartUpStatus; /* 枚举变量,定义高速时钟的起动状态*/ … void RCC_Configuration (void) { /* 将外设RCC寄存器重设为默认值,即有关寄存器复位,但该函数不改动寄存器RCC_CR的 HSITRIM[4:0]位,也不重置寄存器RCC_BDCR和寄存器RCC_CSR */ RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); /* 使能外部HSE高速晶振*/ /* 等待HSE高速晶振稳定,或者在超时的情况下退出*/ HSEStartUpStatus = RCC_WaitForHSEStartUp(); /* SUCCESS:HSE晶振稳定且就绪,ERROR:HSE晶振未就绪 */ if (HSEStartUpStatus == SUCCESS) { /*HCLK=SYSCLK设置高速总线时钟=系统时钟*/ RCC_HCLKConfig(RCC_SYSCLK_Div1); /*PCLK2=HCLK设置低速总线2时钟=高速总线时钟*/ RCC_PCLK2Config(RCC_HCLK_Div1); /*PCLK1=HCLK/2 设置低速总线1的时钟=高速时钟的二分频*/ RCC_PCLK1Config(RCC_HCLK_Div2); /* 设置FLASH存储器延时时钟周期数,2是针对高频时钟的, FLASH_Latency_0:0延时周期,FLASH_Latency_1:1延时周期 FLASH_Latency_2:2延时周期 */ FLASH_SetLatency(FLASH_Latency_2); /* 使能flash预取指令缓冲区。这两句跟RCC没直接关系*/ FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); /* Set PLL clock output to 72MHz using HSE (8MHz) as entry clock */ /* 利用锁相环将HSE外部8MHz晶振9倍频到72MHz*/ RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); /* Enable PLL:使能PLL锁相环*/ RCC_PLLCmd(ENABLE); /* Wait till PLL is ready:等待锁相环输出稳定 */ /* RCC_FLAG_HSIRDY:HSI晶振就绪,RCC_FLAG_HSERDY:HSE晶振就绪 RCC_FLAG_PLLRDY:PLL就绪,RCC_FLAG_LSERDY:LSE晶振就绪 RCC_FLAG_LSIRDY:LSI晶振就绪,RCC_FLAG_PINRST:引脚复位 RCC_FLAG_PORRST:POR/PDR复位,RCC_FLAG_SFTRST:软件复位 RCC_FLAG_IWDGRST:IWDG复位,RCC_FLAG_WWDGRST:WWDG复位 RCC_FLAG_LPWRRST:低功耗复位 */ while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) { } /* Select PLL as system clock source:将锁相环输出设置为系统时钟 */ /* RCC_SYSCLKSource_HSI:选择HSI作为系统时钟 RCC_SYSCLKSource_HSE:选择HSE作为系统时钟 RCC_SYSCLKSource_PLLCLK:选择PLL作为系统时钟*/ RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); /* 等待PLL作为系统时钟标志位置位*/ /* 0x00:HSI作为系统时钟;0x04:HSE作为系统时钟 0x08:PLL作为系统时钟 */ while (RCC_GetSYSCLKSource() != 0x08) { } } /* Enable GPIOA~E and AFIO clocks:使能外围端口总线时钟。注意各外设的隶属情况,不同芯片 和开发板的分配不同*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE | RCC_APB2Periph_AFIO, ENABLE); /* USART1 clock enable:USART1时钟使能 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); /* TIM1 clock enable:TIM1时钟使能 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); /* TIM2 clock enable:TIM2时钟使能*/ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); /* ADC1 clock enable:ADC1时钟使能*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); }
在初始化阶段,RCC_Configuration函数完成系统的复位和时钟设置。这些函数的具体实现在库文件“stm32f10x_rcc.c”中(\library\src目录下)。复位和时钟设置函数RCC_Configuration中的第一条语句是RCC_DeInit(),其作用是复位定义在结构体RCC_TypeDef中的各个RCC配置寄存器。教学开发板上有一个8MHz的晶振,将PLL设置为9 倍频,这样系统时钟为72MHz(STM32F103 增强型单片机最高工作频率为72MHz),高速总线和低速总线2 都为72MHz,低速总线1为36MHz。这里应注意:PLL的设定需要在使能之前,一旦PLL使能后参数不可更改。
由上述程序可以看出,系统时钟的设定是比较复杂的,外设越多,需要考虑的因素就越多。这种设定是有规律可循的,设定参数也很规范。
例如,加入RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA, ENABLE);语句,则使能DMA外设时钟。如果你想给模数转换器(ADC)设置时钟,可以在if(HSEStartUpStatus ==SUCCESS)逻辑块中加入:
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
这样,ADC的时钟就设为12MHz,即系统时钟的6分频。
注意:由于USB时钟的数据传输标准为48MHz,因此你需经过1.5分频设置才可实现。
时钟设置
一般的,时钟设置需要先考虑系统时钟的来源,是内部RC、外部晶振、还是外部的振荡器,是否需要PLL。然后再考虑内部总线和外部总线,最后考虑外设的时钟信号。遵从先倍频作为CPU时钟,然后再由内向外分频的原则。
要注意的是,STM32处理器因为低功耗的需要,各模块需要分别独立开启时钟,所以,一定不要忘记给用到的模块和引脚使能时钟。
系统复位后,HSI振荡器被选为系统时钟。当时钟源被直接或通过PLL间接作为系统时钟时,它将不能被停止。只有当目标时钟源准备就绪了(经过启动稳定阶段的延迟或PLL稳定),从一个时钟源到另一个时钟源的切换才会发生。在被选择时钟源没有就绪前,系统时钟的切换不会发生;直至目标时钟源就绪,才发生切换。时钟控制寄存器(RCC_CR)中的状态位指示了哪个时钟已经准备好了,哪个时钟目前被用做系统时钟。
STM32单片机的时钟安全系统
时钟安全系统(CSS)可以通过软件被激活。一旦其被激活,时钟监测器将在HSE振荡器启动延迟后被使能,并在HSE时钟关闭后关闭。如果HSE时钟发生故障,HSE振荡器被自动关闭,时钟失效事件将被送到高级定时器(TIM1和TIM8)的刹车输入端,并产生时钟安全中断CSSI,允许软件进行紧急处理操作。此CSSI中断连接到Cortex-M3的NMI中断(不可屏蔽中断)。关于STM32单片机的中断,在后面的章节再做介绍。
注意:一旦CSS被激活,并且HSE时钟出现故障,CSS中断就产生,并且NMI也自动产生。NMI将被不断执行,直到CSS中断挂起位被清除。因此,在NMI的处理程序中必须通过设置时钟中断寄存器(RCC_CIR)里的CSSC位来清除CSS中断。如果HSE振荡器被直接或间接(经PLL倍频)地作为系统时钟,时钟故障将导致系统时钟自动切换到HSI振荡器,同时外部HSE振荡器被关闭。在时钟失效时,如果HSE振荡器时钟是用做系统时钟的PLL的输入时钟,PLL也将被关闭。因此,STM32单片机的时钟系统具有很高的安全型。