Driving RainbowBits with Cytron’s sk1632!

Skill level: Advanced

Note: This tutorial is for those who are already experienced in C, MPLAB X and some knowledge in PWM.

I’m sure you have seen a lot of those RGB lights everywhere and they do sure look amazing and brilliant. Here, you will be amazed that you can experiment with these LEDs at your own home. We will be experimenting on a couple of those Cytron’s RainbowBits on the breadboard and show you how these little smart WS2812 RGB LEDs on these RainbowBits work.

If you want to play with these awesome RainbowBits first, you can skip to the Let’s Color the LEDs section. However, it is encouraged for you to read up a bit on how the WS2812 LEDs and how we can drive them using DMA and PWM.

Driving the WS2812

The first thing you will notice is, the LEDs are not the common ones which you can just directly connect the supply and run immediately. You must feed the data into the LED to display the color that you want. Of course, a microcontroller is needed to supply the data into the LED, or it will not work as what you have expected. You need to send out 24bits (3 bytes) of color data into the LED, the first byte is the Green, Blue and finally Red.

ws2812 data

Each bit of the data is represented by the following diagram:

0 code: If you are sending a bit ‘0’, the ‘0 code’ pulse is sent out and being registered into the LED.

1 code: Similarly, if you are sending a bit ‘1’, the ‘1 code’ pulse is registered into the LED.

RET code: The reset code ‘RET code’ is an empty signal which occurs equal or more than the Treset. This reset code is meant for registering, or saving the colors in the LEDs once the system has done sending the pulses.

ws2812 timing


The pulses must adhere to the timing specifications (T0H, T0L, T1H, T1L) or else the LED does not accept the bit sent out by the microcontroller. The timing table is as follows:

T0H 0.35 us
T0L 0.80 us
T1H 0.70 us
T1L 0.60 us
Treset above 50 us
TxH + TxL 1.25 us


By looking at the timing, it looks pretty tough and tight for smaller 8-bit microcontrollers like Microchip’s Atmega328P in Arduino and PIC16/18F. Many 8-bit microcontrollers are not capable of delivering pulses that are very short if the program is written in C. Hence the driver program for these microcontrollers must be written in assembly language so that the timings are perfect. To make matters worse, not all 8-bit microcontrollers are blazing fast! Some microcontrollers like Microchip’s Atmega328P, when run at the maximum speed of 16 MIPS could drive these LEDs without a hitch, but the program must be written in assembly. Here’s a small part of the code from Adafruit’s Neopixel library:

ws2812 neopixel code

That’s one tough and impressive work! But sadly, we can’t use the same assembler code to other brands of microcontrollers without heavy modifications. No worries though, this tutorial can show you another way to drive these LEDs.

If you look back at the sequence chart earlier, you may notice that each bit looks like a Pulse Width Modulation pulse. Then, to send the data to the LED, we can program the microcontroller to send these PWM pulses for 24 times:

1.) Extract bit from the 24-bit color data, starting from the Most Significant Bit (MSB).

2.) Convert bit 1 or 0 to the respective PWM duty cycle.

3.) Place this value into the PWM duty cycle register.

4.) Wait for timer to go up.

5.) Go back to (1) and extract get the next bit. Repeat for 24 times!

That looks easy. You are now relieved from counting cycles, or writing tight assembly code. One issue is, the microcontroller still have to wait for the timer to go up! It will be fine for situations where you don’t need to keep updating the LEDs very often but if you love to show dancing colors while doing something else (like calculating the changes of colors), this isn’t too practical.

All is not lost though because in this tutorial, we are using a 32-bit PIC32 microcontroller in our SK1632 which contains a DMA (direct memory access) module inside. Read on to see how the DMA works.

PIC32’s DMA module, an introduction

DMA stands for Direct Memory Access. With this feature, data is directly sent to or received from the peripherals such as GPIO, PWM, ADC, DAC without the involvement of the CPU. Therefore the CPU is not being occupied when the data is being copied to or from the peripherals to the memory.

Here is an example of sending multiple data from the memory to the peripheral with or without using DMA:

ws2812 dma

To read more about the Direct Memory Access here are some useful articles:



So after knowing about how DMA works, let’s apply this to the PIC32 microcontroller!

PIC32’s DMA module

The PIC32’s DMA module is fairly complicated and there are a lot of options to play around. Here, we are coupling the PWM module to the DMA module so that the bits can be sent more efficently without the CPU’s involvement.

Do not worry if these look very complicated. You will be only briefed on the most important parts of the code fragment.

Here’s the initialization code of the PIC32’s DMA module:

uint8_t ws2812sendBit[1024];

void ws2812_init_DMA(void) {
 memset(ws2812sendBit, 1024, 0x00); // buffer for 24 individual bits of the WS2812 to be sent to the PWM module!
 IEC1bits.DMA0IE = 1;
 IFS1bits.DMA0IF = 0;
 DMACONSET = 0x8000; // enable DMA.
 DCH0CON = 0x0000;
 DCRCCON = 0x00; // 
 DCH0INTCLR = 0xff00ff; // clear DMA interrupts register.
 DCH0INTbits.CHBCIE = 1; // DMA Interrupts when channel block transfer complete enabled.
 DCH0ECON = 0x00;
 DCH0SSA = KVA_TO_PA(&ws2812sendBit); // DMA source address.
 DCH0DSA = KVA_TO_PA(&OC1RS); // DMA destination address.
 DCH0SSIZ = 25; // DMA Source size (default).
 DCH0DSIZ = 1; // DMA destination size.
 DCH0CSIZ = 1; // DMA cell size.
 DCH0ECONbits.CHSIRQ = _TIMER_2_IRQ; // DMA transfer triggered by which interrupt? (On PIC32MX - it is by _IRQ suffix!)
 DCH0ECONbits.AIRQEN = 0; // do not enable DMA transfer abort interrupt.
 DCH0ECONbits.SIRQEN = 1; // enable DMA transfer start interrupt.
 DCH0CONbits.CHAEN = 0; // DMA Channel 0 is always disabled right after the transfer.
 OC1CONSET = 0x8000;
 T2CONCLR = 0x8000;
 PR2 = 25;

1.) We reserve 1024 bytes for the WS2812’s bits and the reset code.

uint8_t ws2812sendBit[1024];

That is 40×24 bytes + another 40 bytes for the reset code and a few extra bytes for padding purposes. This drives 40 WS2812s in a row.

2.) The DMA modules is enabled, and the interrupt flags are cleared.

3.) To trigger the interrupt if the DMA module has completed sending all the data to the peripheral, we enable this bit in that register: (CHBCIE = channel block transfer complete interrupt enable)


4.) The memory and the destination addresses are set as well. Here in this situation, the source memory address is the “ws2812sendBit”, and the destination is to the PWM module “OC1RS”.

DCH0SSA = source address.

DCH0DSA = destination address.

They have to be converted to the physical addresses first due to the PIC32’s architecture, and the macro KVA_TO_PA does this.

DCH0SSA = KVA_TO_PA(&ws2812sendBit); // DMA source address.
DCH0DSA = KVA_TO_PA(&OC1RS); // DMA destination address.

DCH0SSIZ  (DMA channel 0 source size) stores the size of the source memory. Here, it is 25 for example. The destination size, of course, is 1 byte, so we set this DCH0DSIZ (DMA channel 0 destination size) to 1. Meanwhile, we set this DCH0CSIZ (DMA channel 0 cell size) to 1 because we transfer 1 byte each to the destination.

DCH0SSIZ = 25; // DMA source size.
DCH0DSIZ = 1; // DMA destination size.
DCH0CSIZ = 1; // DMA cell size.

We need to also alert the DMA module to start a transfer a byte everytime there is a peripheral interrupt (finish transferring or counting). We do this:

DCH0ECONbits.SIRQEN = 1; // enable DMA transfer start interrupt.

Since the PWM module is connected to Timer2 here, the DMA transfer is started each time the timer finish counting:

DCH0ECONbits.CHSIRQ = _TIMER_2_IRQ; // DMA transfer triggered by which interrupt? (On PIC32MX - it is by _IRQ suffix!)

To sum this up: “25 bytes source, each transfer from the source is one byte and the destination register is one byte size. Each transfer is triggered by the Timer2’s interrupt. For the DMA to complete its operation, 25 transfers are done.”

Finally, we need to start the transfer! In this tutorial, the transfers are only one-off, which means the DMA channel has to be re-enabled again each time it finishes its operation:

DCH0CONbits.CHAEN = 0;  // DMA Channel 0 is always disabled right after the transfer.

To adhere to the strict WS2812’s timing, the PR2 (period register 2) for the Timer2 must be set in accordance to the clock speed of the microcontroller:

OC1CONSET = 0x8000;
T2CONCLR = 0x8000;
PR2 = 25;

How do you get the “PR2 = 25”? Simple:

System Clock: 40MHz

Peripheral Clock: 40MHz/2 = 20MHz (divide by two)

Prescaler for Timer2: 1 (divide by one)

One Timer2 tick: 1/20MHz = 0.05uS

One time period for WS2812: 1.25uS (TxH + TxL in the table)

