|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
/ P8 e) C# p$ s1 n首先讲述一下驱动程序的概念。& y" l x' G! N' A( n
2 E1 z! K8 r+ o" ?
驱动程序实际上就是硬件与应用程序之间的中间层。驱动程序工作在内核空间,应用程序一般运行于用户态。在内核态下,CPU可执行任何指令,在用户态下CPU只能执行非特权指令。当CPU处于内核态,可以随意进入用户态,而当CPU处于用户态,只能通过特殊的方式进入内核态,比如linux操作系统中的系统调用。系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。6 X4 f% h3 V: K( r# ?( u# ]
! U1 j* A% C2 ZLinux支持三类硬件设备:字符设备、块设备及网络设备。首先学习一下字符设备驱动的编写。
( k. \8 l6 K u9 O$ t/ R7 `, d. u& S$ s/ r X p% K; G% Q; M& j9 G
一、字符设备注册
6 g+ F0 ?9 }. l8 j9 e3 m在linux 2.6内核中采用的是以下函数进行字符设备注册,而对于新版本的字符设备注册改成新的注册方式。
0 P: h; U; L( A- T5 z4 ~, m* @* }$ i
3 q5 \; B8 x, c8 O1 N6 h+ ?8 _int register_chrdev(unsigned int major, const char *name, struct file_operations : F6 k0 p ]/ B2 W9 y
*fops);
6 X: x- o2 p3 P1 f, }新的注册方式:
6 P3 G, e- T+ x
& X% Y- l7 ]2 y8 ^(1)在linux内核中使用结构体cdev结构来描述字符设备,在驱动程序中必须将已分配到的设备号以及设备操作接口赋予struct cdev结构变量。首先使用cdev_alloc()函数向系统
; S9 l. @- L$ n- E: P6 f( `/ H/ s- h* ]. q" z
申请分配struct cdev结构,函数原型为:
7 I+ J/ S9 h* z* C: [
5 j' _3 B( j9 t8 L4 d' Kstruct cdev *cdev_alloc(void);
. T/ S- I$ s* C6 v7 S(2)初始化struct cdev,并与file_operations结构关联起来,即赋值cdev.owner=THIS_MODULE,函数原型为:
6 S* g! e5 m6 g( X# a
- a' `/ Q8 B* X) }6 Ovoid cdev_init(struct cdev *cdev,struct file_operations *fops);2 A/ s% S* d6 v u
(3)调用cdev_add()函数将设备号与struct cdev结构进行关联并向内核正式报告新设备的注册。
: C$ |/ Q# _8 j; L" Q1 z* l+ y& S1 }$ g: A6 H# K- _) i
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);1 q2 k' \+ @. [& U/ ?/ `. J F
//成功返回0,出错返回-1
5 e$ ^" Y; w/ L, @' L//cdev:需要初始化/注册/删除的struct cdev结构- U6 ~% U8 V; W+ \. ~
//fops:该字符设备的file_opertions结构3 y) i0 Z: D8 d/ h7 A, R
//num:系统给该设备分配的第一个设备号
1 K2 s0 A) N" B5 z//count:该设备对应的设备号数量, x/ D+ y# E/ c6 r' h+ A
(4)删除一个设备则要调用cdev_del()函数。) z2 _" O4 K/ K! N3 L6 l
, W# Z) |7 B7 x1 C% f6 m" Z2 Z$ x
void cdev_del(struct cdev *dev);9 D7 k) h( b! I% z @' |& j6 S
二、设备的打开和释放
; H8 I% V7 c% `/ a* T7 |3 T打开设备的函数接口是open,函数原型为:
7 e0 z+ y# w, Y; y- O) _static int device_open(struct inode *inode, struct file *file)
# i! n! M. H; j: h- }- B- c) b7 T% [4 y! i* N% B6 l
主要完成如下工作:
' M7 Q* C& ~3 w) c( _
4 h. W6 A% y9 R, o4 i' n0 ~(1)递增计数器,检查错误; `1 Z+ r' G- {2 S" d+ ?
- d6 T. [% I: Y9 t(2)如果未初始化,则进行初始化;' W+ h& I3 C# g+ X. I
0 S$ L$ V: t* w4 M- N
(3)识别次设备号,如果必要,更新f_op指针;, p/ E* v" ` Y
/ R. v5 Z7 }/ f) x; B# F
(4)分配并填写被置于filp->private_data的数据结构
5 A, W7 s/ [, }9 t R2 V& @7 [
" a. ~* ~* k; m4 F$ `其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。 T5 Z8 b; S- B
. z, [- K6 ]/ U k% R
释放设备设备的函数接口是release,函数原型为:" w% Q0 G% j. s3 P" h" J' |3 ~
static int device_release(struct inode *inode, struct file *file)6 P3 B7 x" z l% ?, J+ M7 a
3 H1 A9 C% s- y主要完成工作:' X7 P/ C5 t& I) E5 X# {
' o* v6 \: t$ M! v4 Y( O(1)递减计数器;
* V3 m9 ^# Z* r
& `) H q E( {4 y: l+ O3 _(2)释放打开设备时系统所分配的内存空间;
% ?) j7 f8 ^) h' g: n( h# e; ~4 g, {6 P" p% [& w5 f0 p
(3)在最后一次释放设备操作时关闭设备
0 i/ Y7 A! T0 [( x- L! y; ^% T: ^! ^; U3 M! q
三、读写设备
) U6 v- a- l% W+ E读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,函数原型为:. f" T2 X$ v; k/ y* ^$ g* A5 I
2 A, H8 \5 P) k- cssize_t (*read)(struct file *filp, char *buff, size_t count, loff_t *offp);, e; r8 P' h7 a- o
2 @: |2 H$ Y$ Q) c/ ~
ssize_t (*write)(struct file *filp,const char *buff, size_t count, loff_t *offp);/ P+ J# w6 h P6 [$ k$ j$ k
) G. v6 q1 o6 s. f' h0 U& `: Y5 [8 h这里重点说明一下buff指的是用户空间指针,不能被内核代码直接引用。有如下理由:; j# h! N4 `" G2 w4 F
( d9 M! K; r3 K+ H$ a. V
(1)依赖于你的驱动运行的体系,用户空间指针当运行于内核模式可能根本是无效的,可能没有那个地址的映射,或者它可能指向一些其他的随机数据。
- W8 z6 h( v2 L! n/ y/ \
q- q! g2 \8 u n5 U(2)就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时,这个内存可能没有在RAM中。试图直接引用用户空间内存可能产生一个页面错,这是内核代码不允许做的事情,导致进行系统调用的进程死亡。" `" j. R# f+ V& U0 K9 f
2 W, u$ w% k, s3 E% a5 m- @其中的读设备的意思就是把数据从内核空间复制到用户空间,而写设备是把数据从用户空间复制到内核空间,这里用到的函数原型为(在asm/uaccess.h)中定义:
4 j/ \) s3 c6 W% |: u
7 u6 c9 f! |0 c- x, aunsigned long copy_to_user(void *to, const void *from, unsigned long count);! y) q' q _6 } t% @
$ P$ e% i8 i' k
unsigned long copy_from_user(void *to, const void *from, unsigned long count);6 S& u8 L8 l& o9 m
! ?- }" @; L8 I3 Pint put_user(dataum,ptr);$ A: L& S* f4 Y9 W& `: |( x X
, n' S. M/ ?: n1 X5 lint get_user(local,ptr);. t' m7 k+ e3 V: B; |! K7 B$ a; A
: V+ e9 H8 a E8 S5 E0 o
//内核空间和用户空间的单值交互(如char、int、long)" s. j. T) |$ L
四、IO控制函数3 { d: e; a: x
IO控制函数包括对设备的所有操作,包括设置设备、读写设备等等,这样最大的好处就是给了应用程序一个统一的接口,让应用程序编写非常简单而且易懂。函数原型为:" D3 a! \4 K6 h9 ?8 Z8 m9 J
2 _ `$ p3 L6 M1 y7 v% |/ Z
static int device_ioctl(struct inode *inode, /* see include/linux/fs.h */3 K+ |: ~/ r3 B# d
struct file *file, /* ditto */* j% L& u- O! o# E7 e) `
unsigned int ioctl_num, /* number and param for ioctl */5 F. ~8 Q7 B: [6 i
unsigned long ioctl_param)" n8 _( s" ]9 P/ m r
+ H& s# ~; S2 j. \6 R6 ~
ioctl_num表示IO控制的类型,可以为IOCTL_SET_MSG、IOCTL_SET_DISP_WAIT、IOCTL_GET_MSG、IOCTL_GET_NTH_BYTE等等,这里仅仅举个例子。- o* b" c7 T6 Q7 D: ]& v, A2 a
7 a* J2 ^8 S9 l: u4 r; \
ioctl_param参数表示传入的参数。+ O7 { v- D3 S
3 b. T5 i) B8 t5 p2 w+ L% o0 l
这里有一个比较重要的概念就是ioctl命令号。: Q+ {" g/ L( c6 \
' C, L) ?6 X6 O
ioctl命令号9 v' I9 H2 x' Z* P- L8 l2 r
ioctl命令号是这个函数中最重要的参数,它描述的ioctl要处理的命令。linux中使用一个32位的数据来编码ioctl命令,它包含四个部分:dir,type,nr,size% y% c/ w/ g( k5 P* h
: F% A( H" }; c" H; `) x+ w2 J6 pdir:( P. t: L! J' r" v1 E
代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,OU),_IOC_WRITE(向设备写数据)或_IOC_READ(从设备读数据)或者他们的逻辑组合,当然这里只有_IOC_WRITE和_IOC_READ的组合才有意义。; w% T0 t2 q4 Z9 _# k) m; k
9 D( C# |* i( A( A5 Etype:
" U$ f8 l" @4 [- B3 V描述ioctl命令的类型,8bit。每种设备或系统都可以指定自己的一个类型号,ioctl用这个类型来表示ioctl命令所属的设备或驱动。9 C( V; D9 d4 P1 i- g
7 w" `& z9 \. u3 a! rnr
. K/ C5 O8 T7 Kioctl命令序号,一般8bit,对于一个指定的设备驱动,可以对他的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。+ K e8 p0 i0 v7 k' X; R
! F" L4 [5 y t- m
size1 `4 R# e* ?9 j$ i+ s# K5 C5 @: J
ioctl命令的参数大小,一般为14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是建议你指定这个数据成员,通过它可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。 ^4 r8 r6 S) ^
v. B& p* J' ?- t对于自己写的驱动,如果需要使用特定的ioctl命令,则必须创建ioctl命令以及编号。内核中给出了创建ioctl命令的宏。
, t* t* u# d% C7 \6 ^+ f; b2 ]
8 c5 h" b+ H- {5 t2 [7 N1 n9 L* }/* used to create numbers */
5 `2 P9 ^4 u* e0 f; [0 Y#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)# L: `, ]% [, O6 a& [/ H( C
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
c7 J! A5 `4 ~4 r, @+ Q' B#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))" O) ~% q; V, d4 B3 w$ y
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))) ]3 V+ W; c2 [& J& x
#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
8 r+ W, `; J- G* ~" l. `#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))8 Z" U$ l" e# \- q
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))& ~3 H/ U9 `3 ?7 `! d8 j- Z, I6 I
& C* h9 l! ?! t7 M8 m
总结:上述是基本的字符驱动编写基本知识,针对具体的字符驱动,涉及到具体硬件的一些设置,里面的读写函数以及设置函数都有很多区别,而且大部分都要用到延迟,因为所有的串行硬件设备都是工作在KHZ的频率上,而CPU已经工作在GHZ的水平,所以,这里需要进行软延迟。另外可能还有就是中断方式。 |
|