罗升阳博客里是典型的字符型设备的驱动实现,但是我打算实现一个misc设备驱动。misc设备本质上还是一个字符设备,主设备号为10,但是它实现起来比较简单。
我们驱动要实现的功能是,用户空间调用设备的write函数写入一个int类型的值,然后再调用read函数把它读出来。功能简单,但这一来一往足以让我们了解驱动的写法。
驱动编写
驱动是内核层面的事情,所以我们先进入到内核源码的drivers目录,新建一个hello文件夹用来装驱动的代码。
cd /kernel/goldfish/drivers
mkdir hello
cd hello
然后在此文件夹里新建hello.c文件,驱动代码就写在这个文件里。
首先定义设备名称常量和用来接收数据的变量
// 设备名称
#define DEVICE_NAME "hello"
//用来操作的数据
static int value = -1;
然后进行函数声明和设备操作函数表file_operations的填充
/*声明要实现的设备操作函数 */
static int hello_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos);
static int hello_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos);
/*设备文件操作函数表*/
static struct file_operations hello_fops =
{
.owner = THIS_MODULE,
.read = hello_read,
.write = hello_write,
};
填充miscdevice结构体
static struct miscdevice misc =
{
.name = DEVICE_NAME,
.minor = MISC_DYNAMIC_MINOR,
.fops = &hello_fops
};
给这个设备取名为hello,让Linux为这个设备动态分配次设备号,并且指定它的函数表为刚刚写好的hello_fops。
接下来实现声明的read和write函数
// 读取
static ssize_t hello_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos)
{
ssize_t err = -1;
if(copy_to_user(buf, &value, sizeof(value))) {
err = -EFAULT;
goto out;
}
out:
return err;
}
// 写入
static ssize_t hello_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos) {
ssize_t err = -1;
if(copy_from_user(&value, buf, count)) {
err = -EFAULT;
goto out;
}
out:
return err;
}
在Linux中内核空间和用户空间的内存是不能随便互相访问的,但是copy_to_user和copy_from_user提供了内核和用户传输数据的通道。
copy_to_user的函数原型如下
unsigned long copy_to_user(void *to, const void *from, unsigned long n);
to:目标地址(用户空间)
from:源地址(内核空间)
n:将要拷贝数据的字节数
copy_from_user的函数原型如下
unsigned long copy_from_user(void *to, const void *from, unsigned long n);
to:目标地址(内核空间)
from:源地址(用户空间)
n:将要拷贝数据的字节数
明白了copy_to_user和copy_from_user函数的功能,以及参数的意义。hello_read和hello_write的实现也就像Hello World一样简单了。
模块初始化和卸载方法
static int hello_init(void)
{
int ret;
ret = misc_register(&misc);
return ret;
}
static void hello_exit(void)
{
misc_register(&misc);
}
//注册驱动初始化函数
module_init(hello_init);
//注册驱动卸载函数
module_exit(hello_exit);
在初始化函数中使用misc_register函数注册misc设备,然后注册了模块初始化和卸载函数。
最后给这个驱动签上自己的名字,就写完了。
MODULE_AUTHOR("windcake");
MODULE_DESCRIPTION("windcake driver learn");
MODULE_LICENSE("GPL");
准备编译
在hello文件夹里新建Kconfig文件,写上如下内容
config HELLO
tristate "Hello Android Driver"
default y
help
This is the hello android driver.
新建Makefile文件,写上如下内容。
obj-$(CONFIG_HELLO) += hello.o
在drivers目录下的Kconfig文件里添加一行
source "drivers/hello/Kconfig"
Makefile文件里添加一行
obj-$(CONFIG_HELLO) += hello/
然后就可以重新编译内核,并用新编译的内核启动模拟器了。
测试
测试程序源码如下
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#define DEVICE_NAME "/dev/hello"
int main(int argc, char** argv)
{
int fd = -1;
int val = 0;
fd = open(DEVICE_NAME, O_RDWR);
read(fd, &val, sizeof(val));
printf("Original value:%d.\n", val);
val = 1;
printf("Write value %d to %s.\n", val, DEVICE_NAME);
write(fd, &val, sizeof(val));
read(fd, &val, sizeof(val));
printf("Read the value again:%d.\n", val);
close(fd);
return 0;
}
首先应该明确的是,这个测试函数是运行在用户空间的。它先打开了hello设备,并调用read方法,最终调用到内核的hello_read方法,把内核中value的值
拷贝给了用户空间的val,然后将它打印出来。再往下,改变了用户空间val的值,通过write方法并把它拷贝给了内核空间的value,紧接着又读出来。
新建Android.mk文件,写上如下内容。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE := hello
LOCAL_SRC_FILES := $(call all-subdir-c-files)
include $(BUILD_EXECUTABLE)
将这两个文件放在hello文件夹里,然后把hello文件夹放到aosp下的external文件夹下。
在aosp目录下执行
mmm ./external/hello
看到如下输出,说明编译成功
Install: out/target/product/generic/system/bin/hello
make: Leaving directory '/home/windcake/Documents/aosp'
#### make completed successfully (3 seconds) ####
最后把编译出来的hello可执行文件push到模拟器上的system/bin路径下执行。
adb remount
adb push '/aosp/out/target/product/generic/system/bin/hello' system/bin
adb shell
cd system/bin
./hello
如果一切正确,会看到如下输出
Original value:
-1.
Write value 5 to /dev/hello.
Read the value again:
5.