知識整理–Linux字元裝置驅動開發基礎
我理解的linux驅動:封裝對底層硬體的操作,向上層應用提供操作介面
文中有些地方沒貼出相應的函數原型,請自行查閱,或者用SouceInsight搜尋自己的核心源碼樹(本人就是用該方式查閱函數的使用)簡單裝置驅動開發基礎知識,暫不考慮驅動架構。文章根據GFM排版https://github.com/TongxinV
開發環境的搭建:核心源碼樹、nfs掛載的roofs、開發配置好相應的bootcmd和bootargs
驅動開發的步驟:1.驅動源碼代碼的編寫、Makefile檔案編寫、編譯得到;2.insmod裝載模組、測試,rmmod卸載模組。
bootcmd和bootargs
1.設定bootcmd使開發板能夠通過tftp下載自己建立的核心源碼樹編譯得到的zImage set bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'(註:bootcmd=movi read kernel 30008000; movi read rootfs 30B00000 300000; bootm 30008000 30B00000 這樣的bootcmd是從inand啟動核心的時候用的)2.設定bootargs使開發板從nfs去掛載rootfs(核心配置記得開啟使能nfs形式的rootfs)setenv bootargs root=/dev/nfs nfsroot=192.168.1.141:/root/x210_porting/rootfs/rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC2,115200
編譯驅動源碼的Makefile檔案
#ubuntu的核心源碼樹,如果要編譯在ubuntu中安裝的模組就開啟這2個#KERN_VER = $(shell uname -r)#KERN_DIR = /lib/modules/$(KERN_VER)/build # 開發板的linux核心的源碼樹目錄,根據自己在源碼樹存放的目錄修改KERN_DIR = /root/driver/kernelobj-m += module_test.o //-m 表示我們要將module_test.c編譯成一個模組 //-y表示我們要將module_test.c編譯連結進zImageall: make -C $(KERN_DIR) M=`pwd` modules //-C 表示進入到某一個目錄下去編譯 //`pwd`:表示把兩個`號中間的內容當成命令執行 //M=`pwd`則表示把pwd列印的內容儲存起來,目的是為了編譯好了之後能夠返回原來的目錄 //modules就是真正用來編譯模組的命令,在核心的其他地方定義過了cp: cp *.ko /root/porting_x210/rootfs/rootfs/driver_test.PHONY: clean //把clean當成一個偽目標clean: make -C $(KERN_DIR) M=`pwd` modules clean
總結:模組的makefile非常簡單,本身並不能完成模組的編譯,而是通過make -C進入到核心源碼樹下借用核心源碼的體系來完成模組的編譯連結的。
知識整理Linux字元裝置驅動開發基礎 字元裝置基礎1 從一個最簡單的模組源碼說起 字元裝置驅動工作原理 字元裝置驅動代碼實踐給空模組添加驅動殼子 應用程式如何調用驅動 字元裝置基礎2 添加讀寫介面應用和驅動之間的資料交換 驅動中如何操控硬體和裸機代碼有何不同 靜態映射和動態映射 靜態映射操作LED 動態映射操作LED 字元裝置基礎3 字元裝置驅動註冊新介面cdev 結構體cdev與相關的操作函數介紹 中途出錯的倒影式處理方法 使用cdev_alloc 字元裝置驅動註冊程式碼分析 register_chrdev register_chrdev_regionalloc_chrdev_region 總結字元裝置驅動在核心中如何調用1 自動建立和刪除裝置檔案 關於sys檔案系統 核心提供的讀寫寄存器介面 總結
字元裝置基礎1 從一個最簡單的模組源碼說起
#include <linux/module.h> // module_init module_exit#include <linux/init.h> // __init __exit// 模組安裝函數static int __init chrdev_init(void){ printk(KERN_INFO "chrdev_init helloworld init\n"); return 0;}// 模組卸載函數static void __exit chrdev_exit(void){ printk(KERN_INFO "chrdev_exit helloworld exit\n");}module_init(chrdev_init);module_exit(chrdev_exit);// MODULE_xxx這種宏作用是用來添加模組描述資訊MODULE_LICENSE("GPL"); // 描述模組的許可證
(1)使用printk列印調試資訊,printk可以設定列印層級。常見的KERN_DBUG-8\KERN_INFO-7,當前系統也有一個列印資訊的層級0-7(比如當前系統列印資訊的層級為4,則printk列印小於層級4)。
查看當前系統列印資訊的層級:cat /proc/sys/kernel/printk;修改:echo 8 > /proc/sys/kernel/printk
(2)驅動原始碼中包含的標頭檔和原來應用編程程式中包含的標頭檔不是一回事。應用編程中包含的標頭檔是應用程式層的標頭檔,是應用程式的編譯器帶來的(譬如gcc的標頭檔路徑在/usr/include下,這些東西是和作業系統無關的)。驅動源碼屬於核心源碼的一部分,驅動源碼中的標頭檔其實就是核心原始碼目錄下的include目錄下的標頭檔。
(3)函數修飾符__init(前面加底線的表示這是給核心使用的函數),本質上是個宏定義,在核心原始碼中就有#define __init xxxx。這個__init的作用就是將被他修飾的函數放入.init.text段中去(本來預設情況下函數是被放入.text段中)。
#define __init __section(.init.text) __cold notrace ├──#define __section(S) __attribute__ ((__section__(#S)))
整個核心中的所有的這類函數都會被連結器根據連結指令碼放入.init.text段中,所以所有的核心模組的__init修飾的函數其實是被統一放在一起的。
核心啟動時統一會載入.init.text段中的這些模組安裝函數,載入完後就會把這個段給釋放掉以節省記憶體。__exit同理。 字元裝置驅動工作原理
可以理解模組是一種機制,驅動使用了模組這種機制來實現
系統整體工作原理:(1)應用程式層->API->裝置驅動->硬體;(2)API:open、read、write、close等;(3)驅動源碼中提供真正的open、read、write、close等函數實體
file_operations結構體(另外一種為attribute方式後面再講):(1)元素主要是函數指標,用來掛接實體函數地址;(2)每個裝置驅動都需要一個該結構體類型的變數;(3)裝置驅動向核心註冊時提供該結構體類型的變數。
註冊字元裝置驅動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); }
(1)作用,驅動向核心註冊自己的file_operations結構體,註冊的過程其實主要是將要註冊的驅動的資訊儲存在核心中專門用來儲存註冊的字元裝置驅動的數組中相應的位置
(2)參數:裝置號major–major傳0進去表示要讓核心幫我們自動分配一個合適的空白的沒被使用的主裝置,核心如果成功分配就會返回分配的主裝置號;如果分配失敗會返回負數
(3)inline和static
inline:當把函數定義在標頭檔裡面的時候,如果你這個標頭檔被兩個及兩個以上的函數包含的時候,在連結的時候就會出錯。inline的作用就是解決這個問題,原地展開並能夠實現靜態檢查。另外一個原因是函數本身就比較短。
核心如何管理字元裝置驅動
(1)核心中用一個數組來儲存註冊的字元裝置驅動;(2)register_chrdev內部將我們要註冊的驅動的資訊(fops結構體地址)儲存在數組中相應的位置;(3)cat /proc/devices查看核心中已經註冊過的字元裝置驅動(和塊裝置驅動) 字元裝置驅動代碼實踐–給空模組添加驅動殼子
核心工作:定義file_operations類型變數及其元素填充、註冊驅動
簡單的驅動程式樣本
module_test.c ├── 模組安裝函數xxx │ └── 註冊字元裝置驅動register_chrdev(MYNMAJOR, MYNAME, &test_module_fops) ├── 模組安裝函數yyy │ └── 登出字元裝置驅動unregister_chrdev(MYNMAJOR, MYNAME) │ ├── module_init(模組安裝函數xxx); ├── module_exit(模組卸載函數yyy); │ └── MODULE_LICENSE("GPL");
#include <linux/module.h> // module_init module_exit#include <linux/init.h> // __init __exit#include <linux/fs.h> // file_operations 沒寫會報錯:xxx has initializer but incomplete type#define MYNMAJOR 200#define MYNAME "test_chrdev"//file_operations結構體變數中填充的函數指標的實體,函數的格式要遵守static int test_chrdev_open(struct inode *inode, struct file *file){ //這個函數中真正應該放置的是開啟這個裝置的硬體作業碼部分 //但是現在我們暫時寫不了那麼多,所以就就用一個printk列印個資訊來做代表 printk(KERN_INFO "test_module_open\n"); return 0;}static int test_chrdev_release(struct inode *inode, struct file *file){ printk(KERN_INFO "test_chrdev_release\n"); return 0;}//自訂一個file_operations結構體變數,並填充static const struct file_operations test_module_fops = { .owner = THIS_MODULE, //慣例,所有的驅動都有這一個,這也是這結構體中唯一一個不是函數指標的元素 .open = test_chrdev_open, //將來應用open開啟這個這個裝置時實際調用的函數 .release = test_chrdev_release, //對應close,為什麼不叫close呢。詳見後面release和close的區別的講解};/*********************************************************************************/// 模組安裝函數static int __init chrdev_init(void){ printk(KERN_INFO "chrdev_init helloworld init\n"); //在module_init宏調用的函數中去註冊字元裝置驅動 int ret = -1; //register_chrdev 傳回值為int類型 ret = register_chrdev(MYNMAJOR, MYNAME, &test_module_fops); //參數:主裝置號major,裝置名稱name,自己定義好的file_operations結構體變數指標,注意是指標,所以要加上取地址符 //完了之後檢查傳回值 if(ret){ printk(KERN_ERR "register_chrdev fial\n"); //注意這裡不再用KERN_INFO return -EINVAL; //核心中定義了好多error number 不都用以前那樣return -1;負號要加 。。 } printk(KERN_ERR "register_chrdev success...\n"); return 0;}// 模組卸載函數static void __exit chrdev_exit(void){ printk(KERN_INFO "chrdev_exit helloworld exit\n"); //在module_exit宏調用的函數中去登出字元裝置驅動 //實驗中,在我們這裡不寫東西的時候,rmmod 後lsmod 查看確實是沒了,但是cat /proc/device發現裝置號還是被佔著 unregister_chrdev(MYNMAJOR, MYNAME); //參數就兩個 //檢測傳回值 ...... return 0;}/*********************************************************************************/module_init(chrdev_init); //insmod 時調用module_exit(chrdev_exit); //rmmod 時調用// MODULE_xxx這種宏作用是用來添加模組描述資訊MODULE_LICENSE("GPL"); // 描述模組的許可證
應用程式如何調用驅動
驅動裝置檔案的建立:(1)何為裝置檔案:用來索引驅動;(2)裝置檔案的關鍵資訊是:裝置號 = 主裝置號 + 次裝置號;(3)使用mknod建立裝置檔案:mknod /dev/xxx c 主裝置號 次裝置號 (c表示要建立的裝置檔案類型為字元裝置);(4)使用ls xxx -l去查看裝置檔案,就可以得到這個裝置檔案對應的主次裝置號。
註:不可能總用mknod來建立裝置檔案,能否自動產生和刪除裝置檔案。linux核心有一種機制–udev(嵌入式中用的是mdev)後面細講
一個簡單的應用程式樣本app.c
#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h> //man 2 open 查看標頭檔有哪些#define FILE "/dev/test" // 剛才mknod建立的裝置檔案名稱 雙引號不要漏int main(void){ int fd = -1; fd = open(FILE, O_RDWR); if (fd < 0){ printf("open %s error.\n", FILE); return -1; } printf("open %s success..\n", FILE); // 讀寫檔案 ... // 關閉檔案 close(fd); return 0;}
字元裝置基礎2 添加讀寫介面(應用和驅動之間的資料交換)
照貓畫虎
在驅動程式中添加:
ssize_t test_chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)//struct file *file:指向我們要操作的檔案;const char __user *buf:使用者空間的buf { printk(KERN_INFO "test_chrdev_read\n"); ...... static ssize_t test_chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { printk(KERN_INFO "test_chrdev_write\n"); ......
在應用程式中添加:
//讀寫檔案read(fd,buf,100);//最後一個參數為要讀取的位元組數write(fd,"helloworld",10);......
測試:測試前先rmmod 把之前實驗的模組卸載掉,lsmod確認下cat /proc/devices再insmod;還有裝置檔案也要rm /dev/xxx刪裝置檔案,安裝完模組後再mknod重建立立裝置檔案。然後執行應用程式查看列印資訊(在後面我們會講怎麼弄才不會那麼麻煩)
應用和驅動之間的資料交換:
寫函數的本質就是將應用程式層傳遞的過來的資料先複製到核心中,然後將之以正確的方式寫入硬體,完成操作目前接觸到的就兩種:copy_from_user、copy_to_user和mmap,這裡只講第一種
完成write和read函數:
copy_from_user函數的傳回值定義,和常規有點不同。傳回值如果成功複製則返回0,如果不成功複製則返回尚未成功複製剩下的位元組數。
module_test.c:
char kbuf[100];//核心空間的一個buf......static ssize_t test_chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos){ int ret = -1; printk(KERN_INFO "test_chrdev_write\n"); //使用該函數將應用程式層的傳過來的ubuf中的內容拷貝到驅動空間(核心空間)的一個buf中 //memcpy(kbuf,ubuf); //不行,因為2個不在一個地址空間中 menset(kbuf, 0, sizeof(kbuf)); ret = copy_from_user(kbuf,ubuf,count); if(ret){ printk(KERN_ERR "copy_from_user fail\n"); return -EINVAL;//在真正的的驅動中沒複製成功應該有一些錯誤修正機制,這裡我們簡單點 } printk(KERN_ERR "copy_from_user success..\n"); //到這裡我們就成功把使用者空間的資料轉移到核心空間了 //真正的驅動中,資料從應用程式層複製到驅動中後,我們就要根據這個資料去寫硬體完成硬體的操作 //所以下面就應該是操作硬體的代碼 ...... return 0;}ssize_t test_chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *ppos){ int ret = -1; printk(KERN_INFO "test_chrdev_read\n"); ret = copy_to_user(ubuf,kbuf,size); if(ret){ printk(KERN_ERR "copy_to_user fail\n"); return -EINVAL;//在真正的的驅動中沒複製成功應該有一些錯誤修正機制,這裡我們簡單點 } printk(KERN_ERR "copy_to_user success..\n"); return 0;}
app.c
......//讀寫檔案write(fd, “helloworld”, 10);read(fd,buf,100);printf(“讀出來的內容是:%s \n”,buf);
列印結果:...
驅動中如何操控硬體(和裸機代碼有何不同)
在PowerPC、m68k和ARM等體系中,外設I/O連接埠具有與記憶體一樣的物理地址,外設的I/O記憶體資源的物理地址是已知的,由硬體的設計決定。Linux的驅動程式並不能直接通過物理地址訪問I/O記憶體資源,而必須將物理地址映射到核心虛地址空間。
還是那個硬體:
(1)硬體物理原理不變;(2)硬體操作介面(寄存器)不變;(3)硬體作業碼不變
哪裡不同:
(1)寄存器地址不同。原來是直接用物理地址,現在需要用該物理地址在核心虛擬位址空間相對應的虛擬位址。寄存器的物理地址是CPU設計時決定的,從datasheet中尋找到的。(2)編程習慣不同。裸機中習慣直接用函數指標操作寄存器地址,而kernel中習慣用封裝好的io讀寫函數來操作寄存器,以實現最大程度可移植性。
核心的虛擬位址映射方法:
(1)為什麼需要虛擬位址映射:核心運行在自己的虛擬位址空間中(2)核心中有2套虛擬位址映射方法:動態和靜態(3)靜態映射方法的特點: 核心移植時以代碼的形式寫入程式碼,如果要更改必須改原始碼後重新編譯核心 在核心啟動時建立靜態映射表,到核心關機時銷毀,中間一直有效 對於移植好的核心,你用不用他都在那裡(4)動態映射方法的特點: 驅動程式根據需要隨時動態建立映射、使用、銷毀映射 映射是短期臨時的
如何選擇虛擬位址映射方法:
(1)2種映射並不排他,可以同時使用(2)靜態映射類似於C語言中全域變數,動態方式類似於C語言中malloc堆記憶體(3)靜態映射的好處是執行效率高,壞處是始終佔用虛擬位址空間;動態映射的好處是按需使用虛擬位址空間,壞處是每次使用前後都需要代碼去建立映射&銷毀映射(還得學會使用那些核心功能的使用)沒有絕對好絕對壞
靜態映射和動態映射
在ARM 儲存系統中,使用記憶體管理單元(MMU)實現虛擬位址到實際物理地址的映射。MMU的實現過程實際上就是一個查表映射的過程。建立頁表是實現MMU功能不可缺少的一步。頁表位於系統的記憶體中,頁表的每一項對應於一個虛擬位址到物理地址的映射。每一項的長度即是一個字的長度(在32位ARM中,一個字的長度被定義為4B)。頁表項除完成虛擬位址到物理地址的映射功能之外,還定義了存取權限和緩衝特性等。
由於篇幅有限,這裡只分析如何使用(以s5pv210為例),具體如何?點擊這裡Linux核心靜態映射表建立過程分析 靜態映射操作LED
關於靜態映射要說的:
(1)不同版本核心中靜態映射表位置、檔案名稱可能不同(2)不同SoC的靜態映射表位置、檔案名稱可能不同(3)所謂映射表其實就是標頭檔中的宏定義
三星版本核心中的靜態映射表:
(1)主映射表位於:arch/arm/plat-samsung/include/plat/map-base.h和arch/arm/plat-s5p/include/plat/map-s5p.h
map-base.h
...#define S3C_ADDR_BASE (0xFD000000)//三星移植時確定的靜態映射表的基地址,表中的所有虛擬位址都是以這個地址+位移量來指定的#ifndef __ASSEMBLY__#define S3C_ADDR(x) ((void __iomem __force *)S3C_ADDR_BASE + (x))#else#define S3C_ADDR(x) (S3C_ADDR_BASE + (x))#endif#define S3C_VA_IRQ S3C_ADDR(0x00000000) /* irq controller(s) */#define S3C_VA_SYS S3C_ADDR(0x00100000) /* system control */#define S3C_VA_MEM S3C_ADDR(0x00200000) /* memory control */...#define S5P_VA_GPIO S3C_ADDR(0x00500000) ...
map-base.h和map-s5p.h中定義的是各模組的寄存器基地址的虛擬位址。(後面那個可能是九鼎根據自己的硬體自己移植)
CPU在安排寄存器地址時不是隨意亂序分布的,而是按照模組去區分的。每一個模組內部的很多個寄存器的地址是連續的。所以核心在定義寄存器地址時都是先找到基地址,然後再用基地址+位移量來尋找具體的一個寄存器。(map-s5p.h中定義的就是要用到的幾個模組的寄存器基地址。並沒有全,三星唯寫了自己要用的。將來實際工作如果要用到的這裡沒有就自己添加)
(2)GPIO各個連接埠相關的主映射表位於:arch/arm/mach-s5pv210/include/mach/regs-gpio.h表中是GPIO的各個連接埠的基地址的定義。GPIO還分成GPA0、GPA1、GPB0、GPC、E、F、G、H等
regs-gpio.h
/* Base addresses for each of the banks */#define S5PV210_GPA0_BASE (S5P_VA_GPIO + 0x000)#define S5PV210_GPA1_BASE (S5P_VA_GPIO + 0x020)#define S5PV210_GPB_BASE (S5P_VA_GPIO + 0x040)#define S5PV210_GPC0_BASE (S5P_VA_GPIO + 0x060)...
(3)每一個GPIO的具體寄存器定義位於:arch/arm/mach-s5pv210/include/mach/gpio-bank.h
gpio-bank.h
...#define S5PV210_GPA0CON (S5PV210_GPA0_BASE + 0x00)#define S5PV210_GPA0DAT (S5PV210_GPA0_BASE + 0x04)#define S5PV210_GPA0PUD (S5PV210_GPA0_BASE + 0x08)#define S5PV210_GPA0DRV (S5PV210_GPA0_BASE + 0x0c)#define S5PV210_GPA0CONPDN (S5PV210_GPA0_BASE + 0x10)#define S5PV210_GPA0PUDPDN (S5PV210_GPA0_BASE + 0x14)...
Q:為什麼給個虛擬位址就能找到對應的物理地址?—-MMU的存在,Linux核心靜態映射表建立過程分析
驅動中添加相應代碼:
#include <mach/regs-gpio.h> //虛擬位址映射表#include <mach/gpio-bank.h> ...#define rGPJ0CON *((volatile unsigned int *)GPJ0CON) //強制類型轉化為指標類型,再解引用#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)...//寫函數的本質就是將應用程式層傳遞過來的資料先複製到核心中,然後將之以正確的方式寫入硬體static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos){ int ret = -1; printk(KERN_INFO "test_chrdev_write\n"); // 使用該函數將應用程式層傳過來的ubuf中的內容拷貝到驅動空間中的一個buf中 //memcpy(kbuf, ubuf); // 不行,因為2個不在一個地址空間中 memset(kbuf, 0, sizeof(kbuf)); ret = copy_from_user(kbuf, ubuf, count); if (ret){ printk(KERN_ERR "copy_from_user fail\n"); return -EINVAL; } printk(KERN_INFO "copy_from_user success..\n"); if (kbuf[0] == '1'){ rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); }else if (kbuf[0] == '0'){ rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); } return 0;} ...
完整原始碼module_test.c
注:我們驅動這麼寫既是正確的又是不正確的,正確的是說它能實現功能,不正確是說它寫法不符合常規,常規的寫法就是我們在驅動裡只負責單純的操作硬體,而應該把一些判斷啊跟使用者相關的商務邏輯寫到應用裡而不應該把它寫到驅動裡。
動態映射操作LED
如何建立動態映射:
(1)request_mem_region,向核心申請(報告)需要映射的記憶體資源。參數:寄存器物理地址,寄存器佔用位元組數,name(2)ioremap,真正用來實現映射,傳給他物理地址他給你映射返回一個虛擬位址。參數:寄存器物理地址,寄存器佔用位元組數;傳回值:映射到虛擬位址的地址的指標。
如何銷毀動態映射
(1)iounmap(2)release_mem_region注意:映射建立時,是要先申請再映射;然後使用;使用完要解除映射時要先解除映射再釋放申請。(倒影式結構)
驅動中添加相應代碼:
在模組安裝中去申請資源和實現映射;在模組卸載中去解除映射和釋放資源
...#define GPJ0CON_PA 0xe0200240#define GPJ0DAT_PA 0xe0200244unsigned int *pGPJ0CON;unsigned int *pGPJ0DAT;...// 模組安裝函數static int __init chrdev_init(void){ printk(KERN_INFO "chrdev_init helloworld init\n"); // 在module_init宏調用的函數中去註冊字元裝置驅動 mymajor = register_chrdev(0, MYNAME, &test_fops);//分配就會返回分配的主裝置好;如果分配失敗會返回負數 if (mymajor < 0){ printk(KERN_ERR "register_chrdev fail\n"); return -EINVAL; } printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor); // 使用動態映射的方式來操作寄存器 if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON")) return -EINVAL; if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON")) return -EINVAL; pGPJ0CON = ioremap(GPJ0CON_PA, 4); pGPJ0DAT = ioremap(GPJ0DAT_PA, 4); /***後面就可以通過pGPJ0CON、pGPJ0DAT來操作相應寄存器從而控制硬體了***/ *pGPJ0CON = 0x11111111; *pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮 return 0;}// 模組下載函數static void __exit chrdev_exit(void){ printk(KERN_INFO "chrdev_exit helloworld exit\n"); *pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); // 解除映射 iounmap(pGPJ0CON); iounmap(pGPJ0DAT); release_mem_region(GPJ0CON_PA, 4); release_mem_region(GPJ0DAT_PA, 4); // 在module_exit宏調用的函數中去登出字元裝置驅動 unregister_chrdev(mymajor, MYNAME);}
實現同時映射多個寄存器:
因為地址是挨著的,所以可以一