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

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

图片

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

这篇文章以项目代码的形式实现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”查看更多内容

图片

觉得有用请点个免费的赞

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/309079.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Spring 篇】JdbcTemplate:轻松驾驭数据库的魔法工具

欢迎来到数据库的奇妙世界,在这里,我们将一同揭开Spring框架中JdbcTemplate的神秘面纱。JdbcTemplate是Spring提供的一个简化数据库操作的工具,它为我们提供了一种轻松驾驭数据库的魔法。本篇博客将详细解释JdbcTemplate的基本使用&#xff0…

跑代码相关 初始环境配置

是看了这个视频:深度学习python环境配置_哔哩哔哩_bilibili 总结的个人笔记 这个是从零开始配python环境的比较好的经验教程: 深度学习python的配置(Windows) - m1racle - 博客园 (cnblogs.com) 然后关于CUDA和cuDNN&#xff…

基于NFC(215芯片)和酷狗音乐实现NFC音乐墙

前言: 本文方案可以实现直接调起酷狗音乐app自动播放,而非跳转网址 准备工作: nfc toolsnfc task酷狗音乐APPalook浏览器APP 步骤 1.选一首歌 2.右上角选择分享,选择复制链接 复制内容为: 分享胡夏的单曲《爱夏…

U-Net——第一课

一.论文研究背景、成果及意义二、unet论文结构三、算法架构 一.论文研究背景、成果及意义 医学图像分割是医学图像处理与分析领域的复杂而关键的步骤,目的是将医学图像中具有某些特殊含义的部分分割出来,并提取相关特征,为临床诊疗和病理学研…

算法通关村番外篇-LeetCode编程从0到1系列一

大家好我是苏麟 , 今天开始带来LeetCode编程从0到1系列 . 编程基础 0 到 1 , 50 题掌握基础编程能力 大纲 1768.交替合并字符串389. 找不同28. 找出字符串中第一个匹配项的下标283. 移动零66. 加一 1768.交替合并字符串 描述 : 给你两个字符串 word1 和 word2 。请你从 word1…

外汇天眼:CQG 与 TradeStation Securities 的经纪服务集成

TradeStation Securities, Inc.,一家自营的在线股票、ETF、期权和期货交易经纪公司,宣布与CQG合作,CQG是一家为交易员、经纪商、商业套保者和交易所提供高性能技术解决方案的全球供应商,已与TradeStation Securities的经纪服务集成…

MySql -数据库进阶

