Rust
assembly
ARM

Rust(FFIを活用)micro:bitをいじってみる


はじめに

こちらの派生記事(真似してやってみた)となります。

Rustでmicro:bit

リポジトリはこちら。

segfo/microbit


よくあるHello world

シリアル経由でHello worldして見るサンプル

#![no_std]

#![no_main]
use microbit;
use microbit::hal::prelude::*;
use microbit::hal::serial;
use microbit::hal::serial::BAUD9600;
use core::fmt::Write;
use core::panic::PanicInfo;
extern crate cortex_m_rt;
use cortex_m_rt::*;

#[entry]
fn main() -> ! {
if let Some(p) = microbit::Peripherals::take() {
let mut gpio = p.GPIO.split();
let tx = gpio.pin24.into_push_pull_output().downgrade();
let rx = gpio.pin25.into_floating_input().downgrade();
let (mut tx, rx) = serial::Serial::uart0(p.UART0, tx, rx,BAUD9600 ).split();
u32);
let _ = write!(tx, "Hello world!!!!");

}
loop {}
}

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
loop {}
}

はい。

というわけでですね、できるんですけど

プログラムが終了するとWaitもなにもない無限ループの実装じゃないですか。

なんか電気もったいないですよね。低電力にしたい。

※そんなに発熱もしないし、うまいこと制御掛かってるのかもしれないけど、気分的に。

Intel系CPUで言うところのhlt命令ってARMでなんだろう?


ARMのアセンブリ言語で関数を作ってみる

とりあえず、ARMのアセンブリ言語でhlt(割り込みがあるまで休止)する命令を書くことができればOK!

というわけで、まずはこういうファイルを作ってみた

.text

.global wait_for_interrupt

wait_for_interrupt:
push {lr}
wfi
pop {pc}


  1. 呼び出し元が、lr(Link register)に入っているのでそれをまずスタックに押し込む

  2. wfi(Wait-for-interrupt)命令を発行(これがIntel系CPUで言うhltに相当)

  3. pc(Program counter)にリンクレジスタの値をpopしてリターン(Intel系のret命令に相当)

この関数をRustから呼び出せば、ループで電力を使わないエコなHello worldプログラムが完成。

で、いざリンク


リンクしてみる その1:設定ファイルを書く

ここからが結構重要なので、色々書きます。

リンカの設定やリンカスクリプトの設定っぽい。ほぼデフォルト。

唯一違うのは、GCCのリンカ(LD)を使うように明示的にしているところ。

(Rust 2018ではLLVMのリンカ(LLD)がデフォルトなので、)


いろいろ書き出す前にARMのCPUモードについて

ARMの機械語モードには2種類あります。


  • Thumbモード

  • ARMモード

で、これらが噛み合わないとうまく動きませんというか別の機械語として認識されます。(blx命令というので機械語の処理機能を切り替えることができれば問題ないですが、どうもこのmicro:bitのMPUは対応していない模様)

運が良ければハードフォールトして止まりますが、運が悪いと動き続けてしまうので意図した挙動になりません。

で、何かの手違いで食い違いが発生するようなミスをした時、LDであれば「なんか機械語が混在してるよ?大丈夫?とりあえず止めとくね」という感じでエラーを吐いてくれます。

LLDだとこの警告がなくリンクされてしまうことがあります。

多分、LLVMのアセンブラを使っていれば教えてくれたかも。

”Rustコンパイラ:LLVM、アセンブラ:GCC、リンカ:LLD”という組み合わせが悪かった気がする。

今の設定ファイル上では”Rustコンパイラ:LLVM、アセンブラ・リンカ:GCC”という組み合わせ

何かの手違いで食い違いが発生するようなミス

Rust側:Thumbモード(--target=thumbv6m-none-eabi)これはThumbモードで吐かれる

アセンブラ:GCCの既定(arm-none-eabi-gcc -g -c)これはARMモードで吐かれる。

こういうミスをするとアウトです。リンカが教えてくれない場合はそれなりに知識がなければ気づけません。(最初の10時間位はマジで気づかなかった。)

とはいえ、これはあくまでも保険対策でしか無いので根本対策は知識つけろって話ですね。


本命の設定ファイル

# Configure builds for our target, the micro:bit's architecture

[target.thumbv6m-none-eabi]
# Execute binary using gdb when calling cargo run
runner = "arm-none-eabi-gdb"
# Tweak to the linking process required by the cortex-m-rt crate
rustflags = [
"-C", "link-arg=-Tlink.x",
# The LLD linker is selected by default
"-C", "linker=arm-none-eabi-ld",
]

# Automatically select this target when cargo building this project
[build]
target = "thumbv6m-none-eabi"


リンク その2:Rustのビルドスクリプト

ここで、アセンブラとオブジェクトアーカイバ的なやつを実行させます。

use std::process::Command;

use std::env;
use std::path::Path;

fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
// wfi命令が書かれた関数のあるファイル
let srcs=["io.S"];
// myffi.aというファイルが出来上がって、こいつがリンクされる。
let lib_name="myffi";
// ファイル数分アーカイブを作っていく。今は一つだからあんまり意味はない
for i in 0..srcs.len(){
let src=Path::new(srcs[i]).file_stem().unwrap().to_str().unwrap();
Command::new("arm-none-eabi-as").args(&[&format!("src/asm/{}",srcs[i]),"-g","-mthumb","-o"])
.arg(&format!("{}/{}.o", out_dir,src))
.status().unwrap();
Command::new("arm-none-eabi-gcc-ar").args(&["crus", &format!("lib{}.a",lib_name), &format!("{}.o",src)])
.current_dir(&Path::new(&out_dir))
.status().unwrap();
}
// リンクサーチパス・リンク方法を指定する。
println!("cargo:rustc-link-search=native={}", out_dir);
println!("cargo:rustc-link-lib=static={}",lib_name);
}

はい、できました。


ソースコード(main.rs)

#![no_std]

#![no_main]
use microbit;
use microbit::hal::prelude::*;
use microbit::hal::serial;
use microbit::hal::serial::BAUD9600;
use core::fmt::Write;
use core::panic::PanicInfo;
extern crate cortex_m_rt;
use cortex_m_rt::*;

extern{
fn wait_for_interrupt();
}

#[entry]
fn main() -> ! {
if let Some(p) = microbit::Peripherals::take() {
let mut gpio = p.GPIO.split();
let tx = gpio.pin24.into_push_pull_output().downgrade();
let rx = gpio.pin25.into_floating_input().downgrade();
let (mut tx, rx) = serial::Serial::uart0(p.UART0, tx, rx,BAUD9600 ).split();
let _ = write!(tx, "cortex_m_rt entry.\r\n");
let _ = write!(tx,"heap_start : {:x}\r\n",cortex_m_rt::heap_start() as u32);
unsafe{wait_for_interrupt();}
let _ = write!(tx, "get interrupt\r\n");

}
loop {
unsafe{wait_for_interrupt();}
}
}

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
loop {
unsafe{wait_for_interrupt();}
}
}


デモ+wfi命令がうまく動いているか動作確認

wfi命令が動いているかどうかの確認はデバッグ割り込みを発生させて確認します。

やり方は簡単。

continue後に、^Cしてデバッガに制御を移すと起こります。

で、またcontinueすると、続きの命令が実行されます。

予想以上に検証動画がちっちゃい。

クリックすると大きくなるので大きくしてみてほしいです。

embedded.gif

それでは皆様、良いお年を。