写点什么

Robot OS 驱动开发

作者:轻口味
  • 2022 年 4 月 21 日
  • 本文字数:7353 字

    阅读完需:约 24 分钟

Robot OS驱动开发

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/hello/Kconfig"
复制代码


在 drivers/Makefile 中增加一行:


obj-$(CONFIG_DEMO) += demo/
复制代码


回到 kernel 目录执行make menuconfig会弹出 U 操作界面:



在 Device Drivers 菜单下选中 First Android Driver 项为 M,作为 module。然后保存配置,执行 make 命令,就可以看到 CC [M] drivers/demo/demo.o 的 log 了,demo 目录里生成了 demo.o demo.ko 的等文件


因为配置 First Android Driver 为 M(即编为模块,而不是编进 linux 内核)


则.config 中就会多一行CONFIG_DEMO = m


如此一来,drivers/Makefileobj-$(CONFIG_DEMO) += demo/就变成了obj-m +=demo/


于是执行 make 命令时,便会进入 demo 目录里找 makefile,MakeFile 内容 obj-$(CONFIG_DEMO) += demo.o 变成了 obj-m +=demo.o,所以 demo.c 就被编译成模块了


然后执行编译,烧制,就可以在系统中看到我们的驱动文件了。

7. 总结

本文介绍了 Android9.0 开发内核驱动的一般流程,以及开发一个 4 字节寄存器用到的 linux 函数,并且列出 Linux 内核 4.4 及之前版本接口上的一些区别。后续将完整代码放出,并录制一个完整的流程。

发布于: 刚刚阅读数: 3
用户头像

轻口味

关注

🏆2021年InfoQ写作平台-签约作者 🏆 2017.10.17 加入

Android、音视频、AI相关领域从业者。 欢迎加我微信wodekouwei拉您进InfoQ音视频沟通群 邮箱:qingkouwei@gmail.com

评论

发布
暂无评论
Robot OS驱动开发_c++_轻口味_InfoQ写作社区