月份: 2020 年 10 月

ESP8266 中斷與計時器

No Comments

在單晶片的功能當中,中斷(interrupt)與計時器(timer)是很重要的硬體設施,對於新手而言逐漸地會發覺某些場合下會需要用上它們。因此本文就來介紹它們的用法。
請注意本文所介紹的是適用在 Arduino IDE 環境下使用 ESP8266 開發套件請參見前面文章;若是使用官方 SDK,不見得是如下用法(筆者未查證)。

中斷

  • attachInterrupt(which_pin, ISR_function, mode);
  • 說明:
  • 呼叫(invoke/call)後,便可配置出一個中斷。呼叫時,相關的暫存器便會被設置,該 pin 因而規劃成為中斷 pin。至於 ISR function 的位址會被置於所規範的 SRAM 當中的中斷常式位址區塊。當中斷發生會正確地從區塊中取出對應 ISR 位址並呼叫之。
  • which_pin:除了 GPIO16,其餘 GPIOs 都可當中斷腳位。而 which_pin 還須先以此呼叫才行 digitalPinToInterrupt(which_pin);
  • ISR_function:當中斷發生,此函式便會被呼叫。此函式宣告必須帶有 ICACHE_RAM_ATTR 屬性。
  • mode:有三種規劃模式,CHANGE/RISING/FALLING,各代表中斷腳位電壓準位發生改變(LOW 變 HIGH,HIGH 變 LOW),LOW 變 HIGH,HIGH 變 LOW。當指定的模式事件發生便代表該中斷發生。後補充,是五種,再加上 HIGH,LOW。
    https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
    http://www.gammon.com.au/interrupts
  • 例如:attachInterrupt(digitalPinToInterrupt(GPIO), ISR, mode);
  • 將該 GPIO 的中斷除能:detachInterrupt(which_pin); 要再致能就再呼叫一次中斷設置。

計時器

計時器泛指有幾種類型:計數器,延遲,計時,並搭配中斷與驅動其他器件。本文只介紹延遲與計時

  • delay(mili_seconds); CPU 會停下來等待所指定的時間 mili_seconds 經過,參數是正整數或零。
  • delayMicroseconds(us); 同理,microseconds。
  • millis(); 會回傳當前的 CPU 時間,單位 mili-second。CPU 時間是時間,從程式一開始執行後便一直遞增。因此若我們非連續地呼叫兩次 millis(),所得到的時間相減(後減前)便代表著這兩次呼叫當中所經過的時間。
  • micros(); 同理,microseconds。
  • 以上,與時間相關的控制只要有這幾個函式就足足夠用了。

定時中斷之 Ticker

  • 多次定時觸發
  • 1. 引入含括檔 #include<Ticker.h> 在 Linux 下注意大小寫
  • #include<Ticker.h>
  • 2. 物件宣告,須注意它的生命週期,故通常使用全域
  • Ticker myTicker;
  • 3. 建立 callback function,例如,
  • void tickerHandler();
  • 而 callback function 可為不接參數或只接一個參數。
  • 4. 使用 attach() 或 attach_ms() 成員函式來設定 myTicker,分別代表以“秒”或“亳秒”為計時單位,並且時間可為浮點數,例如,3 秒後呼叫 callback function,
  • myTicker.attach(3, tickerHandler);
  • 並且欲更改計時時間只要重新呼叫即可,例如,
  • myTicker.attach_ms(3, tickerHandler); 將從 3 秒改為 3ms
  • 當 callback function 是接一個參數時,例如,void tickerHandleAnother(int arg); 則如下呼叫,參數放在第三個
  • myTicker.attach_ms(3, tickerHandleAnother, 100);
  • 5. 欲停止該物件計時
  • myTicker.detach();
  • 還有其他可使用的公開函式成員可直接查閱 Ticker.h,並實作測試。例如若要更精準的 us 時間,我們找到如下函式:
  • attach_ms_scheduled_accurate(time, func); 使用純小數並勾示波器查看。很不幸地,這個函式並非如我預期能掌控 us 時間。並且筆者還發現,attach_ms() 使用純小數進入到 us 等級,或帶小數,例如 1.8ms,同樣都是不能用的(1.8ms 變為 1ms),即,只有 1ms 以上才能正確觸發。故建議,attach 及 attach_ms 仍使用正整數為佳。
  • 最後注意,ESP8266 有 timer0,timer1 兩支硬體計時器,但難避免有其他函式庫使用上它們,若然我們使用上將造成衝突,故避開使用它們而轉用 Ticker 為上策。
