Linux设备驱动开发入门之——hello驱动
1. Linux驅動程序的分類
Linux 中主要分為三大類驅動:字符設備驅動、塊設備驅動和網絡設備驅動。
1、字符設備驅動:因為軟件操作設備是是以字節為單位進行的,是按照字節流進行讀寫操作的一種設備。典型的如LCD、蜂鳴器、SPI、觸摸屏等驅動,都屬于字符設備驅動的范疇。大部分的驅動程序都是屬于字符設備驅動。
2、塊設備驅動:塊設備驅動是相對于字符設備驅動而定義的,因為塊設備被軟件操作時,是以塊為單位進行操作的(塊指的是多個字節組成一個塊)。塊設備大多指的都是各種存儲類類設備,比如EMMC、SD卡、NANDFlash、U盤等等。
3、網絡設備驅動:專門針對網絡設備而設計的一種驅動,不管是有線還是無線網絡,都屬于網絡設備驅動。
另外,一個設備可以屬于多種設備驅動類型,比如 USB WIFI設備,其使用 USB 接口,所以屬于字符設備,但是其又能上網,所以也屬于網絡設備驅動。
2. 與Linux驅動開發相關的介紹
1、Linux下的應用程序是如何調用驅動程序的
應用程序在使用C庫函數所提供的 open/read/write 等等函數時,最終會進入到內核里面,調用內核所提供的 sys_open/sys_read/sys_write 等等函數。而此時如果內核發現應用程序需要訪問的是驅動的話,那么就會調用該驅動程序所提供的 drv_open/drv_read/drv_write 等函數;如果發現應用程序訪問的不過是普通文件的話,那么內核就會調用訪問普通文件的那套函數。下圖形象的給出了調用關系:
驅動程序實際上起到承上啟下的作用,上承應用程序,對下則實現了具體的硬件操作。
2、Linux驅動程序的兩種運行方式
- 可以把驅動程序編譯進內核里面,這樣內核啟動后就會自動運行驅動程序了;
- 將驅動程序編譯成以.ko為后綴模塊文件,然后在Linux啟動后,我們自己手動安裝驅動程序。一般來說,這種方式在開發驅動階段常用。
3、Linux驅動開發中常用的幾個命令
- insmod(install module):用于安裝以Linux的驅動模塊
- rmmod(remove module):卸載驅動模塊
- lsmod(list module):打印出當前內核中已經安裝的模塊
- modinfo(module information):打印出某個 xxx.ko 文件的模塊信息。用法:modinfo xxx.ko
3. Linux驅動開發需要準備的工作
1、已經安裝好的交叉編譯工具鏈
? 我們開發的驅動程序是要運行在ARM架構上的,所以需要準備好ARM架構的編譯工具鏈。一般來說我們使用開發板廠商,或者SoC原廠提供交叉編譯工具鏈即可。具體如何安裝交叉編譯工具鏈這里不多啰嗦了。
2、準備已經配置和編譯好你對應板子的內核源碼
? 驅動程序是運行在內核空間的,不同于應用程序運行在用戶空間。驅動程序已經是屬于內核的一部分了,而編譯驅動程序需要借助于內核源碼來編譯。
? 另外,內核源碼的版本一定要和你板子上實際運行的版本相一致,否則編譯出來的驅動程序會因為版本不同而無法在你的板子上運行。
3、你的開發板接線正常,網絡正常(要保證開發板和ubuntu之間可以相互ping通,因為我們通過nfs方式把ubuntu編譯好的 xxx.ko 等文件傳輸到開發板)。
4. show出你的代碼
4.1 hello驅動的編寫
我們在編寫應用程序的時候,首先也是先學會如何再電腦屏幕上輸出 “helllo world”。同樣的,我們編寫的第一個驅動程序,也是先學會hello驅動,該驅動不涉及任何的硬件操作,而且也是屬于字符設備驅動的范疇。主要實現的功能是:
1、應用程序調用 open、read、write 等函數時,對應的驅動函數都打印出內核信息;
2、應用程序調用 write 函數時,傳入的數據保存在驅動中;
3、應用程序調用 read 函數時,把驅動中保存的數據再返回給應用程序,并打印出來
驅動程序的編寫其實也是有跡可循的,主要的編寫步驟如下:
其中,驅動程序核心中的核心就是 file_operations 這個結構體了。在這個結構體里面,就是要實現這個驅動程序自己的 open、read、write等函數,并通過Linux內核提供的接口注冊到內核里面去。而其他的一些步驟都是為了遵循LInux驅動程序的編寫規范,用于完善這個驅動程序的。
驅動代碼的編寫也不用完全都自己寫,我們可以參考Linux內核提供的一些已有的驅動程序,下面我們就參考內核的一份 misc 驅動,編寫我們自己的hello驅動程序,hello驅動代碼如下:
#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. 確定主設備號 */ static int major = 0; static char kernel_buf[1024]; static struct class *hello_class;#define MIN(a, b) (a < b ? a : b)/* 3. 實現對應的open/read/write等函數,填入file_operations結構體 *//** @description : 從設備讀取數據* @param - file : 內核中的文件描述符* @param - buf : 要存儲讀取的數據緩沖區(就是用戶空間的內存地址)* @param - size : 要讀取的長度* @param - offset : 相對于文件首地址的偏移量(一般讀取信息后,指針都會偏移讀取信息的長度)* @return : 返回讀取的字節數,如果讀取失敗則返回-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 : 向設備寫數據* @param - file : 內核中的文件描述符* @param - buf : 要寫給設備驅動的數據緩沖區* @param - size : 要寫入的長度* @param - offset : 相對于文件首地址的偏移量* @return : 返回寫入的字節數,如果寫入失敗則返回-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 : 打開設備* @param - node : 設備節點* @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 : 關閉設備* @param - node : 設備節點* @param - file : 文件描述符* @return : 關閉成功返回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結構體 */ 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結構體告訴內核:注冊驅動程序 */ /* 5. 誰來注冊驅動程序啊?得有一個入口函數:安裝驅動程序時,就會去調用這個入口函數 */ 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. 其他完善:提供設備信息,自動創建設備節點 */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. 有入口函數就應該有出口函數:卸載驅動程序時,就會去調用這個出口函數 */ 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"); }/* 指定驅動的入口和出口,以及聲明自己的驅動遵循GPL協議(不聲明的話無法把驅動加載進內核) */ module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");hello驅動程序的幾點說明:
驅動程序要打印信息的時候,調用的函數是 printk ,而應用程序調用的是 printf
應用程序和驅動程序之間傳遞數據,不能使用簡單的賦值或者memcpy等,要使用內核提供的 copy_from_user/copy_to_user 函數。當然如果需要傳遞大量數據的時候,還可以使用內存映射的方式
閱讀一個驅動程序,首先要找到驅動程序的入口函數。上面的驅動入口函數就是hello_init函數,該函數做的事情就是向內核注冊了一個 file_oprations 結構體,并且完成自動創建設備節點相關的代碼
file_oprations 結構體是驅動程序的核心,里面提供了本驅動 open/read/write/release 等成員,當應用程序調用了open/read/write/release 等函數時,就會導致對應驅動的這些成員函數被調用
4.2 編寫hello應用程序測試
下面我們編寫一個hello應用程序來測試我們編寫好的驅動程序。該應用程序要實現的功能是:
hello_drv_test 應用程序代碼如下:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h>/* 該應用程序用法:* ./hello_drv_test -w abc 向hello驅動寫入字符串 abc * ./hello_drv_test -r 讀取驅動程序中保存的數據*/ int main(int argc, char **argv) {int fd;char buf[1024];int len;/* 1. 判斷參數 */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 驅動程序的Makefile文件
一個驅動程序最簡單的Makefile包含以下內容:
# 1. 使用不同的開發板內核時, 一定要修改KERN_DIR # 2. KERN_DIR中的內核要事先配置、編譯, 為了能編譯內核, 要先設置下列環境變量: # 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 # 注意: 不同的開發板不同的編譯器上述3個環境變量不一定相同,根據具體情況指定# KERN_DIR 是指定內核源碼的路徑的,需要根據不同開發環境指定 KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.4 all:make -C $(KERN_DIR) M=`pwd` modules # 本條指令是用于編譯應用程序的,放在這里是為了不用在單獨編譯應用程序而已$(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 指定具體要編譯的驅動程序5. 在開發板上測試運行hello驅動
編寫完上述的代碼之后,就可以進行編譯了。
直接在hello驅動所在的目錄下,輸入 make 即可編譯了。編譯完之后就會看到生成對應的 .ko 文件了。
然后我們把編譯好的 .ko 文件,和測試驅動的應用程序都拷到 nfs 共享目錄下,我的 nfs 共享目錄是在 /home/lbh/nfs 下。
然后我們打開開發板后,進入到開發板的控制臺。掛載 ubuntu 中的 nfs 共享目錄到開發板的 /mnt 目錄下,在開發板輸入如下命令:
mount -t nfs -o nolock,nfsvers=3 192.168.1.33:/home/lbh/nfs /mnt其中 192.168.1.33 這個IP地址是你的ubuntu的IP地址。
掛載完成之后,就可以去 /mnt 目錄下看到了自己編譯好的 .ko 文件和對應的應用程序文件了。
我們執行 insmod hello_drv.ko 命令,就可以把該驅動程序安裝到內核中了。而且可以看到內核打印出了相應的信息,如下:
[ 293.594910] hello_drv: loading out-of-tree module taints kernel. [ 293.616051] /home/lbh/linux/drv/hello_drv.c hello_init line 70說明驅動加載成功了。
注意:如果板子沒有看到打印信息的話,那么就輸入如下命令把內核的打印信息打開:
echo "7 4 1 7" > /proc/sys/kernel/printk當然有些板子內核打印信息是默認已經打開了的。或者我們輸入 demsg 命令也可以看到內核的打印信息。
然后我們運行 hello_drv_test 應用程序來測試內核,都可以看到內核的打印信息,和我們讀取到應用程序寫給內核的字符串。
6. 和驅動調試有關的其他知識
總結
以上是生活随笔為你收集整理的Linux设备驱动开发入门之——hello驱动的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 人工智能 一种现代方法 第6章 约束满足
- 下一篇: WinRAR 曝出代码执行漏洞,你的官方