重要的内容写在前面:

  1. 该系列是以up主江协科技的STM32视频教程为基础写下去的,大部分内容都参考了老师的课件,对于一些个人认为比较重要但是老师仅口述的部分,笔者都有用文字的方式记录并标出了重点。
  2. 文中的图片基本都来源于老师的课件以及开发板和芯片的手册,粘贴过来是为了方便阅读。
  3. 如果有条件的可以先学习一些相关课程再去看STM32的教程,学起来会更加轻松(不太建议零基础开始直接STM32,听起来可能会有点困难,可以先学51单片机),相关课程有数字电路(强烈推荐先学数电,不然可能会有很多地方理解起来很困难)、模拟电路、计算机组成原理(像寄存器、存储器、中断等在这门课里有很详细的介绍)、计算机网络等。
  4. 如有错漏欢迎指出。

视频链接:[3-1] GPIO输出_哔哩哔哩_bilibili

一、GPIO外设介绍

1、概述

(1)GPIO(General Purpose Input Output)的全称是通用输入输出口。

①输出模式下,可控制端口输出高低电平,可以但不局限于驱动LED、控制蜂鸣器、模拟通信协议输出时序等。

②输入模式下,可读取端口的高低电平或电压,可以但不局限于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等。

(2)引脚电平:一般是0V~3.3V,部分引脚可容忍5V。

2、GPIO基本结构

(1)在STM32中,所有GPIO都是挂载在APB2外设总线上的,每个GPIO外设各有16个引脚。

(2)GPIO的寄存器每一位对应一个引脚寄存器有32位,只有低16位有对应引脚,高16位闲置)。输出寄存器写1,对应的引脚就会输出高电平;输出寄存器写0,对应的引脚就会输出低电平;输入寄存器读取为1,证明对应的端口目前是高电平;输入寄存器读取为0,证明对应的端口目前是低电平。

(3)寄存器与引脚间串联了驱动器,它是用来增加信号的驱动能力的。

3、GPIO位结构

(1)如下图所示,整个结构可以分为输入部分(上)和输出部分(下),除了寄存器是8个I/O引脚共用一个之外,每一个I/O引脚都有自己的位结构

(2)每一个I/O引脚都接了两个保护二极管,作用是对输入电压进行限幅,VDD为3.3V,VSS为0V,如果输入电压高于3.3V,那么上方的二极管将会导通,输入电压产生的电流就会直接往VDD流入,避免过高电压对内部的电路产生破坏;如果输入电压低于0V,那么下方的二极管将会导通,电流就会直接往VSS流出,而不会从内部电路汲取电流,也起到保护电路的作用。

(3)输入部分:输入驱动器及其后续的电路。

①输入驱动器中连接了上拉电阻(和VDD)和下拉电阻(和VSS),两个开关均可以通过程序进行配置,如果上开关导通、下开关断开就是上拉输入模式,上开关断开、下开关导通就是下拉输入模式,上开关断开、下开关断开就是浮空输入模式

假如I/O引脚没有外部输入,即处于高阻态,如果配置为浮空输入模式,那么引脚的输入电平极易受到外界干扰而发生改变;如果将其配置为上拉输入模式,那么此时它是高电平如果将其配置为下拉输入模式,那么此时它是低电平。上拉电阻和下拉电阻的阻值都比较大,所以两个输入模式的全名分别是弱上拉输入模式弱下拉输入模式如果I/O引脚有外部输入,那么VDD和VSS也不会影响I/O的正常使用(具体工作原理可根据基尔霍夫定律得出)。

③施密特触发器的作用是对输入电压进行整形,如果输入电压大于某一阈值,将会输出高电平,如果输入电压低于某一阈值,将会输出低电平(两个阈值不同,形成一个回差逻辑,可以有效地避免因信号波动造成的输出抖动现象)。

④输入信号经过施密特触发器处理后,将输入输入数据寄存器,单片机可在程序中读取输入数据寄存器对应的某一位数据,从而得知对应端口处于高电平还是低电平

