Author:teacherXue
一、按钮or开关
开关的作用
在物联网场景中,是否需要实体的物理开关或者按钮,它们又能提供怎样的功能呢。开关的工作原理,非常简单,就是导通或者切断电流导通。多数的同学可能快速的想到,通过电路通断控制电器的开关,如下图所示,但是在智能家居环境中,这种方式是否全部适用?

智能场景中的开关
设想这样的场景,我通过物理开关切断了厨房的灯光,现在我需要在卧室通过APP或者语音开灯,嗯,我还是走过去吧。
所以除了需要保证安全的强制断电外,功能性的开关最好不要通过直接的电源切断,要通过采集按钮的控制信号,改变设备的开光状态全局标识。这样app、开关、语音等都可以对设备进行控制。
开关的类型
![]() |
![]() |
二、ESP8266开关元件的应用
接线
前面已经分析过了,在智能场景中,需要保存全局的设备开关状态,我们只需要读取开关元件所在IO口的输入即可。所以首先先把按钮元件接入电路,这里采用的是双引脚复位按钮元件,控制单条电路的通断如下图:

面包板上,我们将MCU的D4接口引出到开关的一个引脚,我这里仍然采用硬质导线,并且将电源扩展的接地引出到另一个引脚。
![]() |
![]() |
直接读取开关状态
1)在上章节多任务代码中,声明D4引脚的引用。
#define BTN_1 D4 // 开关按钮
2)声明开关的任务方法,判断读取的开关IO的数字值,低电平为按下导通状态。当导通触发时,将全局的灯光开关状态标识变量置反向。
// 开关任务
void task_btn()
{
if (digitalRead(BTN_1) == 0)
{
btn1_state = !btn1_state;//切换设备开关状态
}
Serial.println(digitalRead(BTN_1));//串口打印输出结果
}
3)声明t4任务,300毫秒调用一次
Task t4(300, TASK_FOREVER, &task_btn);
4)setup增加开关引脚的电平设定,需要注意,不能单纯的定义为INPUT,因为开关没有负载电阻,无法测出阻值。还好的是ESP8266为每个引脚内置了上拉电阻,需要时可以通过代码启用。并加入t4任务调度,并开启任务。
pinMode(BTN_1, INPUT_PULLUP);//开关按钮为输入开启上拉电阻
runner.addTask(t4);
t4.enable();
5) 全部代码如下
#include <Arduino.h>
#include <TaskScheduler.h>
#define analogInPin A0 // 模拟输入引脚A0
#define LED_1 D5 // D5绿色LED引脚,本例中先不使用
#define LED_2 D6 // D6绿色LED引脚
#define BTN_1 D4 // 开关按钮
bool switch_led1 = false; // 灯光开关状态
int knobValue = 0; // 旋钮读数
bool btn1_state = false;
Scheduler runner; // 任务调度器对象
int dutyCycle = 0; // 亮度值
// 亮灯任务
void task_led1_H()
{
analogWrite(LED_1, switch_led1 ? dutyCycle += 3 : dutyCycle--);
}
// 旋钮任务
void task_knob()
{
if (btn1_state)
{
knobValue = analogRead(analogInPin);
// 读取模拟数值
analogWrite(LED_2, map(knobValue, 15, 1008, 0, 255));
pinMode(LED_2, HIGH);
}
else
{
pinMode(LED_2, LOW);
}
}
// 开关任务
void task_btn()
{
if (digitalRead(BTN_1) == 0)
{
btn1_state = !btn1_state;
}
Serial.println(digitalRead(BTN_1));
}
// 定义任务
Task t1(3, TASK_FOREVER, &task_led1_H); // 任务名称t1,间隔3ms一直执行.
Task t3(30, TASK_FOREVER, &task_knob); // 任务名称t3,间隔50ms,一直执行。
Task t4(300, TASK_FOREVER, &task_btn);
void setup()
{
// put your setup code here, to run once:
Serial.begin(115200); // 设置串口通信波特率
pinMode(LED_1, OUTPUT);
pinMode(LED_2, OUTPUT);
pinMode(BTN_1, INPUT_PULLUP);//开关按钮为输入开启上拉电阻
// pinMode(BTN_1, HIGH);
runner.init(); // 初始化任务调度器
runner.addTask(t1); // 增加任务调度
runner.addTask(t3);
runner.addTask(t4);
t1.enable(); // 任务开始
t3.enable();
t4.enable();
}
void loop()
{
// 达到最亮后后转为逐渐熄灭,熄灭200毫秒后改为逐渐亮起,有400毫秒的全黑时间
if (dutyCycle >= 255)
{
switch_led1 = false;
}
if (dutyCycle <= -200)
{
switch_led1 = true;
}
runner.execute(); // 执行任务
}
6)烧录代码,观察运行结果,因为D4引脚本身是内置LED的引脚,所以可以看见板载LED反馈了按键的状态。

