找回密码
 注册
关于网站域名变更的通知
查看: 287|回复: 1
打印 上一主题 下一主题

linux驱动程序之字符驱动

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2020-5-11 10:17 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

EDA365欢迎您登录!

您需要 登录 才可以下载或查看,没有帐号?注册

x
. o6 V) m; D3 d7 Q
首先讲述一下驱动程序的概念。) m8 k9 x5 f/ e/ k' c, _( b, p$ |
1 }; |+ D; {6 ~. U; |7 I2 B
驱动程序实际上就是硬件与应用程序之间的中间层。驱动程序工作在内核空间,应用程序一般运行于用户态。在内核态下,CPU可执行任何指令,在用户态下CPU只能执行非特权指令。当CPU处于内核态,可以随意进入用户态,而当CPU处于用户态,只能通过特殊的方式进入内核态,比如linux操作系统中的系统调用。系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。$ m6 ^8 S7 |; |4 q
# `3 y1 x1 p, H' W, a( w: [
Linux支持三类硬件设备:字符设备、块设备及网络设备。首先学习一下字符设备驱动的编写。* J6 n8 o5 ]- F  n8 w3 f
8 }- s+ M" i* Q" N! u
一、字符设备注册
% c1 ~7 Z/ ]$ w" b1 O2 e) x在linux 2.6内核中采用的是以下函数进行字符设备注册,而对于新版本的字符设备注册改成新的注册方式。
8 H7 F5 B. \5 |: x+ [. P; b3 g8 }
- N/ w3 w7 T3 `0 z4 [9 C9 o. ]: B, |* Q4 T
int register_chrdev(unsigned int major, const char *name, struct file_operations 7 b1 ^( @$ h9 m& E% W7 U
*fops);- X# n* \" T# t9 O1 g
新的注册方式:  c6 z/ U% s; q8 \1 K) ?! x; T0 s5 f! T

: j$ \7 Y& G9 |3 }7 b6 ?. w(1)在linux内核中使用结构体cdev结构来描述字符设备,在驱动程序中必须将已分配到的设备号以及设备操作接口赋予struct cdev结构变量。首先使用cdev_alloc()函数向系统
, h" D* K3 C2 M; _& J$ j5 [5 }9 i+ E
: H) Q. @7 {4 B9 l" n6 k+ ^4 W+ {申请分配struct cdev结构,函数原型为:; m" L& i) J. j  Z5 P* X

4 V- M5 Q. M9 p* H& Gstruct cdev *cdev_alloc(void);# i4 I& G5 E% _
(2)初始化struct cdev,并与file_operations结构关联起来,即赋值cdev.owner=THIS_MODULE,函数原型为:. d4 k! J6 z" b( _7 I2 j8 d) p
' U' I$ ^( u8 K' U
void cdev_init(struct cdev *cdev,struct file_operations *fops);
1 ?0 l- C* G$ N, T! q1 J" Y(3)调用cdev_add()函数将设备号与struct cdev结构进行关联并向内核正式报告新设备的注册。
  j0 j2 R5 v5 T0 Y* a: g. @! ~% T$ F+ Y) M+ n
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);3 L5 u4 Y. h$ _
//成功返回0,出错返回-1& K  R. c$ U. r, A- F
//cdev:需要初始化/注册/删除的struct cdev结构
! n& e9 V; b2 H* f  A3 L5 w//fops:该字符设备的file_opertions结构
$ H! |" A: X) }" t) ]- X//num:系统给该设备分配的第一个设备号8 G/ {3 e) B) f0 V7 J
//count:该设备对应的设备号数量
) Z, j5 y; g5 Z; N4 E6 L+ y2 F# `(4)删除一个设备则要调用cdev_del()函数。8 M8 X) C' _2 x3 e2 p0 [! l
/ P9 B) e+ y. Z3 T
void cdev_del(struct cdev *dev);
5 L$ u: S7 i$ v. [" J6 W' P" D" Q二、设备的打开和释放
3 ?4 i: g4 i1 ?$ j# l9 N( J+ F打开设备的函数接口是open,函数原型为:5 E+ {/ q, N2 C1 P0 a
static int device_open(struct inode *inode, struct file *file)
6 N% J# b) I" o. t& g1 Q. `7 a5 |) n$ M+ Z) x7 v' P5 R" p
主要完成如下工作:5 z8 J0 a1 Q, V) k6 C1 n

