Commit 448c9f7f authored by Erik Strand's avatar Erik Strand

Add input devices

parent 6e9e1b1d
+++
title = "Input Devices"
date = "2018-11-7"
menu = "main"
weight = 11
+++
Files: [audio_input.sch](/designs/10_audio_input.sch) [audio_input.brd](/designs/10_audio_input.brd) [audio_top.png](/designs/10_audio_top.png) [audio_cutout.png](/designs/10_audio_cutout.png) [sampler.c](/designs/10_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](https://en.wikipedia.org/wiki/Pickup_(music_technology)) 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, a five string Musicman Stingray.
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).
### A note on processors
At this point I'm reasonably certain I will use an ATMEL SAMD51 microcontroller for my final project, but to get started with audio sampling I'm going to explore the functionality of the ATTINY44A. It's a lot easier to program, and should teach me some things that I might otherwise miss in the SAMD51's more complex datasheet.
For maximum resolution (10 bits), the ATTINY44A's ADC desires a clock input of 50kHz to 200kHz. A single conversion takes 13 ADC clock cycles, which means that the maximum sampling frequency is about 15kHz (section 16.5 of the datasheet). This is a bit slow for audio, but is sufficient for my current test purposes.
### Circuit Design
To gain experience with analog circuitry, I'm going to make an off-chip preamp stage 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](https://www.instructables.com/id/Arduino-Audio-Input/), but added some modifications I found on [Electronics Stackexchange](https://electronics.stackexchange.com/questions/14404/dc-biasing-audio-signal). 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.
![](/img/10_schematic.png)
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. I did have to 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 components. So I changed the design rules as I worked.
![](/img/10_board.png)
### Testing
I connected my bass guitar's output to the preamp, in a maximally professional manner.
![](/img/10_phono_input.jpg)
As an initial test of the preamp, I measured its input and output with a [Saleae Logic Analyzer](https://www.saleae.com/). It looks promising!
![](/img/10_amplification.jpg)
Upon closer inspection, however, it's clear that my amplification isn't linear. Clearly there's something wrong with my op-amp circuit. At least the DC bias appears to be right at 2.5V.
![](/img/10_nonlinear.png)
### Sampling
Finally I connected the output of my preamplifer to a spare input on my microcontroller board.
![](/img/10_isp_reuse.jpg)
On the software side, I started with Neil's [echo program](http://academy.cba.mit.edu/classes/embedded_programming/index.html#echo). This way I have all the serial communication ready to go. Here is my new main method.
{{< highlight c >}}
#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
}
}
{{< /highlight >}}
This reads samples and spits them out over the serial connection.
![](/img/10_oscillations.png)
This diff is collapsed.
This diff is collapsed.
//
// sampler.c
// based on hello.ftdi.44.echo.c
//
// 115200 baud FTDI character echo, with flash string
//
// set lfuse to 0x5E for 20 MHz xtal
//
// Neil Gershenfeld
// 12/8/10
// Erik Strand
// 11/6/2018
//
// (c) Massachusetts Institute of Technology 2010
// This work may be reproduced, modified, distributed,
// performed, and displayed for any purpose. Copyright is
// retained and must be preserved. The work is provided
// as is; no warranty is provided, and users accept all
// liability.
//
#include <avr/io.h>
#include <util/delay.h>
#include <avr/pgmspace.h>
#define output(directions,pin) (directions |= pin) // set port direction for output
#define set(port,pin) (port |= pin) // set port pin
#define clear(port,pin) (port &= (~pin)) // clear port pin
#define pin_test(pins,pin) (pins & pin) // test for port pin
#define bit_test(byte,bit) (byte & (1 << bit)) // test for bit set
#define bit_delay_time 8.5 // bit delay for 115200 with overhead
#define bit_delay() _delay_us(bit_delay_time) // RS232 bit delay
#define half_bit_delay() _delay_us(bit_delay_time/2) // RS232 half bit delay
#define char_delay() _delay_ms(10) // char delay
#define serial_port PORTA
#define serial_direction DDRA
#define serial_pins PINA
#define serial_pin_in (1 << PA0)
#define serial_pin_out (1 << PA1)
#define led_pin (1 << PB2)
#define input_pin (1 << PA5)
void get_char(volatile unsigned char *pins, unsigned char pin, char *rxbyte) {
//
// read character into rxbyte on pins pin
// assumes line driver (inverts bits)
//
*rxbyte = 0;
while (pin_test(*pins,pin))
//
// wait for start bit
//
;
//
// delay to middle of first data bit
//
half_bit_delay();
bit_delay();
//
// unrolled loop to read data bits
//
if pin_test(*pins,pin)
*rxbyte |= (1 << 0);
else
*rxbyte |= (0 << 0);
bit_delay();
if pin_test(*pins,pin)
*rxbyte |= (1 << 1);
else
*rxbyte |= (0 << 1);
bit_delay();
if pin_test(*pins,pin)
*rxbyte |= (1 << 2);
else
*rxbyte |= (0 << 2);
bit_delay();
if pin_test(*pins,pin)
*rxbyte |= (1 << 3);
else
*rxbyte |= (0 << 3);
bit_delay();
if pin_test(*pins,pin)
*rxbyte |= (1 << 4);
else
*rxbyte |= (0 << 4);
bit_delay();
if pin_test(*pins,pin)
*rxbyte |= (1 << 5);
else
*rxbyte |= (0 << 5);
bit_delay();
if pin_test(*pins,pin)
*rxbyte |= (1 << 6);
else
*rxbyte |= (0 << 6);
bit_delay();
if pin_test(*pins,pin)
*rxbyte |= (1 << 7);
else
*rxbyte |= (0 << 7);
//
// wait for stop bit
//
bit_delay();
half_bit_delay();
}
void put_char(volatile unsigned char *port, unsigned char pin, char txchar) {
//
// send character in txchar on port pin
// assumes line driver (inverts bits)
//
// start bit
//
clear(*port,pin);
bit_delay();
//
// unrolled loop to write data bits
//
if bit_test(txchar,0)
set(*port,pin);
else
clear(*port,pin);
bit_delay();
if bit_test(txchar,1)
set(*port,pin);
else
clear(*port,pin);
bit_delay();
if bit_test(txchar,2)
set(*port,pin);
else
clear(*port,pin);
bit_delay();
if bit_test(txchar,3)
set(*port,pin);
else
clear(*port,pin);
bit_delay();
if bit_test(txchar,4)
set(*port,pin);
else
clear(*port,pin);
bit_delay();
if bit_test(txchar,5)
set(*port,pin);
else
clear(*port,pin);
bit_delay();
if bit_test(txchar,6)
set(*port,pin);
else
clear(*port,pin);
bit_delay();
if bit_test(txchar,7)
set(*port,pin);
else
clear(*port,pin);
bit_delay();
//
// stop bit
//
set(*port,pin);
bit_delay();
//
// char delay
//
bit_delay();
}
void put_string(volatile unsigned char *port, unsigned char pin, char *str) {
//
// print a null-terminated string
//
static int index;
index = 0;
do {
put_char(port, pin, str[index]);
++index;
} while (str[index] != 0);
}
#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 neither sample rate nor accuracy is yet a concern.
// 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
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment