• STM32实战总结:HAL之GPIO


    GPIO概述

    一、先认真阅读参考手册中GPIO部分内容

    GPIO部分大概有25页,关键内容记录如下:

    1、每个GPI/O端口有两个32位配置寄存器(GPIOx_CRLGPIOx_CRH),两个32位数据寄存器 (GPIOx_IDR和GPIOx_ODR),一个32位置位/复位寄存器(GPIOx_BSRR),一个16位复位寄存器(GPIOx_BRR)和一个32位锁定寄存器(GPIOx_LCKR)

    2、端口可配置四种输入和四种输出:

    输入浮空

    输入上拉

    输入下拉

    模拟输入

    开漏输出

    推挽式输出

    推挽式复用功能

    开漏复用功能

    GPIO八种模式的区别与应用

    参考:单片机的GPIO端口的8中工作模式 - 知乎

    注意事项:

    01

    在IO口外接按键的情况下,如果按键未按下时是低电平,按下后是高电平,那么,单片机引脚在初始化时,最好设置成下拉,以保证单片机引脚处于按键未触发时的低电平,如果不设置成下拉,那么单片机引脚就会处于悬空状态,导致引脚的电平状态不定,从而影响引脚的功能实现;

    02 

    模拟输入一般只有在ADC引脚时才会使用;

    03

    配置成输出时,为什么还要配置上拉和下拉?

    其实,在思考这个上拉和下拉不必非得跟输入关联到一起,不管是输入还是输出,都有个空闲态,比如输入时按键未按下,或者输出时LED不亮,都有个空闲的状态,我们根据需要设置对应的上下拉即可。

    这里附一张M4的GPIO原理图,就将上下拉移到了外面,让输入和输出通道都能用

    04

    配置成输出时,输入功能可以同时使用。即具有双向驱动口的功能。

    具体参考:GPIO做输出还能作外部中断输入吗? - 知乎

    不论GPIO通用输出还是复用输出,外部管脚的电平都可以连接到内部输入单元,管脚上的电平也可以被内部边沿检测器检测到。也就是说,当它被配置为输出时是具备双向特性的。

    当然,一般来讲,如果希望GPIO做为双向驱动口使用时,建议将其配置为OD开漏结合上拉模式。比方在做I2C应用时,将通信GPIO端口配置为开漏模式结合上拉电阻即可进行双向数据通信,无须对通信口的GPIO模式来回切换。

    注意,开漏输出无法直接输出高电平,所以想输出1时,需要设置成上拉,以提供高电平。 

    3、端口位输出表:

    4、必须以字(32)的方式操作这些外设寄存器。

    ……

    更多内容详见参考手册。

    二、GPIO有哪些变量和函数?

    ……

    更多内容详见源代码。

    三、学会使用这些函数

    1. static void Run(void)
    2. {
    3.     HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
    4.     HAL_Delay(100);
    5. }  

    以上代码可以实现LED1的闪烁。

    注意这里的名字中的LED1和MX中的标签名是一致的。

    GPIO复用和重映射

    STM32上有很多I/O口,也有很多的内置外设如I2C、ADC、ISP、USART等,为了节省引出管脚,这些内置外设基本上是与I/O口共用管脚的,也就是I/O管脚的复用功能。

    很多复用的I/O引脚可以通过重映射功能从其他的I/O管脚引出,即复用功能的引脚是可通过程序改变的。具体查看技术手册。

    数据手册上有说明:

    复位后主功能是GPIO,只有启用了相应的外设功能,才会成为相对应的复用功能。

    阅读GPIO相关源码

    从main函数开始。。。

    一开始就是HAL初始化HAL_Init();这个函数是干什么的?在哪定义的?在哪声明的?

    注意:在c中,基本上都是c和h文件成双成对的,头文件是在c文件前面被展开的。所以,需要配合头文件和c文件才完整。要看就两个一起看。

    main.h

    在该头文件中,包含了一个头文件stm32f1xx_hal.h,这个头文件看起来像是总的头文件,因为它的命名中,只有32和hal,没有其他任何外设的信息。

    有错误处理函数的声明,另外有个void SystemClock_Config(void);是main的私有函数,所以就没有声明在头文件中,不过,该私有函数并没有加上static来限制其私有性。不加也可以,只要没有声明出去,别的地方调用时就会报错,但是此时可以选择声明出去。而一旦加上了static,就无法声明出去了,一旦声明出去就会报错。

    该头文件中的这段代码关注一下,这是main.h中最关键的代码:

    1. /* Private defines -----------------------------------------------------------*/
    2. #define LED1_Pin GPIO_PIN_4
    3. #define LED1_GPIO_Port GPIOE
    4. #define LED2_Pin GPIO_PIN_5
    5. #define LED2_GPIO_Port GPIOE
    6. #define LED3_Pin GPIO_PIN_6
    7. #define LED3_GPIO_Port GPIOE

    这里对应的是MX中配置引脚时的标签名,本来我们还要去查看原理图去查看配置的到底是哪个端口,这个端口的哪个引脚,但是,这里自动生成了相应的宏定义,通过这种定义别名的方式,让我们直接面向“对象”编程。用LED1/LED2/LED3相关符号就可以了。

    上面宏定义中的GPIO_PIN_4和GPIOE这些名称是啥意思?打开定义和声明查看。

    首先,看到这些名称都是大写,猜想可能是一个宏定义。

    先打开GPIO_PIN_4的声明/定义(对于宏定义来说,打开声明或者定义都是同一个地方)

    跳转到了stm32f1xx_hal_gpio.h文件。

    里面对各个引脚进行了编号。

    用的是个16位的二进制数,通过最低位到高位依次赋予高电平来选择。

    uint16_t是什么意思?

    再次跳转定义。

    发现无法跳转。说明其既不是宏定义,也不是变量,而是一种c语言的语法。

    其实,我知道这是单片机中自定义的一种类型,也就是unsigned short,只不过进行了类型重定义,以简化使用。

    怎么找到其源头?

    既然能使用,那么肯定有地方进行了类型重定义,要么就在文件上面,要么就在包含的头文件中。

    上面没有,那就只能在头文件中。其包含了一个头文件stm32f1xx_hal_def.h

    1. ******************************************************************************
    2. * @file stm32f1xx_hal_def.h
    3. * @author MCD Application Team
    4. * @brief This file contains HAL common defines, enumeration, macros and
    5. * structures definitions.
    6. ******************************************************************************

    可知该头文件包含了HAL共用的定义、枚举、宏定义以及结构体定义。

    但在该头文件中,依然没有找到uint16_t,那就只能继续找其包含的头文件stm32f1xx.h,看名字像一个最顶层的头文件。继续找,还是没找到。

    还是没找到,奇了怪了。。。。。。。。。。。。。。。。。。。。。。。

    那就只能直接搜索了。搜索太多了,不好找。

    无意中发现,uint32_t右键能够跳转。。。。跳到了stdint.h文件。

    1. ……
    2. /* exact-width unsigned integer types */
    3. typedef unsigned char uint8_t;
    4. typedef unsigned short int uint16_t;
    5. typedef unsigned int uint32_t;
    6. typedef unsigned __INT64 uint64_t;
    7. ……

    stdint.h是c99中引进的一个标准C库的头文件,里面定义了一些整数类型,具体参考:关于stdint.h头文件_willorfang的博客-CSDN博客

    继续看GPIOE是啥意思?

    跳转,打开了stm32f103xe.h,这个就跟具体型号有关了,具体到了103这一款。

    看其描述:

    1. /** @addtogroup Peripheral_declaration
    2. * @{
    3. */

    这好像是所有外设的什么声明,具体看:

    以GPIOE为例

    #define GPIOE               ((GPIO_TypeDef *)GPIOE_BASE)

    该宏定义替换后,是后面的内容,看起来像是把一个宏定义内容进行强制类型转换,那么,被转换的是什么呢?又是转换成了什么类型呢?最终的效果又是什么呢?

    在该文件上面,找到了宏定义:

    #define GPIOE_BASE            (APB2PERIPH_BASE + 0x00001800UL)

    可知,被转换的好像是个地址,一个基地址再加上一个数,继续查找基地址:

    #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000UL)

    继续查找PERIPH_BASE

    #define PERIPH_BASE           0x40000000UL /*!< Peripheral base address in the alias region */

    涉及到基地址加上一个数,联想到基地址加偏移量,又提到了别名区,难道是位带操作?

    具体查找数据手册,看内存映射图中外设起始地址是哪个。

    或者查看参考手册中,存储器映像表,看起始地址。

    0x4000 0000 - 0x4000 03FF      TIM2定时器

    可知,起始地址为:0x4000 0000

    上面的地址经过相加,可得出为0x4001 1800,查阅手册得知,正好是端口E的地址。0x4001 1800 - 0x4001 1BFF      GPIO端口E

    由此可知,GPIOE表示的就是端口E的地址所对应的值。

    此时,只是一个值,被转成了一个指针。

    那么,又转换成了什么类型的指针呢?

    那就要看,GPIO_TypeDef,是怎么定义的?

    1. typedef struct
    2. {
    3. __IO uint32_t CRL;
    4. __IO uint32_t CRH;
    5. __IO uint32_t IDR;
    6. __IO uint32_t ODR;
    7. __IO uint32_t BSRR;
    8. __IO uint32_t BRR;
    9. __IO uint32_t LCKR;
    10. } GPIO_TypeDef;

    这是一个结构体,定义了GPIO每个端口的寄存器。

    因为,结构体的指针,指向的是首元素的首地址,所以,上面的强制转换的含义就是,指明端口E每一个寄存器的地址。

    搞了这么多,就是定义了端口E各个寄存器的地址。虽然略显复杂,但是实现了标准化。

    所以,GPIOE啥意思?就是端口E对应寄存器结构体的地址。所以对端口E的操作都基于GPIOE,其他端口,甚至所有外设,同理。

    再回过头看看stm32f103xe.h的功能说明:

    1. * @file stm32f103xe.h
    2. * @author MCD Application Team
    3. * @brief CMSIS Cortex-M3 Device Peripheral Access Layer Header File.
    4. * This file contains all the peripheral register's definitions, bits
    5. * definitions and memory mapping for STM32F1xx devices.
    6. *
    7. * This file contains:
    8. * - Data structures and the address mapping for all peripherals
    9. * - Peripheral's registers declarations and bits definition
    10. * - Macros to access peripheral抯 registers hardware

    HAL_Init();

    看名称就知道是HAL库的初始化,那么HAL库的初始化要做哪些事情呢?

    先看声明:

    跳转打开了stm32f1xx_hal.h

    1. /* Initialization and de-initialization functions */
    2. HAL_StatusTypeDef HAL_Init(void);
    3. HAL_StatusTypeDef HAL_DeInit(void);
    4. ……

    可知,其返回一个什么,可查看到返回的是一个状态码,该状态码是个枚举类型,定义在头文件stm32f1xx_hal_def.h中,

    1. /**
    2. * @brief HAL Status structures definition
    3. */
    4. typedef enum
    5. {
    6. HAL_OK = 0x00U,
    7. HAL_ERROR = 0x01U,
    8. HAL_BUSY = 0x02U,
    9. HAL_TIMEOUT = 0x03U
    10. } HAL_StatusTypeDef;

    再跳转到对应c中去看定义:

    1. HAL_StatusTypeDef HAL_Init(void)
    2. {
    3. /* Configure Flash prefetch */
    4. #if (PREFETCH_ENABLE != 0)
    5. #if defined(STM32F101x6) || defined(STM32F101xB) || defined(STM32F101xE) || defined(STM32F101xG) || \
    6. defined(STM32F102x6) || defined(STM32F102xB) || \
    7. defined(STM32F103x6) || defined(STM32F103xB) || defined(STM32F103xE) || defined(STM32F103xG) || \
    8. defined(STM32F105xC) || defined(STM32F107xC)
    9. /* Prefetch buffer is not available on value line devices */
    10. __HAL_FLASH_PREFETCH_BUFFER_ENABLE();
    11. #endif
    12. #endif /* PREFETCH_ENABLE */
    13. /* Set Interrupt Group Priority */
    14. HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
    15. /* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */
    16. HAL_InitTick(TICK_INT_PRIORITY);
    17. /* Init the low level hardware */
    18. HAL_MspInit();
    19. /* Return function status */
    20. return HAL_OK;
    21. }

    根据英文注释不难看出来每个代码是干嘛的。

    一直往下追踪,可以发现,其实底层都是对相关寄存器进行操作。

    main.c中的后续初始化等代码类似,不再赘述。 

    GPIO模块化编程

    单片机的封装通常都是分层的。

    怎么说呢?

    首先,底层针对寄存器的读写时序是第一层;

    再往上第二层,就是针对特定硬件的一些基本功能,比如读和写;

    再往上就是第三层,可能是面向对象层,实现特定硬件的具体功能;

    再往上,就是业务层,通过硬件的具体功能来实现不同的业务需求。

    ……

    现在,有三个LED灯,对其进行模块化编程。

    首先,HAL提供了写引脚、转换引脚电平、读引脚电平等功能。

    我们进一步封装,实现该LED灯的具体功能,有打开、关闭以及转换开关状态(此时不用关注下一层的细节问题,面向的是具体的LED灯)

    思路如下:

    为LED外设单独创建一个文件;

    创建LED.c和LED.h,放到MyApplications中;

    在myapplication.h中添加对应的头文件;

    要实现哪些功能?对应的要提供什么函数,什么变量?将这些变量封装成一个结构体。

    1. #ifndef _LED_H_
    2. #define _LED_H_
    3. #include "stdint.h"
    4. //确定要实现的led功能
    5. typedef struct
    6. {
    7. //点亮
    8. void (*led_light)(uint8_t);
    9. //熄灭
    10. void (*led_extinguish)(uint8_t);
    11. //转换亮灭
    12. void (*led_switch)(uint8_t);
    13. } led_funtcions;
    14. //有三个LED灯,定义成枚举,并编号
    15. typedef enum
    16. {
    17. LED1 = 1u, LED2, LED3
    18. } led_status;
    19. //将结构体声明出去
    20. extern led_funtcions led_operater;
    21. #endif

    接着,在对应的c中,定义一个结构体全局变量(相应的在头文件中要声明出去),这个变量就作为一个对接人,也就是该文件的一个对象。之后,依次实现其中所定义的函数,并将函数赋值给结构体。同时注意,将所有函数设置成当前文件可见的,即加上static。我们不会对外暴露任何函数,要想访问函数,必须通过结构体变量去访问元素的形式。

    1. #include "myapplication.h"
    2. static void LedLight(uint8_t lednum);
    3. static void LedExtinguish(uint8_t lednum);
    4. static void LedSwitch(uint8_t lednum);
    5. static void Led1Blink(void);
    6. led_funtcions led_operater =
    7. {
    8. LedLight,
    9. LedExtinguish,
    10. LedSwitch
    11. };
    12. static void LedLight(uint8_t lednum)
    13. {
    14. switch(lednum)
    15. {
    16. case LED1 :
    17. HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
    18. break;
    19. case LED2 :
    20. HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET);
    21. break;
    22. case LED3 :
    23. HAL_GPIO_WritePin(LED3_GPIO_Port, LED3_Pin, GPIO_PIN_SET);
    24. break;
    25. default :
    26. Led1Blink();
    27. }
    28. }
    29. static void LedExtinguish(uint8_t lednum)
    30. {
    31. switch(lednum)
    32. {
    33. case LED1 :
    34. HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
    35. break;
    36. case LED2 :
    37. HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
    38. break;
    39. case LED3 :
    40. HAL_GPIO_WritePin(LED3_GPIO_Port, LED3_Pin, GPIO_PIN_RESET);
    41. break;
    42. default :
    43. Led1Blink();
    44. }
    45. }
    46. static void LedSwitch(uint8_t lednum)
    47. {
    48. switch(lednum)
    49. {
    50. case LED1 :
    51. HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
    52. break;
    53. case LED2 :
    54. HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
    55. break;
    56. case LED3 :
    57. HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
    58. break;
    59. default :
    60. Led1Blink();
    61. }
    62. }
    63. //如果输入的不是LED1/LED2/LED3则LED1闪烁
    64. static void Led1Blink(void)
    65. {
    66. HAL_Delay(100);
    67. HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
    68. }

    通过结构体变量去访问:

    1. static void Run(void)
    2. {
    3. HAL_Delay(500);
    4. led_operater.led_light(LED1);
    5. led_operater.led_light(LED2);
    6. led_operater.led_light(LED3);
    7. HAL_Delay(500);
    8. led_operater.led_extinguish(LED1);
    9. led_operater.led_extinguish(LED2);
    10. led_operater.led_extinguish(LED3);
    11. HAL_Delay(500);
    12. led_operater.led_switch(LED1);
    13. HAL_Delay(500);
    14. led_operater.led_switch(LED2);
    15. HAL_Delay(500);
    16. led_operater.led_switch(LED3);
    17. HAL_Delay(500);
    18. led_operater.led_extinguish(LED3);
    19. HAL_Delay(500);
    20. led_operater.led_extinguish(LED2);
    21. HAL_Delay(500);
    22. led_operater.led_extinguish(LED1);
    23. }

    在已有的框架下,我们不用去动任何main函数里的内容。只用处理好这里的Run函数和相应外设即可,便于移植和维护。

    状态机

    我们所说的状态机是有限状态机,是一种思想,把复杂的控制逻辑分解成有限个稳定状态,组成闭环系统,通过事件触发,让状态机按设定的顺序处理事务。

    如果不用状态机思想,那么我们要想根据条件来做出不同的应对,最直接的就是在while里面疯狂使用if来判断。(不管什么时候,在程序里使用过多的if语句,都不是优先选项,会让程序易出错,且可读性很差)

    单片机C语言的状态机编程,是利用条件选择语句(switch-case)切换状态,通过函数内部指令改变状态机状态,让程序按照设定的顺序执行。

    举例说明,一个简单状态机:

    同理,先创建两个文件,即stamachine.c和stamachine.h。

    有五个状态,对应一个枚举;

    每种状态对应一个函数执行;

    需要有一个变量用于状态切换。

    stamachine.h

    1. #ifndef _STAMACHINE_H_
    2. #define _STAMACHINE_H_
    3. #include "stdint.h"
    4. //5种状态
    5. typedef enum
    6. {
    7. STA1 = 1u,
    8. STA2,
    9. STA3,
    10. STA4,
    11. STA5,
    12. } machineState;
    13. //对应的函数封装
    14. typedef struct
    15. {
    16. machineState stateLocation;
    17. void (*sta1Func)(void);
    18. void (*sta2Func)(void);
    19. void (*sta3Func)(void);
    20. void (*sta4Func)(void);
    21. void (*sta5Func)(void);
    22. } state_machine;
    23. //将结构体声明出去
    24. extern state_machine state_machiner;
    25. #endif

    stamachine.c

    1. #include "myapplication.h"
    2. static void Sta1Func(void);
    3. static void Sta2Func(void);
    4. static void Sta3Func(void);
    5. static void Sta4Func(void);
    6. static void Sta5Func(void);
    7. state_machine state_machiner =
    8. {
    9. STA1,
    10. Sta1Func,
    11. Sta2Func,
    12. Sta3Func,
    13. Sta4Func,
    14. Sta5Func,
    15. };
    16. static void Sta1Func(void)
    17. {
    18. HAL_Delay(500);
    19. led_operater.led_extinguish(LED1);
    20. led_operater.led_extinguish(LED2);
    21. led_operater.led_extinguish(LED3);
    22. state_machiner.stateLocation = STA2;
    23. }
    24. static void Sta2Func(void)
    25. {
    26. HAL_Delay(500);
    27. led_operater.led_light(LED1);
    28. HAL_Delay(500);
    29. led_operater.led_extinguish(LED1);
    30. state_machiner.stateLocation = STA3;
    31. }
    32. static void Sta3Func(void)
    33. {
    34. HAL_Delay(500);
    35. led_operater.led_light(LED2);
    36. HAL_Delay(500);
    37. led_operater.led_extinguish(LED2);
    38. state_machiner.stateLocation = STA4;
    39. }
    40. static void Sta4Func(void)
    41. {
    42. HAL_Delay(500);
    43. led_operater.led_light(LED3);
    44. HAL_Delay(500);
    45. led_operater.led_extinguish(LED3);
    46. state_machiner.stateLocation = STA5;
    47. }
    48. static void Sta5Func(void)
    49. {
    50. HAL_Delay(500);
    51. led_operater.led_light(LED1);
    52. led_operater.led_light(LED2);
    53. led_operater.led_light(LED3);
    54. state_machiner.stateLocation = STA1;
    55. }

    状态机执行:

    1. static void Run(void)
    2. {
    3. switch(state_machiner.stateLocation)
    4. {
    5. case STA1 :
    6. state_machiner.sta1Func();
    7. break;
    8. case STA2 :
    9. state_machiner.sta2Func();
    10. break;
    11. case STA3 :
    12. state_machiner.sta3Func();
    13. break;
    14. case STA4 :
    15. state_machiner.sta4Func();
    16. break;
    17. case STA5 :
    18. state_machiner.sta5Func();
    19. break;
    20. default :
    21. state_machiner.stateLocation = STA1;
    22. }
    23. }

    还是不用动主函数。添加新文件即可。

    提示:如果状态过多过于繁杂,可以直接上操作系统比如freertos



    重点补充

    对于默认使用的HSI,虽然不怎么精准,但是如果只是做一些简单的控制,不需要进行通信,那么用HSI也没什么影响。

    CubeMX中这里应该是被用户自定义修改过的意思。

    补充 

    GPIO每个引脚都有一个位来定义,可用于选中

  • 相关阅读:
    VScode使用M5stack-c plus基于arduino-环境搭建
    在 macOS 上使用 Homebrew 安装和配置 Python 及 Tk 库
    【深度学习21天学习挑战赛】4、初尝循环神经网络(RNN)——股票预测
    一文读懂为什么需要跨链?跨链是什么?跨链实现技术?
    文本特征处理——N-Gram、长度规范及数据增强
    Python语言:经典例题分析讲解
    Java:如何去优雅地优化接口
    JS的正则表达式
    pip安装依赖报错
    android Compose Text文本
  • 原文地址:https://blog.csdn.net/qq_28576837/article/details/126593128