メモリマップドレジスタ
組込みシステムでは、通常のRustコードを実行し、データをRAM内で移動させるだけではたいしたことはできません。 LEDの点滅やボタンの押下検出、もしくは、バス上のオフチップペリフェラルとの通信など、 システムが情報を入出力するには、ペリフェラルとその「メモリマップドレジスタ」の世界に足を踏み入れる必要があります。
マイクロコントローラのペリフェラルにアクセスするためのコードが、次のいずれかのレベルで、既に書かれています。
- マイクロアーキテクチャクレート。この種のクレートは、マイクロコントローラに搭載されているプロセッサコアで共通となる便利なルーチンを扱っています。 また、特定のプロセッサコアを使用する全てのマイクロコントローラに共通のペリフェラルも取り扱います。 例えば、cortex-mクレートは、割り込みの有効化と無効化を行う関数を提供しています。これは全てのCortex-Mベースマイクロコントローラで同じものです。 cortex-mクレートは、「SysTick」ペリフェラルへのアクセスも提供しています。このペリフェラルは、全てのCortex-Mベースマイクロコントローラに搭載されています。
- ペリフェラルアクセスクレート(PAC)。この種のクレートは、薄いラッパーです。特定の型番のマイクロコントローラに対して定義されている、 様々なメモリマップドレジスタのラッパーを提供します。例えば、テキサスインスツルメンツのTiva-C TM4C123シリーズ向けのtm4c123xクレートや、 STマイクロのSTM32F30xシリーズ向けのstm32f30xクレートです。マイクロコントローラのテクニカルリファレンスマニュアルに記載されている各ペリフェラルの操作手順に従って、 レジスタと直接やり取りします。
- HALクレート。これらのクレートは、特定のプロセッサに対して、よりユーザフレンドリなAPIを提供しています。embedded-halで定義されている共通のトレイトを使って実装されていることが多いです。 例えば、このクレートは、
Serial構造体を提供しているでしょう。そのコンストラクタは、適切なGPIOピンの一式とボーレートを引数に取ります。そして、データを送信するためのwrite_byte関数一式を提供します。 embedded-halに関する詳細は、移植性の章を参照して下さい。 - ボードクレート。これらのクレートは、HALクレートのさらに一歩先を進んでいます。これらは、STM32F3DISCOVERYボード向けのstm32f3-discoveryのように、 特定の開発キットやボード向けに、様々なペリフェラルとGPIOピンを事前に設定してあります。
Board Crate
A board crate is the perfect starting point, if you’re new to embedded Rust. They nicely abstract the HW details that might be overwhelming when starting studying this subject, and makes standard tasks easy, like turning a LED on or off. The functionality it exposes varies a lot between boards. Since this book aims at staying hardware agnostic, the board crates won’t be covered by this book.
If you want to experiment with the STM32F3DISCOVERY board, it is highly recommended to take a look at the stm32f3-discovery board crate, which provides functionality to blink the board LEDs, access its compass, bluetooth and more. The Discovery book offers a great introduction to the use of a board crate.
But if you’re working on a system that doesn’t yet have dedicated board crate, or you need functionality not provided by existing crates, read on as we start from the bottom, with the micro-architecture crates.
Micro-architecture crate
全ての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テクニカルリファレンスマニュアルにおいて、このペリフェラルに定義されている機能と非常によく似ています。 「Xミリ秒遅延」といった具合のAPIはありません。whileループを使って愚直に実装する必要があります。Peripherals::take()を呼び出すまでは、 SYST構造体にアクセスできないことに注意して下さい。これは、プログラム全体で唯一のSYST構造体が存在することを保証する特別な手順です。 詳しくは、ペリフェラルセクションをご覧下さい。
ペリフェラルアクセスクレート(PAC)の使用
全てのCortex−Mに搭載されている基本的なペリフェラルのみに限定するのであれば、組込みソフトウェア開発はあまり進まないでしょう。 どこかの時点で、使用している特定のマイクロコントローラ固有のコードを書く必要があります。今回の例では、テキサスインスツルメンツのTM4C123があるとしましょう。 TM4C123はミドルレンジのマイクロコントローラで、80MHzのCortex-M4と256 KiBのフラッシュメモリが搭載されています。 このチップを利用するために、tm4c123xクレートを取得します。
#![no_std]
#![no_main]
use panic_halt as _; // パニックハンドラ
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());
}
先ほどSYSTにアクセスした時と全く同じ方法で、PWM0ペリフェラルにアクセスします。違う点は、tm4c123x::Peripherals::take()を呼ぶことです。 このクレートは、svd2rustを使って自動生成されたものです。レジスタフィールドのアクセス関数は、数値の引数ではなく、クロージャを取ります。 このコードは量が多いように見えますが、Rustコンパイラは一連のチェックを実行し、手書きのアセンブラに近いマシンコードを生成します。 自動生成されたコードが、特定のアクセサ関数への全引数が有効であることを判断できない場合、その関数はunsafeとマークされます。 例えば、SVDがレジスタを32ビットと定義しているが、それらの32ビット値の一部が特別な意味を持つかどうか、記述していない場合です。 上記の例では、bits()関数を使ってloadとcompaサブフィールドを設定する時に、unsafeをマークしています。
読み込み
read()関数は、メーカーのSVDファイルで定義されている通り、レジスタ内の様々なサブフィールドに対して、読み込み専用のアクセスオブジェクトを返します。 特定チップ上にある、特定ペリフェラルの、特定レジスタに対して、固有の返り値R型があり、このR型で使える全ての関数は、tm4c123xドキュメントで見ることができます。
if pwm.ctl.read().globalsync0().is_set() {
// 処理をする
}
書き込み
write()関数は、単一引数のクロージャを取ります。通常は、この引数を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は、シリアルポートのnew関数が、Clock構造体オブジェクトの借用を必要とするようなことをするかもしれません。Clock構造体オブジェクトは、 PLLと全てのクロック周波数とを設定する関数呼び出しによってのみ、生成することが可能です。この方法では、最初にクロックレートを設定しないでシリアルポートオブジェクトを作成したり、 シリアルポートオブジェクトがボーレートをクロック数に誤って変換するようなことは、静的に起こり得ません。 一部のクレートでは、各GPIOが取り得る状態のための特別なトレイトを定義することさえあります。このトレイトは、ペリフェラルにピンを渡す前に、 ユーザがピンを正しい状態(例えば、適切なAlternate Functionモードを選択することによって)にすることを求めます。 これらは全て、ランタイムのコストを必要としません。
例を見てみましょう。
#![no_std]
#![no_main]
use panic_halt as _; // panic handler
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();
}
}