2 Commits 8b519bbb30 ... baf7fd8560

Autor SHA1 Mensaje Fecha
  Your Name baf7fd8560 working with external pwm code hace 10 meses
  Your Name 7401015041 testing shows that analogwriterange does not work as it states, analogwriteresolution works, but only gives three options, 8, 9, and 10. tested 8 and 10, they work as intended hace 10 meses
Se han modificado 2 ficheros con 537 adiciones y 44 borrados
  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;
+}
+