Linux 系統中,RGB LED 驅動程式開發涉及 GPIO 控制、裝置樹解析及 LED 子系統整合等關鍵技術。透過精確設定 GPIO 輸出、解析裝置樹資源以及操作 LED 子系統介面,驅動程式能有效控制 LED 亮度與狀態。程式碼中使用巨集定義簡化 GPIO 操作,並運用 platform_get_resource 和 devm_ioremap 等函式管理硬體資源。此外,led_control 函式實作亮度調節功能,藉由 iowrite32 控制 GPIO 暫存器。裝置樹設定與子節點解析則確保驅動程式正確辨識不同 LED,並根據 label 屬性設定控制遮罩。
平台驅動程式章節 5:RGB LED 驅動實作詳解
本章節探討 Linux 平台驅動程式的開發,特別是以 RGB LED 為例,展示如何利用 LED 子系統進行驅動程式的設計與實作。
驅動程式核心結構與 GPIO 控制
驅動程式的核心在於對 GPIO 的控制,特別是如何設定 GPIO 的輸出功能以及如何切換 LED 的開關狀態。程式碼中定義了一系列巨集來簡化 GPIO 的操作:
#define GPSET0_offset 0x1C
#define GPCLR0_offset 0x28
#define GPIO_27_INDEX (1 << (GPIO_27 % 32))
#define GPIO_22_INDEX (1 << (GPIO_22 % 32))
#define GPIO_26_INDEX (1 << (GPIO_26 % 32))
內容解密:
GPSET0_offset和GPCLR0_offset分別代表設定和清除 GPIO 輸出暫存器的偏移地址。GPIO_27_INDEX、GPIO_22_INDEX和GPIO_26_INDEX用於表示對應 GPIO 引腳的索引值,用於控制紅、綠、藍三色 LED。
驅動程式初始化與裝置樹解析
在 ledclass_probe 函式中,驅動程式進行了裝置樹的解析和必要的初始化工作:
static int ledclass_probe(struct platform_device *pdev)
{
void __iomem *g_ioremap_addr;
struct device_node *child;
// ...
/* 取得第一個記憶體資源 */
r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// ...
/* 對記憶體區域進行對映 */
g_ioremap_addr = devm_ioremap(dev, r->start, resource_size(r));
// ...
}
內容解密:
platform_get_resource用於從裝置樹中取得指定的資源,在此例中為記憶體資源。devm_ioremap將取得的實體位址對映到虛擬位址空間,以便驅動程式能夠存取硬體暫存器。
LED 控制函式與亮度調節
驅動程式實作了 led_control 函式來控制 LED 的亮度:
static void led_control(struct led_classdev *led_cdev, enum led_brightness b)
{
struct led_dev *led = container_of(led_cdev, struct led_dev, cdev);
iowrite32(GPIO_SET_ALL_LEDS, led->base + GPCLR0_offset);
if (b != LED_OFF)
iowrite32(led->led_mask, led->base + GPSET0_offset);
else
iowrite32(led->led_mask, led->base + GPCLR0_offset);
}
內容解密:
- 該函式根據傳入的亮度值 (
enum led_brightness) 控制對應的 LED 開關。 - 當亮度值非零時,LED 被開啟;否則,LED 被關閉。
- 使用
iowrite32對 GPIO 的 SET 和 CLR 暫存器進行寫入,以控制 LED 狀態。
裝置樹設定與子節點解析
驅動程式解析裝置樹的子節點以設定不同的 LED:
for_each_child_of_node(dev->of_node, child) {
// ...
of_property_read_string(child, "label", &cdev->name);
if (strcmp(cdev->name, "red") == 0) {
led_device->led_mask = GPIO_27_INDEX;
}
// ...
}
內容解密:
- 使用
for_each_child_of_node遍歷裝置樹的子節點。 - 根據子節點中的
label屬性設定對應的 LED 控制遮罩。
模組載入與測試
載入模組後,可以透過 sysfs 控制 LED 的狀態:
root@raspberrypi:/home/pi# insmod ledRGB_rpi3_class_platform.ko
root@raspberrypi:/home/pi# echo 1 > /sys/class/leds/red/brightness
root@raspberrypi:/home/pi# echo timer > /sys/class/leds/green/trigger
內容解密:
- 載入模組後,系統會自動在
/sys/class/leds/下建立對應的 LED 控制介面。 - 可以透過寫入
brightness檔案控制 LED 的開關。 - 將
trigger設定為timer可以讓 LED 以特定頻率閃爍。
本章節透過實作 RGB LED 的驅動程式,展示了 Linux 平台驅動程式開發的基本流程和關鍵技術點,包括裝置樹的解析、GPIO 控制、以及與 LED 子系統的整合。透過這些技術細節的解析,可以更深入地理解 Linux 驅動程式開發的核心概念和實務技巧。
平台驅動程式章節5
移除模組
在樹莓派上執行以下指令移除模組:
root@raspberrypi:/home/pi# rmmod ledRGB_rpi3_class_platform.ko
led driver enter
leds_remove enter
leds_remove exit
led driver exit
使用者空間中的平台裝置驅動程式
在Linux中,裝置驅動程式傳統上執行在核心空間,但也可以執行在使用者空間。當沒有兩個應用程式需要獨佔存取權時,不一定需要撰寫裝置驅動程式。最有用的例子是記憶體對映裝置,但也可以對I/O空間中的裝置執行此操作。
Linux使用者空間為裝置驅動程式提供了多個優勢,包括更健全和靈活的處理程式管理、標準化的系統呼叫介面、更簡單的資源管理和大量的函式庫,用於XML或其他組態方法以及正規表示式解析等。每次呼叫核心(系統呼叫)都必須從使用者模式切換到管理模式,然後再傳回。這需要時間,如果呼叫頻繁,就會成為效能瓶頸。
使用者空間驅動程式的優缺點
使用者空間驅動程式的優點:
- 易於除錯,因為應用程式開發的除錯工具更為豐富。
- 可使用浮點數等使用者空間服務。
- 裝置存取非常高效,因為不需要系統呼叫。
- Linux上的應用程式API非常穩定。
- 驅動程式可以用任何語言撰寫,而不僅限於C語言。
使用者空間驅動程式的缺點:
- 無法存取核心框架和服務。
- 無法在使用者空間處理中斷,必須由核心驅動程式處理。
- 沒有預先定義的API允許應用程式存取裝置驅動程式。
核心空間驅動程式的優點:
- 在最高許可權模式下執行於核心空間,允許存取中斷和硬體資源。
- 有許多核心服務可用於設計複雜裝置的核心空間驅動程式。
- 核心提供API給使用者空間,允許多個應用程式同時存取核心空間驅動程式。
核心空間驅動程式的缺點:
- 存取驅動程式時有系統呼叫的額外負擔。
- 除錯困難。
- 核心API頻繁變更。為一個核心版本建置的核心驅動程式可能無法在另一個版本上建置。
使用者定義I/O:UIO
Linux核心提供了一個稱為UIO的框架,用於開發使用者空間驅動程式。這是一個通用的核心驅動程式,允許撰寫能夠存取裝置暫存器和處理中斷的使用者空間驅動程式。
UIO驅動程式的工作原理
每個UIO裝置都透過一個裝置檔案和多個sysfs屬性檔案進行存取。裝置檔案將被稱為/dev/uio0(用於第一個裝置),以及/dev/uio1、 /dev/uio2``等(用於後續裝置)。
UIO驅動程式在sys檔案系統中建立檔案屬性。目錄/sys/class/uio/是所有檔案屬性的根目錄。為每個UIO裝置在/sys/class/uio/下建立一個獨立的編號目錄結構。
UIO平台裝置驅動程式
UIO平台裝置驅動程式(drivers/uio_pdrv_genirq.c)提供了UIO所需的核心空間驅動程式。它與裝置樹(Device Tree)一起工作。裝置樹節點需要在其相容屬性中設定“generic-uio”。UIO平台裝置驅動程式從裝置樹組態並註冊一個UIO裝置。
設定UIO驅動程式
要啟用UIO驅動程式,需要在核心中進行設定。使用menuconfig組態核心。從主選單導航到Device Drivers -> Userspace I/O drivers。按一次<spacebar>以在新組態旁邊顯示<*>。按<Exit>直到離開menuconfig GUI,並記得儲存新的組態。
使用者空間驅動程式設計
下圖顯示了使用者空間驅動程式的設計方式。應用程式與驅動程式的使用者空間部分介面。使用者空間部分處理硬體,但使用其核心空間部分進行啟動、關閉和接收中斷。
程式碼範例:UIO裝置存取
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#define UIO_DEV "/dev/uio0"
int main() {
int fd = open(UIO_DEV, O_RDWR);
if (fd < 0) {
perror("open");
exit(1);
}
// 存取UIO裝置
void *map = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 對對映的記憶體進行讀寫操作
unsigned int *reg = (unsigned int *)map;
printf("Register value: 0x%x\n", *reg);
// 清理
munmap(map, 4096);
close(fd);
return 0;
}
內容解密:
- 開啟UIO裝置檔案
/dev/uio0,並檢查是否成功開啟。如果失敗,印出錯誤訊息並離開。 - 使用
mmap將UIO裝置的記憶體對映到程式的位址空間。如果對映失敗,印出錯誤訊息並離開。 - 將對映的記憶體視為一個無符號整數指標,並讀取該暫存器的值,印出結果。
- 清理資源,先解除記憶體對映,然後關閉檔案描述符。
平台驅動程式(Platform Drivers)與UIO(Userspace I/O)技術詳解
UIO技術概述
UIO(Userspace I/O)是一種允許使用者空間程式直接存取硬體裝置的技術,減少了核心空間的負擔。UIO提供了一種簡單的API,讓開發者能夠將硬體裝置的記憶體對映到使用者空間,並處理中斷。
UIO裝置的sysfs介面
UIO裝置在sysfs中提供了豐富的資訊,包括:
/sys/class/uio/uio0/name:裝置名稱,與struct uio_info中的name欄位對應。/sys/class/uio/uio0/maps:包含裝置的所有記憶體對映區域。- 每個對映區域都有獨立的目錄,如
/sys/class/uio/uioX/maps/map0/,包含以下檔案:name:對映區域的名稱。addr:可對映記憶體的起始位址。size:記憶體區域的大小(位元組)。offset:從mmap()傳回的指標到實際裝置記憶體的偏移量。
中斷處理
UIO裝置的中斷可以透過讀取/dev/uioX來處理。當中斷發生時,read()函式會傳回,並提供中斷計數。
核心UIO API
UIO API提供了以下關鍵結構和函式:
struct uio_info:描述UIO驅動程式的詳細資訊,包括:name和version:裝置名稱和版本。mem[]:記憶體對映區域的陣列。irq:中斷號,或使用UIO_IRQ_CUSTOM或UIO_IRQ_NONE。- 可選的回呼函式,如
mmap()、open()、release()和irqcontrol()。
uio_register_device():將UIO驅動程式註冊到UIO框架,通常在平台裝置驅動程式的probe()函式中呼叫。uio_unregister_device():取消註冊UIO裝置,刪除相關的裝置檔案和sysfs屬性。
UIO驅動程式開發重點
- 設定
uio_info結構,包括必要的記憶體對映和中斷資訊。 - 使用
uio_register_device()註冊裝置。 - 可選地實作自訂的
mmap()、open()、release()和irqcontrol()函式。
實驗室5.4:LED UIO平台模組
此實驗室旨在開發一個UIO使用者空間驅動程式,以控制LED。核心驅動程式負責將硬體暫存器位址暴露給使用者空間。
Device Tree修改
需要在Device Tree中新增UIO節點,指定暫存器的基位址和其他屬性,如下所示:
&soc {
UIO {
compatible = "arrow,UIO";
reg = <0x7e200000 0x1000>;
pinctrl-names = "default";
pinctrl-0 = <&led_pins>;
};
};
程式碼實作
核心驅動程式需要包含必要的標頭檔,如<linux/module.h>,並實作UIO驅動程式的主要功能,包括註冊UIO裝置和設定uio_info結構。
#include <linux/module.h>
#include <linux/uio_driver.h>
// 定義uio_info結構
static struct uio_info uio_info = {
.name = "LED UIO",
.version = "1.0",
// 其他欄位初始化...
};
// 驅動程式初始化函式
static int __init led_uio_init(void)
{
// 初始化uio_info結構的其他欄位...
return uio_register_device(&uio_info);
}
// 驅動程式離開函式
static void __exit led_uio_exit(void)
{
uio_unregister_device(&uio_info);
}
module_init(led_uio_init);
module_exit(led_uio_exit);
#### 內容解密:
- 包含必要的標頭檔:引入必要的核心標頭檔以支援模組開發和UIO功能。
- 定義uio_info結構:初始化
uio_info結構,提供裝置名稱、版本和其他必要資訊。 - 實作初始化和離開函式:使用
module_init()和module_exit()巨集註冊初始化和離開函式,分別呼叫uio_register_device()和uio_unregister_device()來註冊和取消註冊UIO裝置。
開發UIO平台驅動程式與應用程式
本章節將探討如何在Linux環境下開發UIO(Userspace I/O)平台驅動程式,並與其配套的使用者空間應用程式。UIO是一種允許在使用者空間直接存取硬體的機制,從而避免了傳統核心驅動程式的複雜性。
UIO平台驅動程式開發
步驟1:包含必要的標頭檔案
驅動程式開發的第一步是包含必要的標頭檔案。這些標頭檔案提供了驅動程式開發所需的函式宣告和資料結構定義。
#include <linux/platform_device.h> /* platform_get_resource() */
#include <linux/io.h> /* devm_ioremap() */
#include <linux/uio_driver.h> /* struct uio_info, uio_register_device() */
步驟2:宣告uio_info結構
uio_info結構是UIO驅動的核心,它包含了UIO裝置的相關資訊。
static struct uio_info the_uio_info;
步驟3:在probe()函式中取得資源並對映記憶體
在probe()函式中,我們使用platform_get_resource()函式取得裝置樹中定義的記憶體資源,並使用devm_ioremap()函式將物理地址對映到核心虛擬地址。
struct resource *r;
void __iomem *g_ioremap_addr;
/* 取得第一個記憶體資源 */
r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
/* 對映記憶體區域 */
g_ioremap_addr = devm_ioremap(dev, r->start, resource_size(r));
步驟4:初始化uio_info結構
初始化uio_info結構,設定UIO裝置的名稱、版本、記憶體型別、物理地址、大小和內部虛擬地址。
the_uio_info.name = "led_uio";
the_uio_info.version = "1.0";
the_uio_info.mem[0].memtype = UIO_MEM_PHYS;
the_uio_info.mem[0].addr = r->start; /* 物理地址 */
the_uio_info.mem[0].size = resource_size(r);
the_uio_info.mem[0].name = "demo_uio_driver_hw_region";
the_uio_info.mem[0].internal_addr = g_ioremap_addr; /* 內部虛擬地址 */
步驟5:註冊UIO裝置
使用uio_register_device()函式註冊UIO裝置。
uio_register_device(&pdev->dev, &the_uio_info);
#### 內容解密:
platform_get_resource()用於從裝置樹中取得指定的資源。devm_ioremap()將物理地址對映到核心虛擬地址,以便驅動程式可以存取硬體。uio_info結構的初始化提供了UIO裝置的相關資訊,包括名稱、版本和記憶體對映資訊。uio_register_device()註冊UIO裝置,使其可以被使用者空間應用程式存取。
使用者空間應用程式開發
步驟1:包含必要的標頭檔案
使用者空間應用程式需要包含必要的標頭檔案,以使用相關的系統呼叫和函式。
#include <sys/mman.h> /* mmap() */
步驟2:開啟UIO裝置
使用open()系統呼叫開啟UIO裝置。
open("/dev/uio0", O_RDWR | O_SYNC);
步驟3:取得記憶體大小
讀取sysfs中的檔案以取得UIO裝置的記憶體大小。
FILE *size_fp = fopen(UIO_SIZE, "r");
fscanf(size_fp, "0x%08X", &uio_size);
fclose(size_fp);
步驟4:對映記憶體
使用mmap()系統呼叫將UIO裝置的物理地址對映到使用者空間虛擬地址。
void *virt_addr = mmap(NULL, uio_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
#### 內容解密:
open()系統呼叫用於開啟UIO裝置檔案,獲得檔案描述符。fopen()和fscanf()用於讀取sysfs中的檔案,以取得UIO裝置的記憶體大小。mmap()系統呼叫將UIO裝置的物理地址對映到使用者空間虛擬地址,使應用程式可以直接存取硬體。