Linux驱动开发2-字符驱动入门之低级LED

本文介绍了Linux设备驱动的概念,强调了Linux系统下驱动与应用程序的职责划分。文章详细讲解了字符设备驱动,包括主次编号的概念、静态与动态分配设备号的方法。还探讨了字符设备的重要数据结构file_operations、inode和file,以及如何注册字符设备。最后,文章给出了一个LED驱动的实例,包括源码和测试步骤,展示了如何编写和测试字符驱动。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、Linux设备驱动简介

    在单片机驱动编程中,这些硬件的设备驱动由我们自己编写,然后在代码中由我们自己调用,他们没有一个统一的规范,一百个人有一百种驱动的写法。而在inux系统下编写驱动,它有严格的规范,哪些该驱动做,哪些该应用程序做;驱动程序编写要先做什么、然后再做什么都有严格的定义。正是因为由这样的规范,所以每个人只需要注重自己的角色,做自己该做的事,这也将嵌入式Linux开发的岗位分为两个,一个是底层驱动开发的BSP(Board Support Packet)开发, 另外就是应用程序(Application)开发。

    对于BSP开发的岗位,相应的开发人员应该了解各种硬件知识,如电路基础、各种接口技术和硬件调试工具的使用(如万用表、示波器、甚至逻辑分析仪等),此外还需要计算机组成、操作系统原理等理论基础,同时还要了解各种体系架构的CPU、汇编语言等,当然最重要的是C语言编程能力和数据结构的知识以及对Linux内核源码的大量阅读和分析。

    对于嵌入式应用程序开发的岗位,我们不需要了解底层驱动的具体实现细节,而只需要怎么使用他们即可,因为Linux是一个模块化、严格分层的系统,所以应用程序人员只需要了解Linux的驱动调用的统一API(Application Program Interface,应用程序编程接口)即可,这些API就是Linux的系统调用(System Call),他们在《UNIX环境高级编程》这本书里有较为详细的描述。对于嵌入式应用程序的开发,我们大部分使用C、C++或python(树莓派)和数据结构,根据应用程序的不同需求,我们需要补充额外的计算机网络、数据库等知识。

    Linux内核设计的哲学是把所有的东西都抽象成文件进行访问,这样对设备的访问都是通过文件I/O来进行操作。Linux内核将设备按照访问特性分为三类:字符设备、块设备、网络设备:


字符设备

    一个字符设备是一种可以当作一个字节流来存取的设备( 如同一个文件 ),对于这些设备他一次I/O只访问一个字节。 一个字符驱动负责实现这种行为,这样的驱动常常至少实现 open, close, read, 和 write 系统调用. 文本控制台( /dev/console )和串口( /dev/ttyS0 )是字符设备的例子, 因为它们很好地展现了流的抽象. 字符设备通过文件系统设备结点来存取, 例如/dev/tty1 和 /dev/lp0. 在一个字符设备和一个普通文件之间唯一有关的不同就是, 你经常可以在普通文件中移来移去, 但是大部分字符设备仅仅是数据通道, 你只能顺序存取.然而, 存在看起来象数据区的字符设备, 你可以在里面移来移去. 例如LCD驱动framebuffer经常这样, 应用程序可以使用 mmap 或者 lseek 存取整个要求的图像。Linux内核里,绝大部分的设备都是字符设备,今后我们所写的驱动99%也都是处理字符设备。

块设备

    我们常见的块设备有磁盘、固态硬盘、光盘、U盘、SD卡、Nandflash、Norflash等。一个块设备一次I/O通常访问一个块的大小数据,这个大小通常是2的幂次方字节( 如磁盘512字节,Nandflash使用2048个字节)。对于块设备的使用,需要分区格式化后建立文件系统,之后在应用程序空间中使用mount命令挂载起来之后使用。

网络设备

    网络设备包括有线网卡(eth0)、无线网卡(wlan0)、回环设备(lo0)、拨号网络设备(ppp0)等,他们在Linux内核里由网络协议栈实现,在/dev路径下并没有相应的设备节点,通过ifconfig -a命令可以查看所有的网络设备。在应用程序编程时,所有对网络设备的访问都是通过socket()来访问的。

二、字符设备驱动

2.1 主次编号

    字符设备通过文件系统中的设备名来存取,惯例上它们位于 /dev 目录. 字符驱动的特殊文件由使用 ls -l 的输出的第一列的"c"标识. 块设备也出现在 /dev 中, 但是它们由"b"标识。如果你发出 ls -l 命令, 你会看到在设备文件项中有 2 个数(由一个逗号分隔)在最后修改日期前面, 这里通常是文件长度出现的地方. 这些数字是给特殊设备的主次设备编号。其中逗号前面的为主设备号,逗号后面的为次设备号。其中主设备号表示一类设备,而次设备号表示它是这类设备的第几个设备。现代 Linux 内核允许多个驱动共享主编号, 但是你看到的大部分设备仍然按照一个主编号一个驱动的原则来组织。

    例如下图中的fb0设备即为字符设备,ls -l 的第一个字符'c'说明他是字符(character)设备,他的主设备号为29,次设备号为0。其中fb0设备就是我们的LCD显示屏设备,从驱动层面上来讲,如果我们想让LCD显示某些内容,我们只需要open()打开该设备获取文件描述符(fd, filedescription),然后往里面按照相应格式write()相应数据即可,而如果我们想截屏,只需要read()读该设备的内容就行。当然,对LCD的操作并没有表面上的这么简单。


    在内核编程中, 使用dev_t 类型(在 <linux/types.h>中定义)来定义设备编号, 对于 2.6.0 内核, dev_t 是 32 位的量, 其中12 位用作主编号, 20 位用作次编号. 在编码时,我们不应该管哪些位是主设备号,哪些位是次设备号。而是应当利用在 <linux/kdev_t.h>中的一套宏定义来获取一个 dev_t 的主、次编号:

