嵌入式linux驱动开发-字符设备驱动

之前在嵌入式系统移植停留了一段时间,枯燥地看uboot、kernel和跟文件系统的源码感觉没有成就感,在这之前已经对嵌入式linux的启动流程有了大体的了解,因此先跳到驱动开发,体验学习一下子哈!!

Linux 中的三大类驱动:字符设备驱动、块设备驱动和网络设备驱动

字符设备驱动:IO口的驱动,比如点灯、 I2C、 SPI、音频等。
块设备驱动:存储器设备的驱动,比如 EMMC、 NAND、 SD 卡和 U 盘等存储设备
网络设备驱动:网络驱动,比如 USB WIFI,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。



Linux 下的应用程序是如何调用驱动程序的?
在这里插入图片描述
在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作

比如, led 灯/dev/led 的驱动文件,应用程序使用open()、close()、write()、read()等函数对文件操作,进而控制硬件。



字符设备驱动简介

应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。

在这里插入图片描述
如上图,从应用程序到具体驱动程序都有相应的函数与之对应,比如调用open 这个函数。

内核部分: Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。(听名字就知道是管理驱动文件操作的)

1588 struct file_operations {
1589 	struct module *owner;
1590 	loff_t (*llseek) (struct file *, loff_t, int);
1591 	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
1592 	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
1593 	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
1594 	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
1595 	int (*iterate) (struct file *, struct dir_context *);
1596 	unsigned int (*poll) (struct file *, struct poll_table_struct *);
1597 	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
1598 	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
1599 	int (*mmap) (struct file *, struct vm_area_struct *);
1600 	int (*mremap)(struct file *, struct vm_area_struct *);
1601 	int (*open) (struct inode *, struct file *);
1602 	int (*flush) (struct file *, fl_owner_t id);
1603 	int (*release) (struct inode *, struct file *);
1604 	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1605 	int (*aio_fsync) (struct kiocb *, int datasync);
......
1618 #ifndef CONFIG_MMU
1619 	unsigned (*mmap_capabilities)(struct file *);
1620 #endif
1621 };

file_operation 结构体中比较重要的、常用的函数:
第 1589 行, owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
第 1590 行, llseek 函数用于修改文件当前的读写位置。
第 1591 行, read 函数用于读取设备文件
第 1592 行, write 函数用于向设备文件写入(发送)数据
第 1596 行, poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
第 1597 行, unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
第 1598 行, compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
第 1599 行, mmap 函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
第 1601 行, open 函数用于打开设备文件。
第 1603 行, release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
第 1604 行, fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
第 1605 行, aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。

字符设备驱动开发步骤

在 Linux 驱动开发中肯定也是要初始化相应的外设寄存器, Linux 驱动开发中我们需要按照其规定的框架来编写驱动,所以说学 Linux 驱动开发重点是学习其驱动框架。

驱动模块的加载和卸载

Linux 驱动有两种运行方式:
第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。

调试时推荐使用将驱动编译成模块,在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。
驱动开发完成可以将驱动编译进Linux 内核中。

模块有加载卸载两种操作,

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

module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“insmod”命令加载驱动的时候, xxx_init 这个函数就会被调用。
module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。

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

1 /* 驱动入口函数 */
2 static int __init xxx_init(void)
3 {
4 		/* 入口函数具体内容 */
5 		return 0;
6 }
7 8
/* 驱动出口函数 */
9 static void __exit xxx_exit(void)
10 {
11 		/* 出口函数具体内容 */
12 }
13
14 /* 将上面两个函数指定为驱动的入口和出口函数 */
15 module_init(xxx_init);
16 module_exit(xxx_exit);

驱动编译完成以后扩展名为.ko的模块,终端命令常用insmod、rmmod,另外modprobe命令可以将具有依赖关系的模块一锅端的加载和卸载

//加载驱动模块
insmod drv.ko
或 modprobe drv.ko
//卸载驱动模块
rmmod drv.ko
或 modprobe -r drv.ko

字符设备注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备
(就是你不仅要加载自己的驱动模块,还要向内核注册申请,这样才能是合法公民)

字符设备的注册和注销函数原型:

static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)

major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。

注册函数register_chrdev()位于加载函数调用的驱动入口函数中;
注销函数unregister_chrdev()位于卸载函数调用的驱动出口函数中。

