|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
, F( b, n) @5 F! U
首先讲述一下驱动程序的概念。( }+ ~& P! S+ S. _) P
) w1 K6 {! Z3 h0 f* E/ X
驱动程序实际上就是硬件与应用程序之间的中间层。驱动程序工作在内核空间,应用程序一般运行于用户态。在内核态下,CPU可执行任何指令,在用户态下CPU只能执行非特权指令。当CPU处于内核态,可以随意进入用户态,而当CPU处于用户态,只能通过特殊的方式进入内核态,比如linux操作系统中的系统调用。系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。4 Q1 a% J2 X, z$ C9 Z+ z4 I1 P
6 N4 _! Q6 G8 B8 V6 V6 |
Linux支持三类硬件设备:字符设备、块设备及网络设备。首先学习一下字符设备驱动的编写。1 S5 V, Y0 W1 e4 ?, X: q
( x: i- v8 G: w; }8 P9 B
一、字符设备注册1 I: _" Q, X) @6 A9 `) n% ^& W
在linux 2.6内核中采用的是以下函数进行字符设备注册,而对于新版本的字符设备注册改成新的注册方式。
8 n. j% s) }4 }' k2 n1 Q' p# y- x* l5 a" i
2 e+ i" g* F* {& }( N" h- h$ D
int register_chrdev(unsigned int major, const char *name, struct file_operations 9 k* P+ x$ S7 b% S W7 e
*fops);
: |3 {# y0 {' s/ _新的注册方式:
" z+ H. ^ Z+ K' y) S3 _# u
+ X( c7 i0 g& D, n(1)在linux内核中使用结构体cdev结构来描述字符设备,在驱动程序中必须将已分配到的设备号以及设备操作接口赋予struct cdev结构变量。首先使用cdev_alloc()函数向系统
1 C( e9 e2 \, u4 }8 e) ^2 G {. o, V1 _
申请分配struct cdev结构,函数原型为:( C( F2 ?, f5 K1 q& u1 _6 s
& x( B/ [6 @' ]0 L7 O6 j! V2 N# s% sstruct cdev *cdev_alloc(void);
- S0 ]% k' t$ w* b(2)初始化struct cdev,并与file_operations结构关联起来,即赋值cdev.owner=THIS_MODULE,函数原型为:* |# ` K$ S V5 d( V; T+ v U5 E
( h: W7 J, B$ V3 n* B# L( L! Svoid cdev_init(struct cdev *cdev,struct file_operations *fops);- V* B1 K4 p2 L* D
(3)调用cdev_add()函数将设备号与struct cdev结构进行关联并向内核正式报告新设备的注册。
& n; Q; r. h, n) ~! D* j2 O7 ?% ?6 C; h! R4 m
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);! l: G/ d7 {+ o9 T6 E+ [
//成功返回0,出错返回-1
1 T8 k- U5 }' P$ z8 Q& A/ ?# _( d//cdev:需要初始化/注册/删除的struct cdev结构2 ]6 Y0 j5 H. y! q! }& K
//fops:该字符设备的file_opertions结构; q) p, Y2 a- w% T4 @
//num:系统给该设备分配的第一个设备号
- m* |! e, j. J% m/ x4 A//count:该设备对应的设备号数量
: G; [' |: G% J7 I S(4)删除一个设备则要调用cdev_del()函数。
7 k3 }# s7 I) y9 p' o; a& r+ |" o9 R
void cdev_del(struct cdev *dev);
; p, j. S( E9 }# X5 N二、设备的打开和释放
. F0 r2 z4 J- Z' L9 o/ z( m+ B打开设备的函数接口是open,函数原型为:9 E5 g$ K* u6 T
static int device_open(struct inode *inode, struct file *file); f9 U6 a* F6 D( M. V) d' \( p
1 T- s5 L5 R9 G% ] _6 P W
主要完成如下工作:
2 ~ Z7 K J: l3 L+ J3 @0 i% ?* l8 D
3 `9 Q; L( k1 ?6 X G* u(1)递增计数器,检查错误;
/ T9 h1 q9 O& `( \: Q6 K! ~% }, M. s& `* }
(2)如果未初始化,则进行初始化;
* u: q/ Y9 r8 O6 Z. r+ f D1 H& B( {" ], t
(3)识别次设备号,如果必要,更新f_op指针;6 [0 `! q3 [8 M5 V
* M3 e3 m/ _0 ^, ?; C(4)分配并填写被置于filp->private_data的数据结构3 p% o9 S, n* ?& i( O- D
# ]( w4 v2 N. Y其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。' U3 h: d, x* G( B7 [
! m" o; I* m$ J, h
释放设备设备的函数接口是release,函数原型为:
3 l; a( d- f6 E- U: i5 Jstatic int device_release(struct inode *inode, struct file *file)
' r5 n0 z. f* f1 J, n0 l: P1 \; }( o
主要完成工作:
% z9 j, p3 f1 U' Q5 t
7 N! |# H! f3 y$ J1 K( K" S(1)递减计数器;
. D1 L5 t- m1 G) x3 X" @2 `5 g' j2 x6 T6 ~
(2)释放打开设备时系统所分配的内存空间;% o; [! W: G9 T8 U; L
8 o, p% D5 v% w5 O' A6 L, ?* j(3)在最后一次释放设备操作时关闭设备
* P" R5 i. W8 i9 u+ q$ M% T% P! f, ]) y* D# K+ z& ?+ J3 R% C6 X
三、读写设备
3 Q( L' \( s# G( T$ i读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,函数原型为:: m' x9 ?' H. R J
8 I: `; ^1 W& Y9 m* dssize_t (*read)(struct file *filp, char *buff, size_t count, loff_t *offp);
- S G% ~6 g! |! h' {& q" C# ^1 ?9 `/ r/ v4 n* o$ Z
ssize_t (*write)(struct file *filp,const char *buff, size_t count, loff_t *offp);
, r6 H& @( K! a
8 B% s+ [: E" H/ g这里重点说明一下buff指的是用户空间指针,不能被内核代码直接引用。有如下理由:
U. V+ |) v/ l. |1 _, a: e
/ z& r) ?; _9 K; w" @4 t(1)依赖于你的驱动运行的体系,用户空间指针当运行于内核模式可能根本是无效的,可能没有那个地址的映射,或者它可能指向一些其他的随机数据。7 z4 i3 F' w! o6 h" D' [
7 c" O" w# [6 _! ?; r- V(2)就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时,这个内存可能没有在RAM中。试图直接引用用户空间内存可能产生一个页面错,这是内核代码不允许做的事情,导致进行系统调用的进程死亡。
0 x: ~2 p& O; A3 W" d
* w, J! A# X/ G& f' q/ v其中的读设备的意思就是把数据从内核空间复制到用户空间,而写设备是把数据从用户空间复制到内核空间,这里用到的函数原型为(在asm/uaccess.h)中定义:
. a5 W. r% F- Y4 J1 w, j
% t! c& K# {$ \% d: X# Ounsigned long copy_to_user(void *to, const void *from, unsigned long count);9 K1 O4 b' U+ B6 H6 `+ P
1 l% c, _/ v, u4 p, J8 ^unsigned long copy_from_user(void *to, const void *from, unsigned long count);5 q) n' K& |8 \" r- w$ _
" w% w" P0 N R' e6 N7 vint put_user(dataum,ptr);2 ]" O! v+ N8 I5 ~3 m; D+ A0 e
9 H2 F$ a* Q [" i8 w; ?int get_user(local,ptr);8 C0 x& E8 Q V% j+ p1 V4 ^! O
1 @# v/ M! x4 U1 r" g: d& J! c, I//内核空间和用户空间的单值交互(如char、int、long)
5 m+ e- n Y5 w# ]6 z% |四、IO控制函数9 K/ @6 s: t B
IO控制函数包括对设备的所有操作,包括设置设备、读写设备等等,这样最大的好处就是给了应用程序一个统一的接口,让应用程序编写非常简单而且易懂。函数原型为:0 A4 W+ E% O; R% T& {% i" R0 N
$ d% v5 ]8 \( y. J+ v: s. q
static int device_ioctl(struct inode *inode, /* see include/linux/fs.h */
, y( v9 A# T+ c- X) ]/ W) y* p# P6 ~ struct file *file, /* ditto */
" o, F. c4 N3 B) b- T8 Y unsigned int ioctl_num, /* number and param for ioctl */, ~( w: E4 A4 J+ `/ e* s: A2 t
unsigned long ioctl_param)
3 l+ o" P% U" z' @5 d$ @9 N' |) S/ T0 e3 \ c2 n, L% l5 N: G
ioctl_num表示IO控制的类型,可以为IOCTL_SET_MSG、IOCTL_SET_DISP_WAIT、IOCTL_GET_MSG、IOCTL_GET_NTH_BYTE等等,这里仅仅举个例子。+ Y& T( H& m$ j& ?6 Q0 g
* P. |( y( v2 u
ioctl_param参数表示传入的参数。
; M7 w6 ^+ i- b( [& O2 c0 K( X W" B* c; e2 e* U
这里有一个比较重要的概念就是ioctl命令号。
$ x4 m7 X2 }' g
( ], H6 M* Z5 ]. Z# @4 ]* U- F+ Jioctl命令号
+ e; P8 v7 R, Q1 P. eioctl命令号是这个函数中最重要的参数,它描述的ioctl要处理的命令。linux中使用一个32位的数据来编码ioctl命令,它包含四个部分:dir,type,nr,size
* j) L1 ?% W# D8 j& w0 @8 F, E, l9 t7 M0 X2 z
dir:
) g0 z5 u4 D6 z% K6 ]代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,OU),_IOC_WRITE(向设备写数据)或_IOC_READ(从设备读数据)或者他们的逻辑组合,当然这里只有_IOC_WRITE和_IOC_READ的组合才有意义。
# I- A+ H; n" R7 h5 {: @7 J
, ]% n8 M4 Z% U& U3 ~type:0 P4 r' {' a! `3 L
描述ioctl命令的类型,8bit。每种设备或系统都可以指定自己的一个类型号,ioctl用这个类型来表示ioctl命令所属的设备或驱动。
! [# h3 p% ]9 r# V% r0 u5 p' r) }. r9 d9 j, K7 F
nr7 @5 t9 O- X' h$ B- s. Y
ioctl命令序号,一般8bit,对于一个指定的设备驱动,可以对他的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。
8 _! ?4 C# k' G s0 H/ r* p2 I9 w7 H( m- y6 f: R7 Z. A
size u7 ]/ K4 r1 [- b4 b- I {" G
ioctl命令的参数大小,一般为14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是建议你指定这个数据成员,通过它可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。
2 _. ^1 V0 k! _ f3 |9 i9 f
7 M, L5 J) s( g1 W, I& n对于自己写的驱动,如果需要使用特定的ioctl命令,则必须创建ioctl命令以及编号。内核中给出了创建ioctl命令的宏。
5 f/ P& H$ a( r! c5 G8 i9 G! v8 v& W r
/* used to create numbers */
# @8 x. l. K: e$ `#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)" x6 C K- x5 s. p" _
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
: k& x4 x: o; Q! ^' p#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))); o. [) n5 o* M5 A" `
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))& h% x: a% q- `2 r: y; \
#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
u3 ]2 S7 R q( N, I#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
! i% R- `) w8 e$ ^5 z#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size)): L3 C! }* }/ r0 O5 V2 l: y4 ?
# R6 L% m0 ]+ o" U" @) {6 a5 r! w$ O
总结:上述是基本的字符驱动编写基本知识,针对具体的字符驱动,涉及到具体硬件的一些设置,里面的读写函数以及设置函数都有很多区别,而且大部分都要用到延迟,因为所有的串行硬件设备都是工作在KHZ的频率上,而CPU已经工作在GHZ的水平,所以,这里需要进行软延迟。另外可能还有就是中断方式。 |
|