打印
[其他ST产品]

基于STM32CubeIDE的GPIO原理和底层源码经验分享

[复制链接]
216|2
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
两只袜子|  楼主 | 2023-4-6 10:14 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
一、GPIO介绍
        1.1 GPIO 简述

        GPIO(General purpose input/output,通用型输入输出),一个引脚可以用于输入、输出或其他特殊功能,PIN脚依现实需要可作为通用输入(GPI)或通用输出(GPO)或通用输入与输出(GPIO),并多个GPIO一起组合可实现诸如UART、SPI等外设功能。

        GPIO是通过寄存器操作来实现电平信号输入输出的,对于输入,通过读取输入数据寄存器(IDR,Input Data Register)来确定引脚电位的高低;对于输出,通过写入输出数据寄存器(ODR,Output Data Register)来让这个引脚输出高电位或者低电位;对于其他特殊功能,则有另外的寄存器来控制它们。GPIO的输出通常可以输出0/1信号,用来实现如LED灯、继电器、蜂鸣器等控制,而GPIO的输入可识别0/1信号,用来实现开关、按键等等动作或状态判定。




       1.2 STM32 GPIO寄存器
         STM32中GPIO的寄存器是MCU分配的一个连续存储区域,按存储地址划分出不同功能寄存器,如下图所示,有Port mode寄存器、Output Type寄存器等等。




        在HLA库中,这些寄存器的数值(宏定义)通常会放在/Drivers/CMSIS/Device/ST/STM32L4xx/Include/stm32XXX.h头文件中,供开发者调用,例如STM32L496VGTx的开发板,该头文件具体名是stm32l496xx.h,自8977行起到10008行,具是按上述表宏定义了GPIO各寄存器的各个数据位可设置数据值(偏移(POS)及相应数值)。



        1.3 GPIO引脚与标记
         在一个STM32 MCU芯片中,所有引脚本质都是GPIO引脚,只是鉴于全GPIO引脚开发者使用起来比较不方便,于是才通过各个GPIO组合形成针对各个外设的IO接口。在CubeMX中,以STM32L496VGTx芯片为例,可以直观看到该芯片有100个引脚:LQFP100,LQFP是封装,100指的是引脚数。



        STM32的MCU标记一般有两种,直插IC以半圆条口为标记,贴片IC以小圆点为标记,也有缺口及小点都有标记的。通常将标记圆点或缺口正面朝左上角,左上角的下方脚为第一脚,依次围绕芯片中心逆时针计数,在旋转360度以后,左上角右方角为最后一脚。如上图所示,STM32L496VGTx引脚序号如蓝色字体及箭头所示标记。STM32的MCU的引脚标记有三种方式:

        一种是基于上面所述的100个序号排序P1、Pn等,这种标记目前一般是在直接操作寄存器地址或使用标准库中使用,目前HLA库、LL库已经不提供直接地址偏移方式的API了,而是给没一组分组提供了独立寄存器来处理该组引脚;

        第二种是标准库或HLA库宏定义了引脚分组,一般为A、B、C、...,按大写字母次序网线,目前支持到16组类别,每组为PX0~PX15,其标记就是分组名+0~15序号,例如GPIOB+GPIO_PIN_3。

        注意,目前STM32的MCU设计是最大16组类别,支持到256引脚(16*16),通过STM32命名规范可以得知,STM32最做引脚支持到256个,但一般会少于引脚数/16的组数,因为不少引脚在芯片设计时就特别支持了其引脚标识,如VSS、VDD等。另外也不是每组必须0~15序号全用完,分组主要是方便设计及使用时进行引脚区分而已。



         目前HLA库主要就是这种方式,例如来看看GPIO读取数据的API(HAL_GPIO_ReadPin)实现,主要就是根据引脚序号(0~15)来读取IDR寄存器内对应数据位的数值。



         GPIO分组引脚地址在HLA宏定义数值如下。
  • #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 */

