|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
0 c1 [' K# a. a# p1 `" A9 z
首先讲述一下驱动程序的概念。, r4 e \( g8 }1 A n+ }+ U/ W
, O* D+ m/ l/ X* A* a. ^8 S驱动程序实际上就是硬件与应用程序之间的中间层。驱动程序工作在内核空间,应用程序一般运行于用户态。在内核态下,CPU可执行任何指令,在用户态下CPU只能执行非特权指令。当CPU处于内核态,可以随意进入用户态,而当CPU处于用户态,只能通过特殊的方式进入内核态,比如linux操作系统中的系统调用。系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。' N: J( C- ]2 Y0 S
. r( x* i' Q6 _. V- I
Linux支持三类硬件设备:字符设备、块设备及网络设备。首先学习一下字符设备驱动的编写。; W+ l4 K c- [& E& c: W* \2 V- x
. S. V. A6 z6 Y4 e0 ]8 M3 \一、字符设备注册* t0 S `8 p8 Z. v }) y4 K
在linux 2.6内核中采用的是以下函数进行字符设备注册,而对于新版本的字符设备注册改成新的注册方式。
% K" [& D9 H# i' U. g' D
: B7 e; Z$ o1 _+ q! C- p4 `6 a T0 {) w2 R
int register_chrdev(unsigned int major, const char *name, struct file_operations , ?- \2 g0 M, x# L) Y6 I1 ]
*fops);
M0 \: {* G6 Q! ^新的注册方式:
7 h% ]+ ^) s/ g: R% p$ N$ l
* ?: |7 C M% c# [0 K8 B(1)在linux内核中使用结构体cdev结构来描述字符设备,在驱动程序中必须将已分配到的设备号以及设备操作接口赋予struct cdev结构变量。首先使用cdev_alloc()函数向系统
# }+ S1 h) ?; d5 L- q9 p% C) V
+ B6 ~5 M) \& z# `' ~申请分配struct cdev结构,函数原型为:* g8 R; M9 @$ R* @" a: i
! q3 v: Z! ]; U v0 u7 W
struct cdev *cdev_alloc(void);
7 V- t ? a) B" k$ I+ b(2)初始化struct cdev,并与file_operations结构关联起来,即赋值cdev.owner=THIS_MODULE,函数原型为:/ v, E0 q% h/ M: F
' i5 V: F, l1 G& Y" o: R' t* \void cdev_init(struct cdev *cdev,struct file_operations *fops);7 K& K( I# `# _3 y& f0 @" W6 n
(3)调用cdev_add()函数将设备号与struct cdev结构进行关联并向内核正式报告新设备的注册。
. S8 _8 g% S8 I, C0 L, W- b: t# e+ S" C' b
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);
, N! [) S9 s) H; n7 K4 ?//成功返回0,出错返回-1. J" m1 C$ F, w- f. ]* {
//cdev:需要初始化/注册/删除的struct cdev结构+ Y1 B. r! J' B/ j) ?7 ?
//fops:该字符设备的file_opertions结构
/ V! M! ?$ Q4 N& I- D//num:系统给该设备分配的第一个设备号
& i( L1 K. i+ M//count:该设备对应的设备号数量
! e. Q) H: Z1 R0 ~1 Y$ H, y" C(4)删除一个设备则要调用cdev_del()函数。0 P% j C; E$ D# I+ p$ s
* \ N) y% F+ w* z4 u; V! y' P# bvoid cdev_del(struct cdev *dev);
9 s& s+ h. y% ^8 X T1 Y二、设备的打开和释放
/ [. b9 h/ ?) n. ?! C打开设备的函数接口是open,函数原型为:
& K3 r5 T7 p5 F! m1 ystatic int device_open(struct inode *inode, struct file *file)
, A& ?* |, q/ `& G( D
& y: v1 a7 {" a) V主要完成如下工作:
4 i/ T* j7 X- w. t$ u( i m& B- \: v, @& W# I% } o* i2 a5 L
(1)递增计数器,检查错误;
9 Q" A' E' O4 I8 k9 d% j9 C! t# _! A
(2)如果未初始化,则进行初始化;4 v2 c/ H) n0 S1 ~8 P% Y( E
% h0 ^* b/ R; q% M# e" N7 B; Q. v9 ~: k(3)识别次设备号,如果必要,更新f_op指针;
% I% H6 Y( J D2 s
3 y+ @& q' N8 h% z3 O) H0 `6 V6 o+ J(4)分配并填写被置于filp->private_data的数据结构, Y/ U8 e" J7 G. E. h: r. f
# U1 t% n/ f0 f$ }9 Z' p( c, ^7 E
其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。# l) m1 R' S' g4 l) A8 ^
% {! u; Y9 E5 n) T
释放设备设备的函数接口是release,函数原型为:
% z8 J- v, t: s# f, Bstatic int device_release(struct inode *inode, struct file *file)
* _+ x+ R. `6 L2 l+ }& \; y
; \: X2 D" ?& D2 Y, H+ i主要完成工作:
/ B" i* B$ a; }( a4 ^% U/ ]! ]7 v' _/ T q$ x a
(1)递减计数器;
- V$ ?+ J* W' K1 G( B3 k0 u1 D! Y$ _5 ^. \ \
(2)释放打开设备时系统所分配的内存空间;6 }. D+ k# r/ C4 x( w; a( m: k) S
x4 g1 Y' K5 U8 L5 T& j2 p2 f(3)在最后一次释放设备操作时关闭设备
/ l$ S. k& l! J* \+ U2 }
% ^( b* J+ s% O' O4 A三、读写设备1 v4 Z3 X7 f# n1 ~" N0 j, p
读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,函数原型为:
5 q+ E2 ?1 n9 Y; b+ g k- @. T5 g7 h. j. f0 u
ssize_t (*read)(struct file *filp, char *buff, size_t count, loff_t *offp);
' M0 H% x! A4 m5 M2 P
! n+ k8 k: { c0 z; I/ ossize_t (*write)(struct file *filp,const char *buff, size_t count, loff_t *offp);, `3 x! e3 N" S) V7 S, q
$ o$ }, V; |- A$ i6 \( W5 e这里重点说明一下buff指的是用户空间指针,不能被内核代码直接引用。有如下理由:9 T) A: m. |% e: }' o
0 R4 U; N% J8 o% O(1)依赖于你的驱动运行的体系,用户空间指针当运行于内核模式可能根本是无效的,可能没有那个地址的映射,或者它可能指向一些其他的随机数据。
4 t5 h) q L3 a9 W" V+ w+ D2 T A" O& T/ j# s, w
(2)就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时,这个内存可能没有在RAM中。试图直接引用用户空间内存可能产生一个页面错,这是内核代码不允许做的事情,导致进行系统调用的进程死亡。
! w# @8 z5 @& m a% O+ V# B* z) B5 @5 n( A6 K
其中的读设备的意思就是把数据从内核空间复制到用户空间,而写设备是把数据从用户空间复制到内核空间,这里用到的函数原型为(在asm/uaccess.h)中定义:8 X" ]0 s8 s/ ^" q# P$ Z. k! w9 q5 Y
, S0 v6 d% p4 Q8 H' Nunsigned long copy_to_user(void *to, const void *from, unsigned long count);
9 ?: ]/ E8 V& X+ O5 _& Y$ U& x C' ]7 S- @" G1 `2 _
unsigned long copy_from_user(void *to, const void *from, unsigned long count);
) m8 y2 @5 u$ e5 n% i3 i5 A8 f
+ `( T& E. |0 d4 x8 e, K8 A0 T4 Rint put_user(dataum,ptr);8 P6 w. q* [3 T8 p' j* [
2 C6 }1 Z( |1 h7 h: I& N* r
int get_user(local,ptr);
1 O9 ~& B: O* Z) K4 D
0 x, t6 T0 Y$ l* f; c//内核空间和用户空间的单值交互(如char、int、long)8 j: K- P$ g5 y
四、IO控制函数( T" v- q5 @0 C" ]$ Q
IO控制函数包括对设备的所有操作,包括设置设备、读写设备等等,这样最大的好处就是给了应用程序一个统一的接口,让应用程序编写非常简单而且易懂。函数原型为:4 k7 m) U) Z/ A" L' Q, v4 D% ^1 b
: j- @; l" j0 g8 G" B; T% ustatic int device_ioctl(struct inode *inode, /* see include/linux/fs.h */5 e3 X0 o+ g# e; ^% x6 t" v' g
struct file *file, /* ditto */: p i/ c0 }5 R- y7 _6 }$ x6 c
unsigned int ioctl_num, /* number and param for ioctl */& O; {* v+ T0 X* _1 I5 I5 L) E. Y
unsigned long ioctl_param)
1 L7 c6 M* j5 r: H8 D1 B7 ^
& N( U; t, } R) C9 i5 v; P4 nioctl_num表示IO控制的类型,可以为IOCTL_SET_MSG、IOCTL_SET_DISP_WAIT、IOCTL_GET_MSG、IOCTL_GET_NTH_BYTE等等,这里仅仅举个例子。
0 z6 g% |# W, z( C( X, Q' V% l- t3 }% [ h
ioctl_param参数表示传入的参数。
" m2 Y- s$ ~6 n R" t0 N {/ n( ^# p$ ^/ J8 l
这里有一个比较重要的概念就是ioctl命令号。
8 B/ i0 b0 D7 h, ^* G
' S# T5 @1 q+ O5 T/ f2 e( aioctl命令号
3 v6 L) l- ^+ lioctl命令号是这个函数中最重要的参数,它描述的ioctl要处理的命令。linux中使用一个32位的数据来编码ioctl命令,它包含四个部分:dir,type,nr,size! k" O/ P. D; G% ^4 Y# x
& j' R; I+ W) S+ Y! \. N. ~
dir:( M# O" P; z2 T& m9 B
代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,OU),_IOC_WRITE(向设备写数据)或_IOC_READ(从设备读数据)或者他们的逻辑组合,当然这里只有_IOC_WRITE和_IOC_READ的组合才有意义。
( ]% R4 a2 N1 {- o8 v# g* s# x+ I7 a
type:; P+ ]" ?9 F: G2 ]% }9 x. \
描述ioctl命令的类型,8bit。每种设备或系统都可以指定自己的一个类型号,ioctl用这个类型来表示ioctl命令所属的设备或驱动。/ j8 u8 b$ L) {$ P7 [/ ?# p
+ U5 u1 c& X2 e& i) |nr& c0 ]2 y; N0 V( b4 F( L0 c+ h% _
ioctl命令序号,一般8bit,对于一个指定的设备驱动,可以对他的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。
: i, A& P2 ^) v5 o% r" B8 F; |( a+ Y( p: a* H; @
size) p9 w- E5 o5 y, ?3 C
ioctl命令的参数大小,一般为14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是建议你指定这个数据成员,通过它可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。$ e/ G9 w* w# E2 a! a6 B: x
/ S# H( f8 P9 k
对于自己写的驱动,如果需要使用特定的ioctl命令,则必须创建ioctl命令以及编号。内核中给出了创建ioctl命令的宏。$ W, V: v# @; D! g
! e ^$ r) h7 ~6 [9 C5 h; p/* used to create numbers */" r: j; D' _* b
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0). Y0 h2 E) v+ ~
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
% M: m9 l. m. n" A" H5 A$ t2 \#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
; F" G( Q6 p* P$ m6 q#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))/ \: T7 P3 d2 ~7 b
#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
6 x+ d- r/ W F' r+ b m& [8 @; C#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))1 _1 y. ]8 P* J7 z9 j* n- r
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
4 K; b6 q* S( q4 e/ Y( m) T* k: q# @ s8 H1 ^+ f
总结:上述是基本的字符驱动编写基本知识,针对具体的字符驱动,涉及到具体硬件的一些设置,里面的读写函数以及设置函数都有很多区别,而且大部分都要用到延迟,因为所有的串行硬件设备都是工作在KHZ的频率上,而CPU已经工作在GHZ的水平,所以,这里需要进行软延迟。另外可能还有就是中断方式。 |
|