引言:當(dāng)單片機(jī)遇上音樂
你是否想過,用一顆只有64KB Flash的廉價(jià)單片機(jī)(如PY32F030)就能播放復(fù)雜的MIDI音樂?傳統(tǒng)嵌入式音頻開發(fā)往往需要高性能芯片,但通過Rust語言的高效和現(xiàn)代嵌入式生態(tài),我們可以在資源受限的設(shè)備上實(shí)現(xiàn)比較滿意的的音樂效果。
本文將帶你探索:
- MIDI文件的解析工具
- 如何用Rust為ARM Cortex-M0單片機(jī)的定時(shí)器外設(shè)開發(fā)音頻蜂鳴器程序
硬件準(zhǔn)備:簡約而不簡單
核心器件:
- PY32_Rust_Dev Board:Py32f030主控,ARM Cortex-M0內(nèi)核,主頻48MHz,64KB Flash/8KB RAM
- 有源蜂鳴器(或無源蜂鳴器+驅(qū)動(dòng)電路或其他喇叭)
硬件連接:
- 使用蜂鳴器的兩端連接GND和PA0或PA3。
- 使用USB或SWD口供電
軟件設(shè)計(jì)
- Midi轉(zhuǎn)換工具由于單片機(jī)資源有限,因此需要先將Midi文件解析為直接用于控制頻率和延時(shí),因此開發(fā)了一個(gè)小工具用于轉(zhuǎn)換。部分代碼如下:
for track in &tracks {
match track {
Track::Midi(midi) => {
for m in midi {
let event = &m.event;
let tick_ms: u16 = (m.delta_time asf32 * one_tick_ms) asu16;
println!("track event: {:?}", m);
match event {
MidiMsg::Meta { msg } => match *msg {
Meta::SetTempo(tempo) => {
let bpm_ms = tempo asf32 / 1000.0;
one_tick_ms = bpm_ms / tpqn asf32;
println!("tempo: {} bpm, one tick ms: {}", bpm_ms, one_tick_ms);
}
_ => {}
},
MidiMsg::ChannelVoice { channel, msg } => match *msg {
ChannelVoiceMsg::NoteOn { note, velocity } => {
note_list.push(if velocity == 0 {
Note::new(*channel asu8, 0, tick_ms)
} else {
Note::new(*channel asu8, note, tick_ms)
});
}
ChannelVoiceMsg::NoteOff {
note: _,
velocity: _,
} => {
// 關(guān)閉聲音時(shí),需要將note設(shè)置為0,否則會(huì)一直播放
note_list.push(Note::new(*channel asu8, 1, tick_ms));
}
_ => {
continue;
}
},
_ => {}
}
}
}
Track::AlienChunk(alien_chunk) => {
for a in alien_chunk {
println!("alien chunk: {}", a);
}
}
}
}
可使用命令直接安裝在Cargo中。
cargo install --git https://github.com/hysonglet/midi2rust.git
使用方式如下:
midi2rust ~/Downloads/PLACE.MID place
執(zhí)行后將會(huì)生成rust數(shù)組如下:
struct Note {
channel: u8,
note: u8,
delay: u16,
}
pubconst MIDI_CONTENT: [Note; 5898] = [
Note {
channel: 0,
note: 60,
delay: 24705,
},
Note {
channel: 0,
note: 0,
delay: 192,
},
...
Note {
channel: 9,
note: 0,
delay: 3,
},
];
2.單片機(jī)播放MIDI音樂
單片機(jī)只需要遍歷音頻數(shù)組,執(zhí)行播放指定的頻率和延時(shí)即可,代碼如下:
#![no_std]
#![no_main]
use core::u16;
use hal::gpio::{Af, PinIoType, Speed};
// use hal::timer::advanced_timer::TimerChannel1Pin;
use hal::timer::advanced_timer::{AnyTimer, ChannelConfig, ChannelOutputConfig};
use py32f030_hal::gpio::gpioa::PA0;
use py32f030_hal::gpio::PinAF;
use py32f030_hal::{selfas hal, mode::Blocking, timer::advanced_timer::Channel};
use embassy_executor::Spawner;
use embassy_time::Timer;
// use hal::mcu::peripherals::TIM1;
use embedded_hal_027::Pwm;
use defmt::info;
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
asyncfn main(_spawner: Spawner) {
info!("time1 start...");
let p = hal::init(Default::default());
let gpioa = p.GPIOA.split();
let timer: AnyTimer<_, Blocking> = AnyTimer::new(p.TIM1).unwrap();
letmut pwm = timer.as_pwm();
pwm.set_channel_1_pin::<_, _>(Some(gpioa.PA3), Some(gpioa.PA0));
// 配置定時(shí)器
pwm.config(
/* 配置通道1 */
Some(ChannelConfig::default().ch(ChannelOutputConfig::default())),
None,
None,
None,
);
// 計(jì)數(shù)頻率為1M
pwm.set_frequency(1_000_000);
pwm.set_duty(Channel::CH1, 50);
// 設(shè)置計(jì)數(shù)周期為1000,則波形的頻率為 1000_000/1000 = 1K
// pwm.set_period(1000u16 - 1);
// let max_duty = pwm.get_max_duty();
// // 33%的占空比
// pwm.set_duty(Channel::CH1, max_duty / 3);
// 使能通道
pwm.enable(Channel::CH1);
// 開始計(jì)數(shù)器
pwm.start();
loop {
for note in &MIDI_CONTENT {
let delay = note.delay;
let channel = note.channel;
let note = note.note asu32;
// 只播放指定的 通道
if channel == 0 {
let period = (1000_000.0 / NOTE_FREQ[note asusize] - 1.0) asu16;
info!("freq: {}, note: {}, delay: {}", period, note, delay);
Timer::after_millis((delay) asu64).await;
pwm.set_period(period);
}
}
}
}
const NOTE_FREQ: [f32; 128] = [
// 8.18, /* 0 */
0.05, /* 0 */
8.66, 9.18, 9.72, 10.3, 10.91, 11.56, 12.25, 12.98, 13.75, 14.57, /* 1~10 */
15.43, 16.35, 17.32, 18.35, 19.45, 20.6, 21.83, 23.12, 24.5, 25.96, 27.5, /* 11~21 */
29.14, 30.87, 32.7, 34.65, 36.71, 38.89, 41.2, 43.65, 46.25, 49.0, 51.91, /* 22~32 */
55.0, 58.27, 61.74, 65.41, 69.3, 73.42, 77.78, 82.41, 87.31, 92.5, 48.99, /* 33~43 */
51.91, 55.00, 58.27, 61.74, 65.41, 69.30, 73.42, 77.78, 82.41, 87.31, 92.5, /* 44~54 */
98.0, 103.8, 110.0, 116.5, 123.5, 130.8, 138.6, 146.8, 155.6, 164.8, 174.6, /* 55~65 */
185.0, 196.0, 207.7, 220.0, 233.1, 246.9, 261.6, 277.2, 293.7, 311.1, 329.6, /* 66~76 */
349.2, 370.0, 392.0, 415.3, 440.0, 466.2, 493.9, 523.3, 554.4, /* 77~85 */
1174.66, 1244.51, 1318.51, 1396.91, 1479.98, 1567.98, 1661.22, 1760.0, 1864.66,
1975.53, /* 86~95 */
2093.0, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, 3135.96, 3322.44,
3520.0, /* 96~105 */
3729.31, 3951.07, 4186.01, 4434.92, 4698.64, 4978.03, 5274.04, 5587.65, 5919.91,
6271.93, /* 106~115 */
6644.88, 7040.0, 7458.62, 7902.13, 8372.02, 8869.84, 9397.27, 9956.06, 10548.08,
11175.3, /* 116~125 */
11839.82, 12543.85, /* 126~127 */
];
struct Note {
channel: u8,
note: u8,
delay: u16,
}
const MIDI_CONTENT: [Note; 896] = [
Note {
channel: 0,
note: 71,
delay: 26,
},
Note {
channel: 0,
note: 0,
delay: 465,
},
...
Note {
channel: 0,
note: 0,
delay: 0,
},
];
實(shí)驗(yàn)
如果下載了庫py32f030-hal
,執(zhí)行以下命令即可用usb串口或stlink或jlink下載并運(yùn)行:
# 使用jlink或stlink下載
cargo r --example embassy_pwm_midi
# 使用USB串口下載
# 生成bin文件
cargo objcopy --example embassy_pwm_midi -- -O binary embassy_pwm_midi.bin
# 串口下載
pyisp -s tty.usbserial-1140 -g -f embassy_pwm_midi.bin
固件大小為:
# Debug 編譯
text data bss dec hex filename
22336 72 5752 28160 6e00 embassy_pwm_midi
# Release 編譯
text data bss dec hex filename
19504 72 5752 25328 62f0 embassy_pwm_midi
結(jié)語
使用這個(gè)小demo,音質(zhì)和效果雖然有很多待改善的地方,但是仍然非常有趣,相比使用Arduno或C/C++去實(shí)現(xiàn)相同的功能,Rust的更加簡潔,在這個(gè)小嘗試中,我們可以感受到:
- Rust的零成本抽象在資源受限設(shè)備上的優(yōu)勢(shì)
- 現(xiàn)代嵌入式開發(fā)可以兼顧性能和開發(fā)效率
- 即使0.5美元的MCU也能實(shí)現(xiàn)復(fù)雜音頻功能
附錄
完整代碼已開源([GitHub鏈接]),歡迎繼續(xù)優(yōu)化!
- midi2rust:https://github.com/hysonglet/midi2rust
- py32f030-hal: https://github.com/hysonglet/py32f030-hal
- midi音樂庫:https://www.aigei.com/music/midi/
- midi頻率表:https://newt.phys.unsw.edu.au/jw/notes.html