Recently I built electronics for Lostmachine Andy's fire effects on his awesome pirate ship for Burning Man.
"Read more" for technical details....
To get a better idea of Andy's amazing Pirate Ship project, check out the video on his kickstarter page. The info about the fire effects starts at 2:58 into the video.
Here is a video where 1 of the 8 fire nozzels was tested in Andy's driveway!
My little piece of this huge project is the circuit board, which you can see in the center of this nice metal box Andy machined.
Here are images of the circuit board.
The 8 solenoid valves connect on the top edge. Eight manual fire pushbuttons connect on the bottom edge. Those pushbuttons directly turn on the transistor for each valve. Of course, the software running on a Teensy 2.0 can control the valves too. There's inputs for 4 buttons and 2 knobs to the software. There's a place to plug in a 16x2 LCD, which shows info about the sequence to be used.
Circuitry-wise, the board is pretty simple. There's a big P-channel mosfet at the input, acting as a diode to protect against reverse polarity power. Each valve draws about 1.5 amps, so at 12 amps, a regular diode didn't seem like a good idea. In the center is a Teensy 2.0 board in sockets, and to the right is a LM7805 regulator. The 5 volt power to the 2 knobs goes through little PTC fuses. The knobs and buttons just wire into analog and digital pins, through some little R-C filters, just in case there's any radio frequency noise pickup. Since I'm not going to Burning Man this year and can't be there to troubleshoot, I wanted to play it safe (and it only takes a few extra cheap parts).
On the top edge are the 8 transistor circuits. There too, I decided to play things safe, perhaps a bit overly cautious? The valves are switched with IRFR5305 P-channel mosfets. Rated at 31 amps, 55 volts, they're a bit overkill. Then again, I wanted to make sure they wouldn't get hot, so their on resistance of 0.065 ohms is nice. At 1.5 amps, that ought to be 0.15 watts, which isn't much at all for a package with a metal tab. A lesser transistor would have worked, but might have needed a heatsink. This PCB was made at Sunstone and just barely fit into 9 square inches. Using heatsinks would have bumped the cost up to the next bracket, which is a lot more than the cost of these nicer transistors.
Andy was concerned about the valves switching quickly. So for the back-EMF catch diode, I used a B130 schottky in series with a 12 volt, 3 watt zener. That lets the valve create -12 volts while discharging, so it ought to switch from on-to-off about as far as off-to-on. We talked a bit about possibly using a very complex approach where the valves might be driven with about 50 volts until they get up to the correct current, and then sustain at the rated 12 volts. Yes, that's risky, but I'm pretty sure I could do it safely. Maybe after the burn this year we'll be able to play with the valves and experiment to see if forcing the valve to open and close faster than it can with only 12 volts actually allows any interesting fire effects?
A couple extra overly cautious things I did do were resistors to slow down the gate drive into the microseconds range (still far faster than any mechanical valve can move), and just to be extra cautious, I put a tiny R-C snubber on the output, just in case the transistor somehow switches faster that the reverse recovery time of those diodes. I guess over an amp of current flowing in an almost purely inductive load makes me a bit nervous. Those parts probably aren't necessary and the 2 diodes are probably all the protection necessary from inductive spikes... but I wanted to play it extra safe since things are so hard to fix out on the playa at Burning Man (especially at night, when the fire will be in use).
So with all this circuitry hooked up, the good news is it's all programmable with Arduino. Here's the sketch I delivered to Andy. It reads a 12 position rotary knob and a speed pot, and plays 1 of the 12 sequences when a "Go" button is pressed. There's a "stop" and "fire all" button, and the 4th button is unused.
While this code is fairly long, and a bit complex in the custom delay and other stuff near the end, I tried to keep the definition of the 12 sequences very simple. Just digitalWrite and the custom delay function.
#include <LiquidCrystal.h>
#include <Bounce.h>
// input pinsconstint go_button_pin = 4;
constint stop_button_pin = 1;
constint all_button_pin = 2;
constint speed_knob_pin = 20; // analogconstint rotary_select_pin = 21; // analog// output pinsconstint valve1_pin = 19;
constint valve2_pin = 18;
constint valve3_pin = 17;
constint valve4_pin = 16;
constint valve5_pin = 15;
constint valve6_pin = 14;
constint valve7_pin = 13;
constint valve8_pin = 12;
constint lcd_rs_pin = 5;
constint lcd_en_pin = 6;
constint lcd_d4_pin = 7;
constint lcd_d5_pin = 8;
constint lcd_d6_pin = 9;
constint lcd_d7_pin = 10;
constint led_pin = 11;
constbyte valvelist[8] = {valve1_pin, valve2_pin, valve3_pin, valve4_pin,
valve5_pin, valve6_pin, valve7_pin, valve8_pin};
// unused pinsconstint unused_button_pin = 3;
constint unused1_pin = 0;
constint unused2_pin = 22;
constint unused3_pin = 23;
constint unused4_pin = 24;
// Objects for the buttons and displayBounce go_button(go_button_pin, 10);
Bounce stop_button(stop_button_pin, 10);
Bounce all_button(all_button_pin, 10);
LiquidCrystal lcd(lcd_rs_pin, lcd_en_pin,
lcd_d4_pin, lcd_d5_pin, lcd_d6_pin, lcd_d7_pin);
#define Pattern_Name_1 "Sweep Left+Right";
void play1()
{
digitalWrite(valve1_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve1_pin, LOW);
digitalWrite(valve2_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve2_pin, LOW);
digitalWrite(valve3_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve3_pin, LOW);
digitalWrite(valve4_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve4_pin, LOW);
digitalWrite(valve5_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve5_pin, LOW);
digitalWrite(valve6_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve6_pin, LOW);
digitalWrite(valve7_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve7_pin, LOW);
digitalWrite(valve8_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve8_pin, LOW);
}
#define Pattern_Name_2 "Sweep Right-Left"void play2()
{
digitalWrite(valve8_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve8_pin, LOW);
digitalWrite(valve7_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve7_pin, LOW);
digitalWrite(valve6_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve6_pin, LOW);
digitalWrite(valve5_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve5_pin, LOW);
digitalWrite(valve4_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve4_pin, LOW);
digitalWrite(valve3_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve3_pin, LOW);
digitalWrite(valve2_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve2_pin, LOW);
digitalWrite(valve1_pin, HIGH);
if (delayCust(150, 100)) return;
digitalWrite(valve1_pin, LOW);
}
#define Pattern_Name_3 "Alt Side-Side"void play3()
{
for (int count=0; count < 6; count++) {
digitalWrite(valve1_pin, HIGH);
digitalWrite(valve2_pin, HIGH);
digitalWrite(valve3_pin, HIGH);
digitalWrite(valve4_pin, HIGH);
if (delayCust(90, 75)) return;
digitalWrite(valve1_pin, LOW);
digitalWrite(valve2_pin, LOW);
digitalWrite(valve3_pin, LOW);
digitalWrite(valve4_pin, LOW);
digitalWrite(valve5_pin, HIGH);
digitalWrite(valve6_pin, HIGH);
digitalWrite(valve7_pin, HIGH);
digitalWrite(valve8_pin, HIGH);
if (delayCust(90, 75)) return;
digitalWrite(valve5_pin, LOW);
digitalWrite(valve6_pin, LOW);
digitalWrite(valve7_pin, LOW);
digitalWrite(valve8_pin, LOW);
}
}
#define Pattern_Name_4 "Alt Odd-Even"void play4()
{
for (int count=0; count < 6; count++) {
digitalWrite(valve1_pin, HIGH);
digitalWrite(valve3_pin, HIGH);
digitalWrite(valve5_pin, HIGH);
digitalWrite(valve7_pin, HIGH);
if (delayCust(90, 75)) return;
digitalWrite(valve1_pin, LOW);
digitalWrite(valve3_pin, LOW);
digitalWrite(valve5_pin, LOW);
digitalWrite(valve7_pin, LOW);
digitalWrite(valve2_pin, HIGH);
digitalWrite(valve4_pin, HIGH);
digitalWrite(valve6_pin, HIGH);
digitalWrite(valve8_pin, HIGH);
if (delayCust(90, 75)) return;
digitalWrite(valve2_pin, LOW);
digitalWrite(valve4_pin, LOW);
digitalWrite(valve6_pin, LOW);
digitalWrite(valve8_pin, LOW);
}
}
#define Pattern_Name_5 "Mid to Outside";
void play5()
{
digitalWrite(valve4_pin, HIGH);
digitalWrite(valve5_pin, HIGH);
if (delayCust(80, 25)) return;
digitalWrite(valve3_pin, HIGH);
digitalWrite(valve6_pin, HIGH);
if (delayCust(80, 25)) return;
digitalWrite(valve2_pin, HIGH);
digitalWrite(valve7_pin, HIGH);
if (delayCust(80, 25)) return;
digitalWrite(valve1_pin, HIGH);
digitalWrite(valve8_pin, HIGH);
if (delayCust(80, 25)) return;
digitalWrite(valve4_pin, LOW);
digitalWrite(valve5_pin, LOW);
if (delayCust(80, 25)) return;
digitalWrite(valve3_pin, LOW);
digitalWrite(valve6_pin, LOW);
if (delayCust(80, 25)) return;
digitalWrite(valve2_pin, LOW);
digitalWrite(valve7_pin, LOW);
if (delayCust(80, 25)) return;
digitalWrite(valve1_pin, LOW);
digitalWrite(valve8_pin, LOW);
}
#define Pattern_Name_6 "(unused) Six"void play6()
{
digitalWrite(valve1_pin, HIGH);
if (delayCust(25, 50)) return;
digitalWrite(valve1_pin, LOW);
}
#define Pattern_Name_7 "Random, 1 valve"void play7()
{
for (byte i=0; i<20; i++) {
byte myValve = valvelist[random(0, 8)];
digitalWrite(myValve, HIGH);
if (delayCust(50, 75)) return;
digitalWrite(myValve, LOW);
}
}
// this is a comment // You can type a description// Or deep thoughts
#define Pattern_Name_8 "Random, 2 valves"void play8()
{
for (byte i=0; i<20; i++) {
byte myValve1 = valvelist[random(0, 8)];
byte myValve2 = myValve1;
while (myValve2 == myValve1) {
myValve2 = valvelist[random(0, 8)];
}
digitalWrite(myValve1, HIGH);
digitalWrite(myValve2, HIGH);
if (delayCust(50, 75)) return;
digitalWrite(myValve1, LOW);
digitalWrite(myValve2, LOW);
}
}
#define Pattern_Name_9 "Random, 2 Valves";
void play9()
{
byte prev1 = 100;
byte prev2 = 101;
byte myValve1 = 102;
byte myValve2 = 103;
for (byte i=0; i<20; i++) {
while (myValve1 == prev1 || myValve1 == prev2 || myValve2 == prev1 || myValve2 == prev2) {
myValve1 = valvelist[random(0, 8)];
myValve2 = myValve1;
while (myValve2 == myValve1) {
myValve2 = valvelist[random(0, 8)];
}
}
digitalWrite(myValve1, HIGH);
digitalWrite(myValve2, HIGH);
if (delayCust(50, 75)) return;
digitalWrite(myValve1, LOW);
digitalWrite(myValve2, LOW);
prev1 = myValve1;
prev2 = myValve2;
}
}
#define Pattern_Name_10 "Pi Pattern"void play10()
{
digitalWrite(valve3_pin, HIGH);
if (delayCust(50, 75)) return;
digitalWrite(valve3_pin, LOW);
if (delayCust(50, 75)) return;
digitalWrite(valve1_pin, HIGH);
if (delayCust(50, 75)) return;
digitalWrite(valve1_pin, LOW);
if (delayCust(50, 75)) return;
digitalWrite(valve4_pin, HIGH);
if (delayCust(50, 75)) return;
digitalWrite(valve4_pin, LOW);
if (delayCust(50, 75)) return;
digitalWrite(valve1_pin, HIGH);
if (delayCust(50, 75)) return;
digitalWrite(valve1_pin, LOW);
if (delayCust(50, 75)) return;
digitalWrite(valve5_pin, HIGH);
if (delayCust(50, 75)) return;
digitalWrite(valve5_pin, LOW);
if (delayCust(50, 75)) return;
digitalWrite(valve8_pin, HIGH);
if (delayCust(50, 75)) return;
digitalWrite(valve8_pin, LOW);
}
#define Pattern_Name_11 "(unused) Eleven"void play11()
{
}
#define Pattern_Name_12 "(unused) Twelve"void play12()
{
}
// initialize hardware - only done once at bootupvoidsetup() {
pinMode(go_button_pin, INPUT_PULLUP);
pinMode(stop_button_pin, INPUT_PULLUP);
pinMode(all_button_pin, INPUT_PULLUP);
pinMode(unused_button_pin, INPUT_PULLUP);
pinMode(unused1_pin, INPUT_PULLUP);
pinMode(unused2_pin, INPUT_PULLUP);
pinMode(unused3_pin, INPUT_PULLUP);
pinMode(unused4_pin, INPUT_PULLUP);
pinMode(valve1_pin, OUTPUT);
pinMode(valve2_pin, OUTPUT);
pinMode(valve3_pin, OUTPUT);
pinMode(valve4_pin, OUTPUT);
pinMode(valve5_pin, OUTPUT);
pinMode(valve6_pin, OUTPUT);
pinMode(valve7_pin, OUTPUT);
pinMode(valve8_pin, OUTPUT);
lcd.begin(16, 2);
}
int prev_num = -100;
int prev_knob_reading = -100;
voidloop() {
// first, update the status of the buttons
go_button.update();
stop_button.update();
all_button.update();
// the "ALL" button is highest priorityif (all_button.read() == LOW) {
lcd.clear();
lcd.print(" All Valves ON");
digitalWrite(valve1_pin, HIGH);
digitalWrite(valve2_pin, HIGH);
digitalWrite(valve3_pin, HIGH);
digitalWrite(valve4_pin, HIGH);
digitalWrite(valve5_pin, HIGH);
digitalWrite(valve6_pin, HIGH);
digitalWrite(valve7_pin, HIGH);
digitalWrite(valve8_pin, HIGH);
while (all_button.read() == LOW) {
all_button.update();
// do nothing, just wait for button release
}
digitalWrite(valve1_pin, LOW);
digitalWrite(valve2_pin, LOW);
digitalWrite(valve3_pin, LOW);
digitalWrite(valve4_pin, LOW);
digitalWrite(valve5_pin, LOW);
digitalWrite(valve6_pin, LOW);
digitalWrite(valve7_pin, LOW);
digitalWrite(valve8_pin, LOW);
lcd.clear();
prev_num = -100;
prev_knob_reading = -100;
}
// when not doing anything, read the knobs// and show status info on the LCDint rotary_reading = analogRead(rotary_select_pin);
int num;
constchar *name;
if (rotary_reading < 47) {
num = 1;
name = Pattern_Name_1;
} elseif (rotary_reading < 140) {
num = 2;
name = Pattern_Name_2;
} elseif (rotary_reading < 233) {
num = 3;
name = Pattern_Name_3;
} elseif (rotary_reading < 326) {
num = 4;
name = Pattern_Name_4;
} elseif (rotary_reading < 419) {
num = 5;
name = Pattern_Name_5;
} elseif (rotary_reading < 512) {
num = 6;
name = Pattern_Name_6;
} elseif (rotary_reading < 605) {
num = 7;
name = Pattern_Name_7;
} elseif (rotary_reading < 698) {
num = 8;
name = Pattern_Name_8;
} elseif (rotary_reading < 791) {
num = 9;
name = Pattern_Name_9;
} elseif (rotary_reading < 884) {
num = 10;
name = Pattern_Name_10;
} elseif (rotary_reading < 977) {
num = 11;
name = Pattern_Name_11;
} else {
num = 12;
name = Pattern_Name_12;
}
if (num != prev_num) {
lcd.setCursor(0, 0);
lcd.print(name);
for (int i=strlen(name); i < 16; i++) {
lcd.print(' ');
}
prev_num = num;
}
get_speed();
lcd.setCursor(9, 1);
lcd.print("Ready");
if (go_button.fallingEdge()) {
lcd.setCursor(9, 1);
lcd.print("Running");
switch (num) {
case 1: play1(); break;
case 2: play2(); break;
case 3: play3(); break;
case 4: play4(); break;
case 5: play5(); break;
case 6: play6(); break;
case 7: play7(); break;
case 8: play8(); break;
case 9: play9(); break;
case 10: play10(); break;
case 11: play11(); break;
case 12: play12(); break;
default: play1();
}
// after playing, always make sure all valves are offdigitalWrite(valve1_pin, LOW);
digitalWrite(valve2_pin, LOW);
digitalWrite(valve3_pin, LOW);
digitalWrite(valve4_pin, LOW);
digitalWrite(valve5_pin, LOW);
digitalWrite(valve6_pin, LOW);
digitalWrite(valve7_pin, LOW);
digitalWrite(valve8_pin, LOW);
// and reset the state, so the screen fully redraws
lcd.clear();
prev_num = -100;
prev_knob_reading = -100;
}
delay(10);
}
// Read the speed knob. This is a bit tricky, because we don't want to// rapidly alternate between two speed settings if the knob is exactly at// the mid-point and a tiny bit of noise changes the reading slightly.// So instead, this code remember the previous setting and implements a// tiny dead band (hysteresis) to so the user always sees a smooth// appearance on the LCD.//byte get_speed(void)
{
staticbyte knob_speed = 1;
int knob_reading = analogRead(speed_knob_pin);
if (knob_reading < prev_knob_reading - 2 || knob_reading > prev_knob_reading + 2) {
lcd.setCursor(0, 1);
lcd.print("Speed:");
knob_speed = (knob_reading / 94) + 1; // range is 1 to 11
lcd.print((int)knob_speed);
if (knob_speed < 10) lcd.print(" ");
prev_knob_reading = knob_reading;
}
return knob_speed;
}
// Delay based on speed setting. "mult" is a multiplier for// the speed, and "fixed" is a fixed delay that is always// added regardless of the speed setting. This fancy code// allows the delay setting to change, and also aborts if the// STOP or ALL buttons are pressed.//boolean delayCust(int mult, int fixed)
{
unsignedlong us = micros();
unsignedlong elapsed_ms = 0;
unsignedlong target_ms;
while (1) {
target_ms = (11 - get_speed()) * mult + fixed; // moving targetif (elapsed_ms >= target_ms) returnfalse; // delay completedif (micros() - us >= 1000) {
elapsed_ms = elapsed_ms + 1;
us = us + 1000;
}
if (stop_button.update()) returntrue; // delay interruptedif (all_button.update()) returntrue;
}
}
The last bit of technical info to share is the PCB gerber files, which were sent to Sunstone. The files are attached below.
If you use these files, please be aware 1 small error was discovered. The power for the LCD was mistakenly connected to +12 volts. If you use a LCD, you must cut that trace and solder a wire to the +5 volt output of the LM7805. Other that that 1 error, the board works great.
Hopefully after the burn this year, Andy will have lots of picture and video on his blog of the pirate ship and its fire effects in action!