作者: ken

使用 ESP8266 作電機控制(1)驅動風扇

No Comments

風扇,不就是通個直流或交流電就轉得起來了嗎?是這樣沒錯,筆者也只懂得這樣而已XD,也因此才要學習與筆記的。
電機控制涵蓋的內容範圍是相當深廣的,深廣到不容不懂的筆者在此置喙。因此再次強調只是學習筆記。
以下會先收集沿路看到的參考資料。

參考資料

風扇

  • 在此要學習的,更細分地說,是散熱風扇,是直流散熱風扇。
  • 再細分有有刷,及無刷。
  • 無刷就會涉及到幾相,其就須有相位控制驅動,然而散熱風扇本身就是目的元件,因此相位控制驅動的元件,簡稱驅動 IC 就會內建在風扇上面。也就是通常我們使用上無刷風扇並不需要再外掛一顆驅動器。無刷散熱風扇直接通上電源就可以使用了。
  • 不管有刷或無刷,馬達是電流驅動元件,因為都有線圈為主體,就會有電感上的物理特性。馬達構造種類也繁多控制方式不一而足。因此讓馬達動起來可能不難,但要每一時刻如所預期地運轉著就涉及到很多技術層面了。
  • 直流有刷馬達一般是二線,通電就能用。我們以 5V 全速 3000 轉的直流有刷馬達為例。若要做到轉速調控,只要降低電壓就能做到降轉控制的目的。也可以是三線的,第三線就會是轉速的回饋。一般是由 Hall sensor 來回饋。我們透過供應電壓,轉速回饋,調節電壓,如此反覆地,來做到對風扇的定速控制,稱為閉迴路控制,例如,要定速在 1000 轉,透過此閉迴路控制,得到 1.80007 V 時轉速穩定保持在 1000 轉。但看到了嗎?純類比電路比較難實現電壓的任意微調,故 PWM 因運而生。透過定電壓開關開關地供應,由開關頻率(freq)與佔空比(duty cycle)輸出此定電壓,便可輕易實現 Vrms 的線性調整(但這沒有意義)。要基於成一個系統的觀點下控制才有意義,即,閉迴路系統。開迴路控制的話筆者完全沒認知就只能避開談它了。
  • 無刷風扇內建驅動 IC。二線式與三線式的概念與有刷相同如上述。而四線式,即筆者要找來玩來學習的,便是多一腳位做 PWM 輸入。即,Vdd,Gnd,PWM_in,R_out。如下圖筆者淘了一顆來,但沒有它的規格,所以第一步要做的就是讓它轉起來,並量測另外兩根 pin 是否有轉速的回饋訊號。黑(Gnd)綠(Vdd)PWM_in(?)R_out(?)
  • 結果兩支訊號線都勾不到訊號,只好回去看一下以前的商品頁才驚覺三條線,不,它是四條線,黑-(Gnd)綠+(Vdd)黃Y(R_out)藍P(溫度控制)。所以 PWM 輸入就直接走 Vdd(見續述)。而 R_out 筆者還是量不到訊號包含加了上拉電阻。很有可能是筆者在試正負極時把它燒掉了吧 XD。另外仔細想想藍線溫控,應該就是指 PWM 控制,低電平反驅動。實際將藍線短地確實就不轉了。將電源及 PWM_in 分開有幾個好處,一來供電源穩定也毌須額外的 MOSFET 元件,二來獨立的 PWM 通道可識別小訊號並由單晶片直接控制。再者可直接單單直流驅動或加上 PWM 低電平反驅動控速兩用。
  • 主要就是要它的轉速回饋訊號我們才玩得下去。故找來另一顆如下圖。正負極明顯就不用逐線上電嘗試,故這次就抓得到 R_out 訊號了;需加上拉電阻。
圖中顯示是 40Hz,duty-clcle 是 50%,轉速改變 duty-cycle 不變,所以是驅動 IC 回傳的轉速訊號 2400 轉。因此對這顆三線的風扇控速,我們就必須使用電晶體開關來從電源端作 PWM 調控。

ESP8266 v.s. RPMs

