GPIO通用输入输出口-stm32入门

1. GPIO 简介

GPIO(General Purpose Input Output)通用输入输出口

  • 引脚电平:0V~3.3V,部分引脚可容忍5V(可以在这个端口输入 5V 电压,也认为是高电平)(数据 1 就是高电平,数据 0 就是低电平)

对于输出而言,最大只能输出 3.3V,因为供电就只有 3.3V

  • 可配置为8种输入输出模式
    • 输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议(比如 I2C、SPI 或者某个芯片特定的协议)输出时序等
    • 输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等

2. GPIO 基本结构

在这里插入图片描述

  1. APB2 外设总线:在 STM32 中,所有的 GPIO 都是挂载在 APB2 外设总线上的。
  2. GPIO 外设的名称 按照 GPIOA 、GPIOB、GPIOC 等等来命名的,每个 GPIO 外设总共有 16 个引脚,编号是从 0 到 15。 GPIOA 的第 0 号引脚,我们一般把它称作 PA0,接着第 1 号就是 PA1,以此类推到 PA15;GPIOB 也一样,从 PB0 到 PB15 来命名的。
  3. 每个 GPIO 模块内主要包含 寄存器和驱动器这些东西,寄存器是一段特殊的存储器,内核可以通过 APB2 总线对寄存器进行读写,这样就可以完成输出电平和读取电平的功能了,寄存器每一位对应一个引脚,输出寄存器写 1,对应引脚输出高电平,写 0,输出低电平;输入寄存器读取为 1,证明对应端口目前为高电平,读取为 0,就是低电平。
  4. STM32 内部的寄存器都是 32 位的,但端口只有 16 位,所以寄存器只有低16位对应有端口,高 16 位没有用到。
  5. 驱动器增加信号驱动能力,寄存器只负责存储数据。

GPIO 位结构