37 /* 驱动入口函数 */
38 static int __init xxx_init(void)
39 {
40 		/* 入口函数具体内容 */
41 		int retvalue = 0;
42
43 		/* 注册字符设备驱动 */
44 		retvalue = register_chrdev(200, "chrtest", &test_fops);
45 		if(retvalue < 0){
46 				/* 字符设备注册失败,自行处理 */
47 		}
48 		return 0;
49 }
50
51 /* 驱动出口函数 */
52 static void __exit xxx_exit(void)
53 {
54 		/* 注销字符设备驱动 */
55 		unregister_chrdev(200, "chrtest");
56 }

设备号不能和其他驱动文件冲突,可使用命令查看当前已经被使用掉的设备号:

cat /proc/devices

实现设备的具体操作函数

比如,对chrtest 设备进行驱动,对file_operations结构体类型的变量 test_fops进行初始化,内部存有驱动文件操作函数,用什么操作函数就添加什么操作函数

打开和关闭操作:最基本的要求;
读写操作:拥有读写一段缓冲区(内存)。

1 /* 打开设备 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 		/* 用户实现具体功能 */
5 		return 0;
6 }
7 
8 /* 从设备读取 */
9 static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
10 {
11 		/* 用户实现具体功能 */
12 		return 0;
13 }
14
15 /* 向设备写数据 */
16 static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
17 {
18 		/* 用户实现具体功能 */
19 		return 0;
20 }
21
22 /* 关闭/释放设备 */
23 static int chrtest_release(struct inode *inode, struct file *filp)
24 {
25 		/* 用户实现具体功能 */
26 		return 0;
27 }
28//初始化操作函数结构体变量
29 static struct file_operations test_fops = {
30 		.owner = THIS_MODULE,
31 		.open = chrtest_open,
32 		.read = chrtest_read,
33 		.write = chrtest_write,
34 		.release = chrtest_release,
35 };

添加 LICENSE 和作者信息

LICENSE 是必须添加的,否则的话编译的时候会报错,LICENSE 采用 GPL 协议。
作者信息可以添加也可以不添加。

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

总结

一个完整的字符设备驱动模板就上以上4条:

/*1、文件操作函数file_operations结构体 变量初始化*/
......
/*2、驱动入口函数注册 驱动出口函数注销*/
......
/*3、加载驱动模块 卸载驱动模块*/
......
/*4、license和作者信息*/
(执行流程:3调用22调用1

Linux 设备号

设备号的组成

Linux 中每个设备都有一个设备号,设备号由主设备号次设备号两部分组成,
主设备号表示某一个具体的驱动,
次设备号表示使用这个驱动的各个设备。

设备号由dev_t数据类型表示

typedef __kernel_dev_t dev_t;
......
typedef __u32 __kernel_dev_t;
......
typedef unsigned int __u32;
......
经过来回的定义,设备号就是c语言中unsigned int32位数据类型;
其中高 12 位为主设备号, 低 20 位为次设备号。

文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏函数):

6 #define MINORBITS 20
7 #define MINORMASK ((1U << MINORBITS) - 1)
8 
9 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
10 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
11 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

第 6 行,宏 MINORBITS 表示次设备号位数,一共是 20 位。
第 7 行,宏 MINORMASK 表示次设备号掩码。
第 9 行,宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
第 10 行,宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
第 11 行,宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。

设备号的分配

1、静态分配设备号
使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号,人为选择没有使用的设备号。
2、动态分配设备号
Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统自动给你一个没有被使用的设备号,这样就避免了冲突。
设备号的申请函数如下:

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

dev:	保存申请到的设备号。
baseminor: 次设备号起始地址, alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count: 要申请的设备号数量。
name:	设备名字。

设备号释放函数如下:

void unregister_chrdev_region(dev_t from, unsigned count)

from:要释放的设备号。
count: 表示从 from 开始,要释放的设备号数量。

chrdevbase 字符设备驱动开发实验

为了完整的编写一个字符设备驱动模块,假设一个虚拟设备: chrdevbase
设备描述:chrdevbase 设备有两个缓冲区,一个为读缓冲
区,一个为写缓冲区,这两个缓冲区的大小都为 100 字节。
应用程序描述:在应用程序中可以向 chrdevbase 设备的写缓冲区中写入数据,从读缓冲区中读取数据。

