Linux驱动开发(2)------- 字符设备驱动基础

目录

  • 一,字符设备驱动工作原理
  • 二,字符设备驱动代码实践
  • 三,应用程序如何调用驱动
    • 1.驱动设备文件的创建
    • 2.添加读写接口
    • 3.读写接口实践
  • 四,驱动中如何操控硬件
    • 1.静态映射操作LED
    • 2.动态映射操作LED


一,字符设备驱动工作原理


1、系统整体工作原理

(1)应用层->API->设备驱动->硬件
(2)API:open、read、write、close等
(3)驱动源码中提供真正的open、read、write、close等函数实体

2、file_operations结构体

Linux驱动开发(2)------- 字符设备驱动基础_第1张图片

(1)元素主要是函数指针,用来挂接实体函数地址
(2)每个设备驱动都需要一个该结构体类型的变量
(3)设备驱动向内核注册时提供该结构体类型的变量

3、注册字符设备驱动
驱动向内核注册函数register_chrdev()在(#include )中

在这里插入图片描述

(1)作用,驱动向内核注册自己的file_operations
(2)参数和返回值:

返回值
int 返回0表示注册成功,返回一个负整数表示注册失败
参数
unsigned int major 主设备号,可人为向内核申请,不能与已有的设备号重复
const char *name 输入型参数,表示驱动设备的名字
const struct file_operations *fops 输入型参数,用于内核注册的结构体指针

4、内核如何管理字符设备驱动
(1)内核中有一个数组(最多有256个元素)用来存储注册的字符设备驱动数组下标跟主设备号有关系
(2)register_chrdev内部将我们要注册的驱动的信息存储在数组中相应的位置
(3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)

Linux驱动开发(2)------- 字符设备驱动基础_第2张图片


二,字符设备驱动代码实践


1、思路和框架
(1)目的:给空模块添加驱动壳子
(2)核心工作量:file_operations及其元素填充、注册驱动

2、如何动手写驱动代码
(1)脑海里先有框架,知道自己要干嘛
(2)细节代码不需要一个字一个字敲,可以到内核中去寻找参考代码复制过来改
(3)写下的所有代码必须心里清楚明白,不能似懂非懂

3、开始动手
(1)先定义file_operations结构体变量
(2)open和close函数原型确定、内容填充

Linux驱动开发(2)------- 字符设备驱动基础_第3张图片
Linux驱动开发(2)------- 字符设备驱动基础_第4张图片

4、注册驱动
(1)主设备号的选择(cat /proc/devices 查看当前可用的主设备号,确定没用过的先随便定一个)
(2)返回值的检测
Linux驱动开发(2)------- 字符设备驱动基础_第5张图片
(3)模块卸载

Linux驱动开发(2)------- 字符设备驱动基础_第6张图片

5、驱动测试
(1)编译等 make && make cp
(2)insmod并且查看设备注册的现象
(3)rmmod并且查看设备注销的现象

Linux驱动开发(2)------- 字符设备驱动基础_第7张图片

6、让内核自动分配主设备号
在这里插入图片描述
到此阶段的源码:(makefile还不用改,记得包含头文件

#include 		// module_init  module_exit
#include 			// __init   __exit
#include 

#define MYMAJOR		200
#define MYNAME		"testchar"
int mymajor;

static int test_chrdev_open(struct inode *inode, struct file *file)
{
	// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
	// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
	printk(KERN_INFO "test_chrdev_open\n");
	
	return 0;
}

static int test_chrdev_release(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "test_chrdev_release\n");
	
	return 0;
}

// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
	.owner		= THIS_MODULE,				// 惯例,直接写即可
	
	.open		= test_chrdev_open,			// 将来应用open打开这个设备时实际调用的
	.release	= test_chrdev_release,		// 就是这个.open对应的函数
};


// 模块安装函数
static int __init chrdev_init(void)
{	
	printk(KERN_INFO "chrdev_init helloworld init\n");

	// 在module_init宏调用的函数中去注册字符设备驱动
	// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
	// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数
	mymajor = register_chrdev(0, MYNAME, &test_fops);
	if (mymajor < 0)
	{
		printk(KERN_ERR "register_chrdev fail\n");
		return -EINVAL;
	}
	printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);

	return 0;
}

