返回文章列表

Linux RGB LED 驅動程式開發與UIO整合

本文探討 Linux 平台 RGB LED 驅動程式的開發,涵蓋 GPIO 控制、裝置樹解析、LED 子系統整合及 UIO 技術應用。從驅動核心結構、初始化流程到亮度調節,詳細說明程式碼細節與硬體互動。此外,更進一步探討 UIO

嵌入式系統 驅動程式開發

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裝置的物理地址對映到使用者空間虛擬地址,使應用程式可以直接存取硬體。