Linux驱动开发

  本文为一个简单的字符设备驱动,涉及驱动编写、测试程序编写、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 设备已被卸载。
 
  加载/卸载模块时的日志如下:
 
 
posted @ 2023-06-13 09:03  阿风小子  阅读(169)  评论(0编辑  收藏  举报