0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【小型装置のWeb UI・第5回】「レベル42」はどこでビットになるのか ― アプリ/FW/HAL/ドライバの境界

0
Last updated at Posted at 2026-08-08

「レベル42」はどこでビットになるのか

第1回では、2行LCDしかない装置へWeb UIを足す話を書きました。第2回第3回ではブラウザ側の状態表示、第4回ではPHPのexec()から先へ渡す境界を扱いました。ここまでは、いわば装置の手前の話です。

この回では境界の向こう側へ入ります。Web UIから来た「レベル42」という数字が、device_ctlの中でどんな姿に変わりながらデバイスへ届くのか。そして、その変換をどこに置くかで、あとから来る変更がどこまで波及するのかを見ます。

例示用の割り当てです

この記事に出てくるレジスタ名、アドレス、ビット配置は、説明のために新しく決めたもので、実製品のものではありません。

公開サンプルは、WebからC言語の境界までを動かすため、装置状態を/tmpのファイルで模擬しています。本記事のファームウェア層、HAL層、SPI転送、LEVEL/CTRLレジスタは、その境界の先を説明するための設計例です。公開サンプルへ実装済みの機能ではありません。

この記事で分かること

  • ユーザーが入力した42が、層を降りるたびにどんな表記へ変わるか
  • 単位の変換(% → デバイス値)をどの層に置くべきか、そしてなぜHALに置かないか
  • レジスタが2本あるとき、「書く順序」を持つのはどの層か
  • 境界を守った場合と崩した場合で、ひとつの変更がどこまで波及するか
  • 「層が多いほど良い」ではないこと。差の本質は層数ではなく、確認に必要な範囲であること

「42」が姿を変えていく

小型装置WebUI_⑥装置制御層の内側とレジスタ_icon.png

上から順に、同じひとつの操作が別の言葉へ翻訳されていきます。

そこでの「レベル42」 その層が知っていること
Web UI / 2行LCD 42 人が入力した数。装置の内部表現は知らない
アプリケーション層 set-level 42 装置機能の言葉。範囲検証とログ
ファームウェア層 42% → 0x6B 機能をデバイス仕様へ変換する。設定順序とリトライ
HAL層 hal_write(0x12, 0x6B) レジスタ1本の読み書き。バスの差を吸収
デバイスドライバ / SPI SPI転送 2バイト spidev、CS制御、クロック、転送
デバイス LEVEL = 0110 1011 受け取ったビットがそのまま回路の状態

換算はこれだけです。フルスケール255に対する42%なので、

42 × 255 / 100 = 107.1 → 107 = 0x6B = 0b0110_1011

大した計算ではありません。問題は、この1行をどこに書くかです。

なぜ換算をファームウェア層に置くのか

候補は3つあります。アプリケーション層、ファームウェア層、HAL層。

アプリケーション層に置くと、「レベルとは0〜100の値である」という装置機能の言葉と、「フルスケールは255である」というデバイスの都合が同じ関数の中で混ざります。デバイスを10bit品へ替えたとき、機能の記述を触ることになります。

HAL層に置くと、もっと具合が悪くなります。HALの仕事は「レジスタ1本を読み書きする」ことだけであってほしい。ここに%という単位の知識が入ると、hal_write()は「アドレスと値」を受ける汎用の関数ではなく、「レベルという概念を知っている関数」になります。次にゲイン設定を足したくなったとき、HALにゲインの単位も足すことになり、レジスタの数だけHALが太っていきます。

残るのがファームウェア層です。ここは「機能をデバイス仕様へ変換する」のが仕事だと最初から決めてある層なので、%もフルスケールも、置き場所として自然です。デバイスを替えるときに書き換えるのはこの層だけになります。

/* ファームウェア層:機能の言葉をデバイス仕様へ変換する */
#define FS_LEVEL 255  /* デバイスのフルスケール */

int fw_set_level(int percent)
{
    unsigned char raw;

    if (percent < 0 || percent > 100) {
        return -1;
    }
    raw = (unsigned char)((percent * FS_LEVEL) / 100);

    /* ここから下は「アドレスと値」しか知らない */
    return hal_write(REG_LEVEL, raw);
}

FS_LEVELがこの層にしか現れない、というのが判定基準になります。grepしてこの定数がHALやアプリ側にも出てきたら、境界が漏れています。

レジスタが2本あるとき、順序を持つのは誰か

実際のデバイスは、値を1本書けば動くとは限りません。この記事の例では2本あることにします。

アドレス 名前 役割
0x12 LEVEL レベル値そのもの(8bit)
0x10 CTRL 有効化、モード、エラークリア

CTRLのビット割り当てはこうです。

ビット 名前 意味
7 EN 1 有効
6:5 MODE 01 動作モード
4 ERR_CLR 0 書くとエラーフラグをクリア
3:0 予約 0000 0を書く

ここで「LEVELを書いてからCTRLでENを立てる」のか、「ENを立ててからLEVELを書く」のかは、デバイスの仕様で決まります。中間状態が回路に出てしまう順序なら、前者しか選べません。

この順序の知識をどこに置くかが、この記事のもうひとつの分かれ目です。

  • アプリケーション層に置くと、set-levelという機能の記述の中にレジスタ2本の話が出てきます
  • HAL層に置くと、「1本の読み書き」という約束が壊れます
  • ファームウェア層に置けば、fw_set_level()の内側に閉じます

