「Zynq + Linux Kernel 4.5(以降) で "mmc0: Timeout waiting for hardware interrupt" が起きる場合の対処方法」 の続編です。
症状
Zynq で Linux Kernel 4.5以降 を動作させた時、"mmc0: Timeout waiting for hardware interrupt" が起きて SD-Card が読めません。
SD-Card に root file system を作っていた場合、Linux が起動出来なくなります。
参考
実はこの原因は「技術書典2」で販売されていた次の本で説明されていました。
- 「AGPF WORKS Vol 1.0」
- 発行者: AQUAXIS TECHNOLOGY
- 発行日: 2017年4月9日
- Web: http://www.sweetcafe.jp
この本には、このバグの説明や解析する過程が詳細に記述されています。ぜひ読んでみてください。
原因
sdhci.c のデバイスドライバ内で DMA 転送に失敗しています。「AGPF WORKS Vol 1.0」で記載されていたDMA転送の失敗のメカニズムを簡単に説明すると次の通りです。
- Zynq にはシステムメモリ空間上に DMA 転送出来ない領域があります。
- 具体的には 0x00000000 - 0x0007FFFF の 512KByte
- 詳細は「Zynq-7000 All Programmable SoC Techinical Reference Manual」UG585 Xilinx
- Linux Kernel の v4.4 以前に比べて、それ以降のバージョンではメモリ管理がより緻密になりました。
- v4.4 以前では kmalloc 等でメモリ領域を確保した時、Linux がロードされた後ろのメモリ領域から確保されます。
- v4.5 以降では kmalloc 等でメモリ領域を確保した時、場合によっては メモリの低位の方からも確保されるようになりました。
- mmc ドライバはバッファを kmalloc で確保しています。
- v4.4 以前ではバッファは たまたま 物理アドレス 0x00080000 以降に確保されていました。
- v4.5 以降ではバッファが 物理アドレス 0x00000000 - 0x0007FFFF に確保されることがあります。
- sdhci ドライバは mmc からのリクエストやデータを DMA 転送しています。
- バッファの物理アドレスが Zynq の DMA転送不可領域だった時、DMA に失敗してタイムアウトします。
- DMA 転送にはスキャッターギャザー可能な ADMA モードと、シンプルな SDMA モードがあります。
- DMA エラーが起きるのは ADMA モードの時。SDMA モードでは「何故か」起きませんでした。
対処方法
Linux 起動時に使用する Device Tree に次の記述を追加して Zynq の DMA 転送不可領域をリザーブします。
reserved-memory {
#address-cells = <0x1>;
#size-cells = <0x1>;
ranges;
zynq-dma-unusable-area {
reg = <0x0 0x80000>;
};
};
このようにすれば、Linux が使う領域に Zynq の DMA 転送不可領域が含まれなくなるようです。
補遺
ここで疑問点があります。原因が DMA 転送時のエラーなら、ADMA モードでも SDMA モードでも同じようにエラーになるはずです。しかし、SDMA モードでは発生しませんでした。
カーネルのソースコードを見たり、sdhci.c にデバッグ出力を組み込んで実動作で確認したところ、次のような事がわかりました。
- MMC ドライバは SDHCI ドライバに対して ADMA モードでは一度に数ブロックの転送要求を行っています。
- MMC ドライバは SDHCI ドライバに対して SDMA モードでは1ブロックずつ転送要求を行っています。
- SDMA モードでは MMC が確保しているバッファは常に 0x00080000 以降でした。
sdhici.c の次の記述があります。
int sdhci_setup_host(struct sdhci_host *host)
{
:
:
/*
* Maximum number of segments. Depends on if the hardware
* can do scatter/gather or not.
*/
if (host->flags & SDHCI_USE_ADMA)
mmc->max_segs = SDHCI_MAX_SEGS;
else if (host->flags & SDHCI_USE_SDMA)
mmc->max_segs = 1;
else /* PIO */
mmc->max_segs = SDHCI_MAX_SEGS;
:
:
}
MMCドライバはこの max_segs をみてバッファを確保するのですが、SDMA モードの場合は たまたま 確保領域が Zynq の DMA 転送不可領域にならないようでした。
まとめ
前回の「Zynq + Linux Kernel 4.5(以降) で "mmc0: Timeout waiting for hardware interrupt" が起きる場合の対処方法」で説明した対策では、おそらく不十分です。v4.5 以降の sdhic の ADMA モードで たまたま 症状が出た(地雷を踏んだ)だけであって、実は SDMA モードや、あるいは別のドライバでも同じような症状がでる可能性があります。この記事で説明したように、Linux が使う領域に Zynq の DMA 転送不可領域を含まないようにした方が確実です。