Ubuntu11.04下测试并口驱动程序过程--看经典驱动书LDD实践。

来源:互联网 发布:portraiture mac 编辑:程序博客网 时间:2024/05/21 12:44

一、前言

        一直想多学习一下Linux,总得找一个方向吧。书太多,也看过许许多多的Linux书,代码太多,看过比较多的代码,可是为啥一直在入门ing,从未进入过。。。。。。好吧,最近找完工作,时间也蛮多,于是想系统的学习一下Linux下的驱动程序设计。一是为了学习,二是为了实验室实际项目需要。所以我买了传说中的经典书籍LDD<<Linux设备驱动程序>>,之前早已听闻这本书。

        花了大概大半个月时间把这本书的前十二章看完了,于是想自己动手写写驱动程序,来实际验证究竟是不是像书上讲的那样写,就能在Linux下实现驱动程序控制设备的功能。于是找了书中的例子,拿并口驱动程序做实验。此文主要想记录我实验的详细过程和实验过程中遇到的问题和解决办法。

         以下是我的实验环境:

          1、一台带并口的PC机:研华工业计算机,酷睿CPU

          2、ubuntu11.04,安装盘原装内核2.6.38.8。

         

二、实验过程

1、确保并口是功能完好的。

       我刚开始没想到先验证并口是否可用,于是就先按照书上讲的方法先写了一个简单的驱动程序测试并口。结果当然是并口并不能受我的驱动程序控制,于是就很难定位问题出在哪里,很可能是驱动程序问题吧?因为我第一次写驱动,于是一遍一遍的查找是不是程序有问题。。。。。最后发现我找的第一台电脑的并口就是坏的,然后才找到研华工控机的并口是好的。

      检查方法我试过二种,第一种是在windows系统下,上网上找并口测试程序,然后用万用表去量并口电压是否受能按设定改变。第二种是在Ubuntu下用原装系统自带的并口驱动程序提供的接口写用户空间测试程序,照样是用万用表去量并口管脚,主要参照http://mockmoon-cybernetics.ch/computer/linux/programming/parport.html。以下是我亲自在我平台上测试通过的:

//Access via raw IO (not recommended)在用户空间获得权限直接访问并口I/O端口,只在X86平台支持。#include <stdio.h>#include <sys/io.h>#define BASEADDR 0x378int main(int argc, char **argv){int i;if(ioperm(BASEADDR, 4, 1) < 0) {fprintf(stderr, "could not get i/o permission. are you root ?\n");return 5;}for(i=0; i < 256; i++) {outb(i, BASEADDR);}ioperm(BASEADDR, 4, 0);return 0;} 
//这是利用UBUNTU自带的并口驱动ppdev和并口设备/dev/parport0(ubuntu已建好)进行并口测试。//lsmod可以看见lp,ppdev parport_pc parport等并口驱动,cat /dev/ioports能看见并口端口被占的情况#include<stdio.h>#include<fcntl.h>#include<sys/types.h>#include<sys/stat.h>#include<asm/ioctl.h>#include<linux/parport.h>#include<linux/ppdev.h>#define DEVICE "/dev/parport0"int main(){struct ppdev_frob_struct frob;int fd;int mode;char data = 0xFF;fd = open(DEVICE, O_RDWR);if(fd < 0){printf("open error:\n");return -1;}if(ioctl(fd, PPCLAIM))        //这里也可以是其它一些测试代码,按照上面网站教程所说。{perror("PPCLAIM");close(fd);return -1;}ioctl(fd, PPWDATA, &data );ioctl(fd, PPRELEASE);close(fd);return 0;}

