1. 程式人生 > >最簡單的驅動模組

最簡單的驅動模組

一. 驅動開發的準備工作
    1. 正常執行linux系統的開發板
    要求開發板中的linux的zImage必須是自己編譯的
    在這裡的目錄路徑是:/home/zhao/zhao/5.linux_drive/kernel/arch/arm/boot
    2. 核心原始碼樹,就是一個經過了配置編譯之後的核心原始碼
    1)ubuntu的核心原始碼樹,如果要編譯在ubuntu中安裝的模組就開啟這2個               
           KERN_VER = $(shell uname -r)
           KERN_DIR = /lib/modules/$(KERN_VER)/build
    2)開發板linux系統的核心原始碼樹在這裡的目錄路徑是:/home/zhao/zhao/5.linux_drive/kernel
    3. nfs掛載的rootfs
    在這裡ubuntu系統下的路徑是:/root/rootfs/rootfs1/driver_test

二. 常用的模組操作命令
    1. lsmod(list module,將模組列表顯示)
    功能是打印出當前核心中已經安裝的模組列表
    2. insmod(install module,安裝模組)
    功能是向當前核心中去安裝一個模組,用法是insmod xxx.ko
    3. modinfo(module information,模組資訊)
    功能是打印出一個核心模組的自帶資訊,用法是modinfo xxx.ko
    4. rmmod(remove module,解除安裝模組)
    功能是從當前核心中解除安裝一個已經安裝了的模組,用法是rmmod xxx(解除安裝模組時只需要輸入模組名即可,不加.ko字尾)

三. 模組的安裝與解除安裝
    1. 模組的安裝
       insmod與module_init巨集
    1)模組原始碼中用module_init巨集聲明瞭一個函式(在模組程式碼的這個例子裡是chrdev_init函式),作用就是指定chrdev_init這個函式和insmod命令繫結起來,也
       就是說當insmod module_test.ko時,insmod命令內部實際執行的操作就是呼叫chrdev_init函式
    2)在沒有指定主裝置號的情況下核心會將最新安裝的模組放在lsmod顯示的最前面
    3)正常情況執行了insmod安裝模組,會打印出相應的提示資訊,但是由於核心的printk設定了列印級別,所以這裡就算安裝成功後也不會顯示出提示資訊;要想看到
       提示資訊,在ubuntu中使用dmesg命令就可以看到了
    4)核心中printk的列印級別
        (1)printk是linux核心原始碼中自己封裝出來的一個列印函式,是核心原始碼中的一個普通函式,只能在核心原始碼範圍內使用,不能在應用程式設計中使用
        (2)printk的列印級別是用來控制printk列印的這條資訊是否在終端上顯示的,核心非常龐大,列印資訊非常多,所以才有了列印級別
        (3)作業系統的命令列中也有一個列印資訊級別屬性,值為0-7。當前作業系統中執行printk的時候會去對比printk中的列印級別和命令列中設定的列印級別,小
        於命令列設定級別的資訊會被放行打印出來,大於的就被攔截的。譬如ubuntu中的列印級別預設是4,那麼printk中設定的級別比4小的就能打印出來,比4
        大的就不能打印出來
        (4)參照核心的列印級別的文件......
    2. 模組的版本資訊
    (1)使用modinfo檢視模組的版本資訊
    (2)核心zImage中也有一個確定的版本資訊
    (3)insmod時模組的vermagic必須和核心的相同,否則不能安裝
       不能安裝時的報錯資訊為:insmod: ERROR: could not insert module module_test.ko: Invalid module format
    (4)編譯模組的核心原始碼樹要與編譯正在執行的這個核心的那個核心原始碼樹是同一個
    3. 模組解除安裝
    上面安裝模組時insmod與module_init巨集是對應關係
    這裡解除安裝模組時module_exit和rmmod是對應關係
    4. 驅動模組中的標頭檔案
    驅動原始碼屬於核心原始碼的一部分,驅動原始碼中的標頭檔案其實就是核心原始碼目錄下的include目錄下的標頭檔案

四. 用開發板來除錯模組
    1. 用開發板來除錯模組
    可以設定bootcmd使開發板通過tftp下載自己建立的核心原始碼樹編譯得到的zImage;設定如下
    set bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'
    2. 設定bootargs使開發板從nfs去掛載rootfs
    3. 修改Makefile中的KERN_DIR使其指向自己建立的核心原始碼樹
    在這裡原始碼樹的目錄是:/home/zhao/zhao/5.linux_drive/kernel
    4. 將編譯好的驅動.ko檔案放入nfs共享目錄下
    在這裡nfs的共享目錄是:/root/rootfs/rootfs1/driver_test
    5. 啟動開發板後使用insmod、rmmod、lsmod等去進行模組測試

