|
@@ -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;
|
|
|
- }
|
|
|
}
|