实验程序编写

实现功能描述:
应用程序调用 open 函数打开 chrdevbase 这个设备,
使用 write 函数向chrdevbase 的写缓冲区 writebuf 中写入数据(不超过 100 个字节),
使用 read 函数读取读缓冲区 readbuf 中的数据操作,
应用程序使用 close 函数关闭 chrdevbase 设备。

驱动程序chrdevbase.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: chrdevbase.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: chrdevbase驱动文件。
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/

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

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

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
	//printk("chrdevbase open!\r\n");
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t chrdevbase_read(struct file *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");
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t chrdevbase_write(struct file *filp, const char __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");
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
	//printk("chrdevbase release!\r\n");
	return 0;
}

/*
 * 设备操作函数结构体
 */
static struct file_operations chrdevbase_fops = {
	.owner = THIS_MODULE,	
	.open = chrdevbase_open,
	.read = chrdevbase_read,
	.write = chrdevbase_write,
	.release = chrdevbase_release,
};

/*
 * @description	: 驱动入口函数 
 * @param 		: 无
 * @return 		: 0 成功;其他 失败
 */
static int __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");
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __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("zuozhongkai");

分析:文件架构



头文件调用的都是linux内核文件;
4个驱动文件操作函数;
驱动文件操作函数结构体初始化;

驱动入口函数,注册设备;
驱动出口函数,注销设备;

加载驱动模块
卸载驱动模块

许可证
人名


printk函数 相当于 printf 的孪生兄妹, printf运行在用户态 printk 运行在内核态,printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件include/linux/kern_levels.h。

#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息*/

比如:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");	//0级别
printk("gsmi: Log Shutdown Reason\n");	//MESSAGE_LOGLEVEL_DEFAULT 默认级别为4

参数 filp 有个叫做 private_data 的成员变量, private_data 是个 void 指针,一般在驱动中将private_data 指向设备结构体,设备结构体会存放设备的一些属性。



用户程序chrdevbaseApp.c


C 库文件操作基本函数
编写测试 APP 就是编写 Linux 应用,需要用到 C 库里面和文件操作有关的一些函数,比如open、 read、 write 和 close 这四个函数。
①、 open 函数

int open(const char *pathname, int flags)

pathname:要打开的设备或者文件名。
flags: 文件打开模式,以下三种模式必选其一:
	O_RDONLY 只读模式
	O_WRONLY 只写模式
	O_RDWR 读写模式
	另外,<逻辑或> 来选择多种模式:
	O_APPEND 每次写操作都写入文件的末尾
	O_CREAT 如果指定文件不存在,则创建这个文件
	O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
	O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
	O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
	O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继I/O 设置为非阻塞
	DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
	O_RSYNC read 等待所有写入同一区域的写操作完成后再进行。
	O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O。

返回值:如果文件打开成功的话返回文件的文件描述符。

在 Ubuntu 中输入“man 2 open” 即可查看 open 函数的详细内容

②、close 函数

int close(int fd);

fd:要关闭的文件描述符。
返回值: 0 表示关闭成功,负值表示关闭失败。

③、read 函数
当然是从文件里读内容啦~需要open函数返回的文件描述符。

ssize_t read(int fd, void *buf, size_t count)

fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 数据读取到此 buf 中。
count: 要读取的数据长度,也就是字节数。
返回值: 读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败。

在 Ubuntu 中输入“man 2 read”命令即可查看 read 函数的详细内容。

④、write 函数

ssize_t write(int fd, const void *buf, size_t count);

fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 要写入的数据。
count: 要写入的数据长度,也就是字节数。
返回值: 写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败。


编写一个简单的测试 APP,控制驱动文件,测试 APP 运行在用户空间,测试 APP 很简单通过输入相应的指令来对 chrdevbase 设备执行读或者写操作。

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: chrdevbaseApp.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: chrdevbase驱测试APP。
其他	   	: 使用方法:./chrdevbase /dev/chrdevbase <1>|<2>
  			 argv[2] 1:读文件
  			 argv[2] 2:写文件		
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/

static char usrdata[] = {"usr data!"};   //写入的用户数据

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	char readbuf[100], writebuf[100];
	
	//要求只能传入3个参数
	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;
	}

	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;
}




