Using the Arduino to send audio via pulse width modulation

I’m still interested in doing light based communication, but I haven’t made a lot of progress. I did build an LTSpice model of the circuit I used yesterday, but other than verifying that it probably would work as built (which it did) I didn’t feel like I had enough brain cells working to optimize the circuit. So, instead, I decided to try to see if I could use an Arduino to send reasonably high quality audio over light using pulse width modulation.

It doesn’t really seem all that hard in principle: the Arduino libraries include an analogWrite() command which can be used to generate a PWM signal on an output pin. But the problem is the frequency of operation is quite low: around 500Hz or so. Since I was interested in sending voice bandwidth signals (say sampled at 8000Hz or so) the PWM “carrier” frequency simply wasn’t high enough.

So, I did a bit of digging. It turns out that you can configure the timers on the ATMEGA328 on board the Arduino pretty easily, and if you dig through the datasheet, scratch your head a bit, and then type carefully, you can come up with the right incantation. Which I did: in fact, it worked the very first time I downloaded it to the board.

I recorded a second or so of audio using Audacity, dumped it as an 8 bit raw audio file, and then converted it to bytes. I then created a very simple program which simply copies each byte to the PWM overflow register, and then delays for 125 microseconds (1/8000 of a second). Other than that, just some simple bit twiddling to change the PWM prescaler to operate at the full 16Mhz clockrate, and… voila.

Witness the video:

Here’s the core of the code (the actual audio data has been stripped for brevity):

#include <avr/pgmspace.h>

prog_uchar bwdat[] PROGMEM = {
0x80, 0x80, 0x80, 0x7f, 0x80, 0x80, 0x80, 0x81, 0x80, 0x80, 0x80, 0x80,
// ... lots of lines deleted for brevity...
0x80, 0x80, 0x80, 0x80, 0x80, 0x81, 0x80, 0x7f, 0x7f, 0x80, 0x80, 0x80,
0x80, 0x81, 0x81, 0x80, 0x80, 0x81
} ;

  pinMode(11, OUTPUT);
  TCCR2A = _BV(COM2A1) | _BV(WGM21) | _BV(WGM20);
  TCCR2B = _BV(CS20) ;
  OCR2A = 180;

  int i ;
  char ch ;
  for (i=0; i<sizeof(bwdat); i++) {
    ch = pgm_read_byte_near(bwdat+i) ;
    OCR2A = ch ;
    delayMicroseconds(125) ;