avr_hal中Arduino tone()函数的等效实现及Rust代码改写
问题描述
尝试将Arduino IDE中的C代码转换为使用avr_hal crate的Rust代码,驱动无源蜂鸣器播放音符,但不清楚如何通过PWM引脚设置频率(当前仅能设置占空比),同时希望了解Arduino tone()函数的底层工作原理。
原Arduino C代码:
#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); }
当前未完成的Rust代码:
#![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); } }
Arduino
tone()函数底层原理科普 - 核心逻辑:
tone()通过AVR单片机的定时器模块生成指定频率的50%占空比方波,驱动无源蜂鸣器振动发声。无源蜂鸣器本身没有振荡源,必须依赖外部交替变化的电平才能振动。 - 底层实现:
- 配置定时器的预分频值,把系统时钟(通常16MHz)分频到定时器可处理的范围;
- 根据目标频率计算定时器的匹配寄存器值,让定时器按指定周期触发引脚电平翻转;
- 当指定
duration时,定时器会在对应时间后停止输出,引脚恢复到初始状态。
- 关键概念:
- 频率:音符的音调由频率决定,比如C5对应523Hz,意味着引脚每秒完成523次高低电平切换;
- 50%占空比:方波的高低电平持续时间相等,这是驱动无源蜂鸣器的最优占空比,能让蜂鸣器振动效率最高;
- 定时器预分频:系统时钟频率过高,直接用定时器无法生成低频信号,预分频就是把时钟频率降低,让定时器可以生成对应频率的周期信号。
Rust代码解决方案(基于avr_hal)
avr_hal的simple_pwm模块需要通过配置定时器的TOP值来设置输出频率,再配合50%占空比实现方波输出。以下是完整可运行代码:
#![no_std] #![no_main] mod pitches; use arduino_hal::simple_pwm::{IntoPwmPin, Prescaler, Timer4Pwm}; use arduino_hal::Delay; 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 mut delay = Delay::new(); // 系统时钟频率(Arduino Uno为16MHz) const F_CPU: u32 = 16_000_000; // 预分频值,和初始化定时器时的设置一致 const PRESCALER: u32 = 8; // 初始化定时器4,启用PWM功能 let mut timer = Timer4Pwm::new(dp.TC4, Prescaler::Prescale8); // 将D8引脚转为PWM输出,并设置50%占空比(方波) let mut buzzer = pins.d8.into_output().into_pwm(&timer); buzzer.set_duty(buzzer.max_duty() / 2); // 音符数组,转为u32类型用于频率计算 let melody: [u32; 8] = [ NOTE_C5 as u32, NOTE_D5 as u32, NOTE_E5 as u32, NOTE_F5 as u32, NOTE_G5 as u32, NOTE_A5 as u32, NOTE_B5 as u32, NOTE_C6 as u32, ]; loop { for &freq in &melody { // 计算定时器TOP值,公式:(系统时钟 / (预分频 * 2 * 目标频率)) - 1 let ocr = (F_CPU / (PRESCALER * 2 * freq)) - 1; // 设置定时器TOP值,决定PWM输出频率 timer.set_top(ocr as u16); // 启用PWM输出,开始发声 buzzer.enable(); // 持续发声500ms,对应原C代码的duration delay.delay_ms(500u16); // 禁用PWM输出,停止发声 buzzer.disable(); // 等待剩余500ms,总间隔1000ms delay.delay_ms(500u16); } // 循环结束后等待2秒再重启 delay.delay_ms(2000u16); } }
代码关键点说明:
- TOP值计算:通过系统时钟、预分频和目标频率,算出定时器的周期匹配值,直接决定PWM输出的频率;
- 50%占空比设置:用
max_duty()/2让引脚高低电平持续时间相等,生成标准方波; - 发声/静音控制:通过
enable()和disable()控制PWM输出,对应原C代码中tone()的发声和停止逻辑。
内容的提问来源于stack exchange,提问作者JasterV
相关产品推荐
相关产品推荐

