跳至主要内容

MediaTek UART APDMA:從 Virtual FIFO 硬體模型讀懂 mtk-uart-apdma.c

本文對照 torvalds/linux master 分支(2026-07)的 drivers/dma/mediatek/mtk-uart-apdma.cdrivers/tty/serial/8250/8250_mtk.c 撰寫,所有 register offset、常數與函式名皆取自 upstream source,非記憶推測。文末附驗證說明。

一、先把名字搞清楚

在 MediaTek 平台上「APDMA」這個詞會出現在兩個不同層次,很容易混淆:

  • 硬體上的 APDMA:Application Processor DMA,掛在 peripheral bus 上、服務低速周邊(UART / I2C / SPI 等)的 DMA controller。
  • Linux driver 裡的 APDMA:upstream 目前只有 UART 這一支被 mainline 化,檔案是 drivers/dma/mediatek/mtk-uart-apdma.c,Kconfig 是 CONFIG_MTK_UART_APDMA

所以當有人說「APDMA driver 掛掉了」,在 upstream 語境下九成是指 UART 那支。

另外要跟同目錄下的兩個兄弟區分開,它們是完全不同的 controller,不要混用除錯經驗:

檔案用途
mtk-uart-apdma.cUART 專用,Virtual FIFO 模型
mtk-hsdma.cHigh-Speed DMA,memcpy 類
mtk-cqdma.cCommand-Queue DMA

還有一個常見誤記:DT compatible 是 mediatek,mt6577-uart-dma中間沒有 ap。寫 mediatek,mt6577-uart-apdma 是 probe 不起來的。

二、為什麼 UART 需要 DMA

傳統 8250 UART 靠 FIFO + 中斷收送。FIFO 通常只有 16~64 bytes,baud rate 拉到 3~4 Mbps(典型場景:藍牙 H4/H5 over UART、GNSS、modem control channel)時:

  • RX 端 FIFO 很快滿,來不及進中斷就 overrun,封包直接掉。
  • 中斷頻率高到吃掉可觀的 CPU 時間,而且每次都是 IRQ context。

DMA 的價值不只是「省 CPU」,更關鍵的是把 timing 容忍度從微秒級拉到毫秒級。這在 tablet / phone 專案上直接決定 BT firmware download 會不會失敗。

三、核心:Virtual FIFO(VFF)模型

MediaTek 這顆 DMA 不是常見的「給我一個 buffer、搬完通知你」的 one-shot 模型,而是 ring buffer + 硬體讀寫指標,也就是把 DRAM 的一塊區域當成一個「虛擬的、很大的 FIFO」——這就是 VFF 前綴的由來。

概念上:

VFF_ADDR ─────────────────────────► DRAM ring buffer
┌───────────────────────────────────┐
│ │
│ ◄─ VFF_LEN (ring 大小) ─► │
└───────────────────────────────────┘
▲ ▲
VFF_RPT VFF_WPT
(read pointer) (write pointer)

TX 方向:SW 推進 WPT → HW 追著讀,推進 RPT
RX 方向:HW 推進 WPT → SW 追著讀,推進 RPT

方向決定了誰是生產者。這點在讀 code 時非常重要,因為同一組 register 在 TX / RX 的語意是鏡像的。source 裡有兩行註解把這件事講得最清楚:

/* TX: the buffer size HW can read. RX: the buffer size SW can read. */
#define VFF_VALID_SIZE 0x3c
/* TX: the buffer size SW can write. RX: the buffer size HW can write. */
#define VFF_LEFT_SIZE 0x40

也就是 VALID_SIZE 永遠是「已經有資料、可被消費的量」,LEFT_SIZE 永遠是「還有空間、可被生產的量」,只是消費者/生產者的身分依方向對調。

3.1 Ring wrap 的處理

Ring 大小上限與 wrap 標記是分開的兩個欄位,藏在同一個 32-bit 值裡:

