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_offsetGPCLR0_offset 分別代表設定和清除 GPIO 輸出暫存器的偏移地址。
  • GPIO_27_INDEXGPIO_22_INDEXGPIO_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或其他組態方法以及正規表示式解析等。每次呼叫核心(系統呼叫)都必須從使用者模式切換到管理模式,然後再傳回。這需要時間,如果呼叫頻繁,就會成為效能瓶頸。

使用者空間驅動程式的優缺點

  1. 使用者空間驅動程式的優點:

    • 易於除錯,因為應用程式開發的除錯工具更為豐富。
    • 可使用浮點數等使用者空間服務。
    • 裝置存取非常高效,因為不需要系統呼叫。
    • Linux上的應用程式API非常穩定。
    • 驅動程式可以用任何語言撰寫,而不僅限於C語言。
  2. 使用者空間驅動程式的缺點:

    • 無法存取核心框架和服務。
    • 無法在使用者空間處理中斷,必須由核心驅動程式處理。
    • 沒有預先定義的API允許應用程式存取裝置驅動程式。
  3. 核心空間驅動程式的優點:

    • 在最高許可權模式下執行於核心空間,允許存取中斷和硬體資源。
    • 有許多核心服務可用於設計複雜裝置的核心空間驅動程式。
    • 核心提供API給使用者空間,允許多個應用程式同時存取核心空間驅動程式。
  4. 核心空間驅動程式的缺點:

    • 存取驅動程式時有系統呼叫的額外負擔。
    • 除錯困難。
    • 核心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;
}

內容解密:

  1. 開啟UIO裝置檔案/dev/uio0,並檢查是否成功開啟。如果失敗,印出錯誤訊息並離開。
  2. 使用mmap將UIO裝置的記憶體對映到程式的位址空間。如果對映失敗,印出錯誤訊息並離開。
  3. 將對映的記憶體視為一個無符號整數指標,並讀取該暫存器的值,印出結果。
  4. 清理資源,先解除記憶體對映,然後關閉檔案描述符。

平台驅動程式(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提供了以下關鍵結構和函式:

  1. struct uio_info:描述UIO驅動程式的詳細資訊,包括:

    • nameversion:裝置名稱和版本。
    • mem[]:記憶體對映區域的陣列。
    • irq:中斷號,或使用UIO_IRQ_CUSTOMUIO_IRQ_NONE
    • 可選的回呼函式,如mmap()open()release()irqcontrol()
  2. uio_register_device():將UIO驅動程式註冊到UIO框架,通常在平台裝置驅動程式的probe()函式中呼叫。

  3. 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);

#### 內容解密:

  1. 包含必要的標頭檔:引入必要的核心標頭檔以支援模組開發和UIO功能。
  2. 定義uio_info結構:初始化uio_info結構,提供裝置名稱、版本和其他必要資訊。
  3. 實作初始化和離開函式:使用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);

#### 內容解密:

  1. platform_get_resource()用於從裝置樹中取得指定的資源。
  2. devm_ioremap()將物理地址對映到核心虛擬地址,以便驅動程式可以存取硬體。
  3. uio_info結構的初始化提供了UIO裝置的相關資訊,包括名稱、版本和記憶體對映資訊。
  4. 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);

#### 內容解密:

  1. open()系統呼叫用於開啟UIO裝置檔案,獲得檔案描述符。
  2. fopen()fscanf()用於讀取sysfs中的檔案,以取得UIO裝置的記憶體大小。
  3. mmap()系統呼叫將UIO裝置的物理地址對映到使用者空間虛擬地址,使應用程式可以直接存取硬體。