一、开发环境说明
我使用的是ubuntu 20.04桌面系统,在vmare虚机中运行。系统中已经安装gcc编译器:
sudo apt-get install gcc
如果系统无法上网,需要事先下载好gcc压缩包,传入系统中,然后解压。
二、实例
本实例大部分参照博客Linux设备驱动程序——一个简单的字符设备驱动程序_Alex-wu的博客-优快云博客
原博客有关键的操作步骤没有讲到,我这里补充完整。
1、写驱动代码
char.c源代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#define MAX_SIZE 0x1000
#define MEM_CLEAR 0x1
static struct class *class;
static struct device *dev;
struct chardev_dev //定义设备
{
struct cdev cdev;
unsigned char mem[MAX_SIZE];
};
struct chardev_dev *devp;
dev_t devno; //设备号
static int chardev_open(struct inode *inode, struct file *filp) //open函数
{
filp->private_data = devp;
return 0;
}
static int chardev_release(struct inode *inode, struct file *filp) //release函数
{
return 0;
}
static long chardev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) //ioctl函数
{
struct chardev_dev *dev = filp->private_data;
switch (cmd)
{
case MEM_CLEAR:
memset(dev->mem, 0, MAX_SIZE);
printk(KERN_INFO "chardev is set to zero\n");
break;
default:
return -EINVAL;
}
return 0;
}
static ssize_t chardev_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) //read函数
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct chardev_dev *dev = filp->private_data;
if (p >= MAX_SIZE)
return -EINVAL;
if (count > MAX_SIZE - p)
count = MAX_SIZE - p;
if (copy_to_user(buf, dev->mem + p, count)) //从内核空间得到数据
ret = -EINVAL;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "read %u byte(s) from %lu\n", count, p);
}
return ret;
}
static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t size, //write函数
loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct chardev_dev *dev = filp->private_data;
if (p >= MAX_SIZE)
return -EINVAL;
if (count > MAX_SIZE - p)
count = MAX_SIZE - p;
if (copy_from_user(dev->mem + p, buf, count)) //从用户空间得到写入的数据
ret = -EINVAL;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "write %u byte(s) from %lu\n", count, p);
}
return ret;
}
static loff_t chardev_llseek(struct file *filp, loff_t offset, int orig) //llseek函数
{
loff_t ret = 0;
switch (orig) //判断文件指针的位置,确定从何开始读写
{
case 0:
if (offset < 0)
{
ret = -EINVAL;
break;
}
if (offset > MAX_SIZE)
{
ret = -EINVAL;
break;
}
filp->f_pos = offset;
ret = filp->f_pos;
break;
case 1:
if ((filp->f_pos + offset) < 0 )
{
ret = -EINVAL;
break;
}
if ((filp->f_pos + offset) > MAX_SIZE)
{
ret = -EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = -EINVAL;
break;
}
return ret;
}
static const struct file_operations chardev_fops = //字符设备操作函数定义
{
.owner = THIS_MODULE,
.llseek = chardev_llseek,
.read = chardev_read,
.write = chardev_write,
.unlocked_ioctl = chardev_ioctl,
.open = chardev_open,
.release = chardev_release,
};
static char *chardev_devnode(struct device *dev, umode_t *mode)
{
if (mode)
*mode = 0666;
return NULL;
}
static int __init chardev_init(void) //初始化,入口函数
{
int ret;
int err;
ret = alloc_chrdev_region(&devno, 0, 1, "chardev"); //动态申请设备号
if (ret < 0)
return ret;
devp = kzalloc(sizeof(struct chardev_dev), GFP_KERNEL); //分配内存空间
if (!devp)
{
ret = -ENOMEM;
goto fail_malloc;
}
class = class_create(NULL, "chardev"); //创建类节点
if (IS_ERR(class))
{
ret = PTR_ERR(class);
printk(KERN_ERR "class create error %d\n", ret);
goto fail_malloc;
}
class->devnode = chardev_devnode; //创建设备节点
dev = device_create(class, NULL, devno, NULL, "chardev");
if (IS_ERR(class))
{
ret = PTR_ERR(dev);
printk(KERN_ERR "device create error %d\n", ret);
goto bad_device_create;
}
cdev_init(&devp->cdev, &chardev_fops); //绑定操作函数的结构体
devp->cdev.owner = THIS_MODULE;
err = cdev_add(&devp->cdev, devno, 1); //调用cdev_add函数将cdev结构体注册到内核
if (err)
printk(KERN_NOTICE "Error %d adding chardev", err);
return 0;
bad_device_create:
class_destroy(class);
fail_malloc:
unregister_chrdev_region(devno, 1);
return ret;
}
static void __exit chardev_exit(void) //注销
{
device_destroy(class, devno);
class_destroy(class);
cdev_del(&devp->cdev);
kfree(devp);
unregister_chrdev_region(devno, 1);
}
module_init(chardev_init); //加载模块
module_exit(chardev_exit); //退出模块
MODULE_AUTHOR("lql");
MODULE_LICENSE("GPL");
驱动文件的makefile,注意这里是Makefile,不是makefile,文件首字母要大写。因为在执行make时系统自动找Makefile文件,而不是makefile文件。也许可以通过什么配置解决这个问题。
obj-m := char.o #确定文件名
modules:
$(MAKE) -C /lib/modules/5.13.0-30-generic/build M=/home/wang modules
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
Makefile文件放在/home/wang目录下,char.c放在/lib/modules/5.13.0-30-generic/build目录下。
说明:不用纠结5.13.0-30,你的环境中在/lib/modules/下面只有一个叫***-generic/build的路径。
当前路径是/home/wang,敲命令:make
不出意外生成文件char.ko.
将该文件拷贝到/lib/modules/5.13.0-30-generic/。这里的5.13.0-30版本号和makefile中的版本号一致。
进入上述的驱动模块路径: cd /lib/modules/5.13.0-30-generic/
敲 depmod, 产生模块依赖的映射文件
敲 insmod char.ko 加载此驱动
敲 mknod /dev/chardev 创建/dev/chardev设备节点。
如果执行报错 可以试试mknod /dev/chardev c 50 1,或者mknod /dev/chardev c,其中c表示是字符驱动,50是主设备号,1是从设备号。因为本实例自动获取设备号,所以加不加无所谓。
然后ls /dev/ 看看在dev路径下是否生成了chardev文件。
说明:mknod 执行一次创建设备节点就行了,以后卸载(rmmod char.ko)再加载(insmod),不用执行mknod ,加载时自动创建相同的设备节点。
2、写应用代码并测试
这个驱动文件就像是一个文件的fd一样,我们可以往里写数据再读数据。
char_write.c
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
int main(int argc, char *argv[])
{
int fd;
char msg[100];
fd= open(argv[1],O_RDWR,S_IRUSR|S_IWUSR);
if(fd!=-1)
{
while(1)
{
printf("Please input the globar:(input quit go out)\n");
scanf("%s",msg);
if(strcmp(msg,"quit")==0)
{
close(fd);
break;
}
write(fd,msg,strlen(msg));
}
}
else
{
printf("device open failure\n");
}
return 0;
}
编译:在源文件当前目录下执行:gcc char_write.c -o write.out
这里是纯系统环境,没有硬件。如果是硬件,可以使用arm的交叉编译器。
当前目录下会生成文件write.out
char_read.c
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
int main(int argc, char *argv[])
{
int fd,i;
char msg[101];
fd= open(argv[1],O_RDWR,S_IRUSR|S_IWUSR);
if(fd!=-1)
{
for(i=0;i<101;i++)
msg[i]='\0';
read(fd,msg,100);
printf("%s\n",msg);
}
else
{
printf("device open failure,%d\n",fd);
}
return 0;
}
编译:在源文件当前目录下执行:gcc char_read.c -o read.out
测试:
当前目录敲 ./write.out /dev/chardev 运行写程序。往驱动文件中写一串字符进去,敲quit退出程序。
root@ubuntu:/home/wang# ./write.out /dev/chardev
Please input the globar:(input quit go out)
123
Please input the globar:(input quit go out)
quit
root@ubuntu:/home/wang#
然后敲 ./read.out /dev/chardev 打印输出。
root@ubuntu:/home/wang# ./read.out
123
root@ubuntu:/home/wang#
至此结束。
具体代码见个人百度网盘路径:我的网盘/驱动开发练习/简单的字符设备linux开发/