再探 ESP8266 GPIO 中斷

No Comments

緣起:中斷形式分為可/不可遮罩。但二者都同樣受限於,若中斷常式處理時間過長,最小公倍數時間內無法應付所有中斷,那麼假以時日將造成堆疊區崩潰。因為至少就有返回函式位址被推入堆疊區。或者其他中斷堆疊地等待服務。而這樣的風險當然會被平台設計者以硬體或軟體盡可能地避免掉。因此,我們必須要了解它的能力上界到哪,才能更善用它或避免 bug.
接著談到計時中斷其使用硬體計時器,更為敏感。原生的 hw timer,筆者並沒有去踹,然而筆者的 MultiTimers 確定是無法提供這樣的中斷函式,其處理時間接近或超過中斷週期的。別忘了平台設計者就已叮嚀過 ISR 內頂多幾個 us 或幾十個 us 就好。或許筆者再開一篇看看 MultiTimers/MultiPwms 在 ISR 內耗用多少 us。但先前好像都做過了說/忘了。
因此,簡單講問題,或許原生計時器也有這樣的問題:無法在 Timer ISR 內做 ADC 的取樣,無論取樣週期多麼長;過短,便造成 MultiTimers 崩潰,而過長是不是就沒事了?五秒才取一次樣很仁慈了吧。不幸地,ADC 取樣最小週期是約 166us,其意指,ADC 需時 166us 才能準確地抓取到回傳一次樣本。而這樣的設定將造成 WiFi 失效/在使用 MultiTimers 的測試下即便不會崩潰。
註:所謂遮罩/斷,有兩個意涵就是當 GPIO 中斷發生,該 GPIO 是否能即刻再次接受中斷。及,當中斷函式處理過程中,是否允許被自己或其他優先權更高的中斷中斷之。

補充:提醒使用中斷,中斷並不會帶給我們整體工作效率上的提升(廢話),因為羊毛(運算資源)出在羊身上,無關乎中斷;也會給我們帶來整體工作效率上的提升(胡謅,此話何意?),但其乃相依於時機與場合;更可能降低我們的效率。我們以這樣的實作例子來看就懂意思了。中斷 isr 中,僅設定旗標便退出。之後我們在 main loop 內依據旗標來做對應的事假若 main loop 是個巨大的組合結構,那麼輪到看該旗標來做事時可能已曠日廢時了。當然我們也可以在中斷中就直接做掉;如此顯然爭取了立即性,若我們是要告知另一外設我們的決定,即時性使得此外設可以立即接續它的工作,我們便可以更快收到下一次中斷,故整體效率提升了(但,顧此失彼不是嗎)。二者同樣都使用了中斷但效益卻大相涇庭。同樣 1 秒做一件事的兩種計時器,時基 1us 的將産生 100 萬次中斷,時基 1ms 的只要 1 千次,一個是 1000000 x isr_payload,一個是 1000 x isr_payload,相信不必再多說了。因此中斷能載舟覆舟,用不用與如何用,絕沒有哪種做法一定較優,而單單是相依於應用。故當通盤想過。

因此,計時中斷無法 ADC 取樣,那麼剩下唯一一條路就是腳位中斷了。這就是本篇要探究的。而為何 ADC 取樣一定非要中斷不可?因為非要中斷不可,當無法隨時地即時地獲得外界資訊時。

典型的 GPIO 中斷範例,使用 D1 mini

#define SET_INT_PIN D5 // generate interrupt to D6
#define GET_INT_PIN D6 // the int pin
#define TRS_INT_PIN D4 // D6 is interrupted, D4/LED repsonses on/off

ICACHE_RAM_ATTR void gpioIsrD6(){
    static int x=0;

    // delay(10000);
    digitalWrite(TRS_INT_PIN, x^=1);
}

void setup() {
    pinMode(SET_INT_PIN, OUTPUT);
    digitalWrite(SET_INT_PIN, LOW);

    pinMode(TRS_INT_PIN, OUTPUT);
    digitalWrite(TRS_INT_PIN, HIGH);

    attachInterrupt(digitalPinToInterrupt(GET_INT_PIN), gpioIsrD6, CHANGE);
}

void loop() {
    static int i=0, j=0;
    if (i++>25){ // 0.25 seconds.
        i=0;
        digitalWrite(SET_INT_PIN, j^=1);
    }
    delay(10);
}

