Cadence Raw NAND Controller:在 Linux、U-Boot 與 OP-TEE 的一致化實作與效能診斷
本文記錄我將 Cadence Raw NAND Controller (HPNFC) 於 Linux、U-Boot、OP-TEE 三個軟體域進行一致化的驅動實作與修補。內容涵蓋:中斷(IRQ)與等待語意的標準化、CDMA/SDMA 命令路徑修正、R/B# 與時序處理、三層…
Cadence Raw NAND Controller:在 Linux、U-Boot 與 OP-TEE 的一致化實作與效能診斷

本文記錄我將 Cadence Raw NAND Controller (HPNFC) 於 Linux、U-Boot、OP-TEE 三個軟體域進行一致化的驅動實作與修補。內容涵蓋:中斷(IRQ)與等待語意的標準化、CDMA/SDMA 命令路徑修正、R/B# 與時序處理、三層 ECC(HWECC / On-Die / SW)並存、U-Boot 的 raw 資料擷取能力(含 OOB)與 BBM(壞塊標記)策略。文末也討論 多處理器(SMP)下的 MMIO 次序化 — — 為何必須嚴謹區分 readl/writel 與 *_relaxed,以及我加上的 barrier I/O 包裝。
1. 動機
現成的開源 HPNFC 驅動(多起自 Cadence 舊版)在實務中存在幾個主要問題:
- 命令與等待語意分歧:CDMA/SDMA/generic command 在不同路徑的等待條件不一致,R/B#(Ready/Busy)與超時處理缺乏系統化,造成效能抖動與偶發 timeout。
- OOB/BBM 可觀測性不足:缺乏「真正 raw」的擷取手段,工程師常被 HWECC/控制器的自動處理遮蔽觀測視角,很難精準定位錯誤來源。
- 跨域不一致:Linux 能動,但 U-Boot 或 OP-TEE 因時序或等待語意不同步而行為不一,導致 bring-up 與除錯成本高。
因此,本次工作的目標是:讓 Linux、U-Boot、OP-TEE 對 HPNFC 的操作語意一致、等待機制一致、可觀測性一致,並在此基礎上進行效能與可靠度的診斷與優化。
2. NAND Controller 與 NAND Chip Pins 背景知識簡易解說

