关注 + 点赞 不错过精彩内容
大家好,我是硬核王同学,最近在做免费的嵌入式知识分享,帮助对嵌入式感兴趣的同学学习嵌入式、做项目、找工作!
这篇文章以项目代码的形式实现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”查看更多内容
觉得有用请点个免费的赞