(三)寫一個完整的Linux驅動程序訪問硬件並寫應用程序進行測試

本系列導航
(一)初識Linux驅動
(二)Linux設備驅動的模塊化編程
(三)寫一個完整的Linux驅動程序訪問硬件並寫應用程序進行測試
(四)Linux設備驅動之多個同類設備共用一套驅動
(五)Linux設備驅動模型介紹
(六)Linux驅動子系統-I2C子系統
(七)Linux驅動子系統-SPI子系統
(八)Linux驅動子系統-PWM子系統
(九)Linux驅動子系統-Light子系統
(十)Linux驅動子系統-背光子系統
(十一)Linux驅動-觸摸屏驅動

1. Linux設備驅動的分類

Linux內核驅動按照訪問方式,可以分爲以下三類:
字符設備驅動
字符設備是能夠像訪問字節流(類似文件)的方式一樣被訪問的設備,最終在文件系統中以設備文件的形式存在。
常見的字符設備:鼠標、鍵盤(IO設備),LCD、Camera(幀緩衝設備)等。
塊設備驅動
塊設備和字符設備的區別在於內核內部管理數據的方式,塊設備的訪問方式是按照塊進行隨機訪問的。
常見的塊設備:磁盤、flash等存儲設備。
網絡設備驅動
如網卡。

2. Linux字符設備驅動框架

1). 設備號

Linux內核中有很多的字符設備驅動,內核是如何區分它們的? 每個字符設備都有一個唯一的標識 – 設備號
設備號的本質: 32位的無符號整數(dev_t)
設備號由兩部分組成:
  1 – 高12位稱爲主設備號,表明這個設備屬於哪一類設備。
  2 – 低20位成爲次設備號,表明這個設備是同類設備中得具體哪一個。

設備號的申請方法:
第一種方法:靜態定義並註冊設備號
首先查看系統中有哪些設備號沒有被分配給具體的設備,然後確定一個給當前的設備使用(cat /proc/devices可以看哪些號被佔用了),定義方法如下:
dev_t devno = 主設備號<<20 | 次設備號;
或者使用系統接口進行組合

int maj = xx, int min = xx; 
dev_t devno = MKDEV(maj, min);

註冊設備號 – 使申請的設備號生效並保證設備號在Linux內核中的唯一性
使用下面的接口:

int register_chrdev_region(dev_t from, unsigned count, const char *name)

參數:
from: 要註冊的設備號
count:要註冊多少個設備號,例如count = 3, 主設備號是255, 次設備號是0,那麼將按照順序依次註冊三個設備號,分別是(主:255,從:0)、(255,1)、(255,2)
name:給要註冊的設備命名,註冊成功可以通過cat /proc/devices查看到
第二種方法:動態申請並註冊設備號
此方法無需自己去確定哪個設備號可用,內核會查詢哪個設備號沒有被使用,然後分配給當前驅動進行註冊,所以大部分驅動都採用這種註冊方法,使驅動更加具有通用性(如果用靜態註冊,你選的設備號在當前設備上沒有使用,但是當這個驅動移植到其他的設備上,可是其他設備上的某個驅動也使用的這個這個設備號,那麼這個驅動就會註冊失敗)。
函數原型:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,  const char *name) 

功能:申請一個或多個設備號並進行註冊
參數:
dev:要註冊的設備的設備號,輸入參數,內核會尋找沒有使用的設備號,然後填充到這個dev參數中。
baseminor:主設備號由內核來確定,次設備號由我們自己去確定,baseminor就對應要申請的設備號的次設備號。
count:可以一次申請多個設備號,count表示要申請的設備號的數量,當申請多個設備號時,他們的主設備號一致,次設備號會在baseminor的基礎上依次加1。
name:要註冊的設備的名字,註冊成功可以通過cat /proc/devices查看到

最後,無論通過哪種方式註冊的設備號,在卸載模塊的時候都需要將註冊的設備號資源進行釋放:

