マイコンのHALについて考察する
マイコンの開発において、MCUメーカーから提供されるHAL(Hardware Abstraction Layer)は何故イマイチなのか、どうすれば改善できるのか考察してみました。
ADC2020
この記事はMicroMouse AdventCalendar 2020 の15日目の記事です。
昨日はコヒロさんのスラローム自動生成で楽をしたかった話でした。
スラローム自動生成いいよね。私もほぼ同じ方式で生成しています。楽なのでみんなやろう。
では、本題です。
HALとは何か
HALとは、HardwareAbstractionLayerの略で、上位のプログラムからハードウェアを抽象化するレイヤです。
HALの目的
プログラムの移植性を上げる事が目的です。適切に設計されたHALがあれば、 異なるハードウェア上で、HALより上位のプログラムに変更を加える事なく動作させる事ができます。
我々は日常的にHALの恩恵を受けています。 普段使っているPCやスマートフォンが、多様なメーカーの多様な機種(ハードウェア)上で同じように動作しているのは、WindowsやLinux等のOSが、適切なHALと、その実装を提供しているからです。
HALは誰のもの?
依存関係を整理しておきます。
下図のようになります。(以下、HALより上位のプログラムを総合してApplicationと表記します)
抽象化レイヤを挟むという事はDIP(依存性逆転の原則)の考え方を導入するという事です。 つまり、コンポーネントレベルで考えると、HALはApplicationコンポーネントの要求インタフェースです。
言語化すると、「HALはApplicationがハードウェアに要求する機能のセット」を表しています。 HALはApplicationの設計に依存して仕様が決まります。
HALとは何か? まとめ
HALとは
- Applicationがハードウェアに要求する機能のセット
- Applicationにハードウェアの多様性に対するポータビリティを持たせるための抽象化レイヤ
PCのHALと組み込みのHAL
PCではHALがうまく機能していますが、マイコンまわりではあまり良い話を聞きません。どこが違うのか比較してみましょう。
PCのHAL
PCというアプリケーションのあるべき姿をOSが決めて、そのために必要なHALをOSが定義して使用しています。
マイコンのHAL
こんな感じです。繰り返しますが、抽象化の目的は上位コンポーネントが下位コンポーネントに依存しないようにする事です。 それなのに、下位のコンポーネントに属するインタフェースをして抽象化レイヤを名乗っているというのは意味がわかりません。これはただのAPIです。
結論として、マイコンのHALはハードウェア抽象化レイヤではありません。
このタイプのHALを名乗るライブラリを直接使うと、ハードウェアに依存した移植性の低いアプリケーションが出来上がります。
ペリフェラル単位で分割するのは良い設計なのか?
今までの話だと、HALというちょっとカッコイイ名前をつけようとして失敗しただけで、ハードウェアを操作するライブラリとしては使えるはずです。 でも、妙に使いづらいですよね。
抽象化という観点以外にも、HALの構成には無理があると考えています。 より正確には、ペリフェラルにへのアクセスを提供するライブラリ全般に無理があると考えています。
それは、ペリフェラル単位で操作するように設計されている点です。
現状確認
マイコンのライブラリはだいたいこんな感じの構成になってると思います。
このオブジェクト図を見たままのソフトウェアであれば、設計には何の問題も無いでしょう。
しかし現実はTimer1やADC1の実体は電子回路と、そのインタフェースとしてバスに接続されたレジスタです。 更に言うと、他のペリフェラルやバス、割り込みコントローラ等とのインターコネクトもあります。
これらの要素を上の図に重ねてみると、こうなります。
責任範囲はどうなる?
上のような構成で、タイマによってA/Dコンバータのトリガをするようなケースは良くあると思います。
この構成の場合、Timer1とADC1は「一定周期のA/D変換」機能を提供するために、インターコネクトを通して1つのペリフェラルのように振る舞います。 「一定周期のA/D変換」機能に対して、Timer1とADC1の2つのオブジェクトが操作しています。
1つの機能は1つのオブジェクトから操作するべきでしょう。
内部にインターコネクトを持つという事を設計上豊かに表現できていません。 したがって、ユーザが内部実装に気を使う必要があります。
これがペリフェラル単位アクセスを提供するライブラリの問題点その1です。
メモリマップドIO
続いて、問題点その2を紹介します。
「ペリフェラルのレジスタ」というような呼び方をしていますが、ソフトウェアから見るとアドレス空間上のメモリに見えます。 メモリマップドIOと呼ばれる概念です。
この概念を使って上の図をソフトウェアの立場で見ると、こうなります。
共有メモリを複数のオブジェクトで共有しています。
何が起きるかわかりますよね。もちろんデータ競合です。
一般的な共有メモリへのデータ競合を防ぐ方法は、Mutex等のロックを使う事ですが、 レジスタへのアクセスという速度が命になる箇所でそんな事してられません。
では、現実どうやりくりしているかと言うと、こうです。
それぞれ排他的な領域にしかアクセスしないから大丈夫だよね、という前提でロックせずに共有メモリにアクセスしているのです。
かなり危ういように見えますが、本質的に排他なアクセスを前提にする事は問題は無いと思います。 というか、他に方法がありません。ハードウェアとソフトウェアの境界の限界ですね。
ところで、Timer1やADC1のようなペリフェラルが専用のADC1レジスタにアクセスするだけで動作する事は無いですよね。
たいていの場合、最低でもバス設定レジスタや電源設定レジスタ、クロック設定レジスタ、Timer<n>やADC<n>共通のレジスタの設定等が必要になります。 さらに、まっとうにマイコンを使いこなそうとすると、割り込みコントローラやDMAコントローラの設定も必要になるでしょう。
こうなった時、Timer1やADC1はどのように設計されるべきでしょうか?
Timer1やADC1オブジェクトにアクセスするだけでその他のレジスタも設定されるように設計した場合・・・
設定によってはデータ競合が起きる可能性があります。
データ競合が起きるのは仕方ありません。本質的に排他的である事を前提としていて、それに反した設定をしたのですから。
ここで問題となるのは透明性です。レジスタの、しかも、bit単位で排他的かどうかを確認しながら設定する必要があるのに、 Timer1やADC1というオブジェクトのインターフェイスにより、どのレジスタを操作しているのかが隠蔽されています。 隠蔽するのであれば、内部で排他処理をしてほしい物ですが、実装しているライブラリは見たことがありません。
このようなライブラリは便利さを提供しているつもりでも、実際に提供しているのは不安感とデバッグ時の難読化です。
以上が問題点その2です。
ちなみに、私はこれに起因するライブラリのデバッグを3回やらされた事があるので、ペリフェラルを取り扱うライブラリ全般が嫌いです。
解決策
HALが抽象化レイヤになっていない問題と、ペリフェラル単位で分割する事の難しさに対する私なりの解決策を提示します。
私の考えるマイコンのハードウェアアクセスにおける答えはこうです。
HALは普通にアプリケーション側の都合で定義します。 そのインタフェースを全て実装したレジスタ操作(および割り込みハンドリング、DMAの転送先メモリ管理)を全てをまとめたモノリシックなHardware Accessを作成します。
これで今回議論していた問題点は全て解決します。
正しい抽象化レイヤにより、アプリケーションの移植性やテスト性は高まり、
HALのインタフェースにより、ある機能へのアクセスは単一のインタフェースにより提供され、
モノリシックなハードウェアアクセスオブジェクトにより、レジスタアクセスの透明性が保たれます。
モノリシックなオブジェクトを作成するのはソフトウェア設計としてどうなんだ、と思われるかもしれません。
この点については、ペリフェラル別にオブジェクトを分割すると隠ぺいされてしまうペリフェラル間のインターコネクトや 設計上本質的に排他的に扱える共通レジスタのビット等を、一か所にまとめる事で見えるようにする意図があります。 HALのインタフェース毎にオブジェクトを用意しても、同様に透明性が保たれません。
ちなみに、ペリフェラルへのアクセスはどうするのか?の答えは、「レジスタを叩こう」です。 レジスタアクセスと1対1で対応する薄いラッパーのようなライブラリであれば、可読性のために使うのも良いと思います。
データシート読んでレジスタ叩けない組み込みプログラマなんて存在するはずがないので、ちょっと面倒なくらいで何とでもなるでしょう。 ライブラリの中をデバッグする労力に比べれば、アドレス直打ちする労力なんて微々たるものですよ。
一応、机上の空論ではなく、去年作ったさくらねずみ玄1はこの方式で作っていて、ハードウェアはすべて問題なく動いています。
ADC next
社会人になってからも、あんなに尖っていて、かつ、完成度の高いマウスを作り続けているのは本当にすごいですよね。お楽しみに。