linux驅動開發一般步驟(以S5PV210開發板爲例)

1、驅動的價值就在於實現API

2、驅動是內核的一部分

3、驅動開發的步驟
1)驅動源碼的編寫、Makefile編寫、編譯
2)insmod裝載模塊、用應用程序測試、rmmod卸載模塊

4、在linux中用find來查找某個文件所在的路徑
find . -name ‘x210ii_qt_defconfig’ //在當前文件夾下面查找x210ii_qt_defconfig的路徑
一般的xxx_defconfig文件在arch/arm/config/文件夾下

5、常用的模塊操作指令
lsmod 將當前內核中已經安裝的模塊打印出來
insmod 向當前內核中去安裝一個模塊,用法是insmod xxx.ko
modinfo 打印出一個模塊的自帶信息,用法是modinfo xxx.ko
rmmod 從當前內核中卸載一個模塊,用法是rmmod xxx(注意,不能加.ko)

6、__init(下載模塊使用)是函數的修飾符,本質上是一個宏定義,在內核源代碼中就有#define __init xxx。這個__init的作用就是
將被他修飾的函數放入到.init.text段中去(本來默認情況下函數是被放到.text段中)。整個內核中的所有的這類
函數都會被鏈接器放入到.init.text段中,所以所有的內核模塊的__init修飾的函數其實是被統一放在一起的。內核
啓動時統一加載.init.text段中的這些模塊安裝函數,加載完後就會把這個段給釋放掉以節省內存。
__exit(卸載模塊使用),原理和__init相似。

7、驅動源代碼中包含的頭文件是內核源碼目錄下的include目錄下的頭文件。

8、要保證模塊的vermagic(版本信息)和內核的vermagic的一致,否則模塊不能掛接到內核中去。
如何保證兩者相等呢:需要編譯模塊的內核就是將來我們要掛載的內核。

9、file_operations結構體(裏面的元素都是函數指針)的作用就是,做API和驅動之間的橋樑。
API中的:open、read、write、close等函數接口,其函數的實體在驅動源碼中。file_operations結構體
就是充當實體和接口之間的連接橋樑。
每個驅動設備都需要一個該結構體類型的變量。
在使用file_operations結構體的時候一定要包含頭文件<linux/fs.h>

10、register_chrdev是內核開發者所編寫的註冊函數。

11、驅動通過register_chrdev函數向內核註冊自己的file_operations結構體。
register_chrdev函數的詳解:
函數體:
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
參數解析:
static 用來防止和其它文件衝突
inline 用來提高效率
major 主設備號,傳0進去表示讓內核給我們自動分配一個設備號(也可以自己指定)
name 設備的名字
fops 把我們自己的file_operations結構體的變量,通過指針傳給這個函數,然後這個函數向內核註冊這個驅動。
256 的意思是最多裝載256個設備
成功返回0,失敗返回一個負整數

12、unregister_chrdev函數與上面的註冊函數相對應,用來卸載。
原型:
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}

13、cat /proc/devices查看內核中已經註冊過的字符設備驅動(和塊設備驅動)

14、close在file_operations中的原型是release。

15、驅動的編寫順序大致爲:
1)file_operations結構體中,函數指針元素所對應的各個函數實體的編寫
2)file_operations結構體的元素填充
3)安裝模塊chrdev_init函數的編寫
4)卸載模塊chrdev_exit函數的編寫

16、在file_operations結構體中,第一句.owner = THIS_MODULE,是慣例,直接寫就行。

17、使用mknod /dev/xxx(xxx爲設備文件名) c 主設備號 從設備號(沒有的話爲0)創建設備文件;
創建設備文件的目的是:以後就是通過設備文件來進行應用程序控制調用驅動的。

18、可以用dmesg查看所有被ubuntu攔截的所有打印信息。

19、應用層調用誰,誰在驅動層對應的實體函數纔會被執行。

20、copy_from_user()函數:用來將數據從用戶空間複製到內核空間(在應用層對應於write函數,也就是說在應用層
向內核寫數據,應用層方面用的是write函數,在驅動層(內核層)方面用的是copy_from_user()函數)。
函數原型:
static inline unsigned long__must_check copy_from_user(void *to, const void__user *from, unsigned long n)
{

}
to =>kernel
from =>user
n =>多少個
成功返回0,失敗返回尚未成功複製剩下的字節數。
不能使用memcpy()函數進行復制,因爲二者不在同一地址空間。

22、copy_to_user()函數:將數據從內核空間複製到用戶空間(在應用層對應於read函數…)
函數原型:
static inline unsigned long__must_check copy_to_user(void__user *to, const void *from, unsigned long n)
{

}
to =>user
from =>kernel
n =>多少個

