哈嘍,我是老吳。
非常懷念寫文章的感覺。
昨晚復習了一些 Linux 驅動的基礎知識,給大家分享一下吧。
先說結論:
多年來,我接觸到的 Linux 驅動教程大多都是從 0 編寫,這樣對初學者而言最大的好處,就是可以接觸到比較多的底層原理。
但是在真正的工作場景里,其實是應該盡量避免從 0 構建自己的設備驅動的。
更好的做法是在高度模塊化的驅動框架里添加自己的設備驅動。
這樣做的好處是最大程度地復用內核現有的代碼,同時獲得極大的彈性和可維護性,并且為應用程序提供出統一的訪問接口。
下面詳細地說說。
設備驅動程序 (device driver) 是對硬件的抽象:

提供基礎框架來編寫和運行設備驅動程序是操作系統內核責任的一部分。
盡管可以在用戶空間中運行設備驅動程序(通過一些內核接口,如 UIO 或 I2CDEV),更常見的情況是讓它們在內核空間中運行。
以字符設備驅動為例:

字符設備 (char device) 是一種最常見的硬件抽象。
/dev 目錄下的設備節點文件就是內核導出給用戶空間的訪問設備驅動的接口。
設備節點文件中有三個基本信息:
Type,用于標識是 block 還是 char device;
Major number,用于標志是那一類 char deivce;
Minor number,用于標志是哪一個 char device;
編寫字符設備驅動的流程:

1、分配設備號,這通過 register_chrdev_region() 或 alloc_chrdev_region() 來完成;
2、實現文件操作(open、read、write、ioctl)等。
3、使用 cdev_init() 和 cdev_add() 向內核中注冊字符設備。

如果按照從 0 構建的思路編寫驅動的話,偽代碼如下:
硬件訪問相關:
static?struct?{
?dev_t?devnum;
?struct?cdev?cdev;
?unsigned?int?led_status;
?void?__iomem?*regbase;
}?drvled_data;
static?void?drvled_setled(unsigned?int?status)
{
?u32?val;
?/*?set?value?*/
?val?=?readl(drvled_data.regbase?+?GPIO1_REG_DATA);
?if?(status?==?LED_ON)
??val?|=?GPIO_BIT;
?else?if?(status?==?LED_OFF)
??val?&=?~GPIO_BIT;
?writel(val,?drvled_data.regbase?+?GPIO1_REG_DATA);
?/*?update?status?*/
?drvled_data.led_status?=?status;
}
static?void?drvled_setdirection(void)
{
?...
}
文件操作相關:
static?ssize_t?drvled_read(struct?file?*file,?char?__user?*buf,
??????size_t?count,?loff_t?*ppos)
{
?...
}
static?ssize_t?drvled_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?=?drvled_write,
?.read?=?drvled_read,
};
注冊和卸載字符設備相關:
static?int?__init?drvled_init(void)
{
?int?result?=?0;
?if?(!request_mem_region(GPIO1_BASE,?GPIO1_SIZE,?DRIVER_NAME))?{
??pr_err("%s:?Error?requesting?I/O!\n",?DRIVER_NAME);
??result?=?-EBUSY;
??goto?ret_err_request_mem_region;
?}
?drvled_data.regbase?=?ioremap(GPIO1_BASE,?GPIO1_SIZE);
?if?(!drvled_data.regbase)?{
??pr_err("%s:?Error?mapping?I/O!\n",?DRIVER_NAME);
??result?=?-ENOMEM;
??goto?err_ioremap;
?}
?result?=?alloc_chrdev_region(&drvled_data.devnum,?0,?1,?DRIVER_NAME);
?if?(result)?{
??pr_err("%s:?Failed?to?allocate?device?number!\n",?DRIVER_NAME);
??goto?ret_err_alloc_chrdev_region;
?}
?cdev_init(&drvled_data.cdev,?&drvled_fops);
?result?=?cdev_add(&drvled_data.cdev,?drvled_data.devnum,?1);
?if?(result)?{
??pr_err("%s:?Char?device?registration?failed!\n",?DRIVER_NAME);
??goto?ret_err_cdev_add;
?}
?drvled_setdirection();
?drvled_setled(LED_OFF);
?pr_info("%s:?initialized.\n",?DRIVER_NAME);
?goto?ret_ok;
ret_err_cdev_add:
?unregister_chrdev_region(drvled_data.devnum,?1);
ret_err_alloc_chrdev_region:
?iounmap(drvled_data.regbase);
err_ioremap:
?release_mem_region(GPIO1_BASE,?GPIO1_SIZE);
ret_err_request_mem_region:
ret_ok:
?return?result;
}
static?void?__exit?drvled_exit(void)
{
????...
}
module_init(drvled_init);
module_exit(drvled_exit);
運行效果:
$?install?ledrv.ko
$?ls?/dev/led
#?燈亮
$?echo?1?>/dev/led
#?燈滅
$?echo?1?>/dev/led?
單從功能的角度看,上面的程序完全滿足控制一個 LED 的需求。
但是,它不是一個好的驅動,這里有 3 個問題。

