STM32学习笔记(一)丨建立工程丨GPIO 通用输入输出

​  本次课程采用单片机型号为STM32F103C8T6。
​  课程链接:江科大自化协 STM32入门教程


一、软件下载注册、驱动安装

1.1 软件(器件支持包)下载

​  由于新型号芯片的不断产生,需要下载器件支持包来更新Keil 5所支持的芯片型号。可以采用离线安装在线安装两种方式。

​  软件注册采用注册机方式解决。

1.2 ST-LINK驱动安装

​  插入ST-LINK到Windows电脑。打开设备管理器,在“其他设备”中“STM32-Link”图标旁会带黄色感叹号。如果没有携带,说明电脑上已经存在ST-Link的驱动。在Keil 5的文件目录中就可以找到驱动安装文件。

​  除此之外,JLink也是一种常见的调试器。在文件目录中同样可以找到对应的驱动安装文件。

1.3 USB转串口驱动安装

​  插入USB转串口模块到Windows电脑。打开设备管理器,在“其他设备”中找到“USB-SERIAL CH340”,并且如果图标旁边有感叹号,说明电脑没有安装驱动。

​  在Windows 11系统中可以在“端口”中找到“USB-SERIAL CH340”。


二、工程的建立

2.1 STM32的开发方式

​  目前STM32的开发方式主要有基于寄存器的方式,基于标准库(库函数)的方式和基于HAL库的方式。

​  基于寄存器的方式是用程序直接配置寄存器,来达到想要的功能。这种方式最底层,最直接,效率会更高一些。但是由于STM32的寄存器复杂,寄存器太多,所以不推荐这种方式。

​  基于库函数的方式是使用ST官方提供的封装好的函数,通过这些函数来间接地配置寄存器。由于ST对寄存器封装的比较好,所以这种方式既能满足对寄存器的配置,对开发人员也比较友好,有利于提高开发效率。本课程采用基于库函数的开发方式

​  基于HAL库的方式可以用图形化界面快速配置STM32, 但是这种方法隐藏了底层逻辑。推荐学习过标准库的开发方式之后,对这一种开发方式进行了解和学习。

2.2 新建工程的步骤

  1. 建立工程文件夹,在Keil中新建工程(工程文件名称为Project,存放在一个文件夹下,对该工程文件的说明写在文件夹上),选择芯片型号(本次课程选用STM32F103C8T6,故选择STM32F103C8);
  2. 工程文件夹里建立StartLibraryUser等文件夹,复制固件库中的文件到工程文件夹;
  • 配置STM32的启动文件(STM32的程序从启动文件开始执行)

    • 打开“固件库”文件夹,打开Libraries → \rightarrow CMSIS → \rightarrow CM3 → \rightarrow DeviceSupport → \rightarrow ST → \rightarrow STM32F10x → \rightarrow startup → \rightarrow arm,将文件夹中的文件复制到新建的Start文件夹中。

    • 将启动文件导入到Keil的工程中时,启动文件只能导入一个。课程所用型号需要选择startup_stm32f10x_md.s文件。

      启动文件的选择
      在这里插入图片描述
        选择启动文件时,需要考量芯片的两个指标:Flash容量、型号。
        根据上表,如果使用STM32F100的型号,就选择启动文件后缀为vl的启动文件,根据Flash容量选择后缀为ld_vlmd_vlhd_vl的文件,表中其他型号同理。
      在这里插入图片描述

    • 回到STM32F10x文件夹,将stm32f10x.hsystem_stm32f10x.csystem_stm32f10x.h复制到Start文件夹中,并导入到工程组Start中stm32f10x.h就是STM32外设寄存器描述文件,它的作用就同51单片机的头文件REGX52.H一样,是用来描述STM32有哪些寄存器和它对应的地址的)。

  • 配置内核寄存器的描述文件

    • 打开CM3 → \rightarrow CoreSupport,core_cm3.hcore_cm3.c就是内核的寄存器描述文件(其中还包含了一些内核的配置函数),同样将它们复制到Start文件夹中,并导入到工程组Start中
  • 配置User文件夹

    • 点击工程选项(魔术棒)、C/C++、在Include Path内声明所有包含头文件的文件夹(Start,Library,User);
    • 在Keil工程文件中新建User文件夹,添加main.c文件(注意:添加main.c文件时的路径要选择User文件夹,否则默认放在文件夹外),这时对main.c文件进行编译检查,如果没有报错和警告说明工程建立成功。 至此,基于寄存器开发的工程就建立完成了。
    • 在在“固件库”文件夹中打开Project → \rightarrow STM32F10x_StdPeriph_Template,其中stm32f10x_conf.h是用来配置库函数头文件的包含关系的,其中还包含用来参数检查的函数定义,是所有库函数都需要的。此外stm32f10x_it.hstm32f10x_it.c是用来存放中断函数的。将这三个文件复制下来,粘贴到工程的User文件夹中
  • 配置库函数Library文件夹

    • 在工程文件夹中新建Library文件夹。
    • 在“固件库”文件夹中打开Libraries → \rightarrow STM32F10x_StdPeriph_Driver(STM32标准外设驱动) → \rightarrow src,其中的文件就是库函数的源文件,其中misc.c是内核的库函数,其他都是内核外的外设库函数。将其中所有文件复制到Library文件夹中。
    • 在Libraries → \rightarrow STM32F10x_StdPeriph_Driver(STM32标准外设驱动) → \rightarrow inc 中是库函数的头文件,将其中所有文件复制到Library文件夹中。

注意事项
  需要用的文件一定要复制到工程的文件夹里面,不要添加工程文件夹外面的文件,否则外面的文件一旦挪动位置,工程里就找不到文件了。所以我们要将用的文件复制到工程中,以保证工程的独立性。

  1. 工程里对应建立Start,Library,User等同名称的分组(Group),然后将文件夹内的文件添加到对应的分组里;

  2. 点击工程选项C/C++、在Define内定义USE_STDPERIPH_DRIVER(宏定义,意为使用标准外设驱动这是库函数的条件编译,使用库函数开发就必须要定义

  • 该宏定义在头文件stm32f10x.h中(8296行)
#ifdef USE_STDPERIPH_DRIVER
  #include "stm32f10x_conf.h"
#endif

  其他工程在这个位置还声明了 STM32F10X_MD的字符串,但是Keil5在新建工程之后会自动声明该字符串,不需要再额外声明。

  1. 点击工程选项Debug中下拉列表选择对应的调试器(本次课程使用ST-Link进行调试,故选择ST-Link Debugger),点击Settings,在Flash Download里勾选Reset and Run,勾选上这一项之后,下载的程序会立马复位并执行,省去了在开发板上手动按下复位的步骤。

2.3 工程文件的架构

​  工程的建立非常灵活,学会建立工程后可以依照自己的风格建立工程。
STM32的工程描架构图
​  根据上图,我们可以了解STM32的工程架构(总结)如下所示:
​  首先是主动执行的文件:

  • startup_xx.s 启动文件
    • 启动文件是程序执行最基本的文件,由汇编语言编写。启动文件内定义了中断向量表,中断服务函数等。
    • 在中断服务函数中有一个复位中断,是整个程序的入口。 复位中断中调用StstemInit函数和main函数。StstemInit函数定义在system_xx.c/.h文件中,这个函数的作用是设置微控制器的启动,初始化嵌入式闪存接口,锁相环,更新系统内核的时钟变量。这个函数仅在复位后需要调用。
    • 在启动文件中还定义了STM32所有的其他中断函数。这些中断函数的定义,存放在stm32f10x_it.c/.h中。在这两个文件最后还可以定义用户自己写的中断函数。当然,写在别的地方也可以。
  • system_xx.c/.h 文件:定义SystemInit函数
  • main.c文件:定义main函数
  • stm32f10x_it.c/.h文件:定义中断函数
  • 其他用户文件:封装一些模块函数供主函数和中断函数调用

​  下面是被动执行的文件,相当于STM32的资源:

  • stm32f10x.h文件:外设寄存器描述
    • 该文件中都是寄存器和每一位寄存器的名字、地址信息等
  • core_cm3.c/.h文件:内核寄存器描述
  • misc.c/.hstm32f10x_adc.c/.h…文件:库函数
  • stm32f10x_conf.h文件:库函数配置文件
    • 该文件用来配置头文件的包含关系,它include了所有的库函数头文件,同时在stm32f10x.h中又include了stm32f10x_conf.h。所以,在使用这些库函数时,只需要包含stm32f10x.h这一个头文件,就相当于包含了所有的头文件

2.4 环境的优化处理

  1. 为防止出现中文乱码在设置🔧中将Encoding改为UTF-8格式(如果打开别人的文件出现乱码,可以尝试修改其他的编码格式)
  2. 在设置🔧中的Colors & Fonts中可以修改字体样式和大小

2.5 使用寄存器开发实操

​  下面对开发板上的LED PC13 进行点灯操作。(通过查询数据手册寻找对应寄存器直接赋值。)

#include "stm32f10x.h"                  // Device header

int main(void)
{
	RCC->APB2ENR = 0x00000010;
	GPIOC->CRH = 0x00300000;
	GPIOC->ODR = 0x00000000;	// 灯亮
	// GPIOC->ODR = 0x00002000;	// 灯灭
	while (1)
	{
		
	}
}

  这种操作方式需要不断地查手册来了解每个寄存器的每一位的作用;且弊端为我们把除了PC13之外的位都配置为0,这会影响到其他端口的原有配置。如果想单独配置PC13位,需要使用 &=和 |= 的操作,太过麻烦,对开发人员不友好。
​  需要注意,编程完成后最后需要多空一行,否则程序会报出一个警告。

2.6 使用库函数配置寄存器实操

​  使用库函数配置寄存器虽然代码较为复杂,但是逻辑清晰,易于操作。下面对开发板上的LED PC13 进行点灯操作,初步体验GPIO的使用方法。在下一章会对GPIO作详细介绍。

#include "stm32f10x.h"                  // Device header

int main(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);	
	// RCC_APB2外设时钟控制命令,用来开启时钟,RCC_APB2Periph_GPIOC是APB2的一个外设
	// 在这个函数内部一样采用了&=和|=操作来配置RCC->APB2ENR寄存器,但是经过函数包装,我们不再需要去查手册来确认寄存器内每一位的功能
	
	// 下面配置GPIO_Init函数第二个参数需要的结构体
	GPIO_InitTypeDef GPIO_InitStructure;	// 定义一个结构体
	// 下面是结构体的三个参数
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;	// GPIO模式,GPIO_Mode_Out_PP为通用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;	//GPIO端口
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	// GPIO速度
	// 下面配置端口模式
	GPIO_Init(GPIOC, &GPIO_InitStructure);
	// GPIO_SetBits(GPIOC, GPIO_Pin_13);	// 把指定端口设置为高电平,灯灭
	GPIO_ResetBits(GPIOC, GPIO_Pin_13);		// 把指定端口设置为低电平,灯亮
	while (1)
	{
		
	}
}

  在配置结构体GPIO_InitStructure的三个参数时,需要手动查找定义的位置。以GPIO_InitStructure.GPIO_Mode为例,右键跳转到定义,选中GPIOMode_TypeDef,按下Ctrl+F,点击Find next即可找到GPIOMode_TypeDef的定义。可以看到它是一个枚举,而GPIO_InitStructure.GPIO_Mode应该赋的值就是这里对应的一个值(根据实际情况而定)。