23、寫驅動用的是虛擬地址。內核中一般用封裝號的IO讀寫函數來操作寄存器。

24、因爲編寫驅動程序需要用到虛擬地址,並且多個虛擬地址可以對應於同一個物理地址,所以這裏
就需要虛擬地址映射的方法。
兩種映射方法:
1)靜態映射
內核移植的時候用代碼的形式硬編碼(直接寫死),如果需要更改必須改源代碼之後重新編譯內核,在
內核啓動時建立映射表,到內核關機時銷燬。類似於C語言中的全局變量
好處是執行效率高,壞處是始終佔用虛擬地址空間。
2)動態映射
驅動程序根據需要隨時動態的建立映射、使用、銷燬,是臨時的。類似於C語言中malloc堆內存
好處是俺需使用虛擬地址空間,壞處是每次使用前後都需要用代碼進行建立和銷燬。
兩種方法是可以同時使用的。

25、不同版本的內核、SOC的靜態映射表的位置、文件名可能不同,所謂的映射表其實就是頭文件中的宏定義。

26、GPIO相關的主映射表位於arch/arm/mach-s5pv210/include/mach/regs-gpio.h
GPIO的具體寄存器定義在arch/arm/mach-s5pv210/include/mach/gpio-bank.h
所以在編寫程序的時候需要包含mach/regs-gpio.h和mach/gpio-bank.h

27、在應用程序裏面寫控制硬件的開關時,開關函數寫到應用層裏面的哪個函數裏面,對應的就把
硬件的控制函數寫到對應的驅動層函數裏面。
例子:
在應用層裏面我把控制LED亮的開關(on、off)寫到write函數裏面,那麼在驅動層方面就把控制LED
的寄存器控制操作寫到chrdev_write函數裏面。

28、用動態映射方式,只需要在驅動層安裝模塊的函數裏面建立動態映射,在卸載模塊的函數裏面銷燬動態映射。
建立動態映射兩步:
1)利用resuest_mem_region(start, n, name);申請IO內存,成功返回0
2)利用ioremap(start, n);實現映射,成功返回值是對應的虛擬地址
三個參數的含義:start是起始的物理地址,n是它的長度佔幾個字節,name就是爲這段內存起個名字。
銷燬動態映射兩步:
1)利用iounmap(ioremap函數的返回值);解除映射
2)利用release_mem_region(start,n);銷燬申請的內存

29、用新接口進行驅動設備的註冊
註冊分爲三步:
1)先註冊驅動設備號,
用register_chrdev_region()/alloc_chrdev_region()前者是指定設備號,後者是讓系統自動分配
register_chrdev_region(dev_t from, unsigned count, const char *name)
from的意思是主設備號,count的意思是指定有幾個次設備號,name爲設備的名字
一般from來自於MKDEV(主設備號,起始次設備號)函數的返回值。
如果用alloc_chrdev_region()的話就不用MKDEV()函數了。
2)再初始化驅動
利用cdev_init(struct cdev *cdev, const struct file_operations *fops)函數來初始化驅動
cdev是cdev結構體的變量(這個在包含<linux/cdev.h>的前提下,直接全局定義聲明就可以用,真正的實現函數在頭文件裏),
fops是file_operations結構體的變量。
3)最後註冊設備驅動
利用cdev_add(struct cdev *cdev, dev_t from, unsigned count);參數含義前面都有
註銷分爲兩步:
1)先註銷驅動
利用cdev_del(struct cdev *cdev);參數含義前面都有
2)再註銷驅動設備號
利用unregister_chrdev_region(dev_t from, unsigned count);參數含義前面都有
注:cdev結構體是頭文件<linux/cdev.h>中包含的結構體,該結構體裏面包含的兩個重要的元素是
file_operations結構體和dev_t 類型的設備號變量。

30、使用MAJOR()函數和MINJOR()函數從dev_t得到主設備號和次設備號;用MKDEV()函數從主設備號和次設備號得到dev_t。

31、cdev_alloc函數用來創造內存空間的,cdev_alloc()不需要參數,返回值是一個cdev結構體類型的指針變量
例子:
1)
static struct cdev *pcdev;
pcdev = cdev_alloc();
pcdev -> owner = THIS_MODULE;
pcdev -> ops = &test_fops;(test_fops是file_operations結構體的變量)
cdev_del(pcdev);
2)
static struct cdev *pcdev;
pcdev = cdev_alloc();
cdev_init(pcdev, &test_fops);
cdev_del(pcdev);

32、使用mdev應用程序,讓內核在在註冊和註銷驅動的時候,自動在應用層進行設備文件的創建和刪除(以前都是使用
mknod進行創建)。
mdev是應用層的一個應用程序,他的自動啓動時在我們根文件系統中/etc/init.d文件夾裏面的rcS文件裏面定義;
這個rcS文件是在關在根文件系統的時候自動執行的,也就是說在掛載完根文件系統之後,mdev應用程序就自動啓動了。

