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

linux驱动程序之字符驱动

[复制链接]

该用户从未签到

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

EDA365欢迎您登录!

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

x
- W6 {, x: T$ E8 v+ m
首先讲述一下驱动程序的概念。
$ R8 z& @1 p2 J) ?' F4 A6 b; A5 b( R6 D. L+ Z/ s
驱动程序实际上就是硬件与应用程序之间的中间层。驱动程序工作在内核空间,应用程序一般运行于用户态。在内核态下,CPU可执行任何指令,在用户态下CPU只能执行非特权指令。当CPU处于内核态,可以随意进入用户态,而当CPU处于用户态,只能通过特殊的方式进入内核态,比如linux操作系统中的系统调用。系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。7 M* g  q4 v) @# d% {% D
4 B. l3 @& l2 E9 P4 n7 q' t: O
Linux支持三类硬件设备:字符设备、块设备及网络设备。首先学习一下字符设备驱动的编写。
- o; h8 {  M5 `% }6 \* j0 j. ]
* q/ P; B. N/ X) W一、字符设备注册
1 ~: o) W  t+ }: r* q& N. C在linux 2.6内核中采用的是以下函数进行字符设备注册,而对于新版本的字符设备注册改成新的注册方式。
1 ~1 }& j8 l' m5 c, `1 n4 m
& p+ m" S. ]/ [+ I& P6 H
6 G. f* Z  n8 \/ L; M2 [int register_chrdev(unsigned int major, const char *name, struct file_operations 7 g' `( S& K0 y) ^
*fops);6 t9 O' D5 b- u4 a, K: X" A9 S
新的注册方式:
2 F4 d8 U9 u. C: l  u* T
- l7 c6 w5 S! T1 i" C( s(1)在linux内核中使用结构体cdev结构来描述字符设备,在驱动程序中必须将已分配到的设备号以及设备操作接口赋予struct cdev结构变量。首先使用cdev_alloc()函数向系统
6 t$ H3 i( _) I% f' c' s
/ k, ]' N2 c1 X  w/ d9 J申请分配struct cdev结构,函数原型为:; b- p1 F+ A0 ^4 O
$ g3 I, R" Y& L3 j0 h7 K
struct cdev *cdev_alloc(void);( w& o8 P* a& z) t" i* w# L
(2)初始化struct cdev,并与file_operations结构关联起来,即赋值cdev.owner=THIS_MODULE,函数原型为:
) W4 W4 l! ?9 x. S8 [: e8 W1 C" ^# ?7 ]4 J6 M
void cdev_init(struct cdev *cdev,struct file_operations *fops);! N6 U  N$ y& z' J# R( i: O$ b- o
(3)调用cdev_add()函数将设备号与struct cdev结构进行关联并向内核正式报告新设备的注册。
! m- ~, T& r* d1 }+ Z4 i/ H) b+ \2 E+ s7 o
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);
  f9 V$ Z; \: _; ~//成功返回0,出错返回-1# k8 @' m9 ~7 b8 z+ p2 R0 y' _
//cdev:需要初始化/注册/删除的struct cdev结构
* P& I( Y0 r- {9 b//fops:该字符设备的file_opertions结构/ F# e- S) V7 l5 }: {, |, F
//num:系统给该设备分配的第一个设备号8 H, N: Z  G. S: r5 `$ y) M
//count:该设备对应的设备号数量
2 z" K* ?- w4 ~4 x5 z# d9 ~(4)删除一个设备则要调用cdev_del()函数。8 m! j" @  ^) G% p: S' a7 H

' o, T9 j3 n. @- r( [' @0 f0 ^, Nvoid cdev_del(struct cdev *dev);
& J3 m1 _$ A5 @1 t二、设备的打开和释放+ j" L2 f2 L' F5 |4 u! i8 _( g3 M' Y
打开设备的函数接口是open,函数原型为:, @+ a: X! X- a0 |& L
static int device_open(struct inode *inode, struct file *file)' }% H8 b: x8 L& r8 R8 ^

/ z: v6 }, c. \* p主要完成如下工作:& E, i4 O! r! M* h/ B