#define VFF_RING_SIZE 0xffff
/* invert this bit when wrap ring head again */
#define VFF_RING_WRAP 0x10000

低 16 bits 是實際 offset,bit 16 是 wrap flag。每繞完一圈就把這個 bit 反轉。這是經典的「多一位元解決 full/empty 歧義」手法——只看 offset 的話,RPT == WPT 無法分辨 ring 是空的還是滿的;加上 wrap bit 之後就沒有歧義。

RX handler 計算已收到多少 bytes 時就是靠這個:

rg = mtk_uart_apdma_read(c, VFF_RPT);
wg = mtk_uart_apdma_read(c, VFF_WPT);
cnt = (wg & VFF_RING_SIZE) - (rg & VFF_RING_SIZE);

/*
* The buffer is ring buffer. If wrap bit different,
* represents the start of the next cycle for WPT
*/
if ((rg ^ wg) & VFF_RING_WRAP)
cnt += len;

(rg ^ wg) & VFF_RING_WRAP 就是在問「這兩個指標在不在同一圈」。不在同一圈的話 cnt 會是負的,補一個 ring 長度回來。

3.2 中斷門檻不對稱

TX 跟 RX 的 threshold 策略刻意不同:

/*
* interrupt trigger level for tx
* if threshold is n, no polling is required to start tx.
* otherwise need polling VFF_FLUSH.
*/
#define VFF_TX_THRE(n) (n)
/* interrupt trigger level for rx */
#define VFF_RX_THRE(n) ((n) * 3 / 4)
  • TX threshold = 整個 ring 大小。意思是「等到 ring 全空才中斷」,等同於「這批資料全部送完了」。註解也點出,如果設成別的值就得去 polling VFF_FLUSH,那就失去意義了。
  • RX threshold = 3/4 ring。留 1/4 的緩衝空間給「中斷發出後、SW 還沒來得及處理完」這段時間繼續寫入。這是防 overrun 的安全邊際,不是隨手取的數字。

四、Register map 全表

全部來自 source,base 是每個 channel 各自的 iomem:

Offset名稱說明
0x00VFF_INT_FLAG中斷狀態 / 清除
0x04VFF_INT_EN中斷致能
0x08VFF_ENDMA 致能
0x0cVFF_RSTWarm reset
0x10VFF_STOP停止請求
0x14VFF_FLUSH把殘留資料沖出去
0x1cVFF_ADDRRing buffer 實體位址(低 32 bits)
0x24VFF_LENRing 長度
0x28VFF_THRE中斷門檻
0x2cVFF_WPTWrite pointer
0x30VFF_RPTRead pointer
0x3cVFF_VALID_SIZE可消費量(方向相關)
0x40VFF_LEFT_SIZE可生產量(方向相關)
0x50VFF_DEBUG_STATUS除錯狀態,卡住時的第一手線索
0x54VFF_ADDR2位址高位元(>32-bit 定址用)

注意 0x180x200x340x38 是空洞——driver 沒用到,不代表硬體沒有,只是 upstream 用不上。

五、Driver 架構

5.1 建立在 virt-dma 之上

#include "../virt-dma.h"

driver 不自己管 descriptor queue,而是用 kernel 的 virtual DMA channel 框架。三層結構:

struct mtk_uart_apdmadev { /* controller 層 */
struct dma_device ddev;
struct clk *clk;
bool support_ext_addr;
unsigned int dma_requests;
};

struct mtk_chan { /* channel 層 */
struct virt_dma_chan vc;
struct dma_slave_config cfg;
struct mtk_uart_apdma_desc *desc;
enum dma_transfer_direction dir;
void __iomem *base;
unsigned int irq;
unsigned int rx_status;
};

struct mtk_uart_apdma_desc { /* descriptor 層 */
struct virt_dma_desc vd;
dma_addr_t addr;
unsigned int avail_len;
};

