8.1 关于GPIO
GPIO(General-Purpose IO ports,通用输入/输出接口),用于感知外界信号(输入模式)和控制外部设备(输出模式),如图 6.1.1 所示的STM32F103ZET6芯片四周的细引脚就是GPIO。
在嵌入式开发中,经常需要用到一些外部功能模块,比如LED、按键、蜂鸣器、温度传感器等,这些外设模块都比较简单,只需要MCU的GPIO与模块连接,控制引脚输出/读取高低电平即可。还有一些外部功能模块,需要多个引脚构成的“协议”进行通信,比如UART、I2C、SPI接口等。
如今的MCU大都采用引脚复用技术,即一个GPIO,即可以直接控制其输出高低电平,也可以设置为某个协议的引脚之一,比如I2C的时钟信号引脚SCK。此外,有些MCU的引脚,还能设置为ADC模式读取模拟信号,或者设置为DAC模式输出模拟信号,本章主要针对引脚的GPIO模式讲解。
对于GPIO模式,不同的MCU的功能细节略有差异,比如STM32的GPIO可以设置输出速度,51单片机就没有该功能。因此本章主要针对STM32的GPIO特性进行讲解,其它MCU大同小异,稍微类比一下也能很快上手。
8.1.1STM32的GPIO
GPIO(General-Purpose IO ports,通用输入/输出接口),用于感知外界信号(输入模式)和控制外部设备(输出模式),如图 6.1.1 所示的STM32F103ZET6芯片四周的细引脚就是GPIO。
在嵌入式开发中,经常需要用到一些外部功能模块,比如LED、按键、蜂鸣器、温度传感器等,这些外设模块都比较简单,只需要MCU的GPIO与模块连接,控制引脚输出/读取高低电平即可。还有一些外部功能模块,需要多个引脚构成的“协议”进行通信,比如UART、I2C、SPI接口等。
如今的MCU大都采用引脚复用技术,即一个GPIO,即可以直接控制其输出高低电平,也可以设置为某个协议的引脚之一,比如I2C的时钟信号引脚SCK。此外,有些MCU的引脚,还能设置为ADC模式读取模拟信号,或者设置为DAC模式输出模拟信号,本章主要针对引脚的GPIO模式讲解。
对于GPIO模式,不同的MCU的功能细节略有差异,比如STM32的GPIO可以设置输出速度,51单片机就没有该功能。因此本章主要针对STM32的GPIO特性进行讲解,其它MCU大同小异,稍微类比一下也能很快上手。
8.1.1STM32的GPIO
STM32F103ZET6一共有144个引脚,除去电源引脚、晶振时钟引脚、复位引脚、启动选择引脚、程序下载引脚(大部分为最小系统必须引脚),剩下的则是GPIO引脚。
STM32将这些众多按GPIOx(x为A、B、C等)分组,每组包含16个引脚,编号为0~15。STM32F103ZET6就有7组GPIO,每组16个引脚,即112个GPIO引脚。对于本开发板,读者可以通过《原理图》或表 3.4.1 查询每个引脚的用处,比如引脚名为PB0引脚,表示GPIOB组的0号引脚,在原理图的网络标识为LED_R,用于控制LED三色灯的红色控制引脚。
下图 8.1.1 为STM32F103系列GPIO的基本结构,左侧连接MCU内部,中间上半部分为输入,中间下半部分为输出,右侧为MCU引出的外设I/O引脚。
8.1.2 GPIO工作模式
STM32F103系列的I/O引脚共有8种工作模式,其中输出模式有四种:推挽输出、开漏输出、复用推挽输出、复用开漏输出;输入模式有四种:上拉输入、下拉输入、浮空输入、模拟输入。
1)推挽输出(Push-Pull,PP)
推挽结构由两个MOS管按互补对称的方式连接,任意时刻总是其中一个三极管导通,另一个三极管截止。如图 8.1.1 中①所示,内部由一个P-MOS管和一个N-MOS管组成,两个MOS管的栅极(Gate,G)接到了左侧“输出控制”,漏极(Drain,D)接到外部输出,P-MOS管的源极(Source,S)接到VDD(3.3V),N-MOS管的源极接到Vss(0V)。
MOS管作为开关使用,“输出控制”向两个MOS管栅极加一定电压,P-MOS管源极和漏极之间导通,VDD 经过P-MOS管的S->G->D输出,N-MOS管处于高阻态(电阻很大,近似开路),整体对外为高电平;“输出控制”取消向两个MOS管栅极施加电压,P-MOS管源极和栅极截止,P-MOS管处于高阻态,N-MOS管源极和漏极导通,整体对外为低电平。
推挽模式,让“输出控制”变为了VDD/Vss输出,使得输出电流增大,提高了输出引脚的驱动能力,提高了电路的负载能力和开关的动作速度。
2)开漏输出(Open-Drain,OD)
开漏模式下,“输出控制”不会控制P-MOS管,“输出控制”只会向N-MOS管栅极加一定电压,两个MOS管都处于截止状态,两个漏极处于悬空状态,称之为漏极开路。“输出控制”取消栅极的施加电压,P-MOS管依旧处于高阻态,N-MOS管导通,整体对外为低电平。
开漏输出模式可以等效将图 8.1.2 中灰色框的P-MOS管看作不存在。即该模式下只能输出低电平,若要输出高电平,则需要外接电阻,所接的电阻称为上拉电阻,此时输出电平取决于此时上拉电阻的外部电源电压情况,如图 8.1.2 中蓝色框的外部电路。
推挽输出模式可以直接输出高电平,开漏输出模式需要外接上拉电阻才能输出高电平,但开漏输出拥有一些推挽输出不具有的特性:
①利用外部电路驱动能力。如图 8.1.2 所示,“输出控制”只需要提供一个很小的栅极驱动电流,VCC经过上拉电阻为外部负载提高驱动电流;
②实现电平转换。推挽输出模式由VDD提供,即只能提供3.3V电平。使用开漏输出模式后,VCC可以为5V,从而实现了电平转换的效果。
③方便实现“逻辑与”功能。多个开漏的引脚可以直接并在一起使用,统一接一个合适的上拉电阻,就可以实现“逻辑与”关系,即当所有引脚均输出高电平时,输出才为高电平,若任一引脚输出低电平,则输出低电平。在I2C、SMBUS等总线电路中经常会用到。
3)复用功能推挽/开漏输出(Alternate Function,AF)
GPIO引脚除了作为通用输入/输出引脚使用外,还可以作为片上外设(USART、I2C、SPI等)专用引脚,即一个引脚可以有多种用途,但同一时刻一个引脚只能使用复用功能中的一个。
当引脚设置为复用功能时,可选择复用推挽输出模式或复用开漏输出模式,在设置为复用开漏输出模式时,需要外接上拉电阻。
4)上拉输入模式(Input Pull-up)
如图 8.1.1 中②所示,VDD经过开关、上拉电阻,连接外部I/O引脚。当开关闭合,外部I/O无输入信号时,默认输入高电平。
该模式的典型应用就是外接按键,当没有按键按下时候,MCU的引脚为确定的高电平,当按键按下时候,引脚电平被拉为低电平。
5)下拉输入模式(Input Pull-down)
如图 8.1.1 中②所示,Vss经过开关、下拉电阻,连接外部I/O引脚。当开关闭合,外部I/O无输入信号时,默认输入低电平。
6)浮空输入模式(Floating Input)
如图 8.1.1 中②所示,两个上/下拉电阻开关均断开,既无上拉也无下拉,I/O引脚直接连接TTL肖特基触发器,此时I/O引脚浮空,读取的电平是不确定的,外部信号是什么电平,MCU引脚就输入什么电平。MCU复位上电后,默认为浮空输入模式。
7)模拟输入模式(Analog mode)
如图 8.1.1 中②所示,两个上/下拉电阻开关均断开,同时TTL肖特基触发器开关也断开,引脚信号直接连接模拟输入,实现对外部信号的采集。
8.1.3GPIO输出速度
STM32的I/O引脚工作再输出模式下时,需要配置I/O引脚的输出速度。该输出速度不是输出信号的速度,而是I/O口驱动电路的响应速度。
STM32提供三个输出速度:2MHz、10MHz、50MHz。实际开发中需要结合实际情况选择合适的相应速度,以兼顾信号的稳定性和低功耗。通常,当设置为高速时,功耗高、噪声大、电磁干扰强;当设备为低速时,功耗低、噪声小、电磁干扰弱。
通常简单外设,比如LED灯、蜂鸣器灯,建议使用2MHz的输出速度,而复用为I2C、SPI等通信信号引脚时,建议使用10MHz或50MHz以提高响应速度。
8.2硬件设计
LED(Light Emitting Diode,发光二极管),是一种能够将电能转化为可见光的半导体器件,当给P极施加正向电压后,空穴和自由电子在P-N结复合,辐射出光子而发光。如图 8.2.1 所示为目前市面常见的LED灯,第一个是插件LED灯,第二个是贴片LED灯,第三个是贴片三色LED灯,由三个红、绿、蓝小灯组成,后面可以通过PWM实验控制每个灯亮度,实现颜色组合,更具可玩性。
如图 8.2.2 所示为开发板三色LED灯部分的原理图,LED灯的正极直接连接了VDD_3V3,LED灯的负极分别连接了三个GPIO引脚,红色LED连接的PB0,绿色LED连接的PB1,蓝色LED连接的PB5,只需要控制PB0、PB1、PB5为相应低电平,即可点亮对应LED灯,输出为高电平时熄灭对应LED灯。
8.3软件设计
本节将会对软件设计中关键部分进行讲解,完整工程请参考本章配套工程。同时,为了方便移植与代码阅读,完整工程中将LED部分的驱动代码新建独立,源代码文件与头文件分别是“driver_led.c”,“driver_led.h”,保存在工程文件夹“Driver”中。
8.3.1软件设计思路
实验目的:体验嵌入式的“Hello Word”,点亮LED灯。
1)选择LED对应的GPIO;
2)使能所选择GPIO的时钟;
3)配置其为上拉输出模式;
4)控制其输出高或低来控制LED的亮与灭;
本实验配套代码位于“5_程序源码\2_GPIO—LED点灯”。
8.3.2软件设计讲解
1)GPIO选择与接口定义
宏定义GPIO接口的作用是,当实际设计中的LED对应的GPIO发生变化时,只需在宏定义处改变GPIO的值即可完成对整个LED设计的修改,这样就增强了可移植性。代码段 8.3.1 引脚宏定义(driver_led.h)
/*********************
* 引脚宏定义
**********************/
#define R_LED_GPIO_PIN GPIO_PIN_0
#define R_LED_GPIO_PORT GPIOB
#define R_LED_GPIO_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()
#define G_LED_GPIO_PIN GPIO_PIN_1
#define G_LED_GPIO_PORT GPIOB
#define G_LED_GPIO_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()
#define B_LED_GPIO_PIN GPIO_PIN_5
#define B_LED_GPIO_PORT GPIOB
#define B_LED_GPIO_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()
根据原理图可知,RGB灯三色对应的引脚分别是:红灯-PB0、绿灯-PB1、蓝灯-PB5。因此得到代码段 8.3.1 的引脚和引脚对应时钟使能函数的宏定义,当用户工程LED引脚发生变化时,只需在此处做出修改即可。然后是对三个GPIO的输出函数进行接口宏定义以方便移植与阅读,如代码段 8.3.2 所示。
/*
* LED亮灭函数宏定义
*/
#define ON GPIO_PIN_RESET
#define OFF GPIO_PIN_SET
#define RLED(flag) HAL_GPIO_WritePin(R_LED_GPIO_PORT, R_LED_GPIO_PIN, flag)
#define GLED(flag) HAL_GPIO_WritePin(G_LED_GPIO_PORT, G_LED_GPIO_PIN, flag)
#define BLED(flag) HAL_GPIO_WritePin(B_LED_GPIO_PORT, B_LED_GPIO_PIN, flag)
HAL库中,对GPIO的输出控制函数是:
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
第一个参数 GPIOx表示GPIOA/B/C/D/E…/H中某一组端口,此处我们的实验是GPIOB,但是为方便移植我们使用宏定义的端口R_LED_GPIO_PORT、G_LED_GPIO_PORT、B_LED_GPIO_PORT;
第二个参数GPIO_Pin表示在某组端口中的某一个引脚,与选择端口类似,我们选择已宏定义好的R _LED_GPIO_PIN、G _LED_GPIO_PIN、B _LED_GPIO_PIN;
第三个参数PinState表示对这个IO控制输出的状态,是一个枚举类型,包含两个成员:GPIO_PIN_RESET和GPIO_PIN_SET。因为低电平亮灯,所以定义ON对应GPIO_PIN_RESET,OFF对应GPIO_PIN_SET。
2)GPIO的初始化
当选择好LED对应的GPIO后,还需要对其进行初始化,以完成对这些GPIO时钟的使能,工作模式的选择以及输出速度的设置。
代码段 8.3.3 GPIO初始化(driver_led.c)
/*
* 函数名:void LedGpioInit(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:初始化LED的引脚,配置为上拉推挽输出
*/
void LedGpioInit(void)
{
// 定义GPIO的结构体变量
GPIO_InitTypeDef GPIO_InitStruct = {
0};
// 使能LED的GPIO对应的时钟
R_LED_GPIO_CLK_EN();
G_LED_GPIO_CLK_EN();
B_LED_GPIO_CLK_EN();
GPIO_InitStruct.Pin = R_LED_GPIO_PIN | G_LED_GPIO_PIN | B_LED_GPIO_PIN; // 选择LED的引脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 设置为推挽输出模式
GPIO_InitStruct.Pull = GPIO_PULLUP; // 默认上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;// 引脚反转速度设置为快
// 初始化引脚配置
HAL_GPIO_Init(R_LED_GPIO_PORT, &GPIO_InitStruct);
// 默认LED灭:OFF-灭,ON-亮
RLED(OFF);
GLED(OFF);
BLED(OFF);
}
在此函数中,首先定义了一个结构体变量GPIO_InitStruct,先看看这个结构体中都有哪些成员。代码段 8.3.4 结构体GPIO_InitStruct定义(stm32f1xx_hal_gpio.h)
/**
* @brief GPIO Init structure definition
*/
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;
- 6行:第一个成员pin是引脚选择;
- 9行:第二个成员mode是模式选择:输出、输入、复用、中断、事件;
- 12行:第三个成员是上拉、下拉、悬空选择;
- 15行:第四个成员是输出速度选择;
当我们不清楚这些成员可以设置成哪些值时,可以看到每个成员的注释的最后有个@ref xxx,比如第7行末尾的“@ref GPIO_pins_define”,我们只需要选中这个GPIO_pins_define,然后按Ctrl+f寻找这个GPIO_pins_define,就会找到例如pin如下宏定义。
代码段 8.3.5 GPIO引脚宏定义(stm32f1xx_hal_gpio.h)
/** @defgroup GPIO_pins_define GPIO pins define
* @{
*/
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */
#define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */
#define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */
#define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */
#define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */
#define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */
#define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */
#define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */
#define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */
#define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */
#define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */
#define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */
#define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
#define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */
#define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */
#define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */
#define GPIO_PIN_All ((uint16_t)0xFFFF) /* All pins selected */
#define GPIO_PIN_MASK 0x0000FFFFu /* PIN mask for assert test */
这里只需要选择其中需要的宏定义即可。然后使用三个宏定义的时钟使能函数使能了选择的GPIO的时钟。用上述设置GPIO_InitStruct成员讲解的方法设置每个成员的值之后,使用下述函数对选择的某组端口的GPIO引脚进行初始化。
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
此处我们选择的是红灯的端口,但是在程序中我们的引脚选择是红绿蓝三个引脚都选择且进行了初始化,且红绿蓝三个灯所对应的端口均是GPIO B,所以实际上这个初始化函数是将三个LED的引脚都完成了初始化。
3)GPIO输出控制
在初始化函数的末尾,我们使用了宏定义的IO控制接口控制三个IO输出高电平,即让三灯均呈熄灭状态:代码段 8.3.6 控制LED灯熄灭(driver_led.c)
// 默认LED灭:OFF-灭,ON-亮
RLED(OFF);
GLED(OFF);
BLED(OFF);
4)HAL库延时
HAL库使用系统滴答定时器(此定时器在后序章节中详细讲解)封装了一个延时函数:
__weak void HAL_Delay(uint32_t Delay)
在不对系统滴答定时器进行重新定义的情况下,此延时函数的效果是ms级,即延时Delay个毫秒。
5)整体控制逻辑
代码段 8.3.7 LED灯主函数(main.c)
int main(void)
{
// 初始化HAL库函数必须要调用此函数
HAL_Init();
// 系统时钟即AHB/APB时钟配置
SystemClock_Config();
// 初始化LED
LedGpioInit();
// 点亮三色灯
RLED(ON);
HAL_Delay(1000);
RLED(OFF);
GLED(ON);
HAL_Delay(1000);
GLED(OFF);
BLED(ON);
HAL_Delay(1000);
BLED(OFF);
while(1)
{
RLED(ON);
HAL_Delay(1000);
RLED(OFF);
HAL_Delay(1000);
}
}
- 4行:对HAL库的初始化,这一步的作用是初始化中断优先级组别以及对系统滴答定时器进行默认的初始化;
- 6行:时钟初始化,本实验所选择的是外部高速时钟,最终配置为72MHz系统时钟,关于时钟配置在后一章节讲解;
- 9行:初始化LED;
- 11~22行:初始控制逻辑:只红灯亮->只绿灯亮->只蓝灯亮;
- 24~30行:死循环逻辑:闪烁红灯,周期是2s;
8.4实验效果
开发板三色LED灯,只红灯亮->只绿灯亮->只蓝灯亮,最后红灯间隔1秒闪烁。
百问网技术论坛:
http://bbs.100ask.net/
百问网嵌入式视频官网:
https://www.100ask.net/index
百问网开发板:
淘宝:https://100ask.taobao.com/
天猫:https://weidongshan.tmall.com/
技术交流群(鸿蒙开发/Linux/嵌入式/驱动/资料下载)
QQ群:869222007(已满)752871361
单片机-嵌入式Linux交流群:
QQ群:536785813
转载:https://blog.csdn.net/thisway_diy/article/details/115695101