Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some improvements to make the WS281x output visually glitch-free even… #13

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 201 additions & 67 deletions main/ws2812.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* signals sent to the WS2812 LEDs.
*
* This code is placed in the public domain (or CC0 licensed, at your option).
*
* Adapted to p44utils context and made flicker-free 2020 by Lukas Zeller <[email protected]>
*
*/

#include "ws2812.h"
Expand All @@ -19,6 +22,10 @@
#include <stdio.h>
#include <stdlib.h>
#include <driver/rmt.h>
#include "esp_log.h"
#include "esp_timer.h"

static const char* TAG = "ws2812";

#define ETS_RMT_CTRL_INUM 18
#define ESP_RMT_CTRL_DISABLE ESP_RMT_CTRL_DIABLE /* Typo in esp_intr.h */
Expand All @@ -27,13 +34,22 @@
#define DURATION 12.5 /* minimum time of a single RMT duration
in nanoseconds based on clock */

#define PULSE_T0H ( 350 / (DURATION * DIVIDER));
#define PULSE_T1H ( 900 / (DURATION * DIVIDER));
#define PULSE_T0L ( 900 / (DURATION * DIVIDER));
#define PULSE_T1L ( 350 / (DURATION * DIVIDER));
#define PULSE_TRS (50000 / (DURATION * DIVIDER));
#define PULSE_T0H 350
#define PULSE_T0L 900
#define PULSE_T1H 900
#define PULSE_T1L 350
#define PULSE_TRS 50000

#define PULSE_TO_RMTDELAY(t) ((uint16_t)((double)t/(DURATION*DIVIDER)))

#define MAX_PULSES_RELOAD_TIME_US (MAX_PULSES*(PULSE_T0H+PULSE_T1H)/1000)

#define TIMING_DEBUG 0 // if set, console shows some statistics about reload delay (IRQ response), retries and errors
#define GPIO_LOGICANALYZER_OUTPUT 0 // if set, GPIO 21 and 22 output additional signals to catch reload timing and slow IRQs with a logic analyzer

#define MEM_BLOCKS_PER_CHANNEL 1 // how many mem blocks to use. Can be set to max 8 when no other RMT unit is in use

#define MAX_PULSES 32
#define MAX_PULSES (32*MEM_BLOCKS_PER_CHANNEL) // number of pulses per half-block

#define RMTCHANNEL 0

Expand All @@ -53,12 +69,26 @@ static xSemaphoreHandle ws2812_sem = NULL;
static intr_handle_t rmt_intr_handle = NULL;
static rmtPulsePair ws2812_bits[2];


#define MAX_RESEND_RETRIES 3 // number of retries in case of timing fail, 0=none
static int retries = 0;
#if TIMING_DEBUG
static int64_t timeOfLastLoad = 0;
static int64_t minReloadTime = 1000000; // should be below one second
static int64_t maxReloadTime = 0;
static int totalRetries = 0;
static int totalErrors = 0;
#endif




void ws2812_initRMTChannel(int rmtChannel)
{
RMT.apb_conf.fifo_mask = 1; //enable memory access, instead of FIFO mode.
RMT.apb_conf.mem_tx_wrap_en = 1; //wrap around when hitting end of buffer
RMT.conf_ch[rmtChannel].conf0.div_cnt = DIVIDER;
RMT.conf_ch[rmtChannel].conf0.mem_size = 1;
RMT.conf_ch[rmtChannel].conf0.mem_size = MEM_BLOCKS_PER_CHANNEL;
RMT.conf_ch[rmtChannel].conf0.carrier_en = 0;
RMT.conf_ch[rmtChannel].conf0.carrier_out_lv = 1;
RMT.conf_ch[rmtChannel].conf0.mem_pd = 0;
Expand All @@ -73,60 +103,161 @@ void ws2812_initRMTChannel(int rmtChannel)
return;
}


