几乎所有的MCU芯片都提供有不同数量的引脚,这些引脚可以用作GPIO。这些GPIO也是和外部世界进行通信的方式,例如最简单的LED,还有UART、USB、SPI等。再了解GPIO的使用方法之前,可以先了解下STM32外设如何映射到逻辑地址以及它们在HAL库中的表示方式。
1 STM32外设映射
每个STM32外设都通过几个总线连接到MCU。
system bus连接了Cortex-M Core到总线矩阵(Bus Matrix)。Bus Matrix负责管理Cortex-M Core和DMA之间的仲裁。Cortex-M Core和DMA都充当主设备。
DMA bus连接了DMA的Advanced High-performance Bus(AHB)主接口到Bus Matrix,Bus Matrix管理CPU和DMA对SRAM、闪存存储器和外设的访问。
Bus Matrix管理Cortex-M Cored的system bus和DMA bus之间的访问仲裁,采用了循环轮询算法。总线矩阵由两个主设备(CPU、DMA)和四个从设备(闪存存储接口、SRAM、具有AHB到高级外设总线(APB)桥接的AHB1和AHB2)组成。AHB外设通过总线矩阵连接到系统总线,以允许DMA访问。AHB到APB桥提供了AHB总线和APB总线之间的全同步连接,其中大多数外设连接到APB总线。
在【STM32Cube开发记录】5-寄存器和存储器映射中提到的外设映射到一个4GB的地址。从0x4000 0000到最高0x5FFF FFFF。这个区域被进一步划分几个区域。
这种分配方式,是特定于给定的STM32微控制器的。例如,在STM32F072微控制器中,AHB2总线映射到地址范围从0x48000000到0x480017FF。这意味着这个区域宽度为6144字节。这个区域进一步分为多个子区域,每个子区域对应一个特定的外设。以前面的例子为例,GPIOA外设从0x48000000到0x480003FF映射,这意味着它占用了1KB的地址映射外设存储空间。
这个内存映射空间的分配方式取决于具体的外设。下表显示了GPIO外设的内存布局。这种内存映射结构使MCU能够直接访问外设的寄存器和功能,从而实现与外部世界的通信和控制。每个外设都有自己的地址范围,其中包含了与该外设相关的寄存器和功能。这使得软件能够轻松地与外设进行交互和配置。
外设就是通过修改和读取这些映射区域的每个寄存器来进行控制。举个例子,以GPIOA外设为例,要将PA4引脚配置为输出引脚,我们需要配置MODER寄存器,使位[9:8]配置为01(对应于通用输出模式)。
接下来,为了将引脚拉高,我们需要设置Output Data Register(ODR)中相应的位[4],该寄存器映射到GPIOA + 0x14的内存位置,即0x48000000 + 0x14。以下是一个示例,展示了如何使用指针来访问STM32F72微控制器中GPIOA外设的内存映射:
volatile uint32_t *GPIOA_MODER = 0x0, *GPIOA_ODR = 0x0;
GPIOA_MODER = (uint32_t*)0x48000000; // GPIOA->MODER 寄存器地址
GPIOA_ODR = (uint32_t*)(0x48000000 + 0x14); // GPIOA->ODR 寄存器地址
*GPIOA_MODER = *GPIOA_MODER | 0x100; // Sets MODER[9:8] = 0x1
*GPIOA_ODR = *GPIOA_ODR | 0x10; // Sets ODR[4] = 0x1, PA4输出高电平
HAL的作用就是将上面这种具体的外设映射进行抽象化,通过为每个外设定义多个处理程序来实现这一点。处理程序实际上就是一个C结构体,其引用用于指向实际的外设地址。我们通常可以看到下面配置GPIOA4的例子
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
这里面的GPIOA变量是一个指向GPIO_TypeDef类型的指针,它的定义如下
typedef struct {
volatile uint32_t MODER;
volatile uint32_t OTYPER;
volatile uint32_t OSPEEDR;
volatile uint32_t PUPDR;
volatile uint32_t IDR;
volatile uint32_t ODR;
volatile uint32_t BSRR;
volatile uint32_t LCKR;
volatile uint32_t AFR[2];
volatile uint32_t BRR;
} GPIO_TypeDef;
GPIOA指向的地址就是:
GPIO_TypeDef *GPIOA = 0x48000000;
GPIOA->MODER |= 0x100;
GPIOA->ODR |= 0x10;
2 GPIO的配置
HAL提供了一些接口给我们去初始化GPIO,不需要使用者去知道如何配置它的寄存器。我们使用HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)函数配置GPIO。其中GPIO_InitTypeDef是一个C的结构体,定义如下:
typedef struct {
uint32_t Pin;
uint32_t Mode;
uint32_t Pull;
uint32_t Speed;
uint32_t Alternate;
} GPIO_InitTypeDef;
Pin(引脚):从0开始的引脚数字,比如,GPIO_PIN_4。可以一次性配置多个引脚,(GPIO_PIN_1 | GPIO_PIN_5 | GPIO_PIN_6)。
Mode(模式):引脚模式。可配置以下选项:
- GPIO_MODE_INPUT:浮空输入模式
- GPIO_MODE_OUTPUT_PP:推挽输出模式
- GPIO_MODE_OUTPUT_OD:开漏输出模式
- GPIO_MODE_AF_PP:复用推挽输出模式
- GPIO_MODE_AF_OD:复用开漏输出模式
- GPIO_MODE_ANALOG:模拟输入模式
- GPIO_MODE_IT_RISING:外部中断模式,上升沿触发检测
- GPIO_MODE_IT_FALLING:外部中断模式,下降沿触发检测
- GPIO_MODE_IT_RISING_FALLING:外部中断模式,上升沿和下降沿触发检测
- GPIO_MODE_EVT_RISING:外部事件模式,上升沿触发检测
- GPIO_MODE_EVT_FALLING:外部事件模式,下降沿触发检测
- GPIO_MODE_EVT_RISING_FALLING:外部事件模式,上升沿和下降沿触发检测
Pull(上拉):配置上拉下拉。如果处于输出模式,一般配置为NOPULL,当处于输入模式,根据默认输入值来进行配置。上拉就是将不确定的信号通过一个电阻钳位在高电平,电阻同时起到限流作用;而下拉就是将不确定的信号通过一个电阻钳位在低电平,电阻同时起到限流作用(开漏输出假如不连接外部的上拉电阻,则只能输出低电平)。
- GPIO_NOPULL:无上拉或下拉
- GPIO_PULLUP :上拉
- GPIO_PULLDOWN : 下拉
Speed(速度) :定义Pin引脚的开关速度。这个速度是几个常量值,MCU的GPIO都有最大的开关频率。
- GPIO_SPEED_FREQ_LOW
- GPIO_SPEED_FREQ_MEDIUM
- GPIO_SPEED_FREQ_HIGH
- GPIO_SPEED_FREQ_VERY_HIGH
Alternate(复用):复用功能,选择你需要连接的外设。
2.1 GPIO模式
STM32的IO口硬件结构如下,通过GPIO_InitTypeDef的配置,MCU的IO会变更以下工作方式。
(GPIO模式GPIO_MODE_EVT_*与睡眠模式有关。当 I/O 配置为在这些模式之一下工作时,如果触发了相应的 I/O,CPU 将被唤醒(当使用 WFE 指令置于睡眠模式时),而不会产生相应的中断。GPIO模式GPIO_MODE_IT_*模式与中断管理有关。这可能由 STM32 系列不一样而异,尤其是对于低功耗系列。必要时务必参考 MCU 的参考手册)
这种设计也可以优化PCB的设计,比如你外部需要使用上拉电阻,无需在单独配置专用的电阻,相应的GPIO可以进行配置,节省成本和空间:
- GPIO_InitTypeDef.Mode = GPIO_MODE_OUTPUT_PP
- GPIO_InitTypeDef.Pull = GPIO_PULLUP
2.2 GPIO的复用功能
大多数GPIO都具有复用功能,它们可以用作至少一个内部外设的I/O引脚。但是,一个 I/O 一次只能关联到一个外设。如果想知道哪些IO口可以复用哪些功能,可以查手册,也可以在CubeMX的IO配置直接查看PIN脚。之后会自动生成代码,给出一个示例:
GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_3;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
GPIO_InitStruct.Alternate = GPIO_AF1_USART2;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
2.3 GPIO几个关键函数
CubeHAL提供了一些针对GPIO日常高频率使用的函数。
1. 读IO口的函数
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
当 I/O是低电平时,返回GPIO_PIN_RESET,如果I/O是高电平时,返回GPIO_PIN_SET。
2. 改变IO口状态函数
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
3. 翻转IO口状态函数
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
4. 锁定IO口状态
任何更改其配置的后续尝试都将失败,直到发生reset
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
2.4 取消初始化GPIO
可以将 GPIO 引脚设置为其默认重置状态,即输入浮空模式。
void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin)
这个函数在对资源或功耗进行管理的情况下非常有用:
- 当不再需要特定的外设时。
- 在 CPU 进入休眠模式时,以避免浪费功耗。