// 範例:使用 D1-mini,
// 我們在 loop() 當中運用 millis() 讓 GPIO4(D2) 定時地 HIGH/LOW 變化,每 1 秒。
// 並將 GPIO4 透過杜邦線接至 GPIO5(D1)及 GPIO0(D3),而 GPIO5 規劃成 falling 中斷。GPIO0 規劃成 rising 中斷。
// GPIO5 中斷常式是點燈,GPIO0 則是關燈。燈是 GPIO2(D4)
// 如此,我們可透過斷開任一條杜邦線來看到中斷確實發生。

// 定義 GPIO5 ISR function
ICACHE_RAM_ATTR void ISR_GPIO5(){
    digitalWrite(LED_BUILTIN, HIGH);
}

// 定義 GPIO0 ISR function
ICACHE_RAM_ATTR void ISR_GPIO0(){
    digitalWrite(LED_BUILTIN, LOW);
}

// 呼叫這個函式時,這個函式會檢查當前時間,若距上次呼叫已經過1秒以上,
// 便會回傳 true,否則回傳 false. 因此我們使用靜態變數儲存(更新)本次檢查時的 CPU 時間,
// 用以比對下次呼叫的 CPU 時間。但須注意當歴時未超過 1 秒,我們並不需要更新。
bool Timeout_1s(){
    static int current_time = millis();
    int new_time = millis();
    if (new_time < (current_time + 1000)) return false;
    current_time = new_time;
    return true;
}

void setup() {
  // put your setup code here, to run once:

  // 宣告成中斷 pin
  attachInterrupt(digitalPinToInterrupt(5), ISR_GPIO5, FALLING);
  attachInterrupt(digitalPinToInterrupt(0), ISR_GPIO0, RISING);

  // 將 GPIO2(D4) 規劃成驅動 LED
  pinMode(LED_BUILTIN, OUTPUT);

  // 將 GPIO4(D2) 規劃成輸出,用以打出 high 或 low 位準
  pinMode(4, OUTPUT);
}

void loop() {
  // put your main code here, to run repeatedly:

  // 這 delay 有四個涵意,假設所有程式行全被跑過一次耗時 1 micro second...
  // 咦。。。這時筆者要註解了:粗略地說,1 MHz 的 CPU,執行 1 條 CPU 指令耗時 1 micro second。
  // 對應到 C/C++ 我們取 1/2 或 1/3 都可大略地合理評估,即,80MHz 的 CPU 在 1 micro second 可跑 30 行
  // C++ 程式行。因此可斷定本程式耗用不到 1us。
  // 涵意一,故,CPU 總是在睡覺。
  // 涵意二,因此,閃燈的時間誤差,最多只有 10ms。
  // 涵意三,不能不加 delay,否則 CPU 總是眼睜睜地在看手錶很不禮貌。
  // 涵意四,那麼,delay 1ms 和 10ms,差別呢?即,看手錶的頻率差了 10 倍。然而從數量級來看,
  // CPU 的發熱狀況幾乎相同。
  delay(10);

  // 初始設定成 low(0), 使用靜態變數用以可 toggle high/low
  static int GPIO4_pin_state = 0;
  if (Timeout_1s()) digitalWrite(4, GPIO4_pin_state ^= 1);
}

最後,須注意到,D3 和 D4 是有上拉電阻的,故 LED 在沒有被 drive 的前提下就是會亮。如此在實驗中若會有存疑的現象便是此原因請自行細究。

這個範例實驗很簡單只要把 D1,D2,D3 接在一起就行了

進階補充-關於硬體計時器 hardware timer

ESP8266 的硬體計時器有兩個,timer0 and timer1。timer0 已被 Wifi 的軟硬體組件所佔用,故只剩下 timer1 可供運用。但在 ESP8266 官方與第三方社群裏都不建議用戶端使用,因一來官方會拿來實作進其他函式庫,同理第三方庫也會難以迴避地而使用它。因為它真的太重要了,太被需要了。然則,一方面考量是用戶端不去使用它才可能避免衝突,但另一方面,事實上,衝突在所難免,發不發生,單純存在性與機率問題;要避免就是檢視過所有載入的,使用的函式庫都沒使用 timer1。既然如此,筆者偏向的想法是,那就用它吧,衝突,再除錯及設法循較簡單的問題源頭克服。簡單講,衝突是什麼?即,timer 已被交付任務正在計數當中,計數器重新地又被其他程式段改寫。又或 timer 背負的任務獨佔整個 CPU 資源致其他時效性任務發生延遲。這些都稱為計時器崩潰。在 non-os 環境下的硬體計時器的運用,唯一的法則就是計時器任務須佔用最少的 CPU 時間,以最大降低崩潰的機率,並且除非有自信否則避免使用 NMI 不可遮罩式中斷。反之在 os 的管理下則沒有這個問題。接下來筆者將已搜集到的相關資料列於下;因,大家都避免使用它故資訊便不充足。

硬體計時器 hw timer 1 的使用範例

