ESP8266 PWM on One Output

According to the ESP8266 API – you have control over ports 12,13 and 15 for PWM.

I’m interested only in using GPIO15 as I have uses for the other port bits – so I took PWM.H and removed reference to the other two ports – leaving only GPIO15. I changed PWM_CHANNEL to 1 and deleted references to the other two pins.

#ifndef __PWM_H__
#define __PWM_H__
#define PWM_CHANNEL 1
struct pwm_single_param {
   uint16 gpio_set;
   uint16 gpio_clear;
   uint16 h_time;
struct pwm_param {
   uint16 period;
   uint16 freq;
   uint8 duty[PWM_CHANNEL];
#define PWM_DEPTH 255
#define PWM_1S 1000000
#define PWM_0_OUT_IO_NUM 15
void pwm_init(uint16 freq, uint8 *duty);
void pwm_start(void);
void pwm_set_duty(uint8 duty, uint8 channel);
uint8 pwm_get_duty(uint8 channel);
void pwm_set_freq(uint16 freq);
uint16 pwm_get_freq(void);

I then performed an init… and LO – 0% duty cycle out of GPIO15

Here’s my code..

uint8_t PWM_CH[]= {0};
uint16_t freq = 1000;
pwm_init(freq, PWM_CH);
pwm_set_duty(pwm_CH[0], 0);

and to change the duty cycle (actual values 0-255)

pwm_set_duty(pwm_CH[0], 0);

Voila. The otherwise wasted GPIO15 now usable.


22 thoughts on “ESP8266 PWM on One Output

  1. Hi Peter
    I have been trying with the PWM in the SDK 1.2, but for some reason I dont know. I can´t get 100% of the duty cycle.

    According with the SDK programming guide:

    “Sets duty cycle of a PWM output. Set the time that high-level single will
    last, duty depends on period, the maximum value can be Period * 1000 /45.
    For example, 1KHz PWM, duty range is 0 ~ 22222”

    “uint32 duty : the time that high-level single will last, duty cycle will be
    (duty*45)/ (period*1000)”

    but does not work

    Do you know something about it?

    1. Hi

      I think they are up to SDK 1.3 or 1.4 now.. however – you CANNOT get up to 100% duty cycle – I don’t know why. I think it is very high though and the extra output would be negligible. From memory you get to about 23,000 and it just won’t go any higher.

      Beware also that once you set the value to 0, i.e. PWM off – it is not actually OFF… I’ve asked them to implement a proper off function but they’ve ignored this up to now. So for example once you’ve used PWM, then functions that require exacting precision – such as serial LEDS will not work properly as the timers are still hooked up to the PWM. Nothing else I’ve tried seems to be affected.


        1. thats a shame, I hoped maybe in the next SDK (1.3) will be fix it. but like you said it´s now 1.41 and nothing.

          But Do you know if its posible shutdown the PWM and handle the pin like a digital I/O. because I tried handle like that for that need. but I can´t find something like pwm_stop(); or the memory space where I stop the pwm and set encargarse dethe pin an output digital.

          thanks a lot

  2. Hi Peter… I have played around with an input toggling an output using ints, and have had states which just piled the queue up till a fail. Then the int setup was a bit baffling. Two strikes when activated then a further three when it shouldn’t be triggered etc. Edge triggering the same?
    Anyway, however rough this may be right now, it works with all doing what it should. ie. spy streaming in, led driver pumping away, pwm’s changed ,external publish to it, uart in and publish, toggling input switching an output.
    This is some code I used for the input interrupt and output.
    Although it is set for HILEVEL active, it is active on a low level?
    The only way I could get a single hit ( regardless of component level debounce filtering) was to re enable the interrupt outside the int routine.
    Fortunately the rgb call back is convenient so I put in in there. I double checked that the bit flag for the interrupt on the port I’m using (5), was low before allowing it.
    I don’t know if this will be of any use, but I thought I would send it through.

    init ………….
    GPIO_DIS_OUTPUT(5); /* should make input */
    gpio_pin_intr_state_set(GPIO_ID_PIN(5), GPIO_PIN_INTR_HILEVEL);
    uint32 gpio_status;

