?前言:
本文是根據嗶哩嗶哩網站上“正點原子【第四期】手把手教你學Linux系列課程之 Linux驅動開發篇”視頻的學習筆記,該課程配套開發板為正點原子alpha/mini Linux開發板。在這里會記錄下正點原子 I.MX6ULL 開發板的配套視頻教程所作的實驗和學習筆記內容。本文大量引用了正點原子教學視頻和鏈接中的內容。
引用:
正點原子IMX6U倉庫 (GuangzhouXingyi) - Gitee.com
正點原子【第四期】手把手教你學 Linux之驅動開發篇_嗶哩嗶哩_bilibili
《【正點原子】I.MX6U嵌入式Linux驅動開發指南V1.5.2.pdf》
正點原子資料下載中心 — 正點原子資料下載中心 1.0.0 文檔
正點原子imx6ull-mini-Linux驅動之Linux I2C 驅動實驗(21)-CSDN博客
uboot移植(4)--在NXP官方uboot適配ALPHA開發板網絡_uboot sr8201f-CSDN博客
正文:
本文是 “正點原子【第四期】手把手教你學 Linux之驅動開發篇-1.1 Linux驅動開發與裸機開發的區別”。本節將參考正點原子的視頻教程和配套的正點原子開發指南文檔進行學習。
?
0. 概述
前面 3 篇,我們學習 Ubuntu 操作系統、學習 ARM 裸機、學習系統移植,其目的就是為了本篇做準備。本篇應該是大家最期待的內容了,畢竟大部分學習者的最終目的就是學習 Linux驅動開發。本篇我們將會詳細講解 Linux 中的三大類驅動:字符設備驅動、塊設備驅動和網絡設備驅動。其中字符設備驅動是占用篇幅最大的一類驅動,因為字符設備最多,從最簡單的點燈到 I2C、 SPI、音頻等都屬于字符設備驅動的類型。塊設備和網絡設備驅動要比字符設備驅動復雜,就是因為其復雜所以半導體廠商一般都給我們編寫好了,大多數情況下都是直接可以使用的。所謂的塊設備驅動就是存儲器設備的驅動,比如 EMMC、 NAND、 SD 卡和 U 盤等存儲設備,因為這些存儲設備的特點是以存儲塊為基礎,因此叫做塊設備。網絡設備驅動就更好理解了,就是網絡驅動,不管是有線的還是無線的,都屬于網絡設備驅動的范疇。一個設備可以屬于多種設備驅動類型,比如 USB WIFI,其使用 USB 接口,所以屬于字符設備,但是其又能上網,所以也屬于網絡設備驅動。本篇我們就圍繞著三大設備驅動類型展開,盡可能詳細的講解每種設備驅動的開發方式。
本書使用的 Linux 內核版本為 4.1.15,其支持設備樹(Device tree),所以本篇所有例程均采用設備樹。設備樹將是本篇的重點!從設備樹的基本原理到設備樹驅動的開發方式,從最簡單的點燈到復雜的網絡驅動開發,本篇均有詳細的講解,是學習設備樹的不二之選。
1.字符設備驅動開發
本章我們從 Linux 驅動開發中最基礎的字符設備驅動開始,重點學習 Linux 下字符設備驅動開發框架。本章會以一個虛擬的設備為例,講解如何進行字符設備驅動開發,以及如何編寫測試 APP 來測試驅動工作是否正常,為以后的學習打下堅實的基礎。
1.1 字符設備驅動簡介
字符設備是 Linux 驅動中最基本的一類設備驅動,字符設備就是一個一個字節,按照字節流進行讀寫操作的設備,讀寫數據是分先后順序的。比如我們最常見的點燈、按鍵、 IIC、 SPI,LCD 等等都是字符設備,這些設備的驅動就叫做字符設備驅動。
在詳細的學習字符設備驅動架構之前,我們先來簡單的了解一下 Linux 下的應用程序是如何調用驅動程序的, Linux 應用程序對驅動程序的調用如圖 40.1.1 所示:
在 Linux 中一切皆為文件,驅動加載成功以后會在“/dev”目錄下生成一個相應的文件,應用程序通過對這個名為“/dev/xxx” (xxx 是具體的驅動文件名字)的文件進行相應的操作即可實現對硬件的操作。比如現在有個叫做/dev/led 的驅動文件,此文件是 led 燈的驅動文件。應用程序使用 open 函數來打開文件/dev/led,使用完成以后使用 close 函數關閉/dev/led 這個文件。 open和 close 就是打開和關閉 led 驅動的函數,如果要點亮或關閉 led,那么就使用 write 函數來操作,也就是向此驅動寫入數據,這個數據就是要關閉還是要打開 led 的控制參數。如果要獲取led 燈的狀態,就用 read 函數從驅動中讀取相應的狀態。
應用程序運行在用戶空間,而 Linux 驅動屬于內核的一部分,因此驅動運行于內核空間。當我們在用戶空間想要實現對內核的操作,比如使用 open 函數打開/dev/led 這個驅動,因為用戶空間不能直接對內核進行操作,因此必須使用一個叫做“系統調用”的方法來實現從用戶空間“陷入” 到內核空間,這樣才能實現對底層驅動的操作。 open、 close、 write 和 read 等這些函數是由 C 庫提供的,在 Linux 系統中,系統調用作為 C 庫的一部分。當我們調用 open 函數的時候流程如圖 40.1.2 所示:
其中關于 C 庫以及如何通過系統調用“陷入” 到內核空間這個我們不用去管,我們重點關注的是應用程序和具體的驅動,應用程序使用到的函數在具體驅動程序中都有與之對應的函數,比如應用程序中調用了 open 這個函數,那么在驅動程序中也得有一個名為 open 的函數。每一個系統調用,在驅動中都有與之對應的一個驅動函數,在 Linux 內核文件 include/linux/fs.h 中有個叫做 file_operations 的結構體,此結構體就是 Linux 內核驅動操作函數集合,內容如下所示
include/linux/fs.h
struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);int (*iterate) (struct file *, struct dir_context *);unsigned int (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*mremap)(struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*aio_fsync) (struct kiocb *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **, void **);long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMUunsigned (*mmap_capabilities)(struct file *);
#endif
};
簡單介紹一下 file_operation 結構體中比較重要的、常用的函數:
第 1589 行, owner 擁有該結構體的模塊的指針,一般設置為 THIS_MODULE。
第 1590 行, llseek 函數用于修改文件當前的讀寫位置。
第 1591 行, read 函數用于讀取設備文件。
第 1592 行, write 函數用于向設備文件寫入(發送)數據。
第 1596 行, poll 是個輪詢函數,用于查詢設備是否可以進行非阻塞的讀寫。
第 1597 行, unlocked_ioctl 函數提供對于設備的控制功能,與應用程序中的 ioctl 函數對應。
第 1598 行, compat_ioctl 函數與 unlocked_ioctl 函數功能一樣,區別在于在 64 位系統上,32 位的應用程序調用將會使用此函數。在 32 位的系統上運行 32 位的應用程序調用的是unlocked_ioctl。
第 1599 行, mmap 函數用于將設備的內存映射到進程空間中(也就是用戶空間),一般幀緩沖設備會使用此函數,比如 LCD 驅動的顯存,將幀緩沖(LCD 顯存)映射到用戶空間中以后應用程序就可以直接操作顯存了,這樣就不用在用戶空間和內核空間之間來回復制。
第 1601 行, open 函數用于打開設備文件。
第 1603 行, release 函數用于釋放(關閉)設備文件,與應用程序中的 close 函數對應。
第 1604 行, fasync 函數用于刷新待處理的數據,用于將緩沖區中的數據刷新到磁盤中。
第 1605 行, aio_fsync 函數與 fasync 函數的功能類似,只是 aio_fsync 是異步刷新待處理的數據。
在字符設備驅動開發中最常用的就是上面這些函數,關于其他的函數大家可以查閱相關文檔。我們在字符設備驅動開發中最主要的工作就是實現上面這些函數,不一定全部都要實現,但是像 open、 release、 write、 read 等都是需要實現的,當然了,具體需要實現哪些函數還是要看具體的驅動要求。
1.2 字符設備驅動開發步驟
上一小節我們簡單的介紹了一下字符設備驅動,那么字符設備驅動開發都有哪些步驟呢?我們在學習裸機或者 STM32 的時候關于驅動的開發就是初始化相應的外設寄存器,在 Linux 驅動開發中肯定也是要初始化相應的外設寄存器,這個是毫無疑問的。
只是在 Linux 驅動開發中我們需要按照其規定的框架來編寫驅動,所以說學 Linux 驅動開發重點是學習其驅動框架。
1.2.0 VSCode工程配置
創建文件 .vscode/c_cpp_properties.json 內容如下,把路徑里的替換成自己虛擬機上Linux內核源碼的路徑。
{"configurations": [{"name": "Linux","includePath": ["${workspaceFolder}/**","/home/dimon/I.MX6ULL/linux_altek_driver/include","/home/dimon/I.MX6ULL/linux_altek_driver/arch/arm/include","/home/dimon/I.MX6ULL/linux_altek_driver/arch/arm/include/generated"],"defines": ["DEBUG","__KERNEL__"],"compilerPath": "/usr/bin/gcc","cStandard": "c11","cppStandard": "c++17","intelliSenseMode": "gcc-x64"}],"version": 4}
VSCode工程的配置,
關于vscode使用中出現大量未識別的關鍵字的處理: 剛看到這節,不知道后面老師設置了沒有,我先貼出來吧。視頻中結構體用.不能自動補全 __init后提示確實;等其實是缺少了__KERNEL__宏定義。具體做法是打開c_cpp_properties.json,在"defines": []里面加上__KERNEL__即:"defines": ["__KERNEL__"]
另一種配置方法
安裝clang,安裝bear插件,編譯內核的時候bear make xxxxxx 來生成json文件,然后在?編輯vscode里面安裝clangd,隨便點擊一個C文件,clangd會自動索引,就會取消所有的未識別關鍵字。。 如果還有clangd waring啥問題,記得創建一個.clang,取消就好了 不用vscode,直接source ?編輯Insight最方便,寫起來也比較輕松
1.2.1?驅動模塊的加載和卸載
Linux 驅動有兩種運行方式,第一種就是將驅動編譯進 Linux 內核中,這樣當 Linux 內核啟動的時候就會自動運行驅動程序。第二種就是將驅動編譯成模塊(Linux 下模塊擴展名為.ko),在Linux 內核啟動以后使用“insmod”命令加載驅動模塊。在調試驅動的時候一般都選擇將其編譯為模塊,這樣我們修改驅動以后只需要編譯一下驅動代碼即可,不需要編譯整個 Linux 代碼。而且在調試的時候只需要加載或者卸載驅動模塊即可,不需要重啟整個系統。總之,將驅動編譯為模塊最大的好處就是方便開發,當驅動開發完成,確定沒有問題以后就可以將驅動編譯進Linux 內核中,當然也可以不編譯進 Linux 內核中,具體看自己的需求。
模塊有加載和卸載兩種操作,我們在編寫驅動的時候需要注冊這兩種操作函數,模塊的加載和卸載注冊函數如下.
module_init(xxx_init); //注冊模塊加載函數
module_exit(xxx_exit); //注冊模塊卸載函數
module_init 函數用來向 Linux 內核注冊一個模塊加載函數,參數 xxx_init 就是需要注冊的具體函數,當使用“insmod”命令加載驅動的時候, xxx_init 這個函數就會被調用。
module_exit()函數用來向 Linux 內核注冊一個模塊卸載函數,參數 xxx_exit 就是需要注冊的具體函數,當使用“rmmod”命令卸載具體驅動的時候 xxx_exit 函數就會被調用。
字符設備驅動模塊加載和卸載模板如下所示
#include <linux/module.h>/*Module Entry*/static int __init chrdevbase_init(void)
{return 0;
}static void __exit chrdevbase_exit(void)
{}module_init(chrdevbase_init); /* Entry */
module_exit(chrdevbase_exit); /* Exit */
第 2 行,定義了個名為 xxx_init 的驅動入口函數,并且使用了“__init”來修飾。
第 9 行,定義了個名為 xxx_exit 的驅動出口函數,并且使用了“__exit”來修飾。
第 15 行,調用函數 module_init 來聲明 xxx_init 為驅動入口函數,當加載驅動的時候 xxx_init函數就會被調用。
第16行,調用函數module_exit來聲明xxx_exit為驅動出口函數,當卸載驅動的時候xxx_exit函數就會被調用。
驅動編譯完成以后擴展名為.ko,有兩種命令可以加載驅動模塊: insmod和modprobe, insmod是最簡單的模塊加載命令,此命令用于加載指定的.ko 模塊,比如加載 drv.ko 這個驅動模塊,命令如下:
insmod drv.ko
insmod 命令不能解決模塊的依賴關系,比如 drv.ko 依賴 first.ko 這個模塊,就必須先使用insmod 命令加載 first.ko 這個模塊,然后再加載 drv.ko 這個模塊。但是 modprobe 就不會存在這個問題, modprobe 會分析模塊的依賴關系,然后會將所有的依賴模塊都加載到內核中,因此modprobe 命令相比 insmod 要智能一些。 modprobe 命令主要智能在提供了模塊的依賴性分析、錯誤檢查、錯誤報告等功能,推薦使用 modprobe 命令來加載驅動。 modprobe 命令默認會去 /lib/modules/<kernel-version> 目錄中查找模塊,比如本書使用的 Linux kernel 的版本號為 4.1.15,因此 modprobe 命令默認會到/lib/modules/4.1.15 這個目錄中查找相應的驅動模塊,一般自己制作的根文件系統中是不會有這個目錄的,所以需要自己手動創建。
驅動模塊的卸載使用命令“rmmod”即可,比如要卸載 drv.ko,使用如下命令即可:
rmmod drv.ko
也可以使用“modprobe -r”命令卸載驅動,比如要卸載 drv.ko,命令如下:
modprobe -r drv.ko
使用 modprobe 命令可以卸載掉驅動模塊所依賴的其他模塊,前提是這些依賴模塊已經沒有被其他模塊所使用,否則就不能使用 modprobe 來卸載驅動模塊。所以對于模塊的卸載,還是推薦使用 rmmod 命令。
1.2.2 字符設備注冊與注銷
對于字符設備驅動而言,當驅動模塊加載成功以后需要注冊字符設備,同樣,卸載驅動模塊的時候也需要注銷掉字符設備。字符設備的注冊和注銷函數原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev 函數用于注冊字符設備,此函數一共有三個參數,這三個參數的含義如下:
major: 主設備號, Linux 下每個設備都有一個設備號,設備號分為主設備號和次設備號兩部分,關于設備號后面會詳細講解。
name:設備名字,指向一串字符串。
fops: 結構體 file_operations 類型指針,指向設備的操作函數集合變量。
unregister_chrdev 函數用戶注銷字符設備,此函數有兩個參數,這兩個參數含義如下:
major: 要注銷的設備對應的主設備號。
name: 要注銷的設備對應的設備名。
一般字符設備的注冊在驅動模塊的入口函數 xxx_init 中進行,字符設備的注銷在驅動模塊的出口函數 xxx_exit 中進行。在示例代碼 40.2.2.1 中字符設備的注冊和注銷,內容如下所示:
#include <linux/module.h>
#include <linux/fs.h>static struct file_operations test_fops;
/*Module Entry*/static int __init chrdevbase_init(void)
{int ret_value = 0;//register char devieret_value = register_chrdev(200, "chrtest", &test_fops);if(ret_value < 0){//chrdev register fail}return 0;
}static void __exit chrdevbase_exit(void)
{// unregister chrdevunregister_chrdev(200, "chrtest");
}module_init(chrdevbase_init); /* Entry */
module_exit(chrdevbase_exit); /* Exit */
第 1 行,定義了一個 file_operations 結構體變量 test_fops, test_fops 就是設備的操作函數集合,只是此時我們還沒有初始化 test_fops 中的 open、 release 等這些成員變量,所以這個操作函數集合還是空的。
第 10 行,調用函數 register_chrdev 注冊字符設備,主設備號為 200,設備名字為“chrtest”,設備操作函數集合就是第 1 行定義的 test_fops。要注意的一點就是,選擇沒有被使用的主設備號,輸入命令“cat /proc/devices”可以查看當前已經被使用掉的設備號,如圖 40.2.2.1 所示(限于篇幅原因,只展示一部分):
在圖 40.2.2.1 中可以列出當前系統中所有的字符設備和塊設備,其中第 1 列就是設備對應的主設備號。 200 這個主設備號在我的開發板中并沒有被使用,所以我這里就用了 200 這個主設備號。
第 21 行,調用函數 unregister_chrdev 注銷主設備號為 200 的這個設備。
1.2.3 實現設備的具體操作函數
file_operations 結構體就是設備的具體操作函數,在示例代碼 40.2.2.1 中我們定義了file_operations結構體類型的變量test_fops,但是還沒對其進行初始化,也就是初始化其中的open、release、 read 和 write 等具體的設備操作函數。本小節我們就完成變量 test_fops 的初始化,設置好針對 chrtest 設備的操作函數。在初始化 test_fops 之前我們要分析一下需求,也就要對 chrtest這個設備進行哪些操作,只有確定了需求以后才知道我們應該實現哪些操作函數。假設對 chrtest這個設備有如下兩個要求:
1、能夠對 chrtest 進行打開和關閉操作
設備打開和關閉是最基本的要求,幾乎所有的設備都得提供打開和關閉的功能。因此我們需要實現 file_operations 中的 open 和 release 這兩個函數。
2、對 chrtest 進行讀寫操作
假設 chrtest 這個設備控制著一段緩沖區(內存),應用程序需要通過 read 和 write 這兩個函數對 chrtest 的緩沖區進行讀寫操作。所以需要實現 file_operations 中的 read 和 write 這兩個函數。需求很清晰了,修改示例代碼 40.2.2.1,在其中加入 test_fops 這個結構體變量的初始化操作,完成以后的內容如下所示:
?
?