static inline uint32_t MicrosecondsToCycles(uint32_t microseconds){
    return /*system_get_cpu_freq()*/80*microseconds;
}

static void initTimer(void(*Farg)()){ // Farg, callback function
    ets_intr_lock();
    timer1_disable();
    timer1_isr_init();
    timer1_attachInterrupt(Farg);
    timer1_write(MicrosecondsToCycles(1000));
    timer1_enable(TIM_DIV1, TIM_EDGE, TIM_LOOP);
    ets_intr_unlock();
}

static void deinitTimer(){
    timer1_attachInterrupt(NULL);
    timer1_disable();
    timer1_isr_init();
}

硬體計時器 hw timer 1

  • 名稱 FRC1,時脈來源是 CPU 時脈(Apb_clk),80 MHz
  • 有除頻器(降頻),分別是 1,16,256
  • 故分別對應除頻,每個 count 是 12.5ns,0.2us,3.2us
  • 計時暫存器(COUNT_VALUE)長度是 23 bits,即,8388608 counts
  • 裝填暫存器是 FRC1_LOAD_VALUE
  • 下數計時,載入裝填後即開始倒數計時/在致能下,數到 0 便觸發中斷
  • 故分別對應除頻最大計時是 105ms,1678ms,26844ms
  • 有自動裝填及非自動裝填兩種模式:
  • auto-feed-mode:當中斷觸發時,FRC1_LOAD_VALUE 又會再自動載入至 COUNT_VALUE 並繼續下數
  • non-auto-feed-mode:同理不續載入 FRC1_LOAD_VALUE,而以 8388607(0x 7F FFFF)載入至 COUNT_VALUE 並繼續下數,並且也會達零觸發中斷。
  • 中斷觸發有兩種選擇,可遮罩(FRC,level 1)及不可遮罩(NMI, level 3)
  • 若使用 160 MHz,當然時間精度會加倍,最大時間長度會減半

SDK implements HW_TIMER1

  • 使用除頻 16,故每個 count 是 0.2us
  • hw_timer_arm() 參數使用 us,故可用的最大時間是 1677000us
  • 以上談到的時間大小有點誤差,只因 0 或最大數 8388607,這種邊界遞移與裝填所耗時的問題,請自行考量。

SDK hw_timer 的用法

  • hw_timer_init(FRC1_SOURCE, 1); // 設置計時源 FRC1 及自動重裝填。計時源有兩個,NMI 中斷及 FRC1 中斷。
    如果使用 NMI 中斷源,且為自動裝填,則調用 hw_timer_arm 時參數 val 必須最小 100
    如果使用 NMI 中斷源,那麼該定時器將為最高優先級,可打斷其他 ISR
    如果使用 FRC1 中斷源,那麼該定時器就無法打斷其他 ISR
    hw_timer.c 的接口不能跟 PWM 驅動接口函式同時使用,因為二者共用了同一個硬體定時器。
  • hw_timer_set_func(hw_timer_callback);
    定義 callback function 並配置上去。
  • hw_timer_arm(1000000); // 函式使用的時間單位是 1us。此參數例是 1 秒。
    裝填計時長度。一旦呼叫便開始倒數計時。
    自動裝填模式:
    使用 FRC1 中斷源(FRC1_SOURCE),(暫存器內?)取值範圍:50~0x7F FFFF;
    使用 NMI 中斷源(NMI_SOURCE),(暫存器內?)取值範圍:100~0x7F FFFF;
    非自動裝填模式:
    (暫存器內?)取值範圍:10~0x7F FFFF;
  • callback function
    IRAM_ATTR void hw_tmer_callback(){
    os_printf(” 1 秒時間到\r\n”);
    }
    註:ICACHE_FLASH_ATTR 禁用
  • 致能 TM1_EDGE_INT_ENABLE();
  • 除能 TM1_EDGE_INT_DISABLE();

備註

  • 或許有讀者已經意識到,除頻 1,或者使用不同的中斷源而有不同的取值下界,就是因為 CPU 指令最小執行時間或中斷耗用時間酬載(payload)就已與計時器能力不相上下。因此 SDK 使用除頻 16 應是很恰當的。使用者尚須注意到/確保若使用了接近下界的計數,ISR 內能夠執行的時間相當有限絕不能溢時。
  • 其次我們將實驗非自動載入模式,是否中斷會持續發生。而此前也要先測試自動載入模式,及萬一溢時,二種模式的結果如何。

從 nonos-sdk hw_timer.c 攫取出來的程式碼

/*
 * ESPRESSIF MIT License
 *
 * Copyright (c) 2016 <ESPRESSIF SYSTEMS (SHANGHAI) PTE LTD>
 *
 * Permission is hereby granted for use on ESPRESSIF SYSTEMS ESP8266 only, in which case,
 * it is free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished
 * to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */

typedef enum {
    DIVDED_BY_1 = 0,        //timer clock
    DIVDED_BY_16 = 4,    //divided by 16
    DIVDED_BY_256 = 8,    //divided by 256
} time_predived_mode;

typedef enum {            //timer interrupt mode
    TM_LEVEL_INT = 1,    // level interrupt
    TM_EDGE_INT   = 0,    //edge interrupt
} time_int_mode;

typedef enum {
    FRC1_SOURCE = 0,
    NMI_SOURCE = 1,
} frc1_timer_source_type;

// not special, seems to prevent from calculation overflowing
// the t uses 1us
#define US_TO_RTC_TIMER_TICKS(t)          \
    ((t) ?                                   \
     (((t) > 0x35A) ?                   \
      (((t)>>2) * ((APB_CLK_FREQ>>4)/250000) + ((t)&0x3) * ((APB_CLK_FREQ>>4)/1000000))  :    \
      (((t) *(APB_CLK_FREQ>>4)) / 1000000)) :    \
     0)

#define FRC1_ENABLE_TIMER  BIT7
#define FRC1_AUTO_LOAD     BIT6


/******************************************************************************
* FunctionName : hw_timer_arm
* Description  : set a trigger timer delay for this timer.
* Parameters   : uint32_t val :
in autoload mode
                        50 ~ 0x7fffff;  for FRC1 source.
                        100 ~ 0x7fffff;  for NMI source.
in non autoload mode:
                        10 ~ 0x7fffff;
* Returns      : NONE
*******************************************************************************/
void  hw_timer_arm(uint32_t val)
{
    RTC_REG_WRITE(FRC1_LOAD_ADDRESS, US_TO_RTC_TIMER_TICKS(val));
}

static void (* user_hw_timer_cb)(void) = NULL;
/******************************************************************************
* FunctionName : hw_timer_set_func
* Description  : set the func, when trigger timer is up.
* Parameters   : void (* user_hw_timer_cb_set)(void):
                        timer callback function,
* Returns      : NONE
*******************************************************************************/
void  hw_timer_set_func(void (* user_hw_timer_cb_set)(void))
{
    user_hw_timer_cb = user_hw_timer_cb_set;
}

static void hw_timer_isr_cb(void *arg)
{
    if (user_hw_timer_cb != NULL) {
        (*(user_hw_timer_cb))();
    }
}

static void hw_timer_nmi_cb(void)
{
    if (user_hw_timer_cb != NULL) {
        (*(user_hw_timer_cb))();
    }
}

/******************************************************************************
* FunctionName : hw_timer_init
* Description  : initilize the hardware isr timer
* Parameters   :
frc1_timer_source_type source_type:
                        FRC1_SOURCE,    timer use frc1 isr as isr source.
                        NMI_SOURCE,     timer use nmi isr as isr source.
uint8_t req:
                        0,  not autoload,
                        1,  autoload mode,
* Returns      : NONE
*******************************************************************************/
void ICACHE_FLASH_ATTR hw_timer_init(frc1_timer_source_type source_type, uint8_t req)
{
    if (req == 1) {
        RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
                      FRC1_AUTO_LOAD | DIVDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);
    } else {
        RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
                      DIVDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);
    }

    if (source_type == NMI_SOURCE) {
        ETS_FRC_TIMER1_NMI_INTR_ATTACH(hw_timer_nmi_cb);
    } else {
        ETS_FRC_TIMER1_INTR_ATTACH(hw_timer_isr_cb, NULL);
    }

    TM1_EDGE_INT_ENABLE();
    ETS_FRC1_INTR_ENABLE();
}

//-------------------------------Test Code Below--------------------------------------
#if 0
void   hw_test_timer_cb(void)
{
    static uint16_t j = 0;
    j++;

    if ((WDEV_NOW() - tick_now2) >= 1000000) {
        static u32 idx = 1;
        tick_now2 = WDEV_NOW();
        os_printf("b%u:%d\n", idx++, j);
        j = 0;
    }

    //hw_timer_arm(50);
}

void ICACHE_FLASH_ATTR user_init(void)
{
    hw_timer_init(FRC1_SOURCE, 1);
    hw_timer_set_func(hw_test_timer_cb);
    hw_timer_arm(100);
}
#endif
/*
NOTE:
1 if use nmi source, for autoload timer , the timer setting val can't be less than 100.
2 if use nmi source, this timer has highest priority, can interrupt other isr.
3 if use frc1 source, this timer can't interrupt other isr.

    //hw_timer_init(FRC1_SOURCE, 1);
    //hw_timer_set_func(hw_timer_callback);
    //TM1_EDGE_INT_DISABLE();
    //TM1_EDGE_INT_ENABLE();
    //hw_timer_arm(50); // 函式使用的時基是 1us。此參數例是 50us
    ////ETS_FRC1_INTR_ENABLE();
    ////ETS_FRC1_INTR_DISABLE();
*/