リトライも同じ場所です。書いたあとに読み返して一致を確認する、失敗したら決まった回数だけやり直す、という手順は、デバイス仕様に属する知識なので、ファームウェア層が持ちます。アプリケーション層は「成功したか失敗したか」だけを受け取ります。

予約ビットに0を書く、という約束も同様に、この層から外へ出しません。上位が0b1010_0000のような生の値を組み立て始めた時点で、ビット配置の知識が漏れています。

境界を守った場合と、崩した場合

小型装置WebUI_⑦変更の波及範囲_icon.png

デバイスのリビジョン更新で、LEVELが8bitから12bitになり、CTRLのビット配置も変わったとします。ただし、上位へ見せる装置機能は変わりません。画面では引き続き0〜100%を扱い、操作名もstartstopset-levelのままです。

境界を守っている側では、直すのはFS_LEVEL、レジスタ値の組み立て、書き順を持つファームウェア層です。変更を知るのは1層に閉じます。HALのhal_write(addr, value)という約束と、Web UIやPHPが使う装置機能の言葉は変えません。

崩している側、つまりブラウザのJavaScriptが直接ビット列を組み立て、PHPがレジスタアドレスやマスクを持ち、HALにも同じ知識が漏れている構成では、レジスタ仕様を知る場所が3層に散ります。Web UI/JS、PHP、HALです。

ここは正直に書いておきたいのですが、「5箇所」とは書きません。ファームウェア層は崩した側ではPHPが兼ねていて層として存在していないので、独立に数えられません。SPIドライバは、値が何であれ2バイト転送することに変わりがないので、この変更では触りません。数を大きく見せると、かえって話が疑わしくなります。3対1で十分に差が出ています。

本当の差は、層の数ではない

1対3という数え方は分かりやすいのですが、実務で効いてくるのはむしろ確認に必要な範囲のほうです。

境界を守っている側で換算を直したときは、Web UIとPHPを起動せず、まずdevice_ctl以下だけで変更を確認できます。

$ ./device_ctl set-level 42
{"ok":true,"level":42,"running":true}

最初の切り分けに、ブラウザもPHPもlighttpdも要りません。ターミナルから下位層を確認したあと、必要な結合範囲だけを広げられます。

崩している側では、同じ変更を確認するのに、ブラウザを開き、PHPを通し、Cを動かす必要があります。3つの実行環境を同時に立ち上げないと、直したかどうかが分からない。デバッグの1周が数十秒から数分になり、原因の切り分けも「どこで値が化けたか」から始まります。

変更のたびに払うのはこのコストです。修正箇所が3つあることより、確認のたびにスタック全体が要ることのほうが、日々の作業を重くします。

バスが変わったらどうなるか

もうひとつ、境界の効き目が出る変更があります。SPIで繋がっていたデバイスを、次の機種ではI2Cで繋ぐ、というものです。

ここでは、デバイスの論理的なレジスタ仕様は同じまま、接続バスだけがSPIからI2Cへ変わる場合を考えます。HAL層の役目が「レジスタ1本の読み書きへ落とす。バスの差をここで吸収する」と決まっていれば、hal_write(addr, value)という呼び出しの形は変わりません。差し替わるのはHALの実装だけで、ファームウェア層から上はそのまま使えます。デバイス自体も別品種へ変わり、レジスタ仕様まで変わるなら、前節のとおりファームウェア層も変更対象です。

逆に、ファームウェア層がioctl(SPI_IOC_MESSAGE(1), ...)を直接呼んでいると、バスの変更がレベル設定の記述まで届いてしまいます。バスの都合と機能の都合は、変わるタイミングが違うので、分けておく価値があります。

小型装置WebUI_⑧HALがバスの差を吸収する_icon.png

この記事に無いもの

  • 実製品のレジスタ名、アドレス、ビット配置。すべて例示用に決めた値です
  • 実測したタイミングや転送レート。測っていないので書きません
  • 割り込みを使う設計。ここでは同期的な読み書きだけを扱いました
  • 複数プロセスから同時にデバイスを触る場合の排他。これは第4回のflockの話に寄せています

まとめ

  • 同じ「42」が、層を降りるたびにset-level 420x6Bhal_write(0x12, 0x6B) → SPI 2バイト → LEVEL = 0110 1011と姿を変える
  • %からデバイス値への換算はファームウェア層に置く。アプリ層に置くと機能の記述にデバイスの都合が混ざり、HALに置くとHALがレジスタの数だけ太る
  • フルスケール、レジスタアドレス、ビット配置がファームウェア層に閉じていることが、境界を守れている印になる
  • レジスタを書く順序、リトライ、予約ビットの扱いは、すべてファームウェア層の内側に閉じる
  • 境界を崩すと、レジスタ仕様を知る場所が3層に散る。ただし「5層」と盛らない。存在しない層は数えない
  • 効いてくるのは修正箇所の数より、確認に必要な範囲。守っていればdevice_ctlの単体実行で済み、崩していればブラウザ・PHP・Cの3つが要る
  • バスがSPIからI2Cへ変わっても、HALの約束が保たれていれば上の層は動かさなくてよい

2行LCDしかない装置にWeb UIを足す、という小さな話から始めて、ブラウザの状態表示、exec()の境界、そして装置制御層の内側まで来ました。全体を通してやっていたことはひとつで、同じ操作を表す言葉を、層ごとにひとつだけにするということです。境界が一本に保たれていれば、変更を知る層を限定でき、確認も小さな範囲から始められます。

関連記事

シリーズで扱ったサンプルコード一式はGitHubに公開しています。→ embedded-web-control-demo

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?