没有合适的资源?快使用搜索试试~ 我知道了~
首页嵌入式Linux设备驱动开发笔记--赖永诚
资源详情
资源评论
资源推荐
嵌入式 Linux 设备驱动开发 赖永诚 hqulyc@126.com
第1页 共55 页
嵌入式 Linux 设备驱动开发
驱动程序就是操作系统中用于控制和访问各种输入/输出设备的程序,驱动程序的重要性勿需多言。
而且,在实际的嵌入式系统开发中,操作系统的内核一般不需要做太多的修改,大量的工作往往是针对独
特的硬件平台编写、修改和调试各个外设的驱动程序,因此掌握驱动程序的设计对于嵌入式 Linux 系统开
发特别重要。
1. 设备驱动概述
在 Linux 中,几乎所有的内容都是文件,对设备驱动的访问也是以文件操作的方法实现的。无论是字
符设备还是块设备,用户对设备的操作都是通过虚拟文件系统转换为设备驱动与硬件操作例程的交互;即
使是访问网络的 Socket 接口,也是通过虚拟文件系统(VFS)实现的。Linux 通过虚拟文件系统为用户提供
了一个统一的设备访问接口,使用户能够透明地访问设备驱动程序。设备驱动程序是 Linux 内核的重要组
成部分。它向操作系统的其他部分屏蔽了设备硬件的复杂性,实现了硬件设备的透明管理。
1.1 设备类型分类
根据物理设备的特点和工作方式,以及驱动程序向内核注册的调用接口的不同,Linux 系统的设备驱
动程序分为字符设备(Char Device)、块设备(Block Device)和网络设备(Network Device)三种。
(1) 字符设备
字符设备是一类像键盘那样以字符(字节)为单位,逐个进行输入/输出的设备,如键盘、打印机等。
字符设备接口支持面向字符的 I/O 操作,不经系统的快速缓存,所以它们负责管理自己的缓冲区结构。字
符设备接口只支持顺序存取的功能,一般不能进行任意长度的 I/O 请求,而是限制 I/O 请求的长度必须是
设备要求的基本块长的倍数。
在系统内部,I/O 操作的存取通过一组固定的入口点来进行,这组入口点是由每个设备的设备驱动程
序提供的。一般来说,字符型设备驱动程序能够提供如下几个入口点:
① open 入口点:打开设备准备 I/O 操作。对字符特别设备文件进行打开操作,都会调用设备的 open
入口点。open 子程序必须对将要进行的 I/O 操作做好必要的准备工作,如清除缓冲区等。如果设备是独占
的,即同一时刻只能有一个程序访问此设备,则 open 子程序必须设置一些标志以表示设备处于忙碌状态。
② close 入口点:关闭一个设备。当最后一次使用设备终结后,调用 close 子程序。独占设备必须标
记设备可再次使用。
③ read 入口点:从设备上读数据。对于有缓冲区的 I/O 操作,一般是从缓冲区里读数据。对字符特
别设备文件进行读操作将调用 read 子程序。
④ write 入口点:向设备写数据。对于有缓冲区的 I/O 操作,一般是把数据写入缓冲区里。对字符特
别设备文件进行写操作将调用 write 子程序。
⑤ ioctl 入口点:执行读、写之外的操作。
(2) 块设备
块设备是一类像磁盘那样以记录块或者“扇区”为单位,成块进行输入/输出的设备,如磁盘、USB 接
口等。块设备接口仅支持面向块的 I/O 操作,所有 I/O 操作都通过在内核地址空间中的 I/O 缓冲区进行,
它可以支持几乎任意长度和任意位置上的 I/O 请求,即提供随机存取的功能。
(3) 网络设备
网卡、Modem 是最常见的网络设备。网络设备与字符设备和块设备最大的不同之处就在与网络设备没
有对应的设备文件。网卡是最典型的一个例子,网卡把向外发送的数据写入通往远程计算机系统的一条通
信线路上,把从远程系统中接受到的报文装入内核内存。在 Linux 系统中,计算机为每一个网卡分配一个
不同的符号名,例如:eth0,eth1 等。然而,这个名字并没有对应的设备文件,也没有对应的索引节点。
由于没有使用文件系统,所以系统管理员必须建立设备名和网络地址之间的联系。因此,应用程序和网络
接口之间的数据通信不是基于标准的有关文件的 read()、write()等系统调用,而是基于 BSD socket 机制,
使用 socket()、bind()、listen()、accept()、connect()等系统调用对网络地址进行操作。在系统和驱
嵌入式 Linux 设备驱动开发 赖永诚 hqulyc@126.com
第2页 共55 页
动程序之间定义有专门的数据结构(sk_buff)进行数据传递。
1.2 设备驱动程序开发的特性与共性
Linux 操作系统内核中存在着许多不同的设备驱动,如网卡、打印机、声卡等驱动程序。虽然这些驱
动程序各自负责不同的硬件,并且完成相应的功能,但它们具有如下一些共性。
(1)内核代码
设备驱动程序是内核的一部分,像内核中其他代码一样,出错将导致系统的严重损伤。一个编写较差
的设备驱动程序甚至能使系统崩溃并导致文件系统的破坏和数据丢失。
(2)内核接口
设备驱动程序必须为 Linux 内核或者其从属子系统提供一个标准接口。
(3)接口内核机制与服务
设备驱动程序可以使用标准的内核服务,如内存分配中断、发送和等待队列等。
(4)动态可加载
多数 Linux 设备驱动程序可以在内核模块发出加载请求时加载,同时在不再使用时卸载。
(5)可配置
Linux 设备驱动程序可以链接到内核中。当内核被编译时,哪些内核被链接到内核是可配置的。
1.3 设备驱动程序的组成
设备驱动程序可以分为 3 个主要组成部分:
⑴ 自动配置和初始化子程序:这部分程序负责检测所要驱动的硬件设备是否存在和是否能正常工作。
如果该设备正常,则对这个设备及其相关的、设备驱动程序需要的软件状态进行初始化。这部分驱动程序
仅在初始化的时候被调用一次。
⑵ 服务于 I/O 请求的子程序:这部分程序又成为驱动程序的上半部分。由系统调用来进行程序的调
用。这部分程序在执行的时候,系统仍认为是与进行调用的进程属于同一个进程,只是由用户态变成了内
核态,具有进行此系统调用的用户程序的运行环境,因此可以在其中调用 sleep()等与进程运行环境有关
的函数。
⑶ 中断服务子程序:中断服务子程序又称为驱动程序的下半部分。
2. 内核的数据类型及链表
现代版本的 Linux 内核的可移植性是非常好的,可以运行在许多不同的体系结构上。由于 Linux 的多
平台特性,任何一个重要的驱动程序都应该都是可移植的。
当 Linux 内核在体系结构差异较大的平台之间移植时,会产生与数据类型相关的问题。坚持使用严格
的数据类型,并且在编译内核时使用-Wall –Wstrict-prototypes 选项,可以防止大多数的代码缺陷。
内核使用的数据类型主要被分为三大类:
① 类似 int 这样的标准 C 语言类型;
② 类似 u32 这样的有确定大小的类型
③ 像 pid_t 这样的用于特定内核对象的类型。
在不同的 CPU 体系结构上,C 语言的数据类型所占空间不一样。我们将讨论应该在什么情况下使用这
三种典型类型,以及如何使用。
如果读者遵循我们提供的指导方针,读者的驱动程序甚至可能在那些未经测试的平台上编译和运行。
2.1 使用标准 C 语言类型
尽管大多数程序员习惯于自由使用像 int 和 long 这样的标准类型,但编写设备驱动程序时需要小心,
以避免类型冲突和潜在的代码缺陷。
问题是,当我们需要“两个字节的填充符”或者“用四个字节字符串表示的某个东西”时,我么不能
使用标准类型,因 为 在不同的体系架构上,普通 C 语言的数据类型所占空间的大小并不相同。下面是在 x86
下数据类型所占的字节数:
嵌入式 Linux 设备驱动开发 赖永诚 hqulyc@126.com
第3页 共55 页
arch char
short int long ptr long-long
u8 u16 u32 u64
i686 1 2 4 4 4 8 1 2 4 8
下面是在其他平台上的数据类型所占的字节数:
arch char
short int long ptr long-long
u8 u16 u32 u64
i386 1 2 4 4 4 8 1 2 4 8
alpha 1 2 4 8 8 8 1 2 4 8
armv41 1 2 4 4 4 8 1 2 4 8
ia64 1 2 4 8 8 8 1 2 4 8
m68k 1 2 4 4 4 8 1 2 4 8
mips 1 2 4 4 4 8 1 2 4 8
ppc 1 2 4 4 4 8 1 2 4 8
sparc 1 2 4 4 4 8 1 2 4 8
sparc64
1 2 4 4 4 8 1 2 4 8
其中基于 sparc64 平台的 Linux 用户空间可以运行 32 位代码,用户空间指针是 32 位宽的,但内核空
间是 64 位的。
内核中的地址是 unsigned long 类型,指针大小和 long 类型相同。
尽管在混合使用不同数据类型时我们必须小心谨慎,但有时有理由这样做。这样的一种情况是内存地
址,只要一涉及到内核,内存地址就变得很特殊。虽然从概念上讲地址是指针,但是通过使用无符号整数
类型(unsigned long)可以很好地实现内存管理;内核把物理内存看作是一个巨型数组,一个内存地址就
是该数组的一个索引。此外,我们可以很方便地对指针取值;但在直接处理内存地址时,我们几乎从来不
会以这种方式对它们取值。使用一个整数类型可以防止这种取值,因而可避免代码缺陷。所以,内核中的
普通内存地址通常是 unsigned long,这利用了如下事实:至少在当前 Linux 支持的所有平台上,指针和
long 整型的大小总是相同的。
C99 标准定义了 intptr_t 和 uintptr_t 类型,它们是能够保存指针值的整型变量。这些类型在 Linux
2.6 的内核中几乎没有用到。
2.2 为数据项分配确定的空间大小
有时内核代码需要特定大小的数据项,多半是用来匹配预定义的二进制结构,或者和用户空间进行通
信,或者通过在结构体中插入“填白”字段来对齐数据。
当我 们需要知道自己的数据大小时,内核提供了下列数据类型。所有这些类型都在头文件
<asm/types.h>中申明,这个文件又被头文件<linux/types.h>包含:
相应的有符号类型也存在,但是几乎没有。如果需要它们的话,只需将名字中的 u 用 s 替换就可以了。
2.3 接口特定的类型
内核中最常用的数据类型由它们自己的 typedef 申明,这样可以防止出现任何移植性问题。例如,一
个进程的标识符(pid)通常使用 pid_t 类型,而不是 int。使用 pid_t 屏蔽了在实际的数据类型中任何可能
的差异。“接口特定(interface-specific)”是指由某个库定义的一种数据类型,以便为某个特定的数据
结构提供接口。
注意,近来已经很少定义新的接口特定的类型。许多内核开发人员已经不再喜欢使用 typedef 语句,
他们更愿意看到直接用在代码中的真实的类型信息,而不是隐藏在用户定义的类型之后。不过,许多较老
的接口特定类型还是保留在内核中,它们不会很快就消失。
即使没有定义接口特定的类型,也应该始终使用和内核其余部分一致的、适当的数据类型。例如,
jiffies 计数总是属于 unsigned long 类型,而不管它的实际大小如何,因此,在使用 jiffies 的时候应
typedef unsigned char u8; /* 无符号字节(8 位) */
typedef unsigned short u16; /* 无符号字(32 位) */
typedef unsigned int u32; /* 无符号 32 位值 */
typedef unsigned long long u64; /* 无符号 64 位值 */
嵌入式 Linux 设备驱动开发 赖永诚 hqulyc@126.com
第4页 共55 页
该始终使用 unsigned long 类型。
完整的_t 类型在<linux/types.h>中定义,但很少使用这个清单。当需要某个特定类型时,可在所需
调用的函数原型或者所使用的数据结构中找到这个类型。
2.4 有关移植性的其他问题
在编写一个能在不同的 Linux 平台间移植的驱动程序时,除了数据类型定义的问题之外,还必须注意
其他一些软件上的问题。
一个通用的原则是要避免使用显式的常量值。通常,代码通过使用预处理的宏使之参数化。这一节列
出了最重要的移植性问题。在遇到其他已经被参数化的值时,可以在头文件和随后正是内核版本一起发布
的设备驱动程序中找到一些线索。
2.4.1 时间间隔
在处理时间间隔时,不要假定每秒一定有 100 个 jiffies。尽管对于当前的 i386 架构这是正确的,但
并不是每一种 Linux 平台都以这个速度运行。即使在 x86 上这种假设也可能是错误的,因为 HZ 值可能已
经改变(有这种情况),更何况没有人知道未来的内核将来发生什么变换。使用 jiffies 计算时间间隔的时
候,应该用 HZ(每秒定时其中断的次数)来衡量。例如,为了检测半秒的超时,可以将消逝的时间与 HZ/2
作比较。更常见的,与 msec 毫秒对应的 jiffies 数目总是 msec*HZ/1000。
2.4.2 页面大小
使用内存时,要记住内存页的大小为 PAGE_SIZE 字节,而不是 4KB。在不同的平台上,页大小范围可
以是 4KB 到 64KB。PAGE_SHIFT 的作用是通过对地址右移 PAGE_SHIFT 得到一个地址所在页的页号。对于用
户空间,可以使用 getpagesize 函数来得到页的大小。
例如,使用 get_free_pages 函数申请 16KB 空闲空间(即
14
2
),先将 16KB 转换成
order
2
空闲页数。在 x86
下定义 PAGE_SHIFT 为 12,即
14
2
为 4KB。
2.4.3 字节存储顺序
字节存储顺序有两种:低字节优先(little-endian)或称为小端字节序,高字节优先(big-endian)或
称大端字节序。低字节优先的方式是在存储多字节数值时,低字节在前面,高字节在后面。高字节优先则
正好相反。尽管 PC 是按照先是低字节(little-endian)的方式存储多字节数值的,但某些高端平台是以另
一种方式(big-endian)工作的。只要可能,就应该将代码编写成不依赖于所操作数据的字节序的方式。可
是,有时驱动程序需要从单字节建立整型数或者相反,或者它必须和要求特定字节序的设备通信。
头文件<asm/byteorder.h>定义了__BIG_ENDIAN 或者__LITTLE_ENDIAN,取决于处理器的字节序。在 处
理字节序问题时,我们可能要编写一组#ifdef __LITTLE_ENDIAN 条件语句,但是有一个更好的方法。Linux
内核定义了一组宏,它可以在处理器字节序和特殊字节之间进行转换。例如:
这两个宏将一个 CPU 使用的值转换成一个无符号的 32 位小头数值,或者相反。不管 CPU 是大头还是
小头它们都可以正常工作,也不管 CPU 是否是一个 32 位处理器。如果没有转换工作需要做,它们就返回
未经修改的参数。使用这些宏可以使编写可移植代码的工作变得更加容易,而无需使用很多条件编译。
2.4.4 数据对齐
u32 cpu_to_le32(u32);
u32 le32_to_cpu(u32);
int order = ((14 – PAGE_SHIFT) > 0) ? (14 – PAGE_SHIFT) : 0;
buf = get_free_pages(GRP_KERNEL, order);
嵌入式 Linux 设备驱动开发 赖永诚 hqulyc@126.com
第5页 共55 页
2.4.5 指针和错误值
许多内部的内核函数返回一个指针值给调用者,而这些函数中很多可能会失败。在大部分情况下,失
败是通过返回一个 NULL 指针值来表示的。这种技巧有作用,但是它不能传递问题的确切性质。某些接口
确实需要返回一个实际的错误编码,以使调用者可以根据实际出错的情况做出正确的决策。
许多内核接口通过把错误值编码到一个指针值中来返回错误信息。这种函数必须小心使用,因为它们
的返回值不能简单地和 NULL 比较。为了帮助创建和使用这种类型的接口,<linux/err.h>中提供了一小组
函数。
返回指针类型的函数可以通过如下函数返回一个错误值:
这里 error 是通常的负的错误编码。该函数定义如下:
调用者可以使用 IS_ERR 来检查所返回的指针是否是一个错误编码:
函数 IS_ERR 定义如下:
如果需要实际的错误编码,可以通过如下函数把它提取出来:
函数 PTR_ERR 定义如下:
应该只有在 IS_ERR 对某值返回真值时才对该值使用 PTR_ERR,因为任何其他值都是有效的指针。
<linux/err.h>中包含头文件<asm/errno.h> ,而其有包含头文件<asm-generic/errno.h>,其又包含
头文件<asm-generic/errno-base.h>。
2.5 内核通用链表
就像很多其他程序一样,操作系统内核经常需要维护数据结构的链表。有时,Linux 内核中同时存在
多个链表的实现代码。为了减少重复代码的数量,内核开发者已经建立了一套标准的循环、双向链表的实
现。如果你需要操作链表,那么建议你使用这一内核设施。
当使用这些链表接口时,应该始终牢记这些链表函数不进行任何锁定。如果你的驱动程序有可能试图
对同一个链表执行并发操作的话,则有责任实现一个锁方案。否则,崩溃的链表结构体、数据丢失、内核
混乱等问题是很难诊断的。
2.5.1 链表头结构
为了使用这个链表机制,驱动程序必须包含头文件<linux/list.h>。该 文件定义了一个简单的
list_head 类型的结构体。
static inline long PTR_ERR(const void *ptr)
{
return (long) ptr;
}
long PTR_ERR(const void *ptr);
static inline long IS_ERR(const void *ptr)
{
return unlikely((unsigned long)ptr > (unsigned long)-1000L);
}
long IS_ERR(const void *ptr)
static inline void *ERR_PTR(long error)
{
return (void *) error;
}
void *ERR_PTR(long error);
剩余54页未读,继续阅读
embedlinux
- 粉丝: 0
- 资源: 2
上传资源 快速赚钱
- 我的内容管理 收起
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助
会员权益专享
最新资源
- ExcelVBA中的Range和Cells用法说明.pdf
- 基于单片机的电梯控制模型设计.doc
- 主成分分析和因子分析.pptx
- 共享笔记服务系统论文.doc
- 基于数据治理体系的数据中台实践分享.pptx
- 变压器的铭牌和额定值.pptx
- 计算机网络课程设计报告--用winsock设计Ping应用程序.doc
- 高电压技术课件:第03章 液体和固体介质的电气特性.pdf
- Oracle商务智能精华介绍.pptx
- 基于单片机的输液滴速控制系统设计文档.doc
- dw考试题 5套.pdf
- 学生档案管理系统详细设计说明书.doc
- 操作系统PPT课件.pptx
- 智慧路边停车管理系统方案.pptx
- 【企业内控系列】企业内部控制之人力资源管理控制(17页).doc
- 温度传感器分类与特点.pptx
资源上传下载、课程学习等过程中有任何疑问或建议,欢迎提出宝贵意见哦~我们会及时处理!
点击此处反馈
安全验证
文档复制为VIP权益,开通VIP直接复制
信息提交成功
评论1