Linux驱动开发——字符设备驱动基础

一、驱动简介


Linux驱动分为字符设备驱动、块设备驱动、网络设备驱动

  • 字符设备驱动

字符设备指必须以串行顺序依次访问的设备,如led、触摸屏、鼠标等

通过open、close、read、write等系统调用访问

  • 块设备驱动

块设备可以按任意顺序访问,以块为单位进行操作,如硬盘、EMMC等

块设备和字符设备的驱动设计有很大的差异,但是也可以通过open、close、read、write等系统调用进行访问,不过一般都是使用文件系统来进行管理

  • 网络设备驱动

网络设备是面向数据包的接收和发送设计的,在文件系统中并没有对应的设备结点,通过socket接口进行访问

本文主要讲解字符设备。

二、字符设备驱动概念


Linux下一切皆文件,例如一个led设备,在文件系统中表现为一个设备节点/dev/led,应用层通过open、read、write等系统调用就可以控制led

例如想让led亮,就打开设备文件写1

int fd;
int val = 1;

fd = open("/dev/led", xxx);
write(fd, &val, sizeof(val));

这样就可以使得led被点亮了。

现在思考一个问题,为什么这样就可以使led点亮?

要想让led点亮,必须操作硬件,操作硬件这部分工作就是led的驱动所完成的

最简单的方式就是,应用层对led进行open操作,那么就对应led驱动程序中的一个led_open。应用层对led进行write操作,就对应led驱动程序的一个led_write操作,然后在led_write操作中去操控硬件,进而控制led

那么对led设备节点进行open、write系统调用,怎么才能调用到驱动程序的led_open、led_write呢?

当应用层调用open、read、write等操作时,会引发一个异常,导致系统变为内核态,然后再去执行相应的系统调用sys_open、sys_read、sys_write等

在sys_open、sys_read、sys_write中会去找到相应的驱动程序,然后再调用驱动程序的open、read、write去操作硬件

如下图所示

在这里插入图片描述

 那么在虚拟文件系统中调用sys_open,是怎么找到led驱动程序而不是其他驱动呢?

首先介绍一下设备号

每一个设备都有一个设备号,使用32位表示,其中高12位表示主设备号,低20位表示次设备号

使用ls /dev/led -l查看设备,可以得到下面信息

crw-rw----    1 root     0          10, 131 Jan  1 12:00 /dev/led

其中10, 131表示设备号,10表示主设备号,131表示次设备号

为了方便理解,我们可以认为内核中有一个字符设备数组,以主设备号为下标,字符设备本身为数组元素,字符设备中有一个文件操作集,设置了一系列的操作函数(如led_open、led_write、led_read)

sys_open等系统调用通过设备文件的主设备好找到数组中的一项,通过字符设备的文件操作集合调用到led驱动中的led_open等函数

字符设备驱动就是要完善这个fops(文件操作集),然后指定设备号,向内核注册字符设备。

三、注册字符设备


首先看一下字符设备对象

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

struct cdev {
    struct kobject kobj;
    struct module *owner; /* 所属模块 */
    const struct file_operations *ops; /* 文件操作集 */
    struct list_head list;
    dev_t dev; /* 设备号 */
    unsigned int count;
};

其中的dev_t成员定义了设备号,为32位,高12位为主设备号,低20位为次设备号

内核使用下面两个宏获取主次设备号

MAJOR(dev_t dev) //主设备号
MINOR(dev_t dev) //次设备号

使用下面该宏获取设备号

MKDEV(int major, int minor)

每个字符设备都有自己对应的设备号范围,下面介绍怎么申请设备号

3.1 申请设备号

(1)动态分配

此函数指定次设备号和设备号个数,可以动态分配主设备号

/*
 * dev:返回申请到的设备号
 * baseminor:起始次设备号
 * count:申请的设备号个数
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
            const char *name)


(2)静态分配

此函数必须主次设备号和设备号个数

/*
 * from:起始设备号
 * count:设备号个数
 */
