Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
Below is a short demo that sets the colour of the WS2812 LED on RP2040-Zero boards. I couldn't find anything on driving WS2812 LEDs using RP2040 PWM with DMA with a casual search, so I wrote one (to pollute the Internet further :D ). My goal is simple -- to use the single onboard WS2812 without using PIO. Tested on a cheap RP2040-Zero board.
Code: Select all
/**
* =====================================================================
* sets the WS2812 LED on a RP2040-Zero board using PWM and DMA
* public domain code written by katak255 2025年10月30日
* =====================================================================
* - Thanks to Greg Chadwick's blog post and github code on PWM and DMA.
* =====================================================================
*/
#include "pico/stdlib.h"
#include "hardware/dma.h"
#include "hardware/pwm.h"
/*
* =====================================================================
* WS2812 function
* =====================================================================
*/
#define WS2812_PIN PICO_DEFAULT_WS2812_PIN
#define WS2812_DMA_SIZE 25 // 24 bits colour + idle
// PWM constants for 125MHz clk_sys (8ns clock)
//
#define WS2812_PWM_CLK 150 // 1200ns
#define WS2812_T1H_CLK 100 // 800ns
#define WS2812_T0H_CLK 50 // 400ns
// WS2812B version V5 specifies >280us, but who cares if 50us works
//
#define WS2812_RES_US 50
void ws2812_set(uint rgb)
{
static uint ws_data[WS2812_DMA_SIZE];
uint grb = ((rgb >> 8) & 0x00FF00)
| ((rgb << 8) & 0xFF0000)
| (rgb & 0x0000FF);
// convert GRB colour value into PWM channel A CC data
// - colour bits always have non-zero CC values
// - the last data value of 0 idles the PWM output
//
for (int i = 0; i < (WS2812_DMA_SIZE - 1); i++) {
ws_data[i] = (grb & 0x800000) ? WS2812_T1H_CLK : WS2812_T0H_CLK;
grb <<= 1;
}
ws_data[WS2812_DMA_SIZE - 1] = 0;
// set up and start PWM
//
gpio_set_function(WS2812_PIN, GPIO_FUNC_PWM);
int ws_slice = pwm_gpio_to_slice_num(WS2812_PIN);
pwm_set_wrap(ws_slice, WS2812_PWM_CLK - 1);
pwm_set_gpio_level(WS2812_PIN, 0); // initially idle Low
pwm_set_enabled(ws_slice, true); // start PWM
// set up and start DMA
// - 32-bit transfer, read increments, write fixed
//
int pdma = dma_claim_unused_channel(true);
dma_channel_config pcfg = dma_channel_get_default_config(pdma);
channel_config_set_transfer_data_size(&pcfg, DMA_SIZE_32);
channel_config_set_read_increment(&pcfg, true);
channel_config_set_write_increment(&pcfg, false);
channel_config_set_dreq(&pcfg, DREQ_PWM_WRAP0 + ws_slice);
dma_channel_configure(
pdma,
&pcfg,
&pwm_hw->slice[ws_slice].cc, // write addr
ws_data, // read addr
WS2812_DMA_SIZE, // count
true // start immediately
);
// wait for DMA to finish, then cleanup
dma_channel_wait_for_finish_blocking(pdma);
dma_channel_cleanup(pdma);
dma_channel_unclaim(pdma);
// a short pause for the WS2812 to reset
// - no more High pulses after the last colour bit goes Low
//
busy_wait_us_32(WS2812_RES_US);
pwm_set_enabled(ws_slice, false); // stop PWM
}
/*
* =====================================================================
* main
* =====================================================================
*/
uint clr_dat[] = {
0xFFFFFF, 0x808080, 0x404040, 0x202020,
0xFF0000, 0x800000, 0x400000, 0x200000,
0x00FF00, 0x008000, 0x004000, 0x002000,
0x0000FF, 0x000080, 0x000040, 0x000020,
0x000000
};
int main()
{
// show some colours on the WS2812 LED
//
int pause = 1000;
forever:
for (int i = 0; i < count_of(clr_dat); i++) {
ws2812_set(clr_dat[i]);
sleep_ms(pause);
}
if (pause == 125) pause = 1000; else pause /= 2;
goto forever;
}
Re: Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
How does it work? Sending 32bit words for each PWM bit at each PWM wrap?
Re: Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
Yes, 32 bits each wrap. Assumes there are plenty of PWM slices for everyone :) . Wanted to keep it simple and self-contained.
Interestingly, I was trying things out with a 1Hz PWM (clk_sys on XOSC, 1Hz for the visual LED feedback on a regular Pico) and stopping the 1Hz PWM immediately after dma_channel_wait_for_finish_blocking() causes 2 DMA CC values to go missing. Might be stuff in-flight [1]. For 1Hz PWM I waited for CC to end up with the idle 0 value, acting as a sentinel. Or wait for just over 1 wrap period, then all DMA values appear correctly in the output. Apparently this isn't needed for the higher or normal speed WS2812 thing, but I put the 0 in as a last CC value to simplify the end of colour bit transmit and avoid headaches.
Edited to add: [1] Or rather, one value in CC waiting to be latched on wrap and perhaps one in-flight DMA transaction. Cores and PWM are on clk_sys (XOSC 12MHz) but I dunno about the rest of the blocks and I guess funny things can happen with certain configs. Still it's a nice PWM-DMA learning exercise for me and the function is easy to add into any program.
Interestingly, I was trying things out with a 1Hz PWM (clk_sys on XOSC, 1Hz for the visual LED feedback on a regular Pico) and stopping the 1Hz PWM immediately after dma_channel_wait_for_finish_blocking() causes 2 DMA CC values to go missing. Might be stuff in-flight [1]. For 1Hz PWM I waited for CC to end up with the idle 0 value, acting as a sentinel. Or wait for just over 1 wrap period, then all DMA values appear correctly in the output. Apparently this isn't needed for the higher or normal speed WS2812 thing, but I put the 0 in as a last CC value to simplify the end of colour bit transmit and avoid headaches.
Edited to add: [1] Or rather, one value in CC waiting to be latched on wrap and perhaps one in-flight DMA transaction. Cores and PWM are on clk_sys (XOSC 12MHz) but I dunno about the rest of the blocks and I guess funny things can happen with certain configs. Still it's a nice PWM-DMA learning exercise for me and the function is easy to add into any program.
Re: Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
Ah, I see that 16-bit transfers can be arranged too. I changed two lines and it worked fine with 16-bit transfers:
BUT. Then I tried uint8_t and DMA_SIZE_8 and it compiled fine but nothing from the WS2812. Strange, it looks straightforward to me... well maybe I missed something blindingly obvious. I might have to dig up some test equipment later to check the output, but will test CC behaviour first.
Code: Select all
...
static uint16_t ws_data[WS2812_DMA_SIZE];
...
channel_config_set_transfer_data_size(&pcfg, DMA_SIZE_16);
...
Re: Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
Okay, I crashed into byte or halfword replication on bus transfers to peripheral registers. :D
So one can save array space and have a 16-bit CC data array for the DMA transfer, but the value will be replicated across both A and B channels of the slice's PWM CC. For DMA_SIZE_32 and DMA_SIZE_16, we can't really have channel A or B do their own thing, it's best to assign the entire PWM slice for its use. For DMA_SIZE_8, there will be replication of a byte value into all 4 bytes of a CC register, so it's best to stay clear of that.
So one can save array space and have a 16-bit CC data array for the DMA transfer, but the value will be replicated across both A and B channels of the slice's PWM CC. For DMA_SIZE_32 and DMA_SIZE_16, we can't really have channel A or B do their own thing, it's best to assign the entire PWM slice for its use. For DMA_SIZE_8, there will be replication of a byte value into all 4 bytes of a CC register, so it's best to stay clear of that.
Re: Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
Note that you could avoid the PWM altogether and use the DMA pacing timer to directly switch a GPIO with DMA.
Note that you can't reach the SIO GPIO registers with DMA (as they are in the SIO, CPU fast bus only), but you can reach the GPIOn_CTRL which contains the OUTOVER. Hence DMA can twiddle an individual GPIO without touching the others.
I haven't checked that the range of the pacing timers suits the WS2812 timing, but it probably does...
Note that you can't reach the SIO GPIO registers with DMA (as they are in the SIO, CPU fast bus only), but you can reach the GPIOn_CTRL which contains the OUTOVER. Hence DMA can twiddle an individual GPIO without touching the others.
I haven't checked that the range of the pacing timers suits the WS2812 timing, but it probably does...
Re: Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
It starts to look ugly, :)
I wouldn't count too much on DMA's "precise timing".
Can try with SPI as well (if it matches the pin), 3 SPI bits/PWM bit.
Screw the WS2812 timings, they are meant just to pretend being rocket science.
P.S. Keep in mind that also the DMA has its own FIFOs: Transfer Data, Read Address, Write Address
I wouldn't count too much on DMA's "precise timing".
Can try with SPI as well (if it matches the pin), 3 SPI bits/PWM bit.
Screw the WS2812 timings, they are meant just to pretend being rocket science.
It might be some hardware bug/limitation specific to RP2040 (like RP2040-E13).katak255 wrote:stopping the 1Hz PWM immediately after dma_channel_wait_for_finish_blocking() causes 2 DMA CC values to go missing
P.S. Keep in mind that also the DMA has its own FIFOs: Transfer Data, Read Address, Write Address
Re: Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
Here it is, DMA-only code for setting the one WS2812 LED on RP2040-Zero boards.
Mostly worked the first time except for one error of not unclaiming the DMA timer.
Code: Select all
/**
* =====================================================================
* sets the WS2812 LED on a RP2040-Zero board using PWM only
* public domain code written by katak255 2025年11月01日
* =====================================================================
*/
#include "pico/stdlib.h"
#include "hardware/dma.h"
/*
* =====================================================================
* WS2812 function
* =====================================================================
*/
#define WS2812_PIN PICO_DEFAULT_WS2812_PIN
#define WS2812_DMA_SIZE (24 * 3)
// Notes on DMA timing and format:
// - for 125MHz clk_sys (8ns clock), DMA pacing is 50 cycles or 400ns
// - a colour bit consists of 3 DMA transfers in 1200ns
// - T1 is 2-High 1-Low while T0 is 1-High 2-Low
//
#define WS2812_DMA_DIV 50 // 8*50 == 400ns
// GPIO CTRL bits used are OEOVER and OUTOVER:
//
#define GPIO_CTRL_OEOVER_ENABLE (0x3u << 12) // output enable
#define GPIO_CTRL_OUTOVER_LOW (0x2u << 8) // output drive low
#define GPIO_CTRL_OUTOVER_HIGH (0x3u << 8) // output drive high
// WS2812B version V5 specifies >280us, but who cares if 50us works
//
#define WS2812_RES_US 50
void ws2812_set(uint rgb)
{
static uint ws_data[WS2812_DMA_SIZE];
uint grb = ((rgb >> 8) & 0x00FF00)
| ((rgb << 8) & 0xFF0000)
| (rgb & 0x0000FF);
// GPIOn_CTRL values needed
//
uint v0 = io_bank0_hw->io[WS2812_PIN].ctrl; // original reg value
uint v1 = v0 | GPIO_CTRL_OEOVER_ENABLE; // output enabled
uint vL = v1 | GPIO_CTRL_OUTOVER_LOW; // enabled & Low
uint vH = v1 | GPIO_CTRL_OUTOVER_HIGH; // enabled & High
// convert GRB colour value into PWM data
//
for (int i = 0; i < WS2812_DMA_SIZE; ) {
ws_data[i++] = vH;
ws_data[i++] = (grb & 0x800000) ? vH : vL;
ws_data[i++] = vL;
grb <<= 1;
}
// set up and start DMA
// - 32-bit transfer, read increments, write fixed
//
int pdma = dma_claim_unused_channel(true);
dma_channel_config pcfg = dma_channel_get_default_config(pdma);
channel_config_set_transfer_data_size(&pcfg, DMA_SIZE_32);
channel_config_set_read_increment(&pcfg, true);
channel_config_set_write_increment(&pcfg, false);
int ptimer = dma_claim_unused_timer(true);
dma_timer_set_fraction(ptimer, 1, WS2812_DMA_DIV);
channel_config_set_dreq(&pcfg, dma_get_timer_dreq(ptimer));
dma_channel_configure(
pdma,
&pcfg,
&io_bank0_hw->io[WS2812_PIN].ctrl, // write addr
ws_data, // read addr
WS2812_DMA_SIZE, // count
true // start immediately
);
// wait for DMA to finish, then cleanup
dma_channel_wait_for_finish_blocking(pdma);
dma_channel_cleanup(pdma);
dma_timer_unclaim(ptimer);
dma_channel_unclaim(pdma);
// a short pause for the WS2812 to reset
// - no more High pulses after the last colour bit goes Low
//
busy_wait_us_32(WS2812_RES_US);
io_bank0_hw->io[WS2812_PIN].ctrl = v0; // restore GPIOn_CTRL
}
/*
* =====================================================================
* main
* =====================================================================
*/
uint clr_dat[] = {
0xFFFFFF, 0x808080, 0x404040, 0x202020,
0xFF0000, 0x800000, 0x400000, 0x200000,
0x00FF00, 0x008000, 0x004000, 0x002000,
0x0000FF, 0x000080, 0x000040, 0x000020,
0x000000
};
int main()
{
// show some colours on the WS2812 LED
//
int pause = 1000;
forever:
for (int i = 0; i < count_of(clr_dat); i++) {
ws2812_set(clr_dat[i]);
sleep_ms(pause);
}
if (pause == 125) pause = 1000; else pause /= 2;
goto forever;
}
- VincentARM
- Posts: 117
- Joined: Sat Feb 06, 2021 8:00 pm
Re: Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
Hello Thanks for this solution.
I tested it on an RP2040Zero and it works.
I tried it on an RP2350Zero after modifying WS2812_DMA_DIV (with 67 for a frequency of 150MHz), but it doesn't work.
Have you tried it on an RP2350, or do you have any suggestions for another modification?
Thanks
I tested it on an RP2040Zero and it works.
I tried it on an RP2350Zero after modifying WS2812_DMA_DIV (with 67 for a frequency of 150MHz), but it doesn't work.
Have you tried it on an RP2350, or do you have any suggestions for another modification?
Thanks
Re: Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
I don't have any RP2350 boards yet, can't test it for you. :D
If you're modifying WS2812_DMA_DIV, I guess it's the second DMA-to-peripheral-register example that you're trying. I haven't acquired the habit of using the register defs from the SDK header yet, so better check the SDK and RP2350 datasheet for the target peripheral register to write to. The RP2350 GPIO/pad stuff is much more complex versus RP2040.
[Edit] PWM would be the safer option, because as others have indicated in earlier postings, there is a concern whether the WS2812 timings can be reliably met when using DMA to push the waveform under a heavy processing load. Besides, unless driving motors, PWMs are probably under-utilized as well.
If you're modifying WS2812_DMA_DIV, I guess it's the second DMA-to-peripheral-register example that you're trying. I haven't acquired the habit of using the register defs from the SDK header yet, so better check the SDK and RP2350 datasheet for the target peripheral register to write to. The RP2350 GPIO/pad stuff is much more complex versus RP2040.
[Edit] PWM would be the safer option, because as others have indicated in earlier postings, there is a concern whether the WS2812 timings can be reliably met when using DMA to push the waveform under a heavy processing load. Besides, unless driving motors, PWMs are probably under-utilized as well.
Re: Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
Is that divider OK?VincentARM wrote: I tried it on an RP2350Zero after modifying WS2812_DMA_DIV (with 67 for a frequency of 150MHz), but it doesn't work.
I think the DMA period should be close to 1250 ns /3 = 417 ns (2.400 MHz)
With 67 divider it gives 150 MHz / 67 = 2.239 MHz (467 ns)
Eventually try lower divider / higher frequency, or a more conventional method to test the timings of your WS2812.
- VincentARM
- Posts: 117
- Joined: Sat Feb 06, 2021 8:00 pm
Re: Function to use the WS2812 LED of RP2040-Zero boards using PWM and DMA
Thank you for your replies, I will look into this more closely.
Return to "Other RP2040 boards"
Jump to
- Community
- General discussion
- Announcements
- Other languages
- Deutsch
- Español
- Français
- Italiano
- Nederlands
- 日本語
- Polski
- Português
- Русский
- Türkçe
- User groups and events
- Raspberry Pi Official Magazine
- Using the Raspberry Pi
- Beginners
- Troubleshooting
- Advanced users
- Assistive technology and accessibility
- Education
- Picademy
- Teaching and learning resources
- Staffroom, classroom and projects
- Astro Pi
- Mathematica
- High Altitude Balloon
- Weather station
- Programming
- C/C++
- Java
- Python
- Scratch
- Other programming languages
- Windows 10 for IoT
- Wolfram Language
- Bare metal, Assembly language
- Graphics programming
- OpenGLES
- OpenVG
- OpenMAX
- General programming discussion
- Projects
- Networking and servers
- Automation, sensing and robotics
- Graphics, sound and multimedia
- Other projects
- Gaming
- Media centres
- AIY Projects
- Hardware and peripherals
- Camera board
- Compute Module
- Official Display
- HATs and other add-ons
- Device Tree
- Interfacing (DSI, CSI, I2C, etc.)
- Keyboard computers (400, 500, 500+)
- Raspberry Pi Pico
- General
- SDK
- MicroPython
- Other RP2040 boards
- Zephyr
- Rust
- AI Accelerator
- AI Camera - IMX500
- Hailo
- Software
- Raspberry Pi OS
- Raspberry Pi Connect
- Raspberry Pi Desktop for PC and Mac
- Beta testing
- Other
- Android
- Debian
- FreeBSD
- Gentoo
- Linux Kernel
- NetBSD
- openSUSE
- Plan 9
- Puppy
- Arch
- Pidora / Fedora
- RISCOS
- Ubuntu
- Ye Olde Pi Shoppe
- For sale
- Wanted
- Off topic
- Off topic discussion