——STM32 HAL库开发系列<3>
GPIO——General Purpose Input/Output,通用型输入输出的简称,其功能引脚可以按照使用者编写的程序配合相应硬件电路自由使用,即可以作为输入引脚又可以作为输出引脚,同样的可以配置为上拉或者下拉电阻模式使用,拥有极大的灵活性,是单片机功能中最基础、最常用、最具有普适性的重要功能。
本页内容概览
1.STM32 GPIO功能简述
STM32中的GPIO功能相当重要,在上一节教程中作为例程的LED闪烁工程本质上就是通过延时函数和GPIO反转函数(Toggle)作为输出,不断地改变LED灯的电平从而使LED灯在点亮和熄灭之间不断的切换。STM32F1系列芯片的结构示意图如下所示,来源于官方芯片手册:
可以看到红框中标出的部分为stm32F1的GPIO端口,F1系列具有五组GPIO,分别为GPIOA-GPIOE,每组输入输出端口具有0-15共计16个引脚,也就是说STM32F1芯片最多具有80个GPIO端口。事实上整个STM32的产品线的GPIO都是这种命名和组织方式,即具有n组GPIO,从GPIOA开始命名为GPIOx,每组端口具有16个独立的引脚;但是很遗憾的是在一些中低端芯片上,例如教程实例使用的stm32f103c8t6芯片由于引脚数量等原因其部分功能被阉割,只有GPIOA-GPIOC具有有效引脚并且并不是所有0-15引脚都能正常生效,而在一些高端版型上80个GPIO是满血的,在其他高性能系列例如F4系列中甚至具有7组GPIO(A-G)。在常见的原理图、PCB板丝印以及各类产品说明中,这些引脚常常以PC13、B11等形式出现,代表着GPIOC[13],GPIOB[11]引脚,使用时注意。
值得一提的是STM32芯片的引脚复用功能,这个词汇常见于各种教程和说明之中,其本意十分简单易懂:在stm32cubeMX和芯片手册中可以看到STM32在其两种外设总线AHB和APB上具有大量的“内置”外设,例如串口UART、USART等,这种外设之所以称为”内置“就是因为ST在生产芯片时已经将相关功能所需的硬件电路集成制造进去。当我们使用这些功能时难免会占用原有的GPIO引脚,那么当这个GPIO引脚作为内置外设的引脚端口使用的时候就称之为复用。
当然,为了加深学习效果,为了灵活配置芯片的应用方式等等原因(也有ST官方硬件实现方式性能差劲的原因,例如IIC协议在HAL库和标准库中堪称BUG集散地,这是因为芯片硬件电路部分出现了巨大设计隐患并不是软件编写有问题)我们经常通过手动配置GPIO输出/输入角色和时序的方法实现”软协议“,这也是GPIO的重要功能和使用方式。事实上,在较为早期的嵌入式开发中,例如使用STC系列51单片机,通信协议需要手动编写,甚至使用汇编语言直接调动寄存器,十分繁琐,这更加能够体现HAL库等底层驱动的优越性、便利性、不可替代性。
2.STM32 GPIO的硬件原理与各类工作方式
STM32芯片中的GPIO引脚电路原理图如下所示,首先引入眼帘的就是标红FT的两个保护二极管,这里需要重点说明的就是,STM32F1芯片的标准工作电压是3.3V,但是很多场景中电路使用的标准高电压为5V,那么这就出现了一个问题:是否需要为每一个引脚都配备电平转换电路?要知道一般超过2.3或者2.5V的电压就可以被视为逻辑高电平,显然加入内部保护电路相比于为了每一个引脚都配置复杂庞大的电平转换电路要经济实惠且方便,那么他是怎么实现的呢?
2.1 GPIO引脚的电压保护功能
在STM32的非常多相关材料中3.3V电压也就是高电平被标记为VDD而0V也就是GND低电平被标记为Vss,可以看到保护的原理非常简单:假设引脚上的电压高于3.3V,也就是可能出现高压烧毁的风险时,上臂二极管正向导通,下臂二极管仍然反向截止,由于二极管的正向导通等效电阻非常非常小,可以视为某种程度上的短路,因此电流不会灌入芯片内部,而是通过上臂二极管泄出;同样的当引脚上的电压低于0V,也就是可能出现反向电压烧毁的风险时,下臂二极管正向导通,上臂二极管此时反向截止,下臂二极管再次形成短路泄出反向电压;而在电压位于0-3.3V的正常工作区间中时两个二极管全部反向截止,电路正常工作,可以得到下表:
端口输入电压 | 上臂二极管 | 下臂二极管 | 电路状态 |
大于3.3V | 导通 | 截止 | 上臂短路泄压 |
小于0V | 截止 | 导通 | 下臂短路泄压 |
0-3.3V | 截止 | 截止 | 电路正常工作 |
这样一来,STM32芯片就可以在保持3.3V工作电压的同时适配大多数的5V工作电压的外部电路,但是这并不意味着我们可以无限制的使用这一功能,首先数据手册中没有FT标志的引脚不具有这一功能,贸然使用一定会烧坏芯片或者对应功能区。其次面对过高的电压或者过低的电压,都会造成保护电路因为无法承受电流或者电压的冲击而损坏,失去了保护电路后接踵而来的就是芯片的损毁。
2.2 GPIO的四种输出模式
STM32的GPIO具有如下四种输出模式,其实严格来说应该是12种模式,因为每种输出模式都可以配置为上拉电阻、下拉电阻或者浮空(没有上拉下拉)三种情况。通过观察不难发现,其输出方式就是是否复用和开漏/推挽方式的排列组合:
- GPIO_Mode_Out_OD 开漏输出(包含上下拉)
- GPIO_Mode_Out_PP 推挽输出(包含上下拉)
- GPIO_Mode_AF_OD 开漏复用输出(包含上下拉)
- GPIO_Mode_AF_PP 推挽复用输出(包含上下拉)
回到这张原理图,在输出驱动器中极其醒目的就是由两个MOSFET组成的控制驱动电路,没有相关基础的同学可以认为MOS管就是加强版的BJT三极管,或者说一种电子开关,由其第三个引脚(左侧)控制右侧的两个引脚导通和关断的状态。例如下臂MOS关断,上臂MOS导通时,桥臂中间点的电压就会被拉高到3.3V从而输出一个逻辑高电平,反之上臂关断下臂导通,IO口上的电压就会被拉低至GND,输出逻辑低电平。
通过之前的介绍我们已经了解了复用功能的意涵,所以我们可以十分容易的理解输出部分的左侧电路,如果输出模式配置为Out,那么输出控制调取上方输出数据寄存器中的数据作为输出结果,反之如果配置为复用AF模式那么输出控制器调取下方来自片上外设的数据进行输出。那么什么是开漏(OD)什么是推挽(PP),他们之间的区别又是什么呢?
使用开漏模式,中文互联网中也将其称之为漏极模式(Open-Drain),只有下桥臂的N-MOS正常工作,也就是当N-MOS导通,那么IO口会被强制拉低电压为低电平;但是不能主动的驱动为高电平,也就是数据上输出为1时事实上两个MOS都不工作,处于关断状态,IO口的电压理论上是浮动的,只能依靠IO口外接电路的母线电压或者上下拉电阻将其拉高拉低。而推挽模式(Push-Pull)的工作状态则是完整的驱动整个桥臂进行工作,P-MOS也参加到其中去,可以主动的拉高电平(强制拉高)产生较强的灌电流。前者适用于一些通讯协议例如IIC等,而后者则是最常见的输出工作模式,例如驱动LED灯,使用PWM信号驱动H桥等等场景,相应的能耗也会加大。
最后需要提到的就是GPIO输出具有输出速度上的区分,也就是系统总线在处理数据过程中需要对数据流的快慢进行控制,同样的也是对芯片整体运算能力的负载和功率进行控制,具有以下四种分类:
- 2MHz Low Speed 低速模式
- 25MHz Middle Speed 中速模式
- 50MHz High Speed 快速模式
- 100MHz Super Speed 高速模式(F1系列最大主频72M,这个速度在F1是不可能达到的)
2.3 GPIO的四种输入模式
STM32的GPIO同样也具有四种输入模式,输入模式没有OD和PP的区别,也没有OUT和AF的区别,其主要的区别是是否使用模拟信号,是否使用上下拉电阻,因此有如下四种模式:
- GPIO_Mode_IN_Floating 浮空逻辑输入
- GPIO_Mode_IPU 上拉逻辑输入
- GPIO_Mode_IPD 下拉逻辑输入
- GPIO_Mode_AIN 模拟输入
仍旧回到这张原理图,我们这次来关注上拉下拉电阻电路,是的,STM32内部带有片上上下拉电阻电路 ,相比于51系列的某些单片机这非常方便。可以看到这个电路的本质就是一个电阻桥,上下桥臂都带有开关,任意一个桥臂的开关闭合,当IO口没有确切的电平控制时,IO线上的电平就会被下拉或者上拉到对应的电压,加入电阻是为了保证当IO口存在有内部或者外部主动驱动的电平而同时又开启了上下拉功能时,不至于发生短路影响电路的正常功能甚至烧毁芯片。需要注意的是,输入输出部分实际上是公用上拉下拉电阻的,图中将其粗暴的归入输入驱动部分是不准确的,是有失偏颇的。
至于输入的另一个大的区别:数字信号和模拟信号输入首先要明确二者的区别,例如一个2.97V的电平在逻辑信号也就是数字信号中将直接视为高电平也就是TRUE或者说1,但是在模拟信号中其表示为90%,该芯片的分辨率为12bit,也就是具有4096个分度值,那么将测定为3685,这部分相关的详细内容将会在本教程的ADC部分中出现,在此不做赘述。STM32芯片将之区分开的方式就是加入一个肖特基触发器:将高于2.3V的电平全部转换为一个标准的VDD高电平而低于2.3V的电平全部转换为一个VSS低电平处理,这样就完成了逻辑输入和模拟输入的区分,所有的逻辑输入在肖特基触发器后采样,模拟输入在肖特基触发器之前进行采样。
不同于输出信号,数据的来源可能是GPIO输出或者是片上外设的复用,而输出只有一个IO母线;输入部分恰好相反:只有一个来自于IO母线的输入但是输出有两大类三小类:模拟输入(不经过肖特基触发器),逻辑输入(经过肖特基触发器进行处理),逻辑输入的目的地虽然都是寄存器但是又可以分为GPIO输入的读取寄存器和其他片上外设的读取寄存器。这些通过芯片其他功能的配置即可实现,没有必要再GPIO阶段进行无谓的拆箱处理,所以输入模式并不做模式区分。
3. HAL库中使用GPIO功能
在HAL库中使用GPIO功能所用到的函数、宏定义、变量结构体等如下所示,使用GPIO功能时在HAL库中需要先配置初始化结构体,而后才能够使用GPIO相关功能,列表如下:
//以下所有内容定义于stm32f1xx_hal_gpio.c
#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)
//以上源码为ABCDE五个组的GPIO寄存器宏定义
#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 */
//以上源码为选定GPIOx组中0-15以及所有引脚的宏定义
#define GPIO_MODE_INPUT 0x00000000u //普通输入模式
#define GPIO_MODE_OUTPUT_PP 0x00000001u //推挽输出模式
#define GPIO_MODE_OUTPUT_OD 0x00000011u //开漏输出模式
#define GPIO_MODE_AF_PP 0x00000002u //推挽复用输出模式
#define GPIO_MODE_AF_OD 0x00000012u //开漏复用输出模式
#define GPIO_MODE_AF_INPUT GPIO_MODE_INPUT //复用输入模式,事实上与普通模式相同
#define GPIO_MODE_ANALOG 0x00000003u //模拟输入模式
//以上源码为GPIO输入输出模式宏定义
#define GPIO_NOPULL 0x00000000u //不上拉或者下拉
#define GPIO_PULLUP 0x00000001u //上拉模式
#define GPIO_PULLDOWN 0x00000002u //下拉模式
//以上源码为GPIO针脚上拉下拉的宏定义
#define GPIO_SPEED_FREQ_LOW (GPIO_CRL_MODE0_1) //低速模式
#define GPIO_SPEED_FREQ_MEDIUM (GPIO_CRL_MODE0_0) //中速模式
#define GPIO_SPEED_FREQ_HIGH (GPIO_CRL_MODE0) //高速模式
//以上源码为GPIO针脚最大频率限制,F1 HAL源码不包括超高速
typedef enum{
GPIO_PIN_RESET = 0u, //定义GPIO针脚低电平变量,即0
GPIO_PIN_SET //定义GPIO针脚高电平变量,隐含为1
} GPIO_PinState;
//以上枚举体定义了GPIO针脚的高低电平状态,可以直接用0/1使用
typedef struct{
uint32_t Pin; //定义了GPIO具体是组内的哪个针脚
uint32_t Mode; //定义了GPIO的输入输出模式
uint32_t Pull; //定义了GPIO针脚是否上拉下拉
uint32_t Speed; //定义了GPIO输出的最大频率
} GPIO_InitTypeDef;
//以上结构体应用于GPIO针脚在初始化时使用的配置
#define __HAL_RCC_GPIOA_CLK_ENABLE()
#define __HAL_RCC_GPIOB_CLK_ENABLE()
#define __HAL_RCC_GPIOC_CLK_ENABLE()
#define __HAL_RCC_GPIOD_CLK_ENABLE()
#define __HAL_RCC_GPIOE_CLK_ENABLE()
//以上均为宏定义函数,用于打开GPIOx组与系统时钟的同步
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
//该函数用于开启某一组内的GPIO针脚,第一个参数是GPIO组别,第二个是上文中的初始化结构体
void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin)
//该函数用于关闭某一个针脚,第一个参数是GPIO组别,第二个是具体的针脚编号
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
//该函数用于设置GPIO针脚输出电平,第一个参数为组别,第二个具体针脚,第三个高低电平参数
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
//该函数用于反转GPIO针脚输出电平,第一个参数为组别,第二个具体针脚
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
//该函数用于锁定GPIO因外部电平变化引起的电平改变,第一个参数为组别,第二个参数具体针脚
//注意,如果使用Toggle或者Write函数或者其他AF功能改变电平仍然能够实现改变
//该函数只是遏制由于外部电平变化引起的连锁反应
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
//该函数用于读取GPIO针脚电平值,第一个参数为组别,第二个参数为具体针脚
//该函数返回值为GPIO_PIN_RESET或者GPIO_PIN_SET也就是0或者1
以上代码有能力的同学应当详细查看打好基础,在适当的情况下应当阅读相关函数源码,了解STM32单片机的寄存器如何工作如何书写相关代码,以上代码在配置PC13引脚作为LED灯闪烁例程中CubeMX生成的代码如下所示,可以从中分析出GPIO针脚功能使用方法和代码书写流程:
// 在Src/main.c main()中调用GPIO初始化的流程:
HAL_Init(); //首先初始化HAL库,只有经过这个步骤才能进行其他一些操作
SystemClock_Config(); //其次初始化时钟功能,没有系统时钟其他功能大多不能工作
MX_GPIO_Init();//完成时钟初始化后开始初始化GPIO
while(1){}//进入循环
//以下是MX_GPIO_Init()的定义:
static void MX_GPIO_Init(void){
GPIO_InitTypeDef GPIO_InitStruct = {0}; //定义一个空的初始化结构体
__HAL_RCC_GPIOC_CLK_ENABLE(); //开启GPIO C组时钟(因为调用PC13针脚)
__HAL_RCC_GPIOD_CLK_ENABLE(); //开启GPIO D组时钟(PD0和PD1接入外部时钟)
__HAL_RCC_GPIOA_CLK_ENABLE(); //开启GPIO A组时钟(PC13和PC14接入ST-link调试器)
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);//为保证安全将引脚拉低
GPIO_InitStruct.Pin = GPIO_PIN_13; //设定13号针脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; //设定推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; //设定无上拉下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; //设定低速模式
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); //设定GPIO C组初始化以上针脚
}
以上是STM32CubeMX自动生成的代码,完整的展示了相关功能的使用流程,如有需要手写相关代码也可以参照上述代码执行流程。在STM32CubeMX中的配置在完整了解了上述内容后是傻瓜图形化操作,配置模拟输入的部分应当在ADC部分中设定,配置AF功能在相关复用功能中自动配置,无需纠结在GPIO功能之中,关于GPIO针脚中断的部分在NVIC教程中讲解。