正点原子ov7725与野火区别_「正点原子STM32Mini板资料连载」第四章 STM32F1 基础知识入门...

1)实验平台:正点原子stm32mini 开发板

2)摘自《正点原子STM32 不完全手册(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

c1de9fc089c7c122982da88cf8d96ac6.png

第四章 STM32F1 基础知识入门

这一章,我们将着重 STM32 开发的一些基础知识,让大家对 STM32 开发有一个初步的了

解,为后面 STM32 的学习做一个铺垫,方便后面的学习。这一章的内容大家第一次看的时候

可以只了解一个大概,后面需要用到这方面的知识的时候再回过头来仔细看看。这章我们分 7

个小结,

·4.1 MDK 下 C 语言基础复习

·4.2 STM32F1 系统架构

·4.3 STM32F103 时钟系统

·4.4 IO 引脚复用器和映射

·4.5 STM32F1 NVIC 中断优先级管理

·4.6 MDK 中寄存器地址名称映射分析

·4.7 MDK 固件库快速开发技巧

4.1 MDK 下 C 语言基础复习

这一节我们主要讲解一下 C 语言基础知识。C 语言知识博大精深,也不是我们三言两语能

讲解清楚,同时我们相信学 STM32F4 这种级别 MCU 的用户,C 语言基础应该都是没问题的。我

们这里主要是简单的复习一下几个 C 语言基础知识点,引导那些 C 语言基础知识不是很扎实的

用户能够快速开发 STM32 程序。同时希望这些用户能够多去复习一下 C 语言基础知识,C 语言

毕竟是单片机开发中的必备基础知识。对于 C 语言基础比较扎实的用户,这部分知识可以忽略

不看。

4.1.1 位操作

C 语言位操作相信学过 C 语言的人都不陌生了,简而言之,就是对基本类型变量可以在位级

别进行操作。这节的内容很多朋友都应该很熟练了,我这里也就点到为止,不深入探讨。下面

我们先讲解几种位操作符,然后讲解位操作使用技巧。

C 语言支持如下 6 种位操作

9d93f24e45d068a47cfeb20709e983bf.png

表 4.1.1 16 种位操作

这些与或非,取反,异或,右移,左移这些到底怎么回事,这里我们就不多做详细,相信

大家学 C 语言的时候都学习过了。如果不懂的话,可以百度一下,非常多的知识讲解这些操作

符。下面我们想着重讲解位操作在单片机开发中的一些实用技巧。

1) 不改变其他位的值的状况下,对某几个位进行设值。

这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,

然后用|操作符设值。比如我要改变 GPIOA->ODR 的状态,可以先对寄存器的值进行&清零

操作

GPIOA->ODR &=0XFF0F; //将第 4-7 位清 0

然后再与需要设置的值进行|或运算

GPIOA->ODR |=0X0040;

//设置相应位的值,不改变其他位的值

2) 移位操作提高代码的可读性。

移位操作在单片机开发中也非常重要,我们来看看下面一行代码

GPIOA->ODR| = 1 << 5;

这个操作就是将 ODR 寄存器的第 5 位设置为 1,为什么要通过左移而不是直接设置一个

固定的值呢?其实,这是为了提高代码的可读性以及可重用性。这行代码可以很直观

明了的知道,是将第 5 位设置为 1,其他位的值不变。如果你写成

GPIOA->ODR =0x0020;

这样的代码可读性非常差同时也不好重用。

3) ~取反操作使用技巧

例如 GPIOA->ODR 寄存器的每一位都用来设置一个 IO 口的输出状态,某个时刻我们

希望去设置某一位的值为 0,同时其他位都为 1,简单的作法是直接给寄存器设置一个值:

GPIOA->ODR =0xFFF7;

这样的作法设置第 3 位为 0,但是这样的写法可读性很差。看看如果我们使用取反操作怎

么实现:

GPIOA->ODR= (uint16_t)~(1<<3);

看这行代码应该很容易明白,我们设置的是 ODR 寄存器的第 3 位为 0,其他位为 1,可读性

非常强。

4.1.2 define 宏定义

define 是 C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供

方便。常见的格式:

#define 标识符 字符串

“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。例如:

#define HSI_VALUE ((uint32_t)16000000)

定义标识符 HSI_VALUE 的值为 16000000。这样我们就可以在代码中直接使用标识符