五. 最簡單的模組
    1. 模組的安裝
    執行insmod xxx.ko時,呼叫的是 module_init(chrdev_init)巨集,最終通過這個巨集來呼叫chrdev_init函式,來實現模組的安裝
    2. 模組的解除安裝
    執行rmmod xxx.ko時,呼叫的是  module_exit(chrdev_exit)巨集,最終通過這個巨集來呼叫chrdev_exit函式,來實現模組的解除安裝

六. 新增相關結構體,新增應用層對應的相關函式,新增對應的註冊與登出模組
    1. 新增file_operations結構體
    1)這個結構體中的元素是應用層與驅動層銜接的對接函式介面
    2)元素主要是函式指標,用來掛接實體函式地址
    3)每個裝置驅動都需要一個該結構體型別的變數
    4)裝置驅動向核心註冊時提供該結構體型別的變數
    2. 新增應用層對應的相關函式
    1)在上面結構體中填充了應用層中對應的介面,需要在新增對應的實體函式
    2)這裡是下面這幾個函式
        (1)zhao_test_chrdev_open()函式
        (2)zhao_test_chrdev_close()函式
    3. 添加註冊與登出模組
    1)手動自己分配主裝置號
        (1)在chrdev_init函式中添加註冊驅動的模組
           ret = register_chrdev(MYMAJOR, MYNAME, &zhao_test_fops);
        1>左值是用來接收註冊函式返回來的主裝置好
        2>引數1:是自定義的主裝置號
        3>引數2:自定義的裝置名稱
        4>引數3:結構體變數名的地址
        (2)在chrdev_exit函式中添加註銷驅動的模組
           unregister_chrdev(mymajor, MYNAME);
        1>引數1:是上面註冊函式返回來的驅動的主裝置號
        2>引數2:是自定義的裝置名稱
    2)讓核心自動分配主裝置號
        定義一個全域性變數,這個全域性變數用來接收核心分配的主裝置號,定義成全域性變數是因為在註冊與登出的時候都會用到這個變數
        (1)在chedev_init函式中
           mymajor = register_chrdev(0, MYNAME, &zhao_test_fops);
        1>左值是定義的那個全域性變數
        2>引數1:當讓核心自動分配主裝置號的時候,這裡的引數1是設定為0
        3>引數2:自定義的裝置名稱
        4>引數3:結構體變數名的地址

七. 新增應用層的程式碼,實現應用層呼叫驅動
    1. 新增應用層的程式碼
    1)上面已經在驅動層的程式碼中填充了應用層對應的介面(open、close、write、read),這裡完成那個結構體中填充的在應用層中對應的介面
      具體程式碼如實際的app.c檔案
    2)當insmod註冊驅動號成功後,再完成驅動檔案的建立,如下命令操作
       mknod /dev/zhao_test c 250 0
       這個裝置檔案"zhao_test"要與應用層宣告的那個裝置檔名字相同,否則執行應用層程式碼時會報錯
        當裝置檔案建立後,只要不關機,登出與註冊裝置號等操作不會影響裝置檔案,裝置檔案是一直存在的,除非關機...
    2. 應用層呼叫驅動
    完成驅動號的註冊,驅動檔案的建立,驅動層去應用層的介面(file_operations結構體以及內部元素的填充、驅動層的介面函式、應用層的介面函式)實現,就可以實現簡單的應層呼叫驅動了
    3. 讀寫介面
    1)要實現讀寫資料,就要在驅動層使用下面兩個函式
       copy_from_user    用來將資料從使用者空間複製到核心空間
       copy_to_user        用來將資料從核心空間複製到使用者空間
       使用這兩個函式需要定義一個字元陣列,作為核心空間的buf,這個全域性變數用來傳輸應用層和驅動層之間的資料
       這兩個函式的返回值如果成功複製則返回0,如果不成功複製則返回尚未成功複製剩下的位元組數
       兩個函式的使用方法如下
       (1)copy_from_user(kbuf, ubuf, count)
        在核心空間的write函式中新增這個函式
        引數1:核心空間的字元陣列
        引數2:應用空間的字元陣列
        引數3:用來記錄傳輸數量
       (2)copy_to_user(ubuf, kbuf, count)
        在核心空間的read函式中新增這個函式
        引數1:應用空間的字元陣列
        引數2:核心空間的字元陣列
        引數3:用來記錄傳輸數量
    2)在應用層的程式碼中新增讀與寫對應的函式,具體程式碼參見具體檔案
        開機,註冊insmod驅動,建立字元裝置(mknod),執行應用層的檔案,實現讀寫的迴環測試

