作者: ken

ESP8266 HW Timer1 硬體計時器之規劃與改寫-MultiTimers

No Comments

硬體計時器 hw_timer1,我們若不用它似乎對不起單晶片,在運用上也會綁手綁腳的甚至於無法正確實現我們要的功能。因此筆者就來改寫 hw_timer.c,之後,也再創建 PWM 於此計時函式的基礎上。當然,任何不是自己寫的函式庫用上 hw timer1 則會造成衝突,也就只能嘗試改寫第三方用到 timer1 的程式內容成使用此處的函式了。

目的

計時器只有一個,如何來讓多個不同功能取向的程式共用。例如三相馬達,必須三個不同相位的 PWM 來驅動;擴展 GPIO 必須輪詢每一個用上的 IO 通道;LCD display 也有所謂 refresh rate 來保持畫面或更新畫面,其他軟體定時器函式庫根源也是硬體計時器 timer1/沒查證/八九不離十,例如 Ticker。官方 non-os-sdk 其實已配置好 timer1,但應是只做半套,即,只將 timer1 用在 pwm 上面並稱它為虛擬的硬體 pwm,即,ESP8266 並無硬體 pwm 模組,但用硬體 timer1 來專用在軟體 pwm 上。並且此軟體 pwm 本身就已實作多個 pwm 共享一個 timer 的管理了。其實若當初有完善規劃現在也不會有 timer1 的相關使用疑問或衝突了。
故此處的目的,就是最大化共用 hw timer1。
程式碼列於下。

原則上用上較少的計時器及較大的週期會較準確。並且 milisecond 等級的會較準確;而 microsecond 的仍是稍準確。事實上,單單就是看前述原則。
而若用上較多的計時器,則當中最大週期的計時器誤差會較大,原因就是累積性誤差。
其次每一顆計時器也不會有固定誤差,這也是此實作品的先天限制。
簡單講,用於實務上並且於程式內有參數可調整誤差補償,則此多重計時器仍是相當實用準確的。
程式中的測試範例用上 9 個計時器,最多可設定 15 個,若改一下參數則可用上 2 的 9 次方 -1 顆計時器,只要記憶體夠用,當然這只是理論上及程式支援上,筆者未親試。而設計上,參數改為最多 6 顆才是符合最佳設計。
以本例而言,
[1310] [1320] [1330] [1340] [1350] [2360] [2370] [3380] [5390]
[1318] [1328] [1340] [1348] [1358] [2379] [2391] [3408] [5436]
誤差:
8us, 8us, 10us, 8us, 8us, 19us, 21us, 28us, 46us
相信使用者就知道該怎麼做了:用上較少(必要數量)的計時器;並於主系統中能共用同一顆計時器就共用之,即,將計時器的誤差分攤到於主程式中,則時間準確度將最大化。

#ifndef _c_HW_TIMER_
#define _c_HW_TIMER_


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


// multiple timers (max 6) share a single HW Timer.
// timers can be synced at the same time(but not suggest, rather, do in client) and can be offset(1.67s max).
// B syncs to A which means B's isr is going to issued once after B's sync call, at the same time by A's isr issuing(both timing still unaffect).
// B's offset sync call which means the moment A's isr issued, and then offset-value of time past would issue B's isr.
// so, once B's sync called, B's isr would not according to B's timing, once isr issued will back to normal timing(reloaded).
// since the timeout-reload and isr servicing have the payload around 4us in average(div-16 used).
// the lib constraints user to only between 30us(33.3KHz) to 1.67s(0.6Hz) a safe range, with 1us resolution.
// the timers timeout under a span of time of PAYLOAD_US(4us) will be served as well at a call of main isr.
// here is the scenario, suppose there is B timer within 4us to be served after A having served at this call,
// B will be served as well at this call with a delay. if C got to be served within 4us after B served,
// delay and serve C as well and so on. So there are cases stay at a main ISR call too long, but not too bad.
// so, the minimal period as a constraint for every timer and the max 6 timers could ease such situations.
// such a strategy keeps timing accuracy and minimally affect to entire main system.
// for efficiency, the hw counter reg is 23-bit, we have 9, say, 3 bits as for an identifier tailing to the counter value.


#define C_HW_TIMER_DEBUG    0
#define SYNC_WAITING        0 // if enabled, sync could avoid mem-fault however, timings could affected at a syncing
#define TIMER_NUM           9 // up to number of timers simul used(recommended max 6, allowable 15)
#define ID_MASK_VAL         0x0F // to extract id from counter value. note that id is from 1 to TIMER_NUM.(recommended 0x07)
#define ID_MASK_BIT         4 // number of bits to extract id from counter value(recommended 3)
#define PAYLOAD_US          4 // payload in us to substract(recommended 4us)
#define LO_BOUND_US         30
#define UP_BOUND_US         1670000

#define PAYLOAD_CNT         ((PAYLOAD_US)*5) // payload in counts to substract, since 1us=5x0.2us; 1us has 5 counts
#define LO_BOUND_CNT        ((LO_BOUND_US)*5)
#define UP_BOUND_CNT        ((UP_BOUND_US)*5)
#define OP_GET_CNT(x)       ((x)>>ID_MASK_BIT)
#define OP_GET_ID(x)        ((x)&ID_MASK_VAL)
#define OP_MERGE_CNTID(cnt, id) (((cnt)<<ID_MASK_BIT)|(id))
#define OP_HEAP_TOP_CNT     OP_GET_CNT(timer_obj.current[1])
#define OP_HEAP_TOP_ID      OP_GET_ID(timer_obj.current[1])
#define INTRUDE_TIME_CNT    100 // the counts(20us*5) safely/enough-time for access data
#define CNT_TO_US(x)        (((x)+2)/5)
#define US_TO_CNT(x)        ((x)*5)


#if C_HW_TIMER_DEBUG
    ICACHE_RAM_ATTR int hw_timer_overflow, hw_timer_stay_time_us1, hw_timer_stay_time_us;
#endif

#if SYNC_WAITING
    /*ICACHE_RAM_ATTR */bool sync_spin=false; // do not know the attrs affection
#endif


class cHwTimer{
    typedef void (*void_func1)();
    typedef struct{
        unsigned reload[TIMER_NUM+1];           // num of counts for each timer to be reloaded(payload is evaled).
                                                // since we support one-shot, it's better as to use this array
                                                // and the reload[0] to be always 0 for the procedure reloading 0,
                                                // which automatically stop.
                                                // simply to say, set reload[i] to 0(should use loc to deallocate) or
                                                // the current[i] being reloaded from reload[0] yields current[i] stop counting.
        unsigned current[TIMER_NUM+1];          // current nit counts for each timer to be reloaded(incl. payload is evaled).
                                                // this array is dedicated for using in heap funcs,
                                                // so, the current[0] is the current number of running timers.
                                                // timers occupy current[1] to current[number_of_timers],
                                                // which are current[1] to current[ current[0] ].
                                                // note that this array is entirely maintained by heap funcs called in main isr,
                                                // but there are cases we have to intrude on this array(danger).
        void_func1 isr[TIMER_NUM+1];            // callback functions.
                                                // the up two arrays have distinct purpose of the [0] element,
                                                // so is the isr[0] which used for the current isr going to run.
                                                // Besides, isr[i] equals zero uniquely denotes the timer slot is empty.
    } sHwTimerObj;

        static sHwTimerObj timer_obj;
        static void main_isr();
        static void default_isr();
        static void min_heap_insertion(unsigned key, unsigned *t);
        static unsigned min_heap_removal(unsigned *t);

        char loc;                               // identifies loc in array, please note that the loc starts from 1 to TIMER_NUM.
                                                // loc 0 is reserved for one-shot identification.
                                                // loc -1 identifies for uninitialized value.
        void_func1 func_for_pause;

        void arm(unsigned period_us, void(*isr)(), bool periodically){
            for (unsigned i=1; i<=TIMER_NUM; i++){

                // please especially note that we use isr[i] to identify whether a timer slot is empty.
                // and we use who uses reload[0] as to identify one-shot(the id field is 0 and reload from reload[0] which is 0)
                
                if (!timer_obj.isr[i]){ // search for an empty slot
                    unsigned cnt=US_TO_RTC_TIMER_TICKS(period_us-PAYLOAD_US);
                    timer_obj.isr[i]=isr;
                    func_for_pause=isr;
                    cnt=OP_MERGE_CNTID(cnt, i);
                    if (periodically) timer_obj.reload[i]=cnt;
                    else timer_obj.reload[i]=i; // id without count is used to deallocate
                    loc=i;
                    min_heap_insertion(cnt, timer_obj.current);
                    return;
                }
            }
        };
        
        cHwTimer(){};
        
