STM32中文手册解读(2)

GPIO简介
GPIO 是通用输入输出端口的简称,简单来说就是 STM32 可控制的引脚,STM32 芯片
的 GPIO 引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。
STM32 芯片的 GPIO 被分成很多组,每组有 16 个引脚,如型号为 STM32F103VET6 型号的
芯片有 GPIOA、GPIOB、GPIOC 至 GPIOE 共 5 组 GPIO,芯片一共 100 个引脚,其中
GPIO 就占了一大部分,所有的 GPIO 引脚都有基本的输入输出功能。
最基本的输出功能是由 STM32 控制引脚输出高、低电平,实现开关控制,如把 GPIO
引脚接入到 LED 灯,那就可以控制 LED 灯的亮灭,引脚接入到继电器或三极管,那就可
以通过继电器或三极管控制外部大功率电路的通断。

基本结构分析
下面我们按图中的编号对 GPIO 端口的结构部件进行说明。
在这里插入图片描述GPIO框图

1. 保护二极管及上、下拉电阻
引脚的两个保护二级管可以防止引脚外部过高或过低的电压输入,当引脚电压高于
VDD 时,上方的二极管导通,当引脚电压低于 VSS 时,下方的二极管导通,防止不正常电
压引入芯片导致芯片烧毁。尽管有这样的保护,并不意味着 STM32 的引脚能直接外接大功
率驱动器件,如直接驱动电机,强制驱动要么电机不转,要么导致芯片烧坏,必须要加大
功率及隔离电路驱动。
2. P-MOS 管和 N-MOS 管
GPIO 引脚线路经过两个保护二极管后,向上流向“输入模式”结构,向下流向“输出
模式”结构。先看输出模式部分,线路经过一个由 P-MOS 和 N-MOS 管组成的单元电路。
这个结构使 GPIO 具有了“推挽输出”和“开漏输出”两种模式。
所谓的推挽输出模式,是根据这两个 MOS 管的工作方式来命名的。在该结构中输入
高电平时,经过反向后,上方的 P-MOS 导通,下方的 N-MOS 关闭,对外输出高电平;而
在该结构中输入低电平时,经过反向后,N-MOS 管导通,P-MOS 关闭,对外输出低电平。
当引脚高低电平切换时,两个管子轮流导通,P 管负责灌电流,N 管负责拉电流,使其负
载能力和开关速度都比普通的方式有很大的提高。推挽输出的低电平为 0 伏,高电平为 3.3
伏,它是推挽输出模式时的等效电路。
而在开漏输出模式时,上方的 P-MOS 管完全不工作。如果我们控制输出为 0,低电平,
则 P-MOS 管关闭,N-MOS 管导通,使输出接地,若控制输出为 1 (它无法直接输出高电平)
时,则 P-MOS 管和 N-MOS 管都关闭,所以引脚既不输出高电平,也不输出低电平,为高
阻态。为正常使用时必须外部接上拉电阻,参考图 8-3 中等效电路。它具有“线与”特性,
也就是说,若有很多个开漏模式引脚连接到一起时,只有当所有引脚都输出高阻态,才由
上拉电阻提供高电平,此高电平的电压为外部上拉电阻所接的电源的电压。若其中一个引
脚为低电平,那线路就相当于短路接地,使得整条线路都为低电平,0 伏。
推挽输出模式一般应用在输出电平为 0 和 3.3 伏而且需要高速切换开关状态的场合。 在 STM32 的应用中,除了必须用开漏模式的场合,我们都习惯使用推挽输出模式。
开漏输出一般应用在 I2C、SMBUS 通讯等需要“线与”功能的总线电路中。除此之外,
还用在电平不匹配的场合,如需要输出 5 伏的高电平,就可以在外部接一个上拉电阻,上 拉电源为 5 伏,并且把 GPIO 设置为开漏模式,当输出高阻态时,由上拉电阻和电源向外
输出 5 伏的电平
3. 输出数据寄存器
前面提到的 双 MOS 管结构电路的输入信号,是由 GPIO“输出数据寄存 器
GPIOx_ODR”提供的,因此我们通过修改输出数据寄存器的值就可以修改 GPIO 引脚的输
出电平。而“置位/复位寄存器 GPIOx_BSRR”可以通过修改输出数据寄存器的值从而影响
电路的输出。
4. 复用功能输出
“复用功能输出”中的“复用”是指 STM32 的其它片上外设对 GPIO 引脚进行控制,
此时 GPIO 引脚用作该外设功能的一部分,算是第二用途。从其它外设引出来的“复用功
能输出信号”与 GPIO 本身的数据据寄存器都连接到双 MOS 管结构的输入中,通过图中的
梯形结构作为开关切换选择。
例如我们使用 USART 串口通讯时,需要用到某个 GPIO 引脚作为通讯发送引脚,这个
时候就可以把该 GPIO 引脚配置成 USART 串口复用功能,由串口外设控制该引脚,发送数

