1 GPIO详解

1.1 gpio框图

与GPIO相关的寄存器,不涉及复用,简单理解就是电灯、蜂鸣器控制等,与之相关的寄存器一共有7个

  • GPIOx_CRL(x = A..E)端口配置低寄存器
  • GPIOx_CRH(x = A..E)端口配置高寄存器
  • GPIOx_IDR(x = A..E)端口输入数据寄存器
  • GPIOx_ODR(x = A..E)端口输出数据寄存器
  • GPIOx_BRR(x = A..E)端口位清除寄存器
  • GPIOx_BSRR(x = A..E)端口位设置/清除寄存器
  • GPIOx_LCKR(x = A..E)端口配置锁定寄存器

1.2 库函数操作

对于寄存器的详细描述在此不展示,下文我们通过一个GPIO输出一个低/高电平控制蜂鸣器的过程中所使用到的库函数以及相关结构体定义的例子展开,来一一详细解析库如何封装寄存器操作,同时也学习GPIO外设使用流程,这是大家熟悉的库开发流程

main->GPIO初始化->控制外设

  1. GPIO_InitTypeDef GPIO_InitStructure;
//这是外设库STM32f10x_gpio.h中GPIO结构体相关的定义typedef struct //GPIO结构体定义{uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured.This parameter can be any value of @ref GPIO_pins_define */GPIOSpeed_TypeDef GPIO_Speed;/*!< Specifies the speed for the selected pins.This parameter can be a value of @ref GPIOSpeed_TypeDef */GPIOMode_TypeDef GPIO_Mode;/*!< Specifies the operating mode for the selected pins.This parameter can be a value of @ref GPIOMode_TypeDef */}GPIO_InitTypeDef;typedef enum //输出速度枚举定义,注意一个细节,只使用了bit0和bit1低两位{6 GPIO_Speed_10MHz = 1,// 10MHZ (01)b7 GPIO_Speed_2MHz, // 2MHZ (10)b8 GPIO_Speed_50MHz // 50MHZ (11)b9 } 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;#define GPIO_Pin_0 ((uint16_t)0x0001)/*!< Pin 0 selected */#define GPIO_Pin_1 ((uint16_t)0x0002)/*!< Pin 1 selected */#define GPIO_Pin_2 ((uint16_t)0x0004)/*!< Pin 2 selected */#define GPIO_Pin_3 ((uint16_t)0x0008)/*!< Pin 3 selected */#define GPIO_Pin_4 ((uint16_t)0x0010)/*!< Pin 4 selected */#define GPIO_Pin_5 ((uint16_t)0x0020)/*!< Pin 5 selected */#define GPIO_Pin_6 ((uint16_t)0x0040)/*!< Pin 6 selected */#define GPIO_Pin_7 ((uint16_t)0x0080)/*!< Pin 7 selected */#define GPIO_Pin_8 ((uint16_t)0x0100)/*!< Pin 8 selected */#define GPIO_Pin_9 ((uint16_t)0x0200)/*!< Pin 9 selected */#define GPIO_Pin_10((uint16_t)0x0400)/*!< Pin 10 selected */#define GPIO_Pin_11((uint16_t)0x0800)/*!< Pin 11 selected */#define GPIO_Pin_12((uint16_t)0x1000)/*!< Pin 12 selected */#define GPIO_Pin_13((uint16_t)0x2000)/*!< Pin 13 selected */#define GPIO_Pin_14((uint16_t)0x4000)/*!< Pin 14 selected */#define GPIO_Pin_15((uint16_t)0x8000)/*!< Pin 15 selected */#define GPIO_Pin_All ((uint16_t)0xFFFF)/*!< All pins selected */

2.void GPIO_Init(GPIO_TypeDef * GPIOx,GPIO_InitTypeDef * GPIO_InitStruct)