复制代码


        而GPIO各个分组的IDR寄存器为32bit宽度的数据,目前只用到低16位字段。
  • typedef struct
  • {
  •   __IO uint32_t MODER;       /*!< GPIO port mode register,               Address offset: 0x00      */
  •   __IO uint32_t OTYPER;      /*!< GPIO port output type register,        Address offset: 0x04      */
  •   __IO uint32_t OSPEEDR;     /*!< GPIO port output speed register,       Address offset: 0x08      */
  •   __IO uint32_t PUPDR;       /*!< GPIO port pull-up/pull-down register,  Address offset: 0x0C      */
  •   __IO uint32_t IDR;         /*!< GPIO port input data register,         Address offset: 0x10      */
  •   __IO uint32_t ODR;         /*!< GPIO port output data register,        Address offset: 0x14      */
  •   __IO uint32_t BSRR;        /*!< GPIO port bit set/reset  register,     Address offset: 0x18      */
  •   __IO uint32_t LCKR;        /*!< GPIO port configuration lock register, Address offset: 0x1C      */
  •   __IO uint32_t AFR[2];      /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
  •   __IO uint32_t BRR;         /*!< GPIO Bit Reset register,               Address offset: 0x28      */
  • } GPIO_TypeDef;

复制代码


        第三种引脚标记其实就是在第二种方式基础上,为引脚设置别名,在cubeMX设置引脚mode后,会开启该引脚,在引脚参数设置列表可以设置该引脚标识名,例如下图,设置PE3(P2)为输出模式(GPIO_Output),的别名为LED1。



         那么在生成输出代码时,会通过#define实现标识名替换。


使用特权

评论回复
沙发
两只袜子|  楼主 | 2023-4-6 10:17 | 只看该作者
1.4 GPIO原理框图及CubeMX配置
        进一步,再看看GPIO结构原理图,它支持4种GPIO输入模式(浮空输入、上拉输入、下拉输入、模拟输入)和4种GPIO输出模式(开漏输出、开漏复用输出、推挽输出、推挽复用输出)。因此才能通过多种输入输出模式组合来实现更复杂的其他外设功能。



         在cubeMX中在GPIO配置界面中,可以配置GPIO引脚的GPIO输入模式以及其他参数设置。



         同样地,在cubeMX中在GPIO配置界面中,可以配置GPIO引脚的GPIO输出模式以及其他参数设置。



        另外各种基于GPIO组合实现的外设,也可在GPIO栏的各个外设页面下,单独设置各个引脚的GPIO输入或输出模式。



二、GPIO的HLA源码分析
        2.1 HLA库GPIO初始化

        通过cubeMX生成输出源码,如果配置GPIO引脚功能,则cubeMX生成代码时,设置了为每个外设生成独立.h/.c文件时,会在Core源码目录下的Inc及Src目录,分别生成gpio.h和gpio.c驱动文件,否则将会GPOI相关输出函数放置main.h和main.c中。

        在gpio.c文件中,主要定义了MX_GPIO_Init函数,该主要做三件事情,一是启动各个GPIO分组寄存器的时钟,二是对于输出GPIO引脚实现初始值写入,三是将在cubeMX上配置引脚参数写入到参数缓存区域,并依据缓存参数,调用HLA库的HAL_GPIO_Init来实现真正的初始化设定。如下面代码所示。

  • //main.h
  • /* Private defines -----------------------------------------------------------*/
  • #define LED1_Pin GPIO_PIN_3
  • #define LED1_GPIO_Port GPIOE
  • #define KEY2_Pin GPIO_PIN_10
  • #define KEY2_GPIO_Port GPIOE
  • #define KEY0_Pin GPIO_PIN_11
  • #define KEY0_GPIO_Port GPIOE
  • #define KEY1_Pin GPIO_PIN_14
  • #define KEY1_GPIO_Port GPIOE
  • #define LED2_Pin GPIO_PIN_15
  • #define LED2_GPIO_Port GPIOD
  • #define LED0_Pin GPIO_PIN_6
  • #define LED0_GPIO_Port GPIOB
  • /* USER CODE BEGIN Private defines */
  • //gpio.c
  • void MX_GPIO_Init(void)
  • {
  •   GPIO_InitTypeDef GPIO_InitStruct = {0};
  •   /* GPIO Ports Clock Enable */
  •   __HAL_RCC_GPIOE_CLK_ENABLE();
  •   __HAL_RCC_GPIOC_CLK_ENABLE();
  •   __HAL_RCC_GPIOH_CLK_ENABLE();
  •   __HAL_RCC_GPIOB_CLK_ENABLE();
  •   __HAL_RCC_GPIOD_CLK_ENABLE();
  •   __HAL_RCC_GPIOA_CLK_ENABLE();
  •   /*Configure GPIO pin Output Level */
  •   HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
  •   /*Configure GPIO pin Output Level */
  •   HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
  •   /*Configure GPIO pin Output Level */
  •   HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
  •   /*Configure GPIO pin : PtPin */
  •   GPIO_InitStruct.Pin = LED1_Pin;
  •   GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  •   GPIO_InitStruct.Pull = GPIO_NOPULL;
  •   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  •   HAL_GPIO_Init(LED1_GPIO_Port, &GPIO_InitStruct);
  •   /*Configure GPIO pins : PEPin PEPin PEPin */
  •   GPIO_InitStruct.Pin = KEY2_Pin|KEY0_Pin|KEY1_Pin;
  •   GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  •   GPIO_InitStruct.Pull = GPIO_NOPULL;
  •   HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
  •   /*Configure GPIO pin : PtPin */
  •   GPIO_InitStruct.Pin = LED2_Pin;
  •   GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  •   GPIO_InitStruct.Pull = GPIO_NOPULL;
  •   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  •   HAL_GPIO_Init(LED2_GPIO_Port, &GPIO_InitStruct);
  •   /*Configure GPIO pin : PtPin */
  •   GPIO_InitStruct.Pin = LED0_Pin;
  •   GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  •   GPIO_InitStruct.Pull = GPIO_NOPULL;
  •   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  •   HAL_GPIO_Init(LED0_GPIO_Port, &GPIO_InitStruct);
  • }