5. 输入数据寄存器
看 GPIO 结构框图的上半部分,GPIO 引脚经过内部的上、下拉电阻,可以配置成上/
下拉输入,然后再连接到施密特触发器,信号经过触发器后,模拟信号转化为 0、1 的数字
信号,然后存储在“输入数据寄存器 GPIOx_IDR”中,通过读取该寄存器就可以了解
GPIO 引脚的电平状态。
6. 复用功能输入
与“复用功能输出”模式类似,在“复用功能输入模式”时,GPIO 引脚的信号传输到
STM32 其它片上外设,由该外设读取引脚状态。
同样,如我们使用 USART 串口通讯时,需要用到某个 GPIO 引脚作为通讯接收引脚,
这个时候就可以把该 GPIO 引脚配置成 USART 串口复用功能,使 USART 可以通过该通讯
引脚的接收远端数据。
7. 模拟输入输出
当 GPIO 引脚用于 ADC 采集电压的输入通道时,用作“模拟输入”功能,此时信号是
不经过施密特触发器的,因为经过施密特触发器后信号只有 0、1 两种状态,所以 ADC 外
设要采集到原始的模拟信号,信号源输入必须在施密特触发器之前。类似地,当 GPIO 引
脚用于 DAC 作为模拟电压输出通道时,此时作为“模拟输出”功能,DAC 的模拟信号输
出就不经过双 MOS 管结构,模拟信号直接输出到引脚

**

GPIO 工作模式

**
总结一下,由 GPIO 的结构决定了 GPIO 可以配置成以下模式

1 typedef enum
2 {
3 GPIO_Mode_AIN = 0x0,                      // 模拟输入
4 GPIO_Mode_IN_FLOATING = 0x04,             // 浮空输入
5 GPIO_Mode_IPD = 0x28,                     // 下拉输入
6 GPIO_Mode_IPU = 0x48,                     // 上拉输入
7 GPIO_Mode_Out_OD = 0x14,                  // 开漏输出
8 GPIO_Mode_Out_PP = 0x10,                  // 推挽输出
9 GPIO_Mode_AF_OD = 0x1C,                   // 复用开漏输出
10 GPIO_Mode_AF_PP = 0x18                   // 复用推挽输出
11 } GPIOMode_TypeDef;

在固件库中,GPIO 总共有 8 种细分的工作模式,稍加整理可以大致归类为以下三类:
1. 输入模式(模拟/浮空/上拉/下拉)
在输入模式时,施密特触发器打开,输出被禁止,可通过输入数据寄存器 GPIOx_IDR
读取 I/O 状态。其中输入模式,可设置为上拉、下拉、浮空和模拟输入四种。上拉和下拉
输入很好理解,默认的电平由上拉或者下拉决定。浮空输入的电平是不确定的,完全由外
部的输入决定,一般接按键的时候用的是这个模式。模拟输入则用于 ADC 采集。
2. 输出模式(推挽/开漏)
在输出模式中,推挽模式时双 MOS 管以轮流方式工作,输出数据寄存器 GPIOx_ODR
可控制 I/O 输出高低电平。开漏模式时,只有 N-MOS 管工作,输出数据寄存器可控制 I/O
输出高阻态或低电平。输出速度可配置,有 2MHz\10MHz\50MHz 的选项。此处的输出速
度即 I/O 支持的高低电平状态最高切换频率,支持的频率越高,功耗越大,如果功耗要求
不严格,把速度设置成最大即可。
在输出模式时施密特触发器是打开的,即输入可用,通过输入数据寄存器 GPIOx_IDR
可读取 I/O 的实际状态。
3. 复用功能(推挽/开漏)
复用功能模式中,输出使能,输出速度可配置,可工作在开漏及推挽模式,但是输出
信号源于其它外设,输出数据寄存器 GPIOx_ODR 无效;输入可用,通过输入数据寄存器
可获取 I/O 实际状态,但一般直接用外设的寄存器来获取该数据信号。
通过对 GPIO 寄存器写入不同的参数,就可以改变 GPIO 的工作模式,再强调一下,
要了解具体寄存器时一定要查阅《STM32F10X-中文参考手册》中对应外设的寄存器说明。
在 GPIO 外设中,控制端口高低控制寄存器 CRH 和 CRL 可以配置每个 GPIO 的工作模式和
工作的速度,每 4 个位控制一个 IO,CRH 控制端口的高八位,CRL 控制端口的低 8 位,
具体的看 CRH 和 CRL 的寄存器描述。

