Browse Source

Merge branch 'master' of http://git.thestrengthfoundry.com/kyle/esp8266-house-led-rgb

kyle 1 year ago
parent
commit
b22fa92621
2 changed files with 537 additions and 44 deletions
  1. 88 44
      esp8266-house-led-rgb.ino
  2. 449 0
      pwm.c

+ 88 - 44
esp8266-house-led-rgb.ino

@@ -77,7 +77,7 @@ uint8_t animate = 0;
 uint8_t red = 1;
 uint8_t green = 1;
 uint8_t blue = 1;
-uint8_t brightness = 50;
+uint8_t brightness = 0x07;
 
 #define DISABLE_FASTLED_CORRECTION 0
 #define ENABLE_FASTLED_CORRECTION 1
@@ -125,7 +125,7 @@ static uint8_t bg100_sCurveGamma8[] = {
   0xF9, 0xF9, 0xF9, 0xF9, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFB, 0xFB, 0xFB, 0xFB, 0xFB, 0xFB, 0xFC,
   0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD,
   0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFF
-}
+};
 
 /*  Fast-Led typical smd5050 led RGB color correction (0xFFB0F0 red 255, green 176, blue 240)
  *  measured brightness from typical LED vary by pn junction semiconductor material.
@@ -139,9 +139,9 @@ static uint8_t bg100_sCurveGamma8[] = {
  *  I believe that this color correction has to do with mechanical measurements as they are linear
  *  and I know that the human eye sensitivities are anything but.  Otherwise this is very crude.
  */
