Linux字符设备驱动开发框架介绍

嵌入式技术

1368人已加入

描述

1. 字符设备驱动简介

字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。 比如常见的点灯、按键、IIC、SPI、LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

Linux驱动基本原理 :Linux中一切皆为文件,驱动加载成功后会在/dev目录下生成一个相应的文件,应用程序通过对这个名为/dev/xxx的文件进行相应的操作即可实现对硬件的操作。

比如LED驱动,会有/dev/led驱动文件,应用程序使用open函数来打开该文件; 若要点亮或关闭led,就使用write函数写入开关值; 若要获取led灯的状态,就用read函数从驱动文件中读取相应的状态; 使用完成后使用close函数关闭该驱动文件。

Linux软件从上到下可分为4层结构,如下图左示。 以控制LED为例,具体过程如下图右示:

Linux

每个系统调用,在驱动中都有与之对应的驱动函数,内核include/linux/fs.h文件中有个file_operations结构体,就是Linux内核驱动操作函数集合:

struct file_operations {
  struct module *owner;
  loff_t (*llseek) (struct file *, loff_t, int);
  ssize_t (*read) (struct file *, char __user *, size_t, loff_t*);
  ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  ......
  ......
};

Linux驱动运行方式有以下两种:

  • 将驱动编译进内核中, 当Linux内核启动时就会自动运行驱动程序
  • 将驱动编译成模块, 在内核启动后使用insmod命令加载驱动模块

在驱动开发阶段一般都将其编译为模块,不需要编译整个Linux代码,方便调试驱动程序。 当驱动开发完成后,根据实际需要,可以选择是否将驱动编译进Linux内核中。

2. Linux设备号

2.1 设备号的组成

Linux中每个设备都有一个设备号,由主设备号和次设备号两部分组成:

  • 主设备号表示某一个具体的驱动
  • 次设备号表示使用这个驱动的各个设备

Linux 提供了名为dev_t的数据类型表示设备号,其本质是32位的unsigned int数据类型,其中高12位为主设备号,低20位为次设备号,因此Linux中主设备号范围为0~4095

在文件include/linux/kdev_t.h中提供了几个关于设备号操作的宏定义:

#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
  • MINORBITS:表示次设备号位数,一共20位
  • MINORMASK:表示次设备号掩码
  • MAJOR:用于从dev_t中获取主设备号,将dev_t右移20位即可
  • MINOR:用于从dev_t中获取次设备号,取dev_t的低20位的值即可
  • MKDEV:用于将给定的主设备号和次设备号的值组合成dev_t类型的设备号

2.2 主设备号的分配

主设备号的分配包括静态分配和动态分配。 静态分配需要手动指定设备号,并且要注意不能与已有的重复,一些常用的设备号已经被Linux内核开发者给分配掉了,可使用cat /proc/devices命令查看当前系统中所有已经使用了的设备号。

动态分配是在注册字符设备之前先申请一个设备号,系统会自动分配一个没有被使用的设备号, 这样就避免了冲突。 在卸载驱动的时候释放掉这个设备号即可。

设备号的申请函数

//设备号申请函数
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
// dev:保存申请到的设备号
// baseminor:次设备号起始地址
// count:要申请的设备号数量
// name:设备名字

设备号的释放函数

//设备号释放函数
void unregister_chrdev_region(dev_t from, unsigned count)
// from:要释放的设备号
// count:表示从 from 开始,要释放的设备号数量

3. 字符设备驱动开发模板

3.1 加载与卸载

在编写驱动的时候需要注册模块加载和卸载这两种函数:

module_init(xxx_init);    //注册模块加载函数
module_exit(xxx_exit);    //注册模块卸载函数

module_init():向内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当使用insmod命令加载驱动时,xxx_init函数就会被调用

module_exit():向内核注册一个模块卸载函数,参数xxx_exit就是需要注册的具体函数,当使用rmmod命令卸载驱动时,xxx_exit函数就会被调用

字符设备驱动模块加载和卸载模板如下所示:

/* 驱动入口函数 */
staticint __init xxx_init(void)
{
/*入口函数具体内容*/
  return0;
}

/* 驱动出口函数 */
staticvoid __exit xxx_exit(void)
{
/*出口函数具体内容*/
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块:

  • insmod:最简单的模块加载命令,但不能解决模块的依赖关系
  • modprobe:会分析模块的依赖关系,将所有的依赖模块都加载到内核中

卸载驱动也有两种命令:

  • rmmod:最简单的模块卸载命令
  • modprobe -r:除了卸载指定的驱动,还卸载其所依赖的其他模块,若依赖模块还在被其它模块使用,就不能使用该命令来卸载驱动模块

3.2 注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,卸载驱动模块时也要注销掉字符设备。 字符设备的注册和注销函数原型如下所示:

static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
//major:主设备号
//name:设备名字,指向一串字符串
//fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量
static inline void unregister_chrdev(unsigned int major, const char *name)
//major:要注销的设备对应的主设备号
//name:要注销的设备对应的设备名

一般字符设备的注册在驱动模块的入口函数xxx_init中进行,字符设备的注销在驱动模块的出口函数xxx_exit中进行

//定义了一个file_operations结构体变量,就是设备的操作函数集合
static struct file_operations test_fops;

/* 驱动入口函数 */
static int __init xxx_init(void){
  /* 入口函数具体内容 */
  int retvalue = 0;
  /* 注册字符设备驱动 */
  retvalue = register_chrdev(200, "chrtest", &test_fops);
  if(retvalue < 0){
    /* 字符设备注册失败,自行处理 */
  }
  return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void){
  /* 注销字符设备驱动 */
  unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

3.3 实现设备的具体操作函数

file_operations结构体就是设备的具体操作函数。 假设对chrtest这个设备有如下两个要求:

  • 能够实现打开和关闭操作:需要实现open和release这两个函数
  • 能够实现进行读写操作:需要实现read和write这两个函数

实现file_operations中的这四个函数,完成后的内容框架如下所示:

/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp){
  /* 用户实现具体功能 */
  return 0;
}

/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){
  /* 用户实现具体功能 */
  return 0;
}

/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt){
  /* 用户实现具体功能 */
  return 0;
}

/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp){
  /* 用户实现具体功能 */
  return 0;
}

然后是驱动的入口(init)和出口(exit) 函数:

//定义了一个file_operations结构体变量test_fops,就是设备的操作函数集合
static struct file_operations test_fops = {
  .owner = THIS_MODULE,
  .open = chrtest_open,
  .read = chrtest_read,
  .write = chrtest_write,
  .release = chrtest_release,
}

/* 驱动入口函数 */
static int __init xxx_init(void){
  /* 入口函数具体内容 */
  int retvalue = 0;
  /* 注册字符设备驱动 */
  retvalue = register_chrdev(200, "chrtest", &test_fops);
  if(retvalue < 0){
    /* 字符设备注册失败,自行处理 */
  }
  return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void){
  /* 注销字符设备驱动 */
  unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

3.4 添加LICENSE和作者信息

LICENSE是必须添加的,否则编译时会报错,作者信息可加可不加

MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息

综上所述,字符设备驱动开发流程如下图所示:

Linux

4. 字符设备驱动开发实验

下面以正点原子的IMX6ULL开发板为平台,完整的编写一个虚拟字符设备驱动模块。 chrdevbase不是实际存在的一个设备,只是为了学习字符设备的开发的流程

4.1 驱动程序编写

宏定义及变量定义

#include 
#include 
#include 
#include 
#include 
#include 

#define CHRDEVBASE_MAJOR    200		 /* 主设备号 */
#define CHRDEVBASE_NAME	    "chrdevbase" /* 设备名   */

static char readbuf[100];		 /* 读缓冲区 */
static char writebuf[100];		 /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};

打开、关闭、读取、写入函数实现

staticintchrdevbase_open(structinode*inode,structfile*filp){
  printk("chrdevbase open!\\r\\n");
  return0;
}

staticssize_tchrdevbase_read(structfile*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 senddata ok!\\r\\n");
  }else{
    printk("kernel senddata failed!\\r\\n");
  }
	
  printk("chrdevbase read!\\r\\n");
  return0;
}

staticssize_tchrdevbase_write(structfile*filp,constchar __user *buf,size_t cnt,loff_t*offt){
  int retvalue =0;
  /* 接收用户空间传递给内核的数据并且打印出来 */
  retvalue =copy_from_user(writebuf, buf, cnt);
  if(retvalue ==0){
    printk("kernel recevdata:%s\\r\\n", writebuf);
  }else{
    printk("kernel recevdata failed!\\r\\n");
  }
	
  printk("chrdevbase write!\\r\\n");
  return0;
}

staticintchrdevbase_release(structinode*inode,structfile*filp){
printk("chrdevbase release!\\r\\n");
return0;
}

驱动加载与注销

staticstructfile_operations chrdevbase_fops ={
  .owner = THIS_MODULE,	
  .open = chrdevbase_open,
  .read = chrdevbase_read,
  .write = chrdevbase_write,
  .release = chrdevbase_release,
};

/*驱动入口函数  */
staticint __init chrdevbase_init(void){
  int retvalue =0;
  retvalue =register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,&chrdevbase_fops);
  if(retvalue <0){
    printk("chrdevbase driver register failed\\r\\n");
  }
  printk("chrdevbase init!\\r\\n");
  return0;
}

