What are we talking about here?
A while ago, I was afflicted with the desire to build something with an Arduino board--not to solve any particular problem, but merely as a learning experience.
What's an Arduino?
It's a low-cost, single-board computer for hobbyists, based on an 8-bit microcontroller chip (the ATmega328). The ATmega328 has plenty of digital IO pins (some with pulse width modulation) and 6 analog-to-digital inputs. It even comes with a decent development environment.
The Arduino Uno looks like this:
I got an Arduino Uno for Christmas, along with a microphone/preamp board (sparkfun BOB-09964), an Arduino breadboard (a "protoshield", sparkfun DEV-07914), a breadboard power supply (sparkfun 00114) and some white LEDs.
Then I let the components age on a bookshelf for over a year.
Because you have kids, right?
No, don't be ridiculous! Electronics work better after aging.
By the time I finally dusted off the Arduino board, my ideas had acquired more solidity. I wanted to build something electro-mechanical.
I played around with an old solenoid from a broken lawn sprinkler valve. Energizing the solenoid and watching the plunger shoot out gave me an idea: I could have the Arduino listen to the microphone for impulses (knocks, claps), and respond by having the solenoid violently strike a hard surface. A Completely useless project. I call it Knock*Knock.
Why not build something completely useful?
I wanted to leave myself the option of approaching the Sandhill Road VCs or Y Combinator. I'd pitch it as "Knockr: it's like mobile chat for paperweight designers. We leverage the zero utility of twitter with the boxy pomposity of Dwell magazine."
Holy cow, that must be worth billion$!
Well, maybe just one billion.
First I tackled the solenoid output:
I purchased a beefier solenoid (trossen robotics, medium solenoid). Typically, solenoids are activated by relays--digitally-controlled on-off switches. However, I wanted to perhaps use the Arduino's pulse-width modulation (PWM) output to modulate the solenoid plunger's force (p.s. this never happened). That means a relay wouldn't suffice; I needed a circuit to drive the solenoid with variable current. Luckily, my friend at work just happened to have some appropriate chips: TI SN754410 Quadruple Half-H Driver, which are specifically designed to provide current to motors and solenoids.
Here's my circuit:
What's with the diode?
A solenoid is basically an inductor. When you suddenly cut the current to an inductor, the inductor's collapsing magnetic field draws additional current from whatever is connected. The diode is a flyback diode, which protects the rest of the circuit from the resulting voltage spike.
Recall the scene in Catch-22 where the soldier in white--completely encased in plaster--is hooked up to an IV jar and a urine collection jar, and the nurse periodically switches the jars. It's exactly like that, only with electrons instead of urine.
That makes perfect sense!
I know! Then I wired everything up on a breadboard, enabled current by hooking the chip's input pin to a battery, and pow! The solenoid clacked on command.
What about the inputs?
I wanted to have the system listen for impulse signals on the microphone. The electret microphone board I purchased from Sparkfun included an amplifier, so I merely needed to supply a 5V bias voltage and ground, then read the voltage wiggles off the signal line.
To test the microphone, I powered it with a battery, then clipped a 3.5mm headphone jack to the signal line and plugged it into my laptop. I fretted that I my laptop would puke over the input signal's DC bias. Just stick it in the hole, my friend recommended; that's what I did, and it worked fine.
Here's where the Arduino shines: both 5V and ground are supplied by the Arduino, as well as analog-to-digital conversion. Wiring an amplified microphone to the Arduino required no extra components. The Arduino software framework includes commands for reading A-to-D values.
I also wired up a Big Happy Button as an alternate input for testing the software.
How does the Arduino sample the microphone?
The Arduino expects the microphone input to have a reference voltage bias of 2.5 V. The A-to-D will measure voltage inputs of 0..5 V with 10-bit accuracy. So the A-to-D maps readings of 0..5 V to [0..1024], where 512 represents 2.5 V.
What if the bias voltage is just a little off?
Then the samples are a little off as well, and you get a DC bias in your readings. If I were concerned about making accurate signal measurements, I'd have to add analog circuitry to adjust the bias voltage to exactly 2.5 V. Fortunately, Knock*Knock is merely looking for impulses, so signal fidelity isn't important. I merely needed to re-map the A-to-D readings from 0..1024 to -512..-512 before applying my detection algorithm.
How did you get rid of the DC bias?
With a high-pass IIR filter in software:
y[n] = alpha * (y[n-1) + x[n] - x[n-1])
Why not just attach the microphone to a digital input pin and avoid all that sampling and filtering stuff?
Yeah, my friend mentioned that. To be honest, I didn't think of it. Plus my way just felt cooler.
Let's talk about the software...
My software needed to
- monitor the inputs sources (microphone, button),
- process the input signals and decide whether a legitimate event had occurred, and
- react appropriately by energizing the solenoid.
Fortunately, the Arduino software environment includes a framework and library functions (Wiring) which make programming the Arduino gratifyingly simple. Software and CPU setup go in a function called setup(), steady-state actions go in a function called loop(), etc. I implemented a clock-based interrupt service routine (ISR) with the Timer1 library.
Here's a data flow diagram:
What's going on in the Interrupt Service Routine (ISR)?
Most processors get crotchety if you try to do too much in the ISR, and the Arduino is no exception. So I tried to do as little as possible in the ISR:
- The on-chip timer invokes the ISR every 500 usec.
Every M interrupts, the ISR sets a handle_input flag telling the main loop to process the inputs.
Every M interrupts, the ISR sets a handle_output flag telling the main loop to process the outputs.
- Every interrupt, the ISR updates the PWM output pins, which are hooked up to an LED (input confirmation, or IACK) and the solenoid. Using the PWM output with the LEDs let me fade the lights smoothly.
- Finally, the ISR processes the microphone input.
Processing the microphone involves reading the digitized value off the input pin, applying an IIR high-pass filter (to remove DC), then adding the absolute value of the filtered value to an accumulator. The ISR runs every 500 usec, so the microphone sample rate is 2KHz.
What's happening in the main loop?
The main loop checks if the handle_input flag was set by the ISR. If so, then the software applies heuristics to check the microphone sample accumulator (a simple threshold) or button (debounce) for an input event; if an event occurred, then the software adds the event to an input queue. If no event has occurred for a longish time, say a second, then it sets the timeout_flag.
If the timeout_flag is set, then the sequencer tries to match the contents of the input queue against a known sequence; in this case, the sequence is the first five beats of "shave and a haircut...two bits." It does this by converting the input queue into a sequence of delta time periods, i.e. milliseconds between input events, and computing the correlation coefficient against the known sequence. In order to speed things up, I precomputed the statistics (mean, variance) of the a-priori sequence during initialization.
If the correlation coefficient exceeds a threshold, then the sequencer responds by writing the a command sequence to the output queue (e.g. "two bits" = RAMP_SOLENOID, WAIT, RAMP_SOLENOID). Otherwise, the sequencer writes the input sequence to the output queue (so Knock*Knock tries to mimic the input knocks).
Finally, if the handle_output flag was set by the ISR, then the software updates its output state machine. The state machine reads commands from an output queue, e.g. RAMP_SOLENOID or WAIT. It performs the command, then processes the next command or goes quiet if the queue is empty.
When the solenoid knocks, doesn't the microphone hear the knock as an impulse, resulting in events in the output queue, resulting in a solenoid knock ad infinitum?
Yes, you get an annoying feedback loop. The solution: when the output queue is non-empty, the microphone input is disabled.
All of the source code can be found on github.
And then you were done?
No. At this point, I had a bunch of wiggly wires and components stuck into a breadboard. I rebuilt the hardware, soldering the circuits onto a protoboard from Radioshack. I actually enjoy soldering, but working with protoboard is horrible--even for a relatively uncomplicated design. If I were to do this again, I would try soldering on just one side of the protoboard (the friend's suggestion), or investigate going straight to PCB.
I bought a cigar box from Edward's Pipe and Tobacco in Los Altos, circuit board standoffs (sparkfun, part 11796), and some nice panel LEDs (digikey, part 492-1546-ND). And then I stuffed the entire mess into the cigar box, lashing down the wiggly bits with hot-pink duct tape.
Finally, I learned how to use iMovie to produce a very classy video. Gotta love those transitions!
And that's all about KNOCK*KNOCK.