// 模块卸载函数
static void __exit chrdev_exit(void)
{
	printk(KERN_INFO "chrdev_exit helloworld exit\n");
	
	// 在module_exit宏调用的函数中去注销字符设备驱动
	unregister_chrdev(mymajor, MYNAME);
	
}

module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证
MODULE_AUTHOR("aston");				// 描述模块的作者
MODULE_DESCRIPTION("module test");	// 描述模块的介绍信息
MODULE_ALIAS("alias xxx");			// 描述模块的别名信息

三,应用程序如何调用驱动


1.驱动设备文件的创建

(1)设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。
(2)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号

在这里插入图片描述

2.添加读写接口

1、在驱动代码中添加
Linux驱动开发(2)------- 字符设备驱动基础_第8张图片

2、在应用代码中添加

Linux驱动开发(2)------- 字符设备驱动基础_第9张图片

3、应用和驱动之间的数据交换
(1)copy_from_user,用来将数据从用户空间复制到内核空间
(2)copy_to_user,用来将数据从内核空间复制到用户空间
注意:复制是和mmap的映射相对应去区分的

Linux驱动开发(2)------- 字符设备驱动基础_第10张图片
注意:copy_from_user函数的返回值定义,和常规有点不同。返回值如果成功复制则返回0,如果 不成功复制则返回尚未成功复制剩下的字节数

3.读写接口实践

1、完成write和read函数

Linux驱动开发(2)------- 字符设备驱动基础_第11张图片
Linux驱动开发(2)------- 字符设备驱动基础_第12张图片

2、读写回环测试
Linux驱动开发(2)------- 字符设备驱动基础_第13张图片

Linux驱动开发(2)------- 字符设备驱动基础_第14张图片

3、总结
(1)目前为止应用已经能够读写驱动(中的内存)
(2)后续工作:添加硬件操作代码

附上到此为止的源码:
驱动源码

#include 		// module_init  module_exit
#include 			// __init   __exit
#include 
#include 

#define MYMAJOR		200
#define MYNAME		"testchar"

int mymajor;

char kbuf[100];			// 内核空间的buf

static int test_chrdev_open(struct inode *inode, struct file *file)
{
	// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
	// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
	printk(KERN_INFO "test_chrdev_open\n");
	
	return 0;
}

static int test_chrdev_release(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "test_chrdev_release\n");
	
	return 0;
}

ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
	int ret = -1;
	
	printk(KERN_INFO "test_chrdev_read\n");
	
	ret = copy_to_user(ubuf, kbuf, count);
	if (ret)
	{
		printk(KERN_ERR "copy_to_user fail\n");
		return -EINVAL;
	}
	printk(KERN_INFO "copy_to_user success..\n");
		
	return 0;
}

// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,
	size_t count, loff_t *ppos)
{
	int ret = -1;
	
	printk(KERN_INFO "test_chrdev_write\n");

	// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
	//memcpy(kbuf, ubuf);		// 不行,因为2个不在一个地址空间中
	ret = copy_from_user(kbuf, ubuf, count);
	if (ret)
	{
		printk(KERN_ERR "copy_from_user fail\n");
		return -EINVAL;
	}
	printk(KERN_INFO "copy_from_user success..\n");

	// 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据
	// 去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码
	
	return 0;
}


// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
	.owner		= THIS_MODULE,				// 惯例,直接写即可
	
	.open		= test_chrdev_open,			// 将来应用open打开这个设备时实际调用的
	.release	= test_chrdev_release,		// 就是这个.open对应的函数
	.write 		= test_chrdev_write,
	.read		= test_chrdev_read,
};


// 模块安装函数
static int __init chrdev_init(void)
{	
	printk(KERN_INFO "chrdev_init helloworld init\n");

	// 在module_init宏调用的函数中去注册字符设备驱动
	// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
	// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数
	mymajor = register_chrdev(0, MYNAME, &test_fops);
	if (mymajor < 0)
	{
		printk(KERN_ERR "register_chrdev fail\n");
		return -EINVAL;
	}
	printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);

	return 0;
}

// 模块卸载函数
static void __exit chrdev_exit(void)
{
	printk(KERN_INFO "chrdev_exit helloworld exit\n");
	
	// 在module_exit宏调用的函数中去注销字符设备驱动
	unregister_chrdev(mymajor, MYNAME);
	
}

module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证
MODULE_AUTHOR("aston");				// 描述模块的作者
MODULE_DESCRIPTION("module test");	// 描述模块的介绍信息
MODULE_ALIAS("alias xxx");			// 描述模块的别名信息