void unregister_chrdev_region(dev_t from, unsigned count)

功能:釋放一個已經註冊的設備號
參數:
from:要釋放的設備號
count:要一次釋放的設備號的數量,當釋放多個設備號時,系統會從from開始,依次加1作爲新的設備號進行釋放

2).字符設備操作集合 – file_operations結構體

設備驅動有各種各樣的, 鼠標驅動需要獲取用戶的座標以及單雙擊動作、LCD驅動需要寫framebuffer等等,但是對上層開發調用這些驅動的人來說,他們可能不懂也不關心底層設備是如何工作的,爲了簡化上層應用的操作,驅動程序給上層提供了統一的操作接口–open、read、write等,這樣,對應做應用開發的人來說,不管你是什麼樣的設備,我只需要去打開(open)你這個設備,然後進行讀寫等操作就可以操作這個設備了。那麼,驅動程序如何實現這樣統一的接口呢?需要實現下面的file_operations結構體:

struct file_operations {
       struct module *owner;
       ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
       ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
       int (*open) (struct inode *, struct file *);
       int (*release) (struct inode *, struct file *);
       ...
} 

這裏只列出了幾個最基本的成員:
owner:一般填充THIS_MODULE,表示這個驅動(模塊)歸自己所有(這個概念對於初學者可能難以理解,到後面我會繼續說明)
open:打開設備驅動的函數指針
release:關閉設備驅動的函數指針,爲了對每個設備驅動的訪問保護,所以用戶必須先打開設備,才能對設備進行讀寫等操作,操作完必須再關掉。
read:讀設備驅動的函數指針(比如用戶可以通過read接口讀取按鍵驅動的按鍵狀態等)
write:寫設備驅動的函數指針(比如用戶可以通過write接口寫LCD驅動的framebuffer顯存,將畫面顯示再lcd上)
用法:
定義一個屬於自己設備的操作集合xxx_fops,xxx通常命名爲設備的名字,例如lcd_fops, key_fops等。

struct file_operations  xxx_fops ={  
       .owner   = THIS_MODULE,   //表示這個模塊爲自己所有
       .open    = xxx_open,      //當用戶調用open接口時,內核就會根據系統調用來調用對應的xxx_fops裏面的xxx_open函數(xxx表示自己命名)
       .release = xxx_close,       
       .read    = xxx_read,
       ...
};

xxx_open、xxx_read等函數需要自己去實現,根據不同的驅動,去做不同的事情,從而達到了不同的驅動給上層提供統一的接口。

3). 字符設備的核心 – cdev結構體

分配、設置、註冊cdev結構體
內核用cdev結構體來表示一個字符設備,所以每個字符設備驅動,都需要註冊一個cdev結構體

struct cdev {	
	struct kobject kobj;
  	struct module *owner;
  	const struct file_operations *ops;
  	struct list_head list;
  	dev_t dev;
  	unsigned int count;
};

owner:一般填充THIS_MODULE,表示這個驅動(模塊)歸自己所有。
ops:對應這個設備的文件操作集合。
list:內核中有很多字符設備,每個設備對應一個自己的cdev,這些cdev通過這個list連在一起,當註冊一個新的cdev時,就會通過cdev裏面的list掛到內核的cdev鏈表上。
count:同類設備,可以一次註冊多個cdev,但是他們的操作方法(fops)是一樣的,比如usb設備,多個usb共用一套操作方法(fops),但是每個usb都有自己的cdev。

分配(創建)cdev

struct cdev cdev;

設置(初始化)cdev,函數原型:

void cdev_init(struct cdev *, const struct file_operations *);

使用:

cdev_init(&cdev, &xxx_fops);

註冊cdev結構體 – 添加一個字符設備(cdev)到系統中,函數原型:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)