    public:
        cHwTimer(unsigned period_us, void(*isr)(), bool periodically=true): loc(-1), func_for_pause(default_isr){
            if (!isr || period_us<LO_BOUND_US || period_us>UP_BOUND_US || (timer_obj.current[0]==TIMER_NUM)) return;

            arm(period_us, isr, periodically);

            if (!(RTC_REG_READ(FRC1_CTRL_ADDRESS)&FRC1_ENABLE_TIMER)){ // timer is disabled, regarded as a brand new use.

                timer_obj.isr[0]=default_isr;
                
                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);
                TM1_EDGE_INT_ENABLE();
                ETS_FRC1_INTR_ENABLE();
            }
        };
        
        cHwTimer(const cHwTimer &a){loc=a.loc; func_for_pause=a.func_for_pause;};

        bool Sync(const cHwTimer &ref, unsigned offset_delay_us){
            // this func might fail, if so, affect nothing
            // this func might fault, if so, mem-fault or wdt-reset
            if (loc>0 && ref.loc>0){
                
                // this step can be dangerous or long-hold because main isr may alter this array.
                unsigned i, j;
                j=10; // for at most delay 200us

                do {
                    // there is a threshold time to ensure we have much time to do something.
                    // so we check the minimum time of the upcoming timer, which longer than 20us.
                    // in the following if-condition, we can use flag to prevent main isr from access array and fault,
                    // however main isr spin-waiting could disturb all timer timings, of course could wdt-reset.

                    #if SYNC_WAITING
                        sync_spin=true;
                    #endif

                    if ((i=timer_obj.current[0]) && (OP_HEAP_TOP_CNT>INTRUDE_TIME_CNT)){
                        unsigned m, n, z;
                        while (i){
                            z=OP_GET_ID(timer_obj.current[i]);
                            if (z==ref.loc) m=i;
                            else if (z==loc) n=i;
                            --i;
                        }
                        if ((z=OP_GET_CNT(timer_obj.current[m])+US_TO_CNT(offset_delay_us))<=UP_BOUND_CNT){
                            timer_obj.current[n]=OP_MERGE_CNTID(z+1, loc); // need not count on payload

                            #if SYNC_WAITING
                                sync_spin=false;
                            #endif

                            return true;
                        }

                        #if SYNC_WAITING
                            sync_spin=false;
                        #endif

                    }

                    #if SYNC_WAITING
                        sync_spin=false;
                    #endif

                    delayMicroseconds(20);
                } while (--j);
            }
            return false;
        };

        bool isActive(){return (loc>0) && (timer_obj.reload[loc]>loc);};

        void Pause(){
            
            #if C_HW_TIMER_DEBUG
                printf("\r\n\r\ntimer(%d) paused\r\n\r\n", loc);
            #endif
            
            if (isActive()) timer_obj.isr[loc]=default_isr;
        };

        void Resume(){
            
            #if C_HW_TIMER_DEBUG
                printf("\r\n\r\ntimer(%d) resumed\r\n\r\n", loc);
            #endif
            
            if (isActive()) timer_obj.isr[loc]=func_for_pause;
        };

        void Stop(){

            #if C_HW_TIMER_DEBUG
                printf("\r\n\r\ntimer(%d) stopped\r\n\r\n", loc);
            #endif

            if (loc>0){
                timer_obj.reload[loc]=loc;
                loc=-1;
            }
        };

        ~cHwTimer(){Stop();};

        // the following 3 funcs are user functions for easy access,
        // in addition, if you want to not bind to objects,
        // you can comment off the destructor,
        // such that timers can still run even objects are destructed.
        // such case is suitable for one-shot; non-oneshot hence has to be stopped explicitly.
        static void StopAll(){for (int i=1; i<=TIMER_NUM; i++) timer_obj.reload[i]=i;};
        static void PauseAll(){for (int i=0; i<=TIMER_NUM; i++) timer_obj.isr[i]=default_isr;};
        static void ResumeAll(){/*can not resume all*/};
};


ICACHE_RAM_ATTR cHwTimer::sHwTimerObj cHwTimer::timer_obj={
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
};

IRAM_ATTR void cHwTimer::default_isr(){
    #if C_HW_TIMER_DEBUG
        //printf("\r\n-\r\n");
    #endif
}