在这里插入图片描述

  1. 左边为寄存器,中间为驱动,右边为 IO 引脚;上面为输入部分,下面为输出部分。
  2. 输入部分:
    • 两个保护二极管对输入电压进行限幅,上端接 3.3 V,下端接 地,避免过高或过低的电压对内部这些电路产生伤害。只有输入电压在 0~3.3V 之间,两个二极管均不会导通,此时二极管对电路没有影响。
    • 接下来连接一个上拉电阻和下拉电阻,上拉电阻至 VDD,下拉电阻至 VSS,这个开关可以通过程序进行配置的。如果上面导通,下面断开,就是上拉输入模式;如果下面导通,上面断开,就是下拉输入模式;如果两个都断开,就是浮空输入模式。(上拉和下拉是为了给输入提供一个默认的输入电平的,因为对于一个数字的端口,输入不是高电平就是低电平,如果输入引脚啥都不接,这是输入就会处于一种浮空的状态,引脚的输入电平极易受外界干扰而改变,为了避免引脚悬空导致的输入数据不确定,就需要在这里加上上拉电阻或者下拉电阻。如果接上上拉电阻,当引脚悬空时,如果上拉电阻来保证引脚的高电平,所以上拉输入又可以称作是默认的高电平的输入模式;下拉同理,就是默认为低电平的输入方式。这里上拉电阻和下拉电阻的阻值比较大,是一种弱上拉和弱下拉,目的是尽量不影响正常的输入操作。
    • 接着是施密特触发器,作用是对输入电压进行整形的,它的执行逻辑是:如果输入电压大于某一阈值,输出就会瞬间升为高电平;如果输入电压小于某一阈值,输出就会瞬间降为低电平。因为这个引脚的波形是外界输入的,虽然是数字信号,实际情况下可能会产生各种失真,是一个夹杂了波动的高低变化电平信号,如果没有施密特触发器,很有可能因为干扰而导致误判,如果有了施密特触发器,定一个阈值的上限和下限,高于上限输出高,低于下限输出低。使用两个比较阈值进行判断,中间留有一定的变化范围,这样可以有效的避免因信号波动造成的输出抖动现象,相比较输入信号,经过整形的信号就会变得很完美了。
    • 经过施密特触发器整形的波形就可以直接写入输入数据寄存器了,我们再用程序读取输入数据寄存器对应某一位的数据,就可以知道端口的输入电平了。
    • 最后上面还有两路线路,就是连接到片上外设的一些端口。其中有模拟输入,这个连接到 ADC 上,因为 ADC 需要接收模拟量,所以这根线是接到施密特触发器前面的;另一个是复用功能输入,这个是连接到其他需要读取端口的外设上的,比如串口的输入引脚等,这根线接收的是数字量,所以在施密特触发器后面。
  3. 输出部分:
    • 数字部分可以由输出数据寄存器或片上外设控制。两种控制方式通过数据选择器接到了输出控制部分。如果选择通过输出数据寄存器进行控制,就是普通的 IO 口输出,写这个数据寄存器的某一位就可以操作对应的某个端口了。那左边还有个叫做 位设置 / 清除寄存器,这个可以用来单独操作输出数据寄存器的某一位,而不影响其他位,因为这个输出数据寄存器同时控制 16 个端口,并且这个寄存器只能整体读写,所以如果想单独控制其中某一个端口而不影响其他端口的话,就需要一些特殊的操作方式。(第一种方式:先读出这个寄存器,然后用 按位与 和 按位或 的方式更改某一位,最后再将更改后的数据写回去,这种方法比较麻烦,效率不高,对于 IO 口的操作而言不太合适;第二种方式(库函数方法):通过设置这个 位设置 / 清除寄存器,如果我们要读某一位进行置 1 的操作,在 位设置寄存器的对应位写 1 即可,剩下不需要操作的位 写 0,这样它内部就会有电路,自动将输出数据寄存器中对应位置为 1,而剩下写 0 的位则保持不变,这样就保证了只操作其中某一位而不影响其他位,并且这是一步到位的操作。如果想对某一位进行清 0 的操作,在位清除寄存器的对应位写 1 即可,这样内部电路就把这一位清 0 了,这就是 位设置 / 清除寄存器的作用;第三种方式:读写 STM32 中的 “位带” 区域,在 STM32 中专门分配有一段地址区域,这段地址映射了 RAM 和外设寄存器所有的位,读写这段地址的数据,相当于读写所映射位置的某一位,这就是位带的操作方式。
    • 接下来输出控制之后就接到了两个 MOS 管,上面是 P-MOS,下面是 N-MOS,这个 MOS 管就是一种电子开关,我们的信号来控制开关的导通和关闭,开关负责将 IO 口接到 VDD 或者 VSS,可以选择 推挽、开漏和关闭三种输出方式。(在推挽输出模式下,P-MOS 和 N-MOS 均有效,数据寄存器为 1 时,上管导通,下管断开,输出直接接到 VDD,就是输出高电平;数据寄存器为 0 时,上管断开,下管导通,输出直接接到 VSS,就是输出低电平。这种模式下,高低电平均有较强的驱动能力,所以推挽输出模式也叫强推输出模式,STM32 对 IO 口具有绝对的控制器,高低电平都有 STM32 说的算;在开漏输出模式下,这个 P-MOS 是无效的,只有 N-MOS 在工作,数据寄存器为 1 时,下管断开,这时输出相当于断开,也就是高阻模式;数据寄存器为 0 时,下管导通,输出直接接到 VSS,也就是输出低电平。这种模式下只有低电平有驱动能力,高电平是没有驱动能力的,这个开漏模式可以作为通信协议的驱动方式,比如 I2C 通信的引脚,在多机通信的情况下,这个模式可以避免各个设备相互干扰,另外开漏模式还可以用于输出 5V 的电平信号,比如在 IO 口外接一个上拉电阻到 5V 的电源,当输出低电平时,由内部的 N-MOS 直接接 VSS,当输出高电平时,由外部的上拉电阻拉高至 5V,这样就可以输出 5V 的电平信号,用于兼容一些 5V 电平的设备;剩下一种状态就是关闭,这个是当引脚配置为输入模式的时候,这两个 MOS 管都无效,也就是输出关闭,端口的电平由外部信号来控制。

3. GPIO 的八种工作模式

通过配置GPIO的端口配置寄存器,上面的位结构电路就会根据我们的配置进行改变,比如开关的通断、N-MOS 和 P-MOS 是否有效、数据选择器的选择等,端口可以配置成以下8种模式:

模式名称性质特征
浮空输入数字输入可读取 引脚/端口 电平,若引脚悬空,则电平不确定
上拉输入数字输入可读取 引脚/端口 电平,内部连接上拉电阻,悬空时默认高电平
下拉输入数字输入可读取 引脚/端口 电平,内部连接下拉电阻,悬空时默认低电平
模拟输入模拟输入GPIO无效,引脚直接接入内部ADC
开漏输出数字输出可输出引脚电平,高电平为高阻态,低电平接VSS
推挽输出数字输出可输出引脚电平,高电平接VDD,低电平接VSS
复用开漏输出数字输出由片上外设控制,高电平为高阻态,低电平接VSS
复用推挽输出数字输出由片上外设控制,高电平接VDD,低电平接VSS

除了模拟输入会关闭数字的输入功能,在其他的 7 个模式中,所有的输入都是有效的。

在使用浮空输入时,端口一定要接上一个连续的驱动源,不能出现悬空的状态。

3.1 浮空/上拉/下拉输入

在这里插入图片描述
在输入模式下,输出驱动器是断开的,端口只能输入而不能输出,上面两个电阻(上拉工作/下拉工作/都不工作)控制工作模式,然后输入通过施密特触发器经过波形整形之后,连接到输入数据寄存器中。

右边的输入保护上面写的是 VDD 或者 VDD_FT,这就是 3.3V 端口和容忍 5V 端口的区别。这个容忍 5V 的引脚,它的上边保护二极管要做一下处理,要不然这里直接接 VDD 3.3V 的话,外部再接入 5V 电压就会导致上边二极管开启,并且产生比较大的电流,这是不太妥当的。

3.2 模拟输入

在这里插入图片描述
模拟输入可以说是 ADC 模数转换器的专属配置了,输出是断开的,输入的施密特触发器也是关闭的无效状态,所以整个 GPIO 都是没用的,只剩下从引脚直接接入片上外设,也就是 ADC,所以当我们使用 ADC 的时候,将引脚配置为模拟输入就行了,其他时候一般用不到模拟输入。

3.3 开漏/推挽输出

在这里插入图片描述
输出是由输出寄存器控制,如果 P-MOS 无效,就是开漏输出,如果 P-MOS 和 N-MOS 都有效,就是推挽输出。

另外我们可以看到,在输出模式下,输入模式也是有效的;但是在输入模式下,输出都是无效的。这是因为一个端口只能有一个输出,但是可以有多个输入,所以当配置成输出模式的时候,内部也可以顺便输入一下,这个也是没啥影响的。

区别就是开漏输出的高电平呈现的是 高阻态,没有驱动能力;而推挽输出的高低电平都是具有驱动能力的。

3.4 复用开漏/复用推挽输出

在这里插入图片描述
通用输出没有连接,引脚的控制权转移到片上外设,由片上外设来控制,在输入部分,片上外设也可以读取引脚的电平,同时普通的输入也是有效的,顺便接收一下电平信号。

跟普通输出模式差不多,只不过复用的输出,引脚电平是由片上外设控制的。

4. 三个输出示例程序

4.1 LED 闪烁

4.1.1 硬件电路搭建

硬件电路图:
在这里插入图片描述

  1. 首先将最小系统板插在面包板上。
  2. 接着拿出跳线,将最小系统板正负极接到面包板的供电引脚上,这样上下四排供电引脚就可以通过最小系统板获取电源了。
  3. 把 STLINK 按照上面的引脚标识符,连接到最小系统上。
  4. 最后我们拿出一个 LED,长脚正极接到正极供电孔,短脚负极接到 PA0 端口上。(使用低电平点亮的操作方式,为了方便,没有接限流电阻)

整个系统的供电是 STLINK 的 3.3V 接到最小系统,然后最小系统又通过跳线接到上下4排供电孔中。

4.1.2 库函数

操作 STM32 的 GPIO 总共需要 3 个步骤:

  1. 使用 RCC 开启 GPIO 时钟。
  2. 使用 GPIO_Init 函数初始化 GPIO。
  3. 使用输出或者输入的函数控制 GPIO 口。

总共涉及 RCC 和 GPIO 两个外设。

  • RCC 常用的库函数
//RCC AHB外设时钟控制(第一个参数:选择哪个外设;第二个参数:ENABLE/DISABLE(使能/失能))
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
//RCC APB2外设时钟控制(第一个参数:选择哪个外设;第二个参数:ENABLE/DISABLE(使能/失能))
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
//RCC APB1外设时钟控制(第一个参数:选择哪个外设;第二个参数:ENABLE/DISABLE(使能/失能))
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);

如果不清楚哪个外设连接在哪个总线上,可以在外设参数列表上找。列表上出现了肯定就是这个总线的外设。

  • GPIO 常用的库函数
void GPIO_DeInit(GPIO_TypeDef* GPIOx);//复位所指定的 GPIO 外设
void GPIO_AFIODeInit(void);//复位 AFIO 外设

//用结构体参数初始化 GPIO 口,非常重要
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);//结构体变量赋默认值

//GPIO 读取函数,实现读功能
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//读取输入数据寄存器某一个端口的输入值,返回值代表这一端口的高低电平
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);//读取整个输入数据寄存器,返回值是一个 16 位的数据,每一位代表一个端口值
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//读取输出数据寄存器某一个端口的输入值,返回值代表这一端口的高低电平
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);//读取整个输出数据寄存器,返回值是一个 16 位的数据,每一位代表一个端口值
//从原则上来说,该俩函数并不是用于读取端口的输入数据的,一般用于输出模式下,用来看一下自己输出的是什么

