Linux设备驱动开发入门之——hello驱动

1. Linux驱动程序的分类

Linux 中主要分为三大类驱动:字符设备驱动、块设备驱动和网络设备驱动。

1、字符设备驱动:因为软件操作设备是是以字节为单位进行的,是按照字节流进行读写操作的一种设备。典型的如LCD、蜂鸣器、SPI、触摸屏等驱动,都属于字符设备驱动的范畴。大部分的驱动程序都是属于字符设备驱动。

2、块设备驱动:块设备驱动是相对于字符设备驱动而定义的,因为块设备被软件操作时,是以块为单位进行操作的(块指的是多个字节组成一个块)。块设备大多指的都是各种存储类类设备,比如EMMC、SD卡、NANDFlash、U盘等等。

3、网络设备驱动:专门针对网络设备而设计的一种驱动,不管是有线还是无线网络,都属于网络设备驱动。

另外,一个设备可以属于多种设备驱动类型,比如 USB WIFI设备,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。

2. 与Linux驱动开发相关的介绍

1、Linux下的应用程序是如何调用驱动程序的

应用程序在使用C库函数所提供的 open/read/write 等等函数时,最终会进入到内核里面,调用内核所提供的 sys_open/sys_read/sys_write 等等函数。而此时如果内核发现应用程序需要访问的是驱动的话,那么就会调用该驱动程序所提供的 drv_open/drv_read/drv_write 等函数;如果发现应用程序访问的不过是普通文件的话,那么内核就会调用访问普通文件的那套函数。下图形象的给出了调用关系:
Linux设备驱动开发入门之——hello驱动_第1张图片
驱动程序实际上起到承上启下的作用,上承应用程序,对下则实现了具体的硬件操作。

2、Linux驱动程序的两种运行方式

  • 可以把驱动程序编译进内核里面,这样内核启动后就会自动运行驱动程序了;
  • 将驱动程序编译成以.ko为后缀模块文件,然后在Linux启动后,我们自己手动安装驱动程序。一般来说,这种方式在开发驱动阶段常用。

3、Linux驱动开发中常用的几个命令

  • insmod(install module):用于安装以Linux的驱动模块
  • rmmod(remove module):卸载驱动模块
  • lsmod(list module):打印出当前内核中已经安装的模块
  • modinfo(module information):打印出某个 xxx.ko 文件的模块信息。用法:modinfo xxx.ko

3. Linux驱动开发需要准备的工作

1、已经安装好的交叉编译工具链

​ 我们开发的驱动程序是要运行在ARM架构上的,所以需要准备好ARM架构的编译工具链。一般来说我们使用开发板厂商,或者SoC原厂提供交叉编译工具链即可。具体如何安装交叉编译工具链这里不多啰嗦了。

2、准备已经配置和编译好你对应板子的内核源码

​ 驱动程序是运行在内核空间的,不同于应用程序运行在用户空间。驱动程序已经是属于内核的一部分了,而编译驱动程序需要借助于内核源码来编译。

​ 另外,内核源码的版本一定要和你板子上实际运行的版本相一致,否则编译出来的驱动程序会因为版本不同而无法在你的板子上运行。

3、你的开发板接线正常,网络正常(要保证开发板和ubuntu之间可以相互ping通,因为我们通过nfs方式把ubuntu编译好的 xxx.ko 等文件传输到开发板)。

4. show出你的代码

4.1 hello驱动的编写

我们在编写应用程序的时候,首先也是先学会如何再电脑屏幕上输出 “helllo world”。同样的,我们编写的第一个驱动程序,也是先学会hello驱动,该驱动不涉及任何的硬件操作,而且也是属于字符设备驱动的范畴。主要实现的功能是:

1、应用程序调用 open、read、write 等函数时,对应的驱动函数都打印出内核信息;

2、应用程序调用 write 函数时,传入的数据保存在驱动中;

3、应用程序调用 read 函数时,把驱动中保存的数据再返回给应用程序,并打印出来

