Structure of Linux Kernel Device Driver(Part II)

Hang3發表於2024-07-16

Structure of Linux Kernel Device Driver

ref. https://www.youtube.com/watch?v=XoYkHUnmpQo&list=LL&index=1&t=272s

Talk to the hardware

在作業系統中有幾種機制能夠讓CPU與硬體裝置進行通訊:

  • Port I/O: 使用一個專用的匯流排進行用心
  • Memory-mapped I/O: 與硬體裝置共享記憶體地址空間進行通訊,(have the I/O devices mapped to our address spaces, so in the address space you have the registers there where you can talk to the hardware ),這種方法更為常見

在Memory-Mapped I/O(MMIO)機制中,將I/O裝置對映到記憶體地址空間,然後CPU可以透過暫存器與硬體進行通訊,所以當使用者使用指標向某個地址寫入資料,實際上是將資料寫入了暫存器中。

Structure of Linux Kernel Device Driver(Part II)

使用mmio與硬體裝置進行通訊的三個步驟:

  1. 向MMIO暫存器傳送請求,透過一些核心API完成,比如request_mem_region(), it's recommended, not mandatory
  2. 將暫存器的實體地址對映到虛擬地址,比如使用函式ioremap()
  3. 使用核心API讀寫暫存器,比如readb()\writeb(), readw()\writew(), readl()\writel(), readq()\writeq(),分別進行8-bit, 16-bit, 32-bit以及64-bit的讀寫

當然也可以使用函式ioremap()返回的指標進行讀寫,不過推薦使用核心封裝的函式對這個指標進行操作,注意使用iounmap()釋放掉這些地址空間。完成地址對映後,可以透過cat \proc\iomem來檢視I/O裝置的地址對映。

下面這段程式碼用於控制一個LED燈裝置的驅動:

#define GPIO1_BASE	0x0209C000
#define GPIO1_SIZE	8
#define LED_OFF	0
#define LED_ON	1

static struct {
	dev_t devnum;
	struct cdev cdev;
	void __iomem *regbase;
    // device datas area
} drvled_data;

static void drvled_setled(unsigned int status)
{
	u32 val;

	/* set value */
	val = readl(drvled_data.regbase);
	if (status == LED_ON)
		val |= GPIO_BIT;
	else if (status == LED_OFF)
		val &= ~GPIO_BIT;
	writel(val, drvled_data.regbase);

	/* update status */
	drvled_data.led_status = status;
}

static ssize_t my_write(struct file *file, const char __user *buf,
			    size_t count, loff_t *ppos)
{
	char kbuf = 0;
	if (copy_from_user(&kbuf, buf, 1))
		return -EFAULT;

	if (kbuf == '1') {
		drvled_setled(LED_ON);
		pr_info("LED ON!\n");
	} else if (kbuf == '0') {
		drvled_setled(LED_OFF);
		pr_info("LED OFF!\n");
	}
	return count;
}

static const struct file_operations drvled_fops = {
	.owner = THIS_MODULE,
	.write = my_write,
};

static int __init init_module(void)
{
    
	if (!request_mem_region(GPIO1_BASE, GPIO1_SIZE, "my_device_driver")) {
		// handle error
	}

	drvled_data.regbase = ioremap(GPIO1_BASE, GPIO1_SIZE);
	if (!drvled_data.regbase) {
		// handle error
	}

    // Device driver initialization and installation

    return 0;
}

static int __exit exit_module(void)
{
    iounmap(drvled_data.regbase);
	release_mem_region(GPIO1_BASE, GPIO1_SIZE);
    // Unloading and unregistering the device driver
}

透過上面的這些API,可以透過讀寫對應了裝置檔案控制對應的硬體裝置。下圖是一個LED的驅動,其整體框架如下:

led driver 1

這樣的框架存在一些問題:

  1. 使用者訪問裝置檔案所使用的介面是自定義的,而沒有進行標準化
  2. 從GPIO控制器中為裝置驅動分配了兩個暫存器,那麼其他GPIO將無法訪問這兩個暫存器,
    假設GPIO控制器中有32個GPIO,那麼沒有人能夠使用其餘的另外31個GPIO
  3. 在裝置驅動中採用硬編碼的方式寫入硬體相關的資訊,那麼如果修改了硬體,也必須修改驅動

因此,這樣的框架還需要一定程度的解耦合並進行模組化。

Driver Model

Linux驅動模型提供了多個裝置驅動抽象(abstraction to device drivers),這能夠使驅動程式碼更模組化、可重用並且容易維護。

該驅動模型的組成如下:

  • Framework:根據裝置型別匯出的標準化介面
  • Buses:從裝置驅動中抽象出來的裝置資訊以及裝置所連線的位置