//GPIO 写入函数,实现写功能
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//把指定端口设置为 高电平
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//把指定端口设置为 低电平
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);//根据第 3 个参数的值设置指定的端口
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);//可以同时对 16 个端口进行写入操作

//暂时不会用到
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_EventOutputCmd(FunctionalState NewState);
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);

想读取 GPIO 口的话,需要用 ReadInput 的这两个函数;如果在输出模式下,想要看一下现在输出了什么,才需要用到 ReadOutput 的这两个函数。

  • GPIO 八种工作模式
typedef enum
{ GPIO_Mode_AIN = 0x0,//模拟输入
  GPIO_Mode_IN_FLOATING = 0x04,//浮空输入
  GPIO_Mode_IPD = 0x28,//下拉输入
  GPIO_Mode_IPU = 0x48,//上拉输入
  GPIO_Mode_Out_OD = 0x14,//开漏输出
  GPIO_Mode_Out_PP = 0x10,//推挽输出
  GPIO_Mode_AF_OD = 0x1C,//复用开漏
  GPIO_Mode_AF_PP = 0x18//复用推挽
}GPIOMode_TypeDef;

这种 Init 函数基本在 STM32 中基本所有的外设都有,一般我们初始化外设基本都是使用 Init 函数来完成的。

  • GPIO 输出速度

