ESP8266 HW Timer1-MultiTimers Lib ver.0.3

No Comments

應該是最後一版了。關於 fine-tune 的部份應該就是在整個主系統下做。

附上測試碼,是派生 10 個 400us 的 timers,並且相鄰的 timer 同步間隔是 40us,而測得誤差是 5%,也就是說相鄰 timer 應該是相距約 42us,這也表示了非同步過取樣率(async-oversampling)提昇至 50us/20kHz,是有機會實現的。並且還剩餘 5 顆 timers 未用而可利用。

本文將就本例作些展示,以示本函式庫已趨於成熟穩定。並點到為止地揭示可行的運用。
後面的文章將有一篇先前展示過的範例的整理集合。

按,1. 當下筆者就想到可能改版的原因了XD,以目前測果觀之,其實若 timer 不濫用,若以不暴增某時 timer 該處理 isr 的數量為原則的話(即,降低同餘數,如本例同餘數就是零,而當然此可能性很低,因我們不可能以此原則而來設定多少 timer 週期,將是本末倒置),其任一次的 main-isr 的時間誤差都是接近同一定值的,若此,這將確保任一時刻 waveform 的精確度。那麼,hw timer 解析度 0.2us,我是否該將 1us 最低限制改為 0.2us 以爭取更好的精確度?
2. instant one-shot,例如 PWM 蠻適合用以實作的。
3. the char loc is not reliable, instead must use signed char or int。
4. by 3., because IRAM_ATTR data can not in-bitfield use, the char loc should change to int only, or causes fatal error. it will updated in version 0.4 but not yet release because no other modifications.
5. micros() will wrap around and possibly cause wrong if and were used. fortunately could be fixed.

Test Code 10 timers of 400us with offset 40us of each

// test 1
#include"Esp8266HwSwTimers.h"

unsigned x=0, u=0, v=0, w=0;
unsigned ay1;
unsigned times[1]={400};
float a1;


void IRAM_ATTR user1(void)
{
  x=micros();
  digitalWrite(D2, u^=1);
  //delayMicroseconds(1);
}
void IRAM_ATTR user2(void)
{
  x=micros();
  digitalWrite(D5, v^=1);
  //delayMicroseconds(1);
}
void IRAM_ATTR user3(void)
{
  x=micros();
  digitalWrite(D6, w^=1);
  delayMicroseconds(1);
}


bool Timeout_4s(){
    static int current_time = millis();
    int new_time = millis();
    if (new_time < (current_time + 4000)) return false;
    current_time = new_time;
    return true;
}

cHwTimer t[10]={
    cHwTimer(400, user1),
    cHwTimer(400, user1),
    cHwTimer(400, user1),
    cHwTimer(400, user1),
    cHwTimer(400, user1),
    cHwTimer(400, user1),
    cHwTimer(400, user1),
    cHwTimer(400, user1),
    cHwTimer(400, user1),
    cHwTimer(400, user1)
};

void setup() {
  Serial.begin(115200);
  delay(4000);
  printf("\r\nstarting evaluations...\r\n");

    pinMode(D2, OUTPUT);
    pinMode(D5, OUTPUT);
    pinMode(D6, OUTPUT);
    delay(1000);

    t[1].ForceHaltForSync(t[0], 40);
    t[2].ForceHaltForSync(t[1], 40);
    t[3].ForceHaltForSync(t[2], 40);
    t[4].ForceHaltForSync(t[3], 40);
    t[5].ForceHaltForSync(t[4], 40);
    t[6].ForceHaltForSync(t[5], 40);
    t[7].ForceHaltForSync(t[6], 40);
    t[8].ForceHaltForSync(t[7], 40);
    t[9].ForceHaltForSync(t[8], 40);
}


int transit=0, idx=0;

void pr(float c){
    printf("[% 3.4f%%]", c*100.f);
    transit=0;
}


void loop() {

    if (Timeout_4s()){
        if (!transit){t[idx++].Resume(); transit=1;}
        if (transit==1){
            if (idx>9) idx=0;
            if (!t[idx].ProbeAccuracy(1, 3, pr)) return; // certainly failed after 70 minutes, i do not know why.
            printf("\r\nTimer%d: ", idx+1);
            transit=2;
        }
    }

    delay(200);
}

