© 2006 Embedded
Linux 设备驱动程序设计
Linux设备驱动程序设计
郗闽军
牛建伟
© 2006 Embedded
实验目的
• 学习Linux下进行驱动程序设计的原理
• 掌握Linux设备驱动程序开发的基本过程和设计方
法
© 2006 Embedded
实验内容
• 内核驱动设计入门-模块方式驱动程序()
• 内核驱动设计实验-触摸屏驱动()
– 写一个简单的应用程序,显示触摸位置的坐标(x,y)
• 开发一个LED(数码管)驱动程序,并编写一个
应用程序对所开发的驱动程序进行测试(大作业)
– 实验实现的功能是上电复位后,数码管显示数字0-7,
然后每一个数字依次闪烁一次,小数点也要点亮,即:
.
© 2006 Embedded
Linux的设备驱动程序
• 硬件设备与应用程序之间的一个中间软件层
• 它使得某个特定硬件能够响应一个定义良好的内
部编程接口,同时完全隐蔽了设备的工作细节
• 用户通过一组与具体设备无关的标准化的调用来
完成相应的操作
• 驱动程序的任务就是把这些标准化的系统调用映
射到具体设备对于实际硬件的特定操作上
• 驱动程序是内核的一部分,可以使用中断、DMA
等操作
• 驱动程序在用户态和内核态之间传递数据
© 2006 Embedded
设备驱动程序的分类
• 字符设备
– 所有能够象字节流一样访问的设备都通过字符设备来实现
– 它们被映射为文件系统中的节点,通常在/dev/目录下面
– 一般要包含open read write close等系统调用的实现
• 块设备
– 通常是指诸如磁盘、内存、Flash等可以容纳文件系统的存储设备。
– 块设备也是通过文件系统来访问,与字符设备的区别是:内核管理数
据的方式不同
– 它允许象字符设备一样以字节流的方式来访问,也可一次传递任意多
的字节。
• 网络接口设备
– 通常它指的是硬件设备,但有时也可能是一个软件设备(如回环接口
loopback),它们由内核中网络子系统驱动,负责发送和接收数据包。
– 它们的数据传送往往不是面向流的,因此很难将它们映射到一个文件
系统的节点上。
© 2006 Embedded
基本概念
• 主设备号和次设备号
– 主设备号和次设备号能够唯一地标识一个设备
• 128(以前), 256(以后)
– 主设备号相同的设备使用相同的驱动程序,次设备号用于区分具体
设备的实例
– 动态获取主设备号
– Linux下对设备号的分配请参考Documentation/
• 设备文件
– Linux使用设备文件来统一对设备的访问接口,将设备文件放在
/dev/目录下
– 设备的命名一般为设备文件名+数字或者字母表示的子类,例如
/dev/hda1, /dev/hda2等
– Linux 以后引入了设备文件系统(devfs)的概念,所有的设备
文件作为一个可以挂装的文件系统,这样就可以被文件系统统一管
理,从而设备文件就可以挂装到任何需要的地方。一般将主设备建
立一个目录,再将具体的子设备文件建立在此目录下。例如,
/dev/mtdblock0
© 2006 Embedded
基本概念
• 驱动程序使用的2个重要结构
– struct file
– struct file_operations
© 2006 Embedded
基本概念
• struct file
© 2006 Embedded
基本概念
• struct file数据结构
– 定义位于include/
– struct file结构与驱动相关的成员
• mode_t f_mode 标识文件的读写权限
• loff_t f_pos 当前读写位置
• unsigned int_f_flag 文件标志,主要进行阻塞/非阻塞型操作时检
查
• struct file_operation * f_op 文件操作的结构指针
• void * private_data 驱动程序一般将它指向已经分配的数据
• struct dentry* f_dentry 文件对应的目录项结构
© 2006 Embedded
基本概念
• 设备驱动程序接口( struct file_operations), 标记化方法:
static struct file_operations demo_fops = {
owner: THIS_MODULE,
write: demo_write,
read: demo_read,
ioctl: demo_ioctl,
open: demo_open,
release: demo_release,
};
© 2006 Embedded
基本概念
• 设备驱动程序接口( struct file_operations )
– 通常所说的设备驱动程序接口是指struct file_operations{ },它的
定义位于include/linux/中。
– 在嵌入式系统的开发中,通常只要实现如下几个接口函数就能完
成系统所需要的功能
• init 加载驱动程序时,内核自动调用
• read 从设备中读取数据
• write 向字符设备中写数据
• ioctl 控制设备,实现除读写操作以外的其他控制命令
• open 打开设备并进行初始化
• release 关闭设备并释放资源
• exit 卸载驱动程序时,内核自动调用
© 2006 Embedded
基本概念
• 驱动程序注册过程(动态分配主设备号)
– insmod module_name ;加载驱动程序,运行init函数
(register_chrdev(dev_Major, “module_name”, * fs ))
– 查看/proc/devices
– mknod /dev/module_name c/b 主设备号 次设备号
– rmmod module_name ;卸载驱动,运行 exit函数
(unregister_chrdev(dev_Major, “module_name”, * fs ))
• 用户程序调用
– Open(“/dev/module_name”, mode) ;O_RDWR
– Ioctl()
– Write()
– Read()
– Close()
© 2006 Embedded
开发驱动程序时须注意的事项
• 中断处理
– 中断是现代微处理器的一个重要功能
– Linux驱动程序中的中断处理函数
extern int request_irq(unsigned int irq,
void(*handler)(int, void*, struct pt_regs *),
unsigned long flag, const char *dev_name,
void *dev_id); //请求为中断号irq分配中断处理函数
extern void free_irq(unsigned int, void*);//释放中断
– 注意事项
• 不能向用户空间发送或者接收数据
• 不能执行有睡眠操作的函数
• 不能调用调度函数
• 谨慎使用全局变量(可重入)
• 自旋锁的使用
© 2006 Embedded
基本概念
• 字符设备的管理
– 驱动程序模块通过函数
int register_chrdev(unsigned int major, const char
*name, struct file_operations *fops) 完成向内核的注册,
其中major是主设备号,name是设备名,fops是针对该
设备的驱动程序的接口。
– 在系统中为驱动程序模块建立一个设备节点
[minjun@RedHatAS ~]$ mknod /dev/demo c 254 0
• 其中/dev/demo标识设备名为demo,“c”说明是字
符设备,254是指定的主设备号, 0是次设备号
© 2006 Embedded
基本概念
• 驱动程序的编译
– 以为例
• Makefile的形式-参考实验指导书
• 命令行的形式
[minjun@RedHatAS ~]$ armv4l-unknown-linux-gcc -Wall -c -O
-D__KERNEL__ -I/home/minjun/embedded/kernel-2410s/include -o
• 加载驱动
– [minjun@RedHatAS ~]$ insmod
• 卸载驱动
– [minjun@RedHatAS ~]$ rmmod
© 2006 Embedded
基本概念
• 测试程序实例
//
#include<>
#include<>
#include<>
int main() {
int fd;
fd=open("/dev/demo", O_RDWR);
if(fd < 0) {
exit(fd);
}
//your code here
read(fd, buffer, size);
write(fd, buffer, size);
......
close(fd);
return 0;
}
© 2006 Embedded
驱动程序的实现-驱动程序框架
#include <linux/>
#include <linux/>
#include <linux/>
#include <linux/> /* printk() */
#include <linux/> /* everything... */
#include <linux/> /* error codes */
#include <linux/> /* size_t */
#include <linux/>
#include <linux/> /* O_ACCMODE */
#include <linux/> /* COPY_TO_USER */
#include <asm/> /* cli(), *_flags */
#define DEVICE_NAME "demo"
#define demo_MAJOR 250
#define demo_MINOR 0
static ssize_t demo_write(struct file *filp,const char *buffer, size_t count) {
copy_from_user(drv_buf , buffer, count);
WRI_LENGTH = count;
printk("user write data to driver\n");
//your code here
return count;
}
© 2006 Embedded
驱动程序的实现-驱动程序框架
static ssize_t demo_read(struct file *filp, char *buffer, size_t count, loff_t *ppos) {
if(count > MAX_BUF_LEN)
count=MAX_BUF_LEN;
copy_to_user(buffer, drv_buf,count);
printk("user read data from driver\n");
return count;
}
static int demo_ioctl(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg) {
printk("ioctl runing\n");
switch(cmd){
case 1:printk("runing command 1 \n");break;
case 2:printk("runing command 2 \n");break;
default:
printk("error cmd number\n");break;
}
return 0;
}
static int demo_open(struct inode *inode, struct file *file) {
MOD_INC_USE_COUNT;
sprintf(drv_buf,"device open sucess!\n");
printk("device open sucess!\n");
return 0;
}
© 2006 Embedded
驱动程序的实现-驱动程序框架
static int demo_release(struct inode *inode, struct file *filp) {
MOD_DEC_USE_COUNT;
printk("device release\n");
return 0;
}
static struct file_operations demo_fops = {
owner: THIS_MODULE,
write: demo_write,
read: demo_read,
ioctl: demo_ioctl,
open: demo_open,
release: demo_release,
};
static int __init demo_init(void) {
SET_MODULE_OWNER(&demo_fops);
result = register_chrdev(demo_MAJOR, "demo", &demo_fops);
if (result < 0) return result;
printk(DEVICE_NAME " initialized\n");
return 0;
}
static void __exit demo_exit(void) {
unregister_chrdev(demo_MAJOR, "demo");
printk(DEVICE_NAME " unloaded\n");
}
module_init(demo_init);
module_exit(demo_exit);
© 2006 Embedded
驱动程序的实现-驱动程序框架
• open
– 提供给驱动程序初始化设备的能力,为后续的操作做准备
– 此外一般会递增使用计数,防止文件关闭前模块被卸载
– 通常情况下,open完成如下工作:
• 递增使用计数
• 检查特定设备错误
• 如果设备是首次打开,则对其进行初始化
• 识别次设备号,如有必要,则修改f_op指针
• 分配并填写filp->private_data中的数据
• release
– 与open正好相反
• 释放由open分配的filp->private_data中的数据
• 在最后一次关闭操作时关闭设备
• 使用计数减一
© 2006 Embedded
驱动程序的实现-驱动程序框架
• read和write
– read将数据从内核拷贝到应用程序空间,write则将数据从应用程
序空间拷贝到内核。
– 由于用户空间和内核空间的内存映射方式不同,所以在内核和用
户空间传输数据的时候需要使用如下的函数
• unsigned long copy_to_user(void *to, const void* from, unsigned long count);
• unsigned long copy_from_user(void *to, const void *from, unsigned long count);
– 在阻塞型IO中,read和write调用可能会出现阻塞
• read调用当前无数据可读,而又没有数据马上可读,这时会睡眠并且
等待,write调用也会出现这样的情况
• 等待队列机制-wait_queue_head_t;(定义在<linux/>中)
• 如果声明了等待队列并完成初始化,进程就可以睡眠,可以调用
sleep_on的不同变体来完成睡眠(函数声明位于<linux/>中)
• 大多数情况下应使用“可中断”的函数,如interruptible_sleep_on。
• 睡眠进程被唤醒并不一定代表有数据,也有可能是被其他的信号唤醒,
所以醒来后需要测试condition.
© 2006 Embedded
8段LED显示器外形原理图
一个数码管(LED)由一
个8位的字节控制
当该位为“1”时点亮,
为“0”时灭
a
c
b
d
e
f
h
g
© 2006 Embedded
LED连接原理图
© 2006 Embedded
LED显示驱动器
• ZLG7290 I2C LED/键盘 驱动器
– I2C 串行接口提供键盘中断信号方便
与处理器接口
– 可驱动8 位共阴数码管或64 只独立
LED 和64 个按键
– 可控扫描位数可控任一数码管闪烁
– 提供数据译码和循环移位段寻址等控
制
– 8 个功能键可检测任一键的连击次数
– 无需外接元件即直接驱LED 可扩展
驱动电流和驱动电压
• 详细资料参加课程网站上的:
–
–
图1 ZLG7290引脚图
© 2006 Embedded
LED显示驱动器功能框图
• IIC基地址是0x70
• 有24个8位寄存器
(0x0-0x17),通过
IIC总线访问,必须是
字节操作
• 主要寄存器
SystemReg 0x0
FlashOnOff 0x0c
ScanNum 0x0d
DpRam0~7 0x10-0x17
CmdBuf0~1 0x7-0x8
• 两种控制方式:
-寄存器映象控制
-命令解释控制
• 参考
© 2006 Embedded
LED驱动程序的主要函数
• static int led_write(struct file* filp, const char* buffer, size_t count,
loff_t* ppos) {…}
• static int led_ioctl(struct inode *inode, struct file*file, unsigned int
cmd, unsigned long arg) {…}
• static int led_open(struct inode* inode, struct file* filp) {…}
• static int led_release(struct inode* inode, struct file* filp) {…}
• static int __init led_init(void) {…}
• static void __exit led_exit(void) {…}
• static struct file_operations led_fops = {
owner: THIS_MODULE,
open: led_open,
release: led_release,
write: led_write,
ioctl: led_ioctl,
};
• 可以使用IIC_Write(char baseAddr, char offSet, char data)来设置寄
存器的值(字节操作),该函数在文件中定义。
© 2006 Embedded
LED测试应用程序框架
#include <>
#include <>
#include <>
#include <>
#include <sys/>
int main() {
int fd;
int ret;
if((fd=open("/dev/led", O_RDWR)) < 0) {
printf("open led device failed!\n");
exit(fd);
}
ioctl(int fd, int cmd,…);
write(int fd, char* buf, int length);
close(fd);
return 0;
}
© 2006 Embedded
实验内容
• 内核驱动设计入门-模块方式驱动程序()
– 在PC linux和开发平台上运行通过
• 内核驱动设计实验-触摸屏驱动()
– 写一个简单的应用程序,显示触摸位置的坐标(x,y)
– 在开发平台上装载驱动程序,运行编写的测试应用程序
• 开发一个LED(数码管)驱动程序,并编写一个
应用程序对所开发的驱动程序进行测试(如果做不
完,可以在下下次实验接着做)
– 开发
– 开发
© 2006 Embedded
实验步骤-demo驱动程序
• 阅读和理解源代码
– 进入/arm2410s/exp/drivers/demo, 阅读理解源代码
• 编译驱动模块
– 修改Makefile文件, 编译实验平台运行的驱动程序
• 测试驱动程序
– 装载驱动程序和建立设备文件节点
• insmod
• [ ]mknod /dev/demo c 254 0
– 编译测试程序并运行
• [ ]armv4l-unknown-linux-gcc –o test_demo
• ./test_demo
© 2006 Embedded
实验步骤-触摸屏驱动
• 测试驱动程序
– 驱动程序位于内核源代码目录下面的drivers/char/下面,
– 名称:
• 装载驱动程序和建立设备文件节点
– 在将触摸屏驱动加载到内核中时,可以看到系统为触摸屏设备分配
的设备号
• [ ] insmod
– 如下图所示:分配的主设备号为253
• [ ] mknod /dev/ts c 253 0
© 2006 Embedded
实验步骤-触摸屏驱动
• 编写应用程序需要用到的数据结构TS_RET
当调用read( )函数从触摸屏驱动中读取数据,读出TS_RET类型的数据
结构定义如下:
typedef struct {
unsigned short pressure;
unsigned short x;
unsigned short y;
unsigned short pad;
} TS_RET;
TS_RET ret;
read(fd, (char*)&ret, sizeof(ret)
• 坐标转换的参考公式
– 从驱动里面读取的坐标值并不是在0~640和0~480的范围内,需要进行
一下坐标转换
– x = (-240)*320/(1728-240);
– y = (-1869)*240/(1869-211)+240;
© 2006 Embedded
• LED实验
– 在完成前两个实验后,开始编写LED的驱动程序
– 用于在指定位置显示数字的复合指令
–其中A3~A0 为显示缓存编号范围为0000B~0111B 对应DpRam0
~DpRam7 无效的编号不会产生任何作用;DP=1 时点亮该位小数点
,Flash=1 时该位闪烁显示,Flash=0 时该位正常显示,D4~D0 为要显示的
数据
– 译码规则按ZLG7290芯片资料的表述进行,如下表所示
© 2006 Embedded
© 2006 Embedded
开始实验