聲明
本博客所記錄的關于正點原子i.MX6ULL開發板的學習筆記,(內容參照正點原子I.MX6U嵌入式linux驅動開發指南,可在正點原子官方獲取正點原子Linux開發板 — 正點原子資料下載中心 1.0.0 文檔),旨在如實記錄我在學校學習該開發板過程中所遭遇的各類問題以及詳細的解決辦法。其初衷純粹是為了個人知識梳理、學習總結以及日后回顧查閱方便,同時也期望能為同樣在學習這款開發板的同學或愛好者提供一些解決問題的思路和參考。我盡力保證內容的準確性和可靠性,但由于個人知識水平和實踐經驗有限,若存在錯誤或不嚴謹之處,懇請各位讀者批評指正。
責任聲明:雖然我力求提供有效的問題解決辦法,但由于開發板使用環境、硬件差異、軟件版本等多種因素的影響,我的筆記內容不一定適用于所有情況。對于因參考本筆記而導致的任何直接或間接損失,我不承擔任何法律責任。使用本筆記內容的讀者應自行承擔相關風險,并在必要時尋求專業技術支持。
1. 字符設備驅動簡介
字符設備是 Linux 驅動中最基本的一類設備驅動,字符設備就是一個一個字節,按照字節流進行讀寫操作的設備,讀寫數據是分先后順序的。比如我們最常見的點燈、按鍵、 IIC、 SPI, LCD 等等都是字符設備,這些設備的驅動就叫做字符設備驅動。
Linux 應用程序對驅動程序的調用
在 Linux 中一切皆為文件,驅動加載成功以后會在“/dev”目錄下生成一個相應的文件,應用程序通過對這個名為“/dev/xxx” (xxx 是具體的驅動文件名字)的文件進行相應的操作即可實現對硬件的操作。
應用程序運行在用戶空間,而 Linux 驅動屬于內核的一部分,因此驅動運行于內核空間。當我們在用戶空間想要實現對內核的操作,比如使用 open 函數打開/dev/led 這個驅動,因為用戶空間不能直接對內核進行操作,因此必須使用一個叫做“系統調用”的方法來實現從用戶空間“陷入” 到內核空間,這樣才能實現對底層驅動的操作。
每一個系統調用,在驅動中都有與之對應的一個驅動函數,在 Linux 內核文件 include/linux/fs.h 中有個叫做 file_operations 的結構體,此結構體就是 Linux 內核驅動操作函數集合
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 等都是需要實現的,具體需要實現哪些函數還是要看具體的驅動要求。
2. 字符設備驅動開發步驟
2.1 驅動模塊的加載和卸載
Linux 驅動有兩種運行方式,第一種就是將驅動編譯進 Linux 內核中,這樣當 Linux 內核啟動的時候就會自動運行驅動程序。第二種就是將驅動編譯成模塊(Linux 下模塊擴展名為.ko),在Linux 內核啟動以后使用“insmod”/“modprob”命令加載驅動模塊。在調試驅動的時候一般都選擇將其編譯為模塊,這樣我們修改驅動以后只需要編譯一下驅動代碼即可,不需要編譯整個 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 函數就會被調用。字符設備驅動模塊加載和卸載模板如下所示:
驅動編譯完成以后擴展名為.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 命令。
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 中進行。
加入字符設備注冊和注銷后的代碼:
CHRDEVBASE_MAJOR是宏定義,使用cat /proc/devices可以查看當前設備的設備號,自己取一個沒被占用的,我選的是66, CHRDEVBASE_NAME就是名字,也是宏定義。
這里使用了 printk 來輸出信息,而不是 printf!因為在 Linux 內核中沒有 printf 這個函數。 printk 相當于 printf 的孿生兄妹, printf運行在用戶態, printk 運行在內核態。在內核中想要向控制臺輸出或顯示一些內容,必須使用printk 這個函數。不同之處在于, printk 可以根據日志級別對消息進行分類,一共有 8 個消息級別,這 8 個消息級別定義在文件 include/linux/kern_levels.h 里面,定義如下:
一共定義了 8 個級別,其中 0 的優先級最高, 7 的優先級最低。如果要設置消息級別,參考如下示例:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");
上述代碼就是設置“gsmi: Log Shutdown Reason\n”這行消息的級別為 KERN_EMERG。在具體的消息前面加上 KERN_EMERG 就可以將這條消息的級別設置為 KERN_EMERG。如果使用 printk 的 時 候 不 顯 式 的 設 置 消 息 級 別 , 那 么 printk 將 會 采 用 默 認 級 別MESSAGE_LOGLEVEL_DEFAULT, MESSAGE_LOGLEVEL_DEFAULT 默認為 4。在 include/linux/printk.h 中有個宏 CONSOLE_LOGLEVEL_DEFAULT,定義如下: #define CONSOLE_LOGLEVEL_DEFAULT 7
CONSOLE_LOGLEVEL_DEFAULT 控制著哪些級別的消息可以顯示在控制臺上,此宏默認為7,意味著只有優先級高于 7 的消息才能顯示在控制臺上。
這個就是 printk 和 printf 的最大區別,可以通過消息級別來決定哪些消息可以顯示在控制臺上。默認消息級別為 4, 4 的級別比 7 高,所示直接使用 printk 輸出的信息是可以顯示在控制臺上的。
2.3 設備號
Linux 中每個設備都有一個設備號,設備號由主設備號和次設備號兩部分組成,主設備號表示某一個具體的驅動,次設備號表示使用這個驅動的各個設備。 Linux 提供了一個名為 dev_t 的數據類型表示設備號, dev_t 定義在文件 include/linux/types.h 里面,就是 unsigned int 類型,是一個 32 位的數據類型。這 32 位的數據構成了主設備號和次設備號兩部分,其中高 12 位為主設備號, 低 20 位為次設備號。系統中主設備號范圍為 0~4095,所以在選擇主設備號的時候一定不要超過這個范圍。
2.4 實現設備的具體操作函數
&test_fops是Linux 內核驅動操作函數
每個對應的實現:
這四個函數就是 chrtest 設備的 open、 read、 write 和 release 操作函數
2.5 添加LICENSE 和作者信息
最后我們需要在驅動中加入 LICENSE 信息和作者信息,其中 LICENSE 是必須添加的,否則的話編譯的時候會報錯,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用如下兩個函數:
MODULE_LICENSE() //添加模塊 LICENSE 信息
MODULE_AUTHOR() //添加模塊作者信息
3.?chrdevbase 字符設備驅動開發實驗
3.1、創建 VSCode 工程
在 Linux_Drivers 目錄下新建一個名為 1_chrdevbase 的子目錄來存放本實驗所有文件
在 1_chrdevbase 目錄中新建 VSCode 工程
3.2、添加頭文件路徑
打開 VSCode,按下“Crtl+Shift+P”打開 VSCode 的控制臺,然后輸入“C/C++: Edit configurations(JSON) ”,打開 C/C++編輯配置文件,第 5 行的 includePath 表示頭文件路徑,需要將 Linux 源碼里面的頭文件路徑添加進來,也就是我們前面移植的 Linux 源碼中的頭文件路徑。添加頭文件路徑以后的 c_cpp_properties.json的文件內容如下所示:
{"configurations": [{"name": "Linux","includePath": ["${workspaceFolder}/**","/home/mayachao/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include","/home/mayachao/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include","/home/mayachao/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated/"],"defines": [],"compilerPath": "/usr/bin/clang","cStandard": "c11","cppStandard": "c++17","intelliSenseMode": "clang-x64"}],"version": 4}
4.3、編寫實驗程序
在 Ubuntu 中輸入“man 2 open”即可查看 open 函數的詳細內容,其他以此類推。
chrdevbaseAPP.c如下所示:
4.4、編譯驅動程序
編譯驅動程序,也就是 chrdevbase.c 這個文件,我們需要將其編譯為.ko 模塊,創建Makefile 文件,然后在其中輸入如下內容:
第 1 行, KERNELDIR 表示開發板所使用的 Linux 內核源碼目錄,使用絕對路徑,大家根據自己的實際情況填寫即可。
Makefile 編寫好以后輸入“make”命令編譯驅動模塊,編譯過程如圖
編譯成功以后就會生成一個叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 設備的驅動模塊。至此, chrdevbase 設備的驅動就編譯成功。
測試 APP 比較簡單,只有一個文件,因此就不需要編寫 Makefile 了,直接輸入命令編譯。因為測試 APP 是要在 ARM 開發板上運行的,所以需要使用 arm-linux-gnueabihf-gcc 來編譯,輸入如下命令:
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
編譯完成以后會生成一個叫做 chrdevbaseApp 的可執行程序,輸入如下命令查看chrdevbaseAPP 這個程序的文件信息:
file chrdevbaseApp
chrdevbaseAPP 這個可執行文件是 32 位 LSB 格式, ARM 版本的,因此 chrdevbaseAPP 只能在 ARM 芯片下運行。
4.5 運行測試
1、加載驅動模塊
驅動模塊 chrdevbase.ko 和測試軟件 chrdevbaseAPP 都已經準備好了,接下來就是運行測試。為了方便測試, Linux 系統選擇通過 TFTP 從網絡啟動,并且使用 NFS 掛載網絡根文件系統,確保 uboot 中 bootcmd 環境變量的值為:
tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000
bootargs 環境變量的值為:
console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.250:/home/mayachao/linux/nfs/ rootfs ip=192.168.1.251:192.168.1.250:192.168.1.1:255.255.255.0::eth0:off
設置好以后啟動 Linux 系統,檢查開發板根文件系統中有沒有“/lib/modules/4.1.15”這個目錄,如果沒有的話自行創建。 注意,“/lib/modules/4.1.15”這個目錄用來存放驅動模塊,使用modprobe 命令加載驅動模塊的時候,驅動模塊要存放在此目錄下。“/lib/modules”是通用的,不管你用的什么板子、什么內核,這部分是一樣的。不一樣的是后面的“4.1.15”,這里要根據你所使用的 Linux 內核版本來設置。
將 chrdevbase.ko 和 chrdevbaseAPP 復制到 rootfs/lib/modules/4.1.15 目錄中,命令如下:
sudo cp chrdevbase.ko chrdevbaseApp /home/mayachao/linux/nfs/rootfs/lib/modules/4.1.15/ -f
輸入“depmod”命令以后會自動生成 modules.alias、modules.symbols 和 modules.dep 這三個文件,如圖
有些根文件系統可能沒有 depmod 這個命令,如果沒有這個命令就只能重新配置busybox,使能此命令,然后重新編譯 busybox。
輸入如下命令加載 chrdevbase.ko 驅動文件:
insmod chrdevbase.ko
或
modprobe chrdevbase.ko
輸入“lsmod”命令即可查看當前系統中存在的模塊
輸入如下命令查看當前系統中有沒有 chrdevbase 這個設備:
cat /proc/devices
2、創建設備節點文件
輸入如下命令創建/dev/chrdevbase 這個設備節點文件:
mknod /dev/chrdevbase c 66 0
“mknod”是創建節點命令,“/dev/chrdevbase”是要創建的節點文件,“c”表示這是個字符設備,“ 66”是設備的主設備號,“ 0”是設備的次設備號。創建完成以后就會存在/dev/chrdevbase 這個文件,可以使用“ls /dev/chrdevbase -l”命令查看
如果 chrdevbaseAPP 想要讀寫 chrdevbase 設備,直接對/dev/chrdevbase 進行讀寫操作即可。
3、 chrdevbase 設備操作測試
輸入如下命令:
./chrdevbaseApp /dev/chrdevbase
4、卸載驅動模塊
如果不再使用某個設備的話可以將其驅動卸載掉,比如輸入如下命令卸載掉 chrdevbase 這個設備:
rmmod chrdevbase.ko
卸載以后使用 lsmod 命令查看 chrdevbase 這個模塊還存不存在,結果如圖