Input Devices
Files: audio_input.sch audio_input.brd audio_top.png audio_cutout.png sampler.c
This week we’re exploring input devices, so our microcontrollers can start sensing the physical world. Since my final project involves manipulating signals from steel bass guitar strings, I will use an electromagnetic pickup as an input.
Design Considerations
For my final project I plan on winding my own pickups, but I’d like to start by interfacing with existing ones so I can be certain that they work. So my goal is to read the signal from the pickups on my bass guitar.
The pickups generate an analog electrical signal, so the most direct way to read their output is with the ADC found on pretty much any microcontroller. But there are a few complications. First, the raw signal from my bass is probably in the hundreds of millivolts range, but to get the most out of the ADC’s bit depth I’d like its maximum peak-to-peak amplitude to span almost the full difference between VCC and GND. Second, the pickups produce a signal with no DC bias. The ADCs on many microcontrollers don’t read negative voltages, so I may need to add a DC bias of 0.5 * VCC. Alternatively, I can find a microcontroller with an ADC that supports bipolar input (whether single-ended or differential).
Circuit Design
To gain experience with analog circuitry, I’m going to make an off-chip preamp that boosts the signal and provides a DC offset. Instead of adding a microcontroller to this board, I plan on connecting it to my existing board using an ISP header. This will save me some soldering, and will allow me to test the same preamp stage with different microcontrollers.
I mostly followed Amanda’s Instructables article, but added some modifications I found on Electronics Stackexchange. To amplify the signal, I use an op-amp in a non-inverting configuration, and to provide a DC offset I pass the amplified signal through a capacitor that’s connected to a voltage divider circuit. I’m using the MISO ISP pin for my input, and MOSI for output.
I think I found a pretty good layout for the board. By routing signals underneath capacitors and resistors, I can keep the traces pretty short. As you can see I cheat a little with the design rules: mods is generally happier when the clearances are much larger than the diameter of the end mill, but using these rules globally would prevent me from routing under standard 1206 components (edit: not sure why I didn’t use the 1206FAB variants here…). So I changed the design rules as I worked.
Testing
I connected my bass guitar’s output to the preamp, in a maximally professional manner.
As an initial test of the preamp, I measured its input and output with a Saleae Logic Analyzer. It looks promising!
Upon closer inspection, however, it’s clear that there are some serious problems. Though the output’s DC bias does appear to be at 2.5V, it gets much closer to 5V than it does to 0V. Even the input appears altered: the waveforms are clipped on the bottom end.
Sampling
Ignoring for the moment the strange preamp behavior, I connected the output of my preamplifer to a spare input on my microcontroller board.
On the software side, I started with Neil’s echo program. This way I have all the serial communication ready to go. Here is my new main method.
#define input_pin (1 << PA5)
int main(void) {
// Set clock divider to /1
CLKPR = (1 << CLKPCE);
CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0);
// Initialize serial output pins
set(serial_port, serial_pin_out);
output(serial_direction, serial_pin_out);
// Initialize the input pin.
DDRA &= ~input_pin;
// Make sure power is getting to the ADC.
PRR &= ~1u;
// Turn on the ADC.
ADCSRA |= (1u << 7);
// Use VCC as the reference voltage, and connect the ADC to PA5.
ADMUX = 0u;
ADMUX |= 0b00000101;
// Make ADC samples "left adjusted" so that we only have to read one register
// to get the 8 most significant bits.
ADCSRB |= (1u << 4);
// Note: I should configure the ADC's clock divider.
// I don't bother here since we're just reading samples one at a time
// and sending them over the serial connection.
// We'll store audio samples here.
uint8_t samples[256];
uint8_t n_samples = 0;
while (1) {
// Tell the ADC to record a value.
ADCSRA |= (1u << 6);
// Wait until the reading is finished.
while (ADCSRA & (1u << 6)) {}
// Read the result.
samples[0] = ADCH;
// Write the result (in binary).
put_string(&serial_port, serial_pin_out, "hello.ftdi.44.echo.c: read sample \"");
for (int i = 1u; i <= 8; ++i) {
if (samples[0] & (1u << (8 - i))) {
put_char(&serial_port, serial_pin_out, '1');
} else {
put_char(&serial_port, serial_pin_out, '0');
}
}
put_char(&serial_port, serial_pin_out, 10); // new line
}
}
This reads samples and spits them out over the serial connection.
Circuit Design Revisited
A friend pointed me to Mark Feldmeier’s wonderful op-amp guide, which I relied on for a total redesign of my preamp. It includes a number of improvements. First, my voltage divider gets a buffer (i.e. an op-amp wired so that its output is always the same voltage as its input). Once I’m processing audio from multiple strings at once, this will help prevent crosstalk via the bias voltage rail. I’m also biasing my signal before amplification, which means the bias voltage rail is only connected to the high-impedance op-amp inputs, and that the low-impedance op-amp output can be used directly.
I also made a number of improvements for my instrumentation. I connected header pins not just to the output, but also to both sides of the guitar input and my bias voltage. This should help with debugging if there are still problems. I also soldered some jumper wires to proper audio jacks, so that I can plug my bass in without clips.
This board’s output looks a lot better than the previous one’s.
I decided to plug it into my (commercial) audio interface so I could listen to the output directly. It sounds just like the untreated output of my bass.