Pirate Ship Fire Sequencer

Many years I built electronics for Lostmachine Andy‘s fire effects on his awesome pirate ship for Burning Man.

The electronics from this old project were reused in several other propane fire art projects.

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 nozzles 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 16×2 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 pins
const int go_button_pin      = 4;
const int stop_button_pin    = 1;
const int all_button_pin     = 2;
const int speed_knob_pin     = 20;  // analog
const int rotary_select_pin  = 21; // analog

// output pins
const int valve1_pin         = 19;
const int valve2_pin         = 18;
const int valve3_pin         = 17;
const int valve4_pin         = 16;
const int valve5_pin         = 15;
const int valve6_pin         = 14;
const int valve7_pin         = 13;
const int valve8_pin         = 12;
const int lcd_rs_pin         = 5;
const int lcd_en_pin         = 6;
const int lcd_d4_pin         = 7;
const int lcd_d5_pin         = 8;
const int lcd_d6_pin         = 9;
const int lcd_d7_pin         = 10;
const int led_pin            = 11;

const byte valvelist[8] = {valve1_pin, valve2_pin, valve3_pin, valve4_pin,
                           valve5_pin, valve6_pin, valve7_pin, valve8_pin};

// unused pins
const int unused_button_pin  = 3;
const int unused1_pin        = 0;
const int unused2_pin        = 22;
const int unused3_pin        = 23;
const int unused4_pin        = 24;

// Objects for the buttons and display
Bounce 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 bootup
void setup() {
  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;


void loop() {
  // first, update the status of the buttons
  go_button.update();
  stop_button.update();
  all_button.update();
  
  // the "ALL" button is highest priority
  if (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 LCD
  int rotary_reading = analogRead(rotary_select_pin);
  
  int num;
  const char *name;
  if (rotary_reading < 47) {
    num = 1;
    name = Pattern_Name_1;
  } else if (rotary_reading < 140) {
    num = 2;
    name = Pattern_Name_2;
  } else if (rotary_reading < 233) {
    num = 3;
    name = Pattern_Name_3;
  } else if (rotary_reading < 326) {
    num = 4;
    name = Pattern_Name_4;
  } else if (rotary_reading < 419) {
    num = 5;
    name = Pattern_Name_5;    
  } else if (rotary_reading < 512) {
    num = 6;
    name = Pattern_Name_6;      
  } else if (rotary_reading < 605) {
    num = 7;
    name = Pattern_Name_7;        
  } else if (rotary_reading < 698) {
    num = 8;
    name = Pattern_Name_8;          
  } else if (rotary_reading < 791) {
    num = 9;
    name = Pattern_Name_9;            
  } else if (rotary_reading < 884) {
    num = 10;
    name = Pattern_Name_10;              
  } else if (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 off
    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);
    // 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)
{
  static byte 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)
{
  unsigned long us = micros();
  unsigned long elapsed_ms = 0;
  unsigned long target_ms;
  
  while (1) {
    target_ms = (11 - get_speed()) * mult + fixed; // moving target
    if (elapsed_ms >= target_ms) return false;  // delay completed
    if (micros() - us >= 1000) {
      elapsed_ms = elapsed_ms + 1;
      us = us + 1000;
    }
    if (stop_button.update()) return true; // delay interrupted
    if (all_button.update()) return true;
  }
}

The last bit of technical info to share is the PCB gerber files, which were sent to Sunstone.  Here are the PCB gerber files.

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.

 

This article was originally published on the DorkbotPDX website, on August 23, 2011.  In late 2018, DorkbotPDX removed its blog section.  An archive of the original article is still available on the Internet Archive.  I am republishing this article here, so the info and files for this project can be found and used by anyone wanting to build it.

PinSim – Virtual Reality Pinball Machine

Jeremy Williams built PinSim, a cabinet controller for virtual reality pinball to get much more realistic game playing experience.

Jeremy, a huge pinball enthusiast, knew he had to build a cabinet when he saw Pinball FX 2 VR at Oculus Days.  Digital pinball games have been around for quite a while, but as Jeremy noted, one of the problems with them is that your perspective is static.  Virtual reality (VR) pinball games allows users to change their perspective while playing, allowing for more finesse.

The PinSim cabinet improves the game play experience by adding a few elements that are crucial for realistic pinball play such as tactile controls, “non-clicky” buttons for the flippers, and an accelerometer-based nudge system.  Not only can you nudge the cabinet to move the ball, but if you nudge too much, you’ll tilt.

For the electronics a Teensy-LC using the MSF-XINPUT library by Zack Littell is used to emulate an Xbox 360 gamepad.

Build instructions for the cabinet can be found over on Tested.  Code for the project can be found on GitHub.  Over on the forum there’s a thread helping a user get all the libraries need to compile the PinSim code.

 

Battery Pack Load

I purchased a cheap USB power pack, thinking it would be ideal for powering small projects.  But it automatically shuts off if the device isn’t drawing a lot of power, since it’s meant for charging cell phones.

Here’s a 2 transistor circuit that keeps it on with very little battery drain by using brief pulses.

I wish I would have thought of this idea, but it came from this forum post by “Jp3141”.  The battery pack automatically turns off if it doesn’t see a high current draw.  But drawing a high current for only a brief time is enough to keep its internal timer going.

First, I did some experimenting and found a 22 ohm resistor keeps the power on indefinitely.  A 27 ohm resistor kept it on for 19 seconds.  With no load, it stays on for only 13 seconds.  So a 22 ohm load it is!

Just connecting a 22 ohm resistor to the 5 volt power is a pretty heavy load that would drain the battery.  A 22 ohm resistor also burns about 1.1 watts, so it gets HOT.  But the load doesn’t need to be on most of the time.  The next step was connecting a Teensy and transistor circuit to turn on the load under software control.

Here the 5V pin drives a LED in series with a NPN base-emitter junction, to apply about 2.3 volts to a 10 ohm resistor.  Of course, the NPN transistor has high current gain, so most of the 230 mA that flows through the 10 ohm resistor comes from the battery through the collector.

A little experimenting determined pretty quickly that pulses in the 8 to 10 ms range usually keep the battery pack on, but it sometimes turns off after a couple minutes.  20 ms seems very stable.

Knowing 20 ms is needed, I switched from using the Teensy++ to this simple 2 transistor oscillator:

A quick napkin calculation seemed to suggest this would need a really large capacitor.  But with a little fiddling, it turned out 22 uF was enough.  This circuit creates a pulse slight over 20 ms approximately every 1.4 seconds.

Here’s another close-up of the circuit on the breadboard.  Just 2 transistors, 1 capacitor, and 2 resistors.

 

I let this run for about half an hour, with the battery pack happily remaining on the whole time.

The average battery current is ~3.5 mA.

While the transistors are on, the current is approx 222 mA (4.9V on 22 ohms).  But the duty cycle is about 1.6%.

Inside the pack, a switching power supply is running to step up the batteries from 3.7 to 5 volts, and it’s powering those 4 blue LEDs.  The internal stuff inside the pack probably wastes a lot more than only 3.5 mA.

Of course, then I did a quick PCB layout.  I added a switch in series with the 100K resistor, so it can be left plugged in and turned off to allow the battery pack to shut itself off.  It’s a tiny board, only about the size of the USB connector itself.

I sent the files in to OSH Park.  Here’s their preview.

Here’s the board on their site, if anyone else wants to build this:

http://oshpark.com/shared_projects/Da8m8oAz

The 5 parts are on the bottom side of the PCB.  Here’s a placement diagram:

 

Here’s a list of part numbers:

 1276-5649-1-ND        22 ohm resistor, 1/4 watt
 490-1719-1-ND         22 uF capacitor, X5R, 6.3V
 RMCF0603FT100KCT-ND   100K resistor
 MMBT2222A-FDICT-ND    NPN Transistor
 MMBT3906-FDICT-ND     PNP Transistor
 WM17118-ND            USB Connector
 EG1941-ND             Switch

If you need to tune the timing for a different battery pack, increasing the capacitor makes the pulse wider and lengthens the time between pulses.  Decreasing the 100K resistor makes the pulse occur more frequently, without changing the width of the pulse.

 

 

EDIT: My battery pack turned off a couple times times after many minutes.  I increased the capacitor to 47 uF and it ran for an hour.  22 uF might be a little on the low side?  If you build this circuit, a little tweaking on the capacitor or resistor values might be needed if your battery pack is different.

EDIT AGAIN: Later I purchased an identical-looking battery pack, except it had black plastic instead of white.  It turned out to have a completely different power detecting scheme.  The pulses wouldn’t work.  This little circuit clearly doesn’t apply to every battery pack, but it worked very well on the earlier white one.

 

This article was originally published on the DorkbotPDX website, on November 5, 2013.  In late 2018, DorkbotPDX removed its blog section.  An archive of the original article is still available on the Internet Archive.  I am republishing this article here, so anyone still using these battery packs can try this circuit.

El Wire Color Changing

Years ago I experimented with shifting the color of El Wire by changing the AC frequency.

The color effect is subtle, but can be seen by most people with good color vision.

Not long after I published this video, I discovered “dcroy” built this same thing a few months before I did, and using only a 555 timer! Looks like it’s not as unique as I thought!

This is the schematic for the circuit I built.  Most of the parts are small surface mount and they’re hidden under the transformer.

Here’s the code from the Arduino IDE. Pretty simple stuff. But beware, if you try this, it’s essential to drive each transistor for the same length of time, and not more than about 1ms. If you drive one more than the other, or leave either on too long, the transform sees a DC or low frequency signal. That could result in far too much current, probably destroying the transistors if the battery is fresh.

const int pin1 = 12;
const int pin2 = 19;

void setup() {
  Serial.end();  // USB off, to save power
  for (byte i=0; i<25; i++) {
    pinMode(i, OUTPUT);    // all unused pins low
    digitalWrite(i, LOW);  // to avoid wasting power
  }
  pinMode(pin1, OUTPUT);
  pinMode(pin2, OUTPUT);
}

unsigned int us = 250 * 64;
const int usMax = 500 * 32;
const int usMin = 120 * 32;
byte dir = 0;

void loop() {
  digitalWrite(pin1, HIGH);
  delayMicroseconds(us / 32);
  digitalWrite(pin1, LOW);
  if (dir) {
    us = us + 1 + us / 2048;
    if (us >= usMax) dir = 0;
  } else {
    us = us - 1 - us / 2048;
    if (us <= usMin) dir = 1; 
  }
  digitalWrite(pin2, HIGH);
  delayMicroseconds(us / 32);
  digitalWrite(pin2, LOW);
}

This article was originally published on the DorkbotPDX website, on July 12, 2011.  In late 2018, DorkbotPDX removed its blog section.  An archive of the original article is still available on the Internet Archive.  I am republishing this article here, in case anyone might wish to try El Wire color changing.  Admittedly, El Wire became less popular around 2013 when inexpensive LED strips appeared on the market, but it still offers interesting effects which are hard to accomplish with LEDs.

 

Big 7-Segment Countdown Timer

Years ago, several “short” talks at a hackerspace that went far beyond their allotted time inspired me to make this nice countdown timer.

I found these 4 inch 7 segment displays cheap on Ebay, made a little constant-current driver board, and put it all together into a simple project.

Here’s what the back side looks like, with buttons to control the countdown.

The green buttons starts & stops the countdown.  The 2 blue buttons add or subtract 1 minute.  It’s a very simple and minimal user interface!

 

Here’s a rough schematic for the whole project.  Well, except I left off the 7805 regulator and maybe some other mundane stuff, but this is pretty close.

The main challenge was driving the LEDs with a constant current, because they need about 10.5 volts across the several series LEDs.  I wanted to run from 12 volts, so there wasn’t much voltage left over for the normal current limiting resistors.  Instead, I used this opamp circuit.

(edit: opps, I wrote LM358A on the schematic, but it’s actually a LM324A opamp.  Really, they’re the same, just 2 vs 4 per package… so you could use either if you try to build this on your own board, but if you use the PCB files below, get a 14 pin LM324A opamps)

The project runs from a Teensy 2.0.  The code is very simple, using the SPI and Bounce libraries for the hardware interfacing.

#include <SPI.h>
#include <Bounce.h>

// pins
//  0 - Latch
//  1 - Clock
//  2 - Data
//  4 - Enable (low=on)
//  9 - Dots (high=on)

Bounce button1 = Bounce(10, 12);
Bounce button2 = Bounce(23, 12);
Bounce button3 = Bounce(22, 12);

uint8_t min=5;
uint8_t sec=0;
uint8_t unused_pins[] = {3,5,6,7,8,11,12,13,14,15,16,17,18,19,20,21,24};

void setup()
{
	for (uint8_t i=0; i < sizeof(unused_pins); i++) {
		pinMode(i, OUTPUT);
		digitalWrite(i, LOW);
	}
	pinMode(10, INPUT_PULLUP);
	PORTC |= 0x80;
	pinMode(22, INPUT_PULLUP);
	pinMode(23, INPUT_PULLUP);
	digitalWrite(4, HIGH);
	pinMode(0, OUTPUT);
	pinMode(1, OUTPUT);
	pinMode(2, OUTPUT);
	pinMode(4, OUTPUT);
	pinMode(9, OUTPUT);
	digitalWrite(0, LOW);
	digitalWrite(1, LOW);
	digitalWrite(2, LOW);
	digitalWrite(4, HIGH);
	digitalWrite(9, LOW);
	SPI.begin();
	update();
}

uint8_t sevenseg[10] = {
	// gfedcba
	0b00111111, //  aaa
	0b00000110, // f   b
	0b01011011, // f   b
	0b01001111, //  ggg
	0b01100110, // e   c
	0b01101101, // e   c
	0b01111101, //  ddd
	0b00000111,  
	0b01111111,  
	0b01101111  
};

void update(void)
{
	if (min > 99) min == 99;
	if (sec > 59) sec = 59;
	SPI.transfer(min >= 10 ? sevenseg[min/10] : 0);
	SPI.transfer(min > 0 ? sevenseg[min%10] : 0);
	SPI.transfer(sevenseg[sec/10]);
	SPI.transfer(sevenseg[sec%10]);
	delayMicroseconds(2);
	digitalWrite(0, LOW);
	delayMicroseconds(2);
	digitalWrite(0, HIGH);
	digitalWrite(9, HIGH);
	digitalWrite(4, LOW);
	delayMicroseconds(5);
	digitalWrite(0, LOW);
}

elapsedMillis count = 0;
uint8_t running = 0;

void loop()
{
	button1.update();
	button2.update();
	button3.update();
	if (button1.fallingEdge()) {
		Serial.println("button1 - Start/Stop");
		if (running) {
			running = 0;
		} else {
			running = 1;
			count = 750;
		}
	}
	if (button2.fallingEdge()) {
		Serial.println("button2 - Add 1 minute");
		if (min < 99) {
			min++;
			update();
		}
	}
	if (button3.fallingEdge()) {
		Serial.println("button3 - Subtract 1 minute");
		if (min > 0) {
			min--;
		} else {
			sec = 0;
			running = 0;
		}
		update();
	}

	if (running && count >= 1000) {
		count -= 1000;
		if (sec > 0) {
			sec--;
		} else {
			if (min > 0) {
				min--;
				sec = 59;
			} else {
				running = 0;
			}
		}
		update();
	}
}

The 7 segment displays were something I’d purchased from an E-bay merchant about a year ago.  I can’t find then anymore (at least not for low prices), which is sad because they were really cheap at the time.

I actually created this PCB only days after the last Dorkbot open mic meetup (where talks went way beyond allotted time).  Here’s a photo of the board.

There’s actually quite a few parts on the board.  Here are the placement diagrams:

The circuit board can be ordered from OSH Park, or you can download the original gerber files if you’d like to have them made elsewhere.

If you find any of these awesome 4 inch displays, hopefully this little board will come in handy for driving them efficiently with only 12 volts.

 

This article was originally published on the DorkbotPDX website, on March 22, 2013.  In late 2018, DorkbotPDX removed its blog section.  An archive of the original article is still available on the Internet Archive.  I am republishing this article here, in the hope it may continue to be found and used by anyone interested in these 7 segment displays.

Embrace Heart Sequenced Incandescent Light Dimming

I worked on a tiny piece of the Embrace sculpture at Burning Man 2014.

Inside were 2 hearts, one made here in Portland by Lostmachine Andy & others at Flat Rat Studios.  I made electronics to gradually fade 4 incandescent light bulbs in heart beating patterns.

These wonderful photos where taken by Sarah Taylor.

Inside the enormous sculpture were two hearts.  The blue on was built by a group in Vancouver, B.C., Canada, and of course this one was built here in Portland, Oregon, USA.

Andy wanted this heart to have a very warm, gentle atmosphere, with warn incandescent bulbs slowly fading to create the heart beat.  These effect turned out quite well.  Andy really knows his stuff!

Here’s a great time-lapse video where you can see the slow, gradual incandescent light fading as a rapid heart beat.  Skip forward to about 0:36 in this video.

 

The light fading was done using a Teensy-based 4-channel AC dimmer board, on this 4 by 3.5 inch circuit board.

Here’s a quick video, from the first test of the light controller.

Four BT139X Triacs that actually switch the AC voltage are mounted on the bottom side to a heatsink that’s meant to dissipate any heat to the metal case.  Originally Andy believed the lights might be 500 watts each, so I was concerned about heat.  In the end, four 60 watt bulbs were used and the Triacs did not get noticeably warm.

Here is a parts placement diagram for building the circuit board.  Two boards were built, the one that ran the project and a spare… just in case!

The PCB cad files are attached below, if anyone wants to make more of these boards.

The AC switching circuitry was basically Fairchild Semiconductor’s recommended circuit for the MOC3023 optical isolator, which allows a Teensy 2.0 board to safely control the AC voltage.  Four copies of this circuit were built on the board.

This circuit requires the Teensy 2.0 to know the AC voltage timing, so it can trigger the Triac at the right moment.  Triggering early in the AC waveform causes the Triac to conduct near the full AC voltage for maximum brightness.  Triggering later reduces the brightness.

To get the AC timing, I built this special power supply onto the board.

The Teensy 2.0 receives pulses on pins 5 and 6 as the AC waveform cycles positive and negative.

One caveat is this approach depends on the AC voltage being a sine wave.  The AC voltage was one of the first questions I asked Andy, and he was told Burning Man would supply a true sine wave AC voltage.  When he got out there, it turned out the power was actually a “modified sine wave”, which really isn’t anything like a sine wave.  This circuit didn’t work well.  Fortunately, they were able to run the lighting from a small generator that produced a true sine wave.

With the AC timing arriving on pins 5 and 6, and 4 pins able to trigger Triacs, and 3 pins connected to analog voltages for changing speed, brightness and pattern, the only other major piece of this technology puzzle is the software.

In this code, loop() tracks the changes in the waveform on pins 5 & 6, and it fires the Triacs at their programmed times.  120 times per second (each AC half cycle), the recompute_levels() function runs, which reads the analog controls and changes the Triac time targets, which loop() uses to actually control the voltage outputs.

Here’s all the code:

void setup()
{
	pinMode(0, INPUT_PULLUP);	// unused
	pinMode(1, INPUT_PULLUP);	// unused
	pinMode(2, INPUT_PULLUP);	// unused
	pinMode(3, INPUT_PULLUP);	// unused
	pinMode(4, INPUT_PULLUP);	// unused
	pinMode(5, INPUT);		// Phase A
	pinMode(6, INPUT);		// Phase B
	pinMode(7, INPUT_PULLUP);	// unused
	pinMode(8, INPUT_PULLUP);	// unused
	pinMode(9, INPUT_PULLUP);	// unused
	pinMode(10, INPUT_PULLUP);	// unused
	digitalWrite(11, LOW);
	pinMode(11, OUTPUT);		// LED
	digitalWrite(12, HIGH);
	pinMode(12, OUTPUT);		// trigger4, low=trigger
	digitalWrite(13, HIGH);
	pinMode(13, OUTPUT);		// trigger3, low=trigger
	digitalWrite(14, HIGH);
	pinMode(14, OUTPUT);		// trigger2, low=trigger
	digitalWrite(15, HIGH);
	pinMode(15, OUTPUT);		// trigger1, low=trigger
	pinMode(16, INPUT_PULLUP);	// unused
	pinMode(17, INPUT_PULLUP);	// unused
	pinMode(18, INPUT_PULLUP);	// unused
	analogRead(19);			// pot #3
	analogRead(20);			// pot #2
	analogRead(21);			// pot #1
	pinMode(22, INPUT_PULLUP);	// unused
	pinMode(23, INPUT_PULLUP);	// unused
	pinMode(24, INPUT_PULLUP);	// unused
}


uint8_t pot1=0, pot2=0, pot3=0;
uint8_t level1=100, level2=128, level3=0, level4=250;


uint8_t phase_to_level(uint16_t phase)
{
	uint16_t amplitude;

	// 10923 = 32768 / 3
	//     0 to 10922 = increasing: 0 -> 32767
	// 10923 to 21845 = decreasing: 32767 -> 0
	// 21846 to 32768 = increasing: 0 -> 32767
	// 32769 to 43691 = decreasing: 32767 -> 0
	// 43692 to 65535 = resting: 0

	if (phase < 10923) {
		amplitude = phase * 3;
	} else if (phase < 21845) {
		phase = phase - 10923;
		phase = 10922 - phase;
		amplitude = phase * 3;
	} else if (phase < 32768) {
		phase = phase - 21846;
		amplitude = phase * 3;
	} else if (phase < 43691) {
		phase = phase - 32769;
		phase = 10922 - phase;
		amplitude = phase * 3;
	} else {
		amplitude = 0;
	}
	//amplitude = (phase < 32768) ? phase : 65535 - phase;
	amplitude >>= 6;  // range 0 to 511
	amplitude *= (pot2 + 84) / 6;  //
	amplitude += 6000 + pot2 * 8; // minimum brightness
	return (amplitude < 32768) ? amplitude >> 7 : 255;
}

void recompute_levels()
{
	static uint16_t phase=0;
	static uint8_t n=0;

	analog_update();
	//Serial.print("pot: ");
	//Serial.print(pot1);
	//Serial.print(", ");
	//Serial.print(pot2);
	//Serial.print(", ");
	//Serial.print(pot3);
	phase += (((uint16_t)pot1 * 83) >> 5) + 170;
	//Serial.print(", phase: ");
	//Serial.print(phase);
	if (pot3 < 128) {
		level1 = phase_to_level(phase);
		level2 = level1;
		level3 = phase_to_level(phase + pot3 * 52);
		level4 = level3;
	} else {
		uint16_t n = (pot3 - 127) * 26;
		level1 = phase_to_level(phase);
		level2 = phase_to_level(phase + 6604 - n);
		level3 = phase_to_level(phase + 6604);
		level4 = phase_to_level(phase + 6604 + n);
	}
	//Serial.print(", levels: ");
	//Serial.print(level1);
	//Serial.print(", ");
	//Serial.print(level2);
	//Serial.print(", ");
	//Serial.print(level3);
	//Serial.print(", ");
	//Serial.print(level4);
	//Serial.println();
}


void loop()
{
	uint8_t a, b, prev_a=0, prev_b=0, state=255, triggered=0;
	uint32_t usec, abegin, bbegin, alen, blen;
	uint16_t atrig1, atrig2, atrig3, atrig4;
	uint16_t btrig1, btrig2, btrig3, btrig4;
	bool any;

	while (1) {
		// read the phase voltage and keep track of AC waveform timing
		a = digitalRead(5);
		b = digitalRead(6);
		if (a && !prev_a) {
			// begin phase A
			usec = micros();
			if (state == 0) {
				state = 1;
				abegin = usec;
				triggered = 0;
				Serial.print("A");
				Serial.println(usec);
			} else if (state == 255) {
				state = 11;
				abegin = usec;
			} else {
				state = 255;
			}
		}
		if (!a && prev_a) {
			// end phase A
			usec = micros();
			if (state == 1) {
				state = 2;
				alen = usec - abegin;
				Serial.print("a");
				Serial.print(usec);
				Serial.print(",");
				Serial.println(alen);
				if (alen < 12000) {
					// compute trigger offsets for next A phase
					recompute_levels();
					atrig1 = level1 ? ((256 - level1) * alen) >> 8 : 30000;
					atrig2 = level2 ? ((256 - level2) * alen) >> 8 : 30000;
					atrig3 = level3 ? ((256 - level3) * alen) >> 8 : 30000;
					atrig4 = level4 ? ((256 - level4) * alen) >> 8 : 30000;
				} else {
					state = 255;
				}
			} else if (state == 11) {
				state = 12;
				alen = usec - abegin;
			} else {
				state = 255;
			}
		}
		if (b && !prev_b) {
			// begin phase B
			usec = micros();
			if (state == 2) {
				state = 3;
				bbegin = usec;
				triggered = 0;
				Serial.print("B");
				Serial.println(usec);
			} else if (state == 12) {
				state = 13;
				bbegin = usec;
			} else {
				state = 255;
			}
		}
		if (!b && prev_b) {
			// end phase B
			usec = micros();
			if (state == 3) {
				state = 0;
				blen = usec - bbegin;
				Serial.print("b");
				Serial.print(usec);
				Serial.print(",");
				Serial.println(blen);
				if (blen < 12000) {
					// compute trigger offsets for next B phase
					recompute_levels();
					btrig1 = level1 ? ((256 - level1) * blen) >> 8 : 30000;
					btrig2 = level2 ? ((256 - level2) * blen) >> 8 : 30000;
					btrig3 = level3 ? ((256 - level3) * blen) >> 8 : 30000;
					btrig4 = level4 ? ((256 - level4) * blen) >> 8 : 30000;
				} else {
					state = 255;
				}
			} else if (state == 13) {
				state = 0;
				blen = usec - bbegin;
			} else {
				state = 255;
			}
		}
		prev_a = a;
		prev_b = b;

		// trigger triacs at the right moments
		if (state == 1) {
			usec = micros();
			any = false;
			if (!(triggered & 1) && usec - abegin >= atrig1) {
				digitalWrite(15, LOW);
				triggered |= 1;
				any = true;
				//Serial.println("trig1(a)");
			}
			if (!(triggered & 2) && usec - abegin >= atrig2) {
				digitalWrite(14, LOW);
				triggered |= 2;
				any = true;
				//Serial.println("trig2(a)");
			}
			if (!(triggered & 4) && usec - abegin >= atrig3) {
				digitalWrite(13, LOW);
				triggered |= 4;
				any = true;
				//Serial.println("trig3(a)");
			}
			if (!(triggered & 8) && usec - abegin >= atrig4) {
				digitalWrite(12, LOW);
				triggered |= 8;
				any = true;
				//Serial.println("trig4(a)");
			}
			if (any) {
				delayMicroseconds(25);
				digitalWrite(15, HIGH);
				digitalWrite(14, HIGH);
				digitalWrite(13, HIGH);
				digitalWrite(12, HIGH);
			}
		} else if (state == 3) {
			usec = micros();
			any = false;
			if (!(triggered & 1) && usec - bbegin >= btrig1) {
				digitalWrite(15, LOW);
				triggered |= 1;
				any = true;
				//Serial.println("trig1(b)");
			}
			if (!(triggered & 2) && usec - bbegin >= btrig2) {
				digitalWrite(14, LOW);
				triggered |= 2;
				any = true;
				//Serial.println("trig2(b)");
			}
			if (!(triggered & 4) && usec - bbegin >= btrig3) {
				digitalWrite(13, LOW);
				triggered |= 4;
				any = true;
				//Serial.println("trig3(b)");
			}
			if (!(triggered & 8) && usec - bbegin >= btrig4) {
				digitalWrite(12, LOW);
				triggered |= 8;
				any = true;
				//Serial.println("trig4(b)");
			}
			if (any) {
				delayMicroseconds(25);
				digitalWrite(15, HIGH);
				digitalWrite(14, HIGH);
				digitalWrite(13, HIGH);
				digitalWrite(12, HIGH);
			}
		}
	}
}



#define ADMUX_POT1  0x60
#define ADMUX_POT2  0x61
#define ADMUX_POT3  0x64
void analog_update()
{
	static uint8_t count=0;

	switch (count) {
	  case 0: // start conversion on pot #1
		ADMUX = ADMUX_POT1;
		ADCSRA |= (1<<ADSC);
		count = 1;
		return;
	  case 1: // read conversion on pot #1
		if (ADCSRA & (1<<ADSC)) return;
		pot1 = ADCH;
		ADMUX = ADMUX_POT2;
		count = 2;
		return;

	  case 2: // start conversion on pot #2
		ADMUX = ADMUX_POT2;
		ADCSRA |= (1<<ADSC);
		count = 3;
		return;
	  case 3: // read conversion on pot #2
		if (ADCSRA & (1<<ADSC)) return;
		pot2 = ADCH;
		ADMUX = ADMUX_POT3;
		count = 4;
		return;

	  case 4: // start conversion on pot #3
		ADMUX = ADMUX_POT3;
		ADCSRA |= (1<<ADSC);
		count = 5;
		return;
	  case 5: // read conversion on pot #3
		if (ADCSRA & (1<<ADSC)) return;
		pot3 = ADCH;
		ADMUX = ADMUX_POT1;
		count = 0;
		return;
	  default:
		count = 0;
	}
}

 

This article was originally published on the DorkbotPDX website, on September 3, 2014.  In late 2018, DorkbotPDX removed its blog section.  An archive of the original article is still available on the Internet Archive.  I am republishing this article here, in the hope it may continue to be found and used by anyone interested in the Embrace art installation or any other project needing sequenced AC light dimming effects.

 

These comments where written on the old site:

 

From Brandon:

Once we determined that the AC source was modified sine, I knew there wasn’t anything I could do to help the situation easily in the middle of the desert 🙂
As someone who looked over the board and what not, nice job! Worked really well and looked amazing in operation (on the right power source).

Rev two, perhaps rectified DC drive of the incandescent lights to avoid the modified sine issue? So many folks use those types of inverters to cut costs on big artwork solar installations.

Cheers and thanks for contributing!

 

From Anonymous:

The embrace structure was prettty cool, I got a chance to explore it at Burninman this year. Wish I got to see it burn down, the videos looked amazing. I assume you guys removed the heart materials before that happened.

 

Better SPI Bus Design in 3 Steps

Most Arduino SPI tutorials show this simple but poor SPI bus design:

Better SPI bus design can prevent conflicts.  3 simple improvements are needed:

  1. Use pullup resistors on all chip select signals.

  2. Verify tri-state behavior on MISO: use a tri-state buffer chip if necessary.

  3. Protect bus access with SPI.beginTransaction(settings) and SPI.endTransaction().

Step 1: Pullup Resistors for Chip Select & Reset Signals

When multiple SPI devices are used, and especially when each is supported by its own library, pullup resistors are needed on the chip select pins.

Without a pullup resistor, the second device can “hear” and respond to the communication taking place on the first device, if that second device’s chip select pin is not pulled up.  This is easy to understand in hindsight, but it can be temendously confusing and frustrating to novice Arduino users who purchase shields or breakout boards without pullup resistors.  Each SPI device works when used alone, but they sometimes mysteriously fail when used together, only because both devices are hearing communication meant to initialize only the first device!

A simpe workaround for devices without pullup resistor involves adding code at the beginning of setup.

    void setup() {
      pinMode(4, OUTPUT);
      digitalWrite(4, HIGH);
      pinMode(10, OUTPUT);
      digitalWrite(10, HIGH);
      delay(1);
      // now it's safe to use SD.begin(4) and Ethernet.begin()
    }

Step 2: Proper MISO Tri-State Behavior

Most SPI chips will tri-state (effectively disconnect) their MISO pin when their chip select signal is high (inactive).

However, some chips do not have proper MISO tri-state behavior.  Fortunately, checking MISO tri-state is easy, especially when prototyping on a breadboard.  Just connect two 10K resistors to the MISO line, like this:

When all SPI chips are disabled, the MISO signal should “float” to approximately half the Vcc voltage.  If any device is still driving the MISO line, you’ll see a logic high (usually close to 3.3V or 5.0V) or logic low (close to zero volts).  This test is so easy, it should always be performed by designers of Arduino compatible products.

Arduino shields and breakout boards with poorly-behaved chips should always include a tri-state buffer.  Adafruit’s CC3000 breakout board is a good example:

Step 3: USB SPI Transactions in Software

Newer versions of Arduino’s SPI library support transactions.  Transactions give you 2 benefits:

  • Your SPI settings are used, even if other devices use different settings
  • Your device gains exclusive use of the SPI bus.  Others will not disturb you.

These improvements solve software conflicts, allowing multiple SPI devices to properly share the SPI bus.

A typical use of transactions looks like this:

    SPI.beginTransaction(SPISettings(14000000, MSBFIRST, SPI_MODE0));
    digitalWrite(chipSelectPin, LOW);
    SPI.transfer(mybyte1);
    SPI.transfer(mybyte2);
    digitalWrite(chipSelectPin, HIGH);
    SPI.endTransaction();

SPI.beginTransaction() takes a special SPISettings variable, which give the maximum clock speed, the data order, and clock polarity mode.  The speed is give as an ordinary number, expressing the maximum clock speed that device can use.  The SPI library will automatically select the fastest clock available which is equal or less than your number.  This allows your code to always use the best speed, even on board with different clock speeds.

If your code will ever call SPI library functions from within an interrupt (eg, from attachInterrupt), you must call SPI.usingInterrupt().  For example:

    SPI.begin();
    SPI.usingInterrupt(digitalPinToInterrupt(mypin));
    attachInterrupt(digitalPinToInterrupt(mypin), myFunction, LOW);

If you are developing a library that must be compatible with older versions of Arduino, which lack these SPI transaction functions, you can use SPI_HAS_TRANSACTION to check for the new version.  For example:

    #ifdef SPI_HAS_TRANSACTION
    SPI.beginTransaction(SPISettings(2000000, LSBFIRST, SPI_MODE1));
    #endif

Please Share and Use This Information

Many SPI-based products for Arduino do not work well together.  My hope is this information can help all makers of Arduino compatible devices to achieve much better compatibility.

Long-term, sharing of knowledge is needed.  Please share this information and ask makers of SPI devices and libraries to consider these suggestions.

This article may be shared and copied under the terms of the Creative Commons Attribution 4.0 International License.  Please, copy & share!  🙂

 

This article was originally published on the DorkbotPDX website, on November 24, 2014.  In late 2018, DorkbotPDX removed its blog section.  An archive of the original article is still available on the Internet Archive.  I am republishing this article here, in the hope it may continue to be found and used by everyone using SPI chips, and especially companies making SPI-based products for the Arduino community.

 

 

 

Control Voltage to 1.2V Analog Input Pin

Often the question is asked, what is the simplest way to get modular synth control voltage (CV) into an analog input pin?

This simple circuit using only 3 resistors and 1 capacitor converts the -5V to +5V CV signal range to the 0 to 1.2V ADC input range.

For Teensy 3.2, 3.5 and 3.6, you would use analogReference(INTERNAL) to configure for the 0 to 1.2V range.  Then you can read the CV signal at any time with analogRead().

While this circuit is the simplest and easiest way, it is not necessarily the best way.  The input impedance, or load placed on the CV signal, it 31K ohms, rather than the standard 100K impedance normally used for modular synthesis systems.  This forum discussion goes into the circuit’s limitations and suggests more advanced ways using opamps.

 

Originally this article was published on the DorkbotPDX website.  Since that time, the DorkbotPDX blog section has vanished.  I’m reposting it here, to preserve this info.  A copy of the original can also be found at the internet archive.