はじめに
この記事は TRIAL&RetailAI Advent Calendar 2024 の23日目の記事です。昨日は @Plath さんの 「GitLabからGitHubに移行した時に直面した課題と対策」 という記事でした。実際に業務で役に立ちそうな記事でした。
それでは、本題の「🎄X'masにRustで南無阿弥陀仏🙏」に入っていきます。
最近、プログラミング言語「Rust」で動くマイコン「Baker link. Dev Rev. 1 」を手に入れました。
こちらは、Raspberry Piで使われているRP2040マイコン搭載されています。簡単な環境構築で、Rustをつかって手軽に組み込み開発を行うことができます。
そんなマイコンボードを使ってRustをつかった組み込み開発を入門してみました。
やってみたこと
手元にあった仏教音楽再生チップを繋げて遊んでみました。
Aliexpressで売られているこのチップには、8曲のよくわからん宗教音楽が入っています。
このチップはマイコンから制御せずとも簡単に動きますが、あえて制御してみます。
また現在再生している楽曲番号を保持し、マイコン再起動時に前回再生した楽曲を再生するようにしてみます。
環境構築につまずいた
チュートリアルどおり開発環境を構築してみましたが、Baker link. Envが起動しませんでした。結論としては、チュートリアルに問題はなかったです。
詳細情報を確認すると、/opt/homebrew/*/libssl.3.dylib
が見つからないと表示されていました。
Termination Reason: Namespace DYLD, Code 1 Library missing
Library not loaded: /opt/homebrew/*/libssl.3.dylib
Referenced from: <05A125A9-C91A-3877-BF5D-8FB1BF3F82C6> /Applications/Baker link. Env.app/Contents/MacOS/baker-link-env
Reason: tried: '/opt/homebrew/*/libssl.3.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/*/libssl.3.dylib' (no such file), '/opt/homebrew/*/libssl.3.dylib' (no such file)
(terminated at launch; ignore backtrace)
どうやら、Intel MacからApple Silicon Macに移行した際、Homebrewのデフォルトのインストール先が/usr/local
配下になっていることが原因でした。
Apple Silicon Macでは/opt/homebrew
配下が正しいようです。
※ 詳しくはこちら参照
Apple SiliconにおけるHomebrewのベストプラクティス
そこでHomebrewを入れ直したところ、Baker link. Envが正常に起動するようになりました。
# インストール済みのパッケージバックアップ
brew bundle dump --global
# /usr/local配下のHomebrewを削除
export PATH=/usr/local/bin:${PATH}
brew uninstall $(brew list)
# Homebrewをアンインストール
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)"
# Homebrewをインストール
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# バックアップしたパッケージをインストール
brew bundle --global
# パスを追加
export PATH="/opt/homebrew/bin:$PATH"
# ディレクトリの確認
# /opt/homebrewと出ればOK
brew --prefix
つくってみたもの
動作様子
マイコンのボタンを押した時の楽曲再生の様子👇
マイコンの電源ON/OFF時の楽曲再生の様子👇
回路図
仏教音楽再生チップのA0、A1ピンに電圧を加えると、次もしくは前の曲に進めることができます。今回は、A0ピンのみマイコンのGPIO15で制御することにします。
フローチャート
ソースコード
全体のコードはこちらになります。
かいつまんで説明していきます。
#![no_std]
#![no_main]
use defmt::*;
use defmt_rtt as _;
use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;
use hal::pac::interrupt;
use panic_probe as _;
use rp2040_hal::{self as hal, halt};
use rp2040_flash::flash;
use core::cell::UnsafeCell;
// bootloaderの設定
#[link_section = ".boot2"]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;
const XTAL_FREQ_HZ: u32 = 12_000_000u32;
// グローバル変数宣言
type GreenLedPin =
hal::gpio::Pin<hal::gpio::bank0::Gpio22, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>;
type RedLedPin =
hal::gpio::Pin<hal::gpio::bank0::Gpio20, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>;
type OrangeLedPin =
hal::gpio::Pin<hal::gpio::bank0::Gpio21, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>;
type ButtonPin =
hal::gpio::Pin<hal::gpio::bank0::Gpio23, hal::gpio::FunctionSioInput, hal::gpio::PullUp>;
type SoundPin = hal::gpio::Pin<hal::gpio::bank0::Gpio15, hal::gpio::FunctionSioOutput, hal::gpio::PullDown>;
type DelayTimer = hal::Timer;
type LedAndButton = (GreenLedPin, RedLedPin, OrangeLedPin, ButtonPin, SoundPin,DelayTimer);
static GLOBAL_PINS: critical_section::Mutex<core::cell::RefCell<Option<LedAndButton>>> =
critical_section::Mutex::new(core::cell::RefCell::new(None));
// Flashメモリの1ブロック(4096バイト)を操作するための構造体
#[repr(C, align(4096))]
struct FlashBlock {
data: UnsafeCell<[u8; 4096]>,
}
// impl : struct上で動作する関数を定義
impl FlashBlock {
// Flashメモリ上のdata領域のアドレスを取得
#[inline(never)]
fn addr(&self) -> u32 {
&self.data as *const _ as u32
}
/*
データの読み取り
- Flashメモリのデータを参照
- UnsafeCellを使い、データの可変参照を取得
*/
#[inline(never)]
fn read(&self) -> &[u8; 4096] {
let addr = self.addr();
unsafe { &*(*(addr as *const Self)).data.get() }
}
/*
データの書き込み
- Flashメモリの指定アドレスにデータを書き込む
*/
unsafe fn write_flash(&self, data: &[u8; 4096]) {
let addr = self.addr() - 0x10000000;
defmt::assert!(addr & 0xfff == 0);
// Flashメモリの操作中に書き込みが発生しないように保護
cortex_m::interrupt::free(|_cs| {
flash::flash_range_erase_and_program(addr, data, true);
});
}
}
unsafe impl Sync for FlashBlock {}
/*
静的変数を設定
- Flashメモリの.rodataセクションに配置。
- 初期値としてすべて0x55で埋めたデータ(4096バイト)を保持
*/
#[link_section = ".rodata"]
static CURRENT_TRACK: FlashBlock = FlashBlock {
data: UnsafeCell::new([0x00u8; 4096]),
};
#[rp2040_hal::entry]
fn main() -> ! {
info!("Program start! (Interrupt) ");
let mut pac = hal::pac::Peripherals::take().unwrap();
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
let clocks = hal::clocks::init_clocks_and_plls(
XTAL_FREQ_HZ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
let mut timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
let sio = hal::Sio::new(pac.SIO);
let pins = hal::gpio::Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
// LED:GPIO22(Green), GPIO21(orange), GPIO20(RED)
let green_led = pins.gpio22.into_push_pull_output();
let orange_led = pins.gpio21.into_push_pull_output();
let mut red_led = pins.gpio20.into_push_pull_output();
red_led.set_high().unwrap();
// GPIO15
let mut sound_pin: rp2040_hal::gpio::Pin<rp2040_hal::gpio::bank0::Gpio15, rp2040_hal::gpio::FunctionSio<rp2040_hal::gpio::SioOutput>, rp2040_hal::gpio::PullDown> = pins.gpio15.into_push_pull_output();
// Flashメモリに保存したデータの読み込み
let mut read_data: [u8; 4096] = *CURRENT_TRACK.read();
let current_track_number = read_data[0];
// 前回再生された曲の再生
if current_track_number != 0{
for _ in 0..current_track_number{
sound_pin.set_high().unwrap();
timer.delay_ms(50);
sound_pin.set_low().unwrap();
timer.delay_ms(50);
}
}
// 割り込みを行うGPIOの設定
// Button:GPIO23
let button = pins.gpio23.into_pull_up_input();
button.set_interrupt_enabled(hal::gpio::Interrupt::EdgeLow, true);
// 変数を格納
critical_section::with(|cs| {
GLOBAL_PINS
.borrow(cs)
.replace(Some((green_led, red_led, orange_led, button, sound_pin, timer)));
});
// 割り込み設定の登録
unsafe {
hal::pac::NVIC::unmask(hal::pac::Interrupt::IO_IRQ_BANK0);
}
let core = hal::pac::CorePeripherals::take().unwrap();
let psm = pac.PSM;
psm.frce_off().modify(|_, w| w.proc1().set_bit());
while !psm.frce_off().read().proc1().bit_is_set() {
cortex_m::asm::nop();
}
psm.frce_off().modify(|_, w| w.proc1().clear_bit());
// JEDEC IDとユニークIDを取得
let jedec_id: u32 = unsafe { cortex_m::interrupt::free(|_cs| flash::flash_jedec_id(true)) };
let mut unique_id = [0u8; 8];
unsafe { cortex_m::interrupt::free(|_cs| flash::flash_unique_id(&mut unique_id, true)) };
info!("JEDEC ID {:x}", jedec_id);
info!("Unique ID {:#x}", unique_id);
info!("Addr of flash block is {:#x}", CURRENT_TRACK.addr());
// 無限ループ
loop {
cortex_m::asm::wfi();
}
}
// 割り込み処理
#[hal::pac::interrupt]
fn IO_IRQ_BANK0() {
static mut LED_AND_BUTTON: Option<LedAndButton> = None;
if LED_AND_BUTTON.is_none() {
critical_section::with(|cs| {
*LED_AND_BUTTON = GLOBAL_PINS.borrow(cs).take();
});
}
if let Some(gpios) = LED_AND_BUTTON {
let (green_led, red_led, orange_led, button, sound_pin, timer) = gpios;
if button.interrupt_status(hal::gpio::Interrupt::EdgeLow) {
info!("Button pressed");
// 読み込み処理
let mut read_data: [u8; 4096] = *CURRENT_TRACK.read();
let current_track_number = read_data[0];
// 16進数で表示
info!("Contents start with {:#x}", read_data[0]);
// 10進数で表示
info!("Contents start with {}", read_data[0]);
sound_pin.set_high().unwrap();
timer.delay_ms(50);
sound_pin.set_low().unwrap();
timer.delay_ms(50);
if current_track_number > 8 {
// 初期値にもどす
read_data[0] = 0x00u8;
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
} else {
// 読み込んだdataの先頭バイトをインクリメント
read_data[0] = read_data[0].wrapping_add(1);
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
// 書き込み
unsafe { CURRENT_TRACK.write_flash(&read_data) };
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
button.clear_interrupt(hal::gpio::Interrupt::EdgeLow);
}
}
}
Flashメモリへのデータの読み込み・書き込み
Flashメモリは不揮発性なので、電源を切っても保存したデータは消えません。
Flashメモリにデータを読み込み・書き込みは、初見ではよくわかりませんでした。
そんな中、使えそうなクレート(外部ライブラリ)を探していたところ、「rp2040-flash」を見つけました。
これをcargo
コマンドを実行してクレートを追加します。
cargo add rp2040-flash
すると、Cargo.toml
のdependencies
に下記が追加されます。
rp2040-flash = "0.5.1"
ソースコードへの組み込みは、サンプルコードを参考にしました。
前回再生した楽曲への移動
main
関数でマイコン起動時に一度だけ実行します。
Flashメモリにインクリメントした回数だけ、GPIO15ピンをON/OFFします。
// Flashメモリに保存したデータの読み込み
let mut read_data: [u8; 4096] = *CURRENT_TRACK.read();
let current_track_number = read_data[0];
// 前回再生された曲の再生
if current_track_number != 0{
for _ in 0..current_track_number{
sound_pin.set_high().unwrap();
timer.delay_ms(50);
sound_pin.set_low().unwrap();
timer.delay_ms(50);
}
}
割り込み処理の設定
割り込み処理はBaker link. Dev Rev. 1のチュートリアル通りです。
詳細はそちらを確認するとわかりやすいです。
要点としては割り込み関数(コード内ではIO_IRQ_BANK0
関数)内で変数を使用するため、「所有権限を借用する」といった操作が必要になります。初見だと難解に思えます。。。
critical_section::with(|cs| {
GLOBAL_PINS
.borrow(cs) // 所有権を借用
.replace(Some((green_led, red_led, orange_led, button, sound_pin, timer)));
});
ボタン押下時の割り込み処理
ボタン押下時の割り込み処理は下記のような流れになっています。
- GPIOピンをHIGH → LOW
- Flashメモリのデータを読み込み
a. インクリメント
b. 楽曲数を超える場合は初期化 - Flashメモリにデータを書き込み
#[hal::pac::interrupt]
fn IO_IRQ_BANK0() {
static mut LED_AND_BUTTON: Option<LedAndButton> = None;
if LED_AND_BUTTON.is_none() {
critical_section::with(|cs| {
*LED_AND_BUTTON = GLOBAL_PINS.borrow(cs).take();
});
}
if let Some(gpios) = LED_AND_BUTTON {
let (green_led, red_led, orange_led, button, sound_pin, timer) = gpios;
// 割り込みされたか確認
if button.interrupt_status(hal::gpio::Interrupt::EdgeLow) {
info!("Button pressed");
// 読み込み処理
let mut read_data: [u8; 4096] = *CURRENT_TRACK.read();
let current_track_number = read_data[0];
// 16進数で表示
info!("Contents start with {:#x}", read_data[0]);
// 10進数で表示
info!("Contents start with {}", read_data[0]);
sound_pin.set_high().unwrap();
timer.delay_ms(50);
sound_pin.set_low().unwrap();
timer.delay_ms(50);
if current_track_number > 8 {
// 初期値にもどす
read_data[0] = 0x00u8;
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
} else {
// 読み込んだdataの先頭バイトをインクリメント
read_data[0] = read_data[0].wrapping_add(1);
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
// 書き込み
unsafe { CURRENT_TRACK.write_flash(&read_data) };
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
// 割り込み情報をクリア
button.clear_interrupt(hal::gpio::Interrupt::EdgeLow);
}
}
}
おわりに
今回、Rustで実装できるマイコン「Baker link. Dev Rev. 1」で遊んでみました。。
C言語ベースで扱うことができるArduinoに比べ、そもそもRustの仕様をよく知っていないと難解な印象です。
「所有権?」「借用?」と分からないことだらけなので、まずは基本を知った方が良さそうですね。継続してこのマイコンで遊びつつ、Rustを学んでみようかと思います。
明日は@kakine_juriさんの「CUEって何?」という記事になります。
ぜひ、次の記事もお楽しみください!
RetailAIとTRIALではエンジニアを募集しています。
もし興味がある方はご連絡ください!