前面文章曾談及 ESP8266 的 GPIO 中斷用法。很直覺地我們可透過一支 GPIO 接到風扇的 R_out 腳位,獲取每次上緣(或每次下緣)觸發的中斷,相隔的時間就是風扇轉動週期。當然也可用 change 觸發更為敏捷,但中斷數更頻繁。
我們接著看若 3000 轉的風扇:3000rpms / 60second = 50revolutions / 1000ms = 1rev / 20ms。即,每 20ms 就會觸發一次中斷,這在獲取風扇轉速時的代價(panality)不低。因此若只基於偵測轉速的考量下,是可以有別的折衷作法的。不過於此我們是要做到風扇定速的閉迴路控制,所以實時地偵測是必要的。再者,若風扇不轉則 GPIO 中斷豈不是不觸發?有問題的做法是轉速低於某值(取兩秒一轉視為合適/30rpms)便直接設為零。然而若高轉風扇突然卡死呢?沒問題的作法是?我們只能在每次計算出轉速後打開或重設定時中斷,例如 2 秒,定時中斷觸發後關掉定時中斷,來處理不轉。至於觸發模式的選用,原則上若不轉是高準位則用上緣,低準位則用下緣則必能在風扇停止那時觸發最後一次。對了,那定時中斷呢?前面文章也談到就使用 Ticker。

驅動風扇的考量

在硬體的能力範圍內,PWM 輸出頻率愈高愈佳因為代表單位時間 Vrms 能夠愈穩定或愈細緻/例如在 1s 下有著高斯曲線的輸出,瞬間輸出愈彈性/例如在 10ms 下輸出三成。但 ESP8266 據說無硬體 PWM,只有軟體模擬函式庫,且筆者尚不懂得去使用,加上至此我們只學達控制 ms 級的精細度,故,不幸地當前筆者只能做到 500Hz 的 PWM。甚著,若要做到 duty cycle 的調整只能再粗分;20 階,PWM 也只能是 50Hz 了。當然這是在不使用 loop() 的前提下。若使用 loop(),精細度約略可達 1s/80MHz=125ns。程式模組化是大前提也是本文如此作法的原因。我們將用定時中斷産生 PWM。GPIO 輸入中斷偵測轉速。
我們立基於使用一顆沒使用過它的直流無刷三線風扇。故會有一個測試函式檢測風扇 duty cycle 的上下界。一旦跑了此函式便會更新預設的上下界。PWM 50Hz,duty cycle 20 階,不在階上階會取 floor()。將使用 WeMos TTGO 板子,轉速便可顯示在顯示器上。

程式碼

#include <ESP8266WiFi.h>
#include <Arduino.h>
#include <U8g2lib.h>                // make sure to add U8g2 library and restart Arduino IDE
#include <SPI.h>
#include <Wire.h>
#include <Ticker.h>

/////////////////////// for TickerVerify
int ticker_verify_cnt, ticker_verify_los;
void TickerVerify(){
    static int current_time = millis();
    int new_time = millis();
    int diff=new_time-current_time;
    if (diff >= 2){
        if (ticker_verify_los<diff) ticker_verify_los=diff;
        ticker_verify_cnt++;
    }
    current_time = new_time;
}
///////////////////////

String str_speed; // for display fan speed

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




#define MY_FAN_SPD_PIN 0            // GPIO0, for sensing fan speed feedback
#define MY_PWM_OUT_PIN 5            // GPIO5, for PWM output
#define MY_FAN_SPD_TGR FALLING      // the edges to trigger regarding to a period

Ticker myTicker_pwm, myTicker_speed; // toggle PWM; measure periods
int fan_period;
int fan_step_cnt_low;               // dec counter
int fan_step_cnt_high;              // dec counter
int fan_step_reld_low;              // current fixeds for reload
int fan_step_reld_high;             // current fixeds for reload
int fan_duty_cycle;                 // steps should be 0 to 20, d-c should be 0 to 100

