Bootstrap

【STM32】GPIO(超详细)

使用的单片机机型为STM32F103C8T6

在这里插入图片描述

GPIO(General Purpose Input/Output)通用输入输出接口,是一种可以在微控制器(MCU)或其他数字电子设备上配置为输入或输出的接口。常用于与外部设备进行通信和控制。

GPIO的基本功能包括:

  • 输入模式(Input):将GPIO引脚配置为输入模式时,设备可以读取外部设备(如传感器、开关等)的信号。例如,接收温度传感器的输出,或检测按键的状态。

  • 输出模式(Output):将GPIO引脚配置为输出模式时,设备可以向外部设备发送信号。例如,控制LED灯的开关,或者驱动继电器、蜂鸣器等硬件。


STM32系统结构

在这里插入图片描述

  • Cortex-M3:CPU核心
  • DMA1/2:辅助CPU,做一些频繁的数据搬运的工作
  • APB1/2:外设总线
    • APB1:适用于低功耗、低速、低带宽的外设,适合一些控制任务和低速通信任务
    • APB2:适用于对时钟频率要求较高的外设,适合需要更高带宽、更高精度的任务

GPIO 就处于 APB2 总线上

GPIO基本结构

在这里插入图片描述

GPIO 总共有 A ~ G,7个外设,每个 GPIO 有一个寄存器和驱动器,因为引脚为16根,所以寄存器为16位,每位控制一个一个引脚。STM32地址为32位,所以寄存器只用到了地址的低16位,高16位没有使用

在这里插入图片描述

输出:当寄存器写入0,通过驱动器输出对应端口低电平;当寄存器写入1,通过驱动器输出对应端口高电平

输入:驱动器将端口的高低电平转化为0/1,写入寄存器,再通过 APB2 总线读取

GPIO模式(重点)

在这里插入图片描述

GPIO内部,由输入输出寄存器输入输出驱动器外部引脚组成


I/O引脚及保护电路

首先讲解一些 I/O引脚及其保护电路

输出模式时,I/O引脚由输出驱动器控制,不会有问题。但为输入模式时,I/O引脚由外部控制。若无保护电路,当外部电压过高或过低时,都会对驱动器及寄存器造成损害

保护二极管作用如下:
VDD为电源正极,通常为3.3V;VSS为负极

当电压过高时,电流会导通到VDD
在这里插入图片描述

当电压过低时,电流会导通到外部

在这里插入图片描述

如此就不会损害内部电路


输入模式

输入有四个模式:浮空输入,上拉输入,下拉输入,模拟输入

在这里插入图片描述

在这里插入图片描述

数据从 I/O引脚进入输入驱动器,会先经过两个开关和电阻,一个接VDD(正极)为上拉电阻,一个接VSS(负极)位下拉电阻,可通过程序配置哪个开关导通

当上面开关导通,下面开关关闭,为上拉输入模式
可读取引脚电平,若引脚悬空,默认输入高电平
在这里插入图片描述

当上面开关关闭,下面开关打开,为下拉输入模式
可读取引脚电平,若引脚悬空,默认输入低电平

在这里插入图片描述

当上下开关都不导通,为浮空输入模式
可读取引脚电平,当引脚悬空时,电平不确定,极易受外界影响。
使用浮空输入时,需要确保输出源是连续不断的,不能出现浮空的状态

上拉电阻和下拉电阻的阻值都较大,防止影响电路,为弱上/下拉


左侧为施密特触发器(肖特基为翻译错误),作用是将不稳定的电平转化为稳定的电平。因为电平转化为数字只有0/1,而不稳定的电平会造成不稳定的输入

原理:设定上下阈值,当电平高于上阈值,切换为高电平;当电平低于下阈值,切换为低电平

示例:

在这里插入图片描述

最往左将输入写入输入数据寄存器,可通过APB2总线读出数据

在这里插入图片描述


模拟输入

GPIO无效,引脚直接接入内部ADC,用于AD电模转换。因为读取电平值,所以在施密特触发器前

在这里插入图片描述

复用功能输入

直接接在一个需要输入的外设上,如串口的输入引脚,因为需要的是数据量,所以在施密特触发器后


输出模式

输出有四种模式:开漏输出,推挽输出,复用开漏输出,复用推挽输出

在这里插入图片描述

推挽输出

在这里插入图片描述

首先,通过寄存器写入数据0/1。
当写入数据1时,上面的晶体管导通,连接VDD输出高电平

在这里插入图片描述

当写入数据0时,下面的晶体管导通,连接VSS输出低电平

在这里插入图片描述

推挽输出的高低电平由正负极决定,有较强的电流和驱动能力
典型的应用包括LED驱动、电机控制、PWM信号输出、串行通信、信号发生器、继电器驱动以及高频信号传输等领域。


开漏输出

在这里插入图片描述

开漏输出不使用上面的晶体管,通常还需要外接一颗上拉电阻

当寄存器写入数据0,下面的晶体管导通,输出低电平

在这里插入图片描述

当寄存器写入数据1,下面的晶体管断开,此时为高阻态,不会有电流流过,由上拉电阻接的电源提供高电平

在这里插入图片描述

为什么要这样设计呢?

因为单片机有多机通信的场景,例如 I2C、SPI。
多个设备挂载在一条通信总线上

在这里插入图片描述

如果采用推挽输出,假设一个设备输出高电平,一个设备输出低电平,会出现以下场景

在这里插入图片描述

此时必有一个晶体管会被烧毁
而开漏输出搭配上拉电阻就可以规避这个场景

在这里插入图片描述

高电平由上拉电阻输出
对于每个设备,想输出低电平,就导通N-MOS晶体管;想输出高电平,就关闭N-MOS,由上拉电阻输出高电平

此时,可能场景如下:

  1. 两个都输出低电平,导通N-MOS,VDD会导向任意一个,此时输出低电平
    在这里插入图片描述

  2. 两个都输出高电平,都关闭N-MOS,为高阻态,不会有电流流过,由上拉电阻输出高电平在这里插入图片描述

  3. 一个输出高电平,一个输出低电平。一个关闭N-MOS,一个开启N-MOS。此时VDD被拉低,输出低电平在这里插入图片描述

尽管无法满足部分设备的输出,但是避免了晶体管烧毁的情况


推挽输出 和 开漏输出由左侧的数据输出寄存器控制,而复用推挽输出 和 复用开漏输出由片上外设控制

在这里插入图片描述

编程实例

GPIO闪烁灯

ST封装了GPIO的库函数,步骤如下:

  1. 开启指定的GPIO外设时钟使能
//使能时钟
//接在A0端口,属于GPIOA
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  1. 初始化/配置GPIOA
//初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;	//推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;			//A0引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//50MHz速度
GPIO_Init(GPIOA, &GPIO_InitStructure);
  • GPIO_Mode:GPIO的8种模式
    在这里插入图片描述
  • GPIO_Pin:要配置的引脚,共16个
    在这里插入图片描述
    GPIO_All 代表全部16个引脚,多个引脚可以同时配置,使用 |
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2;
  • GPIO_Speed:I/O口翻转的速度,配置 GPIO 引脚工作速度

在这里插入图片描述

  • 低速(GPIO_Speed_Level_1):
    适用于一些不需要快速响应的简单应用,比如低频信号的数字输入输出(例如按键扫描、开关控制等)

  • 中速(GPIO_Speed_Level_2):
    适合一般用途的 GPIO 引脚,响应速度适中。例如常见的外设接口(如 UART),或者中等速度的信号控制

  • 高速(GPIO_Speed_Level_3):
    用于要求较高数据传输速率的应用,如 SPI、I2C 通信,或者需要频繁切换的数字输出

  1. 读/写数据

ST同样提供了数据读写的方法,在stm32f10x_gpio.h 最后可以看到定义

