ESP8266 跟時間賽跑 中斷篇
繼前篇文章的跟時間賽跑,聚焦在 gpio 本身的響應時間,及 cycle count。本文要再初賽一次才能晉級,將聚焦在中斷。
本文將摸索 gpio 中斷及 timer 中斷。中斷的種類就不展開了也不洗殘廢澡了更省了貼圖了,就直接講結果。因此建議先看過前篇的初賽再看本篇多少有點啣接性。
參考資料
- 一直以來遍尋不著,今天終於讓我給找到了,
- https://www.esp8266.com/wiki/doku.php?id=esp8266_memory_map
- https://www.pcboard.ca/_documents/The-ESP8266-Book-September-2015.pdf page(114)
- https://github.com/esp8266/Arduino/blob/master/tools/sdk/include/eagle_soc.h
- https://github.com/esp8266/Arduino/blob/master/tools/sdk/include/ets_sys.h
- https://github.com/espressif/ESP8266_NONOS_SDK/blob/master/driver_lib/driver/hw_timer.c
- https://www.esp8266.com/viewtopic.php?p=34411
GPIO 中斷
#include <ESP8266WiFi.h>
// @160MHz
// use pin4 for delay, pin12 for signaling(output) xor gpio interrupt(input), pin13 for indicating int occurred.
// stop software/hardware wdt at these tests. note that wdt-reset after 71 seconds but enough time for probing.
// 2 of wemos d1 mini are parallel connected. one to test the other.
// macro switches: 1. tester or testee. 2. arduino or api in testee. 3. printout or gpio13 indication in testee.
// 4. tester gpio latency/behaviors.
// note: if multiple gpios are interrupts, use GPIO_REG_READ(GPIO_STATUS_ADDRESS) to check respective pin triggered or not.
IRAM_ATTR volatile unsigned &PIN_OUT_SET = *((volatile unsigned*)0x60000304);
IRAM_ATTR volatile unsigned &PIN_OUT_CLEAR = *((volatile unsigned*)0x60000308);
IRAM_ATTR volatile unsigned &HardwareWDT = *((volatile unsigned*)0x60000900);
#define PIN4 0x10
#define PIN12 0x1000
#define PIN13 0x2000
#define DELAY_75ns (PIN_OUT_SET=PIN4)
#define digitalWrite12HIGH (PIN_OUT_SET=PIN12)
#define digitalWrite12LOW (PIN_OUT_CLEAR=PIN12)
#define digitalWrite13HIGH (PIN_OUT_SET=PIN13)
#define digitalWrite13LOW (PIN_OUT_CLEAR=PIN13)
#define disableHardwareWDT (HardwareWDT&=~1)
#define enableHardwareWDT (HardwareWDT|=1)
IRAM_ATTR static uint32_t _getCycleCount() __attribute__((always_inline));
IRAM_ATTR static inline uint32_t _getCycleCount(){
uint32_t ccount;
__asm__ __volatile__("rsr %0,ccount":"=a" (ccount));
return ccount;
}
#if 0 // 1 tester or 0 testee, tester only by pin12 signaling.
#define GPIO_LATENCY 0 // in order to probe the latency between the signal source and the int triggered.
void setup() {
pinMode(1, INPUT); // disable tx
pinMode(3, INPUT); // disable rx
pinMode(4, OUTPUT);
pinMode(12, OUTPUT);
pinMode(13, INPUT);
}
IRAM_ATTR void loop() {
static unsigned t1=1;
if (t1){
WiFi.mode(WIFI_OFF);
WiFi.forceSleepBegin(-1);
t1=0;
system_soft_wdt_feed();
system_soft_wdt_stop();
disableHardwareWDT;
delay(7500);
}
while (1){
#if GPIO_LATENCY
delayMicroseconds(random(300)+300); // from 300us to 600us
digitalWrite12LOW;
static unsigned k=0;
if (++k>20) k=0;
for (int i=0; i<k; i++) DELAY_75ns; // from 75ns to 1.5us
delayMicroseconds(300); // at least 300us
digitalWrite12HIGH;
#else // GPIO_LATENCY
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns; // 900
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns; // 1350
// ----------- this block of code for expanding for fit arduino-style testee
#if 0
DELAY_75ns; // 1650
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns; // 1650, 1650+1425=3075
#endif
// ----------- this block of code for expanding for fit arduino-style testee
//digitalWrite(12, LOW);
digitalWrite12LOW; // 1425ns
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns; // 900
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns; // 1350
// ----------- this block of code for expanding for fit arduino-style testee
#if 0
DELAY_75ns; // 1650
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns;
DELAY_75ns; // 1650, 1650+1425=3075
#endif
// ----------- this block of code for expanding for fit arduino-style testee
//digitalWrite(12, HIGH);
digitalWrite12HIGH; // 1425ns
#endif // GPIO_LATENCY
}
}
#else // testee, pin12 for gpio interrupt, pin13 shows the responses.
IRAM_ATTR volatile unsigned old_cnt=_getCycleCount(), cur_cnt=0, min_cnt=-1, max_cnt=0;
#define PRINTOUT 0
#define ARDUINO_STYLE 0
#define API_STYLE !ARDUINO_STYLE
#if ARDUINO_STYLE
IRAM_ATTR void arduino_isr(){
#if !PRINTOUT // gpio13 indication.
IRAM_ATTR volatile static unsigned u=0;
digitalWrite(13, u^=1);
#else // print out
cur_cnt=_getCycleCount();
unsigned tmp=cur_cnt-old_cnt;
if (tmp<min_cnt) min_cnt=tmp;
else if (tmp>max_cnt) max_cnt=tmp;
old_cnt=cur_cnt;
#endif // print or not
}
#else // API_STYLE
IRAM_ATTR void api_isr(){ // assume only 1 gpio int set, the gpio12
#if !PRINTOUT // gpio13 indication.
GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, GPIO_REG_READ(GPIO_STATUS_ADDRESS)); // clear all gpios int flags
IRAM_ATTR volatile static unsigned u=0;
if (u^=1) digitalWrite13HIGH;
else digitalWrite13LOW;
#else // print out
GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, GPIO_REG_READ(GPIO_STATUS_ADDRESS)); // clear all gpios int flags
cur_cnt=_getCycleCount();
unsigned tmp=cur_cnt-old_cnt;
if (tmp<min_cnt) min_cnt=tmp;
else if (tmp>max_cnt) max_cnt=tmp;
old_cnt=cur_cnt;
#endif // print or not
}
#endif // styles
void setup() {
pinMode(4, INPUT);
pinMode(13, OUTPUT);
}
IRAM_ATTR void loop() {
static unsigned t1=1;
if (t1){
WiFi.mode(WIFI_OFF);
WiFi.forceSleepBegin(-1);
t1=0;
delay(3000);
system_soft_wdt_feed();
system_soft_wdt_stop();
disableHardwareWDT;
#if ARDUINO_STYLE // arduino style
attachInterrupt(digitalPinToInterrupt(12), arduino_isr, CHANGE); // both edges
#else // API_STYLE
PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12); // set as gpio
GPIO_DIS_OUTPUT(GPIO_ID_PIN(12)); // set gpio12 as input
ETS_GPIO_INTR_DISABLE(); // disable all gpios interrupts
ETS_GPIO_INTR_ATTACH(api_isr, NULL); // set gpio12 isr
gpio_pin_intr_state_set(GPIO_ID_PIN(12), GPIO_PIN_INTR_ANYEDGE); // both edges
ETS_GPIO_INTR_ENABLE(); // enable gpio ints
#endif // styles
}
#if PRINTOUT
IRAM_ATTR static unsigned t=1;
while (t){
if (max_cnt && ++t>=18){ // about 100us if no any ints
ETS_GPIO_INTR_DISABLE();
Serial.begin(115200);
delay(6000);
Serial.printf("%u,%u\r\n", min_cnt, max_cnt);
delay(300);
Serial.end();
//old_cnt=_getCycleCount(), cur_cnt=0, min_cnt=-1, max_cnt=0;
//delay(200);
t=0;
//ETS_GPIO_INTR_ENABLE();
}
}
#endif
}
#endif // 1 tester or 0 testee.
- 上面這份程式碼大概就可測出想要的數據了,透過 comment/uncomment 一些註解及開關一些 switches,建議全盤了解這一點點的程式碼便可知如何測/測了哪些東西,及了解中斷的用法。
- 其使用了兩張 wemos d1 mini,the same pin to the same pin 對接,由其中一張發訊號讓另一張觸發中斷,並在 isr 中由另一支 pin toggle 電平來反映 timing。有可能無法啟動,則反覆按 reset button,若兩張的 pin12 & pin13 初始態都是 low 則表示成功啟動。
- 有些狀況並未測試到,例如 level trigger,及多支 gpios 中斷,及同時讓多支 gpios 中斷等。這些也都是 timing critical 而該測,但或許直接由現有的結論推論即可因為,當前測果並不是很令筆者滿意 XD。
- 函式 cycle-count 仍會飄,所以關於 cycle count,未來可能還需再好好地正確地測出結果。
- 因 timing 的量測是使用解析度不高的 LA/80MHz,所以此本身就是一個不確定因素;但該指出的是,CPU 結構下的中斷機制,依筆者不負責任的認知(臆測),可能是每一個指令週期(數個不等的時脈週期)之後會去檢查中斷,故須多花費一個時脈週期,斥或,每個指令週期中就包含了中斷檢查,即與管線同等地位,斥或使用一個專用的指令週期去檢查中斷。該強調的是 CPU 的中斷仍等同於取樣機制,故來源訊號的頻率大小,不絕對與中斷反應時間成反比更何況是 LA 的量測結果。但所描述的數據仍是具參考價值的。
- 該提出的是,筆者仍納悶為何中斷會花這麼多的時脈週期,約 (900ns-150ns-75ns)/6.25ns=108 clock cycles。
- 結論:
- 使用 arduino 中斷,訊號來源是 75ns,則中斷響應時間是 3us。其中,首次的延遲時間是 2.85us,故預估淨響應時間是 (2850-150-75=2625ns),當 isr 中僅用於計算時便可參考。訊號源改成 900ns,有同樣結果。
- 使用 arduino 中斷,唯有將訊號來源調高成(至少) 3075ns,則中斷響應時間才會跟來源同步,即 71 秒內來源與觸發總數相同。
- 使用 api 中斷,訊號來源是 75ns,則中斷響應時間是 900ns。其中,首次的延遲時間是 1.15us,故預估淨響應時間是 (1150-150-75=925ns),當 isr 中僅用於計算時便可參考。訊號源改成 225ns,有同樣結果。但當訊號源改成 300ns,雖有同樣結果但卻會定時穿插誤失,即有規律地某次響應遠多於 900ns。(針對此點作個補充如下:)
- (我們看到,300ns v.s. 900ns,中斷響應至少需時 900ns,此期間會發生 3 次來源訊號的改變並且第三次恰發生在第 900ns 的時期,因此在時間誤差/一定會有/的前提下,任一次中斷都可能誤失。但若是 level trigger 呢?答案是,還是可能誤失。因為,edge-transition,若早發生於中斷取樣則誤失。level-transition,若晚發生於中斷取樣則同樣誤失(直到下次 900ns 才成立)。除非中斷機制並非如前筆者所述是取樣機制而是其他機制,但這就需要有官方文件來端詳正因了。第二點相對地說,若是此未知機制是狀態改變便舉旗標,則 300 v.s. 900 便不會有誤失此易明之。)
- 使用 api 中斷,唯有將訊號來源調高成(至少) 1425ns,則中斷響應時間才會跟來源同步,即 1425ns。(註:響應者,少了一對 high/low,原因未知,要不就是再加 75ns)
Hardware Timer 1 中斷探究
ESP8266 有兩隻 hardware timers,hwtimer1 & hwtimer2。其中在 sdk & arduino 的規範下 hwtimer1 可給使用者自由運用,但要在意一些官方軟體模組例如 sdk or arduino pwm 也是使用 hwtimer1,故 hwtimer1 一旦自訂用上,再用上 sdk or arduino pwm 則必然衝突故只能擇其一運用。而 hwtimer2 則有被 wifi 使用到/待查證,官方也明言不要去動它,故就打消染指它的念頭吧。此處只針對 hwtimer1 故不再強調。
其次,timer 的行為是從 reload register 載入到 counter register 中,由 counter register 下數,此 counter 唯若遇到成零即産生中斷,又 counter 自身又會不停下數,即,從 0 轉而成負值(-1 等於無號數最大值)下數。又因 timer 行為可規劃成 auto reload 斥或 manual reload;若是前者,counter register 遇零後自動從 reload register 載入並下數。後者,則 counter 不自動載入轉而從最大值下數,即若使用者此期間填入 reload register 某值便會立即觸發 counter register 從 reload register 中載入此值並下數。reload register 可讀可寫。counter register 可讀不可寫。
官方也說明到,timer reload register 要使用數值至少 100 起跳(@div16),其中 1count@div16 對應到 0.2us。這意謂著我們必須使用至少 20us 起跳的,說,最小時基,這是非常不 OK 的。不過在該前提下精細度是可細到 0.2us 乃至於 12ns 這麼細。當然本文目的就是希望追時基盡可能地小;不過先說,筆者已是失望了。而所探究者仍極具應用價值。
附加說明在對此 timer 設定上,它還有區分成 edge/level trigger 兩種。筆者不知此二者差異。但我們最好使用 edge trigger 的設定,因為 level 的當發生中斷還需某些額外的處置。再一點,在官方的某些扣上,是預設使用了 div16 的設定。因此規劃 hwtimer1,需留意源頭的設定以防出入;務求全盤檢視過 timer 的相關設定為佳。至於還有岔斷優先權的兩種型式規範,NMI 將可岔斷任何的 isr 並且不被岔斷;FRC1 則無法岔斷任何的 isr 且可被 NMI 岔斷/不過應沒機會遇到。
再來,此計時器能算多大?官方說是 23 bits。我們用以下程式片斷便可探得。得到結果 0x7FFFFF。
RTC_REG_WRITE((FRC1_LOAD_ADDRESS), -1);
Serial.printf("\r\n0x%X\r\n", (unsigned)RTC_REG_READ(FRC1_LOAD_ADDRESS));
因此,以 div16 而言,就是 0x7FFFFF x 0.2us = 1677721.4us 合 1.67 秒,合 0.1 秒@div1。所欲計時者不能超過此值。
之前已在 multitimers 運用了 hwtimer1 了。以下貼出已攫取出的必要的程式片斷,它將作為我們的入口程式碼開始探究之路。
//////////////////////////////// 測試用入口程式碼,"程式 0" ///////////////////////////////////
#include <ESP8266WiFi.h>
/*
* pin4: only for delay purpose.
* pin12: the signal source pin we make.
* pin13: the subject another wemos d1 mini which 1-to-1 connected, whose pin13 shows responses of its pin12 being interrupted.
*
* note: the hwtimer1, 1count@div16 is 0.2us.
*/
IRAM_ATTR volatile unsigned &PIN_OUT_SET = *((volatile unsigned*)0x60000304);
IRAM_ATTR volatile unsigned &PIN_OUT_CLEAR = *((volatile unsigned*)0x60000308);
IRAM_ATTR volatile unsigned &HardwareWDT = *((volatile unsigned*)0x60000900);
IRAM_ATTR volatile unsigned &HwTimer1Load = *((volatile unsigned*)0x60000600);
#define PIN4 0x10
#define PIN12 0x1000
#define PIN13 0x2000
#define DELAY_75ns (PIN_OUT_SET=PIN4)
#define digitalWrite12HIGH (PIN_OUT_SET=PIN12)
#define digitalWrite12LOW (PIN_OUT_CLEAR=PIN12)
#define digitalWrite13HIGH (PIN_OUT_SET=PIN13)
#define digitalWrite13LOW (PIN_OUT_CLEAR=PIN13)
#define disableHardwareWDT (HardwareWDT&=~1)
#define enableHardwareWDT (HardwareWDT|=1)
IRAM_ATTR static uint32_t _getCycleCount() __attribute__((always_inline));
IRAM_ATTR static inline uint32_t _getCycleCount(){
uint32_t ccount;
__asm__ __volatile__("rsr %0,ccount":"=a" (ccount));
return ccount;
}
//#define FREQ_USED 40
#define FREQ_USED 100
#define FRC1_ENABLE_TIMER (BIT7)
#define FRC1_AUTO_LOAD (BIT6)
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;
void StopHwTimer1(){
RTC_REG_WRITE((FRC1_LOAD_ADDRESS), FREQ_USED);
ETS_FRC1_INTR_DISABLE();
TM1_EDGE_INT_DISABLE();
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (/*FRC1_ENABLE_TIMER | FRC1_AUTO_LOAD | */DIVDED_BY_16 | TM_EDGE_INT));
#if 0 // old version
/////RTC_REG_WRITE((FRC1_CTRL_ADDRESS), ((RTC_REG_READ(FRC1_CTRL_ADDRESS))&~(FRC1_ENABLE_TIMER)));
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (/*FRC1_ENABLE_TIMER | FRC1_AUTO_LOAD | */DIVDED_BY_16 | TM_EDGE_INT));
TM1_EDGE_INT_DISABLE();
#endif
};
void ResumeHwTimer1(){
RTC_REG_WRITE((FRC1_LOAD_ADDRESS), FREQ_USED);
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (FRC1_ENABLE_TIMER/* | FRC1_AUTO_LOAD*/ | DIVDED_BY_16 | TM_EDGE_INT));
TM1_EDGE_INT_ENABLE();
ETS_FRC1_INTR_ENABLE();
#if 0 // old version
RTC_REG_WRITE((FRC1_LOAD_ADDRESS), FREQ_USED);
/////RTC_REG_WRITE((FRC1_CTRL_ADDRESS), ((RTC_REG_READ(FRC1_CTRL_ADDRESS))|(FRC1_ENABLE_TIMER)));
TM1_EDGE_INT_ENABLE();
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (FRC1_ENABLE_TIMER/* | FRC1_AUTO_LOAD*/ | DIVDED_BY_16 | TM_EDGE_INT));
#endif
};
bool SetHwTimer1(void (*main_isr)()){
if (!((RTC_REG_READ(FRC1_CTRL_ADDRESS))&FRC1_ENABLE_TIMER)){ // timer is disabled
ETS_FRC_TIMER1_INTR_ATTACH(main_isr, NULL);
/////ETS_FRC_TIMER1_NMI_INTR_ATTACH(main_isr);
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (FRC1_ENABLE_TIMER | FRC1_AUTO_LOAD | DIVDED_BY_16 | TM_EDGE_INT));
RTC_REG_WRITE((FRC1_LOAD_ADDRESS), FREQ_USED);
TM1_EDGE_INT_ENABLE();
ETS_FRC1_INTR_ENABLE();
return true;
}
return false;
}
IRAM_ATTR void isr1() {
//HwTimer1Load=FREQ_USED;
IRAM_ATTR static unsigned z=1;
if (z^=1) digitalWrite12HIGH;
else digitalWrite12LOW;
////HwTimer1Load=FREQ_USED;
}
void setup() {
pinMode(1, INPUT); // disable tx
pinMode(3, INPUT); // disable rx
pinMode(4, OUTPUT);
pinMode(12, OUTPUT);
digitalWrite(12, LOW);
pinMode(13, INPUT);
}
void loop() {
static unsigned t1=1;
if (t1){
t1=0;
WiFi.mode(WIFI_OFF);
WiFi.forceSleepBegin(-1);
system_soft_wdt_feed();
system_soft_wdt_stop();
disableHardwareWDT;
delay(7500);
SetHwTimer1(isr1);
}
}
實驗一
- 上面這份扣(”程式 0″)原封不動執行之得到全時一致的 waveform 其週期是 40us。
- 但觀察到每連續數次週期後週期總會接著連續來幾個誤差約幾 ns 的週期。
- 結論:
- 符合預期一個 count 為 0.2us@div16。
- 與其說程式中的每行程式碼運行時間不固定,不如說是,要不是 0.2us 僅僅只是個約略值,要不就是此 timer 的計時並不精確,即,容易飄。因此,忌當計時大時間時,使用 hwtimer1,因會有累積偏差。背馳地說,我們可在 isr 內計次/使用 manual reload,以達任意長的時間的計時此易明;衍申地說,我們可藉此例如 10 小時,來觀察累積性誤差到底多大(理論數 v.s. 實際時間)。
實驗二
- 將 reload register 從 100 改為 1600,div16 改為 div1,如下的這兩行,
#define FREQ_USED 1600
//// at here, bool SetHwTimer1(void (*main_isr)()){
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (FRC1_ENABLE_TIMER | FRC1_AUTO_LOAD | DIVDED_BY_1 | TM_EDGE_INT));
- 我們得到與實驗一同樣的結果。
- 週期仍有數 ns 的偏差,並且並無規律可言例如 40.025, 40.025, 40.038, 40.788, 40.025, 40.025, 40.35, 40.025, 40.025, 40.025, …。
- 結論:
- 0.2us/16=12.5ns@div1 也是符合預期。
- 很高的確信度可聲稱此 timer 很會飄。但可被原諒因為此 timer 速度高達 80MHz。一般的 mcu 的 hardware timers,很少有這麼快的。
實驗三
- 接下來在 “程式 0” 的基礎下,修改使用 DIVDED_BY_1,FREQ_USED(100),預期將是 1.250us x 2 的週期;
- 不幸地,結果,這些半週期 1.25us 中總會夾雜約略 1600ns 半週期加 650ns 的半週期,顯然,counter underflow 很常發生。
- (p.s. 1600+650!=650+1600。則可能,[1] counter 被短暫暫停/則不會有 650ns。[2] 某次中斷過程中,耗用的時間比預期長。[2-1] 當 underflow 且正在 isr 內因該次 isr 做得比預期久,該次中斷將不立即發生此為規範;而退出 isr 後會立即補岔斷因為旗標舉著,才有所謂的下一個 650ns。[2-2] 某次中斷過程中被未知的 NMI 岔斷了/其並不影響 counter 計數及重載。[3] 魔力,我們下面會看到。)
- isr 內容已是最精簡了因為只有一個判斷與一次的 75ns 的 gpio output。
- 由於沈澱了一下才發現存在魔力干預。故我們須先作幾件確定的結論,
- CPU 執行指令,花費的時間是確定的且固定的。即,我們可以肯定,此 isr 僅僅花費約抓 150ns。
- branch to specific isr,勢必先跳到總表,再分發,故必有時間損耗在此行為上;否則我們不必如此大費周章。
- 接下來就揭示魔力了,在 loop() 內的 SetHwTimer1(isr1); 下一行加入 while (1); 再跑一次。
- 我們終於得到一致的 1.25us 半週期了。
- 接著,抓最小時基,
- FREQ_USED(95)。只要低於此值,便開始錯亂了。事實上只要低於 95,都可發現有連續多個約 650ns 的半週期。故我們大膽認定,一出 isr 隨即再進,就會有這樣的時間,即,單純跑一次 isr 所花費的時間。
- 結論:
- frc1,div1,auto-reload,最小時基 count=95,1188ns。
- 跳到總表再跳入 isr,至少花費 650ns-150ns=500ns。這是一個無法攻克的時間。
- 出了 loop() 的底層程式碼,的確會影響 timer isr 的運行,推論就是會不定時暫時關掉 frc1 中斷。
- 於此已導引出了做第四個實驗的想法與提問/diff(div1, div16)。若是 div1 則 counter register 等都會比較忙。若是 div16 時基能不能更縮小?
實驗四
- 在 “程式 0” 的基礎下,使用 DIVDED_BY_16,FREQ_USED(5),預期將是 1us x 2 的週期;
- 得到結果是含有 650ns 半週期的雜亂無章的波型,
- 拿掉魔力干預,雜亂現象依舊。
- 最後在沒有魔力的干預下,才抓到最小時基 FREQ_USED(6),即 1200ns 半週期。往後說沒有魔力干預,筆者將特稱“在超跑 isr 下”。
- 結論:
- frc1,div16,auto-reload,最小時基 count=6,1200ns。
- div16 or div1 並不影響 timer 行為下的耗時。
實驗五
- 承繼實驗四,即在超跑 isr 的條件下,frc1,div16,manual-reload,count=6,1200ns。只改為實驗 manual reload。
- 只要將 FRC1_AUTO_LOAD 註解掉,並在 isr 中加入 reload register。並且區分為前載或後載。
- 我們發現前載及後載 waveforms 都具一致性,不過,半週期都多於 1200ns。並且,後載 2400ns 多於前載 1800ns。
- 結論:
- auto-reload 時間的精準性優於 manual-reload,這無庸置疑。
- 手動載入,便多了一行指令作載入故是時間增多的原因之一。
- 前載,幾乎等同 auto-reload,但仍有可觀的差別,由時間便可看出。此時 isr 內的程式碼的耗時便會與 counter 計數重疊故說幾乎等同。
- 後載表示,counter 歸零後立即從最大數下數,凡數過的數字例如 3 個 counts,代表著 isr 內程式碼的執行時間,接著最後才填 reload register。故 waveform 成型的時間距便是 3 counts + reload register counts 所成的時間。此應不難理解。
- 總結 manual/auto reload,除上述外,manual reload 好處是不想載入便不載入但仍需留意最大值而成零的中斷。而若在 isr 內便遇到歸零中斷亦不會被岔斷要等離開才會立即岔斷故說二者無異;但 auto-reload counter 從未停過故會導致後來的 isr 時間錯亂;manual-reload 則有每次“自新”的機會。因此原則上 isr 的處理時間不能多於 timer 時間是使用者的責任外,除非有特別的理由才需使用 manual reload。
實驗六
- 接下來來看一個特別的結果,承繼實驗三,於超跑 isr 條件下,
- frc1,div1,auto-reload,最小時基 count=95,1188ns。結果是低於 95 就會有錯亂的情形。此處實驗將 reload count 改為 10。同理,若是 div16,將 reload count 設為 1,也會有同樣的結果。
- 結果,得到一致地半週期為 670ns 的 waveform。
- 結論:
- 此為特例沒有可應用的地方。670ns 應就是此 timer 的不可用的極限了。
總結
- 咦,那 NMI 呢?
- 沒錯還有 NMI,就是上面這些實驗再跑一次,只是設定成 NMI,其只要將 frc1 的註解掉改成 nmi 的即可。
//// ETS_FRC_TIMER1_INTR_ATTACH(main_isr, NULL);
ETS_FRC_TIMER1_NMI_INTR_ATTACH(main_isr);
- 筆者是有小試了一下,結果發現 NMI 的最小時基應都是 2us 以上,不過 waveforms 都是很漂亮的一致性。故也懶得深究了。
- 並且,依照 NMI 的特性,將沒有任何中斷可以中斷它及加上先前察覺到了那片干預的魔力,表示 loop() 外的底層程式有必要關掉中斷做其他某些事。故一旦用上 NMI 將導致那方面的異常/很可能是 wifi。當然話說回來,在必要應用精準細 timer 的需求下,關掉 wifi 或輪流使用 timer/wifi 都是可運用的方式。再另一方面只要不 overflow/underflow,NMI/FRC1 也只是換行程式罷了。
- 務求有東西備而不用的彈性而不是要用時沒東西用就自縛手腳動彈不得了。
Timer 的啟停
ETS_FRC1_INTR_DISABLE();
TM1_EDGE_INT_DISABLE();
TM1_EDGE_INT_ENABLE();
ETS_FRC1_INTR_ENABLE();
- 這兩行程式碼,個別都可讓 timer 啟停。但皆無法讓 counter 停止計數。至於二者有何差異就不得而知了。
- 還有一個 bit,FRC1_ENABLE_TIMER,可讓 counter 暫停計數,一樣可達 timer 啟停的效果,如下行程式碼停止計數。
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (/*FRC1_ENABLE_TIMER |*/ FRC1_AUTO_LOAD | DIVDED_BY_16 | TM_EDGE_INT));
- 而我們可以讀取查看 counter register,Serial.println((unsigned)RTC_REG_READ(FRC1_COUNT_ADDRESS));,是否停止。
- 最後,我們將這幾個函式重新整理如下:
- 有沒有頓時覺得使用 hwtimer1 是如此地簡單了!
#define FREQ_USED 100
bool SetHwTimer1(void (*main_isr)()){
if (!((RTC_REG_READ(FRC1_CTRL_ADDRESS))&(FRC1_ENABLE_TIMER))){ // counter stopped counting.
ETS_FRC_TIMER1_INTR_ATTACH(main_isr, NULL); // FRC1
/// ETS_FRC_TIMER1_NMI_INTR_ATTACH(main_isr); // NMI
RTC_REG_WRITE((FRC1_LOAD_ADDRESS), FREQ_USED);
TM1_EDGE_INT_ENABLE();
ETS_FRC1_INTR_ENABLE();
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (FRC1_ENABLE_TIMER | FRC1_AUTO_LOAD | DIVDED_BY_16 | TM_EDGE_INT));
return true;
}
return false;
}
void StopHwTimer1(){ // whole stopped.
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (RTC_REG_READ(FRC1_CTRL_ADDRESS))&(~(FRC1_ENABLE_TIMER)));
///// RTC_REG_WRITE((FRC1_LOAD_ADDRESS), -1);
ETS_FRC1_INTR_DISABLE();
TM1_EDGE_INT_DISABLE();
};
void PauseHwTimer1(){ // pause the timer counting.
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (RTC_REG_READ(FRC1_CTRL_ADDRESS))&(~(FRC1_ENABLE_TIMER)));
};
void ResumeHwTimer1(){
TM1_EDGE_INT_ENABLE();
ETS_FRC1_INTR_ENABLE();
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (RTC_REG_READ(FRC1_CTRL_ADDRESS))|(FRC1_ENABLE_TIMER));
};
// hwtimer1 完整範例.
// -------------------- the following snippet is for esp8266 hardware timer1; do not use while pwm is used too.
// FREQ_USED should be defined. use SetHwTimer1(my_isr) to start timer.
// note, since 1us=5x0.2us; 1us has 5 counts, for div16 @80MHz(fixed, irrelevant to cpu speed).
//#define FREQ_USED (25) // the counts 25 corresponds to 5us timer trigger, so the freq is 1/10us equals 100KHz.
#define FREQ_USED (25*100*1000) // we set it to 1 second. so will see the LED blinking in 1 second period.
#include<ESP8266WiFi.h>
bool Timeout(unsigned ms, unsigned &store_start_time_us, bool one_shot=false, bool one_shot_lasting=false){
if (one_shot && !store_start_time_us) return one_shot_lasting;
if (int(micros())-int(ms*1000+store_start_time_us)>=0){
store_start_time_us=one_shot? 0: micros();
return true;
}
return false;
}
#define FRC1_ENABLE_TIMER (BIT7)
#define FRC1_AUTO_LOAD (BIT6)
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;
bool SetHwTimer1(void (*main_isr)()){
if (!((RTC_REG_READ(FRC1_CTRL_ADDRESS))&(FRC1_ENABLE_TIMER))){ // counter stopped counting.
ETS_FRC_TIMER1_INTR_ATTACH(main_isr, NULL); // FRC1
/// ETS_FRC_TIMER1_NMI_INTR_ATTACH(main_isr); // NMI
RTC_REG_WRITE((FRC1_LOAD_ADDRESS), FREQ_USED);
TM1_EDGE_INT_ENABLE();
ETS_FRC1_INTR_ENABLE();
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (FRC1_ENABLE_TIMER | FRC1_AUTO_LOAD | DIVDED_BY_16 | TM_EDGE_INT));
// note, remove FRC1_AUTO_LOAD if needs manual reload.
return true;
}
return false;
}
void StopHwTimer1(){ // whole stopped.
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (RTC_REG_READ(FRC1_CTRL_ADDRESS))&(~(FRC1_ENABLE_TIMER)));
///// RTC_REG_WRITE((FRC1_LOAD_ADDRESS), -1);
ETS_FRC1_INTR_DISABLE();
TM1_EDGE_INT_DISABLE();
}
void PauseHwTimer1(){ // pause the timer counting.
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (RTC_REG_READ(FRC1_CTRL_ADDRESS))&(~(FRC1_ENABLE_TIMER)));
}
void ResumeHwTimer1(){
TM1_EDGE_INT_ENABLE();
ETS_FRC1_INTR_ENABLE();
RTC_REG_WRITE((FRC1_CTRL_ADDRESS), (RTC_REG_READ(FRC1_CTRL_ADDRESS))|(FRC1_ENABLE_TIMER));
}
IRAM_ATTR void wrap_isr(){ // manual reload timer counter
extern void my_isr();
my_isr();
RTC_REG_WRITE((FRC1_LOAD_ADDRESS), FREQ_USED);
}
IRAM_ATTR void my_isr(){
// -------------- user code here --------------------------------------
static unsigned x=0;
digitalWrite(2, x^=1);
}
void setup() {
// put your setup code here, to run once:
pinMode(2, OUTPUT);
SetHwTimer1(my_isr);
// now the hwtimer1 is running.
delay(20000); // we inspect it for 20 seconds.
}
void loop() {
// put your main code here, to run repeatedly:
// performing stress test.
static void (*foos[])()={StopHwTimer1, PauseHwTimer1, ResumeHwTimer1};
static unsigned tmo=micros();
if (Timeout(5000, tmo)) foos[random(0, 3)](); // every 5 seconds to start/stop the timer.
}