Showing posts with label soldering. Show all posts
Showing posts with label soldering. Show all posts

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.