通用I/O
- GPIO概述
- GPIO硬件架构与寄存器
- GPIO硬件架构
- GPIO工作模式
- GPIO相关寄存器
- 4个配置寄存器
- 2个数据寄存器
- 1个置位/复位寄存器
- 1个锁存寄存器
- 2个复用功能寄存器
- GPIO库文件架构与代码剖析
- 寄存器版本
- GPIO结构体定义
- GPIO地址映射
- GPIO初始化(寄存器配置)
- GPIO赋值操作
- HAL库版本
- GPIO结构体定义
- GPIO初始化设置
- GPIO赋值操作
- GPIO外部中断
本文从两个方面剖析GPIO:
- 结合《STM32F4xx中文参考手册》,从硬件和寄存器的角度剖析GPIO
- 结合库例程,从软件角度剖析GPIO如何配置,同时剖析嵌入式代码规范
阅读完本文,要能回答以下问题:
- 简述GPIO口的硬件架构与配置寄存器
- 配置GPIO口寄存器的代码流程
- 然后就是结合配置GPIO的代码回答一些嵌入式代码规范的问题:
- 寄存器前面的类型修饰符的含义,例如__IO uint32_t ODR
- 为啥要用volatile类型修饰符?
- 使用unsigned int与int声明后续配置有什么区别?
- #define与typedef有什么区别?
- 为什么嵌入式代码中有这么多宏定义?
- GPIO定义的结构体中的这些配置寄存器变量是怎么映射到实际地址的?
- 嵌入式代码中常常需要进行位操作,结合配置GPIO的代码,谈谈如何进行位操作?
- 嵌入式代码中常常需要对多位进行位操作,结合配置GPIO的代码,谈谈如何进行位的遍历?
- M3M4内核通过位带映射支持直接对位进行操作,谈谈你对位带操作的认识?
- 嵌入式代码进入带参数的函数调用中去时,常常会先使用assert_param()函数,结合代码谈谈==assert_param()==的作用
GPIO概述
每组GPIO口包括4个32位的配置寄存器、2个32位的数据寄存器、1个32位的置位/复位寄存器、1个32位锁定寄存器和2个32位复用功能选择寄存器,一共10个寄存器。研究清楚这些寄存器有什么用、以及如何配置,也就搞懂了GPIO。
GPIO硬件架构与寄存器
GPIO硬件架构
GPIO工作模式
GPIO有八种工作模式,四种输入工作模式:
1.== 输入浮空模式==。上拉/下拉电阻不起作用,外部电平直接输入到芯片内部,适合电流要求小的场合。信号送至输入数据寄存器
2.== 输入上拉模式==。上拉电阻起作用。低电平输入时对芯片外部有灌电流。信号送至输入数据寄存器
3. 输入下拉模式。下拉电阻起作用。高电平输入时对芯片外部有拉电流。信号送至输入数据寄存器
4. 模拟模式。上拉/下拉电阻不起作用,信号沿模拟输入线输入到芯片内部ADC
四种输出工作模式:
- 开漏输出模式。CPU通过置位/复位寄存器或直接写操作,设置好输出数据寄存器。输出驱动器只有一个MOS管工作,当输出1时,MOS管截止,通过外接上拉电阻实现输出高电平。当输出0时,MOS管导通,输出低电平。
- 开漏复用输出模式。输出的数据来自CPU的片上复用外设。
- 推挽式输出。输出驱动器的NMOS与PMOS管均工作,当输出1时,PMOS导通,输出高电平,当输出0时,NMOS导通,输出低电平。
- 推挽式复用输出模式。输出的数据来自CPU的片上复用外设。
GPIO上电复位后,默认工作在输入浮空状态。
GPIO相关寄存器
注意,每组GPIO口通过10个相关寄存器管理好,一组包括16个GPIO口,那么,如果某个寄存器需要2位配置一个口,那刚好均分位,如果某个寄存器只需要1位配置一个口,那一般高16位保留。
4个配置寄存器
1、端口模式寄存器(GPIOx_MODER)
- 每两个位控制一个IO口:00—输入浮空模式(复位状态);01—通用输出模式;10—复用功能模式;11—模拟模式。不用记忆、会查DATASHEET看懂就行
2、端口输出类型寄存器(GPIOx_OTYPER)
- 配置成推挽或开漏输出,每1位就能控制一个IO口,所以高16位保留
3、端口输出速度寄存器(GPIOx_OSPEEDR)
- 有四种输出速度,每两个位控制一个IO口
4、端口上拉下拉寄存器(GPIOx_PUPDR)
- 有三种情况(无上拉下拉、上拉、下拉),每两个位控制一个IO口
2个数据寄存器
5、端口输入数据寄存器(GPIOx_IDR)
- 每个GPIO的输入有两种情况,用一位来记录,所以高16位保留,这些位是只读模式的。
6、端口输出数据寄存器(GPIOx_ODR)
- 每个GPIO的输出有两种情况,用一位来记录,所以高16位保留,这些位是可读可写的。
1个置位/复位寄存器
7、端口置位/复位寄存器(GPIOx_BSRR)
- 高16位为复位位、低16位为置位位。这些位为只写,0无影响,1有作用。
- ==已经有数据寄存器了,为啥还要这个寄存器?==如果只有数据寄存器,每次设置某一个IO口的取值时,需要先进行读操作,保证其他IO口不受影响,再设置这个IO口。有了置位/复位寄存器,无需进行读操作了,把不用操作的IO口写0就不会影响到那些IO口了。
1个锁存寄存器
8、端口锁存寄存器(GPIOx_LCKR)
- 用的不多,当IO口被锁住时,无法执行CPU的写操作了。
2个复用功能寄存器
9、复用功能低位寄存器(GPIOx_AFRL)
每4位控制一个IO口的复用功能,低位寄存器控制低8位的8个GPIO口的复用功能。一个GPIO口可以复用16个复用功能
10、复用功能高位寄存器(GPIOx_AFRH)
每4位控制一个IO口的复用功能,高位寄存器控制高8位的8个GPIO口的复用功能。
GPIO库文件架构与代码剖析
两个层次,首先是寄存器版本例程,学习如何操作相应寄存器实现操控GPIO口。
寄存器版本
GPIO结构体定义
根据上文所讲的,每组GPIO口对应的10个相关寄存器,定义了以下结构体:
typedef struct
{
__IO uint32_t MODER;
__IO uint32_t OTYPER;
__IO uint32_t OSPEEDR;
__IO uint32_t PUPDR;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t LCKR;
__IO uint32_t AFR[2];
} GPIO_TypeDef;
看了上面的结构体定义,我产生了几个疑问:
(1)数据类型_IO uint32_t是什么声明类型?
#define __IO volatile
typedef unsigned int uint32_t;
_IO是volatile类型修饰符,uint32_t是unsigned int。
(2)为啥要用volatile类型修饰符?
volatile的意思是告诉编译器,在编译源代码时,对这个变量不要使用优化。具体看这篇文章.
比如写这个io端口的时候,如果没有这个volatile,很可能由于编译器的优化,会先把值先写到一个缓冲区,到一定时候
再写到io端口,这样就不能使数据及时的写到io端口,有了volatile说明以后,就不会再经过cache,write buffer这种,而是直接写到io端口,从而避免了读写io端口的延时
(3)使用unsigned int与int声明的区别?
unsigned int占四个字节,int是默认有符号的即signed int也占4个字节。有符号无符号在使用上有区别,例如我要把寄存器ODR的32位全置1,十进制下,按照无符号类型我要赋值232-1,有符号类型我要赋值-231
(4)#define与typedef有什么区别?
- 在编程中使用typedef目的一般有两个,一个是给变量一个易记且意义明确的新名字,另一个是简化一些比较复杂的类型声明。
- #define 宏名 字符串 。其中的“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。“define”为宏定义命令。“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。
- 关键字typedef在编译阶段有效,由于是在编译阶段,因此typedef有类型检查的功能。
define则是宏定义,发生在预处理阶段,也就是编译之前,它只进行简单而机械的字符串替换,而不进行任何检查。具体看这篇文章.
(5)结构体中的这些变量是怎么跟实际地址映射到一起的呢?
GPIO地址映射
使用上述声明的结构体定义了11组GPIO
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
#define GPIOI ((GPIO_TypeDef *) GPIOI_BASE)
#define GPIOJ ((GPIO_TypeDef *) GPIOJ_BASE)
#define GPIOK ((GPIO_TypeDef *) GPIOK_BASE)
这时候又有疑问了,GPIO的地址映射是什么样的?
每组GPIO的地址是用外设总线基地址+偏移量定义好的
#define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000)
#define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400)
#define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800)
#define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00)
#define GPIOE_BASE (AHB1PERIPH_BASE + 0x1000)
#define GPIOF_BASE (AHB1PERIPH_BASE + 0x1400)
#define GPIOG_BASE (AHB1PERIPH_BASE + 0x1800)
#define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00)
#define GPIOI_BASE (AHB1PERIPH_BASE + 0x2000)
#define GPIOJ_BASE (AHB1PERIPH_BASE + 0x2400)
#define GPIOK_BASE (AHB1PERIPH_BASE + 0x2800)
外设总线基地址是用外设基地址+偏移量定义好的
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000)
#define AHB2PERIPH_BASE (PERIPH_BASE + 0x10000000)
外设基地址是绝对地址了。
#define SRAM2_BASE ((uint32_t)0x2001C000)
#define SRAM3_BASE ((uint32_t)0x20020000)
#define PERIPH_BASE ((uint32_t)0x40000000)
uint32_t Mode;
uint32_t Pull;
uint32_t Speed;
uint32_t Alternate;
}GPIO_InitTypeDef;
跟寄存器形式相比较,似乎少了一些定义,然后发现定义的Mode变量既可以设置端口模式,还可以设置端口输出类型。
#define GPIO_MODE_INPUT ((uint32_t)0x00000000)
#define GPIO_MODE_OUTPUT_PP ((uint32_t)0x00000001)
#define GPIO_MODE_OUTPUT_OD ((uint32_t)0x00000011)
#define GPIO_MODE_AF_PP ((uint32_t)0x00000002)
#define GPIO_MODE_AF_OD ((uint32_t)0x00000012)
怎么实现一个32位的变量去设置两个32位的寄存器的呢?那就要看后续GPIO初始化的程序。
GPIO初始化设置
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOB_CLK_ENABLE(); //使能IO时钟
GPIO_Initure.Pin=GPIO_PIN_0|GPIO_PIN_1; //PB1,0
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //¸高速
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET); //PB0置1
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_SET); //PB1置1
}
首先是对GPIO_InitTypeDef的成员变量赋值,然后进入HAL_GPIO_Init()函数
HAL库版本的GPIO初始化代码有100多行,比寄存器版本的代码多了10倍,为什么?我们接着研究
1、首先,HAL_GPIO_Init()接收两个入口参数,第一个变量是寄存器版本定义的GPIO_TypeDef变量类型,由此我们知道,GPIO原始的寄存器地址声明在这个变量里面,实际上还是要操作这些配置寄存器。第二个变量是GPIO_InitTypeDef类型,是我们新定义的类型
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
2、进入函数内部的第一步,就是进行入口参数的有效性判断。几乎是带参数的函数前面都有调用assert_param()
assert_param(IS_GPIO_ALL_INSTANCE(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Init->Pin));
assert_param(IS_GPIO_MODE(GPIO_Init->Mode));
assert_param(IS_GPIO_PULL(GPIO_Init->Pull));
assert_param()可以对表达式进行判断,如果表达式为真,什么都不执行,如果表达式为假,进入错误日志函数。这个函数常用于调试阶段检查是否有参数输入错误。
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
#define IS_GPIO_SPEED(SPEED) (((SPEED) == GPIO_SPEED_FREQ_LOW) || ((SPEED) == GPIO_SPEED_FREQ_MEDIUM) || \ ((SPEED) == GPIO_SPEED_FREQ_HIGH) || ((SPEED) == GPIO_SPEED_FREQ_VERY_HIGH))
3、有效性判断后,开始进行遍历寻找需要设置的IO口,当IO口被索引到之后,开始正式配置寄存器。
for(position = 0; position < GPIO_NUMBER; position++)
{
ioposition = ((uint32_t)0x01) << position;
iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;
if(iocurrent == ioposition)
{
//开始配置寄存器
}
4、配置寄存器的操作同上,按照先清零(=取反再位与)再设置(位或)的先后顺序来配置
assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
temp = GPIOx->OSPEEDR;
temp &= ~(GPIO_OSPEEDER_OSPEEDR0 << (position * 2));
temp |= (GPIO_Init->Speed << (position * 2));
GPIOx->OSPEEDR = temp;
GPIO赋值操作
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET); //PB0置1
同样,第一个入口参数是寄存器版本定义的GPIO_TypeDef变量类型,由此我们知道,实际上还是操作这些配置寄存器。函数内部就是设置BSRR寄存器,很容易看明白。
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
assert_param(IS_GPIO_PIN(GPIO_Pin));
assert_param(IS_GPIO_PIN_ACTION(PinState));
if(PinState != GPIO_PIN_RESET)
{
GPIOx->BSRR = GPIO_Pin;
}
else
{
GPIOx->BSRR = (uint32_t)GPIO_Pin << 16;
}
}