diff options
| author | Josh Rahm <joshuarahm@gmail.com> | 2025-12-07 15:37:04 -0700 |
|---|---|---|
| committer | Josh Rahm <joshuarahm@gmail.com> | 2025-12-07 15:37:04 -0700 |
| commit | c9efee6f3ffcf19e139d72d946aa4d837bbcbb9e (patch) | |
| tree | dc1c5c4c78e9758140029118a7f7c969703572b5 | |
| parent | b2b344ebec8f7b55506deb0c2138a1e2b9ffeb46 (diff) | |
| download | esp32-ws2812b-c9efee6f3ffcf19e139d72d946aa4d837bbcbb9e.tar.gz esp32-ws2812b-c9efee6f3ffcf19e139d72d946aa4d837bbcbb9e.tar.bz2 esp32-ws2812b-c9efee6f3ffcf19e139d72d946aa4d837bbcbb9e.zip | |
Better timing.
No longer hammering the SPI bus, but rather being more controlled, and
interpolation is handled better.
| -rw-r--r-- | include/param.h | 1 | ||||
| -rw-r--r-- | include/state_params.i | 7 | ||||
| -rw-r--r-- | main/main.c | 49 | ||||
| -rw-r--r-- | main/param/int32_t.c | 43 | ||||
| -rw-r--r-- | main/station.c | 3 | ||||
| -rw-r--r-- | main/ws2812b_writer.c | 209 |
6 files changed, 210 insertions, 102 deletions
diff --git a/include/param.h b/include/param.h index b66b1b1..5259e0a 100644 --- a/include/param.h +++ b/include/param.h @@ -25,3 +25,4 @@ DECL_PARAMETER_INSTANCE(bool); DECL_PARAMETER_INSTANCE(uint32_t); DECL_PARAMETER_INSTANCE(color_int_t); DECL_PARAMETER_INSTANCE(int); +DECL_PARAMETER_INSTANCE(int32_t); diff --git a/include/state_params.i b/include/state_params.i index 875328d..e768768 100644 --- a/include/state_params.i +++ b/include/state_params.i @@ -1,8 +1,10 @@ +// version 1 + // List of parameters. // type name display name, default value -STATE_PARAM(uint32_t, time, "Current Time", 0) -STATE_PARAM(int, timetick, "Speed", 100) +STATE_PARAM(int32_t, time, "Current Time", 0) +STATE_PARAM(int, speed, "Speed", 1000) STATE_PARAM(uint8_t, brightness, "Brightness", 50) STATE_PARAM(uint8_t, n_snow, "Snow Effect", 10) STATE_PARAM(uint8_t, n_red, "Desaturation", 32) @@ -14,3 +16,4 @@ STATE_PARAM(bool, use_static_color, "Use Static Color", false) STATE_PARAM(color_int_t, static_color, "Static Color", 0xffddbb) STATE_PARAM(bool, twinkle, "Twinkle", true) STATE_PARAM(uint8_t, twink_amt, "Twinkle Amount", 0) +STATE_PARAM(uint8_t, base_brightness, "Base Brightness", 0) diff --git a/main/main.c b/main/main.c index c57c414..3248b93 100644 --- a/main/main.c +++ b/main/main.c @@ -48,28 +48,6 @@ static ws_params_t ws_params; -/* Ticks the clock at a constant rate independent of the time it takes to write - * to the led strip. */ -portTASK_FUNCTION(time_keeper, params_) -{ - ws_params_t* params = (ws_params_t*)params_; - - TickType_t last_wake; - const TickType_t freq = 1; - - // Initialise the xLastWakeTime variable with the current time. - last_wake = xTaskGetTickCount(); - - for (;;) { - // Wait for the next cycle. - vTaskDelayUntil(&last_wake, freq); - if (xSemaphoreTake(params->state_lock, portMAX_DELAY) == pdTRUE) { - params->state.time += params->state.timetick; - xSemaphoreGive(params->state_lock); - } - } -} - void app_main(void) { esp_err_t error; @@ -116,30 +94,23 @@ void app_main(void) wifi_init_station("Wort", "JoshIsBau5"); ESP_LOGI("main", "Complete!"); - if (xSemaphoreTake(ws_params.state_lock, portMAX_DELAY) == pdTRUE) { - esp_err_t load_err = ws_state_load(&ws_params.state); - if (load_err == ESP_OK) { - ESP_LOGI("main", "Restored state from NVS"); - } else { - ESP_LOGI("main", "No saved state (%d); using defaults", load_err); - } - xSemaphoreGive(ws_params.state_lock); - } + // if (xSemaphoreTake(ws_params.state_lock, portMAX_DELAY) == pdTRUE) { + // esp_err_t load_err = ws_state_load(&ws_params.state); + // if (load_err == ESP_OK) { + // ESP_LOGI("main", "Restored state from NVS"); + // } else { + // ESP_LOGI("main", "No saved state (%d); using defaults", load_err); + // } + // xSemaphoreGive(ws_params.state_lock); + // } - xTaskCreate( - time_keeper, - "time_keeper", - /* stack_depth = */ 1024, - &ws_params, - /* priority = */ 1, - NULL); /* Pin the LED writer to core 1 to avoid preemption by Wi-Fi/IDF tasks. */ xTaskCreatePinnedToCore( ws2812b_write_task, "ws2812b_writer", /* stack_depth = */ 4096, &ws_params, - /* priority = */ 1, + /* priority = */ 4, NULL, /* core_id = */ 1); xTaskCreate( diff --git a/main/param/int32_t.c b/main/param/int32_t.c new file mode 100644 index 0000000..ebf9d4e --- /dev/null +++ b/main/param/int32_t.c @@ -0,0 +1,43 @@ +#include "param.h" +#include "http_server.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +void handle_option__int32_t( + const char* attr, sockbuf_t* sockbuf, int32_t* ptr) +{ + int tmp = (int)(*ptr); + handle_option__int(attr, sockbuf, &tmp); + *ptr = (int32_t)tmp; +} + +size_t serialize_to_json__int32_t( + char** out, size_t len, const char* attr, const char* dn, int32_t value) +{ + size_t newlen = snprintf( + *out, + len, + "\"%s\":{\"type\":\"int\",\"value\":\"%ld\",\"disp\":\"%s\"}", + attr, + (long)value, + dn); + *out += newlen; + return len - newlen; +} + +#define TAG "param_int32" +void http_handle__int32_t(httpd_req_t* req, int32_t* val) +{ + int tmp = (int)(*val); + http_handle__int(req, &tmp); + *val = (int32_t)tmp; +} + +void print__int32_t(sockbuf_t* sockbuf, const char* attr, int32_t val) +{ + char buf[128]; + snprintf(buf, sizeof(buf), "%s: %ld :: int32_t\n", attr, (long)val); + sockbuf_write(sockbuf, buf); +} diff --git a/main/station.c b/main/station.c index d4c2d00..6f66a79 100644 --- a/main/station.c +++ b/main/station.c @@ -68,6 +68,9 @@ void wifi_init_station(const char* ssid, const char* psk) esp_event_handler_instance_t instance_any_id; esp_event_handler_instance_t instance_got_ip; + /* Keep Wi-Fi awake to reduce latency/jank impacting LED timing. */ + esp_wifi_set_ps(WIFI_PS_NONE); + ESP_ERROR_CHECK(esp_event_handler_instance_register( WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id)); diff --git a/main/ws2812b_writer.c b/main/ws2812b_writer.c index 50d89b5..90c7405 100644 --- a/main/ws2812b_writer.c +++ b/main/ws2812b_writer.c @@ -1,8 +1,10 @@ #include "ws2812b_writer.h" +#include <math.h> #include <stdlib.h> #include <string.h> +#include "esp_log.h" #include "pattern/twinkle.h" const int NUMBER_PARAMS = @@ -12,6 +14,8 @@ const int NUMBER_PARAMS = ; #define SIZE 300 +/* 60 FPS -> ~16.67ms per frame. Keep time scaling aligned. */ +#define FRAME_TIME_DELTA 167 /* Returns round(sin(2πn / 256) * 127.5 + 127.5). */ uint8_t byte_sin(uint8_t n); @@ -27,6 +31,13 @@ static inline uint8_t byte_scale(uint8_t n, uint8_t sc) /* returns (in / 256)^n * 256. */ uint8_t amp(uint8_t in, uint8_t n); +/* Per-channel calibration to balance the LEDs (boost red, soften green/blue). + */ +#define CAL_R_NUM 140 /* 1.40x */ +#define CAL_G_NUM 90 /* 0.90x */ +#define CAL_B_NUM 90 /* 0.90x */ +#define CAL_DEN 100 + void reset_parameters(ws_params_t* params) { if (params->state_lock) { @@ -42,80 +53,149 @@ void reset_parameters(ws_params_t* params) } } -void calculate_colors(const ws_state_t* state, ws2812b_buffer_t* buffer) +struct rgba { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; +}; + +uint32_t asi(struct rgba rgb) +{ + return (rgb.r << 16) | (rgb.g << 8) | rgb.b; +} +struct rgba get_rgba(int x, int64_t time, const ws_state_t* state) { uint32_t red = 0, green = 0, blue = 0; - int time; + uint8_t brightness = state->brightness; + if (!state->use_static_color) { + red = byte_scale( + byte_sin((uint8_t)(time / 1000 + x * 4)), 255 - state->n_red) + + state->n_red; + green = 255 - red; + + if (state->cool) { + uint32_t tmp = green; + green = blue; + blue = tmp; + } + + /* Add a little white flair that comes around every once in a while. */ + + uint32_t whitesum = 0; + if (state->n_snow) { + uint32_t white[] = { + /* Parallax "snow" */ + time / 179 + x * 8, + time / 193 + x * 12, + time / 211 + x * 16, + time / 233 + x * 8, + // (state->time + 128) / 233 + x * 8, + }; + + for (int i = 0; i < sizeof(white) / sizeof(uint32_t); ++i) { + if ((white[i] / 256) % state->n_snow == 0) { + white[i] = amp(byte_sin(white[i]), 20); + } else { + white[i] = 0; + } + whitesum += white[i]; + } + } + + red = min(red + whitesum, 255); + green = min(green + whitesum, 255); + blue = min(blue + whitesum, 255); + } else { + red = (state->static_color >> 16) & 0xff; + green = (state->static_color >> 8) & 0xff; + blue = state->static_color & 0xff; + } + + if (state->invert) { + red = 255 - red; + green = 255 - green; + blue = 255 - blue; + } + + /* Channel calibration to balance perceived brightness. */ + red = min((red * CAL_R_NUM) / CAL_DEN, 255); + green = min((green * CAL_G_NUM) / CAL_DEN, 255); + blue = min((blue * CAL_B_NUM) / CAL_DEN, 255); + + uint8_t twink = 0xff; + if (state->twinkle) { + twink = twinkle(time / 300, x, state->twink_amt); + } + + red = byte_scale( + red, max(byte_scale(brightness, twink), state->base_brightness)); + green = byte_scale( + green, max(byte_scale(brightness, twink), state->base_brightness)); + blue = byte_scale( + blue, max(byte_scale(brightness, twink), state->base_brightness)); + + return (struct rgba){red, green, blue, 0xff}; +} + +/* return a new color which is an interoplation between c1 and c2 by the + * provided amount. The semantic amount is amt / 255.0. */ +struct rgba interpolate(struct rgba c1, struct rgba c2, uint8_t amt) +{ +#define scale(a, b, s) ((a * (255 - s) + b * s) / 255) + + return (struct rgba){scale(c1.r, c2.r, amt), + scale(c1.g, c2.g, amt), + scale(c1.b, c2.b, amt), + 0xff}; +#undef scale +} + +void calculate_colors(const ws_state_t* state, ws2812b_buffer_t* buffer) +{ for (int i = 0; i < SIZE; ++i) { int x = i * 255 / state->x_scale; - time = state->time; if (!state->power) { ws2812b_buffer_set_rgb(buffer, i, 0, 0, 0); continue; } + struct rgba c; - uint8_t brightness = state->brightness; - if (!state->use_static_color) { - red = byte_scale(byte_sin(time / 1000 + x * 4), 255 - state->n_red) + - state->n_red; - green = 255 - red; - - if (state->cool) { - uint32_t tmp = green; - green = blue; - blue = tmp; + int64_t real_time = state->time * FRAME_TIME_DELTA; + if (state->speed % 1000 == 0) { + // speed = 1000 is the "canonical" time speed. If speed is divisible by + // 1000, then no interpolation is required. + c = get_rgba(x, (real_time * state->speed) / 1000, state); + } else { + int64_t adj_time = real_time * state->speed; + int64_t bot = adj_time / 1000; + int64_t top = + adj_time / 1000 + FRAME_TIME_DELTA * (adj_time >= 0 ? 1 : -1); + int64_t mod = adj_time % 1000; + if (mod < 0) { + mod += 1000; } - - /* Add a little white flair that comes around every once in a while. */ - - uint32_t whitesum = 0; - if (state->n_snow) { - uint32_t white[] = { - /* Parallax "snow" */ - time / 179 + x * 8, - time / 193 + x * 12, - time / 211 + x * 16, - time / 233 + x * 8, - // (state->time + 128) / 233 + x * 8, - }; - - for (int i = 0; i < sizeof(white) / sizeof(uint32_t); ++i) { - if ((white[i] / 256) % state->n_snow == 0) { - white[i] = amp(byte_sin(white[i]), 20); - } else { - white[i] = 0; - } - whitesum += white[i]; - } + uint8_t interp = (uint8_t)((mod * 255) / 1000); + + struct rgba c1 = get_rgba(x, bot, state); + struct rgba c2 = get_rgba(x, top, state); + + if (i == 5 && asi(c1) > 0) { + ESP_LOGI( + "main", + "%06x %06x %d = %06x", + asi(c1), + asi(c2), + interp, + asi(interpolate(c1, c2, interp))); } - red = min(red + whitesum, 255); - green = min(green + whitesum, 255); - blue = min(blue + whitesum, 255); - } else { - red = (state->static_color >> 16) & 0xff; - green = (state->static_color >> 8) & 0xff; - blue = state->static_color & 0xff; - } - - if (state->invert) { - red = 255 - red; - green = 255 - green; - blue = 255 - blue; + c = interpolate(c1, c2, interp); } - uint8_t twink = 0xff; - if (state->twinkle) { - twink = twinkle(time / 300, x, state->twink_amt); - } - - red = byte_scale(red, byte_scale(brightness, twink)); - green = byte_scale(green, byte_scale(brightness, twink)); - blue = byte_scale(blue, byte_scale(brightness, twink)); - - ws2812b_buffer_set_rgb(buffer, i, red, green, blue); + ws2812b_buffer_set_rgb(buffer, i, c.r, c.g, c.b); } } @@ -124,12 +204,14 @@ portTASK_FUNCTION(ws2812b_write_task, params) ws_params_t* ws_params = (ws_params_t*)params; ws2812b_buffer_t* buffer = ws2812b_new_buffer(SIZE); + TickType_t last_wake = xTaskGetTickCount(); + const TickType_t period = pdMS_TO_TICKS(17); /* ~60 FPS */ for (;;) { // Copy the parameters to avoid a change occuring during the calculation. ws_state_t state_snapshot; if (xSemaphoreTake(ws_params->state_lock, portMAX_DELAY) != pdTRUE) { - vTaskDelay(1); + vTaskDelayUntil(&last_wake, period); continue; } state_snapshot = ws_params->state; @@ -139,10 +221,15 @@ portTASK_FUNCTION(ws2812b_write_task, params) if (ws2812b_write(ws_params->drv, buffer) != ESP_OK) { /* If we fail to write, back off briefly instead of hammering the SPI * bus, which can otherwise cause visible flicker. */ - vTaskDelay(1); + vTaskDelayUntil(&last_wake, period); continue; } - vTaskDelay(1); + /* Advance time once per frame. */ + if (xSemaphoreTake(ws_params->state_lock, portMAX_DELAY) == pdTRUE) { + ws_params->state.time += 1; + xSemaphoreGive(ws_params->state_lock); + } + vTaskDelayUntil(&last_wake, period); } } |