編寫 MUSB Glue Layer¶
- 作者:
Apelete Seketeli
簡介¶
Linux MUSB 子系統是更大的 Linux USB 子系統的一部分。 它為不使用通用主機控制器介面(UHCI)或開放主機控制器介面(OHCI)的嵌入式 USB 裝置控制器(UDC)提供支援。
相反,這些嵌入式 UDC 依賴於 USB On-the-Go(OTG)規範,它們至少部分實現該規範。 在大多數情況下使用的矽參考設計是在 Mentor Graphics Inventra™ 設計中找到的多點 USB 高速雙角色控制器(MUSB HDRC)。
作為自學練習,我已經為 Ingenic JZ4740 SoC 編寫了一個 MUSB glue layer,該層模仿核心原始碼樹中的許多 MUSB glue layer。 該層可以在 drivers/usb/musb/jz4740.c 中找到。 在本文件中,我將介紹 jz4740.c glue layer 的基礎知識,解釋不同的部分以及編寫自己的裝置 glue layer 需要做什麼。
Linux MUSB 基礎¶
要開始討論這個主題,請閱讀 USB On-the-Go 基礎(請參閱資源),其中提供了硬體級別 USB OTG 操作的介紹。 Texas Instruments 和 Analog Devices 的一些 Wiki 頁面也提供了 Linux 核心 MUSB 配置的概述,儘管重點是這些公司提供的一些特定裝置。 最後,熟悉 USB 主頁上的 USB 規範可能會派上用場,並透過編寫 USB 裝置驅動文件提供實際示例(同樣,請參閱資源)。
Linux USB 堆疊是一個分層架構,其中 MUSB 控制器硬體位於最低層。 MUSB 控制器驅動程式將 MUSB 控制器硬體抽象到 Linux USB 堆疊
------------------------
| | <------- drivers/usb/gadget
| Linux USB Core Stack | <------- drivers/usb/host
| | <------- drivers/usb/core
------------------------
⬍
--------------------------
| | <------ drivers/usb/musb/musb_gadget.c
| MUSB Controller driver | <------ drivers/usb/musb/musb_host.c
| | <------ drivers/usb/musb/musb_core.c
--------------------------
⬍
---------------------------------
| MUSB Platform Specific Driver |
| | <-- drivers/usb/musb/jz4740.c
| aka "Glue Layer" |
---------------------------------
⬍
---------------------------------
| MUSB Controller Hardware |
---------------------------------
如上所述,glue layer 實際上是位於控制器驅動程式和控制器硬體之間的平臺特定程式碼。
正如 Linux USB 驅動程式需要向 Linux USB 子系統註冊自身一樣,MUSB glue layer 也需要首先向 MUSB 控制器驅動程式註冊自身。 這將使控制器驅動程式知道 glue layer 支援哪些裝置以及在檢測到或釋放支援的裝置時要呼叫的函式; 請記住,我們在這裡討論的是嵌入式控制器晶片,因此在執行時不能插入或移除。
所有這些資訊都透過一個 platform_driver 結構傳遞給 MUSB 控制器驅動程式,該結構在 glue layer 中定義為
static struct platform_driver jz4740_driver = {
.probe = jz4740_probe,
.remove = jz4740_remove,
.driver = {
.name = "musb-jz4740",
},
};
當檢測到匹配的裝置時,分別呼叫 probe 和 remove 函式指標,並釋放匹配的裝置。 name 字串描述了此 glue layer 支援的裝置。 在當前情況下,它與 arch/mips/jz4740/platform.c 中宣告的 platform_device 結構匹配。 請注意,我們在此處不使用裝置樹繫結。
為了向控制器驅動程式註冊自身,glue layer 需要經過幾個步驟,基本上是分配控制器硬體資源並初始化幾個電路。 為此,它需要跟蹤在這些步驟中使用的資訊。 這是透過定義私有 jz4740_glue 結構來完成的
struct jz4740_glue {
struct device *dev;
struct platform_device *musb;
struct clk *clk;
};
dev 和 musb 成員都是裝置結構變數。 第一個儲存有關裝置的通用資訊,因為它是基本的裝置結構,而後者儲存與設備註冊到的子系統更密切相關的資訊。 clk 變數儲存與裝置時鐘操作相關的資訊。
讓我們來看看 probe 函式的步驟,該函式使 glue layer 向控制器驅動程式註冊自身。
注意
為了便於閱讀,每個函式將被分成邏輯部分,每個部分都顯示為獨立於其他部分。
static int jz4740_probe(struct platform_device *pdev)
{
struct platform_device *musb;
struct jz4740_glue *glue;
struct clk *clk;
int ret;
glue = devm_kzalloc(&pdev->dev, sizeof(*glue), GFP_KERNEL);
if (!glue)
return -ENOMEM;
musb = platform_device_alloc("musb-hdrc", PLATFORM_DEVID_AUTO);
if (!musb) {
dev_err(&pdev->dev, "failed to allocate musb device\n");
return -ENOMEM;
}
clk = devm_clk_get(&pdev->dev, "udc");
if (IS_ERR(clk)) {
dev_err(&pdev->dev, "failed to get clock\n");
ret = PTR_ERR(clk);
goto err_platform_device_put;
}
ret = clk_prepare_enable(clk);
if (ret) {
dev_err(&pdev->dev, "failed to enable clock\n");
goto err_platform_device_put;
}
musb->dev.parent = &pdev->dev;
glue->dev = &pdev->dev;
glue->musb = musb;
glue->clk = clk;
return 0;
err_platform_device_put:
platform_device_put(musb);
return ret;
}
probe 函式的前幾行分配並分配 glue、musb 和 clk 變數。GFP_KERNEL 標誌(第 8 行)允許分配過程休眠並等待記憶體,因此可以在鎖定情況下使用。 PLATFORM_DEVID_AUTO 標誌(第 12 行)允許自動分配和管理裝置 ID,以避免裝置名稱空間與顯式 ID 發生衝突。 使用 devm_clk_get()(第 18 行),glue layer 分配時鐘 -- devm_ 字首指示 clk_get() 是受管理的:當裝置釋放時,它會自動釋放分配的時鐘資源資料 -- 並啟用它。
然後是註冊步驟
static int jz4740_probe(struct platform_device *pdev)
{
struct musb_hdrc_platform_data *pdata = &jz4740_musb_platform_data;
pdata->platform_ops = &jz4740_musb_ops;
platform_set_drvdata(pdev, glue);
ret = platform_device_add_resources(musb, pdev->resource,
pdev->num_resources);
if (ret) {
dev_err(&pdev->dev, "failed to add resources\n");
goto err_clk_disable;
}
ret = platform_device_add_data(musb, pdata, sizeof(*pdata));
if (ret) {
dev_err(&pdev->dev, "failed to add platform_data\n");
goto err_clk_disable;
}
return 0;
err_clk_disable:
clk_disable_unprepare(clk);
err_platform_device_put:
platform_device_put(musb);
return ret;
}
第一步是透過 platform_set_drvdata()(第 7 行)將 glue layer 私有持有的裝置資料傳遞給控制器驅動程式。 下一步是透過 platform_device_add_resources()(第 9 行)傳遞裝置資源資訊,這些資訊此時也私有持有。
最後一步是將平臺特定資料傳遞給控制器驅動程式(第 16 行)。 平臺數據將在 裝置平臺數據 中討論,但這裡我們關注 musb_hdrc_platform_data 結構(第 3 行)中的 platform_ops 函式指標(第 5 行)。 此函式指標使 MUSB 控制器驅動程式知道要呼叫哪個函式進行裝置操作
static const struct musb_platform_ops jz4740_musb_ops = {
.init = jz4740_musb_init,
.exit = jz4740_musb_exit,
};
在這裡,我們有最小的情況,其中控制器驅動程式在需要時僅呼叫 init 和 exit 函式。 事實是,JZ4740 MUSB 控制器是一個基本控制器,缺少其他控制器中的一些功能,否則我們可能還有指向一些其他函式的指標,例如電源管理函式或在 OTG 和非 OTG 模式之間切換的函式。
在該註冊過程的這一點上,控制器驅動程式實際上呼叫 init 函式
static int jz4740_musb_init(struct musb *musb) { musb->xceiv = usb_get_phy(USB_PHY_TYPE_USB2); if (!musb->xceiv) { pr_err("HS UDC: no transceiver configured\n"); return -ENODEV; } /* Silicon does not implement ConfigData register. * Set dyn_fifo to avoid reading EP config from hardware. */ musb->dyn_fifo = true; musb->isr = jz4740_musb_interrupt; return 0; }
jz4740_musb_init() 的目標是獲取 MUSB 控制器硬體的收發器驅動程式資料,並像往常一樣將其傳遞給 MUSB 控制器驅動程式。 收發器是控制器硬體內部的電路,負責傳送/接收 USB 資料。 由於它是 OSI 模型物理層的實現,因此收發器也稱為 PHY。
使用 usb_get_phy() 獲取 MUSB PHY 驅動程式資料,該函式返回指向包含驅動程式例項資料的結構的指標。 接下來的幾個指令(第 12 行和第 14 行)分別用作怪癖和設定 IRQ 處理。 怪癖和 IRQ 處理將在後面的 裝置怪癖 和 處理 IRQ 中討論
static int jz4740_musb_exit(struct musb *musb)
{
usb_put_phy(musb->xceiv);
return 0;
}
作為 init 的對應方,exit 函式在即將釋放控制器硬體本身時釋放 MUSB PHY 驅動程式。
同樣,請注意,在這種情況下,由於 JZ4740 控制器硬體的基本功能集,init 和 exit 非常簡單。 為更復雜的控制器硬體編寫 musb glue layer 時,您可能需要在這兩個函式中處理更多處理。
從 init 函式返回後,MUSB 控制器驅動程式跳回到 probe 函式
static int jz4740_probe(struct platform_device *pdev)
{
ret = platform_device_add(musb);
if (ret) {
dev_err(&pdev->dev, "failed to register musb device\n");
goto err_clk_disable;
}
return 0;
err_clk_disable:
clk_disable_unprepare(clk);
err_platform_device_put:
platform_device_put(musb);
return ret;
}
這是設備註冊過程的最後一部分,其中 glue layer 將控制器硬體裝置新增到 Linux 核心裝置層次結構中:在此階段,有關該裝置的所有已知資訊都將傳遞給 Linux USB 核心堆疊
static int jz4740_remove(struct platform_device *pdev) { struct jz4740_glue *glue = platform_get_drvdata(pdev); platform_device_unregister(glue->musb); clk_disable_unprepare(glue->clk); return 0; }
作為 probe 的對應方,remove 函式登出 MUSB 控制器硬體(第 5 行)並停用時鐘(第 6 行),從而允許對其進行門控。
處理 IRQ¶
除了 MUSB 控制器硬體的基本設定和註冊之外,glue layer 還負責處理 IRQ
static irqreturn_t jz4740_musb_interrupt(int irq, void *__hci) { unsigned long flags; irqreturn_t retval = IRQ_NONE; struct musb *musb = __hci; spin_lock_irqsave(&musb->lock, flags); musb->int_usb = musb_readb(musb->mregs, MUSB_INTRUSB); musb->int_tx = musb_readw(musb->mregs, MUSB_INTRTX); musb->int_rx = musb_readw(musb->mregs, MUSB_INTRRX); /* * The controller is gadget only, the state of the host mode IRQ bits is * undefined. Mask them to make sure that the musb driver core will * never see them set */ musb->int_usb &= MUSB_INTR_SUSPEND | MUSB_INTR_RESUME | MUSB_INTR_RESET | MUSB_INTR_SOF; if (musb->int_usb || musb->int_tx || musb->int_rx) retval = musb_interrupt(musb); spin_unlock_irqrestore(&musb->lock, flags); return retval; }
在這裡,glue layer 主要必須讀取相關的硬體暫存器並將其值傳遞給控制器驅動程式,控制器驅動程式將處理觸發 IRQ 的實際事件。
中斷處理程式關鍵部分受到 spin_lock_irqsave() 和對應 spin_unlock_irqrestore() 函式(分別為第 7 行和第 24 行)的保護,這些函式可防止中斷處理程式程式碼同時被兩個不同的執行緒執行。
然後讀取相關的中斷暫存器(第 9 到 11 行)
MUSB_INTRUSB:指示當前哪些 USB 中斷處於活動狀態,MUSB_INTRTX:指示當前哪些 TX 端點的中斷處於活動狀態,MUSB_INTRRX:指示當前哪些 TX 端點的中斷處於活動狀態。
請注意,musb_readb() 用於最多讀取 8 位暫存器,而 musb_readw() 允許我們最多讀取 16 位暫存器。 可以根據您的裝置暫存器的大小使用其他函式。 有關更多資訊,請參見 musb_io.h。
第 18 行的指令是 JZ4740 USB 裝置控制器的另一個怪癖,將在後面的 裝置怪癖 中討論。
但是,glue layer 仍然需要註冊 IRQ 處理程式。 請記住 init 函式的第 14 行上的指令
static int jz4740_musb_init(struct musb *musb)
{
musb->isr = jz4740_musb_interrupt;
return 0;
}
此指令設定指向 glue layer IRQ 處理程式函式的指標,以便控制器硬體在 IRQ 來自控制器硬體時回撥該處理程式。 現在已實現並註冊中斷處理程式。
裝置平臺數據¶
為了編寫 MUSB glue layer,您需要一些描述控制器硬體硬體功能的資料,稱為平臺數據。
平臺數據特定於您的硬體,儘管它可能涵蓋廣泛的裝置,並且通常位於 arch/ 目錄中的某個位置,具體取決於您的裝置架構。
例如,JZ4740 SoC 的平臺數據位於 arch/mips/jz4740/platform.c 中。 在 platform.c 檔案中,JZ4740 SoC 的每個裝置都透過一組結構進行描述。
這是 arch/mips/jz4740/platform.c 中涵蓋 USB 裝置控制器(UDC)的部分
/* USB Device Controller */ struct platform_device jz4740_udc_xceiv_device = { .name = "usb_phy_gen_xceiv", .id = 0, }; static struct resource jz4740_udc_resources[] = { [0] = { .start = JZ4740_UDC_BASE_ADDR, .end = JZ4740_UDC_BASE_ADDR + 0x10000 - 1, .flags = IORESOURCE_MEM, }, [1] = { .start = JZ4740_IRQ_UDC, .end = JZ4740_IRQ_UDC, .flags = IORESOURCE_IRQ, .name = "mc", }, }; struct platform_device jz4740_udc_device = { .name = "musb-jz4740", .id = -1, .dev = { .dma_mask = &jz4740_udc_device.dev.coherent_dma_mask, .coherent_dma_mask = DMA_BIT_MASK(32), }, .num_resources = ARRAY_SIZE(jz4740_udc_resources), .resource = jz4740_udc_resources, };
jz4740_udc_xceiv_device 平臺裝置結構(第 2 行)使用名稱和 ID 編號描述了 UDC 收發器。
在撰寫本文時,請注意 usb_phy_gen_xceiv 是用於所有內建引用 USB IP 或自主且不需要任何 PHY 程式設計的收發器的特定名稱。 您需要在核心配置中設定 CONFIG_NOP_USB_XCEIV=y 才能使用相應的收發器驅動程式。 如果我們想要一個特定的 ID 編號,則 id 欄位可以設定為 -1(相當於 PLATFORM_DEVID_NONE)、-2(相當於 PLATFORM_DEVID_AUTO)或以 0 開頭表示第一個此類裝置。
jz4740_udc_resources 資源結構(第 7 行)定義了 UDC 暫存器基地址。
第一個陣列(第 9 到 11 行)定義了 UDC 暫存器基記憶體地址:start 指向第一個暫存器記憶體地址,end 指向最後一個暫存器記憶體地址,flags 成員定義了我們正在處理的資源型別。 因此,IORESOURCE_MEM 用於定義暫存器記憶體地址。 第二個陣列(第 14 到 17 行)定義了 UDC IRQ 暫存器地址。 由於只有一個 IRQ 暫存器可用於 JZ4740 UDC,因此 start 和 end 指向相同的地址。 IORESOURCE_IRQ 標誌指示我們正在處理 IRQ 資源,並且名稱 mc 實際上在 MUSB 核心中進行了硬編碼,以便控制器驅動程式可以透過按名稱查詢來檢索此 IRQ 資源。
最後,jz4740_udc_device 平臺裝置結構(第 21 行)描述了 UDC 本身。
musb-jz4740 名稱(第 22 行)定義了用於此裝置的 MUSB 驅動程式; 請記住,這實際上是我們在 Linux MUSB 基礎 中的 jz4740_driver 平臺驅動程式結構中使用的名稱。 id 欄位(第 23 行)設定為 -1(相當於 PLATFORM_DEVID_NONE),因為我們不需要裝置的 ID:MUSB 控制器驅動程式已經設定為在 Linux MUSB 基礎 中分配自動 ID。 在 dev 欄位中,我們關心此處與 DMA 相關的資訊。 dma_mask 欄位(第 25 行)定義了將要使用的 DMA 掩碼的寬度,而 coherent_dma_mask(第 26 行)具有相同的目的,但對於 alloc_coherent DMA 對映:在這兩種情況下,我們都使用 32 位掩碼。 然後,resource 欄位(第 29 行)只是指向之前定義的資源結構的指標,而 num_resources 欄位(第 28 行)跟蹤資源結構中定義的陣列數量(在本例中,之前定義了兩個資源陣列)。
現在快速概述了 arch/ 級別的 UDC 平臺數據,讓我們回到 drivers/usb/musb/jz4740.c 中的 MUSB glue layer 特定平臺數據
static struct musb_hdrc_config jz4740_musb_config = { /* Silicon does not implement USB OTG. */ .multipoint = 0, /* Max EPs scanned, driver will decide which EP can be used. */ .num_eps = 4, /* RAMbits needed to configure EPs from table */ .ram_bits = 9, .fifo_cfg = jz4740_musb_fifo_cfg, .fifo_cfg_size = ARRAY_SIZE(jz4740_musb_fifo_cfg), }; static struct musb_hdrc_platform_data jz4740_musb_platform_data = { .mode = MUSB_PERIPHERAL, .config = &jz4740_musb_config, };
首先,glue layer 配置與控制器硬體規範相關的控制器驅動程式操作的某些方面。 這是透過 jz4740_musb_config musb_hdrc_config 結構完成的。
定義控制器硬體的 OTG 功能,multipoint 成員(第 3 行)設定為 0(相當於 false),因為 JZ4740 UDC 與 OTG 不相容。 然後 num_eps(第 5 行)定義控制器硬體的 USB 端點數量,包括端點 0:這裡我們有 3 個端點 + 端點 0。 接下來是 ram_bits(第 7 行),它是 MUSB 控制器硬體的 RAM 地址匯流排的寬度。 當控制器驅動程式無法透過讀取相關的控制器硬體暫存器來自動配置端點時,需要此資訊。 當我們在 裝置怪癖 中處理裝置怪癖時,將討論此問題。 最後兩個欄位(第 8 行和第 9 行)也是關於裝置怪癖的:fifo_cfg 指向 USB 端點配置表,fifo_cfg_size 跟蹤該配置表中條目數量的大小。 有關更多資訊,請參見後面的 裝置怪癖。
然後,此配置嵌入在 jz4740_musb_platform_data musb_hdrc_platform_data 結構(第 11 行)中:config 是指向配置結構本身的指標,mode 告訴控制器驅動程式控制器硬體是否只能用作 MUSB_HOST、僅用作 MUSB_PERIPHERAL 還是用作雙模 MUSB_OTG。
請記住,正如我們在 Linux MUSB 基礎 中的 probe 函式中所見,然後使用 jz4740_musb_platform_data 來傳遞平臺數據信息。
裝置怪癖¶
完成特定於您的裝置的平臺數據後,您可能還需要在 glue layer 中編寫一些程式碼來解決某些裝置特定的限制。 這些怪癖可能是由於某些硬體錯誤引起的,或者僅僅是不完全實現 USB On-the-Go 規範的結果。
JZ4740 UDC 表現出此類怪癖,即使在您正在使用的控制器硬體中可能找不到這些怪癖,我們也會在此處討論其中一些怪癖以供參考。
讓我們先回到 init 函式
static int jz4740_musb_init(struct musb *musb) { musb->xceiv = usb_get_phy(USB_PHY_TYPE_USB2); if (!musb->xceiv) { pr_err("HS UDC: no transceiver configured\n"); return -ENODEV; } /* Silicon does not implement ConfigData register. * Set dyn_fifo to avoid reading EP config from hardware. */ musb->dyn_fifo = true; musb->isr = jz4740_musb_interrupt; return 0; }
第 12 行上的指令可幫助 MUSB 控制器驅動程式解決控制器硬體缺少用於 USB 端點配置的暫存器的事實。
如果沒有這些暫存器,控制器驅動程式將無法從硬體讀取端點配置,因此我們使用第 12 行的指令繞過從矽讀取配置,而是依賴於描述端點配置的硬編碼表
static const struct musb_fifo_cfg jz4740_musb_fifo_cfg[] = {
{ .hw_ep_num = 1, .style = FIFO_TX, .maxpacket = 512, },
{ .hw_ep_num = 1, .style = FIFO_RX, .maxpacket = 512, },
{ .hw_ep_num = 2, .style = FIFO_TX, .maxpacket = 64, },
};
檢視上面的配置表,我們看到每個端點都由三個欄位描述:hw_ep_num 是端點編號,style 是其方向(對於控制器驅動程式在控制器硬體中傳送資料包,為 FIFO_TX,或者從硬體接收資料包,為 FIFO_RX),maxpacket 定義了可以透過該端點傳輸的每個資料包的最大大小。 從表中讀取,控制器驅動程式知道端點 1 可用於一次傳送和接收 512 位元組的 USB 資料包(這實際上是一個批次輸入/輸出端點),端點 2 可用於一次傳送 64 位元組的資料包(這實際上是一箇中斷端點)。
請注意,此處沒有關於端點 0 的資訊:該端點在每個矽設計中預設實現,並根據 USB 規範具有預定義的配置。 有關端點配置表的更多示例,請參見 musb_core.c。
現在讓我們回到中斷處理程式函式
static irqreturn_t jz4740_musb_interrupt(int irq, void *__hci) { unsigned long flags; irqreturn_t retval = IRQ_NONE; struct musb *musb = __hci; spin_lock_irqsave(&musb->lock, flags); musb->int_usb = musb_readb(musb->mregs, MUSB_INTRUSB); musb->int_tx = musb_readw(musb->mregs, MUSB_INTRTX); musb->int_rx = musb_readw(musb->mregs, MUSB_INTRRX); /* * The controller is gadget only, the state of the host mode IRQ bits is * undefined. Mask them to make sure that the musb driver core will * never see them set */ musb->int_usb &= MUSB_INTR_SUSPEND | MUSB_INTR_RESUME | MUSB_INTR_RESET | MUSB_INTR_SOF; if (musb->int_usb || musb->int_tx || musb->int_rx) retval = musb_interrupt(musb); spin_unlock_irqrestore(&musb->lock, flags); return retval; }
以上第 18 行上的指令是控制器驅動程式解決 MUSB_INTRUSB 暫存器中缺少用於 USB 主機模式操作的某些中斷位(因此處於未定義的硬體狀態)的事實的一種方法,因為此 MUSB 控制器硬體僅在外圍裝置模式下使用。 因此,glue layer 透過在從 MUSB_INTRUSB 讀取的值與實際在暫存器中實現的位之間執行邏輯 AND 運算來遮蔽這些丟失的位,以避免出現寄生中斷。
這些只是 JZ4740 USB 裝置控制器中發現的一些怪癖。 一些其他的怪癖已在 MUSB 核心中直接解決,因為這些修復程式足夠通用,可以為其他控制器硬體最終提供更好的問題處理。
結論¶
編寫 Linux MUSB glue layer 應該是一項更容易的任務,因為本文件試圖展示此練習的來龍去脈。
JZ4740 USB 裝置控制器非常簡單,我希望它的 glue layer 可以作為好奇者的一個很好的例子。 結合當前的 MUSB glue layer 使用,本文件應該提供足夠的指導來幫助您入門; 如果任何事情失控,linux-usb 郵件列表存檔是另一個有用的瀏覽資源。
鳴謝¶
非常感謝 Lars-Peter Clausen 和 Maarten ter Huurne 在我編寫 JZ4740 glue layer 時回答我的問題,並幫助我使程式碼處於良好狀態。
我也要感謝 Qi-Hardware 社群的廣泛熱情指導和支援。
資源¶
USB 主頁:https://www.usb.org
linux-usb 郵件列表存檔:https://marc.info/?l=linux-usb
USB On-the-Go 基礎:https://www.maximintegrated.com/app-notes/index.mvp/id/1822
Texas Instruments USB 配置 Wiki 頁面:https://web.archive.org/web/20201215135015/http://processors.wiki.ti.com/index.php/Usbgeneralpage