diff --git a/Firmware/heatbed_pwm.cpp b/Firmware/heatbed_pwm.cpp index 8a913b638..a3e5444c3 100755 --- a/Firmware/heatbed_pwm.cpp +++ b/Firmware/heatbed_pwm.cpp @@ -19,91 +19,162 @@ // So the automaton runs atop of inner 8 (or 16) cycles. // The finite automaton is running in the ISR(TIMER0_OVF_vect) +// 2019-08-14 update: the original algorithm worked very well, however there were 2 regressions: +// 1. 62kHz ISR requires considerable amount of processing power, +// USB transfer speed dropped by 20%, which was most notable when doing short G-code segments. +// 2. Some users reported TLed PSU started clicking when running at 120V/60Hz. +// This looks like the original algorithm didn't maintain base PWM 30Hz, but only 15Hz +// To address both issues, there is an improved approach based on the idea of leveraging +// different CLK prescalers in some automaton states - i.e. when holding LOW or HIGH on the output pin, +// we don't have to clock 62kHz, but we can increase the CLK prescaler for these states to 8 (or even 64). +// That shall result in the ISR not being called that much resulting in regained performance +// Theoretically this is relatively easy, however one must be very carefull handling the AVR's timer +// control registers correctly, especially setting them in a correct order. +// Some registers are double buffered, some changes are applied in next cycles etc. +// The biggest problem was with the CLK prescaler itself - this circuit is shared among almost all timers, +// we don't want to reset the prescaler counted value when transiting among automaton states. +// Resetting the prescaler would make the PWM more precise, right now there are temporal segments +// of variable period ranging from 0 to 7 62kHz ticks - that's logical, the timer must "sync" +// to the new slower CLK after setting the slower prescaler value. +// In our application, this isn't any significant problem and may be ignored. +// Doing changes in timer's registers non-correctly results in artefacts on the output pin +// - it can toggle unnoticed, which will result in bed clicking again. +// That's why there are special transition states ZERO_TO_RISE and ONE_TO_FALL, which enable the +// counter change its operation atomically and without artefacts on the output pin. +// The resulting signal on the output pin was checked with an osciloscope. +// If there are any change requirements in the future, the signal must be checked with an osciloscope again, +// ad-hoc changes may completely screw things up! + ///! Definition off finite automaton states enum class States : uint8_t { - ZERO = 0, - RISE = 1, - ONE = 2, - FALL = 3 -}; - -///! State table for the inner part of the finite automaton -///! Basically it specifies what shall happen if the outer automaton is requesting setting the heat pin to 0 (OFF) or 1 (ON) -///! ZERO: steady 0 (OFF), no change for the whole period -///! RISE: 8 (16) fast PWM cycles with increasing duty up to steady ON -///! ONE: steady 1 (ON), no change for the whole period -///! FALL: 8 (16) fast PWM cycles with decreasing duty down to steady OFF -///! @@TODO move it into progmem -static States stateTable[4*2] = { -// off on -States::ZERO, States::RISE, // ZERO -States::FALL, States::ONE, // RISE -States::FALL, States::ONE, // ONE -States::ZERO, States::RISE // FALL + ZERO_START = 0,///< entry point of the automaton - reads the soft_pwm_bed value for the next whole PWM cycle + ZERO, ///< steady 0 (OFF), no change for the whole period + ZERO_TO_RISE, ///< metastate allowing the timer change its state atomically without artefacts on the output pin + RISE, ///< 16 fast PWM cycles with increasing duty up to steady ON + RISE_TO_ONE, ///< metastate allowing the timer change its state atomically without artefacts on the output pin + ONE, ///< steady 1 (ON), no change for the whole period + ONE_TO_FALL, ///< metastate allowing the timer change its state atomically without artefacts on the output pin + FALL, ///< 16 fast PWM cycles with decreasing duty down to steady OFF + FALL_TO_ZERO ///< metastate allowing the timer change its state atomically without artefacts on the output pin }; ///! Inner states of the finite automaton -static States state = States::ZERO; +static States state = States::ZERO_START; -///! Inner and outer PWM counters -static uint8_t outer = 0; -static uint8_t inner = 0; +///! Fast PWM counter is used in the RISE and FALL states (62.5kHz) +static uint8_t slowCounter = 0; +///! Slow PWM counter is used in the ZERO and ONE states (62.5kHz/8 or 64) +static uint8_t fastCounter = 0; +///! PWM counter for the whole cycle - a cache for soft_pwm_bed static uint8_t pwm = 0; -///! the slow PWM duty for the next 30Hz cycle +///! The slow PWM duty for the next 30Hz cycle ///! Set in the whole firmware at various places extern unsigned char soft_pwm_bed; -/// Fine tuning of automaton cycles -#if 1 -static const uint8_t innerMax = 16; -static const uint8_t innerShift = 4; -#else -static const uint8_t innerMax = 8; -static const uint8_t innerShift = 5; -#endif +/// fastMax - how many fast PWM steps to do in RISE and FALL states +/// 16 is a good compromise between silenced bed ("smooth" edges) +/// and not burning the switching MOSFET +static const uint8_t fastMax = 16; + +/// Scaler 16->256 for fast PWM +static const uint8_t fastShift = 4; + +/// Increment slow PWM counter by slowInc every ZERO or ONE state +/// This allows for fine-tuning the basic PWM switching frequency +/// A possible further optimization - use a 64 prescaler (instead of 8) +/// increment slowCounter by 1 +/// but use less bits of soft PWM - something like soft_pwm_bed >> 2 +/// that may further reduce the CPU cycles required by the bed heating automaton +/// Due to the nature of bed heating the reduced PID precision may not be a major issue, however doing 8x less ISR(timer0_ovf) may significantly improve the performance +static const uint8_t slowInc = 1; ISR(TIMER0_OVF_vect) // timer compare interrupt service routine { - if( inner ){ - switch(state){ - case States::ZERO: - OCR0B = 255; - // Commenting the following code saves 6B, but it is left here for reference - // It is not necessary to set it all over again, because we can only get into the ZERO state from the FALL state (which sets this register) -// TCCR0A |= (1 << COM0B1) | (1 << COM0B0); - break; - case States::RISE: - OCR0B = (innerMax - inner) << innerShift; -// TCCR0A |= (1 << COM0B1); // this bit is always 1 - TCCR0A &= ~(1 << COM0B0); - break; - case States::ONE: - OCR0B = 255; - // again - may be skipped, because we get into the ONE state only from RISE (which sets this register) -// TCCR0A |= (1 << COM0B1); - TCCR0A &= ~(1 << COM0B0); - break; - case States::FALL: - OCR0B = (innerMax - inner) << innerShift; // this is the same as in RISE, because now we are setting the zero part of duty due to inverting mode - // must switch to inverting mode already here, because it takes a whole PWM cycle and it would make a "1" at the end of this pwm cycle - TCCR0A |= /*(1 << COM0B1) |*/ (1 << COM0B0); - break; + switch(state){ + case States::ZERO_START: + pwm = soft_pwm_bed << 1;// expecting soft_pwm_bed to be 7bit! + if( pwm != 0 ){ + state = States::ZERO; // do nothing, let it tick once again after the 30Hz period + } + break; + case States::ZERO: // end of state ZERO - we'll either stay in ZERO or change to RISE + // In any case update our cache of pwm value for the next whole cycle from soft_pwm_bed + slowCounter += slowInc; // this does software timer_clk/256 or less (depends on slowInc) + if( slowCounter > pwm ){ + return; + } // otherwise moving towards RISE + state = States::ZERO_TO_RISE; // and finalize the change in a transitional state RISE0 + break; + // even though it may look like the ZERO state may be glued together with the ZERO_TO_RISE, don't do it + // the timer must tick once more in order to get rid of occasional output pin toggles. + case States::ZERO_TO_RISE: // special state for handling transition between prescalers and switching inverted->non-inverted fast-PWM without toggling the output pin. + // It must be done in consequent steps, otherwise the pin will get flipped up and down during one PWM cycle. + // Also beware of the correct sequence of the following timer control registers initialization - it really matters! + state = States::RISE; // prepare for standard RISE cycles + fastCounter = fastMax - 1;// we'll do 16-1 cycles of RISE + TCNT0 = 255; // force overflow on the next clock cycle + TCCR0B = (1 << CS00); // change prescaler to 1, i.e. 62.5kHz + TCCR0A &= ~(1 << COM0B0); // Clear OC0B on Compare Match, set OC0B at BOTTOM (non-inverting mode) + break; + case States::RISE: + OCR0B = (fastMax - fastCounter) << fastShift; + if( fastCounter ){ + --fastCounter; + } else { // end of RISE cycles, changing into state ONE + state = States::RISE_TO_ONE; + OCR0B = 255; // full duty + TCNT0 = 254; // make the timer overflow in the next cycle + // @@TODO these constants are still subject to investigation + } + break; + case States::RISE_TO_ONE: + state = States::ONE; + OCR0B = 255; // full duty + TCNT0 = 255; // make the timer overflow in the next cycle + TCCR0B = (1 << CS01); // change prescaler to 8, i.e. 7.8kHz + break; + case States::ONE: // state ONE - we'll either stay in ONE or change to FALL + OCR0B = 255; + slowCounter += slowInc; // this does software timer_clk/256 or less + if( slowCounter < pwm ){ + return; + } + if( (soft_pwm_bed << 1) >= (255 - slowInc - 1) ){ //@@TODO simplify & explain + // if slowInc==2, soft_pwm == 251 will be the first to do short drops to zero. 252 will keep full heating + return; // want full duty for the next ONE cycle again - so keep on heating and just wait for the next timer ovf + } + // otherwise moving towards FALL + // @@TODO it looks like ONE_TO_FALL isn't necessary, there are no artefacts at all + state = States::ONE;//_TO_FALL; +// TCCR0B = (1 << CS00); // change prescaler to 1, i.e. 62.5kHz +// break; +// case States::ONE_TO_FALL: +// OCR0B = 255; // zero duty + state=States::FALL; + fastCounter = fastMax - 1;// we'll do 16-1 cycles of RISE + TCNT0 = 255; // force overflow on the next clock cycle + TCCR0B = (1 << CS00); // change prescaler to 1, i.e. 62.5kHz + // must switch to inverting mode already here, because it takes a whole PWM cycle and it would make a "1" at the end of this pwm cycle + // COM0B1 remains set both in inverting and non-inverting mode + TCCR0A |= (1 << COM0B0); // inverting mode + break; + case States::FALL: + OCR0B = (fastMax - fastCounter) << fastShift; // this is the same as in RISE, because now we are setting the zero part of duty due to inverting mode + //TCCR0A |= (1 << COM0B0); // already set in ONE_TO_FALL + if( fastCounter ){ + --fastCounter; + } else { // end of FALL cycles, changing into state ZERO + state = States::FALL_TO_ZERO; + TCNT0 = 128; //@@TODO again - need to wait long enough to propagate the timer state changes + OCR0B = 255; + } + break; + case States::FALL_TO_ZERO: + state = States::ZERO_START; // go to read new soft_pwm_bed value for the next cycle + TCNT0 = 128; + OCR0B = 255; + TCCR0B = (1 << CS01); // change prescaler to 8, i.e. 7.8kHz + break; } - --inner; - } else { - if( ! outer ){ // at the end of 30Hz PWM period - // synchro is not needed (almost), soft_pwm_bed is just 1 byte, 1-byte write instruction is atomic - pwm = soft_pwm_bed << 1; - } - if( pwm > outer || pwm >= 254 ){ - // soft_pwm_bed has a range of 0-127, that why a <<1 is done here. That also means that we may get only up to 254 which we want to be full-time 1 (ON) - state = stateTable[ uint8_t(state) * 2 + 1 ]; - } else { - // switch OFF - state = stateTable[ uint8_t(state) * 2 + 0 ]; - } - ++outer; - inner = innerMax; - } } diff --git a/Firmware/timer02.c b/Firmware/timer02.c index 6b6cbe2cf..e2f696c9d 100644 --- a/Firmware/timer02.c +++ b/Firmware/timer02.c @@ -26,7 +26,7 @@ void timer0_init(void) OCR0B = 255; // Set fast PWM mode and inverting mode. TCCR0A = (1 << WGM01) | (1 << WGM00) | (1 << COM0B1) | (1 << COM0B0); - TCCR0B = (1 << CS00); // no clock prescaling + TCCR0B = (1 << CS01); // CLK/8 prescaling TIMSK0 |= (1 << TOIE0); // enable timer overflow interrupt // Everything, that used to be on timer0 was moved to timer2 (delay, beeping, millis etc.)