xmas-snoopy/firmware/main/led_ctrl.c

410 lines
11 KiB
C

/*
* led_ctrl.c
*
* Copyright (C) 2023 Sylvain Munaut <tnt@246tNt.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <sys/param.h>
#include "config.h"
#include "pmu.h"
#include "utils.h"
/* ------------------------------------------------------------------------ */
/* Hardware definitions */
/* ------------------------------------------------------------------------ */
/*
* Subframe format:
*
* 31 24 23 16 15 8 7 0
* -----------------------------------------------------------------------
* | stop[0] | start[0] | / | anode_sel |
* -----------------------------------------------------------------------
* | stop[2] | start[2] | stop[1] | start[1] |
* -----------------------------------------------------------------------
*/
#define LED_N_FRAMES 16
#define LED_N_SUBFRAMES 16
struct led_subframe {
union {
struct {
uint32_t w[2];
};
struct {
uint8_t anode;
uint8_t _rsvd;
struct {
uint8_t start;
uint8_t stop;
} cathode[3];
};
};
} __attribute__((packed,aligned(4)));
struct wb_led_ctrl {
uint32_t csr;
uint32_t _rsvd[511];
struct {
struct led_subframe subframe[LED_N_SUBFRAMES];
} frame[LED_N_FRAMES];
} __attribute__((packed,aligned(4)));
#define LED_CSR_TRIG_CLR (1 << 31)
#define LED_CSR_TRIG_ENA (1 << 30)
#define LED_CSR_TRIG_FRAME(n) (((n) & 0xf) << 24)
#define LED_CSR_GET_FRAME(v) (((v) >> 16) & (LED_N_FRAMES-1))
#define LED_CSR_DRV_CURREN (1 << 2)
#define LED_CSR_DRV_RGBLEDEN (1 << 1)
#define LED_CSR_DRV_SCAN_ENA (1 << 0)
static volatile struct wb_led_ctrl * const led_regs = (void*)(LED_CTRL_BASE);
/* ------------------------------------------------------------------------ */
/* CIE luminosity */
/* ------------------------------------------------------------------------ */
/* Maps every PWM [1-256] to a perceived luminosity value [0-65535] */
static const uint16_t cie[] = {
1156, 3468, 5703, 7651, 9253, 10628, 11843, 12938,
13938, 14863, 15724, 16533, 17295, 18018, 18705, 19362,
19991, 20595, 21176, 21737, 22279, 22804, 23312, 23806,
24285, 24752, 25207, 25651, 26084, 26507, 26920, 27325,
27721, 28109, 28489, 28862, 29228, 29587, 29941, 30288,
30629, 30964, 31295, 31620, 31940, 32255, 32566, 32873,
33175, 33473, 33767, 34058, 34344, 34627, 34906, 35182,
35455, 35724, 35990, 36254, 36514, 36772, 37027, 37279,
37528, 37775, 38019, 38261, 38501, 38738, 38973, 39206,
39437, 39665, 39891, 40116, 40338, 40559, 40777, 40994,
41209, 41422, 41634, 41843, 42051, 42258, 42463, 42666,
42868, 43068, 43267, 43464, 43660, 43854, 44047, 44239,
44429, 44618, 44806, 44993, 45178, 45362, 45545, 45726,
45907, 46086, 46264, 46441, 46617, 46792, 46966, 47139,
47310, 47481, 47651, 47820, 47988, 48155, 48320, 48485,
48649, 48812, 48975, 49136, 49297, 49456, 49615, 49773,
49930, 50087, 50242, 50397, 50551, 50704, 50856, 51008,
51159, 51309, 51458, 51607, 51755, 51902, 52049, 52195,
52340, 52485, 52629, 52772, 52915, 53057, 53198, 53339,
53479, 53618, 53757, 53896, 54033, 54170, 54307, 54443,
54578, 54713, 54848, 54981, 55114, 55247, 55379, 55511,
55642, 55773, 55903, 56032, 56161, 56290, 56418, 56545,
56672, 56799, 56925, 57051, 57176, 57301, 57425, 57549,
57672, 57795, 57918, 58040, 58161, 58282, 58403, 58524,
58643, 58763, 58882, 59001, 59119, 59237, 59354, 59471,
59588, 59704, 59820, 59936, 60051, 60166, 60280, 60394,
60508, 60621, 60734, 60847, 60959, 61071, 61182, 61293,
61404, 61515, 61625, 61735, 61844, 61953, 62062, 62171,
62279, 62387, 62495, 62602, 62709, 62816, 62922, 63028,
63134, 63239, 63344, 63449, 63553, 63658, 63762, 63865,
63968, 64071, 64174, 64277, 64379, 64481, 64582, 64684,
64785, 64886, 64986, 65087, 65187, 65287, 65386, 65485,
};
/* Converts a requested luminosity value to PWM length */
static uint16_t
cie_lum2pwm(uint16_t lum)
{
int min = 0;
int max = 256;
while (max > min) {
int idx = (max + min) >> 1;
if (cie[idx] > lum)
max = idx;
else if (cie[idx] < lum)
min = idx + 1;
else
return idx;
}
return min;
}
/* ------------------------------------------------------------------------ */
/* Animation */
/* ------------------------------------------------------------------------ */
static void
_led_render(uint32_t frame, uint16_t *leds)
{
/* Decrease luminosoty so they all fade out in 15 second */
for (int i=0; i<42; i++)
{
if (leds[i] > 40000)
leds[i] -= 150 + ((leds[i] - 40000) >> 4);
if (leds[i] > 500)
leds[i] -= 150;
else
leds[i] = 0;
}
/* Every second, pick a led and make it bright */
if ((frame & 15) == 0) {
int i;
while (leds[i = (rand16() % 42)] > 10000);
leds[i] = 65535;
}
else if ((frame & 7) == 0) {
int i;
i = (rand16() % 42);
leds[i] = 35000;
}
}
/* ------------------------------------------------------------------------ */
/* Frame mapping */
/* ------------------------------------------------------------------------ */
/* Mapping priority */
#define K0 1 /* Pink */
#define K1 0 /* Emerald */
#define K2 2 /* Blue */
/* Scaling factor to match intensity */
static const int k_scale[] = {
189, /* Emerald */
256, /* Pink */
236, /* Blue */
};
static void
_led_map_one(struct led_subframe *subframe, int anode, uint16_t *pwm)
{
/* Select anode */
subframe->anode = anode;
/* Check various cases */
if (pwm[0] + pwm[1] + pwm[2] <= 128)
{
/* Full serial case */
int s = (128 - (pwm[0] + pwm[1] + pwm[2])) >> 1;
int o = 0;
for (int k=0; k<3; k++) {
if (pwm[k] > 0) {
subframe->cathode[k].start = o;
subframe->cathode[k].stop = o + pwm[k] - 1;
o += pwm[k] + s;
}
}
}
else if ((pwm[K0] + MAX(pwm[K1], pwm[K2])) <= 128)
{
int o = 0;
/* K0 split, K1 & K2 overlap */
/* K0 starting at 0 */
if (pwm[K0] > 0) {
subframe->cathode[K0].start = o;
subframe->cathode[K0].stop = o + pwm[K0] - 1;
o += pwm[K0];
}
/* K1 starts right after K0 */
if (pwm[K1] > 0) {
subframe->cathode[K1].start = o;
subframe->cathode[K1].stop = o + pwm[K1] - 1;
}
/* K2 aligned to the end */
if (pwm[K2] > 0) {
subframe->cathode[K2].start = 0x7f - pwm[K2] + 1;
subframe->cathode[K2].stop = 0x7f;
}
}
else
{
/* Default, K0 on one side, then K1 & K2 full overlap on the other */
/* K0 aligned at the end */
if (pwm[K0] > 0) {
subframe->cathode[K0].start = 0x7f - pwm[K0] + 1;
subframe->cathode[K0].stop = 0x7f;
}
/* K1 & K2 overlapping at beginning */
if (pwm[K1] > 0) {
subframe->cathode[K1].start = 0;
subframe->cathode[K1].stop = pwm[K1] - 1;
}
if (pwm[K2] > 0) {
subframe->cathode[K2].start = 0;
subframe->cathode[K2].stop = pwm[K2] - 1;
}
}
}
static void
_led_map(struct led_subframe *subframes, uint16_t *leds)
{
uint16_t pwm[42];
int k, a, l;
int sf_l, sf_h;
/* Pre-clear */
for (int sf=0; sf<16; sf++) {
subframes[sf].w[0] = 0x007f0000;
subframes[sf].w[1] = 0x007f007f;
}
/* Convert all linear brightness to pwm times */
for (a=0, l=0; a<14; a++)
for (k=0; k<3; k++, l++)
pwm[l] = (cie_lum2pwm(leds[l]) * k_scale[k]) >> 8;
/* Attempt packing */
sf_l = 0; /* Low boundary */
sf_h = 16; /* High boundary */
for (a=0, l=0; a<14; a++, l+=3)
{
/* Check if we're overbright */
if ((pwm[l+0] > 128) ||
(pwm[l+1] > 128) ||
(pwm[l+2] > 128))
{
uint16_t p0[3], p1[3];
/* Split all brightness in two and map in two subframes */
for (int i=0; i<3; i++) {
p0[i] = (pwm[l+i] + 0) >> 1;
p1[i] = (pwm[l+i] + 1) >> 1;
}
_led_map_one(&subframes[sf_l++], a, p0);
_led_map_one(&subframes[--sf_h], a, p1);
}
else if ((pwm[l+0] > 0) ||
(pwm[l+1] > 0) ||
(pwm[l+2] > 0))
{
/* Map all in one subframe */
_led_map_one(&subframes[sf_l++], a, &pwm[l]);
}
}
}
/* ------------------------------------------------------------------------ */
/* Frame mapping */
/* ------------------------------------------------------------------------ */
static struct {
uint8_t frame_nxt;
uint32_t time;
uint16_t leds[14*3];
struct led_subframe subframe[LED_N_SUBFRAMES];
} g_led;
static bool
_led_fill(void)
{
int f0, f1;
int frame_limit;
bool work = false;
/* "Safe" read from hardware */
do {
f0 = LED_CSR_GET_FRAME(led_regs->csr);
f1 = LED_CSR_GET_FRAME(led_regs->csr);
} while (f0 != f1);
/* Compute the first frame we can't write to */
frame_limit = (f0 - 1) & (LED_N_FRAMES - 1);
/* Fill to catch up */
while (g_led.frame_nxt != frame_limit)
{
/* Render frame */
_led_render(g_led.time++, g_led.leds);
/* Convert to subframe */
_led_map(g_led.subframe, g_led.leds);
/* Copy to hardware */
for (int subframe=0; subframe<LED_N_SUBFRAMES; subframe++) {
led_regs->frame[g_led.frame_nxt].subframe[subframe].w[0] = g_led.subframe[subframe].w[0];
led_regs->frame[g_led.frame_nxt].subframe[subframe].w[1] = g_led.subframe[subframe].w[1];
}
/* Next frame */
g_led.frame_nxt = (g_led.frame_nxt + 1) & (LED_N_FRAMES - 1);
work = true;
}
return work;
}
/* ------------------------------------------------------------------------ */
/* External API */
/* ------------------------------------------------------------------------ */
void
led_init(void)
{
/* Shutdown hardware */
led_regs->csr = 0;
/* Clear internal state */
memset(&g_led, 0x00, sizeof(g_led));
/* Clear frame memory */
for (int frame=0; frame<LED_N_FRAMES; frame++) {
for (int subframe=0; subframe<LED_N_SUBFRAMES; subframe++) {
led_regs->frame[frame].subframe[subframe].w[0] = 0x007f0000;
led_regs->frame[frame].subframe[subframe].w[1] = 0x007f007f;
}
}
}
void
led_start(void)
{
/* Reset frame number */
g_led.frame_nxt = 0;
/* Fill the full buffer */
_led_fill();
/* Enable HW */
led_regs->csr |= LED_CSR_DRV_CURREN | LED_CSR_DRV_RGBLEDEN | LED_CSR_DRV_SCAN_ENA;
}
void
led_stop(void)
{
led_regs->csr &= ~(LED_CSR_DRV_CURREN | LED_CSR_DRV_RGBLEDEN | LED_CSR_DRV_SCAN_ENA);
}
void
led_poll(bool suspend)
{
/* Refill anything we can */
_led_fill();
/* Schedule wake up 8 frames before expiry */
if (suspend) {
uint8_t frame_trig = (g_led.frame_nxt - 8) & (LED_N_FRAMES - 1);
led_regs->csr = (led_regs->csr & 0xffff) | LED_CSR_TRIG_CLR | LED_CSR_TRIG_ENA | LED_CSR_TRIG_FRAME(frame_trig);
pmu_sys_suspend();
led_regs->csr = (led_regs->csr & 0xffff) | LED_CSR_TRIG_CLR;
}
}