/* output
19:30:16.308 -> Timer1: [ 4.9029%]
19:30:20.319 -> Timer2: [ 4.9019%]
19:30:24.297 -> Timer3: [ 4.9342%]
19:30:28.304 -> Timer4: [ 4.9232%]
19:30:32.311 -> Timer5: [ 4.9361%]
19:30:36.320 -> Timer6: [ 4.9015%]
19:30:40.326 -> Timer7: [ 4.8726%]
19:30:44.332 -> Timer8: [ 4.8873%]
19:30:48.339 -> Timer9: [ 4.9112%]
19:30:52.316 -> Timer10: [ 4.8956%]
19:30:56.324 -> Timer1: [ 4.9134%]
19:31:00.332 -> Timer2: [ 4.8952%]
19:31:04.339 -> Timer3: [ 4.9241%]
19:31:08.346 -> Timer4: [ 4.9380%]
19:31:12.354 -> Timer5: [ 4.8739%]
19:31:16.328 -> Timer6: [ 4.8885%]
19:31:20.342 -> Timer7: [ 4.8986%]
19:31:24.352 -> Timer8: [ 4.8943%]
19:31:28.360 -> Timer9: [ 4.8738%]
19:31:32.367 -> Timer10: [ 4.9065%]
*/

基於這個例子,如圖(A)所示,用上 D2,D5 兩支 GPIO 作 toggle。
二者設定上相差 40us,即相位差 40/(400×2)x360 等於 18 度

圖 (A) 由 D2,D5 所産生的 50% duty waveform,period=800us

因此,總共 10 支 timers,每支所産生的相位差 18 度,共涵蓋了半週就是等同一週,waveform period 是 800us。因此將可合成具有 18 度步進可調相位的 800us waveform。如圖(B),相鄰的 timer,量測約 42us offset,符合函式計算誤差的結果。

圖 (B) 大略就是 42us,符合函式所量測的誤差

因此,如圖(C),我們將 10 支 timer 都設定成觸發 isr-user1,每次進 user1 都能對同一支 GPIO 做一次 toggle。這表示,這支 GPIO 將産生 42us 半週期的 waveform,以 50us 來算就是可産生 10kHz 頻率不低的方波。

圖 (C) 由 10 支 timers 共同産生的方波
圖 (D) 週期大約 83us

這至少表明一件事,若我們在一個 isr 內,同時驅動 10 支 GPIOs,所花費時間不過數個 us,將不拖累到整個 timer 系統,10 個不同的 isr 各司這 10 支 GPIOs 在當時的 high/low 準位,結果將是有,前文也有提過 pwm 週期的取決就是最小準位時間乘上解析度,若是 10% 的話,就是 500us/2kHz pwm waveform 的 10 支 GPIO 腳位可用,且精確度相當高;透過設定不同的 timer timing,再配合 delay 及搭配計算誤差函式以補足 timer 至 50us/所需的異步週期。

Esp8266HwSwTimers.h ver.0.3

// Esp8266HwSwTimers.h ver.0.3
// https://waterfalls.ddns.net
// by Ken Woo
// 2020.11.20

/*
ver.0.1    initial release
ver.0.2    a. fixed bugs in sync functions
           b. minor improvements
ver.0.3    a. fixed bugs in sync functions
           b. add yet another heap function
           c. consolidate hw timer control
no more revision unless necessary.
*/


#include"ESP8266WiFi.h"
#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 being 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(for max 6 timers) as for an identifier tailing to the counter value.
// however, 2^9-1 timers at most could be used in if you willing to try; wdt reset is foreseeing(stay in a single isr too long).


// switches
#define C_HW_TIMER_DEBUG    0
#define SYNC_WAITING        0 // (careful!) if enabled, sync could avoid mem-fault however, timings could affected at a syncing
#define COMPENSATION        1 // independent to COMPENSATION_ADV, a little bit income
#define COMPENSATION_ADV    1 // (careful!) much unstable and probably erroneous. it's rear part of COMPENSATION, but can disabled alone.
#define TIMER_NUM           15 // up to number of timers simul used(recommended max 6, here setting is 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 bits)
#define PAYLOAD_US          4 // payload in us to subtract(recommended 4us)
#define INTRUDE_TIME_CNT    100 // the counts(20us*5) safely/enough-time for access data when syncing
#define HEAP_LOCK           0 // using lock to protect heap operations, should use 2 vars, here for simplicity.
#define LO_BOUND_US         30
#define UP_BOUND_US         1670000

