ESP8266 中斷與計時器
在單晶片的功能當中,中斷(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 的前提下就是會亮。如此在實驗中若會有存疑的現象便是此原因請自行細究。
進階補充-關於硬體計時器 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);
}
結論
- 筆者也從源碼查覺到停止計數器的用法如下:
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 則不用怕。