完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
|
本帖最后由 正点原子运营官 于 2020-4-15 18:18 编辑
1)实验平台:ALIENTEK NANO STM32F411 V1开发板 2)摘自《正点原子STM32F4 开发指南(HAL 库版》关注官方微信号公众号,获取更多资料:正点原子 第四章 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 中位操作 这些与或非,取反,异或,右移,左移这些到底怎么回事,这里我们就不多做详细,相信大家学 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 的系统架构图: 图 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 手册没有中文版) 图 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)); //Fu***=Fvco/pllq=Fs*(plln/(pllm*pllq)); //Fvco:VCO 频率 //Fsys:系统时钟频率 //Fu***: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 // Fu***=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 来确定的: 如果我们要配置HCLK时钟为96Mhz,在AHB的分频系数为1的情况下需要时钟为96Mhz, 那么我们必须配置调试器输出电压级别 VOS 为级别 1,源码如下: __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); 配置好调节器电压级别 VOS 之后,如果需要 HCLK 达到 96Mhz,还需要配置 FLASH 延迟 Latency。对于 STM32F411 系列,FLASH 延迟配置参数值是通过下表来确定的: 表 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: 图 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 所示: 图 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 的几个端口为例方便大家理解: 表 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 的端口复用和映射就给大家讲解到这里,希望大家课余结合相关实验工程和手册 巩固本小节知识。 |
|
|
相关推荐
|
|
1470 浏览 1 评论
1132 浏览 0 评论
996 浏览 0 评论
STM32F405驱动DS1302时钟模块,输出时间错乱该怎么排查?
5373 浏览 2 评论
stm32f405rgt6驱动DS1302ZN出现时间错乱问题
4163 浏览 1 评论
/9
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2025-12-14 18:59 , Processed in 0.769714 second(s), Total 64, Slave 46 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191

淘帖