作者: ken

範例-製作 PWM/使用 ESP8266 MultiTimers Lib ver.0.3

No Comments

本文會示範很輕易地便可製作一顆 PWM,透過 MultiTimers v.0.3。

MTv0.3 的使用考量

前面文章曾給出範例是用 10 顆 timers,週期 400us,用以組合出半週期 42us 的一支 waveform。經調試,若改用 5 支 timers,週期 240us,offset 48us,則 waveform 的半週期將會接近 50us。這也是筆者將要使用在 pwm 上的時基。
有人會問了,為何不直接使用 1 顆 50us 的 timer,經實測只誤差 1.5us,優於上述的使用方式。當然可以。軟體 timers 的使用原則就是基於實務上的應用來決定運用。若只用於一途,就用一顆,若多途使用,則便須考量多顆 timers 在運作之下,會不會同(短)時間多顆 timer 須觸發而致 timer 失準的問題了-這個就讓筆者簡稱為“疊失”吧。又,或許失點準無妨,故,就是全依實務考量之。
故現在問題轉而問道,
我若要使用 2kHz pwm 來驅動四顆馬達。
故若派生出 4 顆 50 us 的 timer,若直接用了,那麼疊失問題必然發生。尚且這樣來看問題,若某兩顆 timer offset 12.5us 呢?沒錯,它們倆永遠都不會相遇,不過,別忘了,計時器中斷 payload 就已 4us 了,假設 service 做完只花 1us,就離開,下次進來,必得提前 4us,也就是說,第 8.5us 計時器就得準備再進來一次,接下來就不用多說了;1 秒內有 0.2 秒都在跑計時器服務。況且,這意謂著 50us 的時距下,要塞入 4 顆 timer 的 serving 沒錯吧,若視 payload 為無物,在等分時距下也要 12.5us 就須跑一次 timer service。結論是,1 秒內總共耗用 0.4 秒做計時器服務,wdt reset 是可望達成的。
反觀,剛剛是 4 顆 50us 的 timers,我們現在想要有 5 顆 50us 的 timers(使用本文的範例),每 50us 才會做一次 timer servicing,payload 必然可被視為無物,因為下一次 timer 進來是 50us 之後了。唯一的差別是每一次 timer 進來會把 4 支 pwm 該做的事情都全做掉,若是驅動 4 支 GPIO,耗時應不會太大,但免除不了該有的運算,這,正給了我們該思考此問題的真正問題核心。
誠然,最後讀者偷笑了說,我只要派生 1 支 50us timer,每次都做 4 支 GPIOs 該做的事不就得了。筆者說,是的 XD,一雷驚醒夢中人。
筆者默想著,嗯可能那個唯一的 isr 內會變得相當複雜且更耗時;因為處理異相及 duty 等問題。而筆者的不同之處便是把它分攤到 5 支 isr 內。按,又說了,那我每次進來就換成下一次不同的 isr 不就得了,筆者無言道,是的。默言,寫程式就是這麼好玩。

範例一

// example 1: using MultiTimersV0.3 to generate four 2kHz PWMs with 10-step duty by using D2, D3, D4, D5

#include"Esp8266HwSwTimers.h"


cHwTimer timers[5];         // 5 timers, all are 240us, sync-offset 48us of each other, to yield a 50us timer.
IRAM_ATTR int pwm_data[4];  // store duty point for transition from high to low
                            // candidate values are from 0 to 10, 0 stands for 0% duty, 10 for 100%.
                            // 1 for 10% high, 2 for 20% high and the like.
IRAM_ATTR int pwm_idx;      // the running index, from 1 to 10, represents 10 steps. set 0 for sync.

int test_val1;
IRAM_ATTR void timerISR(){
    static int z;

    z=micros();

    (pwm_idx<=pwm_data[0])? digitalWrite(D2, 1): digitalWrite(D2, 0);
    (pwm_idx<=pwm_data[1])? digitalWrite(D3, 1): digitalWrite(D3, 0);
    (pwm_idx<=pwm_data[2])? digitalWrite(D4, 1): digitalWrite(D4, 0);
    (pwm_idx<=pwm_data[3])? digitalWrite(D5, 1): digitalWrite(D5, 0);

    if (++pwm_idx>10) pwm_idx=1;
    test_val1=micros()-z;
}


