《【正点原子】I.MX6U嵌入式Linux驱动开发指南》学习笔记

字符设备驱动简介

字符设备是 Linux 驱动中最基本的一类设备驱动,字节设备就是按照字节流来读写的设备,常见的字符设备包括:LED、蜂鸣器、按键、I2C 以及 SPI 等。

Linux 中一切皆文件,字符设备驱动加载成功后会在 /dev 目录下生成相应的设备文件,应用程序可以通过 open() 函数来打开这个设备文件,然后可以通过 write() 和 read() 对这个设备进行读写操作。

上面提到的 open()、write() 等函数在驱动中都对应一个函数,驱动中这类函数有很多,它们都在结构体 file_operations 当中(kernel/include/linux/fs.h),该结构体就是 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 *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	int (*show_fdinfo)(struct seq_file *m, struct file *f);
	/* get_lower_file is for stackable file system */
	struct file* (*get_lower_file)(struct file *f);
};

file_operations 结构体函数成员太多,这里介绍几个常用的:

  1. llseek() 函数用于修改文件指针偏移量
  2. read() 函数用于读取设备文件
  3. write() 函数用于向设备文件写入数据
  4. poll() 函数用于轮询监听设备状态
  5. unlocked_ioctl() 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应
  6. compat_ioctl() 函数与 unlocked_ioctl() 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl()
  7. mmap() 函数用于将设备的内存映射到进程空间中(也就是用户空间)
  8. open() 函数用于打开设备文件
  9. release() 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应
  10. fasync() 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中
  11. aio_fsync() 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据

字符设备驱动开发步骤

驱动模块的加载和卸载

Linux 驱动的运行方式有两种,编译进内核和编译成模块,调试时常用的是第二种,这样修改驱动时不需要编译内核源码,但是编译成驱动模块后,还需要用 insmod 命令将驱动模块加载进系统。

在我们使用加载和卸载命令时,驱动会调用下面两个函数:

module_init(xxx_init);  // 模块加载函数,使用 insmod 命令时该函数被调用
module_exit(xxx_exit);  // 模块卸载函数,使用 rmmod 命令时该函数被调用

模块加载和卸载模板如下:

在这里插入图片描述

驱动文件的扩展名为 .ko,我们可以用两种命令来安装驱动,insmod 和 modeprobe,modeprobe 比 insmode 更加智能,modeprobe 能提供模块的依赖性分析、错误检查、错误报告,它默认会去 /lib/modules/<kernel-version> 目录中查找(内核中编译时指定了编译成动态模块的驱动文件会放在这个目录)。但是我平时很少使用 modeprobe,虽然正点原子文档上推荐我们使用这个命令。

使用 insmod 安装驱动:

insmod test.ko

使用 modprobe 安装驱动

modprobe test.ko
或者 
modpobe test

使用 rmmod 卸载驱动

rmmod test.ko

使用 modprobe -r 卸载驱动

modprobe -r test.ko
或者 
modpobe -r test

字符设备注册与注销

注册函数

函数原型:

#include <linux/fs.h>
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)

功能:注册一个字符设备

参数:

  • major: 主设备号(填 0 时,会自动分配)
  • name: 设备名
  • fops: 文件操作方法结构体指针

返回值:
成功返回分配的主设备号,失败返回负数。

注销函数

函数原型:

#include <linux/fs.h>
static inline void unregister_chrdev(unsigned int major, const char *name)

功能:注销一个已经存在字符设备

参数:

  • major: 主设备号
  • name: 设备名

示例代码(模板):

在这里插入图片描述

上面一个模板,test_fops 只是被定义,它的成员(如 open、release)还没有被初始化(定义)。

在 linux 命令行,使用 cat /proc/devices 可以查看当前系统已经占用的设备号。下图是我的虚拟机上 Ubuntu 已被使用的部分设备号:

在这里插入图片描述

设备具体操作函数

上面的设备驱动注册函数中有一个参数 fops,它是 file_operations 结构体变量,这个结构体前文已经介绍了,有很多成员函数,我们需要实现需求定义部分函数,这里我们就拿最常见的 open、clease、read 和 write 写一个简单的示例:(模板,不包含头文件)


