linux驅動等於單片機操作+linux各個子系統架構+作業系統,但是學習的時候要把重心放在架構和作業系統上面,這樣驅動設計得更好。我一直不滿足於在產品側當一個看代碼的bsp驅動工程師,有朝一日我想去大廠,去當晶片端的bsp驅動工程師,讓別人用我設計的驅動。因此,我真就只遵循套路來寫一些簡單的設備驅動就行了嗎?
重新學習字符設備的時候發現了第一次學習沒有思考的幾個問題:
總的來看當我們打開一個設備節點,系統發生了什麼?如何和我的驅動操作集關聯?想要一個驅動兼容同類的設備時候,次設備號扮演了什麼角色?如何去設置cdev?cdev需要多少個?以上問題並不能直接解答,先看看我們是怎麼把一個字符設備以及他的操作集加到系統中的。
設備號的管理眾所周知,設備驅動中有個很重要的概念叫設備號,一個32位設備號分為主設備號和次設備號,高20位是主設備號,次設備號的低12位。主設備號對應的是某個驅動程序.ko,次設備號標識這個驅動管理下的各種同類設備。
我使用alloc_chrdev_region函數來分配和註冊設備號
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
#第一個參數是你傳入的一個變量用於保存系統分配的設備號
#第二、三個參數是以次設備號開始申請多少個次設備號
#第四個參數是管理這組設備的一個管理員名字上面說的管理員實際是結構體char_device_struct。
然後現在啥也別想char_device_struct是什麼了,全都拋棄。系統中有個管理設備號的數組:chardevs[255]。裡面每一個元素都是一個char_device_struct指針。
比如我這樣分配
alloc_chrdev_region(&imx6ull_led_beep.device_num, 0, 2, "led_beep");那麼對應的狀況就會是這樣的
為什麼major是1或者256?因為如果要找的話,是通過求模找的比如1%255等於1, 256%255也等於1。如果同時1和256主設備號同時存在,那麼也可以加到chrdevs[1]下面來管理,按照major大小從小到大鍊表排列。比如下面這樣
這個結論就是:設備號是通過chedevs數組管理的。但是這並沒有接近我思考的問題的核心。
字符設備cdev的管理字符設備驅動中最重要的就是cdev和它的操作集。那它是如何管理的。
涉及兩個函數:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)cdev_init就是綁定cdev和操作集,cdev_add就是加入到管理中來。
cdev的管理是由cdev_map指針來管理的,指向一個struct kobj_map
比如我向系統這樣增加
cdev_init(&imx6ull_led_beep.cdev, &imx6ull_led_beep_fops);
cdev_add(&imx6ull_led_beep.cdev, imx6ull_led_beep.device_num, 2);
或者
cdev_init(&imx6ull_led_beep.cdev, &imx6ull_led_beep_fops);
cdev_add(&imx6ull_led_beep.cdev, imx6ull_led_beep.device_num, 1);
cdev_add(&imx6ull_led_beep.cdev, imx6ull_led_beep.device_num+1, 1);
#以上兩種方法一樣的,為什麼呢?因為imx6ull_led_beep.device_num
#和imx6ull_led_beep.device_num+1都是同一個major對應的狀況是這樣:
因此可以得出結論:一個驅動(主設備號)下,只設置一個cdev也能夠管理多個同類設備,只要你認為同類設備都能用同一個操作集。但是注意cdev註冊和設備號管理並沒有關係!
注意:cdev_map同樣是根據求模取得位置的,和上面chrdevs一樣,但是這個不要太關注也行,因為我們很少能用完255個major,別給自己徒增煩惱。
設備節點生成設備節點就是一個文件,牢牢記住和inode是僅僅相依的。我也不用mknod那套了,mknod相當於是應用程式幫你生成一個設備節點,但是我希望我的驅動加載後自動生成設備節點。
涉及兩個函數:
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
這樣用
class = class_create(THIS_MODULE, "led_beep1");//出現在sys/class
device = device_create(class , \ //出現/dev/imx6ull_led
NULL, imx6ull_led_beep.device_num, NULL, "imx6ull_led");
imx6ull_led_beep.beep_device = device_create(class , \
NULL, imx6ull_led_beep.device_num+1, NULL, "imx6ull_beep");因為是同類設備啊,所以創造一個類唄。
上面就是管理了兩個同類設備,可以看到參數有設備號,所以當device_create完成後,設備節點對應的inode→i_rdev就保存了設備號。這個很重要,這意味著/dev下已經有兩個設備節點並且擁有了設備號信息。
設備節點和驅動程序關聯流程首先我們在驅動程序中申請設備號,用申請到的設備號使用device_create創造設備節點。如果申請的是有多個minor的多個同類設備的,那麼註冊的時候記得也要註冊對應的設備號。這裡能看到的是申請設備號和設備節點文件直接的聯繫。使用cdev_add給我們的cdev註冊進系統(cdev_map),這裡要提供起始設備號以及想註冊的同類子設備個數,因此我們驅動寫的cdev可以只有一個,但是設備號個數要和你申請的一致。這裡可以看到的是一個驅動程序完全可以對應一個cdev,前提是你認為這個cdev的操作集能供你這些同類設備公用。而不需要註冊那麼多cdev。這也是本篇文章我思考的最重要的一個點。應用open的時候,會根據設備節點文件打的inode→i_rdev在cdev_map查找對應的cdev,找到後,就將cdev賦值給inode→i_cdev,以後就能直接找打cdev而不通過cdev_map。同時會分配file結構體,讓file拿到cdev的操作集。因此在進程中我們open一個設備節點會返回一個fd,fd就是file的索引,有了fd就能拿到file,有了file就能拿到cdev的操作集,如圖所示:4.後續read、write就能通過fd去調到驅動中的read、write
總結弄清楚這些機制並不是很重要,甚至當我弄清楚之後,覺得用處沒有那麼大。但是有一點點作用就是我知道我編寫的驅動每一步的意義在於什麼,而不是按照市面上的教材套路來。這裡最重要的是搞清楚了一個驅動程序對應多少個cdev的問題!