int register_chrdev_region(dev_t from, unsigned count, const char *name)


在申请完设备号后,就可以利用此设备号去注册字符设备了。

3.2 注册字符设备

和字符设备相关的函数

void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);

cdev_init:用来关联字符设备和fops,其中file_operations就是一个文件操作集,里面设置了一系列的操作函数(例如open、read、write)

cdev_alloc:分配一个字符设备对象

cdev_add:注册字符设备

cdev_del:注销一个字符设备

注册一个字符设备的步骤

struct cdev *cdev_alloc(void); //分配字符设备

void cdev_init(struct cdev *, struct file_operations *); //绑定一个fops

int cdev_add(struct cdev *, dev_t, unsigned); //将字符设备和申请到的设备号注册进内核


下面是一个简单的字符设备驱动

mydev.c

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

static dev_t dev_id;
static struct cdev *mydev;

ssize_t mydev_read(struct file *file, char __user *data, size_t size, loff_t * loff)
{
    printk("mydev_read\n");
    
    return 0;
}

ssize_t mydev_write(struct file *file, const char __user *data, size_t size, loff_t *loff)
{
    printk("mydev_write\n");

    return 0;
}

int mydev_open(struct inode *inode, struct file *file)
{
    printk("mydev_open\n");

    return 0;
}

/* 文件操作集合 */
static struct file_operations mydev_fops = {
    .owner = THIS_MODULE,
    .read   = mydev_read,
    .open   = mydev_open,
    .write  = mydev_write,
};

static __init int mydev_init(void)
{
    /* 申请设备号 */
    alloc_chrdev_region(&dev_id, 1, 1, "mydev");

    /* 分配字符设备 */
    mydev = cdev_alloc();

    /* 设置字符设备 */
    cdev_init(mydev, &mydev_fops);

    /* 注册字符设备 */
    cdev_add(mydev, dev_id, 1);

    /* 打印申请到的主次设备号 */
    printk("major:%d; minor:%d\n", MAJOR(dev_id), MINOR(dev_id));

    return 0;
}

static __exit void mydev_exit(void)
{
    cdev_del(mydev);
    kfree(mydev);
    unregister_chrdev_region(dev_id, 1);
}

module_init(mydev_init);
module_exit(mydev_exit);

MODULE_LICENSE("GPL");


四、编译模块


编写以驱动程序后,我们应该如何编译驱动程序,有两种方法,一种是将驱动程序和内核编译到一起,一种是单独将驱动程序编写成模块。

这里介绍第二种

将驱动程序编译成模块需要编写Makefile

Makefile如下

KERN_DIR = /work/linux/kernel/

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

clean:
        make -C $(KERN_DIR) M=`pwd` modules clean
        rm -rf modules.order

obj-m   += mydev.o



其中KERN_DIR = /work/linux/kernel/表示内核源码树,这个需要根据你自己内核所在的路径修改

obj-m += mydev.o表明要编译mydev.c文件

make -C $(KERN_DIR) M=pwd modules表明跳转到内核源码树,编译模块

将Makefile和mydev.c放到一起,执行make,可以得到驱动模块mydev.ko

五、加载模块


使用insmod xxx.ko可以加载驱动模块

使用lsmod可以查看当前加载的模块

使用rmmod xxx可以卸载已加载的驱动模块

将上面的生成的mydev.ko拷贝到实验平台,执行insmod mydev.ko

一旦加载模块,内核就会运行module_init(mydev_init)指定的模块入口,mydev_init函数中,我们申请的设备号,注册了字符设备,并打印了主次设备号

可以看到打印信息

major:250; minor:1

表示主设备号250,次设备号1(你所看到的可能不同)

此时模块已经加载了,我们可以通过cat /proc/devices查看到我们已经注册了字符设备

但是在/dev目录下并没有生成设备节点,我们暂时还无法对设备进行操作,下面介绍如何创建设备节点

六、创建设备节点


