Fix #3566 use an hardware timer for software PWM stability (#3615)

With my XD60, I noticed that when typing the backlight was flickering.

The XD60 doesn't have the backlight wired to a hardware PWM pin.
I assumed it was a timing issue in the matrix scan that made the PWM
lit the LED a bit too longer. I verified it because the more keys that
were pressed, the more lighting I observed.

This patch makes the software PWM be called during CPU interruptions.
It works almost like the hardware PWM, except instead of using
the CPU waveform generation, the CPU will fire interruption
when the LEDs need be turned on or off.

Using the same timer system as for hardware PWM, when the counter
will reach OCRxx (the current backlight level), an Output Compare
match interrupt will be fired and we'll turn the LEDs off.
When the counter reaches its maximum value, an overflow interrupt
will be triggered in which we turn the LEDs on.
This way we replicate the hardware backlight PWM duty cycle.

This gives a better time stability of the PWM computation than pure
software PWM, leading to a flicker free backlight.

Since this is reusing the hardware PWM code, software PWM also supports
backlight breathing.

Note that if timer1 is used for audio, backlight will use timer3, and if
timer3 is used for audio backlight will use timer1.
If both timers are used for audio, then this feature is disabled and we
revert to the matrix scan based PWM computation.

Signed-off-by: Brice Figureau <brice@daysofwonder.com>
pull/5683/head
Brice Figureau 6 years ago committed by MechMerlin
parent c28a432112
commit b61baf4281

@ -30,7 +30,31 @@ You should then be able to use the keycodes below to change the backlight level.
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 use multiple different coloured LEDs on a keyboard.
Hardware PWM is only supported on certain pins of the MCU, so if the backlighting is not connected to one of them, a software implementation will be used, and backlight breathing will not be available. Currently the supported pins are `B5`, `B6`, `B7`, and `C6`.
Hardware PWM is only supported on certain pins of the MCU, so if the backlighting is not connected to one of them, a software PWM implementation triggered by hardware timer interrupts will be used.
Hardware PWM is supported according to the following table:
| Backlight Pin | Hardware timer |
|---------------|----------------|
|`B5` | Timer 1 |
|`B6` | Timer 1 |
|`B7` | Timer 1 |
|`C6` | Timer 3 |
| other | Software PWM |
The [audio feature](feature_audio.md) also uses hardware timers. Please refer to the following table to know what hardware timer the software PWM will use depending on the audio configuration:
| Audio Pin(s) | 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 |
| `Bx` & `Cx` | Timer 1 & 3 | None |
When all timers are in use for [audio](feature_audio.md), the backlight software PWM will not use a hardware timer, but instead will be triggered during the matrix scan. In this case the backlight doesn't support breathing and might show lighting artifacts (for instance flickering), because the PWM computation might not be called with enough timing precision.
## Configuration
@ -39,11 +63,26 @@ To change the behaviour 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 15 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 hardware PWM is used |
|`BACKLIGHT_BREATHING`|*Not defined*|Enable backlight breathing, if supported |
|`BREATHING_PERIOD` |`6` |The length of one backlight "breath" in seconds |
## 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.
@ -53,6 +92,15 @@ In this way `OCRxx` essentially controls the duty cycle of the LEDs, and thus th
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.
## Backlight Functions
|Function |Description |