// 定義 GPIO0 ISR function, for speed sensing
// a period is measured from between recurrent edges
ICACHE_RAM_ATTR void ISR_GPIO0(){
    static int current_time = millis();
    
    int new_time = millis();
    fan_period = new_time-current_time;
    current_time=new_time;

    // set 2 seconds for timeout
    myTicker_speed.attach(2, [](){myTicker_speed.detach();fan_period=-1;});
}


unsigned Fan_speed(){
  if (fan_period<=0) return 0;
  return 1000*60/fan_period; // RPM
}


void Fan_setDutyCycle(unsigned dc){ // at 50Hz, 20 steps.
  if (dc>100) dc=100;
  fan_duty_cycle=dc;
  fan_step_reld_high=dc/5;
  fan_step_reld_low=20-fan_step_reld_high;
  fan_step_cnt_high=fan_step_reld_high;
  fan_step_cnt_low=fan_step_reld_low;
}


// 定義 GPIO5 Ticker function, for pwm output at 50Hz, 20 steps. So, still 1ms should go.
void Ticker_GPIO5(){
  ///myTicker_pwm.attach_ms(1, Ticker_GPIO5);
  if (fan_step_cnt_high){
    digitalWrite(MY_PWM_OUT_PIN, HIGH);
    fan_step_cnt_high--;
  }
  else if (fan_step_cnt_low){
    digitalWrite(MY_PWM_OUT_PIN, LOW);
    fan_step_cnt_low--;
  }
  else if (fan_duty_cycle){ // neither then is time to reload or reset new
    // since an intending high or low is action after leaving this function,
    // so inside this function sould always "renew" high or low.
    // and in this block the state is low.
    if (fan_step_reld_high){
        fan_step_cnt_high=fan_step_reld_high-1;
        digitalWrite(MY_PWM_OUT_PIN, HIGH);
    }
    else fan_step_cnt_high=fan_step_reld_high;
    fan_step_cnt_low=fan_step_reld_low? fan_step_reld_low-1: fan_step_reld_low;
  }

/////////////////////// for TickerVerify
  TickerVerify();
///////////////////////

}


// it must called before using the fan or used after a fan-stop,
// since duty cycle set 0 can stop fan but pwm func still polled so stop-fan if not uses.
// set duty cycle before starting fan, however reverse might ok.
void Fan_start(){
  myTicker_pwm.attach_ms(1, Ticker_GPIO5);
}


void Fan_stop(){
  myTicker_pwm.detach();
  digitalWrite(MY_PWM_OUT_PIN, LOW);
}




#define OLED_SDA  2
#define OLED_SCL 14
#define OLED_RST  4
U8G2_SSD1306_128X32_UNIVISION_F_SW_I2C u8g2(U8G2_R0, OLED_SCL, OLED_SDA , OLED_RST);

char str1[17];              // scroll this text
byte dir1;                  // for str1


// a null-terminated string already in an frmsz-size frm-frame with or without leading spaces will be wiggling in it.
// dir stores 0(forward) or 1(backward, non-zero) represents current direction which will be auto reversed.
void TextInFrameWiggle(byte &dir, byte frmsz, char *frm){
  
  for (int i=0, j=0; i<frmsz; i++){
    if (frm[i]!=' '){ // find the 1st non-space character incl. 0
      if (dir){ // backward
        if (i){
          do {frm[i-1]=frm[i];} while (frm[i++]);
          return;
        }
        else dir=0;
      }

      // forward
      j=i;
      while (frm[j++]);
      if (j-i==frmsz) return; // means no-leading-space string occupies entire frame
      if (j==frmsz){ // time to turnaround
        --i;
        dir=1;
        continue;
      }
      do {frm[j]=frm[j-1];} while (--j!=i);
      frm[j]=' ';
      return;
    }
  }
}


void setup_u8g2() {
  delay(100);
  u8g2.begin();
  strcpy(str1, "-=*U8g2*=-");
  Serial.println("");
  Serial.println("U8g2 Ready.");
}


void setup_fan() {
  // 宣告成中斷 pin for fan speed measuring

  /// pinMode(MY_FAN_SPD_PIN, INPUT_PULLUP); /// 可能需要這一步規劃;待測

  attachInterrupt(digitalPinToInterrupt(MY_FAN_SPD_PIN), ISR_GPIO0, MY_FAN_SPD_TGR);
  
  // 將 GPIO5 規劃成 PWM 驅動
  pinMode(MY_PWM_OUT_PIN, OUTPUT);
  
  Fan_start();
}


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