//请配合上面真值表分析,如果不够直观,请查看GPIOMode_TypeDef结构体的枚举值二进制,我们这里以GPIO_Mode_Out_PP = 0x10推挽输出 (0001 0000)b为例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;// 定义一些临时变量,因为寄存器都是32位,所以定义32位/*---------------------------- GPIO模式配置 -----------------------*/currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);//分析真值表不难看出bit2 和 bit3 对应寄存器的 CNFY[1:0]位,是我们真正要写入到 CRL 和 CRH 这两个端口控制寄存器中的值。//取出传入参数GPIO_Mode的低4位并复制,其实际作用是将currentmode的bit2和bit3两位赋值为我们传入的,但是这里为什么使用0x0F呢,这不是对4位都赋值了吗,因为我们传入的参数GPIO_Mode的低2位bit0和bit1所有枚举值都未使用,我们在下一步操作if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00){ //取出bit4是否为0,我们通过真值表分析,可以分析出,0为输入,1为输出,当bit4 != 0时,条件成立进入这里currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed; //来到这里确定为输出模式,输出模式需要配置输出速度 //重点:通过CRL寄存器位说明得知,每4位中,低2位控制输出速度,通过真值表也能发现,我们定义GPIO_Mode枚举值的时候,并未使用bit0和bit1两位 //重点:而GPIO_Speed的时候,只使用了低两位 //代码分析:这条代码的作用是将传入的输出速度参数的bit0和bit1赋值给至currentmode这个临时变量的bit0和bit1,其他位不变 //重点结论 :currentmode中,bit2 和 bit3 对应寄存器的 CNFY[1:0]位,bit0 和 bit1 对应寄存器的MODEY[1:0] 位 //例:加入我们传入要的是推挽输出,50MHZ的话,现在currentmode的bit3~bit0分别是: 0 0 1 1}/*---------------------------- GPIO CRL Configuration ------------------------*///每一组有16个IO口,CRL控制低8位IO/* Configure the eight low port pins */if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00){//注意观察上面引脚的宏定义,都是16位,此操作判断出是否是属于低8位的IO口,如果是则进入这里tmpreg = GPIOx->CRL;//使用一个临时变量读取GPIOx_CRL的值,作为备份for (pinpos = 0x00; pinpos < 0x08; pinpos++){//这个for找出具体引脚pos = ((uint32_t)0x01) << pinpos;//每次循环都往左移一位currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;//令输入的引脚参数与pos做与运算if (currentpin == pos){//因为pos中只有一位为1,传入参数中也只有一位为1,则与运算之后如果相等,则表明找到引脚pos = pinpos << 2;//重点:左移两位实际上是乘4,因为4个bit才控制一个引脚,GPIOx_CRL有32位,控制8个引脚,现在我们找出了引脚索引,所以引脚索引*4就是在寄存器中的实际位置pinmask = ((uint32_t)0x0F) << pos;//将0x0F左移pos位, 0xF = ……0000 0000 0000 0000 1111,大概就是这样,左移3位后 = ……0000 0000 1111 0000 0000 0000tmpreg &= ~pinmask;//tmpreg刚刚备份了GPIO_CRL寄存器,此处将上一步的pinmask取反之后再与,相当于将tmpreg中的控制pos引脚的位清零tmpreg |= (currentmode << pos);//将我们上面配置好的currentmode写入tmpregif (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD){//判断传入的参数是否为下拉模式GPIOx->BRR = (((uint32_t)0x01) << pinpos); //如果是下拉则对BRR寄存器对应位写1对引脚置0}else{if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU){//否则判断是否为上拉模式GPIOx->BSRR = (((uint32_t)0x01) << pinpos); // 上拉输入模式, 引脚默认值为 1, 对 BSRR 寄存器写 1 对引脚置1}}}}GPIOx->CRL = tmpreg;//将我们的配置写入GPIOx_CRL,就达到了,只改变了想要控制引脚的对应的4个bit,其他bit并未受到影响}//下面的跟上面那个一样,就是引脚如果是8~15则跑下面这段,代码一模一样,在这里就不赘述/*---------------------------- GPIO CRH Configuration ------------------------*//* Configure the eight high port pins */if (GPIO_InitStruct->GPIO_Pin > 0x00FF){tmpreg = GPIOx->CRH;for (pinpos = 0x00; pinpos < 0x08; pinpos++){pos = (((uint32_t)0x01) << (pinpos + 0x08));/* Get the port pins position */currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);if (currentpin == pos){pos = pinpos << 2;/* Clear the corresponding high control register bits */pinmask = ((uint32_t)0x0F) << pos;tmpreg &= ~pinmask;/* Write the mode configuration in the corresponding bits */tmpreg |= (currentmode << pos);/* Reset the corresponding ODR bit */if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD){GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));}/* Set the corresponding ODR bit */if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU){GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));}}}GPIOx->CRH = tmpreg;}}

2 寄存器由来

上面我们将库函数源码拿出来逐句分析,已经知道了库函数是如何操作寄存器的,但是又引出了一个问题,寄存器是哪来的?为什么能直接用寄存器?好的家人们,那下面我们就来一步一步探讨库是如何封装,将硬件地址操作映射为结构体操作,让我们可以非常便捷的使用外设结构体的

2.1 地址映射

在图 STM32F10xx 系统框图 中,被控单元的 FLASH, RAM, FSMC 和 AHB 到 APB 的桥(即片
上外设),这些功能部件共同排列在一个 4GB 的地址空间内。我们在编程的时候,可以通过他们
的地址找到他们,然后来操作他们(通过 C 语言对它们进行数据的读和写)。

存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射,具体见图存储器映射 。如果给存储器再分配一个地址就叫存储器重映射。

