快捷键分享
C t r l + s − − − > 保存文件 Ctrl + s--->保存文件 Ctrl+s−−−>保存文件
C t r l + A l t + S p a c e − − − − > 弹出代码提示框 Ctrl + Alt + Space ---->弹出代码提示框 Ctrl+Alt+Space−−−−>弹出代码提示框
按键控制LED
我们按照视频教程,把按键控制LED的硬件电路接好
如图,按键一端接在GND,一端接在GPIO口,就是我们PPT中的第一种接法。LED一端接GPIO口,另一端接VCC,就是低电平点亮的接法


接下来我们就需要在这个工程上完成LED 和按键 的驱动代码
但是如果把这两部分驱动代码都混在主函数里面,显得很乱,不容易管理,也不容易移植
所以对于这种驱动代码,我们采取封装函数 的办法,单独放在另外的.c和.h文件里,这就是模块化编程的方式
后续我们跟随视频教程完成模块化编程
我们在添加文件时,这个LED.c用来存放驱动程序的主体代码 ,LED.h用来存放这个驱动程序可以对外提供的函数或变量的声明

注意.h文件要添加一个防止头文件重复包含 的代码,格式都是固定的
最后这个***#endif是和#ifndef***组成的括号,注意最后要以空行结尾

接着我们就来封装一下LED的代码,我们打开LED.c文件,首先写一个LED初始化函数,里面写的就是打开时钟,配置端口模式这些东西

- 时钟相当于单片机的心脏,开启后才能实现相应的功能
- 我们需要把初始化函数放在.h文件这里,记得加分号。
- 这样就是对模块外部声明,这个函数是可以被外部调用的函数
如下图,配置成功。这说明我们得端口配置和模块化编程是没有问题的

这里因为GPIO配置好之后默认就是低电平,所以无需其他操作,LED就会亮起来,所以我们可以先用在3.2.STM32-LED闪烁&LED流水灯&蜂鸣器里面学到的GPIO_ResetBits 和GPIO_SetBits输出函数手动配置高低电平

貌似可以折叠起来

然后复制,我们再粘贴到.h文件里面声明一下

这样LED的驱动函数模块就封装好了
接下来我们根据教程写一下按键初始化函数

注意输入模式要填GPIO_Mode_IPU(上拉输入) ,因为这几个按键都是接地的,浮空时输入低电平,那么输出高电平怎么实现?就需要上拉电阻输出高电平,我们可以在3.3.GPIO输入看到按键的硬件电路

接着我们再来写一个读取按键值的函数uint8_t Key_GetNum(),调用这个函数,就可以返回按下按键的键码,键码默认给0,松开按键时,返回0

uint8_t Key_GetNum()的返回值是uint8_t,我们在3.3.GPIO输入的C语言的数据类型同样讲过,这就是unsigned char的意思

在中间这里,我们就需要用到读取GPIO端口的功能了。
我们打开库函数
按键读取函数
第一个
GPIO_ReadInputDataBit,这个函数是用来读取输入数据寄存器某一个端口的输入值 的,它的参数是GPIOx 和GPIO_Pin ,用来指定某一个端口,返回值uint8_t代表这个端口的高低电平。读取按键我们就需要用到这个函数
第二个
GPIO_ReadInputData,这个函数比上一个函数少了一个Bit,它是用来读取整个输入数据寄存器的,参数只有一个GPIOx,用来指定外设,返回值是uint16_t,是一个16位的数据,每一位代表一个端口值
第三个
GPIO_ReadOutputDataBit,这个函数是用来读取输出数据寄存器的某一个位,原则上来说,它并不是用来读取端口的输入数据的,这个函数一般用于输出模式,用来看一下自己输出的是什么
第四个
GPIO_ReadOutputData,这个函数也是少了一个Bit,意思也是一样,是用来读取整个输出寄存器的
为了解释这些函数的功能,我们回顾一下3.1.STM32-GPIO通用输入输出口中对GPIO位结构的介绍