创建设备节点分为手动创建和自动创建,下面分别介绍

(1)手动创建

在上面驱动程序打印出major:250; minor:1,我们可以利用这些信息来创建设备节点

通过下面命令创建设备节点

mknod 设备名 设备类型(字符:c,块:b) 主设备号 从设备号

执行

mknod /dev/mydev c 250 1


运行后就生成了设备节点/dev/mydev

(2)自动创建

自动创建利用的是udev机制或者mdev机制,当驱动在/sys创建相关的设备信息时,udev或者mdev会根据这些信息创建设备节点

下面介绍创建设备节点

static struct class *mydev_class;

mydev_class = class_create(THIS_MODULE, "mydev"); //创建一个类
device_create(mydev_class, NULL, dev_id, NULL, "mydev"); //根据设备号创建设备节点


销毁设备节点

device_destroy(mydev_class, dev_id); //销毁设备节点
class_destroy(mydev_class); //销毁类


修改后的驱动

mydev.c

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

static dev_t dev_id;
static struct cdev *mydev;
static struct class *mydev_class;

ssize_t mydev_read(struct file *file, char __user *data, size_t size, loff_t * loff)
{
    printk("mydev_read\n");
    
    return 0;
}

ssize_t mydev_write(struct file *file, const char __user *data, size_t size, loff_t *loff)
{
    printk("mydev_write\n");

    return 0;
}

int mydev_open(struct inode *inode, struct file *file)
{
    printk("mydev_open\n");

    return 0;
}

static struct file_operations mydev_fops = {
    .owner = THIS_MODULE,
    .read   = mydev_read,
    .open   = mydev_open,
    .write  = mydev_write,
};

static __init int mydev_init(void)
{
    /* 申请设备号 */
    alloc_chrdev_region(&dev_id, 1, 1, "mydev");

    /* 分配字符设备 */
    mydev = cdev_alloc();

    /* 设置字符设备 */
    cdev_init(mydev, &mydev_fops);

    /* 注册字符设备 */
    cdev_add(mydev, dev_id, 1);

    /* 打印申请到的主次设备号 */
    printk("major:%d; minor:%d\n", MAJOR(dev_id), MINOR(dev_id));

    mydev_class = class_create(THIS_MODULE, "mydev");
    device_create(mydev_class, NULL, dev_id, NULL, "mydev");

    return 0;
}

static __exit void mydev_exit(void)
{
    device_destroy(mydev_class, dev_id);
    class_destroy(mydev_class);

    cdev_del(mydev);
    kfree(mydev);
    unregister_chrdev_region(dev_id, 1);
}

module_init(mydev_init);
module_exit(mydev_exit);

MODULE_LICENSE("GPL");


重新编译加载模块

查看/sys/dev/char目录,会发现多了一些信息250:1

此时查看ls /dev/mydev,会发现/dev目录已经有了mydev了

七、测试


每个驱动程序写完之后,都需要编写应用程序测试

下面是我们的测试程序

mydev_test.c

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

int main(int argc, char* argv[])
{
    int val = 1;

    int fd = open("/dev/mydev", O_RDWR);

    write(fd, &val, sizeof(val));

    read(fd, &val, sizeof(val));

    return 0;
}



编译

arm-linux-gcc mydev_test.c

加载模块

insmod mydev.ko

执行测试程序

./a.out

可以看到控制台打印

[ 2639.832633] mydev_open
[ 2639.833528] mydev_write
[ 2639.836014] mydev_read

证明驱动程序正常
 

八、原文链接