PR2 = 1.25uS/0.05uS = 25

After the value of PR2 is known, we can proceed to calculate the bit 0 and 1 lengths:

‘Bit 0’ high (T0H) = 0.35 us.

Value to be placed into OCxRS register = 0.35us / Timer2_tick

= 0.35us / 0.05us = 7.

‘Bit 1’ high (T1H) = 0.70 us.

Value to be placed into OCxRS register = 0.70us / Timer2_tick

= 0.70us / 0.05us = 14.

If we want to send 0b10100011 to the WS2812 for example to the PWM, the duty cycles are 14, 7, 14, 7,  7, 7, 14 and 14.

In the function ws2812_setColorStrip, after extracting the bits out from the 24-bit data, converting them into duty cycles and placing them into memory, we command the DMA to immediately streaming the data out.

“DCH0ECONbits.CHEN = 1” means channel 0 is enabled.

“DCH0ECONbits.CFORCE = 1” simply means force the DMA module to perform the operation. The timers are then enabled for the PWM to work.

DCH0CONbits.CHEN = 1; 
TMR2 = 0x0000;
T2CONSET = 0x8000;

Right after when the DMA has done pushing all the data out to the peripheral, the interrupt occurs:

(in system_interrupt.c)

void __ISR(_DMA0_VECTOR, ipl7AUTO) _IntHandlerSysDmaCh0(void)
 if(DCH0INTbits.CHBCIF) {
 //IFS0bits.T2IF = 0;
 T2CONCLR = 0x8000;
 TMR2 = 0x0000;
 OC1RS = 0x00;
 LATBbits.LATB7 = 0;
 SYS_DMA_TasksISR(sysObj.sysDma, DMA_CHANNEL_0);

It is required to shut off the Timer2, clear out the counter register (TMR2) and the duty cycle register (OC1RS) to avoid the Timer2 module to continue running and possibly disturb the next DMA transmission.

Let’s color the LEDs!

What do you need before you start experimenting with the tutorial:

Required components:

1.) MPLAB X IDE 3.26 or higher.

2.) MPLAB Harmony 1.07.01 or higher.

3.) MPLAB xc32 1.42 or higher.

4.) Cytron SK1632 with PIC32MX250F128B

5.) 2 or more Cytron RainbowBit.

6.) Breadboard and some wires.

7.) 0.1uF capacitor.

8.) AC adapter (12V) or smartphone charger to supply power to the SK1632.

Assemble the SK1632, RainbowBit and a capacitor according to the following picture:

tutorial sk1632_2_bb

Since the SK1632 cannot be powered by PICKit3 alone, you have to use an AC Adapter or a smartphone charger.

It’s straightforward! In main.c, for example, write 2 in the first parameter if you want to ‘paint’ only 2 LEDs, and last parameter is an address to the array:

 ws2812_setColorStrip(2, ws2812array);

The array is earlier declared:

uint32_t ws2812array[16] = {0x000000, 0x000000};

If you want the 1st LED to be all Green, while the 2nd LED is to be all Blue, we put:

ws2812array[0] = 0xff0000;
ws2812array[1] = 0x00ff00;

Simple! Then call the function “ws2812_setColorStrip(2, ws2812array)”, compile and upload the program into the sk1632 and you will see two lit LEDs in green and red.

ws2812 sk1632 example

(The LEDs are extremely bright – please do not stare at them too long!)

If you want to experiment, change the values in the “ws2812array”. Try using a RGB888 color picker and select whichever color (in hex number) you want over there. You can use the link: http://www.barth-dev.de/online/rgb565-color-picker/

That’s it for now! Play them at the fun sunny weekend! You can also make dancing colors there! A hint for you: use the “delay_ms” function between switching colors, and put them in a loop. 🙂

Get the file here, and extract the folder into “C:microchipharmonyv1_xx_xxapps” (default installation path, and v1_xx_xx is the version of your Harmony installed) : skpic32-pwm-dma

Leave a Comment

Your email address will not be published.

Share this Tutorial

Share on facebook
Share on whatsapp
Share on email
Share on print
Share on twitter
Share on pinterest
Share on facebook
Share on whatsapp
Share on email
Share on print
Share on twitter
Share on pinterest

Latest Tutorial

BLTouch Installation for Ender 3 with 32-bit V4.2.2 Board
Pick and Send Random Meal’s Option and Locations through Telegram Bot Using Grove WiFi 8266 on micro:bit
DIY Automated Vacuum Cleaner Using REKA:BIT With Micro:bit
Rainbow Spark in Mini House using Maker Uno.
TinyML on Arduino using Edge Impulse
Tutorials of Cytron Technologies Scroll to Top