在嵌入式系統開發中,控制外部裝置是常見的需求。本文將探討如何在 Raspberry Pi 3 Model B 平台上,開發一個控制 RGB LED 的平台驅動程式。此過程涉及到 Device Tree 的修改,以及 Linux 核心驅動程式的編寫,包含 GPIO 的初始化、控制以及與使用者空間的互動。透過理解這些核心概念,開發者可以更有效地控制硬體資源,並建立客製化的嵌入式系統應用。以下將逐步說明 Device Tree 的修改方式、驅動程式碼的架構及核心函式的實作細節,並提供必要的程式碼片段和解說,以幫助讀者理解整個開發流程。
平台驅動程式 Chapter 5
LAB 5.2 硬體描述
BCM2837 SoC 擁有 54 個通用輸入/輸出(GPIO)線,分為兩個銀行。所有 GPIO 鎖都至少有兩個替代功能在 SoC 內部。替代功能通常是周邊 IO,並且單個周邊裝置可能出現在每個銀行中,以允許 IO 電壓的靈活性。GPIO 有 41 個暫存器,所有存取都被假設為 32 位。
GPIO 暫存器功能
- 功能選擇暫存器用於定義通用輸入/輸出引腳的操作。每個 GPIO 引腳至少有兩個替代功能。FSEL{n} 欄位決定了第 n 個 GPIO 引腳的功能。所有未使用的替代功能線都被拉到地,並且如果被選擇,將輸出 “0”。
- 輸出設定暫存器用於設定 GPIO 引腳。SET{n} 欄位定義了要設定的相應 GPIO 引腳,向該欄位寫入 “0” 沒有任何效果。如果 GPIO 引腳被用作輸入(預設情況下),則 SET{n} 欄位中的值將被忽略。但是,如果引腳隨後被定義為輸出,則該位將根據最後的設定/清除操作被設定。分開設定和清除功能消除了讀取-修改-寫入操作的需要。
- 輸出清除暫存器用於清除 GPIO 引腳。CLR{n} 欄位定義了要清除的相應 GPIO 引腳,向該欄位寫入 “0” 沒有任何效果。如果 GPIO 引腳被用作輸入(預設情況下),則 CLR{n} 欄位中的值將被忽略。但是,如果引腳隨後被定義為輸出,則該位將根據最後的設定/清除操作被設定。
LAB 5.2 裝置樹描述
正如您在第 5 章的 Pin Controller 一節中已經看到的那樣,引腳控制器允許處理器在幾個功能區塊之間分享一個 PAD。透過多路復用 PAD 輸入/輸出訊號來實作分享。由於不同的模組需要不同的 PAD 設定(例如,上拉,保持器),引腳控制器還控制 PAD 設定引數。受引腳組態影響的硬體模組被稱為「客戶端裝置」。每個客戶端裝置必須像其他硬體模組一樣,在裝置樹中表示為一個節點。要使客戶端裝置正常工作,某些引腳控制器必須設定特定的引腳組態。每個引腳控制器必須在裝置樹中表示為一個節點。
BCM2837 SoC 的引腳控制器
BCM2837 SoC 的引腳控制器在核心原始碼樹的 arch/arm/boot/dts/bcm283x.dtsi 中宣告,如下所示:
gpio: gpio@7e200000 {
compatible = "brcm,bcm2835-gpio";
reg = <0x7e200000 0xb4>;
interrupts = <2 17>, <2 18>, <2 19>, <2 20>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
dpi_gpio0: dpi_gpio0 {
brcm,pins = <0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27>;
brcm,function = <BCM2835_FSEL_ALT2>;
};
emmc_gpio22: emmc_gpio22 {
brcm,pins = <22 23 24 25 26 27>;
brcm,function = <BCM2835_FSEL_ALT3>;
};
// ...
};
引腳控制器屬性
brcm,pins:一個單元格陣列,每個單元格包含一個引腳的 ID。有效的 ID 是整數 GPIO ID;0==GPIO0,1==GPIO1,…,53==GPIO53。brcm,function:整數,包含要多路復用到引腳的功能:- 0:GPIO 輸入
- 1:GPIO 輸出
- 其他值對應不同的替代功能
brcm,pull:整數,表示要應用於引腳的上拉/下拉:- 0:無
- 1:下拉
- 2:上拉
LAB 5.2 操作與實作
要控制 LED,需要使用 BCM2837 SoC 的三個引腳,並在裝置樹中將它們多路復用為 GPIO。首先,需要在裝置樹中建立一個節點來描述這些引腳的組態。然後,在驅動程式中使用 copy_to_user() 和 copy_from_user() 等函式來交換內核和使用者空間之間的字元陣列,從而實作對 LED 的控制。
連線硬體
- 將 Raspberry Pi 的 GPIO27 引腳連線到 Color click™ 的 RD 引腳。
- 將 GPIO22 引腳連線到 GR 引腳。
- 將 GPIO26 引腳連線到 BL 引腳。
- 在兩個板子之間連線 5V 電源和 GND。
程式碼範例
// 定義 GPIO 引腳
#define GPIO27 27
#define GPIO22 22
#define GPIO26 26
// 初始化 GPIO 引腳
void init_gpio(void) {
// 將 GPIO27、GPIO22 和 GPIO26 設定為輸出模式
gpio_set_function(GPIO27, BCM2835_FSEL_OUTP);
gpio_set_function(GPIO22, BCM2835_FSEL_OUTP);
gpio_set_function(GPIO26, BCM2835_FSEL_OUTP);
}
// 控制 LED 的函式
void control_led(int led, int state) {
switch (led) {
case 0: // 紅色 LED
if (state) {
gpio_write(GPIO27, HIGH);
} else {
gpio_write(GPIO27, LOW);
}
break;
case 1: // 綠色 LED
if (state) {
gpio_write(GPIO22, HIGH);
} else {
gpio_write(GPIO22, LOW);
}
break;
case 2: // 藍色 LED
if (state) {
gpio_write(GPIO26, HIGH);
} else {
gpio_write(GPIO26, LOW);
}
break;
default:
break;
}
}
#### 程式碼解說:
1. 首先定義了要用於控制 LED 的 GPIO 引腳編號,分別是 GPIO27、GPIO22 和 GPIO26。
2. `init_gpio` 函式用於初始化這些 GPIO 引腳,將它們設定為輸出模式,以便控制 LED。
3. `control_led` 函式根據傳入的引數控制對應的 LED,可以開啟或關閉指定的 LED。
#### Plantuml 圖表說明
```plantuml
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
skinparam rectangleBackgroundColor #F5F5F5
skinparam rectangleBorderColor #333333
skinparam arrowColor #333333
title Plantuml 圖表說明
rectangle "GPIO27" as node1
rectangle "GPIO22" as node2
rectangle "GPIO26" as node3
rectangle "5V" as node4
rectangle "GND" as node5
node1 --> node2
node2 --> node3
node3 --> node4
node4 --> node5
@enduml
此圖示展示了 Raspberry Pi 與 Color click™ 板之間的連線方式,包括 GPIO 引腳和電源連線。
平台驅動程式章節 5:RGB LED 平台裝置驅動程式開發
在 Raspberry Pi 3 Model B 上開發 RGB LED 平台驅動程式,需要對 Device Tree 和 GPIO 控制有深入的瞭解。首先,修改 bcm2710-rpi-3-b.dts 檔案以新增必要的 pin 組態節點和裝置節點。
Device Tree 修改
在 gpio 節點下新增 led_pins pin 組態節點,並在 soc 節點下新增 ledred、ledgreen 和 ledblue 裝置節點。
&gpio {
[...]
led_pins: led_pins {
brcm,pins = <27 22 26>;
brcm,function = <1>; /* 輸出 */
brcm,pull = <1 1 1>; /* 下拉 */
};
};
&soc {
[...]
ledred {
compatible = "arrow,RGBleds";
label = "ledred";
pinctrl-0 = <&led_pins>;
};
ledgreen {
compatible = "arrow,RGBleds";
label = "ledgreen";
};
ledblue {
compatible = "arrow,RGBleds";
label = "ledblue";
};
[...]
};
RGB LED 平台裝置驅動程式碼解析
1. 引入必要的標頭檔案
#include <linux/module.h>
#include <linux/fs.h> /* struct file_operations */
#include <linux/platform_device.h> /* platform_driver_register(), platform_set_drvdata() */
#include <linux/io.h> /* devm_ioremap(), iowrite32() */
#include <linux/of.h> /* of_property_read_string() */
#include <linux/uaccess.h> /* copy_from_user(), copy_to_user() */
#include <linux/miscdevice.h> /* misc_register() */
2. 定義 GPIO 掩碼
定義用於組態 GPIO 暫存器的掩碼。
#define GPIO_27 27
#define GPIO_22 22
#define GPIO_26 26
#define GPIO_27_INDEX (1 << (GPIO_27 % 32))
#define GPIO_22_INDEX (1 << (GPIO_22 % 32))
#define GPIO_26_INDEX (1 << (GPIO_26 % 32))
#define GPIO_27_FUNC (1 << ((GPIO_27 % 10) * 3))
#define GPIO_22_FUNC (1 << ((GPIO_22 % 10) * 3))
#define GPIO_26_FUNC (1 << ((GPIO_26 % 10) * 3))
#define FSEL_27_MASK (0b111 << ((GPIO_27 % 10) * 3))
#define FSEL_22_MASK (0b111 << ((GPIO_22 % 10) * 3))
#define FSEL_26_MASK (0b111 << ((GPIO_26 % 10) * 3))
#define GPIO_SET_FUNCTION_LEDS (GPIO_27_FUNC | GPIO_22_FUNC | GPIO_26_FUNC)
#define GPIO_MASK_ALL_LEDS (FSEL_27_MASK | FSEL_22_MASK | FSEL_26_MASK)
#define GPIO_SET_ALL_LEDS (GPIO_27_INDEX | GPIO_22_INDEX | GPIO_26_INDEX)
#### 內容解密:
- GPIO 引腳定義:定義了控制 RGB LED 的 GPIO 引腳編號(27、22、26)。
- 掩碼定義:定義了用於設定和清除每個 LED 的掩碼,以及選擇輸出功能的掩碼。
- 功能選擇掩碼:定義了用於選擇 GPIO 引腳功能的掩碼,以控制 RGB LED。
3. 宣告物理 I/O 暫存器地址
#define BCM2710_PERI_BASE 0x3F000000
#define GPIO_BASE (BCM2710_PERI_BASE + 0x200000)
#define GPFSEL2 (GPIO_BASE + 0x08)
#define GPSET0 (GPIO_BASE + 0x1C)
#define GPCLR0 (GPIO_BASE + 0x28)
#### 內容解密:
- 基地址定義:定義了 BCM2710 的外設基地址和 GPIO 基地址。
- 暫存器地址:定義了用於控制 GPIO 的暫存器地址,包括功能選擇暫存器(GPFSEL2)、設定暫存器(GPSET0)和清除暫存器(GPCLR0)。
4. 宣告虛擬地址指標
static void __iomem *GPFSEL2_V;
static void __iomem *GPSET0_V;
static void __iomem *GPCLR0_V;
#### 內容解密:
- 虛擬地址指標:聲明瞭用於存放暫存器虛擬地址的指標,透過
ioremap()函式對映物理地址到虛擬地址空間。
5. 定義私有資料結構
struct led_dev {
struct miscdevice led_misc_device; /* 為每個 LED 分配裝置 */
u32 led_mask; /* 不同 LED 的掩碼(紅、綠、藍) */
const char *led_name; /* 儲存 "label" 字串 */
char led_value[8];
};
#### 內容解密:
led_dev結構:定義了一個私有資料結構,用於存放每個 LED 裝置的相關資訊,包括雜項裝置結構、LED 掩碼、名稱和值。
6. probe() 函式中的私有結構例項化
struct led_dev *led_device;
led_device = devm_kzalloc(&pdev->dev, sizeof(struct led_dev), GFP_KERNEL);
#### 內容解密:
- 記憶體分配:在
probe()函式中為每個新的探測到的裝置分配記憶體,用於存放私有資料結構。
7. 初始化虛擬地址
在 led_init() 例程中,使用 ioremap() 取得暫存器地址的虛擬地址。
GPFSEL2_V = ioremap(GPFSEL2, sizeof(u32));
GPSET0_V = ioremap(GPSET0, sizeof(u32));
GPCLR0_V = ioremap(GPCLR0, sizeof(u32));
#### 內容解密:
- 虛擬地址對映:透過
ioremap()將物理暫存器地址對映到虛擬地址空間,以便內核可以存取這些暫存器。
8. 初始化雜項裝置結構
在 probe() 例程中初始化每個雜項裝置結構。
of_property_read_string(pdev->dev.of_node, "label", &led_device->led_name);
led_device->led_misc_device.minor = MISC_DYNAMIC_MINOR;
led_device->led_misc_device.name = led_device->led_name;
led_device->led_misc_device.fops = &led_fops;
#### 內容解密:
- 讀取標籤屬性:使用
of_property_read_string()從裝置樹節點中讀取 “label” 屬性。 - 初始化雜項裝置:設定雜項裝置的次編號、名稱和檔案操作結構。
9. 定義檔案操作結構
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.read = led_read,
.write = led_write,
};
#### 內容解密:
- 檔案操作結構:定義了一個檔案操作結構,用於指定當使用者開啟、關閉、讀取和寫入字元檔案時要呼叫的驅動程式函式。