// copy half the RMT transmit buffer (MAX_PULSES number of pulses)
// each of the 8 RMT channel has a buffer for 512/8 = 64 pulses, so half normally is 32 pulses @ MEM_BLOCKS_PER_CHANNEL==1
// in addition, a safety stop (for when IRQ is delayed too long) is placed in the first pulse of the *other* half buffer
void ws2812_copy()
{
unsigned int i, j, offset, len, bit;

unsigned int i, j, offset, len, ledbyte;

offset = ws2812_half * MAX_PULSES;
ws2812_half = !ws2812_half;
#if TIMING_DEBUG
int64_t now = esp_timer_get_time();
#endif

len = ws2812_len - ws2812_pos;
if (len > (MAX_PULSES / 8))
len = (MAX_PULSES / 8);
offset = ws2812_half * MAX_PULSES; // alternating offset to beginning or middle of RMT tx buffer
ws2812_half = !ws2812_half; // alternate

if (!len) {
for (i = 0; i < MAX_PULSES; i++)
RMTMEM.chan[RMTCHANNEL].data32[i + offset].val = 0;
return;
len = ws2812_len - ws2812_pos; // remaining bytes to send
if (len > (MAX_PULSES / 8)) {
len = (MAX_PULSES / 8); // limit to an even number of bytes
}

// convert len bytes to pulses (if any)
for (i = 0; i < len; i++) {
bit = ws2812_buffer[i + ws2812_pos];
for (j = 0; j < 8; j++, bit <<= 1) {
ledbyte = ws2812_buffer[i + ws2812_pos]; // get the byte
for (j = 0; j < 8; j++, ledbyte <<= 1) {
// set the high and low pulse part of this bit (from ws2812_bits[] template)
RMTMEM.chan[RMTCHANNEL].data32[j + i * 8 + offset].val =
ws2812_bits[(bit >> 7) & 0x01].val;
ws2812_bits[(ledbyte >> 7) & 0x01].val;
}
// modify the duration of the last low pulse to become reset if this was the last byte
if (i + ws2812_pos == ws2812_len - 1) {
RMTMEM.chan[RMTCHANNEL].data32[7 + i * 8 + offset].duration1 = PULSE_TO_RMTDELAY(PULSE_TRS);
}
if (i + ws2812_pos == ws2812_len - 1)
RMTMEM.chan[RMTCHANNEL].data32[7 + i * 8 + offset].duration1 = PULSE_TRS;
}

for (i *= 8; i < MAX_PULSES; i++)
RMTMEM.chan[RMTCHANNEL].data32[i + offset].val = 0;

ws2812_pos += len;
// fill remaining pulses in this half block with TX end markers
for (i *= 8; i < MAX_PULSES; i++) {
RMTMEM.chan[RMTCHANNEL].data32[i + offset].val = 0; // TX end marker
}
ws2812_pos += len; // update pointer
// Now assuming (quite safely, as IRQ response time<2uS is impossible) that the first pulse of
// the other (now running) block half is already out by now, overwrite it with a
// reset-length 0 and a stopper.
// In case the next IRQ is late and has NOT been able to re-fill that block, output will
// stop without sending wrong byte data and causing visual glitches.
// However if IRQ is in time, it will overwrite that stopper with more valid data.
RMTMEM.chan[RMTCHANNEL].data32[ws2812_half*MAX_PULSES].val = PULSE_TO_RMTDELAY(PULSE_TRS); // <<16; // first a 0 with reset length, then stop
#if TIMING_DEBUG
if (timeOfLastLoad>0 && ws2812_pos<ws2812_len) {
// still sending, update timing stats
int64_t reloadTime = now-timeOfLastLoad;
if (reloadTime>maxReloadTime) maxReloadTime = reloadTime;
if (reloadTime<minReloadTime) minReloadTime = reloadTime;
}
timeOfLastLoad = now;
#endif
return;
}


void start_transfer()
{
#if GPIO_LOGICANALYZER_OUTPUT
gpio_set_level(22, 1);
gpio_set_level(21, 1);
#endif
#if TIMING_DEBUG
timeOfLastLoad = 0;
#endif
// init buffer pointers
ws2812_pos = 0;
ws2812_half = 0;
// copy at least one half of data
ws2812_copy(); // include a stopper (in case ws2812_len is exactly one half)
// start RMT now
// - note we must disable IRQs on this core completely to avoid starting RMT and then copying next data
// is not *delayed* by a long duration IRQ routine. Note that this blocking is *not* because of
// access to shared data (for which single core IRQ block would not help)!
portDISABLE_INTERRUPTS();
RMT.conf_ch[RMTCHANNEL].conf1.mem_rd_rst = 1;
RMT.conf_ch[RMTCHANNEL].conf1.tx_start = 1;
// - safely assuming RMT engine will have sent the first pulse long before we are done filling the second half
// fill the second half ALSO including a stopper overwriting the first pulse of the first half.
// This way, if the first THR-IRQ is too late, data will stop after two halves, avoiding send
// of old data in the first half a second time.
// If THR-IRQ is in time, it will overwrite the stopper with new data before RMT runs into it
ws2812_copy(); // include a stopper
portENABLE_INTERRUPTS();
}


void ws2812_handleInterrupt(void *arg)
{
portBASE_TYPE taskAwoken = 0;


if (RMT.int_st.ch0_tx_thr_event) {
ws2812_copy();
RMT.int_clr.ch0_tx_thr_event = 1;
}
else if (RMT.int_st.ch0_tx_end && ws2812_sem) {
// must check stop event first, in case we missed the tx threshold IRQ
if (RMT.int_st.ch0_tx_end) {
// end of transmission, transmitter entered idle state
if (ws2812_pos<ws2812_len) {
#if GPIO_LOGICANALYZER_OUTPUT
gpio_set_level(22, 0);
#endif
// stop has occurred (because of IRQ delay) before all data was out
if (retries<MAX_RESEND_RETRIES) {
// restart transmission
retries++;
#if TIMING_DEBUG
totalRetries++;
#endif
RMT.int_clr.ch0_tx_thr_event = 1; // first clear the THR IRQ, in case it is pending already
RMT.int_clr.ch0_tx_end = 1; // ack end IRQ
start_transfer();
return;
}
else {
#if TIMING_DEBUG
totalErrors++;
#endif
}
}
#if GPIO_LOGICANALYZER_OUTPUT
gpio_set_level(21, 0);
#endif
// - get rid of old memory buffer
free(ws2812_buffer);
// - unlock ws2812_setColors() again
xSemaphoreGiveFromISR(ws2812_sem, &taskAwoken);
RMT.int_clr.ch0_tx_end = 1;
RMT.int_clr.ch0_tx_thr_event = 1; // first clear the THR IRQ, in case it is pending already
RMT.int_clr.ch0_tx_end = 1; // ack IRQ
}
else if (RMT.int_st.ch0_tx_thr_event) {
// sent until middle of buffer (tx threshold)
RMT.int_clr.ch0_tx_thr_event = 1; // ack IRQ
ws2812_copy(); // copy new data into now-free part of buffer
}

return;
}


void ws2812_init(int gpioNum)
{
#if GPIO_LOGICANALYZER_OUTPUT
gpio_pad_select_gpio(22);
gpio_set_direction(22, GPIO_MODE_DEF_OUTPUT);
gpio_set_level(22, 0);
gpio_pad_select_gpio(21);
gpio_set_direction(21, GPIO_MODE_DEF_OUTPUT);
gpio_set_level(21, 0);
#endif


// semaphore for locking buffer
ws2812_sem = xSemaphoreCreateBinary(); // semaphore is created taken...
xSemaphoreGive(ws2812_sem); // ...so to begin, give it, so ws2812_setColors() can start sending

// prepare HW
DPORT_SET_PERI_REG_MASK(DPORT_PERIP_CLK_EN_REG, DPORT_RMT_CLK_EN);
DPORT_CLEAR_PERI_REG_MASK(DPORT_PERIP_RST_EN_REG, DPORT_RMT_RST);

Expand All @@ -138,52 +269,55 @@ void ws2812_init(int gpioNum)
RMT.int_ena.ch0_tx_thr_event = 1;
RMT.int_ena.ch0_tx_end = 1;

// template for 0 and 1 bit pattern
ws2812_bits[0].level0 = 1;
ws2812_bits[0].level1 = 0;
ws2812_bits[0].duration0 = PULSE_T0H;
ws2812_bits[0].duration1 = PULSE_T0L;
ws2812_bits[0].duration0 = PULSE_TO_RMTDELAY(PULSE_T0H);
ws2812_bits[0].duration1 = PULSE_TO_RMTDELAY(PULSE_T0L);
ws2812_bits[1].level0 = 1;
ws2812_bits[1].level1 = 0;
ws2812_bits[1].duration0 = PULSE_T1H;
ws2812_bits[1].duration1 = PULSE_T1L;
ws2812_bits[1].duration0 = PULSE_TO_RMTDELAY(PULSE_T1H);
ws2812_bits[1].duration1 = PULSE_TO_RMTDELAY(PULSE_T1L);

esp_intr_alloc(ETS_RMT_INTR_SOURCE, 0, ws2812_handleInterrupt, NULL, &rmt_intr_handle);

return;
}


void ws2812_setColors(unsigned int length, rgbVal *array)
{
unsigned int i;


ws2812_len = (length * 3) * sizeof(uint8_t);
ws2812_buffer = malloc(ws2812_len);

for (i = 0; i < length; i++) {
ws2812_buffer[0 + i * 3] = array[i].g;
ws2812_buffer[1 + i * 3] = array[i].r;
ws2812_buffer[2 + i * 3] = array[i].b;
if (xSemaphoreTake(ws2812_sem, 0)) {
#if TIMING_DEBUG
if (timeOfLastLoad>0) {
ESP_LOGI(TAG,
"reload time = %lld..%lld uS (theoretically %d), retries=%d, totalRetries=%d, totalErrors=%d",
minReloadTime, maxReloadTime, MAX_PULSES_RELOAD_TIME_US,
retries, totalRetries, totalErrors
);
}
minReloadTime = 1000000; // should be below one second
maxReloadTime = 0;
#endif
retries = 0;

// ready for new data
// - create output buffer
ws2812_len = (length * 3) * sizeof(uint8_t);
ws2812_buffer = malloc(ws2812_len);
// - fill output buffer
for (i = 0; i < length; i++) {
ws2812_buffer[0 + i * 3] = array[i].g;
ws2812_buffer[1 + i * 3] = array[i].r;
ws2812_buffer[2 + i * 3] = array[i].b;
}
// - start transferring the buffer
start_transfer();
}
else {
ESP_LOGW(TAG, "ws2812_setColors called again too soon");
}

ws2812_pos = 0;
ws2812_half = 0;

ws2812_copy();

if (ws2812_pos < ws2812_len)
ws2812_copy();

ws2812_sem = xSemaphoreCreateBinary();

RMT.conf_ch[RMTCHANNEL].conf1.mem_rd_rst = 1;
RMT.conf_ch[RMTCHANNEL].conf1.tx_start = 1;

xSemaphoreTake(ws2812_sem, portMAX_DELAY);
vSemaphoreDelete(ws2812_sem);
ws2812_sem = NULL;

free(ws2812_buffer);

return;
}
2 changes: 2 additions & 0 deletions main/ws2812.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* This is a driver for the WS2812 RGB LEDs using the RMT peripheral on the ESP32.
*
* This code is placed in the public domain (or CC0 licensed, at your option).
*
* Adapted to p44utils context and made flicker-free 2020 by Lukas Zeller <[email protected]>
*/

#ifndef WS2812_DRIVER_H
Expand Down