這個範例是每 0.25 秒就會變化燈號一次,也就是每一秒閃兩次。
作動的方式是:
D5 每 0.25 秒就會改變準位,0 -> 1,1 -> 0。
將 D5 腳位透過杜邦線接至 D6 腳位。
D6 規劃成中斷腳位,訊號 CHANGE,即準位有變化便做一次 ISR。
ISR 內,每一次做,都會改變一次 D4 燈號。
插拔杜邦線,便能肯定中斷完美作動。

接下來讓我們迫不及待在 ISR 內插入 delay(來個十秒吧)。

GPIO ISR 內的 delay

加進去了 delay(10000),程式碼不必再重貼一次。結果如何讀者可自行嘗試。當然此時您必不在嘗試,而是只要用看的下一瞬間就知道結果了。

結果是程式崩潰了!!!


而下二瞬間才知道正確的結果:有加跟沒加 delay 結果都一樣!!!
這個結果很重要,別忘了在 ISR 內加 delay 其是沒有作用的。
當然,沒事沒人會在 ISR 內加 delay。我們要的是要拖累 ISR。故將 delay 換成:
for (int i=0; i<10000000; i++);
這樣一行,若是空作用的敍述,編譯器可能會直接移除掉,故筆者測試下,
for (int i=0, j=0; i<10000000; i++) j=micros();
這樣才是有效果的。結果是顯著的。燈號很久才變化,約六秒變化一次。
然則,本篇就將結束了,因為目的達到了,因為,ADC 隨時即時取樣有救了。因為 GPIO 中斷並未因過長的 ISR 而造成堆疊崩潰。不過,不過,很可能是時間上早晚的問題罷了。
因此設後還是得理一下。設定 ISR 耗用過長的時間,還是得理清背後的作用。
幾點來烘托出到底發生了什麼事。

  • 加入列印。
  • 在 ISR 內加入閘門。
  • 加入識別子。
  • 順便將 ADC 加入以示沒事。
  • 因中斷每 0.25 秒才一次,故以上的處置都不會有副作用。
  • 再注意燈號從原始每 0.25 秒增為 6 秒,表示拖累(累積大量 ISR/24 個)是可能必然的。
  • 程式碼及執行結果如下。
  • 簡單講結論:一旦 GPIO 中斷發生,該中斷腳位的中斷便被暫時除能直至 ISR 返回才又打開(見後述)。
#define SET_INT_PIN D5 // generate interrupt to D6
#define GET_INT_PIN D6 // the int pin
#define TRS_INT_PIN D4 // D6 is interrupted, D4/LED repsonses on/off

ICACHE_RAM_ATTR void gpioIsrD6(){
    static int x=0;

    // add print, gate, id, and ADC.
    static int gate=0, id=0;
    Serial.printf("ID %d entered; ", ++id);
    if (gate) return;
    gate=1;

    for (int i=0, j=0; i<10000000; i++) j=micros();
    digitalWrite(TRS_INT_PIN, x^=1);

    // add print, gate, id, and ADC.
    Serial.printf("ID %d leave; its Voltage is (%d, %f)\r\n", id, analogRead(A0), analogRead(A0)/1024.0f*3.3f);
    gate=0;
}

void setup() {
    // add print, gate, id, and ADC.
    Serial.begin(115200);
    delay(5000);

    pinMode(SET_INT_PIN, OUTPUT);
    digitalWrite(SET_INT_PIN, LOW);

    pinMode(TRS_INT_PIN, OUTPUT);
    digitalWrite(TRS_INT_PIN, HIGH);

    attachInterrupt(digitalPinToInterrupt(GET_INT_PIN), gpioIsrD6, CHANGE);
}

void loop() {
    static int i=0, j=0;

    if (i++>25){ // 0.25 seconds.
        i=0;
        digitalWrite(SET_INT_PIN, j^=1);
    }
    delay(10);
}