八. 新增控制硬體的程式碼
    1. 在驅動中和在裸機中通過暫存器控制硬體有些不同
       1)暫存器地址不同
     原來是直接用實體地址,現在需要用該實體地址在核心虛擬地址空間相對應的虛擬地址。暫存器的實體地址是CPU設計時決定的,從datasheet中查詢到的。
       2)程式設計方法不同
     裸機中習慣直接用函式指標操作暫存器地址,而kernel中習慣用封裝好的io讀寫函式來操作暫存器,以實現最大程度可移植性
    2. 記憶體的對映方法有兩種
    1)靜態對映記憶體
          靜態對映方法的特點:
              核心移植時以程式碼的形式硬編碼,如果要更改必須改原始碼後重新編譯核心
              在核心啟動時建立靜態對映表,到核心關機時銷燬,中間一直有效
              對於移植好的核心,用不用他都在那裡
    2)動態對映記憶體
          動態對映方法的特點:
              驅動程式根據需要隨時動態的建立對映、使用、銷燬對映
              對映是短期臨時的
    (1)2種對映並不排他,可以同時使用
    (2)靜態對映類似於C語言中全域性變數,動態方式類似於C語言中malloc堆記憶體
    (3)靜態對映的好處是執行效率高,壞處是始終佔用虛擬地址空間;動態對映的好處是按需使用虛擬地址空間,壞處是每次使用前後都需要程式碼去建立對映&銷燬對映
    3. 靜態對映記憶體操作led
    1)三星版本核心中的靜態對映表
      (1)主對映表位於:arch/arm/plat-s5p/include/plat/map-s5p.h
         1>CPU在安排暫存器地址時不是隨意亂序分佈的,而是按照模組去區分的。每一個模組內部的很多個暫存器的地址是連續的。所以核心在定義暫存器地址時都是
           先找到基地址,然後再用基地址+偏移量來尋找具體的一個暫存器
         2>map-s5p.h中定義的就是要用到的幾個模組的暫存器基地址。
         3>map-s5p.h中定義的是模組的暫存器基地址的虛擬地址。
      (2)虛擬地址基地址定義在:arch/arm/plat-samsung/include/plat/map-base.h
         #define S3C_ADDR_BASE    (0xFD000000)     // 三星移植時確定的靜態對映表的基地址,表中的所有虛擬地址都是以這個地址+偏移量來指定的
      (3)GPIO相關的主對映表位於:arch/arm/mach-s5pv210/include/mach/regs-gpio.h
         表中是GPIO的各個埠的基地址的定義
      (4)GPIO的具體暫存器定義位於:arch/arm/mach-s5pv210/include/mach/gpio-bank.h
    2)在驅動層程式碼中用巨集定義定義好暫存器的虛擬地址,例如下
         #define GPJ0CON          S5PV210_GPJ0CON
         #define GPJ0DAT          S5PV210_GPJ0DAT
         #define GPD0CON          S5PV210_GPD0CON
         #define GPD0DAT          S5PV210_GPD0DAT
         #define rGPJ0CON        *((volatile unsigned int *)GPJ0CON)
          #define rGPJ0DAT        *((volatile unsigned int *)GPJ0DAT)
         #define rGPD0CON        *((volatile unsigned int *)GPD0CON)
         #define rGPD0DAT        *((volatile unsigned int *)GPD0DAT)
      在chrdev_init函式中新增對控制暫存器的資料寫入程式碼,例如下
        rGPJ0CON = 0X11111111;
            rGPD0CON = 0X1111;
      在open函式模組中新增對資料暫存器的資料寫入程式碼,例如下
        rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
            rGPD0DAT = (0<<1);
    這樣新增這些程式碼後,
    開機,insmod,mknod,執行應用層的程式碼後就會控制硬體LED的點亮與熄滅
    還可以通過操作write介面從應用層向驅動層傳入使用者的具體操作來實現對硬體LED具體控制(如:on,off,flash等),具體程式碼可參見具體檔案...
    4. 動態對映記憶體操作led
    1)使用時:要先申請再對映
      使用完:先解除對映再釋放申請
      注:在解除對映之前要關閉對硬體的所有操作
    2)暫存器分開對映來控制硬體
        分下面4步來完成:
        (1)申請記憶體:使用request_mem_region函式;用法如下:
        if(!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON")){
                return -EINVAL;
            }
        函式中:
            引數1:暫存器的實體地址
            引數2:暫存器所佔空間大小
        申請成功的話函式返回0;如果不成功的話,return -EINVAL;
        (2)動態對映虛擬記憶體:使用ioremap函式
        pGPJ0CON = ioremap(GPJ0CON_PA, 4);
        函式中:
            左值是對映到的虛擬地址
            引數1:暫存器實體地址
            引數2:所佔空間大小
        (3)解除對映:使用iounmap(pGPJ0CON);函式
        函式中:
            引數:對映到的虛擬地址
        (4)釋放申請:使用release_mem_region(GPJ0CON_PA, 4);函式
        函式中:
            引數1:暫存器的實體地址
            引數2:所佔空間大小
    3)暫存器一起對映來控制硬體
      具體的實現見下一章的最後......