1. 程式人生 > >19 字元裝置驅動基礎

19 字元裝置驅動基礎

字元裝置驅動基礎


裝置驅動通常是給使用者程序來呼叫的

最常用的是裝置驅動裡實現字元裝置驅動,實現後在”/dev/”目錄裡提供一個裝置檔案,然後使用者程序就可以通過操作該裝置檔案來呼叫驅動


如pc上的uart裝置檔案:

crw-rw- 1 root dialout 4, 64 Jun 8 09:20 /dev/ttyS0 
crw-rw- 1 root dialout 4, 65 Jun 8 09:20 /dev/ttyS1 
crw-rw- 1 root dialout 4, 66 Jun 8 09:20 /dev/ttyS2 
crw-rw- 1 root dialout 4
, 67 Jun 8 09:20 /dev/ttyS3 第一個字元'c'表示字元裝置檔案,也就是此裝置檔案對應著一個字元裝置驅動 裝置檔名字不重要,不管改成什麼名字,功能還是可以用的 裝置檔案的裝置號才是最重要的,裝置號由主裝置號和次裝置號組成 如:ttyS0的主裝置號是4,次裝置號是64 主裝置號通常表示一個字元裝置驅動,上面4個uart裝置它們驅動方法應是一樣的,可以共用一個驅動 在驅動裡可通過次裝置號來區分不同的硬體介面 裝置驅動裡在初始化時也需指定使用哪些裝置號 當用戶程序操作裝置檔案時,核心會根據裝置檔案的裝置號找到對應的裝置驅動,從而讓使用者程序通過核心與裝置驅動建立聯絡,實現呼叫驅動裡實現的功能 裝置號很重要,在系統裡是不可以重用的資源,裝置號不能重用

在”include/linux/kdev_t.h”標頭檔案裡有提供裝置號的操作巨集:

#define MINORBITS   20 //次裝置號位數
#define MINORMASK   ((1U << MINORBITS) - 1)

#define MAJOR(dev)  ((unsigned int) ((dev) >> MINORBITS)) //獲取主裝置號
#define MINOR(dev)  ((unsigned int) ((dev) & MINORMASK)) //獲取次裝置號
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi)) //由主裝置號和次裝置號生成一個裝置號

裝置號使用前需要向核心申請,使用完後需返還核心

裝置號操作方法:

#include <linux/fs.h>    

//靜態申請指定的裝置號,from指裝置號(需已指定主裝置和次裝置號),count指使用該驅動有多少個裝置(次裝置號),name為裝置名(用於檢視用,長度不能超過64位元組)
int register_chrdev_region(dev_t from, unsigned count, const char *name);

//動態申請裝置號,由核心分配沒有使用的主裝置號,分配好的裝置號存在dev(不需要初始化),baseminor指次裝置號從多少開始,count指裝置個數,name為裝置名
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

//釋放裝置號,from指裝置號,count指裝置數
void unregister_chrdev_region(dev_t from, unsigned count);

簡單的事例程式碼1(申請裝置號)(xxx.c):

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>

#define MYMA  1234
#define MYMI  3344
#define COUNT    3

dev_t devid;//用於存放裝置號 

static int __init test_init(void)
{
    int ret;

    devid = MKDEV(MYMA, MYMI);//生成一個裝置號
    ret = register_chrdev_region(devid, COUNT, "mydev"); //申請裝置號
    if (ret < 0)
        goto err0;

    //執行到這裡,則有三個裝置號(1234, 3344), (1234, 3345), (1234, 3346)
    return 0;
err0:
    return ret;
}

static void __exit test_exit(void)
{

    unregister_chrdev_region(devid, COUNT);//使用完後需回收裝置號
}

module_init(test_init);
module_exit(test_exit);

MODULE_LICENSE("GPL");

Makefile檔案:

obj-m += xxx.o

KSRC := /目錄路徑/orangepi_sdk/source/linux-3.4.112/
export ARCH := arm
export CROSS_COMPILE := arm-linux-gnueabihf-

all : 
    make -C $(KSRC) modules M=`pwd`

.PHONY : clean
clean : 
    make -C $(KSRC) modules clean M=`pwd`

編譯後,載入模組後,可通過”cat /proc/devices”檢視系統裡主裝置號的使用狀況


在linux核心裡使用”struct cdev”結構體型別的一個物件來描述一個字元裝置驅動

include <linux/cdev.h>

struct cdev {
    struct kobject kobj;//核心用於管理字元裝置驅動,kobject就是核心裡最底層的類(核心裡會自動管理此成員)
    struct module *owner;//通常設為THIS_MODULE,用於防止驅動在使用中時解除安裝驅動模組
    const struct file_operations *ops;//怎樣操作(vfs),也就是實現當用戶程序進行的open/read/write等操作時,驅動裡對應的操作
    struct list_head list;//核心連結串列節點(核心裡自動管理此成員)
    dev_t dev;//裝置號
    unsigned int count;//裝置數
};

同時核心裡也提供對cdev物件操作的函式:

#include <linux/cdev.h>

void cdev_init(struct cdev *, const struct file_operations *);//初始化cdev物件

struct cdev *cdev_alloc(void);//動態分配一個cdev物件

int cdev_add(struct cdev *, dev_t, unsigned);//設定cdev物件使用裝置號及裝置個數,再把cdev物件增加到核心裡,讓它工作起來

void cdev_del(struct cdev *);//從核心裡移除cdev物件

字元裝置驅動實現的基本流程:

1.申請裝置號:
    register_chrdev_region(...);

2.宣告一個全域性的cdev物件:
    struct cdev mycdev;

3.宣告一個全域性的file_operations的檔案操作物件:
    struct file_operations fops = {
        .owner = THIS_MODULE,
        .read = 讀函式地址,
        ....,
    };

4.初始化cdev物件,並把fops物件與cdev物件關聯起來:
    cdev_init(&mycdev, &fops);//mycdev.ops = &fops;
    mycdev.owner = THIS_MODULE; 

5.把cdev物件加入核心裡cdev_map(字元裝置驅動的雜湊表),並指定該驅動物件的裝置號:
    cdev_add(&mycdev, 裝置號, 次裝置號的個數);

6.解除安裝模組時,要把裝置驅動物件從核心裡移除,並把裝置號反註冊:
    unregister_chrdev_region(..);
    cdev_del(&mycdev);

事例程式碼2(xxx.c):

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

#define MYMA  1234
#define MYMI  3344
#define COUNT    3

dev_t devid;//用於存放裝置號
struct cdev mycdev; 

int myopen(struct inode *ind, struct file *fl)
{
    printk("in %s\n", __func__);
    return 0;
}

ssize_t myread(struct file *fl, char *__user buf, size_t len, loff_t *off)
{
    printk("in %s\n", __func__);
    return len;
}

ssize_t mywrite(struct file *fl, const char __user *buf, size_t len, loff_t *off)
{
    printk("in %s\n", __func__);
    return len;
}

struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = myopen,
    .read = myread,
    .write = mywrite,
};

static int __init test_init(void)
{
    int ret;

    devid = MKDEV(MYMA, MYMI);//生成一個裝置號
    ret = register_chrdev_region(devid, COUNT, "mydev");
    if (ret < 0)
        goto err0;

    cdev_init(&mycdev, &fops);
    mycdev.owner = THIS_MODULE;
    ret = cdev_add(&mycdev, devid, COUNT);//將cdev加入核心
    if (ret < 0)
        goto err1;  

    return 0;

err1:
    unregister_chrdev_region(devid, COUNT);
err0:
    return ret;
}

static void __exit test_exit(void)
{ 
    unregister_chrdev_region(devid, COUNT);//使用完後需回收裝置號
    cdev_del(&mycdev);
}

module_init(test_init);
module_exit(test_exit);

MODULE_LICENSE("GPL");

編譯載入驅動模組後,需要用”mknod /dev/裝置檔名 c 主裝置號 次裝置號”來建立裝置檔案與裝置號相對應:
(以後操作裝置檔案時,就可以觸發相應的裝置號所對應的裝置驅動)

mknod /dev/mydev0 c 1234 3344
mknod /dev/mydev1 c 1234 3345
mknod /dev/mydev2 c 1234 3346

然後可以寫應用程式來操作裝置檔案,也可以用命令來簡單地測試:

cat /dev/mydev0  //會觸發驅動裡的openread函式
echo "kkk" > /dev/mydev0   //會觸發驅動裡的openwrite函式