ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

《STM32 HAL 库源码解析(一):GPIO 初始化 - 位操作宏与 MODIFY_REG 底层原理》

《STM32 HAL 库源码解析(一):GPIO 初始化 - 位操作宏与 MODIFY_REG 底层原理》 引言在 STM32 嵌入式开发中绝大多数开发者日常都是基于 HAL 库完成外设配置。大部分人熟练调用HAL_GPIO_Init、填充配置结构体即可完成引脚初始化但往往并不了解其底层执行逻辑将 HAL 库当作 “黑盒工具” 使用。实际上HAL 库并非独立封装的高级逻辑其本质是对寄存器读写、位运算操作、硬件配置逻辑的标准化宏封装。所有上层结构体配置最终都会落地为最基础的寄存器位操作。修改后在实际开发与面试当中很多开发者只会调用上层 API却答不出这些底层核心问题为什么多比特寄存器域不能直接按位或赋值MODIFY_REG为什么要先清零再赋值SET_BIT与MODIFY_REG二者的使用场景有什么区别本系列文章将以STM32F103ZET6作为目标芯片逐层拆解 HAL 库源码。本文是系列第一篇以最基础的 GPIO 外设作为切入点重点剖析 HAL 库核心位操作宏与 GPIO 初始化底层实现讲清单比特置位 / 清零和多比特域配置之间的底层差异以及背后的设计逻辑。后续将持续更新 RCC、UART、TIM、中断等外设源码解析搭建完整的 STM32 底层知识体系。一.GPIO的寄存器想要理解 GPIO 的工作原理核心要抓住一点所有对 GPIO 引脚的读写与配置本质上都是对 GPIO 外设内部寄存器执行读写操作。因此在正式阅读 HAL 库源码之前我们需要先梳理 GPIO 相关寄存器搞清楚每一个寄存器的功能以及如何通过配置寄存器位来实现对应的引脚功能。查阅 ST 官方参考手册可以得知每个 GPIO 端口包含如下寄存器我们将结合这份参考手册逐个拆解各个寄存器的功能与配置方法。(一).GPIOx_CRL(GPIOx_CRH)GPIOx_CRL和 GPIOx_CRH是一组配套的端口配置寄存器用于配置 GPIO 端口 0~15 号引脚的输入、输出相关工作模式。其中GPIOx_CRL管理 Pin0~Pin7GPIOx_CRH管理 Pin8~Pin15。两个寄存器均为 32 位每 4 个比特位控制一个引脚。这 4 位分为 CNF 配置位、MODE 模式位用于指定引脚的电路结构以及输出速度。(二).GPIOx_IDR(GPIOx_ODR)GPIOx_IDR和GPIOx_ODR是一组配套的数据寄存器用于读取与控制 GPIO 端口 0~15 号引脚的电平状态。其中GPIOx_IDR用于读取引脚外部输入电平GPIOx_ODR用于存放软件设定的输出电平。两个寄存器均为 32 位每 1 个比特位对应一个引脚该位电平反映引脚输入状态或是软件设定的输出值。(三).GPIO_BSRRGPIOx_BSRR是置位 / 复位寄存器可以原子化完成 GPIO0‑15 引脚的置 1、清 0 操作。该寄存器为 32 位低 16 位BS[15:0]用来把对应引脚置高高 16 位BR[15:0]用来把对应引脚拉低。向对应 bit 写 1 代表执行对应动作写 0 不会产生任何效果借此可以修改单个引脚电平而不干扰其余引脚。二.GPIO源码追踪本文着重讲解GPIO初始化的HAL库源码(一).HAL_GPIO_Init 整体框架预览void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init) { uint32_t position 0x00u; uint32_t ioposition; uint32_t iocurrent; uint32_t temp; uint32_t config 0x00u; __IO uint32_t *configregister; /* Store the address of CRL or CRH register based on pin number */ uint32_t registeroffset; /* offset used during computation of CNF and MODE bits placement inside CRL or CRH register */ /* Check the parameters */ assert_param(IS_GPIO_ALL_INSTANCE(GPIOx)); assert_param(IS_GPIO_PIN(GPIO_Init-Pin)); assert_param(IS_GPIO_MODE(GPIO_Init-Mode)); /* Configure the port pins */ while (((GPIO_Init-Pin) position) ! 0x00u) { /* Get the IO position */ ioposition (0x01uL position); /* Get the current IO position */ iocurrent (uint32_t)(GPIO_Init-Pin) ioposition; if (iocurrent ioposition) { /* Check the Alternate function parameters */ assert_param(IS_GPIO_AF_INSTANCE(GPIOx)); /* Based on the required mode, filling config variable with MODEy[1:0] and CNFy[3:2] corresponding bits */ switch (GPIO_Init-Mode) { /* If we are configuring the pin in OUTPUT push-pull mode */ case GPIO_MODE_OUTPUT_PP: /* Check the GPIO speed parameter */ assert_param(IS_GPIO_SPEED(GPIO_Init-Speed)); config GPIO_Init-Speed GPIO_CR_CNF_GP_OUTPUT_PP; break; /* If we are configuring the pin in OUTPUT open-drain mode */ case GPIO_MODE_OUTPUT_OD: /* Check the GPIO speed parameter */ assert_param(IS_GPIO_SPEED(GPIO_Init-Speed)); config GPIO_Init-Speed GPIO_CR_CNF_GP_OUTPUT_OD; break; /* If we are configuring the pin in ALTERNATE FUNCTION push-pull mode */ case GPIO_MODE_AF_PP: /* Check the GPIO speed parameter */ assert_param(IS_GPIO_SPEED(GPIO_Init-Speed)); config GPIO_Init-Speed GPIO_CR_CNF_AF_OUTPUT_PP; break; /* If we are configuring the pin in ALTERNATE FUNCTION open-drain mode */ case GPIO_MODE_AF_OD: /* Check the GPIO speed parameter */ assert_param(IS_GPIO_SPEED(GPIO_Init-Speed)); config GPIO_Init-Speed GPIO_CR_CNF_AF_OUTPUT_OD; break; /* If we are configuring the pin in INPUT (also applicable to EVENT and IT mode) */ case GPIO_MODE_INPUT: case GPIO_MODE_IT_RISING: case GPIO_MODE_IT_FALLING: case GPIO_MODE_IT_RISING_FALLING: case GPIO_MODE_EVT_RISING: case GPIO_MODE_EVT_FALLING: case GPIO_MODE_EVT_RISING_FALLING: /* Check the GPIO pull parameter */ assert_param(IS_GPIO_PULL(GPIO_Init-Pull)); if (GPIO_Init-Pull GPIO_NOPULL) { config GPIO_CR_MODE_INPUT GPIO_CR_CNF_INPUT_FLOATING; } else if (GPIO_Init-Pull GPIO_PULLUP) { config GPIO_CR_MODE_INPUT GPIO_CR_CNF_INPUT_PU_PD; /* Set the corresponding ODR bit */ GPIOx-BSRR ioposition; } else /* GPIO_PULLDOWN */ { config GPIO_CR_MODE_INPUT GPIO_CR_CNF_INPUT_PU_PD; /* Reset the corresponding ODR bit */ GPIOx-BRR ioposition; } break; /* If we are configuring the pin in INPUT analog mode */ case GPIO_MODE_ANALOG: config GPIO_CR_MODE_INPUT GPIO_CR_CNF_ANALOG; break; /* Parameters are checked with assert_param */ default: break; } /* Check if the current bit belongs to first half or last half of the pin count number in order to address CRH or CRL register*/ configregister (iocurrent GPIO_PIN_8) ? GPIOx-CRL : GPIOx-CRH; registeroffset (iocurrent GPIO_PIN_8) ? (position 2u) : ((position - 8u) 2u); /* Apply the new configuration of the pin to the register */ MODIFY_REG((*configregister), ((GPIO_CRL_MODE0 | GPIO_CRL_CNF0) registeroffset), (config registeroffset)); /*--------------------- EXTI Mode Configuration ------------------------*/ /* Configure the External Interrupt or event for the current IO */ if ((GPIO_Init-Mode EXTI_MODE) EXTI_MODE) { /* Enable AFIO Clock */ __HAL_RCC_AFIO_CLK_ENABLE(); temp AFIO-EXTICR[position 2u]; CLEAR_BIT(temp, (0x0Fu) (4u * (position 0x03u))); SET_BIT(temp, (GPIO_GET_INDEX(GPIOx)) (4u * (position 0x03u))); AFIO-EXTICR[position 2u] temp; /* Enable or disable the rising trigger */ if ((GPIO_Init-Mode RISING_EDGE) RISING_EDGE) { SET_BIT(EXTI-RTSR, iocurrent); } else { CLEAR_BIT(EXTI-RTSR, iocurrent); } /* Enable or disable the falling trigger */ if ((GPIO_Init-Mode FALLING_EDGE) FALLING_EDGE) { SET_BIT(EXTI-FTSR, iocurrent); } else { CLEAR_BIT(EXTI-FTSR, iocurrent); } /* Configure the event mask */ if ((GPIO_Init-Mode GPIO_MODE_EVT) GPIO_MODE_EVT) { SET_BIT(EXTI-EMR, iocurrent); } else { CLEAR_BIT(EXTI-EMR, iocurrent); } /* Configure the interrupt mask */ if ((GPIO_Init-Mode GPIO_MODE_IT) GPIO_MODE_IT) { SET_BIT(EXTI-IMR, iocurrent); } else { CLEAR_BIT(EXTI-IMR, iocurrent); } } } position; } }(二).while 循环解析多引脚掩码while (((GPIO_Init-Pin) position) ! 0x00u) { ioposition (0x01uL position); iocurrent (uint32_t)(GPIO_Init-Pin) ioposition; if (iocurrent ioposition) { // 当前position对应的引脚需要初始化 } position; }1.循环条件while 的判断条件while (((GPIO_Init-Pin) position) ! 0x00u)position将引脚掩码整体右移 position 位。移位后结果不为 0说明掩码中还有未遍历的引脚继续循环当所有 bit 全部被移出结果为 0循环结束。2.变量含义ioposition 0x01uL position生成一个只有第 position 位是 1 的掩码iocurrent GPIO_Init-Pin ioposition通过按位与运算判断掩码的第 position 位是否为 1。如果该位为 1运算结果等于ioposition进入 if 分支执行引脚初始化如果该位为 0运算结果等于 0跳过 if不对该引脚做配置。(三).switch 分支组装 4bit 配置 config这块的代码逻辑比较类似我们就以通用推挽输出来举例说明case GPIO_MODE_OUTPUT_PP: assert_param(IS_GPIO_SPEED(GPIO_Init-Speed)); config GPIO_Init-Speed GPIO_CR_CNF_GP_OUTPUT_PP; break;config是一个 32 位变量但只使用最低 4bit用来存放当前引脚的 CNF 与 MODE 配置。回顾第一节对 CRL、CRH 寄存器的介绍可以知道这 4bit 当中高 2 位为 CNF 配置位低 2 位为 MODE 配置位具体含义可以参考芯片手册表格。我们查看源码不难发现每一个输入输出模式都有其对应的宏值。/* Definitions for bit manipulation of CRL and CRH register */ #define GPIO_CR_MODE_INPUT 0x00000000u /*! 00: Input mode (reset state) */ #define GPIO_CR_CNF_ANALOG 0x00000000u /*! 00: Analog mode */ #define GPIO_CR_CNF_INPUT_FLOATING 0x00000004u /*! 01: Floating input (reset state) */ #define GPIO_CR_CNF_INPUT_PU_PD 0x00000008u /*! 10: Input with pull-up / pull-down */ #define GPIO_CR_CNF_GP_OUTPUT_PP 0x00000000u /*! 00: General purpose output push-pull */ #define GPIO_CR_CNF_GP_OUTPUT_OD 0x00000004u /*! 01: General purpose output Open-drain */ #define GPIO_CR_CNF_AF_OUTPUT_PP 0x00000008u /*! 10: Alternate function output Push-pull */ #define GPIO_CR_CNF_AF_OUTPUT_OD 0x0000000Cu /*! 11: Alternate function output Open-drain */而对应的高中低三种输出速度和上下拉等输入模式也都有做对应的宏值。我们按照前文所说用通用输出推挽来举例说明#define GPIO_CR_CNF_GP_OUTPUT_PP 0x00000000u /*! 00: General purpose output push-pull */ #define GPIO_CRL_MODE0_1 (0x2UL GPIO_CRL_MODE0_Pos) /*! 0x00000002 */ #define GPIO_CRL_MODE0_Pos (0U) config GPIO_Init-Speed GPIO_CR_CNF_GP_OUTPUT_PP;代入数值config 0x01 0x00 0x0001其二进制最低 4bit 为00 01。高 2bit00CNF 段通用推挽输出低 2bit01MODE 段10MHz 输出速度查看手册后发现0001的4位bit所代表的config变量正是配置通用推挽输出模式最大速度2MHz。⚠️ 这里有一个很多人会疑惑的细节源码使用加法而不是嵌入式代码常见的按位或|。 原因CNF 宏占用 bit2、bit3SpeedMODE宏只占用 bit0、bit1两段 bit 完全不重叠。 当 bit 域没有重叠时和|的运算结果完全一致。ST 这里只是源码编码风格换成按位或不会改变运行效果。(四).config配置信息的写入这一行代码很长我们想要理解这一行代码究竟在做什么底层是怎么运转的需要我们进行详细的拆解这也是本文的重中之重。1.configregister和registeroffsetiocurrent是当前需要初始化的引脚前文有提到忘记的可以自行查找而configregister和registeroffset则是寄存器地址指针和bit偏移量。先看configregister这里使用三目运算符将当前引脚与GPIO_PIN_8做对比引脚 0‑7 选择 CRL 寄存器引脚 8‑15 则选择 CRH 寄存器。这样设计的原因前面寄存器章节已经介绍过CRL、CRH 分别负责 0‑7、8‑15 引脚下面继续结合参考手册说明。可以看到我们的CRL和CRH的每4位位一个I/O端口的配置信息分别对应0-7和8-15引脚这样我们就明白前面的代码为什么要判断当前引脚是否小于PIN_8了。选择完寄存器后我们来看第二行代码它同样是先判断当前引脚属于哪个范围不同的是后面的内容。position是当前引脚的编号位运算大家很清楚左移1位在数值上就相当于乘以1个2如果我们写成二进制就相当于在最低位加了一个0。举个例子当前为 1 号引脚position 1执行position 2等价于1 * 4 4代表该引脚在 CRL 寄存器内从 bit4 开始连续占用 4 个 bitbit4 正是 1 号引脚配置信息的起始位。如果是 8‑15 号引脚则需要先把引脚编号减去 8再执行左移从而在 CRH 寄存器中得到和 CRL 逻辑完全一致的 bit 偏移。至此寄存器选择、bit 偏移的逻辑已经讲完接下来重点讲解如何把组装完成的config配置写入 CRL/CRH 寄存器。2.GPIO_CRL_MODE0 | GPIO_CRL_CNF0根据寄存器位定义可以看到GPIO_CRL_MODE0和GPIO_CRL_CNF0由数值 3 分别左移 2 位、左移 0 位得到。二者分别对应单个 GPIO 引脚 4 位配置当中高 2 位 MODE、低 2 位 CNF将二者做或运算GPIO_CRL_MODE0 | GPIO_CRL_CNF0即可得到完整连续的 4 位掩码。这里很容易踩坑很多人会误以为这个或运算结果就是我们要写入寄存器的config配置值。但并不是它本质上是这 4 个位的占位掩码作用只是标记寄存器中哪几位需要修改而不是实际要写入的配置参数。GPIO_CRL_MODE0 | GPIO_CRL_CNF0的二进制值为0b1111而传参时我们将此值左移registeroffset位也就是前文所提及的偏移量此时我们的模版掩码就会左移到需要修改数值的指定位置而其他位为0。config registeroffset这最后一个参数想必不用过多介绍前文已经对它做过说明。3.MODIFY_REG我们已经把MODIFY_REG的3个参数全部讲解完毕现在就可以着重于关注MODIFY_REG的宏实现。这是MODIFY_REG宏的原始定义它接收三个参数(1).REG要操作的目标寄存器地址(2).CLEARMASK清零掩码掩码中为 1 的 bit表示我们想要修改的位(3).SETMASK准备写入到目标 bit 的新数值。我们把宏展开拆成两步理解(1).READ_REG读取寄存器当前的值(2).(~CLEARMASK):把CLEARMASK中标记为 1 的那些 bit 全部清零寄存器其余 bit 原样保留(3).|(SETMASK):把我们准备好的新配置值写入刚才已经清零的那几个 bit(4).WRITE_REG:把计算完成后的新值写回寄存器。核心逻辑先读 → 把要改的位清 0 → 把新值填进去 → 写回寄存器。回到我们前面 GPIO 的代码CLEARMASK((GPIO_CRL_MODE0 | GPIO_CRL_CNF0)registeroffset)也就是0xF左移偏移量标记目标引脚对应的 4bit准备清零SETMASKconfigregisteroffset,就是我们的 GPIO4 位配置信息放到对应 bit 位置。⚠️ 注意一个关键点SETMASK里面只有 CLEARMASK 标记为 1 的那些 bit 才会生效SETMASK 其余 bit 建议保持为 0避免意外改写别的位。这也就是很多同学不明白这个地方为什么要采用MODIFY_REG而不直接使用WRITE_REG的原因。至此就完成了单引脚配置信息向寄存器的写入。三.总结本文我们聚焦HAL_GPIO_Init最核心的寄存器配置逻辑拆解了 config 变量组装、掩码的含义以及MODIFY_REG多比特域改写的底层原理。 至于源码中剩下的 EXTI 外部中断相关部分本篇不再展开。这是 STM32 HAL 库源码解析系列的第一篇。很多开发者日常只是调用库函数却对底层寄存器与位运算一知半解。吃透 GPIO 这部分底层逻辑能够为后续学习 RCC、UART、TIM 等外设打下基础。后续系列文章会继续深挖其他外设的 HAL 库底层实现。如果本文对你有帮助欢迎点赞收藏期待后续更新。
返回列表