HSI_VALUE,而不用直接使用常量 16000000,同时也很方便我们修改 HSI_VALUE 的值。

至于 define 宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。

4.1.3# ifdef 和 #if defined 条件编译

单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而

当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:

#ifdef 标识符

程序段 1

#else

程序段 2

#endif

它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编译,

否则编译程序段 2。 其中#else 部分也可以没有,即:

#ifdef

程序段 1

#endif

这个条件编译在 MDK 里面是用得很多的,在 stm32f4xx_hal_conf.h 这个头文件中会看到这样的

语句:#ifdef HAL_GPIO_MODULE_ENABLED

#include "stm32f1xx_hal_gpio.h"

#endif

这段代码的作用是判断宏定义标识符 HAL_GPIO_MODULE_ENABLED 是否被定义,如果被定

义了,那么就引入头文件 stm32f1xx_hal_gpio.h。

对于条件编译,还有个常用的格式,如下:

#if defined XXX1

程序段 1

#elif defined XXX2

程序段 2

#elif defined XXXn

程序段 n

#endif

这种写法的作用实际跟 ifdef 很相似,不同的是 ifdef 只能在两个选择中判断是否定义,

而 if defined 可以在多个选择中判断是否定义。

条件编译也是 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

}

但是我们希望在main.c的 changeId(void)函数中使用变量id,这个时候我们就需要在main.c

里面去申明变量 id 是外部定义的了,因为如果不申明,变量 id 的作用域是到不了 main.c 文件

中。看下面 main.c 中的代码:

extern u8 id;//申明变量 id 是在外部定义的,申明可以在很多个文件中进行void test(void){

id=2;

}

在 main.c 中申明变量 id 在外部定义,然后在 main.c 中就可以使用变量 id 了。

对于 extern 申明函数在外部定义的应用,这里我们就不多讲解了。

4.1.5 typedef 类型别名

typedef 用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。

typedef 在 MDK 用得最多的就是定义结构体的类型别名和枚举类型了。

struct _GPIO

{

__IO uint32_t MODER;

__IO uint32_t OTYPER;

};

定义了一个结构体 GPIO,这样我们定义变量的方式为:

struct _GPIO GPIOA;//定义结构体变量 GPIOA

但是这样很繁琐,MDK 中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别

名 GPIO_TypeDef,这样我们就可以在其他地方通过别名 GPIO_TypeDef 来定义结构体变量了。

方法如下:

typedef struct

{

__IO uint32_t MODER;

__IO uint32_t OTYPER;

} GPIO_TypeDef;

Typedef 为结构体定义一个别名 GPIO_TypeDef,这样我们可以通过 GPIO_TypeDef 来定义结构体

变量:

GPIO_TypeDef _GPIOA,_GPIOB;

这里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。 这样是不是方便很多?

4.1.6 结构体

经常很多用户提到,他们对结构体使用不是很熟悉,但是 MDK 中太多地方使用结构体以及

结构体指针,这让他们一下子摸不着头脑,学习 STM32 的积极性大大降低,其实结构体并不是

那么复杂,这里我们稍微提一下结构体的一些知识,还有一些知识我们会在下一节的“寄存器

地址名称映射分析”中讲到一些。

声明结构体类型:

Struct 结构体名{

成员列表;

}变量名列表;

例如:

Struct G_TYPE {

uint32_t Pin;uint32_t Mode;

uint32_t Speed;

}GPIOA,GPIOB;

在结构体申明的时候可以定义变量,也可以申明之后定义,方法是:

Struct 结构体名字 结构体变量列表 ;

例如:struct G_TYPE GPIOA,GPIOB;

结构体成员变量的引用方法是:

结构体变量名字.成员名

比如要引用 GPIOA 的成员 Mode,方法是:GPIOA. Mode;

结构体指针变量定义也是一样的,跟其他变量没有啥区别。

例如:struct G_TYPE *GPIOC;//定义结构体指针变量 GPIOC;

结构体指针成员变量引用方法是通过“->”符号实现,比如要访问 GPIOC 结构体指针指向的结

构体的成员变量 Speed,方法是:

GPIOC-> Speed;

上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到这里,

有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过一个实

例回答一下这个问题。

在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如 IO 口。它的初始化状态

