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

linux驱动程序之字符驱动

[复制链接]

该用户从未签到

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

EDA365欢迎您登录!

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

x

1 [+ t4 d) p8 S& r首先讲述一下驱动程序的概念。5 l, G$ g- S% H. _4 ]
1 g1 g- X. E- h: E" J
驱动程序实际上就是硬件与应用程序之间的中间层。驱动程序工作在内核空间,应用程序一般运行于用户态。在内核态下,CPU可执行任何指令,在用户态下CPU只能执行非特权指令。当CPU处于内核态,可以随意进入用户态,而当CPU处于用户态,只能通过特殊的方式进入内核态,比如linux操作系统中的系统调用。系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。
: ]* w2 Q# ?: e2 T  H' w5 J4 z, v9 d- `) e8 e
Linux支持三类硬件设备:字符设备、块设备及网络设备。首先学习一下字符设备驱动的编写。
3 A- w  H4 L% n! [9 \' A( U
% Y! M" J) s" z0 d0 @0 u. _一、字符设备注册
! }% L: e+ l6 C- B在linux 2.6内核中采用的是以下函数进行字符设备注册,而对于新版本的字符设备注册改成新的注册方式。2 a1 D$ M, V, A; N5 i

$ p' n- f* H3 J  h& p
0 f. t1 u. G9 i, N/ b9 Zint register_chrdev(unsigned int major, const char *name, struct file_operations
- @6 @; v9 N" F3 j# |, T* G4 ]& x*fops);
: Q) g$ S& `7 e; ?2 |- v新的注册方式:
' Y8 T' ^  D5 u0 T- E
7 l. O+ r$ E* v(1)在linux内核中使用结构体cdev结构来描述字符设备,在驱动程序中必须将已分配到的设备号以及设备操作接口赋予struct cdev结构变量。首先使用cdev_alloc()函数向系统
2 ~& \1 @3 T* a, V9 y4 ]% Y+ w+ o4 g- F. N7 E
申请分配struct cdev结构,函数原型为:, [9 m$ a* X  m5 i+ p/ {1 c
2 v2 ]' h  v% Q
struct cdev *cdev_alloc(void);
% A' m& _, l% b8 O% Y" k4 {6 v9 @, {(2)初始化struct cdev,并与file_operations结构关联起来,即赋值cdev.owner=THIS_MODULE,函数原型为:
. I. X( @5 }: o( i$ F
& |( {. L* z" j: d* Z* l+ Nvoid cdev_init(struct cdev *cdev,struct file_operations *fops);& m) U1 ?1 x5 D1 j1 }: L; y8 ]$ U% g
(3)调用cdev_add()函数将设备号与struct cdev结构进行关联并向内核正式报告新设备的注册。
( h3 U, V) P. N3 \( P  ~- P) p* y
3 E9 f: q( n( `1 b( T3 {" nint cdev_add(struct cdev *cdev, dev_t num, unsigned int count);. F' \& ?2 \* N
//成功返回0,出错返回-1" l; k. e( `* _+ m
//cdev:需要初始化/注册/删除的struct cdev结构7 P9 i/ [, {4 f8 \6 Q4 ^
//fops:该字符设备的file_opertions结构
. q% U4 o/ m9 r. l$ ~//num:系统给该设备分配的第一个设备号3 R' v$ [% f3 c5 J) J
//count:该设备对应的设备号数量- X" c9 F) \/ L) A5 R! X! \
(4)删除一个设备则要调用cdev_del()函数。
4 m, I  [& M% ^$ ~4 g, t
: s: l0 S6 O8 `! f. P5 c4 }void cdev_del(struct cdev *dev);
# x' ]* i6 {- p) ?7 ?$ a二、设备的打开和释放
% q" O  j! R) o6 N打开设备的函数接口是open,函数原型为:" X; [2 C# F# y5 ~
static int device_open(struct inode *inode, struct file *file)! C2 ?5 p' W# s0 I5 w& X) o# Z& x' ^
+ X; c1 j6 {3 v2 s- Y( H$ J6 N
主要完成如下工作:
/ J: J0 O4 s# ^/ Q: T+ O/ J1 U7 W  W! r9 Q+ u$ ]: h, J) \+ k( C
(1)递增计数器,检查错误;9 O9 L8 [) O5 M/ G

& T8 B/ @3 w8 K% J6 V(2)如果未初始化,则进行初始化;
& q% E) k1 U1 @
) `" L- Q/ Y/ o0 d( h5 K5 r- t(3)识别次设备号,如果必要,更新f_op指针;
( K  Z! d; `! t6 ?! R7 C4 y8 [$ j1 Y6 w" O, u
(4)分配并填写被置于filp->private_data的数据结构) U! ?  d) b' l7 n  G

( N9 L: d+ V8 l  p0 [; P其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。" c* q& Q. w+ w9 V* Y5 I2 ?" y8 x
( Z1 T2 x2 U$ X9 n
释放设备设备的函数接口是release,函数原型为:- M) {" f8 q) @. C
static int device_release(struct inode *inode, struct file *file)
- H9 J; \2 ]8 s
  d3 H; O% b4 |主要完成工作:1 L" B! K( r3 A; A! h

$ G) k: Y9 x& S" u: z+ ~& Q(1)递减计数器;1 D* r3 G% Q7 |8 \& P
) i5 @6 |/ h6 q2 S" u6 y
(2)释放打开设备时系统所分配的内存空间;
  d6 [: f& d  J2 T( f  J/ e
) m3 L' K0 ]& M$ M6 r- ]& v(3)在最后一次释放设备操作时关闭设备& R; k: l- T/ h7 k* \8 W
& s# q/ m. W  I! f# f
三、读写设备* C) C8 ~2 J4 N+ `) _# u/ A3 H0 ?, g5 A
读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,函数原型为:/ z- F4 u. B! ~$ ~# K1 E
" v) m' q( D& X6 y# s; f
ssize_t (*read)(struct file *filp, char *buff, size_t count, loff_t *offp);
" ~0 p: M. v( ?! P9 ]/ {; S: f3 A* N$ R" S1 A8 V5 m+ e
ssize_t (*write)(struct file *filp,const char *buff, size_t count, loff_t *offp);4 G* X+ h& C* l/ ?6 |, a2 k. G

6 }$ x* g6 C/ Q# ]' F+ o7 C" z这里重点说明一下buff指的是用户空间指针,不能被内核代码直接引用。有如下理由:
: m% }' v/ _# e4 j2 i8 v$ f! H% O2 g* _, O; ^1 W6 X
(1)依赖于你的驱动运行的体系,用户空间指针当运行于内核模式可能根本是无效的,可能没有那个地址的映射,或者它可能指向一些其他的随机数据。
" g4 u' |$ g; f; `1 }8 `7 R7 F
% i" D0 C; P$ h$ X1 c9 q5 ^(2)就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时,这个内存可能没有在RAM中。试图直接引用用户空间内存可能产生一个页面错,这是内核代码不允许做的事情,导致进行系统调用的进程死亡。; T' A9 T  }- A; e
; v8 u, l9 T1 J0 m
其中的读设备的意思就是把数据从内核空间复制到用户空间,而写设备是把数据从用户空间复制到内核空间,这里用到的函数原型为(在asm/uaccess.h)中定义:. ?( `$ L6 g1 O% y5 `1 U( y

4 H. }& N8 _4 ?unsigned long copy_to_user(void *to, const void *from, unsigned long count);
- u# H1 L+ e( ?9 D/ E  G8 k! N
/ U: J# q7 U- [1 i$ x1 q% `unsigned long copy_from_user(void *to, const void *from, unsigned long count);
( Q1 M( \# e/ R( P+ |- b7 z7 M( H! o- \) a% [3 i
int put_user(dataum,ptr);
" ~5 J+ }( a0 [0 I+ `; R' _/ G) K1 r  D% h: R3 P
int get_user(local,ptr);
; Y3 J$ t+ V9 v- y7 {+ z& p2 L; r* |: P- {  {+ k4 q
//内核空间和用户空间的单值交互(如char、int、long)
5 w: ?: X  v8 E; U0 P四、IO控制函数
( G! z: c4 ?  w; m1 _1 XIO控制函数包括对设备的所有操作,包括设置设备、读写设备等等,这样最大的好处就是给了应用程序一个统一的接口,让应用程序编写非常简单而且易懂。函数原型为:
' E+ g) V: v' U
( r9 w' K& k2 s9 g8 t. Wstatic int device_ioctl(struct inode *inode,    /* see include/linux/fs.h */' V% r6 _/ }* t
         struct file *file,    /* ditto */. r/ i5 }5 r6 w, p5 A0 X7 C; ]
         unsigned int ioctl_num,    /* number and param for ioctl */
6 C, a5 j4 A1 w8 U7 A         unsigned long ioctl_param)
0 h# q5 \* O6 M7 Z( \; y
  a9 y8 C4 ?7 j4 l6 [) L6 ~2 b9 zioctl_num表示IO控制的类型,可以为IOCTL_SET_MSG、IOCTL_SET_DISP_WAIT、IOCTL_GET_MSG、IOCTL_GET_NTH_BYTE等等,这里仅仅举个例子。
! e% ~7 |% Y8 o9 v4 p% {
& k* [6 I: s' |1 H/ B8 k* P" k  ~ioctl_param参数表示传入的参数。! w, d) N. Y+ |) A( H1 A3 r7 e( f
( I( |8 ]) t% r: G5 w) ~+ e
这里有一个比较重要的概念就是ioctl命令号。, Z: H- X1 _3 \( T
1 Z) R( @8 A4 x; N
ioctl命令号
, |# d% ~3 \; O4 a" O% jioctl命令号是这个函数中最重要的参数,它描述的ioctl要处理的命令。linux中使用一个32位的数据来编码ioctl命令,它包含四个部分:dir,type,nr,size& r8 E; B% V& h7 s
2 C6 ]' x  v, k/ t; W7 T; F
dir:
6 s1 J3 B6 D! n( j代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,OU),_IOC_WRITE(向设备写数据)或_IOC_READ(从设备读数据)或者他们的逻辑组合,当然这里只有_IOC_WRITE和_IOC_READ的组合才有意义。) p9 F! }4 m3 d2 P7 K

9 d" S7 f- z1 [8 j- b+ D& Xtype:
4 G0 ]5 \, u4 Z1 Q! }7 o描述ioctl命令的类型,8bit。每种设备或系统都可以指定自己的一个类型号,ioctl用这个类型来表示ioctl命令所属的设备或驱动。$ J; [! ]; U' W* D. y; P7 x

! e2 ]) T" }# Y: q; A9 Anr2 M. C7 g; g; u! z) U  B6 k
ioctl命令序号,一般8bit,对于一个指定的设备驱动,可以对他的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。& `6 z3 |7 G) F, ]# W
0 m3 y' S* z; K! L2 ^8 Y1 o
size% v- A1 ~, A7 t3 e( \! f6 a
ioctl命令的参数大小,一般为14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是建议你指定这个数据成员,通过它可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。' x' N4 @* f  o; B

/ _  V* Q5 P$ e& p对于自己写的驱动,如果需要使用特定的ioctl命令,则必须创建ioctl命令以及编号。内核中给出了创建ioctl命令的宏。
- `7 V& Y& U* S% O+ Q4 C& f1 u) y4 j( D6 @+ @0 R. ~, S
/* used to create numbers */
  Z/ I( _  l; a) C3 A4 g3 p# p#define _IO(type,nr)            _IOC(_IOC_NONE,(type),(nr),0)
' p/ d0 x. s  d# }" t" b#define _IOR(type,nr,size)      _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
! @! Q% W, e3 @' N- t$ _4 l* ~#define _IOW(type,nr,size)      _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))  v4 U! k/ N4 M2 B1 S
#define _IOWR(type,nr,size)     _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
, g% p  X$ Q/ B- x#define _IOR_BAD(type,nr,size)  _IOC(_IOC_READ,(type),(nr),sizeof(size))6 W. ]7 ^% {+ B" w
#define _IOW_BAD(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
0 S' l2 s5 z$ Z: B) f#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))2 W+ \5 y# w; W! {/ L9 M( D
8 r" m% d2 b7 X# I4 Q# Q/ e
总结:上述是基本的字符驱动编写基本知识,针对具体的字符驱动,涉及到具体硬件的一些设置,里面的读写函数以及设置函数都有很多区别,而且大部分都要用到延迟,因为所有的串行硬件设备都是工作在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 16:48 , Processed in 0.187500 second(s), 25 queries , Gzip On.

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

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

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