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可以透過暫存器與硬體進行通訊,所以當使用者使用指標向某個地址寫入資料,實際上是將資料寫入了暫存器中。
使用mmio與硬體裝置進行通訊的三個步驟:
- 向MMIO暫存器傳送請求,透過一些核心API完成,比如request_mem_region(), it's recommended, not mandatory
- 將暫存器的實體地址對映到虛擬地址,比如使用函式ioremap()
- 使用核心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的驅動,其整體框架如下:
這樣的框架存在一些問題:
- 使用者訪問裝置檔案所使用的介面是自定義的,而沒有進行標準化
- 從GPIO控制器中為裝置驅動分配了兩個暫存器,那麼其他GPIO將無法訪問這兩個暫存器,
假設GPIO控制器中有32個GPIO,那麼沒有人能夠使用其餘的另外31個GPIO - 在裝置驅動中採用硬編碼的方式寫入硬體相關的資訊,那麼如果修改了硬體,也必須修改驅動
因此,這樣的框架還需要一定程度的解耦合並進行模組化。
Driver Model
Linux驅動模型提供了多個裝置驅動抽象(abstraction to device drivers),這能夠使驅動程式碼更模組化、可重用並且容易維護。
該驅動模型的組成如下:
- Framework:根據裝置型別匯出的標準化介面
- Buses:從裝置驅動中抽象出來的裝置資訊以及裝置所連線的位置
使用驅動框架(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匯流排裝置為例,下圖演示了這個過程:
這個過程可以大致分為三步:
- 在Bus Core註冊驅動
- 在Bus Core註冊裝置,隨後Bus Core將會匹配對應的驅動
- 匹配成功後,Bus Core將會透過probe()函式呼叫驅動對應的回撥函式,進行例項化
有很多種方法向匯流排註冊一個裝置:
- 使用Bus Core提供的介面,在使用者應用中靜態註冊一個裝置,比如I2C Bus提供的
i2c_register_board_info()
,或者虛擬匯流排Platform Bus提供的platform_device_register()
- 使用硬體平臺提供的序號產生器制,比如X86提供的ACPI
- 使用裝置樹,比如PowerPC以及ARM提供的標準化機制
- 有一些匯流排支援透過裝置列舉(device enumeration)來自動新增裝置,比如PCI匯流排的
lspci
命令