STM32-GPIO

GPIO概念
GPIO为通用输入输出
输入输出的本质就是上图看到的输入或输出高低电平,达到获取外设信息和控制外设的目的。
通用的含义就是,GPIO可以被多种外设来使用。
APB1和APB2下面连接的就是片上外设
GPIO外部引脚与IO

其中红色框住的部分,就是GPIO所对应的针脚,从A-G一共七组,每组从0-15一共16个针脚。那么我们已经知道了这些针脚,那这些针脚是如何和外部进行关联的呢。就是通过输入和输出来联系的。输入比如按钮,当按钮按下,输入低电平,输出比如LED灯,当输出低电平LED灯亮。
这是按键的例子
这是LED灯的例子
到这里我们已经基本了解了GPIO和针脚之间的关系。简单总结就是,有七组GPIO,每组包含16个针脚,针脚连接外部设备,使用GPIO来和MCU内部进行通讯,下来我们来介绍一个针脚下的电路原理图
这个电路图我们可以大概的拆为两部分,一部分为上面的输入,一部分为下面的输出,我们会分别来进行介绍。
这里一定注意,我们现在研究的是GPIO中的一个针脚,一个GPIO中有16个这样的电路图。

I/O端口保护

这一部分就是对端口进行保护,当输入电压过大,与VDD形成较大电压差,则电流会从上面二极管流走,当电压过小时,则下面二极管导通,从而不让电压输入MCU内部。
输出
到这里我们只剩下输入部分和输出部分没有讲解,接下来我们先来讲解输出。
首先我们要知道输出一共包含四种模式,分为通用推挽输出,通用开漏输出,复用推挽输出,复用开漏输出。通用就是指由CPU直接进行输出,复用指由其他片上外设进行输出。我们下来把上图简化一下
其中由于我们现在只研究一个针脚,所以输出寄存器只有一位。复用器的作用是从多组输入中,选择一个作为有效输入。即在输出过程中,是选择ARM-CPU的进行输出还是其他外设进行输出,就是它说了算。输出控制器就是驱动MOS管的栅极,从而将信号转换为高低电平输出出去。
P-MOS和N-MOS
想要理解输出电路,前面我们解释的东西都非常简单,最核心的就在这里。
这是两个MOS管的电路图,G就是上面我们说的输出控制器驱动的栅极。S为源极,D为漏极,这里我建议大家当作目的地来记也可以。P-MOS管的P为positive,当栅极施加电压,电流的流向为S->D,N-MOS管的N为negative,当栅极释放电压,电流的留向为D->S。

在我们的电路当中,是这样的
我们把整体的图就可以改为
通用推挽输出
当P-MOS管连通,N-MOS管断开,相当于输出高电平。当N-MOS管连通,P-MOS管断开,相当于输出低电平。
如果两个同时连通呢?
如果同时连通,就会造成短路的情况。
推挽输出流程
首先,ARM-CPU执行用户代码指令,用户要求向寄存器中写入1,CPU执行用户代码,写入1,复用器选择信号输入源是寄存器,输出控制读取到1,将P-MOS打开,N-MOS不进行操作,输出1。
通用开漏输出
通用开漏输出就是将P-MOS永久关闭,只使用N-MOS管,当N-MOS管闭合输出0,当N-MOS管断开,输出高阻抗,我们一般会在外面接一个上拉电阻,使其输出1。

这里为什么要使用上拉电阻呢?
当N-MOS管闭合时,如果没有上拉电阻,就会造成短路。
复用推挽输出和复用开漏输出,就是把输出源改为了其他片上外设。
输入

我们还是提取出来输入部分
再进一步简化
输入部分分为了上拉输入,下拉输入,浮空输入和模拟输入。
这里的上拉电阻,下拉电阻和上面的一样,为特定情况提供高低电平。TTL施密特触发器,由于输入的电平是模拟信号,就用它让电平变得规整。
阈值内不改变0/1值。
上拉输入
即上拉电阻闭合,下拉电阻断开
当输入高电平时,没有电压差,故直接输入高电平,核心在于下面讲的输入低电平。
为什么这里不会把低电平拉高呢?这是由于上拉电阻的值很大,会把压分完。
当引脚不接设备时,由于上拉电阻的存在,会输入1
下拉输入
此时上拉电阻断开,下拉电阻闭合