    ….. interrupt routine
    void ICACHE_FLASH_ATTR interrupt_test() {
    uint32 gpio_status;
    INFO(“GPIO status %d”, gpio_status);
    INFO(“GPIO status2 %d”, gpio_status);
    FluidFlag = 8;
    port2 = !port2;
    strcat (fluid,” FLUID IS OUT”);
    MQTT_Publish(&mqttClient, “/echo”,fluid,strlen(fluid), 0, 0);
    memset(fluid, 0, sizeof(fluid));
    INFO(“GPIO Interrupt! \r\n”);
    // uint16 voltage = readvdd33();
    // ets_sprintf( machine_is, “%d”, voltage );

    ************** at end of rgb driver call back routine

    WS2812OutBuffer(rgb.buffer, 3, rgb.rgbnum);
    // INFO(“rgb’s are %d %d %d \r\n”, rgb.reda,rgb.bluea,rgb.greena);
    uint8_t gpio_status;
    if (gpio_status!= 0 && GPIO_INPUT_GET(LED_GPIO_5) == 0 )

  3. Greetings Peter,
    Thanks for your good work, It has helped me on my own journey of exploration of the ESP SOC chips.
    I have just now succeeded in cobbling together esp-mqtt-lcd with lcd backlight topic driving GPIO 15.
    Perhaps I could use the LDR on the dev board to control the brighness, I’ll see.
    The system is talking to Raspberrypi Thingbox Node-RED,
    The tool chain is Eclipse in WIndows 7-64 installed using online instructions.
    My aim is to make an openenergy monitor display system using a larger display.
    Kind regards, David.

    1. That’s great David and thanks for the kind words. I think the route you’ve taken is good. Having stumbled through figuring out how to write stuff in Node-Red I’m only just at the beginning of marvelling at this tool – I just hope it does not run out of steam on the Pi2 as if it does I’m going to have to dedicate something more powerful as it is definitely the way to go in terms of a control hub. Its amazing how many blog items out there say that GPIO15 is not usable and then all of a sudden it is! In my case the PWM function, simple though it is already has a home – our new bathroom is being kitted out now with LED strip (in some lovely aluminium+frosted plastic covers I found on the web) and the wall-switch will merely be an input to the board. Soft turn on/turn off and dim lighting after midnight are things I’ve gotten used to in one of my favourite hotels (I travel a fair bit) and now that’s going to be standard at home as well – all for one spare port bit I originally had no use for. We’ve already discussed making a mod to our board (once we get the first batch in and make sure it actually works) to add a decent MOSFET for this purpose. Just need to pick the best combination of cheap and working well with 3v3 logic input.

      1. …or a cheap low Rds_on FET driven by 3.3V through a “no cost” transistor and two resistors. It inverts the logic, but the software takes care of that.

  4. Ok, so that’s a no – I must’ve picked this up completely incorrectly. SO – I have in the character serial input some code that builds up a line buffer and when you hit ENTER it shoves it out to my code which normally accepts commands from MQTT. I check every 30ms – but I thought it would be much nicer to do this using your suggestion…. not that I’ve any problem with the existing.

    This is not getting called as it’s ignoring the serial altogether.

    So in INIT I do this.. ( know, lousy coding putting defs where they are)

    #define user_procTaskPrio 1
    #define user_procTaskQueueLen 1
    os_event_t user_procTaskQueue[user_procTaskQueueLen];
    system_os_task(loop, user_procTaskPrio, user_procTaskQueue, user_procTaskQueueLen);
    system_os_post(user_procTaskPrio, 0, 0);

    And my loop is…

    static void ICACHE_FLASH_ATTR loop(os_event_t *events) {

    // trap any serial buffer – and send it to the callback as if it was coming in on WIFI
    if (gotser) {
    gotser = 0;
    mqttDataCb(NULL, “toesp”, 5, serin_buf, strlen(serin_buf));

    There’s something missing – that loop is not being called.


    1. Just ignore me – – reading that carefully it’s blatantly obvious why it’s not working – I missed the call inside LOOP to make it happen again. What I don’t understand is… when you call system_os_post… how the hell does it know which one of these to call. So let’s say I made one called loop1 and another called loop2.. in system_os_post there does not seem to be any way to say which of the two callbacks you’re referring to. I’m putting this to one side until that becomes clear in my head. Any ideas anyone..?

      1. BIG mistake…. Just spend the early hours investigating this loop idea – it’s marvelous – it works a treat… but… TUAN clearly already uses this to monitor his MQTT queue and it is amazingly possible to create this and silently stop his code from working. I had the loop running and my serial input was working fine (which it does anyway under my own timer callback but I thought I’d give this a go)… I was testing the serial and happened to check if my input button still worked (I have one input which can turn an output on and off after de-bouncing – i.e. the new bathroom soft-fade light) and… I noted that whenever I pressed the button – an MQTT message was going out – as it should – BUT the MQTT queue size was growing and never shrinking back down… I went back to my normal callback method of checking for a full line of serial input – and the problem disappeared. I checked this over again and sure enough – creating that loop stops the MQTT queueing system working. Oh well, it seemed like a good idea at the time.

  5. Ok,,, spy sending to three pwm channels including the rgb driver to a 32 seg rgb strip every 100mS. Rgb strip going mental. Then type random uart inputs.. all captured and can’t make it fall over? Sending back pwm settings, adc, port status etc. Must be the brandy I’m drinking.. going to drink some more now!
    This is going to open a few more doors for certain.

    1. Fun isn’t it! I’m going to try out your proc stuff shortly – I’m stinging from my utter failure to get to grips with this atmospheric pressure chip and i2c. I just cannot get sense out of it. More wine I think.

  6. Yes I can. How about this in user main!
    #define user_procTaskPrio 1
    #define user_procTaskQueueLen 1
    os_event_t user_procTaskQueue[user_procTaskQueueLen];

    Yes .. I’ve already called myself that! Sorry Peter.
    Doesn’t appear as an endless loop. Gets called twice on an input. Just tried it combined with the Led cb with no issues as yet. Going to try spy sending fast data along with uart input next.

  7. Ok… mine ran ok? The only issue for me was the client variable and initially wrapping start and finish in curly braces! The system_os_post(user_procTaskPrio, 0, 0 ); exists in the uart.c copied over from tangophi’s files, both in an ‘if endif’ and further down in the recv interrupt handler. The #define user_procTaskPrio 1 is also in there.
    There has to be a reason for the os_delay line 200mS in loop(os_event_t *events)? loops in the background until other ints take priority?

    Last lines in main init …….
    //Start os task
    system_os_task(loop, user_procTaskPrio,user_procTaskQueue, user_procTaskQueueLen);
    system_os_post(user_procTaskPrio, 0, 0 );

    So, basically all I did was copy over the uart.c. Added a couple of lines to the main init, then pasted the routine in the user main with the publish line added, commenting out the OLEDs.
    Can’t think of anything else that would stop it functioning correctly.

  8. I’ve tried using your version… and this one
    and both have bits missing – definitions of priority etc… and hence can’t run on their own… so I’m guessing this is a callback that the system decides when to send back… just need the simple example from somewhere – but one that works out of the box.. anyone any ideas..

  9. Ok, well I hope so as all I’ve been aware of up to now is the setting up of callbacks – easy enough – say, a 50ms callback for pin timing, or a 20 second callback for temperature monitoring or whatever – I also have a 1 second callback for time – seconds …. and they have millisecond values in the callback setup – but yours – does not have any milliseconds settings… so I’m at a loss as to how it gets invoked – for example if you put a permanent loop in there it would surely stop things running…. unless the’re the modern equivalent of the old DOEVENTS from VB… anyone any comments on this please?

  10. If you call yourself slow then I’m a tortoise with one sore leg I’m afraid:) All I have done is try to find a way to get what I want to achieve. I have very little experience with C and am learning all the time, ( I come from the old world of assembly to do what I have needed). So, if you didn’t know that you could have loops, then maybe it isn’t what it seems? I only wish I knew more.
    I’ll have to see what’s going on. At least it’s a start. Maybe this will kick up some explanations.

  11. Just for completeness..
    copy the uart.c into the driver dir, replacing the original. reg and uart h into include\driver. #include “driver/uart.h” in user main.

    In void user_init(void) add .. (after WIFI_connect)
    system_os_task(loop, user_procTaskPrio,user_procTaskQueue, user_procTaskQueueLen);
    system_os_post(user_procTaskPrio, 0, 0 );

    add this .. into the main …
    static void ICACHE_FLASH_ATTR loop(os_event_t *events)
    MQTT_Client* client = (MQTT_Client*)&mqttClient;

    char str[256];
    int position = 0;
    int opening_brace = 0;

    int cc = uart0_rx_one_char();

    if (cc != -1)
    if (cc == 123) // {
    os_bzero(str, sizeof(str));
    // OLED_Print(0, 0, str, 1);
    // struct jsontree_context js;
    str[position] = (char)cc;

    while(cc = uart0_rx_one_char())
    if ((cc >= 32) && (cc <= 126))
    str[position++] = (char)cc;

    if (cc == 125) // }

    if (opening_brace == 0 )

    str[position++] = 0;
    INFO("String received=%s:position=%d\n",str,position);
    // OLED_CLS();
    MQTT_Publish(client, "/echo", str,strlen(str) , 0, 0);
    // OLED_Print(0, 0, str, 1);
    //jsontree_setup(&js, (struct jsontree_value *)&data_set_tree, json_putchar);
    // json_parse(&js, str);


    In this example of use, anything typed in curly braces ie {publish some info} will get sent out. Miss the last brace and expect a wdt. Thankyou to tangophi.

    1. As I’m a little slow – what I did was to capture the character into a little buffer and every now and then in a timer callback – use the incoming characters . You may or may not be ahead of me on this – what’s this “loop” program…. I didn’t know you could have a loop… I’ve assumed after the startup program I have to do everything in callbacks. Can you explain that in a little more detail???

  12. Unrelated, but I remember you wanting to sort out uart input. Just sorted it on my setup using code from
    here … …. code in the multisensory directory. (This link might have been mentioned before on here, I can’t find where I got it from now!). It’s related to Jason use, but I just copied the uart.c and added the few related task lines, and bingo. All respect and dues to tangophi.

Comments are closed.