⑤模拟输入是连接到ADC上的,因为ADC需要接收模拟量,所以它接收的信号不需要施密特触发器进行处理;复用功能输入是连接到其它需要读取端口的外设上的,这根线接收的是数字量,所以它接收的信号需要施密特触发器进行处理。

(4)输出部分:输出驱动器及其前继的电路。

①输出信号可以由输出数据寄存器片上外设控制,两种控制方式通过数据选择器连接到了输出控制部分。如果选择通过输出数据寄存器(也就是普通的I/O口输出)进行控制,往数据寄存器中的某一位写数据,就可以操作对应的某个端口,不过输出数据寄存器不能直接对某一位进行单独配置(程序中可以使用&=、|=运算符,但是开发效率低),这时可以借助位设置/清除寄存器

[1]如果要对某一位进行置1操作,在位设置寄存器的对应位写1即可,剩下不需要操作的位写0,位设置寄存器会自动将输出数据寄存器中的对应位置为1,剩下写0的位则保持不变,这样就保证了只操作数据输出寄存器的某一位而不影响其它位。

[2]如果要对某一位进行置0操作,在位清除寄存器的对应位写1即可,剩下不需要改变的位写0,位清除寄存器会自动将输出数据寄存器中的对应位置为0,剩下写0的位则保持不变,这样就保证了只清除数据输出寄存器的某一位而不影响其它位。

②MOS管就是一种电子开关,其导通或关闭由输入信号控制,开关负责将I/O口接到VDD或VSS,在这里可以选择推挽、开漏或关闭三种输出方式。

[1]在推挽输出模式下,P-MOS和N-MOS均有效。数据寄存器相应位为1时,P-MOS导通、N-MOS断开,I/O引脚直接和VDD连接,也就是输出高电平;数据寄存器相应位为0时,P-MOS断开、N-MOS导通,I/O引脚直接和VSS连接,也就是输出低电平。在这种模式下,高低电平均有较强的驱动能力,所以推挽输出模式也可以叫强推输出模式,STM32对I/O口具有绝对的控制权。

[2]在开漏输出模式下,P-MOS无效。数据寄存器相应位为1时,N-MOS断开,这时输出相当于断路,也就是高阻模式;数据寄存器相应位为0时,N-MOS导通,I/O引脚直接和VSS连接,也就是输出低电平。在这种模式下,只有低电平有驱动能力,高电平没有驱动能力,该模式可以作为通信协议的驱动方式,在多机通信的情况下,这个模式可以避免各个设备的相互干扰。另外,该模式还可以用于输出5V的电平信号,具体做法是在I/O引脚处接一个上拉电阻连接5V电源,数据寄存器相应位为1时,N-MOS断开,I/O口输出5V电压)。

[3]在关闭模式下,两个MOS管均无效,端口的电平由外部信号控制。

4、端口模式配置

        通过配置GPIO的端口配置寄存器,端口可以被配置成以下8种模式:

模式名称

性质

特征

浮空输入

数字输入

可读取引脚电平,若引脚悬空,则电平不确定

上拉输入

数字输入

可读取引脚电平,内部连接上拉电阻,悬空时默认高电平

下拉输入

数字输入

可读取引脚电平,内部连接下拉电阻,悬空时默认低电平

模拟输入

模拟输入

GPIO无效,引脚直接接入内部ADC

开漏输出

数字输出

可输出引脚电平,高电平为高阻态,低电平接VSS

推挽输出

数字输出

可输出引脚电平,高电平接VDD,低电平接VSS

复用开漏输出

数字输出

由片上外设控制,高电平为高阻态,低电平接VSS

复用推挽输出

数字输出

由片上外设控制,高电平接VDD,低电平接VSS

        ①浮空/上拉/下拉输入:在输入模式下,输出驱动器断开,端口只能用于输入,否则输出部分产生的信号可能会影响输入

        ②模拟输入:基本上可以说是ADC模数转换器的专属配置,不接上拉电阻和下拉电阻,输入信号往片上外设输送

        ③开漏/推挽输出:开漏输出的高电平呈现高阻态,没有驱动能力;推挽输出的高电平和低电平均有驱动能力(配置成输出模式的时候,输入部分并没有断开,程序依然可以读取I/O引脚的电平)

        ④复用开漏/推挽输出:引脚电平由片上外设控制(配置成复用输出模式的时候,输入部分并没有断开,程序依然可以读取I/O引脚的电平)