@ -1138,30 +1138,38 @@ void matrix_scan_quantum() {
matrix_scan_kb();
}
#if defined(BACKLIGHT_ENABLE) && defined(BACKLIGHT_PIN)
#if defined(BACKLIGHT_ENABLE) && (defined(BACKLIGHT_PIN) || defined(BACKLIGHT_PINS))
static const uint8_t backlight_pin = BACKLIGHT_PIN;
// The 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 other backlight)
// 3. full software PWM
// depending on the pin, we use a different output compare unit
#if BACKLIGHT_PIN == B7
# define HARDWARE_PWM
# define TCCRxA TCCR1A
# define TCCRxB TCCR1B
# define COMxx1 COM1C1
# define OCRxx OCR1C
# define ICRx ICR1
#elif BACKLIGHT_PIN == B6
# define HARDWARE_PWM
# define TCCRxA TCCR1A
# define TCCRxB TCCR1B
# define COMxx1 COM1B1
# define OCRxx OCR1B
# define ICRx ICR1
#elif BACKLIGHT_PIN == B5
# define HARDWARE_PWM
# define TCCRxA TCCR1A
# define TCCRxB TCCR1B
# define COMxx1 COM1A1
# define OCRxx OCR1A
# define ICRx ICR1
#elif BACKLIGHT_PIN == C6
# define HARDWARE_PWM
# define TCCRxA TCCR3A
# define TCCRxB TCCR3B
# define COMxx1 COM1A1
@ -1175,28 +1183,115 @@ static const uint8_t backlight_pin = BACKLIGHT_PIN;
# define ICRx ICR1
# define TIMSK1 TIMSK
#else
# define NO_HARDWARE_PWM
# if !defined(BACKLIGHT_CUSTOM_DRIVER)
# if !defined(B5_AUDIO) && !defined(B6_AUDIO) && !defined(B7_AUDIO)
// timer 1 is not used by audio , backlight can use it
#pragma message "Using hardware timer 1 with software PWM"
# define HARDWARE_PWM
# define BACKLIGHT_PWM_TIMER
# define TCCRxA TCCR1A
# define TCCRxB TCCR1B
# define OCRxx OCR1A
# define OCRxAH OCR1AH
# define OCRxAL OCR1AL
# define TIMERx_COMPA_vect TIMER1_COMPA_vect
# define TIMERx_OVF_vect TIMER1_OVF_vect
# define OCIExA OCIE1A
# define TOIEx TOIE1
# define ICRx ICR1
# ifndef TIMSK
# define TIMSK TIMSK1
# endif
# elif !defined(C6_AUDIO) && !defined(C5_AUDIO) && !defined(C4_AUDIO)
#pragma message "Using hardware timer 3 with software PWM"
// timer 3 is not used by audio, backlight can use it
# define HARDWARE_PWM
# define BACKLIGHT_PWM_TIMER
# define TCCRxA TCCR3A
# define TCCRxB TCCR3B
# define OCRxx OCR3A
# define OCRxAH OCR3AH
# define OCRxAL OCR3AL
# define TIMERx_COMPA_vect TIMER3_COMPA_vect
# define TIMERx_OVF_vect TIMER3_OVF_vect
# define OCIExA OCIE3A
# define TOIEx TOIE3
# define ICRx ICR1
# ifndef TIMSK
# define TIMSK TIMSK3
# endif
# 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
#ifdef NO_HARDWARE_PWM // pwm through software
void backlight_on(uint8_t backlight_pin) {
#if BACKLIGHT_ON_STATE == 0
writePinLow(backlight_pin);
#else
writePinHigh(backlight_pin);
#endif
}
__attribute__ ((weak))
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.
// DDRx |= n
_SFR_IO8((backlight_pin >> 4) + 1) |= _BV(backlight_pin & 0xF);
#if BACKLIGHT_ON_STATE == 0
// PORTx &= ~n
_SFR_IO8((backlight_pin >> 4) + 2) &= ~_BV(backlight_pin & 0xF);
#else
// PORTx |= n
_SFR_IO8((backlight_pin >> 4) + 2) |= _BV(backlight_pin & 0xF);
#endif
FOR_EACH_LED(
setPinOutput(backlight_pin);
backlight_on(backlight_pin);
)
}
__attribute__ ((weak))
@ -1207,21 +1302,14 @@ 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)) {
#if BACKLIGHT_ON_STATE == 0
// PORTx &= ~n
_SFR_IO8((backlight_pin >> 4) + 2) &= ~_BV(backlight_pin & 0xF);
#else
// PORTx |= n
_SFR_IO8((backlight_pin >> 4) + 2) |= _BV(backlight_pin & 0xF);
#endif
} else {
#if BACKLIGHT_ON_STATE == 0
// PORTx |= n
_SFR_IO8((backlight_pin >> 4) + 2) |= _BV(backlight_pin & 0xF);
#else
// PORTx &= ~n
_SFR_IO8((backlight_pin >> 4) + 2) &= ~_BV(backlight_pin & 0xF);
#endif
FOR_EACH_LED(
backlight_on(backlight_pin);
)
}
else {
FOR_EACH_LED(
backlight_off(backlight_pin);
)
}
backlight_tick = (backlight_tick + 1) % 16;
}
@ -1233,7 +1321,52 @@ void backlight_task(void) {
#endif
#endif
#else // pwm through timer
#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
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
@ -1265,11 +1398,28 @@ void backlight_set(uint8_t level) {
level = BACKLIGHT_LEVELS;
if (level == 0) {
#ifdef BACKLIGHT_PWM_TIMER
if (OCRxx) {
TIMSK &= ~(_BV(OCIExA));
TIMSK &= ~(_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) {
TIMSK |= _BV(OCIExA);
TIMSK |= _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));
@ -1289,12 +1439,25 @@ 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 !!(TIMSK1 & _BV(TOIE1));
}
#define breathing_interrupt_enable() do {TIMSK1 |= _BV(TOIE1);} while (0)
#define breathing_interrupt_disable() do {TIMSK1 &= ~_BV(TOIE1);} while (0)
#endif
#define breathing_min() do {breathing_counter = 0;} while (0)
#define breathing_max() do {breathing_counter = breathing_period * 244 / 2;} while (0)
@ -1368,10 +1531,14 @@ 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(TIMER1_OVF_vect)
#endif
{
uint16_t interval = (uint16_t) breathing_period * 244 / BREATHING_STEPS;
// resetting after one period to prevent ugly reset at overflow.
@ -1393,19 +1560,21 @@ __attribute__ ((weak))
void backlight_init_ports(void)
{
// Setup backlight pin as output and output to on state.
// DDRx |= n
_SFR_IO8((backlight_pin >> 4) + 1) |= _BV(backlight_pin & 0xF);
#if BACKLIGHT_ON_STATE == 0
// PORTx &= ~n
_SFR_IO8((backlight_pin >> 4) + 2) &= ~_BV(backlight_pin & 0xF);
#else
// PORTx |= n
_SFR_IO8((backlight_pin >> 4) + 2) |= _BV(backlight_pin & 0xF);
#endif
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.)
@ -1419,6 +1588,7 @@ void backlight_init_ports(void)
*/
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;
@ -1428,9 +1598,9 @@ void backlight_init_ports(void)
#endif
}
#endif // NO_HARDWARE_PWM
#endif // hardware backlight
#else // backlight
#else // no backlight
__attribute__ ((weak))
void backlight_init_ports(void) {}

@ -260,8 +260,12 @@ void tap_code16(uint16_t code);
#ifdef BACKLIGHT_ENABLE
void backlight_init_ports(void);
void backlight_task(void);
void backlight_task_internal(void);
void backlight_on(uint8_t backlight_pin);
void backlight_off(uint8_t backlight_pin);
#ifdef BACKLIGHT_BREATHING
void breathing_task(void);
void breathing_enable(void);
void breathing_pulse(void);
void breathing_disable(void);

Loading…
Cancel
Save