应用层源码

#include 
#include 
#include 
#include 


#define FILE	"/dev/test"			// 刚才mknod创建的设备文件名

char buf[100];

int main(void)
{
	int fd = -1;
	
	fd = open(FILE, O_RDWR);
	if (fd < 0)
	{
		printf("open %s error.\n", FILE);
		return -1;
	}
	printf("open %s success..\n", FILE);
	
	// 读写文件
	write(fd, "helloworld2222", 14);
	read(fd, buf, 100);
	printf("读出来的内容是:%s.\n", buf);
	
	// 关闭文件
	close(fd);
	
	return 0;
}

makefile:

#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个
#KERN_VER = $(shell uname -r)
#KERN_DIR = /lib/modules/$(KERN_VER)/build	

		
# 开发板的linux内核的源码树目录
KERN_DIR = /root/driver/kernel

obj-m	+= module_test.o

all:
	make -C $(KERN_DIR) M=`pwd` modules 
	arm-linux-gcc app.c -o app

cp:
	cp *.ko /x210_porting/rootfs/rootfs/driver_test
	cp app /x210_porting/rootfs/rootfs/driver_test
	

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



四,驱动中如何操控硬件


1、还是那个硬件
(1)硬件物理原理不变
(2)硬件操作接口(寄存器)不变
(3)硬件操作代码不变

2、哪里不同了?
(1)寄存器地址不同。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
(2)编程方法不同裸机中习惯直接用指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性

3、内核的虚拟地址映射方法
(1)为什么需要虚拟地址映射:更好维护,可移植
(2)内核中有2套虚拟地址映射方法:动态和静态

(3)静态映射方法的特点
内核移植时以代码的形式硬编码(实质就是宏定义),如果要更改必须改源代码后重新编译内核
内核启动时建立静态映射表,到内核关机时销毁,中间一直有效
对于移植好的内核,你用不用他都在那里

(4)动态映射方法的特点
驱动程序根据需要随时动态的建立映射、使用、销毁映射,映射是短期临时的

4、如何选择虚拟地址映射方法
(1)2种映射并不排斥,可以同时使用
(2)静态映射类似于C语言中全局变量动态方式类似于C语言中malloc堆内存
(3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)

1.静态映射操作LED

1、关于静态映射
(1)不同版本内核中静态映射表位置、文件名可能不同
(2)不同SoC的静态映射表位置、文件名可能不同
(3)所谓映射表其实就是头文件中的宏定义

2、三星版本内核中的静态映射表
(1)主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h

CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器
map-s5p.h中定义的就是要用到的几个模块的寄存器基地址。
map-s5p.h中定义的是模块的寄存器基地址的虚拟地址。

(2)虚拟地址基地址定义在:arch/arm/plat-samsung/include/plat/map-base.h
#define S3C_ADDR_BASE (0xFD000000) // 三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的

(3)GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h
表中是GPIO的各个端口的基地址的定义
(4)GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h

它们的层级关系

Linux驱动开发(2)------- 字符设备驱动基础_第15张图片

3、参考裸机中的操作方法添加LED操作代码

Linux驱动开发(2)------- 字符设备驱动基础_第16张图片

4、添加驱动中的写函数
(1)先定义好应用和用户之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭,应用向驱动写"flash"则驱动让LED闪烁
(2)应用和驱动的接口定义做的尽量简单,譬如用1个字目来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。

注:应用层一般用来满足用户需求,写更多的功能函数;驱动层一般只简单实现硬件的功能,比如控制LED的亮灭。

5、写应用来测试写函数

6、驱动和应用中来添加读功能
注:内核中也有自己的menset()和strcmp();函数用法跟应用层一样,在驱动中使用时要包含头文件#include

应用层app源代码

#include 
#include 
#include 
#include 
#include 

#define FILE	"/dev/test"			// 刚才mknod创建的设备文件名

char buf[100];