int fan_adjust=0;

void loop() {
  if (Timeout_10s()){
    Fan_setDutyCycle(fan_adjust);
    fan_adjust+=7;
    if (fan_adjust>100) fan_adjust=0;
  }
  ////Fan_setDutyCycle(100);


  
  u8g2.clearBuffer();          // clear the internal memory
  u8g2.setFont(u8g2_font_8x13B_mf); // choose a suitable font
  
  TextInFrameWiggle(dir1, 17, str1);
  u8g2.drawStr(0,10,str1);  // write something to the internal memory

  str_speed=Fan_speed();
  u8g2.drawStr(0,28,str_speed.c_str());  // write something to the internal memory
  
  u8g2.sendBuffer();          // transfer internal memory to the display
  delay(1);



  Serial.printf("Fanspeed = %s, dutycycle = %d, fanperiod=%d, (lost-cnt %d, max-lost %d)\r\n",\
      str_speed.c_str(), fan_duty_cycle, fan_period, ticker_verify_cnt, ticker_verify_los);
  delay(1000);
}

當前結論

  • 透過程式碼實測發現可能有一個問題及確定有一個問題
  • 在驅動 IC 的控制下,開關 Vdd 可能造成驅動 IC 重設,即,轉速訊號可能不對。
  • 因加了開關電晶體,風扇的電源是完全異於接於電腦 USB 的開發板電源的。除非離線至使用風扇電源。因此轉速訊號在開發板接電腦時是無法讀取的/浮動的。
  • Ticker 的觸發是會 lost 的或延遲的。但未能定論除非掛示波器來算 pulse 數目。
  • 在前面的文章中有提到,官方 SDK 能避用就避用只因怕衝突。但此處既已避用硬體的 timer1,只使用 Ticker,卻只能有 50Hz 的 PWM,實在是圈圈圈。50Hz 對風扇來說實在是太低了。另找尋 Arduino-based 可用的 Timer 或 PWM,發現包裝出來的函式庫仍用上硬體 timer1。不過,ESP8266_new_pwm 這支,請見參考資料連結,似乎是“更直接”使用到 timer1,即所謂的虛擬硬體 PWM。其稱為改寫 SDK 並與 SDK 相容(SDK 是將 pwm 控制直接包成 lib,故沒有程式源碼)。故,筆者的下二步,先驗證使用 Ticker 確實會 lost 或大延遲,將再加入一個 TickerVerify 函式查看最大的延遲或漏失。再來,就是閱讀這份虛擬硬體 PWM 了:在該程式中的這個函式,pwm_intr_handler,將 static 的宣告字拿掉才編譯得過。
  • 對了,上面筆者寫的範例,有一個很大的缺點就是在每個時基都要中斷以檢查 count 是否已數完/數完轉態。而如果用上硬體計時中斷/也唯有使用之,才能在任意設定的時達後觸發一次中斷/以轉態。
左下角就是電晶體開關。左邊那顆是有帶光耦隔離右邊沒有。但同樣都是能由單晶片驅動開關大壓流。順便註記一下左邊那顆輸出,有供電源正負極及一腳負載。但負載須當負端,正極當正端。
將 TickerVerify 加入程式後,發現確實有高達 95ms 的延遲
將 loop 內的 U8g2 註解掉,最大的延遲只剩 2ms,不過發生的頻率還是頗高。欲用在時間控制,顯然是行不通,因會被其他程式行拖延。

ESP8266_new_pwm source code

為了方便取用,我將作者的程式碼貼於此處,並可在 D1-mini 上閃燈。



/*
 * Copyright (C) 2016 Stefan Brüns <stefan.bruens@rwth-aachen.de>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 */

/* Set the following three defines to your needs */

#ifndef SDK_PWM_PERIOD_COMPAT_MODE
  #define SDK_PWM_PERIOD_COMPAT_MODE 0
#endif
#ifndef PWM_MAX_CHANNELS
  #define PWM_MAX_CHANNELS 8
#endif
#define PWM_DEBUG 1
#define PWM_USE_NMI 1

/* no user servicable parts beyond this point */

#define PWM_MAX_TICKS 0x7fffff
#if SDK_PWM_PERIOD_COMPAT_MODE
#define PWM_PERIOD_TO_TICKS(x) (x * 0.2)
#define PWM_DUTY_TO_TICKS(x) (x * 5)
#define PWM_MAX_DUTY (PWM_MAX_TICKS * 0.2)
#define PWM_MAX_PERIOD (PWM_MAX_TICKS * 5)
#else
#define PWM_PERIOD_TO_TICKS(x) (x)
#define PWM_DUTY_TO_TICKS(x) (x)
#define PWM_MAX_DUTY PWM_MAX_TICKS
#define PWM_MAX_PERIOD PWM_MAX_TICKS
#endif

//#include <c_types.h>
#include <pwm.h>
//#include <eagle_soc.h>
//#include <ets_sys.h>

// from SDK hw_timer.c
#define TIMER1_DIVIDE_BY_16             0x0004
#define TIMER1_ENABLE_TIMER             0x0080

struct pwm_phase {
    uint32_t ticks;    ///< delay until next phase, in 200ns units
    uint16_t on_mask;  ///< GPIO mask to switch on
    uint16_t off_mask; ///< GPIO mask to switch off
};

/* Three sets of PWM phases, the active one, the one used
 * starting with the next cycle, and the one updated
 * by pwm_start. After the update pwm_next_set
 * is set to the last updated set. pwm_current_set is set to
 * pwm_next_set from the interrupt routine during the first
 * pwm phase
 */
typedef struct pwm_phase (pwm_phase_array)[PWM_MAX_CHANNELS + 2];
static pwm_phase_array pwm_phases[3];
static struct {
    struct pwm_phase* next_set;
    struct pwm_phase* current_set;
    uint8_t current_phase;
} pwm_state;

static uint32_t pwm_period;
static uint32_t pwm_period_ticks;
static uint32_t pwm_duty[PWM_MAX_CHANNELS];
static uint16_t gpio_mask[PWM_MAX_CHANNELS];
static uint8_t pwm_channels;

// 3-tuples of MUX_REGISTER, MUX_VALUE and GPIO number
typedef uint32_t (pin_info_type)[3];

struct gpio_regs {
    uint32_t out;         /* 0x60000300 */
    uint32_t out_w1ts;    /* 0x60000304 */
    uint32_t out_w1tc;    /* 0x60000308 */
    uint32_t enable;      /* 0x6000030C */
    uint32_t enable_w1ts; /* 0x60000310 */
    uint32_t enable_w1tc; /* 0x60000314 */
    uint32_t in;          /* 0x60000318 */
    uint32_t status;      /* 0x6000031C */
    uint32_t status_w1ts; /* 0x60000320 */
    uint32_t status_w1tc; /* 0x60000324 */
};
static struct gpio_regs* gpio = (struct gpio_regs*)(0x60000300);

struct timer_regs {
    uint32_t frc1_load;   /* 0x60000600 */
    uint32_t frc1_count;  /* 0x60000604 */
    uint32_t frc1_ctrl;   /* 0x60000608 */
    uint32_t frc1_int;    /* 0x6000060C */
    uint8_t  pad[16];
    uint32_t frc2_load;   /* 0x60000620 */
    uint32_t frc2_count;  /* 0x60000624 */
    uint32_t frc2_ctrl;   /* 0x60000628 */
    uint32_t frc2_int;    /* 0x6000062C */
    uint32_t frc2_alarm;  /* 0x60000630 */
};
static struct timer_regs* timer = (struct timer_regs*)(0x60000600);


ICACHE_RAM_ATTR
////static 
void 
pwm_intr_handler(void)
{
    if ((pwm_state.current_set[pwm_state.current_phase].off_mask == 0) &&
        (pwm_state.current_set[pwm_state.current_phase].on_mask == 0)) {
        pwm_state.current_set = pwm_state.next_set;
        pwm_state.current_phase = 0;
    }

    do {
        // force write to GPIO registers on each loop
        asm volatile ("" : : : "memory");

        gpio->out_w1ts = (uint32_t)(pwm_state.current_set[pwm_state.current_phase].on_mask);
        gpio->out_w1tc = (uint32_t)(pwm_state.current_set[pwm_state.current_phase].off_mask);

        uint32_t ticks = pwm_state.current_set[pwm_state.current_phase].ticks;

        pwm_state.current_phase++;

        if (ticks) {
            if (ticks >= 16) {
                // constant interrupt overhead
                ticks -= 9;
                timer->frc1_int &= ~FRC1_INT_CLR_MASK;
                WRITE_PERI_REG(&timer->frc1_load, ticks);
                return;
            }

            ticks *= 4;
            do {
                ticks -= 1;
                // stop compiler from optimizing delay loop to noop
                asm volatile ("" : : : "memory");
            } while (ticks > 0);
        }

    } while (1);
}

/**
 * period: initial period (base unit 1us OR 200ns)
 * duty: array of initial duty values, may be NULL, may be freed after pwm_init
 * pwm_channel_num: number of channels to use
 * pin_info_list: array of pin_info
 */
void ICACHE_FLASH_ATTR
pwm_init(uint32_t period, uint32_t *duty, uint32_t pwm_channel_num,
              uint32_t (*pin_info_list)[3])
{
    int i, j, n;

    pwm_channels = pwm_channel_num;
    if (pwm_channels > PWM_MAX_CHANNELS)
        pwm_channels = PWM_MAX_CHANNELS;

    for (i = 0; i < 3; i++) {
        for (j = 0; j < (PWM_MAX_CHANNELS + 2); j++) {
            pwm_phases[i][j].ticks = 0;
            pwm_phases[i][j].on_mask = 0;
            pwm_phases[i][j].off_mask = 0;
        }
    }
    pwm_state.current_set = pwm_state.next_set = 0;
    pwm_state.current_phase = 0;

    uint32_t all = 0;
    // PIN info: MUX-Register, Mux-Setting, PIN-Nr
    for (n = 0; n < pwm_channels; n++) {
        pin_info_type* pin_info = &pin_info_list[n];
        PIN_FUNC_SELECT((*pin_info)[0], (*pin_info)[1]);
        gpio_mask[n] = 1 << (*pin_info)[2];
        all |= 1 << (*pin_info)[2];
        if (duty)
            pwm_set_duty(duty[n], n);
    }
    GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, all);
    GPIO_REG_WRITE(GPIO_ENABLE_W1TS_ADDRESS, all);

    pwm_set_period(period);

#if PWM_USE_NMI
    ETS_FRC_TIMER1_NMI_INTR_ATTACH(pwm_intr_handler);
#else
    ETS_FRC_TIMER1_INTR_ATTACH(pwm_intr_handler, NULL);
#endif
    TM1_EDGE_INT_ENABLE();

    timer->frc1_int &= ~FRC1_INT_CLR_MASK;
    timer->frc1_ctrl = 0;

    pwm_start();
}