学习 STM32 最好的方法是用固件库,然后在固件库的基础上了解底层,学习寄存器。
什么是 STM32 函数库
以上所说的固件库是指“STM32 标准函数库”,它是由 ST 公司针对 STM32 提供的函
数接口,即 API (Application Program Interface),开发者可调用这些函数接口来配置 STM32
的寄存器,使开发人员得以脱离最底层的寄存器操作,有开发快速,易于阅读,维护成本
低等优点。
当我们调用库 API 的时候不需要挖空心思去了解库底层的寄存器操作,就像当年我们
刚开始学习 C 语言的时候,用 prinft()函数时只是学习它的使用格式,并没有去研究它的源
码实现,但需要深入研究的时候,经过千锤百炼的库源码就是最佳学习范例。
实际上,库是架设在寄存器与用户驱动层之间的代码,向下处理与寄存器直接相关的
配置,向上为用户提供配置寄存器的接口。

外设寄存器结构体定义
上一章中我们在操作寄存器的时候,操作的是都寄存器的绝对地址,如果每个外设寄存器都这样操作,那将非常麻烦。我们考虑到外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器占 32 个字节,这种方式跟结构体里面的成员类似。
所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。这样我们操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。
在工程中的“stm32f10x.h”文件中,我们使用结构体封装 GPIO 及 RCC 外设的的寄存
器,见代码清单 9-1。结构体成员的顺序按照寄存器的偏移地址从低到高排列,成员类型
跟寄存器类型一样。
9-1 封装寄存器列表

1 //寄存器的值常常是芯片外设自动更改的,即使 CPU 没有执行程序,也有可能发生变化
2 //编译器有可能会对没有执行程序的变量进行优化
3 
4 //volatile 表示易变的变量,防止编译器优化,
5 #define __IO volatile
6 typedef unsigned int uint32_t;
7 typedef unsigned short uint16_t;
8 
9 // GPIO 寄存器结构体定义
10 typedef struct
11 {
12 __IO uint32_t CRL; // 端口配置低寄存器, 地址偏移 0X00
13 __IO uint32_t CRH; // 端口配置高寄存器, 地址偏移 0X04
14 __IO uint32_t IDR; // 端口数据输入寄存器, 地址偏移 0X08
15 __IO uint32_t ODR; // 端口数据输出寄存器, 地址偏移 0X0C
16 __IO uint32_t BSRR; // 端口位设置/清除寄存器,地址偏移 0X10
17 __IO uint32_t BRR; // 端口位清除寄存器, 地址偏移 0X14
18 __IO uint32_t LCKR; // 端口配置锁定寄存器, 地址偏移 0X18
19 } GPIO_TypeDef;

这段代码在每个结构体成员前增加了一个“__IO”前缀,它的原型在这段代码的第一
行,代表了 C 语言中的关键字“volatile”,在 C 语言中该关键字用于表示变量是易变的,
要求编译器不要优化。这些结构体内的成员,都代表着寄存器,而寄存器很多时候是由外
设或 STM32 芯片状态修改的,也就是说即使 CPU 不执行代码修改这些变量,变量的值也
有可能被外设修改、更新,所以每次使用这些变量的时候,我们都要求 CPU 去该变量的地
址重新访问。若没有这个关键字修饰,在某些情况下,编译器认为没有代码修改该变量,
就直接从 CPU 的某个缓存获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数
据,与我们要求的寄存器最新状态可能会有出入。

外设存储器映射
外设寄存器结构体定义仅仅是一个定义,要想实现给这个结构体赋值就达到操作寄存
器的效果,我们还需要找到该寄存器的地址,就把寄存器地址跟结构体的地址对应起来。
所以我们要再找到外设的地址,根据我们前面的学习,我们可以把这些外设的地址定义成
一个个宏,实现外设存储器的映射。