struct mtk_chan 特別值得看:每個 channel 有自己的 base 和自己的 irq。這解釋了為什麼 DT 裡 reginterrupts 都是一長串——不是一個 controller 配一組,而是 N 個獨立的 register block。

預設 channel 數:

#define MTK_UART_APDMA_NR_VCHANS 8

可被 DT 的 dma-requests 覆寫。每個 UART port 要 TX / RX 各一條,所以 8 條大約服務 4 個 port。

5.2 註冊的 dmaengine ops

dma_cap_set(DMA_SLAVE, mtkd->ddev.cap_mask);
mtkd->ddev.device_alloc_chan_resources = mtk_uart_apdma_alloc_chan_resources;
mtkd->ddev.device_free_chan_resources = mtk_uart_apdma_free_chan_resources;
mtkd->ddev.device_tx_status = mtk_uart_apdma_tx_status;
mtkd->ddev.device_issue_pending = mtk_uart_apdma_issue_pending;
mtkd->ddev.device_prep_slave_sg = mtk_uart_apdma_prep_slave_sg;
mtkd->ddev.device_config = mtk_uart_apdma_slave_config;
mtkd->ddev.device_pause = mtk_uart_apdma_device_pause;
mtkd->ddev.device_terminate_all = mtk_uart_apdma_terminate_all;
mtkd->ddev.src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE);
mtkd->ddev.dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE);
mtkd->ddev.directions = BIT(DMA_DEV_TO_MEM) | BIT(DMA_MEM_TO_DEV);
mtkd->ddev.residue_granularity = DMA_RESIDUE_GRANULARITY_SEGMENT;

三個關鍵限制值得記下來:

  1. 只支援 1-byte bus width。這是 UART,本來就是 byte stream。
  2. 只有 slave 方向,沒有 DMA_MEMCPY。要 memcpy 請找 HSDMA/CQDMA。
  3. 沒有 device_resume。有 pause 卻沒有 resume——這是設計上的取捨,pause 之後實際上得靠重新 issue 來恢復。

5.3 只吃一個 scatterlist entry

/*
* dmaengine_prep_slave_single will call the function. and sglen is 1.
* 8250 uart using one ring buffer, and deal with one sg.
*/
static struct dma_async_tx_descriptor *mtk_uart_apdma_prep_slave_sg(...)
{
if (!is_slave_direction(dir) || sglen != 1)
return NULL;
...
}

sglen != 1 直接回 NULL。因為 VFF 模型本來就是「一塊連續 ring buffer」,scatter-gather 在這裡沒有意義。這也是為什麼呼叫端一律用 dmaengine_prep_slave_single() 而非 _sg()

除錯提示:如果你在 log 看到 prep 失敗但參數看起來都對,先確認呼叫端沒有傳多段 sg。

六、三條主要路徑

6.1 TX

mtk_uart_apdma_start_tx() 的邏輯:

vff_sz = c->cfg.dst_port_window_size;
if (!mtk_uart_apdma_read(c, VFF_LEN)) {
/* 第一次才做完整初始化 */
mtk_uart_apdma_write(c, VFF_ADDR, d->addr);
mtk_uart_apdma_write(c, VFF_LEN, vff_sz);
mtk_uart_apdma_write(c, VFF_THRE, VFF_TX_THRE(vff_sz));
mtk_uart_apdma_write(c, VFF_WPT, 0);
mtk_uart_apdma_write(c, VFF_INT_FLAG, VFF_TX_INT_CLR_B);
if (mtkd->support_ext_addr)
mtk_uart_apdma_write(c, VFF_ADDR2, upper_32_bits(d->addr));
}

VFF_LEN == 0 當作「尚未初始化」的判斷條件——ring 長度為 0 是不可能的合法狀態,拿來當 sentinel 很省事。後續傳輸只推進 pointer,不重設 ring。

接著是推進 WPT,注意 wrap 處理:

wpt = mtk_uart_apdma_read(c, VFF_WPT);
wpt += c->desc->avail_len;
if ((wpt & VFF_RING_SIZE) == vff_sz)
wpt = (wpt & VFF_RING_WRAP) ^ VFF_RING_WRAP;