三、GPIO 通用输入输出

  GPIO,全称为General Purpose Input Output,意为通用输入输出,可配置八种输入输出模式。引脚电平: 0 V − 3.3 V 0V-3.3V 0V3.3V V O L = 0 V , V O H = 3.3 V V_{OL}=0V, V_{OH}=3.3V VOL=0V,VOH=3.3V),部分引脚可以容忍 5 V 5V 5V(容忍的意思是可以在这个端口输入 5 V 5V 5V 的电压,也认为是高电平,具体哪些端口可以容忍 5 V 5V 5V 需要查找STM32的引脚定义。在引脚定义中带FT,意为Five Tolerate,就是可以输入 5 V 5V 5V 的端口)。
  GPIO在输出模式下可控制端口输出高低电平,用以驱动LED,控制蜂鸣器,模拟通信协议输出时序等。如果控制的是功率比较大的设备,只需要再加入驱动电路即可。
  GPIO在输入模式下可读取端口的高低电平(电压),用于读取按键输入,外界模块的(电平)信号输入,ADC电压采集,模拟通信协议接收数据等。

3.1 GPIO的基本结构

GPIO的基本结构
  在所有的STM32中,所有的GPIO外设都挂载在APB2外设总线上。其中GPIO外设是按照GPIOA、GPIOB、GPIOC等命名的。
  每个GPIO外设有16个引脚。以GPIOA为例,它的引脚编号分别为PA0,PA1,PA2 … PA15。其他GPIO的引脚也是这样命名的。
  在GPIO模块内部,主要包含了寄存器驱动器两个模块。寄存器就是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写,以完成输出电平和读取电平的功能。
  STM32是32位的单片机,其中的寄存器都是32位的。而端口只有16个,故寄存器只有低16位有对应的端口,高16位没有用到
  驱动器的作用是增加信号的驱动能力。寄存器只负责存储数据,需要用驱动器来增大驱动能力。

