STM32入门教程-2023版【3-4】按键控制制LED

发布时间:2024年01月10日

关注?+?点赞????不错过精彩内容

图片

大家好,我是硬核王同学,最近在做免费的嵌入式知识分享,帮助对嵌入式感兴趣的同学学习嵌入式、做项目、找工作!?

这篇文章以项目代码的形式实现GPIO输入

一、按键控制LED

(1)搭建面包板电路

根据接线图接线,两个按键分别接B1、B11,两个LED接A1、A2,按键一端接GPIO口,一端接GND,就是上一章第一种的接按键的方法,LED一接GPIO口,一端接VCC,就是低电平点亮的接法。这些按键和按键和GPIO端口连接都是随意的,具体接多少个,哪个端口,哪个外设都看自己的需求。

图片

(2)新建工程

打开工程文件夹,复制一下蜂鸣器工程的代码,改个名字叫 3-4 按键控制LED。

打开工程后,此时我们需要完成LED和按键的驱动代码,但把两个混在主函数里,就会太乱,也不好移植,所以这次选择将驱动代码封装起来,单独放在.c和.h文件里,这就是模块化编程的方式。

图片

想封装代码,可以打开工程文件夹,再新建一个文件夹,叫Hardware,用来存放硬件驱动

图片

回到keil,也添加一个Hardware的组

图片

再添加一个Hardware的头文件路径

图片

图片

(3)封装LED代码

在左边的Project中右键Hardware组,添加一个LED的C文件,这个文件就是封装LED的程序

图片

再右键Hardware组,添加一个LED的.H头文件,这个文件就是封装LED的程序

图片

这样在Hardware中我们就有了LED.c和LED.h两个文件用来封装LED的驱动程序。LED.c用来存放驱动程序的主体代码,LED.h用来存放这个驱动程序可以对外提供的函数或变量的声明

这两个文件建好以后还需要添加一些必要的代码使其可以正常使用,首先在LED.c文件中右键,include一个stm32f10x的头文件

图片

在在LED.h文件中添加防止头文件重复包含的代码

图片

在LED.c文件中,我们首先需要一个LED的初始化函数,则可以写成如下形式

图片

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

开启GPIOA端口的时钟,使能对该端口的访问。

GPIO_InitTypeDef GPIO_InitStructure;

定义一个名为GPIO_InitStructure的结构体变量,用于配置GPIO引脚的属性。

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

将GPIO引脚的模式设为推挽输出,表示该引脚可以输出电平。

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;

将GPIO引脚的第1和第2引脚设置为待初始化。

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

设置GPIO引脚的速度为50MHz,表示引脚的最大切换速率为50MHz。

GPIO_Init(GPIOA, &GPIO_InitStructure);

根据上述配置的属性初始化GPIOA端口的引脚。第二个参数是结构体变量名,前面加上取地址的符号,这里使用的是地址传递。

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);}//这个函数是用来初始化LED的

因为这个函数是需要被外部引用的,所以我们需要将这个函数名复制到LED.h的文件中,后面不要忘了加分号。这样就是对模块外部声明,这个函数是可以被外部调用的函数

图片

此时,我们可以回到main.c,把上一个实验蜂鸣器的这些代码删掉,再包含LED模块的头文件,之后在主函数里,直接调用LED_Init,这样就完成了LED的初始化。

在这条代码前,有提示一个警告,这是因为我们新写的代码还没有更新,软件还不知道有这个函数,编译一下就会显示0错误0警告,有时候这个软件也会时不时报个警告或错误,这个有可能是语法检查更新较慢,直接编译一下,没有问题就行了

图片

将此代码编译下载后,可以看到LED灯已经点亮了,这说明我们的代码里写的端口配置和模块化编程是没有问题的

图片

因为GPIO配置好了之后默认就是低电平,所以我们还没操作LED,LED就亮起来了,那我们可以在LED_Init函数的最后加上GPIO_ResetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2);这样初始化之后,如果不操作LED,LED就是熄灭的了

图片

 
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_ResetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2);//GPIO配置好后默认是低电平}//这个函数是用来初始化LED的

将此代码编译下载后,可以看到LED灯已经熄灭了,这样初始化之后LED默认就是关闭的状态了,这样只需调用LED_Init();两个LED的两个GPIO口就初始化完成了

