#+TITLE: Output devices
#+AUTHOR: Dylan Holmes
This week, I made a board that can generate sounds. I began the
process of learning how to play various notes, chords, and instruments
on it, and built a speaker of my own to replace the default speaker.
#+BEGIN_CENTER
#+HTML:
#+END_CENTER
** Designing audio-output board
This design took several iterations. Here's the final result:
Along the way, I made the following mistakes:
1. I used the wrong footprint for the mosfet --- we had small P-type
transistors and large N-type transistors. Because I used the wrong
footprint, I had to do a little hack --- I "deadbugged" by board by
attaching the mosfet indirectly to the board through wires that
were soldered directly on the too-small footprints.
2. I rushed designing my first board, and as a result had an extra
trace going to the TX pin on my six-pin connector; it shouldn't
have been there at all, and contributed to an rc=-1 error when I
tried to program the board.
3. I spent more time designing my second board to be streamlined and
compact. I moved the six-pin from to the right of the ATTiny to the
top (above pin 1). This allowed me to send the ground trace
underneath the microcontroller and the VCC trace around the
righthand side, avoiding my original setup which had the two traces
going around the board together, separated by some distance. (It's
preferrable to avoid putting VCC and ground close together because
proximity makes them easier to short.)
4. With my second board, I was also able to eliminate a 0-ohm resistor
that I previously required. (0-ohm resistors are packages in the
shape of an ordinary 1206 resistor, but with no (i.e. negligible)
resistance; you use them to make an overpass where one trace jumps
over another without them coliding. This is called 2.5D design.)
5. Because my second board was so compact, some traces were too close
together and weren't separated when they were milled. I was able to
use a rectangular razor blade to sever the traces, but it's risky
business. A better fix is to run the design rules checker (KiCad
has one; it's the ladybug icon. Eagle presumably has one, but I
wouldn't know.) which makes sure that your laid out traces meet the
design rules you specify.
6. (!) Another serious problem was that the FTDI 1x06 pin header
didn't have enough room on the board. Its metal feet were on the
board, but the rest of the component dangled off. Solder is not
load-bearing, so this would have unavoidably led to the FTDI header
snapping off. A design fix is to allow ample extra room near the
FTDI header. I also routed the VCC line to go under the plastic
part so that wouldn't be so close to the feet as to cause a short.
7. Deciding that compactness wasn't the most important virtue, I made
a wider, slightly more spread out circuit. I also added an LED so I
could check whether the board was alive or not.
8. Tool hygiene was a big factor as well. I believe what happened on
one board was that I milled with an end mill that I didn't notice
had been slightly broken (which led to burrs which I removed by
scraping with the long edge of a razor blade.) I also believe I
soldered with a dirty soldering iron. The sponge really is
indispensable. After struggling with rc=-1 instead of signs of
life from that board, I made a replacement. I checked the end mill
in great detail before using it, and thoroughly scrubbed the
soldering iron before even turning it on, and then again
afterwards. The result was patently better.
9. (!) At some point in this design process, I realized that although
I only needed two out of four pins of the 2x02 pin connector for
the speaker [see next section], by wiring up all four pins
redundantly (in an A/B//B/A pattern) I would make a connector where
I didn't have to care about the orientation of the speaker I was
plugging in; it would work from many orientations.
10. (!) I did not take into account the power limitations of the
speaker. The off-the-shelf speakers in the inventory are rated for
8Ω of resistance and 0.25 watts of power. I was using USB for
power, so around 5V, which meant that the speaker line was drawing
5V*5V / 8 Ω, which is more than 3 watts. Which is more than 12
times the preferred amount of power consumption. Oops.
My short-term fix was to splice a breadboard-style resistor (two
wires with a color-coded sausage shape in the middle) onto the
speaker wire itself. I cut one of the wires leading to the speaker
(see below for speaker design). I stripped insulation off each of
the cut ends. Using a soldering iron, I mashed one exposed end of
the wire and one wire of the resistor against each other and
against the heat-proof ceramic tile underneath. Using a second
hand, I applied a heap of solder at the junction. By pulling away
carefully (this is an awkward, failure-riddled process), you can
get the solder to cool before the two components slip out of the
still-liquid solder. Repeat for the other side.
My long term fix was to add a resistor to the board itself, on the
trace that's on the speaker side of the mosfet. According to my
math, 100Ω (plus the 8Ω from the speaker itself) would basically
meet the 0.25 watt limit; I used 150Ω for good measure, and it
worked fine. Turns out our eecs lab carries a 150Ω resistor, so I
only needed to use one of the two resistor slots on my board. The
other one I made a 0Ω resistor (I still wanted the two slots
because I imagined needing to swap out various resistances. Turns
out 150Ω worked fine though.)
** Assembling a speaker
*** From the inventory
There are speakers available in the inventory. They are basically just
coils of copper wire atop a small magnet, and connected to a thin
plastic membrane---that's it! It turns out that's all the complexity
you need for a speaker. When current passes through the coil, the
magnet or coil is moved around. Passing precisely-timed high-frequency
bursts of current creates bursts of movment that result in sound
waves.
The standalone speaker would attach somewhere to the board. I needed a
connecting cable and a place to plug it in on the board. On the board,
I used two pins of a 2x02 pin connector (4 pin connector). On the
board design, I wired the four pins together so that two were low and
went to GND [iirc] and the other two were high and went to a pin of
the mosfet.
As for the cable, you can make your own 2x02 ribbon cable. You take
ribbon cable and strip off all but four wires. There are toothed 2x02
cable connectors that look like open-mouthed jaws. You thread the end
of the ribbon cable into the jaws and take careful note about which
teeth will pierce each wire. This tells you which wire will be fed
into which hole of the connector.
It's hard to tell later which hole goes to which wire, so I do
recommend drawing a very careful reference picture in advance. Once
the ribbon is threaded through, you use a vice (you need super-human
pressure) to permanently clamp the jaws shut on the ribbon, so the
teeth can pierce into the wires forming a connection.
There's a connector on one end. The other end goes to the speaker. By
peeling, separate any* two of the four wires from the rest (we only
need two of them; one for high and one for low and order doesn't
matter). [*]You can pick almost any two, but you must refer to your
diagram for the connector and your diagram for your circuit to make
sure you're picking two wires that, when the connector is plugged in,
are connected to different traces on your board.
Use a wire stripper to remove the rubbery insulation from around the
wires, exposing a spray of metallic interior wire. I discovered that I
needed to remove a much shorter length of insulation that I initially
thought. If you remove too much such that the following soldering
stage is too difficult, you can always trim the protruding metal
strands to be shorter.
I have little advice for this next part. Basically, I set up a
workspace on the relatively heat-proof ceramic soldering tiles. I set
down the standalone speaker and fixtured it in place with tape.
*** Build your own
I built my own speaker. Note that volume-wise, it was worse than the
off-the-shelf type. But part of the problem was that it was such a
hefty object (I was going for lots of copper wire and a large magnet),
that it probably needed more current than I could supply via
USB. Changing power supplies will be my next step.
A speaker, as noted above, is a coil of wire, optionally wound around
a magnet for stronger effect, affixed to a membrane so that when the
coil goes back and forth, so does the membrane.
We actually had a special setup for creating coils of wire. We had a
spool for winding copper wire around, as well as a wire-winding
machine. You slide the speaker onto an axel, then tighten it in place,
tape one end of wire to your spool (leave enough length so that when
you're done, the starting bit of thread is dangling off; leave length
on the terminating end when you're finished as well. You want a coil
of wire with two long dangling ends to connect to things), then turn a
crank to wind wire around it. The device even counts the number of
turns! (I saw that others had done 250; I did around 300.)
There's a delicate balance regarding size and force, as I mentioned
above. Thin-gauge wire is tiny, which is good because small mass is
good. But it is also brittle and unable to tolerate much
current. Large magnets increase the amount of electromagnetism in
play, but are also heavy.
My wires were very thin and rather brittle. The hard part was getting
them to interface with the four-pin connector, and also ensuring that
they did not snap in the process. They did snap in the process---at
one point, they snapped off one end so that I didn't have the
requisite two wires exposed and I had to start over and wind it again.
My basic solution was as follows. First, what's obvious in retrospect
but surprised me at first, is that the shiny copper wire is covered in
a thin layer of insulation---overlapping wires don't speak to each
other. So if you attempt to solder the wires directly, they'll appear
dead (and a multimeter will confirm that no current is flowing from
[the outside surface of] one end of the wire to [the outside surface
of] the other.) You have to remove the external surface. I imagined
using acetone, but we didn't have any at hand --- perhaps that would
have worked. Fine sandpaper worked passably well.
It's hard to tell if you've sanded properly. There's only a subtle
visual difference between sanded and unsanded wire. The exposed copper
wire is copper-colored like its covering, but it appears perhaps
whiter and shinier. You only really need to expose the ends. The wire
snapped a few times when I was sanding, even carefully, so I encourage
gentleness and sanding only the smallest end that you need; this way
if it snaps, you still have more length to use up.
I soldered the copper wire to the four-pin ribbon cable just like I
did for the standalone connector. This felt janky and fragile --- and
it was. The thin wire snapped several times. A sort-of solution was to
tape the copper strands to the surface of a small plastic cup I was
using as a membrane. This helped protect the copper from being jostled
by the environment.
** Composing sound software
I taught the speaker how to play many different sorts of sounds and
only wish I had had time for more.
Neil's hello-speaker example was a good start.
#+BEGIN_QUOTE
(!) *Important*. Please note that, as of the time of this writing,
Neil's hello-speaker example uses the internal timers/pwm of the
attiny microcontroller in order to generate sound. As such, /the
device itself constrains sound output to a particular, pre-determined
pin./ You can't use Neil's code to produce output at an arbitrary pin,
so you should design your board so that the mosfet is connected to the
right pin.
Although you can change the =MOSFET_PIN= variable in the code, this
can't change everything you'd expect: The code does two things. First,
it sets the mosfet pin to be an output pin (changing =MOSFET_PIN=
changes this), then it sets up a counter which produces sound waves on
the OC1A (overflow) pin. Check your datasheet to see which pin is the
OC1A pin. The OC1A pin and the =MOSFET_PIN= should be the same pin in
order for the code to work as intended.
#+END_QUOTE
I actually didn't realize this at first. I had connected it to pin
OC1B on my tiny44 (not Neil's tiny45) because I knew that pin could be
used for pwm output and I didn't realize it mattered exactly which
one. I had to do find-and-replace throughout the code to replace many
instances of As with Bs. Read about counters elsewhere for more
details.
I was able to test the speaker code on an LED I had included for just
such debugging purposes. (It's a good idea.) Amusingly, it produces
the visual equivalent of the hello-speaker result: flickers of light
at various levels of brightness instead of flickers of sound. My LED,
as I eventually found out, was coincidentally connected to the OC1A
pin, which meant that hello speaker worked out-of-the-box exactly as I
expected it to, even though I had the wrong mental model of how it
worked. After some serious confusion, I figured out the OC1A vs OC1B
thing.
Here's code for doing various things, assuming your mosfet's gate is
connected to pin OC1B (check your datasheet) :
*Play Neil's Hello Speaker example*
#+BEGIN_SRC
// For brevity, I'm leaving out the various other macros defined in
// Neil's hello speaker code. Put them at the beginning of your
// document and call this function from your main function.
// This code only works if the gate of your MOSFET is connected
// to the OC1A pin which is also pin PB2 on ATTiny44s.
// Neil's original code is for the OC1B pin on Attiny45s.
#define MOSFET_pin (1 << PB2)
#define MOSFET_port PORTB
#define MOSFET_direction DDRB
int minimal_speaker(void) {
//
// main
//
static uint16_t frequency,period,cycle,cycles,delays;
static uint8_t amplitude;
//
// set clock divider to /1
//
CLKPR = (1 << CLKPCE);
CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0);
//
//
//
TCCR0A = ((1 << COM0A0) | (1 << COM0A1) | (1 << WGM01) | (1 << WGM00)); // set OC0B on compare match and set fast PWM mode, 0xFF TOP
TCCR0B = (1 << CS00); // set timer 0 prescalar to 1
//
// initialize MOSFET pin
//
clear(MOSFET_port, MOSFET_pin);
output(MOSFET_direction, MOSFET_pin);
clear(MOSFET_port, MOSFET_pin);
output(DDRA, (1 << PA7));
set(PORTA, (1 << PA7));
while (1) {
//
// loop over amplitudes
//
for (amplitude = amax; amplitude >= amin; amplitude -= astep) {
//
// loop over periods
//
for (period = pmax; period >= pmin; period *= pstep) {
//
// loop over square wave cycles
//
cycles = duration/period;
for (cycle = 0; cycle < cycles; ++cycle) {
//
// set PWM current on
//
OCR0A = amplitude;
// loop over delay
//
for (delays = 0; delays < period; ++delays)
cycle_delay;
//
// set PWM current off
//
OCR0A = off;
//
// loop over delay
//
for (delays = 0; delays < period; ++delays)
cycle_delay;
}
}
}
}
}
#+END_SRC
*Play a single square wave tone*
#+BEGIN_SRC
int atune_simpler(void) {
// initialize MOSFET pin
//
clear(MOSFET_port, MOSFET_pin);
output(MOSFET_direction, MOSFET_pin);
clear(MOSFET_port, MOSFET_pin);
// SET clocks same as usual
CLKPR = (1 << CLKPCE);
CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0);
TCCR0A = ((1 << COM0A0) | (1 << COM0A1) | (1 << WGM01) | (1 << WGM00)); // set OC0B on compare match and set fast PWM mode, 0xFF TOP
TCCR0B = (1 << CS00); // set timer 0 prescalar to 1
int j;
int pin_on = 0;
while(1) {
OCR0A = 255 - pin_on * 55;
// Changing this maximum value 110 changes the pitch
for(j=0;j<110;j+=1) {
cycle_delay;
}
pin_on = 1-pin_on;
}
}
#+END_SRC
*Play custom waveforms using a wavetable (here: triangle wave)*
#+BEGIN_SRC
int wavetable_simpler(void) {
// initialize MOSFET pin
//
clear(MOSFET_port, MOSFET_pin);
output(MOSFET_direction, MOSFET_pin);
clear(MOSFET_port, MOSFET_pin);
// SET clocks
CLKPR = (1 << CLKPCE);
CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0);
//
//
//
TCCR0A = ((1 << COM0A0) | (1 << COM0A1) | (1 << WGM01) | (1 << WGM00)); // set OC0B on compare match and set fast PWM mode, 0xFF TOP
TCCR0B = (1 << CS00); // set timer 0 prescalar to 1
int j;
int pin_on = 0;
int wavetable_size = 1024;
// This is a triangle wave; its amplitude goes linearly from 0 to
// 255 then repeats back at 0.
// !! Note: You must include #include to use
// the extended PROGMEM (program memory) of your device. It's
// needed here because otherwise there isn't enough space in
// run-time memory to store it all in a variable. PROGMEM is
// actually stored in a different physical place on the chip.
static const uint8_t wavetable[] PROGMEM = {0, 0, 0, 0, 1, 1, 1, 1,
2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7,
7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12,
12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16,
16, 16, 17, 17, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20,
20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24,
25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 28, 29,
29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 31, 32, 32, 32, 32, 33, 33,
33, 33, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 36, 37, 37, 37,
37, 38, 38, 38, 38, 39, 39, 39, 39, 40, 40, 40, 40, 41, 41, 41, 41,
42, 42, 42, 42, 43, 43, 43, 43, 44, 44, 44, 44, 45, 45, 45, 45, 46,
46, 46, 46, 47, 47, 47, 47, 48, 48, 48, 48, 49, 49, 49, 49, 50, 50,
50, 50, 51, 51, 51, 51, 52, 52, 52, 52, 53, 53, 53, 53, 54, 54, 54,
54, 55, 55, 55, 55, 56, 56, 56, 56, 57, 57, 57, 57, 58, 58, 58, 58,
59, 59, 59, 59, 60, 60, 60, 60, 61, 61, 61, 61, 62, 62, 62, 62, 63,
63, 63, 63, 64, 64, 64, 64, 65, 65, 65, 65, 66, 66, 66, 66, 67, 67,
67, 67, 68, 68, 68, 68, 69, 69, 69, 69, 70, 70, 70, 70, 71, 71, 71,
71, 72, 72, 72, 72, 73, 73, 73, 73, 74, 74, 74, 74, 75, 75, 75, 75,
76, 76, 76, 76, 77, 77, 77, 77, 78, 78, 78, 78, 79, 79, 79, 79, 80,
80, 80, 80, 81, 81, 81, 81, 82, 82, 82, 82, 83, 83, 83, 83, 84, 84,
84, 84, 85, 85, 85, 85, 86, 86, 86, 86, 87, 87, 87, 87, 88, 88, 88,
88, 89, 89, 89, 89, 90, 90, 90, 90, 91, 91, 91, 91, 92, 92, 92, 92,
93, 93, 93, 93, 94, 94, 94, 94, 95, 95, 95, 95, 96, 96, 96, 96, 97,
97, 97, 97, 98, 98, 98, 98, 99, 99, 99, 99, 100, 100, 100, 100,
101, 101, 101, 101, 102, 102, 102, 102, 103, 103, 103, 103, 104,
104, 104, 104, 105, 105, 105, 105, 106, 106, 106, 106, 107, 107,
107, 107, 108, 108, 108, 108, 109, 109, 109, 109, 110, 110, 110,
110, 111, 111, 111, 111, 112, 112, 112, 112, 113, 113, 113, 113,
114, 114, 114, 114, 115, 115, 115, 115, 116, 116, 116, 116, 117,
117, 117, 117, 118, 118, 118, 118, 119, 119, 119, 119, 120, 120,
120, 120, 121, 121, 121, 121, 122, 122, 122, 122, 123, 123, 123,
123, 124, 124, 124, 124, 125, 125, 125, 125, 126, 126, 126, 126,
127, 127, 127, 127, 128, 128, 128, 128, 129, 129, 129, 129, 130,
130, 130, 130, 131, 131, 131, 131, 132, 132, 132, 132, 133, 133,
133, 133, 134, 134, 134, 134, 135, 135, 135, 135, 136, 136, 136,
136, 137, 137, 137, 137, 138, 138, 138, 138, 139, 139, 139, 139,
140, 140, 140, 140, 141, 141, 141, 141, 142, 142, 142, 142, 143,
143, 143, 143, 144, 144, 144, 144, 145, 145, 145, 145, 146, 146,
146, 146, 147, 147, 147, 147, 148, 148, 148, 148, 149, 149, 149,
149, 150, 150, 150, 150, 151, 151, 151, 151, 152, 152, 152, 152,
153, 153, 153, 153, 154, 154, 154, 154, 155, 155, 155, 155, 156,
156, 156, 156, 157, 157, 157, 157, 158, 158, 158, 158, 159, 159,
159, 159, 160, 160, 160, 160, 161, 161, 161, 161, 162, 162, 162,
162, 163, 163, 163, 163, 164, 164, 164, 164, 165, 165, 165, 165,
166, 166, 166, 166, 167, 167, 167, 167, 168, 168, 168, 168, 169,
169, 169, 169, 170, 170, 170, 170, 171, 171, 171, 171, 172, 172,
172, 172, 173, 173, 173, 173, 174, 174, 174, 174, 175, 175, 175,
175, 176, 176, 176, 176, 177, 177, 177, 177, 178, 178, 178, 178,
179, 179, 179, 179, 180, 180, 180, 180, 181, 181, 181, 181, 182,
182, 182, 182, 183, 183, 183, 183, 184, 184, 184, 184, 185, 185,
185, 185, 186, 186, 186, 186, 187, 187, 187, 187, 188, 188, 188,
188, 189, 189, 189, 189, 190, 190, 190, 190, 191, 191, 191, 191,
192, 192, 192, 192, 193, 193, 193, 193, 194, 194, 194, 194, 195,
195, 195, 195, 196, 196, 196, 196, 197, 197, 197, 197, 198, 198,
198, 198, 199, 199, 199, 199, 200, 200, 200, 200, 201, 201, 201,
201, 202, 202, 202, 202, 203, 203, 203, 203, 204, 204, 204, 204,
205, 205, 205, 205, 206, 206, 206, 206, 207, 207, 207, 207, 208,
208, 208, 208, 209, 209, 209, 209, 210, 210, 210, 210, 211, 211,
211, 211, 212, 212, 212, 212, 213, 213, 213, 213, 214, 214, 214,
214, 215, 215, 215, 215, 216, 216, 216, 216, 217, 217, 217, 217,
218, 218, 218, 218, 219, 219, 219, 219, 220, 220, 220, 220, 221,
221, 221, 221, 222, 222, 222, 222, 223, 223, 223, 223, 224, 224,
224, 224, 225, 225, 225, 225, 226, 226, 226, 226, 227, 227, 227,
227, 228, 228, 228, 228, 229, 229, 229, 229, 230, 230, 230, 230,
231, 231, 231, 231, 232, 232, 232, 232, 233, 233, 233, 233, 234,
234, 234, 234, 235, 235, 235, 235, 236, 236, 236, 236, 237, 237,
237, 237, 238, 238, 238, 238, 239, 239, 239, 239, 240, 240, 240,
240, 241, 241, 241, 241, 242, 242, 242, 242, 243, 243, 243, 243,
244, 244, 244, 244, 245, 245, 245, 245, 246, 246, 246, 246, 247,
247, 247, 247, 248, 248, 248, 248, 249, 249, 249, 249, 250, 250,
250, 250, 251, 251, 251, 251, 252, 252, 252, 252, 253, 253, 253,
253, 254, 254, 254, 254, 255, 255, 255, 255};
int wavetable_index = 0;
int m;
j=0;
while(1) {
j = (++j)%110;
if(j == 0) {pin_on = 1-pin_on;}
wavetable_index = (wavetable_index+3)%wavetable_size;
//OCR0A = 255 - pin_on * 55;
OCR0A = 255 - wavetable[wavetable_index] * 20;
cycle_delay;
}
}
#+END_SRC
*Play a chord*
#+BEGIN_SRC
int parametric_chord(void) {
static uint16_t i,period,cycle,cycles,delays;
static uint8_t amplitude;
static uint16_t steps_per_second = 100000; // 100 000
int k = 4; // a global multiplier to change all note frequencies
// These are the frequencies
uint16_t freqs[] = {440, 466, 493, 523, 554, 587, 622, 659, 698, 740, 784, 830, 880, 350};
int num_notes = 3;
uint16_t notes[] = {0, 4, 7};
//uint16_t notes[] = {0, 12, 8};// {0, 4, 7};
uint16_t periods[num_notes];
uint16_t periodT = 1;
for(i=0;i> 0) ^ (lfsr >> 2) ^ (lfsr >> 3) ^ (lfsr >> 5) ) & 1;
lfsr = (lfsr >> 1) | (bit << 15);
++period;
amplitude = (lfsr & 0xff)
OCR0A = amplitude;
cycle_delay;
}
}
#+END_SRC
*Wavetables through calculation instead of lookup*
Coming soon.
** Debugging using the DC power supply