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引脚的状态,如果低电平就是天气界面,为高电平就是时钟界面。
这会带来什么问题呢:
- 尽管计时函数clock_display执行飞快,但仍需要时间消耗(尽管非常短),而按钮高低电平跳变也是瞬时性的,有可能在clock_display执行的时候按钮按下,执行结束的时候已经弹起了,这就不会导致digitalRead(D4) == 0被判定为真。
- 我们的目的是实现时钟、天气、提醒等功能,通过引脚状态判断为程序的编写带来了局限性。
第一条也跟按钮的质量有关,假如按钮弹起比较缓慢,还是有效的。
但至于第二条,使用软件标志任务调度的方法还可以设定中断提前结束、此外,如果我们再设置按钮2,使得按钮2按下时跳到提醒界面(虽然这个我们目前还没实现),上面的方法就会有局限:
比如说TandW函数中的延时函数正在被执行,这时我们按下了按钮2,但由于程序仍然处在延时状态,因此不会有任何改变,不能做到页面的瞬时跳转。而中断可以解决(这一部分程序是我们后面会编写的)。
未完待续
接下来要实现的是提醒功能以及整体的外形设计,物联网在前面等着我们。