The Arduino doesn't come with a DAC (digital-to-analog converter), so the two options to generate an analog signal are a resistor ladder (remember the Covox Speech Thing?), or PWM. Some commercial trackers, like the TinyTrack, use a 4-bit resistor ladder, but I thought it would be more fun to use PWM. It gets more complicated on the programming side but it takes less external components and only one output pin.
The Arduino has 3 timers capable of doing PWM. Some timing functions in the Arduino library (delay and millis) use Timer 0, so we don't want to disturb them. Timer 1 and 2 both have about the same capabilities, but Timer 1 can count up to 16 bits and Timer 2 only goes up to 8 bits. There is a great write-up by Michael Smith on generating PWM with Arduino and two timers. It uses Timer 1 to feed samples (at whatever the sample rate is) and Timer 2 to do the actual PWM (at the maximum rate possible, 16 MHz / 256 = 62500 Hz). But, by keeping the playback interrupt short enough, it is possible to do away with just one timer and simplify the code a bit. All it takes is telling the AVR to trigger an interrupt when the PWM timer overflows and feed samples from the interrupt service routine. The ISR will be called every 256 clock cycles, but that's long enough for a couple of table look-ups. A code excerpt:
// Configure pins
// Set up Timer 2 to do pulse width modulation on the speaker
// Source timer2 from clkIO (datasheet p.164)
ASSR &= ~(_BV(EXCLK) | _BV(AS2));
// Set fast PWM mode with TOP = 0xff: WGM22:0 = 3 (p.150)
TCCR2A |= _BV(WGM21) | _BV(WGM20);
TCCR2B &= ~_BV(WGM22);
// Do non-inverting PWM on pin OC2B (arduino pin 3) (p.159).
// OC2A (arduino pin 11) stays in normal port operation:
// COM2B1=1, COM2B0=0, COM2A1=0, COM2A0=0
TCCR2A = (TCCR2A | _BV(COM2B1)) & ~(_BV(COM2B0) | _BV(COM2A1) | _BV(COM2A0));
// No prescaler (p.162)
TCCR2B = (TCCR2B & ~(_BV(CS22) | _BV(CS21))) | _BV(CS20);
// Key the radio
// Enable interrupt when TCNT2 reaches TOP (0xFF) (p.151, 163)
TIMSK2 |= _BV(TOIE2);
// Release PTT
// Disable playback per-sample interrupt.
TIMSK2 &= ~_BV(TOIE2);
// Service routine for TIMER2's overflow interrupt.
// This is called at PLAYBACK_RATE Hz to load the next sample.
// [...] load the next sample
OCR2B = next_sample;
And that's it. We call modem_setup() once at the beginning to set Timer 2 to the proper mode. Then we call modem_start() to transmit and modem_stop() to stop. You can see the real implementation here. These are the results:
Received signal (MX146)
Received signal (HX1)
On the first image you can see the PWM output (below) and the same PWM signal filtered by an RC low-pass filter with cut-off frequency = 2800 Hz (above). Can you see the phase offset between the two signals? That's a consequence of the RC filter. There is some ripple at 62.5 KHz resulting from the PWM itself, but the transmitter and receiver's own filters took care of it as you can see in the the other two pictures. One thing I noticed is a bigger gain of the signal transmitted with the MX146 compared to that of the HX1. Perhaps I am overmodulating the MX146?
Nice work! I wonder how would you qualify the audio sound, would it be an 8bit or better? Sampling at 62,5Khz certainly brings the noise out of the hearing range.ReplyDelete
I'm using an 8-bit sine table and 8-bit PWM duty cycle, so... definitely 8 bits. The 62,5 KHz sampling noise should be easily filtered out by the input stage of radio transmitters to avoid frequency splatter.ReplyDelete
I too used pwm for playing wav audio...ReplyDelete
predefined pwm function is timer.pwm(pin,dutycycle,period)
now when i read a wav file, i get values ranging from 0-255. i guess they are duty cycle. I also calculated the sampling frequency of the wav file using matlab. But the main problem is i dont get an audio that is anywhere near the original.....Please Help!!
The file needs to be raw (headerless) 8-bit unsigned PCM. Use Audacity to export your wav as raw data.Delete