当输入高电平时,为什么不会拉为低电平呢,和上面是同样的道理。
输入低电平,同样没有电压差。
这里当引脚不接东西时,由于下拉电阻存在,默认输入0
浮空输入
当我们不使用上拉电阻,也不使用下拉电阻,这就叫浮空输入。这里输入1/0大家应该都明白。主要在引脚不接设备时,输入值是不确定的。
模拟输入
模拟输入就是不经过TTL,直接把模拟信号直接接收。
GPIO寄存器
首先明确,我们上面讲解的是一个针脚,接下来的寄存器是一个GPIO中,即16个针脚共用。

这两个寄存器是对针脚进行的最基本的信息配置,也就是我们上面讲的,输入输出,和输入/输出模式进行配置。
CNF

MODE

其中CRL配置的是针脚0-7,CRH是针脚8-15,每一个针脚对应四位。
代码与源码
我们先来一个最简单的程序,把LED灯点亮

这是我们板子中LED灯对应的针脚。我们使用CubeMX进行快速配置,为开漏模式,默认为高电平(熄灭)。
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, 0);
uint16_t gleds[] = {GPIO_PIN_8,GPIO_PIN_9,GPIO_PIN_10,GPIO_PIN_11};
const uint16_t gleds_num = sizeof(gleds)/sizeof(gleds[0]);
while(1)
{
for(int i = 0; i < gleds_num; i++)
{
HAL_GPIO_TogglePin(GPIOF,gleds[i]);
HAL_Delay(150);
HAL_GPIO_TogglePin(GPIOF,gleds[i]);
}
for(int i = gleds_num-1; i >=0; i--)
{
HAL_GPIO_TogglePin(GPIOF,gleds[i]);
HAL_Delay(150);
HAL_GPIO_TogglePin(GPIOF,gleds[i]);
}
}
这里面的GPIOF其实就是一个地址,GPIO_PIN_x这个对应的是16个针脚,它的位数就代表他是几号针脚,这个设计非常巧妙,我们后面介绍。
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
/* Check the parameters */
assert_param(IS_GPIO_PIN(GPIO_Pin));
assert_param(IS_GPIO_PIN_ACTION(PinState));
if (PinState != GPIO_PIN_RESET)
{
GPIOx->BSRR = GPIO_Pin;
}
else
{
GPIOx->BSRR = (uint32_t)GPIO_Pin << 16u;
}
}
这个代码是设置输出的一个GPIO接口,首先三个参数为第一个是对应哪个GPIO的地址,第二个是我上面说的针脚,第三个是设置为0/1,开始两行代码是检查合法的。接下来是if判断,如果要输出的值为1,则把BSRR寄存器改为这个针脚,否则,把BSRR寄存器高16位改为对应针脚。这里我们来认识一下BSRR寄存器。
这个寄存器靠硬件实现对ODR寄存器的赋值,ODR寄存器就是我们要进行输出的寄存器。
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, 0);
比如在这句代码中,我们要对GPIOF8输出改为0,代码会把GPIOF的BSRR高16位改为0000000100000000,然后同步到ODR中,把pin8对应的寄存器改为0
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
我们对GPIO的操作其实就是对寄存器进行操作。
#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *)GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *)GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *)GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *)GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *)GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *)GPIOG_BASE)
我刚才说GPIOx本质就是一个地址,然后转为了结构体类型,寄存器连续进行存放。
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
uint32_t odr;
/* Check the parameters */
assert_param(IS_GPIO_PIN(GPIO_Pin));
/* get current Output Data Register value */
odr = GPIOx->ODR;
/* Set selected pins that were at low level, and reset ones that were high */
GPIOx->BSRR = ((odr & GPIO_Pin) << GPIO_NUMBER) | (~odr & GPIO_Pin);
}
这段源码本质就是对状态进行反转。使用本来的状态&引脚号,得到该引脚当前的状态,随后左移16位,若本来为1,就会改为0,若本来为0,就用到了后面的逻辑,得到当前当前状态的相反值,放在低16位,把上面ODR会设为1,这里我一直说这个就是一个地址,为什么?
因为这里所有的设备,都占着空间所以我们一切的操作都是在地址之上完成的。
接下来我们讲解GPIO的初始化源码
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11, GPIO_PIN_SET);
/*Configure GPIO pins : PF8 PF9 PF10 PF11 */
GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}
首先第一步定义一个结构体,我们在后面把配置的数据写入寄存器就要使用这个结构体,再往后两行RCC我们先不关注,再往下是设置开始的引脚状态,我们上面已经讲解,再往下是给结构体的赋值,最后,也是最关键的一步,把初始化数据写入寄存器。
typedef struct
{
uint32_t Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */
uint32_t Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIO_mode_define */
uint32_t Pull; /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.
This parameter can be a value of @ref GPIO_pull_define */
uint32_t Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIO_speed_define */
} GPIO_InitTypeDef;
这是结构体的内容,第一个就是哪个引脚,第二个是输入还是输出,第三个是有无上下拉,第四个是输出速度。
这个代码我先讲解核心,第一个参数是哪个GPIO,第二个参数是刚才的结构体。我们直接来看while循环,判断语句用来判断后面还有没有要设置的针脚,进来ioposition用来表明当下设置的是哪个针脚,iocurrent表示传入数据中这个针脚用不用设计,相等则需要配置,进入条件,最后position++逻辑上实现对pin整体右移。这里我们也理解了,为什么我们在配置时可以用|来处理相同配置的针脚。

