STM32的GPIO_从寄存器到库函数
GPIO概述
GPIO的功能
-
输出功能
受控制输出高/低电平,从而控制外围设备
-
输入功能
读取引脚电平状态,从而获得外部信息输入
-
复用功能
作为片内外设的对外接口,例如作为串口通信的数据接收与发送引脚
-
时序模拟
通过改变引脚高低电平,模拟各种时序信号,比如后文将介绍的IIC通信
GPIO的工作模式
-
对应上述四种功能,有四种工作模式:输入模式、输出模式、复用模式和模拟模式,根据具体的配置,各个工作模式又可以进一步细分(更具体的介绍与选用参阅STM8的GPIO章节):
-
输入模式
- 浮空输入:不使用上拉/下拉电阻,此时引脚的电平状态不确定,完全由外部输入决定(也是复位后GPIO默认的工作模式)
- 上拉/下拉输入:使能上拉电阻,在没有外部信号输入时默认为高电平;下拉输入同理,默认为低电平
-
输出模式
- 推挽输出:可以输出高电平与低电平,能够控制驱动外围电路,是最常用的输出模式
- 开漏输出:只能输出低电平,当配置输出数据为1时并不输出高电平,而是由引脚外部的上拉/下拉电阻决定电平状态(如果没有电阻则处于悬空状态)
-
模拟模式
用于片内的模拟外设:AD转换、模拟比较器等的信号通道
工作在模拟模式下会关闭输入模式的施密特触发器、禁用上拉/下拉电阻,此时引脚功耗较小,因此常常将闲置引脚设置为模拟模式以减小系统功耗
-
复用模式
片内外设的外部引脚与GPIO引脚复用,复用模式下引脚的电平状态不再受端口寄存器组的控制,而是由片内外设控制
GPIO的特性
-
端口与引脚
GPIO端口分为多个组:GPIOA、GPIOB……
每个组中包含多个引脚:GPIO_PIN_0、GPIO_PIN_1……
在之后操作各个GPIO时,按端口号与引脚编号来选中要进行操作的GPIO
-
电压容限
尽管引脚标准高电平为3.3V,但大部分引脚具备5V的电压容限,可以接收5V的电压输入从而和5V供电的外围电路相连接
-
外部中断
每一个引脚都具备外部中断能力,在之后的中断一节中介绍
寄存器操作GPIO
寄存器操作原理
-
寄存器映射
在STM8中,我们使用类似
PA_ODR=0x01的语句去操控寄存器的各个位而经过STM8储存器一节的学习,我们可以知道这样的操作本质上是访问寄存器所在储存单元,像
PA_ODR本质上就是一个指针变量,它保存了指向PA端口ODR寄存器值的储存单元的地址,我们通过这个地址去访问该寄存器的储存空间,向其中读写配置内容在STM32中同理,而想要访问一个寄存器,首先要进行寄存器映射:把寄存器储存单元所在的地址转换为寄存器名字,就像C语言通过变量名来访问变量一样,这样就不必每次读写都使用一大串地址了:
#define GPIOA_MODER *(volatile unsigned int *)(0x40020000UL) //(0x40020000UL)是寄存器GPIOA_MODER的起始地址,这可在芯片数据手册中查到 //UL后缀表示unsigned long,即uint32位,相当于把int强制类型转换为unsigned long //(unsigned int *)修饰表示将该地址强制转换为指向unsigned int的指针 //使用unsigned int作基类型让指针一次访问4个字节的连续单元(因为寄存器储存单元共有32位) //volatile关键字用于避免寄存器优化,每次访问时去访问原始数据而非其在内存中的备份值完成映射之后,就能通过指针来访问单个寄存器:
GPIOA_MODER=0x00000003; uint32_t a = GPIOA_MODER; -
开发STM32所用数据类型
STM32与STM8最大的区别便是“32”位:对于STM8这样的8位单片机,寄存器含有8个二进制位,常使用一个无符号8位整数类型,也就是
uint8_t类型的数来对寄存器中的每个控制位进行定义和操作;STM32同理,不过其所需位数较之STM8更多:STM32一个寄存器包含32个位,使用
uint32_t(即unsigned long)进行配置,一个引脚端口组包含16个引脚,因此在配置引脚时使用的数据类型是uint16_t(即unsigned short)
GPIO相关寄存器
-
模式寄存器GPIOx_MODER
共有32位,每2位一组,16组分别控制对应引脚端口组中的16个引脚
用于配置引脚的模式:
- 00:输入模式
- 01:通用输出模式
- 10:复用输出模式
- 11:模拟模式
例如:GPIOA_MODER寄存器的位1与位0写入01,就会将PA0设置为输出模式
-
输出类型寄存器GPIOx_OTYPER
高16位保留,低16位分别控制对应引脚端口组中的16个引脚
用于控制GPIO端口各引脚的输出类型,只在引脚配置为输出模式时生效
- 0:推挽输出
- 1:开漏输出
-
输出速度寄存器GPIOx_OSPEEDR
共有32位,每2位一组,16组分别控制对应引脚端口组中的16个引脚
用于控制GPIO端口各引脚的输出速度,只在引脚配置为输出模式时生效
- 00:低速
- 01:中速
- 10:高速
- 11:超高速
-
上拉/下拉寄存器GPIOx_PUPDR
共有32位,每2位一组,16组分别控制对应引脚端口组中的16个引脚
用于使能GPIO端口各个引脚的上拉/下拉电阻,可用于输入与输出模式
- 00:无上拉与下拉
- 01:上拉
- 10:下拉
- 11:保留
-
输入数据寄存器GPIOx_IDR
高16位保留,低16位分别控制对应引脚端口组中的16个引脚
读取GPIO各个引脚的电平状态,可用于输入与输出模式
- 0:对应引脚输入低电平
- 1:对应引脚输入高电平
-
输出数据寄存器GPIOx_ODR
高16位保留,低16位分别控制对应引脚端口组中的16个引脚
控制GPIO各个引脚输出的电平状态,只用于输出模式
- 0:对应引脚输出低电平
- 1:对应引脚输出高电平
-
置位/复位寄存器GPIOx_BSRR
高16位分别控制对应引脚端口组中的16个引脚输出低电平:写1输出低电平,写0无作用
低16位分别控制对应引脚端口组中的16个引脚输出高电平:写1输出高电平,写0无作用
为何有了GPIOx_ODR还要再设置多一个控制输出的寄存器?因为使用GPIOx_BSRR来控制GPIO的输出与使用GPIOx_ODR相比有如下优势:原子操作,不会被中断打断,在控制多个引脚同时输出时更加方便,原理如下
- 使用GPIOx_ODR本质上要先读取GPIOx_ODR寄存器的值,保存在预先定义的变量中,然后用位操作修改变量,再将变量写回GPIOx_ODR;而GPIOx_BSRR寄存器可以直接进行位操作修改
寄存器操作代码
-
对寄存器的位操作
直接使用一个32位的数来写入寄存器不够直观,我们可以通过位操作来改进这一点
GPIOA_MODER &= ~(3U<<5*2); //MODER是两位一组控制一个对应引脚的寄存器,因此用<<5*2来配置PA5 //3U为无符号11,加上前面的~取反以及&按位与表示将PA5对应的两位清零 GPIOA_ODR |= 1U<<5; //ODR是一位控制一个对应引脚的寄存器的,因此用<<5来配置PA5 //使用|=来将对应位置1,使用&=和~将对应位清零 -
使用结构体指针访问寄存器组
如上的操作需要重新定义每一个寄存器,而且一次定义只能访问一个寄存器,想要简化操作,让一次定义就能访问与该外设相关的所有寄存器,需要借助结构体:定义一个包含端口GPIOx各个寄存器的数据结构,每个寄存器即结构体中的成员变量
这是因为同一端口组相关的10个寄存器在储存空间中地址是连续的,定义一个结构体后再将该端口组的起始地址(第一个寄存器GPIOx_MODER的起始地址)采用强制类型转换为指向结构体的指针,这样,结构体中的各个成员变量就恰好对应其代表寄存器的地址
struct GPIO { volatile unsigned int MODER; …… } //为了增加程序可读性,一般上述定义实际写法为: typedef struct { __IO uint32_t MODER;//通过宏定义将__IO定义为volatile,代表硬件寄存器 …… }GPIO_TypeDef; //之后定义对应端口的结构体变量和指向结构体变量的指针 GPIO_TypeDef GPIOA; GPIO_TypeDef *pGPIOA; //之后进行强制类型转换,将起始地址作为指向结构体的指针 #define GPIOA ((GPIO_TypeDef *)0x40020000UL) //如此在访问这个地址时可以从该地址开始连续访问40个字节(结构体10个uint32_t的成员) GPIO->MODER 等价于 *(volatile unsigned int *)(0x40020000UL) //使用例: GPIOA->BSRR = 1U<<5;这样的定义在ST公司提供的.h文件中预先做好了,文件为
型号xe.h如:STM32F411系列对应的头文件为stm32f411xe.h
HAL库操作GPIO
HAL库操作原理
-
HAL库的设计思想
从寄存器的操作方法中可见,对GPIO的使用实质上就是向对应外设寄存器写入所需的配置参数,而要初始化的寄存器数量较多,配置过程就会比较繁琐;
简化这个配置过程的方式是:构建一块储存空间用于存放初始化寄存器的配置参数,然后通过接口函数把这块空间其中参数写入到对应的外设寄存器中
可以看出,HAL库使用了面向对象的设计思想,GPIO引脚被视作一个对象,相关的参数是这个对象的属性,而操控这些属性的接口函数则是对象的方法,这种封装让用户不必掌握外设寄存器每一位的含义,同时屏蔽了不同型号STM32底层硬件的差异以便程序的移植
-
引脚初始化数据类型GPIO_InitTypeDef
引脚初始化的数据类型使用结构体实现:
typedef struct { uint32_t Pin;//选择GPIO引脚 uint32_t Mode;//设置工作模式 uint32_t Pull;//使能上拉/下拉电阻 uint32_t Speed;//设置输出速度 uint32_t Alternate;//设置复用功能 }GPIO_InitTypeDef;在写入初始化参数时,为了增强可读性,预先使用宏定义为其中各个成员变量的工作参数起别名,或者是使用枚举类型(例如引脚电平状态是
enum GPIO_PinState,被分为GPIO_PIN_SET高电平与GPIO_PIN_RESET低电平),修改成员变量的配置示例如下GPIO_InitTypeDef.Pin = GPIO_PIN_5 | GPIO_PIN_6; //GPIO_PIN_5本质是通过宏定义给0x0020(bit5为1其他位为0)这个常数的别名 //因此可以通过按位或来一次配置多个引脚 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;//推挽输出模式 GPIO_InitStruct.Pull = GPIO_NOPULL;//不使用上拉/下拉电阻 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;//低速输出 //复用功能一般使用CubeMX自动分配,不亲自配置 -
引脚初始化接口函数HAL_GPIO_Init
在初始化所用数据类型中配置好参数后,通过GPIO初始化接口函数
HAL_GPIO_Init将这些参数写入所要配置端口组的对应寄存器地址,这和前文通过结构体指针访问端口组的原理是一样的:HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);//将上述参数写到GPIOA端口地址的接口函数 //GPIOA本质是通过宏定义给((GPIO_TypeDef*)GPIOA_BASE)起的别名 //GPIOA_BASE是一个常数,它表示了GPIOA端口寄存器组的起始地址 //通过(GPIO_TypeDef*)对其类型转换,使其成为指向GPIOA寄存器组的GPIO_TypeDef类型指针 //从而在初始化函数内能够利用端口号->成员变量访问相应寄存器,把配置的参数写到对应寄存器中 -
CubeMX的GPIO配置流程
一般情况下,我们直接借助CubeMX的图形化界面来写入初始化数据结构与相关函数,不必同上文手动对逐个属性进行设置,将配置外设进一步简化以便开发
- 以将PA0设置为推挽输出高电平为例,演示如何使用CubeMX对引脚进行初始化配置:
- 先点击CubeMX左侧栏System Core的GPIO,打开对应窗口
- 在右侧的Pinout view中可以图形化地看到当前的引脚配置,单击所要配置的引脚,在弹出菜单中选择GPIO_output,可将引脚功能选择为输出模式
- 再在中间窗口的引脚列表中单击要配置引脚所对应的一行,在下方Configuration中将输出电平GPIO output level修改为high
- 如此配置后生成的工程文件的gpio.c中,在
MX_GPIO_Init(void)函数里可见对所配置引脚的初始化函数代码,同上文
GPIO相关接口函数
-
配置引脚电平状态HAL_GPIO_WritePin
引脚电平状态分为
GPIO_PIN_SET高电平与GPIO_PIN_RESET低电平,使用枚举类型而非0与1除了提升程序可读性外还通过限定取值确保变量合法void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) //GPIOx:端口号,如GPIOA,GPIOB,GPIOC…… //GPIO_Pin:引脚号,如GPIO_PIN_0,GPIO_PIN_1,GPIO_PIN_2…… //PinState:要配置的状态,GPIO_PIN_SET或GPIO_PIN_RESET,对应高低电平 //使用例:将PC8引脚配置为高电平 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, GPIO_PIN_SET); -
读取引脚电平状态HAL_GPIO_ReadPin
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) /* GPIOx:端口号,如GPIOA,GPIOB,GPIOC…… GPIO_Pin:引脚号,如GPIO_PIN_0,GPIO_PIN_1,GPIO_PIN_2…… 返回值:GPIO_PinState,即引脚状态GPIO_PIN_SET或GPIO_PIN_RESET */ -
反转引脚电平HAL_GPIO_TogglePin
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) //使用例:将PC8引脚电平反转 HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_8);
本文来自博客园,作者:无术师,转载请注明原文链接:https://www.cnblogs.com/artlessist/p/18866958
本文使用知识共享4.0协议许可 CC BY-NC-SA 4.0
特别说明版权归属的文章以及不归属于本人的转载内容(如引用的文章与图片)除外
浙公网安备 33010602011771号