/* 打开设备 */
static int test_open(struct inode *inode, struct file * filp)
{
	printk("Chrdev was opened.\n");
	return 0;
}

/* 读设备 */
static ssize_t test_read(struct file *filp, char __user *buf,
							size_t cnt, loff_t *offt)
{
	return 0;
}

/* 写设备 */
static ssize_t test_write(struct file *filp, const char __user *buf,
							size_t cnt, loff_t *offt)
{
	return 0;
}

/* 释放设备 */
static int test_release(struct inode *inode, struct file *filp)
{
	printk("Chrdev was closed.\n");	
	return 0;
}

// 定义文件操作结构体变量
static struct file_operations test_fops = {
	.owner = THIS_MODULE,
	.open = test_open,
	.read = test_read,
	.write = test_write,
	.release = test_release,
};

/* 驱动入口函数 */
static int __init test_init(void)
{
	int ret = 0;

	/* 注册字符设备驱动 */
	ret = register_chrdev(100, "chrdev_test", &test_fops);
	if(ret < 0)
	{
		printk("Chrdev register failed.\n");
	}
	printk("Driver installed\n");
	return 0;
}

/* 驱动出口函数 */
static void __exit test_exit(void)
{
	/* 注销字符设备驱动 */
	unregister_chrdev(100, "chrdev_test");
	printk("Driver uninstalled\n");
}

/* 指定入口函数和出口函数 */
module_init(test_init);
module_exit(test_exit);

上面编写了四个操作函数 test_open()、test_read()、test_write() 及 test_release(),当应用层使用 open()、read()、write() 或 close() 时,就会调用驱动中对应的函数。

添加 LICENSE 和作者信息

驱动中还需要加入 LICENSE 信息和作者信息,前者是必须加的,不然编译不能通过。添加方式如下:

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

这两两行代码放驱动的最后两行即可。

到这里,一个字符设备驱动的所有组成部分就全部介绍完了。

Linux 设备号

设备号的组成

Linux 里每个设备都有一个设备号,设备号由主设备号和次设备号组成,主设备号代表一个驱动,次设备号对应单个驱动中的各个设备。

设备号是一个 32 位(unsigned int)数据,主设备为高 12位,次设备为低 20 位,所以主设备号的范围为 0 ~ 4095(0~212)。

设备号的分配

静态分配

上文介绍字符设备注册函数时用到的就是静态分配。可以先使用 cat /proc/devices 查看哪些设备号已经被占用。

动态分配

静态分配很容易带来设备号冲突问题,所以推荐使用动态分配设备号,设备号申请函数如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

参数介绍:

  • dev: 申请到的设备号
  • baseminor: 次设备号起始地址,一般填 0,
  • count: 要申请的设备号数量
  • name: 设备名

有申请,就会有对应的注销,下面是 alloc_chrdev_region() 对应的注销函数:

void unregister_chrdev_region(dev_t from, unsigned count)

参数介绍:

  • from: 要释放的设备号
  • count: 要释放的设备数量

前面介绍字符设备注册函数时,设备号填 0,也能实现动态分配!

chrdevbase 字符设备驱动实验

字符设备驱动介绍完了,写一个简单的驱动来作为总结,驱动主要实现的功能:应用层和驱动层互相传输数据。

驱动层代码(驱动代码)

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kernel.h>
//#include <linux/delay.h>
#include <linux/ide.h>
//#include <linux/types.h>

static char rxbuff[255];
static char txbuff[255] = {"\"user read test.\""};

/* 打开设备 */
static int test_open(struct inode *inode, struct file * filp)
{
	printk("Chrdev was opened.\n");
	return 0;
}

/* 读设备 */
static ssize_t test_read(struct file *filp, char __user *buf,
							size_t cnt, loff_t *offt)
{
	int ret = 0;
	// 将数据发送到用户空间
	ret = copy_to_user(buf, txbuff, cnt); 
	if(ret != 0)
	{
		printk("Send failed.\n");
	}
	return 0;
}

