1. 背景
Robot OS中我们要定制一些自己的系统服务,比如前面文章提到的MQTT长连接服务以及机器人移动控制的运动服务,有一些自定义的音频,比如麦克风阵列还涉及到驱动开发。本文介绍在基于Android9.0系统的Robot OS中开发一个最简单的驱动示例。
2. 准备工作
Robot OS使用的板子是基于瑞芯微3399 Pro处理器的开源开发板,厂家提供了开源的硬件设计和内核代码,我们下载源码压缩包解压后既可直接编译出完整系统镜像。
驱动程序的开发我们参考网上示例,实现一个最简单4字节寄存器,别人教程基于低版本的Linux内核,我们使用的是4.4版本,使用到的API上略微有些差异。
3. 定义功能
接下来我们开始编写我们的代码。在kernel/drivers
下面新建demo目录,并在demo目录下新建demo.h文件:
#ifndef _DEMO_ANDROID_H_ #define _DEMO_ANDROID_H_ #include <linux/cdev.h> #include <linux/semaphore.h> #define DEMO_DEVICE_NODE_NAME "demo" #define DEMO_DEVICE_FILE_NAME "demo" #define DEMO_DEVICE_PROC_NAME "demo" #define DEMO_DEVICE_CLASS_NAME "demo" struct demo_android_dev { int val; struct semaphore sem; struct cdev dev; }; #endif
这个头文件定义了一些字符串常量宏,此外,还定义了一个字符设备结构体demo_android_dev,这个就是我们虚拟的硬件设备了,val成员变量就代表设备里面的寄存器,它的类型为int,sem成员变量是一个信号量,是用同步访问寄存器val的,dev成员变量是一个内嵌的字符设备,这是Linux驱动程序自定义字符设备结构体的标准方法。
我们还看到用到两个头文件:
<linux/cdev.h>
:linux内核设备抽象<linux/semaphore.h>
:信号量
4. 功能实现
在demo目录中增加demo.c文件,作为驱动程序的实现部分。驱动程序的功能主要是向上层提供访问设备的寄存器的值,包括读和写。我们提供三种访问设备寄存器的方法:
- 一是通过proc文件系统来访问;
- 二是通过传统的设备文件的方法来访问;
- 三是通过devfs文件系统来访问。
首先我们包含必要的头文件和定义三种访问设备的方法:
#include <linux/init.h> #include <linux/module.h> #include <linux/types.h> #include <linux/fs.h> #include <linux/proc_fs.h> #include <linux/device.h> #include <asm/uaccess.h> #include "demo.h" /*主设备和从设备号变量*/ static int demo_major = 0; static int demo_minor = 0; /*设备类别和设备变量*/ static struct class* demo_class = NULL; static struct demo_android_dev* demo_dev = NULL; /*传统的设备文件操作方法*/ static int demo_open(struct inode* inode, struct file* filp); static int demo_release(struct inode* inode, struct file* filp); static ssize_t demo_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos); static ssize_t demo_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos); /*设备文件操作方法表*/ static struct file_operations demo_fops = { .owner = THIS_MODULE, .open = _open, .release = demo_release, .read = demo_read, .write = demo_write, }; /*访问设置属性方法*/ static ssize_t demo_val_show(struct device* dev, struct device_attribute* attr, char* buf); static ssize_t demo_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count); /*定义设备属性*/ static DEVICE_ATTR(val, S_IRUGO | S_IWUSR, demo_val_show, demo_val_store);
4.1 定义传统的设备文件访问方法
这里主要是定义demo_open、demo_release、demo_read和demo_write这四个打开、释放、读和写设备文件的方法:
/*打开设备方法*/ static int demo_open(struct inode* inode, struct file* filp) { struct demo_android_dev* dev; /*将自定义设备结构体保存在文件指针的私有数据域中,以便访问设备时拿来用*/ dev = container_of(inode->i_cdev, struct demo_android_dev, dev); filp->private_data = dev; return 0; } /*设备文件释放时调用,空实现*/ static int demo_release(struct inode* inode, struct file* filp) { return 0; } /*读取设备的寄存器val的值*/ static ssize_t demo_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos) { ssize_t err = 0; struct demo_android_dev* dev = filp->private_data; /*同步访问*/ if(down_interruptible(&(dev->sem))) { return -ERESTARTSYS; } if(count < sizeof(dev->val)) { goto out; } /*将寄存器val的值拷贝到用户提供的缓冲区*/ if(copy_to_user(buf, &(dev->val), sizeof(dev->val))) { err = -EFAULT; goto out; } err = sizeof(dev->val); out: up(&(dev->sem)); return err; } /*写设备的寄存器值val*/ static ssize_t demo_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos) { struct demo_android_dev* dev = filp->private_data; ssize_t err = 0; /*同步访问*/ if(down_interruptible(&(dev->sem))) { return -ERESTARTSYS; } if(count != sizeof(dev->val)) { goto out; } /*将用户提供的缓冲区的值写到设备寄存器去*/ if(copy_from_user(&(dev->val), buf, count)) { err = -EFAULT; goto out; } err = sizeof(dev->val); out: up(&(dev->sem)); return err; }
我们看到这里面主要用到了linux系统函数:
- container_of
- down_interruptible
- copy_from_user
- copy_from_user
4.2 定义通过devfs文件系统访问方法
这里把设备的寄存器val看成是设备的一个属性,通过读写这个属性来对设备进行访问,主要是实现demo_val_show和demo_val_store两个方法,同时定义了两个内部使用的访问val值的方法__demo_get_val
和__demo_set_val
:
/*读取寄存器val的值到缓冲区buf中,内部使用*/ static ssize_t __demo_get_val(struct demo_android_dev* dev, char* buf) { int val = 0; /*同步访问*/ if(down_interruptible(&(dev->sem))) { return -ERESTARTSYS; } val = dev->val; up(&(dev->sem)); return snprintf(buf, PAGE_SIZE, "%d\n", val); } /*把缓冲区buf的值写到设备寄存器val中去,内部使用*/ static ssize_t __demo_set_val(struct demo_android_dev* dev, const char* buf, size_t count) { int val = 0; /*将字符串转换成数字*/ val = simple_strtol(buf, NULL, 10); /*同步访问*/ if(down_interruptible(&(dev->sem))) { return -ERESTARTSYS; } dev->val = val; up(&(dev->sem)); return count; } /*读取设备属性val*/ static ssize_t demo_val_show(struct device* dev, struct device_attribute* attr, char* buf) { struct demo_android_dev* hdev = (struct demo_android_dev*)dev_get_drvdata(dev); return __demo_get_val(hdev, buf); } /*写设备属性val*/ static ssize_t demo_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count) { struct demo_android_dev* hdev = (struct demo_android_dev*)dev_get_drvdata(dev); return __demo_set_val(hdev, buf, count); }
这里我们用到了:
- up
- simple_strtol
- dev_get_drvdata
4.3 定义通过proc文件系统访问方法
主要实现了demo_proc_read和demo_proc_write两个方法,同时定义了在proc文件系统创建和删除文件的方法demo_create_proc和demo_remove_proc:
/*读取设备寄存器val的值,保存在page缓冲区中*/ static ssize_t demo_proc_read(char* page, char** start, off_t off, int count, int* eof, void* data) { if(off > 0) { *eof = 1; return 0; } return __demo_get_val(demo_dev, page); } /*把缓冲区的值buff保存到设备寄存器val中去*/ static ssize_t demo_proc_write(struct file* filp, const char __user *buff, unsigned long len, void* data) { int err = 0; char* page = NULL; if(len > PAGE_SIZE) { printk(KERN_ALERT"The buff is too large: %lu.\n", len); return -EFAULT; } page = (char*)__get_free_page(GFP_KERNEL); if(!page) { printk(KERN_ALERT"Failed to alloc page.\n"); return -ENOMEM; } /*先把用户提供的缓冲区值拷贝到内核缓冲区中去*/ if(copy_from_user(page, buff, len)) { printk(KERN_ALERT"Failed to copy buff from user.\n"); err = -EFAULT; goto out; } err = __demo_set_val(demo_dev, page, len); out: free_page((unsigned long)page); return err; } /*创建/proc/demo*/ static void demo_create_proc(void) { struct proc_dir_entry* entry; entry = create_proc_entry(DEMO_DEVICE_PROC_NAME, 0, NULL); if(entry) { entry->owner = THIS_MODULE; entry->read_proc = demo_proc_read; entry->write_proc = demo_proc_write; } } /*删除/proc/demo*/ static void demo_remove_proc(void) { remove_proc_entry(DEMO_DEVICE_PROC_NAME, NULL); }
这是低版本内核的代码,在我们环境中直接编译时报错,发现4.4版本有些API做了修改:
create_proc_entry()函数已经被proc_create()函数取代,在proc_fs.h头文件里也没有此函数(proc_create
是在kernel 3.10以及之后的版本中新增的),我们使用proc_create()函数替换create_proc_entry:
#include <linux/module.h> #include <linux/moduleparam.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/proc_fs.h> #include <asm/uaccess.h> #define BUFSIZE 100 MODULE_LICENSE("Dual BSD/GPL"); MODULE_AUTHOR("Liran B.H"); static int irq=20; module_param(irq,int,0660); static int mode=1; module_param(mode,int,0660); static struct proc_dir_entry *ent; static ssize_t mywrite(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos) { int num,c,i,m; char buf[BUFSIZE]; if(*ppos > 0 || count > BUFSIZE) return -EFAULT; if(copy_from_user(buf, ubuf, count)) return -EFAULT; num = sscanf(buf,"%d %d",&i,&m); if(num != 2) return -EFAULT; irq = i; mode = m; c = strlen(buf); *ppos = c; return c; } static ssize_t myread(struct file *file, char __user *ubuf,size_t count, loff_t *ppos) { char buf[BUFSIZE]; int len=0; if(*ppos > 0 || count < BUFSIZE) return 0; len += sprintf(buf,"irq = %d\n",irq); len += sprintf(buf + len,"mode = %d\n",mode); if(copy_to_user(ubuf,buf,len)) return -EFAULT; *ppos = len; return len; } static struct file_operations myops = { .owner = THIS_MODULE, .read = myread, .write = mywrite, }; static int simple_init(void) { ent=proc_create("mydev",0666,NULL,&myops); printk(KERN_ALERT "demo...\n"); return 0; } static void simple_cleanup(void) { proc_remove(ent); printk(KERN_WARNING "bye ...\n"); }
5. 定义模块加载和卸载方法
这里配置执行设备注册和初始化操作:
/*初始化设备*/ static int __demo_setup_dev(struct demo_android_dev* dev) { int err; dev_t devno = MKDEV(demo_major, demo_minor); memset(dev, 0, sizeof(struct demo_android_dev)); cdev_init(&(dev->dev), &demo_fops); dev->dev.owner = THIS_MODULE; dev->dev.ops = &demo_fops; /*注册字符设备*/ err = cdev_add(&(dev->dev),devno, 1); if(err) { return err; } /*初始化信号量和寄存器val的值*/ init_MUTEX(&(dev->sem)); dev->val = 0; return 0; } /*模块加载方法*/ static int __init demo_init(void){ int err = -1; dev_t dev = 0; struct device* temp = NULL; printk(KERN_ALERT"Initializing demo device.\n"); /*动态分配主设备和从设备号*/ err = alloc_chrdev_region(&dev, 0, 1, DEMO_DEVICE_NODE_NAME); if(err < 0) { printk(KERN_ALERT"Failed to alloc char dev region.\n"); goto fail; } demo_major = MAJOR(dev); demo_minor = MINOR(dev); /*分配demo设备结构体变量*/ demo_dev = kmalloc(sizeof(struct demo_android_dev), GFP_KERNEL); if(!demo_dev) { err = -ENOMEM; printk(KERN_ALERT"Failed to alloc demo_dev.\n"); goto unregister; } /*初始化设备*/ err = __demo_setup_dev(demo_dev); if(err) { printk(KERN_ALERT"Failed to setup dev: %d.\n", err); goto cleanup; } /*在/sys/class/目录下创建设备类别目录demo*/ demo_class = class_create(THIS_MODULE, DEMO_DEVICE_CLASS_NAME); if(IS_ERR(demo_class)) { err = PTR_ERR(demo_class); printk(KERN_ALERT"Failed to create demo class.\n"); goto destroy_cdev; } /*在/dev/目录和/sys/class/demo目录下分别创建设备文件demo*/ temp = device_create(demo_class, NULL, dev, "%s", DEMO_DEVICE_FILE_NAME); if(IS_ERR(temp)) { err = PTR_ERR(temp); printk(KERN_ALERT"Failed to create demo device."); goto destroy_class; } /*在/sys/class/demo/demo目录下创建属性文件val*/ err = device_create_file(temp, &dev_attr_val); if(err < 0) { printk(KERN_ALERT"Failed to create attribute val."); goto destroy_device; } dev_set_drvdata(temp, demo_dev); /*创建/proc/demo文件*/ demo_create_proc(); printk(KERN_ALERT"Succedded to initialize demo device.\n"); return 0; destroy_device: device_destroy(demo_class, dev); destroy_class: class_destroy(demo_class); destroy_cdev: cdev_del(&(demo_dev->dev)); cleanup: kfree(demo_dev); unregister: unregister_chrdev_region(MKDEV(demo_major, demo_minor), 1); fail: return err; } /*模块卸载方法*/ static void __exit demo_exit(void) { dev_t devno = MKDEV(demo_major, demo_minor); printk(KERN_ALERT"Destroy demo device.\n"); /*删除/proc/demo文件*/ demo_remove_proc(); /*销毁设备类别和设备*/ if(demo_class) { device_destroy(demo_class, MKDEV(demo_major, demo_minor)); class_destroy(demo_class); } /*删除字符设备和释放设备内存*/ if(demo_dev) { cdev_del(&(demo_dev->dev)); kfree(demo_dev); } /*释放设备号*/ unregister_chrdev_region(devno, 1); } MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("First Android Driver"); module_init(demo_init); module_exit(demo_exit);
这里面,在 2.6.37
之后的 Linux
内核中, init_mutex
已经被废除了, 新版本使用 sema_init
函数,init_MUTEX(&sem);
修改为sema_init(&sem, 1);
。
6. 配置编译环境
我们的模块写好了,需要在内核编译的时候编译上我们的代码,需要在做一些配置:
在demo目录中新增Kconfig和Makefile两个文件,其中Kconfig是在编译前执行配置命令make menuconfig时用到的,而Makefile是执行编译命令make是用到的:
Kconfig文件的内容
config DEMO tristate "First Android Driver" default n help This is the first android driver.
tristate表示编译选项DEMO支持在编译内核时,demo模块支持以模块、内建和不编译三种编译方法,默认是不编译,因此,在编译内核前,我们还需要执行make menuconfig命令来配置编译选项,使得demo可以以模块或者内建的方法进行编译。
Makefile文件的内容
obj-$(CONFIG_DEMO) += demo.o
修改drivers/kconfig文件,在menu "Device Drivers"和endmenu之间添加一行(2.6.25旧版本还需要修改arch/arm/Kconfig,arch/arm/Kconfig中含有Drivers里Kconfig内容的一个复本,只对drivers/kconfig修改会导致无效):
source "drivers/demo/Kconfig"
在drivers/Makefile中增加一行:
obj-$(CONFIG_DEMO) += demo/
回到kernel目录执行make menuconfig
会弹出U操作界面:
选择Device Drivers First Android Driver:
选择我们自己实现的驱动:
First Android Driver有两个选项可选:
- M:作为module。
.config
中就会多一行CONFIG_DEMO = m
,然后保存配置,执行make命令,就可以看到 CC [M] drivers/demo/demo.o 的log了,demo目录里生成了demo.o demo.ko的等文件 - y:编进linux内核,
.config
中就会多一行CONFIG_DEMO = y
如果配置First Android Driver为M,drivers/Makefile
中obj-$(CONFIG_DEMO) += demo/
就变成了obj-m +=demo/
,在执行make命令时,便会进入demo目录里找makefile,MakeFile内容obj-$(CONFIG_DEMO) += demo.o 变成了obj-m +=demo.o,所以demo.c就被编译成模块了。
如果是模块的方式,我们将编译出的demo.ko文件通过adb push到设备SD卡,然后root方式进入adb,执行insmod demo.ko
即可以调试的方式加载我们刚刚的驱动。
如果是内嵌内核的方式的话,这块板子的内核需要将make menuconfig
保存下来的文件(如.config
)拷贝到arch/arm64/configs/firefly_defconfig
下再编译内核才能生效。
7. 验证
编译完成刷机后,可以在/dev/
、/proc/
、sys/class
下看到demo文件或目录了。通过cat demo
查看虚拟寄存器值,通过echo
num > demo`向寄存器写入值。
在/sys/class/demo/
下可以看到val文件,我们可以用类似的方式操作val。
8. 总结
本文介绍了Android9.0开发内核驱动的一般流程,以及开发一个4字节寄存器用到的linux函数,并且列出Linux内核4.4及之前版本接口上的一些区别。后续将完整代码放出,并录制一个完整的流程。