复制代码


        在stm32l4xx_hal_gpio.c源文件中定义了HAL_GPIO_Init函数,它做以下事情:1)诊断先前缓存配置参数是否合规,2)再将缓存配置参数写入到GPIO的寄存器内,完成GPIO的初始化设定(GPIO 的Mode、Pull、Analog、Alternate、Direction、EXTI等)。



使用特权

评论回复
板凳
两只袜子|  楼主 | 2023-4-6 10:18 | 只看该作者
  2.2 GPIO的开发应用接口API简述
        除了提供GPIO初始化API外,STM32的HLA针对GPIO还外开发这提供如下API:



         GPIO对数据的读取主要是HAL_GPIO_ReadPin函数,写入主要是HAL_GPIO_WritePin、HAL_GPIO_TogglePin函数,其参数都是GPIO_TypeDef* GPIOx和 uint16_t GPIO_Pin,前者就是前面参数的分组如GPIOA、GPIOB等,后者就是Pin0~Pin15,例如GPIO_PIN_3。

        关于外部中断回调函数HAL_GPIO_EXTI_Callback,该函数是弱函数



        若需要使用该函数,使其生效,首先需要在cubeMX配置界面,将GPIO引脚调整为GPIO_EXTI类型



        并开启该引脚的中断支持



         就可以重新实现stm32l4xx_hal_gpio.c文件中的HAL_GPIO_EXTI_Callback函数来实现GPIO外部中断回调功能,例如设置如下,则实现按键KEY0按下异常,触发回调函数HAL_GPIO_EXTI_Callback调用,从而实现LED0灯状态反转(点亮或熄灭):
  • /* USER CODE BEGIN 0 */
  • void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
  • {
  •         if(GPIO_Pin==KEY0_Pin)
  •         {
  •                 static uint32_t key0_count = 0;
  •                 printf("this is %lu count for HAL_GPIO_EXTI_Callback!\r\n",key0_count++);
  •                 Toggle_led0();//LED0等状态反转一次
  •         }
  • }
  • /* USER CODE END 0 */

复制代码


         同样地对于外部中断请求函数HAL_GPIO_EXTI_IRQHandler也可以类似地进行调整实现GPIO外部中断触发器调用,从而实现我们需要的业务应用。

        GPIO的HLA库中,HAL_GPIO_LockPin函数,主要是操作lock寄存器数值的,lock寄存器会针对GPIOx_MODER, GPIOx_OTYPER, GPIOx_OSPEEDR等GPIO寄存器配置对应的状态标记,而其他寄存器操作时,会先去lock寄存器读取其使用状态来确定是否能进行相关操作,从而确保防止冲突及达成一致性。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

2055

主题

7458

帖子

10

粉丝