從程式碼看出有很多可以嘗試的實驗,實驗的結果導引我們如何正確有效地善用這個計時器。此外,程式很完整意謂著別的函式庫很可能也會直接取用源碼。故要防範衝突只剩整個系統上的實測了。又或是從目的碼搜尋有存取到 timer1 暫存器;沒搜到代表才安全。
以下將加入主程式逐步測試以作結論。

實驗-FRC1 自動載入模式


int pivot=0, time_cnt=0, loader=-1, counter=-1;

IRAM_ATTR void hw_timer_callback(){
    if (++pivot>=(20*1000)){ // 1 second
        time_cnt++;
        pivot=0;
    }
    
    loader=RTC_REG_READ(FRC1_LOAD_ADDRESS);
    counter=RTC_REG_READ(FRC1_COUNT_ADDRESS);
}

void setup() {
    // put your setup code here, to run once:

    Serial.begin(115200);
    
    hw_timer_init(FRC1_SOURCE, 1);
    hw_timer_set_func(hw_timer_callback);
    hw_timer_arm(50); // 函式使用的時間單位是 1us。此參數例是 每 50us 呼叫一次 ISR
}

void loop() {
    // put your main code here, to run repeatedly:
    delay(5000);

    Serial.printf("\r\n(%d, %d, %d, %d)", pivot, time_cnt, loader, counter);
}

/* 輸出結果
09:08:59.655 -> (1, 5, 250, 233)
09:09:04.656 -> (12, 10, 250, 239)
09:09:09.657 -> (17, 15, 250, 240)
09:09:14.658 -> (25, 20, 250, 236)
09:09:19.658 -> (32, 25, 250, 240)
09:09:24.658 -> (49, 30, 250, 240)
09:09:29.659 -> (57, 35, 250, 237)
09:09:34.659 -> (64, 40, 250, 240)
09:09:39.659 -> (69, 45, 250, 240)
09:09:44.659 -> (74, 50, 250, 240)
09:09:49.659 -> (80, 55, 250, 240)
09:09:54.659 -> (85, 60, 250, 240)
09:09:59.659 -> (90, 65, 250, 235)
09:10:04.660 -> (95, 70, 250, 238)
09:10:09.660 -> (100, 75, 250, 235)
09:10:14.660 -> (108, 80, 250, 235)

** 結論
1. 根據 pivot, time_cnt 都在變動,表示中斷持續發生
2. 裝載器 loader 不變如所預期
3. counter:時基 0.2us x 240 = 48us,表示即便 ISR 內部不做什麼事(少於 1us),time-up 及中斷的 payload 就接近 50us,
這也是為什麼 hw_timer_arm(50) 是規定最小值的原因。而這也意謂著(最小值下)ISR 能做的事也只有 2us。

4. 嘿嘿嘿,筆者都忘了,計數器是下數的。因此第三點的描述是錯的,但觀念沒錯,這也就多給了我們另一個該做的實驗。
正確描述是:0.2us*(250-230)=4us 是計時器中斷的耗時酬載。再不然就是發生溢時了,也就是酬載將是 50us*n+4us。恩不為零就太扯了。
我們將取 60us 作為我們下一個實驗。
若前真所謂溢時,則將看到 counter 的數值接近零。否則,酬載將也是 4us 左右。
*/

實驗-FRC1 自動載入模式與中斷酬載

只要將這行改為 60 即可。
hw_timer_arm(60); // 函式使用的時間單位是 1us。此參數例是 每 60us 呼叫一次 ISR

/* 輸出結果
10:18:08.446 -> (3334, 4, 300, 287)
10:18:13.446 -> (6677, 8, 300, 285)
10:18:18.446 -> (10014, 12, 300, 290)
10:18:23.445 -> (13352, 16, 300, 290)
10:18:28.445 -> (16690, 20, 300, 290)
10:18:33.444 -> (28, 25, 300, 290)
10:18:38.444 -> (3366, 29, 300, 281)
10:18:43.443 -> (6704, 33, 300, 287)
10:18:48.443 -> (10041, 37, 300, 290)
10:18:53.442 -> (13379, 41, 300, 290)
10:18:58.441 -> (16717, 45, 300, 290)
10:19:03.441 -> (55, 50, 300, 290)

** 結論
沒錯,也是 4us 左右。故所規範的最小值 50us 可下修至 10us 以下,但除非必要。
但再想想,前文提到各種模式所規範的上下界,以本例來說,10us 換算(乘五)就是在暫存器中是 50 counts,
所以該規範會不會是指在暫存器中的 counts,即,50~0x7F FFFF。線索之一就是若不是指 counts,則上界應是
(0x7F FFFF / 5) 才是。
再者,若使用 hw_timer_arm(10us),酬載就佔了 4us 也就是說實際會是 14us
(不過不會崩潰因為酬載會跟下次的 counts 數重疊)。而若使用 50us 相形之下誤差便會縮小。
故,使用的原則是,
該規範應是 50~(0x7F FFFF / 5) 才是。
而若要使用更小的時間,就必須將酬載也算進去。
*/

