noisEE, Part 2: Hardware
Previously, noisEE Part 1: Software.
Assumes some hardware knowledge. Gets into the nitty gritty details. Mistakes were made.
Why hardware?
- Part of it was a desire to get back to my EE-influenced roots and actually make something that didn't start and end with "and then we write a bunch of software".
- The other part was that it seems like furiously running a stubby microcontroller to do Digital Signal Processing (DSP) tasks seems silly. When in the world would I have time to read a simple Analog to Digital conversion (ADC), when I have 44 thousand samples to process this second, with a non-significant number of operations to finish? Hobbyist ATMega clocks top out at around 16MHz, for an optimistic around half a thousand cycles per audio sample. Factor in non-8 bit operations and needing to run the ADCs, and that bound starts to look a bit tight, even when we're doing something simple like the 3-pinking filter we discussed. I was hoping to do more specifically EE things, not optimizing assembly.
Plus, I was super into imagining having not just 20kHz bandwidth, but 1MHz! Wow, such bandwidth! Why would you want limited digital audio? Just ignore that almost all intents and purposes 44.1kHz is good enough, and higher frequencies are on par with quality and more expensive at best.
So let's do an analog circuit: filters are literally the first thing EE students cover, so this should be easy[1].
Basic idea: Active Low Pass Filters
Like I mentioned before, the basic idea is that we have low pass filters implemented with a capacitor and resistor, like so:
However, we want to both amplify and allow us to ignore the other filter stages we might be using as input and output (remember, overengineering to overcome ignorance). For an extreme example, if the load is equivalent to some small resistance (say, 33ohm headphones), the input source might not be able to deliver enough current, and the signal will look distorted (quieter). Instead of using this simple example with just passive components, we can add an amplifier:
A quick tutorial on ideal opamps (operational amplifiers): the two wires coming into the opamp on the left are the inputs. The opamp amplifies the difference between the signals by some large amount, say, some hundred times. You would think that imprecision of the amplification is a Very Bad Thing, but in this case we use feedback to overcome the imprecision: by bending back the output from the triangle tip back into the input, we get negative feedback and a stable output voltage. Ideally, we model the inputs as sinking/sourcing no current at all[2], so we can choose some resistors to control the amount of influence the output has on the inputs, so we can get non-1x/unity gains[3]. Then, the capacitor is there to provide the low pass filter part. There's a lot of stuff out there about impedance (not just resistance!) that explains how this all works.
(I should also note that by this point we're completely ignoring the phase of the filter: filters will shift the time delay of a signal. For an extreme but simplified example, consider a signal with a half cycle phase offset: if we then add it back to the original signal, we might naively expect it to end up with a signal with twice the magnitude, but instead the phase shifted signal would cancel each other out and we would get silence instead. I just didn't feel like sitting around for a long time trying to verify that the filter phase changes wouldn't do anything bad before actually making the circuit, so I ignored it.)
What we really care about is how to make this work: remember, the filters we defined in the last post will change in loudness, but not change their cutoff frequency. Now, the formulas for gain (G) and cutoff frequency (f_c) in an active low pass filter are:
Let's assume that we have a voltage controlled resistor (we'll get to this later), so we can change the resistance values in the gain formula. We can't have R_2 be the voltage controlled resistor to change G, since changing R_2 will also change f_c. However, changing R_1 isn't super desirable, either, because then we have an inverted relationship between R_1 and G, so we lose precise gain control exactly when it becomes noticeable, when it's loudest.
We could live with really sloppy gain control, or we could shell out for another set of opamps and set up another gain stage after the 1st, which looks like:
It has the same gain formula, but this time R_2 (R_f) isn't affecting anything else, so we can fiddle with that and get linear loudness control on our filter output.
The downside to this is that we're paying for twice the opamps, and if you end up getting fancy-ass opamps, it adds up. Oh well.
So we can create a low pass filter, and adjust how loud it is. We can do this 4 times, choosing R_2 and C on the low-pass filter to get the f_c we want and then choose R_f and R_{in} on the amplification stage so we get as the maximum resolution out of our voltage controlled resistors, which *might* have a preset upper bound (spoiler: they will).
We have 4 filter branches, but we need to combine them. We can't just tie the wires together, since that can turn into a giant mess that messes up the feedback loops. Instead, we can use an opamp as a summing amplifier:
which adds together all the signals into one output.
Which opamp should we be using? I probably would've ended up using a cheap-o general opamp, but TangentSoft's CMoy guide has plenty of beginner materials on opamp choice. The author went through a number of opamps and rated them on the sound quality they produced, as well as important electrical characteristics like how much current the opamp could put out before it started distorting the output. In the end, I went with the OPA227 across all stages, using the 4 amp packages (OPA4227) where needed. Thinking back, it probably wasn't necessary to have all the amps be able to source this much current, but I knew I was getting "good enough" opamps. (Again, overengineering to overcome my lack of knowledge)
Hardware white noise
There are a few ways to generate white noise. Unlike the digital white noise sources from the first part, we can get pretty good "true" noise sources from hardware.
Zener diodes
Diodes are… a big-to-layperson semiconductor topic that will take forever to explain, so I'm going to just summarize them as components that allow current in one direction, but not the other. Zener diodes do the same thing, but if you run them in the other direction (reverse bias), they will fail at a specific voltage, but not catastrophically[4]; once you put a certain large enough voltage across them in reverse, then they will let as much current as possible pass through while dropping the same voltage.
This behavior is useful if you need to have a certain voltage reference for a chip, say, 6.1V. You can find a zener diode rated for 6.1V, put it in backwards, and know that the voltage on top of the zener is pretty close to 6.1V.
However, it isn't precisely 6.1V. Since zeners incrementally let electrons pass when they get enough energy (avalanche breakdown), the voltage across the zener is noisy, and that noise is close to white noise. Well, to be precise there's a pink noise component for low voltage zeners, but it wasn't detectable by me, even when using low voltage zeners (say, 2.7V vs 6.8V). The only other consideration is that higher voltage zeners produce a bigger noise signal to compete with other noise sources in your circuit, so we want to use the highest voltage zener we can get away with.
(Also, if you're in the NYC area and need components on short notice, Tinkersphere is a hole in the wall shop that might have what you need. Sure, Adafruit is also in NYC, but then you have to wait for shipping.)
I ultimately didn't end up using zeners, though, because of a mistake with my oscilloscope (oscope): when I was checking the noise characteristics (frequency spectrum) of the signal produced by the zeners, I compared the signal to ground instead of the voltage the noise was centered around, which made it look like the signal had bad frequency skews.
Bad[5]:
Good:
I eventually noticed that I had made this mistake, and that zeners of all stripes produced the right white-like spectra, but I had already bought the parts for the other noise generation technique…
NPN transistors
Similar to reverse biasing zeners, it turns out you can reverse bias normal semiconductors and get noise out of them too. See this page for more details, but basic idea is that the disconnected upside down transistor produces white avalanche noise in much the same way. When I tested this set up, I discovered that the noise signal generated was larger than the zeners I was using, so I went with this noise generation method.
(The 2nd transistor is there to amplify the noise coming out of the 1st transistor; I don't think it's strictly necessary to make a matched pair, but stuck to what I knew would work.)
Usually the noise generator is pretty quiet, so we also want to have an amplifier stage to boost the signal I showed earlier. It's similar to the amplifier circuit I showed earlier, but I stuck a log potentiometer (pot; analog adjustable resistor, with possible log vs linear behavior) in the feedback loop to make sure I could adjust the maximum loudness of the circuit. This isn't the same as the external volume knob, since I only need to set this knob once; basically, it's a hedge against fucking up and having half the external volume knob map to intolerably loud or soft volumes[6]. (These sort of "set-and-forget" pots are known as trimpots/trimmers)
While we're on the topic of gain, another thing I looked into was automatic gain control. Basically, instead of having an adjustable trimpot, you have a circuit that controls the gain with, again, feedback. This is particularly nice if you expect large temperature swings with thermally sensitive parts, like our noise generators. However, the automatic gain control literature is not well documented for a hobbyist like myself, so I skipped this research.
Power
Originally I was going to incorporate a rechargeable battery; this circuit was going to be lean, mean, and self contained. It would contain a big lithium-polymer (lipo) battery that could be charged by USB, allowing use and charging at the same time, and incorporate aggressive overcharge protection (charging up your lipos all the way isn't good for your battery life).
It was just that after hours of focusing on power design I gave up: incorporating a lipo charging circuit complicated the circuit a fair amount[7], and I wasn't even sure the default 100mA USB charging circuit could charge up the lipo and power the actual audio parts at the same time. Then there was the question of whether I would even get good runtimes out of any lipo battery, when I needed to boost my voltages and then run most of my circuit off that higher voltage (more on this below). Finally, I already carry a portable USB battery with me, so having an integrated battery just didn't make much sense.
So instead, I would only take power from a micro-USB port, which portable batteries already deliver power over. Without negotiating for more current, USB ports deliver 100mA, which would be enough without asking for it to also charge a battery; going back to TangentSoft's CMoy page, it mentions heavy headphone load could demand around 20mA, which means 100mA should be enough to run the rest of the circuit as well.
The problem is that USB ports deliver 5V, but we want higher voltages for better noise; in particular, the transistor pair (PN2222A) I decided on needed around 7V to get into the breakdown region. I could have gone back to low voltage zeners, but that seemed like compromising too much. Instead, I included a boost converter that would boost from 5V to 9V, at the cost of lower current (maximum draw of 111mA at around 80% efficiency (PDF)). The major power draw is expected to be the final output stage driving the headphones, which fits in the rated output current[8].
Wait, but don't we just need 9V for noise generation? After generating the noise, couldn't we just put the signal on top of the 5V signal? We could, but it would compromise the signal. First, we would need to put the signal on the middle of the available voltage range. Let's assume that we have a 2.5V source (in the middle of 0-5V), and put our noise signal on that and do the amplification and filtering. This leaves us with 2.5V of swing on either side, which is not much when opamps usually have a few volts near the positive and negative rails (in this case, 0 and 5V) where they distort the signal. Yikes. 9V is a nice sweet spot, since it gives us almost twice as much headroom as 5V while also offering enough current to actually run the circuit.
(Another option I thought about using was having two boost converters, and properly making a negative rail. The problem is that most boost converters aren't set up to do this, since most of their datasheets seem to tie input ground to output ground, instead of having both outputs float. I looked into making my own boost converter, but it turns that's difficult (PDF).)
So I mentioned putting our noise signal in between 0 and 9V, but how do we do that? First, we can get 4.5V by making a virtual ground: we can use our same old opamp amplifier circuit in a voltage follower configuration to spit out the same voltage as it takes as input and feed it the voltage in the middle of a voltage divider: we have two resistors of the same value between 9V and ground, and take the middle value, which will be pretty close to 4.5V. We need the amplifier to actually deliver the virtual ground, though, since otherwise the things that depend on it can distort the voltage[9].
Finally, how do we move our signal on top of our virtual ground? We can use coupling capacitors, which will block the DC (constant voltage) that the signal is sitting on top of, and use a resistor after the cap to tie the signal to our virtual ground, like so:
If you noticed that this looks a lot like a weird passive low pass filter, you get a cookie! This is in fact a high pass filter, with the same characteristics as a low pass filter except it blocks low frequencies, including DC. In our case, we can choose the values to put f_c under 20Hz, so the blocking effect is inaudible, but still allows us to move the signal on top of our virtual ground.
I also thought long and hard about including a power switch; did I really want to plug/unplug a battery each time I wanted to turn things off? After deciding that it would be too difficult to panel mount the switch, I thought maybe I could get a combination switch/pot: a pot that when turned all the way off, would also flip a switch on another pin. Appealing idea, but the architecture as I envisioned it at the time meant running my main power traces all the way across the board and then back, which seemed unnecessary and potentially causing later PCB routing problems[10]. Keep it simple, stupid.
Filter Control
How do we control the gain on each of the filter branches? We previously presupposed that we had some magical voltage controlled resistors, but we have to actually get non-magical parts at some point.
First, I started the project thinking I could use a fully analog solution. Unfortunately, the non-linear behavior we derived in the previous part meant that it would be intensely difficult to make an analog-only solution. So we need a digital controller to encode the gain/slope relationships, which means we have some discretization already, even if we control analog parts.
But the analog voltage controlled resistor-analogs are also complicated and lacking:
- You can use a optocoupler/photoresistor package, which draws more current than I was comfortable with.
- MOSFETs (type of transistor) have linear behavior in a range of operating values: however, it's only a short range, and I would need to derive the behavior for the MOSFETs I would end up using, which might end up changing across each device I test.
These seemed like bad options, so I decided to opt out of the analog world and do this fully digitally, going with digital potentiometers.
They're exactly what they sound like, potentiometer replacements that are digitally controllable. Basically, they contain a resistor array that they selectively turn on and off, controllable by normal communication standards like I2C and SPI. The downside is that they are either inaccurate or expensive, choose one. Even 256 position digital pots start to creep up in price, and 1024 or higher position counts really start to get pricey. I eventually opted for the 1024 position AD5292 (PDF) since I wanted the accuracy.
While we're on the subject; something you'll hear about when reading up on digital pots is "zipper noise". Due to the way digital pots are built, changing resistance doesn't happen all at once, but over a short time, which can sound like a zipper as different resistors switch over in the array. A way to reduce zipper noise is to only do updates when the signal is doing a zero crossing: in our case, we wait to do an update when the signal is around the virtual ground. This way, there's a temporary point where introducing multiplicative noise on the signal doesn't matter, because the signal is zero. However, it takes a bunch of extra hardware and hence board space I didn't have for suppressing a max 0.4V glitch (according to the datasheets[11]), so I opted not to do it: besides, the signal is already noise. In practice, updates aren't noticeable in the final audio output.
Microcontroller
So the digital pots need to be controlled, well, digitally, which means including a microcontroller. The AD5292 in particular uses the duplex SPI (Serial Peripheral Interface) protocol, which most common microcontrollers will support out of the box, and is simple enough that one could bit-bangit if there's no hardware support.
I opted for a ATTiny84; I already knew the AVR toolchain[12] and had the programmer hardware, and the device was small and powerful enough for my needs. The ATTiny85 is even smaller (8 pins vs the 14 pins on the ATTiny84), but 8 pins just isn't enough. SPI requires 3+N pins to talk to Ndevices: you need an SPI clock, output, input, and then one pin to select each device, and we have 4 devices. We also require power and ground, which adds up to… 9 pins needed. Hmm. It's true, you can use a multiplexer to try and fake enough pins, but that's still 6 pins, and we haven't even gotten to the ADC. So… we're going for the ATTiny84 instead of trying to work around a masochistic pin count.
It's pretty simple to program; you grab a 2x3 pin 0.1 inch header for in-circuit programming (ICSP), and attach a programmer (in my case, the AVR ISP MKII). Standard practice is to include resistors between the point the ICSP hooks into the microcontroller and other devices trying to use the same communication ports: this way, if the programmer and devices talk at the same time, the programmer takes precedence.
Like I mentioned, we aren't going to ask too much of our processor: what we want it to do is[13]:
- Read the "color" value from using an analog-to-digital converter (ADC). The pin is attached to an analog linear pot that the user can turn, from which we can read values of 0 (GND) to 1023 (5V[14]).
- For each filter, do a binary search[15] on the 20-value table we computed in the previous part to find which linear segment we are in. Do the linear interpolation to get the loudness value we want the filter to be at.
- If the value changed since the last time we computed this filter's value, select that digital pot and write the value to it.
- Take a nap (doing this process thousands of times per second doesn't make sense when the measurements won't change between each run), and repeat.
It's not quite that simple: notably, these simple AVRs don't have a hardware floating point unit, so pulling a JavaScript[16] and just having everything be a float is technically possible, but expensive. Then we're on a 8-bit processor, so even 16 bit operations are not 1-cycle. Instead, what we want to do is use fixed point: we'll use integers, but implicitly treat everything as having a fixed decimal point at some place other than the end. For example, I treated 16 bit numbers as ranging from 0.0 (0) to 1.0 (65535), so a value of 32768 is about 0.5. This means addition work out the same, and multiplications work out correctly if you chop off the last 16 bits of the 32 bit result. We get the semantics we want, but without paying out the nose for non-integer operations.
Note that while I used 16+ bit operations for all the intermediate values, that both the input and output are just 10 bits; in particular, the output is truncated quite a lot relative to our intermediate representations[17]. Oh well.
Schematic->PCB
This was my first time seriously using KiCad instead of the free Eagle version, which wasn't too bad. There's an extra step where you choose the right footprints while you're transitioning from schematic to PCB layout, instead of having the schematic symbol be attached to a footprint, but whatever.
By this point, most of the schematic should already be familiar (you can open the image in a new tab and zoom in: it, and all the other schematic images in this post, are scalable SVGs):
(The only section I haven't mentioned yet is the "Bypass Caps". Basically, you want some small caps on the power lines right next to our chips to smooth out any spikes in power demand from that particular chip.)
I'm also omitting a LOT of datasheet reading. The basic process: hit the part search engine Octopart, and start looking at devices that might meet your requirements, and make sure they actually meet your needs. How do you use the AD5292? Datasheet. Does the ATTiny84 have easy-to-use SPI hardware support? Datasheet. Just keep reading datasheets.
For example, you'll need to read the AD5292 datasheet to find out that you need to hook up a moderately high voltage capacitor to the EXT_CAP pin. Hope and pray your datasheets aren't wrong.
Another consideration is that you can pay varying amounts for your resistors, mostly to get different tolerances in the values you get. You can get 5% resistors that are going to be within 5% of the advertised value, or 0.1%; you're basically paying for people to sort your resistors and capacitors. We don't want our filters to end up far from where we want them to be, so we'll get moderately high precision resistors (0.1%) and capacitors (5%[18]) (again, trying to overengineer to compensate for my lack of knowledge).
After finishing up the schematic comes laying out the board. Can you outsource this work to autorouters yet? Alas, I've heard no. However, I did have to route this board twice, and it came out faster and easier the 2nd time around. There's a balance to strike between packing parts together, but also leaving enough room to route the traces around.
Front:
Back:
When looking at the PCB layout, note:
- I ended up dividing the board up into big 5V and 9V copper planes, and then routing a fat 5V trace around the 9V side to run the power for digital logic in the digital pots.
- I ended up leaving space for ghosts: there are some labels in KiCad that don't show up on the fabricated product, and I made sure not to overlap them during layout, which wasted space.
- I was using 0805[19] sized parts for the resistors and smaller caps: you can get big resistor/capacitor books (Adafruit) commonly in this size. Don't solder these by hand, instead use solder paste + a hot air gun/hot plate/reflow oven. Note that I think going smaller than 0805 is a hairy proposition, even if you're not soldering by hand, since even placing the damn things is hard.
- Instead of choosing the footprint, you can let the footprint choose you: I went with using the micro-USB connector I did because it already had a footprint in the KiCad libraries.
- I had to draw the MEE1SC boost converter footprint and OPA227 symbol, which were surprisingly easy.
- Except for the virtual ground and input/output opamps, the opamps are packaged multiply within one chip, so each chip has 4x opamps inside.
- Virtual ground is VCOM: initially I made the mistake of trying to use EARTH for proper ground, and GND for the virtual ground, but I didn't check that these were connected. It turns out these are connected when I ran the DRC (Design Rule Checker) checker, so I had to go back and redo some layout.
- There's some random pads hanging out in the middle of nowhere. There's a problem with KiCad where tying copper pours together with a PCB trace that doesn't terminate in a "device" will get confused when you re-pour everything, I hacked around it by adding a pad "device" tied to the right voltage, which inadvertently added a bare pad.
Finally, I sent the board off to OSHPark to get fabricated[20], placed an order with Mouser/Newark for my parts, and sat tight.
Enclosure
I'm not really a fan of drilling out existing plastic cases: during college, I tried to do the altoids tin CMoy amplifier, and the "metal working" just didn't work out. But with the expansion of the toolset I have to actually deal with modding an existing case, my desire to outsource enclosure manufacturing has also grown[21].
So, like I mentioned with my NAS build, I designed a lasercut design with Inkscape. Finger joints join most of the planes, and 2 captive screws/washers sit within a notch for a removable top. There's a few holes for the micro-USB socket and panel mount headphone jack on the sides, and analog volume/color pots on the top, with some light burning for volume indicators.
Some non-obvious things:
- The top plate bulges a bit in the middle because I wanted wood on both sides of the captive nut/screw: otherwise, the screw could fall out the side! And, well, while we're using Inkscape, we might as well make a gentle curve.
- There's a tiny hole right below the holes for the pots: this is for the anti-rotation lug. There's a nub on the pot that matches this hole, so that when you turn the knob, the body of the pot doesn't also rotate. Read the datasheets to find out the dimensions of the nub.
Inkscape still sucks for doing CAD designs; I didn't notice that the side notches didn't match up until after getting them cut the first time, and had to shave off a few millimeters.
The panel mount pots and headphone jack just screw on, making sure to line up with anti-rotation nub into the hole. The micro-USB connector just sits on the PCB, so you have to reach in a bit to plug in power; this meant that I went with a thinner wood than I should have (1.5mm vs 3mm), so the enclosure feels a bit more flimsy for the size. And then, I ended up making the micro-USB hole too large, so it became easy to get stuck on top of the connector, instead of inside it. Oops.
I wanted big, fat, beautiful knobs. I spent a lot of time looking for big enough knobs; once again, TangentSoft saved the day by pointing towards these MC21043 knobs. Just slap them on the pot shafts, screw them in with the set screw on the side, and hey presto!
What of the pots themselves? Getting the same brand/line of pots for both volume (log[22]) and color control (linear) would be nice, since I would only have to keep track of one datasheet[23]. A bit of a hunt ensued: after reading up on material choices, I wanted a conductive plastic pot, versus a scratchy/limited life carbon or expensive cermet pot. To fit these knobs, I needed pots with a thick enough shaft: the standard choices are 3mm or 6mm, of which I needed 6mm. I tried to find a flatted metal shaft, which meant the set screw could be firmly screwed into a flat surface without damaging it; eventually, I gave up and figured that I would just dig into a rounded shaft with the knob set screw, since it's not like I was going to use this pot over and over again.
In the end, I went with the Bourn model 91 pots, 91A1A-B24-B15L for linear and 91A1A-B24-D15L for log.
Originally, I was going to solder the pots directly into the PCB, but no one seemed to want to make panel mount pots that had the pins sticking straight down, which would make this easy. In the end, I figured that trying to shove the pots around to get them to fit on the board and the enclosure and have the micro-USB port hanging out at the right spot didn't seem worth it. Instead, I just used some standard female 0.1in connectors to wire up the PCB and pots. And note that unless you've found magical wire soldering tooling, soldering your own connectors is terrible. Just buy them pre-made (Sparkfun, Adafruit), and save yourself a lot of hunched over muttered swearing.
Since I wasn't using the pots to hold the PCB to the enclosure any longer, now I had to make sure it didn't rattle around otherwise. So, I put in some M3 (3mm diameter, standard MX screw measurements) holes into the PCB and bottom plane of the enclosure, and then screwed the PCB into some nylon standoffs[24], and screwed the standoff into the enclosure. To avoid having the external screw heads just sit on whatever table I put it on, I screwed them into some rubber feet. Finally, I put a dab of weak loctite on the screws before screwing them in for the last time[25].
In the end, the enclosure as a whole was too tall for my tastes: I needed to design around both the tall caps I put in, and the boost converter, and the 0.1in header housings, and then put all of that on top of a 10mm standoff. Thinking back, if I did the PCB layout ignoring the label ghosts, I could have saved enough room to put in right angle 0.1in headers, laid the caps on their sides, used short rubber washers as standoffs, and gotten a nice slim case where the thin walls would be stronger. Next time.
Well, there was a partial next time. It turns out while I was paranoid about the placement of the pots, and making sure that they wouldn't collide with the 0.1in headers in place to run connections to them, I wasn't paranoid enough. The case just wouldn't close, because some of the pins just barely jammed up against the left pot. So, I just sent off for the top plate to get re-cut[26], making the knobs barely asymmetrical and including some label text with the extra space.
v1 Problems
Now it becomes important to note that I was working under a self-imposed schedule: I wanted to finish this thing, and finish it fast, and prove I wasn't some hopelessly optimistic schemer that could never be pessimistic enough to undershoot real life. In retrospect, I should have spent more time verifying that all the parts worked on their own before joining everything together, but I didn't feel like spending all that money on extra parts I would throw away; besides, when I would triumphantly finish this Hail Mary push by fabricating everything at once, I would feel pretty badass.
So I bought everything, soldered it up[27], and put it all together. Victory is in sight!
And then I hooked up some headphones to the end and couldn't hear anything coming out the end.
Hmm.
I tried powering things using my USB battery, which would normally light up if there was power draw: no dice, there isn't even power being drawn. A walk around the board with a multimeter and a few puzzled nights eventually showed me that the power wasn't even making it out of the micro-USB connector.
(By the way, these SOIC clips are indispensible when working with these smaller pitch parts. Clip them on, and you get a stable pin to plug or clip on to.)
Well, there wasn't an obvious reason this should be happening, and yet here we are. It seemed plausible that the pins on the bottom of the connector, which I couldn't see, weren't actually soldered, so I flooded the entire area with solder a few times, and eventually it worked! In the future, I "primed" my micro-USB pads with solder before placing them on the board (with more solder paste), which seems to give better results.
Now I was getting a noisy signal out of the headphones, exactly what I wanted. But when I loaded up optimistically written microcontroller code to test out the digital pot control, nothing responded to the changing color knob.
Hmm.
I hooked up an oscope to the SPI channels, programmed the microcontroller to continually do SPI writes, and took a look. It turns out that the pins were backwards: I was using the clever MOSI/MISO pin naming, where M and S stand for Master and Slave[28] and I and O stand for Input and Output. So MOSI is the pin the master uses for output, and the slave uses for input. This is way clearer than the alternative DI/DO (Data Input/Output), where you have to keep track of whether this chip's DI needs to connect to that chip's DO, or vice versa. With MXSX notation, MOSI connects with MOSI, and MISO connects with MISO, always. It's just so much clearer!
Except it's not clearer. When I looked at the SPI channels, I noticed that the channels were switched! The ATTiny84 MISO pin was acting as output, instead of vice versa. At first I just bit-banged the SPI protocol myself in software instead of using the dedicated communication hardware, but this seemed like a huge oversight. How in the world did anyone use this and not notice that the MISO/MOSI pins were backwards[29]?
It wasn't until I started putting v2 of the board together that I realized that there was more than one master: when uploading programs to the ATTiny, the computer is the master and the ATTiny is the slave. It was only during regular operation that the ATTiny was the master, but the pin names referred to the programming-time master, not regular operation master. The DI/DO names correctly reflected this, and had been sitting right next to the MISO/MOSI names I had been using all along.
Lesson learned: if you're looking at MISO/MOSI, make sure you know which master is being referred to. Look at the DO/DI naming (if available) to confirm.
But after bit banging the protocol on the right channels, the digital pot still didn't respond.
Hmm.
So, I bought an extra AD5292, soldered it to one of the SMD break out boards I had lying around (handy! I got these from Sparkfun) and tried to talk to the chip with an Arduino, which has an existing SPI library. The basic principle is if it doesn't work with an Arduino, then you're probablydoing something wrong. Eventually, I figured out I had a bug in my code, where I was writing the wrong command bit (in position 3 instead of 2), which meant I was trying to read instead of writing the resistor value. Oops.
But changing this on the real board didn't change anything. It works with Arduino, so I was doing something wrong, but what? I poked around with an oscope for a while, looking for clues in the SPI signal, but nothing made sense. Puzzling, the RDY (ready) output pin on the AD5292 should fall to GND when it's done with processing, but instead it would barely blip downwards a fraction of a volt.
Hmm.
Was something tying the signal high? I tried to hit all the pins with solder again to make sure that there were no solder bridges, to no avail.
Eventually, I figured out that I had screwed up the schematic: I had run the virtual ground VCOM into the logic ground GND pin on the AD5292, which was why it was dipping on a little, to around… 4.5V. Obviously, a device built for any reasonable voltage required more than a half volt swing between logic high and logic low, which was why nothing was working.
Now, I was faced with a dilemma: how in the world could I fix this?
I could try and twist the pins of the chip up off the PCB, and solder wires on to it to the correct ground. Mildly put, this seemed error prone. On the other hand, paying to re-fabricate the board and get new parts (de-soldering everything would've been a fair amount of work) would be pretty expensive.
I gave up for a bit, but my pride drove me back and I just got everything refabricated.
PCB, Version 2
So the new PCB had some tweaks to fix the problems I had run into:
- The SPI communication channels were swapped to fit the right MOSI/MISO semantics.
- The digital pots got the right logic ground.
So I wire it all up, program everything, and hey presto, I can see the individual digital pots responding on the oscope!
Well, except for one.
Well, that's weird. Maybe the chip is bad: after all, I do literally cook them at uncomfortably high temperatures. So I got a hot air gun[30], desoldered one of the chips from my v1 board, and dropped it into the non-working chip's place. Still no go: the chip select channel won't drop, to signal to the digital pot that it should start listening for commands.
After some more head banging, more close examination of the board to make sure nothing was accidentally bridging 5V to this pin, and more soldering to make sure there weren't any hidden solder bridges, I finally discovered I had screwed up the ADC settings on the ATTiny, and had set the pin I was trying to write to as an external ADC reference (specifically, ADMUX was set incorrectly). Obviously, one pin can't read as a reference and output a signal at the same time, so it just acted as a reference and stayed at 5V forever. After I fixed the bad settings, that chip started responding.
Now that the chips were responding and actually being programmed, the output was changing in response to the pots being programmed. However, I still wasn't getting the right behavior out of the output.
After some more study, there are two major problems:
- The amplifier stages are backwards. Remember how we create amplification circuits with opamps, which gives us a gain of |G| = R_f/R_{in}? Right, so if we want a maximum gain of 18, then we want the maximum R_f to be 18 times bigger than R_{in}, right?
Yeah, I fucked that up. If you look in the schematic, the 16.5Hz filter amplification stage is backwards: R_f = 20k, R_{in} = 360k, so G=1/18. This applies to all 4 filter stages. Really, I should have hacked together a fix for the virtual ground problem before, because this sort of thing isn't getting fixed without yet another refab.
- When I finally scrutinized the signal for the outputs of each stage, I noticed that they weren't acting as low-pass filters: the frequency spectrum looked flat for everything, which was why I was only hearing different intensities of white noise coming out of the end.
I still have no idea why this is happening, and frankly, after months of expensive on-and-off work to make a Hail Mary attempt work out in the end I'm just tired of the entire enterprise.
So, here I give up.
Lessons Learned
So I'm leaving you with some code that kind of works, broken schematics, and drawings for an enclosure purpose-built for a certain project. Is there anything useful we can salvage?
Yeah, lessons learned!
- Don't rush. Rushing means that obvious mistakes get made, and then there's no time to let it bake a bit and go over schematics/layouts later with a fresh mind. For example, I didn't let myself have time when designing the enclosure, and while the design got made marginally faster, I ended up needing to refab the top plate because a margin I thought was safe wasn't. Go slow to go fast.
- Don't throw good time after bad. In the end, I discovered that my Hail Mary attempt had all sorts of bugs throughout the stack, from schematic to software. I would have been better served by throwing away the existing work and then using best engineering principles to do the design, taking a piecemeal approach where I ensured each part and component worked as I thought it did before putting everything together into increasingly larger parts. Sure, the schematic is split up nicely into discrete parts, but I didn't bother testing any of them in isolation. If I had taken such an approach from the beginning, I likely would have spent about as much money on parts as I actually did and ended up with a working circuit.
- Aim lower. I originally did this as a gentle re-introduction into the world of electronics, but I forgot that everything looks easy from 10,000 feet and there are always tons of weeds at ground level. That said, doing a fully digital microcontroller circuit probably would have been too easy (none of the bugs I ran into would have manifested in that circuit), but in the end the complexity of this project was a bit too much.
- If you don't know how much engineering is overengineering, then trying to overengineer to compensate for things is less effective than you might think.
- At least I know how to debug circuits more effectively, have experience with a larger set of hardware bugs (so I know what to look out for next time) and have a new hot air rework station.
I suppose I had to have an experience like this one, where I overreached and couldn't deliver before I got too exasperated: lord knows how many quagmires I've gotten myself into with software[31], where I'm starting to become more measured with my enthusiasm. Hopefully I can learn quickly in the hardware realm, and the next hardware thing I do won't also die an untimely death.
KiCad files/ATTiny code
It's all packaged in the same noisEE directory (Github).
ATTiny84 microcontroller code (Github)
Bill of Materials (BOM, Google Sheets)
[1] ↑ Famous last words, I know.
[2] ↑ Of course, the ideal opamp model breaks down when confronted with reality, but it's a good enough approximation for our purposes.
[3] ↑ So why don't we just build the gain into the opamp themselves? Well, having general purpose high-gain opamps means that it's way cheaper to manufacture them in bulk, and resistors are super cheap: even high precision resistors are more cost effective to swap out than opamps.
[4] ↑ A cute phrase that you'll see some folks use is that the "magic smoke" is escaping.
[5] ↑ Poor screen photo, I know.
[6] ↑ For example, headphones have a really wide range of resistances, like from 33-330ohm, so a single volume knob can end up with different volumes for different headphones. Having a trimpot also allows adjusting the volume knob to your particular headphone set.
[7] ↑ If you are interested in integrated charging circuits, I think this blog is a pretty good starting place.
[8] ↑ Another consideration specific to boost converters is the chop: these devices essentially charge a capacitor up at some high frequency to a voltage greater than the input, so there's some high frequency in the output. Power noise is a big problem, since it affects basically the entire analog circuit, but in this case, the noise is 60kHz, which is well outside human hearing. Sure, it is a little worrying, since if something goes wrong the 60kHz signal could have effects that extend down to 20kHz, but we aren't going to do anything about that.
[9] ↑ Note that while the non-ideal opamp characteristics mean the virtual ground isn't exactly 4.5V even with the amplifier, it's certainly closer than it otherwise would be, and less subject to the whims of the rest of the circuit.
[10] ↑ The architecture at the time was soldering the pots directly into the board: the architecture I ended up with would have been more amenable, since I could have run (beefy) wires out to the pot.
[11] ↑ The AD5292 datasheets also have information about how to make the zero-crossing detector, under the "Audio Volume Control" section.
[12] ↑ Versus the PIC family that is still unfamiliar to me: I swear I'll get around to actually working with a PIC some day.
[13] ↑ If you look in the microcontroller code, you'll notice that there's some empty stub functions mentioning "TP-20". That was an abandoned effort; you can program the AD5292 to start on powering up with a certain value, instead of needing to talk to the device to unlock it and then write a value. The idea would be that pushing a certain button would start the TP-20 programming process, instead of hardwiring it into the microcontroller on start, since you can only do this programming 20 times. However, it's pretty fast to talk to on start up, so I never ended up writing the code to support it.
[14] ↑ This could also be another reference point, instead of 5V: this might get you better resolution at the top end.
[15] ↑ If you take a look at the git repo, you'll notice that there's a script that ensures all values terminate. If your microcontroller loop doesn't terminate by accident, there's no way to ctrl-C and force it to stop: you're just stuck. So I made sure that every possible value would stop, which is easy because there are only 1024 possible inputs.
[16] ↑ DAMMIT BRENDAN EICH WHY, WHY IS EVERYTHING A FLOAT.
[17] ↑ Why don't we create a big lookup table? Fast! Simple! Well, the microcontroller only has like 8kb if memory, and we need 4 different tables encoding 1k 16-bit values, which adds up to… exactly 8kb. There's no room for code to actually talk to the digital pots. There are ways around this, but good luck packing 10-bit values on non-byte boundaries.
[18] ↑ Capacitors in general are more costly to get at higher precisions.
[19] ↑ 0805 = 0.08in x 0.05in. You can apply this to 1206, and so forth and so on.
[20] ↑ I thought about using Dirty PCBs, but they give you 10 PCBs. What the hell would I do with 9 extra PCBs? Plus, my board was just a smidge too large to fab with them. I've used Sunstone for PCBs in the distant past, but their prices are just not competitive for a hobbyist project.
[21] ↑ "But why do you put together your own electronics?" Good question, maybe I should look into assembly shops: if they're not too much of a premium, then I could just avoid all the headaches of once-in-a-while assembly. My intuition says that it probably isn't worth the cost, but maybe…
[22] ↑ Human hearing is logarithmic, which makes log pots a natural fit for volume controls.
[23] ↑ It's eerie how systematized pot names are: it's easy to decompose a pot's product name and figure out "oh, it's a Bourn, 20k, log, flatted shaft that is 20mm long". I figure most things are like this, but I haven't had need to consider lots and lots of closely related alternatives before.
[24] ↑ It's not super important that these are nylon, but I wanted to avoid metal in general for the standoffs because if something went wrong, you might end up running electricity outside of the enclosure without it being obvious. You could also use rubber washers or the like to keep things separated.
[25] ↑ It shouldn't work, since my understanding is that loctite needs two dissimilar metals to do an effective join. Whatever, it's gumming up the works a bit.
[26] ↑ I could have gotten the PCB remade, but that's a fair bit more expensive; not counting the parts, it's around twice as expensive, and not being able to rescue all the parts pushes the price up even more.
[27] ↑ Note that after soldering your board can end up a sticky mess, so after soldering SMD parts but before soldering water-permeable parts, rub the board clean with high concentration alcohol.
[28] ↑ I know, I know, archaic and unhelpful naming.
[29] ↑ Hint: if the rest of the world seems to be wrong, re-consider your priors.
[30] ↑ It took me a long time to finally get a hot air rework station, but it's great for doing spot work with SMD parts.
[31] ↑ At least projects that took longer than anticipated, which is surprise! Basically all of them!