IRAM_ATTR void cHwTimer::main_isr(){

    #if C_HW_TIMER_DEBUG
        hw_timer_stay_time_us1=micros();
        static bool gate=false;
        if (gate){
            hw_timer_overflow++;
            gate=false;
        }
        else gate=true;
    #endif

    #if SYNC_WAITING
        while (sync_spin) yield();
    #endif

    unsigned discard_min, discard_min_cnt, i, j, diff;

#if 0 ///// read the old version to know about what new version does ///// old version
        while (timer_obj.current[0] && (OP_HEAP_TOP_CNT<=PAYLOAD_CNT)){//////////////////////////////////
            timer_obj.isr[0]();
            
            ////delayMicroseconds(CNT_TO_US(OP_HEAP_TOP_CNT));
            diff=micros()+CNT_TO_US(OP_HEAP_TOP_CNT);/////
            
            discard_min=min_heap_removal(timer_obj.current); // discard the used min one
            timer_obj.isr[0]=timer_obj.isr[OP_GET_ID(discard_min)]; // update the isr, which will run next time
            discard_min_cnt=OP_GET_CNT(discard_min); // it will join to calculation

            for (i=timer_obj.current[0]; i; i--){ // update all counters
                j=OP_GET_ID(timer_obj.current[i]); // get an id which needs to update
                timer_obj.current[i]=OP_MERGE_CNTID(OP_GET_CNT(timer_obj.current[i])-discard_min_cnt, j);
            }

            // add the new one that just discard
            i=timer_obj.reload[OP_GET_ID(discard_min)];
            if (i>TIMER_NUM) min_heap_insertion(i, timer_obj.current); // i possibly be 0 or id which are for one-aspect tackling
            else timer_obj.isr[OP_GET_ID(discard_min)]=0; // it must be the one-shot or stopped timer which must deallocated

            if (micros()<(diff-2)) delayMicroseconds(diff-micros());/////

            #if C_HW_TIMER_DEBUG
                else hw_timer_overflow++;
            #endif

        }//////////////////////////////////
#else //////////////////////////////////////////////////////////////////// new version
        if (timer_obj.current[0] && (OP_HEAP_TOP_CNT<=PAYLOAD_CNT)){///////////////////////////
            discard_min_cnt=0;
            while (timer_obj.current[0] && (OP_HEAP_TOP_CNT<=(PAYLOAD_CNT+discard_min_cnt))){
                
                timer_obj.isr[0]();
                
                diff=micros()+CNT_TO_US(OP_HEAP_TOP_CNT-discard_min_cnt); // substract past-time equals to amount time need to spend
                
                discard_min=min_heap_removal(timer_obj.current); // discard the used min one
                timer_obj.isr[0]=timer_obj.isr[OP_GET_ID(discard_min)]; // update the isr, which will run next time
                discard_min_cnt=OP_GET_CNT(discard_min); // update the past-time that is going to be(to substract outside this loop)
                j=OP_GET_ID(discard_min);
                
                // add the new one that just discard
                i=timer_obj.reload[j];
                if (i>TIMER_NUM) min_heap_insertion(OP_MERGE_CNTID(OP_GET_CNT(i)+discard_min_cnt, OP_GET_ID(i)), timer_obj.current);
                else timer_obj.isr[j]=0; // it must be the one-shot or stopped timer which must deallocated

                if (micros()<(diff-2)) delayMicroseconds(diff-micros());

                #if C_HW_TIMER_DEBUG
                    else hw_timer_overflow++;
                #endif

            }
            for (i=timer_obj.current[0]; i; i--){ // update all counters
                j=OP_GET_ID(timer_obj.current[i]); // get an id which needs to update
                timer_obj.current[i]=OP_MERGE_CNTID(OP_GET_CNT(timer_obj.current[i])-discard_min_cnt, j);
            }
        }//////////////////////////////
#endif ////////////////////////////////////////////////////////////////////

    if (timer_obj.current[0]){

        RTC_REG_WRITE(FRC1_LOAD_ADDRESS, OP_HEAP_TOP_CNT); // load the current min asap
        timer_obj.isr[0](); // service the last time of isr asap, which means needs to update var this time

        discard_min=min_heap_removal(timer_obj.current); // discard the used min one
        timer_obj.isr[0]=timer_obj.isr[OP_GET_ID(discard_min)]; // update the isr, which will run next time
        discard_min_cnt=OP_GET_CNT(discard_min); // it will join to calculation
        
        for (i=timer_obj.current[0]; i; i--){ // update all counters
            j=OP_GET_ID(timer_obj.current[i]); // get an id which needs to update
            timer_obj.current[i]=OP_MERGE_CNTID(OP_GET_CNT(timer_obj.current[i])-discard_min_cnt, j);
        }

        // add the new one that just discard
        i=timer_obj.reload[OP_GET_ID(discard_min)];
        if (i>TIMER_NUM) min_heap_insertion(i, timer_obj.current); // i possibly be 0 or id which are for one-aspect tackling
        else timer_obj.isr[OP_GET_ID(discard_min)]=0; // it must be the one-shot or stopped timer which must deallocated
    }
    else { // if here to do, no timer remains, the last one timer which belongs to this isr has been deallocated properly
        timer_obj.isr[0]();
        timer_obj.isr[0]=default_isr; // since hw timer still counting and intring

        RTC_REG_WRITE(FRC1_CTRL_ADDRESS, RTC_REG_READ(FRC1_CTRL_ADDRESS)&~(FRC1_ENABLE_TIMER));
        TM1_EDGE_INT_DISABLE();
    }

    #if C_HW_TIMER_DEBUG
        gate=false;
        hw_timer_stay_time_us=micros()-hw_timer_stay_time_us1;
    #endif

}

