时钟树

时钟树很重要,必须梳理清楚,详细参考《STM32中文参考手册》第6章。

系统刚上电时用的是系团内部8M RC时钟。
我使用的开发板外部晶振频率8M。

时钟配置

首先把一些常用的宏抽出来单独放在common.h中,当前主要包含位操作和位带操作,代码如下:
common.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#ifndef COMMON_H
#define COMMON_H
#include "stm32f10x.h"

#define SETUP_BIT(data, bit) ((data) |= 1UL << (bit))
#define CLR_BIT(data, bit) ((data) &= ~(1UL << (bit)))
#define GET_BIT(data, bit) ((data >> (bit)) & 1UL)
#define BITS_MASK(startbit, endbit) ((1UL << ((startbit)-(endbit)+1)) - 1)
#define SET_BITS(data, startbit, endbit, val) (data)&=~(BITS_MASK(startbit, endbit)<<endbit);data|=((val&BITS_MASK(startbit, endbit))<<endbit);
#define GET_BITS(data, startbit, endbit) (((data) >> (endbit)) & BITS_MASK(startbit, endbit))


#define BITBAND(addr, bit) (((addr) & 0xE0000000) + 0x2000000 + ((addr) & 0x1FFFFFFF) * 8 * 4 + (bit) * 4)
#define MEM_ADDR(addr) (*((volatile unsigned long *)(addr)))
#define BIT_ADDR(addr, bit) MEM_ADDR(BITBAND(addr, bit))

//IO 口地址映射
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C
#define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C
#define GPIOD_ODR_Addr (GPIOD_BASE+12) //0x4001140C
#define GPIOE_ODR_Addr (GPIOE_BASE+12) //0x4001180C
#define GPIOF_ODR_Addr (GPIOF_BASE+12) //0x40011A0C
#define GPIOG_ODR_Addr (GPIOG_BASE+12) //0x40011E0C
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08
#define GPIOC_IDR_Addr (GPIOC_BASE+8) //0x40011008
#define GPIOD_IDR_Addr (GPIOD_BASE+8) //0x40011408
#define GPIOE_IDR_Addr (GPIOE_BASE+8) //0x40011808
#define GPIOF_IDR_Addr (GPIOF_BASE+8) //0x40011A08
#define GPIOG_IDR_Addr (GPIOG_BASE+8) //0x40011E08

#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr, n)
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr, n)
#define PEout(n) BIT_ADDR(GPIOE_ODR_Addr, n)
#define PEin(n) BIT_ADDR(GPIOE_IDR_Addr, n)

#endif

因为STM官方的库文件中已经定义了SET_BIT宏,且含义跟我定义的不同,我只能先改成SETUP_BIT了。

接下来是时钟初始化代码,位于sys.c中,sys.c目前只包含时钟初始化一个函数:
sys.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "common.h"

void Stm32_Clock_Init(void)
{
// 1. 外部晶振配置
CLR_BIT(RCC->CFGR, 17); // 外部晶振不分频
SETUP_BIT(RCC->CR, 16); // 开启外部晶振
while (!GET_BIT(RCC->CR, 17)) ; // 等待外部晶振稳定

// 2. 配置APB1 APB2等时钟
SET_BITS(RCC->CFGR, 10, 8, 0x4); // APB1 2分频,36M。定时器2~7频率=PCLK1x2 = 72M
SET_BITS(RCC->CFGR, 13, 11, 0x0); // APB2不分频,72M。定时器1和8频率=72M
SET_BITS(RCC->CFGR, 15, 14, 0x2); // ADC预分频器=72/6=12M,得到ADCCLK(最大14M)
SET_BITS(RCC->CFGR, 7, 4, 0x0); // AHB不分频,72M。至AHB总线、核心存储器、DMA
// HCLK/8 = 9M,得到Cortex系统时钟,即滴答时钟SYSTICK(无需配置)
// FCLK = HCLK = 72M,Cortex自由运行时钟(无需配置)
// HCLK/2=36M,得到至SDIO的AHP接口(无需配置)

// 3. 配置并使能PLL
SET_BITS(RCC->CFGR, 21, 18, 0x7); // 选择PLL倍频选择x9
SETUP_BIT(RCC->CFGR, 16); // 选择PLLSRC为外部晶振
FLASH->ACR|=0x32; //FLASH 2 个延时周期
SETUP_BIT(RCC->CR, 24); // 使能PLL时钟
while (!GET_BIT(RCC->CR, 25)) ; // 等待PLL时钟锁定

// 4. PLL锁定后,切换系统时钟源为PLL,得到72M的SYSCLk
SET_BITS(RCC->CFGR, 1, 0, 0x2); // 选择PLL作为系统时钟源
while (GET_BITS(RCC->CFGR, 3, 2) != 0x2) ; // 等待系统时钟切换为PLL

// 5. RTCSEL[1:0],RTC时钟源选择LSE(32.768kHz),即外部慢速时钟
//备份域控制寄存器中(RCC_BDCR)的LSEON、LSEBYP、RTCSEL和RTCEN位处于备份域。因此,这些位在复位后处于写保护状态,只有在电源控制寄存器(PWR_CR)中的DBP位置’1’后才能对这些位进行改动。进一步信息请参考5.1节。这些位只能由备份域复位清除(见6.1.3节)。任何内部或外部复位都不会影响这些位。
SETUP_BIT(RCC->BDCR, 0); // 使能LSE
while (!GET_BIT(RCC->BDCR, 1)) ; // 等待外部低速LSE就绪
SET_BITS(RCC->BDCR, 9, 8, 1); // 选择LSE作为RTC时钟
SETUP_BIT(RCC->BDCR, 15); // 使能RTC

// 6. 看门狗时钟
// 看门狗使能后,LSI将被强制打开,并且不能被关闭。
}