實驗-FRC1 非自動載入模式

只要改為 0 即可。
hw_timer_init(FRC1_SOURCE, 0);

/* 輸出結果
09:52:25.492 -> (3, 0, 250, 8388598)
09:52:30.493 -> (6, 0, 250, 8388598)
09:52:35.497 -> (9, 0, 250, 8388598)
09:52:40.497 -> (12, 0, 250, 8388598)
09:52:45.497 -> (15, 0, 250, 8388598)
09:52:50.497 -> (18, 0, 250, 8388598)
09:52:55.497 -> (21, 0, 250, 8388598)
09:53:00.497 -> (24, 0, 250, 8388598)
09:53:05.497 -> (27, 0, 250, 8388598)
09:53:10.496 -> (30, 0, 250, 8388598)
09:53:15.497 -> (33, 0, 250, 8388598)

** 結論
1. pivot 還是在增加,並且規律地加 3,因我們每 5 秒顯示一次,表示大約每 1.6 秒 pivot 就會增 1。符合不僅中斷持續發生,
並且最大時間長度 1.6 秒(請見前文)
2. time_cnt 仍是會增加的,只是要增 1 要 1.6 秒 *20*1000 之後
3. loader 並不會歸零
4. counter 8388598。而最大數 8388607。可猜想載裝需耗時 2us。
5. 故酬載耗時在此區間變動 2us~4us。
*/

實驗-FRC1 自動載入模式溢時

只要將這行分別改為 4, 3, 2 即可。
hw_timer_arm(4); // 函式使用的時間單位是 1us。此參數例是 每 4us 呼叫一次 ISR

/* 輸出結果
10:47:28.304 -> (9785, 62, 20, 10)
10:47:33.303 -> (19880, 124, 20, 12)
10:47:38.303 -> (9933, 187, 20, 10)
10:47:43.307 -> (19934, 249, 20, 10)
10:47:48.307 -> (9943, 312, 20, 10)
10:47:53.306 -> (19923, 374, 20, 14)
10:47:58.306 -> (10025, 437, 20, 10)
10:48:03.306 -> (26, 500, 20, 10)

10:48:23.284 -> (6109, 83, 15, 7)
10:48:28.284 -> (17398, 166, 15, 7)
10:48:33.316 -> (9185, 250, 15, 7)
10:48:38.316 -> (428, 334, 15, 7)
10:48:43.348 -> (12266, 417, 15, 7)
10:48:48.348 -> (4746, 501, 15, 7)
10:48:53.381 -> (16242, 584, 15, 7)
10:48:58.414 -> (7853, 668, 15, 7)
10:49:03.417 -> (19379, 751, 15, 7)

10:49:24.019 ->  ets Jan  8 2013,rst cause:4, boot mode:(3,6)
10:49:24.019 -> 
10:49:24.019 -> wdt reset
10:49:24.019 -> load 0x4010f000, len 3584, room 16 
10:49:24.052 -> tail 0
10:49:24.052 -> chksum 0xb0
10:49:24.052 -> csum 0xb0
10:49:24.052 -> v2843a5ac
10:49:24.052 -> ~ld

** 結論
如所見。不過酬載降低為 1.6us 倒是不知其所。及何致 wdt reset;可能是 CPU 一直忙於中斷。
接著 NMI 的就省略。筆者想測試這兩行的行為。
TM1_EDGE_INT_ENABLE();
ETS_FRC1_INTR_ENABLE();
*/

實驗-FRC1 自動載入模式之開關

在 loop() 的一開頭就加入這段程式碼

    static int u=0;
    u++;
    if (u==5) ETS_FRC1_INTR_DISABLE();
    else if (u==10) ETS_FRC1_INTR_ENABLE();
    else if (u==15) TM1_EDGE_INT_DISABLE();
    else if (u==20) TM1_EDGE_INT_ENABLE();