三、使用JC_Button库
上面的代码不复杂,但是如果我们提升要求,例如增加长按和短按识别功能等,代码量就上来了,所以继续拿来主义。JC_Button用于消除和读取瞬时接触开关,如触觉按钮开关。”可以检测到任意长度的“长按”。在状态机构造中有不错的应用。
引入支持库
先新建项目Lamp_switch_v1.0并设置串口波特率,然后在扩展库里直接搜索JC_Button,并将其安装到项目上。

我们从扩展库里下载的资源,基本都是开源项目。包括了源代码和丰富的演示案例,大多数情况下,通过其演示案例,我们就能获得完整的使用说明。在安装扩展库完成后,依次展开项目目录下的.pio -> libdeps\esp12e -> examples,这里展示的就是官方的案例库,下面还可以看到src源码,有需要的情况下我们可以在遵循开源协议的前提下进行修改和二次开发。

这些示例从名称上就可以看出其用途,具体说明如下:
SimpleOnOff:简单的开关控制,默认使用7号引脚控制13号接口的LED。
LongPress:演示如何检测长按键和短按键。
UpDown:向上或向下计数,一次一个数字,或按下按钮快速计数。
Toggle:演示ToggleButton功能。
SimpleOnOff控灯
打开示例SimpleOnOf的主文件,先阅读代码,可以看出,代码实现了按钮的释放判断,将代码复制到我们自己项目的主文件中。有人会有疑问,示例代码里并没有#include <Arduino.h>,是因为在JC_Button库中已经引入。可以通过代码跟踪引入库查看。
#include <JC_Button.h> // https://github.com/JChristensen/JC_Button
// pin assignments
const byte
BUTTON_PIN(7), // connect a button switch from this pin to ground
LED_PIN(13); // the standard Arduino "pin 13" LED
Button myBtn(BUTTON_PIN); // define the button
void setup()
{
myBtn.begin(); // initialize the button object
pinMode(LED_PIN, OUTPUT); // set the LED pin as an output
}
void loop()
{
static bool ledState; // a variable that keeps the current LED status
myBtn.read(); // read the button
if (myBtn.wasReleased()) // if the button was released, change the LED state
{
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
}
ButtonmyBtn(BUTTON_PIN)构造了按钮对象,其有多种重载方式。Button(pin, dbTime,puEnable, invert);
参数解释:dbTime:以毫秒为单位的去抖动时间。如果未给定,则默认为25ms。(unsigned long)puEnable:true启用微控制器的内部pull-up电阻,否则为false。如果未给定,则默认为true。(bool)反转:false将高逻辑电平解释为按下按钮,true将低电平解释为按下。当使用pull-up电阻器时,应使用true,对于pull-down电阻器,应使用false。如果未给定,则默认为true。(布尔)。
修改代码中的引脚声明,注意:代码中的编号为GPIO编号,不是开发板丝印编号。改用我们之前实验板子中的D4(2)开关和D6(12) LED,然后直接烧录代码吧,并观察结果,因其内部进行了抖动处理,所以按钮响应非常干脆,如果你手抖的厉害,可以尝试构造按钮对象时传递更长的抖动时间,如50毫秒。

LongPress长短按钮控灯
MCU引脚有限,可以通过不同的按键方式和组合实现多种控制,参看LongPress示例代码,可以实现短按开关灯,长按灯光闪烁,复制到我们的主文件中,修改引脚数字。注意:VsCode环境中需要在调用方法前定义或声明。本例中需要在loop方法前增加两个功能方法的声明。
void switchLED();
void fastBlink();
代码如下,并作中文注释:
#include <JC_Button.h>
// pin assignments
const byte
BUTTON_PIN(2), // 开关引脚 pin to ground
LED_PIN(12); // LED引脚
Button myBtn(BUTTON_PIN); // 按钮对象
const unsigned long
LONG_PRESS(1000), // 长按识别为1000毫秒
BLINK_INTERVAL(100); // LED闪烁间隔100毫秒
void setup()
{
myBtn.begin(); // 初始化按钮对象
pinMode(LED_PIN, OUTPUT); // 设置LED引脚输出模式
}
//状态机的可能状态列表
//状态转变 ONOFF --> TO_BLINK --> BLINK --> TO_ONOFF --> ONOFF
//用户能感知到的模式为只有开关和闪烁,另外两个为过渡模式.
enum states_t {ONOFF, TO_BLINK, BLINK, TO_ONOFF};
bool ledState; // 当前led状态
unsigned long ms; // 当前时间戳通过 millis()获取
unsigned long msLast; // 最后一次LED状态切换时间
void switchLED(); //声明方法,定义在后面
void fastBlink();//声明方法,定义在后面
void loop()
{
static states_t STATE; // 状态机当前状态
ms = millis(); // 记录当前时间
myBtn.read(); // 读取按钮值
switch (STATE)
{
//此状态监视短按和长按,切换LED状态
//短按,长按时移动到TO_BLINK状态。
case ONOFF:
if (myBtn.wasReleased())
switchLED();
else if (myBtn.pressedFor(LONG_PRESS))
STATE = TO_BLINK;
break;
// 过渡状态,我们开始快速闪烁作为对用户的反馈。
// 但是还需要等待用户释放按钮,即结束
// 长按,然后切换到闪烁状态.
case TO_BLINK:
if (myBtn.wasReleased())
STATE = BLINK;
else
fastBlink();
break;
//快闪状态。等待另一个长按。
//关闭LED(作为对用户的反馈),并移动到TO_ONOFF状态
case BLINK:
if (myBtn.pressedFor(LONG_PRESS))
{
STATE = TO_ONOFF;
digitalWrite(LED_PIN, LOW);
ledState = false;
}
else
fastBlink();
break;
// 这是一个过渡状态,我们等待用户释放按钮
// 然后切换回ONOFF状态.
case TO_ONOFF:
if (myBtn.wasReleased())
STATE = ONOFF;
break;
}
}
//反转当前LED状态。如果是开着的,就关掉。如果它是关闭的,打开它。
void switchLED()
{
msLast = ms; //记录上一次切换时间
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
//每隔BLINK_INTERVAL毫秒切换一次LED开关
void fastBlink()
{
if (ms - msLast >= BLINK_INTERVAL)
switchLED();
}
烧录程序并观察输出结果:

扩展实验
我们尝试使用按钮库实现,短按开关,长按调光的操作。这在家居的光控制中很常见。新建项目Lamp_switch_v2.0。这次我们不再搜索扩展库安装了,因为在其他项目中已经被欸安装过,通过修改配置文件直接引入,保存后即可被加入当前项目。

1)业务需求如下:
短按开关时,切换LED的状态。
在开灯状态下,长按按钮,持续改变亮度,直到达到某个方向的临界值。
任意亮度停止时,该亮度作为该灯的默认亮度值。
调光时最暗不能为关闭状态。
2)根据需求,修改代码如下:
#include <Arduino.h>
#include <JC_Button.h>
const byte
BUTTON_PIN(2), // 开关引脚 pin to ground
LED_PIN(12); // LED引脚
Button myBtn(BUTTON_PIN); // 按钮对象
const unsigned long
LONG_PRESS(600);
bool ledState; // 当前led状态
unsigned long ms; // 当前时间戳通过 millis()获取
unsigned long msLast; // 最后一次LED状态切换时间
unsigned long LUMCurr; // 最后一次亮度改变的时间
unsigned long LUMLast=millis(); // 最后一次亮度改变的时间
int dutyCycle = 255; // 亮度值
//状态机的可能状态列表
enum states_t {ONOFF, CHANGE_LUM};
states_t STATE; // 存储状态机当前状态
bool directionLED = true; //改变亮度的方向,真变亮,false变暗
//反转当前LED状态。如果是开着的,就关掉。如果它是关闭的,打开它。
void switchLED()
{
msLast = ms; //记录上一次切换时间
ledState = !ledState;
if(ledState){
analogWrite(LED_PIN, dutyCycle);//因为有默认亮度,所以要用PWM亮度输出
}else{
analogWrite(LED_PIN, 0); //关闭时将PWM调为0
}
}
// 亮度变化
void changeLUM(){
LUMCurr = millis();
//间隔不到10毫秒则跳过
if(LUMCurr-LUMLast<=10){
return;
}
// 亮度在超过两边临界值时改为临界值,最暗仍有10单位的亮度
if(dutyCycle>=255){
dutyCycle=255;
}else if(dutyCycle<=10){
dutyCycle=10;
}
// 如果是开灯状态,改变亮度,关灯状态不做处理
if(ledState){
analogWrite(LED_PIN,directionLED?dutyCycle++:dutyCycle--);//根据亮度改变方向改变亮度增减
}
// LUMLast = LUMCurr;//登记改变亮度时间
}
void setup() {
Serial.begin(115200); // 设置串口通信波特率
myBtn.begin(); // 初始化按钮对象
pinMode(LED_PIN, OUTPUT); // 设置LED引脚输出模式
}
void loop() {
ms = millis(); // 记录当前时间
myBtn.read(); // 读取按钮值
switch (STATE)
{
//此状态监视短按和长按,切换LED状态
case ONOFF:
if (myBtn.wasReleased())
switchLED();
else if (myBtn.pressedFor(LONG_PRESS))
STATE = CHANGE_LUM;
break;
// 长按,然后切换到亮度改变状态,在亮度达到临界值并且按键状态发生改变时才改变调光方向.
case CHANGE_LUM:
if (myBtn.wasReleased()){
if(dutyCycle>=255){
directionLED=false;
dutyCycle=255;
}else if(dutyCycle<=10){
directionLED=true;
}
STATE = ONOFF;
}else{
changeLUM();
}
break;
}
}
3) 烧录代码并验证:
