esp8266时钟+天气+提醒(三)代码篇二

esp8266时钟+天气+提醒(三)代码篇二

五、界面切换

1. 导论

本文要探讨的是ESP8266的中断实现,以及软件标志任务调度​​​​​​​的设计思想,故事还要接着上一篇说起。

观察文章中的loop函数(参见上一篇):

void loop(){
    if (sta>=0 && sta<=250){
    clock_display(prevDisplay);

    }else if(sta == 251){
      TandW();
      
    }else{
      threeday();
      
    }

    ++sta;

    if(sta==253){
      sta = 0;
    }
}

这是一个很简单的技术策略,但这并不符合我的要求。我期望界面的切换是自主可控的,例如通过按钮来实现。

接下来我们要撰写代码实现这一功能,逻辑其实很简单,当按钮被按下,显示屏就从原来的时钟界面切换到天气界面。

这要依靠中断机制来实现。

2. 中断

关于中断,可以参看《微机原理与接口技术》,但由于中断非常重要,应用面非常广泛,所以很有必要介绍一下。

举个例子:

我本来在电脑前打字写博客,突然电话响了,我就要跑过去接电话。这就是中断,电话响了是触发中断的事件,接电话是中断函数,即中断执行的操作

挂了电话之后,我再返回去继续打字。这就是中断触发的过程,中断触发时,原来的操作会立即被中止,而中断结束时得到继续。

此外,中断是可以嵌套的,就用上面的例子来说,当我接电话的时候,我听到灶台上的水壶响了(水烧开了),我就要放下电话去关火,然后回来接电话,挂断电话后再去打字。这就是中断嵌套

很显然的,设置中断的要素已经被我们发现了:触发条件中断函数

对于我们的项目,中断函数很显然地就是执行loop函数中的天气操作,这里沿用原作者的函数(实际上下面的函数不可行):

void IRAM_ATTR handleInterrupt() {
  TandW();
}

观察到在定义函数的时候,前面多了一个IRAM_ATTR,这个标志的作用是声明这个函数是中断函数,而不是普通的函数。

IRAM_ATTR用于指示将特定的函数或变量放置在内部RAM(IRAM)中,而不是默认的闪存(SPIFFS或Flash)中。

对于ESP8266或ESP32等微控制器,将中断函数放在IRAM中是很重要的,因为从IRAM执行代码比从闪存执行更快且更可靠。在中断发生时,系统需要能够快速响应,并立即执行中断函数;如果中断函数代码位于闪存中,由于读取速度的限制,可能会导致延迟。

正因为中断函数和普通函数的存储有所差异,所以这就带来了效果上的不同,如果为中断配置的函数是普通函数,没有IRAM_ATTR的标志,那么可能会导致卡顿甚至重启,因此必须为中断函数标明IRAM_ATTR

在过去,这个标志的名称是ICACHE_RAM_ATTR,但随着版本的更新,它已经被IRAM_ATTR取代了。

那么为什么说上面的函数不可行呢,这是因为许多网络库和TCP/IP堆栈并不设计为在中断上下文中安全使用

上面的原因对于萌新和小白来说可能有点困惑(《微机原理与接口技术》中的东西),这里就直接上结论了:在中断函数中使用网络操作可能会导致连接失败、数据损坏或程序重启

如何解决会在下面介绍。

中断函数的问题暂且不提,触发条件很显然,就是按钮被按下,但是我们的ESP8266怎么检测到按钮的状态呢?

3. 按钮

3.1 按钮开关

按钮其实是一种开关,它能够控制所在支路(或者说节点)的通断,当按钮按下时,通常可以说按钮两端是接通的,而当按钮弹起时,通常来说按钮就是断开的,所在支路也就成为了开路。

按钮一般分为两类:一种是按下不弹起的,如果需要使它弹起需要再按一下(闸刀开关、拨码开关等同一效果);另一种是按下自动弹起的,也就是说按下后接通,但只要松手就会立即断开。

这里我使用的是后者。

按钮开关

那么对于后者来说,我们可以发现按钮按下的过程为:按钮按下->极为短暂地停留了那么一小下(手还没离开按钮)->按钮弹起。看起来这个过程与下面很相似(只看一个就好,随便百度的图):

是的,按钮的按下与弹起会产生一个信号的变化,对于这个支路来说,按下时是接通,弹起时是断开,分别就对应这上面这个脉冲序列的高值和低值,所以按钮的按下与弹起会使支路高变低(按下)再变高(弹起),就产生了一个下降沿和一个上升沿。它们都可以作为触发条件。

当然,也不一定是高变低再变高,也可能是低变高再变低,这个要视具体电路而定。

按钮的状态影响了电路,进而产生下降沿和上升沿,ESP8266正是通过它们感知到的按钮的变化。

3.2 中断绑定

刚才说到ESP8266通过上升沿和下降沿感知按钮变化,这就是中断的触发条件:

  attachInterrupt(digitalPinToInterrupt(D4), handleInterrupt, FALLING);

这个函数叫做attachInterrupt,他的作用是为某一引脚的电平变化绑定相应的中断函数,它的三个参数分别代表了引脚号(实际上是中断号)、中断函数、引脚状态。这个函数应当写在setup函数里并且位置应尽量靠前。

digitalPinToInterrupt函数的作用是将引脚号转换为中断号,这是因为并非所有的引脚都能用作外部中断,而且可用作外部中断的引脚在内部可能被映射到不同的中断号,这个函数封装了底层细节,我们直接使用就好。

第二个参数handleInterrupt是我们上面定义的中断函数,不再多解释了。

第三个是引脚状态,它的值包括:

  • LOW:低电平触发
  • HIGH:高电平触发
  • RISING:上升沿触发
  • FALLING:下降沿触发
  • CHANGE:电平翻转时触发(上升沿下降沿都可触发)

在这里我们使用D4引脚关联按钮,阅读第一章第2节的引脚说明表,可以看到D4的说明为:

I/O,上拉

I/O的意思是具备输入输出功能,而上拉的意思是上拉输入,即在没有输入的时候稳定在高电平

也就是说D4默认都是高电平的,再加上按钮要连在D4上,可知按钮的另一端应当接地:因为另一端如果仍连VCC(高电平)的话,按钮的通断就不会对D4的状态有影响:断开的时候,上拉输入默认高电平;接通的时候,VCC输入,还是高电平。

如果另一端接地,当按钮接通的时候就会是低电平了。

所以按钮两端一端接D4,一端接地

我们希望按钮按下时立即执行中断函数(天气操作),那么中断触发条件就可以确定了:下降沿触发,这是个由高变低的过程。

如果设为CHANGE的话,按下和松手时都会触发,所以我们不采用CHANGE。

所以刚才说高低变化不是一定的,需要根据具体电路而定,如果使用的引脚不是上拉输入而是下拉,情况就会截然不同。一定要阅读引脚说明表。

3.3 引脚配置

特别地,一般建议配置引脚模式,例如:

pinMode(2, INPUT)

这个函数将2号引脚设置为INPUT,即输入引脚。

第二个参数是引脚的模式,可选的值包括:

  • OUTPUT:输出

  • INPUT:输入

  • INPUT_PULLUP:上拉输入

输出和输入这两个模式可以顾名思义,而上拉输入的意思上面也解释过了。

而我们使用的引脚D4是默认上拉的,因此不显式配置也可以,但还是建议配置一下的,并且写在中断配置的前面为好

第一个参数用于指定引脚,值得注意的是,我们一直在说D4引脚,而非4号引脚,这是因为引脚号实际上反映了GPIO(通用输入输出)的编号,阅读引脚说明表,可知D4引脚的编号是2,所以我们在指定引脚的时候,D4和2指的是同一个引脚,而不是4号引脚。

所以,再次强调阅读引脚说明表,就可以避免这两个参数使用出错,值得注意的是,并非所有引脚都具备上拉输入的能力,即使你用了pinMode函数,因为这是由硬件决定的。所以,阅读引脚说明表(重要的事情说三遍)。。

4. 软件标志任务调度