是由几个属性来决定的,比如模式,速度等。对于这种情况,在我们没有学习结构体的时候,

我们一般的方法是:

void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed);

这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里

面再传入一个参数,那么势必我们需要修改这个函数的定义,重新加入上下拉 Pull 这个入口参

数。于是我们的定义被修改为:

void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed,uint32_t Pull);

但是如果我们这个函数的入口参数是随着开发不断的增多,那么是不是我们就要不断的修改函

数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢?

这样如果我们使用到结构体就能解决这个问题了。我们可以在不改变入口参数的情况下,

只需要改变结构体的成员变量,就可以达到上面改变入口参数的目的。

结构体就是将多个变量组合为一个有机的整体。上面的函数中 Pin, Mode,

Speed 和 Pull 这些参数,他们对于 GPIO 而言,是一个有机整体,都是来设置 IO 口参数的,所

以我们可以将他们通过定义一个结构体来组合在一个。MDK 中是这样定义的:

typedef struct

{

uint32_t Pin;

uint32_t Mode;

uint32_t Pull;

uint32_t Speed;

}GPIO_InitTypeDef;

于是,我们在初始化 GPIO 口的时候入口参数就可以是 GPIO_InitTypeDef 类型的变量或者指针

变量了,MDK 中是这样做的:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需

要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义

就可以达到增加变量的目的。

理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,

如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可

以提高你的代码的可读性。

使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作

用就远远不止这个了,同时,MDK 中用结构体来定义外设也不仅仅只是这个作用,这里我们只

是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲

解结构体的一些其他知识。

4.2 STM32F1 系统架构

STM32 的系统架构比 51 单片机就要强大很多了。STM32 系统架构的知识可以在《STM32

中文参考手册 V10》的 P25~28 有讲解,这里我们也把这一部分知识抽取出来讲解,是为了大

家在学习 STM32 之前对系统架构有一个初步的了解。这里的内容基本也是从中文参考手册中

参考过来的,让大家能通过我们手册也了解到,免除了到处找资料的麻烦吧。如果需要详细深

入的了解 STM32 的系统架构,还需要在网上搜索其他资料学习学习。

我们这里所讲的 STM32 系统架构主要针对的 STM32F103 这些非互联型芯片。首先我们看

看 STM32 的系统架构图:

e40faa9072c304d33a58ce432248f06d.png

图 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 STM32F103 时钟系统

STM32F1 时钟系统的知识在《STM32 中文参考手册 V10》第六章复位和时钟控制章节有非

常详细的讲解,网上关于时钟系统的讲解也基本都是参考的这里。这些知识也不是什么原创,

纯粹根据官方提供的中文参考手册和自己的应用心得来总结的,如有不合理之处望大家谅解。

这部分内容我们分 3 个小节来讲解:

·4.3.1 STM32F103 时钟树概述

·4.3.2 STM32F103 时钟初始化配置

·4.3.3 STM32F103 时钟使能和配置

4.3.1 STM32F103 时钟树概述

众所周知,时钟系统是 CPU 的脉搏,就像人的心跳一样。所以时钟系统的重要性就不言而

喻了。

STM32F103的时钟系统比较复杂,不像简单的51单片机一个系统时钟就可以解决一切。

于是有人要问,采用一个系统时钟不是很简单吗?为什么 STM32 要有多个时钟源呢? 因为首

先 STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,

比如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁

干扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。

首先让我们来看看 STM32F103 的时钟系统图:

e7139198622ef4d5394ca160847245c5.png

图 4.3.1.1 STM32F103 时钟系统图

在 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 STM32F103 时钟系统配置

上一小节我们对 STM32F103 时钟树进行了详细讲解,接下来我们来讲解通过 STM32F1 的

HAL 库进行 STM32F103 时钟系统配置步骤。实际上,STM32F1 的时钟系统配置也可以通过图

形化配置工具 STM32CubeMX 来配置生成,这里我们讲解初始化代码,是为了让大家对 STM32

时钟系统有更加清晰的理解。

前面我们讲解过,在系统启动之后,程序会先执行 HAL 库定义的 SystemInit 函数,进行系

统一些初始化配置。那么我们先来看看 SystemInit 程序:

void SystemInit (void)

