summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Challis <git@zvecr.com>2019-10-05 16:57:00 +0100
committerFlorian Didron <fdidron@users.noreply.github.com>2020-01-09 08:57:11 +0900
commitd1dc2d5389f5405dbc57e5b5531e08c955ea8ef1 (patch)
tree3a3cb2dc612a84e60368a9f904602a629e2f9175
parent9db7651e7bfb13b8e2ebbd95f4de61ea2658b1c8 (diff)
ARM - Initial backlight support (#6487)
* Move AVR backlight to own file, add borrowed ARM implementation * Tiny fix for backlight custom logic * Remove duplicate board from rebase * Fix f303 onekey example * clang-format * clang-format * Remove backlight keymap debug * Initial pass of ARM backlight docs * Initial pass of ARM backlight docs - resolve todos * fix rules validation logic * Add f072 warning * Add f072 warning * tidy up breathing in backlight keymap * tidy up breathing in backlight keymap * add missing break to backlight keymap
-rw-r--r--common_features.mk25
-rw-r--r--docs/feature_backlight.md159
-rw-r--r--quantum/backlight/backlight.c194
-rw-r--r--quantum/backlight/backlight.h4
-rw-r--r--quantum/backlight/backlight_arm.c218
-rw-r--r--quantum/backlight/backlight_avr.c509
-rw-r--r--quantum/quantum.c505
7 files changed, 910 insertions, 704 deletions
diff --git a/common_features.mk b/common_features.mk
index 401c8f43aa..abafd85f97 100644
--- a/common_features.mk
+++ b/common_features.mk
@@ -229,19 +229,32 @@ ifeq ($(strip $(LCD_ENABLE)), yes)
CIE1931_CURVE = yes
endif
-ifeq ($(strip $(BACKLIGHT_ENABLE)), yes)
+# backward compat
+ifeq ($(strip $(BACKLIGHT_CUSTOM_DRIVER)), yes)
+ BACKLIGHT_ENABLE = custom
+endif
+
+VALID_BACKLIGHT_TYPES := yes custom
+
+BACKLIGHT_ENABLE ?= no
+ifneq ($(strip $(BACKLIGHT_ENABLE)), no)
+ ifeq ($(filter $(BACKLIGHT_ENABLE),$(VALID_BACKLIGHT_TYPES)),)
+ $(error BACKLIGHT_ENABLE="$(BACKLIGHT_ENABLE)" is not a valid backlight type)
+ endif
+
ifeq ($(strip $(VISUALIZER_ENABLE)), yes)
CIE1931_CURVE = yes
endif
-
- COMMON_VPATH += $(QUANTUM_DIR)/backlight
- SRC += $(QUANTUM_DIR)/backlight/backlight.c
- OPT_DEFS += -DBACKLIGHT_ENABLE
-
ifeq ($(strip $(BACKLIGHT_ENABLE)), custom)
OPT_DEFS += -DBACKLIGHT_CUSTOM_DRIVER
endif
+
+ ifeq ($(PLATFORM),AVR)
+ SRC += $(QUANTUM_DIR)/backlight/backlight_avr.c
+ else
+ SRC += $(QUANTUM_DIR)/backlight/backlight_arm.c
+ endif
endif
VALID_WS2812_DRIVER_TYPES := bitbang pwm spi i2c
diff --git a/docs/feature_backlight.md b/docs/feature_backlight.md
new file mode 100644
index 0000000000..6a2946fd6d
--- /dev/null
+++ b/docs/feature_backlight.md
@@ -0,0 +1,159 @@
+# Backlighting
+
+Many keyboards support backlit keys by way of individual LEDs placed through or underneath the keyswitches. This feature is distinct from both the [RGB underglow](feature_rgblight.md) and [RGB matrix](feature_rgb_matrix.md) features as it usually allows for only a single colour per switch, though you can obviously install multiple different single coloured LEDs on a keyboard.
+
+QMK is able to control the brightness of these LEDs by switching them on and off rapidly in a certain ratio, a technique known as *Pulse Width Modulation*, or PWM. By altering the duty cycle of the PWM signal, it creates the illusion of dimming.
+
+The MCU can only supply so much current to its GPIO pins. Instead of powering the backlight directly from the MCU, the backlight pin is connected to a transistor or MOSFET that switches the power to the LEDs.
+
+## Usage
+
+Most keyboards have backlighting enabled by default if they support it, but if it is not working for you, check that your `rules.mk` includes the following:
+
+```make
+BACKLIGHT_ENABLE = yes
+```
+
+## Keycodes
+Once enabled the following keycodes below can be used to change the backlight level.
+
+|Key |Description |
+|---------|------------------------------------------|
+|`BL_TOGG`|Turn the backlight on or off |
+|`BL_STEP`|Cycle through backlight levels |
+|`BL_ON` |Set the backlight to max brightness |
+|`BL_OFF` |Turn the backlight off |
+|`BL_INC` |Increase the backlight level |
+|`BL_DEC` |Decrease the backlight level |
+|`BL_BRTG`|Toggle backlight breathing |
+
+## AVR driver
+
+### Caveats
+
+Hardware PWM is supported according to the following table:
+
+|Backlight Pin|AT90USB64/128|ATmega16/32U4|ATmega16/32U2|ATmega32A|ATmega328P|
+|-------------|-------------|-------------|-------------|---------|----------|
+|`B1` | | | | |Timer 1 |
+|`B2` | | | | |Timer 1 |
+|`B5` |Timer 1 |Timer 1 | | | |
+|`B6` |Timer 1 |Timer 1 | | | |
+|`B7` |Timer 1 |Timer 1 |Timer 1 | | |
+|`C4` |Timer 3 | | | | |
+|`C5` |Timer 3 | |Timer 1 | | |
+|`C6` |Timer 3 |Timer 3 |Timer 1 | | |
+|`D4` | | | |Timer 1 | |
+|`D5` | | | |Timer 1 | |
+
+All other pins will use software PWM. If the [Audio](feature_audio.md) feature is disabled or only using one timer, the backlight PWM can be triggered by a hardware timer:
+
+|Audio Pin|Audio Timer|Software PWM Timer|
+|---------|-----------|------------------|
+|`C4` |Timer 3 |Timer 1 |
+|`C5` |Timer 3 |Timer 1 |
+|`C6` |Timer 3 |Timer 1 |
+|`B5` |Timer 1 |Timer 3 |
+|`B6` |Timer 1 |Timer 3 |
+|`B7` |Timer 1 |Timer 3 |
+
+When both timers are in use for Audio, the backlight PWM will not use a hardware timer, but will instead be triggered during the matrix scan. In this case, breathing is not supported, and the backlight might flicker, because the PWM computation may not be called with enough timing precision.
+
+### AVR Configuration
+
+To change the behavior of the backlighting, `#define` these in your `config.h`:
+
+|Define |Default |Description |
+|---------------------|-------------|-------------------------------------------------------------------------------------------------------------|
+|`BACKLIGHT_PIN` |`B7` |The pin that controls the LEDs. Unless you are designing your own keyboard, you shouldn't need to change this|
+|`BACKLIGHT_PINS` |*Not defined*|experimental: see below for more information |
+|`BACKLIGHT_LEVELS` |`3` |The number of brightness levels (maximum 31 excluding off) |
+|`BACKLIGHT_CAPS_LOCK`|*Not defined*|Enable Caps Lock indicator using backlight (for keyboards without dedicated LED) |
+|`BACKLIGHT_BREATHING`|*Not defined*|Enable backlight breathing, if supported |
+|`BREATHING_PERIOD` |`6` |The length of one backlight "breath" in seconds |
+|`BACKLIGHT_ON_STATE` |`0` |The state of the backlight pin when the backlight is "on" - `1` for high, `0` for low |
+
+### Backlight On State
+
+Most backlight circuits are driven by an N-channel MOSFET or NPN transistor. This means that to turn the transistor *on* and light the LEDs, you must drive the backlight pin, connected to the gate or base, *high*.
+Sometimes, however, a P-channel MOSFET, or a PNP transistor is used. In this case, when the transistor is on, the pin is driven *low* instead.
+
+This functionality is configured at the keyboard level with the `BACKLIGHT_ON_STATE` define.
+
+### Multiple backlight pins
+
+Most keyboards have only one backlight pin which control all backlight LEDs (especially if the backlight is connected to an hardware PWM pin).
+In software PWM, it is possible to define multiple backlight pins. All those pins will be turned on and off at the same time during the PWM duty cycle.
+This feature allows to set for instance the Caps Lock LED (or any other controllable LED) brightness at the same level as the other LEDs of the backlight. This is useful if you have mapped LCTRL in place of Caps Lock and you need the Caps Lock LED to be part of the backlight instead of being activated when Caps Lock is on.
+
+To activate multiple backlight pins, you need to add something like this to your user `config.h`:
+
+```c
+#define BACKLIGHT_LED_COUNT 2
+#undef BACKLIGHT_PIN
+#define BACKLIGHT_PINS { F5, B2 }
+```
+
+### Hardware PWM Implementation
+
+When using the supported pins for backlighting, QMK will use a hardware timer configured to output a PWM signal. This timer will count up to `ICRx` (by default `0xFFFF`) before resetting to 0.
+The desired brightness is calculated and stored in the `OCRxx` register. When the counter reaches this value, the backlight pin will go low, and is pulled high again when the counter resets.
+In this way `OCRxx` essentially controls the duty cycle of the LEDs, and thus the brightness, where `0x0000` is completely off and `0xFFFF` is completely on.
+
+The breathing effect is achieved by registering an interrupt handler for `TIMER1_OVF_vect` that is called whenever the counter resets, roughly 244 times per second.
+In this handler, the value of an incrementing counter is mapped onto a precomputed brightness curve. To turn off breathing, the interrupt handler is simply disabled, and the brightness reset to the level stored in EEPROM.
+
+### Software PWM Implementation
+
+When `BACKLIGHT_PIN` is not set to a hardware backlight pin, QMK will use a hardware timer configured to trigger software interrupts. This time will count up to `ICRx` (by default `0xFFFF`) before resetting to 0.
+When resetting to 0, the CPU will fire an OVF (overflow) interrupt that will turn the LEDs on, starting the duty cycle.
+The desired brightness is calculated and stored in the `OCRxx` register. When the counter reaches this value, the CPU will fire a Compare Output match interrupt, which will turn the LEDs off.
+In this way `OCRxx` essentially controls the duty cycle of the LEDs, and thus the brightness, where `0x0000` is completely off and `0xFFFF` is completely on.
+
+The breathing effect is the same as in the hardware PWM implementation.
+
+## ARM Driver
+
+### Caveats
+
+Currently only hardware PWM is supported, and does not provide automatic configuration.
+
+?> STMF072 support is being investigated.
+
+### ARM Configuration
+
+To change the behavior of the backlighting, `#define` these in your `config.h`:
+
+|Define |Default |Description |
+|------------------------|-------------|-------------------------------------------------------------------------------------------------------------|
+|`BACKLIGHT_PIN` |`B7` |The pin that controls the LEDs. Unless you are designing your own keyboard, you shouldn't need to change this|
+|`BACKLIGHT_PWM_DRIVER` |`PWMD4` |The PWM driver to use, see ST datasheets for pin to PWM timer mapping. Unless you are designing your own keyboard, you shouldn't need to change this|
+|`BACKLIGHT_PWM_CHANNEL` |`3` |The PWM channel to use, see ST datasheets for pin to PWM channel mapping. Unless you are designing your own keyboard, you shouldn't need to change this|
+|`BACKLIGHT_PAL_MODE` |`2` |The pin alternative function to use, see ST datasheets for pin AF mapping. Unless you are designing your own keyboard, you shouldn't need to change this|
+|`BACKLIGHT_LEVELS` |`3` |The number of brightness levels (maximum 31 excluding off) |
+|`BACKLIGHT_CAPS_LOCK` |*Not defined*|Enable Caps Lock indicator using backlight (for keyboards without dedicated LED) |
+|`BACKLIGHT_BREATHING` |*Not defined*|Enable backlight breathing, if supported |
+|`BREATHING_PERIOD` |`6` |The length of one backlight "breath" in seconds |
+
+## Backlight Functions
+
+|Function |Description |
+|----------|-----------------------------------------------------------|
+|`backlight_toggle()` |Turn the backlight on or off |
+|`backlight_enable()` |Turn the backlight on |
+|`backlight_disable()` |Turn the backlight off |
+|`backlight_step()` |Cycle through backlight levels |
+|`backlight_increase()` |Increase the backlight level |
+|`backlight_decrease()` |Decrease the backlight level |
+|`backlight_level(x)` |Sets the backlight level, from 0 to |
+| |`BACKLIGHT_LEVELS` |
+|`get_backlight_level()` |Return the current backlight level |
+|`is_backlight_enabled()`|Return whether the backlight is currently on |
+
+### Backlight Breathing Functions
+
+|Function |Description |
+|----------|----------------------------------------------------------|
+|`breathing_toggle()` |Turn the backlight breathing on or off |
+|`breathing_enable()` |Turns on backlight breathing |
+|`breathing_disable()` |Turns off backlight breathing |
diff --git a/quantum/backlight/backlight.c b/quantum/backlight/backlight.c
index 708022f68f..e26de86bf9 100644
--- a/quantum/backlight/backlight.c
+++ b/quantum/backlight/backlight.c
@@ -1,193 +1 @@
-/*
-Copyright 2013 Mathias Andersson <wraul@dbox.se>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "backlight.h"
-#include "eeconfig.h"
-#include "debug.h"
-
-backlight_config_t backlight_config;
-
-/** \brief Backlight initialization
- *
- * FIXME: needs doc
- */
-void backlight_init(void) {
- /* check signature */
- if (!eeconfig_is_enabled()) {
- eeconfig_init();
- }
- backlight_config.raw = eeconfig_read_backlight();
- if (backlight_config.level > BACKLIGHT_LEVELS) {
- backlight_config.level = BACKLIGHT_LEVELS;
- }
- backlight_set(backlight_config.enable ? backlight_config.level : 0);
-}
-
-/** \brief Backlight increase
- *
- * FIXME: needs doc
- */
-void backlight_increase(void) {
- if (backlight_config.level < BACKLIGHT_LEVELS) {
- backlight_config.level++;
- }
- backlight_config.enable = 1;
- eeconfig_update_backlight(backlight_config.raw);
- dprintf("backlight increase: %u\n", backlight_config.level);
- backlight_set(backlight_config.level);
-}
-
-/** \brief Backlight decrease
- *
- * FIXME: needs doc
- */
-void backlight_decrease(void) {
- if (backlight_config.level > 0) {
- backlight_config.level--;
- backlight_config.enable = !!backlight_config.level;
- eeconfig_update_backlight(backlight_config.raw);
- }
- dprintf("backlight decrease: %u\n", backlight_config.level);
- backlight_set(backlight_config.level);
-}
-
-/** \brief Backlight toggle
- *
- * FIXME: needs doc
- */
-void backlight_toggle(void) {
- bool enabled = backlight_config.enable;
- dprintf("backlight toggle: %u\n", enabled);
- if (enabled)
- backlight_disable();
- else
- backlight_enable();
-}
-
-/** \brief Enable backlight
- *
- * FIXME: needs doc
- */
-void backlight_enable(void) {
- if (backlight_config.enable) return; // do nothing if backlight is already on
-
- backlight_config.enable = true;
- if (backlight_config.raw == 1) // enabled but level == 0
- backlight_config.level = 1;
- eeconfig_update_backlight(backlight_config.raw);
- dprintf("backlight enable\n");
- backlight_set(backlight_config.level);
-}
-
-/** \brief Disable backlight
- *
- * FIXME: needs doc
- */
-void backlight_disable(void) {
- if (!backlight_config.enable) return; // do nothing if backlight is already off
-
- backlight_config.enable = false;
- eeconfig_update_backlight(backlight_config.raw);
- dprintf("backlight disable\n");
- backlight_set(0);
-}
-
-/** /brief Get the backlight status
- *
- * FIXME: needs doc
- */
-bool is_backlight_enabled(void) { return backlight_config.enable; }
-
-/** \brief Backlight step through levels
- *
- * FIXME: needs doc
- */
-void backlight_step(void) {
- backlight_config.level++;
- if (backlight_config.level > BACKLIGHT_LEVELS) {
- backlight_config.level = 0;
- }
- backlight_config.enable = !!backlight_config.level;
- eeconfig_update_backlight(backlight_config.raw);
- dprintf("backlight step: %u\n", backlight_config.level);
- backlight_set(backlight_config.level);
-}
-
-/** \brief Backlight set level
- *
- * FIXME: needs doc
- */
-void backlight_level(uint8_t level) {
- if (level > BACKLIGHT_LEVELS) level = BACKLIGHT_LEVELS;
- backlight_config.level = level;
- backlight_config.enable = !!backlight_config.level;
- eeconfig_update_backlight(backlight_config.raw);
- backlight_set(backlight_config.level);
-}
-
-/** \brief Get backlight level
- *
- * FIXME: needs doc
- */
-uint8_t get_backlight_level(void) { return backlight_config.level; }
-
-#ifdef BACKLIGHT_BREATHING
-/** \brief Backlight breathing toggle
- *
- * FIXME: needs doc
- */
-void backlight_toggle_breathing(void) {
- bool breathing = backlight_config.breathing;
- dprintf("backlight breathing toggle: %u\n", breathing);
- if (breathing)
- backlight_disable_breathing();
- else
- backlight_enable_breathing();
-}
-
-/** \brief Enable backlight breathing
- *
- * FIXME: needs doc
- */
-void backlight_enable_breathing(void) {
- if (backlight_config.breathing) return; // do nothing if breathing is already on
-
- backlight_config.breathing = true;
- eeconfig_update_backlight(backlight_config.raw);
- dprintf("backlight breathing enable\n");
- breathing_enable();
-}
-
-/** \brief Disable backlight breathing
- *
- * FIXME: needs doc
- */
-void backlight_disable_breathing(void) {
- if (!backlight_config.breathing) return; // do nothing if breathing is already off
-
- backlight_config.breathing = false;
- eeconfig_update_backlight(backlight_config.raw);
- dprintf("backlight breathing disable\n");
- breathing_disable();
-}
-
-/** \brief Get the backlight breathing status
- *
- * FIXME: needs doc
- */
-bool is_backlight_breathing(void) { return backlight_config.breathing; }
-#endif
+// TODO: Add common code here, for example cie_lightness implementation
diff --git a/quantum/backlight/backlight.h b/quantum/backlight/backlight.h
index bb1f897ee8..1e581055db 100644
--- a/quantum/backlight/backlight.h
+++ b/quantum/backlight/backlight.h
@@ -26,6 +26,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
# error "Maximum value of BACKLIGHT_LEVELS is 31"
#endif
+#ifndef BREATHING_PERIOD
+# define BREATHING_PERIOD 6
+#endif
+
typedef union {
uint8_t raw;
struct {
diff --git a/quantum/backlight/backlight_arm.c b/quantum/backlight/backlight_arm.c
new file mode 100644
index 0000000000..3f94ccef8e
--- /dev/null
+++ b/quantum/backlight/backlight_arm.c
@@ -0,0 +1,218 @@
+#include "quantum.h"
+#include "backlight.h"
+#include <hal.h>
+#include "debug.h"
+
+// TODO: remove short term bodge when refactoring BACKLIGHT_CUSTOM_DRIVER out
+#ifdef BACKLIGHT_PIN
+
+# if defined(STM32F0XX) || defined(STM32F0xx)
+# error "Backlight support for STMF072 is not available. Please disable."
+# endif
+
+# if defined(STM32F1XX) || defined(STM32F1xx)
+# define USE_GPIOV1
+# endif
+
+// GPIOV2 && GPIOV3
+# ifndef BACKLIGHT_PAL_MODE
+# define BACKLIGHT_PAL_MODE 2
+# endif
+
+// GENERIC
+# ifndef BACKLIGHT_PWM_DRIVER
+# define BACKLIGHT_PWM_DRIVER PWMD4
+# endif
+# ifndef BACKLIGHT_PWM_CHANNEL
+# define BACKLIGHT_PWM_CHANNEL 3
+# endif
+
+static void breathing_callback(PWMDriver *pwmp);
+
+static PWMConfig pwmCFG = {0xFFFF, /* PWM clock frequency */
+ 256, /* PWM period (in ticks) 1S (1/10kHz=0.1mS 0.1ms*10000 ticks=1S) */
+ NULL, /* No Callback */
+ { /* Default all channels to disabled - Channels will be configured durring init */
+ {PWM_OUTPUT_DISABLED, NULL},
+ {PWM_OUTPUT_DISABLED, NULL},
+ {PWM_OUTPUT_DISABLED, NULL},
+ {PWM_OUTPUT_DISABLED, NULL}},
+ 0, /* HW dependent part.*/
+ 0};
+
+static PWMConfig pwmCFG_breathing = {0xFFFF, /** PWM clock frequency */
+ 256, /* PWM period (in ticks) 1S (1/10kHz=0.1mS 0.1ms*10000 ticks=1S) */
+ breathing_callback, /* Breathing Callback */
+ { /* Default all channels to disabled - Channels will be configured durring init */
+ {PWM_OUTPUT_DISABLED, NULL},
+ {PWM_OUTPUT_DISABLED, NULL},
+ {PWM_OUTPUT_DISABLED, NULL},
+ {PWM_OUTPUT_DISABLED, NULL}},
+ 0, /* HW dependent part.*/
+ 0};
+
+// See http://jared.geek.nz/2013/feb/linear-led-pwm
+static uint16_t cie_lightness(uint16_t v) {
+ if (v <= 5243) // if below 8% of max
+ return v / 9; // same as dividing by 900%
+ else {
+ uint32_t y = (((uint32_t)v + 10486) << 8) / (10486 + 0xFFFFUL); // add 16% of max and compare
+ // to get a useful result with integer division, we shift left in the expression above
+ // and revert what we've done again after squaring.
+ y = y * y * y >> 8;
+ if (y > 0xFFFFUL) // prevent overflow
+ return 0xFFFFU;
+ else
+ return (uint16_t)y;
+ }
+}
+
+void backlight_init_ports(void) {
+ // printf("backlight_init_ports()\n");
+
+# ifdef USE_GPIOV1
+ palSetPadMode(PAL_PORT(BACKLIGHT_PIN), PAL_PAD(BACKLIGHT_PIN), PAL_MODE_STM32_ALTERNATE_PUSHPULL);
+# else
+ palSetPadMode(PAL_PORT(BACKLIGHT_PIN), PAL_PAD(BACKLIGHT_PIN), PAL_MODE_ALTERNATE(BACKLIGHT_PAL_MODE));
+# endif
+
+ pwmCFG.channels[BACKLIGHT_PWM_CHANNEL - 1].mode = PWM_OUTPUT_ACTIVE_HIGH;
+ pwmCFG_breathing.channels[BACKLIGHT_PWM_CHANNEL - 1].mode = PWM_OUTPUT_ACTIVE_HIGH;
+ pwmStart(&BACKLIGHT_PWM_DRIVER, &pwmCFG);
+
+ backlight_set(get_backlight_level());
+ if (is_backlight_breathing()) {
+ breathing_enable();
+ }
+}
+
+void backlight_set(uint8_t level) {
+ // printf("backlight_set(%d)\n", level);
+ if (level == 0) {
+ // Turn backlight off
+ pwmDisableChannel(&BACKLIGHT_PWM_DRIVER, BACKLIGHT_PWM_CHANNEL - 1);
+ } else {
+ // Turn backlight on
+ if (!is_breathing()) {
+ uint32_t duty = (uint32_t)(cie_lightness(0xFFFF * (uint32_t)level / BACKLIGHT_LEVELS));
+ // printf("duty: (%d)\n", duty);
+ pwmEnableChannel(&BACKLIGHT_PWM_DRIVER, BACKLIGHT_PWM_CHANNEL - 1, PWM_FRACTION_TO_WIDTH(&BACKLIGHT_PWM_DRIVER, 0xFFFF, duty));
+ }
+ }
+}
+
+uint8_t backlight_tick = 0;
+
+void backlight_task(void) {}
+
+# define BREATHING_NO_HALT 0
+# define BREATHING_HALT_OFF 1
+# define BREATHING_HALT_ON 2
+# define BREATHING_STEPS 128
+
+static uint8_t breathing_period = BREATHING_PERIOD;
+static uint8_t breathing_halt = BREATHING_NO_HALT;
+static uint16_t breathing_counter = 0;
+
+bool is_breathing(void) { return BACKLIGHT_PWM_DRIVER.config == &pwmCFG_breathing; }
+
+static inline void breathing_min(void) { breathing_counter = 0; }
+
+static inline void breathing_max(void) { breathing_counter = breathing_period * 256 / 2; }
+
+void breathing_interrupt_enable(void) {
+ pwmStop(&BACKLIGHT_PWM_DRIVER);
+ pwmStart(&BACKLIGHT_PWM_DRIVER, &pwmCFG_breathing);
+ chSysLockFromISR();
+ pwmEnablePeriodicNotification(&BACKLIGHT_PWM_DRIVER);
+ pwmEnableChannelI(&BACKLIGHT_PWM_DRIVER, BACKLIGHT_PWM_CHANNEL - 1, PWM_FRACTION_TO_WIDTH(&BACKLIGHT_PWM_DRIVER, 0xFFFF, 0xFFFF));
+ chSysUnlockFromISR();
+}
+
+void breathing_interrupt_disable(void) {
+ pwmStop(&BACKLIGHT_PWM_DRIVER);
+ pwmStart(&BACKLIGHT_PWM_DRIVER, &pwmCFG);
+}
+
+void breathing_enable(void) {
+ breathing_counter = 0;
+ breathing_halt = BREATHING_NO_HALT;
+ breathing_interrupt_enable();
+}
+
+void breathing_pulse(void) {
+ if (get_backlight_level() == 0)
+ breathing_min();
+ else
+ breathing_max();
+ breathing_halt = BREATHING_HALT_ON;
+ breathing_interrupt_enable();
+}
+
+void breathing_disable(void) {
+ // printf("breathing_disable()\n");
+ breathing_interrupt_disable();
+ // Restore backlight level
+ backlight_set(get_backlight_level());
+}
+
+void breathing_self_disable(void) {
+ if (get_backlight_level() == 0)
+ breathing_halt = BREATHING_HALT_OFF;
+ else
+ breathing_halt = BREATHING_HALT_ON;
+}
+
+void breathing_toggle(void) {
+ if (is_breathing())
+ breathing_disable();
+ else
+ breathing_enable();
+}
+
+void breathing_period_set(uint8_t value) {
+ if (!value) value = 1;
+ breathing_period = value;
+}
+
+void breathing_period_default(void) { breathing_period_set(BREATHING_PERIOD); }
+
+void breathing_period_inc(void) { breathing_period_set(breathing_period + 1); }
+
+void breathing_period_dec(void) { breathing_period_set(breathing_period - 1); }
+
+/* To generate breathing curve in python:
+ * from math import sin, pi; [int(sin(x/128.0*pi)**4*255) for x in range(128)]
+ */
+static const uint8_t breathing_table[BREATHING_STEPS] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 17, 20, 24, 28, 32, 36, 41, 46, 51, 57, 63, 70, 76, 83, 91, 98, 106, 113, 121, 129, 138, 146, 154, 162, 170, 178, 185, 193, 200, 207, 213, 220, 225, 231, 235, 240, 244, 247, 250, 252, 253, 254, 255, 254, 253, 252, 250, 247, 244, 240, 235, 231, 225, 220, 213, 207, 200, 193, 185, 178, 170, 162, 154, 146, 138, 129, 121, 113, 106, 98, 91, 83, 76, 70, 63, 57, 51, 46, 41, 36, 32, 28, 24, 20, 17, 15, 12, 10, 8, 6, 5, 4, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+// Use this before the cie_lightness function.
+static inline uint16_t scale_backlight(uint16_t v) { return v / BACKLIGHT_LEVELS * get_backlight_level(); }
+
+static void breathing_callback(PWMDriver *pwmp) {
+ (void)pwmp;
+ uint16_t interval = (uint16_t)breathing_period * 256 / BREATHING_STEPS;
+ // resetting after one period to prevent ugly reset at overflow.
+ breathing_counter = (breathing_counter + 1) % (breathing_period * 256);
+ uint8_t index = breathing_counter / interval % BREATHING_STEPS;
+
+ if (((breathing_halt == BREATHING_HALT_ON) && (index == BREATHING_STEPS / 2)) || ((breathing_halt == BREATHING_HALT_OFF) && (index == BREATHING_STEPS - 1))) {
+ breathing_interrupt_disable();
+ }
+
+ uint32_t duty = cie_lightness(scale_backlight(breathing_table[index] * 256));
+
+ chSysLockFromISR();
+ pwmEnableChannelI(&BACKLIGHT_PWM_DRIVER, BACKLIGHT_PWM_CHANNEL - 1, PWM_FRACTION_TO_WIDTH(&BACKLIGHT_PWM_DRIVER, 0xFFFF, duty));
+ chSysUnlockFromISR();
+}
+
+#else
+
+__attribute__((weak)) void backlight_init_ports(void) {}
+
+__attribute__((weak)) void backlight_set(uint8_t level) {}
+
+__attribute__((weak)) void backlight_task(void) {}
+
+#endif
diff --git a/quantum/backlight/backlight_avr.c b/quantum/backlight/backlight_avr.c
new file mode 100644
index 0000000000..445698f47c
--- /dev/null
+++ b/quantum/backlight/backlight_avr.c
@@ -0,0 +1,509 @@
+#include "quantum.h"
+#include "backlight.h"
+#include "debug.h"
+
+#if defined(BACKLIGHT_ENABLE) && (defined(BACKLIGHT_PIN) || defined(BACKLIGHT_PINS))
+
+// This logic is a bit complex, we support 3 setups:
+//
+// 1. Hardware PWM when backlight is wired to a PWM pin.
+// Depending on this pin, we use a different output compare unit.
+// 2. Software PWM with hardware timers, but the used timer
+// depends on the Audio setup (Audio wins over Backlight).
+// 3. Full software PWM, driven by the matrix scan, if both timers are used by Audio.
+
+# if (defined(__AVR_AT90USB646__) || defined(__AVR_AT90USB647__) || defined(__AVR_AT90USB1286__) || defined(__AVR_AT90USB1287__) || defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__)) && (BACKLIGHT_PIN == B5 || BACKLIGHT_PIN == B6 || BACKLIGHT_PIN == B7)
+# define HARDWARE_PWM
+# define ICRx ICR1
+# define TCCRxA TCCR1A
+# define TCCRxB TCCR1B
+# define TIMERx_OVF_vect TIMER1_OVF_vect
+# define TIMSKx TIMSK1
+# define TOIEx TOIE1
+
+# if BACKLIGHT_PIN == B5
+# define COMxx1 COM1A1
+# define OCRxx OCR1A
+# elif BACKLIGHT_PIN == B6
+# define COMxx1 COM1B1
+# define OCRxx OCR1B
+# elif BACKLIGHT_PIN == B7
+# define COMxx1 COM1C1
+# define OCRxx OCR1C
+# endif
+# elif (defined(__AVR_AT90USB646__) || defined(__AVR_AT90USB647__) || defined(__AVR_AT90USB1286__) || defined(__AVR_AT90USB1287__) || defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__)) && (BACKLIGHT_PIN == C4 || BACKLIGHT_PIN == C5 || BACKLIGHT_PIN == C6)
+# define HARDWARE_PWM
+# define ICRx ICR3
+# define TCCRxA TCCR3A
+# define TCCRxB TCCR3B
+# define TIMERx_OVF_vect TIMER3_OVF_vect
+# define TIMSKx TIMSK3
+# define TOIEx TOIE3
+
+# if BACKLIGHT_PIN == C4
+# if (defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__))
+# error This MCU has no C4 pin!
+# else
+# define COMxx1 COM3C1
+# define OCRxx OCR3C
+# endif
+# elif BACKLIGHT_PIN == C5
+# if (defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__))
+# error This MCU has no C5 pin!
+# else
+# define COMxx1 COM3B1
+# define OCRxx OCR3B
+# endif
+# elif BACKLIGHT_PIN == C6
+# define COMxx1 COM3A1
+# define OCRxx OCR3A
+# endif
+# elif (defined(__AVR_ATmega16U2__) || defined(__AVR_ATmega32U2__)) && (BACKLIGHT_PIN == B7 || BACKLIGHT_PIN == C5 || BACKLIGHT_PIN == C6)
+# define HARDWARE_PWM
+# define ICRx ICR1
+# define TCCRxA TCCR1A
+# define TCCRxB TCCR1B
+# define TIMERx_OVF_vect TIMER1_OVF_vect
+# define TIMSKx TIMSK1
+# define TOIEx TOIE1
+
+# if BACKLIGHT_PIN == B7
+# define COMxx1 COM1C1
+# define OCRxx OCR1C
+# elif BACKLIGHT_PIN == C5
+# define COMxx1 COM1B1
+# define OCRxx OCR1B
+# elif BACKLIGHT_PIN == C6
+# define COMxx1 COM1A1
+# define OCRxx OCR1A
+# endif
+# elif defined(__AVR_ATmega32A__) && (BACKLIGHT_PIN == D4 || BACKLIGHT_PIN == D5)
+# define HARDWARE_PWM
+# define ICRx ICR1
+# define TCCRxA TCCR1A
+# define TCCRxB TCCR1B
+# define TIMERx_OVF_vect TIMER1_OVF_vect
+# define TIMSKx TIMSK
+# define TOIEx TOIE1
+
+# if BACKLIGHT_PIN == D4
+# define COMxx1 COM1B1
+# define OCRxx OCR1B
+# elif BACKLIGHT_PIN == D5
+# define COMxx1 COM1A1
+# define OCRxx OCR1A
+# endif
+# elif defined(__AVR_ATmega328P__) && (BACKLIGHT_PIN == B1 || BACKLIGHT_PIN == B2)
+# define HARDWARE_PWM
+# define ICRx ICR1
+# define TCCRxA TCCR1A
+# define TCCRxB TCCR1B
+# define TIMERx_OVF_vect TIMER1_OVF_vect
+# define TIMSKx TIMSK1
+# define TOIEx TOIE1
+
+# if BACKLIGHT_PIN == B1
+# define COMxx1 COM1A1
+# define OCRxx OCR1A
+# elif BACKLIGHT_PIN == B2
+# define COMxx1 COM1B1
+# define OCRxx OCR1B
+# endif
+# else
+# if !defined(BACKLIGHT_CUSTOM_DRIVER)
+# if !defined(B5_AUDIO) && !defined(B6_AUDIO) && !defined(B7_AUDIO)
+// Timer 1 is not in use by Audio feature, Backlight can use it
+# pragma message "Using hardware timer 1 with software PWM"
+# define HARDWARE_PWM
+# define BACKLIGHT_PWM_TIMER
+# define ICRx ICR1
+# define TCCRxA TCCR1A
+# define TCCRxB TCCR1B
+# define TIMERx_COMPA_vect TIMER1_COMPA_vect
+# define TIMERx_OVF_vect TIMER1_OVF_vect
+# if defined(__AVR_ATmega32A__) // This MCU has only one TIMSK register
+# define TIMSKx TIMSK
+# else
+# define TIMSKx TIMSK1
+# endif
+# define TOIEx TOIE1
+
+# define OCIExA OCIE1A
+# define OCRxx OCR1A
+# elif !defined(C6_AUDIO) && !defined(C5_AUDIO) && !defined(C4_AUDIO)
+# pragma message "Using hardware timer 3 with software PWM"
+// Timer 3 is not in use by Audio feature, Backlight can use it
+# define HARDWARE_PWM
+# define BACKLIGHT_PWM_TIMER
+# define ICRx ICR1
+# define TCCRxA TCCR3A
+# define TCCRxB TCCR3B
+# define TIMERx_COMPA_vect TIMER3_COMPA_vect
+# define TIMERx_OVF_vect TIMER3_OVF_vect
+# define TIMSKx TIMSK3
+# define TOIEx TOIE3
+
+# define OCIExA OCIE3A
+# define OCRxx OCR3A
+# else
+# pragma message "Audio in use - using pure software PWM"
+# define NO_HARDWARE_PWM
+# endif
+# else
+# pragma message "Custom driver defined - using pure software PWM"
+# define NO_HARDWARE_PWM
+# endif
+# endif
+
+# ifndef BACKLIGHT_ON_STATE
+# define BACKLIGHT_ON_STATE 0
+# endif
+
+void backlight_on(uint8_t backlight_pin) {
+# if BACKLIGHT_ON_STATE == 0
+ writePinLow(backlight_pin);
+# else
+ writePinHigh(backlight_pin);
+# endif
+}
+
+void backlight_off(uint8_t backlight_pin) {
+# if BACKLIGHT_ON_STATE == 0
+ writePinHigh(backlight_pin);
+# else
+ writePinLow(backlight_pin);
+# endif
+}
+
+# if defined(NO_HARDWARE_PWM) || defined(BACKLIGHT_PWM_TIMER) // pwm through software
+
+// we support multiple backlight pins
+# ifndef BACKLIGHT_LED_COUNT
+# define BACKLIGHT_LED_COUNT 1
+# endif
+
+# if BACKLIGHT_LED_COUNT == 1
+# define BACKLIGHT_PIN_INIT \
+ { BACKLIGHT_PIN }
+# else
+# define BACKLIGHT_PIN_INIT BACKLIGHT_PINS
+# endif
+
+# define FOR_EACH_LED(x) \
+ for (uint8_t i = 0; i < BACKLIGHT_LED_COUNT; i++) { \
+ uint8_t backlight_pin = backlight_pins[i]; \
+ { x } \
+ }
+
+static const uint8_t backlight_pins[BACKLIGHT_LED_COUNT] = BACKLIGHT_PIN_INIT;
+
+# else // full hardware PWM
+
+// we support only one backlight pin
+static const uint8_t backlight_pin = BACKLIGHT_PIN;
+# define FOR_EACH_LED(x) x
+
+# endif
+
+# ifdef NO_HARDWARE_PWM
+__attribute__((weak)) void backlight_init_ports(void) {
+ // Setup backlight pin as output and output to on state.
+ FOR_EACH_LED(setPinOutput(backlight_pin); backlight_on(backlight_pin);)
+
+# ifdef BACKLIGHT_BREATHING
+ if (is_backlight_breathing()) {
+ breathing_enable();
+ }
+# endif
+}
+
+__attribute__((weak)) void backlight_set(uint8_t level) {}
+
+uint8_t backlight_tick = 0;
+
+# ifndef BACKLIGHT_CUSTOM_DRIVER
+void backlight_task(void) {
+ if ((0xFFFF >> ((BACKLIGHT_LEVELS - get_backlight_level()) * ((BACKLIGHT_LEVELS + 1) / 2))) & (1 << backlight_tick)) {
+ FOR_EACH_LED(backlight_on(backlight_pin);)
+ } else {
+ FOR_EACH_LED(backlight_off(backlight_pin);)
+ }
+ backlight_tick = (backlight_tick + 1) % 16;
+}
+# endif
+
+# ifdef BACKLIGHT_BREATHING
+# ifndef BACKLIGHT_CUSTOM_DRIVER
+# error "Backlight breathing only available with hardware PWM. Please disable."
+# endif
+# endif
+
+# else // hardware pwm through timer
+
+# ifdef BACKLIGHT_PWM_TIMER
+
+// The idea of software PWM assisted by hardware timers is the following
+// we use the hardware timer in fast PWM mode like for hardware PWM, but
+// instead of letting the Output Match Comparator control the led pin
+// (which is not possible since the backlight is not wired to PWM pins on the
+// CPU), we do the LED on/off by oursleves.
+// The timer is setup to count up to 0xFFFF, and we set the Output Compare
+// register to the current 16bits backlight level (after CIE correction).
+// This means the CPU will trigger a compare match interrupt when the counter
+// reaches the backlight level, where we turn off the LEDs,
+// but also an overflow interrupt when the counter rolls back to 0,
+// in which we're going to turn on the LEDs.
+// The LED will then be on for OCRxx/0xFFFF time, adjusted every 244Hz.
+
+// Triggered when the counter reaches the OCRx value
+ISR(TIMERx_COMPA_vect) { FOR_EACH_LED(backlight_off(backlight_pin);) }
+
+// Triggered when the counter reaches the TOP value
+// this one triggers at F_CPU/65536 =~ 244 Hz
+ISR(TIMERx_OVF_vect) {
+# ifdef BACKLIGHT_BREATHING
+ if (is_breathing()) {
+ breathing_task();
+ }
+# endif
+ // for very small values of OCRxx (or backlight level)
+ // we can't guarantee this whole code won't execute
+ // at the same time as the compare match interrupt
+ // which means that we might turn on the leds while
+ // trying to turn them off, leading to flickering
+ // artifacts (especially while breathing, because breathing_task
+ // takes many computation cycles).
+ // so better not turn them on while the counter TOP is very low.
+ if (OCRxx > 256) {
+ FOR_EACH_LED(backlight_on(backlight_pin);)
+ }
+}
+
+# endif
+
+# define TIMER_TOP 0xFFFFU
+
+// See http://jared.geek.nz/2013/feb/linear-led-pwm
+static uint16_t cie_lightness(uint16_t v) {
+ if (v <= 5243) // if below 8% of max
+ return v / 9; // same as dividing by 900%
+ else {
+ uint32_t y = (((uint32_t)v + 10486) << 8) / (10486 + 0xFFFFUL); // add 16% of max and compare
+ // to get a useful result with integer division, we shift left in the expression above
+ // and revert what we've done again after squaring.
+ y = y * y * y >> 8;
+ if (y > 0xFFFFUL) // prevent overflow
+ return 0xFFFFU;
+ else
+ return (uint16_t)y;
+ }
+}
+
+// range for val is [0..TIMER_TOP]. PWM pin is high while the timer count is below val.
+static inline void set_pwm(uint16_t val) { OCRxx = val; }
+
+# ifndef BACKLIGHT_CUSTOM_DRIVER
+__attribute__((weak)) void backlight_set(uint8_t level) {
+ if (level > BACKLIGHT_LEVELS) level = BACKLIGHT_LEVELS;
+
+ if (level == 0) {
+# ifdef BACKLIGHT_PWM_TIMER
+ if (OCRxx) {
+ TIMSKx &= ~(_BV(OCIExA));
+ TIMSKx &= ~(_BV(TOIEx));
+ FOR_EACH_LED(backlight_off(backlight_pin);)
+ }
+# else
+ // Turn off PWM control on backlight pin
+ TCCRxA &= ~(_BV(COMxx1));
+# endif
+ } else {
+# ifdef BACKLIGHT_PWM_TIMER
+ if (!OCRxx) {
+ TIMSKx |= _BV(OCIExA);
+ TIMSKx |= _BV(TOIEx);
+ }
+# else
+ // Turn on PWM control of backlight pin
+ TCCRxA |= _BV(COMxx1);
+# endif
+ }
+ // Set the brightness
+ set_pwm(cie_lightness(TIMER_TOP * (uint32_t)level / BACKLIGHT_LEVELS));
+}
+
+void backlight_task(void) {}
+# endif // BACKLIGHT_CUSTOM_DRIVER
+
+# ifdef BACKLIGHT_BREATHING
+
+# define BREATHING_NO_HALT 0
+# define BREATHING_HALT_OFF 1
+# define BREATHING_HALT_ON 2
+# define BREATHING_STEPS 128
+
+static uint8_t breathing_period = BREATHING_PERIOD;
+static uint8_t breathing_halt = BREATHING_NO_HALT;
+static uint16_t breathing_counter = 0;
+
+# ifdef BACKLIGHT_PWM_TIMER
+static bool breathing = false;
+
+bool is_breathing(void) { return breathing; }
+
+# define breathing_interrupt_enable() \
+ do { \
+ breathing = true; \
+ } while (0)
+# define breathing_interrupt_disable() \
+ do { \
+ breathing = false; \
+ } while (0)
+# else
+
+bool is_breathing(void) { return !!(TIMSKx & _BV(TOIEx)); }
+
+# define breathing_interrupt_enable() \
+ do { \
+ TIMSKx |= _BV(TOIEx); \
+ } while (0)
+# define breathing_interrupt_disable() \
+ do { \
+ TIMSKx &= ~_BV(TOIEx); \
+ } while (0)
+# endif
+
+# define breathing_min() \
+ do { \
+ breathing_counter = 0; \
+ } while (0)
+# define breathing_max() \
+ do { \
+ breathing_counter = breathing_period * 244 / 2; \
+ } while (0)
+
+void breathing_enable(void) {
+ breathing_counter = 0;
+ breathing_halt = BREATHING_NO_HALT;
+ breathing_interrupt_enable();
+}
+
+void breathing_pulse(void) {
+ if (get_backlight_level() == 0)
+ breathing_min();
+ else
+ breathing_max();
+ breathing_halt = BREATHING_HALT_ON;
+ breathing_interrupt_enable();
+}
+
+void breathing_disable(void) {
+ breathing_interrupt_disable();
+ // Restore backlight level
+ backlight_set(get_backlight_level());
+}
+
+void breathing_self_disable(void) {
+ if (get_backlight_level() == 0)
+ breathing_halt = BREATHING_HALT_OFF;
+ else
+ breathing_halt = BREATHING_HALT_ON;
+}
+
+void breathing_toggle(void) {
+ if (is_breathing())
+ breathing_disable();
+ else
+ breathing_enable();
+}
+
+void breathing_period_set(uint8_t value) {
+ if (!value) value = 1;
+ breathing_period = value;
+}
+
+void breathing_period_default(void) { breathing_period_set(BREATHING_PERIOD); }
+
+void breathing_period_inc(void) { breathing_period_set(breathing_period + 1); }
+
+void breathing_period_dec(void) { breathing_period_set(breathing_period - 1); }
+
+/* To generate breathing curve in python:
+ * from math import sin, pi; [int(sin(x/128.0*pi)**4*255) for x in range(128)]
+ */
+static const uint8_t breathing_table[BREATHING_STEPS] PROGMEM = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 17, 20, 24, 28, 32, 36, 41, 46, 51, 57, 63, 70, 76, 83, 91, 98, 106, 113, 121, 129, 138, 146, 154, 162, 170, 178, 185, 193, 200, 207, 213, 220, 225, 231, 235, 240, 244, 247, 250, 252, 253, 254, 255, 254, 253, 252, 250, 247, 244, 240, 235, 231, 225, 220, 213, 207, 200, 193, 185, 178, 170, 162, 154, 146, 138, 129, 121, 113, 106, 98, 91, 83, 76, 70, 63, 57, 51, 46, 41, 36, 32, 28, 24, 20, 17, 15, 12, 10, 8, 6, 5, 4, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+// Use this before the cie_lightness function.
+static inline uint16_t scale_backlight(uint16_t v) { return v / BACKLIGHT_LEVELS * get_backlight_level(); }
+
+# ifdef BACKLIGHT_PWM_TIMER
+void breathing_task(void)
+# else
+/* Assuming a 16MHz CPU clock and a timer that resets at 64k (ICR1), the following interrupt handler will run
+ * about 244 times per second.
+ */
+ISR(TIMERx_OVF_vect)
+# endif
+{
+ uint16_t interval = (uint16_t)breathing_period * 244 / BREATHING_STEPS;
+ // resetting after one period to prevent ugly reset at overflow.
+ breathing_counter = (breathing_counter + 1) % (breathing_period * 244);
+ uint8_t index = breathing_counter / interval % BREATHING_STEPS;
+
+ if (((breathing_halt == BREATHING_HALT_ON) && (index == BREATHING_STEPS / 2)) || ((breathing_halt == BREATHING_HALT_OFF) && (index == BREATHING_STEPS - 1))) {
+ breathing_interrupt_disable();
+ }
+
+ set_pwm(cie_lightness(scale_backlight((uint16_t)pgm_read_byte(&breathing_table[index]) * 0x0101U)));
+}
+
+# endif // BACKLIGHT_BREATHING
+
+__attribute__((weak)) void backlight_init_ports(void) {
+ // Setup backlight pin as output and output to on state.
+ FOR_EACH_LED(setPinOutput(backlight_pin); backlight_on(backlight_pin);)
+
+ // I could write a wall of text here to explain... but TL;DW
+ // Go read the ATmega32u4 datasheet.
+ // And this: http://blog.saikoled.com/post/43165849837/secret-konami-cheat-code-to-high-resolution-pwm-on
+
+# ifdef BACKLIGHT_PWM_TIMER
+ // TimerX setup, Fast PWM mode count to TOP set in ICRx
+ TCCRxA = _BV(WGM11); // = 0b00000010;
+ // clock select clk/1
+ TCCRxB = _BV(WGM13) | _BV(WGM12) | _BV(CS10); // = 0b00011001;
+# else // hardware PWM
+ // Pin PB7 = OCR1C (Timer 1, Channel C)
+ // Compare Output Mode = Clear on compare match, Channel C = COM1C1=1 COM1C0=0
+ // (i.e. start high, go low when counter matches.)
+ // WGM Mode 14 (Fast PWM) = WGM13=1 WGM12=1 WGM11=1 WGM10=0
+ // Clock Select = clk/1 (no prescaling) = CS12=0 CS11=0 CS10=1
+
+ /*
+ 14.8.3:
+ "In fast PWM mode, the compare units allow generation of PWM waveforms on the OCnx pins. Setting the COMnx1:0 bits to two will produce a non-inverted PWM [..]."
+ "In fast PWM mode the counter is incremented until the counter value matches either one of the fixed values 0x00FF, 0x01FF, or 0x03FF (WGMn3:0 = 5, 6, or 7), the value in ICRn (WGMn3:0 = 14), or the value in OCRnA (WGMn3:0 = 15)."
+ */
+ TCCRxA = _BV(COMxx1) | _BV(WGM11); // = 0b00001010;
+ TCCRxB = _BV(WGM13) | _BV(WGM12) | _BV(CS10); // = 0b00011001;
+# endif
+ // Use full 16-bit resolution. Counter counts to ICR1 before reset to 0.
+ ICRx = TIMER_TOP;
+
+ backlight_init();
+# ifdef BACKLIGHT_BREATHING
+ if (is_backlight_breathing()) {
+ breathing_enable();
+ }
+# endif
+}
+
+# endif // hardware backlight
+
+#else // no backlight
+
+__attribute__((weak)) void backlight_init_ports(void) {}
+
+__attribute__((weak)) void backlight_set(uint8_t level) {}
+
+#endif // backlight \ No newline at end of file
diff --git a/quantum/quantum.c b/quantum/quantum.c
index 09452ed4ef..0c79d7d399 100644
--- a/quantum/quantum.c
+++ b/quantum/quantum.c
@@ -1019,511 +1019,6 @@ void matrix_scan_quantum() {
matrix_scan_kb();
}
-#if defined(BACKLIGHT_ENABLE) && defined(BACKLIGHT_PIN)
-
-// This logic is a bit complex, we support 3 setups:
-//
-// 1. Hardware PWM when backlight is wired to a PWM pin.
-// Depending on this pin, we use a different output compare unit.
-// 2. Software PWM with hardware timers, but the used timer
-// depends on the Audio setup (Audio wins over Backlight).
-// 3. Full software PWM, driven by the matrix scan, if both timers are used by Audio.
-
-# if (defined(__AVR_AT90USB646__) || defined(__AVR_AT90USB647__) || defined(__AVR_AT90USB1286__) || defined(__AVR_AT90USB1287__) || defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__)) && (BACKLIGHT_PIN == B5 || BACKLIGHT_PIN == B6 || BACKLIGHT_PIN == B7)
-# define HARDWARE_PWM
-# define ICRx ICR1
-# define TCCRxA TCCR1A
-# define TCCRxB TCCR1B
-# define TIMERx_OVF_vect TIMER1_OVF_vect
-# define TIMSKx TIMSK1
-# define TOIEx TOIE1
-
-# if BACKLIGHT_PIN == B5
-# define COMxx1 COM1A1
-# define OCRxx OCR1A
-# elif BACKLIGHT_PIN == B6
-# define COMxx1 COM1B1
-# define OCRxx OCR1B
-# elif BACKLIGHT_PIN == B7
-# define COMxx1 COM1C1
-# define OCRxx OCR1C
-# endif
-# elif (defined(__AVR_AT90USB646__) || defined(__AVR_AT90USB647__) || defined(__AVR_AT90USB1286__) || defined(__AVR_AT90USB1287__) || defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__)) && (BACKLIGHT_PIN == C4 || BACKLIGHT_PIN == C5 || BACKLIGHT_PIN == C6)
-# define HARDWARE_PWM
-# define ICRx ICR3
-# define TCCRxA TCCR3A
-# define TCCRxB TCCR3B
-# define TIMERx_OVF_vect TIMER3_OVF_vect
-# define TIMSKx TIMSK3
-# define TOIEx TOIE3
-
-# if BACKLIGHT_PIN == C4
-# if (defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__))
-# error This MCU has no C4 pin!
-# else
-# define COMxx1 COM3C1
-# define OCRxx OCR3C
-# endif
-# elif BACKLIGHT_PIN == C5
-# if (defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__))
-# error This MCU has no C5 pin!
-# else
-# define COMxx1 COM3B1
-# define OCRxx OCR3B
-# endif
-# elif BACKLIGHT_PIN == C6
-# define COMxx1 COM3A1
-# define OCRxx OCR3A
-# endif
-# elif (defined(__AVR_ATmega16U2__) || defined(__AVR_ATmega32U2__)) && (BACKLIGHT_PIN == B7 || BACKLIGHT_PIN == C5 || BACKLIGHT_PIN == C6)
-# define HARDWARE_PWM
-# define ICRx ICR1
-# define TCCRxA TCCR1A
-# define TCCRxB TCCR1B
-# define TIMERx_OVF_vect TIMER1_OVF_vect
-# define TIMSKx TIMSK1
-# define TOIEx TOIE1
-
-# if BACKLIGHT_PIN == B7
-# define COMxx1 COM1C1
-# define OCRxx OCR1C
-# elif BACKLIGHT_PIN == C5
-# define COMxx1 COM1B1
-# define OCRxx OCR1B
-# elif BACKLIGHT_PIN == C6
-# define COMxx1 COM1A1
-# define OCRxx OCR1A
-# endif
-# elif defined(__AVR_ATmega32A__) && (BACKLIGHT_PIN == D4 || BACKLIGHT_PIN == D5)
-# define HARDWARE_PWM
-# define ICRx ICR1
-# define TCCRxA TCCR1A
-# define TCCRxB TCCR1B
-# define TIMERx_OVF_vect TIMER1_OVF_vect
-# define TIMSKx TIMSK
-# define TOIEx TOIE1
-
-# if BACKLIGHT_PIN == D4
-# define COMxx1 COM1B1
-# define OCRxx OCR1B
-# elif BACKLIGHT_PIN == D5
-# define COMxx1 COM1A1
-# define OCRxx OCR1A
-# endif
-# elif defined(__AVR_ATmega328P__) && (BACKLIGHT_PIN == B1 || BACKLIGHT_PIN == B2)
-# define HARDWARE_PWM
-# define ICRx ICR1
-# define TCCRxA TCCR1A
-# define TCCRxB TCCR1B
-# define TIMERx_OVF_vect TIMER1_OVF_vect
-# define TIMSKx TIMSK1
-# define TOIEx TOIE1
-
-# if BACKLIGHT_PIN == B1
-# define COMxx1 COM1A1
-# define OCRxx OCR1A
-# elif BACKLIGHT_PIN == B2
-# define COMxx1 COM1B1
-# define OCRxx OCR1B
-# endif
-# else
-# if !defined(BACKLIGHT_CUSTOM_DRIVER)
-# if !defined(B5_AUDIO) && !defined(B6_AUDIO) && !defined(B7_AUDIO)
-// Timer 1 is not in use by Audio feature, Backlight can use it
-# pragma message "Using hardware timer 1 with software PWM"
-# define HARDWARE_PWM
-# define BACKLIGHT_PWM_TIMER
-# define ICRx ICR1
-# define TCCRxA TCCR1A
-# define TCCRxB TCCR1B
-# define TIMERx_COMPA_vect TIMER1_COMPA_vect
-# define TIMERx_OVF_vect TIMER1_OVF_vect
-# if defined(__AVR_ATmega32A__) // This MCU has only one TIMSK register
-# define TIMSKx TIMSK
-# else
-# define TIMSKx TIMSK1
-# endif
-# define TOIEx TOIE1
-
-# define OCIExA OCIE1A
-# define OCRxx OCR1A
-# elif !defined(C6_AUDIO) && !defined(C5_AUDIO) && !defined(C4_AUDIO)
-# pragma message "Using hardware timer 3 with software PWM"
-// Timer 3 is not in use by Audio feature, Backlight can use it
-# define HARDWARE_PWM
-# define BACKLIGHT_PWM_TIMER
-# define ICRx ICR1
-# define TCCRxA TCCR3A
-# define TCCRxB TCCR3B
-# define TIMERx_COMPA_vect TIMER3_COMPA_vect
-# define TIMERx_OVF_vect TIMER3_OVF_vect
-# define TIMSKx TIMSK3
-# define TOIEx TOIE3
-
-# define OCIExA OCIE3A
-# define OCRxx OCR3A
-# else
-# pragma message "Audio in use - using pure software PWM"
-# define NO_HARDWARE_PWM
-# endif
-# else
-# pragma message "Custom driver defined - using pure software PWM"
-# define NO_HARDWARE_PWM
-# endif
-# endif
-
-# ifndef BACKLIGHT_ON_STATE
-# define BACKLIGHT_ON_STATE 0
-# endif
-
-void backlight_on(uint8_t backlight_pin) {
-# if BACKLIGHT_ON_STATE == 0
- writePinLow(backlight_pin);
-# else
- writePinHigh(backlight_pin);
-# endif
-}
-
-void backlight_off(uint8_t backlight_pin) {
-# if BACKLIGHT_ON_STATE == 0
- writePinHigh(backlight_pin);
-# else
- writePinLow(backlight_pin);
-# endif
-}
-
-# if defined(NO_HARDWARE_PWM) || defined(BACKLIGHT_PWM_TIMER) // pwm through software
-
-// we support multiple backlight pins
-# ifndef BACKLIGHT_LED_COUNT
-# define BACKLIGHT_LED_COUNT 1
-# endif
-
-# if BACKLIGHT_LED_COUNT == 1
-# define BACKLIGHT_PIN_INIT \
- { BACKLIGHT_PIN }
-# else
-# define BACKLIGHT_PIN_INIT BACKLIGHT_PINS
-# endif
-
-# define FOR_EACH_LED(x) \
- for (uint8_t i = 0; i < BACKLIGHT_LED_COUNT; i++) { \
- uint8_t backlight_pin = backlight_pins[i]; \
- { x } \
- }
-
-static const uint8_t backlight_pins[BACKLIGHT_LED_COUNT] = BACKLIGHT_PIN_INIT;
-
-# else // full hardware PWM
-
-// we support only one backlight pin
-static const uint8_t backlight_pin = BACKLIGHT_PIN;
-# define FOR_EACH_LED(x) x
-
-# endif
-
-# ifdef NO_HARDWARE_PWM
-__attribute__((weak)) void backlight_init_ports(void) {
- // Setup backlight pin as output and output to on state.
- FOR_EACH_LED(setPinOutput(backlight_pin); backlight_on(backlight_pin);)
-
-# ifdef BACKLIGHT_BREATHING
- if (is_backlight_breathing()) {
- breathing_enable();
- }
-# endif
-}
-
-__attribute__((weak)) void backlight_set(uint8_t level) {}
-
-uint8_t backlight_tick = 0;
-
-# ifndef BACKLIGHT_CUSTOM_DRIVER
-void backlight_task(void) {
- if ((0xFFFF >> ((BACKLIGHT_LEVELS - get_backlight_level()) * ((BACKLIGHT_LEVELS + 1) / 2))) & (1 << backlight_tick)) {
- FOR_EACH_LED(backlight_on(backlight_pin);)
- } else {
- FOR_EACH_LED(backlight_off(backlight_pin);)
- }
- backlight_tick = (backlight_tick + 1) % 16;
-}
-# endif
-
-# ifdef BACKLIGHT_BREATHING
-# ifndef BACKLIGHT_CUSTOM_DRIVER
-# error "Backlight breathing only available with hardware PWM. Please disable."
-# endif
-# endif
-
-# else // hardware pwm through timer
-
-# ifdef BACKLIGHT_PWM_TIMER
-
-// The idea of software PWM assisted by hardware timers is the following
-// we use the hardware timer in fast PWM mode like for hardware PWM, but
-// instead of letting the Output Match Comparator control the led pin
-// (which is not possible since the backlight is not wired to PWM pins on the
-// CPU), we do the LED on/off by oursleves.
-// The timer is setup to count up to 0xFFFF, and we set the Output Compare
-// register to the current 16bits backlight level (after CIE correction).
-// This means the CPU will trigger a compare match interrupt when the counter
-// reaches the backlight level, where we turn off the LEDs,
-// but also an overflow interrupt when the counter rolls back to 0,
-// in which we're going to turn on the LEDs.
-// The LED will then be on for OCRxx/0xFFFF time, adjusted every 244Hz.
-
-// Triggered when the counter reaches the OCRx value
-ISR(TIMERx_COMPA_vect) { FOR_EACH_LED(backlight_off(backlight_pin);) }
-
-// Triggered when the counter reaches the TOP value
-// this one triggers at F_CPU/65536 =~ 244 Hz
-ISR(TIMERx_OVF_vect) {
-# ifdef BACKLIGHT_BREATHING
- if (is_breathing()) {
- breathing_task();
- }
-# endif
- // for very small values of OCRxx (or backlight level)
- // we can't guarantee this whole code won't execute
- // at the same time as the compare match interrupt
- // which means that we might turn on the leds while
- // trying to turn them off, leading to flickering
- // artifacts (especially while breathing, because breathing_task
- // takes many computation cycles).
- // so better not turn them on while the counter TOP is very low.
- if (OCRxx > 256) {
- FOR_EACH_LED(backlight_on(backlight_pin);)
- }
-}
-
-# endif
-
-# define TIMER_TOP 0xFFFFU
-
-// See http://jared.geek.nz/2013/feb/linear-led-pwm
-static uint16_t cie_lightness(uint16_t v) {
- if (v <= 5243) // if below 8% of max
- return v / 9; // same as dividing by 900%
- else {
- uint32_t y = (((uint32_t)v + 10486) << 8) / (10486 + 0xFFFFUL); // add 16% of max and compare
- // to get a useful result with integer division, we shift left in the expression above
- // and revert what we've done again after squaring.
- y = y * y * y >> 8;
- if (y > 0xFFFFUL) // prevent overflow
- return 0xFFFFU;
- else
- return (uint16_t)y;
- }
-}
-
-// range for val is [0..TIMER_TOP]. PWM pin is high while the timer count is below val.
-static inline void set_pwm(uint16_t val) { OCRxx = val; }
-
-# ifndef BACKLIGHT_CUSTOM_DRIVER
-__attribute__((weak)) void backlight_set(uint8_t level) {
- if (level > BACKLIGHT_LEVELS) level = BACKLIGHT_LEVELS;
-
- if (level == 0) {
-# ifdef BACKLIGHT_PWM_TIMER
- if (OCRxx) {
- TIMSKx &= ~(_BV(OCIExA));
- TIMSKx &= ~(_BV(TOIEx));
- FOR_EACH_LED(backlight_off(backlight_pin);)
- }
-# else
- // Turn off PWM control on backlight pin
- TCCRxA &= ~(_BV(COMxx1));
-# endif
- } else {
-# ifdef BACKLIGHT_PWM_TIMER
- if (!OCRxx) {
- TIMSKx |= _BV(OCIExA);
- TIMSKx |= _BV(TOIEx);
- }
-# else
- // Turn on PWM control of backlight pin
- TCCRxA |= _BV(COMxx1);
-# endif
- }
- // Set the brightness
- set_pwm(cie_lightness(TIMER_TOP * (uint32_t)level / BACKLIGHT_LEVELS));
-}
-
-void backlight_task(void) {}
-# endif // BACKLIGHT_CUSTOM_DRIVER
-
-# ifdef BACKLIGHT_BREATHING
-
-# define BREATHING_NO_HALT 0
-# define BREATHING_HALT_OFF 1
-# define BREATHING_HALT_ON 2
-# define BREATHING_STEPS 128
-
-static uint8_t breathing_period = BREATHING_PERIOD;
-static uint8_t breathing_halt = BREATHING_NO_HALT;
-static uint16_t breathing_counter = 0;
-
-# ifdef BACKLIGHT_PWM_TIMER
-static bool breathing = false;
-
-bool is_breathing(void) { return breathing; }
-
-# define breathing_interrupt_enable() \
- do { \
- breathing = true; \
- } while (0)
-# define breathing_interrupt_disable() \
- do { \
- breathing = false; \
- } while (0)
-# else
-
-bool is_breathing(void) { return !!(TIMSKx & _BV(TOIEx)); }
-
-# define breathing_interrupt_enable() \
- do { \
- TIMSKx |= _BV(TOIEx); \
- } while (0)
-# define breathing_interrupt_disable() \
- do { \
- TIMSKx &= ~_BV(TOIEx); \
- } while (0)
-# endif
-
-# define breathing_min() \
- do { \
- breathing_counter = 0; \
- } while (0)
-# define breathing_max() \
- do { \
- breathing_counter = breathing_period * 244 / 2; \
- } while (0)
-
-void breathing_enable(void) {
- breathing_counter = 0;
- breathing_halt = BREATHING_NO_HALT;
- breathing_interrupt_enable();
-}
-
-void breathing_pulse(void) {
- if (get_backlight_level() == 0)
- breathing_min();
- else
- breathing_max();
- breathing_halt = BREATHING_HALT_ON;
- breathing_interrupt_enable();
-}
-
-void breathing_disable(void) {
- breathing_interrupt_disable();
- // Restore backlight level
- backlight_set(get_backlight_level());
-}
-
-void breathing_self_disable(void) {
- if (get_backlight_level() == 0)
- breathing_halt = BREATHING_HALT_OFF;
- else
- breathing_halt = BREATHING_HALT_ON;
-}
-
-void breathing_toggle(void) {
- if (is_breathing())
- breathing_disable();
- else
- breathing_enable();
-}
-
-void breathing_period_set(uint8_t value) {
- if (!value) value = 1;
- breathing_period = value;
-}
-
-void breathing_period_default(void) { breathing_period_set(BREATHING_PERIOD); }
-
-void breathing_period_inc(void) { breathing_period_set(breathing_period + 1); }
-
-void breathing_period_dec(void) { breathing_period_set(breathing_period - 1); }
-
-/* To generate breathing curve in python:
- * from math import sin, pi; [int(sin(x/128.0*pi)**4*255) for x in range(128)]
- */
-static const uint8_t breathing_table[BREATHING_STEPS] PROGMEM = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 17, 20, 24, 28, 32, 36, 41, 46, 51, 57, 63, 70, 76, 83, 91, 98, 106, 113, 121, 129, 138, 146, 154, 162, 170, 178, 185, 193, 200, 207, 213, 220, 225, 231, 235, 240, 244, 247, 250, 252, 253, 254, 255, 254, 253, 252, 250, 247, 244, 240, 235, 231, 225, 220, 213, 207, 200, 193, 185, 178, 170, 162, 154, 146, 138, 129, 121, 113, 106, 98, 91, 83, 76, 70, 63, 57, 51, 46, 41, 36, 32, 28, 24, 20, 17, 15, 12, 10, 8, 6, 5, 4, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
-
-// Use this before the cie_lightness function.
-static inline uint16_t scale_backlight(uint16_t v) { return v / BACKLIGHT_LEVELS * get_backlight_level(); }
-
-# ifdef BACKLIGHT_PWM_TIMER
-void breathing_task(void)
-# else
-/* Assuming a 16MHz CPU clock and a timer that resets at 64k (ICR1), the following interrupt handler will run
- * about 244 times per second.
- */
-ISR(TIMERx_OVF_vect)
-# endif
-{
- uint16_t interval = (uint16_t)breathing_period * 244 / BREATHING_STEPS;
- // resetting after one period to prevent ugly reset at overflow.
- breathing_counter = (breathing_counter + 1) % (breathing_period * 244);
- uint8_t index = breathing_counter / interval % BREATHING_STEPS;
-
- if (((breathing_halt == BREATHING_HALT_ON) && (index == BREATHING_STEPS / 2)) || ((breathing_halt == BREATHING_HALT_OFF) && (index == BREATHING_STEPS - 1))) {
- breathing_interrupt_disable();
- }
-
- set_pwm(cie_lightness(scale_backlight((uint16_t)pgm_read_byte(&breathing_table[index]) * 0x0101U)));
-}
-
-# endif // BACKLIGHT_BREATHING
-
-__attribute__((weak)) void backlight_init_ports(void) {
- // Setup backlight pin as output and output to on state.
- FOR_EACH_LED(setPinOutput(backlight_pin); backlight_on(backlight_pin);)
-
- // I could write a wall of text here to explain... but TL;DW
- // Go read the ATmega32u4 datasheet.
- // And this: http://blog.saikoled.com/post/43165849837/secret-konami-cheat-code-to-high-resolution-pwm-on
-
-# ifdef BACKLIGHT_PWM_TIMER
- // TimerX setup, Fast PWM mode count to TOP set in ICRx
- TCCRxA = _BV(WGM11); // = 0b00000010;
- // clock select clk/1
- TCCRxB = _BV(WGM13) | _BV(WGM12) | _BV(CS10); // = 0b00011001;
-# else // hardware PWM
- // Pin PB7 = OCR1C (Timer 1, Channel C)
- // Compare Output Mode = Clear on compare match, Channel C = COM1C1=1 COM1C0=0
- // (i.e. start high, go low when counter matches.)
- // WGM Mode 14 (Fast PWM) = WGM13=1 WGM12=1 WGM11=1 WGM10=0
- // Clock Select = clk/1 (no prescaling) = CS12=0 CS11=0 CS10=1
-
- /*
- 14.8.3:
- "In fast PWM mode, the compare units allow generation of PWM waveforms on the OCnx pins. Setting the COMnx1:0 bits to two will produce a non-inverted PWM [..]."
- "In fast PWM mode the counter is incremented until the counter value matches either one of the fixed values 0x00FF, 0x01FF, or 0x03FF (WGMn3:0 = 5, 6, or 7), the value in ICRn (WGMn3:0 = 14), or the value in OCRnA (WGMn3:0 = 15)."
- */
- TCCRxA = _BV(COMxx1) | _BV(WGM11); // = 0b00001010;
- TCCRxB = _BV(WGM13) | _BV(WGM12) | _BV(CS10); // = 0b00011001;
-# endif
- // Use full 16-bit resolution. Counter counts to ICR1 before reset to 0.
- ICRx = TIMER_TOP;
-
- backlight_init();
-# ifdef BACKLIGHT_BREATHING
- if (is_backlight_breathing()) {
- breathing_enable();
- }
-# endif
-}
-
-# endif // hardware backlight
-
-#else // no backlight
-
-__attribute__((weak)) void backlight_init_ports(void) {}
-
-__attribute__((weak)) void backlight_set(uint8_t level) {}
-
-#endif // backlight
#ifdef HD44780_ENABLED
# include "hd44780.h"