& f* ~$ {. m- e1 @) G+ D(1)递增计数器,检查错误;
! t+ Q7 ]2 ^/ ?2 T7 g% m3 Z4 l) b$ D, ]; ?2 g# l! k% b; [
(2)如果未初始化,则进行初始化;
; V  s  v9 V3 G6 k
4 @: p$ {- n1 t3 p! c  x% ~& H. E! I(3)识别次设备号,如果必要,更新f_op指针;8 q% u9 C1 m" V" |9 }" Q; @

$ a1 n/ f$ ?8 p& G, \0 E" A; V! J, ^(4)分配并填写被置于filp->private_data的数据结构
( N) J$ l0 U# h( \' n8 c; ?7 m' C' Q
其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。
: F4 t- L2 \2 D# \/ \
/ x# ?' M8 B0 o6 a释放设备设备的函数接口是release,函数原型为:
9 k  Y# b: y( [" R5 o) z6 Wstatic int device_release(struct inode *inode, struct file *file), o. l& }0 _4 T: V. f5 i- `0 P+ |: B

9 V9 R2 F3 Q$ h5 H+ F. |. Z6 W: @5 A% b主要完成工作:6 H/ G% j4 R. X9 i+ C

) L$ b$ t8 _$ Q' J+ _4 Y(1)递减计数器;
; f/ b; T7 z1 [/ T8 Q
" ~; s. P( {  F( x- h/ P(2)释放打开设备时系统所分配的内存空间;
7 m+ I( J+ f  K1 ?7 N7 j" \
* n; o! D( w$ E) _1 b1 x  _' c1 [(3)在最后一次释放设备操作时关闭设备
- q# C5 e3 u+ r3 Z
: z2 p5 S. ~% x3 b7 A$ \" J4 x三、读写设备
' N9 }) G6 W. h* I& C读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,函数原型为:
3 d. t9 f- G$ H! F2 A0 |% ^
) h$ N4 L: U2 }3 Essize_t (*read)(struct file *filp, char *buff, size_t count, loff_t *offp);& J8 p, g( y% u

  N! A) q$ d# {' Y$ zssize_t (*write)(struct file *filp,const char *buff, size_t count, loff_t *offp);
, e/ @$ I9 N* C/ O# h% U7 X6 b) C. |( K/ {  _
这里重点说明一下buff指的是用户空间指针,不能被内核代码直接引用。有如下理由:
( B/ v1 ]7 C; l  o, m" k1 w% ?) j; X: F( r
(1)依赖于你的驱动运行的体系,用户空间指针当运行于内核模式可能根本是无效的,可能没有那个地址的映射,或者它可能指向一些其他的随机数据。* V) R6 `$ N, v4 H

' k* x3 n+ A3 X. y(2)就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时,这个内存可能没有在RAM中。试图直接引用用户空间内存可能产生一个页面错,这是内核代码不允许做的事情,导致进行系统调用的进程死亡。2 X* a0 x$ {: Y! M0 f5 g

7 _8 V8 |1 l# d& B2 u( A; G$ D其中的读设备的意思就是把数据从内核空间复制到用户空间,而写设备是把数据从用户空间复制到内核空间,这里用到的函数原型为(在asm/uaccess.h)中定义:& D) D- k' s2 E4 F( n+ Q+ V

1 w& S) e* g8 z8 w. [) P& x% Punsigned long copy_to_user(void *to, const void *from, unsigned long count);2 L8 L8 O1 }, W
6 J9 v: k/ p+ ^
unsigned long copy_from_user(void *to, const void *from, unsigned long count);
0 O' V3 o6 b" h7 ?+ K' b$ m% i( X* |' Y- ~  P( A
int put_user(dataum,ptr);" ?1 x0 w: {( h
. `5 }8 S3 Q  U. C9 R
int get_user(local,ptr);4 Y* r9 M+ q+ }# [3 f$ R# r