IRAM_ATTR void cHwTimer::min_heap_insertion(unsigned key, unsigned *t){ // t[0] must be the current size
    unsigned i=++t[0], j=(i>>1);
    for (; j && t[j]>key; t[i]=t[j], i=j, j>>=1);
    t[i]=key;

    // should use OP_GET_CNT(t[]) however, thinking about same count with diff id, A(111-001), B(111-000),
    // A is larger than B(or say B is smaller than A), no matter which one larger,
    // we ignore this case which still holds heap property since A==B in this fact. (in another words,)
    // (either A or B is larger, will be maintained, however A==B need not maintained by heap even it's maintained.)
    // and the other case A(111-uvw), B(110-xyz), no matter uvwxyz are, all lead to A>B which holds heap property.
    // therefore we needn't use OP_GET_CNT(t[]) within this func.
};

IRAM_ATTR unsigned cHwTimer::min_heap_removal(unsigned *t){ // t[0] must be the current size
    ///if (!t[0]) return -1;
    unsigned key=t[1];
    unsigned j=2, i=1, k=t[0]--;
    for (; j<k; i=j, j<<=1){
        if (t[j]>t[j+1]) ++j;
        t[i]=t[j];
    }
    
    for (j=(i>>1); j && t[j]>t[k]; t[i]=t[j], i=j, j>>=1);
    t[i]=t[k];
    
    return key;
};


#endif // _c_HW_TIMER_





/////////////////#include<ESP8266WiFi.h>system_update_cpu_freq(160);
unsigned x=0;
unsigned ay1, ay2, ay3, ay4, ay5, ay6, ay7, ay8, ay9;

void IRAM_ATTR user1(void)
{
    static unsigned z=micros();
    ay1=micros()-z;
    z=micros();
    x^=0x01;
}
void IRAM_ATTR user2(void)
{
    static unsigned z=micros();
    ay2=micros()-z;
    z=micros();
    x^=0x02;
}
void IRAM_ATTR user3(void)
{
    static unsigned z=micros();
    ay3=micros()-z;
    z=micros();
    x^=0x04;
}
void IRAM_ATTR user4(void)
{
    static unsigned z=micros();
    ay4=micros()-z;
    z=micros();
    x^=0x08;
}
void IRAM_ATTR user5(void)
{
    static unsigned z=micros();
    ay5=micros()-z;
    z=micros();
    x^=0x10;
}
void IRAM_ATTR user6(void)
{
    static unsigned z=micros();
    ay6=micros()-z;
    z=micros();
    x^=0x20;
}
void IRAM_ATTR user7(void)
{
    static unsigned z=micros();
    ay7=micros()-z;
    z=micros();
    x^=0x40;
}
void IRAM_ATTR user8(void)
{
    static unsigned z=micros();
    ay8=micros()-z;
    z=micros();
    x^=0x80;
}
void IRAM_ATTR user9(void)
{
    static unsigned z=micros();
    ay9=micros()-z;
    z=micros();
    x=0xFF;
}
void IRAM_ATTR user10(void)
{
    printf("\r\n\r\none-shot\r\n\r\n");
}


cHwTimer test[9]={
    cHwTimer(1310, user1),
    cHwTimer(1320, user2),
    cHwTimer(1330, user3),
    cHwTimer(1340, user4),
    cHwTimer(1350, user5),
    cHwTimer(2360, user6),
    cHwTimer(2370, user7),
    cHwTimer(3380, user8),
    cHwTimer(5390, user9)
};


void setup() {
    Serial.begin(115200);
}

void loop() {

    if (random(7)==1) test[random(9)].Stop(); // random(9) is 0 to 8
    else if (random(13)==3) test[random(9)].Pause();
    else if (random(5)==3) test[random(9)].Resume();
    else if (random(2)==1) cHwTimer(1660000, user10, false);

    //printf("\r\n\r\n<<%d>>\r\n\r\n", test[random(9)].Sync(test[0], 0));

    delay(1000);


#if C_HW_TIMER_DEBUG
    printf("[% 4u] [% 4u] [% 4u] [% 4u] [% 4u] [% 4u] [% 4u] [% 4u] [% 4u] overflow=%d, stay=%d, %X\r\n",\
        ay1, ay2, ay3, ay4, ay5, ay6, ay7, ay8, ay9, hw_timer_overflow, hw_timer_stay_time_us, x);
#else
    printf("[% 4u] [% 4u] [% 4u] [% 4u] [% 4u] [% 4u] [% 4u] [% 4u] [% 4u] overflow=%d, stay=%d, %X\r\n",\
        ay1, ay2, ay3, ay4, ay5, ay6, ay7, ay8, ay9, 0, 0, x);
#endif
}