// no need to modify
#define PAYLOAD_CNT         ((PAYLOAD_US)*5) // payload in counts to subtract, 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 CNT_TO_US(x)        (((x)+2)/5)
#define US_TO_CNT(x)        ((x)+((x)<<2))


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

#if SYNC_WAITING
    extern int sync_spin; // using bool is inhibited by IxRAM_ATTR
#endif


class cHwTimer{

#if C_HW_TIMER_DEBUG
    public: // remove it when formal use; it is only for debug and print
#endif

    typedef void (*void_func1)();
    typedef void (*void_func2)(float);
    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(not 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, by syncing funcs).
        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;

    typedef struct{
        unsigned load_us;                       // the load in microseconds.
        unsigned in_use;                        // the loc id.
        unsigned run_seconds;                   // how long for run.
        unsigned cnt;                           // for average calculation.
        unsigned var1;                          // for static storage, cpu time.
        void_func2 probe_callback;              // main-loop's callback function with one argument.
        float error_rate;                       // for accumulation and average.
    } sTimerProber;

        static sHwTimerObj timer_obj;
        static sTimerProber timer_prober;

        #if HEAP_LOCK
            static int heap_lock;               // using lock to protect heap operations, should use 2 vars, here for simplicity.
        #endif

        static void main_isr();
        static void default_isr();
        static void prober_isr();
        static void min_heap_insertion(unsigned key, unsigned *t);
        static unsigned min_heap_removal(unsigned *t);
        static void min_heap_deterioration(unsigned new_key, unsigned idx_target, 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.
                // if isr[i]==0 means empty; else if ==default_isr means pause, else means running timer for servicing.
                // 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 when it reloads, for one-shot.
                    loc=i;

                    // ------ using this step to avoid heap_lock use, but it will pause runtime timers a bit.
                    // and might encounter counter value is just 0.
                    if ((RTC_REG_READ(FRC1_CTRL_ADDRESS))&FRC1_ENABLE_TIMER){
                        emergent_shutdown();
                        i=(RTC_REG_READ(FRC1_COUNT_ADDRESS));
                    }
                    else i=0;
                    // ------

                    min_heap_insertion(cnt, timer_obj.current);

                    // ------ using this step to avoid heap_lock use, but it will pause runtime timers a bit.
                    if (i){
                        RTC_REG_WRITE((FRC1_LOAD_ADDRESS), UP_BOUND_CNT);
                        safe_resume();
                        RTC_REG_WRITE((FRC1_LOAD_ADDRESS), i); // load to load reg(not count reg) with expected value.
                    }
                    // ------

                    return;
                }
            }
        };

        void set(unsigned period_us, void(*isr)(), bool periodically){
            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();
            }
        };

        static void emergent_shutdown(){
            // stop the counter reg counting
            /////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();
        };

        static void safe_resume(){
            // continue from emergent_shutdown
            /////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));
        };

        const cHwTimer& operator=(const cHwTimer &a){loc=a.loc; func_for_pause=a.func_for_pause; return *this;};

    public:
        cHwTimer(): loc(-1), func_for_pause(default_isr){};
        cHwTimer(const cHwTimer &a): loc(a.loc), func_for_pause(a.func_for_pause){};
        cHwTimer(unsigned period_us, void(*isr)(), bool periodically=true){set(period_us, isr, periodically);};

        bool setTimer(unsigned period_us, void(*isr)(), bool periodically=true){
            if (isActive()) Stop();
            delay(UP_BOUND_US/1000); // one-shot is not active, however any timer needs to flush.
            set(period_us, isr, periodically);
            if (loc<=0) return false;
            return true;
        };

        void ForceHaltForSync(const cHwTimer &ref, unsigned offset_delay_us){
            if (loc>0 && ref.loc>0){
                unsigned i, k;
                k=US_TO_CNT(offset_delay_us);

                // since we use non-auto reload mode, it's a reasonable treatment to read and write
                // counter's reg with the same value for restart.

                emergent_shutdown();
                unsigned a=(RTC_REG_READ(FRC1_COUNT_ADDRESS));

                    if (i=timer_obj.current[0]){
                        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])+k)<=UP_BOUND_CNT){
                            min_heap_deterioration(OP_MERGE_CNTID(z, loc), n, timer_obj.current);
                        }
                    }

                RTC_REG_WRITE((FRC1_LOAD_ADDRESS), UP_BOUND_CNT);
                safe_resume();
                RTC_REG_WRITE((FRC1_LOAD_ADDRESS), a); // load to load reg(not count reg) with expected value.
            }
        };

        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 at the same time.
                unsigned i, j, k;
                k=US_TO_CNT(offset_delay_us)+1;
                j=10; // for at most delay 200us, which only affect this object is never mind.

                do {
                    // there is a threshold time to ensure we have enough 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])+k)<=UP_BOUND_CNT){
                            min_heap_deterioration(OP_MERGE_CNTID(z, loc), n, timer_obj.current); // need not count on payload

                            #if SYNC_WAITING
                                sync_spin=false;
                            #endif

                            return true;
                        }
                        else { // failed to sync

                            #if SYNC_WAITING
                                sync_spin=false;
                            #endif

                            return false;
                        }
                    }

                    #if SYNC_WAITING
                        sync_spin=false;
                    #endif

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

        bool ProbeAccuracy(unsigned isr_loading_us, unsigned run_seconds, void (*callback)(float result)){
            // assign a loading with how many isr_loading_us of time spent to run some isr.
            // how long to run this probe by run_seconds.
            // when timeout, callback would be called with percentage argument. user can print it out the percentage.
            // *** note that only for running timers and one probe at a time.
            // *** especially note that after callback being called, using Resume() to get back original sub-isr.

            if (!isRunning() || (timer_prober.in_use>0)) return false;
            timer_prober.load_us=isr_loading_us;
            timer_prober.in_use=loc;
            timer_prober.probe_callback=callback;
            timer_prober.cnt=0;
            timer_prober.error_rate=0;
            timer_prober.run_seconds=micros()+run_seconds*1000000;
            timer_prober.var1=micros();
            timer_obj.isr[loc]=prober_isr;
            return true;
        };

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

        bool isRunning(){return (loc>0) && timer_obj.isr[loc] && (timer_obj.isr[loc]!=default_isr);}; // is timer servicing

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

        bool 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; return true;}
            return false;
        };

        bool 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;
                if (timer_prober.in_use==loc) timer_prober.in_use=0;
                loc=-1;
                return true;
            }
            return false;
        };

        static unsigned numMaxTimers(){return TIMER_NUM;};

        static unsigned numTimers(){
            int i, j;
            for (i=1, j=0; i<=TIMER_NUM; i++) if (timer_obj.isr[i]) j++;
            return j;
        };