bool initPWM(){ // init all
    if (pwm_idx) return false;
    timers[0].setTimer(240, timerISR);
    timers[1].setTimer(240, timerISR);
    timers[2].setTimer(240, timerISR);
    timers[3].setTimer(240, timerISR);
    timers[4].setTimer(240, timerISR);
    timers[1].ForceHaltForSync(timers[0], 48);
    timers[2].ForceHaltForSync(timers[1], 48);
    timers[3].ForceHaltForSync(timers[2], 48);
    timers[4].ForceHaltForSync(timers[3], 48);
}


void deletePWM(){ // delete all
    timers[0].Stop();
    timers[1].Stop();
    timers[2].Stop();
    timers[3].Stop();
    timers[4].Stop();
}


bool setDC(int gpio, unsigned duty){
    switch (gpio){
        case D2: gpio=0; break;
        case D3: gpio=1; break;
        case D4: gpio=2; break;
        case D5: gpio=3; break;
        default: return false;
    }
    if (duty>=100) duty=10;  // 100% duty
    else (duty+=9)/=10;
    pwm_data[gpio]=duty;
    return true;
}


void Sync(int gpio_base, int gpio_syncer, unsigned offset_us){ // integer folds of 50us, max 200us
 // it could not be done based on current program structure. it will be presented at the next example
}


void startPWM(){ // start all
    pwm_idx=1;
}


void stopPWM(){ // stop all
    pwm_idx=0;
}


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


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

    pinMode(D2, OUTPUT);
    pinMode(D3, OUTPUT);
    pinMode(D4, OUTPUT);
    pinMode(D5, OUTPUT);
    initPWM();
    setDC(D2, 20);
    setDC(D3, 60);
    setDC(D4, 0);
    setDC(D5, 100);
    startPWM();
}


void loop() {
    static int d2=30, d3=70, d4=10, d5=0;

    if (Timeout_10s()){
        Serial.println(test_val1);
        setDC(D2, (d2+=10)%110);
        setDC(D3, (d3+=10)%110);
        setDC(D4, (d4+=10)%110);
        setDC(D5, (d5+=10)%110);
    }
    delay(200);
}

在 ISR 內,這已是我們最簡化的程序了,但測試的結果只設定了 4 支 GPIO,總共/ISR 耗時 5us。這算很大了 XD。
不過,此範例內就有說了,sync 這個函式還不適合做;也就是說,意義上而言此範例此 4 支 GPIO 都是同步的,每一次進 ISR 全都會被設定。是故,一旦在沒有同步同時的需求下,我們可以將四支 GPIO 錯開/offset 掉。又,我們也看出了/猜測,設定 1 支 GPIO 耗時比執行 1 行指令還要貴,得到寧可跑判斷而避跑設 IO 的結論。
為了證實這點,筆者下個範例只改 ISR 內只設定 4 支 GPIO,看費時多久:
設定 1 支 GPIO 平均費時 1 us。事實上,看相鄰兩支 GPIO 被拉動的波形才最準確;如圖一,相鄰設定了 D2 和 D5,得出拉動 1 支 GPIO 需耗時 800ns。

圖一。triggering a GPIO takes 800ns

範例二 Minimization of accessing the GPIOs

int test_val1;
IRAM_ATTR void timerISR(){
    static int z;

    z=micros();
    if (!pwm_idx) return;

#if 1
    if (pwm_idx==(pwm_data[0]+1)) digitalWrite(D2, 0);
    else if ((pwm_idx==1) && pwm_data[0]) digitalWrite(D2, 1);

    if (pwm_idx==(pwm_data[1]+1)) digitalWrite(D3, 0);
    else if ((pwm_idx==1) && pwm_data[1]) digitalWrite(D3, 1);

    if (pwm_idx==(pwm_data[2]+1)) digitalWrite(D4, 0);
    else if ((pwm_idx==1) && pwm_data[2]) digitalWrite(D4, 1);

    if (pwm_idx==(pwm_data[3]+1)) digitalWrite(D5, 0);
    else if ((pwm_idx==1) && pwm_data[3]) digitalWrite(D5, 1);
#else
    (pwm_idx<=pwm_data[0])? digitalWrite(D2, 1): digitalWrite(D2, 0);
    (pwm_idx<=pwm_data[1])? digitalWrite(D3, 1): digitalWrite(D3, 0);
    (pwm_idx<=pwm_data[2])? digitalWrite(D4, 1): digitalWrite(D4, 0);
    (pwm_idx<=pwm_data[3])? digitalWrite(D5, 1): digitalWrite(D5, 0);
#endif

    if (++pwm_idx>10) pwm_idx=1;
    test_val1=micros()-z;
}