2、按照LDD书或者网上各种资料写自己的并口驱动程序,简单的主要完成控制并口电压,驱动程序通过ioctl或者write方法都行。
      主要的步骤是:
      在module_init(init_function)的初始化函数init_function()中注册一个字符设备cdev,动态静态注册都行。我是用register_chardev()静态注册。如果注册成功,再用request_region(0x378, 8, DEV_NAME)去申请独占并口端口。这个表示这一并口的I/O端口寄存器是从0x378-0x37F的8个连续地址,可以在BIOS设置基址。
       然后就是实现ioctl了或者write方法了,ioctl用copy_from_user()将用户空间数据拷贝到内核或者直接用arg参数都行。write()方法必须用copy_from_user()将需要写的数据从用户空间拷贝到内核。然后调用outb(value, 0x378),就能设置0x378地址的I/O端口了。以下是我参照网上一网友的程序:
#include <linux/init.h>#include <linux/module.h>#include <linux/kernel.h>#include <linux/fs.h>#include <linux/errno.h>#include <linux/types.h>#include <linux/fcntl.h>#include <asm/uaccess.h>#include <asm/io.h>#define DEV_NAME "phoenix_port"#define DEV_MAJOR 223#define PORT_WRITE_ADDR 0X378#define PORT_READ_ADDR 0X379 int phoenix_open(struct inode *inode,struct file *filp){return 0;}int phoenix_release(struct inode *inode,struct file *filp){return 0;}ssize_t phoenix_read(struct file *filp, char *buf, size_t count, loff_t *f_pos){//this fun read one char,not the count you passunsigned char status;//inb is a function that can read a byte form addrstatus=inb(PORT_READ_ADDR);//put the byte to usr bufput_user(status, (char *)buf);return 1;}ssize_t phoenix_write(struct file *filp, char *buf, size_t count, loff_t *fpos){//write one char ,no matter how many you want to write;//get param from usr to kernelunsigned char status;get_user(status, (char*)buf);outb(status, PORT_WRITE_ADDR);printk(KERN_ALERT "write io value:%x",status);return 1;} struct file_operations oper_struct={.owner=THIS_MODULE,.open=phoenix_open,.release=phoenix_release,.read=phoenix_read,.write=phoenix_write,};int phoenix_init(void){int tmp;tmp=register_chrdev(DEV_MAJOR, DEV_NAME, &oper_struct);if(tmp<0){        printk(KERN_ALERT "module_init error !\n");        return tmp;}if(request_region(0x378, 8, DEV_NAME)){printk(KERN_ALERT "get io port region");}else{printk(KERN_ALERT "can not get io port region");}return 0;}void phoenix_exit(void){release_region(0x378, 8);unregister_chrdev(DEV_MAJOR,DEV_NAME);}module_init(phoenix_init);module_exit(phoenix_exit);MODULE_LICENSE("Dual BSD/GPL");
       这段程序是看不出有问题的,然后编译通过生成port.ko文件,用sudo insmod port.ko载入我们写的驱动模块。然后用命令sudo mknod /dev/myport c 223 0生成一个和并口驱动对应的设备文件,其中223必须和驱动一致,次设备号应该是0-255任一个都行,主设备号也可以通过cat /proc/devices查看加载驱动对应的主设备号。系统通过主设备号才能将设备文件和驱动中定义的方法联起来,具体内核怎么实现的,还有待研究,我只知道在主次设备号是设备文件的inode结点的一部分,然后这个inode节点会传给驱动程序的write(),read()方法,在驱动程序中可以通过imajor(inode),iminor(inode)读出设备文件的主次设备号,通常只读次设备号来区分用同一驱动程序的几个类似功能的设备文件,在驱动程序中实现不同的方法。
上述过程可能遇到的问题有:
       1、虚拟控制台看不到内核printk输出信息,用dmesg可以看到。或者切换到真实控制台tty下(非界面UBUNTU)也可以看到。
       2、insmod port.ko会出现request_region()失败,是因为UBUNTU自带的并口驱动已经把端口占了,所以需要sudo rmmod lp ppde parport_pc相关的驱动,然后再insmod port.ko就行了。
       然后就可以写用户空间测试程序了,我参照网上,修改成如下的测试程序:
#include <stdio.h>#include <sys/types.h>#include <sys/ioctl.h>#include <fcntl.h>#include <sys/stat.h>#include <unistd.h>#define DEV_NAME "/dev/phoenix_port" //必须和mknod建立的文件名一致int main(){//open phoenix_portint dev;dev=open(DEV_NAME,O_RDWR|O_NDELAY);if(dev<0){      printf("open dev error!\n");        return -1;}//open rightprintf("have open dev!\n");int count = 1;   char state =0x00;while(1){if(!(count % 2))//every two second{state = state ^ 0x01;printf("write state:%x",state);    write(dev, &state, 1);}if(!(count % 1))//every one second{state = state ^ 0x04;printf("write state:%x",state);write(dev, &state, 1);}count++;sleep(1);}//close devclose(dev);return 0;}
       然后gcc 编译上面的的用户空间测试程序,sudo ./out执行程序,用demsg查看内核输出信息知道驱动程序ssize_t phoenix_write(struct file *filp, char *buf, size_t count, loff_t *fpos) 已经执行,并且写入了用户空间write()调用传入的数据。至此,我已经以为驱动程序成功的时候,拿万用表去测量并口管脚,发现管脚并无任何变化。然后就是各种寻找可能的问题了。
 
3、解决步骤2中遇到的问题
      上面遇到的问题很费解了,差不多花了我一个多星期的时间各种寻找解决办法。去上网查过许多教程都是说request_region()成功后,用outb()写端口就行了问过一些驱动交流群也是如此。
       我只得改驱动程序进行各种尝试了:
       比如不用write(),而用ioctl()方法
       比如不用直接访问I/O端口,而是用remap()将端口映射到内存,X86是有独立的端口编址访问才对,所以我只是尝试。
       比如不用老式的注册设备方法register_chrdev()。而用新的 cdev_init() + cdev_add()。
       以上所有方法,均能注册设备成功,request_region()成功获得端口,用cat /proc/devices能看见设备驱动名称和主设备号, cat /proc/ioports能看见占用的端口,dmesg也能看见往端口0x386写数据了,但是结果都一样,并口没反应。
 
       最费力的方法是我去读内核的ppdev.h ppdev.c驱动文件,想看看它怎么写的。然后想把我的一部分驱动程序利用ppdev.h提供的接口,来定位我的驱动程序的问题所在。比如想把request_region替换为用ppdev.h中的if(ioctl(fd, PPCLAIM)) //方法,或者把write()替换为ppdev.h中的PPWDATA方法,来定位问题,都失败了。
 
        最后最后最后,折腾的我有点疯了。就在今天,我想起以前在哪看过一句话,大意是:写驱动开发,最好不要用LINUX发行版本带的内核,因为有的做过修改。而是要自己去下一个标准的内核源码,编译,安装,在这个标准内核源码上做开发。。。像快死的人抓住最后的救命稻草,我只得去一试,结果就刚刚的,经过换成标准内核,并口可以受驱动程序控制了。至此驱动程序勉强算是测试成功。
 
4、结论
      虽然最后是测试成功了,但是为啥Ubuntu自带的内核测试不成功,还找不到原因,很奇怪。。。。。总不能产品厂商发行驱动时候,都得要普通Ubuntu用户自己去编译内核吧。
 
5、过程中遇到的一些杂问题和解决办法。
       刚装的较老版本的UBUNTU,想用sudo apt-get install装任何软件都说找不到安装包,这时需要更新源信息。参http://www.cnblogs.com/eastson/archive/2012/08/24/2654163.html文章。即编辑/etc/apt/sources.list源文件(添加一些源,比如校内的或者搜狐之类的),再sudo apt-get update即可。好像有不用编制源文件的方法,是直接用一个什么命令。
     编译驱动的时候说file_operation  op结构中的 ioctl()方法无定义,去查找内核asm/ 下ioctl的头文件发现 3.0.0的内核中,ioctl()接口已经更名为unlocked_ioctl()和另外一种ioctl()。所以把取其中一个。

 

6、下一步计划

       用FPGA开发外围设备卡,通过PCI总线或者ISA总线挂在研华的工控机上,自己做外围电路和写驱动程序。

原创粉丝点击