本文转载自:Linux驱动入门(一)字符设备驱动基础_JT同学的博客-CSDN博客,感兴趣的同学可以去阅读该作者的其他文章。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
资源大于15MB分2次上传。 清晰度一般。加到11章 第12,13章没有。 第1章 嵌入式系统基础. 1.1 嵌入式系统简介 1.1.1 嵌入式系统定义 1.1.2 嵌入式系统与PC 1.1.3 嵌入式系统的特点 1.2 嵌入式系统的发展 1.2.1 嵌入式系统现状 1.2.2 嵌入式系统发展趋势 1.3 嵌入式操作系统与实时操作系统 1.3.1 Linux 1.3.2 uC/OS 1.3.3 Windows CE 1.3.4 VxWorks 1.3.5 Palm OS 1.3.6 QNX 1.4 嵌入式系统选型 第2章 基于ARM9处理器的硬件开发平台 2.1 ARM处理器简介 2.1.1 ARM公司简介 2.1.2 ARM微处理器核 .2.2 ARM9微处理器简介 2.2.1 与ARM7处理器的比较 2.2.2 三星S3C2410X处理器详解 2.3 FS2410开发平台 第3章 创建嵌入式系统开发环境 3.1 嵌入式Linux开发环境 3.2 Cygwin 3.3 虚拟机 3.4 交叉编译的预备知识 3.4.1 Make命令和Makefile文件 3.4.2 binutils工具包 3.4.3 gcc编译器 3.4.4 Glibc库 3.4.5 GDB 3.5 交叉编译 3.5.1 创建编译环境 3.5.2 编译binutils 3.5.3 编译bootstrap_gcc 3.5.4 编译Glibc 3.5.5 编译完整的gcc 3.5.6 编译GDB 3.5.7 成果 3.5.8 其他交叉编译方法 3.6 通过二进制软件包创建交叉编译环境 3.7 开发套件 第4章 调试嵌入式系统程序 4.1 嵌入式系统调试方法 4.1.1 实时在线仿真 4.1.2 模拟调试 4.1.3 软件调试 4.1.4 BDM/JTAG调试 4.2 ARM仿真器 4.2.1 techorICE ARM仿真器 4.2.2 ARM仿真器工作原理 4.2.3 ARM仿真器的系统功能层次 4.2.4 使用仿真器和ADS Debugger调试ARM开发板 4.3 JTAG接口 4.3.1 JTAG引脚定义 4.3.2 通过JTAG烧写Flash 4.3.3 烧写Flash技术内幕 第5章 Bootloader 5.1 嵌入式系统的引导代码 5.1.1 初识Bootloader 5.1.2 Bootloader的启动流程 5.2 Bootloader之vivi 5.2.1 vivi简介 5.2.2 vivi的配置与编译 5.2.3 vivi代码导读 5.3 Bootloader之U-Boot 5.3.1 U-Boot代码结构分析 5.3.2 编译U-Boot代码 5.3.3 U-Boot代码导读 5.3.4 U-Boot命令 5.4 FS2410的Bootloader 第6章 Linux系统在ARM平台的移植 6.1 移植的概念 6.2 Linux内核结构 6.3 Linux-2.4内核向ARM平台的移植 6.3.1 根目录 6.3.2 arch目录 6.3.3 arch/arm/boot目录 6.3.4 arch/arm/def-configs目录 6.3.5 arch/arm/kernel目录 6.3.6 arch/arm/mm目录 6.3.7 arch/arm/mach-s3c2410目录 6.4 Linux-2.6内核向ARM平台的移植 6.4.1 定义平台和编译器 6.4.2 arch/arm/mach-s3c2410/devs.c 6.4.3 arch/arm/mach-s3c2410/mach-fs2410.c 6.4.4 串口输出 6.5 编译Linux内核 6.5.1 代码成熟等级选项 6.5.2 通用的一些选项 6.5.3 和模块相关的选项 6.5.4 和块相关的选项 6.5.5 和系统类型相关的选项 6.5.6 和总线相关的选项 6.5.7 和内核特性相关的选项 6.5.8 和系统启动相关的选项 6.5.9 和浮点运算相关的选项 6.5.10 用户空间使用的二进制文件格式的选项 6.5.11 和电源管理相关的选项 6.5.12 和网络协议相关的选项 6.5.13 和设备驱动程序相关的选项 6.5.14 和文件系统相关的选项 6.5.15 和程序性能分析相关的选项 6.5.16 和内核调试相关的选项 6.5.17 和安全相关的选项 6.5.18 和加密算法相关的选项 6.5.19 库选项 6.5.20 保存内核配置 第7章 Linux设备驱动程序开发 7.1 设备驱动概述 7.1.1 设备驱动和文件系统的关系 7.1.2 设备类型分类 7.1.3 内核空间和用户空间.. 7.2 设备驱动基础 7.2.1 设备驱动中关键数据结构 7.2.2 字符设备驱动开发 第8章 网络设备驱动程序开发 8.1 网络设备驱动程序简介 8.1.1 device数据结构 8.1.2 sk_buff数据结构 8.1.3 内核的驱动程序接口 8.2 以太网控制器CS8900A 8.2.1 特性 8.2.2 工作原理 8.2.3 电路连接 8.2.4 引脚 8.2.5 操作模式 8.3 网络设备驱动程序实例 8.3.1 初始化函数 8.3.2 打开函数 8.3.3 关闭函数 8.3.4 发送函数 8.3.5 接收函数 8.3.6 中断处理函数 第9章 USB驱动程序开发 9.1 USB驱动程序简介 9.1.1 USB背景知识 9.1.2 Linux内核对USB规范的支持 9.1.3 OHCI简介 9.2 Linux下USB系统文件结点 9.3 USB主机驱动结构 9.3.1 USB数据传输时序 9.3.2 USB设备连接/断开时序 9.4 主要数据结构及接口函数 9.4.1 数据传输管道 9.4.2 统一的USB数据传输块 9.4.3 USBD数据描述 9.4.4 USBD与HCD驱动程序接口 9.4.5 USBD层的设备管理 9.4.6 设备类驱动与USBD接口 9.5 USBD文件系统接口 9.5.1 设备驱动程序访问 9.5.2 设备拓扑访问 9.5.3 设备信息访问 9.6 设备类驱动与文件系统接口 9.7 USB HUB驱动程序 9.7.1 HUB驱动初始化 9.7.2 HUB Probe相关函数 9.8 OHCI HCD实现 9.8.1 OHCI驱动初始化 9.8.2 与USBD连接 9.8.3 OHCI根HUB 9.9 扫描仪设备驱动程序 9.9.1 USBD接口 9.9.2 文件系统接口 9.10 USB主机驱动在S3C2410X平台的实现 9.10.1 USB主机控制器简介 9.10.2 驱动程序的移植 第10章 图形用户接口 10.1 嵌入式系统中的GUI简介 10.1.1 MicroWindows 10.1.2 MiniGUI 10.1.3 Qt/Embedded 10.2 MiniGUI编程 10.2.1 MiniGUI移植 10.2.2 MiniGUI编程 10.3 初识Qt/Embedded 10.3.1 Qt介绍 10.3.2 系统要求 10.3.3 Qt的架构 10.4 Qt/Embedded嵌入式图形开发基础 10.4.1 建立Qt/Embedded 开发环境 10.4.2 认识Qt/Embedded开发环境 10.4.3 窗体 10.4.4 对话框 10.4.5 外形与感觉 10.4.6 国际化 10.5 Qt/Embedded实战演练 10.5.1 安装Qt/Embedded工具开发包 10.5.2 交叉编译Qt/Embedded库 10.5.3 Hello,World 10.5.4 发布Qt/Embedded程序到目标板 10.5.5 添加一个Qt/Embedded应用到QPE 第11章 Java虚拟机的移植 11.1 Java虚拟机概述 11.1.1 Java虚拟机的概念 11.1.2 J2ME 11.1.3 KVM 11.2 Java虚拟机的移植 11.2.1 获得源码 11.2.2 编译环境的建立 11.2.3 JDK的安装 11.2.4 KVM的移植及编译 11.2.5 KVM的测试 11.3 其他可选的虚拟机 11.4 性能优化

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值