linux驱动之字符设备驱动(一)

先给自己打个广告,本人的微信公众号正式上线了,搜索:张笑生的地盘,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题,二维码如下:

本篇文章是为了记录学习韦东山老师的嵌入式linux教学视频的课程笔记,给大家一个购买韦东山老师视频的链接

一 前言引导

关于linux操作系统,相信做过嵌入式开发的人或多或少都有所了解,都听说过它是一个优秀的,开源的os,在嵌入式设备行业、服务器行业,我们无处不见linux的身影,在这里想给大家纠正一个不太好的观念(可能是我自己以前一直有这样一个误解),很多嵌入式开发人员会认为windows系统真的很糟糕,完全比不上linux,其实不是这样的,windows没有我们认为的那么糟糕,linux也没有我们认为的那么完善,存在即合理,一款os能够存在并能够在某一行业广泛使用,是有它的道理的。
在开始介绍linux字符设备驱动之前,想给大家一个我认为比较有效学习linux的方法。
1. 先熟悉了解并使用linux,这里的使用不是说让大家做linux kernel的移植,而是先熟悉在这个环境下办公。比如说,现状一个ubuntu的虚拟机,然后在这个虚拟机下面写c代码,编译并执行可执行程序。
2. 熟悉在linux环境下的交叉编译,比如基于cortex-M系列的ARM芯片程序的开发,如何交叉编译生成arm芯片的可执行程序,在此给大家推荐电子发烧友上面的韦东山老师的嵌入式linux学习课程的第一部分(不是打广告,不是打广告,不是打广告),虽说我也是自动控制专业出生,但是之前完51,msp430,pic单片机的时候,没有太多关注芯片底层的东西,也没有从系统的角度去理解一款SoC芯片的产品设计开发过程,学完韦老师的课程之后,真的受益颇多,以前很多一知半解、模棱两可的东西突然就都明白了。
3. 熟悉linux系统环境的各种command line的命令,与windows图形界面为主的os所不同的是,linux下面的命令行命令极其丰富(可能是我孤陋寡闻,windows也许也很丰富)
4. 学习linux系统的真正底层部分,包括uboot移植,kernel移植,busybox制作,文件系统制作等等;
5. 修炼内功,和第4步一起阅读源码,掌握linux系统的线程、进程调度原理,同步方式,通信机制等等;

二 what

上面说了那么一大堆正确的废话,接下来进入我们本篇主题,字符驱动设备开发,引入我们今天的几个问题

  1. 什么是字符驱动设备?
    linux系统驱动是用户访问底层硬件设备的桥梁,它将用户访问的底层硬件进行封装,使得用户不必关心底层硬件的操作,针对这些各种各样的设备驱动,linux系统将它们分为了三类:字符设备,块设备,网络设备(后两个在本篇中将不做介绍),如下图
    linux驱动之字符设备驱动(一)_第1张图片
    字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。

三 why

为什么需要字符驱动设备程序?我自己写一个驱动不就好了吗?
越是庞大的系统,越涉及多层次的协作,如果上层应用层开发人员完全没有底层硬件驱动的概念,如果没有这些设备驱动程序,他们将完全不懂如何开发。
在linux系统下,有一句很经典的话,就是"一切皆文件",上层业务开发人员可以把所有需要访问的硬件资源,当成文件一样去操作,那么对于文件的操作一般都有:打开文件、关闭文件、读文件、写文件,所以linux驱动开发需要对这些硬件资源实现这些操作:打开操作、关闭操作、读操作、写操作,linux系统会帮我们封装好这些调用流程,所以这里不得不说linux系统的强大,调用流程大致如下所示
linux驱动之字符设备驱动(一)_第2张图片

四 how

根据上面的分析,字符设备驱动中,我们需要实现的操作有:打开操作,读操作,写操作,关闭操作,这些已经是我们已经知道要做的准备了,那么是不是就只需要实现这些操作就可以了呢?我们还需要哪些准备工作,内核才能正确调用我们实现的这些操作呢?考虑如下几个问题
a. 怎么告诉内核,我们定义的这些操作函数?
b. 谁来调用?或者说驱动的入口在哪里?
请牢记这两个问题,下面我们先实现打开操作、读操作、写操作者三个函数的编写
打开操作,我们先设计好函数名以及函数主体部分,内部实现暂时不用管

static int first_drv_open(struct inode *inode, struct file *file)
{
    return 0;
}

同样地,写操作

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
    return 0;
}

以及读操作

static int first_drv_read(struct file *filp, char __user *buff, 
                                         size_t count, loff_t *offp)
{
    return 0;
}

三个函数的主体框架部分设计完,很多人可能会有疑问,为什么这些函数都需要返回值?为什么这些函数的参数必须是这样,缘由是什么?
这两个问题,从宽泛的角度来说,因为linux系统本身设计时,调用这些字符设备驱动程序的时候,就决定了这些函数的格式,我们设计的函数必须满足linux系统的要求,必须如此定义。针对初学者,我建议大家可以考虑忽略这些,没必要花很多时间在这些细节上,我们前期最主要的目的是如何编写一个字符设备驱动程序。
ok,实现完这三个函数的主体部分,回到我们开始提到的两个问题:
a. 怎么告诉linux内核
定义一个linux系统提供的file_operations结构体,来告诉内核

static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   first_drv_open,     
	.write	=	first_drv_write,	   
};

