1)实验平台:ALIENTEK NANO STM32F411 V1开发板
2)摘自《正点原子STM32F4 开发指南(HAL 库版》关注官方微信号公众号,获取更多资料:正点原子
![4ad334a2b5b38396ca92878b866f3a71.png](https://i-blog.csdnimg.cn/blog_migrate/c2f06f195330924d4a1511751e02a5d5.jpeg)
第四章 STM32F4 开发基础知识入门
这一章,我们将着重 STM32 开发的一些基础知识,让大家对 STM32 开发有一个初步的了
解,为后面 STM32 的学习做一个铺垫,方便后面的学习。这一章的内容大家第一次看的时候
可以只了解一个大概,后面需要用到这方面的知识的时候再回过头来仔细看看。这章我们分 7
个小结,
·4.1 MDK 下 C 语言基础复习
·4.2 STM32F4 系统架构
·4.3 STM32F411 时钟系统
·4.4 端口复用和重映射
·4.5 STM32F4 NVIC 中断管理
·4.6 MDK 中寄存器地址名称映射分析
·4.7 MDK 固件库快速开发技巧
·4.8 手把手教你入门 STM32CubeMX 图像配置工具
4.1 MDK 下 C 语言基础复习
这一节我们主要讲解一下 C 语言基础知识。C 语言知识博大精深,也不是我们三言两语能
讲解清楚,同时我们相信学 STM32 这种级别 MCU 的用户,C 语言基础应该都是没问题的。我们
这里主要是简单的复习一下几个 C 语言基础知识点,引导那些 C 语言基础知识不是很扎实的用
户能够快速开发 STM32 程序。同时希望这些用户能够多去复习一下 C 语言基础知识,C 语言毕
竟是单片机开发中的必备基础知识。对于 C 语言基础比较扎实的用户,这部分知识可以忽略不
看。
4.1.1 位操作
C 语言位操作相信学过 C 语言的人都不陌生了,简而言之,就是对基本类型变量可以在位级
别进行操作。这节的内容很多朋友都应该很熟练了,我这里也就点到为止,不深入探讨。下面
我们先讲解几种位操作符,然后讲解位操作使用技巧。
C 语言支持如下 6 中位操作
![203afd9e25d7f2a8b80c7816da870e9b.png](https://i-blog.csdnimg.cn/blog_migrate/7eb73a74d563100f9714be46148836bd.jpeg)
这些与或非,取反,异或,右移,左移这些到底怎么回事,这里我们就不多做详细,相信
大家学 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 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;
uint32_t Alternate;
}GPIO_InitTypeDef;
于是,我们在初始化 GPIO 口的时候入口参数就可以是 GPIO_InitTypeDef 类型的变量或者指针
变量了,MDK 中是这样做的:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需
要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义
就可以达到增加变量的目的。
理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,
如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可
以提高你的代码的可读性。
使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作
用就远远不止这个了,同时,MDK 中用结构体来定义外设也不仅仅只是这个作用,这里我们只
是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲
解结构体的一些其他知识。
4.2 STM32F4 系统架构
STM32F4 的系统架构比 51 单片机就要强大很多了。STM32F4 系统架构的知识可以在
《STM32F411xC/E 参考手册》第二章有讲解,这里我们也把这一部分知识抽取出来讲解,是为
了大家在学习 STM32F4 之前对系统架构有一个初步的了解。这里的内容基本也是从中文参考
手册中参考过来的,让大家能通过我们手册也了解到,免除了到处找资料的麻烦吧。如果需要
详细深入的了解 STM32F4 的系统架构,还需要在网上搜索其他资料学习学习。
我们这里所讲的 STM32F4 系统架构主要针对的 STM32F411xC/E 系列芯片。首先我们看看
STM32 的系统架构图:
![cbe605da33579e099297e9218f346642.png](https://i-blog.csdnimg.cn/blog_migrate/a80a1042ade9e8500ee6b35623c8bc95.jpeg)
图 4.2.1 STM32F411 系统架构图
主系统由 32 位多层 AHB 总线矩阵构成。总线矩阵用于主控总线之间的访问仲裁管理。仲
裁采集循环调度算法。总线矩阵可实现以下部分互联:
六条主控总线是:
Cortex-M4 内核 I 总线, D 总线和 S 总线;
DMA1 存储器总线, DMA2 存储器总线;
DMA2 外设总线;
四条被控总线:
内部 FLASH ICode 总线;
内部 FLASH DCode 总线;
1
AHB1 外设 和 AHB2 外设;
下面我们具体讲解一下图中几个总线的知识:
1 I 总线(S0):此总线用于将 Cortex-M4 内核的指令总线连接到总线矩阵。内核通过此总
线获取指令。此总线访问的对象是包括代码的存储器。
2 D 总线(S1):此总线用于将 Cortex-M4 数据总线和 64KB CCM 数据 RAM 连接到总线矩
阵。内核通过此总线进行立即数加载和调试访问。
3 S 总线(S2):此总线用于将 Cortex-M4 内核的系统总线连接到总线矩阵。此总线用于访
问位于外设或 SRAM 中的数据。
4 DMA 存储器总线(S3,S4):此总线用于将 DMA 存储器总线主接口连接到总线矩阵。
DMA 通过此总线来执行存储器数据的传入和传出。
5 DMA 外设总线:此总线用于将 DMA 外设主总线接口连接到总线矩阵。DMA 通过此
总线访问 AHB 外设或执行存储器之间的数据传输。
对于系统架构的知识,在刚开始学习 STM32 的时候只需要一个大概的了解,大致知道是个
什么情况即可。对于寻址之类的知识,这里就不做深入的讲解,参考手册都有很详细的讲解。
4.3 STM32F4 时钟系统
STM32F4 时钟系统的知识在《STM32F411xC/E 参考手册》第六章复位和时钟控制章节有
非常详细的讲解,网上关于时钟系统的讲解也基本都是参考的这里,这些知识也不是什么原创,
纯粹的是看网友发的帖子和手册来总结的,如有不合理之处望大家谅解。
这里请大家注意,STM32F411 的时钟系统和 STM32F407 的时钟系统有细微的区别,我们这里
是针对 STM32F411 的时钟系统进行讲解。
这部分内容我们分 3 个小节来讲解:
4.3.1 STM32F411 时钟树概述
4.3.2 STM32F411 时钟初始化配置
4.3.3 STM32F411 时钟使能和配置
4.3.1 STM32F411 时钟树概述
众所周知,时钟系统是 CPU 的脉搏,就像人的心跳一样。所以时钟系统的重要性就不言而
喻了。 STM32 的时钟系统比较复杂,不像简单的 51 单片机一个系统时钟就可以解决一切。于
是有人要问,采用一个系统时钟不是很简单吗?为什么 STM32 要有多个时钟源呢? 因为首先
STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,比
如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干
扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。
首先让我们来看看 STM32F411 的时钟系统图吧:(STM32F411RCT6 手册没有中文版)
![c692df3c6e0085b9fab2677b6773850c.png](https://i-blog.csdnimg.cn/blog_migrate/17b858bd54d59ed90295127129d4d19a.jpeg)
图 4.3.1 STM32F411 时钟系统图
上图从左往右看,就是整个 STM32F411 的时钟走向。这里,我们挑选出 8 个重要的地方
进行介绍(图 5.2.2.1 中标出的 1~8)。
1 这是进人 PLL 之前的一个时钟分频系数(M),取值范围是:2~63,一般取 8。注
意,这个分频系数,对主 PLL 和 PLLI2S 都有效。
2 这是 STM32F411 的主 PLL,该部分控制 STM32F411 的主频率(PLLCLK)和
USB/SDIO 外设的频率(PLL48CK)。其中,N 是主 PLL voo 的倍频系数,其取值范围是
50~432:P 是系统时钟的主 PLL 分频系数,其取值范围是:2、4、6 和 8(仅限这四个值):
Q 是 USB/SDIO 的主 PLL 分频系数,其取值范围是:2~15:R 没用到。
3 这是 STM32F4 I2S 部分的 PLL,该部分主要用于设置 STM32F411 I2S 内部输入时
钟频率。其中 N 是用于 PLLI2S vco 的倍频系数,其取值范围是:50~432;R 是 I2S 时钟的
分频系数,其取值范围是:2~7;P 和 Q 没用到。
4 这是 PLL 之后的系统主时钟(PLLCLK),STM32F411 的主频最高是 100Mhz,所
以我们一般设置 PLLCLK 为 96Mhz,因为 100Mhz 分频不到 48Mhz(USB 时钟 48Mhz),
所以 M=4,N=96,P=2,通过 SW 选择选择 SYSCLK=PLLCLK 即可得到 96Mhz 的系统运
行频率。
5 这是 PLL 之后的 USB/SDIO 时钟频率,由于 USB 必须是 48Mhz 才可以正常运行,
所以这个频率一般设置为 48Mhz(M=4,N=96,Q=4)。
6 是 I2S 的时钟,通过 I2SSRC 选择内 PLLI2SCLK 还是外部 I2SCKIN 作为时钟。
7 这是 Cortex 系统定时器,也就是 SYSTICK 的时钟。上图清楚的表明 SYSTICK 的
来源是 AHB 分频后再 8 分频(这个 8 分频是可以设置的,即 8 分频,或者不分频,一般
使用 8 分频),我们一般设置 AHB 不分频,则 SYSTICK 的频率为:96Mhz/8=12Mhz。前
面介绍的延时函数,就是基于 SYSTICK 来实现的。
8 这里是 STM32F411 很多外设的时钟来源,即两个总线桥:APB1 和 APB2,其中
APB1 是低速总线(最高 50Mhz),APB2 是高速总线(最高 100Mhz)。另外定时器部分,
如果所在的总线(APB1/APB2)的分频系数为 1,那么就不倍频,如果不为 1(比如 2/4/8/16),
那么就会 2 倍频(Fabpx*2)后,作为定时器时钟输入。
关于时钟的详细介绍,在《STM32F411xC/E 参考手册》第 6.2 节(92~101 页)有详细介
绍。有不明白的地方,可以对照手册仔细研究。
从上图可以看出 STM32F411 的时钟设计的比较复杂,各个时钟基本都是可控的,任何外
设都有对应的时钟控制开关,这样的设计,对降低功耗是非常有用的,不用的外设不开启时钟,
就可以大大降低其功耗。
4.3.2 STM32F411 时钟系统配置
上一小节我们对 STM32F411 时钟树进行了详细讲解,接下来我们来讲解通过 STM32F4 的
HAL 库进行 STM32F411 时钟系统配置步骤。实际上,STM32F4 的时钟系统配置也可以通过图
形化配置工具 STM32CubeMX 来配置生成,这里我们讲解初始化代码,是为了让大家对 STM32
时钟系统有更加清晰的理解。我们将在 4.8 小节讲解图形化配置工具 STM32CubeMX,大家可
以对比参考学习。
前面我们讲解过,在系统启动之后,程序会先执行 HAL 库定义的 SystemInit 函数,进行系
统一些初始化配置。那么我们先来看看 SystemInit 程序:
void SystemInit(void)
{
/* FPU 设置------------------------------------------------------------*/
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */
#endif
/* 复位 RCC 时钟配置为默认配置-----------*/
RCC->CR |= (uint32_t)0x00000001;//打开 HSION 位
RCC->CFGR = 0x00000000;//复位 CFGR 寄存器
RCC->CR &= (uint32_t)0xFEF6FFFF;//复位 HSEON, CSSON and PLLON 位
RCC->PLLCFGR = 0x24003010; //复位寄存器 PLLCFGR
RCC->CR &= (uint32_t)0xFFFBFFFF;//复位 HSEBYP 位
RCC->CIR = 0x00000000;//关闭所有中断
#if defined (DATA_IN_ExtSRAM) || defined (DATA_IN_ExtSDRAM)
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */
109
/* 配置中断向量表地址=基地址+偏移地址 ------------------*/
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
#endif
}
从上面代码可以看出,SystemInit 主要做了如下四个方面工作:
1)FPU 设置
2)复位 RCC 时钟配置为默认复位值(默认开始了 HSI)
3)外部存储器配置
4)中断向量表地址配置
HAL 库的 SystemInit 函数并没有像标准库的 SystemInit 函数一样进行时钟的初始化配置。HAL
库的 SystemInit 函数除了打开 HSI 之外,没有任何时钟相关配置,所以使用 HAL 库我们必须编
写自己的时钟配置函数。首先我们打开工程模板看看我们在工程 SYSTEM 分组下面定义的 sys.c
文件中的时钟初始化函数 Stm32_Clock_Init 的内容:
//时钟设置函数
//Fvco=Fs*(plln/pllm);
//Fsys=Fvco/pllp=Fs*(plln/(pllm*pllp));
//Fusb=Fvco/pllq=Fs*(plln/(pllm*pllq));
//Fvco:VCO 频率
//Fsys:系统时钟频率
//Fusb:USB,SDIO 的时钟频率
//Fs:PLL 输入时钟频率,可以是 HSI,HSE 等.
//plln:主 PLL 倍频系数(PLL 倍频),取值范围:50~432.
//pllm:主 PLL 和音频 PLL 分频系数(PLL 之前的分频),取值范围:2~63.
//pllp:系统时钟的主 PLL 分频系数(PLL 之后的分频),取值范围:2,4,6,8.(仅限这 4 个值!)
//pllq:USB/SDIO 的主 PLL 分频系数(PLL 之后的分频),取值范围:2~15.
//外部晶振为 8M 的时候,推荐值:plln=96,pllm=4,pllp=2,pllq=4.
//得到:Fvco=8*(96/4)=192Mhz
//
Fsys=192/2=96Mhz
//
Fusb=192/4=48Mhz
//返回值:0,成功;1,失败。
void Stm32_Clock_Init(u32 plln,u32 pllm,u32 pllp,u32 pllq)
{
HAL_StatusTypeDef ret = HAL_OK;
RCC_OscInitTypeDef RCC_OscInitStructure;
RCC_ClkInitTypeDef RCC_ClkInitStructure;
__HAL_RCC_PWR_CLK_ENABLE(); //使能 PWR 时钟
//下面这个设置用来设置调压器输出电压级别,以便在器件未以最大频率工作
//时使性能与功耗实现平衡,此功能只有 STM32F42xx 和 STM32F43xx 器件有,
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_
VOLTAGE_SCALE1);//设置调压器输出电压级别 1
RCC_OscInitStructure.OscillatorType = RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE
RCC_OscInitStructure.HSEState = RCC_HSE_ON;//打开 HSE
RCC_OscInitStructure.PLL.PLLState = RCC_PLL_ON;//打开 PLL
RCC_OscInitStructure.PLL.PLLSource = RCC_PLLSOURCE_HSE;
//PLL 时钟源选择 HSE
RCC_OscInitStructure.PLL.PLLM = pllm;
//主 PLL 和音频 PLL 分频系数(PLL 之前的分频),取值范围:2~63.
RCC_OscInitStructure.PLL.PLLN = plln;
//主 PLL 倍频系数(PLL 倍频),取值范围:50~432.
RCC_OscInitStructure.PLL.PLLP = pllp;
//系统时钟的主 PLL 分频系数(PLL 之后的分频),取值范围:2,4,6,8.(仅限这 4 个值!)
RCC_OscInitStructure.PLL.PLLQ = pllq;
//USB/SDIO 的主 PLL 分频系数(PLL 之后的分频),取值范围:2~15.
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化
if(ret!=HAL_OK) while(1);
RCC_ClkInitStructure.ClockType = RCC_CLOCKTYPE_HCLK|
RCC_CLOCKTYPE_SYSCLK|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 分频系数为 2
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure, FLASH_LATENCY_3);
//同时设置 FLASH 延时周期为 3WS,也就是 4 个 CPU 周期。
if(ret!=HAL_OK) while(1);
//初始化 HAL Systick 时钟
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}
从函数注释可知,函数 Stm32_Clock_Init 的作用是进行时钟系统配置,除了配置 PLL 相关
参数确定 SYSCLK 值之外,还配置了 AHB,APB1 和 APB2 的分频系数,也就是确定了 HCLK,
PCLK1 和 PCLK2 的时钟值。我们首先来看看使用 HAL 库配置 STM32F429 时钟系统的一般步骤:
1) 使能 PWR 时钟:调用函数__HAL_RCC_PWR_CLK_ENABLE()。
2) 设置调压器输出电压级别:调用函数__HAL_PWR_VOLTAGESCALING_CONFIG()。
3) 选择是否开启 Over-Driver 功能:调用函数 HAL_PWREx_EnableOverDrive()。
4) 配置时钟源相关参数:调用函数 HAL_RCC_OscConfig()。
5) 配置系统时钟源以及 AHB,APB1 和 APB2 的分频系数:调用函数 HAL_RCC_ClockConfig()。
步骤 2 和 3,具有一定的关联性,我们放在后面讲解。对于步骤 1 之所以要使能 PWR 时钟,
是因为后面的步骤设置调节器输出电压级别以及开启 Over-Driver 功能都是电源控制相关配置,
所以必须开启 PWR 时钟。接下来我们先着重讲解步骤 4 和步骤 5 的内容,这也是时钟系统配
置的关键步骤。
对于步骤 4,使用 HAL 来配置时钟源相关参数,我们调用的函数为 HAL_RCC_OscConfig(),
该函数在 HAL 库关键头文件 stm32f4xx_hal_rcc.h 中声明,在文件 stm32f4xx_hal_rcc.c 中定义。
首先我们来看看该函数声明:
__weak HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct);
该函数只有一个入口参数,就是结构体 RCC_OscInitTypeDef 类型指针。接下来我们看看结构体
RCC_OscInitTypeDef 的定义:
typedef struct
{
uint32_t OscillatorType; //需要选择配置的振荡器类型
uint32_t HSEState;
//HSE 状态
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 PLLM;
//PLL 分频系数 M
uint32_t PLLN;
//PLL 倍频系数 N
uint32_t PLLP;
//PLL 分频系数 P
uint32_t PLLQ;
//PLL 分频系数 Q
}RCC_PLLInitTypeDef;
从 RCC_PLLInitTypeDef;结构体的定义很容易看出该结构体主要用来设置 PLL 时钟源以及
相关分频倍频参数。
这个结构体的定义我们就不做过多讲解,接下来我们看看我们的时钟初始化函数Stm32_Clock_Init 中的配置内容:
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON;
//打开 HSE
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON;
//打开 PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;//PLL 时钟源为 HSE
RCC_OscInitStructure.PLL.PLLM=pllm;
RCC_OscInitStructure.PLL.PLLN=plln;
RCC_OscInitStructure.PLL.PLLP=pllp;
RCC_OscInitStructure.PLL.PLLQ=pllq;
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);
通过该段函数,我们开启了 HSE 时钟源,同时选择 PLL 时钟源为 HSE,然后把
Stm32_Clock_Init 的 4 个入口参数直接设置作为 PLL 的参数 M,N,P 和 Q 的值,这样就达到了设
置 PLL 时钟源相关参数的目的。设置好 PLL 时钟源参数之后,也就是确定了 PLL 的时钟频率,
接下来我们就需要设置系统时钟,以及 AHB,APB1 和 APB2 相关参数,也就是我们前面提到
的步骤 5。
接下来我们来看看步骤 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 延迟,这个参数我们放在后面跟步骤 2 和步骤 3 一起讲解。
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_3);
第一个参数 ClockType 配置说明我们要配置的是 SYSCLK,HCLK,PCLK1 和 PCLK2 四个时钟。
第二个参数 SYSCLKSource 配置选择系统时钟源为 PLL。
第三个参数 AHBCLKDivider 配置 AHB 分频系数为 1。
第四个参数 APB1CLKDivider 配置 APB1 分频系数为 2。
第五个参数 APB2CLKDivider 配置 APB2 分频系数为 1。
根据我们在主函数中调用 Stm32_Clock_Init(96,4,2,4)时候设置的入口参数值,我们可以计算出,
PLL 时钟为 PLLCLK=HSE*N/M*P=8MHz*96/(4*2)=96MHz,同时我们选择系统时钟源为 PLL,
所以系统时钟 SYSCLK=96MHz。AHB 分频系数为 1,故其频率为 HCLK=SYSCLK/1=96MHz。
APB1 分频系数为 2,故其频率为 PCLK1=HCLK/2=48MHz。APB2 分频系数为 1,故其频率为
PCLK2=HCLK/1=96/1=96MHz。最后我们总结一下通过调用函数 Stm32_Clock_Init(96,4,2,4)之
后的关键时钟频率值:SYSCLK(系统时钟)
=96MHz
PLL 主时钟
=96MHz
AHB 总线时钟(HCLK=SYSCLK/1)
=96MHz
APB1 总线时钟(PCLK1=HCLK/2)
=48MHz
APB2 总线时钟(PCLK2=HCLK/1)
=96MHz
最后我们来看看步骤 2,步骤 3 以及步骤 5 中函数 HAL_RCC_ClockConfig 第二个入口参数
FLatency 的含义。这里我们不想讲解得太复杂,大家只需要知道调压器输出电压级别 VOS,
Over-Driver 功能开启以及 FLASH 的延迟 Latency 三个参数,在我们芯片电源电压和 HCLK 固
定之后,他们三个参数也是固定的。首先我们来看看调压器输出电压级别 VOS,它是由 PWR
控制寄存器 CR 的位 15:14 来确定的:
![63046823cfe47782322b5a84b124b315.png](https://i-blog.csdnimg.cn/blog_migrate/b70157ba27fbb83e3954384257bdb057.jpeg)
如果我们要配置HCLK时钟为96Mhz,在AHB的分频系数为1的情况下需要时钟为96Mhz,
那么我们必须配置调试器输出电压级别 VOS 为级别 1,源码如下:
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
配置好调节器电压级别 VOS 之后,如果需要 HCLK 达到 96Mhz,还需要配置 FLASH 延迟
Latency。对于 STM32F411 系列,FLASH 延迟配置参数值是通过下表来确定的:
![11d091a2567784c33be43dfd3ec920a3.png](https://i-blog.csdnimg.cn/blog_migrate/532cd88baf2cc04c4d6b96ed7e8be4a0.jpeg)
表 4.3.2.1 STM32F411xC/E 系列等待周期表
从上表可以看出,在电压为 3.3V 的情况下,如果需要 HCLK 为 180MHz,那么等待周期必
须为 5WS,也就是 6 个 CPU 周期。下面我们看看我们在 Stm32_Clock_Init 中调用函数
HAL_RCC_ClockConfig 的时候,第二个入口参数设置值:
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_3);
从上可以看出,我们设置值为 FLASH_LATENCY_3,也就是 3WS,4 个 CPU 周期,与我们预
期一致。时钟系统配置相关知识就给大家讲解到这里。
4.3.3 STM32F4 时钟使能和配置
上一节我们讲解了时钟系统配置步骤。在配置好时钟系统之后,如果我们要使用某些外设,
例如 GPIO,ADC 等,我们还要使能这些外设时钟。这里大家必须注意,如果在使用外设之前
没有使能外设时钟,这个外设是不可能正常运行的。STM32 的外设时钟使能是在 RCC 相关寄
存器中配置的。因为 RCC 相关寄存器非常多,有兴趣的同学可以直接打开《STM32F411xC/E
参考手册》6.3 小节查看所有 RCC 相关寄存器的配置。接下来我们来讲解通过 STM32F4 的 HAL
库使能外设时钟的方法。
在 STM32F4 的 HAL 库中,外设时钟使能操作都是在 RCC 相关固件库文件头文件
stm32f4xx_hal_rcc.h 定义的。大家打开 stm32f4xx_hal_rcc.h 头文件可以看到文件中除了少数几
个函数声明之外大部分都是宏定义标识符。外设时钟使能在 HAL 库中都是通过宏定义标识符
来实现的。首先,我们来看看 GPIOA 的外设时钟使能宏定义标识符:
#define __HAL_RCC_GPIOA_CLK_ENABLE()
do {
__IO uint32_t tmpreg = 0x00;
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);
UNUSED(tmpreg);
} while(0U)
这 几 行 代 码 非 常 简 单 , 主 要 是 定 义 了 一 个 宏 定 义 标 识 符
__HAL_RCC_GPIOA_CLK_ENABLE(),它的核心操作是通过下面运行代码实现的。
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);
这行代码的作用是,设置寄存器 RCC->AHB1ENR 的相关位为 1,至于是哪个位,是由宏
定义 RCC_AHB1ENR_GPIOAEN 的值决定的,而它的值为;
#define RCC_AHB1ENR_GPIOAEN
((uint32_t)0x00000001)
所以,我们很容易理解上面代码的作用是设置寄存器 RCC->AHB1ENR 寄存器的最低位为 1。
我们可以从 STM32F4 的中文参考手册中搜索 AHB1ENR 寄存器定义,最低位的作用是用来使
用 GPIOA 时钟。AHB1ENR 寄存器的位 0 描述如下:
位 0 GPIOAEN:IO 端口 A 时钟使能
由软件置“1”或清“0”
0:禁止 IO 端口 A 时钟。
1:使能 IO 端口 A 时钟。
那 么 我 们 只 需 要 在 我 们 的 用 户 程 序 中 调 用 宏 定 义 标 识 符
__HAL_RCC_GPIOA_CLK_ENABLE()就可以实现 GPIOA 时钟使能。使用方法为:
__HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 时钟
对于其他外设,同样都是在 stm32f4xx_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->AHB1ENR &= ~(RCC_AHB1ENR_GPIOAEN))
同样,宏定义标识符__HAL_RCC_GPIOA_CLK_DISABLE()作用是设置 RCC->AHB1ENR
寄存器的最低位为 0,也就是禁止 GPIOA 时钟。具体使用方法我们这里就不做过多讲解,我们
这里同样列出几个常用的禁止外设时钟的宏定义标识符使用方法:
__HAL_RCC_DMA1_CLK_DISABLE();//禁止 DMA1 时钟
__HAL_RCC_USART1_CLK_DISABLE();/禁止串口 1 时钟
__HAL_RCC_TIM1_CLK_DISABLE();//禁止 TIM1 时钟
关于 STM32F4 的外设时钟使能和禁止方法我们就给大家讲解到这里。
4.4 IO 引脚复用器和映射
STM32F4 有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO
如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。
这部分知识在《STM32F411xC/E 参考手册》第七章和芯片数据手册有详细的讲解哪些 GPIO 管脚
是可以复用为哪些内置外设。
对于本小节知识,STM32F411xC/E 参考手册讲解比较详细,我们同样会从中抽取重要的知识
点罗列出来。同时,我们会以串口使用为例给大家讲解具体的引脚复用的配置。
STM32F4 系列微控制器 IO 引脚通过一个复用器连接到内置外设或模块。该复用器一次只允
许一个外设的复用功能(AF)连接到对应的 IO 口。这样可以确保共用同一个 IO 引脚的外设之
间不会发生冲突。
每个 IO 引脚都有一个复用器,该复用器采用 16 路复用功能输入(AF0 到 AF15),可通过
GPIOx_AFRL(针对引脚 0-7)和 GPIOx_AFRH(针对引脚 8-15)寄存器对这些输入进行配置,每四
位控制一路复用:
1)完成复位后,所有 IO 都会连接到系统的复用功能 0(AF0)。
2)外设的复用功能映射到 AF1 到 AF13。
3)Cortex-M4 EVENTOUT 映射到 AF15。
复用器示意图如下图 4.4.1:
![f906f239d5e343bb7276df6e173f5a65.png](https://i-blog.csdnimg.cn/blog_migrate/12a6ebf92cb1a09b870e6b5dd00e85d7.jpeg)
图 4.4.1 AFRL 和 APRH 复用功能映射关系简图
接下来,我们简单说明一下这个这个图要如何看,举个例子,NANO STM32F411 开发板的
原理图 PA9 的原理图如图 4.4.2 所示:
图 4.4.2 NANO STM32F411 开发板 PA9 原理图
如上图所示,PC11 可以作为 TIM1_CH2/USART1_TX/SDIO_D2 等复用功能输出。这么多
复用功能,如果这些外设之间互相干扰。但是 STM32F4,由于有复用器功能,可以让 PA9 在
某个时刻仅连接到需要使用的特定的外设,因此不存在互相干扰的情况。
上图 4.4.1 图中可以看出,当需要使用复用功能的时候,我们配置相应的寄存器
GPIOx_AFRL 或者 GPIOx_AFRH,让对应引脚通过复用器连接到对应的复用功能外设。这里我
们列出 GPIOx_AFRL 寄存器的描述,GPIOx_AFRH 的作用跟 GPIOx_AFRL 类似,只不过
GPIOx_AFRH 控制的是一组 IO 的高八位,GPIOx_AFRL 控制的是一组 IO 口的低八位。
GPIOx_AFRL 控制的是一组 IO 口的低八位。GPIOx_AFRL 寄存器描述如下图 4.4.3 所示:
![5a58d14cc31b5178275b700f3b3faf55.png](https://i-blog.csdnimg.cn/blog_migrate/58d877d36c97c59a23809cf2e5ff44ac.jpeg)
图 4.4.3 GPIOx_AFRL 寄存器位描述
从表中可以看出,32 位寄存器 GPIOx_AFRL 每四个位控制一个 IO 口,所以每个寄存器控
制 32/4=8 个 IO 口。寄存器对应四位的值配置决定这个 IO 映射到哪个复用功能 AF。
在微控制器完成复位后,所有 IO 口都会连接到系统复用功能 0(AF0)。这里大家需要注意,
对于系统复用功能 AF0,我们将 IO 口连接到 AF0 之后,还要根据所用功能进行配置:
1)JTAG/SWD:在器件复位之后,会将这些功能引脚指定为专用引脚。也就是说,这些引
脚在复位后默认就是 JTAG/SWD 功能。如果我们要做为 GPIO 来使用,就需要对对应
的 IO 口复用器进行配置。
2)RTV_REFIN:此引脚在系统复位之后要使用的话要配置为浮空输入模式。
3)MCO1 和 MCO2:这些引脚在系统复位之后要使用的话要配置为复用功能模式。
对于外设复用功能的配置,除了 ADC 要将 IO 配置为模拟通道之外其他外设功能一律要配
置为复用功能模式,这个配置是在 IO 口对应的 GPIOx_MODER 寄存器中配置的。同时要配置
GPIOx_AFRH 或者 GPIOx_AFRL 寄存器,将 IO 口通过复用器连接到所需要的复用功能对应的
AFx。
不是每个 IO 口都可以复用为任意复用功能外设。到底哪些 IO 可以复用为相关外设呢?这
在芯片对应的数据手册上面会有详细的表格列出来。对于 STM32F411,数据手册里面的 Table
9.Alternae function mapping 表格列出了所有的端口 AF 映射表,因为表格比较大,所以这里只
列出 PORTA 的几个端口为例方便大家理解:
![ad98feb37e4b5ec0b673e4c33bc29af4.png](https://i-blog.csdnimg.cn/blog_migrate/525883c87a5a1971a2c6633208b1363e.jpeg)
![96dd50c6518c5fc96e8b992792f8b0ec.png](https://i-blog.csdnimg.cn/blog_migrate/6aa0f84cff20c54e39ff2ab5619a148a.jpeg)
表 4.4.4 PORTA 复位端口 AF 映射表
从表 4.4.4 可以看出,PA9 连接 AF7 可以复用为串口 1 的发送引脚 USART1_TX,PA0 连接
可以复用为串口 1 的接收引脚 USART1_RX。
接下来我们以串口 1 位例来讲解 GPIOA.9,GPIOA.10 口为串口 1 复用功能的一般步骤。
①首先我们要使用 IO 复用功能,必须先打开对应的 IO 时钟和复用功能外设时钟,这里我
们使用了 GPIOA 以及 UDART1,所以我们要使能 GPIOA 和 USART1 时钟。方法如下:
__HAL_RCC_GPIOA_CLK_ENABLE();
//使能 GPIOA 时钟
__HAL_RCC_USART1_CLK_ENABLE();
//使能 USART1 时钟
②其次,我们在 GPIOx_MODER 寄存器中将需 IO(对应串口 1 是 PA9,PA10)配置为复
用功能(ADC 设置为模拟通道)。
③再次,我们还需要对 IO 口的其他参数,例如上拉/下拉以及输出速度等进行配置。
④最后,我们需要配置 GPIOx_AFRL 或者 GPIOx_AFRH 寄存器,将 IO 连接到所需的 AFx。
对于 PA9,PA10 复用为 USART1 的发送接收引脚,根据表 4.4.4 可知都需要连接 AF7。
上面三步,在我们 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_FAST;
//高速
GPIO_Initure.Alternate=GPIO_AF7_USART1;//连接 AF7 复用为串口 1 的发送引脚
HAL_GPIO_Init(GPIOA,&GPIO_Initure);
//初始化 PA9
通过上面的配置,PA9 就通过映射器链接到 AF7,也就是复用为串口 1 的发送引脚。这个时
候,PA9 将不再作为普通的 IO 口使用。对于 PA10,配置方法一样,同样也是链接 AF7,修改 Pin
成员变量值为 PIN_10 即可。对于 GPIO 初始化结构体成员变量 Alternate 的取值范围,在 HAL
库中有详细定义,取值范围如下:
#define IS_GPIO_AF(AF) (((AF) == GPIO_AF0_RTC_50Hz)||((AF) == GPIO_AF9_TIM14)
||
((AF) == GPIO_AF0_MCO) || ((AF) == GPIO_AF0_TAMPER) ||
((AF) == GPIO_AF0_SWJ) || ((AF) == GPIO_AF0_TRACE)
||
((AF) == GPIO_AF1_TIM1)|| ((AF) == GPIO_AF1_TIM2)
||
...//此处省略部分代码
STM32F4 的端口复用和映射就给大家讲解到这里,希望大家课余结合相关实验工程和手册
巩固本小节知识。