led driver 2

使用驅動框架(linux/leds.h for led device)將介面標準化後,意味著驅動開發者不再需要定義file_operations來指定回撥函式的行為,這些介面以及對應回撥函式都由對應的框架完成定義。Users know beforehand the interface provided by a driver based on its class or type.

為了避免硬體資源被一個裝置獨佔,需要使用特定的API進行動態控制,比如對於LED裝置,Linux核心中實現了一種生產者/消費者的模型(gpiolib)來管理GPIO資源。

  • GPIO producer,類似於GPIO控制器的驅動
  • GPIO consumer,類似於LED裝置的驅動

如下,使用了框架將使用者介面標準化,並使用核心介面管理硬體資源:

#include <linux/leds.h>
// other header files

struct drvled_data_st {
	struct gpio_desc *desc;         // change from "void __iomem *regbase;"
	struct led_classdev led_cdev;   // change from "cdev"
};

static struct drvled_data_st *drvled_data;

static void drvled_setled(unsigned int status)
{
    // not use the writel()
	if (status == LED_ON)
		gpiod_set_value(drvled_data->desc, 1);
	else
		gpiod_set_value(drvled_data->desc, 0);
}

static void drvled_change_state(struct led_classdev *led_cdev,
				enum led_brightness brightness)
{
	if (brightness)
		drvled_setled(LED_ON);
	else
		drvled_setled(LED_OFF);
}

static int __init drvled_init(void)
{
	int result = 0;

    // no need for driver initialization
    // no need for iommp for device

	drvled_data = kzalloc(sizeof(*drvled_data), GFP_KERNEL);
	if (!drvled_data) {
		// handle error
	}

	result = gpio_request(GPIO_NUM, DRIVER_NAME);
	if (result) {
		// handle error
	}

	drvled_data->desc = gpio_to_desc(GPIO_NUM);

	drvled_data->led_cdev.name = "ipe:red:user";
	drvled_data->led_cdev.brightness_set = drvled_change_state;

	result = led_classdev_register(NULL, &drvled_data->led_cdev);
	if (result) {
		// handle error
	}
    // ...
}

static void __exit drvled_exit(void)
{
	led_classdev_unregister(&drvled_data->led_cdev);
    // not use the iounmap()	
    gpio_free(GPIO_NUM);
	release_mem_region(GPIO1_BASE, GPIO1_SIZE);
	kfree(drvled_data);
}
// ...

使用裝置框架對驅動進行重組後,使用者不再直接與/dev/目錄下的裝置檔案進行互動,而是在目錄/sys/class/led目錄下找到所有的LED類的裝置,進入所註冊的驅動目錄下,可以找到控制LED裝置的介面,這些介面仍然以檔案的形式存在,但是和之前所定義的介面相比會更加標準化,也就是幾乎所有的LED裝置都可以使用這樣的介面進行控制。

Bus infrastructure

最後,可以使用匯流排框架實現裝置與驅動的解耦合。匯流排框架組成如下:

  • Bus Core: 對於給定匯流排型別(USB core, PCI core, etc)所實現的API, (represented in the kernel by the "bus_type" structure)
  • Bus adapters:匯流排控制器驅動, (represented in the kernel by the "device_driver" structure)
  • Bus drivers: 負責管理連線到匯流排的裝置, (represented in the kernel by the "device_driver" structure)
  • Bus devices: 所有連線到匯流排的裝置, (represented in the kernel by the structure "device")

匯流排框架如下圖所示:

解耦合的實現:在匯流排框架中,驅動相當於是一種類(class),當使用者在匯流排上註冊裝置時,將會產生這個類的例項。以I2C匯流排裝置為例,下圖演示了這個過程:

這個過程可以大致分為三步:

  1. 在Bus Core註冊驅動
  2. 在Bus Core註冊裝置,隨後Bus Core將會匹配對應的驅動
  3. 匹配成功後,Bus Core將會透過probe()函式呼叫驅動對應的回撥函式,進行例項化

有很多種方法向匯流排註冊一個裝置:

  • 使用Bus Core提供的介面,在使用者應用中靜態註冊一個裝置,比如I2C Bus提供的i2c_register_board_info(),或者虛擬匯流排Platform Bus提供的platform_device_register()
  • 使用硬體平臺提供的序號產生器制,比如X86提供的ACPI
  • 使用裝置樹,比如PowerPC以及ARM提供的標準化機制
  • 有一些匯流排支援透過裝置列舉(device enumeration)來自動新增裝置,比如PCI匯流排的lspci命令

相關文章