範例二就改這個 ISR,測試結果筆者笑了,真的,改善不少。圖二是此 PWM 的 waveform。
20:39:55.436 -> 6
20:40:05.415 -> 2
20:40:15.449 -> 5
20:40:25.452 -> 3
20:40:35.455 -> 3
20:40:45.455 -> 3
20:40:55.458 -> 3
20:41:05.435 -> 5
20:41:15.438 -> 2
20:41:25.446 -> 1
20:41:35.467 -> 2
20:41:45.469 -> 3
20:41:55.468 -> 2
20:42:05.467 -> 2
20:42:15.468 -> 3
20:42:25.477 -> 3
20:42:35.451 -> 2
20:42:45.462 -> 2
20:42:55.479 -> 3
20:43:05.458 -> 2

圖二。PWM waveform

範例三 增加 sync function

// example 3: based on ex1 and ex2. add sync function.
// using MultiTimersV0.3 to generate four 2kHz PWMs with 10-step duty by using D2, D3, D4, D5

//// bug is if the duty point is 0 or 10 after sync, it is non-0 or non-100 duty but becomes to be.

#include"Esp8266HwSwTimers.h"


cHwTimer timers[5];         // 5 timers, all are 240us, sync-offset 48us of each other, to yield a 50us timer.
IRAM_ATTR int pwm_data[4];  // store duty point for transition from high to low
                            // candidate values are from 0 to 10, 0 stands for 0% duty, 10 for 100%.
                            // 1 for 10% high, 2 for 20% high and the like.
IRAM_ATTR int pwm_sync[4]={1, 1, 1, 1};  // init to 1, for the time to high, so, pwm_data is for time to low.
IRAM_ATTR int pwm_idx;      // the running index, from 1 to 10, represents 10 steps. set 0 for sync.

int test_val1;
IRAM_ATTR void timerISR(){
    static int z;

    z=micros();
    if (!pwm_idx) return;

    if (pwm_idx==(pwm_data[0]+1)) digitalWrite(D2, 0);
    else if ((pwm_idx==pwm_sync[0]) && pwm_data[0]) digitalWrite(D2, 1);

    if (pwm_idx==(pwm_data[1]+1)) digitalWrite(D3, 0);
    else if ((pwm_idx==pwm_sync[1]) && pwm_data[1]) digitalWrite(D3, 1);

    if (pwm_idx==(pwm_data[2]+1)) digitalWrite(D4, 0);
    else if ((pwm_idx==pwm_sync[2]) && pwm_data[2]) digitalWrite(D4, 1);

    if (pwm_idx==(pwm_data[3]+1)) digitalWrite(D5, 0);
    else if ((pwm_idx==pwm_sync[3]) && pwm_data[3]) digitalWrite(D5, 1);

    if (++pwm_idx>10) pwm_idx=1;
    test_val1=micros()-z;
}


bool initPWM(){ // init all
    if (pwm_idx) return false;
    timers[0].setTimer(240, timerISR);
    timers[1].setTimer(240, timerISR);
    timers[2].setTimer(240, timerISR);
    timers[3].setTimer(240, timerISR);
    timers[4].setTimer(240, timerISR);
    timers[1].ForceHaltForSync(timers[0], 48);
    timers[2].ForceHaltForSync(timers[1], 48);
    timers[3].ForceHaltForSync(timers[2], 48);
    timers[4].ForceHaltForSync(timers[3], 48);
}


void deletePWM(){ // delete all
    timers[0].Stop();
    timers[1].Stop();
    timers[2].Stop();
    timers[3].Stop();
    timers[4].Stop();
}


