From b61baf4281bde34bfe28aaa1109bd5d5c6471116 Mon Sep 17 00:00:00 2001 From: Brice Figureau Date: Mon, 22 Apr 2019 11:34:13 -0400 Subject: 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 --- quantum/quantum.c | 256 +++++++++++++++++++++++++++++++++++++++++++++--------- quantum/quantum.h | 4 + 2 files changed, 217 insertions(+), 43 deletions(-) (limited to 'quantum') diff --git a/quantum/quantum.c b/quantum/quantum.c index 9aa498dadb..0fb798a748 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -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.) @@ -1417,8 +1586,9 @@ void backlight_init_ports(void) "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; + 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) {} diff --git a/quantum/quantum.h b/quantum/quantum.h index 987516dedb..17cb902740 100644 --- a/quantum/quantum.h +++ b/quantum/quantum.h @@ -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); -- cgit v1.2.3