GPIO 的输出速度 可以限制输出引脚的最大翻转速度,设计出来是为了 低功耗 和 稳定性。一般要求不高的时候配置成 50 MHz 就可以了。

//00:输入模式(复位后的状态)
typedef enum
{ 
  GPIO_Speed_10MHz = 1,// 01: 输出模式,最大速度 10MHz
  GPIO_Speed_2MHz, // 10: 输出模式:最大速度 2MHz
  GPIO_Speed_50MHz// 11: 输出模式:最大速度 50MHz
}GPIOSpeed_TypeDef;
  • Delay 延时函数

在工程文件和 项目文件夹中新建 System 文件夹,拷贝并导入 Delay.h 和 Delay.c 文件,并且在 Include Paths 内声明包含头文件的文件夹 System。

void Delay_us(uint32_t us);//微秒级
void Delay_ms(uint32_t ms);//毫秒级
void Delay_s(uint32_t s);//秒级

4.1.3 示例代码

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

int main(void) {
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//点灯使用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//使用 GPIOA 外设的 0 号引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//输出速度选择 50MHz
	//初始化主要执行逻辑:读取结构体参数,执行判断和运算,最后写入 GPIO 配置寄存器
	GPIO_Init(GPIOA, &GPIO_InitStructure);//执行完自动将 PA0 引脚配置为推挽输出、50MHz
	
	//GPIO_ResetBits(GPIOA, GPIO_Pin_0);//置低电平
	//GPIO_SetBits(GPIOA, GPIO_Pin_0);//置高电平
	//GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);//置低电平
	//GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);//置高电平
	
	while(1){
		GPIO_ResetBits(GPIOA, GPIO_Pin_0);//置低电平
		Delay_ms(1000);//延时 1000 ms
		GPIO_SetBits(GPIOA, GPIO_Pin_0);//置高电平
		Delay_ms(1000);//延时 1000 ms
		
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);//置低电平
		Delay_ms(1000);//延时 1000 ms
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);//置高电平
		Delay_ms(1000);//延时 1000 ms
		
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)0);//使用 0 
		Delay_ms(1000);//延时 1000 ms
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)1);//使用 1
		Delay_ms(1000);//延时 1000 ms
	}
}

