1 Linux PCI、串口驱动程序
1.1 PCI总线概述
总线概念
总线是一种传输信号的信道;总线是连接一个或多个导体的电气连线。总线由电气接口和编程接口组成,我们重点关注编程接口。
PCI概念
PCI是Peripheral Component Interconnect(外围设备互联)的简称,是在桌面及更大型的计算机上普遍使用的外设总线。
PCI特点
PCI总线具有三个非常显著的优点:
在计算机和外设间传输数据时具有更好的性能
能够尽量独立于具体的平台
可以方便地实现即插即用
体系结构-1
从结构上看, PCI总线是一种不依附于某个具体处理器的局部总线,它是在CPU和原来的系统总线之间插入的一级总线,具体由一
个桥接电路实现对这一层的管理,并实现上下之间的接口以协调数据的传送。
体系结构-2
系统的各个部分通过PCI总线和PCI-PCI桥连接在一起。CPU和RAM通过PCI桥连接到PCI总线0(即主PCI总线),而具有PCI接口的显卡直接连接到主PCI总线上。PCI-PCI桥是一个特殊的PCI设备,它负责将PCI总线0和PCI总线1连接在一起。图中连接到 PCI 1号总线上的是SCSI卡和以太网卡。为了兼容旧的ISA总线标准,PCI总线还可以通过PCI-ISA桥来连接ISA总线,从而支持以前的ISA设备,图中ISA总线上连接着一个多功能I/O控制器,用于控制键盘、鼠标和软驱等。
PCI设备寻址
每个PCI设备由一个总线号、一个设备号、和一个功能号确定。PCI规范允许一个系统最多拥有256条总线,每条总线最多带32个设备,但每个设备可以是最多8个功能的多功能板(如一个音频设备带一个CD-ROM驱动器)。
/proc/iomem描述了系统中所有的设备I/O在内存地址空间上
的映射。我们来看地址从1G开始的第一个设备在
/proc/iomem中是如何描述的:
40000000-400003ff :0000:00:1f.1
这是一个PCI设备,40000000-400003ff是它所映射的内存
空间地址,占据了内存地址空间 1024 bytes的位置,而
0000:00:1f.1则是这个PCI外设的地址,它以冒号和逗号分隔
为4个部分,第一个16位表示域,第二个8位表示一个总线
号,第三个5位表示一个设备号,最后是3位,表示功能号。
因为PCI规范允许单个系统拥有最多256条总线,所以总线编号是8位。每个总线上可支持32个设备,所以设备号是5位,而每个设备上最多可有8种功能,所以功能号是3位。由此可以得出上述的PCI设备的地址是0号总线上的31号设备上的1号功能。
PCI寻址
使用lspci命令可以查看系统中的PCI设备,下面是使用lspci
命令得到的一组输出:
00:00.0 Host bridge:Intel Corporation 82845 845
00:01.0 PCI bridge:Intel Corporation 82845 845
00:1d.0 USBController: Intel Corporation 82801CA/CAM USB
00:1d.1 USBController: Intel Corporation 82801CA/CAM USB
00:1e.0 PCI bridge:Intel 82801 Mobile PCI Bridge
00:1f.0 ISA bridge:Intel Corporation 82801CAM ISA Bridge
00:1f.1 IDEinterface: Intel Corporation 82801CAM IDE U100
00:1f.3 SMBus: Intel82801CA/CAM SMBus Controller
00:1f.5 Multimediaaudio controller:Intel Corporation
82801CA/CAM AC'97Audio Controller
00:1f.6 Modem: Intel82801CA/CAM AC'97 Modem Controller
01:00.0 VGAcompatible controller: nVidia Corporation NV17
02:00.0 FireWire:VIA Technologies. 1394 Host Controller
02:01.0 Ethernetcontroller: Realtek Semiconductor Co.
02:04.0 CardBusbridge: O2 Micro, Inc. Cardbus Controller
02:04.1 CardBusbridge: O2 Micro, Inc. Cardbus Controller
配置寄存器
每个PCI设备都有一组固定格式的寄存器,即配置寄存器,配置寄存器由Linux内核中的PCI初始化代码与驱动程序共同使用。内核在启动时负责对配置寄存器进行初始化,包括设置中断号以及I/O基址等。
配置空间
00H—01H Vendor ID 制造商标识
02H—03H Device ID 设备标识
04H—05H Command 命令寄存器
06H—07H Status 状态寄存器
08H Revision ID 版本识别号寄存器
09H—0bH Class Code 分类代码寄存器
0cH Cache Line SizeCACHE行长度寄存器
0dH Latency Timer 主设备延迟时间寄存器
0eH Header Type 头标类型寄存器
0fH Bulit-in-tesetRegister 自测试寄存器
10H—13H Base AddressRegister 0 基地址寄存器0
14H—17H Base AddressRegister 1 基地址寄存器1
18H—1bH Base AddressRegister 2 基地址寄存器2
1cH—19H Base AddressRegister 3 基地址寄存器3
20H—23H Base AddressRegister 4 基地址寄存器4
24H—27H Base AddressRegister 5 基地址寄存器5
28H—2bH Cardbus CISPointer 设备总线CIS指针寄存器
2cH—2dH SubsystemVendor ID 子设备制造商标识
2eH—2fH SubsystemDevice ID 子设备标识
30H—33H Expasion ROMBase Address 扩展ROM基地址
34H—3bH ——— 保留
3cH Interrupt Line 中断线寄存器
3dH Interrupt Pin 中断引脚寄存器
3eH Min_Gnt 最小授权寄存器
3fH Max_Lat 最大延迟寄存器
厂商标识(Vendor Id)
用来标识PCI设备生产厂家的数值。Intel的厂商标识为0x8086,全球厂商标识由PCI Special Interest Group来分配。
设备标识(Device Id)
用来标识设备的数值。Digital 21141快速以太设备的设备标识为0x0009
基地址寄存器(Base Address Registers)记录此设备使用的I/O与内存空间的位置
中断连线(Interrupt Line)
记录此设备使用的中断号
中断引脚(Interrupt Pin)
记录此PCI设备使用的引脚号(A,B,C,D)
1.2 PCI驱动程序设计
驱动描述
在Linux内核中,PCI 驱动使用 struct pci_driver 结构来描述:
struct pci_driver {
。。。。。。
。。。。。。
const structpci_device_id *id_table;
int (*probe) (structpci_dev *dev, const struct pci_device_id *id);
void (*remove)(struct pci_dev *dev);
/* Device removed(NULL if not a hot-plug capable driver) */
。。。。。。
。。。。。。
}
注册驱动
注册 PCI 驱动, 使用如下函数:
pci_register_driver(structpci_driver *drv)
使能设备
在 PCI 驱动使用PCI 设备的任何资源(I/O 区或者中断)之前, 驱动必须调用如下函数来使能设备:
intpci_enable_device(struct pci_dev *dev)
获取基地址
一个PCI设备最多可以实现6个地址区域,大多数PCI设备在这些区域实现I/O寄存器。Linux提供了一组函数来获取这些区间的基地址:
pci_resource_start(structpci_dev *dev, int bar)
返回指定区域的起始地址,这个区域通过参数 bar 指定,范
围从 0-5,表示6个PCI区域中的一个。
pci_resource_end(structpci_dev *dev, int bar)
返回指定区域的末地址。
中断
中断号存放于配置寄存器PCI_INTERRUPT_LINE中, 驱动不必去检查它, 因为从 PCI_INTERRUPT_LINE中找到的值保证是正确的。如果设备不支持中断, 寄存器 PCI_INTERRUPT_PIN 中的值是0,否则它是非零的值。但因为驱动开发者通常知道设备是否是支持中断,所以常常不需要访问 PCI_INTERRUPT_PIN。
实例分析
《国嵌PCI网卡驱动程序分析》
1.3 终端控制台体系
数据通信
数据通信的基本方式可分为并行通信与串行通信两种:
并行通信:利用多条数据线将数据的各位同时传送。它的特点是传输速度快,适用于短距离通信。
串行通信:利用一条数据线将数据一位位地顺序传送。特点是通信线路简单,利用简单的线缆就可实现通信,低成本,适用于远距离通信。
异步通信
异步通信以一个字符为传输单位,通信中两个字符间的时间间隔是不固定的,然而同一个字符中的两个相邻位之间的时间间隔是固定的。
通信协议:是指通信双方约定的一些规则。在使用异步串口传送一个字符的信息时,对数据格式有如下约定:规定有空闲位、起始位、资料位、奇偶校验位、停止位。
起始位:先发一个逻辑”0”信号,表示传输字符的开始
数据位:紧接在起始位之后。数据位的个数可以是4、5、6、7、8等,从最低位开始传送,靠时钟定位。
奇偶校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此校验数据传送的正确性。
停止位:它是一个字符数据的结束标志。
空闲位:处于逻辑“1”状态,表示当前线路上没有数据传送。
波特率
是衡量数据传送速率的指针。表示每秒钟传送的二进制位数。例如数据传送速率为120字符/秒,而每一个字符为10位,则其传送的波特率为10×120=1200位/秒=1200波特。
注:异步通信是按字符传输的,接收设备在收到起始信号之后只要在一个字符的传输时间内能和发送设备保持同步就能正确接收。
传送方式
单工方式:数据始终是从A设备发向B设备。
半双工方式 :资料能从A设备传送到B设备,也能从B设备传送到A设备。但在任何时候都不能在两个方向上同时传送,即每次只能有一个设备发送,另一个设备接收。
全双工方式:允许通信双方同时进行发送和接收。这时,A设备在发送的同时也可以接收,B设备也一样。
终端概述
在Linux中,TTY(终端)是一类字符设备的统称,包括了3种类
型:控制台,串口和伪终端。
控制台
供内核使用的终端为控制台。控制台在Linux启动时,通过命令
console=...指定,如果没有指定控制台,系统把第一个注册的终端(tty)作为控制台。
1. 控制台是一个虚拟的终端,它必须映射到真正的终端上。
2. 控制台可以简单的理解为printk输出的地方。
3. 控制台是个只输出的设备,功能很简单,只能在内核中访问。
伪终端
伪终端设备是一种特殊的终端设备, 由主-从两个成对的设备构成, 当打开主设备时, 对应的从设备随之打开, 形成连接状态。输入到主设备的数据成为从设备的输出,输入到从设备的数据成为主设备的输出, 形成双向管道。伪终端设备常用于远程登录服务器来建立网络和终端的关联。当通过telnet远程登录到另一台主机时,telnet进程与远程主机的telnet服务器相连接. telnet服务器使用某个主设备并通过对应的从设备与telnet进程相互通信。
终端体系
在Linux中,TTY体系分为:TTY核心,TTY线路规程,TTY驱动3部分。TTY核心从用户获取要发送给TTY设备的数据,然后把数据
传递给TTY线路规程, 它对数据进行处理后,负责把数据传递到TTY驱动程序,TTY驱动程序负责格式化数据,并通过硬件发送出
去。
从硬件收到的数据向上通过TTY驱动, 进入TTY线路规程, 再进入TTY核心, 最后被用户获取。TTY驱动可以直接和TTY核心通讯, 但是通常TTY线路规程会修改在两者之间传送的数据。TTY驱动不能直接和线路规程通信,甚至不知道它的存在,线路规程的工作是格式化从用户或者硬件收到的数据. 这种格式化常常实现为一个协议, 如 PPP或 Bluetooth。
1.4 串口驱动程序设计
终端体系—串口
数据流
读操作
TTY驱动从硬件收到数据后,负责把数据传递到TTY 核心,TTY核心将从TTY驱动收到的数据缓存到一个tty_flip_buffer 类型的结构中 。该结构包含两个数据数组。从TTY设备接收到的数据被存储于第一个数组,当这个数组满, 等待数据的用户将被通知。当用户从这个数组读数据时, 任何从TTY驱动新来的数据将被存储在
第2个数组。当第二个数组存满后,数据再次提交给用户, 并且驱动又开始填充第1个数组,以此交替。
驱动描述
Linux内核使用uart_driver描述串口驱动,它包含串口设备
的驱动名、设备名、设备号等信息。
struct uart_driver {
struct module*owner;
const char*driver_name; //驱动名
const char*dev_name; //设备名
int major; //主设备号
int minor; //起始次设备号
int nr; //设备数
struct console*cons;
struct uart_state*state;
struct tty_driver *tty_driver;
};
注册驱动
Linux为串口驱动注册提供了如下接口:
intuart_register_driver(struct uart_driver *drv)
端口描述
uart_port用于描述一个UART端口(一个串口)的地址、FIFO大
小、端口类型等信息
struct uart_port
{
spinlock_t lock; /* 端口锁 */
unsigned int iobase;/* IO端口基地址 */
unsigned char__iomem *membase; /* IO内存基地址 */
unsigned int irq; /*中断号 */
unsigned charfifosize; /* 传输fifo大小 */
const structuart_ops *ops;
...... ...... ............ ...... ...... ...... ...... ...... ...... ......
}
操作串口
uart_ops 定义了针对串口的一系列操作,包括发
送、接收及线路设置等。
struct uart_ops
{
unsignedint(*tx_empty)(struct uart_port*);
void(*set_mctrl)(structuart_port *, unsigned int mctrl);
unsignedint(*get_mctrl)(struct uart_port*);
void(*stop_tx)(structuart_port*); //停止发送
void(*start_tx)(structuart_port*); //开始发送
void(*send_xchar)(structuart_port *, char ch); //发送xchar
void(*stop_rx)(structuart_port*); //停止接收
。。。。。。
}
添加端口
串口核心层提供如下函数来添加1个端口:
intuart_add_one_port(struct uart_driver *drv, struct
uart_port *port)
操作流程
1. 定义一个uart_driver的变量,并初始化;
2. 使用uart_register_driver来注册这个驱动;
3. 初始化uart_port和ops函数表;
4. 调用uart_add_one_port()添加初始化好的uart_port。
实例分析
mini2440串口驱动程序
2 Linux字符设备驱动程序
2.1 Linux驱动程序介绍
Linux驱动程序学习
知识结构:
1. Linux驱动程序设计规范(50%)
2. 内核相关知识(25%)
3. 硬件相关知识(25%)
学习方法:
理论-->实验(疑问)-->理论-->实验-->...
驱动程序?
使硬件工作的软件
早期驱动-模式一
缺点:没有复用驱动程序,每个应用程序都需有驱动程序。
早期驱动-模式二
缺点:应用程序接口需要驱动开发和应用开发人员共同设计,一旦接口发生改变,所有应用程序都需要做相应修改。
驱动分类
字符设备驱动 (重点)
字符设备是一种按字节来访问的设备,字符驱动则负责驱动字符设备,这样的驱动通常实现 open, close,read和 write 系统调用。
网络接口驱动(重点)
任何网络事务都通过一个接口来进行, 一个接口通常是一个硬件设备(eth0), 但是它也可以是一个纯粹的软件设备, 比如回环接口
(lo)。一个网络接口负责发送和接收数据报文。
块设备驱动
在大部分的 Unix 系统, 块设备不能按字节处理数据,只能一次传送一个或多个长度是512字节( 或一个更大的 2 次幂的数 )的整块数据。而Linux则允许块设备传送任意数目的字节。因此, 块和字符设备的区别仅仅是驱动的与内核的接口不同。
驱动程序安装
内核模块方式(已知)
主要用在开发阶段
直接编译进内核
Kconfig ?
位于/kernel/drivers目录下,产生内核menuconfig选项
Makefile ?
位于/kernel/drivers目录下,编译配置。
驱动程序使用
A: Linux用户程序通过设备文件(又名:设备节点)来使用驱动程序操作字符设备和块设备
Q: 设备(字符、块)文件在dev目录下
2.2 字符设备驱动
知识点
设备号
创建设备文件
设备注册
重要数据结构
设备操作
主次设备号
字符设备通过字符设备文件来存取。字符设备文件由使用 ls -l 的输出的第一列的“c”标识。如果使用 ls -l 命令, 会看到在设备文件项中有 2 个数(由一个逗号分隔) 这些数字就是设备文件的主次设备编号。
设备号用来做什么??
主设备号用来标识与设备文件相连的驱动程序。次编号被驱动程序用来辨别操作的是哪个设备。
** 主设备号用来反映设备类型 **
**次设备号用来区分同类型的设备**
Q: 内核中如何描述设备号?
A: dev_t
**其实质为unsigned int 32位整数,其中高12位为主
设备号,低20位为次设备号。
构造设备号:MKDEV(mem_major, 0)
Q: 如何从dev_t中分解出主设备号?
A: MAJOR(dev_t dev)
Q: 如何从dev_t中分解出次设备号?
A: MINOR(dev_t dev)
Linux内核如何给设备分配主设备号?
可以采用静态申请,动态分配两种方法
静态申请
方法:
1、根据Documentation/devices.txt,确定一个没有使用的主设备号
2、使用 register_chrdev_region 函数注册设备号
intregister_chrdev_region(dev_t from, unsigned
count, const char*name)
功能:申请使用从 from 开始的 count 个设备号(主设备号
不变,次设备号增加)
参数:
from:希望申请使用的设备号
count:希望申请使用设备号数目
name:设备名(体现在/proc/devices)
优点: 简单
缺点: 一旦驱动被广泛使用, 这个随机选定的主设备号可能会导致设备号冲突,而使驱动程序无法注册。
动态分配
方法: 使用 alloc_chrdev_region 分配设备号
intalloc_chrdev_region(dev_t *dev, unsigned
baseminor, unsignedcount,const char *name)
功能: 请求内核动态分配 count 个设备号,且次设备号从baseminor开始。
参数:
dev:分配到的设备号
baseminor:起始次设备号
count:需要分配的设备号数目
name:设备名(体现在/proc/devices)
优点: 简单,易于驱动推广
缺点: 无法在安装驱动前创建设备文件(因为安装前还没有分配到主设备号)。
解决办法: 安装驱动后, 从 /proc/devices 中查询设备号
注销设备号
不论使用何种方法分配设备号,都应该在不再使用它们时释放这些设备号。
void unregister_chrdev_region(dev_tfrom,
unsigned count)
功能: 释放从from开始的count个设备号
创建设备文件
1. 使用mknod 命令手工创建
mknod 用法:
mknod filename typemajor minor
filename:设备文件名
type: 设备文件类型
major: 主设备号
minor: 次设备号
例: mknod serial0 c 100 0
2. 自动创建 后面课程介绍
重要数据结构
在Linux字符设备驱动程序设计中,有3种非常重要的数据结构:
Struct file
代表一个打开的文件。系统中每个打开的文件在内核空间都有一个关联的 struct file。它由内核在打开文件时创建, 在文件关闭后释放。
重要成员:
loff_t f_pos /*文件读写位置*/
structfile_operations *f_op
Struct inode
用来记录文件的物理上的信息。因此, 它和代表打开文件的file结构是不同的。一个文件可以对应多个file结构, 但只有一个inode 结构。
重要成员:
dev_t i_rdev:设备号
Structfile_operations
一个函数指针的集合,定义能在设备上进行的操作。结构中的成员指向驱动中的函数, 这些函数实现一个特别的操作, 对于不支持的操作保留为NULL。
例:mem_fops
structfile_operations mem_fops = {
.owner =THIS_MODULE,
.llseek = mem_seek,
.read = mem_read,
.write = mem_write,
.ioctl = mem_ioctl,
.open = mem_open,
.release =mem_release,
};
应用-驱动模型
内核代码导读
应用程序如何访问驱动程序?
(Read_write.c )
设备注册
在linux 2.6内核中,字符设备使用 struct cdev 来描述。
字符设备的注册可分为如下3个步骤:
1. 分配cdev
Struct cdev的分配可使用cdev_alloc函数来完成。
struct cdev*cdev_alloc(void)
2. 初始化cdev
Struct cdev的初始化使用cdev_init函数来完成。
voidcdev_init(struct cdev *cdev, const
structfile_operations *fops)
参数:
cdev: 待初始化的cdev结构
fops: 设备对应的操作函数集
3. 添加cdev
struct cdev的注册使用cdev_add函数来完成。
int cdev_add(structcdev *p, dev_t dev, unsigned count)
参数:
p: 待添加到内核的字符设备结构
dev: 设备号
count: 添加的设备个数
设备注消
字符设备的注销使用cdev_del函数来完成。
int cdev_del(structcdev *p)
参数:
p: 要注销的字符设备结构
设备操作
完成了驱动程序的注册,下一步该做什么呢??
实现设备所支持的操作
int (*open)(structinode *, struct file *)
在设备文件上的第一个操作,并不要求驱动程序一定要实现这个方法。如果该项为NULL,设备的打开操作永远成功。
void(*release)(struct inode *, struct file *)
当设备文件被关闭时调用这个操作。与open相仿,release也可以没有。
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) 从设备中读取数据。
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) 向设备发送数据。
unsigned int (*poll)(struct file *, struct poll_table_struct *) 对应select系统调用
int (*ioctl) (structinode *, struct file *, unsigned int, unsigned long) 控制设备
int (*mmap) (structfile *, struct vm_area_struct *)
将设备映射到进程虚拟地址空间中。
off_t (*llseek)(struct file *, loff_t, int)
修改文件的当前读写位置,并将新位置作为返回值。
Open方法
Open方法是驱动程序用来为以后的操作完成初始化准备工作的。在大部分驱动程序中,open完成如下工作:
初始化设备。
标明次设备号。
Release方法
Release方法的作用正好与open相反。这个设备方法有时也称为close,它应该:
关闭设备。
读和写
读和写方法都完成类似的工作:从设备中读取数据到用户空间;将数据传递
给驱动程序。它们的原型也相当相似:
ssize_txxx_read(struct file * filp, char __user * buff, size_t count, loff_t *offp);
ssize_txxx_write(struct file *filp, char __user * buff, size_t count, loff_t *offp);
对于 2 个方法, filp是文件指针, count是请求传输的数据量。buff 参数指向数据缓存。最后, offp 指出文件当前的访问位置。
Read 和 Write 方法的 buff 参数是用户空间指针。因此, 它不能被内核代码直接引用,理由如下:
用户空间指针在内核空间时可能根本是无效的---没有那个地址的映射。
内核提供了专门的函数用于访问用户空间的指针,例如:
intcopy_from_user(void *to, const void __user *from, int n)
intcopy_to_user(void __user *to, const void *from, int n)
数据模型-读
2.3 字符设备驱动程序实例分析
2.4 驱动调试技术
调试技术分类
对于驱动程序设计来说,核心问题之一就是如何完成调试。当前常用的驱动调试技术可分为:
• 打印调试
• 调试器调试
• 查询调试
打印调试
在调试应用程序时,最常用的调试技术是打印,就是在应用程序中合适的点调用printf。当调试内核代码的时候,可以用printk完成类似任务。
合理使用Printk
在驱动开发时,printk 非常有助于调试。但当正式发行驱动程序时, 应当去掉这些打印语句。但你有可能很快又发现,你又需要在驱动程序中实现一个新功能(或者修复一个bug),这时你又要用到那些被删除的打印语句。这里介绍一种使用printk 的合理方法,可以全局地打开或关闭它们,而不是简单地删除。
#ifdef PDEBUG
#define PLOG(fmt,args...)printk(KERN_DEBUG
"scull:"fmt,##args)
#else
#definePLOG(fmt,args...) /*do nothing */
#endif
Makefile作如下修改:
DEBUG =y
ifeq ($(DEBUG),y)
DEBFLAGS =-O2(优化) -g(调试) -DPDEBUG(define)
else
DEBFLAGS =-O2
endif
CFLAGS +=$(DEBFLAGS)
2.5 并发控制
并发与竞态
并发:多个执行单元同时被执行。
竞态:并发的执行单元对共享资源(硬件资源和软件上的全局变量等)的访问导致的竞争状态
例:
if(copy_from_user(&(dev->data[pos]), buf, count))
ret = -EFAULT;
goto out;
假设有 2 个进程试图同时向一个设备的相同位置写入数据,就会造成数据混乱。
处理并发的常用技术是加锁或者互斥,即确保在任何时间只有一个执行单元可以操作共享资源。在Linux内核中主要通过semaphore机制和spin_lock机制实现。
信号量
Linux内核的信号量在概念和原理上与用户态的信号量是一样的,但是它不能在内核之外使用,它是一种睡眠锁。如果有一个任务
想要获得已经被占用的信号量时,信号量会将这个进程放入一个等待队列,然后让其睡眠。当持有信号量的进程将信号释放后,处于等待队列中的任务将被唤醒,并让其获得信号量。
信号量在创建时需要设置一个初始值,表示允许有几个任务同时访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。
当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果释放后信号量的值为非正数,表明有任务等待当前信号量,因此要唤醒等待该信号量的任务。
信号量的实现也是与体系结构相关的,定义在
<asm/semaphore.h>中,struct semaphore类型用
来表示信号量。
1. 定义信号量
struct semaphoresem;
2. 初始化信号量
void sema_init(struct semaphore *sem, int val)
该函用于数初始化设置信号量的初值,它设置信号量sem的值为val。
void init_MUTEX(struct semaphore *sem)
该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。
v voidinit_MUTEX_LOCKED (struct semaphore *sem)
该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。
定义与初始化的工作可由如下宏一步完成:
DECLARE_MUTEX(name)
定义一个信号量name,并初始化它的值为1。
DECLARE_MUTEX_LOCKED(name)
定义一个信号量name,但把它的初始值设置为0,即锁在创建时就处在已锁状态。
3. 获取信号量
void down(structsemaphore * sem)
获取信号量sem,可能会导致进程睡眠,因此不能在中断上下文使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。
intdown_interruptible(struct semaphore * sem)
获取信号量sem。如果信号量不可用,进程将被置为TASK_INTERRUPTIBLE类型的睡眠状态。
该函数由返回值来区分是正常返回还是被信号中断返回,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。
down_killable(structsemaphore *sem)
获取信号量sem。如果信号量不可用,进程将被置为TASK_KILLABLE类型的睡眠状态。
注:
down()函数现已不建议继续使用。建议使用
down_killable() 或down_interruptible() 函数。
4. 释放信号量
void up(struct semaphore* sem)
该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。
spin_lock
自旋锁最多只能被一个可执行单元持有。自旋锁不会引起调用者睡眠,如果一个执行线程试图获得一个已经被持有的自旋锁,那么线程就会一直进行忙循环,一直等待下去,在那里看是否该自旋锁的保持者已经释放了锁,“自旋”就是这个意思。
spin_lock_init(x)
该宏用于初始化自旋锁x,自旋锁在使用前必须先初始化。
spin_lock(lock)
获取自旋锁lock,如果成功,立即获得锁,并马上返回,否则它将一直自旋在那里,直到该自旋锁的保持者释放。
spin_trylock(lock)
试图获取自旋锁lock,如果能立即获得锁,并返回真,否则立即返回假。它不会一直等待被释放。
spin_unlock(lock)
释放自旋锁lock,它与spin_trylock或spin_lock配对使用。
信号量可能允许有多个持有者,而自旋锁在任何时候只能允许一个持有者。当然也有信号量叫互斥信号量(只能一个持有者),允许有多个持有者的信号量叫计数信号量。
信号量适合于保持时间较长的情况;而自旋锁适合于保持时间非常短的情况,在实际应用中自旋锁控制的代码只有几行,而持有自旋锁的时间也一般不会超过两次上下文切换的时间,因为线程一旦要进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文切换,我们就应该选择信号量。
3 Linux总线设备驱动模型
3.1 Kobject& Kset
Sysfs文件系统
"sysfs is aram-based filesystem initially based on ramfs. It provides a means to exportkernel data structures, their attributes, and the linkages between
them to userspace.”
---documentation/filesystems/sysfs.txt
Linux2.6内核引入了 sysfs 文件系统。sysfs 被看成是与 proc同类别的文件系统。sysfs 把连接在系统上的设备和总线组织成分级的文件,使其从用户空间可以访问到。
Sysfs 被加载在 /sys/ 目录下,它的子目录包括:
• Block:在系统中发现的每个块设备在该目录下对应一个子目录。每个子目录中又包含一些属性文件,它们描述了这个块设备的各方面属性,如:设备大小。(loop块设备是使用文件来模拟的)
• Bus:在内核中注册的每条总线在该目录下对应一个子目录, 如:
ide pci scsi usbpcmcia
其中每个总线目录内又包含两个子目录:
devices 和 drivers , devices目录包含了在整个系统中发现的属于该总线类型的设备,drivers目录包含了注册到该总线的所有驱动。
• Class:将设备按照功能进行的分类,如/sys/class/net目录下包含了所有网络接口。
• Devices:包含系统所有的设备。
• Kernel:内核中的配置参数
• Module:系统中所有模块的信息
• Firmware:系统中的固件
• Fs: 描述系统中的文件系统
• Power:系统中电源选项
Kobject
Kobject 实现了基本的面向对象管理机制,是构成Linux2.6设备模型的核心结构。它与sysfs文件系统紧密相连,在内核中注册的每个kobject对象对应sysfs文件系统中的一个目录。
类似于C++中的基类,Kobject常被嵌入于其他类型(即:容器)中。如bus,devices, drivers 都是典型的容器。这些容器通过kobject连接起来,形成了一个树状结构。
struct kobject {
const char *name;
struct list_headentry;
struct kobject*parent; //指向父对象
struct kset *kset;
struct kobj_type*ktype;
struct sysfs_dirent*sd;
struct kref kref; //对象引用计数
unsigned intstate_initialized:1;
unsigned intstate_in_sysfs:1;
unsigned intstate_add_uevent_sent:1;
unsigned intstate_remove_uevent_sent:1;
};
Kobject操作
•voidkobject_init(struct kobject * kobj)
初始化kobject结构
•int kobject_add(structkobject * kobj)
将kobject对象注册到Linux系统
• intkobject_init_and_add(struct kobject *kobj, struct
kobj_type*ktype,struct kobject *parent, const char
*fmt, ...)
初始化kobject,并将其注册到linux系统
•voidkobject_del(struct kobject * kobj)
从Linux系统中删除kobject对象
•struct kobject*kobject_get(struct kobject *kobj)
将kobject对象的引用计数加1,同时返回该对象指针。
•voidkobject_put(struct kobject * kobj)
将kobject对象的引用计数减1,如果引用计数降为0,则调用release方法释放该kobject对象。
Struct kobj_type
Kobject的ktype成员是一个指向kobj_type结构的指针,该结构中记录了kobject对象的一些属性。
struct kobj_type {
void(*release)(struct kobject *kobj);
struct sysfs_ops*sysfs_ops;
struct attribute**default_attrs;
};
release:用于释放kobject占用的资源,当kobject的引用
计数为0时被调用。
struct attribute {
char * name; /*属性文件名*/
struct module *owner;
mode_t mode; /*属性的保护位*/
};
struct attribute (属性):对应于kobject的目录下的一个文件,Name成员就是文件名。
struct sysfs_ops{
ssize_t(*show)(struct kobject *, struct attribute *,char *);
ssize_t(*store)(struct kobject *,struct attribute *,const char *,size_t);
};
•Show:当用户读属性文件时,该函数被调用,该函数将属性值
存入buffer中返回给用户态;
•Store:当用户写属性文件时,该函数被调用,用于存储用户传
入的属性值。
Kset
kset是具有相同类型的kobject的集合,在sysfs中体现成一个目录,在内核中用kset数据结构表示,定义为:
struct kset {
struct list_headlist; //连接该kset中所有kobject的链表头
spinlock_tlist_lock;
struct kobject kobj;//内嵌的kobject
structkset_uevent_ops *uevent_ops; //处理热插拔事件的操作集合
}
Kset操作
intkset_register(struct kset *kset)
在内核中注册一个kset
voidkset_unregister(struct kset *kset)
从内核中注销一个kset
热插拔在Linux系统中,当系统配置发生变化时,如:添加kset到系统;移动kobject, 一个通知会从内核空间发送到用户空间,这就是热插拔事件。热插拔事件会导致用户空间中相应的处理程序(如udev,mdev)被调用, 这些处理程序会通过加载驱动程序, 创建设备节点等来响应热插拔事件。
操作集合kset_uevent_ops
Structkset_uevent_ops {
int (*filter)(structkset *kset, struct kobject *kobj);
const char*(*name)(struct kset *kset, struct kobject *kobj);
int (*uevent)(structkset *kset, struct kobject *kobj,
struct kobj_uevent_env*env);
}
这三个函数什么时候调用?
当该kset所管理的kobject和kset状态发生变化时(如被加入,移动),这三个函数将被调用。
(例:kobject_uevent调用)
这三个函数的功能是什么?
filter:决定是否将事件传递到用户空间。如果 filter返回 0,将不传递事件。(例: uevent_filter)
name:用于将字符串传递给用户空间的热插拔处理程序。
uevent:将用户空间需要的参数添加到环境变量中。
(例:dev_uevent)
3.2 设备驱动模型
设备模型
随着技术的不断进步,系统的拓扑结构也越来越复杂,对智能电源管理、热插拔的支持要求也越来越高,2.4内核已经难以满足这些需求。为适应这种形势的需要,Linux 2.6内核提供了全新的内核设备模型。
设备模型元素
总线
驱动
设备
总线
总线是处理器和设备之间的通道,在设备模型中, 所有的设备都通过总线相连, 甚至是内部的虚拟“platform”总线。 在 Linux 设
备模型中, 总线由 bus_type 结构表示, 定义在 <linux/device.h>
总线描述
struct bus_type {
const char *name; /*总线名称*/
struct bus_attribute*bus_attrs; /*总线属性*/
structdevice_attribute *dev_attrs; /*设备属性*/
structdriver_attribute *drv_attrs; /*驱动属性*/
int (*match)(structdevice *dev, struct device_driver *drv);
int (*uevent)(structdevice *dev, struct kobj_uevent_env *env);
int (*probe)(structdevice *dev);
int (*remove)(structdevice *dev);
void(*shutdown)(struct device *dev);
int(*suspend)(struct device *dev, pm_message_t state);
int(*suspend_late)(struct device *dev, pm_message_t state);
int (*resume_early)(structdevice *dev);
int (*resume)(structdevice *dev);
struct dev_pm_ops*pm;
structbus_type_private *p;
}
总线注册/删除
总线的注册使用:
bus_register(structbus_type * bus)
若成功,新的总线将被添加进系统,并可在sysfs 的 /sys/bus 下看到。
总线的删除使用:
voidbus_unregister(struct bus_type *bus)
总线方法
int (*match)(structdevice * dev, struct device_driver * drv)
当一个新设备或者驱动被添加到这个总线时,该方法被调用。用于判断指定的驱动程序是否能处理指定的设备。若可以,则返回非零值。
int (*uevent)(structdevice *dev, char **envp, int num_envp,char *buffer, int buffer_size)
在为用户空间产生热插拔事件之前,这个方法允许总线添加环境变量。
总线属性
总线属性由结构bus_attribute 描述,定义如下:
struct bus_attribute{
struct attributeattr;
ssize_t(*show)(struct bus_type *, char * buf);
ssize_t(*store)(struct bus_type *, const char *
buf, size_t count);
}
intbus_create_file(struct bus_type *bus,struct bus_attribute *attr)
创建属性
voidbus_remove_file(struct bus_type *bus, struct bus_attribute *attr)
删除属性
设备
设备描述
Linux 系统中的每个设备由一个 struct device 描述:
struct device {
...... ...... ............ ...... ......
struct kobject kobj;
charbus_id[BUS_ID_SIZE]; /*在总线上唯一标识该设备的字符串 */
struct bus_type /* 设备所在总线 */
*bus;
struct device_driver*driver; /*管理该设备的驱动*/
void *driver_data;
/*该设备驱动使用的私有数据成员 *
struct klist_nodeknode_class;
struct class *class;
structattribute_group **groups;
void(*release)(struct device *dev);
}
设备注册
intdevice_register(struct device *dev)
注册设备
voiddevice_unregister(struct device *dev)
注销设备
**一条总线也是个设备,也必须按设备注册**
设备属性
设备属性由struct device_attribute 描述:
structdevice_attribute
{
struct attributeattr;
ssize_t(*show)(struct device *dev, struct device_attribute *attr,char *buf);
ssize_t(*store)(struct device *dev, struct device_attribute *attr,const char *buf,size_t count);
}
intdevice_create_file(struct device
*device, structdevice_attribute * entry)
创建属性
voiddevice_remove_file(struct device *
dev, structdevice_attribute * attr)
删除属性
驱动
驱动描述
驱动程序由struct device_driver 描述 :
struct device_driver{
const char *name; /*驱动程序的名字( 体现在 sysfs 中 )*/
struct bus_type*bus; /*驱动程序所在的总线*/
struct module*owner;
const char*mod_name;
int (*probe) (structdevice *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
structattribute_group **groups;
struct dev_pm_ops*pm;
struct driver_private*p;
}
驱动注册/注销
intdriver_register(struct device_driver *drv)
注册驱动
voiddriver_unregister(struct device_driver *drv)
注销驱动
驱动属性
驱动的属性使用struct driver_attribute 来描述:
structdriver_attribute {
struct attributeattr;
ssize_t(*show)(struct device_driver *drv,char *buf);
ssize_t(*store)(struct device_driver *drv,const char *buf, size_t count);
}
intdriver_create_file(struct device_driver * drv,
structdriver_attribute * attr)
创建属性
voiddriver_remove_file(struct device_driver * drv,
structdriver_attribute * attr)
删除属性
3.3 platform驱动程序
Platform总线
Platform总线是linux2.6内核加入的一种虚拟总线。platform机制的本身使用并不复杂,由两部分组成:
platform_device和platform_driver
Platform 驱动与传统的设备驱动模型相比,优势在于platform机制将设备本身的资源注册进内核,由内核统一管理,在驱动程序使用这些资源时使用统一的接口,这样提高了程序的可移植性。
工作流程
通过platform机制开发底层设备驱动的流程如图:
平台设备描述
平台设备使用Struct Platform_device来描述:
structplatform_device {
const char *name; /*设备名*/
int id; /*设备编号,配合设备名使用*/
struct device dev;
u32 num_resources;
struct resource*resource; /*设备资源*/
}
StructPlatform_device的分配使用:
structplatform_device
*platform_device_alloc(constchar *name, int id)
参数:
name: 设备名
id: 设备id,一般为-1
平台设备注册
注册平台设备,使用函数:
intplatform_device_add(struct platform_device *pdev)
设备资源
平台设备资源使用struct resource来描述:
struct resource {
resource_size_tstart; //资源的起始物理地址
resource_size_t end;//资源的结束物理地址
const char *name; //资源的名称
unsigned long flags;//资源的类型,比如MEM,IO,IRQ类型
struct resource*parent, *sibling, *child;
//资源链表指针
}
例
static structresource s3c_wdt_resource1 = {
.start = 0x44100000,
.end = 0x44200000,
.flags =IORESOURCE_MEM,
}
static structresource s3c_wdt_resource2 = {
.start = 20,
.end = 20,
.flags =IORESOURCE_IRQ,
}
获取资源
struct resource*platform_get_resource(struct platform_device *dev, unsigned int type, unsignedint num)
参数:
dev: 资源所属的设备
type: 获取的资源类型
num: 获取的资源数
例:
platform_get_resource(pdev,IORESOURCE_IRQ, 0)
获取中断号
平台驱动描述
平台驱动使用struct platform_driver 描述:
structplatform_driver {
int (*probe)(structplatform_device *);
int (*remove)(structplatform_device *);
void(*shutdown)(struct platform_device *);
int (*suspend)(structplatform_device *, pm_message_t state);
int(*suspend_late)(struct platform_device *, pm_message_t state);
int(*resume_early)(struct platform_device *);
int (*resume)(structplatform_device *);
struct device_driverdriver;
}
平台驱动注册
平台驱动注册使用函数:
int platform_driver_register(structplatform_driver *)
3.4 中断处理
中断概念
为什么需要中断?
1.外设的处理速度一般慢于CPU
2.CPU不能一直等待外部事件
所以设备必须有一种方法来通知CPU它的工作进度,这种方法就是中断。
中断实现
在Linux驱动程序中,为设备实现一个中断包含两个步骤:
1.向内核注册中断
2.实现中断处理函数
中断注册
request_irq用于实现中断的注册功能:
intrequest_irq(unsigned int irq,
void (*handler)(int,void*, struct pt_regs *),
unsigned long flags,
const char *devname,
void *dev_id)
返回0表示成功,或者返回一个错误码
unsigned int irq
中断号。
void(*handler)(int,void *,struct pt_regs *)
中断处理函数。
unsigned long flags
与中断管理有关的各种选项。
const char * devname
设备名
void *dev_id
共享中断时使用。
在flags参数中,可以选择一些与中断管理有关的选项,如:
•IRQF_DISABLED(SA_INTERRUPT)
如果设置该位,表示是一个“快速”中断处理程序;如果没有设置这位,那么是一个“慢速”中断处理程序。
•IRQF_SHARED(SA_SHIRQ)
该位表明中断可以在设备间共享。
快速/慢速中断
这两种类型的中断处理程序的主要区别在于:快速中断保证中断处理的原子性(不被打断),而慢速中断则不保证。换句话说,也就是“开启中断”标志位(处理器IF)在运行快速中断处理程序时是
关闭的,因此在服务该中断时,不会被其他类型的中断打断;而调用慢速中断处理时,其它类型的中断仍可以得到服务。
共享中断
共享中断就是将不同的设备挂到同一个中断信号线上。Linux对共享的支持主要是为PCI设备服务。
共享中断也是通过request_irq函数来注册的,
但有三个特别之处:
1. 申请共享中断时,必须在flags参数中指定IRQF_SHARED位
2. dev_id参数必须是唯一的。
3.共享中断的处理程序中,不能使用disable_irq(unsigned int irq)
为什么?
如果使用了这个函数,共享中断信号线的其它设备将同样无法使用中断,也就无法正常工作了。
中断处理程序
什么是中断处理程序,有何特别之处?中断处理程序就是普通的C代码。特别之处在于中断处理程序是在中断上下文中运行的,它的行为受到某些限制:
1. 不能向用户空间发送或接受数据
2. 不能使用可能引起阻塞的函数
3. 不能使用可能引起调度的函数
中断处理函数流程
voidshort_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
/* 判断是否是本设备产生了中断(为什么要做这样的检测?) */
value =inb(short_base);
if (!(value &0x80)) return;
/* 清除中断位(如果设备支持自动清除,则不需要这步) */
outb(value &0x7F, short_base);
/* 中断处理,通常是数据接收 */
。。。。。。。。。
/* 唤醒等待数据的进程 */
wake_up_interruptible(&short_queue);
释放中断
当设备不再需要使用中断时(通常在驱动卸载时), 应当把它们返还给系统,使用:
voidfree_irq(unsigned int irq, void *dev_id)
3.5 按键驱动
工作原理
S3c2440的GPIO_G0,GPIO_G3,GPIO_G5,GPIO_G6, GPIO_G7,GPIO_G11作为输入口,读取按键状态,这六个I/O口分别使用外部中断EINT8,EINT11,EINT13,EINT14,EINT15,
EINT19。当按键松开时,I/O口处于高电平,得到逻辑1,当按键按下时,I/O被拉低,得到逻辑0。
4 Linux网卡触摸屏驱动程序
4.1 网络体系架构
需要掌握的知识点:
1. Linux的协议栈层次
2. Linux的网络子系统架构
协议栈
Linux的优点之一在于它丰富而稳定的网络协议栈。其范围从协议无关层(例如通用 socket 层接口或设备层)到各种具体的网络协议实现。
对于网络的理论介绍一般都采用 OSI(Open Systems Interconnection)模型,但是Linux 中网络栈的介绍一般分为四层的Internet 模型。
协议栈层次对比
网络接口层
网络接口层把数据链路层和物理层合并在了一起,提供访问物理设备的驱动程序,对应的网络协议主要是以太网协议。
网际层
网络层协议管理离散的计算机间的数据传输,如IP协议为用户和远程计算机提供了信息包的传输方法,确保信息包能正确地到达
目的机器。重要的网络层协议包括ARP(地址解析协议)、ICMP(Internet控制消息协议)和IP协议(网际协议)等。
传输层
传输层的功能包括:格式化信息流、提供可靠传输。传输层包括TCP(Transmission Control Protocol,传输控制协议)和UDP
(User DatagramProtocol,用户数据报协议),它们是传输层中最主要的协议。
应用层
应用层位于协议栈的顶端,它的主要任务是服务于应用,如利用FTP(文件传输协议)传输一个文件。常见的应用层协议有:
HTTP,FTP,Telnet等。应用层是Linux网络设定很关键的一层,Linux服务器的配置文档主要针对应用层中的协议。
Linux网络子系统
Linux 网络子系统的顶部是系统调用接口层。它为用户空间的应用程序提供了一种访问内核网络子系统的方法。位于其下面的是一个协议无关层,它提供了一种通用方法来使用传输层协议。然后是具体协议的实现,在 Linux 中包括内嵌的协议 TCP、UDP,当然还有 IP。然后是设备无关层,它提供了协议与设备驱动通信的通用接口,最下面是设备驱动程序。
系统调用接口
为应用程序提供访问内核网络子系统的方法:Socket系统调用。
协议无关接口
实现一组通用函数来访问各种不同的协议:通过socket实现。Linux 中的 socket 使用struct sock来描述,这个结构包含了特定socket 所需要的所有状态信息,还包括socket 所使用的特定协议和在 socket 上可以执行的一些操作。
网络协议
网络协议层用于实现各种具体的网络协议,如: TCP、UDP 等。
设备无关接口
设备无关接口将协议与各种网络设备驱动连接在一起。这一层提供一组通用函数供底层网络设备驱动程序使用,让它们可以对高层协议栈进行操作。首先,设备驱动程序可能会通过调用 register_netdevice 或unregister_netdevice 在内核中进行注册或注销。调用者首先填写 net_device 结构,然后传递这个结构进行注册。内核调用它的 init 函数(如果定义了这种函数),然后执行一组健全性检查,并将新设备添加到设备列表中(内核中的活动设备链表)。
要从协议层向设备发送数据,需要使用dev_queue_xmit 函数,这个函数对数据进行排队,并交由底层设备驱动程序进行最终传输报文的接收通常是使用 netif_rx 执行的。当底层设备驱动
程序接收到一个报文(包含在所分配的 sk_buff中)时,就会通过调用 netif_rx 将数据上传至设备无关层,然后,这个函数通过 netif_rx_schedule将 sk_buff 在上层协议队列中进行排队,供以后进行处理。
驱动程序
网络体系结构的最底部是负责管理物理网络设备的设备驱动程序层。
4.2 网卡驱动设计
设备描述
每个网络接口都由一个net_device结构来描述,该结构可使用如下内核函数动态分配:
1、struct net_device *alloc_netdev(intsizeof_priv, const char *mask, void (*setup)(struct net_device *))
sizeof_priv 私有数据区大小; mask:设备名; setup 初始化函数
2、struct net_device *alloc_etherdev(int sizeof_priv)
net_device
结构 net_device 的主要成员包括:
char name[IFNAMSIZ]
设备名,如:eth%d
unsigned long state
设备状态
unsigned longbase_addr
I/O 基地址
unsigned int irq
中断号
int (*init)(structnet_device *dev)
初始化函数。该函数在register_netdev时被调用来完成对 net_device 结构的初始化
和字符驱动一样, 网络设备也要声明能操作它的函数。有些操作可以保留为 NULL, 有的可以通过 ether_setup 来使用默认设置。网络接口的设备方法可分为两组:基本的和可选的,基本方法包括那些使用接口所必需的;可选的方法实现更多高级的功能。
基本方法
int (*open)(structnet_device *dev)
打开接口。ifconfig 激活时,接口将被打开。
int (*stop)(structnet_device *dev)
停止接口。该什么时候调用呢?
int(*hard_start_xmit) (struct sk_buff *skb, struct
net_device *dev)
数据发送函数。
可选操作
int(*do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd)
处理特定于接口的 ioctl 命令
int(*set_mac_address)(struct net_device *dev, void *addr)
改变Mac地址的函数,需要硬件支持该功能
设备注册
网络接口驱动的注册方式与字符驱动不同之处在于它没有主次设备号,并使用如下函数注册。
intregister_netdev(struct net_device *dev)
sk_buff
Linux内核中的每个网络数据包都由一个套接字缓冲区结构 struct sk_buff 描述,即一个sk_buff结构就是一个包,指向sk_buff的指针通常被称做skb。
该结构包含如下重要成员:
• struct device*dev; //处理该包的设备
• __u32 saddr; //IP源地址
• __u32 daddr; //IP目的地址
• __u32 raddr; //IP路由器地址
unsigned char *head;//分配空间的开始
unsigned char *data;//有效数据的开始
unsigned char *tail;//有效数据的结束
unsigned char *end;//分配空间的结束
unsigned long len;//有效数据的长度
Skb操作函数
操作sk_buff的内核函数如下:
•struct sk_buff*alloc_skb(unsigned int len, int priority)
分配一个sk_buff 结构,供协议栈代码使用
•struct sk_buff*dev_alloc_skb(unsigned int len)
分配一个sk_buff 结构,供驱动代码使用
•unsigned char*skb_push(struct sk_buff *skb, int len)
向后移动skb的tail指针,并返回tail移动之前的值。函数常用来:
•unsigned char*skb_put(struct sk_buff *skb, int len)
向前移动skb的head指针,并返回head移动之后的值。函数常用来:
•kfree_skb(structsk_buff *skb)
释放一个sk_buff 结构,供协议栈代码使用
•dev_kfree_skb(structsk_buff *skb)
释放一个sk_buff 结构,供驱动代码使用
设备打开
Open 请求任何它需要的系统资源并且启动接口 :
注册中断,DMA等
设置寄存器,启动设备
启动发送队列
例
int net_open(structnet_device *dev)
{
/*申请中断*/
request_irq(dev->irq,&net_interrupt, SA_SHIRQ,
“dm9000”, dev);
/* 设置寄存器,启动设备 */
...... ...... ............
/*启动发送队列*/
netif_start_queue(dev);
}
数据发送
当核心需要发送一个数据包时,它调用hard_start_transmit函数,该函数将最终调用到net_device结构中的hard_start_xmit函数指针。
数据接收
网络接口驱动可以实现两种方式的报文接收:中断和查询,Linux中驱动多采用中断方式。
接收流程
1、分配Skb skb = dev_alloc_skb(pkt->datalen +2)
2、从硬件中读取数据到Skb
3、调用netif_rx将数据交给协议栈 netif_rx(skb)
中断处理
网络接口通常支持3种类型的中断: 新报文到达中断、报文发送完成中断和出错中断。中断处理程序可通过查看网卡中的中断状态寄存器,来分辨出中断类型。
4.3 CS8900A网卡驱动分析
芯片介绍
DM9000是开发板经常采用的网络芯片,是一种高度集成而且功耗很低的高速网络控制器,可以和CPU直连,支持10/100M以太网
连接,芯片内部自带16K的SRAM(3KB用来发送,13KB用来接收) 。
Dm9000在收到由上层发来的以太网帧后,开始侦听网络线路,如果线路忙,就等到线路空闲为止,否则立即发送该数据帧。接收时,
它将从以太网收到的数据包在经过解码、去掉帧头和地址检验等步骤后缓存在片内。在CRC校验通过后, 它会通知CPU收到了数据帧。
寄存器
参考《DM9000.pdf》
工作原理
参考《DM9000.pdf》
9.1 host interface
9.3 packet transmission
9.4 packet reception
以太网帧
实例分析
dm9000.c
4.4 输入子系统
前面章节讲解按键设备驱动,实际上,在Linux系统中,一种更值得推荐的实现这类设备驱动的方法是利用input子系统。Linux系
统提供了input子系统,按键、触摸屏、鼠标等都可以利用input接口函数来实现设备驱动。
体系结构
设备描述
在Linux内核中,input设备用input_dev结构体描述,使用input子系统实现输入设备驱动的时候,驱动的核心工作是向系统报告按键、触摸屏、键盘、鼠标等输入事件(event,通input_event
结构体描述),不再需要关心文件操作接口,因为input子系统已经完成了文件操作接口。驱动报告的事件经过InputCore和 Eventhandler最终到达用户空间。
设备注册/注销
注册输入设备的函数为:
intinput_register_device(struct input_dev *dev)
注销输入设备的函数为:
voidinput_unregister_device(struct input_dev *dev)
驱动实现-事件支持
设备驱动通过set_bit()告诉input子系统它支持哪些事件,
如下所示:
set_bit(EV_KEY,button_dev.evbit)
Struct iput_dev中有两个成员,一个是evbit;一个是keybit。分
别用来表示设备所支持的事件类型和按键类型。
事件类型:
EV_RST Reset EV_KEY 按键
EV_REL 相对坐标 EV_ABS 绝对坐标
EV_MSC 其它 EV_LED LED
EV_SND 声音 EV_REP Repeat
EV_FF 力反馈
驱动实现-报告事件
用于报告EV_KEY、EV_REL和EV_ABS事件的函数分别为:
voidinput_report_key(struct input_dev *dev,
unsigned int code,int value)
voidinput_report_rel(struct input_dev *dev,
unsigned int code,int value)
voidinput_report_abs(struct input_dev *dev,
unsigned int code,int value)
code:
事件的代码。如果事件的类型是EV_KEY,该代码code为设备键盘代码。代码值0~127为键盘上的按键代码,0x110~0x116 为鼠标上按键代码,其中0x110(BTN_LEFT)为鼠标左键,0x111(BTN_RIGHT)为鼠标右键,0x112(BTN_ MIDDLE)为鼠标中键。其它代码含义请参看include/linux/input.h文件
value:
事件的值。如果事件的类型是EV_KEY,当按键按下时值为
1,松开时值为0。
input_sync()用于事件同步,它告知事件的接收者:驱动已经发出了一个完整的报告。
例如,在触摸屏设备驱动中,一次坐标及按下状态的整个报告过程如下:
input_report_abs(input_dev,ABS_X, x); //X坐标
input_report_abs(input_dev,ABS_Y, y); //Y坐标
input_report_abs(input_dev,ABS_PRESSURE, pres);
//压力
input_sync(input_dev);//同步
实例分析
/*在按键中断中报告事件*/
static voidbutton_interrupt(int irq, void *dummy, struct pt_regs *fp)
{
input_report_key(&button_dev,BTN_0, inb(BUTTON_PORT0));
input_report_key(&button_dev,BTN_1, inb(BUTTON_PORT1));
input_sync(&button_dev);
}
static int _ _initbutton_init(void){
/*申请中断*/
if(request_irq(BUTTON_IRQ, button_interrupt, 0, "button", NULL))
return - EBUSY;
set_bit(EV_KEY,button_dev.evbit)//支持EV_KEY事件
set_bit(BTN_0,button_dev.keybit); //设备支持两个键
set_bit(BTN_1,button_dev.keybit);
input_register_device(&button_dev);//注册input设备
}
应用程序
struct input_event
{
struct timeval time;//按键时间
__u16 type; //类型,在下面有定义
__u16 code; //要模拟成什么按键
__s32 value;//是按下还是释放
}
struct input_eventev_mouse[2];
fd = open("/dev/input/event3",O_RDWR);
while(1)
{
count=read(fd,ev_mouse, sizeof(struct input_event));
for(i=0;i<(int)count/sizeof(structinput_event);i++)
{
if(EV_REL==ev_mouse[i].type)
{
printf(“time:%ld.%d”,ev_mouse[i].time.tv_sec,ev_mouse[i].time.tv_usec);
printf(“ type:%dcode:%d value:%d\n",ev_mouse[i].type,ev_mouse[i].code,ev_mouse[i].value);
}
if(EV_KEY==ev_mouse[i].type)
{
printf("time:%ld.%d",ev_mouse[i].time.tv_sec,ev_mouse[i].time.tv_usec);
printf("type:%d code:%dvalue:%d\n",ev_mouse[i].type,ev_mouse[i].code,ev_mouse[i].value);
}
}
}
4.5 触摸屏驱动设计
原理概述
触摸屏分为电阻式、电容式、声表面波式和红外线扫描式等类型,使用得最多的是4线电阻式触摸屏。 S3C2440触摸屏由横向电阻比和纵向电阻线组成,由nYPON、YMON、nXPON、XMON四个控制信号控制4个MOS管(S1、S2、S3、S4)的通
断。
控制器
S3C2440触摸屏控制器有2种处理模式:
1X/Y位置分别转换模式。触摸屏控制器包括两个控制阶段,X坐标转换阶段和Y坐标转换阶段。
2X/Y位置自动转换模式。触摸屏控制器将自动转换X和Y坐标。
工作流程
1. Select SeparateX/Y Position Conversion Mode or Auto(Sequential) X/Y Position Conversion Modeto get X/Y position.
2. Set Touch ScreenInterface to Waiting Interrupt Mode,
3. If interruptoccurs, then appropriate conversion (Separate X/Y Position Conversion Mode orAuto (Sequential)X/Y Position Conversion Mode) is activated.
4. After get theproper value about X/Y position, return to Waiting for Interrupt Mode.
中断等待
When Touch ScreenController is in Waitingfor Interrupt Mode, it waits for Stylus down.Thecontroller, generates Interrupt (INT_TC)signals when the Stylus is down onTouchScreen Panel. After an interrupt occurs, Xand Y position can be read bythe proper conversion mode (Separate X/Y position conversion Mode or Auto X/YPosition Conversion Mode).
实例分析
触摸屏驱动程序
5 Linux高级字符设备驱动
5.1 ioctl设备控制
ioctl 用来做什么?
大部分驱动除了需要具备读写设备的能力外,还需要具备对硬件控制的能力。例如,要求设备报告错误信息,改变波特率,这些操作常常通过 ioctl方法来实现。
用户使用方法
在用户空间,使用ioctl 系统调用来控制设备,原型如下:
int ioctl(intfd,unsigned long cmd,...)
原型中的点表示这是一个可选的参数,存在与否依赖于控制命令(第 2 个参数 )是否涉及到与设备的数据交互。
驱动ioctl方法
ioctl 驱动方法有和用户空间版本不同的原型:
int (*ioctl)(structinode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
cmd参数从用户空间传下来,可选的参数 arg 以一个unsigned long 的形式传递,不管它是一个整数或一个指针。如果cmd命令不涉及数据传输,则第 3 个参数arg的值无任何意义。
如何实现ioctl方法?
步骤:
1. 定义命令
在编写ioctl代码之前,首先需要定义命令。为了防止对错误的设备使用正确的命令,命令号应该在系统范围内是唯一的。ioctl 命令编码被划分为几个位段,include/asm/ioctl.h中定义了这些位字段:类型(幻数),序号,传送方向,参数的大小。
Documentation/ioctl-number.txt文件中罗列了在内核中已经使用了的幻数。
定义 ioctl 命令的正确方法是使用 4 个位段, 这个列表中介绍的符号定义在<linux/ioctl.h>中:
Type
幻数(类型): 表明哪个设备的命令,在参考了 ioctl-number.txt之后选出,8 位宽。
Number
序号,表明设备命令中的第几个,8 位宽。
Direction
数据传送的方向,可能的值是 _IOC_NONE(没有数据传输),
_IOC_READ,_IOC_WRITE。 数据传送是从应用程序的观
点来看待的,_IOC_READ 意思是从设备读。
Size
用户数据的大小。(13/14位宽,视处理器而定)
内核提供了下列宏来帮助定义命令:
_IO(type,nr)
没有参数的命令
_IOR(type,nr,datatype)
从驱动中读数据
_IOW(type,nr,datatype)
写数据到驱动
_IOWR(type,nr,datatype)
双向传送,type 和 number 成员作为参数被传递。
定义命令范例
#defineMEM_IOC_MAGIC ‘m’ //定义幻数
#define MEM_IOCSET
_IOW(MEM_IOC_MAGIC,0, int)
#define MEM_IOCGQSET
_IOR(MEM_IOC_MAGIC,1, int)
2. 实现命令
Ioctl函数的实现包括如下3个技术环节:
(1). 返回值
Ioctl函数的实现通常是根据命令执行的一个switch语句。但是,当命令号不能匹配任何一个设备所支持的命令时,通常返回-EINVAL(“非法参数”)。
(2). 参数使用
如果是一个整数,可以直接使用。如果是指针,我们必须确保这个用户地址是有效的,因此使用前需进行正确的检查。
不需要检测:
copy_from_user
copy_to_user
get_user
put_user
需要检测:
__get_user
__put_user
int access_ok(inttype, const void *addr, unsigned long size)
第一个参数是 VERIFY_READ 或者 VERIFY_WRITE,用来表明是读用户内存还是写用户内存。addr 参数是要操作的用户内存地址,size 是操作的长度。如果 ioctl 需要从用户空间读一个整数,那么size参数等于 sizeof(int)。
access_ok 返回一个布尔值: 1 是成功(存取没问题)和 0 是
失败(存取有问题),如果该函数返回失败, 则Ioctl应当返回
–EFAULT 。
if (_IOC_DIR(cmd)& _IOC_READ)
err =!access_ok(VERIFY_WRITE, (void __user *)arg,
_IOC_SIZE(cmd));
//why _IOC_READ 对应VERIFY_WRITE ???
else if(_IOC_DIR(cmd) & _IOC_WRITE)
err =!access_ok(VERIFY_READ, (void __user *)arg,
_IOC_SIZE(cmd));
if (err)
return -EFAULT;
(3). 命令操作
switch(cmd)
{
caseMEM_IOCSQUANTUM: /* Set: arg points to the value */
retval =__get_user(scull_quantum, (int *)arg);
break;
caseMEM_IOCGQUANTUM: /* Get: arg is pointer to result */
retval =__put_user(scull_quantum, (int *)arg);
break;
default:
return –EINVAL;
}
5.2 内核等待队列
等待队列
在Linux驱动程序设计中,可以使用等待队列来实现进程的阻塞,等待队列可看作保存进程的容器,在阻塞进程时,将进程放入等待队列,当唤醒进程时,从等待队列中取出进程。
Linux 2.6内核提供了如下关于等待队列的操作:
1、定义等待队列
wait_queue_head_tmy_queue
2、初始化等待队列
init_waitqueue_head(&my_queue)
3、定义并初始化等待队列
DECLARE_WAIT_QUEUE_HEAD(my_queue)
4、有条件睡眠
wait_event(queue,condition)
当condition(一个布尔表达式)为真时,立即返回;否则让进程进入TASK_UNINTERRUPTIBLE模式的睡眠,并挂在queue参数所指定的等待队列上。
wait_event_interruptible(queue,condition)
当condition(一个布尔表达式)为真时,立即返回;否则让进程进入TASK_INTERRUPTIBLE的睡眠,并挂在queue参数所指定的等待队列上。
intwait_event_killable(wait_queue_t queue, condition)
当condition(一个布尔表达式)为真时,立即返回;否则让进程进入TASK_KILLABLE的睡眠,并挂在queue参数所指定的等待队列上。
5、无条件睡眠(老版本,建议不再使用)
sleep_on(wait_queue_head_t*q)
让进程进入不可中断的睡眠,并把它放入等待队列q。
interruptible_sleep_on(wait_queue_head_t*q)
让进程进入可中断的睡眠,并把它放入等待队列q。
6、从等待队列中唤醒进程
wake_up(wait_queue_t*q)
从等待队列q中唤醒状态为TASK_UNINTERRUPTIBLE,
TASK_INTERRUPTIBLE,TASK_KILLABLE的所有进程。
wake_up_interruptible(wait_queue_t*q)
从等待队列q中唤醒状态为TASK_INTERRUPTIBLE 的进程。
5.3 阻塞型字符设备驱动
前一节我们在设计简单字符驱动程序时,跳过了一个重要的问题:当一个设备无法立刻满足用户的读写请求时应当如何处理? 例如:调用read时没有数据可读, 但以后可能会有;或者一个进程试图向设备写入数据,但是设备暂时没有准备好接收数据。应用程序通常不关心这种问题,应用程序只是调用 read 或 write 并得到返回值。驱动程序应当(缺省地)阻塞进程,使它进入睡眠,直到请求可以得到满足。
阻塞方式
在阻塞型驱动程序中,Read实现方式如下:
如果进程调用read,但设备没有数据或数据不足,进程阻塞。当新数据到达后,唤醒被阻塞进程。
在阻塞型驱动程序中,Write实现方式如下:
如果进程调用了write,但设备没有足够的空间供其写入数据,进程阻塞。当设备中的数据被读走后,缓冲区中空出部分空间,则唤醒进程。
非阻塞方式
阻塞方式是文件读写操作的默认方式,但应用程序员可通过使用O_NONBLOCK标志来人为的设置读写操作为非阻塞方式(该标志定义在<linux/fcntl.h>中,在打开文件时指定)。
如果设置了O_NONBLOCK标志,read和write的行为是不同的。如果进程在没有数据就绪时调用了read,或者在缓冲区没有空间时调用了write,系统只是简单地返回-EAGAIN,而不会阻塞进程。
5.4 poll设备操作
什么是Poll方法,功能是什么?
系统调用(用户空间) 驱动(内核空间)
Open Open
Close Release
Read Read
Write Write
Ioctl Ioctl
lseek llseek
Select Poll
Select系统调用
Select系统调用用于多路监控,当没有一个文件满足要求时,select将阻塞调用进程。
int select(intmaxfd, fd_set *readfds, fd_set *writefds, fe_set *exceptfds, const structtimeval *timeout)
Maxfd:
文件描述符的范围,比待检测的最大文件描述符大1
Readfds:
被读监控的文件描述符集
Writefds:
被写监控的文件描述符集
Exceptfds:
被异常监控的文件描述符集;
Timeout:
定时器
Timeout取不同的值,该调用有不同的表现:
Timeout值为0,不管是否有文件满足要求,都立刻返回,无文件满足要求返回0,有文件满足要求返回一个正值。
Timeout为NULL,select将阻塞进程,直到某个文件满足要求
Timeout 值 为 正 整 数 , 就是 等 待 的 最 长 时 间 , 即
select在timeout时间内阻塞进程。
Select调用返回时,返回值有如下情况:
1.正常情况下返回满足要求的文件描述符个数;
2.经过了timeout等待后仍无文件满足要求,返回值为0;
3.如果select被某个信号中断,它将返回-1并设置errno为EINTR。
4.如果出错,返回-1并设置相应的errno。
使用方法
1. 将要监控的文件添加到文件描述符集
2. 调用Select开始监控
3. 判断文件是否发生变化
系统提供了4个宏对描述符集进行操作:
#include<sys/select.h>
void FD_SET(int fd,fd_set *fdset)
void FD_CLR(int fd,fd_set *fdset)
void FD_ZERO(fd_set*fdset)
void FD_ISSET(intfd, fd_set *fdset)
宏FD_SET将文件描述符fd添加到文件描述符集fdset中;
宏FD_CLR从文件描述符集fdset中清除文件描述符fd;
宏FD_ZERO清空文件描述符集fdset;
在调用select后使用FD_ISSET来检测文件描述符集fdset中的文件fd发生了变化。
FD_ZERO(&fds);//清空集合
FD_SET(fd1,&fds);//设置描述符
FD_SET(fd2,&fds);//设置描述符
maxfdp=fd1+1; //描述符最大值加1,假设fd1>fd2
switch(select(maxfdp,&fds,NULL,NULL,&timeout))
case -1:exit(-1);break; //select错误,退出程序
case 0:break;
default:
if(FD_ISSET(fd1,&fds))//测试fd1是否可读
Poll方法
应用程序常常使用select系统调用,它可能会阻塞进程。这个调用由驱动的 poll 方法实现,原型为:
unsigned int(*poll)(struct file *filp,poll_table *wait)
Poll设备方法负责完成:
1. 使用poll_wait将等待队列添加到poll_table中。
2. 返回描述设备是否可读或可写的掩码。
位掩码
POLLIN 设备可读
POLLRDNORM 数据可读
POLLOUT 设备可写
POLLWRNORM 数据可写
设备可读通常返回(POLLIN|POLLRDNORM)
设备可写通常返回(POLLOUT|POLLWRNORM)
范例
static unsigned intmem_poll(struct file *filp,poll_table *wait)
{
struct scull_pipe*dev =filp->private_data;
unsigned int mask=0;
/* 把进程添加到等待队列 */
poll_wait(filp,&dev->inq,wait);
/*返回掩码*/
if (有数据可读)
mask = POLLIN|POLLRDNORM;/*设备可读*/
return mask;
}
工作原理
Poll方法只是做一个登记,真正的阻塞发生在select.c 中的 do_select函数。
5.5 自动创建设备文件
自动创建(2.4内核)
devfs_register(devfs_handle_t dir,const char *name,unsigned int flags,unsigned intmajor,unsigned int minor,umode_t mode,void *ops,void *info)
在指定的目录中创建设备文件。dir:目录名,为空表示在/dev/目录下创建;name:文件名;flags:创建标志;major:主设号;minor:次设备号;mode:创建模式;ops:操作函数集;info:通常为空
自动创建(2.6内核)
从Linux 2.6.13开始,devfs不复存在,udev成为devfs的替代。相比devfs,udev(mdev用于嵌入式系统)存在于应用层,busybox含有这个软件。利用udev(mdev)来实现设备文件的自动创建很简单,在驱动初始化的代码里调用
class_create为该设备创建一个class,再为每个设备调用 device_create创建对应的设备。
例:
struct class*myclass = class_create(THIS_MODULE,
“my_device_driver”);
device_create(myclass,NULL, MKDEV(major_num, 0), NULL,“my_device”);
当驱动被加载时,udev( mdev )就会自动在/dev下创建my_device设备文件。
6 Linux硬件设备访问
6.1 mmap设备操作
mmap系统调用
void* mmap ( void *addr , size_t len , int prot , int flags ,int fd , off_t offset )
内存映射函数mmap, 负责把文件内容映射到进程的虚拟内存空间, 通过对这段内存的读取和修改,来实现对文件的读取和修改,而不需要再调用read,write等操作。
addr:
指定映射的起始地址, 通常设为NULL, 由系统指定。
length:
映射到内存的文件长度。
prot:
映射区的保护方式, 可以是:
PROT_EXEC: 映射区可被执行
PROT_READ: 映射区可被读取
PROT_WRITE: 映射区可被写入
flags: 映射区的特性, 可以是:
MAP_SHARED:
写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。
MAP_PRIVATE:
对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。
fd:
由open返回的文件描述符, 代表要映射的文件。
offset:
以文件开始处的偏移量, 必须是分页大小的整数倍, 通常为0, 表示从文件头开始映射。
解除映射
int munmap(void*start,size_t length)
功能:
取消参数start所指向的映射内存,参数length表示欲取消的内存大小。
返回值:
解除成功返回0,否则返回-1,错误原因存于errno中。
虚拟内存区域
虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。一个进程的内存映象由下面几部分组成:程序代码、数据、BSS和栈区域,以及内存映射的区域。
一个进程的内存区域可以通过查看
/proc/pid/maps
08048000-0804f000r-xp 00000000 08:01 573748 /sbin/rpc.statd #text
0804f000-08050000rw-p 00007000 08:01 573748 /sbin/rpc.statd #data
08050000-08055000rwxp 00000000 00:00 0 #bss
040000000-40015000r-xp 00000000 08:01 933965 /lib/ld2.3.2.so #text
40015000-40016000rw-p 00014000 08:01 933965 /lib/ld-2.3.2.so #data
每一行的域为:
start_end permoffset major:minor inode
Start: 该区域起始虚拟地址
End: 该区域结束虚拟地址
Perm: 读、写和执行权限;表示对这个区域,允许进程做什么。这个域的最后一个字符要么是p表示私有的,要么是s表示共享的。
Offset: 被映射部分在文件中的起始地址
Major、minor:主次设备号
Inode:索引结点
vm_area_struct
Linux内核使用结构vm_area_struct (<linux/mm_types.h>)来描述虚拟内存区域,其中几个主要成员如下:
unsigned longvm_start
虚拟内存区域起始地址
unsigned long vm_end
虚拟内存区域结束地址
unsigned longvm_flags
该区域的标记。如:VM_IO和VM_RESERVED。VM_IO 将 该 VMA 标记 为 内 存 映 射 的 IO 区域 ,VM_IO会阻止系统将该区域包含在进程的存放转存(core dump )中,VM_RESERVED标志内存区域不能被换出。
mmap设备操作
映射一个设备是指把用户空间的一段地址关联到设备内存上。当程序读写这段用户空间的地址时,它实际上是在访问设备。
mmap设备方法需要完成什么功能?
mmap方法是file_oprations结构的成员,在mmap系统调用发出时被调用。在此之前,内核已经完成了很多工作。mmap设备方法所需要做的就是建立虚拟地址到物理地址的页表。
int (*mmap) (structfile *, struct vm_area_struct *)
mmap如何完成页表的建立?
方法有二:
1、使用remap_pfn_range一次建立所有页表;
intremap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned longpfn, unsigned long
size, pgprot_t prot)
vma:
虚拟内存区域指针
virt_addr:
虚拟地址的起始值
pfn:
要映射的物理地址所在的物理页帧号,可将物理地址
>>PAGE_SHIFT得到。
size:
要映射的区域的大小。
prot:
VMA的保护属性。
intmemdev_mmap(struct file*filp, struct vm_area_struct *vma)
{
Vma->vm_flags |=VM_IO;
Vma->vm_flags |=VM_RESERVED;
if(remap_pfn_range(vma, vma->vm_start,
virt_to_phys(dev->data)>> PAGE_SHIFT,
size,vma->vm_page_prot))
return -EAGAIN;
return 0;
}
2、使用nopage VMA方法每次建立一个页表。
6.2 硬件访问
寄存器与内存
寄存器与内存的区别在哪里呢?
寄存器和 RAM 的主要不同在于寄存器操作有副作用(side effect 或边际效果):读取某个地址时可能导致该地址内容发生
变化,比如很多设备的中断状态寄存器只要一读取,便自动清零。
内存与I/O
在X86处理器中存在I/O空间的概念,I/O空间是相对内存空间而言的,他们是彼此独立的地址空间,在32位的x86系统中,I/O空间大小为64K,内存空间大小为4G。
X86
支持内存空间、IO空间
ARM
只支持内存空间
MIPS
只支持内存空间
PowerPC
只支持内存空间
IO端口:
当一个寄存器或内存位于IO空间时,称其为IO端口。
IO内存:
当一个寄存器或内存位于内存空间时,称其为IO内存。
操作I/O端口
对I/O端口的操作需按如下步骤完成:
1. 申请
2. 访问
3. 释放
申请I/O端口
内核提供了一套函数来允许驱动申请它需要的I/O端口,其中核心的函数是:
struct resource*request_region(unsigned long first,
unsigned long n,const char *name)
这个函数告诉内核,你要使用从 first 开始的n个端口,name参数是设备的名字。如果申请成功,返回非 NULL,申请失败,返回 NULL。
系统中端口的分配情况记录在/proc/ioports 中。如果不能分配需要的端口,可以来这里查看谁在使用。
访问I/O端口
I/O端口可分为8-位, 16-位, 和 32-位端口。Linux内核头文件(体系依赖的头文件 <asm/io.h>) 定义了下列内联函数来访问 I/O 端口:
• unsignedinb(unsigned port)
读字节端口( 8 位宽 )
• void outb(unsignedchar byte, unsigned port)
写字节端口( 8 位宽 )。
unsignedinw(unsigned port)
void outw(unsignedshort word, unsigned port)
存取 16-位端口。
unsignedinl(unsigned port)
void outl(unsignedlongword, unsigned port)
存取 32-位端口。
释放I/O端口
当用完一组 I/O 端口(通常在驱动卸载时),应使用如下函数把它们返还给系统:
voidrelease_region(unsigned long start, unsigned long n)
操作I/O内存
对I/O内存的操作需按如下步骤完成:
1. 申请
2. 映射
3. 访问
4. 释放
申请I/O内存
内核提供了一套函数来允许驱动申请它需要的I/O内存,其中核心的函数是:
struct resource*request_mem_region(unsigned
long start, unsignedlong len, char *name)
这个函数申请一个从start 开始,长度为len 字节的内存区。如果成功,返回非NULL;否则返回NULL,所有已经在使用的I/O内存在
/proc/iomem 中列出。
映射I/O内存
在访问I/O内存之前, 必须进行物理地址到虚拟地址的映射,ioremap 函数具有此功能:
void*ioremap(unsigned long phys_addr, unsigned long size)
访问I/O内存
访问 I/O 内存的正确方法是通过一系列内核提供的函数:
从 I/O 内存读, 使用下列之一:
unsignedioread8(void *addr)
unsignedioread16(void *addr)
unsignedioread32(void *addr)
写I/O 内存, 使用下列之一:
void iowrite8(u8value, void *addr)
void iowrite16(u16value, void *addr)
void iowrite32(u32value, void *addr)
老版本的 I/O 内存访问函数:
从 I/O 内存读, 使用下列之一:
unsignedreadb(address)
unsignedreadw(address)
unsignedreadl(address)
写I/O 内存, 使用下列之一:
unsignedwriteb(unsigned value, address)
unsignedwritew(unsigned value, address)
unsignedwritel(unsigned value, address)
释放I/O内存
I/O内存不再需要使用时应当释放,步骤如下:
1. void iounmap(void* addr)
2. voidrelease_mem_region(unsigned long
start, unsigned longlen)
6.3 混杂设备驱动
定义
在Linux系统中,存在一类字符设备,它们共享一个主设备号(10),但次设备号不同,我们称这类设备为混杂设备(miscdevice)。所有的混杂设备形成一个链表,对设备访问时内核根据次设备号查
找到相应的miscdevice设备。
设备描述
Linux内核使用struct miscdevice来描述一个混杂设备。
struct miscdevice {
int minor; /* 次设备号*/
const char *name; /*设备名*/
const structfile_operations *fops; /*文件操作*/
struct list_headlist;
struct device*parent;
struct device*this_device;
};
设备注册
Linux内核使用misc_register函数来注册一个混杂设备驱动。
intmisc_register(struct miscdevice * misc)
6.4 LED驱动程序设计
上拉/下拉电阻
上拉是将不确定的信号通过一个电阻与电源相连,固定在高电平。下拉是将不确定的信号通过一个电阻与地相连,固定在低
电平。上拉是对器件注入电流,下拉是输出电流。当一个接有上拉电阻的I/O端口设为输入状态时,它的常态为高电平,可用于检测低电平的输入。
S3c2440 I/O端口
S3C2440包含GPA 、GPB 、......、GPJ 九组I/O端口。它们的寄存器是相似的:GPxCON用于设置端口功能(00 表示输入、01表示输出、10表示特殊功能、11 保留不用),GPxDAT 用于读/写数据,GPxUP 用于决定是否使用内部上拉电阻(某位为0 时,相应引脚无内部上拉;为1时,相应引脚使用内部上拉)。
引脚
LED与IO端口引脚的对应关系
1.LED1 GPB5
2.LED2 GPB6
3.LED3 GPB7
4.LED4 GPB8