|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
0 s" r/ _/ V) `8 n首先讲述一下驱动程序的概念。
% R& y$ m0 c- H' _( ~9 |4 J8 J7 O
$ D; J! i& {, M驱动程序实际上就是硬件与应用程序之间的中间层。驱动程序工作在内核空间,应用程序一般运行于用户态。在内核态下,CPU可执行任何指令,在用户态下CPU只能执行非特权指令。当CPU处于内核态,可以随意进入用户态,而当CPU处于用户态,只能通过特殊的方式进入内核态,比如linux操作系统中的系统调用。系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。0 ]# i# `& m5 Y0 [1 B
2 s* ?0 ^2 z. x7 `Linux支持三类硬件设备:字符设备、块设备及网络设备。首先学习一下字符设备驱动的编写。
* o! Z1 r$ Q; K/ T; O2 h
$ Q6 N) S* f0 Q一、字符设备注册
& F* |4 l% S2 g2 p" b( U在linux 2.6内核中采用的是以下函数进行字符设备注册,而对于新版本的字符设备注册改成新的注册方式。
# a" b# y3 P( R' q7 v, n$ ]7 O
2 s5 J' x3 y0 l2 X- m' a( Z0 J- m) V
! f) G ]) Q. H8 ~4 mint register_chrdev(unsigned int major, const char *name, struct file_operations / T0 k' O0 F- i6 \
*fops);
: X. s8 }, D: D/ ~+ C新的注册方式:* j! W" |( g+ h' v* Z( T
# _; M* q, n+ T# r* m(1)在linux内核中使用结构体cdev结构来描述字符设备,在驱动程序中必须将已分配到的设备号以及设备操作接口赋予struct cdev结构变量。首先使用cdev_alloc()函数向系统
3 `4 [: a/ u$ H* R8 w% F4 N1 l# S+ {' B% G
申请分配struct cdev结构,函数原型为:
' Q G+ p1 B6 c* t5 q8 z: F/ Z' L0 T+ A* M
struct cdev *cdev_alloc(void);( d/ k( M; _( y4 i
(2)初始化struct cdev,并与file_operations结构关联起来,即赋值cdev.owner=THIS_MODULE,函数原型为:
& {( K2 x$ K0 p0 J0 w6 X0 B4 w9 T* T" i
' `8 G9 T' D9 H& U6 n& l9 yvoid cdev_init(struct cdev *cdev,struct file_operations *fops);' I& v5 T) n( _+ k1 n3 p
(3)调用cdev_add()函数将设备号与struct cdev结构进行关联并向内核正式报告新设备的注册。
! d8 T% N, h0 D) g# l8 \4 x: G
4 h+ O0 h* h" z$ T, _2 T7 P6 Rint cdev_add(struct cdev *cdev, dev_t num, unsigned int count);
7 F1 G6 K g0 Y" ?) q; j//成功返回0,出错返回-1
; j' Q! d9 ^/ E6 R& S7 X//cdev:需要初始化/注册/删除的struct cdev结构
& ?0 \5 y7 U4 R# E) I//fops:该字符设备的file_opertions结构5 [7 J) n- ]+ w0 ]1 z4 s4 D
//num:系统给该设备分配的第一个设备号8 B2 S* }- B, K7 g7 B! g0 c
//count:该设备对应的设备号数量
8 I( J0 z5 A5 Z( c I* }# ](4)删除一个设备则要调用cdev_del()函数。# x" I$ U3 z$ x
' z& b& j1 E; q K6 H* ]4 V7 Wvoid cdev_del(struct cdev *dev);+ v# G j7 z6 I. z
二、设备的打开和释放2 l+ m3 z. p& ^5 b
打开设备的函数接口是open,函数原型为:
5 ?/ V1 a$ x+ V) [' v+ Mstatic int device_open(struct inode *inode, struct file *file)
6 i( a! |0 {* m" B& [$ A" P8 a. w0 ^) f1 A! b4 Y! U; s- L6 g
主要完成如下工作:: G# k% F1 [5 H+ J7 b2 t$ }
$ e( i6 y7 |0 s5 ^(1)递增计数器,检查错误;) v3 e k4 J$ q2 ?. I0 _( a
2 {0 ~* C) a$ f$ Z* W
(2)如果未初始化,则进行初始化;
: t: t9 N8 p+ `& o) ?3 u C6 I( f& A3 Z9 {
2 T; |) f7 h8 g" \5 n% Q(3)识别次设备号,如果必要,更新f_op指针;
$ ^9 y# J% ]1 ]$ G) H0 U
! f& c! y8 g C/ \9 z! V(4)分配并填写被置于filp->private_data的数据结构% n- a. r. N1 E! X( q: U& A1 W; i9 ~
/ e3 f" E, Q( K
其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。6 q7 U/ |7 I4 g1 {1 H$ X
' g" l( v9 F& b% \" l
释放设备设备的函数接口是release,函数原型为:
5 `9 r% d# S2 K6 {7 ustatic int device_release(struct inode *inode, struct file *file)
2 [ z- c' U0 v+ r$ M. A, R% I! }, ~% P# H
主要完成工作:
( e% ~& w$ f: I/ D1 ?+ \
7 v8 V7 V$ a* G/ S* b(1)递减计数器;
% G, g" E. b m9 [) c: Q
$ \4 M) Z* o# a; O0 x7 T(2)释放打开设备时系统所分配的内存空间;5 X/ g4 g4 S; c
" i: l4 `) \; |+ E6 V(3)在最后一次释放设备操作时关闭设备- J( C1 ^9 t( x( {2 _
. z" c( C' @) B" m% I+ P9 {3 H
三、读写设备* a5 f2 \& _2 E( h; w
读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,函数原型为:
% n% H/ G/ G# H8 d( l- d
, w+ [+ o1 p/ W6 kssize_t (*read)(struct file *filp, char *buff, size_t count, loff_t *offp);$ p$ Z6 D: g; S6 r8 Z- G8 B6 c
1 I2 y4 Q+ {$ L: @1 Fssize_t (*write)(struct file *filp,const char *buff, size_t count, loff_t *offp);
, r5 ^+ E2 J% J& {9 R' W2 g* z% ]$ C$ f# ^" |" i& A! G
这里重点说明一下buff指的是用户空间指针,不能被内核代码直接引用。有如下理由:2 h- p3 x5 {4 Q
- S* q. R! P2 j A" q- ]' Z# h
(1)依赖于你的驱动运行的体系,用户空间指针当运行于内核模式可能根本是无效的,可能没有那个地址的映射,或者它可能指向一些其他的随机数据。
" U6 X8 a, h$ M3 h# }
- u: ]' V1 P b# f(2)就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时,这个内存可能没有在RAM中。试图直接引用用户空间内存可能产生一个页面错,这是内核代码不允许做的事情,导致进行系统调用的进程死亡。
. R. f. Q- D& t% [5 h5 u% W
0 Y: g1 f- a; L; t其中的读设备的意思就是把数据从内核空间复制到用户空间,而写设备是把数据从用户空间复制到内核空间,这里用到的函数原型为(在asm/uaccess.h)中定义:
8 s. r- i! T' K; T9 ?" d7 K* f" W& {! ]& U9 I
unsigned long copy_to_user(void *to, const void *from, unsigned long count);6 K# u' j! \ O5 o; i% V
( I% l+ |5 e) o [: s( \
unsigned long copy_from_user(void *to, const void *from, unsigned long count);
% y* j; I5 T' L/ H2 T# B% {2 z
4 r, y* L" _4 h; O$ oint put_user(dataum,ptr);
* X2 j( ]8 `9 d) C9 W: L* P ^; n: q6 O6 G( v8 \" k. t
int get_user(local,ptr);
) M; F5 D' x& ~" w! z5 ~6 U
$ T3 d7 ~, k8 ~9 g u0 C0 D: Y//内核空间和用户空间的单值交互(如char、int、long)
' H/ z, X& E B) m3 d6 d, Z四、IO控制函数6 S7 B5 R4 @- ^; f' C& z7 x9 R' q
IO控制函数包括对设备的所有操作,包括设置设备、读写设备等等,这样最大的好处就是给了应用程序一个统一的接口,让应用程序编写非常简单而且易懂。函数原型为:
- t2 R/ W& H' ^' l$ B r* ~* }, v) W5 s6 C! ~8 d# @/ @
static int device_ioctl(struct inode *inode, /* see include/linux/fs.h */
4 Z( i5 m8 d. m2 e3 s( ` struct file *file, /* ditto */# b$ @! s. {' q+ [* S& M% E
unsigned int ioctl_num, /* number and param for ioctl */, h/ {. ]4 m2 \
unsigned long ioctl_param)
8 M* ^- @3 I* X2 r' A$ Q" W4 Z0 b. W. Z' A* t
ioctl_num表示IO控制的类型,可以为IOCTL_SET_MSG、IOCTL_SET_DISP_WAIT、IOCTL_GET_MSG、IOCTL_GET_NTH_BYTE等等,这里仅仅举个例子。
! D% X# D E6 s9 z7 ^6 N1 b; C: [: j' v6 a
ioctl_param参数表示传入的参数。# k# h* R! U, \) Y8 B
! d( v! Q) k* @' a8 `0 v
这里有一个比较重要的概念就是ioctl命令号。
' z: M) ?3 G5 ]' V) i+ a
" c2 T% |0 Q A, n8 y* K; d5 mioctl命令号
( b( P+ k% J" f1 Z1 t9 rioctl命令号是这个函数中最重要的参数,它描述的ioctl要处理的命令。linux中使用一个32位的数据来编码ioctl命令,它包含四个部分:dir,type,nr,size& {3 E8 W8 D. X5 o7 _. g
" h( b, j8 a3 i4 Qdir:- a- t } C0 M& q4 |3 {! Z8 [9 Y
代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,OU),_IOC_WRITE(向设备写数据)或_IOC_READ(从设备读数据)或者他们的逻辑组合,当然这里只有_IOC_WRITE和_IOC_READ的组合才有意义。+ W# e6 ~6 _7 P) b% x: m, C5 d
$ @' T) o6 Z2 B }' n
type:: `. m2 c5 d2 ?: D; I* Y7 C
描述ioctl命令的类型,8bit。每种设备或系统都可以指定自己的一个类型号,ioctl用这个类型来表示ioctl命令所属的设备或驱动。
: _1 V: S8 q8 ^3 s( v/ z& n$ d z, i# W' a
nr( U# b/ f& k5 _% F! @* h
ioctl命令序号,一般8bit,对于一个指定的设备驱动,可以对他的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。
% C$ U) n1 Z3 g/ l4 B" @ w' s- c% x7 c2 s
size" J j# Q$ R5 `6 k' k2 C
ioctl命令的参数大小,一般为14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是建议你指定这个数据成员,通过它可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。
* Q0 C0 B3 g$ \6 ]7 ~4 ]/ n S6 o3 Q m# l
对于自己写的驱动,如果需要使用特定的ioctl命令,则必须创建ioctl命令以及编号。内核中给出了创建ioctl命令的宏。. ] a( i: h+ S9 P7 D0 l: B
1 H8 n2 N+ ]- u' S! z& B( z/* used to create numbers */( P, O% u; S5 i, v
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)0 V% I) d, @% p) k4 B/ n ~7 a
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))& h; O4 E' u( ?1 Q
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))& Q" Z5 A0 _8 U/ i" b+ c/ I: ^
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
' B. M3 Y5 d6 m$ Y( f#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
s/ Q% N6 o4 j5 \#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))4 J7 [+ i1 A/ r" h: H( W+ ~2 z
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))) c u; H0 _+ t+ {. Z8 `: y! \. T
4 ]- @( t, T- J+ H6 _, j- b
总结:上述是基本的字符驱动编写基本知识,针对具体的字符驱动,涉及到具体硬件的一些设置,里面的读写函数以及设置函数都有很多区别,而且大部分都要用到延迟,因为所有的串行硬件设备都是工作在KHZ的频率上,而CPU已经工作在GHZ的水平,所以,这里需要进行软延迟。另外可能还有就是中断方式。 |
|