2.1 NAND Controller 的角色
NAND Flash 不能像 DRAM 一樣直接被 CPU 隨機讀寫,因為 NAND 有特殊的 命令/地址/資料 (CMD/ADDR/DATA) 交握流程,以及 Ready/Busy 等待。 因此,系統必須依靠 NAND Controller 來完成下列任務:
- 命令序列管理:例如 READ PAGE、PROGRAM PAGE、BLOCK ERASE。
- 地址解析:將線性地址(offset)轉換成 Page/Block/Column。
- 資料搬移:透過 DMA/CDMA/SDMA 方式搬運資料到記憶體。
- ECC 檢查:控制器內建 HWECC 模組,幫助自動產生或驗證 ECC parity。
- R/B# 等待:透過硬體或軟體方式監看 NAND 的 Ready/Busy 腳位。
- OOB/Skip-bytes:控制是否跳過或保護壞塊標記區(BBM)。
在 Linux、U-Boot、OP-TEE 的驅動程式中,我們主要就是透過 NAND Controller 寄存器來設定這些功能。
2.2 NAND Chip 的 Pins
典型的 NAND Flash 顆粒會有下列主要引腳(pins):
Pin 名稱功能I/O[7:0] 或 I/O[15:0] — 資料匯流排。支援 8-bit 或 16-bit NAND。傳送命令、地址、資料。
CLE (Command Latch Enable) — 當 CLE=1,寫入 I/O 線會被視為「命令」。 ALE (Address Latch Enable) — 當 ALE=1,寫入 I/O 線會被視為「地址」。 CE# (Chip Enable) — 選擇 NAND 晶片,低電位有效。多顆 NAND 時可用來切換。 WE# (Write Enable) — 負脈衝觸發,將 I/O 匯流排的值寫入(命令、地址或資料)。 RE# (Read Enable) — 負脈衝觸發,從 NAND 輸出一個 byte/word 到 I/O 匯流排。 R/B# (Ready/Busy) — 狀態指示腳。低表示 NAND 忙碌,無法接受新命令;高表示 Ready。 WP# (Write Protect) — 寫保護,低電位時禁止程式或擦除操作。 VCC / VSS — 電源/接地。
2.3 主機與 NAND 的交握範例
以 READ PAGE 為例,主機(NAND Controller)會進行下列步驟:
- 命令 0x00 → CLE=1,WE# 拉低,送命令。
- 地址循序 → ALE=1,分別送 Column、Row 地址。
- 命令 0x30 → CLE=1,送執行命令。
- 等待 R/B# → R/B# 拉低(Busy),直到拉高(Ready)。
- 讀取資料 → RE# 觸發,NAND 將資料送到 I/O bus。
這就是文章裡提到的「R/B# 等待」場景。
2.4 與驅動程式的對應
- 在 Controller 寄存器裡,CLE/ALE/WE#/RE# 等動作被包裝成「Generic Command」或「Descriptor」模式。
- R/B# pin 通常會被 Controller 接管,並透過狀態暫存器或中斷(IRQ)回報給驅動程式。
- skip-bytes/BBM 則是 Controller 在「讀/寫 pipeline」裡動手腳,保護或覆寫某些 OOB 區域。
3. 整體改善概述
3.1 Linux:中斷聚合與等待語意標準化
- ISR(中斷服務例程) 改為「先清除硬體狀態,再聚合、最後完成通知」,避免殘留狀態導致上層誤判。
- 等待端使用 completion + timeout 的單一路徑,所有命令(CDMA/SDMA/Generic)統一走相同等待語意,失敗時回拋一致化的狀態快照。
- R/B#(RBN_SETINGS) 與各 timing 的讀寫在同一個層級檢查,避免命令序列與 Ready 判斷脫鉤。
示意(節錄):ISR 聚合與完成通知
static irqreturn_t cadence_nand_isr(int irq, void *dev_id)
{
struct cadence_nand_ctrl *c = dev_id;
struct cadence_nand_irq_status st = {0};
/* 讀取 + 清除硬體狀態 */
cadence_nand_get_and_clear_irq(c, &st);
/* 聚合到軟體狀態(避免多中斷分次遺失) */
c->irq_status.status |= st.status;
c->irq_status.trd_status |= st.trd_status;
c->irq_status.trd_error |= st.trd_error;
/* 發出完成通知,喚醒等待端 */
complete(&c->complete);
return IRQ_HANDLED;
};
示意(節錄):通用等待(CDMA/SDMA/Generic 共用語意)
static int cadence_nand_wait_for_irq(struct cadence_nand_ctrl *c,
const struct cadence_nand_irq_status *mask,
struct cadence_nand_irq_status *out)
{
unsigned long tmo = msecs_to_jiffies(TIMEOUT_MS);
reinit_completion(&c->complete);
cadence_nand_set_irq_mask(c, mask);
if (!wait_for_completion_timeout(&c->complete, tmo))
return -ETIMEDOUT;
/* 讀聚合後的軟體狀態快照 */
*out = c->irq_status;
return 0;
}
3.2 U-Boot:改良等待模型 + 提供 真正 raw 的 dump
- 在 U-Boot 缺少完整 IRQ 框架的前提下,我以 polling + get_timer/udelay 模擬與 Linux 等價的等待語意(含 timeout),確保跨域一致。
- 新增
nand dump.raw指令,用於直接讀取 page + OOB 的原始位元組: - 不經 HWECC、控制器 skip-bytes 或驅動改寫;
- 可直接檢視 BBM 與各家 NAND 的 OOB 佈局(metadata/ECC 區塊分佈)。
示意(節錄):U-Boot 模擬等待
static int cadence_nand_wait_for_irq_poll(struct cadence_nand_ctrl *c, u32 ms)
{
ulong start = get_timer(0);
for (;;) {
if (cadence_nand_isr(c->irq, c) == IRQ_HANDLED)
return 0;
if (get_timer(start) > ms)
return -ETIMEDOUT;
udelay(1);
}
}
示意(節錄):nand dump.raw <off> 讀 page+OOB(真 raw)
off &= ~(mtd->writesize - 1);
page = (int)(off >> chip->page_shift);
chipnr = (int)(off >> chip->chip_shift);
chip->select_chip(mtd, chipnr);
ret = nand_read_page_op(chip, page, 0, buf, mtd->writesize + mtd->oobsize);
chip->select_chip(mtd, -1);
hexdump_page_and_oob(buf, mtd->writesize, mtd->oobsize);
3.3 OP-TEE:與 Linux 對齊的命令與等待語意
- 在 secure world 維持與 Linux 相同的命令輸出與等待時序,避免 normal/secure 的行為差異造成診斷與維護困難。
4. Raw NAND 的命令路徑與等待條件
- Generic Command:設定 mini-controller 命令欄位 → 送出 → 等待 IRQ/Ready。
- CDMA/SDMA:描述子準備 → 啟動 → 以 IRQ/timeout 判斷完成與錯誤。
- R/B#(READY/BUSY):對需要 Ready 的命令,必須在對應路徑上加入 R/B# 判斷或 tWB/tADL 的等待,否則將出現頁面讀寫間歇性錯誤或效能下滑。
實務上,「四連讀 94us」與「單次 CDMA 慢許多」常源自命令路徑不同、等待條件不同與 控制器延伸讀寫模式(extended mode) 設定差異。統一等待語意後,兩條路徑的瓶頸可明確辨識(是 tR 的裝置時間、還是傳輸/軟體路徑造成的間隙)。
5. 三層 ECC:HWECC、On-Die、SW 的並存設計
- HWECC(控制器):速度佳,需正確管理 sector 切分與 syndrome。
- On-Die ECC(顆粒):以廠商定義命令(如 ECC STATUS READ)讀回狀態。
- SW ECC(軟體檢核):在懷疑前兩者報告不一致時,以 raw dump 的位元組為準進行校驗,釐清錯誤來源(顆粒 vs. 控制器 vs. 驅動)。
此三層並存,提供了量測與歸因的「基準座標系」。沒有 raw 視角,工程判斷容易偏誤。
6. U-Boot 的 raw dump 與 OOB/BBM 可觀測性
我新增的 **dump.raw** 可直接觀察:
- OOB 真實內容:未被硬體或驅動改寫的 metadata/ECC 區塊;
- BBM(Bad Block Marker):通常位於 OOB 的前兩個位元組;多數顆粒以
0xFF 0xFF為好塊,任一非0xFF代表壞塊。 - 控制器 skip-bytes 機制:可用來「保留」BBM(以指定 marker 覆寫),但在分析時必須先釐清你看到的是「原始值」或「被覆寫後的值」。
注意:只有 raw dump 才能保證觀測值可被(資料取證/量測)信任。一般 API 若經過 HWECC/skip-bytes,輸出值未必等同晶片實際內容。
diff --git a/cmd/nand.c b/cmd/nand.c
--- a/cmd/nand.c
+++ b/cmd/nand.c
@@ -32,6 +32,7 @@
#include <asm/byteorder.h>
#include <jffs2/jffs2.h>
#include <nand.h>
+#include <linux/mtd/rawnand.h>
#include "legacy-mtd-utils.h"
@@ -43,6 +44,73 @@ int find_dev_and_part(const char *id, struct mtd_device **dev,
u8 *part_num, struct part_info **part);
#endif
+static int nand_dump_raw(struct mtd_info *mtd, ulong off,
+ int repeat)
+{
+ int page, realpage, chipnr;
+ struct nand_chip *chip = mtd_to_nand(mtd);
+ int i;
+ u_char *datbuf, *p;
+ static loff_t last;
+ int ret = 0;
+
+ if (repeat)
+ off = last + mtd->writesize;
+
+ last = off;
+
+ datbuf = memalign(ARCH_DMA_MINALIGN, mtd->writesize + mtd->oobsize);
+ if (!datbuf) {
+ puts("No memory for raw page buffer\n");
+ return 1;
+ }
+
+ // align writesize
+ off &= ~(mtd->writesize - 1);
+
+ // off to page number
+ realpage = (int)(off >> chip->page_shift);
+ page = realpage & chip->pagemask;
+
+ // read page
+ chipnr = (int)(off >> chip->chip_shift);
+ chip->select_chip(mtd, chipnr);
+ ret = nand_read_page_op(chip, page, 0, datbuf, mtd->writesize + mtd->oobsize);
+ chip->select_chip(mtd, -1);
+ if (ret) {
+ printf("Error (%d) reading raw page %08lx\n", i, off);
+ ret = 1;
+ goto free_dat;
+ }
+
+ printf("RAW page addr:%08lx / page:%08x dump:\n", off, page);
+ i = mtd->writesize >> 4;
+ p = datbuf;
+ while (i--) {
+ printf("\t%02x %02x %02x %02x %02x %02x %02x %02x"
+ " %02x %02x %02x %02x %02x %02x %02x %02x\n",
+ p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7],
+ p[8], p[9], p[10], p[11], p[12], p[13], p[14],
+ p[15]);
+ p += 16;
+ }
+
+ printf("\n");
+ printf("RAW OOB addr:%08lx / page:%08x:\n", off, page);
+ i = mtd->oobsize >> 3;
+ p = datbuf + mtd->writesize;
+ while (i--) {
+ printf("\t%02x %02x %02x %02x %02x %02x %02x %02x\n",
+ p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]);
+ p += 8;
+ }
+
+free_dat:
+ free(datbuf);
+
+ return ret;
+}
+
7. BBM 與 skip-bytes 的實作要點
控制器提供兩個寄存器來保護壞塊標記(避免被上層寫入覆蓋):
SKIP_BYTES_CONF(含 marker 值 與 skip bytes 數;需對齊到 8/16/32-bit,視 SDR/DDR 與 extended mode 而定)。SKIP_BYTES_OFFSET(從「本次傳輸資料包的起點」算起的偏移;不以 column address 為基準)。
當 skip 有效時,控制器會在該區段以 marker 取代原始資料,使 BBM 能被「保留為好」。這對量產系統避免誤判壞塊有實際價值;但在偵錯與還原場景,需要以 dump.raw 取得「未覆寫」的位元組佐證。
8. 關鍵:SMP/MP 系統中的 MMIO 次序化(barrier I/O)
在多核心(SMP)或外設深度排程的系統上,MMIO 存取的次序化是穩定性的根本。
我加入以下包裝巨集,強制在需要時使用具備屏障語意的 I/O,避免 *_relaxed 在關鍵路徑造成 out-of-order 或 write-posting 導致的難解問題。
#if HPNFC_ENABLE_BARRIER_IO
#define cdns_readl readl
#define cdns_writel writel
#define cdns_readl_poll_timeout readl_poll_timeout
#else
#define cdns_readl readl_relaxed
#define cdns_writel writel_relaxed
#define cdns_readl_poll_timeout readl_relaxed_poll_timeout
#endif
#define cdns_readl_barrier readl
#define cdns_writel_barrier writel
#define cdns_readl_barrier_poll_timeout readl_poll_timeout
#define cdns_readl_relaxed readl_relaxed
#define cdns_writel_relaxed writel_relaxed
#define cdns_readl_relaxed_poll_timeout readl_relaxed_poll_timeout
為何重要:
readl/writel(非 relaxed)在多數架構上具備 I/O 屏障,可確保對同一外設寄存器的讀寫順序不被 CPU 或匯流排重新排序。*_relaxed允許更多硬體/微架構最佳化(如 write-combine / posting),在時序敏感或「寫後立即讀狀態」的控制序列上容易出錯。- 透過條件編譯
HPNFC_ENABLE_BARRIER_IO,可在量測/驗證期間強制嚴格次序化,問題定位完成後再視情況切回 relaxed 以爭取極限效能。 - 特別是在 SMP(多處理器) 場景,若不同核心交錯存取控制器暫存器,缺乏屏障將引發間歇性不可重現的錯誤。這類錯誤往往只在壓力測試或角落時序(如高頻切換、DMA 連動)出現,延宕專案風險極高。
實務準則:
- 命令發送序列、狀態輪詢、IRQ 使能/遮罩 等關鍵點,*使用 `cdns_`(barrier 版)**;
- 高頻讀資料路徑(確定無時序依賴)可選擇
*_relaxed; - 任何「寫入控制暫存器 → 立即讀狀態判定」都要用 barrier 版本,否則會撞上 out-of-order 或 posted write。
9. 效能診斷方法論(簡述)
- 逐頁面時間模型:以(tR + 傳輸 + 軟體開銷)為基本單位估算可達 MB/s;若
R/B#等待(如 40–70 µs)佔比高,實測吞吐必然低於介面峰值。 - 命令路徑差異:連續讀(預取/流水)與單發 CDMA 的空檔差異,常來自等待語意或控制器擴展模式設定不同。
- 跨域一致性:Linux 與 U-Boot 的等待模型/次序化一致後,才能公平比較與歸因。
- raw 佐證:所有 ECC 狀態與 OOB 佈局爭議,以
dump.raw取樣為準確依據。
10. 結論
本次工作將 Cadence Raw NAND Controller 於 Linux、U-Boot、OP-TEE 的操作語意與等待機制完成一致化,並引入:
- U-Boot
dump.raw:可檢視真實 page + OOB,為 ECC/BBM/metadata 診斷提供可信觀測; - 三層 ECC(HWECC/On-Die/SW):建立完整的錯誤歸因體系;
- skip-bytes 與 BBM 策略:在量產與偵錯兩種場景間具備可交換的保護與觀測;
- barrier I/O 封裝:於 SMP/MP 環境下明確管控 MMIO 次序化,避免非決定性錯誤。
在實作 NAND 子系統時,建議先建立上述「等待語意標準化 + raw 可觀測性 + I/O 次序化」三個基石。它們不直接等於效能參數,卻是所有效能優化與可靠度工程的必要前提。
메타데이터
- post_id
- 4b9fb03356a5
- slug
- cadence-raw-nand-controller-在-linux-u-boot-與-op-tee-的一致化實作與效能診斷-4b9fb03356a5
- url
- https://medium.com/@allenkuo/cadence-raw-nand-controller-%E5%9C%A8-linux-u-boot-%E8%88%87-op-tee-%E7%9A%84%E4%B8%80%E8%87%B4%E5%8C%96%E5%AF%A6%E4%BD%9C%E8%88%87%E6%95%88%E8%83%BD%E8%A8%BA%E6%96%B7-4b9fb03356a5
- canonical_url
- https://medium.com/@allenkuo/cadence-raw-nand-controller-%E5%9C%A8-linux-u-boot-%E8%88%87-op-tee-%E7%9A%84%E4%B8%80%E8%87%B4%E5%8C%96%E5%AF%A6%E4%BD%9C%E8%88%87%E6%95%88%E8%83%BD%E8%A8%BA%E6%96%B7-4b9fb03356a5
- author_url
- https://medium.com/@allenkuo
- status
- ok
- fetched_at
- 2026-06-20 20:29:01