LINUX内核和设备驱动编程

更新时间:2023-09-11 21:19:01 阅读量: 教育文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

实验三 内核和设备驱动编程

一 、实验目的

1、学习Linux操作系统下内核程序的编写和应用 2、学习可编程接口芯片的编程控制方法 3、了解驱动程序的结构 4、了解驱动程序常用结构体 5、了解驱动程序常用函数 二、实验原理

1 关于设备驱动

驱动程序是一组代码,这部分代码负责将应用程序的一些需求,如读、写等操作,正确无误的传递给相关的硬件,并使硬件能够做出正确反应的代码。驱动程序像是一个黑盒子,它隐藏了硬件的工作细节,应用程序只需要通过一组标准化的接口,就可以实现对硬件的操作。 设备驱动程序的作用在于提供机制,即解决提供什么功能的问题,而如何使用这些功能则交给用户程序处理。 设备驱动程序是操作系统内核和机器硬件之间的接口,它为应用程序屏蔽硬件的细节,一般来说,Linux的设备驱动程序需要完成如下功能: (1)初始化设备;

(2)提供各类设备服务;

(3)负责内核和设备之间的数据交换;

(4)检测和处理设备工作过程中出现的错误。

更为方便的是,Linux下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使得Linux下的设备操作犹如文件一般。在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。这些函数就是open ()、close ()、read ()、write () 等。

Linux主要将设备分为二类:字符设备和块设备(当然网络设备及USB等其它设备的驱动编写方法又稍有不同)。这两类设备的不同点在于:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,而块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备主要针对磁盘等慢速设备。本实验做的是字符设备的驱动编写。 2、Linux设备驱动程序分类

Linux设备驱动程序在Linux的内核源代码中占有很大的比例,源代码的长度日益增加,主要是驱动程序的增加。虽然Linux内核的不断升级,但驱动程序的结构还是相对稳定。

驱动程序基本框架如下 (1)初始化设备模块 (2)设备打开模块

(3)数据读写和控制模块

(4)中断处理模块(有的驱动程序没有) (5)设备释放,卸载模块

以上的各个模块基本上都有各自专门的函数,后面会具体介绍

3、内核模块

Linux以模块的形式加载设备类型,通常来说一个模块对应一个设备驱动,这样便于多个设备的协调工作也利于应用程序的开发和扩展。 设备驱动在准备好以后可以编译到内核中,在系统启动时和内核一起启动,这种方法在嵌入式Linux系统中经常被采用。但是通常情况下设备驱动的动态加载更为普遍,这使得开发人员不必在调试过程中频繁启动机器就能完成设备驱动的开发工作,本实验就是动态加载模块。 设备驱动在加载时首先调用入口函数init_module(),该函数完成设备驱动的初始化工作,比如寄存器置位、结构体赋值等一系列工作,其中最重要的一个工作就是向内核注册该设备,对于字符设备调用register_chrdev()完成注册,对于块设备需要调用register_blkdev()完成注册。注册成功后,该设备获得了系统分配的主设备号、自定义的次设备号,并建立起于文件系统的关联。设备在卸载时需要回收相应的资源,令设备的响应寄存器复位并从系统中注销该设备,字符设备调用unregister_chrdev()、块设备调用unregister_blkdev()。系统调用部分则是对设备的操作过程,比如open、read、write、ioctl等。图3-1为一个设备驱动模块动态挂载、卸载和系统调用的全过程。

用户空间内核空间模块insmodInit_module()register_XXX系统调用设备驱动内核模块rmmodClearup_module()unregister_XXX 图3—1 内核模块使用过程简图 4、编程中用到的函数

(1)int init_module()和void cleanup_module(void)

一个模块至少要包含这两个函数,装载模块时首先调用init_module(),在这里完成设备驱动的初始化工作,比如寄存器置位、结构体赋值等一系列动作,当然内核设备的注册也在这里完成。

卸载模块时会调用 cleanup_module(void),如果没有它,加载的模块就没法卸载了,这样累积下来会使得内核变得臃肿。 (2)register_chrdev()和unregister_chrdev()

对于字符设备,注册该设备用到的函数就是register_chrdev(),例如 ret=register_chrdev(MAJOR,NAME,&fops);

其中,参数MAJOR为主设备号, NAME为设备名,fops为包含基本函数入口点的结构体,类型为file_operations。

同样的,当注销该设备时,程序会调用unregister_chrdev(),例如

unregister_chrdev(MAJOR,NAME,&fops);

当模块被卸载时会调用cleanup_module(void),这里面就有unregister_chrdev()这个函数,参数同上

(3) open()函数,对设备特殊文件进行open()系统调用时,将调用驱动程序的open () 函数,int open(struct inode * inode ,struct file * file); 其中参数inode为设备特殊文件的inode (索引结点) 结构的指针,参数file是指向这一设备的文件结构的指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性、控制使用设备的进程数、根据执行情况返回状态码(0表示成功,负数表示存在错误) 等;

(4)read()函数 当对设备特殊文件进行read() 系统调用时,将调用驱动程序read()数,void read(struct inode * inode ,struct file * file ,char * buf ,int count) ;

参数buf是指向用户空间缓冲区的指针,由用户进程给出,count 为用户进程要求读取的字节数,也由用户给出。read() 函数的功能就是从硬设备或内核内存中读取或复制count个字节到buf 指定的缓冲区中。在复制数据时要注意,驱动程序运行在内核中,而buf指定的缓冲区在用户内存区中,是不能直接在内

核中访问使用的,因此,必须使用特殊的复制函数来完成复制工作。 (5) write( ) 函数 当设备特殊文件进行write () 系统调用时,将调用驱动程序的write () 函数,它的形式,参数和read()一样,write ()的功能是将参数buf 指定的缓冲区中的count 个字节内容复制到硬件或内核内存中 (6)release()函数 当最后一个打开设备的用户进程执行close ()系统调用时,内核将调用驱动程序的release () 函数:void release (struct inode * inode ,struct file * file) ; release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。

(7) ioctl() 函数 该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:int ioctl (struct inode * inode ,struct file * file ,unsigned int cmd ,unsigned long arg);

参数cmd为设备驱动程序要执行的命令的代码,由用户自定义,参数arg 为 相应的命令提供参数,类型可以是整型、指针等。

(8)一个重要结构体struct file_operations,例如程序中用到的 static struct file_operations fops={ .open= tyue_open, .ioctl= tyue_ioctl, .release= tyue_close};

程序中不管是open还是read,ioctl等函数都会有个指针类型参数指向这个

结构体,这里会定义各种具体用于操作的函数,基本上看看这里就知道这个驱动程序可以干些什么 (9)创建设备的命令

在注册设备之前必须先创建这个设备,创建设备时会用到设备的主从设备号,当然最好同时指定其权限。应用程序通过设备文件系统(devfs)的名字(或节点)访问硬件设备,所有的设备节点在/dev目录下。利用mknod命令生成设备文件系统的节点,但只有超级用户才能生成设备文件。主设备号用于内核区分设备驱动,次设备号用于设备驱动区分设备。一个设备驱动可能控制多个设备。新的设备驱动要有新的主设备号。比如我创建了一个设备 mknod /dev/fei c 250 0 -m 666

如上我就在/dev/目录下创建了一个名为fei的字符设备,主设备号为250,次设备号为0,权限为666,可以读写,系统中本来已经有了一些设备,因此有些设备号已经被注册过了,为了避免冲突可以键入命令 # more /proc/devices

显示已经存在的设备及其主设备号等信息,如果我的注册成功,也可以在里面看到的

三、实验过程

1、 实验最初是从加载hello world模块开始的,这是一个很简单的模块,只有最基本的初始化模块和卸载模块函数,程序如下 #include int init_module( void ) {

printk(\ return 0; }