注意事项:

  • 在一些旧的编译器中,要求所有的局部变量定义必须放在函数的最前面。
  • 文件夹中 keilkill.bat 是一个批处理文件,可以把工程编译产生的中间文件都删掉。因为工程编译产生的文件比较大。

推挽输出和开漏输出的驱动问题:

  • 在推挽输出的模式下,高低电平都是用驱动能力的(p-mos 和 n-mos 都有效)。
  • 在开漏输出的模式下,高电平是没有驱动能力的,低电平是有驱动能力的(高电平是高阻态无效,低电平有效)。
  • 一般输出用推挽输出就可以,特殊的地方才会用到开漏输出的模式。

4.2 LED 流水灯

4.2.1 硬件电路搭建

硬件电路图:
在这里插入图片描述
接线时先接两边再接中间会容易些。

外设和引脚可以用 按位或( | )一次选择/设置多个。

4.2.2 示例代码

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

int main(void) {
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	/*GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 \
	| GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;//使用 GPIOA 外设的 0~7 号引脚*/
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;//把GPIOA 外设的 16 个端口全部使用
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	
	while(1){
		GPIO_Write(GPIOA, ~0x0001);//0000 0000 0000 0001
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0002);//0000 0000 0000 0010
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0004);//0000 0000 0000 0100
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0008);//0000 0000 0000 1000
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0010);//0000 0000 0001 0000
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0020);//0000 0000 0010 0000
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0040);//0000 0000 0100 0000
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0080);//0000 0000 1000 0000
		Delay_ms(500);
	}
}

GPIO_Write 函数同时对 16 个端口进行写入操作,第二个参数指定写到输出数据寄存器的值,会直接写到 GPIO 的 ODR 寄存器中。

注意事项:

  • C语言不支持直接写 二进制,因此一般写 十六进制表示。
  • 16 位二进制数 从低位到高位 依次是 0 号到 15 号

4.3 蜂鸣器

4.3.1 硬件电路搭建

硬件电路图:
在这里插入图片描述

VCC 正极接到正极供电孔,GND 负极接到负极供电孔,IO 控制极选择一个 IO 口接上就行。

A15、B3、B4 这三个口先别选,默认是 JTAG 的调试端口,要用做普通端口还需要进行一些配置

4.3.2 示例代码

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

int main(void) {
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;//使用 GPIOB 外设的 12 号引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	while(1){
		GPIO_ResetBits(GPIOB, GPIO_Pin_12);
		Delay_ms(500);
		GPIO_SetBits(GPIOB, GPIO_Pin_12);
		Delay_ms(500);
	}
}

4.3.3 使用库函数的方法

  1. 先打开 .h 文件,看一下有哪些库函数,再右键转到定义,查看一下函数和参数的用法。
  2. 打开库函数用户手册,有所有函数的介绍和使用方法,但是用户手册的版本并不对应我们所用的库函数的版本(V3.5.0)。有部分用法会有些出入,但是整体上的差异都不大。
  3. 固件库压缩包中的帮助文档(英文)。
  4. 百度搜索,参考别人代码。

6. 两个输入示例程序

6.1 按键控制 LED

6.1.1 硬件电路搭建

硬件电路图:
在这里插入图片描述

  1. 按键一端接 GPIO 口,另一端接 GND
  2. LED一端接 GPIO 口,另一端接 VCC,就是低电平点亮的接法。

6.1.2 模块化编程

如果把 LED 和按键的驱动代码都混在主函数里面,那代码太乱了,不容易管理,也不容易移植。

概念: 将驱动代码封装起来,单独放在 .c 和 .h 文件里。

作用

  • 简化主函数的逻辑
  • 有利于移植程序
  • 有利于我们进行分工合作