1 /*片上外设基地址 */
2 #define PERIPH_BASE                ((unsigned int)0x40000000)
3 
4 /*APB2 总线基地址 */
5 #define APB2PERIPH_BASE            (PERIPH_BASE + 0x10000)
6 /* AHB 总线基地址 */
7 #define AHBPERIPH_BASE             (PERIPH_BASE + 0x20000)
8 
9 /*GPIO 外设基地址*/
10 #define GPIOA_BASE                (APB2PERIPH_BASE + 0x0800)
11 #define GPIOB_BASE                (APB2PERIPH_BASE + 0x0C00)
12 #define GPIOC_BASE                (APB2PERIPH_BASE + 0x1000)
13 #define GPIOD_BASE                (APB2PERIPH_BASE + 0x1400)
14 #define GPIOE_BASE                (APB2PERIPH_BASE + 0x1800)
15 #define GPIOF_BASE                (APB2PERIPH_BASE + 0x1C00)
16 #define GPIOG_BASE                (APB2PERIPH_BASE + 0x2000)
17 
18 /*RCC 外设基地址*/
19 #define RCC_BASE (AHBPERIPH_BASE + 0x1000)

外设声明
定义好外设寄存器结构体,实现完外设存储器映射后,我们再把外设的基址强制类型
转换成相应的外设寄存器结构体指针,然后再把该指针声明成外设名,这样一来,外设名
就跟外设的地址对应起来了,而且该外设名还是一个该外设类型的寄存器结构体指针,通
过该指针可以直接操作该外设的全部寄存器,见代码清单 9-2。

1 // GPIO 外设声明
2 #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
3 #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
4 #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
5 #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
6 #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
7 #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
8 #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
9 
10 
11 // RCC 外设声明
12 #define RCC ((RCC_TypeDef *) RCC_BASE)
13 
14 /*RCC 的 AHB1 时钟使能寄存器地址,强制转换成指针*/
15 #define RCC_APB2ENR *(unsigned int*)(RCC_BASE+0x18)

首先通过强制类型转换把外设的基地址转换成 GPIO_TypeDef 类型的结构体指针,然
后通过宏定义把 GPIOA、GPIOB 等定义成外设的结构体指针,通过外设的结构体指针我们
就可以达到访问外设的寄存器的目的。
通过操作外设结构体指针的方式,我们把 main 文件里对应的代码修改掉,见代码 9-2
else 部分。
使用寄存器结构体指针操作寄存器