/* the result
...
16:05:07.521 -> ID 108 entered; ID 108 leave; its Voltage is (15, 0.041895)
16:05:12.823 -> ID 109 entered; ID 109 leave; its Voltage is (15, 0.045117)
16:05:18.090 -> ID 110 entered; ID 110 leave; its Voltage is (15, 0.045117)
16:05:23.359 -> ID 111 entered; ID 111 leave; its Voltage is (15, 0.041895)
16:05:28.629 -> ID 112 entered; ID 112 leave; its Voltage is (15, 0.045117)
16:05:33.897 -> ID 113 entered; ID 113 leave; its Voltage is (15, 0.045117)
16:05:39.166 -> ID 114 entered; ID 114 leave; its Voltage is (15, 0.041895)
16:05:44.432 -> ID 115 entered; ID 115 leave; its Voltage is (14, 0.041895)
...
*/

結論

結論就是這並不是結論,因為程式錯了!讀者有發現哪邊出錯了嗎?其實程式完全沒錯,只是少了東西。故,

再探

當前的程式並看不出中斷是否有被堆疊起來了,只能確認 ISR 並沒有複進入,故須要再加入中止條件,才能彰顯中斷堆疊的存在與否。故,必須在 loop() 內開頭再加入這段程式碼,這將能看出潛在的作用因子。這段程式碼單單為了,只製造出已知個中斷。


    // add print, gate, id, and ADC.
    static int gate2=0;
    if (gate2>1000) return;
    gate2++;


然而事情並不這麼單純。這段主程式將有兩種意欲的意涵。
此片段將在 10 秒左右製造出約略 40 個中斷(並被 pending 起來逐一 serve)。case 1。
或者是,此片段在 10 秒左右將只製造出約略 2 個中斷(其餘衝突的被捨棄)。case 2。
而,結果竟出乎意料之出乎意料之外地,是,非 case 1 所意欲的 case 1 ;並且也是非 case 2 所意欲的 case 2。故是 case 1+2:在 180 秒左右製造出 37 個中斷;每 10 秒約 2 個。
case 1,pending 起來 37 次中斷,常理判斷顯然是不可行的。case 2,為何會有後續的 35 個顯然恍悟出來了。
因此,最後的結論是,當 ISR servicing,loop() 是沒機會跑的,才會有與 case 1/2 同樣的結果。並且,究竟 ISR 會不會複進入我們依然沒有結論,唯一一途就是在 ISR 內再加入 GPIO D5 的 trigger。與此同時,在 loop 內 print id,才能得知 loop 執行的時機。
程式碼如下。

#define SET_INT_PIN D5 // generate interrupt to D6
#define GET_INT_PIN D6 // the int pin
#define TRS_INT_PIN D4 // D6 is interrupted, D4/LED repsonses on/off

int id=0;

ICACHE_RAM_ATTR void gpioIsrD6(){
    static int x=0;

    // add print, gate, id, and ADC.
    static int gate=0;
    Serial.printf("ID %d entered; ", ++id);
    if (gate) return;
    gate=1;

    // in order to show isr reentered
    extern int j;
    if (!(id%10)) digitalWrite(SET_INT_PIN, j^=1);

    for (int i=0, j=0; i<10000000; i++) j=micros();
    digitalWrite(TRS_INT_PIN, x^=1);

    // add print, gate, id, and ADC.
    Serial.printf("ID %d leave; its Voltage is (%d, %f)\r\n", id, analogRead(A0), analogRead(A0)/1024.0f*3.3f);
    gate=0;
}

void setup() {
    // add print, gate, id, and ADC.
    Serial.begin(115200);
    delay(5000);

    pinMode(SET_INT_PIN, OUTPUT);
    digitalWrite(SET_INT_PIN, LOW);

    pinMode(TRS_INT_PIN, OUTPUT);
    digitalWrite(TRS_INT_PIN, HIGH);

    attachInterrupt(digitalPinToInterrupt(GET_INT_PIN), gpioIsrD6, CHANGE);
}


int j=0;
void loop() {
    static int i=0;

    // add print, gate, id, and ADC.
    static int gate2=0;
    if (gate2>1000) return;
    gate2++;

    if (i++>25){ // 0.25 seconds.
        i=0;
        digitalWrite(SET_INT_PIN, j^=1);

        Serial.printf("\r\nin loop, see the ID %d\r\n", id);
    }
    delay(10);
}

真正的結論,並且也符合預期

