本文为一个简单的字符设备驱动,涉及驱动编写、测试程序编写、Makefile编写、驱动加载/卸载,运行于Linux虚拟机,不涉及底层配置。撰写本文的主要目的为记录一下驱动的开发流程,参考了正点原子的驱动开发指南。
驱动代码
创建文件夹 1_chrdevbase/ ,下属 APP/ 与 Driver/ 两个文件夹,前者放测试程序,后者放驱动代码。
在 Driver/ 下创建 chrdevbase.c,驱动代码如下
/*
* file name : chrdevbase.c
* description : 一个简单的字符设备demo
* author : 今朝无言
*/
#include<linux/types.h>
#include<linux/kernel.h>
#include<linux/delay.h>
#include<linux/ide.h>
#include<linux/init.h> //引入module_init()以及module_exit()
#include<linux/module.h> //与module相关的宏
MODULE_LICENSE("GPL");
MODULE_AUTHOR("今朝无言");
#define CHRDEVBASE_MAJOR 200 //主设备号,可通过 cat /proc/devices 查看所有设备及其主设备号
#define CHRDEVBASE_NAME "chrdevbase" //设备名
static char readbuf[100]; //读缓冲区
static char writebuf[100]; //写缓冲区
static char kerneldata[] = {"kernel data!"}; //内核数据,用于传递给测试APP,进行读取测试
/*
* description : 打开设备
* @param - inode : 传递给设备的inode
* @param - filp : 设备文件
* @return : 0 success;other failed
*/
static int chrdevbase_open(struct inode *inode, struct file *filp){
printk("chrdevbase open!\n");
return 0;
}
/*
* description : 从设备读取数据
* @param - filp : 设备文件
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对文件首地址的偏移
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){
int retvalue = 0;
memcpy(readbuf, kerneldata, sizeof(kerneldata));
retvalue = copy_to_user(buf, readbuf, cnt);
if(retvalue == 0){
printk("kernel send data ok!\n");
}
else {
printk("kernel send data failed!\n");
}
return 0;
}
/*
* description : 向设备写数据
* @param - filp : 设备文件
* @param - buf : 要写入设备的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对文件首地址的偏移
*/
static ssize_t chrdevbase_write(struct file *filp, const char *buf, size_t cnt, loff_t *offt){
int retvalue = copy_from_user(writebuf, buf, cnt);
retvalue = copy_from_user(writebuf, buf, cnt);
if(retvalue == 0){
printk("kernel receive data: %s \n",writebuf);
}
else {
printk("kernel receive data failed!\n");
}
return 0;
}
/*
* description : 关闭设备
* @param - filp : 设备文件描述符
* @return : 0 success;other failed
*/
static int chrdevbase_release(struct inode *inode, struct file *filp){
printk("chrdevbase release! \n");
return 0;
}
/*
* chrdevbase的file_operations结构体
* file_operations的定义见Kernel/include/linux/fs.h
* 注意函数定义一定要相同,否则报`initialization from incompatible pointer type [-Werror=incompatible-pointer-types]`错
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release
};
/*
* description : 驱动入口函数
*/
static int __init chrdevbase_init(void){
//若函数没有参数,要加void,否则报`function declaration isn’t a prototype [-Werror=strict-prototypes]`错
int retvalue = 0;
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
if(retvalue < 0){
printk("chrdevbase driver register failed!\n");
}
else {
printk("chrdevbase driver register success!\n");
}
return 0;
}
/*
* description : 驱动出口函数
*/
static void __exit chrdevbase_exit(void){
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase exit!\n");
return;
}
//指定驱动入口和出口函数
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
驱动代码Makefile
在 Driver/ 下创建 Makefile,内容如下
KERNELDIR := /lib/modules/4.15.0-189-generic/build
#本机编译就/lib/modules/`uname -r`/build
#交叉编译就使用对应的Kernel源码目录
CURRENT_PATH := $(shell pwd)
#要生成的模块名
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
测试程序代码
在 APP/ 下创建 chrdevbaseAPP.c,代码如下
/*
* file name : chrdevbaseAPP.c
* description : chedevbase驱动的测试程序
* author : 今朝无言
*/
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
static char usrdata[] = {"usr data!"}; //用户数据,用于传递给驱动,进行写入测试
// 用法:./chrdevbaseAPP /dev/chrdevbase arg
int main(int argc, char *argv[]){
int fd, retvalue;
char *filename;
char readbuf[100], writebuf[100];
//检查参数
if(argc != 3){
printf("Error Usage!\n");
return -1;
}
filename = argv[1];
//打开驱动文件
fd = open(filename, O_RDWR);
if(fd < 0){
printf("Can't open file %s!\n", filename);
return -2;
}
//arg=1,从驱动文件读取数据
if(atoi(argv[2]) == 1){
retvalue = read(fd, readbuf, 50);
if(retvalue < 0){
printf("read file %s failed!\n", filename);
}
else {
printf("read data: %s\n", readbuf);
}
}
//arg=2,向驱动写数据
if(atoi(argv[2]) == 2){
memcpy(writebuf, usrdata, sizeof(usrdata));
retvalue = write(fd, writebuf, 50);
if(retvalue < 0){
printf("write file %s failed!\n", filename);
}
else {
printf("write file success!\n");
}
}
//关闭设备
retvalue = close(fd);
if(retvalue < 0){
printf("Can't close file %s!\n", filename);
return -3;
}
return 0;
}
测试程序Makefile
在 APP/ 下创建 Makefile,内容如下
build:
gcc chrdevbaseAPP.c -o chrdevbaseAPP
clean:
rm chrdevbaseAPP
编译测试程序
编译驱动
驱动加载
使用 insmod 命令加载刚刚生成的驱动模块
sudo insmod chrdevbase.ko
执行
cat /proc/devices
查看驱动,如下图,可以看到驱动已经加载
创建设备节点文件
使用 mkmod 命令创建驱动节点
sudo mknod /dev/chrdevbase c 200 0
则创建字符设备文件/dev/chrdevbase,对该文件进行读写操作即可使用驱动,其中 ‘c’ 表示字符设备,200为主设备号,0为次设备号。
测试
进入 APP/ 文件夹,执行
sudo ./chrdevbaseAPP /dev/chrdevbase 1
进行设备读取测试,结果如下
可以看到用户接收到了从内核传递来的数据 ‘kernel data’ 。
执行
sudo ./chrdevbaseAPP /dev/chrdevbase 2
进行设备写入测试,结果如下
查看最后6条日志消息:
dmesg | tail -6
其中前三条是前面进行读取测试的日志输出,后三条是进行写入测试的日志输出,可以看到内核接收到了用户发送来的数据 ‘usr data’ 。
驱动卸载
使用 rmmod 命令卸载驱动:
sudo rmmod chrdevbase.ko
再使用 cat /proc/devices 查看,将发现 chrdevbase 设备已被卸载。
加载/卸载模块时的日志如下:文章来源:https://uudwc.com/A/zrDO
文章来源地址https://uudwc.com/A/zrDO