RTC和看门狗后续章节会讲到。

SysTick

  由于后续的章节里面都会用到延时这一功能,所以我们这里先实现出来,延时基于SysTick。
  SysTick其实是一个捆绑在NVIC中的24位定时器,并且具有专用的中断号,一般用来给操作系统提供“时基”,或者说驱动操作系统。由于我们还没有用到操作系统,所以这里只用它的定时器功能,不开启中断。

  在《Cortex-M3权威指南》第13.1节中有说:SysTick有两个时钟源:“内核自由运行时钟”FCLK和外部时钟。
  在《STM32中文参考手册》的第6.2节有说明:RCC通过AHB时钟(HCLK)8分频后作为Cortex系统定时器(SysTick)的外部时钟。通过对SysTick控制与状态寄存器的设置,可选择上述时钟或Cortex(HCLK)时钟作为SysTick时钟。
  延时功能的初始化即SysTick的初始化,我们把延时相关的功能放在delay.c这个单独的文件中。在delay_init()函数中,选择SysTick的时钟源,并定义了一个变量tick_per_us,在后续的延时函数中会使用这个变量将延时时间转换为tick数。由于SysTick的时钟频率为9MHz,所以tick_per_us=9,以下是delay.c代码
delay.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include "common.h"

unsigned char tick_per_us; // 每1us的tick数
void delay_init(unsigned char SYSCLK)
{
tick_per_us = SYSCLK / 8; // 因为SysTick时钟为SYSCLK的8分频
CLR_BIT(SysTick->CTRL, 2); // 选择外部时钟
}

// 支持传入参数大于24位,最大延时时间4294967296/8 = 536870911us = 536s
void delay_us(unsigned us)
{
unsigned tick = us * tick_per_us;
unsigned tmp, temp1;
// 重装在寄存器只有低24位有效,所以超过24位的情况下要延时多次
while (tick) {
tmp = (tick >= 0xFFFFFF ? 0xFFFFFF : tick);
SysTick->LOAD = tmp; // 写入重装载值
SysTick->VAL = 0; // 往VAL写任意值清除COUNTFLAG标志
SysTick->CTRL = 1; // 使能SysTick
// 等待减到0,加入使能判断的原因是防止定时器在其他地方被关掉而卡死在这里
do
{
temp1=SysTick->CTRL;
}while((temp1&0x01)&&!(temp1&(1<<16))); //等待时间到达
SysTick->CTRL = 0; // 关闭SysTick
SysTick->VAL = 0;
tick -= tmp;
}
}

void delay_ms(unsigned ms)
{
delay_us(ms * 1000);
}

这里分享一个我调试过程中遇到的问题,在我下一章节GPIO中,使用这个延时函数进行LED闪烁测试的时候,发现延时不准确,比实际的时间长,并且忽快忽慢。经过一番排查,终于找到问题所在了,下面放上问题代码和正确代码的对比:
错误代码

1
2
// 等待减到0,加入使能判断的原因是防止定时器在其他地方被关掉而卡死在这里
while (GET_BIT(SysTick->CTRL, 0) && !GET_BIT(SysTick->CTRL, 16)) ;

正确代码

1
2
3
4
5
// 等待减到0,加入使能判断的原因是防止定时器在其他地方被关掉而卡死在这里
do
{
temp1=SysTick->CTRL;
}while((temp1&0x01)&&!(temp1&(1<<16))); //等待时间到达

  这两段代码的区别是,错误代码对CTRL寄存器访问了两次,而此寄存器中的COUNTFLAG位是读清的,所以第一次读取的时候此位已经被清掉了。按理来说代码应该永远卡在这个延时函数里才对,因为COUNTFLAG位在被读之前已经清掉了,但是我还是观察到了LED的闪烁,应该是因为读清需要一段时间,如果第二次读取足够快的话还是有一定几率能够读到COUNTFLAG位被置位的。