程式執行結果片段:
18:58:26.730 -> in loop, see the ID 7
18:58:26.995 -> ID 8 entered; ID 8 leave; its Voltage is (13, 0.038672)
18:58:31.995 -> 
18:58:31.995 -> in loop, see the ID 8
18:58:32.260 -> ID 9 entered; ID 9 leave; its Voltage is (13, 0.038672)
18:58:37.259 -> 
18:58:37.259 -> in loop, see the ID 9
18:58:37.524 -> ID 10 entered; ID 10 leave; its Voltage is (13, 0.038672)
<---------------------這裏少掉了一行 in loop, see the ID 10--------------------------------->
18:58:42.524 -> ID 11 entered; 
  • loop 會因 isr servicing 而只能等待其結束,才有機會執行
  • 當 isr servicing 時,還是能觸發同一支中斷,且被 pending,但是否能 pending 二次以上的觸發不得而知但常理判斷只能 pending 一次,即,iflag 被設。不過,如此卻會導致中斷接續發生,因而其他的 isr 或者 loop 將沒有機會執行而造成 wdt reset。

ADC 定時取樣

再讓我們回到最原始的問題思考,
將 GPIO 的 trigger,置入 timer isr。如此 timer 便能設後不理。trigger 透過實體連接觸發 int pin。如此,ADC 的定時取樣將獨立於 timer 及 loop 了。
但,
唉怎還有但,
當 timer isr 跑完 GPIO 的 trigger,在返回前,將隨即又去跑了 gpio int isr,這將是可預期的,這意謂著 timer isr 將同樣地被拖累。
而筆者的 MultiTimers 又是鞍在 non-NMI 模式下。
因此,筆者將先遭遇到這困難後,再嘗試改為 NMI 試試。但 MultiTimers – NMI 筆者並未測試過,尚不知其可行性及對整個系統的影響程度,將留待本文更新。

20210302 更新

如前一天的陳述,non-NMI 的計時中斷 ISR,可能會在處理過程中再被岔斷。不過前面我們看到同一支 GPIO 並不會對自己岔斷,但會 pending。而計時器中斷很可能優先權序高於 GPIO,若是,那麼不做任何處理便自然解套。若否,則便得將 timer int 改成 NMI 才行了。故這就是接下來要看的。如下程式碼,再加入了 MultiTimers,使用它來觸發 GPIO int,筆者也直接講結果,當在 timer isr 內觸發 GPIO 中斷後,還有一段延遲以提供充份的時間讓 GPIO 中斷岔斷出去跑 GPIO ISR。結果很幸運地,確定 timer isr 跑完後,才會接續去跑 GPIO ISR。亦即 timer int priority 高於 gpio int。這表示,使用 MultiTimers,GPIO INT,將使得隨時即時地 ADC 取樣成為可行。
順道說一下,隨時即時地 ADC 取樣,在 loop 內或獨佔的 CPU 時間下,真的做不到嗎?若以最直白(bare)底層程式碼處理,一定是可以的。但試想,在該時段下,ADC 週期取樣要做到外,其中還很可能需夾雜其他程式碼的判斷或開關因而搗亂取樣週期或更不易調試。一個定時中斷便解決了這樣的困境了。
那麼 ADC 取樣路通了,接下來還有什麼關卡呢?
在這樣的應用下,原則上,
只有需要取樣時,才將 timer int 打開,畢竟 ADC 取樣的成本太高。
並且,取樣週期是最適切所需的,且是短暫時距的,以求避免 loop() 長時間飢餓而造成 wdt reset。

還有,別忘了,今天之所以會有這篇文章,就是因為 ADC 定時取樣造成 WiFi 失效,配合以上原則,才能再現 WiFi!
最後我們就來測試看看 ADC 取樣成本。

#include"Esp8266HwSwTimers.h"

#define SET_INT_PIN D5 // generate interrupt to D6
#define GET_INT_PIN D6 // the int pin
#define TRS_INT_PIN D4 // D6 is interrupted, D4/LED repsonses on/off

cHwTimer timer_get_adc_manip;
IRAM_ATTR unsigned timer_trigger=0;
int id1=0, id2=0, id3=0;

IRAM_ATTR void isr_get_adc_manip(){
    ++id1;
    digitalWrite(SET_INT_PIN, timer_trigger^=1);
    #if 0
        ++id2;
    #else
        for (int i=0; i<1000000; i++) id2=i;
    #endif
}