API参数返回值说明
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);GPIOX:指定GPIO外设,GPIOA ~ GPIOG
GPIO_Pin:指定引脚,0 ~ 15
从指定引脚读到的数据读取指定GPIO外设的输入寄存器数据的某一位
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);GPIOX:指定GPIO外设,GPIOA ~ GPIOG输入寄存器的16位数据读取指定GPIO外设的16位输入寄存器数据
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);GPIOX:指定GPIO外设,GPIOA ~ GPIOG
GPIO_Pin:指定引脚,0 ~ 15
从指定引脚读到的数据读取指定GPIO外设的输出寄存器数据的某一位
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);GPIOX:指定GPIO外设,GPIOA ~ GPIOG输出寄存器的16位数据读取指定GPIO外设的16位输出寄存器数据
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);GPIOX:指定GPIO外设,GPIOA ~ GPIOG
GPIO_Pin:指定引脚,0 ~ 15
将指定GPIO外设的指定引脚置1
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);GPIOX:指定GPIO外设,GPIOA ~ GPIOG
GPIO_Pin:指定引脚,0 ~ 15
将指定GPIO外设的指定引脚置0
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);GPIOX:指定GPIO外设,GPIOA ~ GPIOG
GPIO_Pin:指定引脚,0 ~ 15
BitVal:要写入的数据,0/1
将BitVal写入GPIO外设的指定引脚
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);GPIOX:指定GPIO外设,GPIOA ~ GPIOG
PortVal:要写入的16位数据
将PortVal写入GPIO外设

我们通过操作GPIO输出寄存器输出高低电平,高低电平就可以控制LED的亮灭

LED灯的介绍可参看【51单片机】点亮LED

简单来说,引脚输出低电平——灯亮,输出高电平——灯灭

接线图 如下:
我们将LED接在A0端口
在这里插入图片描述

要实现LED闪烁,需要周期控制LED亮灭,此处直接提供 Delay(延迟) 方法

Delay.h

#ifndef __DELAY_H
#define __DELAY_H

void Delay_us(uint32_t us);
void Delay_ms(uint32_t ms);
void Delay_s(uint32_t s);

#endif

Delay.c

#include "stm32f10x.h"

/**
  * @brief  微秒级延时
  * @param  xus 延时时长,范围:0~233015
  * @retval 无
  */
void Delay_us(uint32_t xus)
{
	SysTick->LOAD = 72 * xus;				//设置定时器重装值
	SysTick->VAL = 0x00;					//清空当前计数值
	SysTick->CTRL = 0x00000005;				//设置时钟源为HCLK,启动定时器
	while(!(SysTick->CTRL & 0x00010000));	//等待计数到0
	SysTick->CTRL = 0x00000004;				//关闭定时器
}

/**
  * @brief  毫秒级延时
  * @param  xms 延时时长,范围:0~4294967295
  * @retval 无
  */
void Delay_ms(uint32_t xms)
{
	while(xms--)
	{
		Delay_us(1000);
	}
}
 
/**
  * @brief  秒级延时
  * @param  xs 延时时长,范围:0~4294967295
  * @retval 无
  */
void Delay_s(uint32_t xs)
{
	while(xs--)
	{
		Delay_ms(1000);
	}
} 

周期控制LED亮灭就可以实现闪烁灯效果
代码如下:

/**
  * @brief		A0端口LED闪烁,以一定频率
  * @parm		无
  * @retval		无
  */
void FlashingLED()
{
	//使能时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	//初始化GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;	//推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;			//A0引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//50MHz速度
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	while(1)
	{
		GPIO_ResetBits(GPIOA, GPIO_Pin_0);	//低电平,灯亮
		Delay_ms(500);
		GPIO_SetBits(GPIOA, GPIO_Pin_0);	//高电平,灯灭
		Delay_ms(500);
	}
}

流水灯

接线图如下:
在这里插入图片描述

流水灯是在闪烁灯的基础上,周期控制不同灯亮灭

代码如下:

/**
  * @brief		A0 ~ A7端口流水灯
  * @parm		无
  * @retval		无
  */
void WaterfallLED()
{
	//使能时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	//初始化GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;	//推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;			//A0 ~ A15
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//50MHz速度
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	while(1)
	{
		unsigned int LED = 0x0001;
		for(int i = 0; i < 8; ++i)
		{
			GPIO_Write(GPIOA, ~LED);
			LED <<= 1;
			Delay_ms(300);
		}
	}
}

引脚输出控制蜂鸣器

蜂鸣器的介绍可参看【51单片机】蜂鸣器演奏天空之城

STM32的蜂鸣器是有源蜂鸣器,不需要我们控制振荡频率,所以发声频率一定。低电平发声,高电平不发声

接线图如下:

在这里插入图片描述

因为蜂鸣器也是低电平导通,本质和LED相同,区别就是我们将I/O口接在了GPIOB的B12引脚
代码如下:

/**
  * @brief		B12控制蜂鸣器
  * @parm		无
  * @retval		无
  */
void Buzzer()
{
	//使能时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	//初始化GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;	//推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;			//B12
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//50MHz速度
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	while(1)
	{		
		GPIO_ResetBits(GPIOB, GPIO_Pin_12);	//低电平,蜂鸣器响
		Delay_ms(100);
		GPIO_SetBits(GPIOB, GPIO_Pin_12);	//高电平,蜂鸣器不响
		Delay_ms(100);
		GPIO_ResetBits(GPIOB, GPIO_Pin_12);	//低电平,蜂鸣器响
		Delay_ms(100);
		GPIO_SetBits(GPIOB, GPIO_Pin_12);	//低电平,蜂鸣器响
		Delay_ms(700);
	}
}

按键控制LED

接线图如下
在这里插入图片描述

按键

详细的介绍可参看【51单片机】独立按键

按键:是常见的输入设备,按下导通,松手断开

按键抖动:由于按键内部使用的是机械式弹簧片进行通断,所以在按下和松手的瞬间会伴随一连串的抖动

在这里插入图片描述

规避按键抖动的方法之一就是,检测到按键按下后,延迟10 ~ 20ms,跳过抖动。松开后同样要延迟

因为按键是输入设备,所以配置GPIO时,需要配置为上拉输入或下拉输入

Key.c

/**
  * @brief		初始化GPIOB部分引脚服务按键
  * @parm		GPIO_Pin:要配置的引脚
  * @retval		无
  */
void Key_Init(uint16_t GPIO_Pin)
{
	//时钟使能
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	//配置GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;		//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin;				//配置引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//I/O口翻转速度
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}

/**
  * @brief		获取按键按下,若两个按键先后相近按下,会返回编号靠后的按键
  * @parm		无
  * @retval		按键按下,根据引脚编号 范围:1 ~ 16
  */
uint8_t Key(void)
{
	uint8_t Key = 0;
	
	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
	{
		Delay_ms(20);									//过滤抖动
		while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0);	//等待按键松开
		Delay_ms(20);
		Key = 1;
	}
	
	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == 0)
	{
		Delay_ms(20);									//过滤抖动
		while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == 0);	//等待按键松开
		Delay_ms(20);
		Key = 11;
	}
	return Key;
}

主程序逻辑就是循环检测按键按下,检测有按键按下后,根据按键控制LED

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"

int main()
{
	LED_Init(GPIO_Pin_0 | GPIO_Pin_1);
	Key_Init(GPIO_Pin_0 | GPIO_Pin_10);
	while(1)
	{
		uint8_t KeyNum = Key();
		if(KeyNum == 1)
			LED_Tun(GPIO_Pin_0);
		if(KeyNum == 11)
			LED_Tun(GPIO_Pin_1);
	}
}

完整项目链接:【STM32】按键控制LED

光敏电阻控制蜂鸣器

光敏电阻用于传感器模块

传感器元件(光敏电阻/热敏电阻/红外接收管等)的电阻会随着外界模拟量的变化而变化,通过与定值电阻分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出

