首頁  >  文章  >  系統教程  >  Linux中級-「驅動」 控制硬體必須學會的底層知識

Linux中級-「驅動」 控制硬體必須學會的底層知識

WBOY
WBOY轉載
2024-02-12 15:21:131249瀏覽
Linux中级——“驱动” 控制硬件必须学会的底层知识

驅動認知

1. 什麼是驅動器

驅動就是將底層硬體設備的操作進行封裝,並向上層提供函數介面。

設備分類:linux系統將設備分為3類:字元設備、區塊設備、網路設備

  • 字元設備:指只能一個位元組一個位元組讀寫的設備,不能隨機讀取設備記憶體中的某一數據,讀取資料需要按照先後順序。字元設備是面向流的設備,常見的字元設備有滑鼠、鍵盤、串列埠、控制台和LED設備等,字元設備驅動程式通常至少要實現open、close、read和write的系統呼叫字元終端(/dev/console)和串列埠(/dev/ttyS0以及類似設備)就是兩個字元設備,它們能很好的說明「流」這種抽象概念。
  • #區塊設備:指可以從設備的任意位置讀取一定長度資料的設備。塊設備包括硬碟、磁碟、USB和SD卡等。
  • 網路設備:網路設備可以是硬體設備,如網卡; 但也可以是純粹的軟體設備, 例如回環介面(lo).網路介面負責傳送和接收資料封包。 Linux中级——“驱动” 控制硬件必须学会的底层知识

#我們來舉一個例子來說一下整體的呼叫過程

  1. 在上層我們呼叫c語言open函數open("/dev/pin4",O_RDWR); 呼叫/dev下的pin4以可讀可寫的方式打開,**==對於上層open呼叫到核心時會發生一次軟中斷中斷號是0X80,從用戶空間進入到核心空間==**
  2. # open會呼叫到system_call(核心函數),system_call會根據/dev/pin4裝置名,去找出你要的裝置號碼。
  3. 再調到虛擬檔案VFS為了上層呼叫到確切的硬體統一化),呼叫VFS裡的sys_open,sys_open會找到在驅動鍊錶裡面,根據主設備號和次設備號找到引腳4裡的open函數,我們在引腳4裡的open是對寄存器操作

在這裡插入圖片描述

#我們寫驅動無非就是做新增驅動新增驅動做哪些事呢?

  1. 設備名稱
  2. 設備號碼
  3. 裝置驅動函數 (操作暫存器 來驅動 IO口)

#

==綜上所述==如果想要開啟dev下面的pin4腳,過程是:使用者態呼叫open “/de/pin4”,O_RDWR),對於核心來說,上層呼叫open函數會觸發一個軟中斷(系統呼叫專用,中斷號是0x80,0x80代表發生了一個系統呼叫),系統進入核心態,並走到system_call,可以認為這個就是此軟中斷的中斷服務程式入口,然後透過傳遞過來的系統呼叫號碼來決定呼叫相應的系統呼叫服務程序(這裡是呼叫VFS中的sys_open)。 sys_open會在核心的驅動鍊錶裡面根據裝置名稱和裝置號查找到相關的驅動函數每一個驅動函數是一個節點),**= =驅動函數裡面有透過暫存器操控IO口的程式碼,進而可以控制IO口實現相關功能==**。

2. 各分態的詳解

#使用者狀態:

#」

  • # 是指使用者編寫程式、執行程式的層面,使用者態在開發時需要C的基礎和C庫,C庫講到文件,進程,進程間通信,線程,網絡,介面(GTk) 。 C函式庫(是linux標準函式庫一定有):就是Clibary,提供了程式支配核心幹活的接口,呼叫的open,read,write,fork,pthread,socket由此處封裝實現,由寫的應用程式調用,C庫中的各種API調用的是內核狀態,支配內核幹活

核心態:

#”

  • 使用者要使用某個硬體裝置時,需要核心狀態的裝置驅動程式,進而驅動硬體幹活,就例如先前文章裡面所提到的 wiringPi庫就是提供了使用者操控硬體設備的介面,在沒有wiringPi函式庫時就需要自己實作wiringPi函式庫的功能,就是自己寫裝置驅動程式。這樣當我們拿到另一種類型的板子時,同樣也可以完成開發。

  • 在linux中一切皆是文件,各種的文件和裝置(例如:滑鼠、鍵盤、螢幕、flash、記憶體、網路卡、如下圖:)都是文件,那既然是檔案了,就可以使用檔案操作函數來操作這些裝置Linux中级——“驱动” 控制硬件必须学会的底层知识

  • 有一個問題,open、read等這些檔案操作函數是如何知道開啟的檔案是哪一種硬體設備呢? ①在open函數裡面輸入對應的檔案名稱,進而操控對應的裝置。 ②通過 ==設備號(主設備號碼和次設備號)== 。除此之外我們還要了解這些驅動程式的位置,以及如何實現這些驅動程序,每一種硬體設備對應不同的驅動(這些驅動程式有我們自己來實現)

  • Linux的設備管理是和檔案系統緊密結合的各種設備都以檔案的形式存放在/dev目錄下,稱為==設備文件==*。應用程式可以開啟、關閉和讀取和寫入這些設備文件,完成對設備的操作,就像操作普通的資料檔案一樣。 **為了管理這些設備,系統為設備編了號碼**,*每個設備號碼又分為==主設備號== 和==次設備號==*(如下圖所示:)。 **Linux中级——“驱动” 控制硬件必须学会的底层知识*#**主設備號碼**用來區分不同種類的設備,而**次設備號用來區分相同類型的多個設備。 對於常用設備,Linux有約定俗成的編號,如硬碟的主設備號碼是3。 ****一個字元設備或區塊設備都有一個主設備號碼和次設備號碼==主設備號和次設備號統稱為設備號==**。

    #主設備號碼用來表示一個特定的驅動程式。
    次裝置號碼用來表示使用該驅動程式的各裝置。

    #例如一個嵌入式系統,有兩個LED指示燈,LED燈需要獨立的開啟或關閉。那麼,可以寫一個LED燈的字元裝置驅動程式,可以將其主裝置號碼註冊成5號裝置次裝置號分別為1和2。這裡,次設備號就分別表示兩個LED燈。

==驅動鍊錶==

#

管理所有裝置的驅動,新增或尋找
新增是發生在我們寫完驅動程序,載入到核心
查找是在呼叫驅動程序,由應用層用戶空間去尋找使用open函數

驅動插入鍊錶的順序由設備號檢索,就是說主設備號和次設備號除了能區分不同種類的設備和不同類型的設備,還能起到將驅動程式載入到鍊錶的某個位置,在下面介紹的驅動程式碼的開發無非就是新增驅動(新增裝置號、裝置名稱和裝置驅動函數)和呼叫驅動

  • #system_call函數是怎麼找到詳細的系統呼叫服務例程的呢? 透過系統呼叫號碼來尋找系統呼叫表格sys_call_table! 軟中斷指令INT 0x80運行時,系統呼叫號碼會被放入eax暫存器中,system_call函數能夠讀取eax暫存器取得,然後將其乘以4,產生偏移位址,然後以sys_call_table為基址。基址加上偏移位址,就能夠得到詳細的系統呼叫服務例程的位址了!然後就到了系統呼叫服務例程了。

補充:

  1. # 每個系統呼叫都對應一個系統呼叫號,而係統呼叫號碼就對應核心中的對應處理函數。
  2. 所有系統呼叫都是透過中斷0x80來觸發的。
  3. 使用系統呼叫時,透過eax 暫存器將系統呼叫號碼傳遞到內核,系統呼叫的入參透過ebx、ecx…依序傳遞到內核
  4. # 和函數一樣,系統呼叫的回傳值會保存在eax中,所有要從eax中取出

3. 字元裝置驅動程式工作原理

#字元裝置驅動程式運作原理在linux的世界裡一切皆文件,所有的硬體裝置操作到應用層都會被抽象化成文件的操作。我們知道如果應用層要存取硬體設備,它必定要呼叫到硬體對應的驅動程式。 Linux核心有這麼多驅動程序,應用怎麼才能精確的呼叫到底層的驅動程式呢?