void cleanup_module( void ) {

printk(\}

make后生成hello.ko,然后 #/sbin/insmod hello.ko

加载模块,然后在系统日志/var/messages/里输出“hello,world”,在我电脑上我有root权限,可以打开系统日志,实验室里我没打开,但是可以用命令 #dmesg

这样可以看到输出的结果,然后 #lsmod

可以看到我刚刚加载进去的模块hello.ko,键入命令 #/sbin/rmmod hello.ko

卸载模块,可以看到Goodbye world的输出,我记得在实验室这些做得很顺利,在我电脑上加载可以,也有输出结果,但是卸载时说moudle is in use,不给我卸载,我也没用模块啊

这个程序本身是没用的,没有实现任何功能,只是让我们熟悉一下模块的加载卸载罢了,为下面8253的驱动编写做些准备

2、可编程计数器8253驱动程序的编写 【1】8253简介

8253可编程定时/计数器是Intel公司生产的通用外围芯片之一,有3个独立的十六位计数器,技术频率范围为0~2MHz,它所有的技术方式和操作方式都通过编程控制。

1. 8253有六种工作方式: 方式0:计数结束中断 方式1:可编程频率发生器 方式2:频率发生器 方式3:方波频率发生器 方式4:软件触发的选通信号 方式5:硬件触发的选通信号 2.8253端口地址

寄存器 端口地址 0#计数器 40H 1#计数器 41H 2#计数器 42H 控制寄存器 43H 8253引脚图 早期的个人计算机中,有一片可编程的定时器/计数器8253,作为系统的硬件时钟设备。8253在系统中占用40H~43H端口。三个定时器/计数器的时钟输入均为1.19MHz,各自承担以下功能:

1. T/C0,系统的日时钟,初始化为工作方式三,计数初值为0,输出接往可编程中断控制器8259A的IR0,作为系统的计时中断信号。

2. T/C1,动态存储器刷新时钟,初始化为工作方式二,计数初值为12H。 3. T/C2,控制系统的扬声器,产生声音信号。它的控制端GATE2和扬声器前均接有控制信号。这些控制信号来自可编程I/O接口芯片8255的PB0和PB1。8255初始化已将B口设为方式0输出。

个人计算机为可编程并行接口8255A分配端口60H~63H,初始化后A口作为键盘输入端口,B口用于一些控制信号输出。

目前的计算机中,尽管以上各个独立的功能芯片都已经不存在,但系统集成化后的功能依旧保留,结构原理和编程控制方法也几乎完全一样。话是这么说,但是我的笔记本电脑上就没有这些机制,没有8253.但是台式机的配置都是标准的统一的,都会有8253这些东西。

注意到方式三是方波发生器,如果能让8253产生适当频率的方波,并且能够通过驱动打开扬声器,就可以演奏音乐了,顺着这个思路,结合前面驱动程序编写的知识,着手编写放音的驱动程序和对应的用户程序

【2】驱动程序的编写

驱动程序大概要做这些事情

(1)init_module()和 cleanup_module这两部分肯定是要有的,这个就不多说了 (2)一开始我没意识到这问题,看别人程序里有这部分,偷来了,就是为了防止同时打开两个相同的设备,设置变量static int Device_Open=0。open后,Device_Open加1。若再次打开,则检测到Device_Open不为0,返回忙碌值,

如果close,则有Device_Open减1,这两个函数在file_operations里可以看到。 (3)剩下的都可以用ioctl来完成了,因为下面要完成的是初始化操作,往8253的2号计数器里写计数初值以完成一定频率方波的产生,控制8255以控制扬声器的开关,为此分别由下面的ioctl函数完成

static int fei_ioctl(struct inode *inode,struct file *file,unsigned int cmd,unsigned long arg) {

unsigned int freq=0;

switch(cmd) //cmd 来自用户程序,根据用户程

{ // 序的需要,驱动会对不同的i/o口操作

case INIT: //如果给的是INIT,就是对8253初始化

break; // 初值,工作方式3 (方波发生器),二进制。

case START: //打开扬声器

outb_p(0x03,0x61); break;

case DAT: //往2号计数器写计数初值以产生特定频率方波 freq=(unsigned int)arg;

outb_p((char)(freq&0x00ff),0x42); outb_p((char)(freq>>8),0x42); break;

case STOP: //关掉扬声器 outb_p(0x00,0x61); break; default:

printk(\ }

return 0; }

【3】以上就是设备驱动程序的主体思想和核心代码部分(完整代码后附),现在编写对应的用户程序来实现放音,先从最简单的放单一声音开始,我们给计数器的数值当然不是声音频率值本身,计数器时钟输入为1.19M,比如音符1的高音为523HZ,因此计数初值应该为1.19M/523=2275=0x08e3,同理完成音符1~7的低音,中音,高音计数初值的转换,按照这个顺序,写到一个数组freq []里面 freq[]={0x237b,0x1f9f,0x1c2c,0x1a90,0x17b7,0x1521,0x12d1,0x11be,0x0fd0,0x0e16,0x0d52,0x0bdc,0x0a91,0x0969,0x08e3,0x07e8,0x070d,0x06a8,0x05ed,0x0548,0x0485};

main函数可以这样写 int main() {

fd=open(\ //打开设备,前面用mknod创建的 ioctl(fd,SINIT,0); //先初始化操作 int yuepu[]={1}; //播放音符1

outb_p(0xb6,0x43); //0x43为8253控制端口。10110110B,计数器2,16位

ioctl(fd,DAT,freq[14]); ioctl(fd,START,0); //打开扬声器,8255控制的 sleep(1); // 放音时间为1秒 ioctl(fd,STOP,0); //关闭扬声器,8255干的事 }

加上头文件后一个简单的放音用户程序就完成了,然后把驱动程序make一下,生成driver.ko,加载如内核,然后 #gcc –o fei fei.c

生成文件名为fei的可执行程序 #./fei

运行后听到一声响,顺利的做完这些后接着来写连续的放音,这些可以通过循环来做,只要在上面的用户程序上改就可以了,驱动程序是完全不用动的,这就是为什么说驱动提供能做什么,而你想怎么做完全由用户程序解决,其实上面用了一个yuepu[ ]的数组就是为下面连续放音做准备的,我们可以约定,比如 用数字1~7 表示音符1~7的高音 用数字8~14 表示音符1~7的中音 用数字15~21 表示音符1~7的低音 在用户程序里修改如下 int main() {

fd=open(\ //打开设备,前面用mknod创建的 ioctl(fd,SINIT,0); //先初始化操作

int yuepu[ ]={1,2,5,7,20,5,6,9,14,12,2}; //待播放的音符 int i;

for(i=0;i< sizeof(yuepu),i++) {

switch(yuepu[i]) {

case 1: ioctl(fd,DATA,freq[14]);break; //每来一个音,选择对应频率 case 2: ioctl(fd,DATA,freq[15]);break; case 3: ioctl(fd,DATA,freq[16]);break; case 4: ioctl(fd,DATA,freq[17]);break; case 5: ioctl(fd,DATA,freq[18]);break; case 6: ioctl(fd,DATA,freq[19]);break; case 7: ioctl(fd,DATA,freq[20]);break; ???

???

default : break; }

ioctl(fd,START,0); //打开扬声器,8255控制的 sleep(1); // 放音时间为1秒 ioctl(fd,STOP,0); //关闭扬声器,8255干的事 sleep(0.1) ; //两个音之间间隔0.1秒 }

} 【4】最后做了一下用键盘模拟琴键,一开始想的是用getchar()函数,读入键入的数据,然后switch一下,实现弹琴的功能,的确可以实现,只是每次按下一个键想让它立即响的话必须按下回车键,这样肯定不行,后来看到说8255初始化后的A口是键盘的输入口,就想到通过读这个口的数据实时的播放键入的音符,等到我下次去的时候,记得是周四,我知道了这样一个命令 #stty cbreak

这个命令使得程序不用等你按下回车确认,就可以放音,其实我之前的想法就是要编写这个命令的,这下省事了,很快完成模拟键盘的用户程序编写,最后附上的用户程序就是模拟键盘程序,这里简要说说实现原理 前面那些就不说了,直接进main了 Int main() { ?? Int i; char b;

b= getchar();

for(i=0;i<10000;i++) {

switch(b) { case 'a': ioctl(fd,DATA,freq[0]);break; case 's': ioctl(fd,DATA,freq[1]);break; case 'd': ioctl(fd,DATA,freq[2]);break; case 'f': ioctl(fd,DATA,freq[3]);break; case 'g': ioctl(fd,DATA,freq[4]);break; case 'h': ioctl(fd,DATA,freq[5]);break; case 'j': ioctl(fd,DATA,freq[6]);break; case 'q': ioctl(fd,DATA,freq[7]);break; case 'w': ioctl(fd,DATA,freq[8]);break; case 'e': ioctl(fd,DATA,freq[9]);break; case 'r': ioctl(fd,DATA,freq[10]);break; case 't': ioctl(fd,DATA,freq[11]);break; case 'y': ioctl(fd,DATA,freq[12]);break; case 'u': ioctl(fd,DATA,freq[13]);break; case '1': ioctl(fd,DATA,freq[14]);break; case '2': ioctl(fd,DATA,freq[15]);break; case '3': ioctl(fd,DATA,freq[16]);break; case '4': ioctl(fd,DATA,freq[17]);break; case '5': ioctl(fd,DATA,freq[18]);break; case '6': ioctl(fd,DATA,freq[19]);break; case '7': ioctl(fd,DATA,freq[20]);break; default : break; }

????

}

就是把想要演奏的音符由数组给定变成键盘输入了,注意输入的应该都是字符型的数据,一开始就是这个没在意出了点问题,还好很快改好了

再次编译用户程序,键入 #stty cbreak #./fei

然后就可以弹琴了,还是蛮好玩的。 最后附上驱动程序和用户程序 #include #include #include #include #include #include #include

#define TIMERIOMAGIC 250 #define INIT _IOW(TIMERIOMAGIC,0,int) #define DAT _IOW(TIMERIOMAGIC,1,int) #define START _IOW(TIMERIOMAGIC,2,int) #define STOP _IOW(TIMERIOMAGIC,3,int) #define MAJOR 240 #define NAME \

static int tyue_open(struct inode *,struct file *);

static int tyue_ioctl(struct inode *,struct file *,unsigned int,unsigned long); static int tyue_close(struct inode *,struct file *); static int Device_open=0;

static struct file_operations fops={ .open= tyue_open, .ioctl= tyue_ioctl, .release= tyue_close, };

int init_module() {int ret; ret=register_chrdev(MAJOR,NAME,&fops); return 0; }

void cleanup_module(void) {

unregister_chrdev(TIMER_MAJOR,TIMER_NAME); }

static int tyue_ioctl(struct inode *inode,struct file *file,unsigned int cmd,unsigned long arg)

{ unsigned int freq=0; switch(cmd) {

case INIT:

outb_p(0xb6,0x43); break; case START:

outb_p(0x03,0x61); break; case DAT:

freq=(unsigned int)arg;

outb_p((char)(freq&0x00ff),0x42); outb_p((char)(freq>>8),0x42); break; case STOP:

outb_p(0x00,0x61); break; default:

printk(\ }

return 0; }

static int tyue_open(struct inode *inode,struct file *file) { if(Device_open) return -EBUSY; Device_open++; return 0; }

static int tyue_close(struct inode *inode,struct file *file) { Device_open--; outb_p(0,0x61); return 0; }

弹琴的用户程序 #include #include #include #include #include

#define INIT _IOW(TIMERIOMAGIC,0,int) #define DAT _IOW(TIMERIOMAGIC,1,int) #define START _IOW(TIMERIOMAGIC,2,int)

#define STOP _IOW(TIMERIOMAGIC,3,int)

freq[]={0x237b,0x1f9f,0x1c2c,0x1a90,0x17b7,0x1521,0x12d1,0x11be,0x0fd0,0x0e16,0x0d52,0x0bdc,0x0a91,0x0969,0x08e3,0x07e8,0x070d,0x06a8,0x05ed,0x0548,0x0485}; int main() {

fd=open(\打开设备,前面用mknod创建的 ioctl(fd,SINIT,0); //先初始化操作 int i;

for(i=0;i<10000;i++) {

char b;

b = getchar(); switch(b) {

case 'a': ioctl(fd,DATA,freq[0]);break; case 's': ioctl(fd,DATA,freq[1]);break; case 'd': ioctl(fd,DATA,freq[2]);break; case 'f': ioctl(fd,DATA,freq[3]);break; case 'g': ioctl(fd,DATA,freq[4]);break; case 'h': ioctl(fd,DATA,freq[5]);break; case 'j': ioctl(fd,DATA,freq[6]);break; case 'q': ioctl(fd,DATA,freq[7]);break; case 'w': ioctl(fd,DATA,freq[8]);break; case 'e': ioctl(fd,DATA,freq[9]);break; case 'r': ioctl(fd,DATA,freq[10]);break; case 't': ioctl(fd,DATA,freq[11]);break; case 'y': ioctl(fd,DATA,freq[12]);break; case 'u': ioctl(fd,DATA,freq[13]);break; case '1': ioctl(fd,DATA,freq[14]);break; case '2': ioctl(fd,DATA,freq[15]);break; case '3': ioctl(fd,DATA,freq[16]);break; case '4': ioctl(fd,DATA,freq[17]);break; case '5': ioctl(fd,DATA,freq[18]);break; case '6': ioctl(fd,DATA,freq[19]);break; case '7': ioctl(fd,DATA,freq[20]);break; default : break; }

ioctl(fd,START,0); sleep(1);

ioctl(fd,STOP,0);

sleep(0.1); } }

#define STOP _IOW(TIMERIOMAGIC,3,int)

freq[]={0x237b,0x1f9f,0x1c2c,0x1a90,0x17b7,0x1521,0x12d1,0x11be,0x0fd0,0x0e16,0x0d52,0x0bdc,0x0a91,0x0969,0x08e3,0x07e8,0x070d,0x06a8,0x05ed,0x0548,0x0485}; int main() {

fd=open(\打开设备,前面用mknod创建的 ioctl(fd,SINIT,0); //先初始化操作 int i;

for(i=0;i<10000;i++) {

char b;

b = getchar(); switch(b) {

case 'a': ioctl(fd,DATA,freq[0]);break; case 's': ioctl(fd,DATA,freq[1]);break; case 'd': ioctl(fd,DATA,freq[2]);break; case 'f': ioctl(fd,DATA,freq[3]);break; case 'g': ioctl(fd,DATA,freq[4]);break; case 'h': ioctl(fd,DATA,freq[5]);break; case 'j': ioctl(fd,DATA,freq[6]);break; case 'q': ioctl(fd,DATA,freq[7]);break; case 'w': ioctl(fd,DATA,freq[8]);break; case 'e': ioctl(fd,DATA,freq[9]);break; case 'r': ioctl(fd,DATA,freq[10]);break; case 't': ioctl(fd,DATA,freq[11]);break; case 'y': ioctl(fd,DATA,freq[12]);break; case 'u': ioctl(fd,DATA,freq[13]);break; case '1': ioctl(fd,DATA,freq[14]);break; case '2': ioctl(fd,DATA,freq[15]);break; case '3': ioctl(fd,DATA,freq[16]);break; case '4': ioctl(fd,DATA,freq[17]);break; case '5': ioctl(fd,DATA,freq[18]);break; case '6': ioctl(fd,DATA,freq[19]);break; case '7': ioctl(fd,DATA,freq[20]);break; default : break; }

ioctl(fd,START,0); sleep(1);

ioctl(fd,STOP,0);

sleep(0.1); } }

本文来源:https://www.bwwdw.com/article/4x1h.html

Top