驱动程序的编写其实也是有迹可循的,主要的编写步骤如下:

  1. 确定主设备号,也可以让内核自动分配
  2. 定义自己的 file_operations 结构体
  3. 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
  4. 把 file_operations 结构体告诉内核:register_chrdev
  5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
  6. 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev
  7. 其他完善:提供设备信息,自动创建设备节点:class_create, device_create

其中,驱动程序核心中的核心就是 file_operations 这个结构体了。在这个结构体里面,就是要实现这个驱动程序自己的 open、read、write等函数,并通过Linux内核提供的接口注册到内核里面去。而其他的一些步骤都是为了遵循LInux驱动程序的编写规范,用于完善这个驱动程序的。

驱动代码的编写也不用完全都自己写,我们可以参考Linux内核提供的一些已有的驱动程序,下面我们就参考内核的一份 misc 驱动,编写我们自己的hello驱动程序,hello驱动代码如下:

#include 

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

/* 1. 确定主设备号                                                                 */
static int major = 0;
static char kernel_buf[1024];
static struct class *hello_class;

#define MIN(a, b) (a < b ? a : b)

/* 3. 实现对应的open/read/write等函数,填入file_operations结构体                   */

/*
 * @description		: 从设备读取数据
 * @param - file	: 内核中的文件描述符
 * @param - buf		: 要存储读取的数据缓冲区(就是用户空间的内存地址)
 * @param - size	: 要读取的长度
 * @param - offset	: 相对于文件首地址的偏移量(一般读取信息后,指针都会偏移读取信息的长度)
 * @return 			: 返回读取的字节数,如果读取失败则返回-1
 */
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_to_user(buf, kernel_buf, MIN(1024, size));
	return MIN(1024, size);
}

/*
 * @description		: 向设备写数据
 * @param - file	: 内核中的文件描述符
 * @param - buf		: 要写给设备驱动的数据缓冲区
 * @param - size	: 要写入的长度
 * @param - offset	: 相对于文件首地址的偏移量
 * @return 			: 返回写入的字节数,如果写入失败则返回-1
 */
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_from_user(kernel_buf, buf, MIN(1024, size));
	return MIN(1024, size);
}

/*
 * @description		: 打开设备
 * @param - node	: 设备节点
 * @param - file	: 文件描述符
 * @return 			: 打开成功返回0,失败返回-1
 */
static int hello_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/*
 * @description		: 关闭设备
 * @param - node	: 设备节点
 * @param - file	: 文件描述符
 * @return 			: 关闭成功返回0,失败返回-1
 */
static int hello_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* 2. 定义自己的file_operations结构体                                              */
static struct file_operations hello_drv = {
	.owner	 = THIS_MODULE,
	.open    = hello_drv_open,
	.read    = hello_drv_read,
	.write   = hello_drv_write,
	.release = hello_drv_close,
};

/* 4. 把file_operations结构体告诉内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 		*/
static int __init hello_init(void)
{
	int err;
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello */

	/* 7. 其他完善:提供设备信息,自动创建设备节点                                 */
	hello_class = class_create(THIS_MODULE, "hello_class");
	err = PTR_ERR(hello_class);
	if (IS_ERR(hello_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "hello");
		return -1;
	}	
	device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
	
	return 0;
}

/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
static void __exit hello_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	device_destroy(hello_class, MKDEV(major, 0));
	class_destroy(hello_class);
	unregister_chrdev(major, "hello");
}

/* 指定驱动的入口和出口,以及声明自己的驱动遵循GPL协议(不声明的话无法把驱动加载进内核) */
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

hello驱动程序的几点说明:

  1. 驱动程序要打印信息的时候,调用的函数是 printk ,而应用程序调用的是 printf

  2. 应用程序和驱动程序之间传递数据,不能使用简单的赋值或者memcpy等,要使用内核提供的 copy_from_user/copy_to_user 函数。当然如果需要传递大量数据的时候,还可以使用内存映射的方式

  3. 阅读一个驱动程序,首先要找到驱动程序的入口函数。上面的驱动入口函数就是hello_init函数,该函数做的事情就是向内核注册了一个 file_oprations 结构体,并且完成自动创建设备节点相关的代码

  4. file_oprations 结构体是驱动程序的核心,里面提供了本驱动 open/read/write/release 等成员,当应用程序调用了open/read/write/release 等函数时,就会导致对应驱动的这些成员函数被调用

