メモリマップドレジスタ
組み込みシステムは、通常の Rust コードを実行して RAM 内でデータをやり取りするだけでは、できることに限界があります。システムに情報を入れたり、システムから情報を取り出したりしたい場合(LED を点滅させる、ボタン押下を検出する、ある種のバスでチップ外のペリフェラルと通信する、といったことです)、ペリフェラルとその「メモリマップドレジスタ」の世界に足を踏み入れなければなりません。
マイクロコントローラ内のペリフェラルにアクセスするために必要なコードは、次のいずれかのレベルですでに書かれていることがよくあります。
- マイクロアーキテクチャクレート - この種のクレートは、マイクロコントローラが使用しているプロセッサコアに共通する有用なルーチンと、その特定の種類のプロセッサコアを使うすべてのマイクロコントローラに共通するペリフェラルを扱います。たとえば cortex-m クレートは、すべての Cortex-M ベースのマイクロコントローラで共通の、割り込みの有効化と無効化を行う関数を提供します。また、すべての Cortex-M ベースのマイクロコントローラに含まれている
SysTickペリフェラルにもアクセスできます。 - Peripheral Access Crate (PAC) - この種のクレートは、使用している特定の型番のマイクロコントローラ向けに定義された、さまざまなメモリマップドレジスタに対する薄いラッパーです。たとえば、Texas Instruments Tiva-C TM4C123 シリーズ向けの tm4c123x や、ST-Micro STM32F30x シリーズ向けの stm32f30x です。ここでは、マイクロコントローラの Technical Reference Manual に記載された各ペリフェラルの操作手順に従って、レジスタを直接操作することになります。
- HAL クレート - これらのクレートは、embedded-hal で定義されているいくつかの共通トレイトを実装することで、多くの場合、特定のプロセッサ向けにより使いやすい API を提供します。たとえば、この種のクレートは
Serial構造体を提供し、そのコンストラクタは適切な GPIO ピンの組とボーレートを受け取り、データ送信のための何らかのwrite_byte関数を提供するかもしれません。embedded-hal の詳細については Portability の章を参照してください。 - ボードクレート - これらのクレートは、使用している特定の開発キットやボードに合うように各種ペリフェラルや GPIO ピンを事前設定することで、HAL クレートよりさらに一歩進んだものです。たとえば、STM32F3DISCOVERY ボード向けの stm32f3-discovery があります。
ボードクレート
組み込み Rust が初めてなら、ボードクレートは最適な出発点です。学び始めたばかりの段階では圧倒されかねないハードウェアの詳細をうまく抽象化してくれますし、LED をオンまたはオフにするといった標準的な作業も簡単にしてくれます。公開される機能はボードによって大きく異なります。この本はハードウェア非依存であることを目指しているため、ボードクレートはこの本では扱いません。
STM32F3DISCOVERY ボードで試してみたい場合は、ボード上の LED を点滅させたり、コンパスや Bluetooth にアクセスしたりする機能などを提供する stm32f3-discovery ボードクレートを見ることを強くおすすめします。Discovery 本では、ボードクレートの使い方について優れた入門が提供されています。
しかし、まだ専用のボードクレートがないシステムで作業している場合や、既存のクレートでは提供されていない機能が必要な場合は、マイクロアーキテクチャクレートから始めて、下の層から見ていきましょう。
マイクロアーキテクチャクレート
すべての Cortex-M ベースのマイクロコントローラに共通する SysTick ペリフェラルを見てみましょう。cortex-m クレートにはかなり低レベルな API があり、次のように使えます。
#![no_std]
#![no_main]
use cortex_m::peripheral::{syst, Peripherals};
use cortex_m_rt::entry;
use panic_halt as _;
#[entry]
fn main() -> ! {
let peripherals = Peripherals::take().unwrap();
let mut systick = peripherals.SYST;
systick.set_clock_source(syst::SystClkSource::Core);
systick.set_reload(1_000);
systick.clear_current();
systick.enable_counter();
while !systick.has_wrapped() {
// ループ
}
loop {}
}
SYST 構造体上の関数は、このペリフェラルについて ARM Technical Reference Manual で定義されている機能にかなり近く対応しています。この API には「X ミリ秒待つ」のようなものはなく、while ループを使って自分たちで素朴に実装しなければなりません。Peripherals::take() を呼び出すまでは SYST 構造体にアクセスできない点に注意してください。これは、プログラム全体に SYST 構造体が 1 つしか存在しないことを保証する特別なルーチンです。これについて詳しくは Peripherals セクションを参照してください。
Peripheral Access Crate (PAC) を使う
すべての Cortex-M に含まれる基本的なペリフェラルだけに限定していては、組み込みソフトウェア開発はあまり先へ進みません。いずれ、使っている特定のマイクロコントローラに固有のコードを書く必要が出てきます。この例では、Texas Instruments の TM4C123、つまり 80MHz 動作で 256 KiB の Flash を備えた中程度の Cortex-M4 があると仮定しましょう。このチップを利用するために tm4c123x クレートを取り込みます。
#![no_std]
#![no_main]
use panic_halt as _; // panic ハンドラ
use cortex_m_rt::entry;
use tm4c123x;
#[entry]
pub fn init() -> (Delay, Leds) {
let cp = cortex_m::Peripherals::take().unwrap();
let p = tm4c123x::Peripherals::take().unwrap();
let pwm = p.PWM0;
pwm.ctl.write(|w| w.globalsync0().clear_bit());
// モード = 1 => アップ/ダウンカウントモード
pwm._2_ctl.write(|w| w.enable().set_bit().mode().set_bit());
pwm._2_gena.write(|w| w.actcmpau().zero().actcmpad().one());
// 528 サイクル (アップとダウンがそれぞれ 264) = ビデオラインごとに 4 ループ (2112 サイクル)
pwm._2_load.write(|w| unsafe { w.load().bits(263) });
pwm._2_cmpa.write(|w| unsafe { w.compa().bits(64) });
pwm.enable.write(|w| w.pwm4en().set_bit());
}
PWM0 ペリフェラルには、先ほど SYST ペリフェラルにアクセスしたときとまったく同じ方法でアクセスしていますが、tm4c123x::Peripherals::take() を呼び出している点だけが異なります。このクレートは svd2rust を使って自動生成されているため、レジスタフィールドのアクセサ関数は数値引数ではなくクロージャを受け取ります。コード量が多く見えるかもしれませんが、Rust コンパイラはこれを使って多くのチェックを行い、そのうえで手書きのアセンブラにかなり近いマシンコードを生成できます。自動生成されたコードが、特定のアクセサ関数に渡され得るすべての引数が有効であると判断できない場合(たとえば、SVD ではレジスタが 32 ビットと定義されているものの、その 32 ビット値の一部に特別な意味があるかどうかが示されていない場合)、その関数は unsafe としてマークされます。上の例では、bits() 関数を使って load および compa サブフィールドを設定している箇所で、これを確認できます。
読み取り
read() 関数は、このチップ向けのメーカーの SVD ファイルで定義されている、このレジスタ内のさまざまなサブフィールドへの読み取り専用アクセスを提供するオブジェクトを返します。この特定のチップ上のこの特定のペリフェラルにあるこの特定のレジスタについて、特別な R 戻り値型で利用可能なすべての関数は、tm4c123xドキュメント にあります。
if pwm.ctl.read().globalsync0().is_set() {
// 何かを行う
}
書き込み
write() 関数は、1 つの引数を持つクロージャを受け取ります。通常、これを w と呼びます。この引数は、このチップ向けのメーカーの SVD ファイルで定義されている、このレジスタ内のさまざまなサブフィールドへの読み書きアクセスを提供します。繰り返しになりますが、この特定のチップ上のこの特定のペリフェラルにあるこの特定のレジスタについて、‘w’ で利用可能なすべての関数は、tm4c123xドキュメント にあります。設定しなかったすべてのサブフィールドにはデフォルト値が設定されるため、レジスタ内の既存の内容はすべて失われる点に注意してください。
pwm.ctl.write(|w| w.globalsync0().clear_bit());
変更
このレジスタ内のある特定のサブフィールドだけを変更し、他のサブフィールドは変更しないままにしたい場合は、modify 関数を使用できます。この関数は 2 つの引数を持つクロージャを受け取ります。1 つは読み取り用、もう 1 つは書き込み用です。通常、これらをそれぞれ r と w と呼びます。r 引数はレジスタの現在の内容を調べるために使用でき、w 引数はレジスタの内容を変更するために使用できます。
pwm.ctl.modify(|r, w| w.globalsync0().clear_bit());
ここでは、modify 関数がクロージャの威力をよく示しています。C では、いったん一時的な値に読み込み、正しいビットを変更してから、その値を書き戻さなければなりません。つまり、かなりのミスの入り込む余地があります。
uint32_t temp = pwm0.ctl.read();
temp |= PWM0_CTL_GLOBALSYNC0;
pwm0.ctl.write(temp);
uint32_t temp2 = pwm0.enable.read();
temp2 |= PWM0_ENABLE_PWM4EN;
pwm0.enable.write(temp); // おっと! 変数を間違えた!
HALクレートを使う
チップ向けの HAL クレートは通常、PAC が公開する生の構造体に対してカスタムトレイトを実装することで動作します。このトレイトは、多くの場合、単一のペリフェラル向けには constrain()、複数のピンを持つ GPIO ポートのようなもの向けには split() という関数を定義します。この関数は、基になる生のペリフェラル構造体を消費し、より高水準の API を持つ新しいオブジェクトを返します。この API では、たとえば Serial ポートの new 関数が、Clock 構造体への借用を要求することもあります。この Clock 構造体は、PLLs を構成してすべてのクロック周波数を設定する関数を呼び出した場合にのみ生成できます。このようにして、先にクロックレートを設定せずに Serial ポートオブジェクトを作成することや、Serial ポートオブジェクトがボーレートをクロックティックに誤変換することは、静的に不可能になります。クレートによっては、各 GPIO ピンが取り得る状態ごとに特別なトレイトを定義し、ユーザーに対して、そのピンをペリフェラルに渡す前に正しい状態にしておくこと(たとえば適切な Alternate Function Mode を選択すること)を要求するものさえあります。しかも、これらはすべて実行時コストなしで実現されます!
例を見てみましょう。
#![no_std]
#![no_main]
use panic_halt as _; // panicハンドラ
use cortex_m_rt::entry;
use tm4c123x_hal as hal;
use tm4c123x_hal::prelude::*;
use tm4c123x_hal::serial::{NewlineMode, Serial};
use tm4c123x_hal::sysctl;
#[entry]
fn main() -> ! {
let p = hal::Peripherals::take().unwrap();
let cp = hal::CorePeripherals::take().unwrap();
// SYSCTL 構造体を、より高水準の API を持つオブジェクトにまとめる
let mut sc = p.SYSCTL.constrain();
// 発振設定を選ぶ
sc.clock_setup.oscillator = sysctl::Oscillator::Main(
sysctl::CrystalFrequency::_16mhz,
sysctl::SystemClock::UsePll(sysctl::PllOutputFrequency::_80_00mhz),
);
// その設定で PLL を構成する
let clocks = sc.clock_setup.freeze();
// GPIO_PORTA 構造体を、より高水準の API を持つオブジェクトにまとめる。
// GPIO ペリフェラルに自動的に電源投入できるように、
// `sc.power_control` を借用する必要があることに注意。
let mut porta = p.GPIO_PORTA.split(&sc.power_control);
// UART を有効化する。
let uart = Serial::uart0(
p.UART0,
// 送信ピン
porta
.pa1
.into_af_push_pull::<hal::gpio::AF1>(&mut porta.control),
// 受信ピン
porta
.pa0
.into_af_push_pull::<hal::gpio::AF1>(&mut porta.control),
// RTS と CTS は不要
(),
(),
// ボーレート
115200_u32.bps(),
// 出力の扱い
NewlineMode::SwapLFtoCRLF,
// ボーレート分周値を計算するにはクロックレートが必要
&clocks,
// UART ペリフェラルに電源投入するにはこれが必要
&sc.power_control,
);
loop {
writeln!(uart, "Hello, World!\r\n").unwrap();
}
}