aboutsummaryrefslogtreecommitdiff
path: root/hw/sofirn
diff options
context:
space:
mode:
Diffstat (limited to 'hw/sofirn')
-rw-r--r--hw/sofirn/blf-lt1-t1616/cfg.h110
-rw-r--r--hw/sofirn/blf-lt1-t1616/hwdef.c210
-rw-r--r--hw/sofirn/blf-lt1-t1616/hwdef.h161
-rw-r--r--hw/sofirn/blf-lt1/cfg.h105
-rw-r--r--hw/sofirn/blf-lt1/hwdef.c204
-rw-r--r--hw/sofirn/blf-lt1/hwdef.h114
-rw-r--r--hw/sofirn/blf-q8-t1616/cfg.h82
-rw-r--r--hw/sofirn/blf-q8-t1616/hwdef.h138
-rw-r--r--hw/sofirn/blf-q8/cfg.h71
-rw-r--r--hw/sofirn/blf-q8/hwdef.h20
-rw-r--r--hw/sofirn/lt1s-pro/cfg.h125
-rw-r--r--hw/sofirn/lt1s-pro/hwdef.c237
-rw-r--r--hw/sofirn/lt1s-pro/hwdef.h151
-rw-r--r--hw/sofirn/sc21-pro/cfg.h14
-rw-r--r--hw/sofirn/sp10-pro/cfg.h81
-rw-r--r--hw/sofirn/sp10-pro/hwdef.c63
-rw-r--r--hw/sofirn/sp10-pro/hwdef.h157
-rw-r--r--hw/sofirn/sp36-t1616/cfg.h37
-rw-r--r--hw/sofirn/sp36/cfg.h36
19 files changed, 2116 insertions, 0 deletions
diff --git a/hw/sofirn/blf-lt1-t1616/cfg.h b/hw/sofirn/blf-lt1-t1616/cfg.h
new file mode 100644
index 0000000..fc02619
--- /dev/null
+++ b/hw/sofirn/blf-lt1-t1616/cfg.h
@@ -0,0 +1,110 @@
+// BLF Lantern config options for Anduril using the Attiny1616
+// Copyright (C) 2021-2023 (original author TBD), Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+#define MODEL_NUMBER "0622"
+#include "hwdef-blf-lt1-t1616.h"
+// ATTINY: 1616
+
+// the button lights up
+#define USE_INDICATOR_LED
+// the button is visible while main LEDs are on
+#define USE_INDICATOR_LED_WHILE_RAMPING
+// off mode: low (1)
+// lockout: blinking (3)
+#define INDICATOR_LED_DEFAULT_MODE ((3<<2) + 1)
+
+// channel modes...
+// CM_CH1, CM_CH2, CM_BOTH, CM_BLEND, CM_AUTO
+#define DEFAULT_CHANNEL_MODE CM_AUTO
+#define DEFAULT_BLINK_CHANNEL CM_BOTH
+
+#define CONFIG_WAITING_CHANNEL CM_BOTH
+#define CONFIG_BLINK_CHANNEL CM_BOTH
+
+// blink numbers on the main LEDs by default (but allow user to change it)
+#define DEFAULT_BLINK_CHANNEL CM_BOTH
+
+// how much to increase total brightness at middle tint
+// (0 = 100% brightness, 64 = 200% brightness)
+//#define TINT_RAMPING_CORRECTION 26 // prototype, 140%
+#define TINT_RAMPING_CORRECTION 10 // production model, 115%
+
+#define RAMP_SIZE 150
+// delta-sigma modulated PWM (0b0HHHHHHHHLLLLLLL = 0, 8xHigh, 7xLow bits)
+// (max is (255 << 7), because it's 8-bit PWM plus 7 bits of DSM)
+// level_calc.py 3.333 1 150 7135 32 0.2 600 --pwm 32640
+#define PWM1_LEVELS 4,35,38,41,45,50,55,61,67,74,82,91,100,110,121,133,146,160,175,192,209,227,247,268,291,314,340,366,395,424,456,489,524,560,599,639,681,726,772,820,871,924,979,1036,1096,1158,1222,1289,1359,1431,1506,1584,1664,1747,1834,1923,2015,2111,2209,2311,2416,2524,2636,2751,2870,2992,3118,3247,3380,3518,3659,3803,3952,4105,4262,4423,4589,4759,4933,5111,5294,5482,5674,5871,6073,6279,6491,6707,6928,7155,7386,7623,7865,8113,8365,8624,8888,9157,9432,9713,10000,10292,10591,10895,11206,11523,11846,12175,12511,12853,13202,13557,13919,14287,14663,15045,15434,15830,16233,16644,17061,17486,17919,18358,18805,19260,19723,20193,20671,21156,21650,22152,22662,23180,23706,24241,24784,25335,25895,26464,27041,27627,28222,28826,29439,30060,30691,31332,31981,32640
+
+#define DEFAULT_LEVEL 75
+#define MAX_1x7135 75
+#define HALFSPEED_LEVEL 0 // always use tint ramping correction
+#define QUARTERSPEED_LEVEL 2 // quarter speed at level 1, full speed at 2+
+//#undef USE_DYNAMIC_UNDERCLOCKING // makes huge bumps in the ramp
+
+#define USE_SET_LEVEL_GRADUALLY
+
+
+// override default ramp style
+#undef RAMP_STYLE
+#define RAMP_STYLE 1 // 0 = smooth, 1 = stepped
+// set floor and ceiling as far apart as possible
+// because this lantern isn't overpowered
+#define RAMP_SMOOTH_FLOOR 1
+#define RAMP_SMOOTH_CEIL 150
+#define RAMP_DISCRETE_FLOOR 1
+#define RAMP_DISCRETE_CEIL 150
+#define RAMP_DISCRETE_STEPS 7
+
+// LT1 can handle heat well, so don't limit simple mode
+#define SIMPLE_UI_FLOOR 10
+#define SIMPLE_UI_CEIL 150
+#define SIMPLE_UI_STEPS 5
+
+// Allow 3C (or 6C) in Simple UI (toggle smooth or stepped ramping)
+#define USE_SIMPLE_UI_RAMPING_TOGGLE
+
+// allow Aux Config and Strobe Modes in Simple UI
+#define USE_EXTENDED_SIMPLE_UI
+
+#define USE_SOS_MODE
+#define USE_SOS_MODE_IN_BLINKY_GROUP
+
+// the default of 26 looks a bit flat, so increase it
+#define CANDLE_AMPLITUDE 40
+
+#define USE_POLICE_COLOR_STROBE_MODE
+#define POLICE_COLOR_STROBE_CH1 CM_CH1
+#define POLICE_COLOR_STROBE_CH2 CM_CH2
+// aux red + aux blue are the correct colors, but are dim
+//#define POLICE_COLOR_STROBE_CH1 CM_AUXRED
+//#define POLICE_COLOR_STROBE_CH2 CM_AUXBLU
+
+#undef TACTICAL_LEVELS
+#define TACTICAL_LEVELS 120,30,(RAMP_SIZE+3) // high, low, police strobe
+
+// party strobe, tac strobe, police, lightning, candle, bike
+#define DEFAULT_STROBE_CHANNELS CM_BOTH,CM_BOTH,CM_BOTH,CM_AUTO,CM_AUTO,CM_AUTO
+
+// the sensor (attiny1616) is nowhere near the emitters
+// so thermal regulation can't work
+#ifdef USE_THERMAL_REGULATION
+#undef USE_THERMAL_REGULATION
+#endif
+
+// don't blink while ramping
+#ifdef BLINK_AT_RAMP_FLOOR
+#undef BLINK_AT_RAMP_FLOOR
+#endif
+#ifdef BLINK_AT_RAMP_MIDDLE
+#undef BLINK_AT_RAMP_MIDDLE
+#endif
+// except the top... blink at the top
+#ifndef BLINK_AT_RAMP_CEIL
+#define BLINK_AT_RAMP_CEIL
+#endif
+
+// for consistency with other models
+#define USE_SOFT_FACTORY_RESET
+
diff --git a/hw/sofirn/blf-lt1-t1616/hwdef.c b/hw/sofirn/blf-lt1-t1616/hwdef.c
new file mode 100644
index 0000000..9d268a4
--- /dev/null
+++ b/hw/sofirn/blf-lt1-t1616/hwdef.c
@@ -0,0 +1,210 @@
+// Sofirn LT1-t1616 PWM helpers
+// Copyright (C) 2023 SiteRelEnby, Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+#include "chan-aux.c"
+
+
+void set_level_zero();
+
+void set_level_ch1(uint8_t level);
+void set_level_ch2(uint8_t level);
+void set_level_both(uint8_t level);
+void set_level_blend(uint8_t level);
+void set_level_auto(uint8_t level);
+
+bool gradual_tick_ch1(uint8_t gt);
+bool gradual_tick_ch2(uint8_t gt);
+bool gradual_tick_both(uint8_t gt);
+bool gradual_tick_blend(uint8_t gt);
+bool gradual_tick_auto(uint8_t gt);
+
+
+Channel channels[] = {
+ { // channel 1 only
+ .set_level = set_level_ch1,
+ .gradual_tick = gradual_tick_ch1,
+ .has_args = 0
+ },
+ { // channel 2 only
+ .set_level = set_level_ch2,
+ .gradual_tick = gradual_tick_ch2,
+ .has_args = 0
+ },
+ { // both channels, tied together (max "200%" power)
+ .set_level = set_level_both,
+ .gradual_tick = gradual_tick_both,
+ .has_args = 0
+ },
+ { // both channels, manual blend (max "100%" power)
+ .set_level = set_level_blend,
+ .gradual_tick = gradual_tick_blend,
+ .has_args = 1
+ },
+ { // both channels, auto blend
+ .set_level = set_level_auto,
+ .gradual_tick = gradual_tick_auto,
+ .has_args = 1
+ },
+ AUX_CHANNELS
+};
+
+
+void set_level_zero() {
+ // disable timer overflow interrupt
+ // (helps improve button press handling from Off state)
+ DSM_INTCTRL = 0;
+
+ // turn off all LEDs
+ ch1_dsm_lvl = 0;
+ ch2_dsm_lvl = 0;
+ CH1_PWM = 0;
+ CH2_PWM = 0;
+ PWM_CNT = 0;
+}
+
+void set_hw_levels(PWM_DATATYPE ch1, PWM_DATATYPE ch2) {
+
+ bool was_on = (CH1_PWM>0) || (CH2_PWM>0);
+
+ // set delta-sigma soft levels
+ ch1_dsm_lvl = ch1;
+ ch2_dsm_lvl = ch2;
+
+ // set hardware PWM levels and init dsm loop
+ CH1_PWM = ch1_pwm = ch1 >> 7;
+ CH2_PWM = ch2_pwm = ch2 >> 7;
+
+ // enable timer overflow interrupt so DSM can work
+ DSM_INTCTRL = DSM_OVF_bm;
+
+ // reset phase when turning on
+ if (! was_on) PWM_CNT = 0;
+
+}
+
+// delta-sigma modulation of PWM outputs
+// happens on each Timer overflow (every 512 cpu clock cycles)
+// uses 8-bit pwm w/ 7-bit dsm (0b 0PPP PPPP PDDD DDDD)
+ISR(DSM_vect) {
+ // set new hardware values first,
+ // for best timing (reduce effect of interrupt jitter)
+ CH1_PWM = ch1_pwm;
+ CH2_PWM = ch2_pwm;
+
+ // calculate next values, now that timing matters less
+
+ // accumulate error
+ ch1_dsm += (ch1_dsm_lvl & 0x007f);
+ // next PWM = base PWM value + carry bit
+ ch1_pwm = (ch1_dsm_lvl >> 7) + (ch1_dsm > 0x7f);
+ // clear carry bit
+ ch1_dsm &= 0x7f;
+
+ // repeat for other channels
+
+ ch2_dsm += (ch2_dsm_lvl & 0x007f);
+ ch2_pwm = (ch2_dsm_lvl >> 7) + (ch2_dsm > 0x7f);
+ ch2_dsm &= 0x7f;
+
+ // clear the interrupt flag to indicate it was handled
+ // as per: https://onlinedocs.microchip.com/pr/GUID-C37FFBA8-82C6-4339-A2B1-ABD9A0F6C162-en-US-8/index.html?GUID-C2A2BEFD-158F-413D-B9D4-0F0556AA79BD
+ DSM_INTFLAGS = DSM_OVF_bm;
+}
+
+
+void set_level_ch1(uint8_t level) {
+ set_hw_levels(PWM_GET(pwm1_levels, level), 0);
+}
+
+void set_level_ch2(uint8_t level) {
+ set_hw_levels(0, PWM_GET(pwm1_levels, level));
+}
+
+void set_level_both(uint8_t level) {
+ PWM_DATATYPE pwm = PWM_GET(pwm1_levels, level);
+ set_hw_levels(pwm, pwm);
+}
+
+void blend_helper(PWM_DATATYPE *warm, PWM_DATATYPE *cool, uint8_t level) {
+ PWM_DATATYPE brightness = PWM_GET(pwm1_levels, level);
+ uint8_t blend;
+ if (channel_mode == CM_AUTO) {
+ blend = 255 * (uint16_t)level / RAMP_SIZE;
+ if (cfg.channel_mode_args[channel_mode] & 0b01000000)
+ blend = 255 - blend;
+ } else {
+ blend = cfg.channel_mode_args[channel_mode];
+ }
+
+ calc_2ch_blend(warm, cool, brightness, DSM_TOP, blend);
+}
+
+void set_level_blend(uint8_t level) {
+ PWM_DATATYPE warm, cool;
+ blend_helper(&warm, &cool, level);
+ set_hw_levels(warm, cool);
+}
+
+void set_level_auto(uint8_t level) {
+ PWM_DATATYPE warm, cool;
+ blend_helper(&warm, &cool, level);
+ set_hw_levels(warm, cool);
+}
+
+///// "gradual tick" functions for smooth thermal regulation /////
+// (and other smooth adjustments)
+
+///// bump each channel toward a target value /////
+bool gradual_adjust(PWM_DATATYPE ch1, PWM_DATATYPE ch2) {
+ // adjust multiple times based on current brightness
+ // (so it adjusts faster/coarser when bright, slower/finer when dim)
+
+ // higher shift = slower/finer adjustments
+ const uint8_t shift = 9; // ((255 << 7) >> 9) = 63 max
+ uint8_t steps;
+
+ steps = ch1_dsm_lvl >> shift;
+ for (uint8_t i=0; i<=steps; i++)
+ GRADUAL_ADJUST_SIMPLE(ch1, ch1_dsm_lvl);
+
+ steps = ch2_dsm_lvl >> shift;
+ for (uint8_t i=0; i<=steps; i++)
+ GRADUAL_ADJUST_SIMPLE(ch2, ch2_dsm_lvl);
+
+ if ((ch1 == ch1_dsm_lvl)
+ && (ch2 == ch2_dsm_lvl )) {
+ return true; // done
+ }
+ return false; // not done yet
+}
+
+bool gradual_tick_ch1(uint8_t gt) {
+ PWM_DATATYPE pwm = PWM_GET(pwm1_levels, gt);
+ return gradual_adjust(pwm, 0);
+}
+
+bool gradual_tick_ch2(uint8_t gt) {
+ PWM_DATATYPE pwm = PWM_GET(pwm1_levels, gt);
+ return gradual_adjust(0, pwm);
+}
+
+bool gradual_tick_both(uint8_t gt) {
+ PWM_DATATYPE pwm = PWM_GET(pwm1_levels, gt);
+ return gradual_adjust(pwm, pwm);
+}
+
+bool gradual_tick_blend(uint8_t gt) {
+ PWM_DATATYPE warm, cool;
+ blend_helper(&warm, &cool, gt);
+ return gradual_adjust(warm, cool);
+}
+
+bool gradual_tick_auto(uint8_t gt) {
+ PWM_DATATYPE warm, cool;
+ blend_helper(&warm, &cool, gt);
+ return gradual_adjust(warm, cool);
+}
+
+
diff --git a/hw/sofirn/blf-lt1-t1616/hwdef.h b/hw/sofirn/blf-lt1-t1616/hwdef.h
new file mode 100644
index 0000000..7c1f10b
--- /dev/null
+++ b/hw/sofirn/blf-lt1-t1616/hwdef.h
@@ -0,0 +1,161 @@
+// BLF LT1 driver layout using the Attiny1616
+// Copyright (C) 2021-2023 (gchart), Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+/*
+ * Driver pinout:
+ * eSwitch: PA5
+ * Aux LED: PB5
+ * PWM cool: PB0 (TCA0 WO0)
+ * PWM warm: PB1 (TCA0 WO1)
+ * Voltage: VCC
+ */
+
+#define ATTINY 1616
+#include <avr/io.h>
+
+#define HWDEF_C_FILE hwdef-blf-lt1-t1616.c
+
+// allow using aux LEDs as extra channel modes
+#include "chan-aux.h"
+
+// channel modes:
+// * 0. warm only
+// * 1. cool only
+// * 2. both channels, tied together, max "200%" power
+// * 3. both channels, manual blend, max "100%" power
+// * 4. both channels, auto blend, reversible
+#define NUM_CHANNEL_MODES 6
+enum channel_modes_e {
+ CM_CH1 = 0,
+ CM_CH2,
+ CM_BOTH,
+ CM_BLEND,
+ CM_AUTO,
+ CM_AUX
+};
+
+// right-most bit first, modes are in fedcba9876543210 order
+#define CHANNEL_MODES_ENABLED 0b00011000
+#define USE_CHANNEL_MODE_ARGS
+// _, _, _, 128=middle CCT, 0=warm-to-cool
+#define CHANNEL_MODE_ARGS 0,0,0,128,0,0
+
+// can use some of the common handlers
+#define USE_CALC_2CH_BLEND
+
+
+#define PWM_CHANNELS 1 // old, remove this
+
+#define PWM_BITS 16 // 0 to 32640 (0 to 255 PWM + 0 to 127 DSM) at constant kHz
+#define PWM_GET PWM_GET16
+#define PWM_DATATYPE uint16_t
+#define PWM_DATATYPE2 uint32_t // only needs 32-bit if ramp values go over 255
+#define PWM1_DATATYPE uint16_t // 15-bit PWM+DSM ramp
+
+// PWM parameters of both channels are tied together because they share a counter
+// dynamic PWM
+#define PWM_TOP TCA0.SINGLE.PERBUF // holds the TOP value for for variable-resolution PWM
+#define PWM_TOP_INIT 255
+#define PWM_CNT TCA0.SINGLE.CNT // for resetting phase after each TOP adjustment
+// (max is (255 << 7), because it's 8-bit PWM plus 7 bits of DSM)
+#define DSM_TOP (255<<7) // 15-bit resolution leaves 1 bit for carry
+
+// timer interrupt for DSM
+#define DSM_vect TCA0_OVF_vect
+#define DSM_INTCTRL TCA0.SINGLE.INTCTRL
+#define DSM_INTFLAGS TCA0.SINGLE.INTFLAGS
+#define DSM_OVF_bm TCA_SINGLE_OVF_bm
+
+#define DELAY_FACTOR 90 // less time in delay() because more time spent in interrupts
+
+// warm LEDs
+uint16_t ch1_dsm_lvl;
+uint8_t ch1_pwm, ch1_dsm;
+#define CH1_PIN PB1
+#define CH1_PWM TCA0.SINGLE.CMP1BUF // CMP1 is the output compare register for PB1
+
+// cold LEDs
+uint16_t ch2_dsm_lvl;
+uint8_t ch2_pwm, ch2_dsm;
+#define CH2_PIN PB0
+#define CH2_PWM TCA0.SINGLE.CMP0BUF // CMP0 is the output compare register for PB0
+
+// lighted button
+#define AUXLED_PIN PIN5_bp
+#define AUXLED_PORT PORTB
+
+// e-switch
+#define SWITCH_PIN PIN5_bp
+#define SWITCH_PORT VPORTA.IN
+#define SWITCH_ISC_REG PORTA.PIN2CTRL
+#define SWITCH_VECT PORTA_PORT_vect
+#define SWITCH_INTFLG VPORTA.INTFLAGS
+
+// average drop across diode on this hardware
+#ifndef VOLTAGE_FUDGE_FACTOR
+#define VOLTAGE_FUDGE_FACTOR 7 // add 0.35V
+#endif
+
+
+inline void hwdef_setup() {
+
+ // set up the system clock to run at 10 MHz instead of the default 3.33 MHz
+ _PROTECTED_WRITE( CLKCTRL.MCLKCTRLB,
+ CLKCTRL_PDIV_2X_gc | CLKCTRL_PEN_bm );
+
+ //VPORTA.DIR = ...;
+ // Outputs
+ VPORTB.DIR = PIN0_bm // cool white
+ | PIN1_bm // warm white
+ // | PIN2_bm // for testing on LT1S Pro, disable red channel
+ | PIN5_bm; // aux LED
+ //VPORTC.DIR = ...;
+
+ // enable pullups on the unused pins to reduce power
+ PORTA.PIN0CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN1CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN2CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN3CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN4CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN5CTRL = PORT_PULLUPEN_bm | PORT_ISC_BOTHEDGES_gc; // eSwitch
+ PORTA.PIN6CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN7CTRL = PORT_PULLUPEN_bm;
+
+ //PORTB.PIN0CTRL = PORT_PULLUPEN_bm; // cold tint channel
+ //PORTB.PIN1CTRL = PORT_PULLUPEN_bm; // warm tint channel
+ PORTB.PIN2CTRL = PORT_PULLUPEN_bm; // comment out for testing on LT1S Pro
+ PORTB.PIN3CTRL = PORT_PULLUPEN_bm;
+ PORTB.PIN4CTRL = PORT_PULLUPEN_bm;
+ //PORTB.PIN5CTRL = PORT_PULLUPEN_bm; // Aux LED
+
+ PORTC.PIN0CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN1CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN2CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN3CTRL = PORT_PULLUPEN_bm;
+
+ // set up the PWM
+ // https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny1614-16-17-DataSheet-DS40002204A.pdf
+ // PB0 is TCA0:WO0, use TCA_SINGLE_CMP0EN_bm
+ // PB1 is TCA0:WO1, use TCA_SINGLE_CMP1EN_bm
+ // For Fast (Single Slope) PWM use TCA_SINGLE_WGMODE_SINGLESLOPE_gc
+ // For Phase Correct (Dual Slope) PWM use TCA_SINGLE_WGMODE_DSBOTTOM_gc
+ // TODO: add references to MCU documentation
+ TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm
+ | TCA_SINGLE_CMP1EN_bm
+ | TCA_SINGLE_WGMODE_DSBOTTOM_gc;
+ TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc
+ | TCA_SINGLE_ENABLE_bm;
+
+ PWM_TOP = PWM_TOP_INIT;
+
+ // set up interrupt for delta-sigma modulation
+ // (moved to hwdef.c functions so it can be enabled/disabled based on ramp level)
+ //DSM_INTCTRL |= DSM_OVF_bm; // interrupt once for each timer cycle
+
+}
+
+
+#define LAYOUT_DEFINED
+
diff --git a/hw/sofirn/blf-lt1/cfg.h b/hw/sofirn/blf-lt1/cfg.h
new file mode 100644
index 0000000..53c3203
--- /dev/null
+++ b/hw/sofirn/blf-lt1/cfg.h
@@ -0,0 +1,105 @@
+// BLF Lantern config options for Anduril
+// Copyright (C) 2018-2023 Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+#define MODEL_NUMBER "0621"
+#include "hwdef-blf-lt1.h"
+// ATTINY: 85
+
+// the button lights up
+#define USE_INDICATOR_LED
+// the button is visible while main LEDs are on
+#define USE_INDICATOR_LED_WHILE_RAMPING
+// off mode: low (1)
+// lockout: blinking (3)
+#define INDICATOR_LED_DEFAULT_MODE ((3<<2) + 1)
+
+// channel modes...
+// CM_CH1, CM_CH2, CM_BOTH, CM_BLEND, CM_AUTO
+#define DEFAULT_CHANNEL_MODE CM_AUTO
+//#define DEFAULT_BLINK_CHANNEL CM_BOTH // takes too much space
+
+// how much to increase total brightness at middle tint
+// (0 = 100% brightness, 64 = 200% brightness)
+//#define TINT_RAMPING_CORRECTION 26 // prototype, 140%
+//#define TINT_RAMPING_CORRECTION 10 // production model, 115%
+#define TINT_RAMPING_CORRECTION 0 // none
+
+#define RAMP_SIZE 150
+// delta-sigma modulated PWM (0b0HHHHHHHHLLLLLLL = 0, 8xHigh, 7xLow bits)
+// (max is (255 << 7), because it's 8-bit PWM plus 7 bits of DSM)
+// level_calc.py 3.333 1 150 7135 32 0.2 600 --pwm 32640
+#define PWM1_LEVELS 32,35,38,41,45,50,55,61,67,74,82,91,100,110,121,133,146,160,175,192,209,227,247,268,291,314,340,366,395,424,456,489,524,560,599,639,681,726,772,820,871,924,979,1036,1096,1158,1222,1289,1359,1431,1506,1584,1664,1747,1834,1923,2015,2111,2209,2311,2416,2524,2636,2751,2870,2992,3118,3247,3380,3518,3659,3803,3952,4105,4262,4423,4589,4759,4933,5111,5294,5482,5674,5871,6073,6279,6491,6707,6928,7155,7386,7623,7865,8113,8365,8624,8888,9157,9432,9713,10000,10292,10591,10895,11206,11523,11846,12175,12511,12853,13202,13557,13919,14287,14663,15045,15434,15830,16233,16644,17061,17486,17919,18358,18805,19260,19723,20193,20671,21156,21650,22152,22662,23180,23706,24241,24784,25335,25895,26464,27041,27627,28222,28826,29439,30060,30691,31332,31981,32640
+
+#define DEFAULT_LEVEL 75
+#define MAX_1x7135 75
+#define HALFSPEED_LEVEL 44
+#define QUARTERSPEED_LEVEL 34
+#undef USE_DYNAMIC_UNDERCLOCKING // makes huge bumps in the ramp
+
+#define USE_SMOOTH_STEPS
+//#define USE_SET_LEVEL_GRADUALLY
+
+// the default of 26 looks a bit flat, so increase it
+#define CANDLE_AMPLITUDE 40
+
+// override default ramp style
+#undef RAMP_STYLE
+#define RAMP_STYLE 1 // 0 = smooth, 1 = stepped
+// set floor and ceiling as far apart as possible
+// because this lantern isn't overpowered
+#define RAMP_SMOOTH_FLOOR 1
+#define RAMP_SMOOTH_CEIL 150
+#define RAMP_DISCRETE_FLOOR 1
+#define RAMP_DISCRETE_CEIL 150
+#define RAMP_DISCRETE_STEPS 7
+
+// LT1 can handle heat well, so don't limit simple mode
+#define SIMPLE_UI_FLOOR 10
+#define SIMPLE_UI_CEIL 150
+#define SIMPLE_UI_STEPS 5
+
+// Allow 3C in Simple UI for switching between smooth and stepped ramping
+#define USE_SIMPLE_UI_RAMPING_TOGGLE
+#define USE_EXTENDED_SIMPLE_UI
+
+// also at Sofirn's request, enable 2 click turbo (Anduril 1 style)
+#define DEFAULT_2C_STYLE 1
+
+
+// the sensor (attiny85) is nowhere near the emitters
+// so thermal regulation can't work
+#ifdef USE_THERMAL_REGULATION
+#undef USE_THERMAL_REGULATION
+#endif
+
+// don't blink while ramping
+#ifdef BLINK_AT_RAMP_FLOOR
+#undef BLINK_AT_RAMP_FLOOR
+#endif
+#ifdef BLINK_AT_RAMP_MIDDLE
+#undef BLINK_AT_RAMP_MIDDLE
+#endif
+#ifdef BLINK_AT_RAMP_CEIL
+#undef BLINK_AT_RAMP_CEIL
+#endif
+
+// too big, turn off extra features
+//#undef USE_STEPPED_TINT_RAMPING
+#undef USE_MOMENTARY_MODE
+#undef USE_TACTICAL_MODE
+#undef USE_SOS_MODE
+//#undef USE_SIMPLE_UI
+//#undef USE_BEACON_MODE
+//#undef USE_RAMP_SPEED_CONFIG
+#undef USE_RAMP_AFTER_MOON_CONFIG
+#undef USE_2C_STYLE_CONFIG
+#undef USE_VOLTAGE_CORRECTION
+//#undef USE_CHANNEL_PER_STROBE
+// party strobe, tac strobe, lightning, candle, bike
+#define DEFAULT_STROBE_CHANNELS CM_BOTH,CM_BOTH,CM_AUTO,CM_AUTO,CM_AUTO
+
+// for consistency with other models
+#define USE_SOFT_FACTORY_RESET
+
diff --git a/hw/sofirn/blf-lt1/hwdef.c b/hw/sofirn/blf-lt1/hwdef.c
new file mode 100644
index 0000000..8a4c0e1
--- /dev/null
+++ b/hw/sofirn/blf-lt1/hwdef.c
@@ -0,0 +1,204 @@
+// BLF LT1 PWM functions
+// Copyright (C) 2023 Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+
+void set_level_zero();
+
+void set_level_ch1(uint8_t level);
+void set_level_ch2(uint8_t level);
+void set_level_both(uint8_t level);
+void set_level_blend(uint8_t level);
+//void set_level_auto(uint8_t level); // redundant
+
+#if 0 // gradual adjustments are disabled to save space
+bool gradual_tick_ch1(uint8_t gt);
+bool gradual_tick_ch2(uint8_t gt);
+bool gradual_tick_both(uint8_t gt);
+bool gradual_tick_blend(uint8_t gt);
+//bool gradual_tick_auto(uint8_t gt); // redundant
+#endif
+
+
+Channel channels[] = {
+ { // channel 1 only
+ .set_level = set_level_ch1,
+ //.gradual_tick = gradual_tick_ch1,
+ .has_args = 0
+ },
+ { // channel 2 only
+ .set_level = set_level_ch2,
+ //.gradual_tick = gradual_tick_ch2,
+ .has_args = 0
+ },
+ { // both channels, tied together (max "200%" power)
+ .set_level = set_level_both,
+ //.gradual_tick = gradual_tick_both,
+ .has_args = 0
+ },
+ { // both channels, manual blend (max "100%" power)
+ .set_level = set_level_blend,
+ //.gradual_tick = gradual_tick_blend,
+ .has_args = 1
+ },
+ { // both channels, auto blend
+ .set_level = set_level_blend,
+ //.gradual_tick = gradual_tick_blend,
+ .has_args = 1
+ },
+};
+
+void set_level_zero() {
+ // disable timer 0 overflow interrupt
+ // (helps improve button press handling from Off state)
+ DSM_INTCTRL &= ~DSM_OVF_bm;
+
+ // turn off all LEDs
+ ch1_dsm_lvl = 0;
+ ch2_dsm_lvl = 0;
+ CH1_PWM = 0;
+ CH2_PWM = 0;
+}
+
+// wrap setting the dsm vars, to get a faster response
+// (just setting *_dsm_lvl doesn't work well for strobes)
+void set_hw_levels(PWM_DATATYPE ch1, PWM_DATATYPE ch2) {
+ // set delta-sigma soft levels
+ ch1_dsm_lvl = ch1;
+ ch2_dsm_lvl = ch2;
+
+ // set hardware PWM levels and init dsm loop
+ CH1_PWM = ch1_pwm = ch1 >> 7;
+ CH2_PWM = ch2_pwm = ch2 >> 7;
+
+ // enable timer overflow interrupt so DSM can work
+ DSM_INTCTRL |= DSM_OVF_bm;
+}
+
+// delta-sigma modulation of PWM outputs
+// happens on each Timer overflow (every 512 cpu clock cycles)
+// uses 8-bit pwm w/ 7-bit dsm (0b 0PPP PPPP PDDD DDDD)
+ISR(DSM_vect) {
+ // set new hardware values first,
+ // for best timing (reduce effect of interrupt jitter)
+ CH1_PWM = ch1_pwm;
+ CH2_PWM = ch2_pwm;
+
+ // calculate next values, now that timing matters less
+
+ // accumulate error
+ ch1_dsm += (ch1_dsm_lvl & 0x007f);
+ // next PWM = base PWM value + carry bit
+ ch1_pwm = (ch1_dsm_lvl >> 7) + (ch1_dsm > 0x7f);
+ // clear carry bit
+ ch1_dsm &= 0x7f;
+
+ // repeat for other channels
+
+ ch2_dsm += (ch2_dsm_lvl & 0x007f);
+ ch2_pwm = (ch2_dsm_lvl >> 7) + (ch2_dsm > 0x7f);
+ ch2_dsm &= 0x7f;
+}
+
+
+void set_level_ch1(uint8_t level) {
+ set_hw_levels(PWM_GET(pwm1_levels, level), 0);
+}
+
+void set_level_ch2(uint8_t level) {
+ set_hw_levels(0, PWM_GET(pwm1_levels, level));
+}
+
+void set_level_both(uint8_t level) {
+ PWM_DATATYPE pwm = PWM_GET(pwm1_levels, level);
+ set_hw_levels(pwm, pwm);
+}
+
+void blend_helper(PWM_DATATYPE *warm, PWM_DATATYPE *cool, uint8_t level) {
+ PWM_DATATYPE brightness = PWM_GET(pwm1_levels, level);
+ uint8_t blend;
+ if (channel_mode == CM_AUTO) {
+ blend = 255 * (uint16_t)level / RAMP_SIZE;
+ if (cfg.channel_mode_args[channel_mode] & 0b01000000)
+ blend = 255 - blend;
+ } else {
+ blend = cfg.channel_mode_args[channel_mode];
+ }
+
+ calc_2ch_blend(warm, cool, brightness, DSM_TOP, blend);
+}
+
+void set_level_blend(uint8_t level) {
+ PWM_DATATYPE warm, cool;
+ blend_helper(&warm, &cool, level);
+ set_hw_levels(warm, cool);
+}
+
+/*
+void set_level_auto(uint8_t level) {
+ PWM_DATATYPE warm, cool;
+ blend_helper(&warm, &cool, level);
+ set_hw_levels(warm, cool);
+}
+*/
+
+///// "gradual tick" functions for smooth thermal regulation /////
+// (and other smooth adjustments)
+
+#if 0 // disabled to save space
+///// bump each channel toward a target value /////
+bool gradual_adjust(PWM_DATATYPE ch1, PWM_DATATYPE ch2) {
+ // adjust multiple times based on current brightness
+ // (so it adjusts faster/coarser when bright, slower/finer when dim)
+
+ // higher shift = slower/finer adjustments
+ const uint8_t shift = 9; // ((255 << 7) >> 9) = 63 max
+ uint8_t steps;
+
+ steps = ch1_dsm_lvl >> shift;
+ for (uint8_t i=0; i<=steps; i++)
+ GRADUAL_ADJUST_SIMPLE(ch1, ch1_dsm_lvl);
+
+ steps = ch2_dsm_lvl >> shift;
+ for (uint8_t i=0; i<=steps; i++)
+ GRADUAL_ADJUST_SIMPLE(ch2, ch2_dsm_lvl);
+
+ if ((ch1 == ch1_dsm_lvl)
+ && (ch2 == ch2_dsm_lvl )) {
+ return true; // done
+ }
+ return false; // not done yet
+}
+
+bool gradual_tick_ch1(uint8_t gt) {
+ PWM_DATATYPE pwm = PWM_GET(pwm1_levels, gt);
+ return gradual_adjust(pwm, 0);
+}
+
+bool gradual_tick_ch2(uint8_t gt) {
+ PWM_DATATYPE pwm = PWM_GET(pwm1_levels, gt);
+ return gradual_adjust(0, pwm);
+}
+
+bool gradual_tick_both(uint8_t gt) {
+ PWM_DATATYPE pwm = PWM_GET(pwm1_levels, gt);
+ return gradual_adjust(pwm, pwm);
+}
+
+bool gradual_tick_blend(uint8_t gt) {
+ PWM_DATATYPE warm, cool;
+ blend_helper(&warm, &cool, gt);
+ return gradual_adjust(warm, cool);
+}
+
+/*
+bool gradual_tick_auto(uint8_t gt) {
+ PWM_DATATYPE warm, cool;
+ blend_helper(&warm, &cool, gt);
+ return gradual_adjust(warm, cool);
+}
+*/
+
+#endif // if 0
+
diff --git a/hw/sofirn/blf-lt1/hwdef.h b/hw/sofirn/blf-lt1/hwdef.h
new file mode 100644
index 0000000..b113fd4
--- /dev/null
+++ b/hw/sofirn/blf-lt1/hwdef.h
@@ -0,0 +1,114 @@
+// BLF LT1 driver layout
+// Copyright (C) 2018-2023 Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+/*
+ * ----
+ * Reset -|1 8|- VCC
+ * eswitch -|2 7|- (unused)
+ * aux LED -|3 6|- PWM (5000K)
+ * GND -|4 5|- PWM (3000K)
+ * ----
+ */
+
+#define ATTINY 85
+#include <avr/io.h>
+
+#define HWDEF_C_FILE hwdef-blf-lt1.c
+
+// channel modes:
+// * 0. channel 1 only
+// * 1. channel 2 only
+// * 2. both channels, tied together, max "200%" power
+// * 3. both channels, manual blend, max "100%" power
+// * 4. both channels, auto blend, reversible
+#define NUM_CHANNEL_MODES 5
+enum channel_modes_e {
+ CM_CH1 = 0,
+ CM_CH2,
+ CM_BOTH,
+ CM_BLEND,
+ CM_AUTO,
+};
+
+
+// right-most bit first, modes are in fedcba9876543210 order
+#define CHANNEL_MODES_ENABLED 0b00011000
+#define USE_CHANNEL_MODE_ARGS
+// _, _, _, 128=middle CCT, 0=warm-to-cool
+#define CHANNEL_MODE_ARGS 0,0,0,128,0
+
+// can use some of the common handlers
+#define USE_CALC_2CH_BLEND
+
+
+#define PWM_CHANNELS 1 // old, remove this
+
+#define PWM_BITS 16 // 8-bit hardware PWM + 16-bit DSM
+
+#define PWM_GET PWM_GET16
+#define PWM_DATATYPE uint16_t
+#define PWM_DATATYPE2 uint32_t // only needs 32-bit if ramp values go over 255
+#define PWM1_DATATYPE uint16_t // 15-bit PWM+DSM ramp
+
+// PWM parameters of both channels are tied together because they share a counter
+#define PWM_TOP_INIT 255
+#define DSM_TOP (255<<7) // 15-bit resolution leaves 1 bit for carry
+
+// timer interrupt for DSM
+#define DSM_vect TIMER0_OVF_vect
+#define DSM_INTCTRL TIMSK
+#define DSM_OVF_bm (1<<TOIE0)
+
+#define DELAY_FACTOR 90 // less time in delay() because more time spent in interrupts
+
+// warm LEDs
+uint16_t ch1_dsm_lvl;
+uint8_t ch1_pwm, ch1_dsm;
+#define CH1_PIN PB1 // pin 6, warm tint PWM
+#define CH1_PWM OCR0B // OCR0B is the output compare register for PB1
+
+// cold LEDs
+uint16_t ch2_dsm_lvl;
+uint8_t ch2_pwm, ch2_dsm;
+#define CH2_PIN PB0 // pin 5, cold tint PWM
+#define CH2_PWM OCR0A // OCR0A is the output compare register for PB0
+
+// lighted button
+#define AUXLED_PIN PB4 // pin 3
+
+// e-switch
+#define SWITCH_PIN PB3 // pin 2
+#define SWITCH_PCINT PCINT3 // pin 2 pin change interrupt
+
+#define ADC_PRSCL 0x07 // clk/128
+
+// average drop across diode on this hardware
+#ifndef VOLTAGE_FUDGE_FACTOR
+#define VOLTAGE_FUDGE_FACTOR 7 // add 0.35V
+#endif
+
+#define FAST 0xA3 // fast PWM both channels
+#define PHASE 0xA1 // phase-correct PWM both channels
+
+
+inline void hwdef_setup() {
+ // configure PWM channels
+ DDRB = (1 << CH1_PIN)
+ | (1 << CH2_PIN);
+
+ TCCR0B = 0x01; // pre-scaler for timer (1 => 1, 2 => 8, 3 => 64...)
+ TCCR0A = PHASE;
+
+ // enable timer overflow interrupt for DSM purposes
+ //DSM_INTCTRL |= DSM_OVF_bm; // moved to hwdef.c functions instead
+
+ // configure e-switch
+ PORTB = (1 << SWITCH_PIN); // e-switch is the only input
+ PCMSK = (1 << SWITCH_PIN); // pin change interrupt uses this pin
+}
+
+
+#define LAYOUT_DEFINED
+
diff --git a/hw/sofirn/blf-q8-t1616/cfg.h b/hw/sofirn/blf-q8-t1616/cfg.h
new file mode 100644
index 0000000..9f0402c
--- /dev/null
+++ b/hw/sofirn/blf-q8-t1616/cfg.h
@@ -0,0 +1,82 @@
+// BLF Q8 config options for Anduril using the Attiny1616
+// Copyright (C) 2021-2023 gchart, Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+#define MODEL_NUMBER "0613"
+#include "hwdef-blf-q8-t1616.h"
+#include "wurkkos-cfg.h" // Sofirn lights are closely related to Wurkkos
+// ATTINY: 1616
+
+// the button lights up
+#define USE_INDICATOR_LED
+// the button is visible while main LEDs are on
+#define USE_INDICATOR_LED_WHILE_RAMPING
+// off mode: low (1)
+// lockout: blinking (3)
+#define INDICATOR_LED_DEFAULT_MODE ((3<<2) + 1)
+
+// copied from Wurkkos TS25 ramp
+#define RAMP_SIZE 150
+// 7135 at 75/150
+// level_calc.py 5.7895 2 150 7135 1 0.1 130 FET 1 10 3000 --pwm dyn:74:4096:255:3
+// (with some manual tweaks)
+#define PWM1_LEVELS 1,1,2,3,3,4,5,6,7,8,9,11,12,13,15,16,18,19,21,23,26,27,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,71,74,76,78,80,82,85,87,90,93,96,100,103,107,112,116,122,127,133,140,147,154,163,171,182,192,203,215,228,241,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0
+// non-zero part of FET channel calculated with:
+// level_calc.py 3 1 75 7135 1 200 3000
+// (FIXME? there's a visible bump when the FET kicks in, even with just 1/255)
+#define PWM2_LEVELS 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,6,7,8,10,11,13,14,16,17,19,21,22,24,26,28,30,32,34,37,39,41,44,46,48,51,54,56,59,62,65,68,71,74,77,81,84,87,91,94,98,102,106,110,114,118,122,126,130,135,139,144,148,153,158,163,168,173,178,184,189,195,200,206,212,218,224,230,236,242,248,255
+#define PWM_TOPS 4095,2701,3200,3586,2518,2778,2834,2795,2705,2587,2455,2582,2412,2247,2256,2091,2062,1907,1860,1802,1737,1605,1542,1477,1412,1347,1284,1222,1162,1105,1050,997,946,898,853,810,768,730,693,658,625,594,564,536,503,485,462,439,418,398,384,366,353,340,327,319,307,298,292,284,280,273,269,266,263,260,258,256,256,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255
+
+#define MAX_1x7135 75
+#define DEFAULT_LEVEL 50
+#define MIN_THERM_STEPDOWN 60
+#define HALFSPEED_LEVEL 20
+#define QUARTERSPEED_LEVEL 5
+
+#define RAMP_SMOOTH_FLOOR 1
+#define RAMP_SMOOTH_CEIL 150
+// 20 38 56 [75] 93 111 130
+#define RAMP_DISCRETE_FLOOR 20
+#define RAMP_DISCRETE_CEIL 130
+#define RAMP_DISCRETE_STEPS 7
+
+// at Sofirn's request, use max (150) for the Simple UI ceiling
+#define SIMPLE_UI_FLOOR 1
+#define SIMPLE_UI_CEIL 150
+#define SIMPLE_UI_STEPS 5
+
+// also at Sofirn's request, enable 2 click turbo (Anduril 1 style)
+#define DEFAULT_2C_STYLE 1
+
+// enable SOS in the blinkies group
+#define USE_SOS_MODE
+#define USE_SOS_MODE_IN_BLINKY_GROUP
+
+// Allow 3C in Simple UI for switching between smooth and stepped ramping
+#define USE_SIMPLE_UI_RAMPING_TOGGLE
+
+// allow Aux Config and Strobe Modes in Simple UI
+#define USE_EXTENDED_SIMPLE_UI
+
+// stop panicking at ~75% power or ~3000 lm, this light has high thermal mass
+#define THERM_FASTER_LEVEL (RAMP_SIZE*9/10) // throttle back faster when high
+
+// show each channel while it scroll by in the menu
+#define USE_CONFIG_COLORS
+
+// blink numbers on the aux LEDs by default
+#define DEFAULT_BLINK_CHANNEL CM_AUX
+
+// the default of 26 looks a bit rough, so increase it to make it smoother
+#define CANDLE_AMPLITUDE 33
+
+// don't blink during the ramp; the button LED brightness is sufficient
+// to indicate which power channel(s) are being used
+#ifdef BLINK_AT_RAMP_MIDDLE
+#undef BLINK_AT_RAMP_MIDDLE
+#endif
+
+// enable factory reset on 13H without loosening tailcap (required)
+#define USE_SOFT_FACTORY_RESET
+
diff --git a/hw/sofirn/blf-q8-t1616/hwdef.h b/hw/sofirn/blf-q8-t1616/hwdef.h
new file mode 100644
index 0000000..d9b981f
--- /dev/null
+++ b/hw/sofirn/blf-q8-t1616/hwdef.h
@@ -0,0 +1,138 @@
+// BLF Q8 driver layout using the Attiny1616
+// Copyright (C) 2021-2023 gchart, Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+/*
+ * (based on Wurkkos TS10 driver layout,
+ * which in turn was based on an older version of this BLF-Q8-t1616 driver)
+ * (should probably merge the two files at some point)
+ * Driver pinout:
+ * eSwitch: PA5
+ * Aux LED: PB5
+ * PWM FET: PB0 (TCA0 WO0)
+ * PWM 1x7135: PB1 (TCA0 WO1)
+ * Voltage: VCC
+ */
+
+#define ATTINY 1616
+#include <avr/io.h>
+
+// nearly all t1616-based FET+1 drivers work pretty much the same
+// (this one has single-color aux like the TS10)
+#define HWDEF_C_FILE hwdef-wurkkos-ts10.c
+
+// allow using aux LEDs as extra channel modes
+#include "chan-aux.h"
+
+// channel modes:
+// * 0. FET+7135 stacked
+// * 1. aux LEDs
+#define NUM_CHANNEL_MODES 2
+enum CHANNEL_MODES {
+ CM_MAIN = 0,
+ CM_AUX
+};
+
+#define DEFAULT_CHANNEL_MODE CM_MAIN
+
+// right-most bit first, modes are in fedcba9876543210 order
+#define CHANNEL_MODES_ENABLED 0b00000001
+
+
+#define PWM_CHANNELS 2 // old, remove this
+
+#define PWM_BITS 16 // dynamic 16-bit, but never goes over 255
+#define PWM_GET PWM_GET8
+#define PWM_DATATYPE uint16_t // is used for PWM_TOPS (which goes way over 255)
+#define PWM_DATATYPE2 uint16_t // only needs 32-bit if ramp values go over 255
+#define PWM1_DATATYPE uint8_t // 1x7135 ramp
+#define PWM2_DATATYPE uint8_t // DD FET ramp
+
+// PWM parameters of both channels are tied together because they share a counter
+#define PWM_TOP TCA0.SINGLE.PERBUF // holds the TOP value for for variable-resolution PWM
+#define PWM_TOP_INIT 255 // highest value used in top half of ramp
+// not necessary when double-buffered "BUF" registers are used
+#define PWM_CNT TCA0.SINGLE.CNT // for resetting phase after each TOP adjustment
+
+// 1x7135 channel
+#define CH1_PIN PB1
+#define CH1_PWM TCA0.SINGLE.CMP1BUF // CMP1 is the output compare register for PB1
+
+// DD FET channel
+#define CH2_PIN PB0
+#define CH2_PWM TCA0.SINGLE.CMP0BUF // CMP0 is the output compare register for PB0
+
+// e-switch
+#define SWITCH_PIN PIN5_bp
+#define SWITCH_PORT VPORTA.IN
+#define SWITCH_ISC_REG PORTA.PIN2CTRL
+#define SWITCH_VECT PORTA_PORT_vect
+#define SWITCH_INTFLG VPORTA.INTFLAGS
+
+// average drop across diode on this hardware
+#ifndef VOLTAGE_FUDGE_FACTOR
+#define VOLTAGE_FUDGE_FACTOR 7 // add 0.35V
+#endif
+
+// lighted button
+#define AUXLED_PIN PIN5_bp
+#define AUXLED_PORT PORTB
+
+
+inline void hwdef_setup() {
+
+ // set up the system clock to run at 10 MHz instead of the default 3.33 MHz
+ _PROTECTED_WRITE( CLKCTRL.MCLKCTRLB,
+ CLKCTRL_PDIV_2X_gc | CLKCTRL_PEN_bm );
+
+ //VPORTA.DIR = ...;
+ // Outputs
+ VPORTB.DIR = PIN0_bm // DD FET
+ | PIN1_bm // 7135
+ | PIN5_bm; // Aux LED
+ //VPORTC.DIR = ...;
+
+ // enable pullups on the unused pins to reduce power
+ PORTA.PIN0CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN1CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN2CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN3CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN4CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN5CTRL = PORT_PULLUPEN_bm | PORT_ISC_BOTHEDGES_gc; // eSwitch
+ PORTA.PIN6CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN7CTRL = PORT_PULLUPEN_bm;
+
+ //PORTB.PIN0CTRL = PORT_PULLUPEN_bm; // FET channel
+ //PORTB.PIN1CTRL = PORT_PULLUPEN_bm; // 7135 channel
+ PORTB.PIN2CTRL = PORT_PULLUPEN_bm;
+ PORTB.PIN3CTRL = PORT_PULLUPEN_bm;
+ PORTB.PIN4CTRL = PORT_PULLUPEN_bm;
+ //PORTB.PIN5CTRL = PORT_PULLUPEN_bm; // Aux LED
+
+ PORTC.PIN0CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN1CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN2CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN3CTRL = PORT_PULLUPEN_bm;
+
+ // set up the PWM
+ // https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny1614-16-17-DataSheet-DS40002204A.pdf
+ // PB0 is TCA0:WO0, use TCA_SINGLE_CMP0EN_bm
+ // PB1 is TCA0:WO1, use TCA_SINGLE_CMP1EN_bm
+ // PB2 is TCA0:WO2, use TCA_SINGLE_CMP2EN_bm
+ // For Fast (Single Slope) PWM use TCA_SINGLE_WGMODE_SINGLESLOPE_gc
+ // For Phase Correct (Dual Slope) PWM use TCA_SINGLE_WGMODE_DSBOTTOM_gc
+ // See the manual for other pins, clocks, configs, portmux, etc
+ TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm
+ | TCA_SINGLE_CMP1EN_bm
+ | TCA_SINGLE_WGMODE_DSBOTTOM_gc;
+ TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc
+ | TCA_SINGLE_ENABLE_bm;
+
+ PWM_TOP = PWM_TOP_INIT;
+
+}
+
+
+#define LAYOUT_DEFINED
+
diff --git a/hw/sofirn/blf-q8/cfg.h b/hw/sofirn/blf-q8/cfg.h
new file mode 100644
index 0000000..29dbcb7
--- /dev/null
+++ b/hw/sofirn/blf-q8/cfg.h
@@ -0,0 +1,71 @@
+// BLF Q8 config options for Anduril
+// Copyright (C) 2018-2023 Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+#define MODEL_NUMBER "0611"
+#include "hwdef-blf-q8.h"
+
+#define RAMP_SIZE 150
+
+// the button lights up
+#define USE_INDICATOR_LED
+// the button is visible while main LEDs are on
+#define USE_INDICATOR_LED_WHILE_RAMPING
+// off mode: high (2)
+// lockout: blinking (3)
+#define INDICATOR_LED_DEFAULT_MODE ((3<<2) + 2)
+
+// copied from Emisar D4 ramp
+// ../../bin/level_calc.py 1 65 7135 1 0.8 150
+// ... mixed with this:
+// ../../bin/level_calc.py 2 150 7135 4 0.33 150 FET 1 10 1500
+#define PWM1_LEVELS 1,1,2,2,3,3,4,4,5,6,7,8,9,10,12,13,14,15,17,19,20,22,24,26,29,31,34,36,39,42,45,48,51,55,59,62,66,70,75,79,84,89,93,99,104,110,115,121,127,134,140,147,154,161,168,176,184,192,200,209,217,226,236,245,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0
+#define PWM2_LEVELS 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,4,5,7,8,9,11,12,14,15,17,19,20,22,24,25,27,29,31,33,35,37,39,41,43,45,48,50,52,55,57,59,62,64,67,70,72,75,78,81,84,87,90,93,96,99,102,105,109,112,115,119,122,126,129,133,137,141,144,148,152,156,160,165,169,173,177,182,186,191,195,200,205,209,214,219,224,229,234,239,244,250,255
+
+#define MAX_1x7135 65
+#define DEFAULT_LEVEL 65
+#define HALFSPEED_LEVEL 14
+#define QUARTERSPEED_LEVEL 5
+
+#define RAMP_SMOOTH_FLOOR 1
+#define RAMP_SMOOTH_CEIL 120
+// 10 28 46 [65] 83 101 120
+#define RAMP_DISCRETE_FLOOR 10
+#define RAMP_DISCRETE_CEIL RAMP_SMOOTH_CEIL
+#define RAMP_DISCRETE_STEPS 7
+
+// at Sofirn's request, use max (150) for the Simple UI ceiling
+#define SIMPLE_UI_FLOOR RAMP_DISCRETE_FLOOR
+#define SIMPLE_UI_CEIL 150
+#define SIMPLE_UI_STEPS 5
+
+// also at Sofirn's request, enable 2 click turbo (Anduril 1 style)
+#define DEFAULT_2C_STYLE 1
+
+// enable SOS in the blinkies group
+#define USE_SOS_MODE
+#define USE_SOS_MODE_IN_BLINKY_GROUP
+
+// Allow 3C in Simple UI for switching between smooth and stepped ramping
+#define USE_SIMPLE_UI_RAMPING_TOGGLE
+#define USE_EXTENDED_SIMPLE_UI
+
+// stop panicking at ~75% power or ~3000 lm, this light has high thermal mass
+#define THERM_FASTER_LEVEL (RAMP_SIZE*9/10) // throttle back faster when high
+
+// don't blink mid-ramp
+#ifdef BLINK_AT_RAMP_MIDDLE
+#undef BLINK_AT_RAMP_MIDDLE
+#endif
+
+#define USE_SMOOTH_STEPS
+
+// too big, remove stuff to make room
+#undef USE_SOS_MODE
+//#undef USE_RAMP_AFTER_MOON_CONFIG
+//#undef USE_RAMP_SPEED_CONFIG
+//#undef USE_VOLTAGE_CORRECTION
+//#undef USE_2C_STYLE_CONFIG
+//#undef USE_TACTICAL_STROBE_MODE
+#undef USE_TACTICAL_MODE
diff --git a/hw/sofirn/blf-q8/hwdef.h b/hw/sofirn/blf-q8/hwdef.h
new file mode 100644
index 0000000..cdf311d
--- /dev/null
+++ b/hw/sofirn/blf-q8/hwdef.h
@@ -0,0 +1,20 @@
+// BLF Q8 driver layout
+// Copyright (C) 2018-2023 Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+// Q8 driver is the same as a D4, basically
+
+// ... except the Q8 has a lighted button
+#ifndef AUXLED_PIN
+#define AUXLED_PIN PB4 // pin 3
+#endif
+
+// ... and slightly different calibration
+#ifndef VOLTAGE_FUDGE_FACTOR
+#define VOLTAGE_FUDGE_FACTOR 7 // add 0.35V
+#endif
+
+// Q8 driver is the same as a D4, basically
+#include "hwdef-emisar-d4.h"
+
diff --git a/hw/sofirn/lt1s-pro/cfg.h b/hw/sofirn/lt1s-pro/cfg.h
new file mode 100644
index 0000000..440a03d
--- /dev/null
+++ b/hw/sofirn/lt1s-pro/cfg.h
@@ -0,0 +1,125 @@
+// Sofirn LT1S Pro config file for Anduril
+// Copyright (C) 2022-2023 (FIXME)
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+#define MODEL_NUMBER "0623"
+#include "hwdef-sofirn-lt1s-pro.h"
+// ATTINY: 1616
+
+// off mode: low (1)
+// lockout: blinking (3)
+// Standby power usage:
+// - aux high: 6.9 mA (30 days)
+// - aux low: 0.16 mA (3.5 years)
+// - red moon: 2.17 mA (96 days)
+// - white moon: 1.47 mA (141 days)
+// Low mode isn't bright enough to be useful on this light,
+// but at least it doesn't drain the battery 3X faster than moon mode.
+// (it seriously would be more practical to just use moon instead)
+#define INDICATOR_LED_DEFAULT_MODE ((3<<2) + 1)
+
+// channel modes...
+// CM_WHITE, CM_AUTO2, CM_AUTO3, CM_RED, CM_WHITE_RED
+#define DEFAULT_CHANNEL_MODE CM_AUTO3
+
+#define FACTORY_RESET_WARN_CHANNEL CM_RED
+#define FACTORY_RESET_SUCCESS_CHANNEL CM_WHITE
+
+#define CONFIG_WAITING_CHANNEL CM_RED
+#define CONFIG_BLINK_CHANNEL CM_WHITE
+
+// blink numbers on the main LEDs by default (but allow user to change it)
+#define DEFAULT_BLINK_CHANNEL CM_RED
+
+// how much to increase total brightness at middle tint
+// (0 = 100% brightness, 64 = 200% brightness)
+// seems unnecessary on this light
+#define TINT_RAMPING_CORRECTION 0
+
+#define RAMP_SIZE 150
+// use dynamic PWM instead of plain 8-bit
+// (so we can get lower lows and a smoother ramp)
+// (also, red LEDs use a QX7138 chip which has max PWM speed of 10 kHz,
+// and it behaves erratically at full speed,
+// so PWM here is 576 clock cycles long to keep the speed low enough)
+//
+// This first ramp seems a bit too low: 0.2 / 1.9 / 10 / 37 / 109 / 272 / 600 lm
+// level_calc.py 5.99 1 150 7135 1 0.2 600 --pwm dyn:77:16383:575:3
+//#define PWM_LEVELS 1,1,2,2,3,4,4,5,6,6,7,8,9,9,10,11,11,12,13,13,14,15,15,16,16,17,18,18,19,19,19,20,20,21,21,21,21,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,23,23,23,23,24,24,24,25,26,26,27,28,29,30,32,33,34,36,38,40,42,44,46,48,50,52,55,57,59,62,65,68,70,73,77,80,83,86,90,94,97,101,105,110,114,118,123,128,133,138,143,148,154,160,166,172,178,185,191,198,205,213,220,228,236,244,252,261,270,279,289,298,308,319,329,340,351,363,374,386,399,411,424,438,452,466,480,495,510,526,542,558,575
+//#define PWM_TOPS 16383,10869,13246,8043,11458,12772,10093,11043,11450,9664,9991,10091,10048,8868,8838,8730,7814,7724,7589,6864,6748,6604,6024,5899,5398,5287,5159,4754,4638,4287,3963,3876,3594,3511,3265,3038,2829,2770,2586,2417,2260,2115,1981,1857,1742,1636,1537,1445,1360,1281,1207,1138,1073,1013,957,904,855,848,803,760,720,714,677,643,637,630,599,592,585,577,569,579,570,560,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575
+//
+// This ramp is a bit higher: 0.2 / 3 / 19 / 61 / 152 / 320 / 600 lm
+// level_calc.py 4.001 1 150 7135 1 0.2 600 --pwm dyn:78:16383:575:3.333
+#define PWM1_LEVELS 1,1,2,4,5,6,7,8,9,10,12,13,14,15,17,18,19,21,22,23,24,25,26,27,28,29,30,31,32,33,33,34,34,35,35,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,37,37,38,38,39,40,40,41,43,44,45,47,48,50,52,54,56,59,62,64,67,69,72,75,78,81,84,87,90,94,97,101,104,108,112,116,120,124,128,133,137,142,147,151,156,161,167,172,177,183,189,194,200,206,213,219,226,232,239,246,253,260,268,275,283,291,299,307,316,324,333,342,351,361,370,380,390,400,410,420,431,442,453,464,476,487,499,511,523,536,549,562,575
+#define PWM_TOPS 16383,8174,7823,14429,13603,12806,12046,11328,10652,10017,10402,9742,9134,8575,8615,8089,7605,7536,7093,6684,6307,5959,5636,5337,5060,4802,4562,4337,4127,3929,3633,3468,3216,3077,2862,2744,2559,2390,2234,2091,1960,1838,1727,1623,1528,1439,1357,1280,1209,1143,1081,1024,970,919,872,828,787,770,732,716,682,668,654,624,611,613,600,587,587,574,573,571,569,566,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575,575
+// TODO? 200% power at top of ramp on white blend mode
+// 2nd table handles "200% power" turbo
+//#define PWM2_LEVELS ...
+// tops for PWM2
+//#define PWM3_LEVELS ...
+#define MAX_1x7135 75
+#define MIN_THERM_STEPDOWN 75 // should be above highest dyn_pwm level
+#define HALFSPEED_LEVEL 12
+#define QUARTERSPEED_LEVEL 5
+
+// the default of 26 looks a bit flat, so increase it
+#define CANDLE_AMPLITUDE 40
+
+// override default ramp style
+#undef RAMP_STYLE
+#define RAMP_STYLE 1 // 0 = smooth, 1 = stepped
+// set floor and ceiling as far apart as possible
+// because this lantern isn't overpowered
+#define RAMP_SMOOTH_FLOOR 1
+#define RAMP_SMOOTH_CEIL 150
+//#define RAMP_DISCRETE_FLOOR 17 // 17 50 83 116 150
+#define RAMP_DISCRETE_FLOOR 1 // 1 25 50 75 100 125 150
+#define RAMP_DISCRETE_CEIL 150
+#define RAMP_DISCRETE_STEPS 7
+
+// LT1S can handle heat well, so don't limit simple mode
+//#define SIMPLE_UI_FLOOR 10 // 10 45 80 115 150
+#define SIMPLE_UI_FLOOR 1
+#define SIMPLE_UI_CEIL 150
+#define SIMPLE_UI_STEPS 7
+
+// Allow 3C (or 6C) in Simple UI (toggle smooth or stepped ramping)
+#define USE_SIMPLE_UI_RAMPING_TOGGLE
+
+// allow Aux Config and Strobe Modes in Simple UI
+#define USE_EXTENDED_SIMPLE_UI
+
+// turn on at med-low brightness by default (level 50/150, or ramp step 3/7)
+// (also sets lockout mode 2H to a useful level)
+#define DEFAULT_MANUAL_MEMORY 50
+// reset to default after being off for 10 minutes
+#define DEFAULT_MANUAL_MEMORY_TIMER 10
+
+// enable 2 click turbo (Anduril 1 style)
+#define DEFAULT_2C_STYLE 1
+
+#define USE_SOS_MODE
+#define USE_SOS_MODE_IN_BLINKY_GROUP
+
+#define USE_POLICE_COLOR_STROBE_MODE
+#define POLICE_COLOR_STROBE_CH1 CM_RED
+#define POLICE_COLOR_STROBE_CH2 CM_WHITE
+
+#undef TACTICAL_LEVELS
+#define TACTICAL_LEVELS 120,30,(RAMP_SIZE+3) // high, low, police strobe
+
+// don't blink while ramping
+#ifdef BLINK_AT_RAMP_MIDDLE
+#undef BLINK_AT_RAMP_MIDDLE
+#endif
+#ifdef BLINK_AT_RAMP_FLOOR
+#undef BLINK_AT_RAMP_FLOOR
+#endif
+#ifdef BLINK_AT_RAMP_CEIL
+#undef BLINK_AT_RAMP_CEIL
+#endif
+// without this, it's really hard to tell when ramping up stops
+#define BLINK_AT_RAMP_CEIL
+
+#define USE_SOFT_FACTORY_RESET
diff --git a/hw/sofirn/lt1s-pro/hwdef.c b/hw/sofirn/lt1s-pro/hwdef.c
new file mode 100644
index 0000000..90c2c07
--- /dev/null
+++ b/hw/sofirn/lt1s-pro/hwdef.c
@@ -0,0 +1,237 @@
+// BLF LT1S Pro hwdef functions
+// Copyright (C) 2023 Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+
+void set_level_zero();
+
+void set_level_red(uint8_t level);
+void set_level_white_blend(uint8_t level);
+void set_level_auto_2ch_blend(uint8_t level);
+void set_level_auto_3ch_blend(uint8_t level);
+void set_level_red_white_blend(uint8_t level);
+
+bool gradual_tick_red(uint8_t gt);
+bool gradual_tick_white_blend(uint8_t gt);
+bool gradual_tick_auto_2ch_blend(uint8_t gt);
+bool gradual_tick_auto_3ch_blend(uint8_t gt);
+bool gradual_tick_red_white_blend(uint8_t gt);
+
+
+Channel channels[] = {
+ { // manual blend of warm and cool white
+ .set_level = set_level_white_blend,
+ .gradual_tick = gradual_tick_white_blend,
+ .has_args = 1
+ },
+ { // auto blend from warm white to cool white
+ .set_level = set_level_auto_2ch_blend,
+ .gradual_tick = gradual_tick_auto_2ch_blend,
+ .has_args = 0
+ },
+ { // auto blend from red to warm white to cool white
+ .set_level = set_level_auto_3ch_blend,
+ .gradual_tick = gradual_tick_auto_3ch_blend,
+ .has_args = 0
+ },
+ { // red only
+ .set_level = set_level_red,
+ .gradual_tick = gradual_tick_red,
+ .has_args = 0
+ },
+ { // manual white blend + adjustable red
+ .set_level = set_level_red_white_blend,
+ .gradual_tick = gradual_tick_red_white_blend,
+ .has_args = 1
+ }
+};
+
+
+// calculate a 3-channel "auto tint" blend
+// (like red -> warm white -> cool white)
+// results are placed in *a, *b, and *c vars
+// level : ramp level to convert into 3 channel levels
+// (assumes ramp table is "pwm1_levels")
+void calc_auto_3ch_blend(
+ PWM_DATATYPE *a,
+ PWM_DATATYPE *b,
+ PWM_DATATYPE *c,
+ uint8_t level) {
+
+ PWM_DATATYPE vpwm = PWM_GET(pwm1_levels, level);
+
+ // tint goes from 0 (red) to 127 (warm white) to 255 (cool white)
+ uint8_t mytint;
+ mytint = 255 * (uint16_t)level / RAMP_SIZE;
+
+ // red is high at 0, low at 255 (linear)
+ *a = (((PWM_DATATYPE2)(255 - mytint)
+ * (PWM_DATATYPE2)vpwm) + 127) / 255;
+ // warm white is low at 0 and 255, high at 127 (linear triangle)
+ *b = (((PWM_DATATYPE2)triangle_wave(mytint)
+ * (PWM_DATATYPE2)vpwm) + 127) / 255;
+ // cool white is low at 0, high at 255 (linear)
+ *c = (((PWM_DATATYPE2)mytint
+ * (PWM_DATATYPE2)vpwm) + 127) / 255;
+
+}
+
+
+void set_level_zero() {
+ WARM_PWM_LVL = 0;
+ COOL_PWM_LVL = 0;
+ RED_PWM_LVL = 0;
+ PWM_CNT = 0; // reset phase
+}
+
+// single set of LEDs with 1 power channel and dynamic PWM
+void set_level_red(uint8_t level) {
+ RED_PWM_LVL = PWM_GET(pwm1_levels, level);
+ // pulse frequency modulation, a.k.a. dynamic PWM
+ PWM_TOP = PWM_GET(pwm_tops, level);
+ // force reset phase when turning on from zero
+ // (because otherwise the initial response is inconsistent)
+ if (! actual_level) PWM_CNT = 0;
+}
+
+
+// warm + cool blend w/ dynamic PWM
+void set_level_white_blend(uint8_t level) {
+ PWM_DATATYPE warm_PWM, cool_PWM;
+ PWM_DATATYPE brightness = PWM_GET(pwm1_levels, level);
+ PWM_DATATYPE top = PWM_GET(pwm_tops, level);
+ uint8_t blend = cfg.channel_mode_args[channel_mode];
+
+ calc_2ch_blend(&warm_PWM, &cool_PWM, brightness, top, blend);
+
+ WARM_PWM_LVL = warm_PWM;
+ COOL_PWM_LVL = cool_PWM;
+ PWM_TOP = top;
+ if (! actual_level) PWM_CNT = 0; // reset phase
+}
+
+
+// same as white blend, but tint is calculated from the ramp level
+void set_level_auto_2ch_blend(uint8_t level) {
+ PWM_DATATYPE warm_PWM, cool_PWM;
+ PWM_DATATYPE brightness = PWM_GET(pwm1_levels, level);
+ PWM_DATATYPE top = PWM_GET(pwm_tops, level);
+ uint8_t blend = 255 * (uint16_t)level / RAMP_SIZE;
+
+ calc_2ch_blend(&warm_PWM, &cool_PWM, brightness, top, blend);
+
+ WARM_PWM_LVL = warm_PWM;
+ COOL_PWM_LVL = cool_PWM;
+ PWM_TOP = top;
+ if (! actual_level) PWM_CNT = 0; // reset phase
+}
+
+
+// "auto tint" channel mode with dynamic PWM
+void set_level_auto_3ch_blend(uint8_t level) {
+ PWM_DATATYPE a, b, c;
+ calc_auto_3ch_blend(&a, &b, &c, level);
+
+ // pulse frequency modulation, a.k.a. dynamic PWM
+ uint16_t top = PWM_GET(pwm_tops, level);
+
+ RED_PWM_LVL = a;
+ WARM_PWM_LVL = b;
+ COOL_PWM_LVL = c;
+ PWM_TOP = top;
+ if (! actual_level) PWM_CNT = 0;
+}
+
+
+// "white + red" channel mode
+void set_level_red_white_blend(uint8_t level) {
+ // set the warm+cool white LEDs first
+ channel_mode = CM_WHITE;
+ set_level_white_blend(level);
+ channel_mode = CM_WHITE_RED;
+
+ PWM_DATATYPE vpwm = PWM_GET(pwm1_levels, level);
+
+ // set the red LED as a ratio of the white output level
+ // 0 = no red
+ // 255 = red at 100% of white channel PWM
+ uint8_t ratio = cfg.channel_mode_args[channel_mode];
+
+ RED_PWM_LVL = (((PWM_DATATYPE2)ratio * (PWM_DATATYPE2)vpwm) + 127) / 255;
+ if (! actual_level) PWM_CNT = 0; // reset phase
+}
+
+
+///// "gradual tick" functions for smooth thermal regulation /////
+
+///// bump each channel toward a target value /////
+bool gradual_adjust(uint16_t red, uint16_t warm, uint16_t cool) {
+ GRADUAL_ADJUST_SIMPLE(red, RED_PWM_LVL );
+ GRADUAL_ADJUST_SIMPLE(warm, WARM_PWM_LVL);
+ GRADUAL_ADJUST_SIMPLE(cool, COOL_PWM_LVL);
+
+ // check for completion
+ if ((red == RED_PWM_LVL )
+ && (warm == WARM_PWM_LVL)
+ && (cool == COOL_PWM_LVL)) {
+ return true; // done
+ }
+ return false; // not done yet
+}
+
+bool gradual_tick_red(uint8_t gt) {
+ uint16_t red = PWM_GET(pwm1_levels, gt);
+ return gradual_adjust(red, 0, 0);
+}
+
+
+bool gradual_tick_white_blend(uint8_t gt) {
+ // figure out what exact PWM levels we're aiming for
+ PWM_DATATYPE warm_PWM, cool_PWM;
+ PWM_DATATYPE brightness = PWM_GET(pwm1_levels, gt);
+ PWM_DATATYPE top = PWM_GET(pwm_tops, gt);
+ uint8_t blend = cfg.channel_mode_args[channel_mode];
+
+ calc_2ch_blend(&warm_PWM, &cool_PWM, brightness, top, blend);
+
+ return gradual_adjust(0, warm_PWM, cool_PWM);
+}
+
+
+// same as white blend, but tint is calculated from the ramp level
+bool gradual_tick_auto_2ch_blend(uint8_t gt) {
+ // figure out what exact PWM levels we're aiming for
+ PWM_DATATYPE warm_PWM, cool_PWM;
+ PWM_DATATYPE brightness = PWM_GET(pwm1_levels, gt);
+ PWM_DATATYPE top = PWM_GET(pwm_tops, gt);
+ uint8_t blend = 255 * (uint16_t)gt / RAMP_SIZE;
+
+ calc_2ch_blend(&warm_PWM, &cool_PWM, brightness, top, blend);
+
+ return gradual_adjust(0, warm_PWM, cool_PWM);
+}
+
+
+bool gradual_tick_auto_3ch_blend(uint8_t gt) {
+ // figure out what exact PWM levels we're aiming for
+ PWM_DATATYPE red, warm, cool;
+ calc_auto_3ch_blend(&red, &warm, &cool, gt);
+ return gradual_adjust(red, warm, cool);
+}
+
+
+bool gradual_tick_red_white_blend(uint8_t gt) {
+ // figure out what exact PWM levels we're aiming for
+ PWM_DATATYPE red, warm, cool;
+ PWM_DATATYPE brightness = PWM_GET(pwm1_levels, gt);
+ PWM_DATATYPE top = PWM_GET(pwm_tops, gt);
+ uint8_t blend = cfg.channel_mode_args[CM_WHITE];
+ uint8_t ratio = cfg.channel_mode_args[channel_mode];
+
+ red = (((PWM_DATATYPE2)ratio * (PWM_DATATYPE2)brightness) + 127) / 255;
+ calc_2ch_blend(&warm, &cool, brightness, top, blend);
+
+ return gradual_adjust(red, warm, cool);
+}
+
diff --git a/hw/sofirn/lt1s-pro/hwdef.h b/hw/sofirn/lt1s-pro/hwdef.h
new file mode 100644
index 0000000..ae6b3bf
--- /dev/null
+++ b/hw/sofirn/lt1s-pro/hwdef.h
@@ -0,0 +1,151 @@
+// BLF LT1S Pro driver layout using the Attiny1616
+// Copyright (C) 2022-2023 (FIXME)
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+/*
+ * Driver pinout:
+ * eSwitch: PA5
+ * Aux LED: PB5
+ * WW PWM: PB0 (TCA0 WO0)
+ * CW PWM: PB1 (TCA0 WO1)
+ * Red PWM: PB2 (TCA0 WO2)
+ * Voltage: VCC
+ */
+
+#define ATTINY 1616
+#include <avr/io.h>
+
+#define HWDEF_C_FILE hwdef-sofirn-lt1s-pro.c
+
+// channel modes:
+// * 0. warm/cool white blend
+// * 1. auto 2ch white blend (warm -> cool by ramp level)
+// * 2. auto 3ch blend (red -> warm -> cool by ramp level)
+// * 3. red only
+// * 4. red + white blend
+#define NUM_CHANNEL_MODES 5
+enum channel_modes_e {
+ CM_WHITE = 0,
+ CM_AUTO2,
+ CM_AUTO3,
+ CM_RED,
+ CM_WHITE_RED,
+};
+
+#define CHANNEL_MODES_ENABLED 0b00011111
+#define USE_CHANNEL_MODE_ARGS
+// 128=middle CCT, _, _, _, 255=100% red
+#define CHANNEL_MODE_ARGS 128,0,0,0,255
+
+// can use some of the common handlers
+#define USE_CALC_2CH_BLEND
+//#define USE_CALC_AUTO_3CH_BLEND
+
+
+#define PWM_CHANNELS 1 // old, remove this
+
+// only using 16-bit PWM on this light
+#define PWM_BITS 16
+
+#define PWM_GET PWM_GET16
+#define PWM_DATATYPE uint16_t
+#define PWM1_DATATYPE uint16_t
+#define PWM_DATATYPE2 uint32_t
+
+// dynamic PWM
+// PWM parameters of all channels are tied together because they share a counter
+#define PWM_TOP_INIT 511 // highest value used in the top half of the ramp
+#define PWM_TOP TCA0.SINGLE.PERBUF // holds the TOP value for for variable-resolution PWM
+#define PWM_CNT TCA0.SINGLE.CNT // for resetting phase after each TOP adjustment
+
+// warm LEDs
+//#define WARM_PWM_PIN PB0
+#define WARM_PWM_LVL TCA0.SINGLE.CMP0BUF // CMP1 is the output compare register for PB0
+
+// cold LEDs
+//#define COOL_PWM_PIN PB1
+#define COOL_PWM_LVL TCA0.SINGLE.CMP1BUF // CMP0 is the output compare register for PB1
+
+// red LEDs
+//#define RED_PWM_PIN PB2
+#define RED_PWM_LVL TCA0.SINGLE.CMP2BUF // CMP2 is the output compare register for PB2
+
+// lighted button
+#define AUXLED_PIN PIN5_bp
+#define AUXLED_PORT PORTB
+
+// the button lights up
+#define USE_INDICATOR_LED
+// the button is visible while main LEDs are on
+#define USE_INDICATOR_LED_WHILE_RAMPING
+
+// e-switch
+#define SWITCH_PIN PIN5_bp
+#define SWITCH_PORT VPORTA.IN
+#define SWITCH_ISC_REG PORTA.PIN2CTRL
+#define SWITCH_VECT PORTA_PORT_vect
+#define SWITCH_INTFLG VPORTA.INTFLAGS
+
+// average drop across diode on this hardware
+#ifndef VOLTAGE_FUDGE_FACTOR
+#define VOLTAGE_FUDGE_FACTOR 7 // add 0.35V
+#endif
+
+
+inline void hwdef_setup() {
+
+ // set up the system clock to run at 10 MHz instead of the default 3.33 MHz
+ _PROTECTED_WRITE( CLKCTRL.MCLKCTRLB,
+ CLKCTRL_PDIV_2X_gc | CLKCTRL_PEN_bm );
+
+ //VPORTA.DIR = ...;
+ // Outputs
+ VPORTB.DIR = PIN0_bm // warm white
+ | PIN1_bm // cool white
+ | PIN2_bm // red
+ | PIN5_bm; // aux LED
+ //VPORTC.DIR = ...;
+
+ // enable pullups on the unused pins to reduce power
+ PORTA.PIN0CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN1CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN2CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN3CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN4CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN5CTRL = PORT_PULLUPEN_bm | PORT_ISC_BOTHEDGES_gc; // eSwitch
+ PORTA.PIN6CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN7CTRL = PORT_PULLUPEN_bm;
+
+ //PORTB.PIN0CTRL = PORT_PULLUPEN_bm; // warm tint channel
+ //PORTB.PIN1CTRL = PORT_PULLUPEN_bm; // cold tint channel
+ //PORTB.PIN2CTRL = PORT_PULLUPEN_bm; // red LEDs
+ PORTB.PIN3CTRL = PORT_PULLUPEN_bm;
+ PORTB.PIN4CTRL = PORT_PULLUPEN_bm;
+ //PORTB.PIN5CTRL = PORT_PULLUPEN_bm; // Aux LED
+
+ PORTC.PIN0CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN1CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN2CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN3CTRL = PORT_PULLUPEN_bm;
+
+ // set up the PWM
+ // https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny1614-16-17-DataSheet-DS40002204A.pdf
+ // PB0 is TCA0:WO0, use TCA_SINGLE_CMP0EN_bm
+ // PB1 is TCA0:WO1, use TCA_SINGLE_CMP1EN_bm
+ // PB2 is TCA0:WO2, use TCA_SINGLE_CMP2EN_bm
+ // For Fast (Single Slope) PWM use TCA_SINGLE_WGMODE_SINGLESLOPE_gc
+ // For Phase Correct (Dual Slope) PWM use TCA_SINGLE_WGMODE_DSBOTTOM_gc
+ // TODO: add references to MCU documentation
+ TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm
+ | TCA_SINGLE_CMP1EN_bm
+ | TCA_SINGLE_CMP2EN_bm
+ | TCA_SINGLE_WGMODE_DSBOTTOM_gc;
+ PWM_TOP = PWM_TOP_INIT;
+ TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc
+ | TCA_SINGLE_ENABLE_bm;
+}
+
+
+#define LAYOUT_DEFINED
+
diff --git a/hw/sofirn/sc21-pro/cfg.h b/hw/sofirn/sc21-pro/cfg.h
new file mode 100644
index 0000000..8fd2dee
--- /dev/null
+++ b/hw/sofirn/sc21-pro/cfg.h
@@ -0,0 +1,14 @@
+// Sofirn SC21 Pro - same setup as a Wurkkos TS10, but with the aux indicator on while ramping
+// Copyright (C) 2022-2023 (FIXME)
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+#include "cfg-wurkkos-ts10.h"
+#undef MODEL_NUMBER
+#define MODEL_NUMBER "0632"
+// ATTINY: 1616
+
+// turn on the aux LED while main LED is on
+#ifndef USE_INDICATOR_LED_WHILE_RAMPING
+#define USE_INDICATOR_LED_WHILE_RAMPING
+#endif
diff --git a/hw/sofirn/sp10-pro/cfg.h b/hw/sofirn/sp10-pro/cfg.h
new file mode 100644
index 0000000..0e2f28d
--- /dev/null
+++ b/hw/sofirn/sp10-pro/cfg.h
@@ -0,0 +1,81 @@
+// Sofirn SP10 Pro config options for Anduril
+// Copyright (C) 2022-2023 (original author TBD), Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+#define MODEL_NUMBER "0631"
+#include "hwdef-sofirn-sp10-pro.h"
+// ATTINY: 1616
+
+// 1....15: level_calc.py 3.01 1 15 7135 1 0.1 2 --pwm dyn:15:64:64
+// 16..150: level_calc.py 5.01 1 135 7135 1 2 800 --pwm dyn:49:3072:255:3.0
+#define RAMP_SIZE 150
+#define _PWM1_LEVELS_ 1, 2, 4, 6, 9,12,15,19,23,28,34,41,48,55,64
+#define _PWM1_TOPS_ 64,64,64,64,64,64,64,64,64,64,64,64,64,64,64
+#define _PWM2_LEVELS_ 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 12, 13, 13, 14, 15, 15, 16, 17, 18, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 33, 34, 35, 37, 38, 39, 41, 42, 44, 46, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 70, 72, 74, 77, 79, 82, 85, 88, 90, 93, 96, 99,103,106,109,113,116,120,123,127,131,135,139,143,147,151,156,160,165,170,175,180,185,190,195,201,206,212,218,223,230,236,242,248,255
+#define _PWM2_TOPS_ 3072,1960,2372,1476,2097,1572,1920,1570,1777,1524,1646,1454,1286,1369,1234,1115,1011,918,837,894,823,759,702,650,603,560,522,487,455,425,398,374,351,330,310,292,275,259,280,265,251,266,253,240,252,240,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255
+#define PWM1_LEVELS _PWM1_LEVELS_,_PWM2_TOPS_
+#define PWM2_LEVELS 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,_PWM2_LEVELS_
+#define PWM_TOPS _PWM1_TOPS_,_PWM2_TOPS_
+
+#define MAX_1x7135 15
+#define HALFSPEED_LEVEL 15
+#define QUARTERSPEED_LEVEL 15
+#define DEFAULT_LEVEL 50
+
+#define RAMP_SMOOTH_FLOOR 1
+#define RAMP_SMOOTH_CEIL 150
+// 1 25 50 [75] 100 125 150
+#define RAMP_DISCRETE_FLOOR 1
+#define RAMP_DISCRETE_CEIL RAMP_SMOOTH_CEIL
+#define RAMP_DISCRETE_STEPS 7
+
+// at Sofirn's request, use max (150) for the Simple UI ceiling
+// 15 48 [82] 116 150
+#define SIMPLE_UI_FLOOR MAX_1x7135
+#define SIMPLE_UI_CEIL 150
+#define SIMPLE_UI_STEPS 5
+
+// turn on at ~6 lm by default (level 50/150, or ramp step 2/5 or 3/7)
+// (also sets lockout mode 2H to a useful level)
+#define DEFAULT_MANUAL_MEMORY 50
+// reset to default after being off for 10 minutes
+#define DEFAULT_MANUAL_MEMORY_TIMER 10
+
+// enable SOS in the blinkies group
+#define USE_SOS_MODE
+#define USE_SOS_MODE_IN_BLINKY_GROUP
+
+// Allow 3C in Simple UI for switching between smooth and stepped ramping
+#define USE_SIMPLE_UI_RAMPING_TOGGLE
+
+// and finally, set the default ramp style to Stepped
+#undef RAMP_STYLE
+#define RAMP_STYLE 1 // 0 = smooth, 1 = stepped
+
+// stop panicking at ~30% power
+#define THERM_FASTER_LEVEL 105
+#define MIN_THERM_STEPDOWN 65 // must be > end of dynamic PWM range
+
+// slow down party strobe; this driver can't pulse for too short a time
+//#define PARTY_STROBE_ONTIME 8
+#define STROBE_OFF_LEVEL 1 // keep the regulator chip on between pulses
+
+// the default of 26 looks a bit flat, so increase it
+#define CANDLE_AMPLITUDE 50
+
+// enable 2 click turbo (replaces USE_2C_MAX_TURBO)
+#define DEFAULT_2C_STYLE 1
+
+// don't blink during the ramp or at the ceiling
+#ifdef BLINK_AT_RAMP_MIDDLE
+#undef BLINK_AT_RAMP_MIDDLE
+#endif
+#ifdef BLINK_AT_RAMP_CEIL
+#undef BLINK_AT_RAMP_CEIL
+#endif
+
+
+// enable factory reset on 13H without loosening tailcap
+#define USE_SOFT_FACTORY_RESET
+
diff --git a/hw/sofirn/sp10-pro/hwdef.c b/hw/sofirn/sp10-pro/hwdef.c
new file mode 100644
index 0000000..42844a7
--- /dev/null
+++ b/hw/sofirn/sp10-pro/hwdef.c
@@ -0,0 +1,63 @@
+// Sofirn SP10 Pro PWM helper functions
+// Copyright (C) 2023 Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+void set_level_zero();
+
+void set_level_main(uint8_t level);
+bool gradual_tick_main(uint8_t gt);
+
+
+Channel channels[] = {
+ { // main LEDs
+ .set_level = set_level_main,
+ .gradual_tick = gradual_tick_main
+ },
+};
+
+
+void set_level_zero() {
+ CH1_PWM = 0;
+ CH2_PWM = 0;
+ PWM_CNT = 0; // reset phase
+ BST_ENABLE_PORT &= ~(1 << BST_ENABLE_PIN); // boost off
+}
+
+// single set of LEDs with 2 stacked power channels
+void set_level_main(uint8_t level) {
+ PWM_DATATYPE ch1_pwm = PWM_GET(pwm1_levels, level);
+ PWM_DATATYPE ch2_pwm = PWM_GET(pwm2_levels, level);
+ // pulse frequency modulation, a.k.a. dynamic PWM
+ uint16_t top = PWM_GET16(pwm_tops, level);
+
+ BST_ENABLE_PORT |= (1 << BST_ENABLE_PIN); // boost on
+
+ CH1_PWM = ch1_pwm;
+ CH2_PWM = ch2_pwm;
+ PWM_TOP = top;
+
+ // force reset phase when turning on from zero
+ // (because otherwise the initial response is inconsistent)
+ if (! actual_level) PWM_CNT = 0;
+}
+
+bool gradual_tick_main(uint8_t gt) {
+ PWM_DATATYPE pwm1 = PWM_GET(pwm1_levels, gt);
+ PWM_DATATYPE pwm2 = PWM_GET(pwm2_levels, gt);
+
+ // ch1 sometimes makes huge leaps; don't make it gradual
+ // if either current or new level is in the leap zone, just leap
+ if ((CH1_PWM + pwm1) > 128) CH1_PWM = pwm1;
+ else GRADUAL_ADJUST_SIMPLE(pwm1, CH1_PWM);
+
+ GRADUAL_ADJUST_SIMPLE(pwm2, CH2_PWM);
+
+ if ( (pwm1 == CH1_PWM)
+ && (pwm2 == CH2_PWM)
+ ) {
+ return true; // done
+ }
+ return false; // not done yet
+}
+
diff --git a/hw/sofirn/sp10-pro/hwdef.h b/hw/sofirn/sp10-pro/hwdef.h
new file mode 100644
index 0000000..5cef6a7
--- /dev/null
+++ b/hw/sofirn/sp10-pro/hwdef.h
@@ -0,0 +1,157 @@
+// Sofirn SP10 Pro pinout
+// Copyright (C) 2022-2023 (original author TBD), Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+/*
+ * ATTINY1616 Mapping:
+ * PB5 : PWM small channel (TCA0 - WO2 Alternate MUX)
+ * PB3 : eSwitch
+ * PB0 : PWM big channel (TCA0 - WO0)
+ * PB4 : Voltage divider (ADC0 - AIN9)
+ * PA1 : Boost Enable
+ */
+
+#define ATTINY 1616
+#include <avr/io.h>
+
+#define HWDEF_C_FILE hwdef-sofirn-sp10-pro.c
+
+// channel modes:
+// * 0. low+high PWM stacked
+#define NUM_CHANNEL_MODES 1
+enum CHANNEL_MODES {
+ CM_MAIN = 0,
+};
+
+#define DEFAULT_CHANNEL_MODE CM_MAIN
+
+// right-most bit first, modes are in fedcba9876543210 order
+#define CHANNEL_MODES_ENABLED 0b00000001
+
+
+#define PWM_CHANNELS 2 // old, remove this
+
+#define PWM_BITS 16 // data type needs 16 bits, not 8
+#define PWM_GET PWM_GET16
+#define PWM_DATATYPE uint16_t // is used for PWM_TOPS (which goes way over 255)
+#define PWM_DATATYPE2 uint32_t // only needs 32-bit if ramp values go over 255
+#define PWM1_DATATYPE uint16_t // low PWM ramp
+#define PWM2_DATATYPE uint16_t // high PWM ramp
+
+// PWM parameters of both channels are tied together because they share a counter
+#define PWM_TOP TCA0.SINGLE.PERBUF // holds the TOP value for for variable-resolution PWM
+#define PWM_CNT TCA0.SINGLE.CNT // for resetting phase after each TOP adjustment
+#define PWM_TOP_INIT 255 // highest value used in top half of ramp (unused?)
+
+// Small channel
+#define CH1_PIN PB5
+#define CH1_PWM TCA0.SINGLE.CMP2BUF // PB5 is Alternate MUX for TCA Compare 2
+
+// Big channel
+#define CH2_PIN PB0
+#define CH2_PWM TCA0.SINGLE.CMP0BUF // PB0 is TCA Compare 0
+
+// boost enable
+#define BST_ENABLE_PIN PIN1_bp
+#define BST_ENABLE_PORT PORTA_OUT
+
+// e-switch
+#define SWITCH_PIN 3
+#define SWITCH_PORT VPORTB.IN
+#define SWITCH_ISC_REG PORTB.PIN3CTRL
+#define SWITCH_VECT PORTB_PORT_vect
+#define SWITCH_INTFLG VPORTB.INTFLAGS
+#define SWITCH_PCINT PCINT0
+#define PCINT_vect PCINT0_vect // ISR for PCINT[7:0]
+
+// Voltage divider battLVL
+#define USE_VOLTAGE_DIVIDER // use a dedicated pin, not VCC, because VCC input is regulated
+#define DUAL_VOLTAGE_FLOOR 21 // for AA/14500 boost drivers, don't indicate low voltage if below this level
+#define DUAL_VOLTAGE_LOW_LOW 7 // the lower voltage range's danger zone 0.7 volts (NiMH)
+#define ADMUX_VOLTAGE_DIVIDER ADC_MUXPOS_AIN9_gc // which ADC channel to read
+
+// Raw ADC readings at 4.4V and 2.2V
+// calibrate the voltage readout here
+// estimated / calculated values are:
+// (voltage - D1) * (R2/(R2+R1) * 1024 / 1.1)
+// Resistors are 300,000 and 100,000
+#ifndef ADC_44
+#define ADC_44 1023 // raw value at 4.40V
+#endif
+#ifndef ADC_22
+#define ADC_22 512 // raw value at 2.20V
+#endif
+
+
+
+inline void hwdef_setup() {
+
+ // set up the system clock to run at 10 MHz instead of the default 3.33 MHz
+ _PROTECTED_WRITE( CLKCTRL.MCLKCTRLB,
+ CLKCTRL_PDIV_2X_gc | CLKCTRL_PEN_bm );
+
+ VPORTA.DIR = PIN1_bm; // Boost enable pin
+ VPORTB.DIR = PIN0_bm // big PWM channel
+ | PIN5_bm; // small PWM channel
+ //VPORTC.DIR = ...;
+
+ // enable pullups on the input pins to reduce power
+ PORTA.PIN0CTRL = PORT_PULLUPEN_bm;
+ //PORTA.PIN1CTRL = PORT_PULLUPEN_bm; // Boost enable pin
+ PORTA.PIN2CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN3CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN4CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN5CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN6CTRL = PORT_PULLUPEN_bm;
+ PORTA.PIN7CTRL = PORT_PULLUPEN_bm;
+
+ //PORTB.PIN0CTRL = PORT_PULLUPEN_bm; // Big PWM channel
+ PORTB.PIN1CTRL = PORT_PULLUPEN_bm;
+ PORTB.PIN2CTRL = PORT_PULLUPEN_bm;
+ PORTB.PIN3CTRL = PORT_PULLUPEN_bm
+ | PORT_ISC_BOTHEDGES_gc; // e-switch
+ //PORTB.PIN4CTRL = PORT_PULLUPEN_bm; // Voltage divider
+ //PORTB.PIN5CTRL = PORT_PULLUPEN_bm; // Small PWM channel
+
+ PORTC.PIN0CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN1CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN2CTRL = PORT_PULLUPEN_bm;
+ PORTC.PIN3CTRL = PORT_PULLUPEN_bm;
+
+ // set up the PWM
+ // https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny1614-16-17-DataSheet-DS40002204A.pdf
+ // PB0 is TCA0:WO0, use TCA_SINGLE_CMP0EN_bm
+ // PB1 is TCA0:WO1, use TCA_SINGLE_CMP1EN_bm
+ // PB2 is TCA0:WO2, use TCA_SINGLE_CMP2EN_bm
+ // For Fast (Single Slope) PWM use TCA_SINGLE_WGMODE_SINGLESLOPE_gc
+ // For Phase Correct (Dual Slope) PWM use TCA_SINGLE_WGMODE_DSBOTTOM_gc
+ // See the manual for other pins, clocks, configs, portmux, etc
+ PORTMUX.CTRLC = PORTMUX_TCA02_ALTERNATE_gc; // Use alternate pin for TCA0:WO2
+ TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm
+ | TCA_SINGLE_CMP2EN_bm
+ | TCA_SINGLE_WGMODE_DSBOTTOM_gc;
+ PWM_TOP = PWM_TOP_INIT;
+ TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc
+ | TCA_SINGLE_ENABLE_bm;
+}
+
+
+// set fuses, these carry over to the ELF file
+// we need this for enabling BOD in Active Mode from the factory.
+// settings can be verified / dumped from the ELF file using this
+// command: avr-objdump -d -S -j .fuse anduril.elf
+FUSES = {
+ .WDTCFG = FUSE_WDTCFG_DEFAULT, // Watchdog Configuration
+ .BODCFG = FUSE_ACTIVE0, // BOD Configuration
+ .OSCCFG = FUSE_OSCCFG_DEFAULT, // Oscillator Configuration
+ .TCD0CFG = FUSE_TCD0CFG_DEFAULT, // TCD0 Configuration
+ .SYSCFG0 = FUSE_SYSCFG0_DEFAULT, // System Configuration 0
+ .SYSCFG1 = FUSE_SYSCFG1_DEFAULT, // System Configuration 1
+ .APPEND = FUSE_APPEND_DEFAULT, // Application Code Section End
+ .BOOTEND = FUSE_BOOTEND_DEFAULT, // Boot Section End
+};
+
+
+#define LAYOUT_DEFINED
+
diff --git a/hw/sofirn/sp36-t1616/cfg.h b/hw/sofirn/sp36-t1616/cfg.h
new file mode 100644
index 0000000..cb29e4f
--- /dev/null
+++ b/hw/sofirn/sp36-t1616/cfg.h
@@ -0,0 +1,37 @@
+// Sofirn SP36 (small Q8) config options for Anduril using the Attiny1616
+// Copyright (C) 2021-2023 (FIXME)
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+// same as the BLF Q8, mostly
+#include "cfg-blf-q8-t1616.h"
+#undef MODEL_NUMBER
+#define MODEL_NUMBER "0614"
+// ATTINY: 1616
+
+// voltage readings were a little high with the Q8 value
+#undef VOLTAGE_FUDGE_FACTOR
+#define VOLTAGE_FUDGE_FACTOR 5 // add 0.25V, not 0.35V
+
+// the high button LED mode on this light uses too much power
+// off mode: low (1)
+// lockout: blinking (3)
+#ifdef INDICATOR_LED_DEFAULT_MODE
+#undef INDICATOR_LED_DEFAULT_MODE
+#define INDICATOR_LED_DEFAULT_MODE ((3<<2) + 1)
+#endif
+
+// don't blink during the ramp; the button LED brightness is sufficient
+// to indicate which power channel(s) are being used
+#ifdef BLINK_AT_RAMP_MIDDLE
+#undef BLINK_AT_RAMP_MIDDLE
+#endif
+#ifdef BLINK_AT_RAMP_CEIL
+#undef BLINK_AT_RAMP_CEIL
+#endif
+
+// stop panicking at ~60% power or ~3000 lm
+#ifdef THERM_FASTER_LEVEL
+#undef THERM_FASTER_LEVEL
+#endif
+#define THERM_FASTER_LEVEL 130
diff --git a/hw/sofirn/sp36/cfg.h b/hw/sofirn/sp36/cfg.h
new file mode 100644
index 0000000..3661686
--- /dev/null
+++ b/hw/sofirn/sp36/cfg.h
@@ -0,0 +1,36 @@
+// Sofirn SP36 (small Q8) config options for Anduril
+// Copyright (C) 2018-2023 Selene ToyKeeper
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+// same as the BLF Q8, mostly
+#include "cfg-blf-q8.h"
+#undef MODEL_NUMBER
+#define MODEL_NUMBER "0612"
+
+// voltage readings were a little high with the Q8 value
+#undef VOLTAGE_FUDGE_FACTOR
+#define VOLTAGE_FUDGE_FACTOR 5 // add 0.25V, not 0.35V
+
+// the high button LED mode on this light uses too much power
+// off mode: low (1)
+// lockout: blinking (3)
+#ifdef INDICATOR_LED_DEFAULT_MODE
+#undef INDICATOR_LED_DEFAULT_MODE
+#define INDICATOR_LED_DEFAULT_MODE ((3<<2) + 1)
+#endif
+
+// don't blink during the ramp; the button LED brightness is sufficient
+// to indicate which power channel(s) are being used
+#ifdef BLINK_AT_RAMP_MIDDLE
+#undef BLINK_AT_RAMP_MIDDLE
+#endif
+#ifdef BLINK_AT_RAMP_CEIL
+#undef BLINK_AT_RAMP_CEIL
+#endif
+
+// stop panicking at ~60% power or ~3000 lm
+#ifdef THERM_FASTER_LEVEL
+#undef THERM_FASTER_LEVEL
+#endif
+#define THERM_FASTER_LEVEL 130