#if !C_HW_TIMER_DEBUG
        ~cHwTimer(){Stop();}; // if weird, mark this(cases for test only), since destructor possibly distroys static vars as well.
#endif

        // 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 test; 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;}; // main isr may access at the same time.
        static void ResumeAll(){/*can not resume all*/};
};


#endif // _c_HW_TIMER_

Esp8266HwSwTimers.cpp ver.0.3

// Esp8266HwSwTimers.cpp ver.0.3
// https://waterfalls.ddns.net
// by Ken Woo
// 2020.11.20
#include"Esp8266HwSwTimers.h"

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

#if SYNC_WAITING
    IRAM_ATTR int sync_spin=false; // using bool is inhibited by IxRAM_ATTR
#endif

#if HEAP_LOCK
    IRAM_ATTR int cHwTimer::heap_lock=false; // using lock to protect heap operations, should use 2 vars, here for simplicity.
#endif

IRAM_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, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};

IRAM_ATTR cHwTimer::sTimerProber cHwTimer::timer_prober={
    0, // load_us
    0, // in_use
    0, // run_seconds
    0, // cnt
    0, // var1
    0, // probe_callback
    0  // error_rate
};

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

IRAM_ATTR void cHwTimer::prober_isr(){

    #if C_HW_TIMER_DEBUG
        printf("\r\nprober\r\n");
    #endif

    unsigned ref=micros()-timer_prober.var1;
    timer_prober.var1=micros();
    ref=US_TO_CNT(ref);

    unsigned diff=OP_GET_CNT(timer_obj.reload[timer_prober.in_use]);
    if (diff>ref) diff-=ref;
    else diff=ref-diff;

    timer_prober.cnt++;
    timer_prober.error_rate+=float(diff)/float(OP_GET_CNT(timer_obj.reload[timer_prober.in_use]));

    diff=micros();
    if (timer_prober.run_seconds>(diff+timer_prober.load_us)){
        diff-=timer_prober.var1;
        if (timer_prober.load_us>diff) delayMicroseconds(timer_prober.load_us-diff);
    }
    else {
        timer_prober.probe_callback(timer_prober.error_rate/float(timer_prober.cnt));
        timer_obj.isr[timer_prober.in_use]=default_isr;
        timer_prober.in_use=0;
    }
}

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 ///// this is old version
        while (timer_obj.current[0] && (OP_HEAP_TOP_CNT<=PAYLOAD_CNT)){////// old version /////////
            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

        }////// old version /////////
#else ///// read the old version to know about what new version does ///// this is new version
        if (timer_obj.current[0] && (OP_HEAP_TOP_CNT<=PAYLOAD_CNT)){////// new version /////////
            discard_min_cnt=0;

            #if (COMPENSATION || COMPENSATION_ADV)
                unsigned compensation=0;
            #endif

            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); // subtract past-time equals to amount time need to spend
                // (since cnt & us are not integer-convertable, so the diff value may smaller than what we want; where)
                // (as larger is ok, smaller means sub-isr will run in advance, but, still ok)
                
                discard_min=min_heap_removal(timer_obj.current); // discard the used min one
                j=OP_GET_ID(discard_min);
                timer_obj.isr[0]=timer_obj.isr[j]; // 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 subtract outside this loop)

                // add the new one that just discard
                i=timer_obj.reload[j];

                //// what if j and OP_GET_ID(i) are not equal?
                ////if (i>TIMER_NUM) min_heap_insertion(OP_MERGE_CNTID(OP_GET_CNT(i)+discard_min_cnt, OP_GET_ID(i)), timer_obj.current);
                if (i>TIMER_NUM) min_heap_insertion(OP_MERGE_CNTID(OP_GET_CNT(i)+discard_min_cnt, j), timer_obj.current);
                else timer_obj.isr[j]=0; // it must be the one-shot or stopped timer which must deallocated

                #if COMPENSATION
                ///////////////////////////////////////////////
                    if (micros()<(diff-3)){ // if here takes runtime more than 3us then will collapse!!!
                        if (compensation<(diff-3-micros())){
                            delayMicroseconds(diff-micros()-compensation);
                            compensation=0;

                        #if C_HW_TIMER_DEBUG
                            hw_timer_overflow--;
                        #endif

                        }
                        else compensation-=(diff-micros());
                    }
                    else {
                        if (micros()>diff) compensation+=(micros()-diff); // compensate only if greater

                    #if C_HW_TIMER_DEBUG
                        hw_timer_overflow++;
                    #endif

                    }
                    /// if true, you can compensate the previous ones who disturb later timers which means run this timer in advance,
                    /// however it's complicated. if false, it's the ones who will disturb later ones and need to accumulate the penalties.
                    /// since all are under PAYLOAD_CNT of time, you have a few us for compete and all are likely willing to over time.
                    /// and if penalty did calculated, it could be subtracted at the below for-loop as well as the above delay.
                ///////////////////////////////////////////////
                #else
                    if (micros()<(diff-2)) delayMicroseconds(diff-micros());

                    #if C_HW_TIMER_DEBUG
                        else hw_timer_overflow++;
                    #endif

                #endif

            }

            #if COMPENSATION_ADV
                compensation=US_TO_CNT(compensation); // convert us to cnt
            #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
                // since the discard_min_cnt which is a past-time not updated yet(postponed) in entire current[], so we do it now

            #if COMPENSATION_ADV
                // remaining compensation if any still needs to compensate in the entire current[].
                // however, (guess)this is a decremental step which yields many sw timers got 0 in current[] for instant triggering,
                // which caused hw timer repeatedly entering main-isr without rest which severely causing memory access fault;
                // that is, read/write from current[] would probably be wrong value.
                // in this aspect, you can observe here the current[i] value before and after, cases exist later larger than former.
                // or directly dump current[] in main loop.
                // in such case means overloading which is solved by accordingly, e.g., using less timers or sub-isr less monopolized.
                // note again, at if-condition, timer_obj.current[i] could get bigger value which is not signed/unsigned problem?!
                
                if (compensation<(OP_GET_CNT(timer_obj.current[i])-discard_min_cnt))
                    timer_obj.current[i]=OP_MERGE_CNTID(OP_GET_CNT(timer_obj.current[i])-discard_min_cnt-compensation, j);

                else {
                    //// else means needs to run immedly, how can i do, i choose do nothing.
                    //// timer_obj.current[i]=OP_MERGE_CNTID(8, j); // may wrong because others in it may less than 8.
                    //// some trials caused wdt-reset, so give up this and use below.
                    //// are aware or not, compensation still can compensate some other timers in current[], there did exist.
                    //// timer_obj.current[i]=j; // set cnt to 0. starvation possible, wdt-reset. if so comment this and use below.
                    timer_obj.current[i]=OP_MERGE_CNTID(OP_GET_CNT(timer_obj.current[i])-discard_min_cnt, j);

                    #if C_HW_TIMER_DEBUG
                        hw_timer_overflow++;
                    #endif

                }
            #else
                timer_obj.current[i]=OP_MERGE_CNTID(OP_GET_CNT(timer_obj.current[i])-discard_min_cnt, j);
            #endif

            }
        }////// new version /////////