4.2 编写hello应用程序测试

下面我们编写一个hello应用程序来测试我们编写好的驱动程序。该应用程序要实现的功能是:

  1. 向驱动程序写入一串字符串
  2. 把驱动程序保存起来的字符串读出来

hello_drv_test 应用程序代码如下:

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

/* 该应用程序用法:
 * ./hello_drv_test -w abc	向hello驱动写入字符串 abc 
 * ./hello_drv_test -r		读取驱动程序中保存的数据
 */
int main(int argc, char **argv)
{
	int fd;
	char buf[1024];
	int len;
	
	/* 1. 判断参数 */
	if (argc < 2) 
	{
		printf("Usage: %s -w \n", argv[0]);
		printf("       %s -r\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open("/dev/hello", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file /dev/hello\n");
		return -1;
	}

	/* 3. 写文件或读文件 */
	if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
	{
		len = strlen(argv[2]) + 1;
		len = len < 1024 ? len : 1024;
		write(fd, argv[2], len);
	}
	else
	{
		len = read(fd, buf, 1024);		
		buf[1023] = '\0';
		printf("APP read : %s\n", buf);
	}
	
	close(fd);
	
	return 0;
}

4.3 驱动程序的Makefile文件

一个驱动程序最简单的Makefile包含以下内容:

# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin 
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,根据具体情况指定


# KERN_DIR 是指定内核源码的路径的,需要根据不同开发环境指定
KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.4	

all:
	make -C $(KERN_DIR) M=`pwd` modules 
	
	# 本条指令是用于编译应用程序的,放在这里是为了不用在单独编译应用程序而已
	$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order
	rm -f hello_drv_test

obj-m	+= hello_drv.o		# obj-m 指定具体要编译的驱动程序

5. 在开发板上测试运行hello驱动

编写完上述的代码之后,就可以进行编译了。

直接在hello驱动所在的目录下,输入 make 即可编译了。编译完之后就会看到生成对应的 .ko 文件了。

然后我们把编译好的 .ko 文件,和测试驱动的应用程序都拷到 nfs 共享目录下,我的 nfs 共享目录是在 /home/lbh/nfs 下。

然后我们打开开发板后,进入到开发板的控制台。挂载 ubuntu 中的 nfs 共享目录到开发板的 /mnt 目录下,在开发板输入如下命令:

mount -t nfs -o nolock,nfsvers=3 192.168.1.33:/home/lbh/nfs /mnt

其中 192.168.1.33 这个IP地址是你的ubuntu的IP地址。

挂载完成之后,就可以去 /mnt 目录下看到了自己编译好的 .ko 文件和对应的应用程序文件了。

我们执行 insmod hello_drv.ko 命令,就可以把该驱动程序安装到内核中了。而且可以看到内核打印出了相应的信息,如下:

[ 293.594910] hello_drv: loading out-of-tree module taints kernel.
[ 293.616051] /home/lbh/linux/drv/hello_drv.c hello_init line 70

说明驱动加载成功了。

注意:如果板子没有看到打印信息的话,那么就输入如下命令把内核的打印信息打开:

echo "7 4 1 7" > /proc/sys/kernel/printk

当然有些板子内核打印信息是默认已经打开了的。或者我们输入 demsg 命令也可以看到内核的打印信息。

然后我们运行 hello_drv_test 应用程序来测试内核,都可以看到内核的打印信息,和我们读取到应用程序写给内核的字符串。

6. 和驱动调试有关的其他知识

  1. cat /proc/devices 命令可以查看当前系统是否有我们刚刚安装的驱动程序
  2. 安装了设备驱动之后,就会在我们的Linux系统 /dev目录下生成对应的设备文件了。linux中没一个驱动程序都有一个与之对应的设备文件,可以使用 ls /dev/hello -l 命令查看该驱动文件

你可能感兴趣的:(Linux驱动开发基础,驱动开发,linux)