__attribute__ ((noinline))
static uint8_t ICACHE_FLASH_ATTR
_pwm_phases_prep(struct pwm_phase* pwm)
{
    uint8_t n, phases;

    uint16_t off_mask = 0;
    for (n = 0; n < pwm_channels + 2; n++) {
        pwm[n].ticks = 0;
        pwm[n].on_mask = 0;
        pwm[n].off_mask = 0;
    }
    phases = 1;
    for (n = 0; n < pwm_channels; n++) {
        uint32_t ticks = PWM_DUTY_TO_TICKS(pwm_duty[n]);
        if (ticks == 0) {
            pwm[0].off_mask |= gpio_mask[n];
        } else if (ticks >= pwm_period_ticks) {
            pwm[0].on_mask |= gpio_mask[n];
        } else {
            if (ticks < (pwm_period_ticks/2)) {
                pwm[phases].ticks = ticks;
                pwm[0].on_mask |= gpio_mask[n];
                pwm[phases].off_mask = gpio_mask[n];
            } else {
                pwm[phases].ticks = pwm_period_ticks - ticks;
                pwm[phases].on_mask = gpio_mask[n];
                pwm[0].off_mask |= gpio_mask[n];
            }
            phases++;
        }
    }
    pwm[phases].ticks = pwm_period_ticks;

    // bubble sort, lowest to hightest duty
    n = 2;
    while (n < phases) {
        if (pwm[n].ticks < pwm[n - 1].ticks) {
            struct pwm_phase t = pwm[n];
            pwm[n] = pwm[n - 1];
            pwm[n - 1] = t;
            if (n > 2)
                n--;
        } else {
            n++;
        }
    }

#if PWM_DEBUG
        int t = 0;
    for (t = 0; t <= phases; t++) {
        ets_printf("%d @%d:   %04x %04x\n", t, pwm[t].ticks, pwm[t].on_mask, pwm[t].off_mask);
    }
#endif

    // shift left to align right edge;
    uint8_t l = 0, r = 1;
    while (r <= phases) {
        uint32_t diff = pwm[r].ticks - pwm[l].ticks;
        if (diff && (diff <= 16)) {
            uint16_t mask = pwm[r].on_mask | pwm[r].off_mask;
            pwm[l].off_mask ^= pwm[r].off_mask;
            pwm[l].on_mask ^= pwm[r].on_mask;
            pwm[0].off_mask ^= pwm[r].on_mask;
            pwm[0].on_mask ^= pwm[r].off_mask;
            pwm[r].ticks = pwm_period_ticks - diff;
            pwm[r].on_mask ^= mask;
            pwm[r].off_mask ^= mask;
        } else {
            l = r;
        }
        r++;
    }

#if PWM_DEBUG
    for (t = 0; t <= phases; t++) {
        ets_printf("%d @%d:   %04x %04x\n", t, pwm[t].ticks, pwm[t].on_mask, pwm[t].off_mask);
    }
#endif

    // sort again
    n = 2;
    while (n <= phases) {
        if (pwm[n].ticks < pwm[n - 1].ticks) {
            struct pwm_phase t = pwm[n];
            pwm[n] = pwm[n - 1];
            pwm[n - 1] = t;
            if (n > 2)
                n--;
        } else {
            n++;
        }
    }

    // merge same duty
    l = 0, r = 1;
    while (r <= phases) {
        if (pwm[r].ticks == pwm[l].ticks) {
            pwm[l].off_mask |= pwm[r].off_mask;
            pwm[l].on_mask |= pwm[r].on_mask;
            pwm[r].on_mask = 0;
            pwm[r].off_mask = 0;
        } else {
            l++;
            if (l != r) {
                struct pwm_phase t = pwm[l];
                pwm[l] = pwm[r];
                pwm[r] = t;
            }
        }
        r++;
    }
    phases = l;

#if PWM_DEBUG
    for (t = 0; t <= phases; t++) {
        ets_printf("%d @%d:   %04x %04x\n", t, pwm[t].ticks, pwm[t].on_mask, pwm[t].off_mask);
    }
#endif

    // transform absolute end time to phase durations
    for (n = 0; n < phases; n++) {
        pwm[n].ticks =
            pwm[n + 1].ticks - pwm[n].ticks;
        // subtract common overhead
        pwm[n].ticks--;
    }
    pwm[phases].ticks = 0;

    // do a cyclic shift if last phase is short
    if (pwm[phases - 1].ticks < 16) {
        for (n = 0; n < phases - 1; n++) {
            struct pwm_phase t = pwm[n];
            pwm[n] = pwm[n + 1];
            pwm[n + 1] = t;
        }
    }

#if PWM_DEBUG
    for (t = 0; t <= phases; t++) {
        ets_printf("%d +%d:   %04x %04x\n", t, pwm[t].ticks, pwm[t].on_mask, pwm[t].off_mask);
    }
    ets_printf("\n");
#endif

    return phases;
}

