/* Controller for Hakko FM-2023 soldering tweezers using ATtiny85. 06/05/2018 johnwa.
* http://loopgain.net/solderingtweezers/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* Heater power is adjusted using phase control. Operation of the program is
* synchronised to the mains frequency using a zero crossing detector. When a zero
* crossing is detected, the heater control output is switched off, and Timer 0 is
* cleared. The output power is determined by the value in OCR0B, with larger values
* corresponding to less power. When the counter counts up to this value, the heater
* is turned on.
*
* To allow correct operation if the mains frequency varies, the half cycle period is
* measured continuously. The turn on time stored in OCR0B is then calculated as a
* proportion of this value. In order to reduce susceptibility to noise, period
* measurements outside a certain range are rejected. Valid measurements are then
* digitally filtered.
*
* The tip temperature is determined from the thermocouple voltage developed across
* the element. This can of course only be measured when the element is switched
* off, so a brief dead time is introduced after each zero crossing for this purpose.
* A differential ADC input with 20x gain is used to give better resolution when
* measuring this low level signal.
*
* The set point temperature is provided by a potentiometer wired to another ADC input.
* It was found that switching ADC channels disturbed the thermocouple voltage
* measurement somewhat, therefore the potentiometer is only sampled once every
* 100 half cycles. The rest of the time, the pot input pin outputs a PWM signal
* proportional to the current tip temperature for debugging.
*
* The ouput power is calculated using a proportional-derivative control strategy,
* for improved transient response. To reduce noise, the derivative term is only updated
* once every 10 half-cycles.
*/
#define F_CPU 1000000L
#include
#include
#include
#include
#define PIN_ZEROCROSS _BV(PB0) // Zero crossing detector input
#define PIN_HEATER _BV(PB1) // Heater enable output
#define PIN_POT _BV(PB2) // Setpoint potentiometer input/tip temp PWM output
#define PIN_SENSORNEG _BV(PB3) // Thermocouple sensor inverting input
#define PIN_SENSORPOS _BV(PB4) // Thermocouple sensor non-inverting input
#define CHAN_POT 1 // Setpoint pot ADC input (single-ended)
#define CHAN_SENSOR 7 // Temp sensor ADC input (differential, ADC2/ADC3, x20 gain)
#define MIN_MAINS_FREQ 35 // Minimum valid mains frequency (Hz)
#define MAX_MAINS_FREQ 75 // Maximum valid mains frequency (Hz)
#define NOM_MAINS_FREQ 50 // Default mains frequency at startup (Hz)
#define KP 1 // Proportional gain coefficient
#define KI 5 // Integral gain coefficient. (not curerntly in use)
#define KD 2 // Derivative gain coefficient. (Note: effectively multiplied by DERIV_SMOOTH_INTERVAL)
#define MAX_POWER 192 // Maximum heater power (/255) (need delay to read sensor, also limit overshoot)
#define TEMP_FILTER_B 8L // IIR filter parameter 'b' (*256) for filtering tip temperature reading
#define PERIOD_FILTER_B 16L // IIR filter parameter 'b' (*256) for filtering mains period
#define SETPOINT_SAMPLE_INTERVAL 100 // Number of half-cycles between setpoint pot readings
#define DERIV_SMOOTH_INTERVAL 10 // Number of half-cycles between derivative term updates
#define F_TIMER0 (F_CPU/64) // Timer 0 tick frequency (15625Hz)
#define MIN_HALFCYC_PERIOD (F_TIMER0 / (2*MAX_MAINS_FREQ)) // 104
#define MAX_HALFCYC_PERIOD (F_TIMER0 / (2*MIN_MAINS_FREQ)) // 223
#define NOM_HALFCYC_PERIOD (F_TIMER0 / (2*NOM_MAINS_FREQ)) // 156
#define PROCESSVAR_OFFSET 1600
#define SETPOINT_OFFSET 1200
volatile uint8_t overflowed = 0; // True if timer 0 has overflowed, indicating loss of zero crossing pulses
ISR(TIM0_OVF_vect) // overflows at 61.03Hz
{
overflowed=1;
}
ISR(TIM0_COMPA_vect)
{
PORTB &= ~PIN_POT;
}
ISR(TIM0_COMPB_vect)
{
// turn on heater, unless timer has overflowed
// (to prevent loss of control if zero crossing pulses are absent)
if (!overflowed)
PORTB |= PIN_HEATER;
}
void init()
{
DDRB = PIN_HEATER | PIN_POT;
ADMUX = 0;
ADCSRA = _BV(ADEN) | _BV(ADPS1) | _BV(ADPS0);
TIMSK = _BV(TOIE0) | _BV(OCIE0A) | _BV(OCIE0B);
TCCR0A = 0;
TCCR0B = _BV(CS01) | _BV(CS00); // div by 64, count at 15625Hz/64us
OCR0B = 255; // start of at max delay / min power
TCNT0 = 0;
PORTB = 0;
sei();
}
int16_t read_adc(uint8_t channel, uint8_t adcsrb)
{
ADCSRB = adcsrb;
ADMUX = channel ;
ADCSRA |= _BV(ADSC);
while (ADCSRA & _BV(ADSC))
;
return ADC;
}
/* Read the setpoint potentiometer if it is time to do so.
* *counter is the number of cycles until the next reading is due. */
void read_pot(uint8_t* counter, int16_t* pot)
{
// disable temperature PWM output so pot can be read, and wait for voltage to stabilise
DDRB &= ~PIN_POT;
PORTB &= ~PIN_POT;
_delay_us(100);
// read the setpoint potentiometer, if it is time to do so
if (!(*counter)--)
{
*pot = read_adc(CHAN_POT, 0);
*counter = SETPOINT_SAMPLE_INTERVAL;
}
// re-enable the PWM output pin
PORTB |= PIN_POT;
DDRB |= PIN_POT;
}
/* Read the last half-cycle period, and reset the timer. If the measured
* period is within acceptable limits, update the filtered period with
* the new sample. */
void update_halfcycle_period(uint16_t* halfcycle_period)
{
uint8_t period_meas = TCNT0;
TCNT0 = 0;
overflowed=0;
if ((period_meas > MIN_HALFCYC_PERIOD) && (period_meas < MAX_HALFCYC_PERIOD))
*halfcycle_period = (256L * period_meas * PERIOD_FILTER_B + 1L * (*halfcycle_period) * (256 - PERIOD_FILTER_B))/256;
}
int main()
{
init();
int16_t processvar; // Current tip temperature/thermocouple voltage.
// 1.1V reference, 10 bit, x20 gain, left adjusted, bipolar,
// so 1 count = 1.1V / 1024 / 20 / 64 * 2 = 1.678uV.
// 2000 =~ 160C, 3000 =~ 240C, 4000 =~ 300C, 5000 =~ 400C */
int16_t processvar_avg = 0; // Digital filtered process variable
int16_t pot = 0; // Current setpoint potentiometer setting (0..1023)
int16_t setpoint; // Desired tip temperature
int16_t error; // Difference between desired and actual tip temperature
int16_t lasterror = 0; // Error value at the last derivative term update
uint8_t pot_sample_counter = 0; // Number of half-cycles before a setpoint pot reading is due
uint8_t deriv_smooth_counter = 0; // Number of half-cycles before a derivative update is due
int16_t deriv_term = 0; // Change in the error between the previous two derivative updates
uint16_t halfcycle_period // Mains half cycle period, 0.25us units (Note: resolution is only
= NOM_HALFCYC_PERIOD * 256; // 64us, but stored *256 to suit digital filter)
int16_t power; // Commanded output power, 0..MAX_POWER
uint8_t on_time; // Triac firing delay, 64us units
while(1)
{
// wait for falling edge of zero cross detector
while (PINB & PIN_ZEROCROSS)
;
// turn off element
PORTB &= ~PIN_HEATER;
// read the mains half-cycle period measurement
update_halfcycle_period(&halfcycle_period);
// read the setpoint potentiometer
read_pot(&pot_sample_counter, &pot);
// wait for rising edge
while (!(PINB & PIN_ZEROCROSS))
;
_delay_ms(1);
// read current temp
processvar = -read_adc(CHAN_SENSOR | _BV(REFS1) | _BV(ADLAR), _BV(BIN)) + PROCESSVAR_OFFSET;
if (processvar < 0)
processvar = 0;
// digitally filter current temp
processvar_avg = (processvar * TEMP_FILTER_B + processvar_avg * (256 - TEMP_FILTER_B))/256;
setpoint = SETPOINT_OFFSET + 4 * pot;
error = setpoint - processvar_avg;
// update the derivative term if it is time to do so,
// using a backward drifference approximation
if (!deriv_smooth_counter--)
{
deriv_smooth_counter = DERIV_SMOOTH_INTERVAL;
deriv_term = error - lasterror;
lasterror = error;
}
// calculate the required output power
power = 1L* error * KP + deriv_term * KD;
if (power > MAX_POWER)
power = MAX_POWER;
else if (power < 0)
power = 0;
// calculate the triac firing delay required for the desired power
on_time = 1L* (halfcycle_period/256) * (255-power) / 256;
OCR0B = on_time;
// output current temperature as a PWM signal on PB2
OCR0A = 1L* (halfcycle_period/256) * (processvar_avg / 64) / 256;
}
}