/* output
13:00:00.310 -> [   0] [   0] [   0] [   0] [   0] [   0] [   0] [   0] [   0] overflow=0, stay=0, 0
13:00:01.303 -> [1315] [1327] [1337] [1347] [1357] [2381] [2390] [3409] [5439] overflow=0, stay=0, 40
13:00:02.329 -> [1319] [1329] [1337] [1347] [1357] [2379] [2387] [3409] [5446] overflow=0, stay=0, EB
13:00:03.322 -> [1320] [1334] [1335] [1350] [1362] [2382] [2394] [3415] [5442] overflow=0, stay=0, FD
13:00:04.316 -> [1319] [1330] [1339] [1350] [1361] [2384] [2392] [3421] [5446] overflow=0, stay=0, EE
13:00:05.342 -> [1321] [1329] [1341] [1352] [1359] [2373] [2396] [3411] [5439] overflow=0, stay=0, 20
13:00:06.336 -> [1320] [1328] [1338] [1348] [1358] [2385] [2393] [3416] [5448] overflow=0, stay=0, 9F
13:00:07.362 -> [1315] [1327] [1340] [1349] [1360] [2379] [2390] [3410] [5439] overflow=0, stay=0, 27
13:00:08.355 -> [1320] [1332] [1340] [1352] [1360] [2380] [2388] [3408] [5442] overflow=0, stay=0, 74
13:00:09.382 -> [1319] [1325] [1340] [1349] [1359] [2379] [2391] [3411] [5443] overflow=0, stay=0, 66
13:00:10.375 -> [1319] [1329] [1340] [1349] [1357] [2388] [2387] [3409] [5454] overflow=0, stay=0, DF
13:00:11.401 -> [1320] [1331] [1340] [1350] [1360] [2383] [2393] [3413] [5443] overflow=0, stay=0, CF
13:00:12.395 -> [1317] [1329] [1340] [1347] [1366] [2379] [2394] [3414] [5442] overflow=0, stay=0, 2F
13:00:13.388 -> [1318] [1328] [1340] [1348] [1358] [2379] [2391] [3408] [5436] overflow=0, stay=0, 64
13:00:14.414 -> [1315] [1330] [1340] [1343] [1353] [2379] [2385] [3405] [5429] overflow=0, stay=0, 3E
*/

另外下面再附上 15 個計時器的主程式,方便使用者評估與測試。
其中由亂數取時可看出,原則上使用 milisecond 等級的計時器,且只要不同餘(使得計時器們擠在短時間內都需要中斷處理),都可降低 main-isr 內 overflow。當然,避免不了的誤差就是 main-isr 內的運算耗時了。因此在此例中,15ms 的計時器,最大誤差來到約 0.35ms,誤差率 2.3%,若以平均來看甚至於更好,如附圖,表示 milisecond-timer 還是有相當的精確度的,並且可同時用上十五個計時器。此任務算順利完成了。

comment off the destructor is required///////////////////////////////////////

unsigned x=0;
unsigned ay1, ay2, ay3, ay4, ay5, ay6, ay7, ay8, ay9, ay10, ay11, ay12, ay13, ay14, ay15;

void IRAM_ATTR user1(void)
{
    static unsigned z=micros();
    ay1=micros()-z;
    z=micros();
    x^=0x01;
}
void IRAM_ATTR user2(void)
{
    static unsigned z=micros();
    ay2=micros()-z;
    z=micros();
    x^=0x02;
}
void IRAM_ATTR user3(void)
{
    static unsigned z=micros();
    ay3=micros()-z;
    z=micros();
    x^=0x04;
}
void IRAM_ATTR user4(void)
{
    static unsigned z=micros();
    ay4=micros()-z;
    z=micros();
    x^=0x08;
}
void IRAM_ATTR user5(void)
{
    static unsigned z=micros();
    ay5=micros()-z;
    z=micros();
    x^=0x10;
}
void IRAM_ATTR user6(void)
{
    static unsigned z=micros();
    ay6=micros()-z;
    z=micros();
    x^=0x20;
}
void IRAM_ATTR user7(void)
{
    static unsigned z=micros();
    ay7=micros()-z;
    z=micros();
    x^=0x40;
}
void IRAM_ATTR user8(void)
{
    static unsigned z=micros();
    ay8=micros()-z;
    z=micros();
    x^=0x80;
}
void IRAM_ATTR user9(void)
{
    static unsigned z=micros();
    ay9=micros()-z;
    z=micros();
    x^=0x100;
}
void IRAM_ATTR user10(void)
{
    static unsigned z=micros();
    ay10=micros()-z;
    z=micros();
    x^=0x200;
}
void IRAM_ATTR user11(void)
{
    static unsigned z=micros();
    ay11=micros()-z;
    z=micros();
    x^=0x400;
}
void IRAM_ATTR user12(void)
{
    static unsigned z=micros();
    ay12=micros()-z;
    z=micros();
    x^=0x800;
}
void IRAM_ATTR user13(void)
{
    static unsigned z=micros();
    ay13=micros()-z;
    z=micros();
    x^=0x1000;
}
void IRAM_ATTR user14(void)
{
    static unsigned z=micros();
    ay14=micros()-z;
    z=micros();
    x^=0x2000;
}
void IRAM_ATTR user15(void)
{
    static unsigned z=micros();
    ay15=micros()-z;
    z=micros();
    x^=0x4000;
    //printf("\r\n\r\none-shot\r\n\r\n");
}


