概述
CPU和外设通信的方式有轮训和中断两种方式;所谓轮训就是主动询问某个状态,看看是否是某个值,如果是则采取行动;中断则是一旦发生了,会主动通知CPU;
本章来研究一下通过如何轮训的方式来响应按键事件。
代码概览
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "../lib/STM32F10x_StdPeriph_Driver/inc/stm32f10x_exti.h"
#include "../lib/STM32F10x_StdPeriph_Driver/inc/misc.h"
#include "../lib/STM32F10x_StdPeriph_Driver/inc/stm32f10x_gpio.h"
void delay(unsigned int time)
{
unsigned int i = 0;
while (time--)
{
i = 1000000;
while (i--)
;
}
}
u8 key_read()
{
u8 result = 0;
result = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12);
return result;
}
void led_init()
{
GPIO_InitTypeDef led;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
led.GPIO_Pin = GPIO_Pin_13;
led.GPIO_Mode = GPIO_Mode_Out_PP;
led.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &led);
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
}
void key_init()
{
GPIO_InitTypeDef key;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
key.GPIO_Pin = GPIO_Pin_12;
key.GPIO_Mode = GPIO_Mode_IPD;
// led.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &key);
// GPIO_WriteBit(GPIOB, GPIO_Pin_12, Bit_RESET);
}
void led_opr(int opr){
if(1 == opr){
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
}else{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
}
}
int main(void)
{
led_init();
key_init();
key_nvid_init();
while (1)
{
if (1 == key_read())
{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
}
else
{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
}
}
return 1;
}
Main函数
从main函数入手,来抽丝剥茧,把轮训方式的实现搞掂。
int main(void)
{
led_init();
key_init();
while (1)
{
if (1 == key_read())
{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
}
else
{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
}
}
return 1;
}
LED初始化
void led_init()
{
GPIO_InitTypeDef led;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
led.GPIO_Pin = GPIO_Pin_13;
led.GPIO_Mode = GPIO_Mode_Out_PP;
led.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &led);
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
}
这个在“STM32(4):基于构件库的点亮LED”有详细介绍,左拐即可看到,使能总线,做PC13的初始化/配置工作,然后配置生效,最后执行了一步操作设置PC13为SET,即高电平,这样实现了灭灯的效果,这样,在操作按下按键的时候,可以实现点亮效果。
键盘操作初始化
void key_init()
{
GPIO_InitTypeDef key;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
key.GPIO_Pin = GPIO_Pin_12;
key.GPIO_Mode = GPIO_Mode_IPD;
GPIO_Init(GPIOB, &key);
}
可以看到,键盘初始化和LED初始化非常类似:
第一步是定义初始化GPIO配置结构体;
第二步是配置GPIO;
第三步是生效GPIO配置;
使用到的引脚
键盘某个按键和GPIOB12引脚相连,另外一端和3v3端口相连;当按键按下的时候,PB12和3.3v电源相连,于是PB12变成了高电平(取值为1),后面就可以根据PB12的值是高电平还是低电平来决定灯的亮灭。
![](https://img.haomeiwen.com/i13054148/842689bbc6199bf0.png)
上拉电阻 vs. 下拉电阻
GPIO的工作模式为IPD,即Input Push Down,下拉电阻,为什么选择这个工作模式呢?首先是外设要将电平信息传输到GPIO引脚,所以方向是输入(Input),按键在没有按下的情况下是浮空,所以需要通过下拉电阻将浮空电平下拉倒低电平。
什么是浮空?前面我们看到一个引脚,比如LED的PC13,一端要么接着电源(VCC),要么接地;接电源是高电平,接地是低电平,泾渭分明,但是还有一种情况就是既没有接地也没有接电源,其中按键就是这样情况,下按情况下,PB12和3V3的电源联通,PB12是高电平;但是,没有下按的情况下,其实一种未知的装态,电平介于高低电平之间,且电压值未知的一种状态;
对于浮空状态需要明确电平状态,比如在本次按键点亮等的场景,因为按键在按下的状态,明确应该是高电平,所以没有按下的状态应该是低电平的状态;所以需要通过设置工作模式下拉电阻的,这样浮空状态,因为有下拉电阻电路,将会被处理为低电平。
那么什么是下拉电阻?如下图所示,如果外设在联通的时候,GPIO端口(x取值范围是A,B,C等端口组,不同级别的STM32提供的端口组数量不一样,y的取值范围是0~15,共计16个引脚)应该高电平,那么在浮空/ 悬空(开关断开)的时候状态就应该低电平,怎么能够让断开的时候是低电平呢?就是在GPIO上面增加一个下拉电阻,因为开关断开,所以相当于串联了一个无限大的电阻,处于懒惰,或者说前软怕硬的电路特性,电路走的就是下拉电阻支路,于是GPIOxy在接口浮空的请跨年,处于低电平装填;
![](https://img.haomeiwen.com/i13054148/39a44f53e7c6437c.png)
类似的是上拉电阻,使用的场景正好和下拉电阻相反,适合于开关另一端接地,浮空的状态应该是高电平状态的场景(开关接通,因为接地,所以铁定端口呈现的是低电平),如下所示,需要构造一个上拉电阻电路,依据电路的欺软怕硬的特性,走的是上拉电阻的支路,于是开关断开,指定的接口呈现的是高电平:
![](https://img.haomeiwen.com/i13054148/c6a5557fb9c5b4be.png)
下面是完整的物理电路图:
![](https://img.haomeiwen.com/i13054148/56a26d593c1360fa.png)
可以看到在电路内部会有上拉/下拉电阻电路,来实现浮空处理;
GPIO速度
在LED的配置中,是需要指定工作速度,但是在KEY的配置中却没有了工作速度,这个是因为指定的GPIO_Speed其实是一个采样速度,即采样GPIO引脚的状态,例如如果指定为10MHz,即每秒10x1000000次采样,用来获取信号量,所以GPIOSpeed按照奎斯特定理,采样频率至少是要信号频率的2倍以上才能够比较准确地还原原始信号,一般实际的工作中,是5~10倍;
所以只有引脚为输出模式的引脚需要设定速度,因为一旦一个引脚被配置为输出模式,就代表是有信号输入到这个引脚,然后再从这个引脚中输出;既然是有信号,那么如果一个引脚想要表达这个信号就需要采样,采样就需要指定速度;
比如我们在点亮LED的源码解读中,信号就是在while循环里面灯的亮灭,信号的频率就是由while语句里面的delay函数决定的,然后把信号写入到PC13上面:
int main(void)
{
... ...
while (1)
{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
delay_2(1);
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
delay_2(1);
}
}
芯片的CPU将会将会基于采样频率(GPIO_Speed)来采集,即由硬件层面来进行采样;其实真正起作用的是采样的结果,采样结果直接决定了LED灯的亮灭:
![](https://img.haomeiwen.com/i13054148/4a6573fb1153b1b2.png)
输入vs.输出
那么,回过头来,再来看我们键盘的GPIO配置,作为PB13输入引脚,代表引脚的数据并不是由软件主动写入,输入输出是站在STM32内部芯片的角度来说的,所谓的输入是指,外设将数据到写入GPIO口,需要STM32芯片获取,比如本节按键相应,就是由外设(按键)将自己的电平状态写入到PB12;
输出,则是由STM32内部电路产生信号,写到GPIO口;比如LED灯里面控制PC13的高低电平,就是由软件层面直接控制到STM32电路实现的,所以如果是软件层面直接来配置GPIO口(寄存器)的值,就是输出;
![](https://img.haomeiwen.com/i13054148/a10684b06dd87554.png)
封装灯的亮灭
void led_opr(int opr){
if(1 == opr){
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
}else{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
}
}
这个函数实现了根据参数,决定向PC13写高电平还是低电平;这里面的写法在“STM32(4):基于构件库的点亮LED”里面有说明,这里将其封装为一个函数,就是为了便于重复调用;Bit_Set就是设置高电平,Bit_Rest就是低电平,还记得LED的物理电路图吗?
如果PC13设置为1,和VCC3V3端电压一致,形成不了电压差,所以LED灯处于熄灭的状态,设置PC13为0,LED2两端有电压差,于是产生电流(电流方向是VCC到PC13,电子方向是PC13到VCC,高中物理知识哦)。
GPIO_WriteBit则是(STM32固件库的)库函数,用于向GPIO引脚写入高低电平值。
轮训方式通信
while (1)
{
if (1 == key_read())
{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
}
else
{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
}
}
下面这一段就是通过轮训方式获取PB12的状态(通过key_read函数),然后根据PB12的状态来设置PC13的状态;所谓的轮训方式,就是不断的获取指定引脚的状态;
轮训和后面要将的中断形成鲜明对比;就像上海的有的快递,送到了指定地点,不通知你,于是你只能一遍一遍的刷新手机,看看是否已经送到,送到了下去去取,这个就是类似于“轮训通信”;而有的良心快递送到之后,回短信或者电话通知你,接到通知之后,你直接下楼取快递即可,这个就是“中断通信”;
读取引脚状态
我们看一下key_read()的实现:
u8 key_read()
{
u8 result = 0;
// delay(1);
result = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12);
return result;
}
这里直接调用的(STM32固件库的)库函数GPIO_ReadInputDataBit函数实现了对于PB12的读取,这里如果担心频繁调用影响性能,可以在key_read或者main主函数的while轮训实现通过调用delay函数实现延迟;轮训方式的缺点就是耗费CPU时间,需要CPU不断的进行轮训;不同中断的主动通知,需要主动的不断的访问;中断优点就会节省CPU时间,但是并非所有事件都能够采用中断通知,在STM32里面预制的中断种类只有256个(其中16个内部中断,240个外部中断)。
总结
本章中主要介绍了通过轮训的方式实现基于按键来实现LED等亮灭控制;主要的步骤有LED初始化,按键初始化以及轮训读取PB12的引脚状态;
需要掌握的是轮训通信机制的原理以及实现,还有引脚控制相关上拉/ 下拉电阻,GPIO速度以及引申出来的输入/ 输出概念,
网友评论