Browse Source

Improve performance of bed PWM automaton - proof of concept
There are still some artefacts on the output pin - work in progress.

DRracer 4 years ago
parent
commit
dc78bc7362
2 changed files with 144 additions and 73 deletions
  1. 143 72
      Firmware/heatbed_pwm.cpp
  2. 1 1
      Firmware/timer02.c

+ 143 - 72
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;
-    }
-    --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 ];
+	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;		
     }
-    ++outer;
-    inner = innerMax;
-  }
 }

+ 1 - 1
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.)