一、约束 1.外键约束 外键约束概念 让表和表之间产生关系,从而保证数据的准确性! 建表时添加外键约束 为什么要有外键约束 -- 创建db2数据库 CREATE DATABASE db2; -- 使用db2数据库 USE db2;-- 创建user用户表 CREATE TABLE USER(id INT PRIMARY KEY …

linux内存浅析

内存的基本概念 操作系统内存非常重要且比较复杂,其中有许多知识点仍然需要掌握才能更进一步分析程序问题。由于是初次全面系统地接触OS内存,目的是为了全面且低层次地理解linux内存相关概念,不会深入其中原理,所以本章也会尽量避…

Java实现CR-图片文字识别功能(超简单)

一.什么是OCR OCR (Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗、亮的模式确定其形状,然后用字符识别方法将形状翻译成计算…

【信息论与编码】【北京航空航天大学】实验一、哈夫曼编码【C语言实现】(上)

信息论与编码 实验1 哈夫曼编码 实验报告 一、运行源代码所需要的依赖&#xff1a; 1、硬件支持 Windows 10&#xff0c;64位系统 2、编译器 DEV-Redpanda IDE&#xff0c;小熊猫C 二、算法实现及测试 1、C语言源程序 # define _CRT_SECURE_NO_WARNINGS # include <std…

Qt QCheckBox复选按钮控件

文章目录 1 属性和方法1.1 文本1.2 三态1.3 自动排他1.4 信号和槽 2 实例2.1 布局2.2 代码实现 Qt中的复选按钮类是QCheckBox它和单选按钮很相似&#xff0c;单选按钮常用在“多选一”的场景&#xff0c;而复选按钮常用在"多选多"的场景比如喜欢的水果选项中&#xf…

知识】分享几个摄像头的选型相关知识

【知识】分享几个摄像头的选型相关知识 目录 【知识】分享几个摄像头的选型相关知识一、前言二、正文1、先了解一下监控摄像头的种类1.1、云台型&#xff08;云台型一体摄像机&#xff09;1.2、枪机型&#xff08;枪型摄像机&#xff09;1.3、球机型&#xff08;球型摄像机&…

LeetCode-字符串转换整数atoi(8)

题目描述&#xff1a; 请你来实现一个 myAtoi(string s) 函数&#xff0c;使其能将字符串转换成一个 32 位有符号整数&#xff08;类似 C/C 中的 atoi 函数&#xff09;。 函数 myAtoi(string s) 的算法如下&#xff1a; 读入字符串并丢弃无用的前导空格 检查下一个字符&…

【MySQL】表设计与范式设计

文章目录 一、数据库表设计一对一一对多多对多 二、范式设计第一范式第二范式第三范式BC范式第四范式 一、数据库表设计 一对一 举个例子&#xff0c;比如这里有两张表&#xff0c;用户User表 和 身份信息Info表。 因为一个用户只能有一个身份信息&#xff0c;所以User表和In…

【数学建模】美赛备战笔记 01 美赛指南与竞赛全流程

美赛指南 整篇论文需要在25页内。 六道赛题特点&#xff1a; A、B题涉及到微分方程和物理概念较多&#xff0c;需要一定的专业知识&#xff1b; C题常常涉及到时间序列、机器学习&#xff1b; D题一般是运筹学/网络科学&#xff0c;图论、优化问题&#xff0c;涉及到的概念多…

Day3Qt

1. &#xff08;1&#xff09;完善对话框&#xff0c;点击登录对话框&#xff0c;如果账号和密码匹配&#xff0c;则弹出信息对话框&#xff0c;给出提示”登录成功“&#xff0c;提供一个Ok按钮&#xff0c;用户点击Ok后&#xff0c;关闭登录界面&#xff0c;跳转到其他界面 …

NAS使用的一些常见命令 ssh sftp 上传 下载 ALL in one

目录 登陆上传/下载内网穿透 登陆 ssh 登陆 ssh usernameserverIP -p portNumsftp 登陆 sftp -P portNum usernameserverIP上传/下载 如ls等&#xff0c;远程服务器操作 如lls等&#xff0c;本机操作&#xff0c;前缀为l 文件 put **** 将本机上文件上传到远程服务器上当…

使用Vivado Design Suite平台板、将IP目录与平台板流一起使用

使用Vivado Design Suite平台板流 Vivado设计套件允许您使用AMD目标设计平台板&#xff08;TDP&#xff09;创建项目&#xff0c;或者已经添加到板库的用户指定板。当您选择特定板&#xff0c;Vivado设计工具显示有关板的信息&#xff0c;并启用其他设计器作为IP定制的一部分以…

Keil编译生成的bin文件自动以版本号命名

Keil编译程序生成bin文件时&#xff0c;如何自动以版本号命名bin文件 一、目的二、方法三、实现过程一、目的二、方法三、实现过程1、脚本形式2、可执行文件形式 一、目的 二、方法 三、实现过程 一、目的 Keil编译程序时&#xff0c;生成的Hex/Bin文件名字是根据Keil中工程…

RK3568平台开发系列讲解(Linux系统篇)Linux 内核打印

🚀返回总目录 文章目录 一、方法一:dmseg 命令二、方法二:查看 kmsg 文件三、方法三:调整内核打印等级一、方法一:dmseg 命令 在终端使用 dmseg 命令可以获取内核打印信息,该命令的具体使用方法如下所示: 首先在串口终端使用 “dmseg”命令,可以看见相应的内核打印信息…