void ICACHE_FLASH_ATTR
pwm_start(void)
{
    pwm_phase_array* pwm = &pwm_phases[0];

    if ((*pwm == pwm_state.next_set) ||
        (*pwm == pwm_state.current_set))
        pwm++;
    if ((*pwm == pwm_state.next_set) ||
        (*pwm == pwm_state.current_set))
        pwm++;

    uint8_t phases = _pwm_phases_prep(*pwm);

        // all with 0% / 100% duty - stop timer
    if (phases == 1) {
        if (pwm_state.next_set) {
#if PWM_DEBUG
            ets_printf("PWM stop\n");
#endif
            timer->frc1_ctrl = 0;
            ETS_FRC1_INTR_DISABLE();
        }
        pwm_state.next_set = NULL;

        GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, (*pwm)[0].on_mask);
        GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, (*pwm)[0].off_mask);

        return;
    }

    // start if not running
    if (!pwm_state.next_set) {
#if PWM_DEBUG
        ets_printf("PWM start\n");
#endif
        pwm_state.current_set = pwm_state.next_set = *pwm;
        pwm_state.current_phase = phases - 1;
        ETS_FRC1_INTR_ENABLE();
        RTC_REG_WRITE(FRC1_LOAD_ADDRESS, 0);
        timer->frc1_ctrl = TIMER1_DIVIDE_BY_16 | TIMER1_ENABLE_TIMER;
        return;
    }

    pwm_state.next_set = *pwm;
}

void ICACHE_FLASH_ATTR
pwm_set_duty(uint32_t duty, uint8_t channel)
{
    if (channel > PWM_MAX_CHANNELS)
        return;

    if (duty > PWM_MAX_DUTY)
        duty = PWM_MAX_DUTY;

    pwm_duty[channel] = duty;
}

uint32_t ICACHE_FLASH_ATTR
pwm_get_duty(uint8_t channel)
{
    if (channel > PWM_MAX_CHANNELS)
        return 0;
    return pwm_duty[channel];
}

void ICACHE_FLASH_ATTR
pwm_set_period(uint32_t period)
{
    pwm_period = period;

    if (pwm_period > PWM_MAX_PERIOD)
        pwm_period = PWM_MAX_PERIOD;

    pwm_period_ticks = PWM_PERIOD_TO_TICKS(period);
}

uint32_t ICACHE_FLASH_ATTR
pwm_get_period(void)
{
    return pwm_period;
}

uint32_t ICACHE_FLASH_ATTR
get_pwm_version(void)
{
    return 1;
}

void ICACHE_FLASH_ATTR
set_pwm_debug_en(uint8_t print_en)
{
    (void) print_en;
}







// To set 100% duty, the duty must be equal to the period.
// The code uses the TIMER1 interrupt. If you use e.g. the softtimer, there is a conflict. You can use NM1 for the PWM instead.

#define PWM_CHANNELS 2
////const uint32_t period = 5000; // * 200ns ^= 1 kHz
const uint32_t period = 5000000; // * 200ns ^= 1 Hz

// PWM setup
uint32 io_info[PWM_CHANNELS][3] = {
    // MUX, FUNC, PIN
    {PERIPHS_IO_MUX_MTDI_U,  FUNC_GPIO2, 2},
    {PERIPHS_IO_MUX_MTDO_U,  FUNC_GPIO5, 5},
};

// initial duty: all off
uint32 pwm_duty_init[PWM_CHANNELS] = {0, 0};


void setup() {
    // put your setup code here, to run once:
    
    pwm_init(period, pwm_duty_init, PWM_CHANNELS, io_info);
    pwm_start();

    // do something like this whenever you want to change duty
    //pwm_set_duty(500, 0);  // GPIO2: 10%
    //pwm_set_duty(5000, 0); // GPIO2: 100%
    //pwm_set_duty(500, 1);  // GPIO5: 10%
    //pwm_set_duty(5000, 1); // GPIO5: 100%

    pwm_set_duty(2500000, 0);  // GPIO2: 50%
    
    pwm_start();           // commit
}

void loop() {
    // put your main code here, to run repeatedly:

}

Categories: Arduino

Tags: , ,

PHP Code Snippets Powered By : XYZScripts.com