void (*test[15])()={
    user1, user2, user3, user4, user5, user6, user7, user8, user9, user10, user11, user12, user13, user14, user15
};


void setup() {
    Serial.begin(115200);
}

void loop() {

    static unsigned times[15];
    
    if (random(13)==11){
        // change all timers' timing to all non-oneshot
        printf("\r\n-=change=-\r\n");
        cHwTimer::StopAll();
        delay(1700);
        for (int i=0; i<15; i++){
            times[i]=random(15000-30)+30; // 15ms at most
            cHwTimer(times[i], test[i]);
        }
    }

#if C_HW_TIMER_DEBUG
    printf("\r\n[overflow=%d, stay=%d, isr_run=%X]\r\n", hw_timer_overflow, hw_timer_stay_time_us, x);
#endif

    printf("[% 5u][% 5u][% 5u][% 5u][% 5u][% 5u][% 5u][% 5u][% 5u][% 5u][% 5u][% 5u][% 5u][% 5u][% 5u]\r\n",\
        times[0], times[1], times[2], times[3], times[4], times[5], times[6], times[7],\
        times[8], times[9], times[10], times[11], times[12], times[13], times[14]);

    printf("[% 5d][% 5d][% 5d][% 5d][% 5d][% 5d][% 5d][% 5d][% 5d][% 5d][% 5d][% 5d][% 5d][% 5d][% 5d]\r\n",\
        ay1-times[0], ay2-times[1], ay3-times[2], ay4-times[3], ay5-times[4], ay6-times[5], ay7-times[6], ay8-times[7],\
        ay9-times[8], ay10-times[9], ay11-times[10], ay12-times[11], ay13-times[12], ay14-times[13], ay15-times[14]);

    // the % cal is diff from the upper two line is because ayi are altered by isri instantly.
    printf("[% 3.1f%%][% 3.1f%%][% 3.1f%%][% 3.1f%%][% 3.1f%%][% 3.1f%%][% 3.1f%%]"\
    "[% 3.1f%%][% 3.1f%%][% 3.1f%%][% 3.1f%%][% 3.1f%%][% 3.1f%%][% 3.1f%%][% 3.1f%%]\r\n",\
        (ay1-times[0])/(float)(times[0])*100.0f, (ay2-times[1])/float(times[1])*100.0f, (ay3-times[2])/float(times[2])*100.0f,\
        (ay4-times[3])/float(times[3])*100.0f, (ay5-times[4])/float(times[4])*100.0f, (ay6-times[5])/float(times[5])*100.0f,\
        (ay7-times[6])/float(times[6])*100.0f, (ay8-times[7])/float(times[7])*100.0f, (ay9-times[8])/float(times[8])*100.0f,\
        (ay10-times[9])/float(times[9])*100.0f, (ay11-times[10])/float(times[10]*100.0f), (ay12-times[11])/float(times[11])*100.0f,\
        (ay13-times[12])/float(times[12])*100.0f, (ay14-times[13])/float(times[13])*100.0f, (ay15-times[14])/float(times[14])*100.0f);

    delay(1500);
}

結論

-> [overflow=0, stay=22, isr_run=5FC4]
-> [ 50][ 92][ 32][ 75][ 66][ 79][ 73][ 33][ 80][ 87][ 41][ 80][ 56][ 76][ 78]
-> [ 93][ 221][ 95][ 163][ 119][ 176][ 192][ 63][ 180][ 175][ 88][ 180][ 112][ 141][ 161]
->
-> [overflow=0, stay=22, isr_run=7D7A]
-> [ 50][ 92][ 32][ 75][ 66][ 79][ 73][ 33][ 80][ 87][ 41][ 80][ 56][ 76][ 78]
-> [ 139][ 177][ 86][ 166][ 161][ 174][ 166][ 30][ 160][ 199][ 99][ 182][ 77][ 153][ 162]
讓我們接著來看上面這個實驗結果。
用上了 15 顆計時器,並且,週期全都在 30us 至 100us 之間,這表示平均每 4.7us 便有一顆計時器需要服務,誤差竟來到了三倍XD。而 overflow 計數有 main-isr 的函式內及函式外,但已將函式內的計數部份關掉。
因此可看出,(此函式庫是使用 FRC1,並且有無 auto-reload 都一樣),一旦 counter reg 時間到中斷觸發,中斷旗標會舉起來,但 isr 並不會被岔斷,會等到離開 isr 後,檢查出又有發生了中斷,便再次進入 isr。這表明 ESP8266 將不斷地跑 isr,所有 CPU 時間全被 isr 佔用。但事實是 loop() 仍能打印,故可以肯定,在每一次作 isr 的服務之前,必定會跑一次 loop()(並且應會有 timeout 機制或掌控權轉移機制)。因此如此的行為全都是 ESP8266 boot-loader 在掌控著,亦即又表明兩件事,bootloader 是必要的,等同一個小型,standalone 的 os,及選用好的 bootloader 對於單晶片的效能有著關鍵性的影響。
其次,筆者曾試著超頻到 160MHz 圖降低 main-isr 的運算的影響,但似乎都一樣。因留待有主程式系統後再調試為佳,故此舉並不能作為改善計時準確度的主要考量。
故結論還是回到那句老話,軟體計時器的使用原則。
最後,例如需要 500us 的計時器 5 顆,作法之一我們可以想定,若時間精準度要 99%,則誤差是 500us x 0.01 得 5us,顯然不可能。在先天限制下,500us / 30us 得 16.7。那就用上此函式庫所提供的最小的 30us 的時基,在 sub-isr 內查看歴時及計數累加,到 500us – 30us 前後時,判斷是要停等觸發或下次觸發,如此以求更高的精確度。各位認為這樣可行嗎?不用跑實驗結論還是不可行,在要求精確度的前提下,一來,要停等觸發,必拖累所有 timers,別忘了最多會等上 30us 以上。要下次觸發,必又亂飄了。因此,筆者又有想法了,便是追加一顆必然的最小計時器。只要有使用者的計時器開始跑了,這顆最小計時器便會開始跑。並用此計時器來補償時間的誤差。該怎麼做來達到所有使用者計時器都有相當的準確度,就等待筆者下篇文章(如果有的話XD)。當前筆者也沒想法XD。
不過好消息是,如下用上五顆 475us~500us 之間的計時器,誤差是 3%,所以本文這個函式庫還是令筆者放心的。

11:39:05.136 -> [overflow=1400650, stay=15, isr_run=1A]
11:39:05.136 -> [ 481][ 495][ 481][ 492][ 491]
11:39:05.169 -> [ 14][ 15][ 14][ 14][ 14]
11:39:06.659 ->
11:39:06.659 -> [overflow=1404060, stay=15, isr_run=1D]
11:39:06.659 -> [ 481][ 495][ 481][ 492][ 491]
11:39:06.692 -> [ 14][ 14][ 13][ 13][ 14]
11:39:08.182 ->
11:39:08.182 -> [overflow=1407465, stay=7, isr_run=7]
11:39:08.182 -> [ 481][ 495][ 481][ 492][ 491]
11:39:08.215 -> [ 14][ 13][ 14][ 14][ 14]
11:39:09.704 ->
11:39:09.704 -> [overflow=1410869, stay=7, isr_run=1F]
11:39:09.704 -> [ 481][ 495][ 481][ 492][ 491]
11:39:09.737 -> [ 17][ 14][ 17][ 14][ 14]

補充

  • 後來看到有玩家也寫了這樣的函式庫,故附了上來以參考改進此處不足之處。
  • https://forum.arduino.cc/t/esp8266-timerinterrupt-library/637769
  • https://github.com/khoih-prog/ESP8266TimerInterrupt

20220316 補充

  • hardware timer 的改版如後面文章。
  • 切記,若使用上 print,經筆者辛苦地除錯及實測,將會使 hardware timer 暫停 200ms。
  • 也有會其他的東東影響 hardware timer 的,筆者未知,猜測有可能是連續的不同的 gpio 中斷且皆尚未完成所致,特例的實測下影響約在 2ms 左右。當然此情況也有可能是如同 print 一樣是某函式庫於某段 critical code 中關開 timer int 所致。
  • 因此我們使用 timer 處理程式時須謹防此類例外;除了後面文章中提到的 timer int + gpio int 的做法可利用(但非解法)外似乎只能 case by case 地處置了,而且很有可能解決不了;即,一來要用計時中斷去處理某些運算的需求,一來又要避免(不了)所製造出此需求者同時去干擾中斷。
  • 別處的 delay() 不會影響 timer int。delayMicroseconds() 則會
PHP Code Snippets Powered By : XYZScripts.com