/* 驱动出口函数 */
staticvoid __exit chrdevbase_exit(void){
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase exit!\\r\\n");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

LICENSE与作者

MODULE_LICENSE("GPL");
MODULE_AUTHOR("andyxi");

4.2 应用程序编写

应用程序运行在用户空间,其通过输入相应的指令来对chrdevbase设备执行读或者写操作。 下面将程序进行分段介绍

头文件和main函数入口,以及main函数的传参处理

#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!"};

int main(int argc, char *argv[]){
  int fd, retvalue;
  char *filename;
  char readbuf[100], writebuf[100];

  if(argc != 3){
    printf("Error Usage!\\r\\n");
    return -1;
  }

  filename = argv[1];

  /* 打开驱动文件 */
  fd  = open(filename, O_RDWR);
  if(fd < 0){
    printf("Can't open file %s\\r\\n", filename);
    return -1;
  }

对 chrdevbase 设备的具体操作

if(atoi(argv[2])==1){/* 从驱动文件读取数据 */
    retvalue =read(fd, readbuf,50);
    if(retvalue <0){
      printf("read file %s failed!\\r\\n", filename);
    }else{
      /* 读取成功,打印出读取成功的数据 */
      printf("read data:%s\\r\\n",readbuf);
    }
  }

  if(atoi(argv[2])==2){
    /* 向设备驱动写数据 */
    memcpy(writebuf, usrdata,sizeof(usrdata));
    retvalue =write(fd, writebuf,50);
    if(retvalue <0){
      printf("write file %s failed!\\r\\n", filename);
    }
  }

关闭设备

/* 关闭设备 */
  retvalue = close(fd);
  if(retvalue < 0){
    printf("Can't close file %s\\r\\n", filename);
    return -1;
  }
  return 0;
}

4.3 程序编译

程序编译包括驱动程序编译和应用程序编译两个部分

驱动程序编译: 将驱动程序编译为.ko模块

创建Makefile文件

# KERNELDIR:开发板所使用的Linux内核源码目录
KERNELDIR := /home/andyxi/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_andyxi
# CURRENT_PATH:当前路径,通过运行“pwd”命令获取
CURRENT_PATH := $(shell pwd)
# obj-m:将 chrdevbase.c 这个文件编译为chrdevbase.ko模块
obj-m := chrdevbase.o

build: kernel_modules
# -C 表示切换工作目录到KERNERLDIR目录
# M 表示模块源码目录
# modules 表示编译模块
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

输入make命令即可编译,编译成功以后就会生成一个叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 设备的驱动模块

Linux

注意:若直接make编译可能会出错,是因为kernel中没有指定编译器和架构,使用了默认的x86平台编译报错。 解决办法就是在内核顶层Makefile中,直接定义ARCH和CROSS_COMPILE这两个的变量值为 arm 和 arm-linux-gnueabihf- 即可

Linux

应用程序编译: 无需内核参与,直接编译即可

arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp

使用file命令,查看生成的chrdevbaseApp文件信息,如下图示,文件是32位LSB格式,ARM版本的,因此只能在ARM芯片下运行

Linux

4.4 运行测试

为了方便测试,Linux系统选择通过TFTP从网络启动,并且使用NFS挂载网络根文件系统。 确保开发板系统移植成功,能正常启动。 具体的实现方法可参考之前介绍过的系统移植专辑系列文章

加载驱动模块

在根文件系统创建/lib/modules/4.1.15文件夹,用来存放驱动模块

  • /lib/modules是通用的
  • 4.1.15根据所使用的内核版本来设置,否则modprobe命令无法加载驱动模块

在Ubuntu中将chrdevbase.ko和chrdevbaseAPP,复制到根文件系统的 rootfs/lib/modules/4.1.15 目录中

Linux

在开发板中使用insmod或modprobe命令来加载驱动文件

Linux

输入lsmod命令即可查看当前系统中存在的模块,输入cat /proc/devices命令,查看当前系统中有没有chrdevbase 这个设备

Linux

创建设备节点文件: 驱动加载成功后,需要在/dev目录下创建一个与之对应的设备节点文件,应用程序通过操作这个设备节点文件来完成对具体设备的操作

使用mknod命令创建/dev/chrdevbase设备节点文件

mknod /dev/chrdevbase c 200 0
#/dev/chrdevbase 是要创建的节点文件
# c 表示这是个字符设备
# 200 是设备的主设备号
# 0 是设备的次设备号

创建完后可使用ls /dev/chrdevbase -l命令查看是否存在

Linux

操作设备测试: 使用应用程序读写设备,对/dev/chrdevbase文件进行读写操作

# 读操作命令
./chrdevbaseApp /dev/chrdevbase 1
# 输出“ kernel senddata ok!”是驱动程序中chrdevbase_read函数输出的信息
#read data:kernel data!”就是chrdevbaseAPP打印出来的接收到的数据
# 写操作命令
./chrdevbaseApp /dev/chrdevbase 2
# “kernel recevdata:usr data!”,是驱动程序中的chrdevbase_write函数输出的

Linux

卸载驱动模块: 若不再使用某个设备的话可以将其驱动卸载掉。 输入rmmod命令卸载驱动后,使用lsmod命令查看chrdevbase这个模块还存不存在

Linux

至此,Linux字符设备驱动开发完成。 本文介绍了驱动开发中的字符驱动开发的基本模式,并使用一个虚拟的字符设备驱动进行测试,了解驱动程序与应用程序之间的调用关系。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分