{

/* 将 RCC 时钟配置重置为默认重置状态(用于调试)*/

RCC->CR |= (uint32_t)0x00000001; //打开 HSION 位

/* 设置 SW, HPRE, PPRE1, PPRE2, ADCPRE 和 MCO 位 */

#if !defined(STM32F105xC) && !defined(STM32F107xC)

RCC->CFGR &= (uint32_t)0xF8FF0000;

#else

RCC->CFGR &= (uint32_t)0xF0FF0000;

#endif /* STM32F105xC */

RCC->CR &= (uint32_t)0xFEF6FFFF; // 复位 HSEON, CSSON 和 PLLON 位

RCC->CR &= (uint32_t)0xFFFBFFFF; // 复位 HSEBYP 位

RCC->CFGR &= (uint32_t)0xFF80FFFF; //复位 CFGR 寄存器

#if defined(STM32F105xC) || defined(STM32F107xC)

RCC->CR &= (uint32_t)0xEBFFFFFF; // 复位 PLL2ON 和 PLL3ON 位

RCC->CIR = 0x00FF0000; // 禁用所有中断并清除挂起位

RCC->CFGR2 = 0x00000000; // 重置 CFGR2 注册

#elif defined(STM32F100xB) || defined(STM32F100xE)

RCC->CIR = 0x009F0000; // 禁用所有中断并清除挂起位

RCC->CFGR2 = 0x00000000; // 重置 CFGR2 注册

#else

RCC->CIR = 0x009F0000; // 禁用所有中断并清除挂起位

#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; //内部 SRAM 中的向量表重定位

#else

SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //在内部 FLASH 中的向量表重定位

#endif

}从上面代码可以看出,SystemInit 主要做了如下三个方面工作:

1) 复位 RCC 时钟配置为默认复位值(默认开始了 HIS)

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);

//设置系统时钟时钟源为 PLL

RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;

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 的时钟值。

接下来我们看看结构体 RCC_OscInitTypeDef 的定义:

typedef struct

{

uint32_t OscillatorType; //需要选择配置的振荡器类型

uint32_t HSEState; //HSE 状态

uint32_t HSEPredivValue; // Prediv1 值

uint32_t LSEState; //LSE 状态

uint32_t HSIState; //HIS 状态

uint32_t HSICalibrationValue; //HIS 校准值

uint32_t LSIState;

//LSI 状态

RCC_PLLInitTypeDef PLL; //PLL 配置

}RCC_OscInitTypeDef;

对于这个结构体,前面几个参数主要是用来选择配置的振荡器类型。比如我们要开启 HSE,

那么我们会设置 OscillatorType 的值为 RCC_OSCILLATORTYPE_HSE,然后设置 HSEState 的值

为 RCC_HSE_ON 开启 HSE。对于其他时钟源 HSI,LSI 和 LSE,配置方法类似。这个结构体还

有一个很重要的成员变量是 PLL,它是结构体 RCC_PLLInitTypeDef 类型。它的作用是配置 PLL

相关参数,我们来看看它的定义:

typedef struct

{

uint32_t PLLState; //PLL 状态

uint32_t PLLSource; //PLL 时钟源

uint32_t PLLMUL; //PLL VCO 输入时钟的乘法因子

}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 的时钟频率,接下来我们就需要设置系统时钟,以及 AHB,APB1 和

APB2 相关参数。

接下来我们来看看步骤 5 中提到的 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);

//设置系统时钟时钟源为 PLL

RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;

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*9 =8MHz*9=72MHz,同时我们选择系统时钟源为

PLL , 所 以 系 统 时 钟 SYSCLK=72MHz 。 AHB 分频系数为 1 ,故其频率为

HCLK=SYSCLK/1=72MHz。APB1 分频系数为 2,故其频率为 PCLK1=HCLK/2=36MHz。APB2

分频系数为 1,故其频率为 PCLK2=HCLK/1=72/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

4.3.3 STM32F1 时钟使能和配置

上一节我们讲解了时钟系统配置步骤。在配置好时钟系统之后,如果我们要使用某些外设,

例如 GPIO,ADC 等,我们还要使能这些外设时钟。这里大家必须注意,如果在使用外设之前

没有使能外设时钟,这个外设是不可能正常运行的。STM32 的外设时钟使能是在 RCC 相关寄

存器中配置的。因为 RCC 相关寄存器非常多,有兴趣的同学可以直接打开《STM32 中文参考手