/* 輸出結果
11:11:23.956 -> (1, 5, 250, 240)
11:11:28.957 -> (13, 10, 250, 240)
11:11:33.956 -> (19, 15, 250, 240)
11:11:38.955 -> (25, 20, 250, 240)
11:11:43.954 -> (31, 20, 250, 227)
11:11:48.954 -> (31, 20, 250, 227)
11:11:53.953 -> (31, 20, 250, 227)
11:11:58.953 -> (31, 20, 250, 227)
11:12:03.952 -> (31, 20, 250, 227)
11:12:08.951 -> (33, 25, 250, 237)
11:12:13.951 -> (39, 30, 250, 240)
11:12:18.951 -> (46, 35, 250, 232)
11:12:23.950 -> (52, 40, 250, 240)
11:12:28.949 -> (61, 45, 250, 240)
11:12:33.949 -> (67, 45, 250, 220)
11:12:38.948 -> (67, 45, 250, 220)
11:12:43.947 -> (67, 45, 250, 220)
11:12:48.946 -> (67, 45, 250, 220)
11:12:53.945 -> (67, 45, 250, 220)
11:12:58.945 -> (67, 50, 250, 240)
11:13:03.945 -> (74, 55, 250, 234)
11:13:08.944 -> (80, 60, 250, 235)
11:13:13.944 -> (86, 65, 250, 240)
11:13:18.951 -> (93, 70, 250, 236)

** 結論
中斷預設是開啟的,但也可以於 init 後接著就關掉中斷再適時打開。
而這兩個呼叫看不出差異,都可開關中斷且都不會清除 loader 及不會停止 counter。


接著,我們將整個 loop() 改寫成如下:
    static int u=0;
    u++;
    if (u==5) ETS_FRC1_INTR_DISABLE();
    else if (u<10){loader=RTC_REG_READ(FRC1_LOAD_ADDRESS);counter=RTC_REG_READ(FRC1_COUNT_ADDRESS);}
    else if (u==10) ETS_FRC1_INTR_ENABLE();
    else if (u==15) TM1_EDGE_INT_DISABLE();
    else if (u<20){loader=RTC_REG_READ(FRC1_LOAD_ADDRESS);counter=RTC_REG_READ(FRC1_COUNT_ADDRESS);}
    else if (u==20) TM1_EDGE_INT_ENABLE();

    // put your main code here, to run repeatedly:
    delay(1000);

    Serial.printf("\r\n(%d, %d, %d, %d)", pivot, time_cnt, loader, counter);

所得到的結果:

11:48:45.506 -> (1, 1, 250, 240)
11:48:46.532 -> (14, 2, 250, 240)
11:48:47.525 -> (22, 3, 250, 239)
11:48:48.519 -> (29, 4, 250, 240)
11:48:49.512 -> (36, 4, 250, 200)-->
11:48:50.505 -> (36, 4, 250, 53)-->
11:48:51.532 -> (36, 4, 250, 211)-->
11:48:52.525 -> (36, 4, 250, 82)-->
11:48:53.518 -> (36, 4, 250, 238)-->
11:48:54.512 -> (38, 5, 250, 239)
11:48:55.505 -> (45, 6, 250, 240)
11:48:56.532 -> (53, 7, 250, 240)
11:48:57.525 -> (61, 8, 250, 240)
11:48:58.519 -> (68, 9, 250, 240)
11:48:59.512 -> (75, 9, 250, 200)-->
11:49:00.505 -> (75, 9, 250, 70)-->
11:49:01.532 -> (75, 9, 250, 226)-->
11:49:02.525 -> (75, 9, 250, 99)-->
11:49:03.518 -> (75, 9, 250, 6)-->
11:49:04.511 -> (75, 10, 250, 240)
11:49:05.538 -> (83, 11, 250, 240)
11:49:06.531 -> (91, 12, 250, 239)
11:49:07.525 -> (98, 13, 250, 240)
11:49:08.518 -> (106, 14, 250, 240)
11:49:09.511 -> (114, 15, 250, 240)
11:49:10.537 -> (122, 16, 250, 229)

發現這兩支都能停止中斷的發生,但計數器仍舊是在作動。
而若在主程式中使用“開關”就單使用這支 TM1_EDGE_INT_ENABLE(),另一支維持 enabled。
(本範例沒有寫得好不過不影響此處結論)
*/

實驗-FRC1 自動載入模式之一旦 reload,計數器跟著 reload

// 我們將整個主程式改寫如下,
// 目的是要確認,一旦 reload-register 被存入數值,
// 下數的計數暫存器也會跟著重新 reload(舊有的當前下數值被放棄/不會數完才 reload)

int u=500;

IRAM_ATTR void hw_timer_callback(){
    if (u==15){
        hw_timer_arm(0x7fffff / 5);
    }
    else RTC_REG_WRITE(FRC1_LOAD_ADDRESS, u);
    u--;
}

void setup() {
    Serial.begin(115200);
    
    hw_timer_init(FRC1_SOURCE, 1);
    hw_timer_set_func(hw_timer_callback);
    hw_timer_arm(50);
}