​​​​​​​

二、LED和蜂鸣器简介

1、LED(发光二极管)

(1)LED的电路符号如下所示,左边是正极,右边是负极(正极的电压高于负极,电流从正极流向负极,即为有正向电流,LED在此条件下可以被点亮

(2)LED一般需要串联限流电阻,如果不加限流电阻,那么流过LED的电流会很大,可能会对LED造成破坏。

2、蜂鸣器

(1)有源蜂鸣器:内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定。(下面实验使用的是有源蜂鸣器)

(2)无源蜂鸣器:内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同频率的声音。

三、示例程序(GPIO输出)

1、LED灯闪烁

(1)按照下图所示接好线路,并将之前建好的工程文件夹作为模板复制一份使用

(2)操作STM32的GPIO总共需要3个步骤,第一步是使用RCC开启GPIO的时钟,第二步是使用GPIO_Init函数初始化GPIO,第三步是使用输出或者输入的函数控制GPIO口

如果需要找到RCC的相关函数,就到stm32f10x_rcc.h文件(在Library组中)的底部找,一般在固件库提供的头文件底部都有所有函数的声明,且函数名称和其作用有很大关联,选中需要调用的函数后右键即可转到函数体,函数体上方有注释以及供用户使用的参数。(下图红框所框选的是RCC最常用的三个函数

[1] RCC_AHBPeriphClockCmd函数:用于使能或禁用STM32中挂载在AHB总线上的外设时钟。

函数原型:void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);

参数解释:

        uint32_t RCC_AHBPeriph:指定要开启或关闭时钟的AHB外设

        FunctionalState NewState:指定外设时钟的新状态(ENABLE表示使能,DISABLE表示失能)

返回值:无返回值

[2] RCC_APB2PeriphClockCmd函数:用于使能或禁用STM32中挂载在APB2总线上的外设时钟。

函数原型:void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);

参数解释:

        uint32_t RCC_APB2Periph:指定要开启或关闭时钟的APB2外设

        FunctionalState NewState:指定外设时钟的新状态(ENABLE表示使能,DISABLE表示失能)

返回值:无返回值

[3] RCC_APB1PeriphClockCmd函数:用于使能或禁用STM32中挂载在APB1总线上的外设时钟。

函数原型:void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);

参数解释:

        uint32_t RCC_APB1Periph:指定要开启或关闭时钟的APB1外设

        FunctionalState NewState:指定外设时钟的新状态(ENABLE表示使能,DISABLE表示失能)

返回值:无返回值

如果需要找到GPIO的相关函数,就到stm32f10x_gpio.h文件(在Library组中)的底部找,下图红框所框选的是GPIO最常用的函数。

[1] GPIO_DeInit函数:将指定的GPIO端口的所有寄存器恢复为默认值。

函数原型:void GPIO_DeInit(GPIO_TypeDef* GPIOx);

参数解释:

        GPIO_TypeDef* GPIOx:指定要被复位为默认值的GPIO端口

返回值:无返回值

[2] GPIO_AFIODeInit函数:将AFIO(Alternate Function I/O,复用功能 I/O)外设的所有寄存器恢复为默认值。(调用该函数时,需要保证AFIO时钟是使能的,否则无效)

函数原型:void GPIO_AFIODeInit(void);

参数解释:无参数

返回值:无返回值

[3] GPIO_Init函数:根据GPIO_InitTypeDef结构体中指定的参数,对选定的一个或多个GPIO引脚进行初始化配置。

函数原型:void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);

参数解释:

        GPIO_TypeDef* GPIOx:指向指定要配置哪个GPIO端口

        GPIO_InitTypeDef* GPIO_InitStruct:指向配置结构体的指针,这个结构体里包含了所有配置参数,有指定要配置的GPIO引脚、指定GPIO引脚的输出速度(仅输出/复用模式有效)、指定GPIO引脚的工作模式,其中选择GPIO引脚时,可以使用“|”运算符同时配置多个引脚

返回值:无返回值

