Linux 核心提供了平台驅動程式框架,方便開發者撰寫與硬體平台無關的驅動程式。本文以 RGB LED 驅動程式為例,說明如何使用此框架,並搭配裝置樹描述硬體資源。驅動程式利用雜項裝置介面簡化開發流程,並透過 container_of() 巨集實作資料結構的存取。核心 API platform_get_resource() 則用於取得硬體資源,例如 GPIO 的記憶體位址。驅動程式實作了 led_probe()、led_write()、led_read() 和 led_remove() 等函式,分別負責裝置初始化、資料讀寫和裝置移除。透過裝置樹的整合,驅動程式能更彈性地適應不同的硬體平台。
平台驅動程式章節 5:實作 LED 驅動程式
驅動程式開發步驟
初始化驅動程式結構:定義
struct led_dev結構來儲存每個 LED 裝置的相關資訊,包括miscdevice、led_mask、led_name和led_value。定義 GPIO 相關巨集:定義 GPIO 的基底位址、LED 對應的 GPIO 腳位、以及控制 LED 的相關巨集,例如
GPIO_27_INDEX、GPIO_SET_FUNCTION_LEDS等。實作
led_probe()函式:在led_probe()函式中,進行裝置的初始化,包括對映 GPIO 控制器的位址、設定 GPIO 功能為輸出、以及註冊miscdevice裝置。註冊
miscdevice裝置:將file_operations結構指派給每個建立的miscdevice,並使用misc_register()函式註冊裝置。實作
led_write()函式:當使用者對裝置檔案進行寫入操作時,led_write()函式會被呼叫。它使用container_of()巨集來取得對應的led_dev結構,並根據使用者傳入的值(“on” 或 “off”)來控制 LED 的開關。實作
led_read()函式:當使用者對裝置檔案進行讀取操作時,led_read()函式會被呼叫。它同樣使用container_of()巨集來取得對應的led_dev結構,並將led_value的值回傳給使用者。宣告支援的裝置列表:定義
of_device_id結構陣列來宣告驅動程式支援的裝置,並使用MODULE_DEVICE_TABLE()巨集將其註冊到核心。註冊平台驅動程式:定義
platform_driver結構並使用platform_driver_register()函式將其註冊到平台匯流排。
程式碼解析
ledRGB_rpi3_platform.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/platform_device.h>
#include <linux/types.h>
#include <linux/io.h>
#include <linux/of.h>
#include <linux/uaccess.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>
// 定義 led_dev 結構
struct led_dev {
struct miscdevice led_misc_device;
u32 led_mask;
const char *led_name;
char led_value[8];
};
// 定義 GPIO 相關巨集
#define BCM2710_PERI_BASE 0x3F000000
#define GPIO_BASE (BCM2710_PERI_BASE + 0x200000)
#define GPIO_27 27
#define GPIO_22 22
#define GPIO_26 26
// ...
static ssize_t led_write(struct file *file, const char __user *buff, size_t count, loff_t *ppos)
{
const char *led_on = "on";
const char *led_off = "off";
struct led_dev *led_device;
pr_info("led_write() is called.\n");
led_device = container_of(file->private_data, struct led_dev, led_misc_device);
if(copy_from_user(led_device->led_value, buff, count)) {
pr_info("Bad copied value\n");
return -EFAULT;
}
led_device->led_value[count-1] = '\0';
pr_info("This message received from User Space: %s", led_device->led_value);
if(!strcmp(led_device->led_value, led_on)) {
iowrite32(led_device->led_mask, GPSET0_V);
}
else if (!strcmp(led_device->led_value, led_off)) {
iowrite32(led_device->led_mask, GPCLR0_V);
}
else {
pr_info("Bad value\n");
return -EINVAL;
}
return count;
}
#### 內容解密:
1. **`led_write()` 函式的作用**:當使用者對 LED 裝置檔案進行寫入操作時,此函式會被呼叫,用於控制 LED 的開關。
2. **`container_of()` 巨集的使用**:透過 `container_of()` 巨集,從 `file->private_data` 中取得對應的 `led_dev` 結構例項。
3. **`copy_from_user()` 函式的應用**:將使用者空間傳入的資料複製到核心空間的 `led_value` 陣列中。
4. **LED 控制邏輯**:根據 `led_value` 的值("on" 或 "off"),使用 `iowrite32()` 函式寫入對應的 GPIO 暫存器來控制 LED 的開關。
5. **錯誤處理**:若資料複製失敗或指令無效,則回傳對應的錯誤碼。
### 重點技術與設計考量
* 使用 `miscdevice` 裝置介面簡化字元裝置驅動程式的開發。
* 利用 `container_of()` 巨集實作從子結構到父結構的指標轉換。
* 透過 `of_device_id` 表來匹配裝置樹中的相容裝置,實作驅動程式與硬體裝置的動態結合。
* 在驅動程式中使用 `iowrite32()` 等函式直接操作硬體暫存器,實作對 LED 的精確控制。
## 平台驅動程式章節 5:RGB LED 驅動實作
### 前言
本文探討根據 Linux 平台驅動程式的 RGB LED 驅動實作,涵蓋驅動程式的核心功能、裝置樹的整合、以及使用者空間的互動操作。驅動程式利用 Linux 的雜項裝置框架(Miscellaneous Device Framework)實作對 RGB LED 的控制。
### 驅動程式核心功能實作
驅動程式的核心功能包括裝置的初始化、讀寫操作以及裝置的移除。以下將詳細分析這些功能的實作。
#### 裝置初始化
在 `led_probe` 函式中,驅動程式首先分配記憶體給 `struct led_dev` 結構,並從裝置樹中讀取 `label` 屬性以設定裝置名稱。根據裝置名稱,設定對應的 GPIO 引腳遮罩(`led_mask`)。
```c
static int led_probe(struct platform_device *pdev)
{
struct led_dev *led_device;
int ret_val;
char led_val[8] = "off\n";
led_device = devm_kzalloc(&pdev->dev, sizeof(struct led_dev), GFP_KERNEL);
of_property_read_string(pdev->dev.of_node, "label", &led_device->led_name);
// 設定裝置名稱和操作函式
led_device->led_misc_device.name = led_device->led_name;
led_device->led_misc_device.fops = &led_fops;
// 根據裝置名稱設定 GPIO 引腳遮罩
if (strcmp(led_device->led_name, "ledred") == 0) {
led_device->led_mask = GPIO_27_INDEX;
} else if (strcmp(led_device->led_name, "ledgreen") == 0) {
led_device->led_mask = GPIO_22_INDEX;
} else if (strcmp(led_device->led_name, "ledblue") == 0) {
led_device->led_mask = GPIO_26_INDEX;
} else {
pr_info("Bad device tree value\n");
return -EINVAL;
}
// 初始化 LED 狀態為關閉
memcpy(led_device->led_value, led_val, sizeof(led_val));
ret_val = misc_register(&led_device->led_misc_device);
if (ret_val) return ret_val;
platform_set_drvdata(pdev, led_device);
return 0;
}
內容解密:
devm_kzalloc:分配記憶體給struct led_dev結構,使用devm_系列函式可自動管理記憶體,無需手動釋放。of_property_read_string:從裝置樹中讀取label屬性,以設定裝置名稱。misc_register:註冊雜項裝置,使其在/dev目錄下可見。
使用者空間互動
驅動程式透過實作 file_operations 結構體來支援使用者空間的讀寫操作。
寫入操作
寫入操作用於控制 LED 的開關狀態。
static ssize_t led_write(struct file *file, const char __user *buff, size_t count, loff_t *ppos)
{
struct led_dev *led_device;
char value[8];
// 從使用者空間複製資料到核心空間
if (copy_from_user(value, buff, count)) {
pr_info("Failed to copy from user space\n");
return -EFAULT;
}
led_device = container_of(file->private_data, struct led_dev, led_misc_device);
// 控制 LED 開關
if (!strncmp(value, "on", count)) {
iowrite32(led_device->led_mask, GPSET0_V);
} else if (!strncmp(value, "off", count)) {
iowrite32(led_device->led_mask, GPCLR0_V);
} else {
pr_info("Bad value\n");
return -EINVAL;
}
return count;
}
內容解密:
copy_from_user:將使用者空間的資料複製到核心空間。iowrite32:寫入暫存器以控制 GPIO 引腳,從而控制 LED 的開關。
裝置移除
當模組被移除時,led_remove 函式會被呼叫,以解除註冊雜項裝置。
static int led_remove(struct platform_device *pdev)
{
struct led_dev *led_device = platform_get_drvdata(pdev);
misc_deregister(&led_device->led_misc_device);
return 0;
}
內容解密:
platform_get_drvdata:取得與裝置相關的資料結構指標。misc_deregister:解除註冊雜項裝置,使其在/dev目錄下不可見。
平台驅動程式資源
每個由特定驅動程式管理的裝置通常使用不同的硬體資源(例如,I/O 暫存器的記憶體位址、DMA 通道和 IRQ 線)。平台驅動程式透過核心 API 存取這些資源。這些核心函式自動從 struct platform_device 資源陣列(在核心原始碼樹的 include/linux/platform_device.h 中宣告)讀取標準平台裝置引數。這個資源陣列已經被填入了裝置樹(DT)裝置節點的資源屬性(例如,reg、clocks 和 interrupts)。在核心原始碼樹的 Documentation/devicetree/bindings/ 中的 resource-names.txt 檔案中,您可以看到可以按索引存取的不同資源屬性。
資源結構
struct platform_device 的宣告如下:
struct platform_device {
const char *name;
u32 id;
struct device dev;
u32 num_resources;
struct resource *resource;
};
struct resource 的宣告如下:
struct resource {
resource_size_t start; /* unsigned int (resource_size_t) */
resource_size_t end;
const char *name;
unsigned long flags;
unsigned long desc;
struct resource *parent, *sibling, *child;
};
每個欄位的意義如下:
start/end:代表資源的開始/結束位置。對於 I/O 或記憶體區域,它代表它們的開始/結束位置。對於 IRQ 線、匯流排或 DMA 通道,start和end必須具有相同的值。flags:是一個遮罩,用於描述資源的型別,例如IORESOURCE_MEM。name:用於識別或描述資源。
取得資源的輔助函式
有多個輔助函式可以從資源陣列中取得資料:
platform_get_resource():在核心原始碼樹的drivers/base/platform.c中定義,它為裝置取得一個資源,並傳回一個填充了 DT 值的資源結構,以便稍後在驅動程式碼中使用,例如,如果它們是實體記憶體位址,則使用devm_ioremap()對其進行對映(以IORESOURCE_MEM型別指定)。
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num) { int i; for (i = 0; i < dev->num_resources; i++) { struct resource *r = &dev->resource[i]; if (type == resource_type(r) && num– == 0) return r; } return NULL; }
第一個引數告訴函式您感興趣的裝置,以便它可以提取所需的資訊。第二個引數取決於您正在處理的資源型別。如果是記憶體(或任何可以對映為記憶體的東西),則為 `IORESOURCE_MEM`。您可以在核心原始碼樹的 `include/linux/ioport.h` 中看到所有巨集。最後一個引數決定了所需的那種型別的資源,其中零表示第一個。因此,例如,驅動程式可以使用以下程式碼行找到並對映其第二個 MMIO 區域:
```c
struct resource *r;
r = platform_get_resource(pdev, IORESOURCE_MEM, 1);
/* ioremap your memory region */
g_ioremap_addr = devm_ioremap(dev, r->start, resource_size(r));
傳回值 `r` 是指向資源變數的指標。`resource_size()` 函式將從資源結構傳回將被對映的記憶體大小:
```c
static inline resource_size_t resource_size(const struct resource *res) { return res->end - res->start + 1; }
2. **`platform_get_irq()`**:此函式從 `platform_device` 結構中提取資源結構,檢索在裝置樹節點中宣告的中斷屬性之一。這個函式將在第 7 章中更詳細地解釋。
#### platform\_get\_resource() 使用範例
```c
struct resource *r;
r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!r) {
dev_err(dev, "Failed to get memory resource\n");
return -ENODEV;
}
g_ioremap_addr = devm_ioremap(dev, r->start, resource_size(r));
if (!g_ioremap_addr) {
dev_err(dev, "Failed to ioremap memory region\n");
return -ENOMEM;
}
內容解密:
- 上述範例程式碼展示瞭如何使用
platform_get_resource()函式取得裝置的第一個記憶體資源。 - 首先,呼叫
platform_get_resource()並傳遞裝置指標、IORESOURCE_MEM和索引值0,以取得第一個記憶體資源。 - 如果成功取得資源,則使用
devm_ioremap()將記憶體區域對映到虛擬位址空間。 - 如果任何步驟失敗,則輸出錯誤訊息並傳回相應的錯誤碼。