/* 写设备 */
static ssize_t test_write(struct file *filp, const char __user *buf,
							size_t cnt, loff_t *offt)
{
	int ret = 0;
	// 从用户空间获取数据
	ret = copy_from_user(rxbuff, buf, cnt); 
	if(ret != 0)
	{
		printk("Receive failed.\n");
	}
	else
	{
		printk("The data received from user is %s\n", rxbuff);
	}
	return 0;
}

/* 释放设备 */
static int test_release(struct inode *inode, struct file *filp)
{
	printk("Chrdev was closed.\n");	
	return 0;
}

// 定义文件操作结构体变量
static struct file_operations test_fops = {
	.owner = THIS_MODULE,
	.open = test_open,
	.read = test_read,
	.write = test_write,
	.release = test_release,
};

/* 驱动入口函数 */
static int __init test_init(void)
{
	int ret = 0;

	/* 注册字符设备驱动 */
	ret = register_chrdev(0, "chrdev_test", &test_fops);
	if(ret < 0)
	{
		printk("Chrdev register failed.\n");
	}
	printk("Driver installed\n");
	return 0;
}


/* 驱动出口函数 */
static void __exit test_exit(void)
{
	/* 注销字符设备驱动 */
	unregister_chrdev(0, "chrdev_test");
	printk("Driver uninstalled\n");
}

/* 指定入口函数和出口函数 */
module_init(test_init);
module_exit(test_exit);

/* LICENSE 和 AUTHOR */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xiaohui");

应用层代码(应用程序)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>


#define DEV_PATH "/dev/chrdev_test"

static char txbuff[] = "\"user write test\"";

int main()
{
	int fd;
	int ret = 0;
	char rxbuff[255];

	// 打开驱动文件
	fd = open(DEV_PATH, O_RDWR);
	if(fd < 0)
	{
		printf("Cannot open device file.\n");
		exit(-1);
	}

	// 从设备读数据
	ret = read(fd, rxbuff, sizeof(rxbuff));
	if(ret < 0)
	{
		printf("Read data failed.\n");
	}
	else
	{
		printf("The data received from kernel is %s\n", rxbuff);
	}

	// 向设备写数据
	ret = write(fd, txbuff, sizeof(txbuff));
	if(ret < 0)
	{
		printf("Write data failed.\n");
	}
	else
	{
		printf("Write data success.\n");	
	}
	
	// 关闭设备
	close(fd);

	return 0;
}

Makfile 文件

我是在 X86 平台测试的,所以用了以下的 Makefile,如果想编译成 arm 平台的驱动,只需指定 arm 交叉编译器和内核路径即可(如下面注释掉的那部分内容)。

obj-m := test_drv.o

#export ARCH=arm
#export CROSS_COMPILE=arm-linux-gnueabihf-

KDIR := /lib/modules/$(shell uname -r)/build
#KDIR := /home/alientek/alpha/alientek-alpha/kernel-alientek

all:
	make -C $(KDIR) M=$(shell pwd) modules

clean:
	rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order

测试结果

首先编译驱动程序和应用程序。

驱动程序的编译是在 Makefile 目录下使用 make 命令,编译成功后会生成 .ko 文件,

在这里插入图片描述

接着编译应用程序,我测试的平台是 x86,所以直接用系统的 gcc 命令即可,

在这里插入图片描述

使用 insmod 安装驱动:

sudo insmod test_drv.ko 

使用 dmesg 查看内核打印信息(不如开发板方便)

在这里插入图片描述
驱动虽然安装成功了,但是没有在 /dev 下生成设备文件(因为注册字符设备后并不会自己生成设备节点,需要在驱动里添加相关代码,或者手动创建,原教程使用了手动创建的方式)

使用 cat /proc/devices 查看驱动申请到的节点号,

在这里插入图片描述

然后使用 mknod 命令手动生成设备节点,

在这里插入图片描述

最后就是运行 app (应用程序)来测试了,下图是应用层的打印信息:

在这里插入图片描述

这是内核驱动层打印信息:

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