Tuesday, July 12, 2022

Driving a WS2812B with an ESP8266 (Arduino)

With most PIC chips disappearing from the market as part of the semiconductor shortage, I looked around for options for my smart servo controller. I finally settled on the ESP8266 module - specifically ESP-01F. One of the features I had in mind was to use a WS2812B RGB LED to indicate the various states. I already have a servo control loop that is executed once every 20ms. It made sense to squirt out the data for the RGB LED as part of this service routine.

There are many ways to drive a WS2812B from an ESP8266 in an Arduino environment. This is yet another way. This drives a single WS2812B using a bit-banging approach.

The signal to drive the WS2812B is quite demanding. The pulses are in the order of hundreds of nanoseconds. Most Arduino function calls would take too long and have to be avoided. Interrupt driven approaches are totally out of the question. Some solutions depend on the hardware for SPI, etc. This level of complexity is OK to drive a string of LEDs but for a single LED, it is overkill.

To start with, what is the fastest way, or sufficiently fast way in the order of 100s of nanoseconds to output a singal on a GPIO? On the ESP8266, the following instruction will set a particular GPIO to high. The mask represents the pin to be set.

    WRITE_PERI_REG(0x60000304, (1<<GPIO_pin))
  

To set the GPIO to low, use the following instruction.

    WRITE_PERI_REG(0x60000308, (1<<GPIO_pin))
  

Running a program with these two instructions alternately gave a waveform with a high and low of approx 100ns. This is well within the ballpark of what we need. So what do we need? The required timings are as below.

Signal Min (ns) Max (ns)
0 High 220 380
0 Low 580 1000
1 High 580 1000
1 Low 220 420
Latch 280,000 -

Now we have commands to set the GPIO bit high and low, what we also need is a delay of the same order. Hmm, of the same order? Easy, just use the same instruction again. It will not change the state of the GPIO pin but will take exactly the same amount of time.

So we have most of the elements. Loop the code for each bit eight times, shifting the mask each time. Vary the time depending on whether it is a 0 or 1. Tweak it for the second half of the cycle when the signal is low to handle the extra delay of looping to the next bit.

There are three bytes to be sent out. I tried doing that in a loop but the delay between bytes was a bit too high. So I just made a macro of it and ran it three times. Even then, there is a slight delay between bytes. To compansate for that, reduce some of the delays for the last bit.

Start by defining GPIO13 (in my case) as the output to drive the LED.

    #define RGB_LED 13
  

A mask for the pin is next

    #define pinMask    (1<<RGB_LED)
  

Let us make macros for the setting the line high or low

    #define RGB_HIGH()  (WRITE_PERI_REG( 0x60000304, pinMask))
    #define RGB_LOW()   (WRITE_PERI_REG( 0x60000308, pinMask))
  

Next is the multi-line macro to send out one byte of data. The variable containing the 8 bit value is passed as a parameter. Note that if this is an expression rather than a simple unsigned char, the delay when it is referenced will increase. The MSB needs to be sent out first, so the mask starts with a value of 128 and is shifted right.

  #define RGB_WRITE(x)   {\
    mask = 128;\
    for (i = 0; i < 8; i++) {\
      RGB_HIGH();\
      if ((x & mask) != 0) {\
        /*T1H=800ns*/\
        RGB_HIGH();\
        RGB_HIGH();\
        RGB_HIGH();\
        RGB_HIGH();\
        RGB_HIGH();\
        RGB_HIGH();\
        RGB_HIGH();\
        RGB_HIGH();\
  \
        /*T1L=400ns*/\
        if (i != 7) {\
          RGB_LOW();\
          RGB_LOW();\
        }\
      }\
      else {\
        /*T0H=300ns*/\
        RGB_HIGH();\
        RGB_HIGH();\
        RGB_HIGH();\
  \
        /*T0L=800ns*/\
        RGB_LOW();\
        RGB_LOW();\
        RGB_LOW();\
        RGB_LOW();\
        if (i != 7) {\
          RGB_LOW();\
          RGB_LOW();\
        }\
      }\
      mask >>= 1;\
      RGB_LOW();\
    }\
  }
    

Some housekeeping - let us declare the R, G, B values and set the GPIO to output in the setup(). We also need a counter and a mask variable.

    extern unsigned char ledR;
    extern unsigned char ledG;
    extern unsigned char ledB;
    
    unsigned char mask;
    unsigned char i;

    void setup() {
      ...
      pinMode(RGB_LED, OUTPUT);
      ...
    }
  

Now the routine to output the RGB value. Note the order for the WS2812B is G, R, B. Also we use the function to disable interrupts and re-enable it afterwards.

    void sendRgbLed() {
      os_intr_lock();
      RGB_WRITE(ledG);
      RGB_WRITE(ledR);
      RGB_WRITE(ledB);
      os_intr_unlock();
    }
  

Let us now revisit the table of timings and compare it with what we see on the scope trace. There is still an issue with the inter-byte delay but it will not cause any problems. The Latch delay is not a problem here. This is called as part of a servo routine and will be called every 20ms.

Signal Min (ns) Max (ns) Actual (ns)
0 High 220 380 340
0 Low 580 1000 680
1 High 580 1000 800
1 Low 220 420 340
Inter-byte - - 920
Latch 280,000 - 20ms


Epilogue

While prototyping, I kept blowing up the WS2812B LEDs. Usually on connecting power, the LED would flash a bright white and die. This did shake my faith in the reliability of these chips. Looking back, having an RGB LED indicator may have been a bit of an affectation and a simple LED may suffice.

No comments:

Post a Comment