4.1 中断的问题

综合以上,我们改进了代码,现在的loop函数长这样(不要忘记中断函数和中断绑定):

void loop(){
    clock_display(prevDisplay);
}

非常简单,循环着只干一件事情,就是时钟效果,而我们一按下按钮,中断函数就会触发,TandW函数就会执行,进而获取天气。

但是如果真正运行我们的程序,就会发现,开始时钟一切正常,然后按下按钮,发现串口监视器中输出连接失败:

client.connect(host, 80)

为false了。

因此,屏幕显示的温度是0,因为没有得到数据。

问题出在中断上,第2节中有结论:在中断函数中使用网络操作可能会导致连接失败、数据损坏或程序重启

解决方案通常是:使用软件标志实现任务调度。

4.2 软件标志任务调度

软件标志任务调度的意思是:在中断服务程序中设置软件标志,然后在主循环中检查这些标志以执行较长的任务。

举个例子:

int mode = 0;
void IRAM_ATTR handleInterrupt() {
  mode = 1;
}

首先定义了一个全局变量mode,然后在中断函数中把它变成1。

接下来的事情似乎顺理成章:

void loop(){
    if (mode == 0){
    clock_display(prevDisplay);
    }else if(mode == 1){
      mode = 0;
      TandW();
    }
}

循环检测mode的值,如果是0就正常时钟操作,如果是1就切换为天气操作,同时将mode再变回0。

这样一来就实现了我们想要的效果。

4.3 好处

前面说到中断嵌套,一个中断函数是可以打断另一个中断函数的,就像听到水烧开了我就要放下电话去关火一样。

通常,中断都是有优先级的,因为我们不希望在跑去关火的时候又听到手机响了,我就放弃关火去看手机。关火的优先级应该比看手机的优先级高,所以设计了中断优先级这一机制。

但是,ESP8266并不支持这一功能,而软件标志可以帮助我们实现。

这种方式可以帮助管理不同任务的执行优先级,尽管这种优先级是通过软件逻辑而不是硬件中断优先级来实现的。

此外,这种设计也遵循工程上常说的防傻、防呆原则,假如按钮被一直按下,中断函数只会执行一次,因为触发条件是下降沿,长时间的低电平并不会触发中断。此外,假如按钮被连续快速按动,中断函数的确会被反复触发,但实际效果只是mode=1这条赋值语句被反复执行,并不会导致TandW函数被反复执行,这是因为TandW函数中有延时语句,大家可以看一下原作者的代码,延时语句可以有效避免重复请求。

有些读者可能会有疑惑,感觉不用中断也能实现类似的效果,为什么一定要使用中断呢(错误示范):

void loop(){
    if (digitalRead(D4) == 0){ 
        TandW();
    }else if(digitalRead(D4) == 1){
        clock_display(prevDisplay);
    }

在这个loop函数中,每次先判断D4引脚的状态,如果低电平就是天气界面,为高电平就是时钟界面。

这会带来什么问题呢:

  1. 尽管计时函数clock_display执行飞快,但仍需要时间消耗(尽管非常短),而按钮高低电平跳变也是瞬时性的,有可能在clock_display执行的时候按钮按下,执行结束的时候已经弹起了,这就不会导致digitalRead(D4) == 0被判定为真。
  2. 我们的目的是实现时钟、天气、提醒等功能,通过引脚状态判断为程序的编写带来了局限性。

第一条也跟按钮的质量有关,假如按钮弹起比较缓慢,还是有效的。

但至于第二条,使用软件标志任务调度的方法还可以设定中断提前结束、此外,如果我们再设置按钮2,使得按钮2按下时跳到提醒界面(虽然这个我们目前还没实现),上面的方法就会有局限:

比如说TandW函数中的延时函数正在被执行,这时我们按下了按钮2,但由于程序仍然处在延时状态,因此不会有任何改变,不能做到页面的瞬时跳转。而中断可以解决(这一部分程序是我们后面会编写的)。

未完待续

接下来要实现的是提醒功能以及整体的外形设计,物联网在前面等着我们。

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值