#endif ///////////////////////////////////////////////////////////////////////////// change version

    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
        j=OP_GET_ID(discard_min);
        timer_obj.isr[0]=timer_obj.isr[j]; // 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
            timer_obj.current[i]=OP_MERGE_CNTID(OP_GET_CNT(timer_obj.current[i])-discard_min_cnt, OP_GET_ID(timer_obj.current[i]));
        }

        // add the new one that just discard
        i=timer_obj.reload[j];

        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[j]=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)); // disable counter reg
        TM1_EDGE_INT_DISABLE(); // disable int
    }

    #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

    #if HEAP_LOCK
        while (heap_lock); heap_lock=true;
    #endif

    unsigned i=++t[0], j=(i>>1);
    for (; j && t[j]>key; t[i]=t[j], i=j, j>>=1);
    t[i]=key;

    #if HEAP_LOCK
        heap_lock=false;
    #endif

    // 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 HEAP_LOCK
        while (heap_lock); heap_lock=true;
    #endif

    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];

    #if HEAP_LOCK
        heap_lock=false;
    #endif

    return key;
};

IRAM_ATTR void cHwTimer::min_heap_deterioration(unsigned new_key, unsigned idx_target, unsigned *t){ // t[0] must be the current size
    // t is the whole tree, the min-root is t[1]. t[0] is used for as the tree size.
    // the idx_target ranges from 1 to t[0].
    // new_key will replace in the t[idx_target], and then this tree will be rearranged well.

    unsigned j=(idx_target<<1), i=idx_target, k=t[0];
    for (; j<k; i=j, j<<=1){
        if (t[j]>t[j+1]) ++j;
        t[i]=t[j];
    }

    // at this point, divided into 2 conds., the empty slot is either the last-leaf's parent or some leaf without child.
    // we can treat separated or unify in one. so the latter is choosed; or say, it covered both.
    if (t[k]<new_key){ // so swap
        t[i]=t[k];
        t[k]=new_key;
        new_key=t[i];
    }

    for (j=(i>>1); (j>=idx_target) && t[j]>new_key; t[i]=t[j], i=j, j>>=1);
    t[i]=new_key;
};

Categories: Arduino

Tags: ,

發佈留言

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

PHP Code Snippets Powered By : XYZScripts.com