GPIO_ReadInputDataBit,就是读取这里输入数据寄存器的每一位
GPIO_ReadInputData,就是读取这整个输入数据寄存器
GPIO_ReadOutputDataBit,就是读取这里输出数据寄存器的某一位

GPIO_ReadOutputData,就是读取这整个输出数据寄存器

所以说,如果你想读取GPIO口 的话,需要用ReadInput 的这两个函数
如果在输出模式下,想要看现在输出了什么,才需要用到ReadOutput这两个函数,这就是这四个函数的用途

我们回到Key.c这里来,在这里我们需要读取外部输入的一个端口值

我们刚刚加入的这个函数
GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1);
它会返回 GPIOB 端口第 1 号引脚当前的电平,是 0 还是 1。
逐字解释
"输入寄存器某一位的值"
其实就是说:
STM32 每个 GPIO 端口都有一个 输入数据寄存器 IDR 。
它里面有 16 个 bit(0~15),每个 bit 对应一个引脚现在的电平情况。
- bit = 1 → 说明这个引脚现在是 高电平
- bit = 0 → 说明这个引脚现在是 低电平
GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1)
干的事情就是:
- 去看 GPIOB->IDR 这个寄存器
- 取出 第 1 位(Pin1)
- 把这个 0 或 1 返回给你
所以我们需要加上一个if语句,如果读取PB1端口值为0 (== 0),就代表按键按下
开始进入if内操作,这时按键刚按下,会有个抖动,所以需要Delay一段时间(要用Delay函数需要声明一下头文件),然后再这里Delay_ms(20)消除一下抖动。接着我们还需要检测一下按键松手的情况
-
因为我们的按键一般是松手之后才有动作的,所以在这里加上一个while循环 ,判断条件还是读取PB1 == 0这个,如果按键一直按下,就卡在这里,直到松手。松手之后,再Delay_ms(20);消除一下按键松手的抖动,接着我们赋值KeyNum = 1,把键码1传递出去,这就是PB1按键的检测
-
PB11按键也是一样的
-
这是一个阻塞式的延时去抖按键检测程序:
按键按下 → 等 20ms → 等你松手 → 再等 20ms → 返回按键编号。 -
虽然能用,但:
-
- Delay_ms(20) 会让 CPU 卡死
- while(...) 也会让 CPU 卡死
- 多任务下会导致其他功能失效(串口丢数据、定时器错乱)
- 不适合复杂项目
·

接着我们在主函数声明一下Key.h文件,再定义一个全局变量KeyNum,用来存一下键码的返回值
注意这个KeyNum是全局变量,函数内的KeyNum是局部变量,两者的作用域不一样。
局部变量只能在本函数里使用,在外面的是全局变量,每个函数都可以使用。在函数里优先使用自己的局部变量,如果没有,才会使用外部的全局变量

那我就用这个全局变量来接收返回值。接着在while循环写上这个
KeyNum = Key_GetNum();,不断读取键码值,放在KeyNum变量里

接着在主函数写下这一段代码,就可以实现按按键1,LED1点亮,按按键2,LED1熄灭
但是我们演示的程序是按一下熄灭,再按一下点亮。也就是按键按下,LED的状态取反,这个怎么实现呢?

这就需要用到GPIO_ReadOutput函数了

我们可以在LED的驱动代码里,再加一个函数void LED1_Turn()
这整个逻辑就是,调用GPIO_ReadOutputDataBit函数,读取当前的端口输出状态,如果当前输出0,就给他置1,否则就置0
这也是GPIO_ReadOutput函数的用途,一般用于输出模式下

把LED2也加上翻转的功能,然后把函数名复制到.h文件声明一下即可。这样我们就可以在主函数使用这两个功能了
接下来我们展示一下最终效果
按键控制LED
因为博主的面包板质量不太好,总是接触不良,所以只演示一个按键控制一个LED的情况
最后注意一下,这个驱动函数模块 写好之后,尽量在函数上面加上一些注释,说明一下函数的用途、参数的取值和返回值的意思这些东西
下面给大家配上图,演示一下
main函数
图片版

代码版
c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
uint8_t KeyNum; //定义用于接收按键键码的变量
int main(void)
{
/*模块初始化*/
LED_Init(); //LED初始化
Key_Init(); //按键初始化
while (1)
{
KeyNum = Key_GetNum(); //获取按键键码
if (KeyNum == 1) //按键1按下
{
LED1_Turn(); //LED1翻转
}
if (KeyNum == 2) //按键2按下
{
LED2_Turn(); //LED2翻转
}
}
}
LED.c文件
图片版





代码版
c
#include "stm32f10x.h" // Device header
/**
* 函 数:LED初始化
* 参 数:无
* 返 回 值:无
*/
void LED_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
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); //将PA1和PA2引脚初始化为推挽输出
/*设置GPIO初始化后的默认电平*/
GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2); //设置PA1和PA2引脚为高电平
}
/**
* 函 数:LED1开启
* 参 数:无
* 返 回 值:无
*/
void LED1_ON(void)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_1); //设置PA1引脚为低电平
}
/**
* 函 数:LED1关闭
* 参 数:无
* 返 回 值:无
*/
void LED1_OFF(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_1); //设置PA1引脚为高电平
}
/**
* 函 数:LED1状态翻转
* 参 数:无
* 返 回 值:无
*/
void LED1_Turn(void)
{
if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_1) == 0) //获取输出寄存器的状态,如果当前引脚输出低电平
{
GPIO_SetBits(GPIOA, GPIO_Pin_1); //则设置PA1引脚为高电平
}
else //否则,即当前引脚输出高电平
{
GPIO_ResetBits(GPIOA, GPIO_Pin_1); //则设置PA1引脚为低电平
}
}
/**
* 函 数:LED2开启
* 参 数:无
* 返 回 值:无
*/
void LED2_ON(void)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_2); //设置PA2引脚为低电平
}
/**
* 函 数:LED2关闭
* 参 数:无
* 返 回 值:无
*/
void LED2_OFF(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_2); //设置PA2引脚为高电平
}
/**
* 函 数:LED2状态翻转
* 参 数:无
* 返 回 值:无
*/
void LED2_Turn(void)
{
if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_2) == 0) //获取输出寄存器的状态,如果当前引脚输出低电平
{
GPIO_SetBits(GPIOA, GPIO_Pin_2); //则设置PA2引脚为高电平
}
else //否则,即当前引脚输出高电平
{
GPIO_ResetBits(GPIOA, GPIO_Pin_2); //则设置PA2引脚为低电平
}
}
Key.c文件
图片版


代码版
c
#include "stm32f10x.h" // Device header
#include "Delay.h"
/**
* 函 数:按键初始化
* 参 数:无
* 返 回 值:无
*/
void Key_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
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_Init(GPIOB, &GPIO_InitStructure); //将PB1和PB11引脚初始化为上拉输入
}
/**
* 函 数:按键获取键码
* 参 数:无
* 返 回 值:按下按键的键码值,范围:0~2,返回0代表没有按键按下
* 注意事项:此函数是阻塞式操作,当按键按住不放时,函数会卡住,直到按键松手
*/
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0; //定义变量,默认键码值为0
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) //读PB1输入寄存器的状态,如果为0,则代表按键1按下
{
Delay_ms(20); //延时消抖
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0); //等待按键松手
Delay_ms(20); //延时消抖
KeyNum = 1; //置键码为1
}
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0) //读PB11输入寄存器的状态,如果为0,则代表按键2按下
{
Delay_ms(20); //延时消抖
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0); //等待按键松手
Delay_ms(20); //延时消抖
KeyNum = 2; //置键码为2
}
return KeyNum; //返回键码值,如果没有按键按下,所有if都不成立,则键码为默认值0
}
按键控制LED 的代码到这里就结束了,接着我们来到光敏传感器控制蜂鸣器的代码编写
光敏传感器控制蜂鸣器
接线图
我们先看一下接线图
这里接了两个模块
左边的蜂鸣器同之前的接法。
VCC,GND 接电源,控制脚接PB12号口
右边的光敏传感器,VCC,GND同样都是接电源,DO(数字输出端)接PB13号口