bool setDC(int gpio, unsigned duty){
    switch (gpio){
        case D2: gpio=0; break;
        case D3: gpio=1; break;
        case D4: gpio=2; break;
        case D5: gpio=3; break;
        default: return false;
    }
    if (duty>=100) duty=10;  // 100% duty
    else (duty+=9)/=10;

    int a=duty;
    if (a && a!=10){ // the dc 0% and 100% can not change to accommodate ISR, however sync point can change.
        (a+=pwm_sync[gpio])-=1;
        if (a>9) (a-=9)-=1; // duty point is from 0 to 9
        duty=a;
    }
    pwm_data[gpio]=duty;

    return true;
}


bool setSync(int gpio_base, int gpio_syncer, unsigned offset_us){ // integer folds of 50us, max 450us
    switch (gpio_base){
        case D2: gpio_base=0; break;
        case D3: gpio_base=1; break;
        case D4: gpio_base=2; break;
        case D5: gpio_base=3; break;
        default: return false;
    }
    switch (gpio_syncer){
        case D2: gpio_syncer=0; break;
        case D3: gpio_syncer=1; break;
        case D4: gpio_syncer=2; break;
        case D5: gpio_syncer=3; break;
        default: return false;
    }
    // can sync itself

    if (offset_us>450) return false;

    offset_us/=50;

    int new_sync_pt=offset_us+pwm_sync[gpio_base];
    if (new_sync_pt>10) new_sync_pt-=10; // sync range from 1 to 10
    if (!pwm_data[gpio_syncer] || (pwm_data[gpio_syncer]==10)){pwm_sync[gpio_syncer]=new_sync_pt; return true;}

    int dc=(pwm_data[gpio_syncer]>=pwm_sync[gpio_syncer])?
        pwm_data[gpio_syncer]-pwm_sync[gpio_syncer]+1:
        ////pwm_data[gpio_syncer]-pwm_sync[gpio_syncer]+9+1;
        pwm_data[gpio_syncer]-pwm_sync[gpio_syncer]+10+1;////
        //// because if wrap around, it would pass by the valid point sync(10),
        //// so, the ref-length is sync not data itself.

    int new_duty_pt=new_sync_pt+dc-1;
    if (new_duty_pt>9) (new_duty_pt-=9)-=1; // data range from 0 to 9

    int store_pwm_idx=pwm_idx;
    pwm_idx=0;
    delayMicroseconds(10); // by test, the ISR runs at most about 10us.

    pwm_sync[gpio_syncer]=new_sync_pt;
    pwm_data[gpio_syncer]=new_duty_pt;

    pwm_idx=store_pwm_idx;
    return true;
}


void startPWM(){ // start all
    pwm_idx=1;
}


void stopPWM(){ // stop all
    pwm_idx=0;
}


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


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

    pinMode(D2, OUTPUT);
    pinMode(D3, OUTPUT);
    pinMode(D4, OUTPUT);
    pinMode(D5, OUTPUT);
    initPWM();
    setDC(D2, 20);
    setDC(D3, 20);
    setDC(D4, 90);
    setDC(D5, 90);
    startPWM();
}


void loop() {
    static int d=0;
    static int a=20, b=90, c=0;

    if (Timeout_10s()){

        if (random(4)==1){
            setDC(D2, a=random(11)*10);
            setDC(D3, a);
            setDC(D4, b=random(11)*10);
            setDC(D5, b);
        }

        setSync(D2, D3, c=(d+=50)%500);
        setSync(D4, D5, c);

        Serial.printf("Duty % 3d, %3d, Sync by % 3d, ISR costs %d us\r\n", a, b, c, test_val1);

    }
    delay(200);
}

範例四 增加 invert function

// example 4: based on ex1 and ex2 and ex3. add invert function.
// using MultiTimersV0.3 to generate four 2kHz PWMs with 10-step duty by using D2, D3, D4, D5

//// bug is if the duty point is 0 or 10 after sync, it is non-0 or non-100 duty but becomes to be.
//// the best remedy way with minimally modifying this code is to create inverse feature specifically in ISR,
//// then detect this bug and invert it which inverted(exchange data and sync).