33、但是應用層啓動mdev之後還需要配合驅動層的接口函數才能實現設備文件的自動創建和刪除。
在驅動層的接口函數:
創建設備文件函數:(在註冊完設備之後使用)
class_create()
函數原型:
#define class_create(owner, name) ({static struct lock_class_key_key;
__class_creat(owner, name, &__key);})
device_create()
函數原型:
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, …)
{

}
刪除設備文件函數:(在卸載設備之前使用)
device_destroy()
函數原型:
void device_destroy(struct class *class, dev_t devt)
{

}
class_destroy()
函數原型:
void class_destroy(struct class *cls)
{

}
例子:
#include<linux/device.h>
#include<linux/cdev.h>

#define MYCNT 1 //次設備號的個數
#define MYNAME "teacher" //驅動設備名字
static dev_t mydev;
static struct cdev *pcdev;
static struct class *test_class;  //該類的實體在linux/device.h頭文件裏面

int ret;
//自動分配設備號
ret = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME); //mydev設備號,包含了主次設備號,12是次設備號的開始
//創建設備文件
test_class = class_create(THIS_MODULE, "aston_class"); //該函數是先創建一個類,THIS_MODULE是file_operations
                                                         //結構體中.owner的值,aston_class是類名
device_create(test_class, NULL, mydev, NULL, "test"); //test就是我們將來要在/dev目錄下創建的設備文件名
...
//刪除設備文件
device_destroy(test_class, mydev);
class_destroy(test_class);

34、內核提供的讀寫寄存器的接口:
writel(要往寄存器裏面寫入的數值,寄存器地址)
redal(寄存器地址)

35、內核開發者提供一些操作設備驅動的一些公共接口一般所在的文件時xxx_class.c,xxx_core.c;
驅動開發者調用這些接口去實現驅動的開發。

36、對於一個內核模塊來說,分析的時候應該從下向上分析。

37、subsys_initcall(func1)將func1函數放到.initcall4.init段
module_exit(func2)將func2函數放到.initcall6.init段
內核在啓動過程中需要順序的做很多事,內核如何實現按照先後順序去做很多初始化操作。內核的解決方案就是給
內核啓動時要調用的所有函數歸類,然後每個類按照一定的次序去調用執行。這些分類名就叫.initcalln.init,n
的值從0到7(0最先調用),內核開發者在編寫內核代碼時只要將函數設置合適的級別,這些函數就會被鏈接的時候
放入特定的段,內核啓動時按照段順序去依次執行各個段即可。

38、利用make menuconfig去添加led驅動框架模塊(devices->)。(前提是在根目錄的drivers目錄下有這個驅動框架)

39、已有的驅動框架都在/sys/class目錄下

40、與不用驅動框架的驅動程序來說,用驅動框架編寫的驅動程序,所自動創建的設備文件在/sys/class/xxx,xxx爲
驅動框架名。

41、cat brightness實際會執行led-class.c中的led_brightness_show函數,去讀取已經寫入的值。
echo 1 > brightness就會把1寫入到驅動中去,例如1就是讓led亮,那麼這個1就先寫入到led_brightness_stroe函數
中去,影響該函數中state的值;然後這個值在經過結構體led_cdev傳入到我們在led_cdev結構體中自己綁定的硬件
操作函數,去控制硬件。

42、內核中提供gpiolib(屬於驅動框架的一部分)來統一管控系統中的所有GPIO,處理GPIO的複用問題。

43、xxx_gpiolib_init()就是gpiolib的初始化函數,在這裏xxx是s5pv210。

44、一個端口可以包含多個IO口,例如:GPA0是個端口,GPA0_1、GPA0_2就是IO口。

45、內核開發者給我們提供的關於gpiolib的框架接口函數主要有(我們常用的)位置在源目錄下drivers/gpio/gpiolib.c:
gpio_request()向內核申請一個GPIO資源(端口/IO口)
gpio_direction_input()/gpio_direction_output()將申請到的gpio設置爲輸入或輸出模式
gpio_set_value()向gpio中寫值,控制硬件,達到目的
gpio_free()釋放申請的gpio資源

46、將編寫調試好的驅動添加到內核中。
操作步驟:
1)將寫好的驅動源文件放入內核源碼中的正確目錄下(例如我們編寫的LED驅動,就需要將這個驅動文件放到
源目錄下面的drivers/leds/目錄下面)
2)在Makefile中添加相應的依賴
3)在Kconfig中添加相應的配置項
4)make menuconfig

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章