3.2 GPIO位结构及其工作模式

3.2.1 GPIO位结构

GPIO位结构
  GPIO每一位具体的电路结构如上图所示。整体结构可以分为图上部的输入部分和图下部的输出部分。

  • 引脚和保护二极管
    • 保护二极管可以保证党引脚电平高于 V D D ( 3.3 V ) V_{DD}(3.3V) VDD(3.3V)或低于 V S S ( 0 V ) V_{SS}(0V) VSS(0V)时保护内部电路安全。
  • 输入数据寄存器和输入驱动器
    • 上拉电阻和下拉电阻:可以通过程序配置上拉输入模式下拉输入模式,其主要作用是设置一个默认的输入电平,即输入引脚空载时内部数字电路接受的电平。为了避免引脚悬空导致的数据不确定,需要配置输入端口的输入模式。如果上下开关全部断开,此时端口处于浮空输入模式
    • TTL肖特基触发器:可以理解为用肖特基管组成的施密特触发器,作用是对输入电压进行整形。对于施密特触发器,之后当输入电压高于 V T + V_{T+} VT+(正向阈值电压)或低于 V T − V_{T-} VT(负向阈值电压)时输出电压才会突变。接入施密特触发器可以有效避免因信号波动造成的输出抖动现象。
    • 经过施密特触发器整型的波形就可以写入输入数据寄存器了。
    • 模拟输入:链接到(片上外设)ADC,用于读取模拟量。
    • 复用功能输入:链接到其他需要读取端口的片上外设(例如串口的输入引脚),用于读取数字量。
  • 输出数据寄存器和输出驱动器
    • 输出数据寄存器:如果选择通过输出数据寄存器进行控制(采用基于寄存器开发的开发方法),即普通的IO口输出,对输出数据寄存器进行写操作就可以操作对应的端口了。输出数据寄存器同时控制16个端口(16位),且该寄存器只能整体读写。 如果想单独操作这个数据寄存器的某一位,需要先读出这个寄存器,然后按照按位与和按位或的方式更改该位,最后再将更改后的数据写入。
    • 位设置/清除寄存器:可以用来单独操作输出数据寄存器的某一位,而不影响其他位。如果需要对输出数据寄存器的某一位写入1,则在位设置寄存器的对应位置写1,其他位置写0(表示不需要操作)即可;如果想对某一位写0,就在位清除寄存器的对应位置写1,其他位置写0(表示不需要操作)即可。在本课程中主要采用库函数进行操作,库函数使用的就是操作位设置/清除寄存器的方法。

        在STM32中,操作寄存器的某一位还可以操作 “位带” 区域,类似于51单片机中的位寻址。在STM32中,专门分配有一段地址区域,这段地址映射了RAM和外设寄存器所有的位。读写这段地址中的数据,就相当于读写所映射位置的某一位。在本课程中不会用到该种操作方式。

    • 输出驱动器:由输出数据寄存器和其他片上外设控制,通过一个(二选一)数据选择器链接到输出控制模块。通过设置可以选择推挽、开漏或关闭三种输出方式
    • 推挽输出模式:又称为“推拉”,“互补”的输出模式。当数据寄存器(复用功能输出)为1时,P-MOS导通,N-MOS截止,输出高电平;数据选择器(复用功能输出)为0时,P-MOS截止,N-MOS导通,输出低电平。在这种模式下,高低电平均有较强的驱动能力 ,所以推挽输出模式也称作强推输出模式。在推挽输出模式下,STM32对IO口具有绝对的控制权 ,高低电平都由STM32说了算。
    • 开漏输出模式:意为N-MOS的漏极和 V D D V_{DD} VDD是开路的,即P-MOS开路(无效)。数据寄存器(复用功能输出)为1时,N-MOS断开,这时IO口呈现高阻模式(没有驱动能力);数据寄存器(复用功能输出)为0时,N-MOS导通,输出低电平。这种模式下只有低电平有驱动能力。 开漏输出模式可以作为通信协议的驱动方式,例如I2C。在多机通信的情况下,这个模式可以避免各个设备的相互干扰。此外,开漏输出可以通过外接上拉电源和电阻以提高端口的带载能力,或兼容一些 5 V 5V 5V 输入的设备(输出 5 V 5V 5V 的电平信号)。
    • 关闭状态:意为当引脚配置为输入模式时,两个MOS管都无效(开路),端口的电平由外部信号控制。(高阻态,没有驱动能力)

