理论分析在上一篇日志中写完了。在这篇写了简单的字符驱动程序,交叉编译后在开发板OK6410上运行。
建立char_simple.c程序:(程序中的printk中最好是用英文,不然在minicom中显示乱码)
下面程序中只要是包含char_simple*是自己取的名字,可以随意改。
#include<linux/module.h>//模块
#include<linux/init.h>//初始化和清除
#include<linux/errno.h>//错误
#include<linux/fs.h>//文件系统file_operations
#include<asm/uaccess.h>//copy_to_user,copy_from_user
#include<linux/kernel.h>//printk
#include<linux/device.h>//创建动态设备文件device,class
#include<linux/cdev.h>//cdev结构
#include<linux/types.h>//sizeof
MODULE_LICENSE("GPL");//必须的不然编译成功但装载模块时会出错
//声明,当然,你要是先实现这些函数就不用声明了
static int char_simple_open(struct inode *innode,struct file *filp);//后面有具体实现
static int char_simple_release(struct inode *inode,struct file *filp);
static ssize_t char_simple_read(struct file *filp,char __user *buf,size_t count,loff_t *f_pos);
static ssize_t char_simple_write(struct file *filp,const char __user *buf,size_t count,loff_t *f_pos);
//定义变量
char char_simple_buffer[1024]; //read和write时用到,内核空间
dev_t dev; //alloc_chrdev_region时用到,包含设备号
struct cdev cdev; //cdev_init时用到
struct class *char_simple_class;//class_creat时用到
//file_operations 数据结构,必须放到static int char_simple_init(void)前面,因为cdev_init会用到
//由于没有定义llseek,所以在后面的测试时,write后read其偏移量会出现问题
struct file_operations char_simple_fops=
{
.owner=THIS_MODULE,
.read=char_simple_read,//可以随便修改名字,但要与后面对应起来
.write=char_simple_write,
.open=char_simple_open,
.release=char_simple_release,
};
//模块初始化加载函数
static int char_simple_init(void)//也可以static int __int char_simple_int(void)
{
int result;
/*动态分配设备号*/
result=alloc_chrdev_region(&dev,0,1,"char_simple");
if(result<0){//检查是否分配成功
printk(KERN_INFO"alloc_chrdev_region error!\n");
return -ENODEV;
}
printk(KERN_INFO"major:%d,minor:%d",MAJOR(dev),MINOR(dev));//看看设备号是多少
/*cdev初始化*/
cdev_init(&cdev,&char_simple_fops);
cdev.owner=THIS_MODULE;
cdev.ops=&char_simple_fops;
/*添加cdev到内核*/
result=cdev_add(&cdev,dev,1);//成功返回0,失败-1
if(result){//检测是否添加成功,等同于if(result!=0)
unregister_chrdev_region(dev,1);//不成功直接取消设备号,这个狠。
printk(KERN_INFO"cdev_add error:error no:%d\n",result);
return -EINVAL;
}
/*动态分配节点,也就是设备文件*/
char_simple_class=class_create(THIS_MODULE,"char_simple");
if(!char_simple_class){//判断创建类是否成功,等同于if(char_simple_class==0)
printk(KERN_INFO"class_creat error\n");
unregister_chrdev_region(dev,1);
unregister_chrdev(MAJOR(dev),"char_simple");//这是老方法时用的卸载,可以注释掉
cdev_del(&cdev);//这是对应上面cdev_add用的卸载
return -EINVAL;
}
device_create(char_simple_class,NULL,dev,NULL,"char_simple");
return 0;
}
//模块卸载函数
static void char_simple_exit(void)
{
unregister_chrdev_region(dev,1);//移除设备号
device_destroy(char_simple_class,dev);//移除设备文件
class_destroy(char_simple_class);//移除类
cdev_del(&cdev);//从内核中取消注册
unregister_chrdev(MAJOR(dev),"char_simple");//用register_chardev在内核注册时用这个删除,是老方法,不建议用了。卸载时会多出提示rmmod: module 'char_simple' not found。但实际已经卸载掉了。卸载后用lsmod查看到底有没有卸载掉。
}
//声明初始化和卸载
module_init(char_simple_init);
module_exit(char_simple_exit);
/*到现在为止就完成了上一篇日志中第一、二步*/
//open实现
static int char_simple_open(struct inode *innode,struct file *filp)
{
/*可以直接return 0,其它不用写,此文件打开操作永远成功,但系统不会通知驱动程序。还可以用container_of宏来将包含cdev的dev存在file数据结构中的private_data中,这样当有多个设备时,指定各自对应的cdev,本例中没有采用这种方式。如果采用,则在后面read,write中,可以不使用char char_simple_buffer[1024],而是用结构dev中的dev->data来指定; 我还没试过怎么用啊,不过这种方法感觉挺好的*/
printk(KERN_INFO"call open\n");//注意KERN_INFO,这样在测试时会提示“call open”
//MOD_INC_USE_COUNT;//2.4老版本内核,增加模块使用计数,防止模块在使用时被卸载掉
try_module_get(THIS_MODULE);//新方法,貌似现在系统自己计算了,也没什么用
return 0;
}
//release实现
static int char_simple_release(struct inode *inode,struct file *filp)
{
printk(KERN_INFO"call release\n");//KERN_INFO级别具体查看printk吧
//MOD_DEC_USE_COUNT;
module_put(THIS_MODULE);//同上
return 0;
}
//read实现,copy_to_user
/*
这里很难懂啊,也可能是我个人觉得,我自己理下:
在用户环境中查看man 2 read :ssize_t read(int fd, void *buf, size_t count),注意和下面比较好懂点。
驱动中,自己定义的char_simple_buffer[1024],是在内核空间,貌似可以用kmalloc申请动态空间。
copy_to_user(to,from,n),to是到用户空间,也就是buf;from是从内核空间,也就是char_simple_buffer
f_pos是在结构体file中的,当前这个设备文件的读/写位置。从此处开始读/写
内核空间: &&&&&&&&&&
文件偏移量: &&&&&& * &&& (*处开始读写) *可以超过,空洞文件
count如果是: &&&&&&,就会超出两个&
*/
static ssize_t char_simple_read(struct file *filp,char __user *buf,size_t count,loff_t *f_pos)
{
printk(KERN_INFO"call read\n");
if(*f_pos>=sizeof(char_simple_buffer)){//如果偏移量大于等于内核空间容量,溢出,但是为什么会溢出呢
printk(KERN_INFO"read buffer overflow\n");//我在想一开始现有个内核空间,read/write使偏移量增加
*f_pos=sizeof(char_simple_buffer);//然后我释放掉这个空间,再建一个空间,而f_pos不变,就溢出了
return 0; //自己瞎想的,不知道???
}
if((count+*f_pos)>sizeof(char_simple_buffer)){//count是传入的数据长度,参考上面&&描述
count=sizeof(char_simple_buffer)-*f_pos;
}
if(copy_to_user(buf,&char_simple_buffer[*f_pos],count)){//copy成功返回0
printk(KERN_INFO"copy_to_user error\n");//不成功返回没copy成功的字节数
return -EFAULT; //这里直接不成功出错
}else{
*f_pos=*f_pos+count; //记录read后的文件偏移量
printk(KERN_INFO"read:%dbytes\n",count);
}
return count;//注意return值和open的比较下
}
//write实现,copy_from_user,同read解释
static ssize_t char_simple_write(struct file *filp,const char __user *buf,size_t count,loff_t *f_pos)
{
printk(KERN_INFO"call write\n");
if(*f_pos>=sizeof(char_simple_buffer)){//*f_pos是用户文件位置
printk(KERN_INFO"write buffer overflow\n");
*f_pos=sizeof(char_simple_buffer);
return 0;
}
if((count+*f_pos)>sizeof(char_simple_buffer)){//count是传入的数据长度
count=sizeof(char_simple_buffer)-*f_pos;
}
if(copy_from_user(&char_simple_buffer[*f_pos],buf,count)){//同read
printk(KERN_INFO"copy_from_user error\n");
return -EFAULT;
}else{
*f_pos=*f_pos+count;
printk(KERN_INFO"write:%dbytes\n",count);
}
return count;//注意这个
}
--------------------------------------------------------------------------------------------------------------
好了,以上就写完了。然后写Makefile文件,参考上上上篇helloworld模块的日志,改下里面的名字就好了
然后make下,生成char_simple.ko文件,传到OK6410开发板。insmod装载,lsmod查看,rmmod卸载。
下面来测试一下,是否可以。这个测试程序write和read用同一个buff,会有些冲突,懒得改了,下一篇改。
测试程序char_simple_test.c
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd,size; //fd文件描述符
char buff[128];//是用户空间,相当于驱动程序中的buf
if((fd=open("/dev/char_simple",2))==-1){//char_simple要根据你自己的名字来定,在驱动中下面定义的
printf("open error!\n");//在device_create(char_simple_class,NULL,dev,NULL,"char_simple");
exit(1);
}
if((size=read(fd,buff,strlen(buff)))<0){//read第一次
printf("read error!\n");
exit(1);
}
printf("read:%dbytes\n",size);//在驱动中printk(KERN_INFO"read:%dbytes\n",count);所以在这可以不写
printf("R:%s\n",buff);
printf("please input:\n");//write同read
scanf("%s",&buff); //随便输入几个字符
if((size=write(fd,buff,strlen(buff)))!=strlen(buff)){
printf("write error!\n");
exit(1);
}
printf("W:%s\n",buff);
/*close(fd);
if((fd=open("/dev/char_simple",2))==-1){
printf("open error!\n");
exit(1);
}*/ //重新打开,重置偏移量,没成功
if((size=read(fd,buff,strlen(buff)))<0){//read第二次
printf("read error!\n");
exit(1);
}
printf("read:%dbytes\n",size);
printf("R:%s\n",buff);
close(fd);//关闭文件
exit(0);
}
----------------------------------------------------------------------------------------
交叉编译arm-linux-gcc 下。将生成的char_simple_test传入开发板中运行。可以看结果了。
可以编译成功,但由于f_ops文件偏移量没法处理,write后接着read时,read会读write后的字符。
当然了,这个测试程序可以在PClinux测试,新建test.txt,当作设备文件。open时加入O_TRUNC打开,或者lseek,或者close后再open来实现字符偏移量的重置。
----------------------------------------------------------------------------------------
终于完成了,当然上面的解释并不一定准确,还需多多学习。
还是应该试验下container_of,dev结构什么的。还有吐槽下LDD3里面的量子集什么的看不懂啊,也没完整程序,整这么复杂,郁闷。
还是要多写下啊,不然不参考书一句都写不出来。