-#define FASTLED_RED_CORRECTION    = 0x00FF;
-#define FASTLED_GREEN_CORRECTION  = 0x00B0;
-#define FASTLED_BLUE_CORRECTION   = 0x00F0;
+#define FASTLED_RED_CORRECTION     0x00FF
+#define FASTLED_GREEN_CORRECTION   0x00B0
+#define FASTLED_BLUE_CORRECTION    0x00F0
 
 uint8_t correctColor(uint8_t input, uint16_t factor) {
     uint16_t out;
@@ -158,19 +158,42 @@ uint8_t gammaCorrection(uint8_t input) {
     switch(gammaCorrectionType) {
         case USE_PHILIP_GAMMA_CORRECTION: return phillip_adafruitGamma8[input];
         case USE_BG100_S_CURVE_GAMMA_CORRECTION: return bg100_sCurveGamma8[input];
-        case DISABLE_GAMMA_CORRECTION:
-        default: 
+        case DISABLE_GAMMA_CORRECTION: ;
+        default: ;
     }
     return input;
 }
 
-
-
 ESP8266WebServer server(80);    // Create a webserver object that listens for HTTP request on port 80
 
 void handleRoot();              // function prototypes for HTTP handlers
 void handleLogin();
 void handleNotFound();
+void handleRGB();
+void handleGetDec();
+void handleEchoHex();
+void handleAnimate();
+void handleGetCfg();
+void handleCfg();
+void handleGetBrightness();
+void handleBrightness();
+
+// pwm.h - https://github.com/StefanBruens/ESP8266_new_pwm
+extern "C" void pwm_start();
+extern "C" void pwm_init(uint32_t period, uint32_t *duty, uint32_t pwm_channel_num, uint32_t (*pin_info_list)[3]);
+extern "C" void pwm_set_duty(uint32_t duty, uint8_t channel);
+
+/* esp8266 12-f breakout board has pins mapped a bit odd, and arduino esp8266 libs use GPIO pin number */
+#define PIN_5 14 /* GPIO_14 */
+#define PIN_6 12 /* GPIO_12 */
+#define PIN_7 13 /* GPIO_13 */
+#define PIN_LED   2 /* GPIO_2 active low */
+
+// new pwm 
+#define PWM_RED   0
+#define PWM_GREEN 1
+#define PWM_BLUE  2
+#define PWM_LED   3
 
 void setup(void){
   Serial.begin(115200);         // Start the Serial communication to send messages to the computer
@@ -203,21 +226,17 @@ void setup(void){
   server.on("/rgb", HTTP_POST, handleRGB); // Call the 'handleRGB' function when a POST request is made to URI "/login"
   server.on("/dec", HTTP_GET, handleGetDec);
   server.on("/echo", HTTP_GET, handleEchoHex);
-  server.on("/animate", HTTP_GET, handleAnimate);
   server.on("/cfg", HTTP_GET, handleGetCfg);
   server.on("/cfg", HTTP_POST, handleCfg);
-  server.on("/brightness", HTTP_POST, handleGetBrightness);
-  server.on("/brightness", HTTP_POST, handleBrightness
+  server.on("/brightness", HTTP_GET, handleGetBrightness);
+  server.on("/brightness", HTTP_POST, handleBrightness);
+  //server.on("/animate", HTTP_GET, handleAnimate);
 
   server.onNotFound(handleNotFound);           // When a client requests an unknown URI (i.e. something other than "/"), call function "handleNotFound"
 
   server.begin();                            // Actually start the server
   Serial.println("HTTP server started");
 
-  /* esp8266 12-f breakout board has pins mapped a bit odd, and arduino esp8266 libs use GPIO pin number */
-  #define PIN_5 14 /* GPIO_14 */
-  #define PIN_6 12 /* GPIO_12 */
-  #define PIN_7 13 /* GPIO_13 */
 
   /*  slowing the PWM frequency decreases the error caused by the power mosfets
    *  having a slow turn-off
@@ -227,19 +246,39 @@ void setup(void){
    *  can tell.
    */
   //analogWriteFreq(1000);
-  analogWriteFreq(250);
+  //analogWriteFreq(250);
 
-  // lower brightness to minimal value
-  brightness = 0;
-  updateBrightness ();
+  // default range is 0..255
+  //analogWriteRange(1023);
   
+  // start code from StefanBruens/ESP8266_new_pwm
+  #define PWM_CHANNELS 4
+  const uint32_t period = 5000; // * 200ns ^= 1 kHz
+  uint32_t io_info[PWM_CHANNELS][3] = {
+    // MUX,                  FUNC,        PIN
+    {PERIPHS_IO_MUX_MTDI_U,  FUNC_GPIO12, PIN_6},
+    {PERIPHS_IO_MUX_MTCK_U,  FUNC_GPIO13, PIN_7},
+    {PERIPHS_IO_MUX_MTMS_U,  FUNC_GPIO14, PIN_5},
+    {PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2 , PIN_LED}
+  };
+  // initial duty: all at 1%
+  uint32_t pwm_duty_init[PWM_CHANNELS] = {50, 50, 50, 50};
+  pwm_init(period, pwm_duty_init, PWM_CHANNELS, io_info);
+  pwm_start();
+  pinMode(PIN_5, OUTPUT);
+  pinMode(PIN_6, OUTPUT);
+  pinMode(PIN_7, OUTPUT);
+
+
+  if(false) {
   // turn on the onboard led for testing brightness
-  analogWrite(/* esp12f onboard led */ 2, 255);
+  analogWrite(/* esp12f onboard led */ 2, 0);
 
   // setup pins with a test pwm
   analogWrite(PIN_5, red);
   analogWrite(PIN_6, green);
   analogWrite(PIN_7, blue);
+  }
 }
 
 void loop(void){
@@ -284,7 +323,7 @@ void loop(void){
      *      not something we want necessarily, but worth mentioning in case one questions
      *      what the heck is happening
      */
-    delay(1);
+    //delay(1);
 
     if(delayCount++ < TEMPORAL_LENGTH) break;
     // do we have any subframes to animate?
@@ -297,7 +336,6 @@ void loop(void){
         if(0 == frame) frame = (ANIMATE_BUFFER_LEN - 1) / 8;
         frame--;
         // get the next animation delay
-        frameAnimationDelay = animationDelayCounter = 
         // is this a stop frame
         if(0xff == frameAnimationDelay) { animate = 0; break; }
         // is this a loop frame
@@ -315,7 +353,7 @@ void loop(void){
       animationDelayCounter--;
     }
     // set color
-    setColor(redSubFrames[subframe -1], greeenSubFrames[subframe -1], blueSubFrames[subframe -1])
+    setColor(redSubFrames[subframe -1], greenSubFrames[subframe -1], blueSubFrames[subframe -1]);
     // track progress through subframes
     subframe--;
   }
@@ -343,9 +381,8 @@ void handleGetDec() {
 }
 
 void handleGetBrightness() {
-  String info = "";
+  String info = String("");
   server.send(200, "text/html", info + brightness + '\n');
-  return;
 }
 
 /*  we are going to cheat the brightness by adjusting pwm resolution
@@ -366,16 +403,23 @@ void handleGetBrightness() {
  *      brightness they are driven at 60% duty 255/432.
  */
 void updateBrightness () {
-    // default range is 0..255
-    analogWriteRange(1023);
+    setColor(red, green, blue);
+    return;
     // brightness is inverted, technically it is darkness
     uint8_t brightnessValue = 6 * (100 - brightness);
     // reducing PWM range will increase brightness
-    analogWriteRange(1023 - brightnessValue);
+    //analogWriteRange(1023 - brightnessValue);
+    // this works, but only gives 3 levels, 8,9,10
+    //if(false) analogWriteResolution(resolution++ % 2 ?10 :8);
+    //analogWriteFreq(250);
+    // all colors need to be re-written after adjusting brightness
+    //setColor(red, green, blue);
+    // turn on the onboard led for testing brightness
+    //analogWrite(/* esp12f onboard led */ 2, 100);
 }
 
 void handleBrightness() {
-    char buffer[4];  
+    uint8_t buffer[4];  
     if( ! server.hasArg("brightness")) {
         server.send(400, "text/plain", "400: Invalid Request");         // The request is invalid, so send HTTP status 400
         return;
@@ -383,16 +427,15 @@ void handleBrightness() {
     // else
     server.arg("brightness").getBytes(buffer, 3);
     brightness = (nibbler(buffer[0]) << 4) + nibbler(buffer[1]);
-    // limit brightness to 100
-    brightness = 100 < brightness ? 100 : brightness;
+    // limit brightness to 15
+    brightness = 0x0f < brightness ? 0x0f : brightness;
     updateBrightness();
     handleGetBrightness();
-    return;
 }
 
 void handleCfg() {
   uint8_t valid = 0;
-  char buf[3];
+  uint8_t buf[3];
   if(server.hasArg("quickGamma")) {
     server.arg("quickGamma").getBytes(buf, 2);
     gammaCorrectionType = '1' == buf[0]
@@ -409,7 +452,7 @@ void handleCfg() {
   }
   if(server.hasArg("colorCorrection")) {
     server.arg("colorCorrection").getBytes(buf, 2);
-    gammaCorrectionType = '1' == buf[0]
+    useFastLedCorrection = '1' == buf[0]
         ? ENABLE_FASTLED_CORRECTION
         : DISABLE_FASTLED_CORRECTION;
     valid += 1;
@@ -419,12 +462,11 @@ void handleCfg() {
 }
 
 void handleGetCfg() {
-  String info = "cfg [quickGamma:";
+  String info = String("cfg [quickGamma:");
   uint8_t fastled = ENABLE_FASTLED_CORRECTION == useFastLedCorrection;
   uint8_t sCurve = USE_PHILIP_GAMMA_CORRECTION == gammaCorrectionType;
   uint8_t quick = USE_BG100_S_CURVE_GAMMA_CORRECTION == gammaCorrectionType;
-  if(valid) server.send(200, "text/html", info + quick + " sCurveGamma:" + sCurve + " colorCorrection:" + fastled + ']' + '\n');
-  return;
+  server.send(200, "text/html", info + quick + " sCurveGamma:" + sCurve + " colorCorrection:" + fastled + ']' + '\n');
 }
 
 void handleRGB() {                         // If a POST request is made to URI /login
@@ -440,8 +482,8 @@ void handleRGB() {                         // If a POST request is made to URI /
     /* red */ (nibbler(hexStr[0]) << 4) + nibbler(hexStr[1]),
     /* green */ (nibbler(hexStr[2]) << 4) + nibbler(hexStr[3]),
     /* blue  */ (nibbler(hexStr[4]) << 4) + nibbler(hexStr[5])
-  )
-  String info = "degub r:";
+  );
+  String info = String("degub r:");
   server.send(200, "text/html", info + red + " g:" + green + " b:" + blue + '\n');
 }
 
@@ -450,6 +492,12 @@ void setColor(uint8_t r, uint8_t g, uint8_t b) {
   red   = r;
   green = g;
   blue  = b;
+  pwm_set_duty((uint16_t) brightness * correctColor(red,   FASTLED_RED_CORRECTION), PWM_RED);
+  pwm_set_duty((uint16_t) brightness * correctColor(green, FASTLED_RED_CORRECTION), PWM_GREEN);
+  pwm_set_duty((uint16_t) brightness * correctColor(blue,  FASTLED_RED_CORRECTION), PWM_BLUE);
+  pwm_set_duty((uint16_t) brightness * 255, PWM_LED);
+  pwm_start();  
+  return;
   /*  [!] it looks like we always correct color, this is not the case
    *  the correctColor() routine, and the chained gammaCorrection() routine
    *  check configuration settings before setting the LED color.
@@ -484,10 +532,6 @@ uint8_t nibbler(uint8_t v) {
   }
 }
 
-uint8_t hexStrToInt(char buf[]) {
-  return (int) strtol(buf, 0, 16);
-}
-
 void handleLogin() {                         // If a POST request is made to URI /login
   if( ! server.hasArg("username") || ! server.hasArg("password") 
       || server.arg("username") == NULL || server.arg("password") == NULL) { // If the POST request doesn't have username and password data

+ 449 - 0
pwm.c

@@ -0,0 +1,449 @@
+/*
+ * 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 0
+#define PWM_USE_NMI 0
+
+/* 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);
+
+static void ICACHE_RAM_ATTR
+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;
+}
+