[4] GPIO_StructInit函数:将GPIO_InitTypeDef结构体中的每一个成员参数填充为默认值(缺省值),便于用户在后续配置中快速修改。

函数原型:void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);

参数解释:

        GPIO_InitTypeDef* GPIO_InitStruct:指向配置结构体的指针,这个结构体里包含了所有配置参数,有指定要配置的GPIO引脚、指定GPIO引脚的输出速度(仅输出/复用模式有效)、指定GPIO引脚的工作模式

返回值:无返回值

[5] GPIO_ReadInputDataBit函数:读取指定GPIO引脚输入数据寄存器中的单一位,并返回。

函数原型:uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

参数解释:

        GPIO_TypeDef* GPIOx:指定要读取的GPIO端口;该参数是一个指向GPIO端口基地址的指针,函数通过它访问对应端口的IDR寄存器

        uint16_t GPIO_Pin:指定要读取的引脚编号;该参数是一个位掩码(bit mask),即只有对应的引脚位为1,其余位为0,

返回值:如果是Bit_SET(1),对应引脚为高电平;如果是Bit_RESET(0),对应引脚为低电平

特别说明:如果将多个引脚的掩码通过“|”组合后作为参数传入,函数将同时读取这些引脚的电平状态,若任意一个被读取的引脚为高电平,函数返回Bit_SET,只有当所有被读取的引脚均为低电平时,才返回Bit_RESET

[6] GPIO_ReadInputData函数:一次性读取指定GPIO端口全部16个引脚的输入电平状态,并将它们合并为一个16位的数值返回。

函数原型:uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);

参数解释:

        GPIO_TypeDef* GPIOx:指定要读取的GPIO端口;该参数是一个指向GPIO端口基地址的指针,函数通过它访问对应端口的IDR寄存器

返回值:16位无符号整数,bit0~bit15分别对应GPIO端口引脚Px0~Px15的输入电平状态

[7] GPIO_ReadOutputDataBit函数:读取指定GPIO端口输出数据寄存器(ODR,Output Data Register)中某一位的当前值,从而获知该引脚被程序设置成的输出电平(而非引脚外部的实际电平)。

函数原型:uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

参数解释:

        GPIO_TypeDef* GPIOx:指定要读取输出数据寄存器的GPIO端口

        uint16_t GPIO_Pin:指定要读取的引脚编号

返回值:如果是Bit_SET(1),对应引脚的输出数据寄存器位为1;如果是Bit_RESET(0),对应引脚的输出数据寄存器位为0

特别说明:如果将多个引脚的掩码通过“|”组合后作为参数传入,函数将同时检查这些引脚在ODR中的对应位,只要其中任意一位为1,函数就会返回Bit_SET,只有当所有被检查的位均为0时,才返回Bit_RESET

[8] GPIO_ReadOutputData函数:一次性读取指定GPIO端口输出数据寄存器(ODR,Output Data Register)的完整16位值,从而获得该端口上所有被配置为输出模式的引脚当前被程序设置的电平状态。

函数原型:uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);

参数解释:

        GPIO_TypeDef* GPIOx:指定要读取输出数据寄存器的GPIO端口

返回值:16位无符号整数,bit0~bit15分别对应GPIO端口引脚Px0~Px15的输出数据寄存器值

[9] GPIO_SetBits函数:(把指定的端口设置为高电平,可以使用按位或“|”同时操作多个引脚)

函数原型:uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);

参数解释:

        GPIO_TypeDef* GPIOx:指定要读取输出数据寄存器的GPIO端口

返回值:16位无符号整数,bit0~bit15分别对应GPIO端口引脚Px0~Px15的输出数据寄存器值

[10] GPIO_ResetBits函数:将指定GPIO端口的选定引脚输出电平设置为低电平(0V)。

函数原型:void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

参数解释:

        GPIO_TypeDef* GPIOx:指定要操作的GPIO端口

        uint16_t GPIO_Pin:指定要复位(拉低)的引脚编号;同样的,这是一个位掩码,即只有对应的引脚位为1,其余位为0

返回值:无返回值

特别说明:如果将多个引脚的掩码通过“|”组合后作为参数传入,函数将同时将这些引脚的电平拉低