==必須知道的知識:==

  1. 在Linux檔案系統中,每個檔案都用一個struct inode結構體來描述,這個結構體記錄了這個檔案的所有信息,例如檔案類型,訪問權限等。
  2. 在linux作業系統中,每個驅動程式在應用程式層的/dev目錄或其他如/sys目錄下都會有一個檔案與之對應。
  3. 在linux作業系統中, 每個驅動程式都有一個裝置號碼
  4. 在linux作業系統中,每開啟一次文件,Linux作業系統會在VFS層分配一個****struct file結構體來描述開啟的文件
Linux中级——“驱动” 控制硬件必须学会的底层知识

(1) 當open函數開啟裝置檔案時,可以根據裝置檔案對應的struct inode結構體所描述的信息,可以知道接下來要操作的裝置類型(字元裝置還是區塊裝置) ,也會分配一個struct file結構體。

(2) 根據struct inode結構體裡面記錄的設備號,可以找到對應的驅動程式。這裡以字元設備為例。在Linux作業系統中每個字元設備都有一個struct cdev結構體。此結構體描述了字元設備所有訊息,其中最重要的一項是字元設備的操作函數介面。

(3) 找到struct cdev結構體後,linux核心就會將struct cdev結構體所在的記憶體空間首位址記錄在struct inode結構體i_cdev成員中,將struct cdev結構體中的記錄的函數操作接口位址記錄在struct file結構體的f_ops成員。

(4) 任務完成,VFS層會為應用程式傳回一個檔案描述符(fd)。這個fd是和struct file結構體對應的。接下來上層應用程式就可以透過fd找到struct file,然後在struct file找到操作字元裝置的函數介面file_operation了。

其中,cdev_init和cdev_add在驅動程式的入口函數中就已經被調用,分別完成字元裝置與file_operation函數操作介面的綁定,和將字元驅動註冊到核心的工作。

基於框架編寫驅動程式碼:

  • 上層呼叫程式碼:操作驅動的上層程式碼(pin4test.c):
#
#include 
#include 
#include 
#include 

void main()
{
        int fd,data;
        fd = open("/dev/pin4",O_RDWR);
        if(fdprintf("open fail\n");
                perror("reson:");
        }
        else{
                printf("open successful\n");
        }
        fd=write(fd,'1',1);
}

-核心驅動 **==最簡單的字元裝置驅動框架==**:

字元裝置驅動框架代碼

#
#include    //file_operations声明
#include     //module_init  module_exit声明
#include       //__init  __exit 宏定义声明
#include   //class  devise声明
#include    //copy_from_user 的头文件
#include      //设备号  dev_t 类型声明
#include           //ioremap iounmap的头文件

static struct class *pin4_class;  
static struct device *pin4_class_dev;

static dev_t devno;                //设备号,devno是用来接收创建设备号函数的返回值,销毁的时候需要传这个参数
static int major =231;       //主设备号
static int minor =0;      //次设备号
static char *module_name="pin4";   //模块名

//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
    printk("pin4_open\n");  //内核的打印函数和printf类似   
    return 0;
}

//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
 
 printk("pin4_write\n");  //内核的打印函数和printf类似
    return 0;
}
//将上面的函数赋值给一个结构体中,方便下面加载到到驱动链表中去
static struct file_operations pin4_fops = {
//static防止其他文件也有同名pin4_fops
//static限定这个结构体的作用,仅仅只在这个文件。
    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};
/*
上面的代码等同于以下代码(但是在单片机keil的编译环境里面不允许以上写法):
里面的每个pin4_fops结构体成员单独赋值
static struct file_operations pin4_fops;  
    pin4_fops.owner = THIS_MODULE;
    pin4_fops.open  = pin4_open;
    pin4_fops.write = pin4_write;
*/
//static限定这个结构体的作用,仅仅只在这个文件。


int __init pin4_drv_init(void)   //真实的驱动入口
{

    int ret;
    devno = MKDEV(major,minor);  //2. 创建设备号
    ret   = register_chrdev(major, module_name,&pin4_fops);  
    //3. 注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中

    pin4_class=class_create(THIS_MODULE,"myfirstdemo");//由代码在dev下自动生成设备,创建一个类
    pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); 
     //创建设备文件,先有上面那一行代码,创建一个类然后这行代码,类下面再创建一个设备。

 
    return 0;
}