8 L+ z# }. [- f; u+ K" C(1)递增计数器,检查错误;
$ B1 Z) w) h2 D# p' }6 q) d3 T/ ?% b( D! r1 T
(2)如果未初始化,则进行初始化;5 A+ B' K# p6 t

7 N; F: L! C) Q- d# I5 n  U(3)识别次设备号,如果必要,更新f_op指针;. d% N. u8 U7 Y& j8 k
; b7 v/ S: [9 K$ w) ?3 v9 x
(4)分配并填写被置于filp->private_data的数据结构
$ v8 e& O0 X( m' U5 _0 V: J9 P# j, d# I9 Z1 p
其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。5 ?+ t( E- N2 L& B# @  H

$ k; j8 D6 f2 @8 g: T释放设备设备的函数接口是release,函数原型为:
, q/ X3 Z* _% O' Cstatic int device_release(struct inode *inode, struct file *file)# w8 U' v! I7 ?9 ^  X9 d( R7 W5 J

5 q& a, R7 I* P+ O. u; l主要完成工作:! U& y- n8 ?+ E/ E

. E- r% D0 b% H. {7 d( O; X(1)递减计数器;
# |9 l9 B' G+ U/ Q# `* l( V' z" A: ]9 G1 M' Q
(2)释放打开设备时系统所分配的内存空间;1 Z  u; Q& t& b

/ o/ g9 f9 m, n/ s(3)在最后一次释放设备操作时关闭设备7 |' `5 o4 [9 S( ^) Y* M0 e
( D$ b* k/ c0 f; S( E
三、读写设备
% ^+ X" N3 {" t5 o# W+ O读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,函数原型为:
8 n; y) }" o5 b, i% a6 E
* l+ E6 V  ^/ M0 Z) R  E. x: l# t* Jssize_t (*read)(struct file *filp, char *buff, size_t count, loff_t *offp);
/ a0 z9 y) s5 z* h- z/ i; H, ?" H; ^5 _9 j& t7 M+ _
ssize_t (*write)(struct file *filp,const char *buff, size_t count, loff_t *offp);+ _. r9 x& K; s9 k5 {* R( @
/ @; e, I9 c4 J& j1 C
这里重点说明一下buff指的是用户空间指针,不能被内核代码直接引用。有如下理由:
0 q4 y2 n. {: k7 p; s5 M" C" H7 \$ g. G
(1)依赖于你的驱动运行的体系,用户空间指针当运行于内核模式可能根本是无效的,可能没有那个地址的映射,或者它可能指向一些其他的随机数据。# K$ b) b8 }8 }6 k0 J  C

( H6 y5 n7 `3 P+ U(2)就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时,这个内存可能没有在RAM中。试图直接引用用户空间内存可能产生一个页面错,这是内核代码不允许做的事情,导致进行系统调用的进程死亡。  I8 K* W$ T) N1 Z

+ I/ J' f$ r- U8 p其中的读设备的意思就是把数据从内核空间复制到用户空间,而写设备是把数据从用户空间复制到内核空间,这里用到的函数原型为(在asm/uaccess.h)中定义:5 l; D) M" v( ?
. J4 E$ t) U! u$ y0 Q4 Z
unsigned long copy_to_user(void *to, const void *from, unsigned long count);, }/ }' s" U, ~- ~2 L
, L1 T* W3 t  q7 l8 `3 Q( D; S# E
unsigned long copy_from_user(void *to, const void *from, unsigned long count);0 l9 F. v8 E( D' A4 h3 q2 n7 E
- C* r" L' Q9 g+ E
int put_user(dataum,ptr);
& s& y) Z2 a9 y5 Q- p* v' ]) r. O% f$ @2 x
int get_user(local,ptr);
# [  ?# _' x9 f6 J1 p* B+ D5 W- B( A, l8 h; _. m2 a$ Z
//内核空间和用户空间的单值交互(如char、int、long): g8 x$ C8 x2 t- M$ A0 K0 U
四、IO控制函数/ o+ u* J6 I. D. g
IO控制函数包括对设备的所有操作,包括设置设备、读写设备等等,这样最大的好处就是给了应用程序一个统一的接口,让应用程序编写非常简单而且易懂。函数原型为:) v2 n1 v% [& r) N+ [
/ C: l* I) @/ r7 T2 p
static int device_ioctl(struct inode *inode,    /* see include/linux/fs.h */
' w% q: |* V2 S' C: e* \! [         struct file *file,    /* ditto */7 z; m# K# m4 s' }6 f& O
         unsigned int ioctl_num,    /* number and param for ioctl */
7 N( b9 x$ C  f7 m' P" a+ F         unsigned long ioctl_param)
7 }8 x7 U2 S) |: e- N+ O, V% {# ]0 X: D: F( v
ioctl_num表示IO控制的类型,可以为IOCTL_SET_MSG、IOCTL_SET_DISP_WAIT、IOCTL_GET_MSG、IOCTL_GET_NTH_BYTE等等,这里仅仅举个例子。& `2 \. q$ r5 n* d2 ^

2 ?9 a$ T& h$ Dioctl_param参数表示传入的参数。% p5 `% r, m" K* g8 v& U1 Q4 ?; V. b

  J/ d; N/ f" A, c& F这里有一个比较重要的概念就是ioctl命令号。
/ l- P9 O7 b5 R; h% C
$ G6 ~" t0 ~. K2 qioctl命令号" @; z: W/ S) u& S
ioctl命令号是这个函数中最重要的参数,它描述的ioctl要处理的命令。linux中使用一个32位的数据来编码ioctl命令,它包含四个部分:dir,type,nr,size
2 L+ g" }, \9 W: F$ Z6 q' t& i3 O5 m
dir:
2 t$ \$ r( D$ W  C: e3 ]' \  S代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,OU),_IOC_WRITE(向设备写数据)或_IOC_READ(从设备读数据)或者他们的逻辑组合,当然这里只有_IOC_WRITE和_IOC_READ的组合才有意义。0 L- d- B+ o1 q1 Y7 j3 ~# V

7 R& |4 a" I1 Z- t6 G5 `type:
1 Q6 ^, u) L! I0 f3 L描述ioctl命令的类型,8bit。每种设备或系统都可以指定自己的一个类型号,ioctl用这个类型来表示ioctl命令所属的设备或驱动。
7 ^! x9 a" U" r) B& J% q. u' |* w7 m
nr9 x/ `7 q- w* r, f7 ?- |
ioctl命令序号,一般8bit,对于一个指定的设备驱动,可以对他的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。0 d/ J8 Y; m5 k/ K. P/ q

: u& R2 k% p& \* f4 A5 u7 Hsize
2 b) Z8 A+ I8 L4 G$ ^) R* kioctl命令的参数大小,一般为14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是建议你指定这个数据成员,通过它可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。" S! [2 T4 G% ?

8 L* t% Y% K! d0 p对于自己写的驱动,如果需要使用特定的ioctl命令,则必须创建ioctl命令以及编号。内核中给出了创建ioctl命令的宏。  S( x" `& p7 Q. n5 r* f; `/ t! z% Z
" s+ b, V- {4 K% @2 L6 w
/* used to create numbers */
" z2 W2 `$ b4 t9 h#define _IO(type,nr)            _IOC(_IOC_NONE,(type),(nr),0)
, B2 j0 [3 O+ L8 A7 h) e8 T- i#define _IOR(type,nr,size)      _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
) M) [" ~" h4 r: I#define _IOW(type,nr,size)      _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
# c9 W2 x; W  x  |7 b9 I( l#define _IOWR(type,nr,size)     _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))): z6 h: N$ }  v+ E; T/ L
#define _IOR_BAD(type,nr,size)  _IOC(_IOC_READ,(type),(nr),sizeof(size)), O5 l3 Z. b6 L, u# e
#define _IOW_BAD(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
* O2 j2 c  u2 Y4 e# W: ]& a#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
# B0 L0 S$ g4 b" f* P* Z  |' a( `; c9 w, _' {
总结:上述是基本的字符驱动编写基本知识,针对具体的字符驱动,涉及到具体硬件的一些设置,里面的读写函数以及设置函数都有很多区别,而且大部分都要用到延迟,因为所有的串行硬件设备都是工作在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 11:37 , Processed in 0.171875 second(s), 23 queries , Gzip On.

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

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

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