/* Let DMA start moving data */
mtk_uart_apdma_write(c, VFF_WPT, wpt);

當 offset 剛好走到 ring 邊界,就把 offset 歸零、翻轉 wrap bit。(wpt & VFF_RING_WRAP) ^ VFF_RING_WRAP 這個寫法同時完成了「清掉低位 offset」和「反轉 wrap bit」兩件事。

最後補一個 flush:

if (!mtk_uart_apdma_read(c, VFF_FLUSH))
mtk_uart_apdma_write(c, VFF_FLUSH, VFF_FLUSH_B);

確保尾端不足一個 burst 的資料也會被推出去,不會卡在硬體裡。

6.2 RX 與 residue 回報

RX 端 mtk_uart_apdma_rx_handler() 有一個容易忽略的 early return:

mtk_uart_apdma_write(c, VFF_INT_FLAG, VFF_RX_INT_CLR_B);

if (!mtk_uart_apdma_read(c, VFF_VALID_SIZE))
return;

先清中斷旗標,再檢查有沒有資料。 順序不能反——反過來會漏掉「中斷已觸發但資料在檢查瞬間被讀完」的競態。而且這個 early return 走的是「不關 DMA、繼續收」的路徑,只有真的有資料時才停下來處理。

計算完 cnt 之後:

c->rx_status = d->avail_len - cnt;
mtk_uart_apdma_write(c, VFF_RPT, wg);

rx_status 存的是 residue(還沒收到的量),透過 mtk_uart_apdma_tx_status() 回報給上層:

dma_set_residue(txstate, c->rx_status);

呼叫端就用 rx_size - residue 反推「這次實際收到多少」。8250_mtk 那邊正是這樣算的:

dmaengine_tx_status(dma->rxchan, dma->rx_cookie, &state);
total = dma->rx_size - state.residue;

這是整條 RX 路徑的資料量真相來源。如果收到的 byte 數不對,rx_status 的計算就是第一個要下斷點的地方。

6.3 Terminate:三步驟停機

mtk_uart_apdma_terminate_all() 裡有一段註解直接寫出硬體要求的順序:

/*
* Stop need 3 steps.
* 1. set stop to 1
* 2. wait en to 0
* 3. set stop as 0
*/
mtk_uart_apdma_write(c, VFF_STOP, VFF_STOP_B);
ret = readx_poll_timeout(readl, c->base + VFF_EN,
status, !status, 10, 100);
if (ret)
dev_err(c->vc.chan.device->dev, "stop: fail, status=0x%x\n",
mtk_uart_apdma_read(c, VFF_DEBUG_STATUS));

mtk_uart_apdma_write(c, VFF_STOP, VFF_STOP_CLR_B);

在此之前還會先 flush 並等 flush 完成,同樣有 100 µs timeout。兩處 timeout 失敗時都會把 VFF_DEBUG_STATUS 印出來——這是 driver 主動留給你的除錯線索,遇到 stop: failflush: fail 一定要把那個 status 值記下來。

最後 synchronize_irq(c->irq) 確保沒有 handler 還在跑,才去釋放 descriptor。這個順序是防 use-after-free 的必要條件。

七、消費端:8250_mtk.c

7.1 用不用 DMA 由 DT 決定

dmacnt = of_property_count_strings(pdev->dev.of_node, "dma-names");
if (dmacnt == 2) {
data->dma = devm_kzalloc(...);
data->dma->fn = mtk8250_dma_filter;
data->dma->rx_size = MTK_UART_RX_SIZE;
data->dma->rxconf.src_maxburst = MTK_UART_RX_TRIGGER;
data->dma->txconf.dst_maxburst = MTK_UART_TX_TRIGGER;
}

