Linux设备驱动开发入门之——hello驱动
1. Linux驅(qū)動程序的分類
Linux 中主要分為三大類驅(qū)動:字符設(shè)備驅(qū)動、塊設(shè)備驅(qū)動和網(wǎng)絡(luò)設(shè)備驅(qū)動。
1、字符設(shè)備驅(qū)動:因?yàn)檐浖僮髟O(shè)備是是以字節(jié)為單位進(jìn)行的,是按照字節(jié)流進(jìn)行讀寫操作的一種設(shè)備。典型的如LCD、蜂鳴器、SPI、觸摸屏等驅(qū)動,都屬于字符設(shè)備驅(qū)動的范疇。大部分的驅(qū)動程序都是屬于字符設(shè)備驅(qū)動。
2、塊設(shè)備驅(qū)動:塊設(shè)備驅(qū)動是相對于字符設(shè)備驅(qū)動而定義的,因?yàn)閴K設(shè)備被軟件操作時(shí),是以塊為單位進(jìn)行操作的(塊指的是多個(gè)字節(jié)組成一個(gè)塊)。塊設(shè)備大多指的都是各種存儲類類設(shè)備,比如EMMC、SD卡、NANDFlash、U盤等等。
3、網(wǎng)絡(luò)設(shè)備驅(qū)動:專門針對網(wǎng)絡(luò)設(shè)備而設(shè)計(jì)的一種驅(qū)動,不管是有線還是無線網(wǎng)絡(luò),都屬于網(wǎng)絡(luò)設(shè)備驅(qū)動。
另外,一個(gè)設(shè)備可以屬于多種設(shè)備驅(qū)動類型,比如 USB WIFI設(shè)備,其使用 USB 接口,所以屬于字符設(shè)備,但是其又能上網(wǎng),所以也屬于網(wǎng)絡(luò)設(shè)備驅(qū)動。
2. 與Linux驅(qū)動開發(fā)相關(guān)的介紹
1、Linux下的應(yīng)用程序是如何調(diào)用驅(qū)動程序的
應(yīng)用程序在使用C庫函數(shù)所提供的 open/read/write 等等函數(shù)時(shí),最終會進(jìn)入到內(nèi)核里面,調(diào)用內(nèi)核所提供的 sys_open/sys_read/sys_write 等等函數(shù)。而此時(shí)如果內(nèi)核發(fā)現(xiàn)應(yīng)用程序需要訪問的是驅(qū)動的話,那么就會調(diào)用該驅(qū)動程序所提供的 drv_open/drv_read/drv_write 等函數(shù);如果發(fā)現(xiàn)應(yīng)用程序訪問的不過是普通文件的話,那么內(nèi)核就會調(diào)用訪問普通文件的那套函數(shù)。下圖形象的給出了調(diào)用關(guān)系:
驅(qū)動程序?qū)嶋H上起到承上啟下的作用,上承應(yīng)用程序,對下則實(shí)現(xiàn)了具體的硬件操作。
2、Linux驅(qū)動程序的兩種運(yùn)行方式
- 可以把驅(qū)動程序編譯進(jìn)內(nèi)核里面,這樣內(nèi)核啟動后就會自動運(yùn)行驅(qū)動程序了;
- 將驅(qū)動程序編譯成以.ko為后綴模塊文件,然后在Linux啟動后,我們自己手動安裝驅(qū)動程序。一般來說,這種方式在開發(fā)驅(qū)動階段常用。
3、Linux驅(qū)動開發(fā)中常用的幾個(gè)命令
- insmod(install module):用于安裝以Linux的驅(qū)動模塊
- rmmod(remove module):卸載驅(qū)動模塊
- lsmod(list module):打印出當(dāng)前內(nèi)核中已經(jīng)安裝的模塊
- modinfo(module information):打印出某個(gè) xxx.ko 文件的模塊信息。用法:modinfo xxx.ko
3. Linux驅(qū)動開發(fā)需要準(zhǔn)備的工作
1、已經(jīng)安裝好的交叉編譯工具鏈
? 我們開發(fā)的驅(qū)動程序是要運(yùn)行在ARM架構(gòu)上的,所以需要準(zhǔn)備好ARM架構(gòu)的編譯工具鏈。一般來說我們使用開發(fā)板廠商,或者SoC原廠提供交叉編譯工具鏈即可。具體如何安裝交叉編譯工具鏈這里不多啰嗦了。
2、準(zhǔn)備已經(jīng)配置和編譯好你對應(yīng)板子的內(nèi)核源碼
? 驅(qū)動程序是運(yùn)行在內(nèi)核空間的,不同于應(yīng)用程序運(yùn)行在用戶空間。驅(qū)動程序已經(jīng)是屬于內(nèi)核的一部分了,而編譯驅(qū)動程序需要借助于內(nèi)核源碼來編譯。
? 另外,內(nèi)核源碼的版本一定要和你板子上實(shí)際運(yùn)行的版本相一致,否則編譯出來的驅(qū)動程序會因?yàn)榘姹静煌鵁o法在你的板子上運(yùn)行。
3、你的開發(fā)板接線正常,網(wǎng)絡(luò)正常(要保證開發(fā)板和ubuntu之間可以相互ping通,因?yàn)槲覀兺ㄟ^nfs方式把ubuntu編譯好的 xxx.ko 等文件傳輸?shù)介_發(fā)板)。
4. show出你的代碼
4.1 hello驅(qū)動的編寫
我們在編寫應(yīng)用程序的時(shí)候,首先也是先學(xué)會如何再電腦屏幕上輸出 “helllo world”。同樣的,我們編寫的第一個(gè)驅(qū)動程序,也是先學(xué)會hello驅(qū)動,該驅(qū)動不涉及任何的硬件操作,而且也是屬于字符設(shè)備驅(qū)動的范疇。主要實(shí)現(xiàn)的功能是:
1、應(yīng)用程序調(diào)用 open、read、write 等函數(shù)時(shí),對應(yīng)的驅(qū)動函數(shù)都打印出內(nèi)核信息;
2、應(yīng)用程序調(diào)用 write 函數(shù)時(shí),傳入的數(shù)據(jù)保存在驅(qū)動中;
3、應(yīng)用程序調(diào)用 read 函數(shù)時(shí),把驅(qū)動中保存的數(shù)據(jù)再返回給應(yīng)用程序,并打印出來
驅(qū)動程序的編寫其實(shí)也是有跡可循的,主要的編寫步驟如下:
其中,驅(qū)動程序核心中的核心就是 file_operations 這個(gè)結(jié)構(gòu)體了。在這個(gè)結(jié)構(gòu)體里面,就是要實(shí)現(xiàn)這個(gè)驅(qū)動程序自己的 open、read、write等函數(shù),并通過Linux內(nèi)核提供的接口注冊到內(nèi)核里面去。而其他的一些步驟都是為了遵循LInux驅(qū)動程序的編寫規(guī)范,用于完善這個(gè)驅(qū)動程序的。
驅(qū)動代碼的編寫也不用完全都自己寫,我們可以參考Linux內(nèi)核提供的一些已有的驅(qū)動程序,下面我們就參考內(nèi)核的一份 misc 驅(qū)動,編寫我們自己的hello驅(qū)動程序,hello驅(qū)動代碼如下:
#include <linux/module.h>#include <linux/fs.h> #include <linux/errno.h> #include <linux/miscdevice.h> #include <linux/kernel.h> #include <linux/major.h> #include <linux/mutex.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> #include <linux/stat.h> #include <linux/init.h> #include <linux/device.h> #include <linux/tty.h> #include <linux/kmod.h> #include <linux/gfp.h>/* 1. 確定主設(shè)備號 */ static int major = 0; static char kernel_buf[1024]; static struct class *hello_class;#define MIN(a, b) (a < b ? a : b)/* 3. 實(shí)現(xiàn)對應(yīng)的open/read/write等函數(shù),填入file_operations結(jié)構(gòu)體 *//** @description : 從設(shè)備讀取數(shù)據(jù)* @param - file : 內(nèi)核中的文件描述符* @param - buf : 要存儲讀取的數(shù)據(jù)緩沖區(qū)(就是用戶空間的內(nèi)存地址)* @param - size : 要讀取的長度* @param - offset : 相對于文件首地址的偏移量(一般讀取信息后,指針都會偏移讀取信息的長度)* @return : 返回讀取的字節(jié)數(shù),如果讀取失敗則返回-1*/ static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset) {int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = copy_to_user(buf, kernel_buf, MIN(1024, size));return MIN(1024, size); }/** @description : 向設(shè)備寫數(shù)據(jù)* @param - file : 內(nèi)核中的文件描述符* @param - buf : 要寫給設(shè)備驅(qū)動的數(shù)據(jù)緩沖區(qū)* @param - size : 要寫入的長度* @param - offset : 相對于文件首地址的偏移量* @return : 返回寫入的字節(jié)數(shù),如果寫入失敗則返回-1*/ static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) {int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = copy_from_user(kernel_buf, buf, MIN(1024, size));return MIN(1024, size); }/** @description : 打開設(shè)備* @param - node : 設(shè)備節(jié)點(diǎn)* @param - file : 文件描述符* @return : 打開成功返回0,失敗返回-1*/ static int hello_drv_open (struct inode *node, struct file *file) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0; }/** @description : 關(guān)閉設(shè)備* @param - node : 設(shè)備節(jié)點(diǎn)* @param - file : 文件描述符* @return : 關(guān)閉成功返回0,失敗返回-1*/ static int hello_drv_close (struct inode *node, struct file *file) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0; }/* 2. 定義自己的file_operations結(jié)構(gòu)體 */ static struct file_operations hello_drv = {.owner = THIS_MODULE,.open = hello_drv_open,.read = hello_drv_read,.write = hello_drv_write,.release = hello_drv_close, };/* 4. 把file_operations結(jié)構(gòu)體告訴內(nèi)核:注冊驅(qū)動程序 */ /* 5. 誰來注冊驅(qū)動程序啊?得有一個(gè)入口函數(shù):安裝驅(qū)動程序時(shí),就會去調(diào)用這個(gè)入口函數(shù) */ static int __init hello_init(void) {int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);major = register_chrdev(0, "hello", &hello_drv); /* /dev/hello *//* 7. 其他完善:提供設(shè)備信息,自動創(chuàng)建設(shè)備節(jié)點(diǎn) */hello_class = class_create(THIS_MODULE, "hello_class");err = PTR_ERR(hello_class);if (IS_ERR(hello_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "hello");return -1;} device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */return 0; }/* 6. 有入口函數(shù)就應(yīng)該有出口函數(shù):卸載驅(qū)動程序時(shí),就會去調(diào)用這個(gè)出口函數(shù) */ static void __exit hello_exit(void) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);device_destroy(hello_class, MKDEV(major, 0));class_destroy(hello_class);unregister_chrdev(major, "hello"); }/* 指定驅(qū)動的入口和出口,以及聲明自己的驅(qū)動遵循GPL協(xié)議(不聲明的話無法把驅(qū)動加載進(jìn)內(nèi)核) */ module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");hello驅(qū)動程序的幾點(diǎn)說明:
驅(qū)動程序要打印信息的時(shí)候,調(diào)用的函數(shù)是 printk ,而應(yīng)用程序調(diào)用的是 printf
應(yīng)用程序和驅(qū)動程序之間傳遞數(shù)據(jù),不能使用簡單的賦值或者memcpy等,要使用內(nèi)核提供的 copy_from_user/copy_to_user 函數(shù)。當(dāng)然如果需要傳遞大量數(shù)據(jù)的時(shí)候,還可以使用內(nèi)存映射的方式
閱讀一個(gè)驅(qū)動程序,首先要找到驅(qū)動程序的入口函數(shù)。上面的驅(qū)動入口函數(shù)就是hello_init函數(shù),該函數(shù)做的事情就是向內(nèi)核注冊了一個(gè) file_oprations 結(jié)構(gòu)體,并且完成自動創(chuàng)建設(shè)備節(jié)點(diǎn)相關(guān)的代碼
file_oprations 結(jié)構(gòu)體是驅(qū)動程序的核心,里面提供了本驅(qū)動 open/read/write/release 等成員,當(dāng)應(yīng)用程序調(diào)用了open/read/write/release 等函數(shù)時(shí),就會導(dǎo)致對應(yīng)驅(qū)動的這些成員函數(shù)被調(diào)用
4.2 編寫hello應(yīng)用程序測試
下面我們編寫一個(gè)hello應(yīng)用程序來測試我們編寫好的驅(qū)動程序。該應(yīng)用程序要實(shí)現(xiàn)的功能是:
hello_drv_test 應(yīng)用程序代碼如下:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h>/* 該應(yīng)用程序用法:* ./hello_drv_test -w abc 向hello驅(qū)動寫入字符串 abc * ./hello_drv_test -r 讀取驅(qū)動程序中保存的數(shù)據(jù)*/ int main(int argc, char **argv) {int fd;char buf[1024];int len;/* 1. 判斷參數(shù) */if (argc < 2) {printf("Usage: %s -w <string>\n", argv[0]);printf(" %s -r\n", argv[0]);return -1;}/* 2. 打開文件 */fd = open("/dev/hello", O_RDWR);if (fd == -1){printf("can not open file /dev/hello\n");return -1;}/* 3. 寫文件或讀文件 */if ((0 == strcmp(argv[1], "-w")) && (argc == 3)){len = strlen(argv[2]) + 1;len = len < 1024 ? len : 1024;write(fd, argv[2], len);}else{len = read(fd, buf, 1024); buf[1023] = '\0';printf("APP read : %s\n", buf);}close(fd);return 0; }4.3 驅(qū)動程序的Makefile文件
一個(gè)驅(qū)動程序最簡單的Makefile包含以下內(nèi)容:
# 1. 使用不同的開發(fā)板內(nèi)核時(shí), 一定要修改KERN_DIR # 2. KERN_DIR中的內(nèi)核要事先配置、編譯, 為了能編譯內(nèi)核, 要先設(shè)置下列環(huán)境變量: # 2.1 ARCH, 比如: export ARCH=arm64 # 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu- # 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin # 注意: 不同的開發(fā)板不同的編譯器上述3個(gè)環(huán)境變量不一定相同,根據(jù)具體情況指定# KERN_DIR 是指定內(nèi)核源碼的路徑的,需要根據(jù)不同開發(fā)環(huán)境指定 KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.4 all:make -C $(KERN_DIR) M=`pwd` modules # 本條指令是用于編譯應(yīng)用程序的,放在這里是為了不用在單獨(dú)編譯應(yīng)用程序而已$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c clean:make -C $(KERN_DIR) M=`pwd` modules cleanrm -rf modules.orderrm -f hello_drv_testobj-m += hello_drv.o # obj-m 指定具體要編譯的驅(qū)動程序5. 在開發(fā)板上測試運(yùn)行hello驅(qū)動
編寫完上述的代碼之后,就可以進(jìn)行編譯了。
直接在hello驅(qū)動所在的目錄下,輸入 make 即可編譯了。編譯完之后就會看到生成對應(yīng)的 .ko 文件了。
然后我們把編譯好的 .ko 文件,和測試驅(qū)動的應(yīng)用程序都拷到 nfs 共享目錄下,我的 nfs 共享目錄是在 /home/lbh/nfs 下。
然后我們打開開發(fā)板后,進(jìn)入到開發(fā)板的控制臺。掛載 ubuntu 中的 nfs 共享目錄到開發(fā)板的 /mnt 目錄下,在開發(fā)板輸入如下命令:
mount -t nfs -o nolock,nfsvers=3 192.168.1.33:/home/lbh/nfs /mnt其中 192.168.1.33 這個(gè)IP地址是你的ubuntu的IP地址。
掛載完成之后,就可以去 /mnt 目錄下看到了自己編譯好的 .ko 文件和對應(yīng)的應(yīng)用程序文件了。
我們執(zhí)行 insmod hello_drv.ko 命令,就可以把該驅(qū)動程序安裝到內(nèi)核中了。而且可以看到內(nèi)核打印出了相應(yīng)的信息,如下:
[ 293.594910] hello_drv: loading out-of-tree module taints kernel. [ 293.616051] /home/lbh/linux/drv/hello_drv.c hello_init line 70說明驅(qū)動加載成功了。
注意:如果板子沒有看到打印信息的話,那么就輸入如下命令把內(nèi)核的打印信息打開:
echo "7 4 1 7" > /proc/sys/kernel/printk當(dāng)然有些板子內(nèi)核打印信息是默認(rèn)已經(jīng)打開了的。或者我們輸入 demsg 命令也可以看到內(nèi)核的打印信息。
然后我們運(yùn)行 hello_drv_test 應(yīng)用程序來測試內(nèi)核,都可以看到內(nèi)核的打印信息,和我們讀取到應(yīng)用程序?qū)懡o內(nèi)核的字符串。
6. 和驅(qū)動調(diào)試有關(guān)的其他知識
總結(jié)
以上是生活随笔為你收集整理的Linux设备驱动开发入门之——hello驱动的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 人工智能 一种现代方法 第6章 约束满足
- 下一篇: Linux驱动开发流程