b. 驱动的入口在哪里
第一步 写一个入口初始化函数,我们这里的函数名为first_drv_init,在初始化字符设备的节点号的时候,我们采用了register_chrdev函数实现,这是一个比较老版本的实现函数,可以实现静态和动态注册两种方法,主要是通过给定的主设备号是否为0来进行区别,为0的时候为动态注册。
另外两个申请字符设备节点号的函数分别是register_chrdev_region以及alloc_chrdev_region,区别请读者自行查找,这里不展开来介绍

int major;
static int first_drv_init(void)
{
	major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 内核自动帮我们分配设备号

   /* 老版本使用的api为 class_create 和  class_device_create*/
   /*
	firstdrv_class = class_create(THIS_MODULE, "firstdrv");
	firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */
	*/

    /* 新版本使用的api为 class_create 和 device_create */
    firstdrv_class = class_create(THIS_MODULE, "firstdrv");
    firstdrv_class_dev = device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz");

	return 0;
}

第二步 利用momdule_init修饰

module_init(first_drv_init);

根据以上过程,我们就搭建了一个字符设备驱动程序的框架,目前还只是一个框架,还不能实现任何功能,如果各位读者想要验证该框架的功能,可以在每个我们自己实现的操作函数中增加打印,看看上层在调用open,write,read函数时,是否调用了我们字符设备驱动文件中的first_drv_open、first_drv_write、first_drv_read函数。
在此,再次强调一遍,上面的函数操作中,实际上我们使用了很多系统内核的结构体数据结构定义,对于初学者没必要花太多时间在这些结构体定义上,我们先主要掌握如何使用这些结构体就行了,等我们后期熟悉linux系统之后,我们可以花时间去研究这些数据结构定义。
给一个完整的字符设备驱动的完整代码,第5步中的测试就是基于该驱动

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

static struct class *firstdrv_class;
static struct class_device	*firstdrv_class_dev;

volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;


static int first_drv_open(struct inode *inode, struct file *file)
{
	//printk("first_drv_open\n");
	/* 配置GPF4,5,6为输出 */
	*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
	*gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
	return 0;
}

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
	int val;

	//printk("first_drv_write\n");

    /* 内核空间和用户空间数据传递 */
	copy_from_user(&val, buf, count); //	copy_to_user();

	if (val == 1)
	{
		// 点灯
		*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
	}
	else
	{
		// 灭灯
		*gpfdat |= (1<<4) | (1<<5) | (1<<6);
	}
	
	return 0;
}

static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   first_drv_open,     
	.write	=	first_drv_write,	   
};


int major;
static int first_drv_init(void)
{
	major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核

	/* 老版本使用的api为 class_create 和  class_device_create*/
    /*
	firstdrv_class = class_create(THIS_MODULE, "firstdrv");
	firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */
	*/

    /* 新版本使用的api为 class_create 和 device_create */
    firstdrv_class = class_create(THIS_MODULE, "firstdrv");
    firstdrv_class_dev = device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz");

	gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
	gpfdat = gpfcon + 1;

	return 0;
}

static void first_drv_exit(void)
{
	unregister_chrdev(major, "first_drv"); // 卸载
   
    /* 老版本使用的api
	class_device_unregister(firstdrv_class_dev);
	*/
	
	/* 新版本使用的api */
	device_unregister(firstdrv_class_dev);
	
	class_destroy(firstdrv_class);
	iounmap(gpfcon);
}

module_init(first_drv_init);
module_exit(first_drv_exit);


MODULE_LICENSE("GPL");


五 test

对于软件开发来说,写完一段功能性代码,一定会手动验证一下代码功能,那么针对字符设备驱动的代码功能,我们应该如何验证呢?

  1. 编写上层应用程序,调用libc提供的open,write函数,如下:
#include 
#include 
#include 
#include 

/* firstdrvtest on
 * firstdrvtest off
 */
int main(int argc, char **argv)
{
	int fd;
	int val = 1;

	fd = open("/dev/xyz", O_RDWR);
	if (fd < 0)
	{
		printf("can't open!\n");
        return 0;
	}
	if (argc != 2)
	{
		printf("Usage :\n");
		printf("%s \n", argv[0]);
		return 0;
	}

	if (strcmp(argv[1], "on") == 0)
	{
		val  = 1;
	}
	else
	{
		val = 0;
	}

	write(fd, &val, 4);
	return 0;
}
  1. 编译动态库,在驱动内核源文件目录下面,执行make
    驱动程序编译的时候,严重依赖内核,所以在makefile中指定好内核的绝对路径
    在这里插入图片描述
    同时,我们是希望编译生成动态库,一份完整的makefile如下:
KERN_DIR = /work/system/linux-2.6.22.6

all:
	make -C $(KERN_DIR) M=`pwd` modules 

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

obj-m	+= first_drv.o
  1. 编译应用程序并上传至nfs目录下
arm-linux-gcc -o firstdrvtest firstdrvtest.c
cp firstdrvtest /work/nfs_root/first_fs
  1. 上传至服务器
    因为我们采用的是网络文件系统启动的方式,所以我们将上面编译生成的动态库first_drv.so上传到我们的服务器:
cp first_drv.ko /work/nfs_root/first_fs

内容如下:
linux驱动之字符设备驱动(一)_第3张图片
6. 启动设备,查看当前设备号信息
因为我们设置的nfs启动方式,设备启动之后,会将服务器上的内容拷贝到设备本地,我们可以通过命令cat /proc/device查看当前设备信息
7. 加载动态库,忽略警告信息

insmod first_drv.ko
  1. 再次查看设备信息 cat /proc/device
  2. 执行可执行程序
    修改可执行程序的权限: chmod 777 firstdrvtest
    执行:./firstdrvtest on

Too be continued

你可能感兴趣的:(linux系统,linux系统,字符设备驱动)