[11] GPIO_WriteBit函数:将指定GPIO端口的单个引脚设置为高电平或低电平。

函数原型:void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);

参数解释:

        GPIO_TypeDef* GPIOx:指定要操作的GPIO端口

        uint16_t GPIO_Pin:指定要操作的引脚编号;同样的,这是一个位掩码,即只有对应的引脚位为1,其余位为0

        BitAction BitVal:指定要写入的电平值,Bit_RESET表示低电平(0),Bit_SET表示高电平(1)

返回值:无返回值

特别说明:如果将多个引脚的掩码通过“|”组合后作为参数传入,函数将同时操作这些引脚

[12] GPIO_Write函数:一次性向指定GPIO端口的输出数据寄存器(ODR,Output Data Register)写入一个完整的16位值,从而同时设置该端口上全部16个引脚的输出电平。

函数原型:void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);

参数解释:

        GPIO_TypeDef* GPIOx:指定要操作的GPIO端口

        uint16_t PortVal:要写入端口输出数据寄存器的16位数值,bit0~bit15分别对应GPIO端口引脚Px0~Px15的输出电平

返回值:无返回值

(3)使能GPIOA的时钟:

①首先去到stm32f10x_rcc.h底部,找到RCC_APB2PeriphClockCmd函数,它的作用是使能APB2上挂载外设的时钟。

②RCC_APB2PeriphClockCmd函数的使用方法如下,本例需要使能GPIOA的时钟,所以函数参数分别为RCC_APB2Periph_GPIOA、ENABLE。

(4)配置端口模式:

①首先去到stm32f10x_gpio.h底部,找到GPIO_Init函数。

②GPIO_Init函数的第一个参数为需要初始化的GPIO口,第二个参数为一个结构体变量,这个结构体变量内有初始化GPIO口需要的相关参数。(第二个参数是一个结构体指针的原因,是指针的运算速度比较快,调用该函数时不用为整个结构体拷贝一份形参)

③右键结构体类型名,找到定义结构体的地方,可以看到定义结构体变量需要三个参数,分别为引脚号、速度和模式:

[1]对于引脚号,可以选中注释中的“GPIO_pins_define”后按下Ctrl+F,点击“Find Next”,很快就能找到引脚相关的宏定义,这些就是可供选择的参数。(GPIO_Pin_ALL——一次选中所有引脚)

[2]对于速度,配置为50MHz即可,那么结构体关于速度的参数设置为GPIO_Speed_50MHz。

[3]对于模式,在该例中使用的是推挽输出,那么结构体关于模式的参数设置为GPIO_Mode_Out_PP。

(5)经过刚刚的步骤,main.c中应该有以下代码,点击编译,待程序编译完成后,将它下载到开发板中,可以看到插在面包板上的LED灯亮起。

#include "stm32f10x.h"                  // Device headerCmd

int main()
{
	//使能GPIOA的时钟
	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_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //结构体参数之一(速度)
	GPIO_Init(GPIOA,&GPIO_InitStructure);	
	
	while(1)
	{
		GPIO_ResetBits(GPIOA,GPIO_Pin_0);  //将PA0置为低电平(点灯)
	}
}

(6)要完成LED闪烁的功能,仅靠反复将PA0置1置0是不够的,因为程序的执行速度非常快,人眼还没发现LED熄灭LED就重新亮起,对此需要一个延时函数,其作用是让程序暂时睡眠一段时间,首先将Delay.c文件和Delay.h文件添加到项目文件夹中新建的“System”文件夹中,然后按照下图所示步骤将它们添加到项目中。

①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);
	}
} 

(7)将下面这段代码复制到main.c文件中,然后编译、下载程序到开发板中验证。

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