册 V10》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 ((uint32_t)0x00000001)

所以,我们很容易理解上面代码的作用是设置寄存器 RCC->APB2ENR 寄存器的位 2 为 1。我

们可以从 STM32F1 的中文参考手册中搜索 APB2ENR 寄存器定义,位 2 的作用是用来使用

GPIOA 时钟。APB2ENR 寄存器的位 2 描述如下:

位 2

IOPAEN:IO 端口 A 时钟使能

由软件置 1 和清零

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_USART2_CLK_ENABLE();//使能串口 2 时钟

__HAL_RCC_TIM1_CLK_ENABLE();//使能 TIM1 时钟

我们使用外设的时候需要使能外设时钟,如果我们不需要使用某个外设,同样我们可以禁

止某个外设时钟。禁止外设时钟使用方法和使能外设时钟非常类似,同样是头文件中定义的宏

定义标识符。我们同样以 GPIOA 为例,宏定义标识符为:

#define __HAL_RCC_GPIOA_CLK_DISABLE()

(RCC->APB2ENR &= ~(RCC_APB2ENR_IOPAEN))

同样,宏定义标识符__HAL_RCC_GPIOA_CLK_DISABLE()的作用是设置 RCC->APB2ENR 寄

存器的位 2 为 0,也就是禁止 GPIOA 时钟。具体使用方法我们这里就不做过多讲解,我们这里

同样列出几个常用的禁止外设时钟的宏定义标识符使用方法:

__HAL_RCC_DMA1_CLK_DISABLE();//禁止 DMA1 时钟

__HAL_RCC_USART2_CLK_DISABLE();//禁止串口 2 时钟

__HAL_RCC_TIM1_CLK_DISABLE();//禁止 TIM1 时钟

关于 STM32F1 的外设时钟使能和禁止方法我们就给大家讲解到这里。

4.4 端口复用和重映射

STM32F1 有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO

如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。

这部分知识在《STM32 中文参考手册 V10》的 P109,P116~P121 有详细的讲解哪些 GPIO 管脚是

可以复用为哪些内置外设的。这里我们就不一一讲解。

大家都知道,MCU 都有串口,STM32 有好几个串口。比如说 STM32F103RCT6 有 5 个串口,我

们可以查手册知道,串口 1 的引脚对应的 IO 为 PA9,PA10.PA9,PA10 默认功能是 GPIO,所以当

PA9,PA10 引脚作为串口 1 的 TX,RX 引脚使用的时候,那就是端口复用。

d60eddca04acd5efa7c7848dc66e3917.png

图 4.4.1.1 串口 1 复用管脚

接下来我们以串口 1 为例来讲解配置 GPOPA.9,GPIOA.10 口为串口 1 复用功能的一般步骤。

① 首先,我们要使用 IO 复用功能,必须先打开对应的 IO 时钟和复用功能外设时钟,这里

我们使用了 GPIOA 以及 USART1,所以我们需要使能 GPIOA 和 USART1 时钟。方法如下:

__HAL_RCC_GPIOA_CLK_ENABLE();

//使能 GPIOA 时钟

__HAL_RCC_USART1_CLK_ENABLE();

//使能 USART1 时钟

__HAL_RCC_AFIO_CLK_ENABLE(); //使能辅助功能 IO 时钟

② 然后,我们在 GIPOx_MODER 寄存器中将所需 IO(对于串口 1 是 PA9,PA10)配置为复

用功能。

③ 最后,我们还需要对 IO 口的其他参数,例如上拉/下拉以及输出速度等进行配置。

上面三步,在我们 HAL 库中是通过 HAL_GPIO_Init 函数来实现的,参考代码如下:

GPIO_InitTypeDef GPIO_Initure;

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

通过上面的配置,PA9 复用为串口 1 的发送引脚。这个时候,PA9 将不再作为普通的 IO 口

使用。对于 PA10,配置方法一样,修改 Pin 成员变量值为 PIN_10 即可。

STM32F1 的端口复用和映射就给大家讲解到这里,希望大家课余结合相关实验工程和手册

巩固本小节知识。

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