1 // 使用寄存器结构体指针点亮 LED
2 int main(void)
3 {
4 #if 0 // 直接通过操作内存来控制寄存器
5 // 开启 GPIOB 端口时钟
6 RCC_APB2ENR |= (1<<3);
7 
8 //清空控制 PB0 的端口位
9 GPIOB_CRL &= ~( 0x0F<< (4*0));
10 // 配置 PB0 为通用推挽输出,速度为 10M
11 GPIOB_CRL |= (1<<4*0);
12 
13 // PB0 输出 低电平
14 GPIOB_ODR |= (0<<0);
15 
16 while (1);
17 
18 #else // 通过寄存器结构体指针来控制寄存器
19 
20 // 开启 GPIOB 端口时钟
21 RCC->APB2ENR |= (1<<3);
22 
23 //清空控制 PB0 的端口位
24 GPIOB->CRL &= ~( 0x0F<< (4*0));
25 // 配置 PB0 为通用推挽输出,速度为 10M
26 GPIOB->CRL |= (1<<4*0);
27 
28 // PB0 输出 低电平
29 GPIOB->ODR |= (0<<0);
30 
31 while (1);
32 
33 #endi

乍一看,除了把“_”换成了“->”,其他都跟使用寄存器点亮 LED 那部分代码一样。
这是因为我们现在只是实现了库函数的基础,还没有定义库函数。
打好了地基,下面我们就来建高楼。接下来使用函数来封装 GPIO 的基本操作,方便
以后应用的时候不需要再查询寄存器,而是直接通过调用这里定义的函数来实现。我们把
针 对 GPIO 外 设 操 作 的 函 数 及 其 宏 定 义 分 别 存 放 在 “ stm32f10x_gpio.c ” 和
“stm32f10x_gpio.h”文件中,这两个文件需要自己新建。

定义位操作函数
在“stm32f10x_gpio.c”文件定义两个位操作函数,分别用于控制引脚输出高电平和低
电平,见代码清单 9-3。

1 /**
2 *函数功能:设置引脚为高电平
3 *参数说明:GPIOx:该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址
4 * GPIO_Pin:选择要设置的 GPIO 端口引脚,可输入宏 GPIO_Pin_0-15,
5 * 表示 GPIOx 端口的 0-15 号引脚。
6 */
7 void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
8 {
9 /*设置 GPIOx 端口 BSRR 寄存器的第 GPIO_Pin 位,使其输出高电平*/
10 /*因为 BSRR 寄存器写 0 不影响,
11 宏 GPIO_Pin 只是对应位为 1,其它位均为 0,所以可以直接赋值*/
12 
13 GPIOx->BSRR = GPIO_Pin;
14 }
15 
16 /**
17 *函数功能:设置引脚为低电平
18 *参数说明:GPIOx:该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址
19 * GPIO_Pin:选择要设置的 GPIO 端口引脚,可输入宏 GPIO_Pin_0-15,
20 * 表示 GPIOx 端口的 0-15 号引脚。
21 */
22 void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
23 {
24 /*设置 GPIOx 端口 BRR 寄存器的第 GPIO_Pin 位,使其输出低电平*/
25 /*因为 BRR 寄存器写 0 不影响,
26 宏 GPIO_Pin 只是对应位为 1,其它位均为 0,所以可以直接赋值*/
27 
28 GPIOx->BRR = GPIO_Pin;
29 }

这两个函数体内都是只有一个语句,对 GPIOx 的 BSRR 或 BRR 寄存器赋值,从而设
置引脚为高电平或低电平,操作 BSRR 或者 BRR 可以实现单独的操作某一位,
其中 GPIOx 是一个指针变量,通过函数的输入参数我们
可以修改它的值,如给它赋予 GPIOA、GPIOB、GPIOH 等结构体指针值,这个函数就可
以控制相应的 GPIOA、GPIOB、GPIOH 等端口的输出。
利用这两个位操作函数,可以方便地操作各种 GPIO 的引脚电平,控制各种端口引脚

1 
2 /*控制 GPIOB 的引脚 10 输出高电平*/
3 GPIO_SetBits(GPIOB,(uint16_t)(1<<10));
4 /*控制 GPIOB 的引脚 10 输出低电平*/
5 GPIO_ResetBits(GPIOB,(uint16_t)(1<<10));
6 
7 /*控制 GPIOB 的引脚 10、引脚 11 输出高电平,使用“|”同时控制多个引脚*/
8 GPIO_SetBits(GPIOB,(uint16_t)(1<<10)|(uint16_t)(1<<11));
9 /*控制 GPIOB 的引脚 10、引脚 11 输出低电平*/
10 GPIO_ResetBits(GPIOB,(uint16_t)(1<<10)|(uint16_t)(1<<10));
11 
12 /*控制 GPIOA 的引脚 8 输出高电平*/
13 GPIO_SetBits(GPIOA,(uint16_t)(1<<8));
14 /*控制 GPIOB 的引脚 9 输出低电平*/
15 GPIO_ResetBits(GPIOB,(uint16_t)(1<<9));

使用位操作函数及宏控制 GPIO

1 
2 /*控制 GPIOB 的引脚 10 输出高电平*/
3 GPIO_SetBits(GPIOB,GPIO_Pin_10);
4 /*控制 GPIOB 的引脚 10 输出低电平*/
5 GPIO_ResetBits(GPIOB,GPIO_Pin_10);
6 
7 /*控制 GPIOB 的引脚 10、引脚 11 输出高电平,使用“|”,同时控制多个引脚*/
8 GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
9 /*控制 GPIOB 的引脚 10、引脚 11 输出低电平*/
10 GPIO_ResetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
11 /*控制 GPIOB 的所有输出低电平*/
12 GPIO_ResetBits(GPIOB,GPIO_Pin_ALL);
13 
14 /*控制 GPIOA 的引脚 8 输出高电平*/
15 GPIO_SetBits(GPIOA,GPIO_Pin_8);
16 /*控制 GPIOB 的引脚 9 输出低电平*/
17 GPIO_ResetBits(GPIOB,GPIO_Pin_9);

使用以上代码控制 GPIO,我们就不需要再看寄存器了,直接从函数名和输入参数就
可以直观看出这个语句要实现什么操作。(英文中―Set‖表示“置位”,即高电平,“Reset”
表示“复位”,即低电平)。

定义初始化结构体 GPIO_InitTypeDef
定义位操作函数后,控制 GPIO 输出电平的代码得到了简化,但在控制 GPIO 输出电
平前还需要初始化 GPIO 引脚的各种模式,这部分代码涉及的寄存器有很多,我们希望初
始化 GPIO 也能以如此简单的方法去实现。为此,我们先根据 GPIO 初始化时涉及到的初
始化参数以结构体的形式封装起来,声明一个名为 GPIO_InitTypeDef 的结构体类型,见代 码 9-3

1 typedef struct
2 {
3 uint16_t GPIO_Pin; /*!< 选择要配置的 GPIO 引脚 */
4 
5 uint16_t GPIO_Speed; /*!< 选择 GPIO 引脚的速率 */
6 
7 uint16_t GPIO_Mode; /*!< 选择 GPIO 引脚的工作模式 */
8 } GPIO_InitTypeDef;

这个结构体中包含了初始化 GPIO 所需要的信息,包括引脚号、工作模式、输出速率。
设计这个结构体的思路是:初始化 GPIO 前,先定义一个这样的结构体变量,根据需要配
置 GPIO 的模式,对这个结构体的各个成员进行赋值,然后把这个变量作为“GPIO 初始化
函数”的输入参数,该函数能根据这个变量值中的内容去配置寄存器,从而实现 GPIO 的
初始化。

**

定义 GPIO 初始化函数

**
接着前面的思路,对初始化结构体赋值后,把它输入到 GPIO 初始化函数,由它来实
现寄存器配置。我们的 GPIO 初始化函数实现见代码

1 /**
2 *函数功能:初始化引脚模式
3 *参数说明:GPIOx,该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址
4 * GPIO_InitTypeDef:GPIO_InitTypeDef 结构体指针,指向初始化变量
5 */
6 void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
7 {
8 uint32_t currentmode =0x00,currentpin = 0x00,pinpos = 0x00,pos = 0x00;
9 uint32_t tmpreg = 0x00, pinmask = 0x00;
10 
11 /*---------------- GPIO 模式配置 -------------------*/
12 // 把输入参数 GPIO_Mode 的低四位暂存在 currentmode
13 currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) &
14 ((uint32_t)0x0F);
15 
16 // bit4 是 1 表示输出,bit4 是 0 则是输入
17 // 判断 bit4 是 1 还是 0,即首选判断是输入还是输出模式
18 if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) &
19 ((uint32_t)0x10)) != 0x00)
20 {
21 // 输出模式则要设置输出速度
22 currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
23 }
24 /*-----GPIO CRL 寄存器配置 CRL 寄存器控制着低 8 位 IO- ----*/
25 // 配置端口低 8 位,即 Pin0~Pin7
26 if (((uint32_t)GPIO_InitStruct->GPIO_Pin &
27 ((uint32_t)0x00FF)) != 0x00)
28 {
29 // 先备份 CRL 寄存器的值
30 tmpreg = GPIOx->CRL;
31 
32 // 循环,从 Pin0 开始配对,找出具体的 Pin
33 for (pinpos = 0x00; pinpos < 0x08; pinpos++)
34 {
35 // pos 的值为 1 左移 pinpos 位
36 pos = ((uint32_t)0x01) << pinpos;
37 
38 // 令 pos 与输入参数 GPIO_PIN 作位与运算
39 currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
40 
41 //若 currentpin=pos,则找到使用的引脚
42 if (currentpin == pos)
43 {
44 //pinpos 的值左移两位(乘以 4),因为寄存器中 4 个位配置一个引脚
45 pos = pinpos << 2;
46 //把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变
47 pinmask = ((uint32_t)0x0F) << pos;
48 tmpreg &= ~pinmask;
49
50 // 向寄存器写入将要配置的引脚的模式
51 tmpreg |= (currentmode << pos);
52 
53 // 判断是否为下拉输入模式
54 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
55 {
56 // 下拉输入模式,引脚默认置 0,对 BRR 寄存器写 1 对引脚置 0
57 GPIOx->BRR = (((uint32_t)0x01) << pinpos);
58 }
59 else
60 {
61 // 判断是否为上拉输入模式
62 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
63 {
64 // 上拉输入模式,引脚默认值为 1,对 BSRR 寄存器写 1 对引脚置 1
65 GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
66 }
67 }
68 }
69 }
70 // 把前面处理后的暂存值写入到 CRL 寄存器之中
71 GPIOx->CRL = tmpreg;
72 }
73 /*--------GPIO CRH 寄存器配置 CRH 寄存器控制着高 8 位 IO- -----*/
74 // 配置端口高 8 位,即 Pin8~Pin15
75 if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
76 {
77 // // 先备份 CRH 寄存器的值
78 tmpreg = GPIOx->CRH;
79 
80 // 循环,从 Pin8 开始配对,找出具体的 Pin
81 for (pinpos = 0x00; pinpos < 0x08; pinpos++)
82 {
83 pos = (((uint32_t)0x01) << (pinpos + 0x08));
84 
85 // pos 与输入参数 GPIO_PIN 作位与运算
86 currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);
87 
88 //若 currentpin=pos,则找到使用的引脚
89 if (currentpin == pos)
90 {
91 //pinpos 的值左移两位(乘以 4),因为寄存器中 4 个位配置一个引脚
92 pos = pinpos << 2;
93 
94 //把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变
95 pinmask = ((uint32_t)0x0F) << pos;
96 tmpreg &= ~pinmask;
97 
98 // 向寄存器写入将要配置的引脚的模式
99 tmpreg |= (currentmode << pos);
100 
101 // 判断是否为下拉输入模式
102 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
103 {
104 // 下拉输入模式,引脚默认置 0,对 BRR 寄存器写 1 可对引脚置 0
105 GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
106 }
107 // 判断是否为上拉输入模式
108 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
109 {
110 // 上拉输入模式,引脚默认值为 1,对 BSRR 寄存器写 1 可对引脚置 1
111 GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
112 }
113 }
114 }
115 // 把前面处理后的暂存值写入到 CRH 寄存器之中
116 GPIOx->CRH = tmpreg;
117 }
118 }