int main(void)
{
	int fd = -1;
	int i = 0;
	
	fd = open(FILE, O_RDWR);
	if (fd < 0)
	{
		printf("open %s error.\n", FILE);
		return -1;
	}
	printf("open %s success..\n", FILE);

	while (1)
	{
		memset(buf, 0 , sizeof(buf));
		printf("请输入 on | off \n");
		scanf("%s", buf);
		if (!strcmp(buf, "on"))
		{
			write(fd, "1", 1);
		}
		else if (!strcmp(buf, "off"))
		{
			write(fd, "0", 1);
		}
		else if (!strcmp(buf, "flash"))
		{
			for (i=0; i<3; i++)
			{
				write(fd, "1", 1);
				sleep(1);
				write(fd, "0", 1);
				sleep(1);
			}
		}	
		else if (!strcmp(buf, "quit"))
		{
			break;
		}
	}
	
	// 关闭文件
	close(fd);
	
	return 0;
}

驱动源代码

#include 		// module_init  module_exit
#include 			// __init   __exit
#include 
#include 
#include 
#include 		// arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include 

#define MYMAJOR		200
#define MYNAME		"testchar"

#define GPJ0CON		S5PV210_GPJ0CON
#define GPJ0DAT		S5PV210_GPJ0DAT
#define rGPJ0CON	*((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT	*((volatile unsigned int *)GPJ0DAT)

int mymajor;
char kbuf[100];			// 内核空间的buf

static int test_chrdev_open(struct inode *inode, struct file *file)
{
	// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
	// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
	printk(KERN_INFO "test_chrdev_open\n");
	
	rGPJ0CON = 0x11111111;
	rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));		// 亮
	
	return 0;
}

static int test_chrdev_release(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "test_chrdev_release\n");
	
	rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
	
	return 0;
}

ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
	int ret = -1;
	
	printk(KERN_INFO "test_chrdev_read\n");
	
	ret = copy_to_user(ubuf, kbuf, count);
	if (ret)
	{
		printk(KERN_ERR "copy_to_user fail\n");
		return -EINVAL;
	}
	printk(KERN_INFO "copy_to_user success..\n");
	
	
	return 0;
}

// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,
	size_t count, loff_t *ppos)
{
	int ret = -1;
	
	printk(KERN_INFO "test_chrdev_write\n");

	// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
	//memcpy(kbuf, ubuf);		// 不行,因为2个不在一个地址空间中
	memset(kbuf, 0, sizeof(kbuf));
	ret = copy_from_user(kbuf, ubuf, count);
	if (ret)
	{
		printk(KERN_ERR "copy_from_user fail\n");
		return -EINVAL;
	}
	printk(KERN_INFO "copy_from_user success..\n");
	
	if (kbuf[0] == '1')
	{
		rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
	}
	else if (kbuf[0] == '0')
	{
		rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
	}
		
	return 0;
}


// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
	.owner		= THIS_MODULE,				// 惯例,直接写即可
	
	.open		= test_chrdev_open,			// 将来应用open打开这个设备时实际调用的
	.release	= test_chrdev_release,		// 就是这个.open对应的函数
	.write 		= test_chrdev_write,
	.read		= test_chrdev_read,
};


// 模块安装函数
static int __init chrdev_init(void)
{	
	printk(KERN_INFO "chrdev_init helloworld init\n");

	// 在module_init宏调用的函数中去注册字符设备驱动
	// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
	// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数
	mymajor = register_chrdev(0, MYNAME, &test_fops);
	if (mymajor < 0)
	{
		printk(KERN_ERR "register_chrdev fail\n");
		return -EINVAL;
	}
	printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
	return 0;
}

// 模块卸载函数
static void __exit chrdev_exit(void)
{
	printk(KERN_INFO "chrdev_exit helloworld exit\n");	
	
	// 在module_exit宏调用的函数中去注销字符设备驱动
	unregister_chrdev(mymajor, MYNAME);
}

module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证
MODULE_AUTHOR("aston");				// 描述模块的作者
MODULE_DESCRIPTION("module test");	// 描述模块的介绍信息
MODULE_ALIAS("alias xxx");			// 描述模块的别名信息

makefile不变

2.动态映射操作LED

1、如何建立动态映射
(1)request_mem_region,向内核申请(报告)需要映射的内存资源

(2)ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址

2、如何销毁动态映射
(1)iounmap ,解除映射
(2)release_mem_region,申请释放
注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。

3、代码实践
(1)2个寄存器分开独立映射

Linux驱动开发(2)------- 字符设备驱动基础_第17张图片

(2)多个寄存器在一起映射

Linux驱动开发(2)------- 字符设备驱动基础_第18张图片
详解见我的博客字符设备驱动高级–多寄存器映射操作硬件

你可能感兴趣的:(#,朱有鹏物联网Linux驱动开发)