在这 4GB 的地址空间中, ARM 已经粗线条的平均分成了 8 个块,每块 512MB,每个块也都规定了用途,具体分类见表格存储器功能分类 。每个块的大小都有 512MB,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而已。
在这 8 个 Block 里面,有 3 个块非常重要,也是我们最关心的三个块。 Block0 用来设计成内部 FLASH, Block1 用来设计成内部 RAM, Block2 用来设计成片上的外设,下面我们简单的介绍下 这三个 Block 里面的具体区域的功能划分

2.1 寄存器映射

我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射,那什么叫寄存器映射?寄存器到底是什么?
在存储器 Block2 这块区域,设计的是片上外设,它们以四个字节为一个单元,共 32bit,每一个 单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起 始地址,然后通过 C 语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个 内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。

片上外设区分为三条总线,根据外设速度的不同,不同总线挂载着不同的外设, APB1 挂载低速外设, APB2 和 AHB 挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中 APB1 总线的地址最低,片上外设从这里开始,也叫外设基地址。

总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX 外设基地址”,也叫 XX 外设的边界地址。具体有关 STM32F10xx 外设的边界地址请参考《STM32F10xx参考手册》的 2.3 小节的存储器映射的表 1: STM32F10xx 寄存器边界地址。这里面我们以 GPIO 这个外设来讲解外设的基地址, GPIO 属于高速的外设,挂载到 APB2 总线上,具体见表格外设 GPIO 基地址 。

在 XX 外设的地址范围内,分布着的就是该外设的寄存器。以 GPIO 外设为例, GPIO 是通用输入输出端口的简称,简单来说就是 STM32 可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把 GPIO 的引脚连接到 LED 灯的阴极, LED 灯的阳极接电源,然后通过 STM32 控制该引脚的电平,从而实现控制 LED 灯的亮灭。GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为 32bit,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以GPIOB 端口为例,来说明 GPIO 都有哪些寄存器

3 C语言封装寄存器

/* 外设基地址 */4 #define PERIPH_BASE ((unsigned int)0x40000000)/* 总线基地址 */5 #define APB1PERIPH_BASE PERIPH_BASE6 #define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)7 #define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000)810 /* GPIO 外设基地址 */11 #define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)12 #define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)13 #define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)14 #define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)15 #define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)16 #define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)17 #define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)20 /* 寄存器基地址,以 GPIOB 为例 */21 #define GPIOB_CRL (GPIOB_BASE+0x00)22 #define GPIOB_CRH (GPIOB_BASE+0x04)23 #define GPIOB_IDR (GPIOB_BASE+0x08)24 #define GPIOB_ODR (GPIOB_BASE+0x0C)25 #define GPIOB_BSRR (GPIOB_BASE+0x10)26 #define GPIOB_BRR (GPIOB_BASE+0x14)27 #define GPIOB_LCKR (GPIOB_BASE+0x18)

结合上面的代码和结构体的定义,我们就不难看出,只要将结构体赋值一个具体的外设基地址,就能够通过4字节偏移的方式将外设绝对地址抽象为结构体偏移地址,从而达到访问结构体等于访问硬件绝对地址一样的效果,没错,寄存器就是这么来的,我们后面的章节将不再分析寄存器由来,因为都是通用的,不过会以每一个外设的硬件地址开始分析。

4 GPIO的常用库函数

4.1 void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

//这有啥好说的,源码就一行,将GPIOx端口的GPIO_Pin设置为低电平,你问我这么做不是影响其他IO了吗,问的好,因为这个寄存器写0没效果看上面void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin){GPIOx->BRR = GPIO_Pin;}

4.2 void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

//使用方法和上面一个一样,这个是置1,如果想用这个寄存器写0其实也是一样的,只要将传入的pin左移16位,这函数就等于GPIO_ResetBits了void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin){GPIOx->BSRR = GPIO_Pin;}

4.3 uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx) 和 uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

//读取一组IO口的状态uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx){ return ((uint16_t)GPIOx->ODR);}//读取一个IO口的状态uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin){uint8_t bitstatus = 0x00;if ((GPIOx->ODR & GPIO_Pin) != (uint32_t)Bit_RESET){bitstatus = (uint8_t)Bit_SET;}else{bitstatus = (uint8_t)Bit_RESET;}return bitstatus;}

4.4 汇总

来来回回就这几个,有手就行,用法也都大差不差,直接贴图吧

读取输入电平函数(2个):uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);读取输出电平函数(2个):uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);设置输出电平函数(4个):void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);

重点是看init函数以及寄存器地址映射,C语言封装,要是那些都理解了,剩下的就是用了,其实也就达到了知其然知其所以然,工作忙,手动肝也是很累,更新较慢大家见谅,太难了加油啊,大家共勉!