硬件电路如下:

在这里插入图片描述

我们逐步分析

首先,N1为光敏电阻/热敏电阻,与R1进行串联分压。

一旁的C2电容,一端接电路,一端接GND,起到了滤波电容的作用,过滤一些不稳定的电压,使得电压更加平滑

在这里插入图片描述

如此,AO 和 IN+ 随着 N1 阻值的改变就会获得不同的电压。

利用上下拉电阻分析
N1 阻值变小,下拉能力增强AO 电压变小。当 N1 无限小时,相当于AO直接接在GND,输出低电平
N1 阻值变大,下拉能力减弱,AO 被 R1 的上拉拉高,AO 电压变大。当 N1 无限大时,相当于断路,AO 与 VCC 相连,输出高电平

左侧的 IN+ 接在电压比较器,用于获得数字电压;AO 接在 外部引脚,用于输出模拟电压

在这里插入图片描述


电压比较器

在这里插入图片描述

右侧的 R2 为可调电阻,用于与 IN+ 输入的电压进行比较

左侧的两个电压比较器构成运算放大器,当 IN+ 电压高于 IN-,输出高电平;当 IN+ 电压低于 IN-,输出低电平。如此就实现了二值化,获得了数字电压
DO输入接在DO引脚


在这里插入图片描述

LED1 为电源指示灯,通电即亮

D0引脚用于输入,获取数字电压。AO用于模拟电压输出

LED2为数字电压指示灯,当DO输入为0点亮,输入为1熄灭

R5为上拉电阻,DO默认输入高电平

到此传感器模块就介绍完了


接线图如下:

在这里插入图片描述

根据光敏电阻传感器控制蜂鸣器
当光亮减弱,光敏电阻阻值变大,输出高电平,反之输出低电平

我们设置光亮时蜂鸣器不响,昏暗时蜂鸣器响

Buzzer.c

#include "stm32f10x.h"                  // Device header

/**
  * @brief		初始化蜂鸣器相关引脚
  * @parm		无
  * @retval		无
  */
void Buzzer_Init(void)
{
	//使能时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	//初始化GPIOB相关引脚
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	//初始化默认高电平
	GPIO_SetBits(GPIOB, GPIO_Pin_12);
}
/**
  * @brief		蜂鸣器发声
  * @parm		无
  * @retval		无
  */
void Buzzer_On(void)
{
	GPIO_ResetBits(GPIOB, GPIO_Pin_12);
}
/**
  * @brief		蜂鸣器不发声
  * @parm		无
  * @retval		无
  */
void Buzzer_Off(void)
{
	GPIO_SetBits(GPIOB, GPIO_Pin_12);
}

/**
  * @brief		蜂鸣器发声切换
  * @parm		无
  * @retval		无
  */
void Buzzer_Tun(void)
{
	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12) == 0)
		GPIO_SetBits(GPIOB, GPIO_Pin_12);
	else
		GPIO_ResetBits(GPIOB, GPIO_Pin_12);
}

LightSensor.c

#include "stm32f10x.h"                  // Device header


/**
* @brief		光敏电阻初始化,配置引脚为GPIOB_13
  * @parm		无
  * @retval		无
  */
void LightSensor_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	//配置GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;		//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;			//配置引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//I/O口翻转速度
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}

/**
  * @brief		获取当前光敏传感器输出的高低电平
  * @parm		无
  * @retval		光敏传感器输出的高低电平,范围:0/1
  */
uint8_t LightSensor_Get(void)
{
	return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13);
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Buzzer.h"
#include "LightSensor.h"

int main()
{
	Buzzer_Init();
	LightSensor_Init();
	uint8_t Light;
	while(1)
	{
		Light = LightSensor_Get();
		if(Light == 1)
			Buzzer_On();
		else
			Buzzer_Off();
	}
}

完整项目链接:【STM32】光敏电阻控制蜂鸣器


以上就是本篇博客的所有内容,感谢你的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

;