Last active
January 6, 2025 18:11
-
-
Save sutaburosu/89920c71a9635fec595e243fa6e7360e to your computer and use it in GitHub Desktop.
Interactively experiment with timings of WS281x LEDs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* @brief A tool to interactively experiment with timings of WS281x LEDs | |
* | |
* @warning Only one LED should be lit at any moment, *BUT* the nature of | |
* overclocking means it's possible that all your LEDs will display full | |
* intensity white light. This has happened to me several times already. | |
* Be ready to unplug things. Ensure proper power supply and cooling. | |
* | |
* Connect using picocom, PuTTY or similar to adjust timings in real time. | |
* Pressing 'o' will overclock all four timings by 1 tick (12.5ns). | |
* | |
* Key controls: | |
* ! - reset all timings to defaults | |
* q a - increase/decrease T0H by 1 tick (12.5ns) | |
* Q A - increase/decrease T0H by 5 ticks (62.5ns) | |
* w s - --------"-------- T0L ---------"--------- | |
* e d - --------"-------- T1H ---------"--------- | |
* r f - --------"-------- T1L ---------"--------- | |
* t g - --------"-------- Treset ------"--------- | |
* o l - increase/decrease overclock by 1 tick (derived from default timings) | |
* + - - increase/decrease the number of LEDs | |
* | |
* @note The one 256 LED panel I tested works well with these timings: | |
* T0H: 225ns T0L: 500ns T1H: 500ns T1L: 225ns | |
* T0: 725ns T1: 725ns Treset: 25µs | |
* 4.50ms/show 17.58µs/LED 1365.06kbps 220.69Hz | |
* | |
* Other H/L ratios work too, but the total time per bit must be >= 725ns. | |
* For reliability I would bump this up to 750ns and use 250ns/500ns. | |
* 4.65ms/show 18.18µs/LED 1320.07kbps 213.54Hz | |
* That's a 65% overclock. | |
*/ | |
#include <Arduino.h> | |
#ifndef NUM_LEDS | |
#define NUM_LEDS 256 | |
#endif | |
#ifndef RGBLED_PIN1 | |
#define RGBLED_PIN1 14 | |
#endif | |
// The color to send to each LED | |
uint32_t colour = 0x4080ff; // 0xGGRRBB | |
// Default timings | |
const uint16_t T0H_ns = 400; | |
const uint16_t T0L_ns = 800; | |
const uint16_t T1H_ns = 800; | |
const uint16_t T1L_ns = 400; | |
const uint16_t Treset_us = 50; | |
// Each LED has 24 bits of GRB data | |
#define BIT_DEPTH 24 | |
//////////////////////////////////////////////////////////////////////// | |
#define NUM_BITS (BIT_DEPTH * NUM_LEDS) | |
// Each bit of RGB data is expanded to 32 bits of RMT data | |
// so this is a very memory hungry approach. | |
rmt_data_t rmt_data[NUM_BITS]; | |
// For better performance we cache the RMT values for a set bit and a clear bit | |
rmt_data_t rmt_cache[2]; | |
const int rmt_clock_mhz = 80; | |
const float rmt_tick_ns = 1000.f / rmt_clock_mhz; | |
int overclock = 0; | |
uint16_t T0H_ticks, T0L_ticks, T1H_ticks, T1L_ticks, Treset; | |
uint16_t num_leds = NUM_LEDS; | |
uint16_t num_bits = num_leds * BIT_DEPTH; | |
void print_timings(); | |
bool handle_serial_input(); | |
void update_timings(bool clear = true); | |
void reset_timings(); | |
void setup() | |
{ | |
Serial.begin(115200); | |
Serial.println("\r\n - ESP32 IDF5 RMT WS2812 overclock playground -\r\n"); | |
reset_timings(); | |
update_timings(); | |
print_timings(); | |
if (!rmtInit(RGBLED_PIN1, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, rmt_clock_mhz * 1e6)) | |
Serial.println("init sender 0 failed\n"); | |
} | |
void show() | |
{ | |
rmtWrite(RGBLED_PIN1, rmt_data, num_bits, RMT_WAIT_FOR_EVER); | |
} | |
void loop() | |
{ | |
static int led_index = 0; // which LED will be lit on this frame | |
static uint64_t total_us = 0; // accumulated time for show()s | |
static uint64_t last_print = 0; // performance stats print() time | |
static uint64_t reset_us = 0; // when Treset is done | |
static int reps = 0; // wraps of led_index since last print | |
// Reset the performance counters when timings have changed | |
if (handle_serial_input()) | |
led_index = total_us = reps = last_print = 0; | |
if (!last_print) | |
last_print = micros(); | |
// Copy the colour to the RMT data for a single LED | |
rmt_data_t *rmtbit = rmt_data + led_index * BIT_DEPTH; | |
for (uint32_t mask = 1 << (BIT_DEPTH - 1); mask; mask >>= 1) | |
*rmtbit++ = rmt_cache[bool(colour & mask)]; | |
// Wait for Treset to complete before sending this frame | |
uint64_t time_us = micros(); | |
if (reset_us > time_us) | |
delayMicroseconds(reset_us - time_us); | |
// Send the RMT data and time how long it takes | |
total_us -= micros(); | |
show(); | |
time_us = micros(); | |
total_us += time_us; | |
reset_us = time_us + Treset; | |
// Clear this LED | |
rmtbit = rmt_data + led_index * BIT_DEPTH; | |
for (uint8_t bit = 0; bit < BIT_DEPTH; bit++) | |
*rmtbit++ = rmt_cache[0]; | |
// Move to the next LED and wrap at the end | |
if (++led_index < num_leds) | |
return; | |
led_index = 0; | |
// print the timing statistics at most once per second | |
reps++; | |
if (micros() - 1000000 < last_print) | |
return; | |
float div = float(num_leds) * reps; | |
Serial.print(float(total_us) / 1000.f / div); | |
Serial.print("ms/show\t"); | |
Serial.print(float(total_us) / num_leds / div); | |
Serial.print("µs/LED\t"); | |
Serial.print(1000.f * num_bits / (total_us / div)); | |
Serial.print("kbps\t"); | |
Serial.print(1000000.f / (micros() - last_print) * div); | |
Serial.println("Hz"); | |
total_us = 0; | |
reps = 0; | |
last_print = micros(); | |
} | |
// calculate the number of RMT ticks for the high and low durations | |
void reset_timings() | |
{ | |
T0H_ticks = T0H_ns / rmt_tick_ns - overclock; | |
T0L_ticks = T0L_ns / rmt_tick_ns - overclock; | |
T1H_ticks = T1H_ns / rmt_tick_ns - overclock; | |
T1L_ticks = T1L_ns / rmt_tick_ns - overclock; | |
Treset = Treset_us; | |
} | |
// update cached values for 0 and 1 bits and clear the framebuffer | |
void update_timings(bool clear) | |
{ | |
num_bits = num_leds * BIT_DEPTH; | |
rmt_cache[0].level0 = 1; | |
rmt_cache[0].duration0 = T0H_ticks; | |
rmt_cache[0].level1 = 0; | |
rmt_cache[0].duration1 = T0L_ticks; | |
rmt_cache[1].level0 = 1; | |
rmt_cache[1].duration0 = T1H_ticks; | |
rmt_cache[1].level1 = 0; | |
rmt_cache[1].duration1 = T1L_ticks; | |
// fill framebuffer with the new value for a 0 bit | |
if (clear) | |
for (int bitnr = 0; bitnr < NUM_BITS; bitnr++) | |
rmt_data[bitnr] = rmt_cache[0]; | |
} | |
void print_timings() | |
{ | |
Serial.print("T0H: "); | |
Serial.print(int(T0H_ticks * rmt_tick_ns)); | |
Serial.print("ns\tT0L: "); | |
Serial.print(int(T0L_ticks * rmt_tick_ns)); | |
Serial.print("ns\tT1H: "); | |
Serial.print(int(T1H_ticks * rmt_tick_ns)); | |
Serial.print("ns\tT1L: "); | |
Serial.print(int(T1L_ticks * rmt_tick_ns)); | |
Serial.println("ns"); | |
Serial.print("T0: "); | |
Serial.print(int((T0H_ticks + T0L_ticks) * rmt_tick_ns)); | |
Serial.print("ns\tT1: "); | |
Serial.print(int((T1H_ticks + T1L_ticks) * rmt_tick_ns)); | |
Serial.print("ns\tTreset: "); | |
Serial.print(Treset); | |
Serial.print("µs\tnum_leds: "); | |
Serial.println(num_leds); | |
} | |
bool handle_serial_input() | |
{ | |
bool reset = true; | |
if (!Serial.available()) | |
return false; | |
char input = Serial.read(); | |
int increment = 1; | |
if (input >= 'A' && input <= 'Z') | |
increment = 5, input += 32; // force to lowercase | |
switch (input) | |
{ | |
case 'q': | |
T0H_ticks += increment; | |
break; | |
case 'a': | |
T0H_ticks -= increment; | |
break; | |
case 'w': | |
T0L_ticks += increment; | |
break; | |
case 's': | |
T0L_ticks -= increment; | |
break; | |
case 'e': | |
T1H_ticks += increment; | |
break; | |
case 'd': | |
T1H_ticks -= increment; | |
break; | |
case 'r': | |
T1L_ticks += increment; | |
break; | |
case 'f': | |
T1L_ticks -= increment; | |
break; | |
case 't': | |
Treset += increment; | |
break; | |
case 'g': | |
Treset -= increment; | |
break; | |
case 'o': | |
overclock += increment; | |
reset_timings(); | |
break; | |
case 'l': | |
overclock -= increment; | |
reset_timings(); | |
break; | |
case '!': | |
overclock = 0; | |
reset_timings(); // reset to default timings | |
break; | |
case '=': | |
if (num_leds < NUM_LEDS) | |
num_leds++; | |
break; | |
case '+': | |
if (num_leds + 5 <= NUM_LEDS) | |
num_leds += 5; | |
else | |
num_leds = NUM_LEDS; | |
break; | |
case '-': | |
show(); // turn off the final LED to prevent ghosts | |
if (num_leds > 1) | |
num_leds--; | |
break; | |
case '_': | |
show(); // turn off the final LED to prevent ghosts | |
if (num_leds > 5) | |
num_leds -= 5; | |
else | |
num_leds = 1; | |
break; | |
default: | |
reset = false; | |
break; | |
} | |
num_bits = num_leds * BIT_DEPTH; | |
update_timings(); | |
print_timings(); | |
return reset; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment