Search code examples
rustembeddedavrrust-embedded

Equivalent of "tone()" in avr_hal


I'm trying to translate the following code from the Arduino IDE Into Rust using the avr_hal crate to make a passive buzzer play notes:

#include "pitches.h"

// notes in the melody:
int melody[] = {
  NOTE_C5, NOTE_D5, NOTE_E5, NOTE_F5, NOTE_G5, NOTE_A5, NOTE_B5, NOTE_C6};
int duration = 500;  // 500 miliseconds
 
void setup() {
 
}
 
void loop() {  
  for (int thisNote = 0; thisNote < 8; thisNote++) {
    // pin8 output the voice, every scale is 0.5 sencond
    tone(8, melody[thisNote], duration);
     
    // Output the voice after several minutes
    delay(1000);
  }
   
  // restart after two seconds 
  delay(2000);
}

I can't figure out how to use a Pwm pin to set the duty and frequency as it only exposes methods to set the duty.

#![no_std]
#![no_main]

mod pitches;

use arduino_hal::simple_pwm::{IntoPwmPin, Prescaler, Timer4Pwm};
use panic_halt as _;
use pitches::{NOTE_A5, NOTE_B5, NOTE_C5, NOTE_C6, NOTE_D5, NOTE_E5, NOTE_F5, NOTE_G5};

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);

    let timer = Timer4Pwm::new(dp.TC4, Prescaler::Prescale8);

    let mut buzzer = pins.d8.into_output().into_pwm(&timer);

    // notes in the melody:
    let melody: [isize; 8] = [
        NOTE_C5, NOTE_D5, NOTE_E5, NOTE_F5, NOTE_G5, NOTE_A5, NOTE_B5, NOTE_C6,
    ];

    loop {
        melody.iter().for_each(|note| {
            // TODO: How do I use the PWM buzzer output here???
            arduino_hal::delay_ms(1000);
        });

        arduino_hal::delay_ms(2000);
    }
}

I'm just starting to learn arduino and electronics in general and honestly I don't understand 100% how the tone function works under the hood.

I would appreciate to get an answer that explains to me how that function works as well as helping me understand the core concepts :D


Solution

  • I achieved the functionality you are looking for. It's not a very elegant solution, and you can no longer use TIMER1, but it's something.

    #![no_std]
    #![no_main]
    #![feature(abi_avr_interrupt)]
    
    use arduino_hal::{
        hal::port::Dynamic,
        port::{mode::Output, Pin},
    };
    use panic_halt as _;
    use core::cell;
    
    static BUZZER: avr_device::interrupt::Mutex<cell::Cell<Option<Pin<Output, Dynamic>>>> =
        avr_device::interrupt::Mutex::new(cell::Cell::new(None));
    
    #[arduino_hal::entry]
    fn main() -> ! {
        let dp = arduino_hal::Peripherals::take().unwrap();
        let pins = arduino_hal::pins!(dp);
        //let mut serial = arduino_hal::default_serial!(dp, pins, 57600);
    
        // Timer Configuration:
        // - WGM = 4: CTC mode (Clear Timer on Compare Match)
        // - Prescaler 256
        // - OCR1A = 31249
        //
        // => F = 16 MHz / (256 * (1 + 237.89)) = 261.626 Hz 
        //     (^ this formula I deduced from reading the datasheet)
        //
        let tmr1 = dp.TC1;
        tmr1.tccr1a.write(|w| w.wgm1().bits(0b00));
        tmr1.tccr1b
            .write(|w| w.cs1().prescale_256().wgm1().bits(0b01));
    
        // Enable the timer interrupt
        tmr1.timsk1.write(|w| w.ocie1a().set_bit());
    
        let mut buzzer = pins.d2.into_output().downgrade();
        buzzer.set_low();
    
        unsafe {
            avr_device::interrupt::enable();
        }
    
        avr_device::interrupt::free(|cs| BUZZER.borrow(cs).replace(Some(buzzer)));
    
        loop {
            tmr1.ocr1a.write(|w| w.bits(238));
            tmr1.tcnt1.write(|w|{ w.bits(0) });
            arduino_hal::delay_ms(1000);
    
            tmr1.ocr1a.write(|w| w.bits(212));
            tmr1.tcnt1.write(|w|{ w.bits(0) });
            arduino_hal::delay_ms(1000);
    
            tmr1.ocr1a.write(|w| w.bits(189));
            tmr1.tcnt1.write(|w|{ w.bits(0) });
            arduino_hal::delay_ms(1000);
    
            tmr1.ocr1a.write(|w| w.bits(178));
            tmr1.tcnt1.write(|w|{ w.bits(0) });
            arduino_hal::delay_ms(1000);
    
            tmr1.ocr1a.write(|w| w.bits(159));
            tmr1.tcnt1.write(|w|{ w.bits(0) });
            arduino_hal::delay_ms(1000);
    
            tmr1.ocr1a.write(|w| w.bits(141));
            tmr1.tcnt1.write(|w|{ w.bits(0) });
            arduino_hal::delay_ms(1000);
    
            tmr1.ocr1a.write(|w| w.bits(126));
            tmr1.tcnt1.write(|w|{ w.bits(0) });
            arduino_hal::delay_ms(1000);
    
        }
    }
    
    #[avr_device::interrupt(atmega328p)]
    fn TIMER1_COMPA() {
        avr_device::interrupt::free(|cs| {
            if let Some(mut x) = BUZZER.borrow(cs).take() {
                x.toggle();
    
            BUZZER.borrow(cs).replace(Some(x));
            }
        })
    }
    

    Explaining a little bit, after setting up the timer, the tone change is done setting the frecuence of the timer-interruptions. The next line is a timer reset, be cause if not done, the program will halt for a moment at the 10th tone (no idea why)

    tmr1.ocr1a.write(|w| w.bits(238));
    tmr1.tcnt1.write(|w|{ w.bits(0) });
    

    If you want to stop the buzzer, just set the frecuency to an impossible to hear one.