|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
% R3 ^1 j }/ ?* M# ]首先讲述一下驱动程序的概念。
$ W4 b3 `6 Z% X2 F J- G
" b- P" s- A: _. P0 l2 ?/ F7 R驱动程序实际上就是硬件与应用程序之间的中间层。驱动程序工作在内核空间,应用程序一般运行于用户态。在内核态下,CPU可执行任何指令,在用户态下CPU只能执行非特权指令。当CPU处于内核态,可以随意进入用户态,而当CPU处于用户态,只能通过特殊的方式进入内核态,比如linux操作系统中的系统调用。系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。; ^1 e. x# e, @3 j5 m
' G8 l3 f+ W+ `( {& {8 ~% @) c' z
Linux支持三类硬件设备:字符设备、块设备及网络设备。首先学习一下字符设备驱动的编写。" F# C: o- {' p
' n/ }# T$ s* ~7 l一、字符设备注册5 b2 s8 s0 U- c/ O
在linux 2.6内核中采用的是以下函数进行字符设备注册,而对于新版本的字符设备注册改成新的注册方式。
- A7 A! {$ |" Z% B% ?
2 O( }) ]9 L( W" }
+ d9 N4 u$ R# d' Z& [ fint register_chrdev(unsigned int major, const char *name, struct file_operations ; [) ]/ @9 Z& M. U! K0 v# b3 c$ `
*fops);
1 v+ ? _2 R8 ~' ]9 E# i2 Y$ U1 Y! N6 z新的注册方式:
- D) u* @1 m- U! v3 y/ s" v3 b2 h0 i* t
(1)在linux内核中使用结构体cdev结构来描述字符设备,在驱动程序中必须将已分配到的设备号以及设备操作接口赋予struct cdev结构变量。首先使用cdev_alloc()函数向系统9 x; Y# ^# s9 i/ G& _& _ {$ T
* G! k4 [' Z; e# O! k5 h; V
申请分配struct cdev结构,函数原型为:
) [* z. {# h* X1 T0 F' x9 l- f. J3 }* [4 d8 o
struct cdev *cdev_alloc(void);
' _/ {* Y3 q) J/ j. J: p! F(2)初始化struct cdev,并与file_operations结构关联起来,即赋值cdev.owner=THIS_MODULE,函数原型为:1 a8 J3 S2 w& ~. x
8 g% e: b5 F+ J' _* Q
void cdev_init(struct cdev *cdev,struct file_operations *fops);9 R5 @( |6 y" [! n
(3)调用cdev_add()函数将设备号与struct cdev结构进行关联并向内核正式报告新设备的注册。
! T0 B. @3 T6 k, C. Q; ?9 M7 M3 q, Z" C+ e$ H; m( a7 y4 k7 m
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);9 F4 v: J$ k/ i( F8 A3 H
//成功返回0,出错返回-1
: ?. q) w$ x" P//cdev:需要初始化/注册/删除的struct cdev结构2 y' j6 k( U" c2 k# O
//fops:该字符设备的file_opertions结构2 A n6 ^4 v1 j. @& f( F
//num:系统给该设备分配的第一个设备号) T( E+ K- C. z/ _. U9 }
//count:该设备对应的设备号数量7 A9 T. L3 x' @( j: t0 O
(4)删除一个设备则要调用cdev_del()函数。
$ J, }8 ]% b2 Z; h2 Q" n
1 W6 _8 j8 E/ u# ^- U" R( q' e2 rvoid cdev_del(struct cdev *dev);/ `& }. X: _+ D- V
二、设备的打开和释放) z3 A* p4 G2 E4 T2 p
打开设备的函数接口是open,函数原型为:* V0 |4 u* v& R5 N( e
static int device_open(struct inode *inode, struct file *file)+ {# R4 ^/ s& d" Z/ r- k$ O& A
8 k/ A# t* V6 ?) u, p主要完成如下工作:
- R0 Y g! }4 h. C. S% Y# l* K6 s$ p3 F" z$ n0 V4 M
(1)递增计数器,检查错误;
* N% q* W: i2 x* b' s4 N' g& r2 e/ l y( n( ?
(2)如果未初始化,则进行初始化;, \0 m1 ` W4 R* n+ j
/ m" Z. P2 W9 }! Y" i5 x(3)识别次设备号,如果必要,更新f_op指针; [3 {' H6 K% N/ I
- M) j |8 s9 ]6 q- u
(4)分配并填写被置于filp->private_data的数据结构
5 H4 e4 i$ g9 p2 E7 x e j3 \' R* o$ g
其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。; {$ F1 O6 ]4 `, x
% ^/ W3 f3 I. C释放设备设备的函数接口是release,函数原型为:! @4 k5 }; I) I
static int device_release(struct inode *inode, struct file *file)
& H7 O" j' |+ D1 Y h, q
W, ~0 @: H: I6 g F主要完成工作:% r9 H. Y$ V, K$ A
5 s) p( o7 e9 r8 S5 i# R1 o8 E( v(1)递减计数器;9 N. T2 V+ U/ y' }9 o1 i
2 T- M1 K7 ]- o @8 l+ g7 m8 Q
(2)释放打开设备时系统所分配的内存空间;" D- J% A: r% O, x
X, P4 V6 K4 d(3)在最后一次释放设备操作时关闭设备3 w. `2 K& [0 N# r0 N6 L8 T& E
7 G# ]# ]; R2 L* Z' X5 B三、读写设备
2 `. A& K1 W+ B: K) q读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,函数原型为:" w/ s) ?6 A e3 i
+ p& z& p& G0 l+ e/ q
ssize_t (*read)(struct file *filp, char *buff, size_t count, loff_t *offp);
9 \3 @' ^2 b0 P7 n) K0 s) F/ X3 H ^- E! ~; w: e2 c! E2 T& C
ssize_t (*write)(struct file *filp,const char *buff, size_t count, loff_t *offp);# \- D3 S4 H, I! w) o
$ E6 f8 r, G9 x: f d( j U
这里重点说明一下buff指的是用户空间指针,不能被内核代码直接引用。有如下理由:
5 V5 F* k C2 m3 S' W3 }1 F
4 r9 b; B7 W( c3 x2 N: O(1)依赖于你的驱动运行的体系,用户空间指针当运行于内核模式可能根本是无效的,可能没有那个地址的映射,或者它可能指向一些其他的随机数据。
- [1 ~* [. L; e' l: d9 y( u, a; l# [4 l
(2)就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时,这个内存可能没有在RAM中。试图直接引用用户空间内存可能产生一个页面错,这是内核代码不允许做的事情,导致进行系统调用的进程死亡。
+ q! c, L% J H' ?9 S8 V7 _) K6 _3 q" x$ E# `$ h- L8 L' a
其中的读设备的意思就是把数据从内核空间复制到用户空间,而写设备是把数据从用户空间复制到内核空间,这里用到的函数原型为(在asm/uaccess.h)中定义:
# Q/ |6 i- {3 y3 c* A
! t# D3 Q) p! ~& S$ Junsigned long copy_to_user(void *to, const void *from, unsigned long count);
: L0 t' N. o1 l/ c& R& ^% @; I2 }7 V+ c1 Y2 o9 k
unsigned long copy_from_user(void *to, const void *from, unsigned long count);
) r; Q3 Q6 N6 b. V5 U v1 P$ O( W$ }! u
int put_user(dataum,ptr);
2 K Z6 \4 n9 p, V# ~. R: ^2 Y2 ^+ d' ]( r9 h7 g, Z% Q
int get_user(local,ptr);
4 Q# }9 ~( q3 C: [9 y) n; `3 a0 r+ b0 L- b
//内核空间和用户空间的单值交互(如char、int、long)
# V6 n9 P/ b* q* ^0 |7 c四、IO控制函数
" q# r/ j" M% d1 m# r$ rIO控制函数包括对设备的所有操作,包括设置设备、读写设备等等,这样最大的好处就是给了应用程序一个统一的接口,让应用程序编写非常简单而且易懂。函数原型为:8 c& f0 S; O) a3 n& S0 w) o' C
4 T" i4 S/ u7 T5 f
static int device_ioctl(struct inode *inode, /* see include/linux/fs.h */
Y9 T) j! `/ p struct file *file, /* ditto */6 B1 j7 H' i4 ?' t8 D- K# {/ F
unsigned int ioctl_num, /* number and param for ioctl */
4 a* I Y" |. q1 M1 n+ p% w unsigned long ioctl_param)
* |3 w5 ^1 g' i: J7 z* D. {
' ~6 ` P& z- D7 N# _, sioctl_num表示IO控制的类型,可以为IOCTL_SET_MSG、IOCTL_SET_DISP_WAIT、IOCTL_GET_MSG、IOCTL_GET_NTH_BYTE等等,这里仅仅举个例子。
0 i7 @ ^, r' d/ [4 X
h6 n2 J0 f0 Z" kioctl_param参数表示传入的参数。
4 y+ J" H5 H, F4 s! k4 J/ ?9 C, b7 _$ n1 f
这里有一个比较重要的概念就是ioctl命令号。& G) @9 J0 Q6 T+ |0 m3 E2 h
' K* `: @" n0 [/ N: Q0 `
ioctl命令号' s, P# L! e6 j8 Q, H
ioctl命令号是这个函数中最重要的参数,它描述的ioctl要处理的命令。linux中使用一个32位的数据来编码ioctl命令,它包含四个部分:dir,type,nr,size
2 g9 B4 w) N: m: L. C6 {
0 s$ J3 ^4 @- j5 c6 Vdir:
4 ?7 |0 t+ ^6 w8 u x1 l代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,OU),_IOC_WRITE(向设备写数据)或_IOC_READ(从设备读数据)或者他们的逻辑组合,当然这里只有_IOC_WRITE和_IOC_READ的组合才有意义。
7 q0 S4 O1 X0 @+ D+ K6 @; [/ q# R) V' q R: T' D
type:1 b$ t+ n- T6 L* Q# u
描述ioctl命令的类型,8bit。每种设备或系统都可以指定自己的一个类型号,ioctl用这个类型来表示ioctl命令所属的设备或驱动。- l5 T0 z E5 p
. r1 o1 l( ]' Z: M
nr
4 o2 t x% O \) q. Cioctl命令序号,一般8bit,对于一个指定的设备驱动,可以对他的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。
( G0 e: t1 f& a5 |* z3 E$ l+ J: ^. k8 l6 {
size
. z5 }/ Y, `4 K; yioctl命令的参数大小,一般为14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是建议你指定这个数据成员,通过它可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。; u# V' {0 {% @& c% r K/ E
! C" W2 y! ]1 U I7 U对于自己写的驱动,如果需要使用特定的ioctl命令,则必须创建ioctl命令以及编号。内核中给出了创建ioctl命令的宏。9 N8 c/ Y) X1 z" I' c5 @( d4 Q" T
9 N9 i% |6 E0 Y9 h7 N" @- |
/* used to create numbers */
) U/ |1 i- v( }0 y9 c2 q#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
! B$ ]* l6 ], N: }#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))# `5 u5 j: U. ?$ n u' [
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
& R3 l5 i& q( S/ L$ ?#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
) A9 R2 ~1 _# B/ N/ M#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
' g6 v! T/ z. ^+ ], o#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))4 V: z/ f6 m) c3 o
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
7 _6 _, u' k# ~ n, i
3 q% P* {5 j1 N总结:上述是基本的字符驱动编写基本知识,针对具体的字符驱动,涉及到具体硬件的一些设置,里面的读写函数以及设置函数都有很多区别,而且大部分都要用到延迟,因为所有的串行硬件设备都是工作在KHZ的频率上,而CPU已经工作在GHZ的水平,所以,这里需要进行软延迟。另外可能还有就是中断方式。 |
|