Linux 核心提供完善的 GPIO 子系統,讓開發者能有效控制硬體。本文從驅動程式角度出發,首先剖析核心中gpio_chip結構體的組成,包含各個欄位的意義與使用方法,並以程式碼片段展示如何註冊 GPIO 晶片到系統中。接著,文章說明 GPIO 中斷處理的實作方式,並列舉 CHAINED、GENERIC CHAINED 和 NESTED THREADED 三種常見的 GPIO IRQ 型別,讓讀者瞭解不同中斷處理機制的差異。除了底層的gpio_chip操作,文章也介紹了根據描述符的 GPIO 介面,包含gpiod_get、gpiod_direction_input、gpiod_set_value等函式,提供更簡潔易用的 GPIO 操作方式。此外,文章也說明如何在裝置樹中描述 GPIO 設定,以及如何使用gpiod_to_irq函式將 GPIO 對應到 IRQ 編號。最後,文章也涵蓋了核心空間與使用者空間資料交換、MMIO 裝置存取等相關議題,並以 RGB LED 平台裝置模組為例,示範如何整合這些技術,讓讀者能更全面地理解 Linux 平台下 GPIO 的應用與開發。
平台驅動程式中的GPIO控制器與IRQ處理
在嵌入式系統中,GPIO(General Purpose Input/Output)控制器扮演著重要的角色,用於控制和處理外部裝置的輸入輸出訊號。與此同時,中斷處理(IRQ)也是系統設計中的關鍵部分,能夠讓系統對外部事件做出及時的回應。本章節將探討GPIO控制器及其IRQ處理的實作細節。
GPIO控制器的實作
GPIO控制器的實作主要透過gpio_chip結構體來完成,該結構體定義在核心原始碼的include/linux/gpio/driver.h中。以下是一個典型的gpio_chip結構體例項:
static struct gpio_chip bcm2835_gpio_chip = {
.label = "bcm2835_gpio",
.owner = THIS_MODULE,
.request = gpiochip_generic_request,
.free = gpiochip_generic_free,
.direction_input = bcm2835_gpio_direction_input,
.direction_output = bcm2835_gpio_direction_output,
.get_direction = bcm2835_gpio_get_direction,
.get = bcm2835_gpio_get,
.set = bcm2835_gpio_set,
.set_config = gpiochip_generic_config,
.base = -1,
.ngpio = BCM2835_NUM_GPIOS,
.can_sleep = false,
};
內容解密:
.label屬性定義了GPIO控制器的名稱。.request和.free方法用於請求和釋放GPIO管腳。.direction_input和.direction_output方法分別用於設定GPIO管腳的方向為輸入或輸出。.get_direction方法用於取得GPIO管腳的當前方向。.get和.set方法分別用於讀取和設定GPIO管腳的狀態。.base屬性表示GPIO控制器的起始編號,-1表示動態分配。.ngpio屬性表示GPIO控制器管理的管腳數量。.can_sleep屬性表示GPIO控制器是否可以在睡眠模式下操作。
GPIO控制器的註冊
在驅動程式初始化過程中,需要將gpio_chip結構體註冊到GPIO子系統中。以下是一個範例:
static int bcm2835_pinctrl_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct bcm2835_pinctrl *pc;
// ...
pc->gpio_chip = bcm2835_gpio_chip;
pc->gpio_chip.parent = dev;
pc->gpio_chip.of_node = np;
// ...
err = gpiochip_add_data(&pc->gpio_chip, pc);
if (err) {
dev_err(dev, "could not add GPIO chip\n");
return err;
}
// ...
}
內容解密:
bcm2835_pinctrl_probe函式是驅動程式的初始化函式,在裝置匹配時被呼叫。- 將
bcm2835_gpio_chip結構體指定給pc->gpio_chip,並設定其父裝置和裝置樹節點。 - 呼叫
gpiochip_add_data函式將GPIO控制器註冊到GPIO子系統中。
GPIO IRQ處理
GPIO控制器通常也提供中斷功能,用於處理外部事件。以下是一個典型的GPIO IRQ處理範例:
static void bcm2835_gpio_irq_handler(struct irq_desc *desc)
{
struct gpio_chip *chip = irq_desc_get_handler_data(desc);
struct bcm2835_pinctrl *pc = gpiochip_get_data(chip);
struct irq_chip *host_chip = irq_desc_get_chip(desc);
int irq = irq_desc_get_irq(desc);
int group, i;
// ...
chained_irq_enter(host_chip, desc);
// 處理中斷事件
chained_irq_exit(host_chip, desc);
}
內容解密:
bcm2835_gpio_irq_handler函式是GPIO IRQ的中斷處理函式。- 使用
chained_irq_enter和chained_irq_exit函式來進入和離開中斷處理流程。 - 在中斷處理流程中,會根據中斷源的不同,呼叫相應的處理函式。
GPIO IRQ型別
GPIO IRQ可以分為三種型別:CHAINED GPIO irqchips、GENERIC CHAINED GPIO irqchips和NESTED THREADED GPIO irqchips。
- CHAINED GPIO irqchips:嵌入式系統中常見的GPIO IRQ型別,使用鏈式中斷處理流程。
- GENERIC CHAINED GPIO irqchips:與CHAINED GPIO irqchips類別似,但使用通用的中斷處理函式。
- NESTED THREADED GPIO irqchips:用於需要慢速匯流排通訊的GPIO擴充套件器,使用巢狀執行緒來處理中斷。
本章節介紹了GPIO控制器及其IRQ處理的實作細節,包括gpio_chip結構體的定義、GPIO控制器的註冊和GPIO IRQ處理的範例。接下來的章節將繼續探討相關主題。
GPIO 描述符消費者介面
本章節描述根據描述符的 GPIO 介面。若需瞭解已棄用的根據整數的 GPIO 介面,請參考 Documentation/gpio/ 目錄下的 gpio-legacy.txt 檔案。
所有使用描述符式 GPIO 介面的函式均以 gpiod_ 為字首。gpio_ 字首則用於舊式介面。核心中的其他函式不應使用這些字首。強烈建議不要使用舊式函式,新程式碼應專門使用 <linux/gpio/consumer.h> 和描述符。
取得和釋放 GPIOs
GPIO 描述符介面透過 devm_gpiod_get() 函式傳回的 gpio_desc 結構來識別每個 GPIO。此函式接受以下引數:GPIO 消費者裝置(dev)、GPIO 消費者中的功能(con_id)以及不同的可選 GPIO 初始化標誌(flags):
struct gpio_desc *devm_gpiod_get(struct device *dev, const char *con_id,
enum gpiod_flags flags)
devm_gpiod_get_index() 是 devm_gpiod_get() 的變體,允許存取在特定 GPIO 功能中定義的多個 GPIOs。devm_gpiod_get_index() 函式透過使用 of_find_gpio() 函式,從裝置樹中查詢 GPIO 功能(con_id)及其索引(idx)來傳回 GPIO 描述符:
struct gpio_desc *devm_gpiod_get_index(struct device *dev,
const char *con_id,
unsigned int idx,
enum gpiod_flags flags);
內容解密:
devm_gpiod_get()和devm_gpiod_get_index()用於取得 GPIO 描述符。flags引數可指定 GPIO 的方向和初始值。devm_gpiod_get_index()可用於存取多個 GPIOs。
flags 引數可以指定 GPIO 的方向和初始值。以下是一些重要的值:
GPIOD_ASIS或0表示不初始化 GPIO,方向必須稍後使用專用函式設定。GPIOD_IN表示將 GPIO 初始化為輸入。GPIOD_OUT_LOW表示將 GPIO 初始化為輸出,初始值為 0。GPIOD_OUT_HIGH表示將 GPIO 初始化為輸出,初始值為 1。
可以使用 devm_gpiod_put() 函式釋放 GPIO 描述符。
使用 GPIOs
每當編寫需要控制 GPIO 的 Linux 驅動程式時,必須指定 GPIO 的方向。這可以透過 devm_gpiod_get*() 函式的 flags 引數或稍後呼叫 gpiod_direction_*() 函式來實作(如果將 flags 引數設為 GPIOD_ASIS):
int gpiod_direction_input(struct gpio_desc *desc);
int gpiod_direction_output(struct gpio_desc *desc, int value);
內容解密:
- 使用 GPIOs 前必須設定其方向。
- 可透過
devm_gpiod_get*()的flags或呼叫gpiod_direction_*()設定方向。 - 成功傳回零,否則傳回負的 errno。
對於輸出 GPIOs,提供的初始值有助於避免在系統啟動期間的訊號抖動。
大多數 GPIO 控制器的存取方式是使用記憶體讀寫指令,這些指令不需要睡眠,可以在硬 IRQ 處理程式中安全地執行。
可使用以下函式在原子上下文中存取 GPIOs:
int gpiod_get_value(const struct gpio_desc *desc);
void gpiod_set_value(struct gpio_desc *desc, int value);
內容解密:
gpiod_get_value()用於取得 GPIO 的值。gpiod_set_value()用於設定 GPIO 的值。- 這兩個函式考慮了 active-low 屬性。
由於驅動程式不應關心物理線路級別,所有 gpiod_set_value_xxx() 函式都根據邏輯值進行操作。這意味著它們會檢查 GPIO 是否組態為 active-low,如果是,則在驅動物理線路級別之前對傳遞的值進行操作。
將 GPIOs 對映到 IRQs
可以透過 GPIO 發生中斷請求。使用以下函式可取得與給定 GPIO 對應的 Linux IRQ 編號:
int gpiod_to_irq(const struct gpio_desc *desc)
內容解密:
- 使用
gpiod_to_irq()可取得與 GPIO 對應的 Linux IRQ 編號。 - 此函式不允許睡眠。
- 傳回值可傳遞給
request_irq()或free_irq()。
裝置樹中的 GPIOs
GPIOs 在裝置樹中對映到裝置和功能。具體方式取決於提供 GPIOs 的 GPIO 控制器(請參閱您的控制器的裝置樹繫結)。
GPIOs 的對映在消費者裝置節點中使用名為 <function>-gpios 的屬性定義,其中 <function> 是由 Linux 驅動程式透過呼叫 gpiod_get() 函式請求的。例如:
foo_device {
compatible = "acme,foo";
...
led-gpios = <&gpioa 15 GPIO_ACTIVE_HIGH>, /* red */
<&gpioa 16 GPIO_ACTIVE_HIGH>, /* green */
<&gpioa 17 GPIO_ACTIVE_HIGH>; /* blue */
power-gpios = <&gpiob 1 GPIO_ACTIVE_LOW>;
};
內容解密:
- 在裝置樹中使用
<function>-gpios屬性對映 GPIOs。 - 例如,
led-gpios和power-gpios分別對映到不同的 GPIOs。
在上述例子中,&gpioa 和 &gpiob 是指向特定 gpio-controller 節點的 phandles。數字 15、16、17 和 1 分別是每個 gpio-controller 的線路偏移量,而 GPIO_ACTIVE_HIGH 是用於 GPIO 的標誌之一。
struct gpio_desc *red, *green, *blue, *power;
red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH);
blue = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH);
power = gpiod_get(dev, "power", GPIOD_OUT_HIGH);
內容解密:
- 使用
gpiod_get_index()和gpiod_get()取得與裝置樹中定義的 GPIOs 對應的描述符。 - 第二個引數
con_id必須與裝置樹中使用的 gpios 字尾的功能字首相同。
在核心和使用者空間之間交換資料
Linux 作業系統透過將整個記憶體分成兩個邏輯部分(使用者空間和核心空間)來防止使用者行程存取其他行程,以及防止行程直接存取或操縱核心資料結構和服務。
系統呼叫是應用程式和 Linux 核心之間的基本介面。系統呼叫在核心空間中實作,其各自的處理程式透過特定的機制被呼叫。
平台驅動程式
在 Linux 核心中,驅動程式是應用程式與硬體之間的介面。當一個行程執行系統呼叫時,核心會代表呼叫的行程在行程上下文中執行。當核心回應中斷時,中斷處理程式會在中斷上下文中非同步執行。
存取使用者空間資料
由於安全性問題和架構差異,直接存取使用者空間指標可能會導致錯誤行為、核心異常或安全漏洞。正確存取使用者空間資料的方法是使用以下巨集或函式:
單一值存取
get_user(type val, type *address);:將使用者空間指標address所指向的值賦給核心變數val。put_user(type val, type *address);:將核心變數val的內容賦給使用者空間指標address所指向的位置。
緩衝區存取
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);:將n位元組從核心空間地址from複製到使用者空間地址to。unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);:將n位元組從使用者空間地址from複製到核心空間地址to。
MMIO(記憶體對映 I/O)裝置存取
週邊裝置透過寫入和讀取其暫存器來控制。通常,一個裝置有多個暫存器,可以在記憶體地址空間(MMIO)或 I/O 地址空間(PIO)中連續的地址上存取。以下是 MMIO 和 PIO 之間的主要區別:
MMIO
- 使用相同的位址匯流排來定址記憶體和 I/O 裝置。
- 使用常規指令存取 I/O 裝置。
- 是 Linux 支援的不同架構中最廣泛使用的 I/O 方法。
PIO
- 記憶體和 I/O 裝置有不同的位址空間。
- 使用特殊類別的 CPU 指令來存取 I/O 裝置。
BCM2837 SoC 使用 MMIO 存取,因此本文將更詳細地描述此方法。
對映 I/O 記憶體
Linux 驅動程式不能直接存取實體 I/O 位址,需要 MMU 對映。為了存取 I/O 記憶體,驅動程式需要一個處理器可以處理的虛擬位址,因為預設情況下,I/O 記憶體未對映到虛擬記憶體中。
可以使用以下兩種函式獲得此 I/O 虛擬位址:
使用
ioremap()和iounmap()函式進行對映和取消對映。ioremap()函式接受實體位址和區域大小,並傳回一個可以被解除參考的虛擬記憶體指標(如果對映不可能,則傳回 NULL)。void __iomem *ioremap(phys_addr_t offset, unsigned long size); void iounmap(void *address);使用由裝置管理的
devm_ioremap()和devm_iounmap()函式進行對映和取消對映,這些函式簡化了驅動程式碼和錯誤處理。使用ioremap()在裝置驅動程式中現在已被棄用。void __iomem *devm_ioremap(struct device *dev, resource_size_t offset, unsigned long size); void devm_iounmap(struct device *dev, void __iomem *addr);
每個裝置(基本的裝置結構)透過其包含的 devres_head 結構管理一個資源的鏈結串列。呼叫一個受管理的資源分配器涉及將資源新增到串列中。當 probe() 函式以錯誤狀態離開或在 remove() 函式傳回後,資源將以相反的順序釋放。在 probe() 函式中使用受管理的函式可以消除錯誤處理時所需的資源釋放,用簡單的傳回陳述式替換了 goto 陳述式和其他資源釋放。它還消除了在 remove() 函式中的資源釋放。
程式碼範例
// 使用 devm_ioremap 進行 I/O 記憶體對映
void __iomem *gpio_base = devm_ioremap(&pdev->dev, GPIO_BASE_ADDR, SZ_4K);
if (IS_ERR(gpio_base)) {
dev_err(&pdev->dev, "Failed to remap GPIO registers\n");
return PTR_ERR(gpio_base);
}
// 使用 ioread32 和 iowrite32 進行暫存器存取
unsigned int gpio_level = ioread32(gpio_base + GPLEV0_OFFSET);
iowrite32(gpio_level | (1 << GPIO_PIN), gpio_base + GPSET0_OFFSET);
內容解密:
devm_ioremap函式:用於將實體 I/O 位址對映到虛擬記憶體,以便於核心存取週邊裝置的暫存器。這裡將 GPIO 的基址對映到虛擬記憶體。ioread32和iowrite32函式:用於讀寫虛擬位址上的暫存器值。這些函式確保了正確的位元組順序和快取一致性。GPLEV0_OFFSET和GPSET0_OFFSET:分別代表 GPIO 等級暫存器和 GPIO 設定暫存器的偏移量,用於控制 GPIO 腳位的輸入輸出狀態。
LAB 5.2:“RGB LED 平台裝置”模組
在此實驗中,您將應用本章節至今所描述的大多數概念。您將控制幾個 LED,將 SoC 的幾個周邊暫存器位址從實體位址對映到虛擬位址。您將使用雜項框架為每個 LED 建立一個字元裝置,並控制 LED 的狀態。
程式碼範例
// 初始化函式範例
static int __init rgb_led_init(void) {
// 向核心註冊雜項裝置
if (misc_register(&rgb_led_misc_device)) {
printk(KERN_ERR "Failed to register misc device\n");
return -EIO;
}
return 0;
}
// 清理函式範例
static void __exit rgb_led_exit(void) {
// 從核心登出雜項裝置
misc_deregister(&rgb_led_misc_device);
}
內容解密:
misc_register和misc_deregister函式:用於註冊和登出雜項裝置,使得應用程式可以透過檔案系統介面(如/dev/rgb_led)與驅動程式互動。- 初始化和清理函式:在模組載入時,初始化函式被呼叫以註冊裝置;在模組解除安裝時,清理函式被呼叫以登出裝置,釋放資源。