必須剛好兩個 dma-names(tx / rx),少一個就整個不啟用,而且不會有錯誤訊息。DT 寫錯時的症狀是「安靜地退回 PIO 模式」,效能掉了但沒有任何 log——這種無聲失敗最難查。

相關常數:

#define MTK_UART_RX_SIZE 0x8000 /* 32 KB ring */
#define MTK_UART_TX_TRIGGER 1
#define MTK_UART_RX_TRIGGER MTK_UART_RX_SIZE

RX ring 是 32 KB。搭配前面 VFF_RX_THRE(n) = n * 3 / 4,實際中斷門檻是 24 KB。

7.2 Window size 是怎麼傳下去的

dma->rxconf.src_port_window_size = dma->rx_size;
dma->rxconf.src_addr = dma->rx_addr;
dma->txconf.dst_port_window_size = UART_XMIT_SIZE;
dma->txconf.dst_addr = dma->tx_addr;

APDMA driver 裡的 vff_sz 就是從 src_port_window_size / dst_port_window_size 讀出來的。這是一個相對少見的 dma_slave_config 欄位用法——它在這裡的語意是 ring buffer 大小,而不是字面上的「port window」。讀 code 時如果直覺套用一般意義會誤解。

TX 用的是 UART_XMIT_SIZE(tty layer 的標準值),跟 RX 的 32 KB 不同。

7.3 Console 一律不走 DMA

/* disable DMA for console */
if (uart_console(port))
up->dma = NULL;

這行在 mtk8250_startup() 裡。只要這個 port 被當成 console,DMA 就被強制關掉。

實務上這造成一個很容易誤判的現象:

你在 chosen/stdout-path 指定了某個 UART 當 console,然後懷疑 APDMA 沒作用,去量效能——結果當然沒作用,因為 code 直接把它關了。

原因是 console 需要在任意 context(包含 panic、atomic、IRQ disabled)同步輸出,DMA 的非同步語意根本無法滿足。這是設計決定,不是 bug。

除錯時務必先確認你測的 port 不是 console。

八、DT 綁定與定址寬度演進

apdma: dma-controller@11000400 {
compatible = "mediatek,mt2712-uart-dma",
"mediatek,mt6577-uart-dma";
reg = <0 0x11000400 0 0x80>, /* channel 0 */
<0 0x11000480 0 0x80>, /* channel 1 */
<0 0x11000500 0 0x80>,
...; /* 每 channel 一個 0x80 block */
interrupts = <...>; /* 每 channel 一個 */
clocks = <...>;
clock-names = "apdma";
#dma-cells = <1>;
};

每個 channel 佔 0x80reginterrupts 的項數必須跟 dma-requests 一致,否則 probe 時 devm_platform_ioremap_resource(pdev, i)platform_get_irq(pdev, i) 會失敗。

of_device_id 表把定址寬度編碼進 .data

static const struct of_device_id mtk_uart_apdma_match[] = {
{ .compatible = "mediatek,mt6577-uart-dma", .data = (void *)32 },
{ .compatible = "mediatek,mt6795-uart-dma", .data = (void *)33 },
{ .compatible = "mediatek,mt6835-uart-dma", .data = (void *)34 },
{ .compatible = "mediatek,mt6985-uart-dma", .data = (void *)35 },
{ /* sentinel */ },
};

32 → 33 → 34 → 35 bits,直接反映了行動裝置記憶體容量往上爬的歷史(4 GB → 8 GB → 16 GB → 32 GB)。

probe 時的處理:

bit_mask = (unsigned int)(uintptr_t)of_device_get_match_data(&pdev->dev);
if (bit_mask > 32)
mtkd->support_ext_addr = true;

rc = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(bit_mask));

超過 32 bits 就啟用 VFF_ADDR2 寫入高位元。

這是實務上的高頻踩雷點:如果 DT 上只寫了 mediatek,mt6577-uart-dma(32-bit),但 buffer 被配置在 4 GB 以上的實體位址,DMA 會寫到錯誤位置。舊 binding 文件裡的 mediatek,dma-33bits 屬性後來被 compatible-based 的做法取代了。新平台請確認 compatible 用對,不要為了「反正相容」就套舊的字串。