if里面主要有这几行,首先使用的什么模式,然后得到config,这里的config刚好就是要写入的数据,这和Speed,模式宏定义的数字有关。得到config后,先configregister来确定往哪个寄存器进行配置,registeroffset来确定往这个寄存器的哪几位去配置。
#define MODIFY_REG(REG, CLEARMASK, SETMASK) WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))
随后进行写入,这里第二个参数对要写入位置全部为1,第三个得到要写入位置实际写入值。随后在下面代码中,二和三结合起来,首先把要写入位置全部置0,然后|实际值,把值写入寄存器。
GPIO按键输入


首先在我们的电路中每一个按键都有一个电容,它的作用就是进行消抖,因为我们在操作中,会存在抖动问题。

当然我们也可以通过软件来完成消抖,我们在后面进行介绍。
我们使用的是浮空输入
typedef enum
{
LED1 = 1,
LED2,
LED3,
LED4
}LED_t;
typedef enum
{
KEY1 = 1,
KEY2,
KEY3,
KEY4
}KEY_t;
void BSP_LED_Control(LED_t led, bool flag);
void BSP_Led_On(LED_t led);
void BSP_Led_Off(LED_t led);
GPIO_PinState BSP_KEY_Control_V1(KEY_t key);
bool BSP_KEY_Control_V2(KEY_t key);
while
{
BSP_KEY_Control_V2(KEY1) ? BSP_Led_On(LED1) : BSP_Led_Off(LED1);
BSP_KEY_Control_V2(KEY2) ? BSP_Led_On(LED2) : BSP_Led_Off(LED2);
BSP_KEY_Control_V2(KEY3) ? BSP_Led_On(LED3) : BSP_Led_Off(LED3);
BSP_KEY_Control_V2(KEY4) ? BSP_Led_On(LED4) : BSP_Led_Off(LED4);
}
//关于KEY
bool BSP_KEY_Control_V2(KEY_t key)
{
bool ret = false;
switch(key)
{
case KEY1:
{
if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_10) == GPIO_PIN_RESET)
{
HAL_Delay(20);
if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_10) == GPIO_PIN_RESET)
{
ret = true;
}
}
}
break;
case KEY2:
{
if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_6) == GPIO_PIN_RESET)
{
HAL_Delay(20);
if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_6) == GPIO_PIN_RESET)
{
ret = true;
}
}
}
break;
case KEY3:
{
if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_8) == GPIO_PIN_RESET)
{
HAL_Delay(20);
if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_8) == GPIO_PIN_RESET)
{
ret = true;
}
}
}
break;
case KEY4:
{
if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_7) == GPIO_PIN_RESET)
{
HAL_Delay(20);
if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_7) == GPIO_PIN_RESET)
{
ret = true;
}
}
}
break;
default:
break;
}
return ret;
}
//关于LED
void BSP_LED_Control(LED_t led, bool flag)
{
GPIO_PinState state = flag ? GPIO_PIN_RESET : GPIO_PIN_SET;
switch(led)
{
case LED1:
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, state);
break;
case LED2:
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, state);
break;
case LED3:
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_10, state);
break;
case LED4:
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_11, state);
break;
default:
break;
}
}
void BSP_Led_On(LED_t led)
{
BSP_LED_Control(led,true);
}
void BSP_Led_Off(LED_t led)
{
BSP_LED_Control(led,false);
}
这里是通过HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_10)来获取状态的,在根据状态对LED灯做出响应。
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) != (uint32_t)GPIO_PIN_RESET)
{
bitstatus = GPIO_PIN_SET;
}
else
{
bitstatus = GPIO_PIN_RESET;
}
return bitstatus;
}

我们讲解过之前的代码,这里就很简单了,获取到输入寄存器对应位数的值,并进行返回。
更多推荐
所有评论(0)