@p: 要註冊的cdev結構體。
@dev: 第一個設備號。
@count: 與此設備對應的連續的次設備號的數量 – 也就是要註冊的cdev的數量,當count > 1時,會向系統註冊多個cdev結構體,這些個cdev的fops是同一個,但是設備號的次設備號不同。
使用:

dev_add(&cdev, devno, 1);

有註冊同樣也有釋放:

void cdev_del(struct cdev *p)
cdev_del(&cdev);

完整代碼:hello.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>

dev_t devno;
int major = 255;

const char DEVNAME[] = "hello_device";

/* 2. 分配file_operations結構體 */
struct file_operations hello_fops = {
    .owner = THIS_MODULE,
};

struct cdev cdev;

static int hello_init(void)
{
    int ret;
    printk("%s : %d\n", __func__, __LINE__);
	
    /* 1. 生成並註冊設備號 */
    devno = MKDEV(major, 0);
    ret = register_chrdev_region(devno, 1, DEVNAME);
    if (ret < 0)
    {
        printk("%s : %d fail to register_chrdev_region\n", __func__, __LINE__);
        return -1;
    }

    /* 3. 分配、設置、註冊cdev結構體 */
    cdev.owner = THIS_MODULE;
    cdev_init(&cdev, &hello_fops);
    ret = cdev_add(&cdev, devno, 1);
    if (ret < 0)
    {
        printk("%s : %d fail to cdev_add\n", __func__, __LINE__);
        return -1;
    }
    return 0;
}

static void hello_exit(void)
{
    printk("%s : %d\n", __func__, __LINE__);
    
    /* 釋放資源 */
    cdev_del(&cdev);
    unregister_chrdev_region(devno, 1);
}

MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);

Makefile

KERNEL_PATH := /lib/modules/`uname -r`/build
PWD := $(shell pwd)
MODULE_NAME := hello

obj-m := $(MODULE_NAME).o

all:
	$(MAKE) -C $(KERNEL_PATH) M=$(PWD)

clean:
	rm -rf .*.cmd *.o *.mod.c *.order *.symvers *.tmp *.ko

測試:
make生成hello.ko
sudo insmod hello.ko //如過出現下面的log,

insmod: ERROR: could not insert module hello.ko: File exists

說明之前安裝的沒有卸載,需要先卸載然後再安裝新的hello.ko。
lsmod | grep hello //如果打印出
說明模塊沒有問題,已經裝載到系統中。
cat /proc/devices | grep hello //打印出
說明字符設備註冊成功,255是註冊的設備號,hello_device是註冊的名字
最後別忘了卸載設備:sudo rmmod hello
有些同學會發現,明明卸載完了,但是在安裝hello.ko的時候,提示:
ERROR: could not insert module hello.ko: Operation not permitted,那麼就有可能是你的代碼中設備號沒有釋放或者cdev沒有釋放,需要檢查代碼,修改後重啓電腦再次安裝即可。

3. 實現文件操作集合

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <asm/uaccess.h>

dev_t devno;
int major = 255;
const char DEVNAME[] = "hello_device";
char data[64]  = "Hello world!";

int hello_open(struct inode * ip, struct file * fp)
{
    printk("%s : %d\n", __func__, __LINE__);
    
    /* 一般用來做初始化設備的操作 */
    return 0;
}

int hello_close(struct inode * ip, struct file * fp)
{
    printk("%s : %d\n", __func__, __LINE__);
    
    /* 一般用來做和open相反的操作,open申請資源,close釋放資源 */
    return 0;
}

ssize_t hello_read(struct file * fp, char __user * buf, size_t count, loff_t * loff)
{
    int ret;
    
    /* 將用戶需要的數據從內核空間copy到用戶空間(buf) */
    printk("%s : %d\n", __func__, __LINE__);
    if ((ret = copy_to_user(buf, data, count)))
    {
        printk("copy_to_user err\n");
        return -1;
    }
    return count;
}

ssize_t hello_write(struct file * fp, const char __user * buf, size_t count, loff_t * loff)
{
    int ret;
    
    /* 將用戶需要的數據從內核空間copy到用戶空間(buf) */
    printk("%s : %d\n", __func__, __LINE__);
    if ((ret = copy_from_user(data, buf, count)))
    {
        printk("copy_from_user err\n");
        return -1;
    }
    return count;
}

/* 2. 分配file_operations結構體 */
struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open  = hello_open,
    .release = hello_close,
    .read = hello_read,
    .write = hello_write
};
struct cdev cdev;

static int hello_init(void)
{
    int ret;
    printk("%s : %d\n", __func__, __LINE__);
    
    /* 1. 生成並註冊設備號 */
    devno = MKDEV(major, 0);
    ret  = register_chrdev_region(devno, 1, DEVNAME);
    if (ret != 0)
    {
        printk("%s : %d fail to register_chrdev_region\n", __func__, __LINE__);
        return -1;
    }
    
    /* 3. 分配、設置、註冊cdev結構體 */
    cdev.owner = THIS_MODULE;
    ret = cdev_add(&cdev, devno, 1);
    cdev_init(&cdev, &hello_fops);
    if (ret < 0)
    {
        printk("%s : %d fail to cdev_add\n", __func__, __LINE__);
        return -1;
    }
    printk("success!\n");
    return 0;
}

static void hello_exit(void)
{
    printk("%s : %d\n", __func__, __LINE__);
      
    /* 釋放資源 */
    cdev_del(&cdev);
    unregister_chrdev_region(devno, 1);
}

MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);

注意:
hello_read是將內核空間的數據copy到用戶空間,hello_write是將用戶數據copy到內核空間。用戶空間(應用程序)和內核空間(驅動)數據的交互一定要用copy_to_user和copy_from_user。在read和write中還應該判斷參數的合法性,比如傳入的count是負數肯定是非法的,這一步也很重要,但是爲了代碼簡潔就沒加。這只是個框架,以後根據具體的驅動,再在函數體裏填充具體的操作。

4. 寫應用程序測試驅動 app.c

#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

int main(char argc, char * argv[])
{
    int fd;
    int ret;
    char buf[64];
    
    /* 將要打開的文件的路徑通過main函數的參數傳入 */
    if (argc != 2)
    {
        printf("Usage: %s <filename>\n", argv[0]);
        return -1;
    }
    
    fd = open(argv[1], O_RDWR);
    if (fd < 0)
    {
        perror("fail to open file");
        return -1;
    }
    
    /* read data */
    ret = read(fd, buf, sizeof(buf));
    if (ret < 0)
    {
        printf("read err!");
        return -1;
    }
    printf("buf = %s\n", buf);
    
    /* write data */
    ret = write(fd, buf, sizeof(buf));
    if (ret < 0)
    {
        printf("read err!");
        return -1;
    }
    
    close(fd);
    return 0;
}

前面說過,用戶訪問字符設備驅動最終以字符設備文件的形式訪問的,所以測試無非就是寫一個應用程序,去open、read、write這個對應的設備節點,然後通過系統調用去訪問驅動裏的fops對應函數。
測試:
安裝驅動:sudo insmod hello.ko
查看是否安裝成功:cat /proc/devices 查找對應設備號和名字
創建設備節點和設備掛鉤:sudo mknod /dev/hello c 255 0
當我們執行insmod後驅動就被安裝到了內核中,但是我們要想訪問驅動,必須先創建設備節點,通過設備節點來訪問驅動,設備節點其實就是個文件,文件類型是c–字符設備文件。
/dev/hello:要創建的設備節點的名字及路徑,一般都在/dev目錄下創建。
c: 表示要創建一個字符設備。
255 0:主設備號和次設備號,表示創建的這個設備節點和對應設備號是(255,0)的這個設備關聯,這樣訪問這個設備節點就可以通過設備號唯一確定一個設備了。
ls -l /dev/hello 可以看到這個設備節點的詳細信息

crw-r--r-- 1 root root 255, 0 1126 19:40 /dev/hello

在命令行編譯並執行應用程序進行測試:

 gcc app.c -o app       //生成用戶空間的可執行程序app
 ./app  /dev/hello       //執行生成的可執行程序app,並傳入參數

打印出了我們從驅動中讀到的數據:

 buf = Hello world!

然後執行dmesg,可以看到驅動的執行過程log:

 [12752.386888] hello_init : 83
 [12752.386891] success!
 [12948.418264] hello_open : 21
 [12948.418269] hello_read : 42
 [12948.418286] hello_write : 58
 [12948.418322] hello_close : 30

5. 畫框圖解釋從應用層訪問到驅動的過程

首先了解幾個概念:
在寫驅動的時候,實現open和close函數都有兩個重要的參數struct inode和struct file結構體。
inode結構體:

struct inode {
    unsigned int  i_flags;
    dev_t       i_rdev;
    ...
 }

一切皆文件,用戶在文件系統下看到和操作的都是文件,但是這個文件對應在內核中是以一個inode結構體的形式存在的,當我們在文件系統下用touch或者mknod等命令創建文件時,內核都會創建唯一一個與之對應的inode結構體,保存這個文件的基本信息,當我們用戶操作這個文件的時候,操作系統(內核)其實操作的是對應的inode結構體,會將我們的訪問需求轉換爲對某個方法的調用,根據你打開的文件的類型進行不同的操作。
file結構體:

struct file {
    struct inode  *f_inode; /* cached value */
    const struct file_operations *f_op;
    unsigned int   f_flags;
    void       *private_data;
    ...
}

操作系統將用戶對某個文件的訪問的需求轉換爲對某個方法的調用,內核根據你打開的文件的類型進行不同的操作,當用戶打開某個文件時,實際上內核操作的是這個文件對應的inode結構體,同時內核會創建一個file結構體與之對應,這個file結構體裏面保存了用戶對這個文件(inode)結構體的操作信息(操作哪個文件:inode;以什麼方式打開的,R/W/RW等:f_flags)。
總結:
也就是說,inode結構體和文件是一一對應的關係,每個文件在內核系統中都有一個唯一的inode結構體與之對應。只有在用戶對文件進行打開操作的時候,內核空間纔會創建一個file結構體,那麼當多個用戶對同一個文件進行打開時,就會創建多個file結構體,分別保存每個用戶的操作,file結構體和文件是多對一的關係。
在這裏插入圖片描述

6. 自動創建設備節點

在實際的項目場景中,不可能每次開機讓用戶自己手動去創建設備節點然後裝載,所以需要我們在代碼中自動創建設備節點。在所有的初始化完成併成功之後加上如下:

struct class *hello_class;
hello_class = class_create(THIS_MODULE, "hello");  //hello:會在/sys/class這個目錄下創建以hello爲名的類,表示註冊的這個設備屬於hello這個類
device_create(hello_class, NULL, devno, NULL, "hello device");  //devno是對應註冊的設備號, hello device就是內核自動在/dev目錄下創建的設備節點的名字

同樣,在卸載設備的時候,也要卸載這個設備節點:

device_destroy(hello_class, devno);
class_destroy(hello_class);

例;

struct class *hello_class;

static int hello_init(void)
{
    /* 1. 生成並註冊設備號 */
    /* 2. 分配file_operations結構體 */
    /* 3. 分配、設置、註冊cdev結構體 */  
    ...  
  
    hello_class = class_create(THIS_MODULE, "hello"); 
    device_create(hello_class, NULL, devno, NULL, "hello device");  
  
    return 0;
}

static void hello_exit(void)
{
    /* 釋放資源 */
    ...
    
    device_destroy(hello_class, devno);
    class_destroy(hello_class);
}

編譯安裝完驅動後:

 /dev$ ls -l hello_device
 crw------- 1 root root 255, 0  218 18:02 hello_device
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章