Skip to content

Instantly share code, notes, and snippets.

@sutaburosu
Last active January 6, 2025 18:11
Show Gist options
  • Save sutaburosu/89920c71a9635fec595e243fa6e7360e to your computer and use it in GitHub Desktop.
Save sutaburosu/89920c71a9635fec595e243fa6e7360e to your computer and use it in GitHub Desktop.
Interactively experiment with timings of WS281x LEDs
/*
* @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