1)实验平台:【正点原子】 NANO STM32F103 开发板
2)摘自《正点原子STM32 F1 开发指南(NANO 板-HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子
第四章 STM32 开发基础知识入门
这一章,我们将着重 STM32 开发的一些基础知识,让大家对 STM32 开发有一个初步的了
解,为后面 STM32 的学习做一个铺垫,方便后面的学习。这一章的内容大家第一次看的时候
可以只了解一个大概,后面需要用到这方面的知识的时候再回过头来仔细看看。这章我们分 7
个小结,
·4.1 MDK 下 C 语言基础复习
·4.2 STM32 系统架构
·4.3 STM32 时钟系统
·4.4 端口复用和重映射
·4.5 STM32 NVIC 中断管理
·4.6 MDK 中寄存器地址名称映射分析
·4.7 MDK 固件库快速开发技巧
4.1 MDK 下 C 语言基础复习
这一节我们主要讲解一下 C 语言基础知识。C 语言知识博大精深,也不是我们三言两语能
讲解清楚,同时我们相信学 STM32 这种级别 MCU 的用户,C 语言基础应该都是没问题的。我们
这里主要是简单的复习一下几个 C 语言基础知识点,引导那些 C 语言基础知识不是很扎实的用
户能够快速开发 STM32 程序。同时希望这些用户能够多去复习一下 C 语言基础知识,C 语言毕
竟是单片机开发中的必备基础知识。对于 C 语言基础比较扎实的用户,这部分知识可以忽略不
看。
4.1.1 位操作
C 语言位操作相信学过 C 语言的人都不陌生了,简而言之,就是对基本类型变量可以在位级
别进行操作。这节的内容很多朋友都应该很熟练了,我这里也就点到为止,不深入探讨。下面
我们先讲解几种位操作符,然后讲解位操作使用技巧。
C 语言支持如下 6 中位操作
表 4.1.1.16 种位操作
这些与或非,取反,异或,右移,左移这些到底怎么回事,这里我们就不多做详细,相信
大家学 C 语言的时候都学习过了。如果不懂的话,可以百度一下,非常多的知识讲解这些操作
符。下面我们想着重讲解位操作在单片机开发中的一些实用技巧。
1) 不改变其他位的值的状况下,对某几个位进行设值。
这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,
然后用|操作符设值。比如我要改变 GPIOA 的状态,可以先对寄存器的值进行&清零操作
GPIOA->CRL&=0XFFFFFF0F; //将第 4-7 位清 0
然后再与需要设置的值进行|或运算
GPIOA->CRL|=0X00000040;
//设置相应位的值,不改变其他位的值
2) 移位操作提高代码的可读性。
移位操作在单片机开发中也非常重要,下面让我们看看固件库的 GPIO 初始化的函数里
面的一行代码
GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
这个操作就是将 BSRR 寄存器的第 pinpos 位设置为 1,为什么要通过左移而不是直接设
置一个固定的值呢?其实,这是为了提高代码的可读性以及可重用性。这行代码可以
很直观明了的知道,是将第 pinpos 位设置为 1。如果你写成
GPIOx->BSRR =0x0030;
这样的代码就不好看也不好重用了。
类似这样的代码很多:
GPIOA->ODR|=1<<5; //PA.5 输出高,不改变其他位
这样我们一目了然,5 告诉我们是第 5 位也就是第 6 个端口,1 告诉我们是设置为 1 了。
3) ~取反操作使用技巧
SR 寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为 0,同时
其他位都保留为 1,简单的作法是直接给寄存器设置一个值:
TIMx->SR=0xFFF7;
这样的作法设置第 3 位为 0,但是这样的作法同样不好看,并且可读性很差。看看库函数
代码中怎样使用的:
TIMx->SR = (uint16_t)~TIM_FLAG;
而 TIM_FLAG 是通过宏定义定义的值:
#define TIM_FLAG_Update ((uint16_t)0x0001)
#define TIM_FLAG_CC1 ((uint16_t)0x0002)
看这个应该很容易明白,可以直接从宏定义中看出 TIM_FLAG_Update 就是设置的第 0 位了,
可读性非常强。
4.1.2 define 宏定义
define 是 C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供
方便。常见的格式:
#define 标识符 字符串
“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。例如:
#define SYSCLK_FREQ_72MHz 72000000
定义标识符 SYSCLK_FREQ_72MHz 的值为 72000000。
至于 define 宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。
4.1.3 ifdef 条件编译
单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而
当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编
译,否则编译程序段 2。 其中#else 部分也可以没有,即:
#ifdef
程序段 1
#endif
这个条件编译在 MDK 里面是用得很多的,在 stm32f10x.h 这个头文件中经常会看到这样的
语句:
#ifdef STM32F10X_HD
大容量芯片需要的一些变量定义
#end
而 STM32F10X_HD 则是我们通过#define 来定义的。条件编译也是 c 语言的基础知识,这里
也就点到为止吧。
4.1.4 extern 变量申明
C 语言中 extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编
译器遇到此变量和函数时在其他模块中寻找其定义。这里面要注意,对于 extern 申明变量可以多
次,但定义只有一次。在我们的代码中你会看到看到这样的语句:
extern u16 USART_RX_STA;
这个语句是申明 USART_RX_STA 变量在其他文件中已经定义了,在这里要使用到。所以,你
肯定可以找到在某个地方有变量定义的语句:
u16 USART_RX_STA;
下面通过一个例子说明一下使用方法。
在 Main.c 定义的全局变量 id,id 的初始化都是在 Main.c 里面进行的。
Main.c 文件
u8 id;//定义只允许一次
main()
{
id=1;
printf("d%",id);//id=1
test();
printf("d%",id);//id=2
}
但是我们希望在 test.c 的 changeId(void)函数中使用变量 id,这个时候我们就需要在
test.c 里面去申明变量 id是外部定义的了,因为如果不申明,变量 id 的作用域是到不了test.c
文件中。看下面 test.c 中的代码:
extern u8 id;//申明变量 id 是在外部定义的,申明可以在很多个文件中进行
void test(void){
id=2;
}
在 test.c 中申明变量 id 在外部定义,然后在 test.c 中就可以使用变量 id 了。
对于 extern 申明函数在外部定义的应用,这里我们就不多讲解了。
4.1.5 typedef 类型别名
typedef 用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。
typedef 在 MDK 用得最多的就是定义结构体的类型别名和枚举类型了。
struct _GPIO
{
__IO uint32_t CRL;
__IO uint32_t CRH;
…
};
定义了一个结构体 GPIO,这样我们定义变量的方式为:
struct _GPIO GPIOA;//定义结构体变量 GPIOA
但是这样很繁琐,MDK 中有很多这样的结构体变量需要定义。这里我们可以为结体定义一
个别名 GPIO_TypeDef,这样我们就可以在其他地方通过别名 GPIO_TypeDef 来定义结构体变量
了。方法如下:
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
…
} GPIO_TypeDef;
Typedef 为结构体定义一个别名 GPIO_TypeDef,这样我们可以通过 GPIO_TypeDef 来定义结
构体变量:
GPIO_TypeDef _GPIOA,_GPIOB;
这里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。 这样是不是方便很多?
4.1.6 结构体
经常很多用户提到,他们对结构体使用不是很熟悉,但是 MDK 中太多地方使用结构体以及
结构体指针,这让他们一下子摸不着头脑,学习 STM32 的积极性大大降低,其实结构体并不是
那么复杂,这里我们稍微提一下结构体的一些知识,还有一些知识我们会在下一节的“寄存器
地址名称映射分析”中讲到一些。
声明结构体类型:
Struct 结构体名{
成员列表;
}变量名列表;
例如:
Struct U_TYPE {
Int BaudRate
Int WordLength;
}usart1,usart2;
在结构体申明的时候可以定义变量,也可以申明之后定义,方法是:
Struct 结构体名字 结构体变量列表 ;
例如:struct U_TYPE usart1,usart2;
结构体成员变量的引用方法是:
结构体变量名字.成员名
比如要引用 usart1 的成员 BaudRate,方法是:usart1.BaudRate;
结构体指针变量定义也是一样的,跟其他变量没有啥区别。
例如:struct U_TYPE *usart3;//定义结构体指针变量 usart1;
结构体指针成员变量引用方法是通过“->”符号实现,比如要访问 usart3 结构体指针指向的结
构体的成员变量 BaudRate,方法是:
Usart3->BaudRate;
上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到
这里,有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过
一个实例回答一下这个问题。
在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如串口,它的初始化状态
是由几个属性来决定的,比如串口号,波特率,极性,以及模式。对于这种情况,在我们没有
学习结构体的时候,我们一般的方法是:
void USART_Init(u8 usartx,u32 u32 BaudRate,u8 parity,u8 mode);
这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函
数里面再传入一个参数,那么势必我们需要修改这个函数的定义,重新加入字长这个入口参数。
于是我们的定义被修改为:
void USART_Init (u8 usartx,u32 BaudRate, u8 parity,u8 mode,u8 wordlength );
但是如果我们这个函数的入口参数是随着开发不断的增多,那么是不是我们就要不断的修
改函数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢?
这样如果我们使用到结构体就能解决这个问题了。我们可以在不改变入口参数的情况下,
只需要改变结构体的成员变量,就可以达到上面改变入口参数的目的。
结构体就是将多个变量组合为一个有机的整体。上面的函数,BaudRate,wordlength,
Parity,mode,wordlength 这些参数,他们对于串口而言,是一个有机整体,都是来设置串口参
数的,所以我们可以将他们通过定义一个结构体来组合在一个。MDK 中是这样定义的:
typedef struct
{
uint32_t USART_BaudRate;
uint16_t USART_WordLength;
uint16_t USART_StopBits;
uint16_t USART_Parity;
uint16_t USART_Mode;
uint16_t USART_HardwareFlowControl;
} USART_InitTypeDef;
于是,我们在初始化串口的时候入口参数就可以是 USART_InitTypeDef 类型的变量或者指
针变量了,MDK 中是这样做的:
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需
要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义
就可以达到增加变量的目的。
理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,
如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可
以提高你的代码的可读性。
使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作
用就远远不止这个了,同时,MDK 中用结构体来定义外设也不仅仅只是这个作用,这里我们只
是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲
解结构体的一些其他知识。
4.2 STM32 系统架构
STM32 的系统架构比 51 单片机就要强大很多了。STM32 系统架构的知识可以在《STM32
中文参考手册 V10》的 P25~28 有讲解,这里我们也把这一部分知识抽取出来讲解,是为了大
家在学习 STM32 之前对系统架构有一个初步的了解。这里的内容基本也是从中文参考手册中
参考过来的,让大家能通过我们手册也了解到,免除了到处找资料的麻烦吧。如果需要详细深
入的了解 STM32 的系统架构,还需要在网上搜索其他资料学习学习。
我们这里所讲的 STM32 系统架构主要针对的 STM32F103 这些非互联型芯片。首先我们看
看 STM32 的系统架构图:
图 4.2.1STM32 系统架构图
STM32 主系统主要由四个驱动单元和四个被动单元构成。
四个驱动单元是:
内核 DCode 总线;
系统总线;
通用 DMA1;
通用 DMA2;
四被动单元是:
AHB 到 APB 的桥:连接所有的 APB 设备;
内部 FlASH 闪存;
内部 SRAM;
FSMC;
下面我们具体讲解一下图中几个总线的知识:
① ICode 总线:该总线将 M3 内核指令总线和闪存指令接口相连,指令的预取在该总线上
面完成。
② DCode 总线:该总线将 M3 内核的 DCode 总线与闪存存储器的数据接口相连接,常量
加载和调试访问在该总线上面完成。
③ 系统总线:该总线连接 M3 内核的系统总线到总线矩阵,总线矩阵协调内核和 DMA 间
访问。
④ DMA 总线:该总线将 DMA 的 AHB 主控接口与总线矩阵相连,总线矩阵协调 CPU 的
DCode 和 DMA 到 SRAM,闪存和外设的访问。
⑤ 总线矩阵:总线矩阵协调内核系统总线和 DMA 主控总线之间的访问仲裁,仲裁利用
轮换算法。
⑥ AHB/APB 桥:这两个桥在 AHB 和 2 个 APB 总线间提供同步连接,APB1 操作速度限于
36MHz,APB2 操作速度全速。
对于系统架构的知识,在刚开始学习 STM32 的时候只需要一个大概的了解,大致知道是个
什么情况即可。对于寻址之类的知识,这里就不做深入的讲解,中文参考手册都有很详细的讲
解。
4.3 STM32F1 时钟系统
STM32 时钟系统的知识在《STM32 中文参考手册 V10》中的 P55~P73 有非常详细的讲解,
网上关于时钟系统的讲解也基本都是参考的这里,讲不出啥特色,不过作为一个完整的参考手
册,我们必然要提到时钟系统的知识。这些知识也不是什么原创,纯粹的是看网友发的帖子和
手册来总结的,有一些直接是 copy 过来的,望大家谅解。
这部分内容我们分 3 个小节来讲解:
◆ 4.3.1 STM32F1 时钟树概述
◆ 4.3.2 STM32F1 时钟初始化配置
◆ 4.3.3 STM32F1 时钟使能和配置
4.3.1 STM32F1 时钟树概述
众所周知,时钟系统是 CPU 的脉搏,就像人的心跳一样。所以时钟系统的重要性就不言而
喻了。 STM32 的时钟系统比较复杂,不像简单的 51 单片机一个系统时钟就可以解决一切。于
是有人要问,采用一个系统时钟不是很简单吗?为什么 STM32 要有多个时钟源呢? 因为首先
STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,比
如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干
扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。
首先让我们来看看 STM32F1 的时钟系统图吧:
图 4.3.1 STM32F1 时钟系统图
在 STM32 中,有五个时钟源,为 HSI、HSE、LSI、LSE、PLL。从时钟频率来分可以分为
高速时钟源和低速时钟源,在这 5 个中 HIS,HSE 以及 PLL 是高速时钟,LSI 和 LSE 是低速时
钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时
钟源,其中 HSE 和 LSE 是外部时钟源,其他的是内部时钟源。下面我们看看 STM32 的 5 个时
钟源,我们讲解顺序是按图中红圈标示的顺序:
①、HSI 是高速内部时钟,RC 振荡器,频率为 8MHz。
②、HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为
4MHz~16MHz。我们的开发板接的是 8M 的晶振。
③、LSI 是低速内部时钟,RC 振荡器,频率为 40kHz。独立看门狗的时钟源只能是 LSI,同
时 LSI 还可以作为 RTC 的时钟源。
④、LSE 是低速外部时钟,接频率为 32.768kHz 的石英晶体。这个主要是 RTC 的时钟源。
⑤、PLL 为锁相环倍频输出,其时钟输入源可选择为 HSI/2、HSE 或者 HSE/2。倍频可选择为
2~16 倍,但是其输出频率最大不得超过 72MHz。
上面我们简要概括了 STM32 的时钟源,那么这 5 个时钟源是怎么给各个外设以及系统提
供时钟的呢?这里我们将一一讲解。我们还是从图的下方讲解起吧,因为下方比较简单。
图中我们用 A~E 标示我们要讲解的地方。
A.
MCO 是 STM32 的一个时钟输出 IO(PA8),它可以选择一个时钟信号输出,可以
选择为 PLL 输出的 2 分频、HSI、HSE、或者系统时钟。这个时钟可以用来给外
部其他系统提供时钟源。
B.
这里是 RTC 时钟源,从图上可以看出,RTC 的时钟源可以选择 LSI,LSE,以及
HSE 的 128 分频。
C.
从图中可以看出 C 处 USB 的时钟是来自 PLL 时钟源。STM32 中有一个全速功能
的 USB 模块,其串行接口引擎需要一个频率为 48MHz 的时钟源。该时钟源只能
从 PLL 输出端获取,可以选择为 1.5 分频或者 1 分频,也就是,当需要使用 USB
模块时,PLL 必须使能,并且时钟频率配置为 48MHz 或 72MHz。
D.
D 处就是 STM32 的系统时钟 SYSCLK,它是供 STM32 中绝大部分部件工作的时
钟源。系统时钟可选择为 PLL 输出、HSI 或者 HSE。系统时钟最大频率为 72MHz,
当然你也可以超频,不过一般情况为了系统稳定性是没有必要冒风险去超频的。
E.
这里的 E 处是指其他所有外设了。从时钟图上可以看出,其他所有外设的时钟最
终来源都是 SYSCLK。SYSCLK 通过 AHB 分频器分频后送给各模块使用。这些
模块包括:
①、AHB 总线、内核、内存和 DMA 使用的 HCLK 时钟。
②、通过 8 分频后送给 Cortex 的系统定时器时钟,也就是 systick 了。
③、直接送给 Cortex 的空闲运行时钟 FCLK。
④、送给 APB1 分频器。APB1 分频器输出一路供 APB1 外设使用(PCLK1,最大
频率 36MHz),另一路送给定时器(Timer)2、3、4 倍频器使用。
⑤、送给 APB2 分频器。APB2 分频器分频输出一路供 APB2 外设使用(PCLK2,
最大频率 72MHz),另一路送给定时器(Timer)1 倍频器使用。
其中需要理解的是 APB1 和 APB2 的区别,APB1 上面连接的是低速外设,包括电源接口、
备份接口、CAN、USB、I2C1、I2C2、UART2、UART3 等等,APB2 上面连接的是高速外设包
括 UART1、SPI1、Timer1、ADC1、ADC2、所有普通 IO 口(PA~PE)、第二功能 IO 口等。居宁
老师的《稀里糊涂玩 STM32》资料里面教大家的记忆方法是 2>1, APB2 下面所挂的外设的时
钟要比 APB1 的高。
在以上的时钟输出中,有很多是带使能控制的,例如 AHB 总线时钟、内核时钟、各种 APB1
外设、APB2 外设等等。当需要使用某模块时,记得一定要先使能对应的时钟。后面我们讲解
实例的时候回讲解到时钟使能的方法。
4.3.2 STM32F1 时钟系统配置
上一小节我们对 STM32F1 时钟树进行了详细讲解,接下来我们来讲解通过 STM32F1 的
HAL 库进行 STM32F1 时钟系统配置步骤。实际上,STM32F1 的时钟系统配置也可以通过图形
化配置工具 STM32CubeMX 来配置生成,这里我们讲解初始化代码,是为了让大家对 STM32
时钟系统有更加清晰的理解。我们将在 4.8 小节讲解图形化配置工具 STM32CubeMX,大家可
以对比参考学习。
前面我们讲解过,在系统启动之后,程序会先执行 HAL 库定义的 SystemInit 函数,进行系
统一些初始化配置。那么我们先来看看 SystemInit 程序:
void SystemInit (void)
{
/* Reset the RCC clock configuration to the default reset state(for debug purpose) */
RCC->CR |= 0x00000001U;
/* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#if !defined(STM32F105xC) && !defined(STM32F107xC)
RCC->CFGR &= 0xF8FF0000U;
#else
RCC->CFGR &= 0xF0FF0000U;
#endif /* STM32F105xC */
/* Reset HSEON, CSSON and PLLON bits */
RCC->CR &= 0xFEF6FFFFU;
/* Reset HSEBYP bit */
RCC->CR &= 0xFFFBFFFFU;
/* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
RCC->CFGR &= 0xFF80FFFFU;
#if defined(STM32F105xC) || defined(STM32F107xC)
/* Reset PLL2ON and PLL3ON bits */
RCC->CR &= 0xEBFFFFFFU;
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x00FF0000U;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000U;
#elif defined(STM32F100xB) || defined(STM32F100xE)
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000U;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000U;
#else
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000U;
#endif /* STM32F105xC */
#if defined(STM32F100xE) || defined(STM32F101xE) || defined(STM32F101xG) ||
defined(STM32F103xE) || defined(STM32F103xG)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM */
#endif
/*配置中断向量表地址=基地址 偏移地址*/
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
#endif
}
从上面代码可以看出,SystemInit 主要做了如下四个方面工作:
1)复位 RCC 时钟配置为默认复位值(根据不同型号的芯片配置,默认开启了 HSI)
2)外部存储器配置
3)中断向量表地址配置
HAL 库的 SystemInit 函数并没有像标准库的 SystemInit 函数一样进行时钟的初始化配置。
HAL 库的 SystemInit 函数除了打开 HSI 之外,没有任何时钟相关配置,所以使用 HAL 库我们
必须编写自己的时钟配置函数。首先我们打开工程模板看看我们在工程 SYSTEM 分组下面定义
的 sys.c 文件中的时钟初始化函数 Stm32_Clock_Init 的内容:
//时钟系统配置函数
//PLL:选择的倍频数,RCC_PLL_MUL2~RCC_PLL_MUL16
//返回值:0,成功;1,失败
void Stm32_Clock_Init(u32 PLL)
{
HAL_StatusTypeDef ret = HAL_OK;
RCC_OscInitTypeDef RCC_OscInitStructure;
RCC_ClkInitTypeDef RCC_ClkInitStructure;
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打开 HSE
RCC_OscInitStructure.HSEPredivValue=RCC_HSE_PREDIV_DIV1;//HSE 预分频
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON;//打开 PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;//PLL 时钟源选择 HSE
RCC_OscInitStructure.PLL.PLLMUL=PLL; //主 PLL 倍频因子
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化
if(ret!=HAL_OK) while(1);
//选中 PLL 作为系统时钟源并且配置 HCLK,PCLK1 和 PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|
RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;
//设置系统时钟时钟源为 PLL
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1;//AHB 分频系数为 1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV2; //APB1 分频系数为 2
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV1; //APB2 分频系数为 1
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_2);
//同时设置 FLASH 延时周期为 2WS,也就是 3 个 CPU 周期。
if(ret!=HAL_OK) while(1);
}
从函数注释可知,函数 Stm32_Clock_Init 的作用是进行时钟系统配置,除了配置 PLL 相关
参数确定 SYSCLK 值之外,还配置了 AHB,APB1,APB2 的分配系数,也就是确定了 HCLK,
PCLK1 和 PCLK2 的时钟值。我们首先来看看使用 HAL 库配置 STM32F1 时钟系统的一般步骤:
1)配置时钟源相关参数:调用函数 HAL_RCC_OscConfig()。
2)配置系统时钟源以及 AHB,APB1 和 APB2 的分频系数;调用函数
HAL_RCC_ClockConfig()。
步骤 1,使用 HAL 来配置时钟源相关参数,我们调用的函数为 HAL_RCC_OscConfig(),
该函数在 HAL 库关键头文件 stm32f1xx_hal_rcc.h 中声明,在文件 stm32f1xx_hal_rcc.c 中定义。
首先我们来看看该函数声明:
HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct);
该函数只有一个入口参数,就是结构体 RCC_OscInitTypeDef 类型指针。接下来我们看看结构体
RCC_OscInitTypeDef 的定义:
typedef struct
{
uint32_t OscillatorType; //需要选择配置的振荡器类型
#if defined(STM32F105xC) || defined(STM32F107xC)
uint32_t Prediv1Source; //设置 Prediv1 源值
#endif /* STM32F105xC || STM32F107xC */
uint32_t HSEState; //HSE 状态
uint32_t HSEPredivValue; //HSE 预分频值
uint32_t LSEState; //LSE 状态
uint32_t HSIState; //HSI 状态
uint32_t HSICalibrationValue; //HSI 校准值
uint32_t LSIState; //LSI 状态
RCC_PLLInitTypeDef PLL; //PLL 配置
#if defined(STM32F105xC) || defined(STM32F107xC)
RCC_PLL2InitTypeDef PLL2; //PLL2 配置
#endif /* STM32F105xC || STM32F107xC */
} RCC_OscInitTypeDef;
对于这个结构体,前面几个参数主要是用来选择配置的振荡器类型。比如我们要开启 HSE,
那么我们会设置 OscillatorType 的值为 RCC_OSCILLATORTYPE_HSE,然后设置 HSEState 的值
为 RCC_HSE_ON 开启 HSE。对于其他时钟源 HSI,LSI 和 LSE,配置方法类似。这个结构体
还有一个很重要的成员变量 PLL,它是结构体 RCC_PLLInitTypeDef 类型(注意:由于我们是
用到 STM32F103xB 系列,关于 STM32F105xC 和 STM32F107xC 的参数成员变量就不讲解了)
它的作用是配置 PLL 相关参数,我们来看看它的定义:
typedef struct
{
uint32_t PLLState; //PLL 状态
uint32_t PLLSource; //PLL 时钟源
uint32_t PLLMUL; //PLL 倍频系数
} RCC_PLLInitTypeDef;
从 RCC_PLLInitTypeDef 结构体的定义很容易看出该结构体主要用来设置 PLL 时钟源以及
相关倍频参数。
这个结构体的定义我们就不做过多讲解,接下来我们看看我们的时钟初始化函数
Stm32_Clock_Init 中的配置内容:
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打开 HSE
RCC_OscInitStructure.HSEPredivValue=RCC_HSE_PREDIV_DIV1; //HSE 预分频
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON;
//打开 PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;//PLL 时钟源选择 HSE
RCC_OscInitStructure.PLL.PLLMUL=PLL;
//主 PLL 倍频因子
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化
通过这段函数,我们开启了 HSE 时钟源,同时选择 PLL 时钟源为 HSE,然后把
Stm32_Clock_Init 的入口参数直接设置作为 PLL 倍频因子,这样就达到了设置 PLL 时钟源相关
参数的目的。设置好 PLL 时钟源参数之后,也就是确定了 PLL 的时钟频率,接下来我们就需
要设置系统时钟,以及 AHB,APB1 和 APB2 相关参数,也就是我们前面提到的步骤 2。
接下来我们来看看步骤 2 中提到的 HAL_RCC_ClockConfig()函数,声明如下:
HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct,
uint32_t FLatency)
该函数有两个入口参数,第一个入口参数 RCC_ClkInitStruct 是结构体 RCC_ClkInitTypeDef
指针类型,用来设置 SYSCLK 时钟源以及 AHB,APB1,APB2 的分频系数。第二个入口参数
FLatency 用来设置 FLASH 延迟,这个参数我们稍后讲解。
RCC_ClkInitTypeDef 结构体类型定义非常简单,这里我们就不列出来,我们看看
Stm32_Clock_Init 函数中的配置内容:
//选中 PLL 作为系统时钟源并且配置 HCLK,PCLK1 和 PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|
RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|
RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;
//设置系统时钟时钟源为 PLL
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1;//AHB 分频系数为 1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV2; //APB1 分频系数为 2
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV1; //APB2 分频系数为 1
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_2);
//同时设置 FLASH 延时周期为 2WS,也就是 3 个 CPU 周期。
第一个参数 ClockType 配置说明我们要配置的是 SYSCLK,HCLK,PCLK1 和 PCLK2 四
个时钟。第二个参数 SYSCLKSource 配置选择系统时钟源为 PLL。
第三个参数 AHBCLKDivider 配置 AHB 分频系数为 1。
第四个参数 APB1CLKDivider 配置 APB1 分频系数为 2。
第五个参数 APB2CLKDivider 配置 APB2 分频系数为 1
根据我们在主函数中调用 Stm32_Clock_Init(RCC_PLL_MUL9)时候设置的入口参数值,
我们可以计算出,PLL 时钟为 PLLCLK=HSE*PLLMUL = 8Mhz*9=72Mhz,同时我们选择系统
时钟源为 PLL ,所以系统时钟 SYSCLK=72Mhz 。 AHB 分 频 为 1 , 故 其 频 率 为
HCLK=SYSCLK/1=72Mhz。APB1 分频系数为 2,故其频率为 PCLK1=HCLK/2=36Mhz。APB2
分频系数为 1,故其频率为 PCLK2=HCLK/1=72Mhz。最后我们总结一下通过调用函数
Stm32_Clock_Init(RCC_PLL_MUL9)之后的关键时钟频率值:
SYSCLK(系统时钟) = 72Mhz
PLL 主时钟 =72Mhz
AHB 总线时钟(HCLK=SYSCLK/1) = 72Mhz
APB1 总线时钟(PCLK1=HCLK/2) = 36Mhz
APB2 总线时钟(PCLK2 = HCLK/1) = 72Mhz
最后我们来看看步骤 2 中函数 HAL_RCC_ClockConfig 第二个入口参数 FLatency 的含义。
这里我们不想讲的太复杂,要知道 SYSCLK 达到 72Mhz,是需要配置 FLASH 延迟 Latency。
对于 STM32F1 系列,FLASH 延迟配置参数值是通过下图来确定的:
图 4.3.2.1 STM32F1 系列等待周期
从上图可以看出,如果需要 SYSCLK 为 72Mhz,那么等待周期必须为 2,也就是 3 个 CPU
周期。下面我们看看我们在 Stm32_Clock_Init 中调用函数 HAL_RCC_ClockConfig 的时候,第
二个入口参数设置置:
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_2);
从上可以看出,我们设置值为 FLASH_LATENCY_2,也就是 2WS,3 个 CPU 周期,与我
们预期一致。时钟系统配置相关知识就给大家讲解到这里。
4.3.3 STM32F1 时钟使能和配置
上一节我们讲解了时钟系统配置步骤。在配置好时钟系统之后,如果我们要使用某些外设,
例如 GPIO,ADC 等,我们还要使能这些外设时钟。这里大家必须注意,如果在使用外设之前
没有使能外设时钟,这个外设是不可能正常运行的。STM32 的外设时钟使能是在 RCC 相关寄
存器中配置的。因为 RCC 相关寄存器非常多,有兴趣的同学可以直接打开《STM32F1 中文参
考手册》6.3 小节查看所有 RCC 相关寄存器的配置。接下来我们来讲解通过 STM32F1 的 HAL
库使能外设时钟的方法。
在 STM32F1 的 HAL 库中,外设时钟使能操作都是在 RCC 相关固件库文件头文件
stm32f1xx_hal_rcc.h 定义的。大家打开 stm32f1xx_hal_rcc.h 头文件可以看到文件中除了少数几
个函数声明之外大部分都是宏定义标识符。外设时钟使能在 HAL 库中都是通过宏定义标识符
来实现的。首先,我们来看看 GPIOA 的外设时钟使能宏定义标识符:
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg; \
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\
tmpreg=READ_BIT(RCC->APB2ENR,RCC_APB2ENR_IOPAEN);\
UNUSED(tmpreg); \
} while(0U)
这 几 行 代 码 非 常 简 单 , 主 要 是 定 义 了 一 个 宏 定 义 标 识 符
__HAL_RCC_GPIOA_CLK_ENABLE(),它的核心操作是通过下面运行代码实现的。
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
这行代码的作用是,设置寄存器 RCC->APB2ENR 的相关位为 1,至于是哪个位,是由宏
定义 RCC_APB2ENR_IOPAEN 的值决定的,而它的值为;
#define RCC_APB2ENR_IOPAEN_Pos (2U)
#define RCC_APB2ENR_IOPAEN_Msk (0x1U << RCC_APB2ENR_IOPAEN_Pos)
#define RCC_APB2ENR_IOPAEN RCC_APB2ENR_IOPAEN_Msk
所以,我们很容易理解上面代码的作用是设置寄存器 RCC->APB2ENR 寄存器的第 2 位为
1。我们可以从 STM32F1 的中文参考手册搜索 APB2ENR 寄存器定义,第 2 位的作用是用来使
用 GPIOA 时钟,APB2ENR 寄存器的为 2 描述如下:
位 2 IOPAEN:IO 端口 A 时钟使能
由软件置“1”或清“0”
0:IO 端口 A 时钟关闭。
1:IO 端口 A 时钟开启。
那 么 我 们 只 需 要 在 我 们 的 用 户 程 序 中 调 用 宏 定 义 标 识 符
__HAL_RCC_GPIOA_CLK_ENABLE()就可以实现 GPIOA 时钟使能。使用方法为:
__HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 时钟
对于其他外设,同样都是在 stm32f1xx_hal_rcc.h 头文件中定义,大家只需要找到相关宏定
义标识符即可,这里我们列出了几个常用使能外设时钟的宏定义标识符使用方法:
__HAL_RCC_DMA1_CLK_ENABLE();//使能 DMA1 时钟
__HAL_RCC_USART1_CLK_ENABLE();//使能串口 1 时钟
__HAL_RCC_TIM1_CLK_ENABLE();//使能 TIM1 时钟
我们使用外设的时候需要使能外设时钟,如果我们不需要使用某个外设,同样我们可以静
止某个外设时钟。禁止外设时钟使用方法和使能外设时钟非常类似,同样是头文件中定义的宏
定义标识符。我们同样以 GPIOA 为例,宏定义标识符为:
#define __HAL_RCC_GPIOA_CLK_DISABLE()
(RCC->APB2ENR &= ~(RCC_APB2ENR_IOPAEN))
同 样 , 宏 定 义 标 识 符 __HAL_RCC_GPIOA_CLK_DISABLE() 作 用 是 设 置
RCC->RCC->APB2ENR 寄存器的最低位为 0,也就是静止 GPIOA 时钟。具体使用方法我们这
里就不做过多讲解,我们这里同样列出几个常用的禁止外设时钟的宏定义标识符使用方法:
__HAL_RCC_DMA1_CLK_DISABLE();//禁止 DMA1 时钟
__HAL_RCC_USART1_CLK_DISABLE();/禁止串口 1 时钟
__HAL_RCC_TIM1_CLK_DISABLE();//禁止 TIM1 时钟
关于 STM32F1 的外设时钟使能和禁止方法我们就给大家讲解到这里。
4.4 端口复用和重映射
4.4.1 端口复用功能
STM32 有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO
如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。
这部分知识在《STM32 中文参考手册 V10》的 P109,P116~P121 有详细的讲解哪些 GPIO 管脚是
可以复用为哪些内置外设的。这里我们就不一一讲解。
大家都知道,MCU 都有串口,STM32 有好几个串口。比如说 STM32F103ZET6 有 5 个串口,我
们可以查手册知道,串口 1 的引脚对应的 IO 为 PA9,PA10.PA9,PA10 默认功能是 GPIO,所以当
PA9,PA10 引脚作为串口 1 的 TX,RX 引脚使用的时候,那就是端口复用。
复用端口初始化有几个步骤:
1) GPIO 端口时钟使能。要使用到端口复用,当然要使能端口的时钟了。
__HAL_RCC_GPIOA_CLK_ENABLE();
2) 复用的外设时钟使能。比如你要将端口 PA9,PA10 复用为串口,所以要使能串口时钟。
__HAL_RCC_USART1_CLK_ENABLE();
3) 端口模式配置。 在 IO 复用位内置外设功能引脚的时候,必须设置 GPIO 端口的模式,至于
在复用功能下 GPIO 的模式是怎么对应的,这个可以查看手册《STM32 中文参考手册 V10》
P110 的表格“8.1.11 外设的 GPIO 配置”。这里我们拿 Usart1 举例:
图 4.4.1.2 串口复用 GPIO 配置
从表格中可以看出,我们要配置全双工的串口 1,那么 TX 管脚需要配置为推挽复用输出,
RX 管脚配置为浮空输入或者带上拉输入。
//USART1_TX
GPIO_Initure.Pin=GPIO_PIN_9;
//PA9
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP;
//上拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9
//USART1_RX
GPIO_Initure.Pin=GPIO_PIN_10;
//PA10
GPIO_Initure.Mode=GPIO_MODE_AF_INPUT;//模式要设置为复用输入模式!
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA10
上面代码的含义在我们的第一个实验学完之后大家自然会了解,这里只是做个概括。
所以,我们在使用复用功能的是时候,最少要使能 2 个时钟:
1) GPIO 时钟使能
2) 复用的外设时钟使能
同时要初始化 GPIO 以及复用外设功能
4.4.2 端口重映射
为了使不同器件封装的外设 IO 功能数量达到最优,可以把一些复用功能重新映射到其他一
些引脚上。STM32 中有很多内置外设的输入输出引脚都具有重映射(remap)的功能。我们知道每
个内置外设都有若干个输入输出引脚,一般这些引脚的输出端口都是固定不变的,为了让设计
工程师可以更好地安排引脚的走向和功能,在 STM32 中引入了外设引脚重映射的概念,即一个
外设的引脚除了具有默认的端口外,还可以通过设置重映射寄存器的方式,把这个外设的引脚
映射到其它的端口。
简单的讲就是把管脚的外设功能映射到另一个管脚,但不是可以随便映射的,具体对应关
系《STM32 中文参考手册 V10》的 P116 页“8.3 复用功能和调试配置”有讲解。这里我们同样
拿串口 1 为例来讲解。
图 4.4.2.1 串口重映射管脚表
上图是截取的中文参考手册中的重映射表,从表中可以看出,默认情况下,串口 1 复用的时候
的引脚位 PA9,PA10,同时我们可以将 TX 和 RX 重新映射到管脚 PB6 和 PB7 上面去。
所以重映射我们同样要使能复用功能的时候讲解的 2 个时钟外,还要使能 AFIO 功能时钟,然后
要调用重映射函数。详细步骤为:
1)使能 GPIOB 时钟:
__HAL_RCC_GPIOB_CLK_ENABLE();
2)使能串口 1 时钟:
__HAL_RCC_USART1_CLK_ENABLE();
3)使能 AFIO 时钟:
__HAL_RCC_AFIO_CLK_ENABLE();
4)开启重映射:
__HAL_AFIO_REMAP_USART1_ENABLE()
这样就将串口的 TX 和 RX 重映射到管脚 PB6 和 PB7 上面了。至于有哪些功能可以重映射,
大家除了查看中文参考手册之外,还可以从 stm32f1xx_hal_gpio_ex.h 文件查看得知。在 stm3
2f10x_gpio.h 文件中定义了取值范围为下面宏定义的标识符,这里我们贴一小部分:
#define __HAL_AFIO_REMAP_SPI1_ENABLE()
SET_BIT(AFIO->MAPR, AFIO_MAPR_SPI1_REMAP)
#define __HAL_AFIO_REMAP_I2C1_ENABLE()
SET_BIT(AFIO->MAPR, AFIO_MAPR_I2C1_REMAP)
#define __HAL_AFIO_REMAP_USART2_ENABLE()
SET_BIT(AFIO->MAPR, AFIO_MAPR_USART2_REMAP)
#define __HAL_AFIO_REMAP_USART3_ENABLE()
do{ CLEAR_BIT(AFIO->MAPR, AFIO_MAPR_USART3_REMAP);\
SET_BIT(AFIO->MAPR, AFIO_MAPR_USART3_REMAP_FULLREMAP); \
}while(0U)
#define __HAL_AFIO_REMAP_USART3_PARTIAL()
do{ CLEAR_BIT(AFIO->MAPR, AFIO_MAPR_USART3_REMAP); \
SET_BIT(AFIO->MAPR,
AFIO_MAPR_USART3_REMAP_PARTIALREMAP); \
}while(0U)
从上面可以看出,USART1 只有一种重映射,而对于 USART3,存在部分重映射和完全重映射。
所谓部分重映射就是部分管脚和默认的是一样的,而部分管脚是重新映射到其他管脚。而完全
重映射就是所有管脚都重新映射到其他管脚。看看手册中的 USART3 重映射表:
图 4.4.2.2 USART3 重映射管脚对应表
部分重映射就是 PB10,PB11,PB12 重映射到 PC10,PC11,PC12 上。而 PB13 和 PB14 和没有重
映射情况是一样的,都是 USART3_CTS 和 USART3_RTS 对应管脚。完全重映射就是将这两个脚重
新映射到 PD11 和 PD12 上去。我们要使用 USART3 的部分重映射,我们调用函数方法为:
__HAL_AFIO_REMAP_USART3_PARTIAL();
这些知识我们后面在使用的过程中间还会讲解,这里只是对重映射概念做个简要的描述。
4.5 STM32 NVIC 中断优先级管理
CM3 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断,并且具有 256
级的可编程中断设置。但 STM32 并没有使用 CM3 内核的全部东西,而是只用了它的一部分。
STM32 有 84 个中断,包括 16 个内核中断和 68 个可屏蔽中断,具有 16 级可编程的中断优先级。
而我们常用的就是这 68 个可屏蔽中断,但是 STM32 的 68 个可屏蔽中断,在 STM32F103 系列
上面,又只有 60 个(在 107 系列才有 68 个)。因为我们开发板选择的芯片是 STM32F103 系
列的所以我们就只针对 STM32F103 系列这 60 个可屏蔽中断进行介绍。
在 MDK 内,与 NVIC 相关的寄存器,MDK 为其定义了如下的结构体:
typedef struct
{
__IO uint32_t ISER[8]; /*!< Interrupt Set Enable Register */
uint32_t RESERVED0[24];
__IO uint32_t ICER[8];
/*!< Interrupt Clear Enable Register */
uint32_t RSERVED1[24];
__IO uint32_t ISPR[8];
/*!< Interrupt Set Pending Register */
uint32_t RESERVED2[24];
__IO uint32_t ICPR[8];
/*!< Interrupt Clear Pending Register */
uint32_t RESERVED3[24];
__IO uint32_t IABR[8];
/*!< Interrupt Active bit Register */
uint32_t RESERVED4[56];
__IO uint8_t IP[240];
/*!< Interrupt Priority Register, 8Bit wide */
uint32_t RESERVED5[644];
__O uint32_t STIR;
/*!< Software Trigger Interrupt Register */
} NVIC_Type;
STM32 的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能方便
的使用 STM32 的中断。下面重点介绍这几个寄存器:
ISER[8]:ISER 全称是:Interrupt Set-Enable Registers,这是一个中断使能寄存器组。上面
说了 CM3 内核支持 256 个中断,这里用 8 个 32 位寄存器来控制,每个位控制一个中断。但是
STM32F103 的可屏蔽中断只有 60 个,所以对我们来说,有用的就是两个(ISER[0]和 ISER[1]),
总共可以表示 64 个中断。而 STM32F103 只用了其中的前 60 位。ISER[0]的 bit0~bit31 分别对
应中断 0~31。ISER[1]的 bit0~27 对应中断 32~59;这样总共 60 个中断就分别对应上了。你要
使能某个中断,必须设置相应的 ISER 位为 1,使该中断被使能(这里仅仅是使能,还要配合中
断分组、屏蔽、IO 口映射等设置才算是一个完整的中断设置)。具体每一位对应哪个中断,请
参考 stm32f10x.h 里面的第 140 行处(针对编译器 MDK5 来说)。
ICER[8]:全称是:Interrupt Clear-Enable Registers,是一个中断除能寄存器组。该寄存器组
与 ISER 的作用恰好相反,是用来清除某个中断的使能的。其对应位的功能,也和 ICER 一样。
这里要专门设置一个 ICER 来清除中断位,而不是向 ISER 写 0 来清除,是因为 NVIC 的这些寄
存器都是写 1 有效的,写 0 是无效的。具体为什么这么设计,请看《CM3 权威指南》第 125 页,
NVIC 概览一章。
ISPR[8]:全称是:Interrupt Set-Pending Registers,是一个中断挂起控制寄存器组。每个位
对应的中断和 ISER 是一样的。通过置 1,可以将正在进行的中断挂起,而执行同级或更高级别
的中断。写 0 是无效的。
ICPR[8]:全称是:Interrupt Clear-Pending Registers,是一个中断解挂控制寄存器组。其作
用与 ISPR 相反,对应位也和 ISER 是一样的。通过设置 1,可以将挂起的中断接挂。写 0 无效。
IABR[8]:全称是:Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。对应位
所代表的中断和 ISER 一样,如果为 1,则表示该位所对应的中断正在被执行。这是一个只读寄
存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。
IP[240]:全称是:Interrupt Priority Registers,是一个中断优先级控制的寄存器组。这个寄
存器组相当重要!STM32 的中断分组与这个寄存器组密切相关。IP 寄存器组由 240 个 8bit 的寄
存器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 240 个可屏蔽中断。而 STM32 只用到
了其中的前 60 个。IP[59]~IP[0]分别对应中断 59~0。而每个可屏蔽中断占用的 8bit 并没有全部
使用,而是 只用了高 4 位。这 4 位,又分为抢占优先级和子优先级。抢占优先级在前,子优先
级在后。而这两个优先级各占几个位又要根据 SCB->AIRCR 中的中断分组设置来决定。
这里简单介绍一下 STM32 的中断分组:STM32 将中断分为 5 个组,组 0~4。该分组的设
置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。具体的分配关系如表 4.5.1 所示:
表 4.5.1 AIRCR 中断分组设置表
通过这个表,我们就可以清楚的看到组 0~4 对应的配置关系,例如组设置为 3,那么此时
所有的 60 个中断,每个中断的中断优先寄存器的高四位中的最高 3 位是抢占优先级,低 1 位是
响应优先级。每个中断,你可以设置抢占优先级为 0~7,响应优先级为 1 或 0。抢占优先级的
级别高于响应优先级。而数值越小所代表的优先级就越高。
这里需要注意两点:第一,如果两个中断的抢占优先级和响应优先级都是一样的话,则看
哪个中断先发生就先执行;第二,高优先级的抢占优先级是可以打断正在进行的低抢占优先级
中断的。而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断。
结合实例说明一下:假定设置中断优先级组为 2,然后设置中断 3(RTC 中断)的抢占优先级
为 2,响应优先级为 1。中断 6(外部中断 0)的抢占优先级为 3,响应优先级为 0。中断 7(外
部中断 1)的抢占优先级为 2,响应优先级为 0。那么这 3 个中断的优先级顺序为:中断 7>中
断 3>中断 6。
上面例子中的中断 3 和中断 7 都可以打断中断 6 的中断。而中断 7 和中断 3 却不可以相互
打断!
通过以上介绍,我们熟悉了 STM32 中断设置的大致过程。接下来我们介绍如何使用 HAL
库函数实现以上中断分组设置以及中断优先级管理,使得我们以后的中断设置简单化。NVIC
中断管理相关函数主要在 HAL 库关键文件 stm32f1xx_hal_cortex.c 中定义。
首先要讲解的是中断优先级分组函数 HAL_NVIC_SetPriorityGrouping,其函数申明如下:
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);
这个函数的作用是对中断的优先级进行分组,这个函数在系统中只能被调用一次,一旦分
组确定就最好不要更改,否则容易造成程序分组混乱。这个函数我们可以找到其函数体内容如
下:
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
/* Check the parameters */
assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup));
/* Set the PRIGROUP[10:8] bits according to the PriorityGroup parameter value */
NVIC_SetPriorityGrouping(PriorityGroup);
}
从函数体以及注释可以看出,这个函数是通过调用函数 NVIC_SetPriorityGrouping 来进行
中断优先级分组设置。通过查找(参考 3.5.3 小节 MDK 中“Go to definition of”的使用方法),
我们可以知道函数 NVIC_SetPriorityGrouping 是在文件 core_cm3.h 头文件中定义的。接下来,
我们分析一下函数 NVIC_SetPriorityGrouping 函数定义。定义如下:
__STATIC_INLINE void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
uint32_t reg_value;
uint32_t PriorityGroupTmp = (PriorityGroup & (uint32_t)0x07UL);
reg_value = SCB->AIRCR;
reg_value& = ~((uint32_t)(SCB_AIRCR_VECTKEY_Msk |
SCB_AIRCR_PRIGROUP_Msk));
reg_value = (reg_value | ((uint32_t)0x5FAUL << SCB_AIRCR_VECTKEY_Pos) |
(PriorityGroupTmp << 8U) );
SCB->AIRCR = reg_value;
}
从函数内容可以看出,这个函数主要作用是通过设置 SCB->AIRCR 寄存器的值来设置中断
优先级分组,这在前面寄存器讲解的过程中已经讲到。
关于函数 HAL_NVIC_SetPriorityGrouping 的函数体内容解读我就给大家介绍到这里。接下来我
们来看看这个函数的入口参数。大家继续回到函数 HAL_NVIC_SetPriorityGrouping 的定义可以
看到,函数的最开头有这样一行函数;
assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup));
其中函数 assert_param 是断言函数,它的作用主要是对入口参数的有效性进行判断。也就
是说我们可以通过这个函数知道入口参数在那些范围内有效的。而其入口参数通过在 MDK 中
双击选中“IS_NVIC_PRIORITY_GROUP”,然后右键“Go to defition of...”可以查看到为:
#define IS_NVIC_PRIORITY_GROUP(GROUP)
(((GROUP) == NVIC_PRIORITYGROUP_0) || \
((GROUP) == NVIC_PRIORITYGROUP_1) || \
((GROUP) == NVIC_PRIORITYGROUP_2) || \
((GROUP) == NVIC_PRIORITYGROUP_3) || \
((GROUP) == NVIC_PRIORITYGROUP_4))
从这个内容可以看出,当 GROUP 的值为 NVIC_PRIORITYGROUP_0
~NVIC_PRIORITYGROUP_4 的时候,IS_NVIC_PRIORITY_GROUP 的值才是为真。这也就是
我 们 上 面 表 4.5.1 讲 解 的 , 分 组 范 围 为 0-4 , 对 应 的 入 口 参 数 为 宏 定 义 值
NVIC_PriorityGroup_0~NVIC_PriorityGroup_4。比如我们设置整个系统的中断优先级分组为 2,
那么方法是:
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
这样就确定了中断优先级分组为 2,也就是 2 位抢占优先级,2 位响应优先级,抢占优先级
和响应优先级的值的范围均为 0-3。
讲到这样,大家对怎么进行系统的中断优先级分组设置,以及具体的中断优先级设置函数
HAL_NVIC_SetPriorityGrouping 的内部函数实现都有了一个详细的理解。接下来我们来看看在
HAL 库里面,是怎样调用 HAL_NVIC_SetPriorityGrouping 函数进行分组设置的。
打开 stm32f1xx_hal.c 文件可以看到,文件内部定义了 HAL 库初始化函数 HAL_Init,这个
函数非常重要,其作用主要是对中断优先级分组,FLASH 以及硬件层进行初始化,我们在 3.1
小节对其进行了比较详细的讲解。这里我们只需要知道,在系统主函数 main 开头部分,我们都
会首先调用 HAL_Init 函数进行一些初始化操作。在 HAL_Init 内部,有如下一行代码:
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
这行代码的作用是把系统中断优先级分组设置为分组 4,这在我们前面已经详细讲解。也
就是说,在主函数中调用 HAL_Init 函数之后,在 HAL_Init 函数内部会通过调用我们之前讲解
的 HAL_NVIC_SetPriorityGrouping 函数来进行系统中断优先级分组设置。所以,我们要进行中
断优先级分组设置,只需要修改 HAL_Init 函数内部的这行代码即可。中断优先级分组的内容我
们就给大家讲解到这里。
设置好了系统中断分组,那么对于每个中断我们又怎么确定他的抢占优先级和响应优先级
呢?官方 HAL 库文件 stm32f1xx_hal_cortex.c 中定义了三个单个中断优先级设置函数。函数如
下:
void HAL_NVIC_SetPriority(IRQn_Type IRQn,
uint32_t PreemptPriority, uint32_t SubPriority);
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);
void HAL_NVIC_DisableIRQ(IRQn_Type IRQn);
第一个函数 HAL_NVIC_SetPriority 是用来设置单个优先级的抢占优先级和响应优先级的
值。
第二个函数 HAL_NVIC_EnableIRQ 是用来是鞥呢某个中断通道。
第三个函数 HAL_NVIC_DisableIRQ 是用来清除某个中断使能的,也就是中断失能。
这三个函数的使用都非常简单,对具体的调用方法,大家可以参考我们后面外部中断实验
讲解。
这里大家还需要注意,中断优先级分组和中断优先级设置是两个不同的概念。中断优先级
分组是用来设置整个系统对于中断分组设置为那个分组,分组号为 0-4,设置函数为
HAL_NVIC_SetPriorityGrouping,确定了中断优先级分组号,也就确定了系统对于单个中断的
抢占优先级和响应优先级设置各占几个位(对应表 4.5.1)。设置好中断优先级分组,确定了分
组号之后,接下来我们就是要对单个优先级进行中断优先级设置。也就是这个中断的抢占优先
级和响应优先级的值,设置方法就是我们上面讲解的三个函数。
最后我们总结一下中断优先级设置的步骤:
①系统运行开始的时候设置中断分组。确定组号,也就是确定抢占优先级和响应优先级的
分配位数。设置函数为 HAL_NVIC_PriorityGroupConfig。对于 HAL 库,在文件 stm32f1xx_hal.c
内部定义函数 HAL_Init 中有调用 HAL_NVIC_PriorityGroupConfig 函数进行相关设置,所以我
们只需要修改 HAL_Init 内部对中断优先级分组设置即可。
②设置单个中断的中断优先级别和使能响应中断通道,使用到的函数主要为函数
HAL_NVIC_SetPriority 和函数 HAL_NVIC_EnableIRQ。
4.6 HAL 库中寄存器地址名称映射分析
之所以要讲解这部分知识,是因为经常会遇到客户提到不明白 HAL 库中那些结构体是怎么
与寄存器地址对应起来的。这里我们就做一个简要的分析吧。
首先我们看看 51 中是怎么做的。51 单片机开发中经常会引用一个 reg51.h 的头文件,下
面我们看看他是怎么把名字和寄存器联系起来的:
sfr P0 =0x80;
sfr 也是一种扩充数据类型,点用一个内存单元,值域为 0~255。利用它可以访问 51 单片
机内部的所有特殊功能寄存器。如用 sfr P1 = 0x90 这一句定义 P1 为 P1 端口在片内的寄存
器。然后我们往地址为 0x80 的寄存器设值的方法是:P0=value;
那么在 STM32 中,是否也可以这样做呢??答案是肯定的。肯定也可以通过同样的方
式来做,但是 STM32 因为寄存器太多太多,如果一一以这样的方式列出来,那要好大的篇
幅,既不方便开发,也显得太杂乱无序的感觉。所以 MDK 采用的方式是通过结构体来将
寄存器组织在一起。下面我们就讲解 MDK 是怎么把结构体和地址对应起来的,为什么我
们修改结构体成员变量的值就可以达到操作对应寄存器的值。这些事情都是在 stm32f10x.h
文件中完成的。我们通过 GPIOA 的几个寄存器的地址来讲解吧。
首先我们可以查看《STM32 中文参考手册 V10》中的寄存器地址映射表(P159):
图 4.6.1 GPIO 寄存器地址映像
从这个表我们可以看出,GPIOA 的 7 个寄存器都是 32 位的,所以每个寄存器占有 4
个地址,一共占用 28 个地址,地址偏移范围为(000h~01Bh)。这个地址偏移是相对 GPIOA
的基地址而言的。GPIOA 的基地址是怎么算出来的呢?因为 GPIO 都是挂载在 APB2 总线
之上,所以它的基地址是由 APB2 总线的基地址 GPIOA 在 APB2 总线上的偏移地址决定
的。同理依次类推,我们便可以算出 GPIOA 基地址了。这里设计到总线的一些知识,我们
在后面会讲到。下面我们打开 stm32f10x.h 定位到 GPIO_TypeDef 定义处:
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
然后定位到:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
可以看出,GPIOA 是将 GPIOA_BASE 强制转换为 GPIO_TypeDef 指针,这句话的意思是,
GPIOA 指向地址 GPIOA_BASE,GPIOA_BASE 存放的数据类型为 GPIO_TypeDef。然后双
击“GPIOA_BASE”选中之后右键选中“Go to definition of ”,便可一查看 GPIOA_BASE
的宏定义:
#define GPIOA_BASE (APB2PERIPH_BASE 0x0800)
依次类推,可以找到最顶层:
#define APB2PERIPH_BASE (PERIPH_BASE 0x10000)
#define PERIPH_BASE ((uint32_t)0x40000000)
所以我们便可以算出 GPIOA 的基地址位:
GPIOA_BASE= 0x40000000 0x10000 0x0800=0x40010800
下面我们再跟《STM32 中文参考手册 V10》比较一下看看 GPIOA 的基地址是不是
0x40010800。截图 P28 存储器映射表我们可以看到,GPIOA 的起始地址也就是基地址确实
是 0x40010800:
图 4.6.2 GPIO 存储器地址映射表
同样的道理,我们可以推算出其他外设的基地址。
上面我们已经知道 GPIOA 的基地址,那么那些 GPIOA 的 7 个寄存器的地址又是怎么
算出来的呢??在上面我们讲过 GPIOA 的各个寄存器对于 GPIOA 基地址的偏移地址,所
以我们自然可以算出来每个寄存器的地址。
GPIOA 的寄存器的地址=GPIOA 基地址 寄存器相对 GPIOA 基地址的偏移值
这个偏移值在上面的寄存器地址映像表中可以查到。
那么在结构体里面这些寄存器又是怎么与地址一一对应的呢?这里就涉及到结构体的
一个特征,那就是结构体存储的成员他们的地址是连续的。上面讲到 GPIOA 是指向
GPIO_TypeDef 类型的指针,又由于 GPIO_TypeDef 是结构体,所以自然而然我们就可以算
出 GPIOA 指向的结构体成员变量对应地址了。
表 4.6.3 GPIOA 各寄存器实际地址表
我们可以把 GPIO_TypeDef 的定义中的成员变量的顺序和 GPIOx 寄存器地址映像对比
可以发现,他们的顺序是一致的,如果不一致,就会导致地址混乱了。
这就是为什么固件库里面:GPIOA->BRR=value;就是设置地址为 0x40010800
0x014(BRR 偏移量)=0x40010814 的寄存器 BRR 的值了。它和 51 里面 P0=value 是设置地
址为 0x80 的 P0 寄存器的值是一样的道理。
看到这里你是否会学起来踏实一点呢??STM32 使用的方式虽然跟 51 单片机不一样,
但是原理都是一致的。
,