注意:要做封装,函数的注释需要写清楚,方便使用者快速上手。

  1. 打开工程文件夹,再建一个文件夹(Hareware),用来存放硬件驱动,在 keil 中执行相同的操作并添加头文件。
  2. 在 Hareware 文件夹中添加 .c 和 .h 文件用来封装 LED 的驱动程序,.c 文件用来存放驱动程序主体代码,.h 文件用来存放驱动程序可以对外提供的函数或变量的声明。文件创建好后添加必要的初始化代码。

.h 文件初始化

#ifndef __LED_H
#define __LED_H



#endif

注意,文件要以空行结尾

.c 文件初始化

#include "stm32f10x.h"                  // Device header



在 英文输入法 下,使用快捷键 CTRL + 空格 / CTRL + Alt + 空格 可以弹出代码提示框。

  1. 封装 LED 驱动程序模块的代码。

LED.h 文件封装

#ifndef __LED_H
#define __LED_H

//模块化声明,函数可以被外部调用
void LED_Init(void);//LED 初始化函数
void LED1_ON(void);//LED1 亮
void LED1_OFF(void);//LED1 灭
void LED1_Turn(void);//LED1 翻转
void LED2_ON(void);//LED2 亮
void LED2_OFF(void);//LED2 灭
void LED2_Turn(void);//LED2 翻转

#endif

模块化声明,函数可以被外部调用

LED.c 文件封装

#include "stm32f10x.h"                  // Device header

void LED_Init(void){
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);//GPIO 配置好默认低电平
	
	GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2);//将 GPIO 配置为高电平
}

void LED1_ON(void) {
	GPIO_ResetBits(GPIOA, GPIO_Pin_1);
}

void LED1_OFF(void) {
	GPIO_SetBits(GPIOA, GPIO_Pin_1);
}

void LED1_Turn(void) {
	if(GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_1) == 0) {//读取当前输出状态
		GPIO_SetBits(GPIOA, GPIO_Pin_1);
	} 
	else {
		GPIO_ResetBits(GPIOA, GPIO_Pin_1);
	}
}

void LED2_ON(void) {
	GPIO_ResetBits(GPIOA, GPIO_Pin_2);
}

void LED2_OFF(void) {
	GPIO_SetBits(GPIOA, GPIO_Pin_2);
}

void LED2_Turn(void) {
	if(GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_2) == 0) {//读取当前输出状态
		GPIO_SetBits(GPIOA, GPIO_Pin_2);
	} 
	else {
		GPIO_ResetBits(GPIOA, GPIO_Pin_2);
	}
}

注意事项:

  1. GPIO 配置好默认为低电平
  2. GPIO_ReadOutputDataBit 函数读取当时的输出状态,可做输出状态的翻转。
  1. 封装 按键 驱动程序模块的代码。

KEY.h 文件封装

#ifndef __KEY_H
#define __KEY_H

void Key_Init(void);//按键 初始化函数
uint8_t Key_GetNum(void);//读取按键值/返回按下按键的键码
	
#endif

KEY.c 文件封装

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

void Key_Init(void) {
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//GPIO 的输出速度,在输入模式下无效
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}

uint8_t Key_GetNum(void) {//读取按键值/返回按下按键的键码
	uint8_t KeyNum = 0;//按键键码默认为 0,没有按键按下,返回 0
	if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) {//返回值 0 代表 低电平,按键按下,1 代表 高电平
		//按键刚按下,会有一个抖动,需要 Delay 一段时间
		Delay_ms(20);//消下抖
		while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);//如果按键一直按下,就卡在这里,直到松手
		Delay_ms(20);//消一下按键松手的抖动
		KeyNum = 1;
	}
	if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0) {
		Delay_ms(20);
		while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0);
		Delay_ms(20);
		KeyNum = 2;
	}
	return KeyNum;
}

注意事项:

  1. 按键配置为 输入模式
  2. GPIO_Speed 是 GPIO 的输出速度,在输入模式下无效
  3. GPIO_ReadInputDataBit 函数读取按键的输入状态,返回值 0 代表 低电平,按键按下;返回值 1 代表 低电平,按键松开。
  4. 按键在按下和松开时需要 延时函数 来消除抖动。
  1. main 函数实现。
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"

uint8_t KeyNum;

int main(void) {
	LED_Init();
	Key_Init();
	while(1){
		KeyNum = Key_GetNum();
		if (KeyNum == 1) {
			LED1_Turn();
		}
		if (KeyNum == 2) {
			LED2_Turn();
		}
	}
}