int main()
{
	//使能GPIOA的时钟
	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_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //结构体参数之一(速度)
	GPIO_Init(GPIOA,&GPIO_InitStructure);	
	
	while(1)
	{
		GPIO_ResetBits(GPIOA,GPIO_Pin_0);  //将PA0置为低电平(亮灯)
		Delay_ms(500);  //睡眠500ms
		GPIO_SetBits(GPIOA,GPIO_Pin_0);    //将PA0置为高电平(灭灯)
		Delay_ms(500);  //睡眠500ms
		/*下面这段代码有同样的效果
		GPIO_WriteBits(GPIOA,GPIO_Pin_0,Bit_RESET);  //将PA0置为低电平(亮灯)
		Delay_ms(500);
		GPIO_WriteBits(GPIOA,GPIO_Pin_0,Bit_SET);    //将PA0置为高电平(灭灯)
		Delay_ms(500);
		*/
	}
}

2、LED灯流水

(1)按照下图所示接好线路,并将上例的项目文件夹作为模板复制一份使用

(2)将下面这段代码复制到main.c文件中,然后编译、下载程序到开发板中验证,可以看到8盏LED灯按照固定顺序轮流闪烁。

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

int main()
{
	//使能GPIOA的时钟
	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;
	//可以使用按位或“|”同时选中多个引脚,也可以使用GPIO_Pin_ALL同时选择GPIOA的16个引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);	
	
	while(1)
	{
		static uint16_t i = 0x0001;  //(高8位)+ 0000 0001
		//高8位对应引脚PA8~PA15,这部分没有连接外设,不予理会
		
		GPIO_Write(GPIOA,~i);  //~i对应二进制数为(高8位)+1111 1110
		//对16个端口同时进行写入操作
		i = i << 1;
		//第一盏灯亮后,下一盏灯应该是第二盏
		if(i == 0x0100)  //第一轮循环结束,下一盏亮灯是第一盏
			i = 0x0001;
		Delay_ms(500);  //睡眠500ms
	}
}

3、让蜂鸣器发出响声

(1)按照下图所示接好线路,并将LED灯闪烁的项目文件夹作为模板复制一份使用。(本实验使用的蜂鸣器是低电平驱动,不是“交流电驱动”)

(2)将下面这段代码复制到main.c文件中,然后编译、下载程序到开发板中验证,可以听到蜂鸣器会发出断断续续的响声。

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

int main()
{
	//使能GPIOB的时钟
	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);	
	
	while(1)
	{
		GPIO_ResetBits(GPIOB,GPIO_Pin_12);  //将PB12置为低电平(响)
		Delay_ms(100);
		GPIO_SetBits(GPIOB,GPIO_Pin_12);    //将PB12置为高电平(静)
		Delay_ms(100);
		GPIO_ResetBits(GPIOB,GPIO_Pin_12);  //将PB12置为低电平(响)
		Delay_ms(100);
		GPIO_SetBits(GPIOB,GPIO_Pin_12);    //将PB12置为高电平(静)
		Delay_ms(700);
	}
}

四、按键与传感器

1、按键

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

(2)由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动,这个抖动称为“按键抖动”,在软件中需要对抖动进行消除(在51单片机教程中对按键消抖有详细的介绍)

2、传感器

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

        简单说,以光敏传感器为例,光线强度中间存在一个临界值,临界值上下传感器的输出引脚分别输出低电平和高电平

五、示例程序(GPIO输入)

1、按键控制LED

(1)按照下图所示接好线路,并将LED灯闪烁的项目文件夹作为模板复制一份使用

(2)当代码量庞大时,需要将代码分模块放在不同的代码文件中进行管理,本例需要添加一个存放硬件驱动代码的组“Hardware”,然后创建一个管理LED模块代码的源文件和头文件(记住选择文件路径为Hardware文件夹),再将两个文件都添加到Hardware组中。

(3)将LED模块的代码添加到LED.c文件中,再在LED.h文件中对函数进行声明,以便main.c调用。

①LED.h文件:

#ifndef __LED_H
#define __LED_H  //防止头文件重复被包含

//函数声明
void LED_Init(void);
void LED1_ON(void);
void LED1_OFF(void);
void LED2_ON(void);
void LED2_OFF(void);

#endif

②LED.c文件:

#include "stm32f10x.h"                  // Device header(每个源文件基本都要包含它)

void LED_Init(void)
{
	//使能GPIOA的时钟
	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;  //同时配置PA1和PA2
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	//LED的初始状态为灭灯
	GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2);
}