//// however, the real ones of 0% and 100% would accordingly failed(become non-0 or non-100 duty).
//// besides, fractional access of critial data belongs to ISR in other functions is high risk.
//// so, no solution in the code structure. there is no example 5 XD.

#include"Esp8266HwSwTimers.h"


cHwTimer timers[5];         // 5 timers, all are 240us, sync-offset 48us of each other, to yield a 50us timer.
IRAM_ATTR int pwm_data[4];  // store duty point for transition from high to low
                            // candidate values are from 0 to 10, 0 stands for 0% duty, 10 for 100%.
                            // 1 for 10% high, 2 for 20% high and the like.
IRAM_ATTR int pwm_sync[4]={1, 1, 1, 1};  // init to 1, for the time to high, so, pwm_data is for time to low.
IRAM_ATTR int pwm_invert[4]={0, 0, 0, 0};  // inverse feature used in ISR.
IRAM_ATTR int pwm_idx;      // the running index, from 1 to 10, represents 10 steps. set 0 for sync.

int test_val1;
IRAM_ATTR void timerISR(){
    static int z;

    z=micros();
    if (!pwm_idx) return;

    if (pwm_idx==(pwm_data[0]+1)) digitalWrite(D2, pwm_invert[0]? 1: 0);
    else if ((pwm_idx==pwm_sync[0]) && pwm_data[0]) digitalWrite(D2, pwm_invert[0]? 0: 1);

    if (pwm_idx==(pwm_data[1]+1)) digitalWrite(D3, pwm_invert[1]? 1: 0);
    else if ((pwm_idx==pwm_sync[1]) && pwm_data[1]) digitalWrite(D3, pwm_invert[1]? 0: 1);

    if (pwm_idx==(pwm_data[2]+1)) digitalWrite(D4, pwm_invert[2]? 1: 0);
    else if ((pwm_idx==pwm_sync[2]) && pwm_data[2]) digitalWrite(D4, pwm_invert[2]? 0: 1);

    if (pwm_idx==(pwm_data[3]+1)) digitalWrite(D5, pwm_invert[3]? 1: 0);
    else if ((pwm_idx==pwm_sync[3]) && pwm_data[3]) digitalWrite(D5, pwm_invert[3]? 0: 1);

    if (++pwm_idx>10) pwm_idx=1;
    test_val1=micros()-z;
}


bool initPWM(){ // init all
    if (pwm_idx) return false;
    timers[0].setTimer(240, timerISR);
    timers[1].setTimer(240, timerISR);
    timers[2].setTimer(240, timerISR);
    timers[3].setTimer(240, timerISR);
    timers[4].setTimer(240, timerISR);
    timers[1].ForceHaltForSync(timers[0], 48);
    timers[2].ForceHaltForSync(timers[1], 48);
    timers[3].ForceHaltForSync(timers[2], 48);
    timers[4].ForceHaltForSync(timers[3], 48);
}


void deletePWM(){ // delete all
    timers[0].Stop();
    timers[1].Stop();
    timers[2].Stop();
    timers[3].Stop();
    timers[4].Stop();
}


bool setDC(int gpio, unsigned duty){
    switch (gpio){
        case D2: gpio=0; break;
        case D3: gpio=1; break;
        case D4: gpio=2; break;
        case D5: gpio=3; break;
        default: return false;
    }
    if (duty>=100) duty=10;  // 100% duty
    else (duty+=9)/=10;

    int a=duty;
    if (a && a!=10){ // the dc 0% and 100% can not change to accommodate ISR, however sync point can change.
        (a+=pwm_sync[gpio])-=1;
        if (a>9) (a-=9)-=1; // duty point is from 0 to 9
        duty=a;
    }
    pwm_data[gpio]=duty;

    return true;
}


bool setInvert(int gpio, int inv=1){
    switch (gpio){
        case D2: gpio=0; break;
        case D3: gpio=1; break;
        case D4: gpio=2; break;
        case D5: gpio=3; break;
        default: return false;
    }
    pwm_invert[gpio]=inv;
    return true;
}