分析:程序架构
调用C库函数;
打开文件;
读写文件;
关闭文件。


判断 argv[2]参数的值是 1 或 2 ,因为输入命令的时候其参数都是字符串格式的,因此需要借助 atoi 函数将字符串格式的数字转换为真实的数字



编译驱动程序和测试 APP

1、编译驱动程序chrdevbase.c 这个文件,将其编译为.ko 模块,创建Makefile 文件,路径自己改改:

KERNELDIR := /home/zxy/linux/kernel_lib/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

KERNELDIR 表示开发板所使用的 Linux 内核源码目录(原子官方linux内核源码,需要编译了好久),使用绝对路径,大家根据自己的实际情况填写即可。
CURRENT_PATH 表示当前路径,直接通过运行“pwd”命令来获取当前所处路径。
obj-m 表示将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块。
后面的modules 表示编译模块,
-C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。
M 表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件

2、编译 chrdevbaseApp.c生成可执行程序,这里差不多就是单纯的C语言程序,用交叉编译器arm-linux-gnueabihf-gcc编译即可:

//编译
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
//查看文件信息,chrdevbaseAPP 这个可执行文件是 32 位 LSB 格式, ARM 版本的,因此 chrdevbaseAPP 只能在 ARM 芯片下运行。
file chrdevbaseApp

3、传文件到开发板验证
左老师使用的 TFTP 从网络启动,并且使用 NFS 挂载网络根文件系统,说白了就是从uboot层面网络启动linux内核和挂在根文件系统,但是这能保存到DRAM中。
本人这里研究了一下ssh传输协议,实现linux系统client和server之间的文件互传:
1、两者连接在同一网络地址;
2、服务器安装并配置ssh server协议;
3、板子可以直接scp获取文件啦。
就是这么简单,有手就行。

具体把驱动文件chrdevbase.ko放到/lib/modules/4.1.15-gb8ddbbc目录中,就是放到这个一种库里;
应用程序chrdevApp放到home目录下或者其他正常的位置都行。

4、验证操作

insmod chrdevbase.ko	//加载驱动模块
cat /proc/devices		//查找能用的设备号
mknod /dev/chrdevbase c 200 0	//创建节点 “ 200”是设备的主设备号,“ 0”是设备的次设备号。

//操作这个设备节点
./chrdevbaseApp /dev/chrdevbase 1	
./chrdevbaseApp /dev/chrdevbase 2

rmmod chrdevbase.ko		//卸载驱动模块
lsmod	//查看某些模块还存不存在,

总结

这种简单的驱动模块,整体就传到板子上驱动文件.ko和应用程序文件。
驱动文件放到/lib下的库文件中,一直放着就行;
使用insmod命令加载,就是跟内核申请了一块儿空间;
使用mknod命令创建节点再/dev下,就是跟内核申请营业许可证;
APP可以放到任何正常的目录下,使用APP应用程序控制/dev下这个节点,就能实现驱动的控制了;
使用rmmod命令卸载,就是跟内核说一声退还这一块儿空间,APP就控制不了了。



遇到的大问题:
在这里插入图片描述
在加载驱动模块的时候出现的这个问题,原因是板子上的内核版本和编译驱动时候用到的内核不匹配。

解决办法: 强行将编译驱动的内核相关版本信息改成一样的,哪里不对改哪里。
内核顶层的Makefile中:

VERSION = 4
PATCHLEVEL = 1
SUBLEVEL = 15
EXTRAVERSION = -gb8ddbbc
NAME = Series 4800

手动修改或者图形界面配置两个文件

make menuconfig
System Type    --->
          Multiple platform selection    --->
                  [ ] ARMv6 based platforms (ARM11)
                   [*] ARMv7 based platforms (Cortex-A, PJ4, Scorpion, Krait)

.config文件和arch/arm/configs/imx_alientek_emmc_config文件:
CPU Core family selection,CPU内核全家桶中只留下----v7=y

然后再重新编译驱动:使用modinfo xxx.ko查看模块版本信息就和板子内核版本信息一样啦。

非常感谢原子论坛,好多问题都是从那上边儿找到的。



  • 4
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值