void loop(){
    delay(1000);

    if (u<15) hw_timer_arm(0x7fffff / 5);

    Serial.printf("\r\n(%d)", u);
}

/* 輸出結果
18:44:11.734 -> (14)
18:44:12.727 -> (14)
18:44:13.721 -> (14)
18:44:14.747 -> (14)
18:44:15.741 -> (14)
18:44:16.734 -> (14)
18:44:17.728 -> (14)
18:44:18.721 -> (14)
18:44:19.748 -> (14)
18:44:20.741 -> (14)
18:44:21.735 -> (14)

** 結論
如所見。
中斷發生了 500-15 次之後,
因為後來都是每 1.6 秒才做一次中斷,但我們每一秒就會重載一次,故中斷永遠不可能發生。
*/

實驗-FRC1 自動載入模式之中斷重新裝填

我們最後這個實驗做在 ISR 中重新裝填。
前面的實驗看到,平均約有暫存器上 15 個 counts 的 payload(3us)。並且重新裝填的 count 數可以下修到 15 counts 還是有效。故,我們一開始取 25 個 counts 作載入,並且逐一遞減至 15 個 counts(此時關掉中斷),每次中斷對 GPIO0 作轉態並量測波形。因為每一次進來 ISR,暫存器業已載入並下數,故初態 low,第一次拉 high,並載入 23,第二次拉 low 並載入 21,依此類推則將看到第一個 high 準位有 23+15 個 counts/7.6us,第一個 low 準位有 21+15/7.2us,依此類推則有兩支 pulse 且終態 high。
而最終此模式的結論是時間最小值 5us 仍可有效,

hw_timer_arm(5),實際呈現是約 8us,但使用要非常謹慎及考慮到酬載。

int u, v;

IRAM_ATTR void hw_timer_callback(){
    
    digitalWrite(0, v^=1); // 一進來就轉態;前面也驗證過此時就已有 15 counts payload
    
    // 終止條件。子句內是關閉 timer1(的中斷)的用法。
    // 但此組合用法非常危險,因一旦被啟用將不停地反覆中斷完全佔用 CPU 造成 WDT Reset。
    // 因此 arm0 應改為最大值較保險,
    // 然而,如何安全地開啟 timer1(的中斷)才是重點。
    if (u<=15){
        TM1_EDGE_INT_DISABLE();
        ETS_FRC1_INTR_DISABLE();
        hw_timer_arm(0);
    }
    else RTC_REG_WRITE(FRC1_LOAD_ADDRESS, u);
    u-=2;
}

void setup() {
    pinMode(0, OUTPUT);

    hw_timer_init(FRC1_SOURCE, 1);
    hw_timer_set_func(hw_timer_callback);
    TM1_EDGE_INT_DISABLE();
    ETS_FRC1_INTR_DISABLE();
    hw_timer_arm(0);
}

void loop() {
    digitalWrite(0, LOW);
    u=23;
    v=0;
    delay(1000);

    // 今天的問題關鍵在於我們不知道如何啟停暫存運算器計數。
    // 前面已驗證了重新裝填必能讓計數器立即重新載入。
    // 想想看如何化解當前困境 - 大數是 1.6 秒
    hw_timer_arm(0x7FFFFF / 5);
    TM1_EDGE_INT_ENABLE();
    ETS_FRC1_INTR_ENABLE();
    hw_timer_arm(5);
    
    delay(4000);
}
得到的波形,每五秒會出現一次方便我們量測
第一個 HIGH 準位,7.28us(23+15)
第二個是 LOW 準位,6.88us(21+15)
第三個是 HIGH 準位,6.40us(19+15)
第四個是 LOW 準位,6.16us(17+15)
將遞滅初值設大,所得的波形

結論

  • 筆者也從源碼查覺到停止計數器的用法如下:
    RTC_REG_WRITE( FRC1_CTRL_ADDRESS, RTC_REG_READ( FRC1_CTRL_ADDRESS ) & ~(FRC1_ENABLE_TIMER) );
  • 除此之外也瞭解到,為何呼叫初始化 timer,計數器已在跑了但 ISR 還未載入怎麼不會發生錯誤。
  • 因此,此份源碼提供了相當充份的資訊讓我們可以改寫 hw_timer.c 無虞。假設官方有附加一份 .h 檔,那麼想用的第三方應就會含括它,這意謂很大機率我們能掌控 timer1 不發生衝突。但事實並沒有,即,第三方很可能也會改寫或直接引用源碼內容而讓我們難以避免衝突。
  • NMI 耗時與 FRC 差不多,故反而 FRC 的 count 下界要大一點因為會被其他 ISR 搶走再回來,而 NMI 則不用怕。

Categories: Arduino

Tags:

PHP Code Snippets Powered By : XYZScripts.com