这个函数有 GPIOx 和 GPIO_InitStruct 两个输入参数,分别是 GPIO 外设指针和 GPIO
初始化结构体指针。分别用来指定要初始化的 GPIO 端口及引脚的工作模式。

总结

**

什么是 ST 标准固件库?不懂的时候总觉得莫测高深,懂了之后一切都是纸老虎。
我们从寄存器映射开始,把内存跟寄存器建立起一一对应的关系,然后操作寄存器点
亮 LED,再把寄存器操作封装成一个个函数。一步一步走来,我们实现了库最简单的雏形,
如果我们不断地增加操作外设的函数,并且把所有的外设都写完,一个完整的库就实现了。
本章中的 GPIO 相关库函数及结构体定义,实际上都是从 ST 标准库搬过来的。这样分
析它纯粹是为了满足自己的求知欲,学习其编程的方式、思想,这对提高我们的编程水平
是很有好处的,顺便感受一下 ST 库设计的严谨性,我认为这样的代码不仅严谨且华丽优
美,不知您是否也有这样的感受。
与直接配置寄存器相比,从执行效率上看会有额外的消耗:初始化变量赋值的过程、
库函数在被调用的时候要耗费调用时间;在函数内部,对输入参数转换所需要的额外运算
也消耗一些时间(如 GPIO 中运算求出引脚号时)。而其它的宏、枚举等解释操作是作编译过
程完成的,这部分并不消耗内核的时间。那么函数库的优点呢?是我们可以快速上手
STM32 控制器;配置外设状态时,不需要再纠结要向寄存器写入什么数值;交流方便,查
错简单。这就是我们选择库的原因。
现在的处理器的主频是越来越高,我们不需要担心 CPU 耗费那么多时间来干活会不会
被累倒,库主要应用是在初始化过程,而初始化过程一般是芯片刚上电或在核心运算之前
的执行的,这段时间的等待是 0.02us 还是 0.01us 在很多时候并没有什么区别。相对来说,
我们还是担心一下如果都用寄存器操作,每行代码都要查数据手册的寄存器说明,自己会
不会被累倒吧。
在以后开发的工程中,一般不会去分析 ST 的库函数的实现。因为外设的库函数是很
类似的,库外设都包含初始化结构体,以及特定的宏或枚举标识符,这些封装被库函数这
些转化成相应的值,写入到寄存器之中,函数内部的具体实现是十分枯燥和机械的工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值