Tuesday, June 10, 2025

Driving a WS2812B with a PIC

One of my projects needed a small footprint μC. For a while, PIC chips had disappeared from the market post COVID and I opted for the ESP8266. But now, they are not only back but newer chips are available. I was using the PIC 12F1840 but I had outgrown the program memory of 7K. The PIC 16F18115 has twice the program memory of 14K. Besides some enhanced features that are not of interest, it is actually cheaper. While it has an internal oscillator of 32 MHz, it is a lot slower than the ESP8266. This will make driving the WS2812B a bit of a challenge.

There may be many ways to drive a WS2812B from a PIC using some peripherals imaginatively. This is yet another way. This drives a single WS2812B using a bit-banging approach.

The SK6812 is a good substitute for the WS2812B. The datasheet timings are a bit different from the WS2812B, though in practice, they may still interoperate. I have tried to generate timings that are valid for both chips as per their datasheets.

The signal to drive the WS2812B is quite demanding. The pulses are in the order of hundreds of nanoseconds. Using C does not provide the fine control required, so some assembly required. Interrupt driven approaches or code using sub-routine calls 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.

Timings

The required timings are as below, taken from the WS2812B and SK6812 datasheets. The PIC timings are based on a 32 MHz clock and were tested on a LogicPort USB Logic Analyser.

Signal WS2812B SK6812 Implementation
Min (ns) Max (ns) Min (ns) Max (ns) Period (ns) Instr Cy
0 High 250 550 150 450 375 3
0 Low 700 1000 750 1050 875 7
1 High 650 950 450 750 750 6
1 Low 300 600 450 750 500 4
Latch 280,000 - 80,000 - 20,000,000 Interrupt

Assembly in XC8

Writing assembly in the new XC8 has gotten a lot harder. The #asm directive has been replaced by the asm() statement. I never quite figured out how to write a label/goto in this new environment. I tried going through some of the forums and "don't use goto" or "why on earth are you using assembly" is not a helpful comment though it is better than "why are you still using a PIC" :-)

The code for a single bit is as follows. The output is on port, the pin is pin and the bit in the pulse train for a byte is n.

    BSF   port, pin     ;   0ns L->H
    NOP                 ; 125ns
    BTFSS reg, n        ; 250ns
    BCF   port, pin     ; 375ns H->L (for 0)
    NOP                 ; 500ns
    NOP                 ; 625ns
    BCF   port, pin     ; 750ns H->L (for 1)
    NOP                 ; 875ns
    NOP                 ;1000ns
    NOP                 ;1125ns
    BSF...              ;1250ns
  

Implementation

As function calls are too slow, the code is written as a macro and repeated 24 times! The RGB values are held in 3 variables. Each value is loaded on to a register and the code is repeated 8 times.

One of the issues is picking a register to store the byte value. To avoid a bank switch, it has to be in the same bank as the port register i.e. Bank 0. There are some general purpose registers in bank 0 but I have just used FSR0L. To be on the safe side, the value should be backed up and then restored once this routine is complete.

Let us start with the macro to bit-bang a single bit. The bit position of the output pin is first defined along with the port pin. The ___mkstr function is used to add the #define value to the asm code.

#define _O_RGBLED_BIT  5
#define _O_RGBLED      LATAbits.LATA5    // Pin 2 RA5 - LED

#define RgbLedSetBit(b) \
    asm("  BSF     PORTA, "___mkstr(_O_RGBLED_BIT));\
    asm("  NOP");\
    asm("  BTFSS   FSR0L, "___mkstr(b));\
    asm("  BCF     PORTA, "___mkstr(_O_RGBLED_BIT));\
    asm("  NOP");\
    asm("  NOP");\
    asm("  BCF     PORTA, "___mkstr(_O_RGBLED_BIT));\
    asm("  NOP");\
    asm("  NOP");\
    asm("  NOP");
  

Before each byte is output, the value has to be copied from a variable to the FSR0L before outputting that byte. When this happens for bytes 2 and 3, there will be an extra delay. The code for this is in C and disassembly shows that this is 3 instructions long. To compensate for this, the last bit is shortened by 3 instructions by dropping the 3 NOPs. So, for the last bit, the same set of instructions are defined except for the last 3 NOPs that are dropped.

#define RgbLedSetLastBit(b) \
    asm("  BSF      PORTA, "___mkstr(_O_RGBLED_BIT));\
    asm("  NOP");\
    asm("  BTFSS    FSR0L, "___mkstr(b));\
    asm("  BCF      PORTA, "___mkstr(_O_RGBLED_BIT));\
    asm("  NOP");\
    asm("  NOP");\
    asm("  BCF      PORTA, "___mkstr(_O_RGBLED_BIT));
  

We can see the timings for a 0 and 1 bit (Interval A->B and Interval B->C) are close enough. Checking for any issues with the inter-byte period and comparing it with a similar bit for both the 0 and 1 bit, the delays (Interval B->C and Interval D->E) seem to be spot-on.

The next macro is for outputting the entire byte.

#define RgbLedSetByte() \
    RgbLedSetBit(7);\
    RgbLedSetBit(6);\
    RgbLedSetBit(5);\
    RgbLedSetBit(4);\
    RgbLedSetBit(3);\
    RgbLedSetBit(2);\
    RgbLedSetBit(1);\
    RgbLedSetLastBit(0);
  

The LED RGB values are stored in the variables ledR, ledG, ledB.

unsigned char ledR = 0x00;
unsigned char ledG = 0xff;
unsigned char ledB = 0xcc;

void setRgbLed() {
    FSR0L = ledG;
    RgbLedSetByte();
    FSR0L = ledR;
    RgbLedSetByte();
    FSR0L = ledB;
    RgbLedSetByte();
}
  

Now all the timings looked great. All I needed to do was to hook up a WS2812B and see it work. But nothing. It refused to light up. I don't know what it was but by experience, this was always a problem with these parts. Either the batch I had was a bad one (the ones with a black case) or my bumbling efforts to SMD solder it ends up frying them. I switched to an LED premounted on a PCB and that worked fine. Phew! A few quick changes to cycle through a few colours and a quick video showing the changing colours along with the changing waveforms from the USB scope in the background and I was done.

The issue of soldered LEDs not working is a bit of a worry. It is possible that my attempts at hot air soldering took too long. Next time, I will avoid the suspect batch and hand solder the LEDs as quickly as possible.

No comments:

Post a Comment