{

__IOM uint32_t ISER[8U];

uint32_t RESERVED0[24U];

__IOM uint32_t ICER[8U];

uint32_t RSERVED1[24U];

__IOM uint32_t ISPR[8U];

uint32_t RESERVED2[24U];

__IOM uint32_t ICPR[8U];

uint32_t RESERVED3[24U];

__IOM uint32_t IABR[8U];

uint32_t RESERVED4[56U];

__IOM uint8_t IP[240U];

uint32_t RESERVED5[644U];

__OM uint32_t STIR;

} 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 所示:

e87facd217274b96a1493b909f6c5cc9.png

表 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 却不可以相互

打断!

通过以上介绍,我们熟悉了 STM32F103 中断设置的大致过程。接下来我们介绍如何使用

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; /* read old register configuration */

reg_value&=~((uint32_t)(SCB_AIRCR_VECTKEY_Msk |SCB_AIRCR_PRIGROUP_Msk));

reg_value = (reg_value|((uint32_t)0x5FAUL << SCB_AIRCR_VECTKEY_Pos) |

(PriorityGroupTmp<< SCB_AIRCR_PRIGROUP_Pos) );

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 是怎么把结构体和地址对应起来的,为什么我

们修改结构体成员变量的值就可以达到操作对应寄存器的值。这些事情都是在 stm32f1xx.h

文件中完成的。我们通过 GPIOA 的几个寄存器的地址来讲解吧。

首先我们可以查看《STM32 中文参考手册 V10》中的寄存器地址映射表(P129)。这里

我们选用 GPIOA 为例来讲解。GPIO 寄存器地址映射如下表 4.6.1:

25e932ed11e5b567a098fa7657a2b387.png

表 4.6.1 GPIO 寄存器地址映射表

从这个表我们可以看出,GPIOA 的 7 个寄存器都是 32 位的,所以每个寄存器占有 4

个地址,一共占用 28 个地址,地址偏移范围为(000h~01Bh)。这个地址偏移是相对 GPIOA

的基地址而言的。GPIOA 的基地址是怎么算出来的呢?因为 GPIO 都是挂载在 APB2 总线

之上,所以它的基地址是由 APB2 总线的基地址+GPIOA 在 APB2 总线上的偏移地址决定

的。同理依次类推,我们便可以算出 GPIOA 基地址了。下面我们打开 stm32f103.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。

然后在 MDK 中双击“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:

ca485bc7035971f5d4c02b718a1a07ac.png

图 4.6.2 GPIO 存储器地址映射表

同样的道理,我们可以推算出其他外设的基地址。

上面我们已经知道 GPIOA 的基地址,那么那些 GPIOA 的 7 个寄存器的地址又是怎么

算出来的呢??在上面我们讲过 GPIOA 的各个寄存器对于 GPIOA 基地址的偏移地址,所

以我们自然可以算出来每个寄存器的地址。

GPIOA 的寄存器的地址=GPIOA 基地址+寄存器相对 GPIOA 基地址的偏移值

这个偏移值在上面的寄存器地址映像表中可以查到。

那么在结构体里面这些寄存器又是怎么与地址一一对应的呢?这里就涉及到结构体的

一个特征,那就是结构体存储的成员他们的地址是连续的。上面讲到 GPIOA 是指向

GPIO_TypeDef 类型的指针,又由于 GPIO_TypeDef 是结构体,所以自然而然我们就可以算

出 GPIOA 指向的结构体成员变量对应地址了。

df399e196faa56936039d26b395ce8ab.png

表 4.6.3 GPIOA 各寄存器实际地址表

我们可以把GPIO_TypeDef的定义中的成员变量的顺序和GPIOx寄存器地址映像对比

可以发现,他们的顺序是一致的,如果不一致,就会导致地址混乱了。

这就是为什么固件库里面:GPIOA->BRR=value;就是设置地址为0x40010800

+0x014(BRR偏移量)=0x40010814的寄存器BRR的值了。它和51里面P0=value是设置地

址为0x80的P0寄存器的值是一样的道理。看到这里你是否会学起来踏实一点呢??STM32使用的方式虽然跟51单片机不一样,

但是原理都是一致的。

.7 MDK中使用HAL库快速组织代码技巧

这一节主要讲解在MDK中使用HAL库开发的一些小技巧,仅供初学者参考。这节的知识

大家可以在学习第一个跑马灯实验的时候参考一下,对初学者应该很有帮助。我们就用最简单