3.2.2 GPIO的八种工作模式

  通过对GPIO的端口配置寄存器进行操作,即配置弱上拉/下拉电阻开关的通断,施密特触发器是否有效,N-MOS和P-MOS是否有效,数据选择器的选择等,端口可以被配置为以下八种模式:
GPIO的工作模式

  下面是各种输入模式的等效电路图:

  1. 浮空/上拉/下拉输入模式
    浮空/上拉/下拉输入模式
  2. 模拟输入模式
    模拟输入模式
  3. 推挽/开漏输出模式
    推挽/开漏输出模式
  4. 复用推挽/开漏输出模式
    复用推挽/开漏输出模式
      可以看到,只有模拟输入模式下,数字输入(施密特触发器)处于关闭状态。输入时输出呈现高阻态,但输出的同时端口同样可以输入(读取端口状态)

3.3 GPIO中的寄存器简介

  • 端口配置寄存器 GPIOx_CRL/GPIOx_CRH (Configuration Register Low/High)
      在一个GPIO中,每一个端口需要4位进行配置,16个端口就需要64位进行配置。由于STM32的寄存器都是32位的,故一共需要两个寄存器,分为端口配置低寄存器端口配置高寄存器。在端口配置寄存器中还可以定义GPIO输出的速度,即限制输出引脚的最大翻转频率,设计输出速度的目的是调节功率和稳定性。
  • 端口输入数据寄存器 GPIOx_IDR (Input Data Register)
      端口输入数据寄存器的低16位对应16个引脚,高16位没有使用。
  • 端口输出数据寄存器 GPIOx_ODR (Output Data Register)
      与输入数据寄存器相同,端口输出数据寄存器的低16位对应16个引脚,高16位没有使用。
  • 端口位设置/清除寄存器 GPIOx_BSRR (Bit Set/Reset Register)
      该寄存器的高16位实现位清除,低16位实现位设置。如果想对多个端口同时进行位设置和位清除,即对GPIO信号的同步性要求较高,可以只使用这一个寄存器
  • 端口位清除寄存器 GPIOx_BRR (Bit Reset Register)
      该寄存器的低16位实现位清除,高16位没有使用。它可以与上面一个寄存器配合使用,使用时两个寄存器都只使用寄存器的低16位,操作更为方便。
  • 端口配置锁定寄存器 GPIO_LCKR (Configuration Lock Register)
      该寄存器可以对端口的配置进行锁定,防止意外更改。

