本文主要是介绍细说ARM MCU中的HAL_GPIO_Init()函数的实现过程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
一、实例背景:
二、HAL_GPIO_Init函数的格式如下:
1、HAL_GPIO_Init函数中while语句的条件表达式
2、HAL_GPIO_Init函数中的iocurrent变量赋值语句
3、HAL_GPIO_Init函数中的三条if语句
5、 GPIO作为输入时的电路
6、I/O作为输入时执行的语句
7、I/O作为输出的相关电路说明
8、I/O作为输入时执行的语句
9、读取GPIO状态HAL_GPIO_ReadPin()函数
三、重写读/写GPIO的代码
继续上一篇文章,分析HAL_GPIO_Init函数是如何实现的,该函数的定义在stm32g4xx_hal _gpio.c文件中。
一、实例背景:
本文中使用ST的开发板NUCLEO-G474RE,板上MCU型号为STM32G474RET6。配套的扩展板:
实例中当开发板上的按键B1被按下时,PC13引脚被上拉至高电平VDD,不按下时,PC13下拉至低电平GND。用按键B1控制板上的LD2灯的亮灭,当PA5输出高电平时LD2亮,否则灯灭。
二、HAL_GPIO_Init函数的格式如下:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx,GPIO_InitTypeDef *GPIO_Init)
HAL_GPIO_Init函数是void类型,不需要返回值。它有两个参数:一个是端口,为结构体类型,与HAL_GPIO_TogglePin()相同;另一个是端口的配置参数,也是结构体类型。stm32g4xx _hal_gpio.c文件中给出的HAL_GPIO_Init区(部分)如下:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
{uint32_t position = 0x00U;uint32_t iocurrent;uint32_t temp;/* 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) != 0U){/* Get current io position */iocurrent = (GPIO_Init->Pin) & (1UL << position);if (iocurrent != 0x00u){/*--------------------- GPIO Mode Configuration ------------------------*//* In case of Output or Alternate function mode selection */if(((GPIO_Init->Mode & GPIO_MODE) == MODE_OUTPUT) ||((GPIO_Init->Mode & GPIO_MODE) == MODE_AF)){/* Check the Speed parameter */assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));/* Configure the IO Speed */temp = GPIOx->OSPEEDR;temp &= ~(GPIO_OSPEEDR_OSPEED0 << (position * 2U));temp |= (GPIO_Init->Speed << (position * 2U));GPIOx->OSPEEDR = temp;/* Configure the IO Output Type */temp = GPIOx->OTYPER;temp &= ~(GPIO_OTYPER_OT0 << position) ;temp |= (((GPIO_Init->Mode & OUTPUT_TYPE) >> OUTPUT_TYPE_Pos) << position);GPIOx->OTYPER = temp;}if ((GPIO_Init->Mode & GPIO_MODE) != MODE_ANALOG){/* Check the Pull parameter */assert_param(IS_GPIO_PULL(GPIO_Init->Pull));/* Activate the Pull-up or Pull down resistor for the current IO */temp = GPIOx->PUPDR;temp &= ~(GPIO_PUPDR_PUPD0 << (position * 2U));temp |= ((GPIO_Init->Pull) << (position * 2U));GPIOx->PUPDR = temp;}/* In case of Alternate function mode selection */if ((GPIO_Init->Mode & GPIO_MODE) == MODE_AF){/* Check the Alternate function parameters */assert_param(IS_GPIO_AF_INSTANCE(GPIOx));assert_param(IS_GPIO_AF(GPIO_Init->Alternate));/* Configure Alternate function mapped with the current IO */temp = GPIOx->AFR[position >> 3U];temp &= ~(0xFU << ((position & 0x07U) * 4U));temp |= ((GPIO_Init->Alternate) << ((position & 0x07U) * 4U));GPIOx->AFR[position >> 3U] = temp;}/* Configure IO Direction mode (Input, Output, Alternate or Analog) */temp = GPIOx->MODER;temp &= ~(GPIO_MODER_MODE0 << (position * 2U));temp |= ((GPIO_Init->Mode & GPIO_MODE) << (position * 2U));GPIOx->MODER = temp;/*--------------------- EXTI Mode Configuration ------------------------*//* Configure the External Interrupt or event for the current IO */if ((GPIO_Init->Mode & EXTI_MODE) != 0x00u){/* Enable SYSCFG Clock */__HAL_RCC_SYSCFG_CLK_ENABLE();temp = SYSCFG->EXTICR[position >> 2U];temp &= ~(0x0FUL << (4U * (position & 0x03U)));temp |= (GPIO_GET_INDEX(GPIOx) << (4U * (position & 0x03U)));SYSCFG->EXTICR[position >> 2U] = temp;/* Clear Rising Falling edge configuration */temp = EXTI->RTSR1;temp &= ~(iocurrent);if ((GPIO_Init->Mode & TRIGGER_RISING) != 0x00U){temp |= iocurrent;}EXTI->RTSR1 = temp;temp = EXTI->FTSR1;temp &= ~(iocurrent);if ((GPIO_Init->Mode & TRIGGER_FALLING) != 0x00U){temp |= iocurrent;}EXTI->FTSR1 = temp;temp = EXTI->EMR1;temp &= ~(iocurrent);if ((GPIO_Init->Mode & EXTI_EVT) != 0x00U){temp |= iocurrent;}EXTI->EMR1 = temp;/* Clear EXTI line configuration */temp = EXTI->IMR1;temp &= ~(iocurrent);if ((GPIO_Init->Mode & EXTI_IT) != 0x00U){temp |= iocurrent;}EXTI->IMR1 = temp;}}position++;}
}
1、HAL_GPIO_Init函数中while语句的条件表达式
HAL_GPIO_Init函数中主要就是一个while的条件表达式:
((GPIO_Init->Pin)>>position) != 0U
其中,最后的“0U”,是无符号数0是无符(Unsigned int)。后面的1UL,“1”后面的“UL”表示数1为Unsigned long int。
此处的GPIO_Init -> Pin中GPIO_Init是通过结构体变量传递过来的参数,实际就是引脚号。因此,对PC13来说,这个GPIO_Init->Pin就是GPIO_PIN_13对应的数(0010 0000 0000 0000),即0x2000,“>>”表示右移。右移的位数在position变量中。由于position初始为0,0x2000右移0位,值不会改变,还是0x2000,条件不等于0,所以,while的条件是满足的,程序继续执行。
2、HAL_GPIO_Init函数中的iocurrent变量赋值语句
继续,给变量iocurrent赋值:
iocurrent = (GPIO_Init->Pin) & (1UL <<position);
当前是在配置PC13,此时GPIO_Init->Pin为0x2000,按位逻辑“与”后面的1UL<<position,是将“1”左移position位,而position此时为0,所以还是1。用0x2000与1按位相“与”,结果为0。所以,接下来的if语句的条件(iocurrent!=0x00u)不满足的,此时程序就会跳到if之外,执行最后的position++,让position自加1;执行后,position为1。随后,会继续判断while条件是否成立。当然,虽然此时position为1,当0x2000右移1位后,依然不等于0,条件是满足的。接着执行iocurrent赋值语句,此时将0x2000与0x0002按位相与,结果iocurrent依然为0。所以,会继续执行position++语句,一直到position为13,此时在while的条件中0x2000右移13位,结果为1,还是不等于0,所以while条件还是满足的。接下来将会继续执行iocurrent赋值语句,此时1左移13位的结果刚好为0x2000,而GPIO_Init->Pin也为0x2000,这两个数按位逻辑“与”的结果就不再为0了;接下来的if语句,条件是满足的,所以会执行if中的语句。
3、HAL_GPIO_Init函数中的三条if语句
在if(iocurrent!=0x00u){...}中还有三条if语句:
第一条if语句用于复用功能(alternate function),本例中是用作GPIO,所以不会执行。
第二条if语句输出或复用功能,由于当前是配置PC13作为输入,所以此时也不会执行。
第三条if语句用于外部中断、触发等模式,本例中也不会执行。
此外,在第一条if语句与第二条if语句之间有段配置I/O模式的语句(配置GPIO的GPIOx->MODER寄存器),在第二条if语句和第三条if语句之间有段配置I/O上拉/下拉功能的(配置GPIO的GPIOx_PUPDR寄存器),这两段代码在配置GPIO的输入与输出功能时都会执行。
4、I/O作为输入时执行的语句
配置PC13作为输入引脚时会执行下面4条语句:
/* Activate the Pull-up or Pull down resistor for the current IO */
temp = GPIOx -> PUPDR;
temp &= ~(GPIO_PUPDR_PUPD0 << (position *2U));
temp |= ((GPIO_Init->Pull) << (position *2U));
GPIOx -> PUPDR = temp;
第一条语句是将GPIOx -> PUPDR赋值给变量temp。PUPDR是GPIO的寄存器。查STM32G4系列MCU的参考手册,可以看到PUPDR的寄存结构:
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
PUPD15[1:0] | PUPD14[1:0] | PUPD13[1:0] | PUPD12[1:0] | PUPD11[1:0] | PUPD10[1:0] | PUPD9[1:0] | PUPD8[1:0] | ||||||||
rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw |
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
PUPD7[1:0] | PUPD6[1:0] | PUPD5[1:0] | PUPD4[1:0] | PUPD3[1:0] | PUPD2[1:0] | PUPD1[1:0] | PUPD0[1:0] | ||||||||
rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw |
GPIOx_PUPDR寄存器(x为A、B、C、D、E、F、G)结构
PUPDR有32位,每2位构成一组,共有16组,即PUPD0~PUPD15(PUPD是Pull-up,Pull-down的缩写)。实际上每一组PUPDx[1:0]对应一个GPIO端口引脚的上拉/下拉配置(二进制):
00:没有上拉,下拉;
01:上拉;
10:下拉;
11:保留。
由于配置的是PC13,所以对应的就是PUPD13[1:0],它们在PUPD寄存器的第26和27位(最低位从0开始)。
此外,还需要提一下PUPDR寄存器的默认值。在STM32G4系列MCU的参考手册中,提到了GPIOx_PUPDR寄存器的默认值(初始值,即复位后的值)。对于端口A(GPIOA),默认值是0x6400 0000。也就是说,PUPD15[1:0]=01(二进制),PA15默认为上拉;PUPD14[1:0]=10(二进制),PA14默认为下拉。
对于端口B(GPIOB),默认值为0x0000 0100。也就是说,除了PUPD4[1:0]=01以外,其它均为00。意思是PB4默认为上拉,其他不开启上拉、下拉功能。
除了GPIOA和GPIOB以外,对于其他GPIO,PUPDR寄存器的值均为0x0000 0000,即为不开启上拉、下拉功能。由于PC13属于GPIOC,所以默认情况下该端口的PUPDR的值为0。因此,执行temp = GPIOx->PUPDR语句后,temp的值为0。
5、 GPIO作为输入时的电路
GPIO的引脚既可以用作输入,也可以用作输出,但是在同一时刻,只能配置为其中一种(即要么为输入,要么为输出)。用作输入时,输出通道是要关闭的。
6、I/O作为输入时执行的语句
继续,分析配置PUPDR的代码。接下来的一行语句是:
temp &= ~(GPIO_PUPDR_PUPD0 << (position *2U));
其中,“&=”是将变量temp与“=”后的表达式的值相“与”,并把结果赋给temp。不过由于执行完前面的赋值语句后,temp已经为0了,所以执行这一条语句后,temp会依然为0。
在这条语句中,GPIO_PUPDR_PUPD0是个常量,实际就是在PUPDR寄存器中PUP0[1:0]所在的位的掩码,也就是表中的最低2位的掩码,用二进制表示掩码就是11,即十进制数值3。
由于此时position为13(十进制数),与2相乘就是26,所以上边语句中等号右侧的表达式意思就是将二进制11左移26位,此时得到的值是一个第26和27位为1、其他位均为0的32位数:0x0C00 0000。然后取反,得到的数值为0xf3ff ffff。不过,与temp相“与”后,结果还是为0。所以,执行完这一条语句后,temp的值还是0。
接下来的语句是:
temp |= ((GPIO_Init->Pull) << (position *2U));
其中,GPIO_Init是HAL_GPIO_Init()函数的第二个参数,是由MX_GPIO_Init(void)函数传递过来的。在MX_GPIO_Init(void)函数中,就是结构体变量GPIO_InitStruct。在该函数中,该变量做过如下赋值:
GPIO_InitStruct.Pull =GPIO_PULLDOWN;
所以,上述语句中的GPIO_Init->Pull就是GPIO_PULLDOWN,也就是下拉。在STM32g4xx _hal_gpio.h中,关于GPIO_PULLDOWN有一个宏定义:
# define GPIO_PULLDOWN (0x0000 0002U)
此时positon为13,GPIO_PULLDOWN的值是2,用二进制表示就是10。重面己知,此时position为13,position*2就是26;二进制数10左移26位,结果是0x0800 0000;,
“|=”是将“=”后的表达式的值与temp相“或”,然后再赋值给temp。所以,此句执行完的值为0x0800 0000。
接下来的语句是:
GPIOx -> PUPDR = temp;
该句表示将temp的值赋值给PUPDR寄存器,也就是说,把0x0800 0000赋值给PUPDR寄存器。对照表中PUPDR寄存器的结构,刚好是PUPD13[1:0]的值为二进制数10,也就是将PC13配置为下拉模式。
至此,PUPDR就配置完毕。
7、I/O作为输出的相关电路说明
要配置PA5作为输出引脚,先来看一下将GPIO配置为输出I/O功能需要做哪些事情。
在STM32 MCU中,输出有两种模式:一种是开漏(open drain),另一种是推挽(push-pull)。这是两种常见的电路输出方式:
在push-pull模式时,会用到P-MOS和N-MOS两个MOS管。这两个管子是互补输出的,也就是说,上面的P-MOS导通,下面的N-MOS就会截止,此时输出高电平;P-MOS截止,N-MOS导通,则输出低电平。
开漏(open drain)的“漏”,就是MOS管的漏极。开漏模式就是由MOS管的漏为输出。此时,只需要将下面那个N-MOS管接在输出和地之间。开漏模式比推挽模式少用一个P-MOS。此时,要输出高电平就需要上拉电阻配合。因此,如果设置了为开漏输出模式,通常要配置为上拉。
8、I/O作为输入时执行的语句
接着分析HAL_GPIO_Init函数。在配置PA5时,程序会执行到中间这条if语句。再来分析一下配置GPIOx_OSPEED寄存器的过程。这个寄存器是配置I/O引脚速度参数的,关键的几句代码如下:
/*In case of Output or Alternate function mode selection */
if(…)
{……/*Configure the IO Speed */temp = GPIOx -> OSPEEDR;temp &= ~(GPIO_OSPEEDR_OSPEED0 << (position *2U));temp |= (GPIO_Init->Speed << (position *2U));GPIOx -> OSPEEDR = temp;……
}
配置这个寄存器也是执行4条语句,与前面介绍的配置PUPDR寄存器的过程基本类似。在配置PA5时,当程序执行到这几句代码时,变量position的值应该是5。
第一条语句是将GPIOx->OSPEEDR赋值给变量temp。OSPEED是GPIO的寄存器。查看STM32G4系列MCU的参考手册,可以看到OSPEED的寄存器结构。OSPEED有32位,每2位构成一组,共有16组,即OSPEED0~OSPEED15。实际上,每一组OSPEEDx[1:0]对应一个GPIO端口引脚的速度配置(二进制):
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
OSPEED15[1:0] | OSPEED14[1:0] | OSPEED13[1:0] | OSPEED12[1:0] | OSPEED11[1:0] | OSPEED10[1:0] | OSPEED9[1:0] | OSPEED8[1:0] | ||||||||
rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw |
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
OSPEED7[1:0] | OSPEED6[1:0] | OSPEED5[1:0] | OSPEED4[1:0] | OSPEED3[1:0] | OSPEED2[1:0] | OSPEED1[1:0] | OSPEED0[1:0] | ||||||||
rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw | rw |
GPIOx_OSPEED寄存器(x为A、B、C、D、E、F、G)结构
00:低速;
01:中速;
10:高速;
11:很高速。
GPIOx_OSPEED寄存器的默认值有两种情况:对于端口A(GPIOA),默认值为0x0C00 0000;对于其他端口,默认值一律为0x0000 0000。
此时配置的是PA5,对应的就是OSPEED5[1:0],它们在OSPEED寄存器的第10和11位(最低位从0开始)。不过,由于GPIOA的OSPEED寄存器,默认值为0x0C00 0000,所以执行完这条赋值语句后,temp的值为0x0C00 0000。然后,
temp &= ~(GPIO_OSPEEDR_OSPEED0 << (position *2U));
“&=”是将变量temp与“=”后的表达式的值相“与”,并把结果赋给temp。GPIO_OSPEEDR _OSPEED0 是个常量,实际就是在OSPEED寄存器中,OSPEEDO[1:0]所在位的掩码,也就是表中的最低2位的掩码,用二进制表示掩码就是11,即数值3(十进制)。
由于此时position为5,与2相乘就是10(十进制)。所以上边的语句中等号右侧的表达式意思就是将二进制11左移10位,结果为0x0000 0C00;然后,将该值取反,可以得到0xFFFF F3FF。
将temp当前的值0x0C00 0000与0xFFFF F3FF相“与”,结果为0x0C00 0000,该值会赋给temp。所以该语句执行完毕后,temp的值为0x0C00 0000。这个结果与OSPEED寄存器的默认值相比较,在数值上没有什么变化。做这些操作有何意义呢?
实际上,这两个temp赋值语句的目的是,将OSPEED寄存器中要配置的相应位(对PA5来说就是第10和11位)变为00,以便接下来做修改。这里与默认值相同的原因是,在此次读取OSPEED寄存器的时刻,这两位本来就为00。继续:
temp |= (GPIO_Init->Speed << (position *2U));
该语句中,GPIO_Init -> Speed是取出在MX_GPIO_Init函数中配置的Speed值:
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_SPEED_FREQ_HIGH在stm32g4**_hal_gpio.h中被定义如下:
# define GPIO_SPEED_FREQ_HIGH (0x00000002U)
也就是二进制数10,是高速时钟。将二进制数10左移10位后,结果为0x0000 0800,与temp的当前值0x0C00 0000相“或”后,得到0x0C00 0800。这个结果与OSPEED寄存器的默认值相比较,OSPEED5[1:0]的值被修改为二进制数10,即配置PA5的速度为高速。
9、读取GPIO状态HAL_GPIO_ReadPin()函数
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{GPIO_PinState bitstatus;/* Check the parameters */assert_param(IS_GPIO_PIN(GPIO_Pin));if ((GPIOx->IDR & GPIO_Pin) != 0x00U){bitstatus = GPIO_PIN_SET;}else{bitstatus = GPIO_PIN_RESET;}return bitstatus;
}
HAL_GPIO_ReadPin函数的类型是GPIO_PinState,而GPIO_PinState是枚举类型。它有两个成员GPIO_PIN_RESET和GPIO_PIN_SET,取值为0和1。调用HAL_GPIO_ReadPin函数,需要返回引脚的状态,该状态要么为0(低电平),要么为1(高电平)。
在HAL_GPIO_ReadPin函数中,首先声明了类型同样为GPIO_PinState的变量bitstatus,紧接着是一个assert_param()语句,用于判断传递过来的引脚号是否在有效范围内;随后,在if语句的条件表达式中,读取了GPIO的输入数据寄存器IDR。GPIOx ->IDR & GPIO_PIN的作用是取出IDR中与引脚号相对应的位,如果该位不为0,则将1(GPIO_PIN_SET)赋给变量bitstatus;如果该位为0,则将0赋给bitstatus。if语句之后,用return返回bitstatus。
在STM32G4系列MCU的参考手册中,可以查到IDR寄存器的结构:
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
Res | Res | Res | Res | Res | Res | Res | Res | Res | Res | Res | Res | Res | Res | Res | Res |
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
ID15 | ID14 | ID13 | ID12 | ID11 | ID10 | ID9 | ID8 | ID7 | ID6 | ID5 | ID4 | ID3 | ID2 | ID1 | ID0 |
r | r | r | r | r | r | r | r | r | r | r | r | r | r | r | r |
GPIOx_IDR寄存器(x为A、B、C、D、E、F、G)结构
IDR只是用了低16位,分别对应GPIO端口的16个引脚。就是相应引脚的输入状态数据,不过,ID[15:0]下面标有“r”,表示该位只可读。
三、重写读/写GPIO的代码
用PC13作为按键状态的输入引脚,该引脚所属端口为GPIOC;用PB5作为控制发光二极管的输出引脚,所属端口为GPIOB。不过,在前面配置端口的模式时,给PC13起一个用户标识KEY,给PB5起的是LED,所以在main.h文件中,可以看到如下的宏定义:
# define KEY_Pin GPIO_PIN_13
# define KEY_GPIO_Port GPIOC
# define LED_Pin GPIO_PIN_5
# define LED_GPIO_Port GPIOA
这样,就可以写出这两个函数的完整语句:
HAL_GPIO_ReadPin(KEY_GPIO_Port,KEY_Pin);
HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);
由于HAL_GPIO_ReadPin函数需要返回引脚的输入状态,类型是GPIO_PinState,所以,可以先在main.c中定义一个变量KEY,类型为GPIO_PinState。因为GPIO_PinState的成员是GPIO_ PIN_RESET和GPIO_PIN_SET,实际就是0和1,所以,也可以将变量KEY的类型定义为uint8_t。
/*USER CODE BEGIN 1 */
GPIO_PinState KEY;
/*USER CODE END 1 */
/*Infinite loop */
while(1)
{/*USER CODE BEGIN 3*/KEY = HAL_GPIO_ReadPin(KEY_GPIO_Port,KEY_Pin);if(KEY == GPIO_PIN_SET){HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);}else{HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);}
}
/*USER CODE END 3*/
代码编写完毕后,编译工程。如果没有出现错误,就可以下载到硬件中。运行后,开发板上的LD2默认状态下是点亮着的,按下B1键后LD2灯熄灭。如此便实现了通过按键控制LED灯亮灭的效果。
这篇关于细说ARM MCU中的HAL_GPIO_Init()函数的实现过程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!