STM32是ST公司开发的32位微控制器,从内核上可以分为Cortex-M0、M3、M4和M7这几种,每个内核又大概分为主流、高性能和低功耗。本系列我们学习的是STM32F1系列,F1代表的是基础型,基于Cortex-M3内核,主频为72MHZ,而F4代表了高性能,基于Cortex-M4内核,主频是180M。
以STM32F103VET6为例
具体的引脚功能可以查看STM32F1数据手册。
寄存器本质上是一块有特定功能的内存单元,给有特定功能的内存单元取一个别名,这个别名就是我们经常说的寄存器。寄存器是位于中央处理器(CPU)内部的高速存储器,容量较小但速度非常快,用于临时存储和操作计算中的数据,可以通过寄存器的编号或名称直接访问寄存器。总而言之,寄存器是CPU内部的高速临时存储器,容量小但速度快,用于存储中间结果和指令和地址等。
1)ICode,我们写好的程序编译之后都是一条条指令,存放在FLASH中, 内核要读取这些指令来执行程序就必须通过ICode总线,它几乎每时每刻都需要被使用,它是专门用来取指的。
2)DCode,这条总线是用来取得数据的,常量数据放在内部的FLASH中,而变量数据(不管是局部还是全局)放在内部的SRAM中。DMA总线也可以访问数据,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。
3)System总线,系统总线主要是访问外设的寄存器,我们通常说的寄存器编程,即读写寄存器都是通过这根系统总线来完成的。
4)DMA总线,DMA总线也主要是用来传输数据,这个数据可以是在某个外设的数据寄存器,可以在SRAM,可以在内部的FLASH。它允许外设设备直接与主存储器进行数据交换,而无需通过中央处理器(CPU)进行介入。可以大大减轻CPU的负担。
1)FSMC,灵活的静态的存储器控制器,通过FSMC,我们可以扩展内存,如外部的SRAM,NANDFLASH和NORFLASH。但有一点我们要注意的是,FSMC只能扩展静态的内存, 即名称里面的S:static,不能是动态的内存,比如SDRAM就不能扩展。
2) FLASH,内部的闪存,一般存放我们编写好的程序,可以通过ICode总线被内核取走。
3)SRAM,即我们通常说的RAM,程序的变量,堆栈等的开销都是基于内部的SRAM。内核通过DCode总线来访问它。
4)AHB,AHB总线又延伸出了APB1和APB2两条总线,其中APB1是低速的,APB2和AHB都是高速的,我们经常说的GPIO、串口、I2C、SPI这些外设就挂载在这两条总线上, 这个是我们学习STM32的重点,就是要学会编程这些外设去驱动外部的各种设备。
在XX外设的地址范围内,分布着的就是该外设的寄存器。以GPIO端口位设置/清除寄存器为例来理解寄存器。
1)寄存器名称,X代表A-E,即GPIOA到FGPIOE都有一个这样的寄存器。
2)偏移地址,此地址是寄存器相对于基地址的偏移量。
3)寄存器位表,表上方的数字为位编号,中间为位名称,最下方为读写权限,其中w表示只写, r表示只读,rw表示可读写。
4)位功能说明,位功能是寄存器说明中最重要的部分,它详细介绍了寄存器每一个位的功能。
//定义一个变量 A= 1001 1111 b(二进制)
unsigned char A = 0x9f;
//对bit2 清零
A &= ~(1<<2);
//括号中的1左移两位,(1<<2)得二进制数:0000 0100 b
//按位取反,~(1<<2)得1111 1011 b
//假如A中原来的值为二进制数: A = 1001 1111 b
//所得的数与A作”位与&”运算,A = (1001 1111 b)&(1111 1011 b),
//经过运算后,A的值 A=1001 1011 b
// A的bit2 位被清零,而其它位不变。
//若把a中的二进制位分成2个一组
//即bit0、bit1为第0组,bit2、bit3为第1组,
// bit4、bit5为第2组,bit6、bit7为第3组
//要对第1组的bit2、bit3清零
a &= ~(3<<2*1);
//括号中的3左移两位,(3<<2*1)得二进制数:0000 1100 b
//按位取反,~(3<<2*1)得1111 0011 b
//假如a中原来的值为二进制数: a = 1001 1111 b
//所得的数与a作”位与&”运算,a = (1001 1111 b)&(1111 0011 b),
//经过运算后,a的值 a=1001 0011 b
// a的第1组的bit2、bit3被清零,而其它位不变。
//&相同为1,相异为0
//上述(~(3<<2*1))中的(1)即为组编号;如清零第3组bit6、bit7此处应为3
//括号中的(2)为每组的位数,每组有2个二进制位;若分成4个一组,此处即为4
//括号中的(3)是组内所有位都为1时的值;若分成4个一组,此处即为二进制数“1111 b”
//例如对第2组bit4、bit5清零
a &= ~(3<<2*2);
//a = 1000 0011 b
//此时对清零后的第2组bit4、bit5设置成二进制数“01 b ”
a |= (1<<2*2);
//a = 1001 0011 b,成功设置了第2组的值,其它组不变
//| 一真则真
//此处先将(1<<2*2)左移两位得到 0001 0000,再和a相或|,得到a = 1001 0011 b
//a = 1001 0011 b
//把bit6取反,其它位不变
a ^=(1<<6);
//^相同为0,相异为1
//1<<6 == 0100 0000
//a = 1101 0011 b
首先先新建一个文件夹,此处以 “LED” 为名,然后在该目录下新建2个文件夹,并且在"LED“中包含 startup_stm32f10x_hd.s、main.c、stm32f10x.h等文件,具体如下:
然后打开KEIL5,新建一个工程,工程名根据喜好命名,我这里取LED-REG,直接保存在LED文件夹下。
接着选择所需的CPU型号,在魔术棒选项下的Device中选择。
由于用寄存器控制STM32时我们不需要在线添加库文件,所以我们直接添加文件。
添加文件的时候只需要在keil左侧任务栏对应的根文件夹双击添加即可,此处主要包括三个文件夹。
3.2.1 startup_stm32f10x_hd.
此文件是启动文件,系统上电后第一个运行的程序,由汇编编写,C编程用的比较少,可暂时不管,这个文件从固件库里面拷贝而来,由官方提供。
3.2.2 stm32f10x.h
用户手动新建,用于存放寄存器映射的代码,暂时为空。
3.2.3 main.c
用户手动新建,用于存放main函数,暂时为空。
为了工程目录更加清晰,我们在本地电脑上新建一个“工程模板”文件夹,在它之下再新建6个文件夹,具体如下:
其中包含的文件及其作用如下:
首先,为了能够使用printf函数,我们需要勾选Target中选中微库“ Use MicroLib”。
其次,Output选项卡中把输出文件夹定位到我们工程目录下的output文件夹, 如果想在编译的过程中生成hex文件,那么那Create HEX File选项勾上。
配置好了之后按照自己的下载器和开发板再进行配置相关的Debug等选项,就可以编译下载程序了。
GPIO是通用输入输出端口的简称,简单来说就是STM32可控制的引脚,STM32芯片的GPIO引脚与外部设备连接起来,从而实现与外部通讯、 控制以及数据采集的功能。其最基本的输出功能是由STM32控制引脚输出高、低电平,实现开关控制。而最基本的输入功能是检测外部输入电平,如把GPIO引脚连接到按键,通过电平高低区分按键是否被按下。
GPIO端口的每个位都可以由软件分别配置成多种模式:
─ 输入浮空
─ 输入上拉
─ 输入下拉
─ 模拟输入
─ 开漏输出
─ 推挽式输出
─ 推挽式复用功能
─ 开漏复用功能
每个I/O端口位可以自由编程,然而I/0端口寄存器必须按32位字被访问(不允许半字或字节访
问)。GPIOx_BSRR和GPIOx_BRR寄存器允许对任何GPIO寄存器的读/更改的独立访问;这
样,在读和更改访问之间产生IRQ时不会发生危险。
引脚的两个保护二级管可以防止引脚外部过高或过低的电压输入,当引脚电压高于VDD(VDD是指电路的正电源电压,也称为电源供电电压)时, 上方的二极管导通,当引脚电压低于VSS(VSS是指电路的负电源电压,也称为地或零电位)时,下方的二极管导通,防止不正常电压引入芯片导致芯片烧毁。即使如此,芯片引脚也不能直接外接大功率器件,必须要加大功率及隔离电路驱动。
输出模式的线路经过一个由P-MOS和N-MOS管组成的单元电路。 这个结构使GPIO具有了“推挽输出”和“开漏输出”两种模式。
推挽输出结构是由两个MOS或者三极管收到互补控制的信号控制,两个管子时钟一个在导通,一个在截止,推挽输出的最大特点是可以真正能真正的输出高电平和低电平,在两种电平下都具有驱动能力。
在该结构中输入高电平时,经过反向后,上方的P-MOS导通,下方的N-MOS关闭, 对外输出高电平;而在该结构中输入低电平时,经过反向后,N-MOS管导通,P-MOS关闭,对外输出低电平。当引脚高低电平切换时,两个管子轮流导通, P管负责灌电流,N管负责拉电流,使其负载能力和开关速度都比普通的方式有很大的提高。推挽输出的低电平为0伏,高电平为3.3伏。
推挽输出具有:输出高低电平和电源电压基本没有压差、高低电平驱动能力较强,一般数字芯片推挽输出IO口驱动电流最大可达20mA以及电平切换速度快的优点,但是其不支持线与。线与指的的两个推挽输出的输出接在一起,此时如果上面的输出1,下面的输出0的话可能造成短路。
而在开漏输出模式时,上方的P-MOS管完全不工作。如果我们控制输出为0,低电平,则P-MOS管关闭,N-MOS管导通,使输出接地, 若控制输出为1 (它无法直接输出高电平)时,则P-MOS管和N-MOS管都关闭,所以引脚既不输出高电平,也不输出低电平,为高阻态。它具有“线与”特性,也就是说,若有很多个开漏模式引脚连接到一起时, 只有当所有引脚都输出高阻态,才由上拉电阻提供高电平,此高电平的电压为外部上拉电阻所接的电源的电压。若其中一个引脚为低电平, 那线路就相当于短路接地,使得整条线路都为低电平,0伏。
由于开漏输出没有输出高电平的能力,所以在外部添加上拉电阻使其具有高电平驱动能力(驱动能力由上拉电阻决定)。
双MOS管结构电路的输入信号,是由GPIO“输出数据寄存器GPIOx_ODR”提供的,因此我们通过修改输出数据寄存器的值就可以修改GPIO引脚的输出电平。 而“置位/复位寄存器GPIOx_BSRR”可以通过修改输出数据寄存器的值从而影响电路的输出。
“复用功能输出”中的“复用”是指STM32的其它片上外设对GPIO引脚进行控制,此时GPIO引脚用作该外设功能的一部分,算是第二用途。 从其它外设引出来的“复用功能输出信号”与GPIO本身的数据据寄存器都连接到双MOS管结构的输入中,通过图中的梯形结构作为开关切换选择。
GPIO结构框图的上半部分,GPIO引脚经过内部的上、下拉电阻,可以配置成上/下拉输入,然后再连接到施密特触发器,信号经过触发器后, 模拟信号转化为0、1的数字信号,然后存储在“输入数据寄存器GPIOx_IDR”中,通过读取该寄存器就可以了解GPIO引脚的电平状态。
与“复用功能输出”模式类似,在“复用功能输入模式”时,GPIO引脚的信号传输到STM32其它片上外设,由该外设读取引脚状态。
当GPIO引脚用于ADC采集电压的输入通道时,用作“模拟输入”功能,此时信号是不经过施密特触发器的,因为经过施密特触发器后信号只有0、1两种状态, 所以ADC外设要采集到原始的模拟信号,信号源输入必须在施密特触发器之前。类似地,当GPIO引脚用于DAC作为模拟电压输出通道时,此时作为“模拟输出”功能, DAC的模拟信号输出就不经过双MOS管结构,模拟信号直接输出到引脚。
GPIO的8种工作模式大致可以分为三类:
在输入模式时,施密特触发器打开,输出被禁止,可通过输入数据寄存器GPIOx_IDR读取I/O状态。其中输入模式,可设置为上拉、 下拉、浮空和模拟输入四种。上拉和下拉输入很好理解,默认的电平由上拉或者下拉决定。浮空输入的电平是不确定的,完全由外部的输入决定, 一般接按键的时候用的是这个模式。模拟输入则用于ADC采集。
在输出模式中,推挽模式时双MOS管以轮流方式工作,输出数据寄存器GPIOx_ODR可控制I/O输出高低电平。开漏模式时,只有N-MOS管工作, 输出数据寄存器可控制I/O输出高阻态或低电平。输出速度可配置,有2MHz10MHz50MHz的选项。此处的输出速度即I/O支持的高低电平状态最高切换频率, 支持的频率越高,功耗越大,如果功耗要求不严格,把速度设置成最大即可。在输出模式时施密特触发器是打开的,即输入可用,通过输入数据寄存器GPIOx_IDR可读取I/O的实际状态。
复用功能模式中,输出使能,输出速度可配置,可工作在开漏及推挽模式,但是输出信号源于其它外设,输出数据寄存器GPIOx_ODR无效; 输入可用,通过输入数据寄存器可获取I/O实际状态,但一般直接用外设的寄存器来获取该数据信号。
GPIO的工作模式可以在端口配置高寄存器里面配置
本实验用到的引脚连接状态如图所示,只需要控制PB引脚的相关电平即可控制灯的亮灭。
首先,配置GPIO端口配置低寄存器CRL,CRL中包含0-7号引脚,每个引脚占用4个寄存器位。此处我们配置PB0为通用推挽输出,速度为10M(类似可以设置配置PB1,PB5) 。
然后,配置端口输出数据寄存器ODR。
最后,设置完GPIO的引脚,控制电平输出,还需要开启时钟。由于STM32的 外设很多,为了降低功耗, 每个外设都对应着一个时钟,在芯片刚上电的时候这些时钟都是被关闭的,如果想要外设工作,必须把相应的时钟打开。所有的 GPIO都挂载到 APB2 总线上,具体的时钟由APB2外设时钟使能寄存器(RCC_ APB2ENR)来控制。
需要点亮的LED灯的原理图如下:
只要我们控制GPIO引脚的电平输出状态,即可控制LED灯的亮灭。
软件设计的编程要点为:
使能GPIO端口时钟;
初始化GPIO目标引脚为推挽输出模式;
编写简单测试程序,控制GPIO引脚输出高、低电平。
此处使用宏常量定义了三种不同的颜色
// R-红色
#define LED1_GPIO_PORT GPIOB
#define LED1_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED1_GPIO_PIN GPIO_Pin_5
// G-绿色
#define LED2_GPIO_PORT GPIOB
#define LED2_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED2_GPIO_PIN GPIO_Pin_0
// B-蓝色
#define LED3_GPIO_PORT GPIOB
#define LED3_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED3_GPIO_PIN GPIO_Pin_1
配置LED的GPIO_Config函数为:
void LED_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct; //定义一个 GPIO_InitTypeDef 类型的结构体
RCC_APB2PeriphClockCmd(LED_G_GPIO_CLK, ENABLE); //使能时钟外设
GPIO_InitStruct.GPIO_Pin = LED_G_GPIO_PIN; //选择GPIO端口,比如GPIOA,GPIOB
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //选择输出模式为 推挽输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //选择传输速度为 50 HZ
GPIO_Init(LED_G_GPIO_PORT, &GPIO_InitStruct); //GPIO函数初始化 ,两个参数,前者
//是GPIO端口号,后者是 GPIO_InitTypeDef 类型的结构体,相当于将里面的参数初始化赋值。
}
整个函数的执行流程如下:
使用GPIO_InitTypeDef定义GPIO初始化结构体变量,以便下面用于存储GPIO配置。
调用库函数RCC_APB2PeriphClockCmd来使能LED灯的GPIO端口时钟,该函数有两个输入参数,第一个参数用于指示要配置的时钟,如本例中的“RCC_APB2Periph_GPIOB”, 应用时我们可以使用“|”操作同时配置3个LED灯的时钟;函数的第二个参数用于设置状态,可输入“Disable”关闭或“Enable”使能时钟。
向GPIO初始化结构体赋值,把引脚初始化成推挽输出模式,其中的GPIO_Pin使用宏“LEDx_GPIO_PIN”来赋值,使函数的实现方便移植。
使用以上初始化结构体的配置,调用GPIO_Init函数向寄存器写入参数,完成GPIO的初始化, 这里的GPIO端口使用“LEDx_GPIO_PORT”宏来赋值,也是为了程序移植方便。
使用同样的初始化结构体,只修改控制的引脚和端口,初始化其它LED灯使用的GPIO引脚。
使用宏控制RGB灯默认关闭。
由于按键的机械特性,通常在开关按下后不会立即断开或者接通,使得按键按下后会产生带波纹的信号,需要用软件或者硬件来消除滤波,本实验采用的是硬件消抖,利用电容的充放电的延时来实现,软件只需要检测引脚电平即可。
从按键的原理图可知,这些按键在没有被按下的时候,GPIO引脚的输入状态为低电平(按键所在的电路不通,引脚接地),当按键按下时, GPIO引脚的输入状态为高电平(按键所在的电路导通,引脚接到电源)。只要我们检测引脚的输入电平,即可判断按键是否被按下。
本例的编程要点:
使能GPIO端口时钟;
初始化GPIO目标引脚为输入模式(浮空输入);
编写简单测试程序,检测按键的状态,实现按键控制LED灯
首先将开关引脚以宏常量表示:
// 引脚定义
#define KEY1_GPIO_CLK RCC_APB2Periph_GPIOA
#define KEY1_GPIO_PORT GPIOA
#define KEY1_GPIO_PIN GPIO_Pin_0
#define KEY2_GPIO_CLK RCC_APB2Periph_GPIOC
#define KEY2_GPIO_PORT GPIOC
#define KEY2_GPIO_PIN GPIO_Pin_13
然后编写开关控制函数
void Key_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*开启按键端口的时钟*/
RCC_APB2PeriphClockCmd(KEY1_GPIO_CLK|KEY2_GPIO_CLK,ENABLE);
//选择按键的引脚
GPIO_InitStructure.GPIO_Pin = KEY1_GPIO_PIN;
// 设置按键的引脚为浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
//使用结构体初始化按键
GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStructure);
//选择按键的引脚
GPIO_InitStructure.GPIO_Pin = KEY2_GPIO_PIN;
//设置按键的引脚为浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
//使用结构体初始化按键
GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStructure);
}
然后进行检测开关的状态(通过引脚电平来检测)
/** 按键按下标置宏
* 按键按下为高电平,设置 KEY_ON=1, KEY_OFF=0
* 若按键按下为低电平,把宏设置成KEY_ON=0 ,KEY_OFF=1 即可
*/
#define KEY_ON 1
#define KEY_OFF 0
/**
* @brief 检测是否有按键按下
* @param GPIOx:具体的端口, x可以是(A...G)
* @param GPIO_PIN:具体的端口位, 可以是GPIO_PIN_x(x可以是0...15)
* @retval 按键的状态
* @arg KEY_ON:按键按下
* @arg KEY_OFF:按键没按下
*/
uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin)
{
/*检测是否有按键按下 */
if (GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON ) {
/*等待按键释放 */
while (GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON);
return KEY_ON;
} else
return KEY_OFF;
}
函数 GPIO_ReadInputDataBit 读取的是GPIOX_IDR,读的是当 IO 口设置为输入状态时候的 IO 口电平状态值。
位操作就是可以单独的对一个比特位读和写, STM32没有这样的关键字,而是通过访问位带别名区来实现。在STM32中,有两个地方实现了位带,一个是SRAM区的最低1MB空间,另一个是外设区最低1MB空间。 这两个1MB的空间除了可以像正常的RAM一样操作外,他们还有自己的位带别名区,位带别名区把这1MB的空间的每一个位膨胀成一个32位的字, 当访问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。
外设外带区的地址为:0X40000000~0X40100000,大小为1MB,这1MB的大小在103系列大/中/小容量型号的单片机中包含了片上外设的全部寄存器, 这些寄存器的地址为:0X40000000~0X40029FFF。外设位带区经过膨胀后的位带别名区地址为:0X42000000~0X43FFFFFF, 这个地址仍然在CM3 片上外设的地址空间中。
SRAM的位带区的地址为:0X2000 0000~X2010 0000,大小为1MB,经过膨胀后的位带别名区地址为:0X2200 0000~0X23FF FFFF, 大小为32MB。操作SRAM的比特位这个用得很少。
位带区的一个比特位膨胀到了位带别名区的4个字节,由于STM32的系统总线是32位的,所以按照4个字节的速度访问是最快的,所以膨胀成4个字节是最高效的。
对于片上外设位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=31)(n的范围根据具体寄存器能控制的位决定),则该比特在别名区的地址为:
AliasAddr= =0x42000000+ (A-0x40000000)*8*4 +n*4
0X42000000是外设位带别名区的起始地址,0x40000000是外设位带区的起始地址,(A-0x40000000)表示该比特前面有多少个字节, 一个字节有8位,所以*8,一个位膨胀后是4个字节,所以*4,n表示该比特在A地址的序号,因为一个位经过膨胀后是四个字节,所以也*4。
对于SRAM位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=31)(n的范围根据具体寄存器能控制的位决定),则该比特在别名区的地址为:
AliasAddr= =0x22000000+ (A-0x20000000)*8*4 +n*4
为了方便操作,我们可以把这两个公式合并成一个公式,把“位带地址+位序号”转换成别名区地址统一成一个宏。
// 把“位带地址+位序号”转换成别名地址的宏
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x00FFFFFF)<<5)+(bitnum<<2))
addr & 0xF0000000是为了区别SRAM还是外设,实际效果就是取出4或者2,如果是外设, 则取出的是4,+0X02000000之后就等于0X42000000,0X42000000是外设别名区的起始地址。 如果是SRAM,则取出的是2,+0X02000000之后就等于0X22000000,0X22000000是SRAM别名区的起始地址。
addr & 0x00FFFFFF 屏蔽了高三位,相当于减去0X20000000或者0X40000000,但是为什么是屏蔽高三位? 因为外设的最高地址是:0X20100000,跟起始地址0X20000000相减的时候,总是低5位才有效, 所以干脆就把高三位屏蔽掉来达到减去起始地址的效果,具体屏蔽掉多少位跟最高地址有关。SRAM同理分析即可。 <<5相当于*8*4,<<2相当于*4。
一共有三种不同的时钟源可以被用来驱动系统时钟(SYSCLK)
● HSI振荡器时钟
● HSE振荡器时钟
● PLL时钟
时钟的主要作用有:设置系统时钟SYSCLK、设置AHB分频因子(决定HCLK等于多少)、设置APB2分频因子(决定PCLK2等于多少)、 设置APB1分频因子(决定PCLK1等于多少)、设置各个外设的分频因子;控制AHB、APB2和APB1这三条总线时钟的开启、 控制每个外设的时钟的开启。
1)HSE 高速外部时钟,可以由有源晶振或者无源晶振提供,频率从4-16MHZ不等。当使用有源晶振时, 时钟从OSC_IN引脚进入,OSC_OUT引脚悬空,当选用无源晶振时,时钟从OSC_IN和OSC_OUT进入,并且要配谐振电容。
HSE最常使用的就是8M的无源晶振。当确定PLL时钟来源的时候,HSE可以不分频或者2分频, 这个由时钟配置寄存器CFGR的位17:PLLXTPRE(PLLXTPRE为HSE分频器作为PLL输入)设置,我们设置为HSE不分频。
2)PLL时钟源 ,从时钟框图可以看出,PLL时钟源可以来自HSE或者是HSI/2,具体用哪个由时钟配置寄存器CFGR的位16:PLLSRC (PLLSRC为PLL输入时钟源) 设置。 HSI是内部高速的时钟信号,频率为8M,由于采用的是RC振荡器,根据温度和环境的情况频率会有漂移,一般不作为PLL的时钟来源。这里我们选HSE作为PLL的时钟来源。
3)PLL时钟PLLCLK,此时钟是通过设置PLL的倍频因子来设置,倍频因子可以是:[2,3,4,5,6,7,8,9,10,11,12,13,14,15,16], 具体设置成多少,由时钟配置寄存器CFGR的位21-18:PLLMUL[3:0] (PLLMUL为PLL倍频系数)设置。我们这里设置为9倍频, 因为上一步我们设置PLL的时钟来源为HSE=8M,所以经过PLL倍频之后的PLL时钟:PLLCLK = 8M *9 = 72M。 72M是ST官方推荐的稳定运行时钟,如果你想超频的话,增大倍频因子即可,最高为128M。 我们这里设置PLL时钟:PLLCLK = 8M *9 = 72M。
4)SYSCLK系统时钟,从框图可知,系统时钟来源可以是:HSI、PLLCLK、HSE,具体的时钟配置寄存器CFGR的位1-0:SW[1:0]设置。 我们这里设置系统时钟:SYSCLK = PLLCLK = 72M。
5)AHB总线时钟HCLK,系统时钟SYSCLK经过AHB预分频器分频之后得到时钟叫APB总线时钟,即HCLK,分频因子可以是:[1,2,4,8,16,64,128,256,512], 具体的由时钟配置寄存器CFGR的位7-4 :HPRE[3:0]设置。片上大部分外设的时钟都是经过HCLK分频得到, 至于AHB总线上的外设的时钟设置为多少,得等到我们使用该外设的时候才设置, 我们这里只需粗线条的设置好APB的时钟即可。我们这里设置为1分频,即HCLK=SYSCLK=72M。
6)APB2总线时钟PCLK2,APB2总线时钟PCLK2由HCLK经过高速APB2预分频器得到,分频因子可以是:[1,2,4,8,16],具体由时钟配置寄存器CFGR的位13-11:PPRE2[2:0]决定。 PCLK2属于高速的总线时钟,片上高速的外设就挂载到这条总线上,比如全部的GPIO、USART1、SPI1等。至于APB2总线上的外设的时钟设置为多少, 得等到我们使用该外设的时候才设置,我们这里只需粗线条的设置好APB2的时钟即可。我们这里设置为1分频,即PCLK2 = HCLK = 72M。
7)APB1总线时钟PCLK1,APB1总线时钟PCLK1由HCLK经过低速APB预分频器得到,分频因子可以是:[1,2,4,8,16],具体的由时钟配置寄存器CFGR的位10-8:PRRE1[2:0]决定。 PCLK1属于低速的总线时钟,最高为36M,片上低速的外设就挂载到这条总线上,比如USART2/3/4/5、SPI2/3,I2C1/2等。 至于APB1总线上的外设的时钟设置为多少,得等到我们使用该外设的时候才设置,我们这里只需粗线条的设置好APB1的时钟即可。 我们这里设置为2分频,即PCLK1 = HCLK/2 = 36M。
1)USB时钟,USB时钟是由PLLCLK经过USB预分频器得到,分频因子可以是:[1,1.5],具体的由时钟配置寄存器CFGR的位22:USBPRE配置。 USB的时钟最高是48M,根据分频因子反推过来算,PLLCLK只能是48M或者是72M。一般我们设置PLLCLK=72M,USBCLK=48M。 USB对时钟要求比较高,所以PLLCLK只能是由HSE倍频得到,不能使用HSI倍频。
2)ADC时钟,ADC时钟由PCLK2经过ADC预分频器得到,分频因子可以是[2,4,6,8],具体的由时钟配置寄存器CFGR的位15-14:ADCPRE[1:0]决定。 很奇怪的是怎么没有1分频。ADC时钟最高只能是14M,如果采样周期设置成最短的1.5个周期的话,ADC的转换时间可以达到最短的1us。 如果真要达到最短的转换时间1us的话,那ADC的时钟就得是14M,反推PCLK2的时钟只能是:28M、56M、84M、112M, 鉴于PCLK2最高是72M,所以只能取28M和56M。
3)MCO时钟,MCO是microcontroller clock output的缩写,是微控制器时钟输出引脚,在STM32 F1系列中 由 PA8复用所得, 主要作用是可以对外提供时钟,相当于一个有源晶振。MCO的时钟来源可以是:PLLCLK/2、HSI、HSE、SYSCLK, 具体选哪个由时钟配置寄存器CFGR的位26-24:MCO[2:0]决定。除了对外提供时钟这个作用之外, 我们还可以通过示波器监控MCO引脚的时钟输出来验证我们的系统时钟配置是否正确。
1、开启HSE/HSI ,并等待 HSE/HSI 稳定
2、设置 AHB、APB2、APB1的预分频因子
3、设置PLL的时钟来源,和PLL的倍频因子,设置各种频率主要就是在这里设置
4、开启PLL,并等待PLL稳定
5、把PLLCK切换为系统时钟SYSCLK
6、读取时钟切换状态位,确保PLLCLK被选为系统时钟
void HSE_SetSysClock(uint32_t pllmul)
{
__IO uint32_t StartUpCounter = 0, HSEStartUpStatus = 0;
// 这个_IO 是指静态 这个 _IO 是指静态 volatile uint32_t 是指32位的无符号
// 整形变量uint32_t 是指32位的无符号整形变量;
// 把RCC外设初始化成复位状态
RCC_DeInit();
//使能HSE,开启外部晶振,野火STM32F103系列开发板用的是8M
RCC_HSEConfig(RCC_HSE_ON);
// 等待 HSE 启动稳定
HSEStartUpStatus = RCC_WaitForHSEStartUp();
// 只有 HSE 稳定之后则继续往下执行
if (HSEStartUpStatus == SUCCESS) {
//-----------------------------------------------------------------//
// 这两句是操作FLASH闪存用到的,如果不操作FLASH,这两个注释掉也没影响
// 使能FLASH 预存取缓冲区
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
// SYSCLK周期与闪存访问时间的比例设置,这里统一设置成2
// 设置成2的时候,SYSCLK低于48M也可以工作,如果设置成0或者1的时候,
// 如果配置的SYSCLK超出了范围的话,则会进入硬件错误,程序就死了
// 0:0 < SYSCLK <= 24M
// 1:24< SYSCLK <= 48M
// 2:48< SYSCLK <= 72M
FLASH_SetLatency(FLASH_Latency_2);
//-----------------------------------------------------------------//
// AHB预分频因子设置为1分频,HCLK = SYSCLK
RCC_HCLKConfig(RCC_SYSCLK_Div1);
// APB2预分频因子设置为1分频,PCLK2 = HCLK
RCC_PCLK2Config(RCC_HCLK_Div1);
// APB1预分频因子设置为1分频,PCLK1 = HCLK/2
RCC_PCLK1Config(RCC_HCLK_Div2);
//-----------------设置各种频率主要就是在这里设置-------------------//
// 设置PLL时钟来源为HSE,设置PLL倍频因子
// PLLCLK = 8MHz * pllmul
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, pllmul);
//-------------------------------------------------------------//
// 开启PLL
RCC_PLLCmd(ENABLE);
// 等待 PLL稳定
while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) {
}
// 当PLL稳定之后,把PLL时钟切换为系统时钟SYSCLK
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
// 读取时钟切换状态位,确保PLLCLK被选为系统时钟
while (RCC_GetSYSCLKSource() != 0x08) {
}
} else {
// 如果HSE开启失败,那么程序就会来到这里,用户可在这里添加出错的代码处理
// 当HSE开启失败或者故障的时候,单片机会自动把HSI设置为系统时钟,
// HSI是内部的高速时钟,8MHZ
while (1) {
}
}
}