void LED1_ON(void)  //点亮第一盏LED灯(LED1负极接在PA1)
{
	GPIO_ResetBits(GPIOA, GPIO_Pin_1);  //PA1置为低电平
}

void LED1_OFF(void)  //熄灭第一盏LED灯
{
	GPIO_SetBits(GPIOA, GPIO_Pin_1);    //PA1置为高电平
}

void LED2_ON(void)  //点亮第二盏LED灯(LED2负极接在PA2)
{
	GPIO_ResetBits(GPIOA, GPIO_Pin_2);  //PA2置为低电平
}

void LED2_OFF(void)  //熄灭第二盏LED灯
{
	GPIO_SetBits(GPIOA, GPIO_Pin_2);    //PA2置为高电平
}

(4)GPIO_ReadInputDataBit(用于读取输入数据寄存器某一位对应I/O引脚的输入值)、GPIO_ReadInputData(用于读取某个GPIO口对应输入数据寄存器的所有端口/引脚的值)、GPIO_ReadOutputDataBit(用于读取数据输出寄存器的某一个位)、GPIO_ReadOutputData(用于读取某个GPIO口对应输出数据寄存器的值)都是GPIO的读取函数。

(5)创建一个管理按键(key)模块代码的源文件和头文件(记住选择文件路径为Hardware文件夹),再将两个文件都添加到Hardware组中,然后将Key模块的代码添加到Key.c文件中,再在Key.h文件中对函数进行声明,以便main.c调用。

①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)
{
	//使能GPIOB的时钟
	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; //同时配置PB1和PB11
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;       //输入模式下速度其实可以不用配置
	GPIO_Init(GPIOB,&GPIO_InitStructure);
}

uint8_t Key_GetNum(void)  //获取按键状态
{
	uint8_t KeyNum = 0;  //默认按键键码为0(无按键被按下)
	
	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)  //如果按键1被按下,PB1会被置为低电平
	{
		Delay_ms(20);  //消除20ms的抖动
		while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);  //待按键松开,再往下执行
		Delay_ms(20);  //消除20ms的抖动
		KeyNum = 1;    //按键键码置为1
	}
	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0)  //如果按键2被按下,PB11会被置为低电平
	{
		Delay_ms(20);  //消除20ms的抖动
		while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0);  //待按键松开,再往下执行
		Delay_ms(20);  //消除20ms的抖动
		KeyNum = 2;    //按键键码置为2
	}
	
	return KeyNum;     //返回按键键码
}

(6)本例需要实现的功能是按键1控制LED1的亮灭、按键2控制LED2的亮灭,为了使主函数的代码看起来更加简洁和清晰,需要在LED模块中再封装两个函数用于翻转两个LED灯的状态。

①LED.h文件需要添加两个函数声明:

void LED1_Turn(void);
void LED2_Turn(void);

②LED.c文件需要添加两个函数实现:

void LED1_Turn(void)  //LED1状态翻转
{
	if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == 0)  //读出PA1的电平并判断是否为低电平
	{
		GPIO_SetBits(GPIOA, GPIO_Pin_1);    //如果读出引脚PA1为低电平,就将其置为高电平(亮->暗)
	}
	else
	{
		GPIO_ResetBits(GPIOA, GPIO_Pin_1);  //如果读出引脚PA1为高电平,就将其置为低电平(暗->亮)
	}
}

void LED2_Turn(void)  //LED2状态翻转
{
	if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) == 0)  //读出PA2的电平并判断是否为低电平
	{
		GPIO_SetBits(GPIOA, GPIO_Pin_2);    //如果读出引脚PA2为低电平,就将其置为高电平(亮->暗)
	}
	else
	{
		GPIO_ResetBits(GPIOA, GPIO_Pin_2);  //如果读出引脚PA2为高电平,就将其置为低电平(暗->亮)
	}
}

(7)主函数需要完成的任务:

①调用各模块的初始化函数,对各个模块进行初始化。

②使用各模块已经封装好的函数进行代码的编写。

#include "stm32f10x.h"                  // Device headerCmd
#include "Delay.h"
#include "LED.h"     //头文件包含
#include "Key.h"

