#include #include "wm_include.h" #include "drivers/nl_led.h" #define SM_ADDR_CURRENT 0xC0u #define SM_ADDR_WY 0xC5u #define SM_CHANNEL_WY 0x80u /* channel select bit 7: 0 = RGB, 1 = WY */ #define SM_CHANNEL_STBY 0x20u /* standby bit 5: powers the outputs down */ extern void delay_us(unsigned int us); /* provided by libwlan.a */ static uint8_t s_clk, s_dat, s_current; static uint8_t s_last[NL_CH_COUNT]; static bool s_have_last; static void pin(uint8_t p, int v) { tls_gpio_write((enum tls_io_name)p, (u8)v); } static void bus_start(void) { pin(s_clk, 1); pin(s_dat, 1); delay_us(1); pin(s_dat, 0); delay_us(1); pin(s_clk, 0); delay_us(1); } static void bus_stop(void) { pin(s_clk, 0); pin(s_dat, 0); delay_us(1); pin(s_clk, 1); delay_us(1); pin(s_dat, 1); delay_us(1); } static void write_byte(uint8_t b) { for (uint8_t m = 0x80; m; m >>= 1) { pin(s_dat, (b & m) ? 1 : 0); delay_us(1); pin(s_clk, 1); delay_us(1); pin(s_clk, 0); delay_us(1); } /* ack clock; the SM2135EH does not drive an ack */ pin(s_dat, 0); delay_us(1); pin(s_clk, 1); delay_us(1); pin(s_clk, 0); pin(s_dat, 0); } static void sm_send(const uint8_t *d, int n) { bus_start(); for (int i = 0; i < n; i++) write_byte(d[i]); bus_stop(); } void nl_sm2135eh_init(const nl_board_t *board) { uint8_t off[NL_CH_COUNT] = {0}; s_clk = board->sm_clk_pin; s_dat = board->sm_dat_pin; s_current = board->sm_current; s_have_last = false; tls_gpio_cfg((enum tls_io_name)s_clk, WM_GPIO_DIR_OUTPUT, WM_GPIO_ATTR_PULLHIGH); tls_gpio_cfg((enum tls_io_name)s_dat, WM_GPIO_DIR_OUTPUT, WM_GPIO_ATTR_PULLHIGH); nl_sm2135eh_set(off); } void nl_sm2135eh_set(const uint8_t ch[NL_CH_COUNT]) { bool white = ch[NL_CH_CW] || ch[NL_CH_WW]; if (s_have_last && memcmp(ch, s_last, NL_CH_COUNT) == 0) return; memcpy(s_last, ch, NL_CH_COUNT); s_have_last = true; if (!white) { /* All channels dark: put the driver into standby instead of just writing zero duty. Without bit 5 the chip stays active and leaks enough current to keep the lamp faintly glowing - measured on a migrated GU10 RGB, 2026-09-13. The old QConnex firmware had this logic but left it commented out (qcl_lamp.c), which is why the behaviour was inherited. Lamps driven straight from PWM pins (G95, the CW variants) are unaffected. Turning back on writes 0x00 or SM_CHANNEL_WY here and clears it. */ bool dark = !ch[NL_CH_R] && !ch[NL_CH_G] && !ch[NL_CH_B]; uint8_t chan = dark ? SM_CHANNEL_STBY : 0x00u; uint8_t d[6] = {SM_ADDR_CURRENT, s_current, chan, ch[NL_CH_B], ch[NL_CH_G], ch[NL_CH_R]}; sm_send(d, 6); /* The WY data registers are cleared here too, even though this is the RGB branch: the chip keeps whatever white value was last written, and a direct white -> colour change never passes through the all-dark frame above. The stale white then bleeds through as soon as the colour dims - the same defect as the colour -> white case below (fixed in 00d5a4c, 2026-09-13), just mirrored: that fix made the WY branch also write the (always-zero) RGB registers, this makes the RGB branch also write the (always-zero) WY registers. Both directions had it; only one was caught first. */ uint8_t w[3] = {SM_ADDR_WY, 0, 0}; sm_send(w, 3); } else { /* The RGB data registers are written along with the channel select, even though they are always zero here: the chip keeps whatever colour was last written, and a direct colour -> white change never passes through the all-dark frame above. The stale colour then bleeds through as soon as the white dims - visible as a coloured fade-out on a migrated GU10 RGB, 2026-09-13. */ uint8_t h[6] = {SM_ADDR_CURRENT, s_current, SM_CHANNEL_WY, ch[NL_CH_B], ch[NL_CH_G], ch[NL_CH_R]}; uint8_t d[3] = {SM_ADDR_WY, ch[NL_CH_WW], ch[NL_CH_CW]}; sm_send(h, 6); sm_send(d, 3); } }