#+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