当我们遮住光线时,输出指示灯灭,代表输出高电平,松手时,输出指示灯亮,代表输出低电平
其上的电位器可以调节高低电平的判断阈值
代码设计
主要步骤还是驱动程序的封装
蜂鸣器+光敏传感器
main.c文件
c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Buzzer.h"
#include "LightSensor.h"
int main(void)
{
/*模块初始化*/
Buzzer_Init(); //蜂鸣器初始化
LightSensor_Init(); //光敏传感器初始化
while (1)
{
if (LightSensor_Get() == 1) //如果当前光敏输出1
{
Buzzer_ON(); //蜂鸣器开启
}
else //否则
{
Buzzer_OFF(); //蜂鸣器关闭
}
}
}
蜂鸣器.c
c
#include "stm32f10x.h" // Device header
/**
* 函 数:蜂鸣器初始化
* 参 数:无
* 返 回 值:无
*/
void Buzzer_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
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); //将PB12引脚初始化为推挽输出
/*设置GPIO初始化后的默认电平*/
GPIO_SetBits(GPIOB, GPIO_Pin_12); //设置PB12引脚为高电平
}
/**
* 函 数:蜂鸣器开启
* 参 数:无
* 返 回 值:无
*/
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_ReadOutputDataBit(GPIOB, GPIO_Pin_12) == 0) //获取输出寄存器的状态,如果当前引脚输出低电平
{
GPIO_SetBits(GPIOB, GPIO_Pin_12); //则设置PB12引脚为高电平
}
else //否则,即当前引脚输出高电平
{
GPIO_ResetBits(GPIOB, GPIO_Pin_12); //则设置PB12引脚为低电平
}
}
蜂鸣器.h
c
#ifndef __BUZZER_H
#define __BUZZER_H
void Buzzer_Init(void);
void Buzzer_ON(void);
void Buzzer_OFF(void);
void Buzzer_Turn(void);
#endif
光敏传感器.c
c
#include "stm32f10x.h" // Device header
/**
* 函 数:光敏传感器初始化
* 参 数:无
* 返 回 值:无
*/
void LightSensor_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
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); //将PB13引脚初始化为上拉输入
}
/**
* 函 数:获取当前光敏传感器输出的高低电平
* 参 数:无
* 返 回 值:光敏传感器输出的高低电平,范围:0/1
*/
uint8_t LightSensor_Get(void)
{
return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13); //返回PB13输入寄存器的状态
}
光敏传感器.h
c
#ifndef __LIGHT_SENSOR_H
#define __LIGHT_SENSOR_H
void LightSensor_Init(void);
uint8_t LightSensor_Get(void);
#endif
最后总结一下
GPIO的使用方法
- 初始化时钟
- 定义结构体
- 赋值结构体
-
- GPIO_Mode可以选择我们之前讲的那8种输入输出模式
-
- GPIO_Pin选择引脚,可以用按位或的方式同时选中多种引脚
-
- GPIO_Speed没啥要求
- 最后使用GPIO_Init函数,将指定的GPIO外设初始化好
- 然后有8个读取和写入的函数,我们读写GPIO口主要就用这些函数就行
- 之后我们学习了模块化编程的方法,一般我们自己要做一个产品的话,外围硬件比较多
- 这时候就要尽量把每个硬件的驱动函数单独提取出来,封装在.c和.h文件里
- 这样有利于简化主函数的逻辑,也有利于移植程序和分工合作
- 做封装时,把函数的注释写清楚,示例已经给出来了