ESP32-S3 Baremetal Support

led.c

Addressable RGB LED (WS2812 / WS2812B) driver, bit-banged on one GPIO.

An addressable LED has no clock line: each bit is a high pulse whose *length* says whether it is a zero or a one, and the whole frame has to go out without a gap. This driver times both edges from the CPU cycle counter and masks interrupts for the duration of a frame, which is about 30 us per pixel.

Colours go in as plain RGB; the GRB wire order the part actually wants is this driver's problem, not the caller's.

led_init(21);
led_set_color(LED_RGB(32, 5, 1));

One strip at a time. led_init() picks which pin that is.

Requires a CPU fast enough to resolve a 350 ns pulse from an 800 ns one - call set_cpu_160mhz() first. At the 20 MHz the ROM leaves behind, every bit reads as a one and the LED simply shows the wrong colour.

API

Declared in led.h

Types

typedef struct {
    uint8_t red;
    uint8_t green;
    uint8_t blue;
} led_color_t;

Functions

void led_init(uint32_t data_pin);

Configures the data pin and drives it low. Call once before writing.

data_pin
GPIO number wired to the LED data line
returns
nothing
void led_write(const led_color_t *pixels, uint32_t pixel_count);

Sends one frame: pixel 0 is the LED nearest the controller.

pixels
pointer to an array of colors, one per LED
pixel_count
number of pixels in the array
returns
nothing
void led_set_color(led_color_t color);

Shows a single color on a one-LED board.

color
the color to display
returns
nothing

Constants

#define LED_RGB(r, g, b) ((led_color_t){ (uint8_t)(r), (uint8_t)(g), (uint8_t)(b) })

Usable anywhere a led_color_t is: as an argument, or as an initialiser.

#define LED_OFF LED_RGB(0, 0, 0)
#define LED_RED LED_RGB(10, 0, 0)
#define LED_GREEN LED_RGB(0, 10, 0)
#define LED_BLUE LED_RGB(0, 0, 10)
#define LED_WHITE LED_RGB(10, 10, 10)

Source

board/led.c
board/led.c
84 lines
/*
 * Addressable RGB LED (WS2812 / WS2812B) driver, bit-banged on one GPIO.
 */

#include "led.h"
#include "esp32s3_clock.h"
#include "esp32s3_delay.h"
#include "esp32s3_gpio.h"

// Wire timing, in nanoseconds, from the WS2812B datasheet. Each bit is a high
// phase followed by a low phase; only the split between them carries data.
#define LED_T0H_NS       350
#define LED_T0_BIT_NS    (350 + 900)
#define LED_T1H_NS       800
#define LED_T1_BIT_NS    (800 + 450)
#define LED_RESET_US     300     // WS2812B-V5 wants >280 us of idle low

// Nanoseconds -> CPU cycles at the current clock, rounded up.
#define LED_CYCLES(ns)   (((ns) * cpu_mhz() + 999u) / 1000u)

// Cycle counts for one bit, worked out once per frame.
typedef struct {
    uint32_t zero_high;
    uint32_t zero_period;
    uint32_t one_high;
    uint32_t one_period;
} led_timing_t;

static uint32_t led_pin;

void led_init(uint32_t data_pin)
{
    led_pin = data_pin;
    gpio_config_output(data_pin);
}

// One byte, most significant bit first.
static void led_send_byte(uint8_t byte, const led_timing_t *timing)
{
    for (int bit = 0; bit < 8; bit++) {
        uint32_t high   = (byte & 0x80) ? timing->one_high   : timing->zero_high;
        uint32_t period = (byte & 0x80) ? timing->one_period : timing->zero_period;
        byte <<= 1;

        // Both edges are timed from one base, so the bit period cannot drift
        // even if the high phase overshoots slightly.
        uint32_t start = cycle_count();
        gpio_set_high(led_pin);
        while (cycle_count() - start < high)   { }
        gpio_set_low(led_pin);
        while (cycle_count() - start < period) { }
    }
}

void led_write(const led_color_t *pixels, uint32_t pixel_count)
{
    const led_timing_t timing = {
        .zero_high   = LED_CYCLES(LED_T0H_NS),
        .zero_period = LED_CYCLES(LED_T0_BIT_NS),
        .one_high    = LED_CYCLES(LED_T1H_NS),
        .one_period  = LED_CYCLES(LED_T1_BIT_NS),
    };
    uint32_t saved_ps;

    // No clock line means one interrupt mid-frame corrupts the data, so mask
    // everything below the exception level until the frame is out.
    __asm__ __volatile__("rsil %0, 3" : "=a"(saved_ps) :: "memory");

    for (uint32_t i = 0; i < pixel_count; i++) {
        led_send_byte(pixels[i].green, &timing);     // GRB on the wire
        led_send_byte(pixels[i].red,   &timing);
        led_send_byte(pixels[i].blue,  &timing);
    }

    __asm__ __volatile__("wsr.ps %0; rsync" :: "a"(saved_ps) : "memory");

    gpio_set_low(led_pin);
    delay_us(LED_RESET_US);         // latch the frame
}

void led_set_color(led_color_t color)
{
    led_write(&color, 1);
}
include/led.hโ€” the header, in full
include/led.h
59 lines
/*
 * Addressable RGB LED (WS2812 / WS2812B) driver, bit-banged on one GPIO.
 *
 * An addressable LED has no clock line: each bit is a high pulse whose
 * *length* says whether it is a zero or a one, and the whole frame has to go
 * out without a gap. This driver times both edges from the CPU cycle counter
 * and masks interrupts for the duration of a frame, which is about 30 us per
 * pixel.
 *
 * Colours go in as plain RGB; the GRB wire order the part actually wants is
 * this driver's problem, not the caller's.
 *
 *     led_init(21);
 *     led_set_color(LED_RGB(32, 5, 1));
 *
 * One strip at a time. led_init() picks which pin that is.
 *
 * Requires a CPU fast enough to resolve a 350 ns pulse from an 800 ns one -
 * call set_cpu_160mhz() first. At the 20 MHz the ROM leaves behind, every bit
 * reads as a one and the LED simply shows the wrong colour.
 */

#ifndef LED_H
#define LED_H

#include <stdint.h>

typedef struct {
    uint8_t red;
    uint8_t green;
    uint8_t blue;
} led_color_t;

// Usable anywhere a led_color_t is: as an argument, or as an initialiser.
#define LED_RGB(r, g, b) ((led_color_t){ (uint8_t)(r), (uint8_t)(g), (uint8_t)(b) })

#define LED_OFF      LED_RGB(0, 0, 0)
#define LED_RED      LED_RGB(10, 0, 0)
#define LED_GREEN    LED_RGB(0, 10, 0)
#define LED_BLUE     LED_RGB(0, 0, 10)
#define LED_WHITE    LED_RGB(10, 10, 10)

// Configures the data pin and drives it low. Call once before writing.
// - data_pin: GPIO number wired to the LED data line
// returns: nothing
void led_init(uint32_t data_pin);

// Sends one frame: pixel 0 is the LED nearest the controller.
// - pixels: pointer to an array of colors, one per LED
// - pixel_count: number of pixels in the array
// returns: nothing
void led_write(const led_color_t *pixels, uint32_t pixel_count);

// Shows a single color on a one-LED board.
// - color: the color to display
// returns: nothing
void led_set_color(led_color_t color);

#endif // LED_H