的GPIO初始化函数为例。

现在我们要初始化某个GPIO端口,我们要怎样快速操作呢?在头文件stm32f1xx_hal_gpio.h 头文件中,声明 GPIO 初始化函数为:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

现在我们想写初始化函数,那么我们在不参考其他代码的前提下,怎么快速组织代码呢?

首先,我们可以看出,函数的入口参数是 GPIO_TypeDef 类型指针和 GPIO_InitTypeDef 类

型指针,因为 GPIO_TypeDef 入口参数比较简单,所以我们就通过第二个入口参数

GPIO_InitTypeDef 类型指针来讲解。双击 GPIO_InitTypeDef 后右键选择“Go to definition of…”,

如下图 4.7.1:

306e59261d4380bf99d51053b624eb1b.png

图 4.7.1 查看类型定义方法

于是定位到 stm32f1xx_hal_gpio.h 中 GPIO_InitTypeDef 的定义处:

typedef struct

{

uint32_t Pin;

uint32_t Mode;

uint32_t Pull;

uint32_t Speed;

}GPIO_InitTypeDef;

可以看到这个结构体有 4 个成员变量,这也告诉我们一个信息,一个 GPIO 口的状态是由模式

(Mode),速度(Speed)以及上下拉(Pull)来决定的。我们首先要定义一个结构体变量,下面

我们定义:

GPIO_InitTypeDef GPIO_InitStructure;

接着我们要初始化结构体变量 GPIO_InitStructure。首先我们要初始化成员变量 Pin,这个时候我

们就有点迷糊了,这个变量到底可以设置哪些值呢?这些值的范围有什么规定吗?

这里我们就回到 HAL_GPIO_Init 声明处,同样双击 HAL_GPIO_Init,右键点击“Go to

definition of …”,这样光标定位到 stm32f1xx_hal_gpio.c 文件中的 HAL_GPIO_Init 函数体开始处,

我们可以看到在函数中有如下几行:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)

{

…//此处省略部分代码

assert_param(IS_GPIO_ALL_INSTANCE(GPIOx));

assert_param(IS_GPIO_PIN(GPIO_Init->Pin));

assert_param(IS_GPIO_MODE(GPIO_Init->Mode));

…//此处省略部分代码

assert_param(IS_GPIO_PULL(GPIO_Init->Pull));

…//此处省略部分代码

}

顾名思义,assert_param 是断言语句,是对函数入口参数的有效性进行判断,所以我们可以从

这个函数入手,确定入口参数范围。第一行是对第一个参数 GPIOx 进行有效性判断,双击

“IS_GPIO_ALL_INSTANCE”右键点击“go to defition of…” 定位到了下面的定义:

#define IS_GPIO_ALL_INSTANCE(INSTANCE) (((INSTANCE) == GPIOA) ||

((INSTANCE) == GPIOB) ||

((INSTANCE) == GPIOC) ||

((INSTANCE) == GPIOD) ||

((INSTANCE) == GPIOE) ||

((INSTANCE) == GPIOF) ||

((INSTANCE) == GPIOG))

很明显可以看出,GPIOx 的取值规定只允许是 GPIOA~GPIOG。

同样的办法,我们双击“IS_GPIO_PIN” 右键点击“go to defition of…”,定位到下面的定义:

#define IS_GPIO_PIN(PIN) (((((uint32_t)PIN) & GPIO_PIN_MASK ) != 0x00u)

&& ((((uint32_t)PIN) & ~GPIO_PIN_MASK) == 0x00u))

同时,宏定义标识符 GPIO_PIN_MASK 的定义为:

#define GPIO_PIN_MASK 0x0000FFFFu

从上面可以看出,PIN 取值只要低 16 位不为 0 即可。这里需要大家注意,因为一组 IO 口只有

16 个 IO,实际上 PIN 的值在这里只有低 16 位有效,所以 PIN 的取值范围为 0x0001~0xFFFF。

那么是不是我们写代码初始化就是直接给一个 16 位的数字呢?这也是可以的,但是大多数情况

下,我们不会直接在入口参数处设置一个简单的数字,因为这样代码的可读性太差,HAL 库会

将这些数字的含义通过宏定义定义出来,这样可读性大大增强。我们可以看到在

GPIO_PIN_MASK 宏定义的上面还有数行宏定义:

#define GPIO_PIN_0 ((uint16_t)0x0001)

#define GPIO_PIN_1 ((uint16_t)0x0002)

#define GPIO_PIN_2 ((uint16_t)0x0004)

…//此处省略部分定义

#define GPIO_PIN_14 ((uint16_t)0x4000)

#define GPIO_PIN_15 ((uint16_t)0x8000)

#define GPIO_PIN_All ((uint16_t)0xFFFF)

这些宏定义 GPIO_PIN_0 ~ GPIO_PIN_All 就是 HAL 库事先定义好的,我们写代码的时候初始

化结构体 成员变量 Pin 的时候入口参数可以是这些宏定义标识符。

同理,对于成员变量 Pull,我们用同样的方法,可以找到其取值范围定义为:

#define IS_GPIO_PULL(PULL) (((PULL) == GPIO_NOPULL)

|| ((PULL) == GPIO_PULLUP) || ((PULL) == GPIO_PULLDOWN))

也就是 PULL 的 取 值 范 围 只 能 是 标 识 符 GPIO_NOPULL , GPIO_PULLUP 以 及

GPIO_PULLDOWN。

对于成员变量 Mode,方法都是一样的,这里基于篇幅考虑我们就不重复讲解。讲到这里,

我们基本对 HAL_GPIO_Init 的入口参数有比较详细的了解了。于是我们可以组织起来下面的代

码:

GPIO_InitTypeDef GPIO_Initure;

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

接着又有一个问题会被提出来,这个初始化函数一次只能初始化一个 IO 口吗?我要同时

初始化很多个 IO 口,是不是要复制很多次这样的初始化代码呢?

这里又有一个小技巧了。从上面的 GPIO_PIN_X 的宏定义我们可以看出,这些值是 0,1,2,4

这样的数字,所以每个 IO 口选定都是对应着一个位,16 位的数据一共对应 16 个 IO 口。这个

位为 0 那么这个对应的 IO 口不选定,这个位为 1 对应的 IO 口选定。如果多个 IO 口,他们都

是对应同一个 GPIOx,那么我们可以通过|(或)的方式同时初始化多个 IO 口。这样操作的前

提是,他们的 Mode,Speed 和 Pull 参数值相同,因为这些参数并不能一次定义多种。所以初始

化多个具有相同配置的 IO 口的方式可以是如下:

GPIO_InitTypeDef GPIO_Initure;

GPIO_Initure.Pin=GPIO_PIN_9| GPIO_PIN_10| GPIO_PIN_11; //PA9,PA10,PA11

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 ,PA10,PA11

对于那些参数可以通过|(或)的方式连接,这既有章可循,同时也靠大家在开发过程中不断积累。

大家会觉得上面讲解有点麻烦,每次要去查找 assert_param()这个函数去寻找,那么有没有

更好的办法呢?大家可以打开 GPIO_InitTypeDef 结构体定义:

typedef struct

{

uint32_t Pin; /*!< Specifies the GPIO pins to be configured.

This parameter can be any value of @ref GPIO_pins_define */

uint32_t Mode; /*!< Specifies the operating mode for the selected pins.

This parameter can be a value of @ref GPIO_mode_define */

uint32_t Pull; /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.

This parameter can be a value of @ref GPIO_pull_define */

uint32_t Speed; /*!< Specifies the speed for the selected pins.

This parameter can be a value of @ref GPIO_speed_define */

}GPIO_InitTypeDef;

从上图的结构体成员后面的注释我们可以看出 Pin 的意思是

“Specifies the GPIO pins to be configured.

This parameter can be any value of @ref GPIO_pins_define”。

从这段注释可以看出 Pin 的取值需要参考注释 GPIO_pins_define,大家可以在 MDK 中搜索注释

GPIO_pins_define,就可以找到上面我们提到的 Pin 的取值范围宏定义。如果要确定详细的信息

我们就得去查看手册了。对于去查看手册的哪个地方,你可以在函数 HAL_GPIO_Init ()的函数

体中搜索 Pin 关键字,然后查看库函数设置 Pin 是设置的哪个寄存器的哪个位,然后去中文参

考手册查看该寄存器相应位的定义以及前后文的描述。

这一节我们就讲解到这里,希望能对大家的开发有帮助。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值