事情是前几天群里有人说做个看门狗不难吧,5分钟的事情,然后我就怼了几句,后来才发现,原来真的没有看门狗模块鸭。
那好吧,那我就写一下好了,今天是(2020年4月30日)想着最后一天了,不如做点什么有价值的事情贡献一下代码好了。
做这个事情前吧,先思考一下模块的接口设计,可以参考一下 esp32 的设计,因为是 micropython 后来的代码,所以在设计上充分考虑了跨平台性。
那么我就以如下的代码为参考开始吧。
import time
from machine import WDT
# test default wdt
wdt0 = WDT(id=0, timeout=3000)
print('into', wdt0)
time.sleep(2)
print(time.ticks_ms())
# 1.test wdt feed
wdt0.feed()
time.sleep(2)
print(time.ticks_ms())
# 2.test wdt stop
wdt0.stop()
print('stop', wdt0)
# 3.wait wdt work
while True:
print('idle', time.ticks_ms())
time.sleep(1)
可以看到这是最朴素的看门狗设计,只有 new 、feed、stop 接口,这足够一般使用了。
接着我在堪智的 code 的接口中注意到有一个有趣的设计,也就是如下的 C code 。
#include "printf.h"
static int wdt0_irq(void *ctx) {
static int s_wdt_irq_cnt = 0;
printk("%s
", __func__);
s_wdt_irq_cnt ++;
if(s_wdt_irq_cnt < 2)
wdt_clear_interrupt((wdt_device_number_t)0);
else
while(1);
return 0;
}
static void unit_test() {
mp_printf(&mp_plat_print, "wdt start!
");
int timeout = 0;
plic_init();
sysctl_enable_irq();
mp_printf(&mp_plat_print, "wdt time is %d ms
", wdt_init((wdt_device_number_t)0, 4000, wdt0_irq,NULL));
while(1) {
vTaskDelay(1000);
if(timeout++ < 3) {
wdt_feed((wdt_device_number_t)0);
} else {
printf("wdt_stop
");
wdt_stop((wdt_device_number_t)0);
sysctl_clock_disable(0 ? SYSCTL_CLOCK_WDT1 : SYSCTL_CLOCK_WDT0); // patch for fix stop
while(1)
{
printf("wdt_idle
");
sleep(1);
}
}
}
}
这个已经是我测试过的底层 code 了,顺便一提的是 SDK 的 stop 没有关掉 wdt 的时钟(sysctl_clock_disable),所以 stop 接口是不工作的,关于这个已经提交 issue 了,之后就会修复上。(2020年5月1日)
我们继续看到这个设计,它允许导入一个 callback 和 context 参数(上下文用途),这个我看到的时候就思考了一下,这个看门狗的使用场景可能有如下几种。
- 满足一般的看门狗的基本功能,不喂狗就复位。
- 允许接入回调函数,在即将复位之前,进入中断函数,此时由用户决定如下三种状态。
- 没有可以处理该异常(未能成功喂狗)的方法,最终只能 pass ,这将导致硬件复位。
- 存在解决方案,并成功解决了问题,则取消这次的复位(继续喂狗)。
- 发现不需要看门狗了,可能手动触发复位或者是其他考虑,则关闭看门狗模块(stop)。
综上所述,我们要能够在代码中体现的要素,配置看门狗的启动(new)、喂狗(feed)、回调(callback+context)、停止(stop)即可。
则拓展处第二种代码设计为
def on_wdt(self):
print(self.context(), self)
#self.feed()
## release WDT
#self.stop()
# test callback wdt
wdt1 = WDT(id=1, timeout=4000, callback=on_wdt, context={})
print('into', wdt1)
time.sleep(2)
print(time.ticks_ms())
# 1.test wdt feed
wdt1.feed()
time.sleep(2)
print(time.ticks_ms())
# 2.test wdt stop
wdt1.stop()
print('stop', wdt1)
# 3.wait wdt work
while True:
print('idle', time.ticks_ms())
time.sleep(1)
当然,这都是在我写完,和测试完之后做的总结了,所以过程是省略了不少,但会挑一下重点来讲。
第一个是,参考以往的代码框架,如 esp32/machine_wdt.c 用来构建 wdt 的基础接口框架,如第一份代码的设计,接着因为回调的原因,这个设计类似于 timer 定时器,所以定时器的 code 也要拿来参考,如 esp32/machine_timer.c ,基本上就可以做出来拉,这并不难。
不难归不难,但也不会是 5 分钟就可以写完的代码,大概也要差不多一天吧(标准8小时工作时间),如果一切顺利的话。
注意写的流程,建议满足如下流程。
-
确认原始 SDK 的功能正常,符合基本的单元测试,可以确定模块的创建、启动、配置、停止、释放等要素。
-
确认 micropython 的接口函数定义,可以先假定接口但不实现,主要是分离开发。
-
基于前者的单元测试,丢入 Micropython 环境中执行,如配套的单元测试,确保可以在不破坏其他变量的条件下实现。
-
此时确保 C 方面的基础接口符合基本的使用,准备接入 Python 代码进行单元测试,直到功能实现没有明显的死角。
走完上述流程后,基本上第一份看门狗 CODE 就可以实现了,很简单,就 new 和 feed ,测试的 code 都不需要很复杂,只要确保不喂狗的时候复位了就好了。
接下来补一点细节和思考,如何添加回调,理解 C 与 MicroPython 函数之间的回调机制关系,主要就是 C 如何调用 Python 中设计的 函数 和传递参数。
这个部分看 timer.c 基本就可以搞定了,不了解的就看提交 MaixPy/commit/c4568e4f174de1c9eaf083506c2019ffbe8c7bf5 ,一是绑定一个本地函数,二是通过 C 接口调用函数传递 mp_obj_t 的 Python 对象。
STATIC void machine_wdt_isr(void *self_in) {
machine_wdt_obj_t *self = self_in;
if (self->callback != mp_const_none) {
// printk("wdt id is %d
", self->id);
if (self->is_interrupt == false) {
wdt_clear_interrupt(self->id);
self->is_interrupt = true;
mp_sched_schedule(self->callback, self);
}
mp_hal_wake_main_task_from_isr();
}
}
wdt_init(self->id, self->timeout, (plic_irq_callback_t)machine_wdt_isr, (void *)self);
在这里 machine_wdt_isr 是用来接收 WDT 的 C 中断,它将会在 WDT 即将复位之前反复重入(执行),为了在进入中断后不重入则需要清理中断的信号 wdt_clear_interrupt(self->id); 。
接着 mp_sched_schedule(self->callback, self); 用来执行 Python 对应的函数对象,参数指为自身,我设计中的上下文通过 self.context() 来处理,这样的好处就是可以在回调函数中决定如何管理当下的看门狗模块状态。
注意 mp_hal_wake_main_task_from_isr(); 是用来继续执行 MicroPython 环境的代码,可以这样理解,中断将会吃掉所有函数的执行,也包括 MicroPython 的主进程,也就是 repl 接口的(main)代码,也就是说,若是不在这里继续执行 MicroPython 环境,整个芯片将会停止工作,全部都陷入了一个空转的中断函数中。
而至于我为什么加上了 is_interrupt 这是因为考虑到第二种设计所添加的标记量,它主要解决以下场景的问题。
is_interrupt 会在 machine_wdt_feed 中设置为 self->is_interrupt = false; 表示这个状态要撤销,结合上述的 machine_wdt_isr 逻辑来看。
我们回顾回调处理的场景之一,没有可以处理该异常(未能成功喂狗)的方法,最终只能 pass ,这将导致硬件复位。
第一次触发进入 is_interrupt 为防止重入则打上标记量 self->is_interrupt = true; 此时在清理信号后,将正常执行 Python 中的回调函数,若回调函数什么也不做,就会进一步触发复位,从这之后若是再次进入中断后,将会反复执行 mp_hal_wake_main_task_from_isr ,直到硬件复位。
此时若是在回调中 feed 喂狗重置了 is_interrupt 标记则允许重入 Python 的回调函数,从而防止进一步的看门狗复位。
此时若是在回调中 stop 了,则整个模块都释放了。
此时若是什么也不做,放一会就复位了。
以上,就这些,想知道更多就去看提交的代码吧,会有不一定的理解的(也许?)。
最后补一下单元测试 Code 。
import time
from machine import WDT
# '''
# test default wdt
wdt0 = WDT(id=0, timeout=3000)
print('into', wdt0)
time.sleep(2)
print(time.ticks_ms())
# 1.test wdt feed
wdt0.feed()
time.sleep(2)
print(time.ticks_ms())
# 2.test wdt stop
wdt0.stop()
print('stop', wdt0)
# 3.wait wdt work
while True:
print('idle', time.ticks_ms())
time.sleep(1)
# '''
# '''
def on_wdt(self):
print(self.context(), self)
#self.feed()
## release WDT
#self.stop()
# test callback wdt
wdt1 = WDT(id=1, timeout=4000, callback=on_wdt, context={})
print('into', wdt1)
time.sleep(2)
print(time.ticks_ms())
# 1.test wdt feed
wdt1.feed()
time.sleep(2)
print(time.ticks_ms())
# 2.test wdt stop
wdt1.stop()
print('stop', wdt1)
# 3.wait wdt work
while True:
print('idle', time.ticks_ms())
time.sleep(1)
# '''
#'''
## test default and callback wdt
def on_wdt(self):
print(self.context(), self)
#self.feed()
## release WDT
#self.stop()
wdt0 = WDT(id=0, timeout=3000, callback=on_wdt, context=[])
wdt1 = WDT(id=1, timeout=4000, callback=on_wdt, context={})
## 3.wait wdt work
while True:
#wdt0.feed()
print('idle', time.ticks_ms())
time.sleep(1)
#'''
其实只是给我自己备份 Code 而已,2020年5月1日留。