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对引脚进行初始化配置:
    1. 先点击CubeMX左侧栏System Core的GPIO,打开对应窗口
    2. 在右侧的Pinout view中可以图形化地看到当前的引脚配置,单击所要配置的引脚,在弹出菜单中选择GPIO_output,可将引脚功能选择为输出模式
    3. 再在中间窗口的引脚列表中单击要配置引脚所对应的一行,在下方Configuration中将输出电平GPIO output level修改为high
    4. 如此配置后生成的工程文件的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);
    

posted on 2025-05-08 19:00  无术师  阅读(216)  评论(0)    收藏  举报