MAJOR(dev_t dev);

MINOR(dev_t dev);

    相反, 如果我们有主、次编号需要将其转换为一个 dev_t, 则使用MKDEV宏:

MKDEV(int major, int minor);

2.2 获取主次编号

    在建立一个字符驱动时你的驱动需要做的第一件事是获取一个或多个设备编号来使用,在Linux内核里,一些主设备编号是静态分派给最普通的设备的,这些设备列表在内核源码树的Documentation/devices.txt 中列出. 因此, 作为一个驱动编写者, 我们有两个选择: 一是简单地捡一个看来没有用的主设备号, 二是让内核以动态方式分配一个主设备号给你. 只要你是你的驱动的唯一用户就可以捡一个编号用; 一旦你的驱动更广泛的被使用了, 一个随机捡来的主编号将导致冲突和麻烦。因此, 对于新驱动, 我们强烈建议使用动态分配来获取你的主设备编号, 而不是随机选取一个当前空闲的编号。

2.2.1 静态设定主次设备号

    静态设备指定主次设备号是指我们根据当前系统的主设备号分配情况,自己选择一个主设备号。当然我们自己随机选择的话,会跟Linux内核其他的驱动冲突,这时我们可以先查看当前系统已经使用了哪些主设备号,然后我们选择一个没有使用作为我们新的驱动使用。Linux系统中正在使用的主设备号会保存在 /proc/devices 文件中。

    我们在编写驱动时可以选定一个未用的主设备号,如251来使用。

dev_t devno;
int result;
int major=251;
devno = MKDEV(major, 0);
result = register_chrdev_region (devno, 4, "led");
if (result < 0)
{
    printk(KERN_ERR "LED driver can't use major %d\n", major);
    return -ENODEV;
}

    这里register_chrdev_region()函数的原型为:

int register_chrdev_region(dev_t first, unsigned int count, char *name);

    first 是你要分配的起始设备编号. first 的次编号部分通常是从0开始,但不是强制的. count 是你请求的连续设备编号的总数. 注意, 如果 count 太大, 你要求的范围可能溢出到下一个次编号;但是只要你要求的编号范围可用, 一切都仍然会正确工作. 最后, name 是应当连接到这个编号范围的设备的名子; 它会出现在 /proc/devices 和 sysfs 中。如同大部分内核函数, 如果分配成功进行, register_chrdev_region 的返回值是 0. 出错的情况下, 返回一个负的错误码, 你不能存取请求的区域。

    当驱动的主、次设备号申请成功后,/proc/devices里将会出现该设备,但是/dev 路径下并不会创建该设备文件。


    如果我们需要创建该文件,则需要使用mknod命令创建。当然我们也可以在驱动里调用相应的函数,来通知应用程序空间自动创建该设备文件。这在将来的课程中讲到:


2.2.2 动态申请主次设备号

    在上面的示例中,我们是首先查看当前的系统使用了哪些设备驱动,然后找一个未用的主设备号来使用。假设将来我们的Linux内核系统升级需要使能其他的设备驱动,如果某个需要的驱动所用的主设备号刚好和我们的设备驱动冲突,那么我们驱动不得不对这个主设备号进行调整,而如果产品已经部署了,这种召回升级是非常致命的。所以我们在写驱动时不应该静态指定一个主设备号,而是由Linux内核根据当前的主设备号使用情况动态分配一个未用的给我们的驱动使用,这样就永远不会冲突。

int result;
int dev_major
dev_t devno;
result = alloc_chrdev_region(&devno, 0, 4, "led");
dev_major = MAJOR(devno);
/* Alloc for device major failure */
if (result < 0)
{
    printk(KERN_ERR "led driver allocate device major number failure: %d\n",
    result);
    return -ENODEV;
}

printk(KERN_ERR "led driver choose device major number: %d\n", dev_major);

    alloc_chrdev_region函数的原型如下,使用这个函数, dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数. firstminor 应当是请求的第一个要用的次编号; 它常常是0. count 和 name 参数如同给 request_chrdev_region 的一样。

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

    如果使用alloc_chrdev_region()函数来动态分配主设备号,那我们驱动在安装后并不知道这个主设备号是多少,那么只能通过 查看 /proc/devices 文件才能知道它的值,然后再创建设备节点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值