void __exit pin4_drv_exit(void)
{

    device_destroy(pin4_class,devno);//先销毁设备
    class_destroy(pin4_class);//再销毁类
    unregister_chrdev(major, module_name);  //卸载驱动

}

module_init(pin4_drv_init);  //入口,内核加载驱动的时候,这个宏(不是函数)会被调用,去调用pin4_drv_init这个函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

手動建立裝置名稱

#
  • 上面這個字元裝置驅動程式碼裡面有讓程式碼自動的在dev下面產生裝置除此之外我們還可以手動建立裝置名稱。使用指令:sudo mknod 設備名字 設備類型(c表示字元設備驅動) 主設備號 次設備號 b :create a block (buffered) pecial file。 c, u:create a character (unbuffered) special file。 p:create a FIFO, 刪除手動建立的裝置名稱直接rm就好。如下圖所示:

驅動框架執行流程:

  • 透過上層程式開啟某個裝置,如果沒有驅動,執行就會報錯, 在核心驅動中,上層系統呼叫open,wirte函數會觸發sys_call 、sys_call會呼叫sys_open,sys_write、sys_open,和sys_write透過主裝置號碼在內核的驅動鍊錶裡把裝置驅動找出來,執行裡面的open和write、我們為了整個流程順利進行,我們要先準備好驅動(裝置驅動檔)。

  • 裝置驅動程式檔案有固定框架:

    1. module_init(pin4_drv_init); //入口 去呼叫 pin4_drv_init函數
    2. #int __init pin4_drv_init(void) //真實的驅動入口
    3. 驅動入口devno = MKDEV(major,minor); // 建立裝置號碼
    4. register_chrdev(major, module_name,&pin4_fops); //註冊驅動程式 告訴內核,把上面準備好的結構體加入到核心驅動的鍊錶中
    5. #pin4_class=class_create(THIS_MODULE,"myfirstdemo");//由程式碼在dev下自動產生裝置,建立一個類別
    6. pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //建立裝置檔案。
    7. 主要是要讓/dev下多了個檔案供我們上層可以open
    8. # 如果沒有,也可以手動sudo mknod 設備名字 設備類型(c表示字元設備驅動) 主設備號碼 次設備號碼的去創造設備

##驅動模組程式碼編譯

驅動模組程式碼編譯

#驅動模組程式碼編譯(模組的編譯需要配置過的核心原始碼,編譯、連線後產生的核心模組後綴為**.ko,編譯過程會先到核心原始碼目錄下,讀取頂層的Makefile文件,然後再返回模組原始碼所在目錄。):**

  • 使用下面的的代码:(就是上面的驱动架构代码)
#include             //file_operations声明
#include     //module_init  module_exit声明
#include       //__init  __exit 宏定义声明
#include         //class  devise声明
#include    //copy_from_user 的头文件
#include      //设备号  dev_t 类型声明
#include           //ioremap iounmap的头文件


static struct class *pin4_class;
static struct device *pin4_class_dev;

static dev_t devno;                //设备号
static int major =231;                     //主设备号
static int minor =0;                       //次设备号
static char *module_name="pin4";   //模块名

//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
        printk("pin4_open\n");  //内核的打印函数和printf类似

        return 0;
}
//read函数
static int pin4_read(struct file *file,char __user *buf,size_t count,loff_t *ppos)
{
        printk("pin4_read\n");  //内核的打印函数和printf类似

        return 0;
}

//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{

        printk("pin4_write\n");  //内核的打印函数和printf类似
        return 0;
}

static struct file_operations pin4_fops = {

        .owner = THIS_MODULE,
        .open  = pin4_open,
        .write = pin4_write,
        .read  = pin4_read,
};
//static限定这个结构体的作用,仅仅只在这个文件。
int __init pin4_drv_init(void)   //真实的驱动入口
{

        int ret;
        devno = MKDEV(major,minor);  //创建设备号
  ret   = register_chrdev(major, module_name,&pin4_fops);  //注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中

        pin4_class=class_create(THIS_MODULE,"myfirstdemo");//让代码在dev下自动>生成设备
        pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name);  //创建设备文件


        return 0;
}

void __exit pin4_drv_exit(void)
{

        device_destroy(pin4_class,devno);
        class_destroy(pin4_class);
        unregister_chrdev(major, module_name);  //卸载驱动
}
module_init(pin4_drv_init);  //入口,内核加载驱动的时候,这个宏会被调用,去调用pin4_drv_init这个函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
  • 在导入虚拟机的内核代码中找到字符设备驱动的那一个文件夹:/SYSTEM/linux-rpi-4.19.y/drivers/char将以上代码复制到一个文件中,然后下一步要做的是就是:将上面的驱动代码编译生成模块,再修改Makefile。(你放那个文件下,就改哪个文件下的Makefile)
  • 文件内容如下图所示:(-y表示编译进内核,-m表示生成驱动模块,CONFIG_表示是根据config生成的) 所以只需要将obj-m += pin4drive.o添加到Makefile中即可。下图:Makefile文件图Linux中级——“驱动” 控制硬件必须学会的底层知识
  • 编译生成驱动模块,将生成的**.ko文件发送给树莓派**然后回/SYSTEM/linux-rpi-4.19.y下使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules进行编译生成驱动模块。然后将生成的.ko文件发送给树莓派:scp drivers/char/pin4driver.ko pi@192.168.0.104:/home/pi编译生成驱动模块会生成以下几个文件:Linux中级——“驱动” 控制硬件必须学会的底层知识
  • .o的文件是object文件,.ko是kernel object,与.o的区别在于其多了一些sections,比如.modinfo.modinfo section是由kernel source里的modpost工具生成的, 包括MODULE_AUTHOR, MODULE_DESCRIPTION, MODULE_LICENSE, device ID table以及模块依赖关系等等。depmod 工具根据.modinfo section生成modules.dep, modules.*map等文件,以便modprobe更方便的加载模块。

  • 編譯過程中,經歷了這樣的步驟
  1. # 先進入Linux核心所在的目錄,並編譯出pin4drive.o檔
  2. 運行MODPOST會產生臨時的pin4drive.mod.c文件,而後根據此文件編譯出pin4drive.mod.o,
  3. 之後連接pin4drive.o和pin4drive.mod.o檔案得到模組目標檔pin4drive.ko,
  4. 最後離開Linux核心所在的目錄。

pin4test.c (上層呼叫程式碼) 進行交叉編譯後傳送給樹莓派,就可以看到pi目錄下存在發送過來的.ko文件pin4test這兩個文件,如下圖所示:Linux中级——“驱动” 控制硬件必须学会的底层知识

載入核心驅動程式

#然後使用指令:sudo insmod pin4drive.ko載入核心驅動(相當於透過insmod呼叫了module_init這個宏,然後將整個結構體載入到驅動鍊錶中) 載入完成後就可以在dev下面看到名字為pin4的裝置驅動程式(這個和驅動程式碼裡面static char *module_name=”pin4″; //模組名稱這行程式碼有關),裝置號碼也和代碼裡面相關。

lsmod可以查看驅動程式已經裝進去了。

  • 我們再執行./pin4test 執行上層程式碼 執行上層程式碼出現以下錯誤:表示沒有權限Linux中级——“驱动” 控制硬件必须学会的底层知识使用指令:sudo chmod 666 /dev/pin4為pin4賦予權限,讓所有人都可以打開成功。

然後再執行pin4test表面上看沒有任何資訊輸出,其實核心裡面有列印訊息只是上層看不到如果想要查看核心列印的資訊可以使用指令:dmesg |grep pin4。如下圖所示:表示驅動呼叫成功

Linux中级——“驱动” 控制硬件必须学会的底层知识

在安裝驅動程式後可以使用指令:sudo rmmod 驅動名稱(不需要寫入ko)將驅動卸載。

為什麼產生驅動模組需要在虛擬機器上產生

  • 為什麼產生驅動模組需要在虛擬機器上產生?樹莓派不行嗎?

    產生驅動模組需要編譯環境(linux原始碼並且編譯,需要下載和系統版本相同的Linux核心原始碼),也可以在樹莓派上面編譯,但在樹莓派裡編譯,效率會很低,要非常久。這篇文章有一個講樹莓派驅動的本地編譯。

以上是Linux中級-「驅動」 控制硬體必須學會的底層知識的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:lxlinux.net。如有侵權,請聯絡admin@php.cn刪除