Zig言語からLPC81xのブートROMAPIを呼び出す

By ikubaku, Thu 26 December 2019, in category Posts

baremetal, programming, technical, ziglang

NOTE

こんにちは。ikubakuです。今日は普段の開発メモということで最近やったLPC812のブートROMAPIをZigで書いたファームウェアから呼び出した話を書こうと思います。

背景

今年の10月ぐらいからマイコンを使ったバッジ+目覚まし時計を作っています(以下全体図)。

ECghTRRUIAA ZR4?format=jpg&name=large
Figure 1. 作ったもの

秋月で買えるリアルタイムクロックとOLEDなどを組み合わせた構成で、制御にはLPC812を使っています。

ROMが足りない!

ハードウェアを作るところまでは順調でしたがソフトを書いているところで問題にぶち当たりました。開発にはmbedを使っていたのですが、mbedのライブラリサイズが大きいらしくLEDを点滅させるだけでもフラッシュメモリの半分を使ってしまい、ディスプレイに文字を表示させるところまで書くとほぼいっぱいになってしまいました。

79357b34a02393c0dc72f96715279018
Figure 2. 食い尽くされるメモリの図

そこでファームウェアは最近知った言語のZigで1から書くことにしました。

ベアメタル開発のためのビルド設定

Zigでのベアメタル開発の方法は次のリポジトリを参考にさせていただきました。

通常のZig実行ファイルの作成と異なるのは build.zig の内容です。

まずリンカースクリプトでエントリポイントに指定するコードが書かれているソースコードを addExecutable で指定します(9行目)。このプログラムでは割り込みベクタの設定とbss領域の初期化を行う部分がある ipl.zig を指定しています。

次にビルドターゲットを指定する必要があります(14行目)。これを行うには addExecutable で作成した実行ファイル作成ステップの setTarget を呼び出します。LPC812はARM Cortex-M0+ CPUを搭載しているのでターゲットトリプルは thumbv6m-freestanding-eabi になります。これをビルドコードに直すと

bin.setTarget(builtin.Arch{ .thumb = builtin.Arch.Arm32.v6m }, builtin.Os.freestanding, builtin.Abi.eabi);

ーのようになります。

さらにハードウェアに合わせたリンカスクリプトを用意して指定します(17行目)。その後最後に成果物をバイナリ形式に変換させます(18行目)。こうしておくと lpc21isp などのファームウェア書き込みプログラムで扱いやすくなります(ELF形式に対応しているプログラムを用いる場合は省略して良い)。

ここまで準備すれば目的のハードウェアでのベアメタル開発ができるようになります。

LPC812のブートROM API

ベアメタルで動作するプログラムを書くとなると普通はペリフェラルに対応するメモリ領域を操作するコードを書くことになるのですがLPC812にはブートROM APIという面白い機能があります。

LPC81xシリーズのブートROM領域にはISP用制御コードや初期化コードの他にUSART、I2C、パワーマネジメント機構の制御ルーチンが含まれていて、それらを呼び出すと簡単な通信を自分でレジスタを読み書きするコードを書くことなく行うことができます。そこで試しにUSARTの制御ルーチンを呼び出して使ってみることにしました。

User Manualによるとそれぞれの制御ルーチンは関数ポインタとしてROM上に位置が保持されていて、ペリフェラルごとにそれら関数ポインタをメンバにもつ構造体としてまとめられています。つまりこれらを呼び出すにはその構造体をソースコードで定義しておいてそのポインタを作り、それにROM上に示されているアドレスをセットすれば良いわけです。

ABIを合わせる

ブートROMのルーチンはC言語のABIに合わせて作られたファームウェアから呼び出すことが想定されているように見える(実際、LPC81xのユーザマニュアルではルーチンの定義がC言語で記述されている)ので、これからZigで書くファームウェアから呼び出すときもそのルールに合わせる必要があります。例えば引数やメモリ上の構造として構造体が使われているなら extern struct を付けて構造体を宣言する必要がありますし、 void * 型のフィールドは *c_void 型を使う必要があります。それらに気をつけながらUARTの制御プログラムを書くと次のリンクのコードのようになりました。

ROM APIがすべて準備してくれるわけではないらしい

ところで、このコードのうち103行目から105行目は大体の実装を終えてデバッグする中で最終的に追加したコードです。これはUSARTペリフェラルのレジスタにクロックを供給するために必要で、ROM APIを呼び出す前に行う必要があるようです(それもUSART0〜2のすべてのビットを立てる必要がある)。これはマニュアルのROM APIについての部分に書かれていなかったので、動作しない原因がこれだとわかるまで時間がかかりました。

とりあえずエコーバックしてみる

最後にルーチンを呼び出して実際に使えていることを確かめるためにエコーバックプログラムを実装してみました。先程上げたコードとともに このGitHubリポジトリに上げているので是非見てください。

bd80b30b4ef98ad0b092992d6f089a1b
Figure 3. 動作確認の様子

今後やりたいことなど

次はI2Cのルーチン呼び出しを試して、それを元に目覚まし時計のファームウェアを仕上げていきたいと思っています。また、ビルドコードを改良してビルド時にターゲットや出力形式を指定できるようにしたいとも思っています(もっとも今回紹介したプログラムはハードウェアに依存しまくっているのであまり意味がありませんが)。

おわりに

エコーバックプログラムのファームウェアサイズは4.0kBだったのでROMサイズの約25%です。この調子なら載せたい機能を載せられそうな気がしてきたので良かったと思います。

また開発の際には今年のセキュリティキャンプで勉強させていただいた開発手法(デバッグのやり方、逆アセンブル、リンカスクリプトの意味など)が非常に役に立ちました。講師・チューターの皆さん、ありがとうございました。