💡

組み込みRust

2024/03/18に公開
  1. 組み込みRustの環境
    通常のRustでは、開発対象はOS上で動作するアプリケーションプログラムであるため
    OSが提供する機能を使用するstd::* クレートが提供されています。
    一方、組み込みRustが実行される環境ではOSが存在しないため、上記の機能は使えません。
    そのため、組み込みRustの実行環境ではno_std環境を使います。

    #![no_std]
    

    ただし no_std環境では(当然ですが) std::* クレートは使えません。
    その代わりにcore::* クレートが使えるので、主にこのクレートを使うことになります。

    use core::*;
    
  2. 組み込みRustでの動的メモリの利用
    通常のRustでは、StringやVectorなど可変サイズのオブジェクトを扱うため動的メモリ
    (ヒープメモリ)を利用しています。
    一方組み込みシステムでは、定められた時間内に必ず処理を完了する必要があります。
    メモリーの確保・開放の都度リンクを辿る必要のある動的メモリの利用は、処理時間の保証
    と相性が良くないため、どうしても必要な場合を除き利用しない方が良いでしょう。

  3. 対象ハードウェアアーキテクチャの理解
    組み込みRustが動作するマイコンには様々なメーカーのものがあり、それぞれのマイコン毎に
    ・ROMやRAMの各メモリエリアの開始・終了アドレス
    ・処理開始時のアドレス
    が異なるため、それらに応じて実行モジュールの作成方法が変わります。
    例えばRaspberry Pi Picoの場合

    メモリ アドレス範囲
    ROM 0x00000000 - 0x0FFFFFFF
    ROM(FLASH) 0x10000000 - 0x1FFFFFFF
    RAM 0x20000000 - 0x2FFFFFFF

    などとなっています。
    プログラムを開発する際には、開発対象マイコンに関する情報を入手する必要があります。

  4. ベクタテーブルの準備
    ベクタテーブルとは、各種例外発生時に呼び出される特定のルーチン(ハンドラ)のアドレス
    を格納する配列です。
    ベクタテーブルの要素数や配置場所も、マイコンの仕様で定められています。
    Raspberry Pi Picoの場合は、ROM内の先頭にあるセカンドブートローダ(256byte)
    の直後に存在する必要があります。

  5. スタートアップ処理
    組み込みRustのプログラムの本体部分はmain()関数に記述されていますが
    この本体処理部分の実行を開始する前に、スタートアップ処理が必要になります。
    スタートアップ処理では通常
    ・マイコン本体や周辺装置の初期化
    ・BSS(Block Started by Symbol)領域の0クリア
    ・初期値を持つグローバル変数は、初期値はROMに保管されていますが、プログラム実行時
     にはRAM上に配置しないと値が書き換えられないため、ROMからRAM領域へコピー
    などを行います。
    スタートアップ処理は通常リセットハンドラから起動され、上記の処理を行った後
    最後にRustのmain()関数にジャンプし、本体処理が開始されます。

  6. リンカスクリプトの記述
    組み込みRustでは、2-4で述べたような制約事項を満たすように実行モジュールを作成
    する必要があります。モジュールはリンカがオブジェクトファイルを結合して作成しますが
    上記の制約事項をリンカに指示する為、リンカスクリプトファイルを作成します。
    (リンカスクリプトの文法は非常に複雑なので、ここでは内容は割愛します)

  7. Rustからアセンブラの呼び出し
    Rustは高級言語のためCPUのレジスタを直接操作するような低レベルの処理を行う命令が
    ありません。しかし割り込みの禁止・解除、プログラムの実行コンテキストのスタックへの
    保管や取り出しなどを実現するには、どうしてもCPUのレジスタを直接操作する必要が
    出てきます。組み込みRustでは、インラインアセンブラマクロ asm! を使う事で
    Rustからアセンブラの命令を発行する事ができます。
    また、アセンブラ命令に与えるパラメータを指定することも出来ます。

Discussion