問題 1:
它創建的接口是 /dev/led,這不是一個通用接口,會增加上層開發人員的學習成本。
解決這個問題需要在 LED char driver 上再添加一層 LED framework,LED framework 負責給用戶空間提供標準化的訪問接口,同時用于添加可復用的邏輯功能。
基本上各種設備驅動都有自己的 framework,例如 input, IIO, ALSA, V2L2, RTC, watchdog 等。
使用這些 framework 驅動工程師不用考慮提供給用戶空間的接口,應用開發人員也只需要學習一次標準的硬件訪問接口接口。
問題 2:
它只是控制 1 個 gpio,但是卻申請使用了 2 個寄存器,這 2 個寄存器負責控制芯片的 8 個 gpio。這意味著其他 7 個 gpio 再也沒法被其他驅動申請使用。
解決這個問題需要引入一個 gpio 的管理者:gpiolib。gpiolib 負責統一管理和分配 gpio 資源。
問題 3:
它包含了硬件信息。如果我們想控制另外一個 gpio或者多個 gpio,就得改動源碼,代碼維護的工作量極大。
解決這個問題我們需要將硬件信息從代碼中抽取出來,具體的就是引入總線、設備、驅動模型。
我們用上面的思路,寫一個更合理的 LED 驅動。

引入 LED framework:
1、初始化 led_classdev 結構體。
2、提供一個回調函數來改變狀態 LED 的。
3、使用 led_classdev_register() 在想 LED framework 注冊驅動程序。
引入 gpiolib:
內核管理 gpio 的思路是典型的 producer/consumer 模型。
GPIO controller driver 是 producer,LED driver 是 consumer。
下面是幾個常用的 gpiolib api,它們的作用一目了然:
#include?
#include?
struct?gpio_desc?*gpiod_get(struct?device?*dev,?const?char?*con_id,enum?gpiod_flags?flags);
void?gpiod_put(struct?gpio_desc?*desc);
int?gpiod_direction_input(struct?gpio_desc?*desc);
int?gpiod_direction_output(struct?gpio_desc?*desc,?int?value);
void?gpiod_set_value(struct?gpio_desc?*desc,?int?value);
int?gpiod_get_value(const?struct?gpio_desc?*desc);
引入總線、設備、驅動模型 :
該模型包含 4 部分。
Bus core: 對硬件總線的抽象,不同總線有不同的 Bus core,例如 USB core, SPI core, I2C core, PCI core ,在內核中由 bus_type 結構表示。
Bus adapters: 總線控制器驅動程序,在內核中由 device_driver 結構體表示。
Bus drivers: 負責管理連接到總線的設備的驅動程序,在內核中由 device_driver 結構體表示。
Bus devices: 連接到總線的設備,在內核中由結構 device 表示。
內核虛擬了一條叫 Platform 的總線,用于適配 LED 這種不屬于任何總線的設備。
看下改造后的代碼:
設備信息:
LED {
<&gpio1 9>
}
硬件控制:
tatic?struct?drvled_data_st?*drvled_data;
static?void?drvled_setled(unsigned?int?status)
{
?//?控制?gpio
?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);
}
向 LED framework 注冊:
static?int?drvled_probe(struct?platform_device?*pdev)
{
?struct?device_node?*np?=?pdev->dev.of_node;
?struct?device_node?*child?=?NULL;
?int?result,?gpio;
?child?=?of_get_next_child(np,?NULL);
?drvled_data?=?devm_kzalloc(&pdev->dev,?sizeof(*drvled_data),
???????GFP_KERNEL);
?if?(!drvled_data)
??return?-ENOMEM;
?//?從設備數中獲得硬件信息
?gpio?=?of_get_gpio(child,?0);
?result?=?devm_gpio_request(&pdev->dev,?gpio,?pdev->name);
?if?(result)?{
??dev_err(&pdev->dev,?"Error?requesting?GPIO\n");
??return?result;
?}
?drvled_data->desc?=?gpio_to_desc(gpio);
?drvled_data->led_cdev.name?=?of_get_property(child,?"label",?NULL);
?drvled_data->led_cdev.brightness_set?=?drvled_change_state;
?//?注冊進?LED?framework
?result?=?devm_led_classdev_register(&pdev->dev,?&drvled_data->led_cdev);
?if?(result)?{
??dev_err(&pdev->dev,?"Error?registering?led\n");
??return?result;
?}
?gpiod_direction_output(drvled_data->desc,?0);
?dev_info(&pdev->dev,?"initialized.\n");
?return?0;
}
static?int?drvled_remove(struct?platform_device?*pdev)
{
?dev_info(&pdev->dev,?"exiting.\n");
?return?0;
}
static?const?struct?of_device_id?of_drvled_match[]?=?{
?{?.compatible?=?"labworks,drvled"?},
?{},
};
static?struct?platform_driver?drvled_driver?=?{
?.driver?=?{
??.name?=?"drvleds",
??.owner?=?THIS_MODULE,
??.of_match_table?=?of_drvled_match,
?},
?.probe??=?drvled_probe,
?.remove??=?drvled_remove,
};
module_platform_driver(drvled_driver);
改造后,應用總是通過下面這種標準的接口訪問 LED:
#?燈亮
$?echo?1?>?/sys/class/leds//brightness
#?燈滅
$?echo?0?>?/sys/class/leds//brightness
并且有大量的 trigger 可供使用,例如讓 LED 呈心跳狀態的 heartbeat trigger:
$?echo?heartbeat?>?/sys/class/leds//trigger
假設這時你想改用 gpio expander 芯片來控制 LED:
只需要添加這個 gpio expander 的驅動代碼,并且修改設備樹即可,其他部分完全不需要改動:
gpioexp {
I2C0, 0x10
}
LED {
<&gpioexp 3>
}

到此,你是否更清楚如何為 Linux 添加設備驅動了呢?
—— The End ——
推薦閱讀:
專輯 | Linux 系統編程
專輯 | Linux 驅動開發
專輯?| Linux?內核品讀
專輯 | 每天一點 C
專輯 |?開源軟件
專輯 | Qt 入門