突然地心血來潮,為 MaixPy( k210 micropython ) 新增看門狗(WDT) C 模組的開發過程記錄,給後來的人做開發參考。

Juwan發表於2020-05-01

事情是前幾天群裡有人說做個看門狗不難吧,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\n", __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!\n");
    int timeout = 0;
    plic_init();
    sysctl_enable_irq();
    mp_printf(&mp_plat_print, "wdt time is %d ms\n", 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\n");
            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\n");
                sleep(1);
            }
        }
    }
}

這個已經是我測試過的底層 code 了,順便一提的是 SDK 的 stop 沒有關掉 wdt 的時鐘(sysctl_clock_disable),所以 stop 介面是不工作的,關於這個已經提交 issue 了,之後就會修復上。(2020年5月1日)

我們繼續看到這個設計,它允許匯入一個 callback 和 context 引數(上下文用途),這個我看到的時候就思考了一下,這個看門狗的使用場景可能有如下幾種。

  1. 滿足一般的看門狗的基本功能,不喂狗就復位。
  2. 允許接入回撥函式,在即將復位之前,進入中斷函式,此時由使用者決定如下三種狀態。
    • 沒有可以處理該異常(未能成功喂狗)的方法,最終只能 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\n", 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日留。

相關文章