除了初始化,我们还需要点亮和熄灭LED的函数,在LED.c文件中可以加上

void LED1_ON(void){ ? ? ? ? GPIO_ResetBits(GPIOA, GPIO_Pin_1);} void LED1_OFF(void){ ? ? ? ? GPIO_SetBits(GPIOA, GPIO_Pin_1);}void LED2_ON(void){ ? ? ? ? GPIO_ResetBits(GPIOA, GPIO_Pin_2);} void LED2_OFF(void){ ? ? ? ? GPIO_SetBits(GPIOA, GPIO_Pin_2);}

图片

这里用了4个函数来实现两个灯的打开和关闭,如果你觉得函数太多了,那你也可以定义一个LED_Set函数,包含两个参数,一个参数选择操作哪个灯,另一个参数选择开还是关,这样写复用性更高,推荐使用这种写法。

这里同时记得去LED.h文件里面声明这四个函数

void LED1_ON(void); void LED1_OFF(void);void LED2_ON(void); void LED2_OFF(void);

图片

下面接着实现LED闪烁,直接在主函数调用,编译后下载这样LED就在交替闪烁了

图片

模块化编程的好处:将驱动代码封装起来,使得主函数中代码变得更加简洁,初始化就是初始化,开灯就是开灯,关灯就是关灯,不需要再管那些底层的各种参数了

(4)封装按键代码

和LED一样,我们可以把按键也封装到驱动函数模块中,右键Hardware组,添加一个Key的C文件,记得Key.c文件中右键添加stm32f10x的头文件

图片

再右键Hardware组,添加一个Key的.H头文件,记得添加防止头文件重复包含的代码

图片

图片

首先在Key.c文件中写一个Key_Init初始化函数,可以写成如下形式

图片

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

函数使能GPIOB的时钟。

GPIO_InitTypeDef GPIO_InitStructure;

定义一个名为GPIO_InitStructure的结构体变量,用于配置GPIO引脚的属性。

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;

将GPIO引脚的模式设为上拉输入模式,在上拉输入模式下,当按钮未按下时,GPIO引脚上的电压会被拉高到VDD电压。

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;

将GPIO引脚的第1和第11引脚,也就是按键所连的引脚,设置为待初始化。

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

设置GPIO引脚的速度为50MHz,表示引脚的最大切换速率为50MHz。

GPIO_Init(GPIOB, &GPIO_InitStructure);

根据上述配置的属性初始化GPIOB端口的引脚。第二个参数是结构体变量名,前面加上取地址的符号,这里使用的是地址传递。

 
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_Init(GPIOB, &GPIO_InitStructure);}

接着再写一个读取按键值的函数,调用这个函数,就可以返回按下按键的键码,它的返回值是uint8 t,就是unsigned char的意思,按键键码默认给0,如果没有按键按下,就返回0。

图片

在这个函数中我们需要用到特殊的GPIO库函数,可以从gpio.h中找一下GPIO的库函数文件,选中的这几个函数是GPIO的读取函数,第一个函数GPIO_ReadInputDataBit是用来读取输入数据奇存器某一个端口的输入值的,它的参数是GPIOx和GPIO_Pin,用来指定某一个端口,返回值是uint8_t,代表这个端口的高低电平,读取按键我们就需要用到这个函数

图片

再看看其他的几个GPIO的读取函数,第一个函数上面刚刚用过,用来读取数据输入寄存器某一个端口的输入值,读取按键要用到这个函数

 
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

第二个函数用来读取整个数据输入寄存器的,参数只有一个GOIOx,用来指定外设,返回值是uint16_t,是一个16位的数据,每一位代表一个端口值

 
*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_ReadInputDataBit读取输入数据寄存器的某一位,GPIO_ReadInputData读取整个输入数据,GPIO_ReadOutputDataBit读取输出数据寄存器的某一位,GPIO_ReadOutputData读取整个输出数据。所以如果想读取GPIO口的数据,就需要用Readlnput的这两个函数,如果在输出模式下,想要看一下现在输出了什么,才需要用到ReadOutput的这两个函数

图片

这里我们需要读取外部输入的端口值,所以可以这样写GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1);用于读取PB1的端口值

图片