bool setSync(int gpio_base, int gpio_syncer, unsigned offset_us){ // integer folds of 50us, max 450us
    switch (gpio_base){
        case D2: gpio_base=0; break;
        case D3: gpio_base=1; break;
        case D4: gpio_base=2; break;
        case D5: gpio_base=3; break;
        default: return false;
    }
    switch (gpio_syncer){
        case D2: gpio_syncer=0; break;
        case D3: gpio_syncer=1; break;
        case D4: gpio_syncer=2; break;
        case D5: gpio_syncer=3; break;
        default: return false;
    }
    // can sync itself

    if (offset_us>450) return false;

    offset_us/=50;

    int new_sync_pt=offset_us+pwm_sync[gpio_base];
    if (new_sync_pt>10) new_sync_pt-=10; // sync range from 1 to 10
    if (!pwm_data[gpio_syncer] || (pwm_data[gpio_syncer]==10)){pwm_sync[gpio_syncer]=new_sync_pt; return true;}

    int dc=(pwm_data[gpio_syncer]>=pwm_sync[gpio_syncer])?
        pwm_data[gpio_syncer]-pwm_sync[gpio_syncer]+1:
        ////pwm_data[gpio_syncer]-pwm_sync[gpio_syncer]+9+1;
        pwm_data[gpio_syncer]-pwm_sync[gpio_syncer]+10+1;////
        //// because if wrap around, it would pass by the valid point sync(10),
        //// so, the ref-length is sync not data itself.

    int new_duty_pt=new_sync_pt+dc-1;
    if (new_duty_pt>9) (new_duty_pt-=9)-=1; // data range from 0 to 9

    int store_pwm_idx=pwm_idx;
    pwm_idx=0;
    delayMicroseconds(10); // by test, the ISR runs at most about 10us.

    pwm_sync[gpio_syncer]=new_sync_pt;
    pwm_data[gpio_syncer]=new_duty_pt;

    pwm_idx=store_pwm_idx;
    return true;
}


void startPWM(){ // start all
    pwm_idx=1;
}


void stopPWM(){ // stop all
    pwm_idx=0;
}


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


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

    pinMode(D2, OUTPUT);
    pinMode(D3, OUTPUT);
    pinMode(D4, OUTPUT);
    pinMode(D5, OUTPUT);
    initPWM();
    setDC(D2, 20);
    setDC(D3, 20);
    setDC(D4, 90);
    setDC(D5, 90);
    startPWM();
}


void loop() {
    static int d=0;
    static int a=20, b=90, c=0, p=0, q=0;

    if (Timeout_5s()){

        if (random(3)==1){
            setDC(D2, a=random(11)*10);
            setDC(D3, a);
            p=random(2);
            setInvert(D3, p);

            setDC(D4, b=random(11)*10);
            setDC(D5, b);
            q=random(2);
            setInvert(D5, q);
        }

        setSync(D2, D3, c=(d+=50)%500);
        setSync(D4, D5, c);

        Serial.printf("Duty % 3d, %3d, Invert %d, %d, Sync by % 3d, ISR costs %d us\r\n", a, b, p, q, c, test_val1);

    }
    delay(200);
}

範例五 Fix Ex4 bug based on invert-invert process

Not applicable。

範例六 總結地製作 PWM

如果製作成通用的 PWM 類別,所投入者含平台本身似乎大過於實務上的應用,例如隨意的 PWM Freq,隨意的 duty cycle 解析度,隨意的 PWM 個數。而基於 MultiTimers 能夠適用的最小時基當派生 4~6 支 timers 時(每支 250us),應就是 50us 了。故使用 50us 就是確定的。再者前例使用 5 支 sync-offset timers,是為了 DC 的設定方便,然則最適用的應是派生 4 支 timers,筆者的實測數據是 4 x 192us timers,offset 48us,error-rate 3.95%,因而取來作為 PWM 的時基/timing control。故 PWM 終版會延用前範例的思路;透過前例也讓我們了解到,將 DC 0%,100% 另外拿出來直接設定將可維持程序的一致性並簡化問題。故終版差別是增加整平機制,將 PWM instances 分攤在 4 個 ISRs 務求降低某 ISR 的獨佔時間,這也是派生多支 timers 的唯一用處。

PHP Code Snippets Powered By : XYZScripts.com