Tuesday, 29 December 2009

Cooking with HydraRaptor

I needed to assemble some PCBs recently, so I set about making a temperature controller for my SMT oven.



First I had to replace the solid state relay on HydraRaptor.



Solid state relays are triacs with an optically coupled input, zero crossing switching and built in snubbers. I used it for controlling a vacuum cleaner when milling. It was massively overrated but for some reason it failed some time ago. I replaced it with a cheaper one and added a varistor across the mains input to kill any transients, as that is the only explanation I can think of for the old one's demise.





The next task was to write a simple graphing program in Python. I tested it by plotting the response of my extruder heater.



With bang-bang control it swings +/- 2°C with a cycle time of about ten seconds.

Here is the code for the graph: -
from Tkinter import *

class Axis:
def __init__(self, min, max, minor, major, scale):
self.scale = scale
self.min = min
self.max = max
self.minor = minor
self.major = major

class Graph:
def __init__(self, xAxis, yAxis):
self.xAxis = xAxis
self.yAxis = yAxis
self.root = Tk()
self.root.title("HydraRaptor")
self.last_x = None
frame = Frame(self.root)
frame.pack()
xmin = xAxis.min * xAxis.scale
xmax = xAxis.max * xAxis.scale
ymin = yAxis.min * yAxis.scale
ymax = yAxis.max * yAxis.scale
width = (xmax - xmin) + 30
height = (ymax - ymin) + 20
#
# X axis
#
self.canvas = Canvas(frame, width = width, height = height, background = "white")
for x in range(xmin, xmax + 1, xAxis.minor * xAxis.scale):
self.canvas.create_line(x, ymin, x, ymax, fill = "grey")
for x in range(xmin, xmax + 1, xAxis.major * xAxis.scale):
self.canvas.create_line(x, ymin, x, ymax, fill = "black")
if x == xmin:
anchor = "nw"
else:
anchor = "n"
self.canvas.create_text((x, ymax), text = x / xAxis.scale, anchor = anchor)
#
# Y axis
#
for y in range(ymin, ymax + 1, yAxis.minor * yAxis.scale):
self.canvas.create_line(xmin, y, xmax, y, fill = "grey")
for y in range(ymin, ymax + 1, yAxis.major * yAxis.scale):
self.canvas.create_line(xmin, y, xmax, y, fill = "black")
if y == ymin:
anchor = "se"
else:
anchor = "e"
self.canvas.create_text((xmin, ymax + ymin - y), text = y / yAxis.scale, anchor = anchor)
self.canvas.pack()
self.canvas.config(scrollregion=self.canvas.bbox(ALL))
self.root.update()

def scaleX(self,x):
return x * self.xAxis.scale

def scaleY(self,y):
axis = self.yAxis;
return (axis.max + axis.min - y) * axis.scale

def plot(self, line, colour = "blue"):
for i in range(len(line) - 1):
self.canvas.create_line(self.scaleX(line[i][0]),
self.scaleY(line[i][1]),
self.scaleX(line[i+1][0]),
self.scaleY(line[i+1][1]), fill = colour)
self.root.update()


def addPoint(self,p, colour="red"):
x = self.scaleX(p[0])
y = self.scaleY(p[1])
if self.last_x != None:
self.canvas.create_line(self.last_x, self.last_y, x, y, fill = colour)
self.last_x = x
self.last_y = y
self.root.update()

def __del__(self):
self.root.mainloop()
The third task was to interface a thermocouple to HydraRaptor. I had a spare analogue input, so I attached one of Zach's thermocouple sensor boards to it. I tested it by attaching the thermocouple to a light bulb with Kapton tape. I then ran a program that turned the bulb on and then off and graphed the temperature response.



As you can see there is a ridiculous amount of noise on the readings. I tracked this down to switching noise on HydraRaptor's 5V rail, which is generated by a simple buck converter from a 24V rail. The AD595 datasheet claims that it has a power supply sensitivity of only 10mV/V so the error should have been a small fraction of a °C. All I can assume is that its rejection of high frequency noise is far less than its DC supply rejection. In fact, pretty much all the supply noise appears on the output.

I fixed it by filtering the supply with a simple RC filter consisting of a 1K series resistor and a 22uF capacitor. I fitted these to the thermocouple board in the unused holes intended for an alarm LED and its series resistor. The power is fed in via the anode connection for the LED. It feeds to the supply rail via the 1K fitted in the R1 position. The positive lead of the capacitor goes into the original +5v connection to the board. The negative lead goes to the GND connection together with the ground lead. This mod will be required whenever the 5V rail comes from a switch mode supply rather than a linear regulator.



Here is the much improved graph with the filter fitted: -



The next thing I tried was bang-bang control of the oven to a fixed temperature with the thermocouple attached to a scrap PCB. No great surprise that there is massive overshoot due to the thermal lag caused by the loose coupling of the PCB to the heating elements via air.



It is obvious some form of proportional control is required, so I implemented PWM control of the mains supply to the oven. As triacs don't turn off until the end of the mains cycle there is no point in varying the pulse width in less than 10ms increments (in the UK). So I implemented a simple firmware scheme where I can specify how many 10ms units to be on for out of a total period, also specified in 10ms units. Setting the period to 1 second allows the heating power to be expressed in 1% units.

My original plan was to implement a PID controller, but after examining the required soldering profile I decided a much simpler scheme would probably perform better.


The is a profile for tin-lead solder that I got from an Altera application note. I mainly use leaded solder at home because the lower melt point gives a much bigger margin for error, it wets and flows a lot better, the fumes are less toxic and it doesn't grow tin whiskers.

Looking at the profile you can see the times are not too critical, but the temperatures are. I reasoned I could simply apply fixed powers to get the right temperature gradient until each target temperature was reached. To get round the overshoot problem I simply measured the overshoot and subtracted it from the target temps.

After a little experimenting I got this profile, which looks pretty good to me: -



The blue line is the target profile, red is actual and the green lines show the time at which each target was reached.

The preheat slope and re-flow slope are simply full power until the temperature is equal to the target minus the overshoot. During the first half of the soak period I had to ramp the power from 0 to 50% to get it to turn the first corner without overshoot. When the reflow peak minus the overshoot is reached I simply turn the oven off. When it gets to the cool section I open the oven door.

Here is the code: -
from Hydra import *
from Graph import *

profile = [(10,20), (120,150), (210,180), (250,210), (330, 180), (420, 20)]

slope = 140.0 / 100
overshoot = 15.0
pre_overshoot = 25

preheat_temp = 150.0
soak_temp = 180.0
soak_time = 90.0
reflow_temp = 210.0
melt_temp = 183.0

preheat_slope = (soak_temp - preheat_temp) / soak_time

s_preheat = 1
s_soak = 2
s_reflow = 3
s_cool = 4

def interp(profile, x):
i = 0
while i < len(profile) - 1 and profile[i + 1][0] < x:
i += 1
if i == len(profile) - 1:
return 0
p0 = profile[i]
p1 = profile[i+1]
return p0[1] + (p1[1]-p0[1]) * (x - p0[0]) / (p1[0] - p0[0])


def oven_cook(profile):
hydra = Hydra(True)
try:
xAxis = Axis(min = 0, max = 500, minor = 5, major = 25, scale = 2)
yAxis = Axis(min = 10, max = 250, minor = 5, major = 20, scale = 2)
graph = Graph(xAxis, yAxis)
graph.plot(profile)

t = 0
state = s_preheat
m_state = s_preheat
hydra.set_mains(100,100)
while t < xAxis.max:
sleep(1)
temp = hydra.get_temperature()
print temp
graph.addPoint((t, temp))
#
# Control the power
#
if state == s_preheat:
if temp >= preheat_temp - pre_overshoot:
hydra.set_mains( 0, 100)
t_soak = t
state = s_soak
elif state == s_soak:
power = (t - t_soak) * 100.0 / soak_time
if power > 50:
power = 50
hydra.set_mains(int(power), 100)
if temp >= soak_temp - overshoot * preheat_slope / slope:
hydra.set_mains(100,100)
state = s_reflow
elif state == s_reflow:
if temp >= reflow_temp - overshoot:
hydra.set_mains(0,100)
state = s_cool
#
# Draw the time lines
#
if m_state == s_preheat:
if temp >= preheat_temp:
graph.plot([(t,10), (t,temp)], "green")
m_state = s_soak
elif m_state == s_soak:
if temp >= melt_temp:
graph.plot([(t,10), (t,temp)], "green")
m_state = s_reflow
elif m_state == s_reflow:
if temp < melt_temp:
graph.plot([(t,10), (t,temp)], "green")
m_state = s_cool

t += 1
hydra.init()

except:
hydra.init()
raise

oven_cook(profile)
This is the first board I soldered with it: -



All the joints were good. I had a few solder balls and some bridging but that was due to not getting the right amount of paste on each pad. I will be working on a solder paste dispenser soon!

I need to do some more testing to see if the arbitrary algorithm will work with large and small boards and with inner planes, etc. It relies on the overshoot being fairly constant, although with leaded solder you have some leeway.

I also want to play with PID to see if I can get a more general solution. The problem I see is that PID does not look into the future, so will always overshoot somewhat, which is exactly what you don't want. I think rather than using the angular profile, that is impossible for the oven to follow, I would have to put in a rounded curve, such as the one the oven actually follows now, as the control input.

11 comments:

  1. This is one of the most interesting blogs that I read.

    You provide good photos, text, ideas and I like the Hydraraptor project. I always enjoy all types of Homebrew CNC and Electronic types of projects.

    Thanks for sharing, and Keep up the very interesting work.

    Eldon - WA0UWH - http://WA0UWH.blogspot.com

    ReplyDelete
  2. Eldon,
    Thanks for the kind words. Glad you enjoy it.

    ReplyDelete
  3. Your filter seems to have cured the issue, the cleaned up graph is as clean as I have ever seen.

    Don't forget the amplifier in that Analog Devices chip has 15kHz bandwidth so it will tend to pass on any noise it sees.

    Did you tune your PID? The amount of overshoot will depend on the tuning and relative power available compared to the losses? http://en.wikipedia.org/wiki/PID_tuning

    ReplyDelete
  4. Yes but it should only pass on noise on the input, not on the power rail and the noise was much higher than 15kHz, more like 150KHz.

    I haven't tried PID yet, but obviously it would need tuning.

    ReplyDelete
  5. Most excelent, I have a two element pizza oven sat here waiting for the same treatment, with a the same type of thermocouple board put to one side as temperature sensor. You are going to save me a whole bundle of effort, you are a star.

    I believe the switching elements on, for half cycles (10ms) calculated over an epoch 91 second in your design)is known as burst firing.

    There is a way to make burst firing work better for you.

    Smoother if you like.

    That is to avoid having the burst in a given epoch be all on followed by all off (or vice versa). If you devide the "off time" into regular small chuncks (pref one off period each) and space it evenly (not always possible to manage equaly) through out the on period. You will take advantage of the averaging effect of the thermal hysterisis/inertia of the heating element to give a smoothed out response.

    1 second is probably a bit on the long side for the thermal hysterisis/inertia of the element to work in your favour. Although it will be much better than a longer epoch.

    For example if you wanted 50% instead of turning the element on for 0.5 of a second, followed by off for 0.5 of a second. Turn the element on every other 10ms. (Half cycle at uk 50hz)

    ReplyDelete
  6. If the input to your PID has less slope than the output achieves at 100%, it should track quite closely after tuning.

    If the input to your PID has too much slope, or steps to other values, or generally moves faster than the output is capable of, you will get integral wind-up and other nasties which cause the overshoot you predict.

    So, see if it's feasible to increase power to your heater, and/or feed the algorithm a shallow enough slope that it never runs the heater at 100%.

    ReplyDelete
  7. I was under the impression that most SMT ovens measured the tempurature of the air, not the tempurature of the PCB? Perhaps if you measure the air rather than the PCB you can avoid most of the overshoot. After all, thermal systems don't have inertia.

    But it looks good :)

    ReplyDelete
  8. @AKA47,
    I PWM leds in my day job so fully aware of various schemes, but the elements don't react in 1 second (they are metal clad) so no point in making it more complicated. The board itself reacts even slower as it is only loosely coupled to the elements by air.

    @Triffid Hunter,
    Yes it makes sense that it can't be at 100% with PID and still be under control so this algorithm will probably give better performance than PID in this case. It certainly seems better than the commercial controller I borrowed from work.

    @geo01005,
    Production machines pass the board through an oven on a conveyor belt that moves at constant speed. They get the correct temperature profile by maintaining different zones of the oven at different temperatures. The board temperature is indeed open loop but they calibrate the profile for each type of board by running some through with thermocouples attached.

    It is the board temperature that is important and all the single board systems I have seen measure the board temp and control the oven.

    ReplyDelete
  9. Another great post your re-use of an oven looks like a better solution than my PID controlled hot plate.

    One thing I'm going to try next with the Pid Contolled hot plate is to make some moulds for making polymorph anti-backlash nuts for tapezodial drive shafts.

    ReplyDelete
  10. I recently built a PID controled egg incubator using an arduino as controler. The thing I found most surprising is how the pid loop is sensitive to the inter iteration delay. ie how often the loop is calculated compared to the thermal delay/hysterisis of the heater circuit and sensor.

    I ended up running the loop quite a bit less often than I had originaly planned. It definitely improved stability.

    I guess this supports TH's observations.

    A side benfit was that I could over sample the thermistor value and apply a simple running average as a low pass filter to remove a bunch of the signal noise.

    ReplyDelete
  11. @nophead,
    PID can't look into the future, but the D term "predicts" the future based on the rate of change of the error signal. With an appropriately tuned loop, there should be no overshoot.

    @AKA47,
    Changing the speed at which you iterate a software-based PID controllers changes the effective gains of both I and D terms. Running faster without scaling the terms appropriately can cause all sorts of instability.

    ReplyDelete