这个函数的返回值就是输入寄存器某一位的值,0代表低电平,1代表高电平,这时我们可以加上if判断,如果读取PB1端口值等于0,就代表按键按下,我们进入if里操作

图片

此时按键刚按下,电平不稳定会有个抖动,所以需要Delay一段时间,消一下抖,在这里加上Delay_ms(20); 也不要忘了在Key.c文件中添加Delay.h的头文件

图片

接着我们还需要检测一下接键松手的情况,因为我们的按键一般是松手之后才有动作的,所以在这里加上一个while循环,判断条件还是读取PB1是否等于0

图片

如果按键一直按下,就卡在这里,直到松手,此时电平又会发生抖动,再Delay_ms(20); 消一下按键松手的抖动

图片

接着我们赋值KeyNum=1;用这个变量将键码1,传递出去 ,这就是PB1按键的检测

图片

PB11的按键,也是一样,可以直接复制粘贴

图片

uint8_t KEY_GETNUM(void) ?//uint8_t相当于unsinged char{ ? ? ? ? ? uint8_t KEY_NUM = 0 ; ?//如果按键没有按下,默认给0 ? ? ? if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0 ){ ? ? ? ? ? Delay_ms(20); ?//需要用到delay函数,头文件还需加上#include "Delay.h" ? ? ? ? ? while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0 );//按键一般是松手才有动作,所以加上判断 ? ? ? ? ? Delay_ms(20); //同样是消抖 ? ? ? ? ? KEY_NUM = 1; ? ? ? ? ? } ? ? ? if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0 ){ ? ? ? ? ? Delay_ms(20); ? ? ? ? ? while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0 ); ? ? ? ? ? Delay_ms(20); ? ? ? ? ? KEY_NUM = 2; ? ? ? ? ? } ? ? ? ?//这段实现的功能就是,按键1按下LED1点亮,按键2按下LED2熄灭 ? ? ? ?return KEY_NUM;}

记得在Key.h中声明一下这两个函数

图片

下面验证一下写的按键,到main.c中,添加"Key.h"头文件,然后主循环之前,初始化一下按键

图片

接着我们定义一个全局变量KeyNum,用来存一下键码的返回值,这里注意一下,我们这个也叫KeyNum和Key.c中的不是同一个!main.c中的是全局变量,Key.c中的是局部变量,两者作用域不一样。即使在main函数中,再定义一个同名的KeyNum,这三个都是不一样的,main函数中的也是一个局部变量。在函数外面的是全局变量,每个函数都可以使用,在函数里,优先使用自己的局部变量,我们就用这个全局变量来获取返回值

图片

下面我们在while中实现当按下按键1,LED1点亮,当按下按键2,LED1熄灭

图片

下载编译后,可以正常实现,证明这两部分的代码模块实现成功了

图片

那如果要实现单独一个按键按一下熄灭,再按一下点亮,该如何实现呢?这就需要用到GPIO_ReadOutputDataBit();函数,在LED.c里加上下面这段函数,使LED的状态取反,此时当按键1按下,LED1就会取反

图片

这个函数逻辑就是,调用GPIO ReadOutputDataBit函数,读取当前的端口输出状态,如果当前输出0,就给LED置1,否则,就置0,这样就实现了端口的电平翻转

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);}//实现了端口的电平反转

那我们可以复制一下这个函数,给按键2和LED2也加上翻转的功能

图片

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);}//实现了端口的电平反转

最后记得把这两个函数放到头文件里声明一下

图片

此时可以在main中实现按下按键1按一下LED1点亮,再按一下LED1熄灭,按键2按一下LED2点亮,再按一下LED2熄灭

图片

图片

最后注意一下,这个驱动函数模块写好之后,尽量在这些函数的上面加上一些注释,尽量在这些函数的上面加上一些注释,这样,别人在使用你的函数驱动时,才知道如何使用,就像STM32的库函数一样,在每个函数上面,写一下这样的注释,自己写代码时也尽量打打注释,这样方便自己和别人理解

作?者?:硬核王同学

-------?END?-------

关注公众号回复“加群”按规则加入技术交流群 ?回复“1024”查看更多内容

图片

觉得有用请点个免费的赞

文章来源:https://blog.csdn.net/qq_56921854/article/details/135514411
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。