uint8_t KeyNum;

int main()
{
	LED_Init();  //LED模块初始化
	Key_Init();  //按键模块初始化
	
	while(1)
	{
		KeyNum = Key_GetNum();
		if(KeyNum == 1)  //按一次按键1,LED1状态翻转一次
		{
			//LED1_ON();   打开LED1
			//LED2_OFF();  关闭LED2
			LED1_Turn();   //LED1状态翻转
		}
		if(KeyNum == 2)  //按一次按键2,LED2状态翻转一次
		{
			//LED2_ON();   打开LED2
			//LED1_OFF();  关闭LED1
			LED2_Turn();   //LED2状态翻转
		}
	}
}

2、光敏传感器控制蜂鸣器

(1)按照下图所示接好线路,并将按键控制LED的项目文件夹作为模板复制一份使用

        光敏传感器有4个引脚,其中两个引脚为VCC和GND,这是供电必须接上的,另外两个引脚为AO和DO,其中DO口输出数字信号,AO口输出模拟信号,一般情况下选择DO口,因为计算机处理数字信号会更加方便

(2)创建一个管理蜂鸣器(Buzzer)模块代码的源文件和头文件(记住选择文件路径为Hardware文件夹),再将两个文件都添加到Hardware组中,然后将Buzzer模块的代码添加到Buzzer.c文件中,再在Buzzer.h文件中对函数进行声明,以便main.c调用。

①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)
{
	//使能GPIOB的时钟
	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;          //配置PB12
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	//蜂鸣器的初始状态为不响
	GPIO_SetBits(GPIOB, GPIO_Pin_12);
}

void Buzzer_ON(void)   //蜂鸣器发出响声
{
	GPIO_ResetBits(GPIOB, GPIO_Pin_12);  //PB12置为低电平
}

void Buzzer_OFF(void)  //蜂鸣器不发出响声
{
	GPIO_SetBits(GPIOB, GPIO_Pin_12);    //PB12置为高电平
}

void Buzzer_Turn(void)  //蜂鸣器状态翻转
{
	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12) == 0)  //读出PB12的电平并判断
	{
		GPIO_SetBits(GPIOB, GPIO_Pin_12);    //如果读出引脚PB12为低电平,就将其置为高电平(响->静)
	}
	else
	{
		GPIO_ResetBits(GPIOB, GPIO_Pin_12);  //如果读出引脚PB12为高电平,就将其置为低电平(静->响)
	}
}

(3)创建一个管理光敏传感器(LightSensor)模块代码的源文件和头文件(记住选择文件路径为Hardware文件夹),再将两个文件都添加到Hardware组中,然后将LightSensor模块的代码添加到LightSensor.c文件中,再在LightSensor.h文件中对函数进行声明,以便main.c调用。

①LightSensor.h文件:

#ifndef __LightSensor_H
#define __LightSensor_H

void LightSensor_Init(void);
uint8_t LightSensor_Get(void);

#endif

②LightSensor.c文件:

#include "stm32f10x.h"                  // Device header

void LightSensor_Init(void)
{
	//使能GPIOB的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	//配置端口模式
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;        //上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;           //配置PB13
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;    //输入模式下速度其实可以不用配置
	GPIO_Init(GPIOB,&GPIO_InitStructure);
}

uint8_t LightSensor_Get(void)  //获取传感器当前状态
{
	return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13); //返回PB13引脚状态
	//光线较暗时,光敏传感器模块的BO口为高电平,也就是PB13为高电平
	//光线较亮时,光敏传感器模块的BO口为低电平,也就是PB13为低电平
}

(4)主函数需要完成的任务:

①调用各模块的初始化函数,对各个模块进行初始化。

②使用各模块已经封装好的函数进行代码的编写。

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

int main()
{
	LightSensor_Init();   //传感器模块初始化
	Buzzer_Init();        //蜂鸣器模块初始化
	
	while(1)
	{
		if(LightSensor_Get() == 1)
		{
			Buzzer_ON();    //光线过暗,蜂鸣器启动
		}
		else
		{
			Buzzer_OFF();   //光线充足,蜂鸣器关闭
		}
	}
}
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