6.2 光敏传感器控制蜂鸣器

6.2.1 硬件电路搭建

硬件电路图:
在这里插入图片描述

光敏传感器 VCC、GND 接电源,DO 数字输出端,接 PB13 号口。

遮住光线时,输出指示灯灭,代表输出高电平;光线照射时,输出指示灯亮,代表输出低电平;通过电位器可以调节高低电平的判断阈值。

6.2.2 示例代码

  1. 蜂鸣器驱动程序的封装

Buzzer.h 文件封装

#ifndef __BUZZER_H
#define __BUZZER_H

void Buzzer_Init(void);
void Buzzer_ON(void);
void Buzzer_OFF(void);
void Buzzer_Turn(void);

#endif

Buzzer.c 文件封装

#include "stm32f10x.h"                  // Device header

void Buzzer_Init(void){
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	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 配置好默认低电平
	
	GPIO_SetBits(GPIOB, GPIO_Pin_12);//将 GPIO 配置为高电平
}

void Buzzer_ON(void) {
	GPIO_ResetBits(GPIOB, GPIO_Pin_12);
}

void Buzzer_OFF(void) {
	GPIO_SetBits(GPIOB, GPIO_Pin_12);
}

void Buzzer_Turn(void) {
	if(GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_12) == 0) {//读取当前输出状态
		GPIO_SetBits(GPIOB, GPIO_Pin_12);
	} 
	else {
		GPIO_ResetBits(GPIOB, GPIO_Pin_12);
	}
}

  1. 光敏传感器驱动程序的封装

LightSensor.h 文件封装

#ifndef __LIGHTSENSOR_H
#define __LIGHTSENSOR_H

void LightSensor_Init(void);// 光敏传感器 初始化函数
uint8_t LightSnesor_Get(void);//读取端口/返回端口值
	
#endif

LightSensor.c 文件封装

#include "stm32f10x.h"                  // Device header

void LightSensor_Init(void) {
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	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;//GPIO 的输出速度,在输入模式下无效
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}

uint8_t LightSnesor_Get(void) {//读取端口/返回端口值
	return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13);
}

输入模式可以选择 上拉输入/浮空输入

  • 如果这个模块始终都连接在端口上,可以选择浮空输入,只要保证引脚不会悬空即可。
  1. main 函数实现
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "Buzzer.h"
#include "LightSensor.h"

uint8_t KeyNum;

int main(void) {
	Buzzer_Init();
	LightSensor_Init();
	
	while(1){
		if (LightSnesor_Get() == 1) {//光线比较暗
			Buzzer_ON();
		}
		else {
			Buzzer_OFF();
		}
	}
}

7. GPIO 内容补充给

7.1 外设的 GPIO 配置

当我们使用片上外设的引脚时,可以参考 STM32F10xxx参考手册 的 8.1.11 中表里给的配置。

7.2 GPIO 寄存器描述

7.2.1 GPIO 配置寄存器

每一个端口的模式由 4 位进行配置,16 个端口就需要 64 位,所以这里的配置寄存器有两个,一个是端口配置低寄存器,一个是端口配置高寄存器

7.2.2 端口输入数据寄存器

低 16 位对应 16 个引脚,高 16 位没有使用。

7.2.3 端口输出数据寄存器

低 16 位对应 16 个引脚,高 16 位没有使用。

7.2.4 端口位设置/清除寄存器

高 16 位是进行位清除的,低 16 位是进行位设置的。写 1 就是设置或者清除,写 0 就是不产生影响。

7.2.5 端口位清除寄存器

低 16 位是进行位清除的,和上面寄存器的高 16 位功能是一样的,是为了方便操作设置的。

  • 如果只想单一的进行位设置或者位清除,那位设置时用上面这个寄存器,位清除时,用下面这个寄存器。在设置和清除时,使用的都是低 16 位的数据,方便一些。
  • 如果想对多个端口同时进行位设置和位清除,使用上面那个寄存器就可以,这样可以保证位设置和位清除的同步性;如果对信号的同步性要求不高的话,先位设置再位清除也没问题)

7.2.6 端口配置锁定寄存器

可以对端口的配置进行锁定,防止意外更改。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值