本篇文章包含的内容
本次课程采用单片机型号为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 新建工程的步骤
- 建立工程文件夹,在Keil中新建工程(工程文件名称为Project,存放在一个文件夹下,对该工程文件的说明写在文件夹上),选择芯片型号(本次课程选用STM32F103C8T6,故选择STM32F103C8);
- 工程文件夹里建立Start,Library,User等文件夹,复制固件库中的文件到工程文件夹;
-
配置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_vl
,md_vl
,hd_vl
的文件,表中其他型号同理。
-
回到STM32F10x文件夹,将
stm32f10x.h
、system_stm32f10x.c
、system_stm32f10x.h
复制到Start文件夹中,并导入到工程组Start中(stm32f10x.h
就是STM32外设寄存器描述文件,它的作用就同51单片机的头文件REGX52.H
一样,是用来描述STM32有哪些寄存器和它对应的地址的)。
-
-
配置内核寄存器的描述文件
- 打开CM3
→
\rightarrow
→ CoreSupport,
core_cm3.h
、core_cm3.c
就是内核的寄存器描述文件(其中还包含了一些内核的配置函数),同样将它们复制到Start文件夹中,并导入到工程组Start中。
- 打开CM3
→
\rightarrow
→ CoreSupport,
-
配置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.h
,stm32f10x_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文件夹中。
注意事项
需要用的文件一定要复制到工程的文件夹里面,不要添加工程文件夹外面的文件,否则外面的文件一旦挪动位置,工程里就找不到文件了。所以我们要将用的文件复制到工程中,以保证工程的独立性。
-
工程里对应建立Start,Library,User等同名称的分组(Group),然后将文件夹内的文件添加到对应的分组里;
-
点击工程选项、C/C++、在Define内定义USE_STDPERIPH_DRIVER(宏定义,意为使用标准外设驱动)这是库函数的条件编译,使用库函数开发就必须要定义;
- 该宏定义在头文件
stm32f10x.h
中(8296行)
#ifdef USE_STDPERIPH_DRIVER
#include "stm32f10x_conf.h"
#endif
其他工程在这个位置还声明了 STM32F10X_MD的字符串,但是Keil5在新建工程之后会自动声明该字符串,不需要再额外声明。
- 点击工程选项、Debug中下拉列表选择对应的调试器(本次课程使用ST-Link进行调试,故选择ST-Link Debugger),点击Settings,在Flash Download里勾选Reset and Run,勾选上这一项之后,下载的程序会立马复位并执行,省去了在开发板上手动按下复位的步骤。
2.3 工程文件的架构
工程的建立非常灵活,学会建立工程后可以依照自己的风格建立工程。
根据上图,我们可以了解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/.h
,stm32f10x_adc.c/.h
…文件:库函数stm32f10x_conf.h
文件:库函数配置文件- 该文件用来配置头文件的包含关系,它include了所有的库函数头文件,同时在
stm32f10x.h
中又include了stm32f10x_conf.h
。所以,在使用这些库函数时,只需要包含stm32f10x.h
这一个头文件,就相当于包含了所有的头文件
- 该文件用来配置头文件的包含关系,它include了所有的库函数头文件,同时在
2.4 环境的优化处理
- 为防止出现中文乱码在设置🔧中将Encoding改为UTF-8格式(如果打开别人的文件出现乱码,可以尝试修改其他的编码格式)
- 在设置🔧中的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
0V−3.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的基本结构
在所有的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每一位具体的电路结构如上图所示。整体结构可以分为图上部的输入部分和图下部的输出部分。
- 引脚和保护二极管
- 保护二极管可以保证党引脚电平高于 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是否有效,数据选择器的选择等,端口可以被配置为以下八种模式:
下面是各种输入模式的等效电路图:
- 浮空/上拉/下拉输入模式
- 模拟输入模式
- 推挽/开漏输出模式
- 复用推挽/开漏输出模式
可以看到,只有模拟输入模式下,数字输入(施密特触发器)处于关闭状态。输入时输出呈现高阻态,但输出的同时端口同样可以输入(读取端口状态)。
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两个外设):
- 使用RCC开启GPIO的时钟;
- 使用
GPIO_Init
函数初始化GPIO - 使用输出或者输入函数控制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闪烁
接线图和代码如下所示:
#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,对各个外设模块化的过程可以分为以下四步:
- 在工程组中方新建模块化文件
xxx.c/.h
,在xxx.c
中首先添加#include "stm32f10x.h"
,在xxx.h
中添加#ifndef
防止重定义格式; - 定义
xxx_Init
初始化函数,在该函数中完成对GPIO工作模式的配置和默认电平的选择; - 在
xxx.c
文件定义外设对应的功能函数; - 将
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的使用