GPIOB->ODR &= ~(1<<0); //GPIOB->ODR在计算机内部会被转化为(*GPIOB).ODR
在用寄存器点亮LED灯时,会发现STM的寄存器都是32位的,每次配置时都要对照《STM32F10X-中文参考手册》中寄存器的说明,然后对每个控制的寄存器位写入特定参数,因此配置时容易出错,而且代码不好理解,难以维护。
学习STM32最好的办法是使用固件库编程,在此基础上了解底层,学习寄存器。
1、什么是STM32固件库
以上所说的固件库是指“STM32标准函数库”,它是ST公司针对STM32提供的函数接口,即API(Application Program Interface),开发者可调用这些函数接口来配置STM32的寄存器,使开发者脱离最底层的寄存器操作。
实际上,库是架设在寄存器与用户驱动层之间的代码,向下处理与寄存器直接相关的配置,向上为用户提供配置寄存器的接口。
库开发方式与直接配置寄存器方式的区别如下图1-1:
2、构件库函数雏形
2.1外部寄存器结构体的定义
上一章在操作寄存器的时候,都是操作的寄存器的绝对地址,如果每个外部寄存器都是这样操作,那将会是非常麻烦。
考虑到外部寄存器的地址都是基于外设基地址上逐个连续递增的,每个寄存器占32个字节,这种方式跟结构体里面的成员类似,所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。
这样在操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。
在工程中的stm32f10x.h文件中,我们使用结构体封装GPIO及RCC外设的寄存器,见代码清单1-1。结构体成员的顺序按照寄存器的偏移地址从低到高排列,成员类型与寄存器类型一样。
代码清单1-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_Type Def;
这段代码在每个结构体成员前增加了一个“__IO”前缀,它的原型在这段代码的第1行,代表了C语言中的关键字volatile,在C语言中该关键字用于表示变量是易变的,要求编译器不要优化。这些结构体内的成员都代表寄存器,而寄存器很多时候是由外设或STM32芯片状态修改的,也就是说即使CPU不执行代码修改这些变量,变量的值也有可能被外设修改、更新,所以每次使用这些变量的时候,我们都要求CPU去该变量的地址重新访问。若没有这个关键字修饰,在某些情况下,编译器认为没有代码修改该变量,就直接从CPU的某个缓存获取该变量值,这样可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会有出入。
2.2 外设存储器映射
外部寄存器结构体定义仅仅是一个定义,要想实现给这个结构体赋值就达到操作寄存器的效果,我们还需要找到该寄存器的地址,把寄存器地址与结构体的地址对应起来。所以还要再找到外设的地址,根据前面的学习,可以把这些外设的地址定义成一个个宏,以实现外设存储器的映射。
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)
2.3 外设声明
定义好外部寄存器结构体,实现外设存储器映射后,我们再把外设的基地址强制类型转换成相应的外部寄存器结构体指针,然后再把该指针声明成外设名,这样一来,外设名就与外设的地址对应起来了,而且该外设名还是一个该外设类型的寄存器结构体指针,通过该指针可以直接操作该外设的全部寄存器,见代码清单1-2。
代码清单1-2 指向外设首地址的结构体指针
1 // GPIO外设声明
2 #define GPIOA ((GPIO_Type Def *) GPIOA_BASE)
3 #define GPIOB ((GPIO_Type Def *) GPIOB_BASE)
4 #define GPIOC ((GPIO_Type Def *) GPIOC_BASE)
5 #define GPIOD ((GPIO_Type Def *) GPIOD_BASE)
6 #define GPIOE ((GPIO_Type Def *) GPIOE_BASE)
7 #define GPIOF ((GPIO_Type Def *) GPIOF_BASE)
8 #define GPIOG ((GPIO_Type Def *) GPIOG_BASE)
9
10
11 // RCC外设声明
12 #define RCC ((RCC_Type Def *) RCC_BASE)
13
14 /* RCC的AHB1时钟使能寄存器地址,强制转换成指针 */
15 #define RCC_APB2ENR *(unsigned int*)(RCC_BASE+0x18)
首先通过强制类型转换把外设的基地址转换成GPIO_Type Def类型的结构体指针,然后通过宏定义把GPIOA、GPIOB等定义成外设的结构体指针,通过外设的结构体指针就可以达到访问外设的寄存器的目的。
通过操作外设结构体指针的方式,我们把main文件里对应的代码修改掉,见代码清单1-3和代码清单中else部分。
代码清单1-3 C语言条件编译
1 /*
2 * C语言知识,条件编译
3 * #if为真
4 * 执行这里的程序
5 * #else
6 * 否则,执行这里的程序
7 * #endif
8 */
代码清单1-4 使用寄存器结构体指针操作寄存器
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 #endif
34 }
乍一看,除了把“_”换成了“->”,其他都与使用寄存器点亮LED的代码一样。这是因为我们现在只是实现了库函数的基础,还没有定义库函数。
打好了地基,下面就来建高楼。接下来使用函数来封装GPIO的基本操作,这样在以后应用的时候就不需要再查询寄存器,而是直接通过调用这里定义的函数即可实现。我们把针对GPIO外设操作的函数及其宏定义分别存放在stm32f10x_gpio.c和stm32f10x_gpio.h文件中,这两个文件需要自己新建。
2.4 定义位操作函数
在stm32f10x_gpio.c文件中定义两个位操作函数,分别用于控制引脚输出高电平和低电平,见代码清单1-5。
代码清单1-5 GPIO置位函数与复位函数的定义
1 /**
2 *函数功能:设置引脚为高电平
3 *参数说明:GPIOx:该参数为GPIO_Type Def类型的指针,指向GPIO端口的地址
4 * GPIO_Pin:选择要设置的GPIO端口引脚,可输入宏GPIO_Pin_0~15,
5 * 表示GPIOx端口的0~15号引脚
6 */
7 void GPIO_Set Bits(GPIO_Type Def* 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_Type Def类型的指针,指向GPIO端口的地址
19 * GPIO_Pin:选择要设置的GPIO端口引脚,可输入宏GPIO_Pin_0~15,
20 * 表示GPIOx端口的0~15号引脚。
21 */
22 void GPIO_Reset Bits(GPIO_Type Def* 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可以实现单独地操作某一位,有关这两个的寄存器说明见图1-2和图1-3。其中GPIOx是一个指针变量,通过函数的输入参数可以修改它的值,如给它赋予GPIOA、GPIOB、GPIOH等结构体指针值,这个函数就可以控制相应的GPIOA、GPIOB、GPIOH等端口的输出。
图1-2 BSRR寄存器说明(摘自《STM32F10X-中文参考手册》)
图1-3 BRR寄存器说明(摘自STM32F10X-中文参考手册)
利用这两个位操作函数,可以方便地操作各种GPIO的引脚电平。控制各种端口引脚的范例见代码清单1-6。
代码清单1-6 位操作函数使用范例
1
2 /*控制GPIOB的引脚10输出高电平*/
3 GPIO_Set Bits(GPIOB,(uint16_t)(1<<10));
4 /*控制GPIOB的引脚10输出低电平*/
5 GPIO_Reset Bits(GPIOB,(uint16_t)(1<<10));
6
7 /*控制GPIOB的引脚10、引脚11输出高电平,使用“|”同时控制多个引脚*/
8 GPIO_Set Bits(GPIOB,(uint16_t)(1<<10)|(uint16_t)(1<<11));
9 /*控制GPIOB的引脚10、引脚11输出低电平*/
10 GPIO_Reset Bits(GPIOB,(uint16_t)(1<<10)|(uint16_t)(1<<10));
11
12 /*控制GPIOA的引脚8输出高电平*/
13 GPIO_Set Bits(GPIOA,(uint16_t)(1<<8));
14 /*控制GPIOB的引脚9输出低电平*/
15 GPIO_Reset Bits(GPIOB,(uint16_t)(1<<9));
使用以上函数输入参数、但设置引脚号时,还是稍感不便,为此我们把表示16个引脚的操作数都定义成宏,见代码清单1-7。
代码清单1-7 选择引脚参数的宏
1 /* GPIO引脚号定义*/
2 #define GPIO_Pin_0 (uint16_t)0x0001) /*!< 选择Pin0 (1<<0)*/
3 #define GPIO_Pin_1
((uint16_t)0x0002) /*!< 选择Pin1 (1<<1)*/
4 #define GPIO_Pin_2 ((uint16_t)0x0004) /*!< 选择Pin2 (1<<2)*/
5 #define GPIO_Pin_3 ((uint16_t)0x0008) /*!< 选择Pin3 (1<<3)*/
6 #define GPIO_Pin_4 ((uint16_t)0x0010) /*!< 选择Pin4 */
7 #define GPIO_Pin_5 ((uint16_t)0x0020) /*!< 选择Pin5 */
8 #define GPIO_Pin_6 ((uint16_t)0x0040) /*!< 选择Pin6 */
9 #define GPIO_Pin_7 ((uint16_t)0x0080) /*!< 选择Pin7 */
10 #define GPIO_Pin_8 ((uint16_t)0x0100) /*!< 选择Pin8 */
11 #define GPIO_Pin_9 ((uint16_t)0x0200) /*!< 选择Pin9 */
12 #define GPIO_Pin_10 ((uint16_t)0x0400) /*!< 选择Pin10 */
13 #define GPIO_Pin_11 ((uint16_t)0x0800) /*!< 选择Pin11 */
14 #define GPIO_Pin_12 ((uint16_t)0x1000) /*!< 选择Pin12 */
15 #define GPIO_Pin_13 ((uint16_t)0x2000) /*!< 选择Pin13 */
16 #define GPIO_Pin_14 ((uint16_t)0x4000) /*!< 选择Pin14 */
17 #define GPIO_Pin_15 ((uint16_t)0x8000) /*!< 选择Pin15 */
18 #define GPIO_Pin_All ((uint16_t)0x FFFF) /*!< 选择全部引脚 */
这些宏代表的参数是某位置为“1”其他位置为“0”的数值,其中最后一个“GPIO_Pin_ALL”是所有数据位都为“1”,所以用它可以一次设置整个端口的0~15,即所有引脚。利用这些宏,GPIO的控制代码可改为代码清单1-8。
代码清单1-8 使用位操作函数及宏控制GPIO
1
2 /*控制GPIOB的引脚10输出高电平*/
3 GPIO_Set Bits(GPIOB,GPIO_Pin_10);
4 /*控制GPIOB的引脚10输出低电平*/
5 GPIO_Reset Bits(GPIOB,GPIO_Pin_10);
6
7 /*控制GPIOB的引脚10、引脚11输出高电平,使用“|”,同时控制多个引脚*/
8 GPIO_Set Bits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
9 /*控制GPIOB的引脚10、引脚11输出低电平*/
10 GPIO_Reset Bits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
11 /*控制GPIOB的所有引脚输出低电平*/
12 GPIO_Reset Bits(GPIOB,GPIO_Pin_ALL);
13
14 /*控制GPIOA的引脚8输出高电平*/
15 GPIO_Set Bits(GPIOA,GPIO_Pin_8);
16 /*控制GPIOB的引脚9输出低电平*/
17 GPIO_Reset Bits(GPIOB,GPIO_Pin_9)
18 #define GPIO_Pin_All ((uint16_t)0x FFFF) /*!< 选择全部引脚 */
这些宏代表的参数是某位置为“1”其他位置为“0”的数值,其中最后一个“GPIO_Pin_ALL”是所有数据位都为“1”,所以用它可以一次设置整个端口的0~15,即所有引脚。利用这些宏,GPIO的控制代码可改为代码清单1-8。
代码清单1-8 使用位操作函数及宏控制GPIO
1
2 /*控制GPIOB的引脚10输出高电平*/
3 GPIO_Set Bits(GPIOB,GPIO_Pin_10);
4 /*控制GPIOB的引脚10输出低电平*/
5 GPIO_Reset Bits(GPIOB,GPIO_Pin_10);
6
7 /*控制GPIOB的引脚10、引脚11输出高电平,使用“|”,同时控制多个引脚*/
8 GPIO_Set Bits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
9 /*控制GPIOB的引脚10、引脚11输出低电平*/
10 GPIO_Reset Bits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
11 /*控制GPIOB的所有引脚输出低电平*/
12 GPIO_Reset Bits(GPIOB,GPIO_Pin_ALL);
13
14 /*控制GPIOA的引脚8输出高电平*/
15 GPIO_Set Bits(GPIOA,GPIO_Pin_8);
16 /*控制GPIOB的引脚9输出低电平*/
17 GPIO_Reset Bits(GPIOB,GPIO_Pin_9);
使用以上代码控制GPIO,就不需要再看寄存器了,直接从函数名和输入参数就可以直观看出这个语句要实现什么操作(英文Set表示“置位”,即高电平,Reset表示“复位”,即低电平)。
4.5 定义初始化结构体
定义完位操作函数后,控制GPIO输出电平的代码得到了简化,但在控制GPIO输出电平前还需要初始化GPIO引脚的各种模式,这部分代码涉及的寄存器有很多,我们希望初始化GPIO也能以如此简单的方法去实现。为此,先将GPIO初始化时涉及的初始化参数以结构体的形式封装起来,声明一个名为GPIO_Init Type Def的结构体类型,见代码清单1-9。
代码清单1-9 定义GPIO初始化结构体
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_Init Type Def;
这个结构体中包含了初始化GPIO所需要的信息,包括引脚号、工作模式、输出速率。设计这个结构体的思路是:初始化GPIO前,先定义一个这样的结构体变量,根据需要配置GPIO的模式,对这个结构体的各个成员进行赋值,然后把这个变量作为“GPIO初始化函数”的输入参数,该函数能根据这个变量值中的内容去配置寄存器,从而实现GPIO的初始化。
4.6 定义引脚模式的枚举类型
上面定义的结构体很直接,美中不足的是在对结构体中各个成员赋值实现某个功能时还需要查手册中的寄存器说明。我们不希望每次用到寄存器的时候都查询手册,所以可以使用C语言中的枚举定义功能,根据手册把每个成员的所有取值都定义好,具体见代码清单1-10。GPIO_Speed和GPIO_Mode这两个成员对应的寄存器是CRL和CRH这两个端口配置寄存器,具体见图1-4和图1-5。

图1-4 端口配置低寄存器

图1-5 端口配置高寄存器
代码清单8-10 GPIO枚举类型定义
1 /**
2 * GPIO输出速率枚举定义
3 */
4 typedef enum
5 {
6 GPIO_Speed_10MHz = 1,// 10MHz (01)b
7 GPIO_Speed_2MHz, // 2MHz (10)b
8 GPIO_Speed_50MHz // 50MHz (11)b
9 } GPIOSpeed_Type Def;
10
11 /**
12 * GPIO工作模式枚举定义
13 */
14 typedef enum
15 {
16 GPIO_Mode_AIN = 0x0, // 模拟输入 (00000000)b
17 GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入 (00000100)b
18 GPIO_Mode_IPD = 0x28,// 下拉输入 (00101000)b
19 GPIO_Mode_IPU = 0x48, // 上拉输入 (01001000)b
20
21 GPIO_Mode_Out_OD = 0x14, // 开漏输出 (00010100)b
22 GPIO_Mode_Out_PP = 0x10, // 推挽输出 (00010000)b
23 GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出 (00011100)b
24 GPIO_Mode_AF_PP = 0x18 // 复用推挽输出 (00011000)b
25 } GPIOMode_Type Def;
关于这两个枚举类型的值如何与端口控制寄存器里面的说明对应起来,我们简单分析一下。有关速度的枚举类型有:(01)b 10MHz、(10)b 2MHz和(11)b 50MHz,这3个值与寄存器说明对得上,很容易理解。至于模式的枚举类型的值理解起来就比较难,这让很多人费了脑筋,下面我们通过一个表格来梳理一下,具体见图8-6。
如果从这些枚举值的十六进制来看,很难发现规律,转化成二进制之后,就比较容易发现规律。bit4用来区分端口是输入还是输出,0表示输入,1表示输出,bit2和bit3对应寄存器的CNFY[1:0]位,是真正要写入CRL和CRH这两个端口控制寄存器中的值。bit0和bit1对应寄存器的MODEY[1:0]位,这里暂不初始化,在GPIO_Init()初始化函数中用来与GPIO-Speed的值相加即可实现速率的配置。有关具体的代码分析见GPIO_Init()库函数。其中在下拉输入和上拉输入中设置bit5和bit6的值为01和10以示区别。
有了这些枚举定义,GPIO_Init Type Def结构体就可以使用枚举类型来限定输入参数,使用枚举定义的GPIO初始化结构体见代码清单1-11。
代码清单1-11 使用枚举定义的GPIO初始化结构体
1 /**
2 * GPIO初始化结构体类型定义
3 */
4 typedef struct
5 {
6 uint16_t GPIO_Pin; /*!< 选择要配置的GPIO引脚
7 可输入GPIO_Pin_ 定义的宏 */
8
9 GPIOSpeed_Type Def GPIO_Speed;/*!< 选择GPIO引脚的速率
10 可输入GPIOSpeed_Type Def定义的枚举值*/
11
12 GPIOMode_Type Def GPIO_Mode; /*!< 选择GPIO引脚的工作模式
13 可输入GPIOMode_Type Def定义的枚举值*/
14 } GPIO_Init Type Def;
如果不使用枚举类型,仍使用“uint16_t”类型来定义结构体成员,那么成员值的范围就是0~255,而实际上这些成员只能输入几个数值。所以使用枚举类型可以对结构体成员起到限定输入的作用,只能输入相应已定义的枚举值。
利用这些枚举定义,给GPIO_Init Type Def结构体类型赋值就变得非常直观,范例见代码清单1-12。
代码清单1-12 给GPIO_Init Type Def初始化结构体赋值范例
1 GPIO_Init Type Def GPIO_Init Structure;
2
3 /* GPIO端口初始化 */
4 /*选择要控制的GPIO引脚*/
5 GPIO_Init Structure.GPIO_Pin = GPIO_Pin_0;
6 /*设置引脚模式为输出模式*/
7 GPIO_Init Structure.GPIO_Mode = GPIO_Mode_Out_PP;
8 /*设置引脚的输出类型为推挽输出*/
9 GPIO_Init Structure.GPIO_Speed = GPIO_Speed_50MHz;
8.3.7 定义GPIO初始化函数
接着前面的思路,对初始化结构体赋值后,把它输入GPIO初始化函数,由它来实现寄存器配置。GPIO初始化函数实现见代码清单1-13。
代码清单1-13 GPIO初始化函数
1 /**
2 *函数功能:初始化引脚模式
3 *参数说明:GPIOx,该参数为GPIO_Type Def类型的指针,指向GPIO端口的地址
4 * GPIO_Init Type Def:GPIO_Init Type Def结构体指针,指向初始化变量
5 */
6 void GPIO_Init(GPIO_Type Def* GPIOx, GPIO_Init Type Def* GPIO_Init Struct)
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的低4位暂存在currentmode
13 currentmode = ((uint32_t)GPIO_Init Struct->GPIO_Mode) &
14 ((uint32_t)0x0F);
15
16 //bit4是1表示输出,bit4是0则是输入
17 //判断bit4是1还是0,即首先判断是输入还是输出模式
18 if ((((uint32_t)GPIO_Init Struct->GPIO_Mode) &
19 ((uint32_t)0x10)) != 0x00)
20 {
21 //若是输出模式,则要设置输出速度
22 currentmode |= (uint32_t)GPIO_Init Struct->GPIO_Speed;
23 }
24 /*-----GPIO CRL寄存器配置CRL寄存器控制低8位IO-----*/
25 //配置端口低8位,即Pin0~Pin7
26 if (((uint32_t)GPIO_Init Struct->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_Init Struct->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_Init Struct->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_Init Struct->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_Init Struct->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_Init Struct->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_Init Struct->GPIO_Mode == GPIO_Mode_IPD)
103 {
104 //若为下拉输入模式,引脚默认置0,对BRR寄存器写1可对引脚置0
105 GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
106 }
107 //判断是否为上拉输入模式
108 if (GPIO_Init Struct->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_Init Struct两个输入参数,分别是GPIO外设指针和GPIO初始化结构体指针,分别用来指定要初始化的GPIO端口及引脚的工作模式。
要充分理解这个GPIO初始化函数,得配合刚刚分析的GPIO引脚工作模式真值表来看(见图1-6)。
1)先取得GPIO_Mode的值,判断bit4是1还是0,判断是输出还是输入。如果是输出,则设置输出速率,即加上GPIO_Speed的值;输入没有速率之说,不用设置。
2)配置CRL寄存器。通过GPIO_Pin的值计算出具体需要初始化哪个引脚,算出后,把需要配置的值写入CRL寄存器中,具体分析见代码注释。有一个比较有趣的现象是上/下拉输入并不是直接通过配置某一个寄存器来实现的,而是通过写BSRR或者BRR寄存器来实现的。这让很多只看手册没看固件库底层源码的人摸不着头脑,因为手册的寄存器说明中没有明确指出如何配置上拉/下拉,具体见图1-7。
图1-7 上拉/下拉寄存器说明
3)配置CRH寄存器过程同CRL。
4.8 全新面貌,使用函数点亮LED
完成以上的准备后,我们就可以用自己定义的函数来点亮LED,见代码清单1-14。
代码清单1-14 使用函数点亮LED
1 //使用固件库点亮LED
2 int main(void)
3 {
4 //定义一个GPIO_Init Type Def类型的结构体
5 GPIO_Init Type Def GPIO_Init Structure;
6
7 //开启GPIO端口时钟
8 RCC_APB2ENR |= (1<<3);
9
10 //选择要控制的GPIO引脚
11 GPIO_Init Structure.GPIO_Pin = GPIO_Pin_0;
12
13 //设置引脚模式为通用推挽输出
14 GPIO_Init Structure.GPIO_Mode = GPIO_Mode_Out_PP;
15
16 //设置引脚速率为50MHz
17 GPIO_Init Structure.GPIO_Speed = GPIO_Speed_50MHz;
18
19 //调用库函数,初始化GPIO引脚
20 GPIO_Init(GPIOB, &GPIO_Init Structure);
21
22 //使引脚输出低电平,点亮LED1
23 GPIO_Reset Bits(GPIOB,GPIO_Pin_0);
24
25 while (1)
26 {
27 //使引脚输出低电平,点亮LED
28 GPIO_Reset Bits(GPIOB,GPIO_Pin_0);
29
30 /*延时一段时间*/
31 Delay(0x FFFF);
32
33 /*使引脚输出高电平,关闭LED1*/
34 GPIO_Set Bits(GPIOB,GPIO_Pin_0);
35
36 /*延时一段时间*/
37 Delay(0x FFFF);
38 }
39 }
现在看起来,使用函数来控制LED的代码与之前直接控制寄存器的代码已经有了很大的区别:main函数中先定义了一个GPIO初始化结构体变量GPIO_Init Structure,然后对该变量的各个成员按点亮LED所需要的GPIO配置模式进行赋值;赋值后,调用GPIO_Init函数,让它根据结构体成员值对GPIO寄存器写入控制参数,完成GPIO引脚初始化。控制电平时,直接使用GPIO_Set Bits和GPIO_Resetbits函数控制输出。若对其他引脚进行不同模式的初始化,只要修改GPIO初始化结构体GPIO_Init Structure的成员值,把新的参数值输入GPIO_Init函数再调用即可。
代码中新增的Delay函数的主要功能是延时,让我们可以看清楚实验现象(不延时的话指令执行太快,肉眼看不出来),它的实现原理是让CPU执行无意义的指令,消耗时间,在此不要纠结它的延时时间,写一个大概输入参数值,下载到实验板实测,觉得太久可把参数值改小,太短就改大即可。若需要精确延时,会使用STM32的定时器外设进行设置。
4.9 下载验证
把编译好的程序下载到开发板并复位,可看到板子上的灯被点亮。
4.10 总结
我们从寄存器映射开始,把内存跟寄存器建立起一一对应的关系,然后操作寄存器点亮LED,再把寄存器操作封装成一个个函数。一步一步走来,实现了库最简单的雏形,如果不断地增加操作外设的函数,并且把所有的外设都写完,一个完整的库就实现了。
本章中的GPIO相关库函数及结构体定义,实际上都是从ST标准库搬过来的。这样分析它纯粹是为了满足自己的求知欲,学习其编程的方式、思想,这对提高我们的编程水平是很有好处的,顺便感受一下ST库设计的严谨性,这样的代码不仅严谨且华丽优美。
与直接配置寄存器相比,从执行效率上看会有额外的消耗:初始化变量赋值的过程、库函数在被调用的时候要耗费调用时间;在函数内部,对输入参数转换所需要的额外运算也消耗一些时间(如在GPIO中运算求出引脚号时)。而其他的宏、枚举等解释操作是在编译过程中完成的,这部分并不消耗内核的时间。那么函数库的优点呢?是可以快速上手STM32控制器;配置外设状态时,不需要再纠结要向寄存器写入什么数值;交流方便,查错简单。这些就是我们选择库的原因。
现在的处理器的主频越来越高,我们不需要担心CPU耗费那么多时间来干活会不会被“累倒”,库主要应用是在初始化过程,而初始化过程一般是在芯片刚上电或在核心运算之前执行的,这段等待时间是0.02μs还是0.01μs在很多时候并没有什么区别。相对来说,我们还是担心如果都用寄存器操作,每行代码都要查数据手册的寄存器说明,自己会被累倒吧。
在以后开发的工程中,一般不会去分析ST的库函数的实现。因为外设的库函数是很类似的,库外设都包含初始化结构体,以及特定的宏或枚举标识符,这些封装被库函数转化成相应的值,写入寄存器中,函数内部的具体实现是十分枯燥和机械的工作。如果读者有兴趣,在掌握了如何使用外设的库函数之后,可以阅读一下它的源码实现。
通常只需要了解每种外设的“初始化结构体”,就能够通过它了解STM32的外设功能及控制。
本文从寄存器映射入手,逐步解析STM32固件库的构建过程,详细介绍寄存器结构体定义、外设存储器映射、初始化结构体设计等内容,最终实现使用库函数点亮LED,旨在帮助读者理解STM32固件库的编程思想与实践。
3044

被折叠的 条评论
为什么被折叠?



