构建库函数的过程
- 寄存器结构体定义
- 编写端口置位复位函数
- 编写GPIO初始化结构体和初始化函数
- 总结及如何提高程序的可移植性
我们上篇文章写到用寄存器映射,实现点亮小灯,但是我们发现每次我们都要查看参考手册找到寄存器的内存地址并且定义别名,这样必然很麻烦。
我们可以定义一个结构体,结构体中内容与外设中寄存器的排列顺序是一样的(外设中的寄存器的偏移地址正好为4个字节递增的,我们按递增的顺序在结构体中起别名定义4个字节外设的寄存器,这样就实现了一一对应),这是我们只要将外设基址强制类型转换为结构体类型的指针,这样这个指针就指向结构体定义的那块内存单元。
过程:
寄存器结构体定义实现点亮
头文件:stm32f10x.h
#ifndef __STM32F10X_H #define __STM32F10X_H #define PERIPH_BASE ((unsigned int)0x40000000) #define APB1PERIPH_BASE PERIPH_BASE #define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) #define AHBPERIPH_BASE (PERIPH_BASE + 0x20000) #define RCC_BASE (AHBPERIPH_BASE + 0x1000) //RCC的基地址 #define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)//GBIOC的基地址 ypedef unsigned int uint32_t; typedef unsigned short uint16_t; typedef struct { uint32_t CRL; uint32_t CRH; uint32_t IDR; uint32_t ODR; uint32_t BSRR; uint32_t BRR; uint32_t LCKR; }GPIO_TypeDef; typedef struct { uint32_t CR; uint32_t CFGR; uint32_t CIR; uint32_t APB2RSTR; uint32_t APB1RSTR; uint32_t AHBENR; uint32_t APB2ENR; uint32_t APB1ENR; uint32_t BDCR; uint32_t CSR; }RCC_TypeDef; #define GPIOC ((GPIO_TypeDef*)GPIOC_BASE) //指针 重定义 #define RCC ((RCC_TypeDef*)RCC_BASE)//指针 重定义 #endif
main.c函数中
#include "stm32f10x.h" int main (void) { // 打开 GPIOB 端口的时钟 RCC->APB2ENR|= ( 1 << 4 ); //配置PC2 IO口为通用推挽输出,速度为10M GPIOC->CRL&=~ ( 0x0f << (4*2) );//GPIOC IO 4位清空 GPIOC->CRL|= ( 1 << (4*2) );// 通用推挽输出,速度为10M // 控制 ODR 寄存器 GPIOC->ODR&= ~(1<<2);//开 // GPIOC->ODR|= (1<<2); //关 }
我们通过上面的代码可以实现点亮小灯,但是我们发现再给相应的寄存器赋值时,使用左移的这种方法显然不怎么方便每次都要查询参考手册,查看寄存器的定义结构才能进行相应的赋值。所有我们要编写端口置位和复位函数来快捷进行赋值。
编写端口置位复位函数
我们新建一个头文件 stm32f10x_gpio.h和stm32f10x_gpio.c文件用于置位和址位。
stm32f10x_gpio.h的内容:
#ifndef __STM32F10X_GPIO_H #define __STM32F10X_GPIO_H #include "stm32f10x.h" #define GPIO_Pin_0 ((uint16_t)0x0001) /*!< 选择Pin0 */ //(00000000 00000001)b #define GPIO_Pin_1 ((uint16_t)0x0002) /*!< 选择Pin1 */ //(00000000 00000010)b #define GPIO_Pin_2 ((uint16_t)0x0004) /*!< 选择Pin2 */ //(00000000 00000100)b #define GPIO_Pin_3 ((uint16_t)0x0008) /*!< 选择Pin3 */ //(00000000 00001000)b #define GPIO_Pin_4 ((uint16_t)0x0010) /*!< 选择Pin4 */ //(00000000 00010000)b #define GPIO_Pin_5 ((uint16_t)0x0020) /*!< 选择Pin5 */ //(00000000 00100000)b #define GPIO_Pin_6 ((uint16_t)0x0040) /*!< 选择Pin6 */ //(00000000 01000000)b #define GPIO_Pin_7 ((uint16_t)0x0080) /*!< 选择Pin7 */ //(00000000 10000000)b #define GPIO_Pin_8 ((uint16_t)0x0100) /*!< 选择Pin8 */ //(00000001 00000000)b #define GPIO_Pin_9 ((uint16_t)0x0200) /*!< 选择Pin9 */ //(00000010 00000000)b #define GPIO_Pin_10 ((uint16_t)0x0400) /*!< 选择Pin10 */ //(00000100 00000000)b #define GPIO_Pin_11 ((uint16_t)0x0800) /*!< 选择Pin11 */ //(00001000 00000000)b #define GPIO_Pin_12 ((uint16_t)0x1000) /*!< 选择Pin12 */ //(00010000 00000000)b #define GPIO_Pin_13 ((uint16_t)0x2000) /*!< 选择Pin13 */ //(00100000 00000000)b #define GPIO_Pin_14 ((uint16_t)0x4000) /*!< 选择Pin14 */ //(01000000 00000000)b #define GPIO_Pin_15 ((uint16_t)0x8000) /*!< 选择Pin15 */ //(10000000 00000000)b #define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< 选择全部引脚*/ //(11111111 11111111)b void GPIO_SetBits(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin); void GPIO_ResetBits( GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin ); #endif
stm32f10x_gpio.c的内容:
#include "stm32f10x_gpio.h" void GPIO_SetBits(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin) {
//BSRR的低16位 0-15 用于给相应的GPIOx的0-15个引脚置位
//我们宏定义GPIO_Pin0-15对应的值
GPIOx->BSRR |= GPIO_Pin;
}
void GPIO_ResetBits( GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin )
{
//BRR的低16位 0-15用于给相应的GPIOx的0-15个引脚清除 GPIOx->BRR |= GPIO_Pin; }
main函数中:
#include "stm32f10x.h"
#include "stm32f10x_gpio.h" int main (void) { // 打开 GPIOB 端口的时钟 RCC->APB2ENR|= ( 1 << 4 ); //配置PC2 IO口为通用推挽输出,速度为10M GPIOC->CRL&=~ ( 0x0f << (4*2) );//GPIOC IO 4位清空 GPIOC->CRL|= ( 1 << (4*2) );// 通用推挽输出,速度为10M // 控制 ODR 寄存器 //GPIOC->ODR&= ~(1<<2);//开 // GPIOC->ODR|= (1<<2); //关
GPIO_SetBits(GPIOC,GPIO_Pin_2); //GPIO_ResetBits(GPIOC,GPIO_Pin_2); }
编写GPIO初始化结构体和初始化函数
定义位操作函数后,控制 GPIO 输出电平的代码得到了简化,但在控制 GPIO 输出电平前还需要初始化 GPIO 引脚的各种模式(例如GPIOC->CRL&=~ ( 0x0f << (4*2) );我们不查看参考手册很难知道1<<4是什么意思),这部分代码涉及的寄存器有很多,我们希望初始化 GPIO 也能以如此简单的方法去实现。为此,我们先根据 GPIO 初始化时涉及到的初始化参数以结构体的形式封装起来,声明一个名为 GPIO_InitTypeDef 的结构体类型。
这个结构体中包含了初始化 GPIO 所需要的信息,包括引脚号、工作模式、输出速率。设计这个结构体的思路是:初始化 GPIO 前,先定义一个这样的结构体变量,根据需要配置 GPIO 的模式,对这个结构体的各个成员进行赋值,然后把这个变量作为“GPIO 初始化函数”的输入参数,该函数能根据这个变量值中的内容去配置寄存器,从而实现 GPIO 的初始化。
stm32f10x_gpio.h继续添加内容,添加内容为:
typedef enum { //枚举类型 后边是,而不是; GPIO_Speed_10MHz = 1, // 10MHZ (01)b GPIO_Speed_2MHz, // 2MHZ (10)b GPIO_Speed_50MHz // 50MHZ (11)b }GPIOSpeed_TypeDef; typedef enum { GPIO_Mode_AIN = 0x0, // 模拟输入 (0000 0000)b GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入 (0000 0100)b GPIO_Mode_IPD = 0x28, // 下拉输入 (0010 1000)b GPIO_Mode_IPU = 0x48, // 上拉输入 (0100 1000)b GPIO_Mode_Out_OD = 0x14, // 开漏输出 (0001 0100)b GPIO_Mode_Out_PP = 0x10, // 推挽输出 (0001 0000)b GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出 (0001 1100)b GPIO_Mode_AF_PP = 0x18 // 复用推挽输出 (0001 1000)b }GPIOMode_TypeDef; typedef struct { uint16_t GPIO_Pin; uint16_t GPIO_Speed; uint16_t GPIO_Mode; }GPIO_InitTypeDef;
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
上面定义的结构体很直接,美中不足的是在对结构体中各个成员赋值实现某个功能时,还需要查询手册的寄存器说明,我们不希望每次用到的时候都要去查询手册, 我们可以使用 C 语言中的枚举定义功能,根据手册把每个成员的所有取值都定义好。 GPIO_Speed 和 GPIO_Mode 这两个成员对应的寄存器是 CRL 和 CRH 这两个端口配置寄存器。
关于这两个枚举类型的值如何跟端口控制寄存器里面的说明对应起来,我们简单分析下。有关速度的枚举类型有(01)b 10MHZ、 (10)b 2MHZ 和(11)b 50MHZ,这三个值跟寄存器说明对得上,很容易理解。至于模式的枚举类型的值理解起来就比较绕,这让很多人费了脑筋,下面我们通过一个表格来梳理下,好帮助我们理解,具体见图 9-6.
如果但从这些枚举值的十六进制来看,很难发现规律,转化成二进制之后,就比较容易发现规律。 bit4 用来区分端口是输入还是输出, 0 表示输入, 1 表示输出, bit2 和 bit3 对应寄存器的 CNFY[1:0]位,是我们真正要写入到 CRL 和 CRH 这两个端口控制寄存器中的值。bit0 和 bit1 对应寄存器的 MODEY[1:0]位,这里我们暂不初始化在 GPIO_Init()初始化函数中用来跟 GPIOSpeed 的值相加即可实现速率的配置。有关具体的代码分析见 GPIO_Init()库函数。 其中在下拉输入和上拉输入中我们设置 bit5 和 bit6 的值为 01 和 10 来以示区别。
如果不使用枚举类型,仍使用“uint16_t”类型来定义结构体成员,那么成员值的范围就是 0-255,而实际上这些成员都只能输入几个数值。所以使用枚举类型可以对结构体成员起到限定输入的作用,只能输入相应已定义的枚举值。
stm32f10x_gpio.c继续添加内容,添加内容为:
定义 GPIO 初始化函数 :对初始化结构体赋值后,把它输入到 GPIO 初始化函数,由它来实现寄存器配置。
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) { uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00; uint32_t tmpreg = 0x00, pinmask = 0x00; /*---------------------- GPIO 模式配置 --------------------------*/ // 把输入参数GPIO_Mode的低四位暂存在currentmode currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F); // bit4是1表示输出,bit4是0则是输入 // 判断bit4是1还是0,即首选判断是输入还是输出模式 if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00) { // 输出模式则要设置输出速度 currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed; } /*-------------GPIO CRL 寄存器配置 CRL寄存器控制着低8位IO- -------*/ // 配置端口低8位,即Pin0~Pin7 if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00) { // 先备份CRL寄存器的值 tmpreg = GPIOx->CRL; // 循环,从Pin0开始配对,找出具体的Pin for (pinpos = 0x00; pinpos < 0x08; pinpos++) { // pos的值为1左移pinpos位 pos = ((uint32_t)0x01) << pinpos; // 令pos与输入参数GPIO_PIN作位与运算,为下面的判断作准备 currentpin = (GPIO_InitStruct->GPIO_Pin) & pos; //若currentpin=pos,则找到使用的引脚 if (currentpin == pos) { // pinpos的值左移两位(乘以4),因为寄存器中4个寄存器位配置一个引脚 pos = pinpos << 2; //把控制这个引脚的4个寄存器位清零,其它寄存器位不变 pinmask = ((uint32_t)0x0F) << pos; tmpreg &= ~pinmask; // 向寄存器写入将要配置的引脚的模式 tmpreg |= (currentmode << pos); // 判断是否为下拉输入模式 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD) { // 下拉输入模式,引脚默认置0,对BRR寄存器写1可对引脚置0 GPIOx->BRR = (((uint32_t)0x01) << pinpos); } else { // 判断是否为上拉输入模式 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU) { // 上拉输入模式,引脚默认值为1,对BSRR寄存器写1可对引脚置1 GPIOx->BSRR = (((uint32_t)0x01) << pinpos); } } } } // 把前面处理后的暂存值写入到CRL寄存器之中 GPIOx->CRL = tmpreg; } /*-------------GPIO CRH 寄存器配置 CRH寄存器控制着高8位IO- -----------*/ // 配置端口高8位,即Pin8~Pin15 if (GPIO_InitStruct->GPIO_Pin > 0x00FF) { // // 先备份CRH寄存器的值 tmpreg = GPIOx->CRH; // 循环,从Pin8开始配对,找出具体的Pin for (pinpos = 0x00; pinpos < 0x08; pinpos++) { pos = (((uint32_t)0x01) << (pinpos + 0x08)); // pos与输入参数GPIO_PIN作位与运算 currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos); //若currentpin=pos,则找到使用的引脚 if (currentpin == pos) { //pinpos的值左移两位(乘以4),因为寄存器中4个寄存器位配置一个引脚 pos = pinpos << 2; //把控制这个引脚的4个寄存器位清零,其它寄存器位不变 pinmask = ((uint32_t)0x0F) << pos; tmpreg &= ~pinmask; // 向寄存器写入将要配置的引脚的模式 tmpreg |= (currentmode << pos); // 判断是否为下拉输入模式 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD) { // 下拉输入模式,引脚默认置0,对BRR寄存器写1可对引脚置0 GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08)); } // 判断是否为上拉输入模式 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU) { // 上拉输入模式,引脚默认值为1,对BSRR寄存器写1可对引脚置1 GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08)); } } } // 把前面处理后的暂存值写入到CRH寄存器之中 GPIOx->CRH = tmpreg; } }
这个函数有 GPIOx 和 GPIO_InitStruct 两个输入参数,分别是 GPIO 外设指针和 GPIO初始化结构体指针。分别用来指定要初始化的 GPIO 端口及引脚的工作模式。要充分理解这个 GPIO 初始化函数,得配合我们刚刚分析的 GPIO 引脚工作模式真值表来看。
1)先取得 GPIO_Mode 的值,判断 bit4 是 1 还是 0 来判断是输出还是输入。如果是输出则设置输出速率,即加上 GPIO_Speed 的值,输入没有速率之说,不用设置。
2) 配置 CRL 寄存器。通过 GPIO_Pin 的值计算出具体需要初始化哪个引脚,算出后,然后把需要配置的值写入到 CRL 寄存器中,具体分析见代码注释。这里有一个比较有趣的是上/下拉输入并不是直接通过配置某一个寄存器来实现的,而是通过写BSRR 或者 BRR 寄存器来实现。这让很多只看手册没看固件库底层源码的人摸不着头脑,因为手册的寄存器说明中没有明确的指出如何配置上拉/下拉,具体见图9-8。
3)配置 CRH 寄存器过程同 CRL。
main.c中的内容:
void Delay(uint32_t count) { for( ; count !=0; count-- ); } int main (void) { RCC->APB2ENR |= ( (1) << 4 ); GPIO_InitStructure.GPIO_Pin = LED_G_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(LED_G_GPIO_PORT, &GPIO_InitStructure); //GPIO_InitStructure地址的内容传递进去 while(1) { GPIO_SetBits(GPIOC,GPIO_PIN_2); Delay(0xFFFF); GPIO_ResetBits(GPIOC,GPIO_PIN_2); Delay(0xFFFF); } }
为了提高程序的可以移植性,我们可以继续这样宏定义(main.c中):
#define LED_G_GPIO_PORT GPIOB #define LED_G_GPIO_PIN GPIO_Pin_0
将 GPIO_SetBits和 GPIO_ResetBits的参数进行替换。