ICACHE_RAM_ATTR void gpioIsrD6(){
    static int x=0, gate=0;
    int y=++id3;

    Serial.printf("ID1[%d] called; its ID2[%d]; ", id1, id2);
    Serial.printf("ID3[%d] entered; ", y);
    if (gate){
        Serial.printf("ID3[%d] aborted; ", y);
        return;
    }
    gate=1;

    for (int i=0, j=0; i<10000000; i++) j=micros();
    digitalWrite(TRS_INT_PIN, x^=1);

    Serial.printf("ID3[%d] leave; its Voltage is (%d, %f)\r\n", y, analogRead(A0), analogRead(A0)/1024.0f*3.3f);
    gate=0;
}

void setup() {
    Serial.begin(115200);
    delay(5000);

    pinMode(SET_INT_PIN, OUTPUT);
    digitalWrite(SET_INT_PIN, LOW);

    pinMode(TRS_INT_PIN, OUTPUT);
    digitalWrite(TRS_INT_PIN, HIGH);

    attachInterrupt(digitalPinToInterrupt(GET_INT_PIN), gpioIsrD6, CHANGE);
    timer_get_adc_manip.setTimer(1500000, isr_get_adc_manip); // 1.5s
}

void loop() {
    Serial.printf("\r\nin loop, see the ID1[%d], ID2[%d], ID3[%d] and the trigger pin[%d]\r\n", id1, id2, id3, digitalRead(SET_INT_PIN));
    delay(500);
}

ADC 取樣成本

如下程式碼,一來展示 ADC 取樣的最大耗時以提供給讀者參考,再來也順道展示 MultiTimers 的好用之處。執行結果如下。

/* 執行結果。

11:13:57.065 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[13]
11:13:57.330 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[13]
11:13:57.562 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[13]
11:13:57.826 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[13]
11:13:58.059 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[13]
11:13:58.324 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[12]
11:13:58.588 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[12]
11:13:58.820 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[13]
11:13:59.085 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[12]
11:13:59.317 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[13]
11:13:59.582 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[13]
11:13:59.847 -> ADC_T_MIN[94], ADC_T_MAX[161], TimerPeriod[166], ADC[13]

*/

#include"Esp8266HwSwTimers.h"


#define SET_INT_PIN D5 // generate interrupt to D6
#define GET_INT_PIN D6 // the int pin
#define TRS_INT_PIN D4 // D6 is interrupted, D4/LED repsonses on/off


cHwTimer timer_get_adc_manip;
IRAM_ATTR unsigned timer_trigger=0;

IRAM_ATTR void isr_get_adc_manip(){
    digitalWrite(SET_INT_PIN, timer_trigger^=1);
}


ICACHE_RAM_ATTR unsigned adc_t=0, adc_mint=1024, adc_maxt=0;
volatile unsigned samplex=0;

ICACHE_RAM_ATTR void gpioIsrD6(){
    adc_t=micros();
    samplex=analogRead(A0);
    adc_t=micros()-adc_t;
    if (adc_t<adc_mint) adc_mint=adc_t;
    else if (adc_t>adc_maxt) adc_maxt=adc_t;
}


unsigned timer_dot=0;

void setup() {
    Serial.begin(115200);
    delay(5000);

    pinMode(SET_INT_PIN, OUTPUT);
    digitalWrite(SET_INT_PIN, LOW);

    pinMode(TRS_INT_PIN, OUTPUT);
    digitalWrite(TRS_INT_PIN, HIGH);

    attachInterrupt(digitalPinToInterrupt(GET_INT_PIN), gpioIsrD6, CHANGE);
    timer_dot=130; // 130us
    timer_get_adc_manip.setTimer(timer_dot, isr_get_adc_manip);
}

void loop() {
    Serial.printf("\r\nADC_T_MIN[%u], ADC_T_MAX[%u], TimerPeriod[%u], ADC[%u]", adc_mint, adc_maxt, timer_dot, samplex);
    if (adc_maxt>timer_dot){
        timer_dot=adc_maxt+5;
        timer_get_adc_manip.setTimer(timer_dot, isr_get_adc_manip);
    }
    delay(250);
}

Categories: Arduino

Tags:

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

PHP Code Snippets Powered By : XYZScripts.com