EDA365电子论坛网
标题:
linux驱动程序之字符驱动
[打印本页]
作者:
uqHZau
时间:
2020-5-11 10:17
标题:
linux驱动程序之字符驱动
9 C2 \- i' W' H/ c/ N- |
首先讲述一下驱动程序的概念。
& c3 j# ]" @8 _+ T3 m
) P3 p$ {& Q% F
驱动程序实际上就是硬件与应用程序之间的中间层。驱动程序工作在内核空间,应用程序一般运行于用户态。在内核态下,CPU可执行任何指令,在用户态下CPU只能执行非特权指令。当CPU处于内核态,可以随意进入用户态,而当CPU处于用户态,只能通过特殊的方式进入内核态,比如linux操作系统中的系统调用。系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。
' l- M: e1 I3 Q# L0 I/ C* u
/ h, G5 t, H* A
Linux支持三类硬件设备:字符设备、块设备及网络设备。首先学习一下字符设备驱动的编写。
/ L! m: w% a( w7 J
4 j( i$ Z3 m d7 M! Q; @
一、字符设备注册
9 X, m" } U' E# O. C8 a- p5 m
在linux 2.6内核中采用的是以下函数进行字符设备注册,而对于新版本的字符设备注册改成新的注册方式。
' _+ M6 P, }7 ~% h5 f: D
4 b( v0 j. a. M
5 O n9 u. M |7 ]
int register_chrdev(unsigned int major, const char *name, struct file_operations
" a! N" o: O3 y8 p
*fops);
9 F( G- k& D8 n
新的注册方式:
# H! ]& I( ]- J- K1 l8 n' H" d
5 X, |, B; e& \7 Z! j, x6 d
(1)在linux内核中使用结构体cdev结构来描述字符设备,在驱动程序中必须将已分配到的设备号以及设备操作接口赋予struct cdev结构变量。首先使用cdev_alloc()函数向系统
7 t3 U9 g8 I' z) M' J2 l
& w, e# d5 m e
申请分配struct cdev结构,函数原型为:
) p6 ]" j8 p( x, {9 J
9 l7 K& O. z" Y7 q
struct cdev *cdev_alloc(void);
1 `/ e# ?- l w" }3 ^2 h; y
(2)初始化struct cdev,并与file_operations结构关联起来,即赋值cdev.owner=THIS_MODULE,函数原型为:
5 k+ `* H# w0 I9 o! }: a$ M
; p" J) r$ ~% P% L# D* w
void cdev_init(struct cdev *cdev,struct file_operations *fops);
' n3 z' E. d( r: }. M2 Z8 C: `
(3)调用cdev_add()函数将设备号与struct cdev结构进行关联并向内核正式报告新设备的注册。
' R7 u! w6 D% m; `, c5 N6 c) A
& B. ^3 d- `" ~1 h6 ?0 R0 _9 C8 ^
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);
5 A$ S# W/ |& S% G, m
//成功返回0,出错返回-1
& \$ K- v7 R1 Y: \ k7 Z: ?: \
//cdev:需要初始化/注册/删除的struct cdev结构
% A, C) y2 ~$ u6 H: O( O
//fops:该字符设备的file_opertions结构
# l( h# o8 l0 ^7 \" L4 T p
//num:系统给该设备分配的第一个设备号
$ t; w+ W5 z! \2 C* n
//count:该设备对应的设备号数量
# _2 {. F+ ~/ d- L% W
(4)删除一个设备则要调用cdev_del()函数。
" {9 |" t5 ^7 u1 v- W6 g
B! q% c# E. P1 ~: n% L' w! s8 U
void cdev_del(struct cdev *dev);
# f; f9 B( W" n
二、设备的打开和释放
* ^, v! S6 L6 Q d9 i, P
打开设备的函数接口是open,函数原型为:
- x* k R7 q' M5 y! U
static int device_open(struct inode *inode, struct file *file)
- V L- f0 m9 V& g X+ a
& ]+ N: Z( [1 W9 J- B5 z) U" y
主要完成如下工作:
' R5 h4 ?& U5 y( c2 {( _
( J$ s) j' W6 L
(1)递增计数器,检查错误;
1 d; ]' c8 B" y4 m1 ?
I8 B" ?/ S4 h
(2)如果未初始化,则进行初始化;
% ]: c+ `' Y! |+ s/ O0 q
) l8 p L* k% f! ~2 O0 N. q, F
(3)识别次设备号,如果必要,更新f_op指针;
! Q5 I( C8 G7 |& W( j7 ~, Q
& X! a/ C' H% A. e& `) Y. R
(4)分配并填写被置于filp->private_data的数据结构
+ q. {2 c: s } `) E% r
4 h% u3 ]# h+ o; q6 F3 I& m
其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。
' {- B& p0 B8 N$ z
, i& x+ P, p: S% s& e
释放设备设备的函数接口是release,函数原型为:
% R8 X# I" p, H( r
static int device_release(struct inode *inode, struct file *file)
P/ i1 E5 e$ P1 R4 \6 v: C
& V4 o- j; Q4 a! B( L- i
主要完成工作:
# A+ R; ?! M, ?2 e0 P+ o
' g0 `7 H4 L W8 c, V2 Z1 r7 I
(1)递减计数器;
7 ~! X( X. C$ x ]
7 O. R' l" b1 U, Z# x/ h
(2)释放打开设备时系统所分配的内存空间;
0 p6 W6 |5 o* E( @! V+ t+ z
- H3 u: z8 m& V1 N6 ?2 o0 t
(3)在最后一次释放设备操作时关闭设备
B( c1 Z! k; Y$ Q+ r+ ?
( \2 o% W" i* H5 x! D) q
三、读写设备
6 \* D4 j5 o. r: y$ x! T
读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,函数原型为:
' |1 I1 Z1 `1 Z( M5 I
$ f, s5 a0 |1 q' i1 a" l
ssize_t (*read)(struct file *filp, char *buff, size_t count, loff_t *offp);
2 v9 f( h7 R% i- H( I
7 j( y; Y% c* n+ o/ x7 J9 q
ssize_t (*write)(struct file *filp,const char *buff, size_t count, loff_t *offp);
0 i$ Z" l* K0 T# I/ m
% r3 i( a" n* U/ p7 B% P! ^/ D" e3 ]
这里重点说明一下buff指的是用户空间指针,不能被内核代码直接引用。有如下理由:
) k( N# O* y3 z" _
% X7 }! N. i0 d" G5 g% c
(1)依赖于你的驱动运行的体系,用户空间指针当运行于内核模式可能根本是无效的,可能没有那个地址的映射,或者它可能指向一些其他的随机数据。
9 ~' Y- W1 L: X; u, g* _- U
* `; I7 s# r6 h5 I8 a
(2)就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时,这个内存可能没有在RAM中。试图直接引用用户空间内存可能产生一个页面错,这是内核代码不允许做的事情,导致进行系统调用的进程死亡。
% u! N% T$ b; l1 E, f* Y
* `$ \9 W7 y/ \! q9 Z6 G9 i
其中的读设备的意思就是把数据从内核空间复制到用户空间,而写设备是把数据从用户空间复制到内核空间,这里用到的函数原型为(在asm/uaccess.h)中定义:
+ t% |# Y" ~( u0 h" N
8 a+ f! E4 q% a( Z* k, f2 V4 L3 |3 Q
unsigned long copy_to_user(void *to, const void *from, unsigned long count);
; e* y+ ^, k9 m
$ r( H% b* O& t& O. T
unsigned long copy_from_user(void *to, const void *from, unsigned long count);
3 ^) ?5 ~: [* v( C" `, g0 F
% B( N. ^, _! h5 @; C
int put_user(dataum,ptr);
# E# n& ]: M$ C# D) R9 M
7 Z0 d0 x* F; h( v, H
int get_user(local,ptr);
6 v, v$ X& @* H
0 e' y8 G$ d, y
//内核空间和用户空间的单值交互(如char、int、long)
7 r4 I1 t7 A0 a* Y |
四、IO控制函数
$ R H# c; R! R, N2 S1 ?8 G" D
IO控制函数包括对设备的所有操作,包括设置设备、读写设备等等,这样最大的好处就是给了应用程序一个统一的接口,让应用程序编写非常简单而且易懂。函数原型为:
7 G6 z5 z' y4 A; Z ^+ D* _! T1 ?8 x1 l
# H3 W! b6 E6 E4 k
static int device_ioctl(struct inode *inode, /* see include/linux/fs.h */
0 I- Y" n" w6 x5 ~% @
struct file *file, /* ditto */
0 Z9 K; J0 G$ t
unsigned int ioctl_num, /* number and param for ioctl */
* Y$ F$ |8 Y) P2 |8 W$ L9 a0 ?/ z4 J
unsigned long ioctl_param)
. E% i# |1 G `8 f
/ P2 y! B3 g9 Y2 L% a' f* H% N
ioctl_num表示IO控制的类型,可以为IOCTL_SET_MSG、IOCTL_SET_DISP_WAIT、IOCTL_GET_MSG、IOCTL_GET_NTH_BYTE等等,这里仅仅举个例子。
$ d8 ^3 F: W$ o7 J
" d( A9 a/ n: _9 I$ w! m
ioctl_param参数表示传入的参数。
/ ]8 K5 p! j) m6 v
G+ L, x, Y# Z) G" |5 f
这里有一个比较重要的概念就是ioctl命令号。
, `" P( N: j9 M: L
6 c0 `& T$ d! B7 d
ioctl命令号
U+ o; W: T8 Q' S" W6 k
ioctl命令号是这个函数中最重要的参数,它描述的ioctl要处理的命令。linux中使用一个32位的数据来编码ioctl命令,它包含四个部分:dir,type,nr,size
& {- D+ A9 r% p* b( N
# S8 ~9 V! {* V! y. p
dir:
) g( _1 h( H5 ^- J8 e# Y; ]5 Y
代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,OU),_IOC_WRITE(向设备写数据)或_IOC_READ(从设备读数据)或者他们的逻辑组合,当然这里只有_IOC_WRITE和_IOC_READ的组合才有意义。
. A& ^- r: d! @. d" M' j6 k
" p1 l# A; u$ _1 V- \$ y' M# V' R+ E
type:
3 g: C9 ]6 K8 k* y# W- z: S) P0 I
描述ioctl命令的类型,8bit。每种设备或系统都可以指定自己的一个类型号,ioctl用这个类型来表示ioctl命令所属的设备或驱动。
) v/ h$ A# I: b! c* D0 @' w- w" E
4 F- y) j. X; l+ Z5 t# O) o
nr
3 q/ W1 d; x1 K9 J
ioctl命令序号,一般8bit,对于一个指定的设备驱动,可以对他的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。
0 g- J; F$ k$ `' k% n. }9 r% S
0 W7 x, q7 D, K3 h, T( }! D3 n8 V
size
/ x8 w) q9 L4 f1 p5 ]
ioctl命令的参数大小,一般为14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是建议你指定这个数据成员,通过它可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。
3 ^- p4 R; e" N& y( o) }6 E# N
" C4 K5 e4 Z+ K8 C/ y3 l1 S0 x
对于自己写的驱动,如果需要使用特定的ioctl命令,则必须创建ioctl命令以及编号。内核中给出了创建ioctl命令的宏。
+ L4 V! @+ t" j
4 S$ _) g8 Z1 H2 x/ K: e
/* used to create numbers */
- U' ^5 W: H0 Y1 `) Z$ |- f4 C/ I
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
8 `4 y7 t ^9 M$ \# u; i S
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
, d" v* H. Q4 X) L( P1 a2 T7 E
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
; G% m/ E) V* D, U
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
; w2 k- g. ^$ w8 h, S
#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
# Q5 o- x' N3 C6 K4 E6 H
#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
. E* t' E1 M- |. r: g* z
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
+ ]2 _7 W! ~8 s: [& J
B; F# `; O) n
总结:上述是基本的字符驱动编写基本知识,针对具体的字符驱动,涉及到具体硬件的一些设置,里面的读写函数以及设置函数都有很多区别,而且大部分都要用到延迟,因为所有的串行硬件设备都是工作在KHZ的频率上,而CPU已经工作在GHZ的水平,所以,这里需要进行软延迟。另外可能还有就是中断方式。
作者:
yin123
时间:
2020-5-11 13:13
linux驱动程序之字符驱动
欢迎光临 EDA365电子论坛网 (https://bbs.eda365.com/)
Powered by Discuz! X3.2