3.4 GPIO输出 应用实操

  操作STM32的GPIO输出一共需要三个步骤(下面步骤中设计RCC和GPIO两个外设):

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

  且下面三个实例都使用了Delay_ms函数。要想使用这个函数,首先应该将其添加在工程文件夹中,然后把Delay.c/.h文件放在工程组System中,在工程选项添加Include Path路径。在main函数前也需要写#include "Delay.h"
  RCC外设拥有很多库函数,但是最常用的是以下三个函数(使用时右键跳转到函数定义查询即可,如果跳转出错可能是没有编译):

void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);

  GPIO外设也有很多库函数,目前需要了解和学习的是以下函数(其中最重要的是GPIO_Init和八个读写函数):

// 使指定的GPIO外设复位
void GPIO_DeInit(GPIO_TypeDef* GPIOx);

// 使指定的AFIO外设复位	
void GPIO_AFIODeInit(void);

// 用结构体的参数初始化GPIO口
// 基本上STM32的所有外设都使用Init函数进行初始化
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);
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);

// 下面四个是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);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);	// 这个函数可以对16个端口同时进行写入操作

  配置GPIO的工作模式需要对GPIO_InitStructure.GPIO_Mode赋值,其对应的枚举的值的定义如下:

typedef enum
{ GPIO_Mode_AIN = 0x0,	// Analog_Input 模拟输入
  GPIO_Mode_IN_FLOATING = 0x04,	// IN_FLOATING 浮空输入
  GPIO_Mode_IPD = 0x28, // Input_Pull_Down 下拉输入
  GPIO_Mode_IPU = 0x48, // Input_Pull_Up 上拉输入
  GPIO_Mode_Out_OD = 0x14,	// Output_Open_Drain 开漏输出
  GPIO_Mode_Out_PP = 0x10,	// Output_Push_Pull 推挽输出
  GPIO_Mode_AF_OD = 0x1C,	// Alternate_function_Open_Drain 复用开漏
  GPIO_Mode_AF_PP = 0x18	// Alternate_function_Push_Pull 复用推挽
}GPIOMode_TypeDef;

  由于端口的数据规律是:16位二进制代码的每一位都对应一个端口,所以如果想同时选中多个端口,可以在给GPIO_InitStructure.GPIO_Pin赋值时采用按位或的操作:

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;	// GPIO_Pin_All == 0xFFFF 选中GPIO的所有引脚

  在GPIO_SetBits函数的第二个参数位置,也可以用按位或的方式选中多个引脚。GPIO_ResetBits函数也可以这样操作。GPIO_WriteBit函数的第二个参数做这样的操作程序不会报错,但我认为这样做没有意义,因为明显使用GPIO_Write函数更加方便。
  类似地,在时钟控制时,也可以用按位或地方式同时开启多个挂载在APB2上的外设的时钟。数据的规律都是:二进制代码的某一位对应一个外设

3.4.1 GPIO 实现LED闪烁

  接线图和代码如下所示:
LED闪烁

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

int main()
{
	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);
	
	// 接下来是GPIO输入函数的使用
	// GPIO_SetBits(GPIOA, GPIO_Pin_0);		// 给相应端口设置高电平
	// GPIO_ResetBits(GPIOA, GPIO_Pin_0);	// 给相应端口设置低电平
	// GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);	// 在相应端口写入数据 (Bit_RESET是一个枚举,它就是写入的数据,意为写入低电平)
	GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);	// 在相应端口写入数据,意为写入高电平
	while(1)
	{
		// 下面三种方式都可以实现LED闪烁
		GPIO_ResetBits(GPIOA, GPIO_Pin_0);
		Delay_ms(500);
		GPIO_SetBits(GPIOA, GPIO_Pin_0);
		Delay_ms(500);
		
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
		Delay_ms(500);
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
		Delay_ms(500);
		
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)0);	// 第三个参数直接写1,或者写0会报出一个警告,原因是1和0不是BitAction这个枚举类型,所以需要用到强制类型转换
		Delay_ms(500);
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)1);
		Delay_ms(500);
	}
}

3.4.2 GPIO 实现流水灯

  接线图和代码如下所示:
流水灯

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