* s1 C0 W* b% r3 U//内核空间和用户空间的单值交互(如char、int、long); i& [  b; V; s% O/ l1 p
四、IO控制函数/ O. k# c& z7 I5 W: }0 o  R
IO控制函数包括对设备的所有操作,包括设置设备、读写设备等等,这样最大的好处就是给了应用程序一个统一的接口,让应用程序编写非常简单而且易懂。函数原型为:* S) D: E/ m2 }/ G2 O  H
& w' f4 u/ Z" B$ r( _3 `/ t
static int device_ioctl(struct inode *inode,    /* see include/linux/fs.h */! a' G: }$ R& a; q+ r
         struct file *file,    /* ditto */0 O2 N- v2 B" n0 E1 _
         unsigned int ioctl_num,    /* number and param for ioctl */2 b4 j6 _5 i/ k# U1 {
         unsigned long ioctl_param)9 x5 q9 i+ c1 [, |5 D( Y

1 u; O" d& {, d" b8 [  l5 Aioctl_num表示IO控制的类型,可以为IOCTL_SET_MSG、IOCTL_SET_DISP_WAIT、IOCTL_GET_MSG、IOCTL_GET_NTH_BYTE等等,这里仅仅举个例子。
: P& A  S+ x* M2 y6 N3 X
- F1 j  G( Q# W! Fioctl_param参数表示传入的参数。
) P$ J% O9 ^; u% |$ m( o) z% T) N2 ?1 k  M8 g
这里有一个比较重要的概念就是ioctl命令号。
% K; y6 {4 e8 X. `
0 v. {, b4 {4 H! l3 pioctl命令号
. w0 J: j# O. p+ {7 u+ H6 Dioctl命令号是这个函数中最重要的参数,它描述的ioctl要处理的命令。linux中使用一个32位的数据来编码ioctl命令,它包含四个部分:dir,type,nr,size& D& [" T: F9 N  w" J4 n/ ^

4 a* T( W, `8 Y! Edir:
6 ~) l1 k4 |, P5 K6 Z代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,OU),_IOC_WRITE(向设备写数据)或_IOC_READ(从设备读数据)或者他们的逻辑组合,当然这里只有_IOC_WRITE和_IOC_READ的组合才有意义。
) q* ]1 _- d6 Q" A3 H% T: ~
8 M) x# ~% W6 V% A, J' btype:* [7 ?8 M* T3 G& ?  g
描述ioctl命令的类型,8bit。每种设备或系统都可以指定自己的一个类型号,ioctl用这个类型来表示ioctl命令所属的设备或驱动。# x- X9 f7 H1 m/ p
/ i- X3 S5 q3 X, l% e
nr& I3 _+ x3 s  o
ioctl命令序号,一般8bit,对于一个指定的设备驱动,可以对他的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。
' _; C; v6 F9 T3 Y9 @; n! H% `5 ?2 v& u- r) i: Z2 A
size8 a5 c" `# f; E2 z& ?
ioctl命令的参数大小,一般为14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是建议你指定这个数据成员,通过它可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。: H+ k! n! W! [/ O* x# ?

* j9 c% \' O& p' p% o/ l! n3 L对于自己写的驱动,如果需要使用特定的ioctl命令,则必须创建ioctl命令以及编号。内核中给出了创建ioctl命令的宏。
/ O( z" K$ u3 R; Y6 Z+ {3 W, m4 F
/* used to create numbers */
  e3 r' n! j; x) q" Q#define _IO(type,nr)            _IOC(_IOC_NONE,(type),(nr),0)
, L+ L& f' e" U" P$ K( \3 t#define _IOR(type,nr,size)      _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))" M7 y0 d2 |5 ]+ V2 c/ Z
#define _IOW(type,nr,size)      _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
# c0 {* L6 b  _#define _IOWR(type,nr,size)     _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))! q% v% x6 ~7 G0 _
#define _IOR_BAD(type,nr,size)  _IOC(_IOC_READ,(type),(nr),sizeof(size))
- Q& Z7 t0 Y5 s4 N( o$ i5 J9 q#define _IOW_BAD(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),sizeof(size))$ O. n% X3 p0 b/ l% m- C9 @
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size)), j2 C4 [3 u, @! B3 ~

4 `% p* l6 f# Y0 S: h! ~总结:上述是基本的字符驱动编写基本知识,针对具体的字符驱动,涉及到具体硬件的一些设置,里面的读写函数以及设置函数都有很多区别,而且大部分都要用到延迟,因为所有的串行硬件设备都是工作在KHZ的频率上,而CPU已经工作在GHZ的水平,所以,这里需要进行软延迟。另外可能还有就是中断方式。
  • TA的每日心情

    2019-11-29 15:37
  • 签到天数: 1 天

    [LV.1]初来乍到

    2#
    发表于 2020-5-11 13:13 | 只看该作者
    linux驱动程序之字符驱动
    您需要登录后才可以回帖 登录 | 注册

    本版积分规则

    关闭

    推荐内容上一条 /1 下一条

    EDA365公众号

    关于我们|手机版|EDA365电子论坛网 ( 粤ICP备18020198号-1 )

    GMT+8, 2025-11-24 15:15 , Processed in 0.187500 second(s), 23 queries , Gzip On.

    深圳市墨知创新科技有限公司

    地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

    快速回复 返回顶部 返回列表