はじめに
Raspberry Pi 5 + Nerves の環境で、SPI 接続の LCD パネル(3.5")を使っていたときに、
-
mix burnで書き込んだファームウェアからのブート → 正常に表示される -
mix upload(OTA 更新)したファームウェアからのブート → 画面が真っ黒のまま
という現象に遭遇しました。
ログを見ると、LCD ドライバの初期化中に Circuits.SPI.open/2 が {:error, :access_denied} を返していて、パターンマッチでクラッシュしていました。
どうやら OTA 経由のブートでは、起動直後にアプリ側が spidev0.0 を開こうとすると、
カーネル側の準備が整う前にアクセスしてしまうことがあるようです。
この問題に対して、リトライすることで安定起動を実現できました。それを軽くメモります。
症状
OTA(mix upload)で更新されたファームウェアからブートした直後、SPI デバイス /dev/spidev0.0 を開こうとした処理で、以下のようなエラーが発生しました。
{:ok, spi} = Circuits.SPI.open("spidev0.0")
# => {:error, :access_denied}
このエラーにより、プロセスがクラッシュし、LCD の初期化処理全体が失敗していました。
対処法
OTA 経由でのブートでは、カーネルが /dev/spidev0.0 をまだ使える状態にしていないうちにアプリ側が SPI を初期化しようとして失敗していると考えられます。
調べてみると、どうも理想的には、以下のような方法でSPI デバイスの準備完了を確認してから初期化すべきようです:
-
nerves_runtimeの起動完了を待つ -
/dev/spidev0.0の存在やパーミッションをチェックする -
udevのデバイス準備通知を受け取る仕組みを入れる
今回は依存の少ない簡単な方法として、Circuits.SPI.open/2 に数回リトライ処理を入れることで、アプリが安定して起動するようになりました。
修正後のコード例
リトライ処理を入れた簡易的な対応方法の一例です:
def open_spi(dev, attempts \\ 5)
def open_spi(_dev, 0), do: {:error, :retry_exceeded}
def open_spi(dev, attempts) do
case Circuits.SPI.open(dev) do
{:ok, spi} -> {:ok, spi}
{:error, _reason} ->
Process.sleep(200)
open_spi(dev, attempts - 1)
end
end
失敗しても 200ms 待って再試行することで、
SPI デバイスの準備が整うまで待機できるようにしています。
おわりに
OTA 経由のブート時、SPI デバイスの初期化タイミングによって
Circuits.SPI.open/2 が失敗するという問題に遭遇しました。
今回は簡易的にリトライ処理を入れることで回避しましたが、
場合によりnerves_runtime の初期化完了や /dev/spidev0.0 の状態をより確実に確認する仕組みを検討する必要があるかもしれません。
同様に RPi5 上で SPI デバイスを扱っていて、起動直後に謎のクラッシュに悩まされている方がいれば、本記事が何らかの手がかりになれば嬉しいです。