九、除錯清單

依「最常見 → 最少見」排序:

  1. 這個 port 是 console 嗎? 是的話 DMA 被 mtk8250_startup() 強制關閉,一切效能測試無效。
  2. DT 的 dma-names 剛好兩個嗎? 不是的話靜默退回 PIO,沒有任何錯誤訊息。
  3. compatible 字串對嗎?-uart-dma 不是 -uart-apdma;定址寬度也綁在這個字串上。
  4. reg / interrupts 的項數對得上 dma-requests 嗎? 對不上會在 probe 迴圈第 i 次失敗。
  5. Clock 和 power domain 上了嗎? devm_clk_get() 失敗會直接 dev_err("No clock specified") 並中止 probe;driver 用 pm_runtime,channel 的 alloc/free 對應 pm_runtime_resume_and_get() / pm_runtime_put_sync()
  6. 看到 stop: failflush: fail 了嗎? 把訊息裡的 VFF_DEBUG_STATUS 值抄下來,那是硬體卡在哪的直接證據。
  7. 收到的 byte 數不對? 斷點下在 mtk_uart_apdma_rx_handler()c->rx_status = d->avail_len - cnt;,檢查 wrap bit 的判斷。
  8. 34/35-bit 平台上偶發資料錯亂? 檢查 support_ext_addr 有沒有被設起來、VFF_ADDR2 有沒有寫入。

Runtime 觀察管道:

# 確認 driver 有註冊
ls /sys/class/dma/

# 中斷次數(有 DMA 的話 UART 中斷應該顯著少於 PIO 模式)
cat /proc/interrupts | grep -i -E 'apdma|uart'

# 動態 log
echo 'file mtk-uart-apdma.c +p' > /sys/kernel/debug/dynamic_debug/control

十、小結

mtk-uart-apdma.c 只有 653 行,但濃縮了幾個值得帶走的通用觀念:

  • Ring buffer 用額外一個 bit 解決 full/empty 歧義,是 lock-free 佇列的經典手法,不限於 DMA。
  • TX / RX threshold 不對稱反映了兩個方向風險不同:TX 慢一點只是延遲,RX 慢一點會掉資料。
  • 同一組 register 在不同方向語意鏡像,讀 MediaTek 硬體 driver 時是常態,先確認方向再讀 code。
  • 「安靜地退回較差路徑」是最難查的失敗模式。 dma-names 數量不對就不啟用 DMA、console port 直接關 DMA,兩個都不會留下 log。遇到「功能正常但效能不對」的問題,優先懷疑這類無聲降級。

驗證說明

本文所有 code 片段、register offset、常數值與註解,皆直接取自以下 upstream 檔案(2026-07-29 抓取自 raw.githubusercontent.com/torvalds/linux/master):

  • drivers/dma/mediatek/mtk-uart-apdma.c(653 行)
  • drivers/tty/serial/8250/8250_mtk.c

DT binding 範例取自 Documentation/devicetree/bindings/dma/mtk-uart-apdma.txt(kernel.org 線上版本)。該文件在較新的 kernel 已轉為 YAML 格式,欄位語意一致但檔名不同,實作前請以你所用 kernel 版本的 binding 為準。

以下為作者依 code 行為所作的推論,非文件明述,引用時請自行驗證:

  • 34/35-bit 定址寬度對應 16 GB / 32 GB 記憶體容量的說法,是依 DMA_BIT_MASK 語意推得,MediaTek 未公開說明各數值的導入動機。
  • 「8 條 channel 約服務 4 個 port」是由「TX/RX 各一條」推得的算術,實際配置依平台 DT 而定。
  • 除錯清單的排序依據為作者經驗,非統計數據。

VFF_DEBUG_STATUS 各 bit 的意義需要 MediaTek 內部文件,本文未涵蓋。