I'm trying to synthesize sound on the Arduboy, which is a handheld gaming device with an AVR ATMega32u4 microcontroller and a speaker attached between its pins C6 and C7.
My plan is to use timer 4 to generate a high-frequency PWM signal on C7, and then use timer 3 to change timer 4's duty cycle. For a "hello world"-level program, I'm trying to read 3906 8-bit samples per second from PROGMEM.
First of all, to make sure my sample file is really in the format I think it is, I have used SoX to play it on a computer:
$ play -e unsigned-integer -r 3906 -b 8 sample2.raw
Here's the relevant parts of my code:
pub fn setup() {
without_interrupts(|| {
PLLFRQ::unset(PLLFRQ::PLLTM1);
PLLFRQ::set(PLLFRQ::PLLTM0);
TCCR4A::write(TCCR4A::COM4A1 | TCCR4A::PWM4A); // Set output C7 to high between 0x00 and OCR4A
TCCR4B::write(TCCR4B::CS40); // Enable with clock divider of 1
TCCR4C::write(0x00);
TCCR4D::write(0x00);
TC4H::write(0x00);
OCR4C::write(0xff); // One full period = 256 cycles
OCR4A::write(0x00); // Duty cycle = OCR4A / 256
TCCR3B::write(TCCR3B::CS32 | TCCR3B::CS30); // Divide by 1024
OCR3A::write(3u16); // 4 cycles per period => 3906 samples per second
TCCR3A::write(0);
TCCR3B::set(TCCR3B::WGM30); // count up to OCR3A
TIMSK3::set(TIMSK3::OCIE3A); // Interrupt on OCR3A match
// Speaker
port::C6::set_output();
port::C6::set_low();
port::C7::set_output();
});
}
progmem_file_bytes!{
static progmem SAMPLE = "sample2.raw"
}
// TIMER3_COMPA
#[no_mangle]
pub unsafe extern "avr-interrupt" fn __vector_32() {
static mut PTR: usize = 0;
// This runs at 3906 Hz, so at each tick we just replace the duty cycle of the PWM
let sample: u8 = SAMPLE.load_at(PTR);
OCR4A::write(sample);
PTR += 1;
if PTR == SAMPLE.len() {
PTR = 0;
}
}
The basic problem is that it just doesn't work: instead of hearing the audio sample, I just hear garbled noise from the speaker.
Note that it is not "fully wrong", there is some semblance of the intended operation. For example, I can hear that the noise has a repeating structure with the right length. If I set the duty cycle sample
to 0 when PTR < SAMPLE.len() / 2
, then I can clearly hear that there is no sound for half of my sample length. So I think timer 3 and its interrupt handler are certainly working as intended.
So this leaves me thinking either I am configuring timer 4 incorrectly, or I am misunderstanding the role of OCR4A
and how the duty cycle needs to be set, or I could just have a fundamentally wrong understanding of how PWM-based audio synthesis is supposed to work.