int main()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;	// 选中所有引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	
	while(1)
	{
		// GPIO_Write函数可以对16个端口同时进行写入操作
		GPIO_Write(GPIOA, ~0x0001);	// 0000 0000 0000 0001 低电平驱动,PA0置低电平
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0002);	// 0000 0000 0000 0010 低电平驱动,PA1置低电平
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0004);	// 0000 0000 0000 0100 低电平驱动,PA2置低电平
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0008);	// 0000 0000 0000 1000 低电平驱动,PA3置低电平
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0010);	// 0000 0000 0001 0000 低电平驱动,PA4置低电平
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0020);	// 0000 0000 0010 0000 低电平驱动,PA5置低电平
		Delay_ms(500);
		GPIO_Write(GPIOA, ~0x0040);	// 0000 0000 0100 0000 低电平驱动,PA6置低电平
		Delay_ms(500);
	}
}

3.4.3 GPIO 蜂鸣器(无源)输出

  接线图和代码如下所示(该蜂鸣器为低电平驱动):
蜂鸣器

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

int main()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

	GPIO_InitTypeDef GPIO_InitStructure_B;
	GPIO_InitStructure_B.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure_B.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure_B.GPIO_Speed = GPIO_Speed_50MHz;

	GPIO_Init(GPIOB, &GPIO_InitStructure_B);
	
	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);
	}
}

3.5 GPIO输入 应用实操

​  本小节实现两个现象:按键控制LED亮灭和光敏传感器控制蜂鸣器模块。在之后的编程实践中,用Delay函数一样,对外设功能的实现都采用模块化编程的方式
​  GPIO端口的四个输入函数定义和用法如下所示:

// 读取输入寄存器对应位的值
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

// 读取整个输入寄存器
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);

// 读取输出寄存器对应位置的值(输出的同时对数据进行检查)
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

// 读取整个输出(输出的同时对数据进行检查)
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);

​  新建工程组Hardware,对各个外设模块化的过程可以分为以下四步:

  1. 在工程组中方新建模块化文件xxx.c/.h,在xxx.c中首先添加#include "stm32f10x.h",在xxx.h中添加#ifndef防止重定义格式;
  2. 定义xxx_Init初始化函数,在该函数中完成对GPIO工作模式的配置和默认电平的选择
  3. xxx.c文件定义外设对应的功能函数;
  4. xxx.c文件中所有的函数头添加到xxx.h文件中,以声明这些函数可供外部调用。

​  注意:所有的文件最后需要空一行,否则会报警告。

3.5.1 按键控制LED亮灭


​  硬件连接图如下所示:
请添加图片描述

  首先实现对LED亮灭的模块化。(LED.h文件仅在此作一次示例,之后不作额外展示)

  • 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);
void LED1_Turn(void);
void LED2_Turn(void);

#endif

  • LED.c
#include "stm32f10x.h"                  // Device header

/**
  * @brief  LED的初始化函数,将GPIOA配置为推挽输出模式
  * @param  无
  * @retval 无
  */
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设置为高电平
}

/**
  * @brief  点亮LED1,将PA1置低电平
  * @param  无
  * @retval 无
  */
void LED1_ON(void)
{
	GPIO_ResetBits(GPIOA, GPIO_Pin_1);
}

/**
  * @brief  熄灭LED1,将PA1置高电平
  * @param  无
  * @retval 无
  */
void LED1_OFF(void)
{
	GPIO_SetBits(GPIOA, GPIO_Pin_1);
}

/**
  * @brief  点亮LED2,将PA2置低电平
  * @param  无
  * @retval 无
  */
void LED2_ON(void)
{
	GPIO_ResetBits(GPIOA, GPIO_Pin_2);
}

/**
  * @brief  熄灭LED2,将PA2置高电平
  * @param  无
  * @retval 无
  */
void LED2_OFF(void)
{
	GPIO_SetBits(GPIOA, GPIO_Pin_2);
}

/**
  * @brief  LED1的翻转函数,实现按一下点亮,再按一下熄灭
  * @param  无
  * @retval 无
  */
void LED1_Turn(void)
{
	if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_1) == 0)	// GPIOA处于输出模式,如果此时GPIOA_Pin_1输出0(LED1亮)
	{
		GPIO_SetBits(GPIOA, GPIO_Pin_1);	// 将灯熄灭
	}
	else	// 如果此时GPIOA_Pin_1输出为1(LED1灭)
	{
		GPIO_ResetBits(GPIOA, GPIO_Pin_1);	// 将灯点亮
	}
}

/**
  * @brief  LED2的翻转函数,实现按一下点亮,再按一下熄灭
  * @param  无
  * @retval 无
  */
void LED2_Turn(void)	// LED2的翻转函数,实现按一下点亮,再按一下熄灭
{
	if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_2) == 0)	// GPIOA处于输出模式,如果此时GPIOA_Pin_2输出0(LED2亮)
	{
		GPIO_SetBits(GPIOA, GPIO_Pin_2);	// 将灯熄灭
	}
	else	// 如果此时GPIOA_Pin_2输出为1(LED2灭)
	{
		GPIO_ResetBits(GPIOA, GPIO_Pin_2);	// 将灯点亮
	}
}

  • Key.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/**
  * @brief  按键初始化函数
  * @param  无
  * @retval 无
  */
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);
}

/**
  * @brief  返回按下按键的值,若不按下按键默认返回0
  * @param  无
  * @retval KeyNum 按键对应的值
  */
uint8_t Key_GetNum(void)
{
	uint8_t KeyNum = 0;
	
	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)	// 读取1端口的值
	{
		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;
}

  • main.c
#include "stm32f10x.h"                  // Device header
#include "LED.h"
#include "Key.h"

uint8_t KeyNum;	// 全局变量,用于存储按键对应的值

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

3.5.2 光敏传感器控制蜂鸣器

​  硬件连接图如下所示:
在这里插入图片描述

  • Buzzer.c
#include "stm32f10x.h"                  // Device header

/**
  * @brief  蜂鸣器的初始化函数,将GPIOB配置为推挽输出模式
  * @param  无
  * @retval 无
  */
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);	// 默认将PB12设置为高电平(蜂鸣器为低电平驱动)
}

/**
  * @brief  蜂鸣器报警,将PB12置低电平
  * @param  无
  * @retval 无
  */
void Buzzer_ON(void)
{
	GPIO_ResetBits(GPIOB, GPIO_Pin_12);
}

/**
  * @brief  蜂鸣器不报警,将PB12置高电平
  * @param  无
  * @retval 无
  */
void Buzzer_OFF(void)
{
	GPIO_SetBits(GPIOB, GPIO_Pin_12);
}

/**
  * @brief  蜂鸣器的翻转函数
  * @param  无
  * @retval 无
  */
void Buzzer_Turn(void)
{
	if (GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_12) == 0)	// GPIOB处于输出模式,如果此时GPIOB_Pin_12输出0(蜂鸣器报警)
	{
		GPIO_SetBits(GPIOB, GPIO_Pin_12);	// 蜂鸣器不报警
	}
	else	// 如果此时GPIOB_Pin_12输出为1(蜂鸣器不报警)
	{
		GPIO_ResetBits(GPIOB, GPIO_Pin_12);	// 蜂鸣器报警
	}
}

  • LightSensor.c
#include "stm32f10x.h"                  // Device header

/**
  * @brief  光敏传感器初始化函数(GPIO配置)
  * @param  无
  * @retval 无
  */
void Light_Sensor_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_Init(GPIOB, &GPIO_InitStructure);
}

/**
  * @brief  获取当前的输入数据
  * @param  无
  * @retval 输入寄存器对应位的数据
  */
uint8_t Light_Sensor_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();
	Light_Sensor_Init();
	
	while(1)
	{
		if (Light_Sensor_Get() == 1)	// 如果感光模块输出为1(光线暗)
		{
			Buzzer_ON();
		}
		else	// 如果光线明亮
		{
			Buzzer_OFF();
		}
	}
}


​  欢迎点赞,收藏,如有错误或疑惑敬请在评论区指出和讨论,非常感谢!


​  其他各期笔记链接:
​  STM32学习笔记(二)丨STM32程序调试丨OLED的使用

  • 13
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Include everything

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值