3. 字符设备驱动---上篇

news2024/11/15 4:20:51

文章目录

  • 引言
  • 一、字符设备驱动工作原理
    • 1.1 系统整体工作原理
    • 1.2 字符驱动模型
      • 1.2.1 file_operations结构体
      • 1.2.2 注册字符设备驱动 --- 申请主设备号
      • 1.2.3 内核如何管理字符设备驱动
  • 二、字符设备驱动代码实践
    • 2.1 编写字符设备驱动的步骤和规范
    • 2.2 实践写驱动代码
    • 2.3 代码实现
    • 2.4 驱动测试
  • 三、应用程序如何调用驱动
    • 3.1 驱动设备文件的创建
    • 3.2 添加读写接口
    • 3.3 应用和驱动之间的数据交换
    • 3.4 读写接口实践
    • 3.5 代码实现
      • 3.5.1 驱动源码
      • 3.5.2 应用层源码
      • 3.5.3 Makefile
    • 3.6 测试结果
  • 四、驱动中如何操控硬件
    • 4.1 硬件物理原理不变
    • 4.2 寄存器地址、编程方法改变
    • 4.3 内核的虚拟地址映射方法
      • 4.3.1 为什么需要虚拟地址映射
      • 4.3.2 内核中有2套虚拟地址映射方法
    • 4.4 如何选择虚拟地址映射方法
    • 4.5 操作寄存器地址的方式
  • 五、静态映射操作LED
    • 5.1 应用层app源代码
    • 5.2 驱动源代码
  • 六、动态映射操作LED
    • 6.1 如何建立动态映射
    • 6.2 如何销毁动态映射
    • 6.3 代码实践
  • 七、总结

本文用于复习字符设备驱动知识,从目前的认知层面进行整理,如有不足,恳请大家指出,一起沟通交流,以博会友。
注: Kernel-4.19内核代码阅读网站

引言

在Linux文件系统中,需要了解以下几点:

  • 每个文件都用一个struct inode结构体来描述,该结构体记录了这个文件的所有信息,例如文件类型,访问权限等。
  • 每个驱动程序在应用层的/dev目录或者其他如/sys目录下都会有一个文件与之对应。
  • 每打开一次文件,Linux操作系统会在VFS层分配一个struct file结构体来描述打开的文件。
  • 每个驱动程序都有一个设备号。(用在众多的设备驱动中进行区分)
  • 用户必须知道设备驱动对应的设备节点(设备文件)
  • linux把所有到设备都看成文件

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

1.1 系统整体工作原理

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

1.2 字符驱动模型

在这里插入图片描述

1.2.1 file_operations结构体

kernel-4.19/include/linux/fs.h

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 (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*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 *);
	unsigned long mmap_supported_flags;
	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 (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
    ......
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
			u64);
	int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,
			u64);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

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

1.2.2 注册字符设备驱动 — 申请主设备号

kernel-4.19/include/linux/fs.h

/* 
* 函数作用: 驱动向内核注册函数register_chrdev()
* 返回值 : int	返回0表示注册成功,返回一个负整数表示注册失败
* 参数 :	
* unsigned int major	主设备号(1~255),可人为向内核申请,不能与已有的设备号重复。(如果等于0,则采用系统动态分配的主设备号;不为0,则表示静态注册)
* const char *name	输入型参数,表示驱动设备的名字
* const struct file_operations *fops	输入型参数,用于内核注册的结构体指针
*/
static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)--------------------------------驱动向内核注册自己的file_operations
{
	return __register_chrdev(major, 0, 256, name, fops);
}

static inline 关键字修饰的函数,大部分表现和普通的static关键字修饰的函数一样。只不过在调用static inline 修饰的函数时,gcc会在其调用处,将其汇编码展开编译,而不为这个函数生成独立的汇编码,可以减少开销。(inline 减少函数调用开销

1.2.3 内核如何管理字符设备驱动

(1) 内核中有一个数组(最多有256个元素)用来存储注册的字符设备驱动,数组下标跟主设备号有关系
(2) register_chrdev() 内部将要注册的驱动信息存储在数组中相应的位置
(3) cat /proc/devices 查看内核中已经注册过的字符设备驱动和块设备驱动在这里插入图片描述

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

2.1 编写字符设备驱动的步骤和规范

    步骤:
        1,实现模块加载和卸载入口函数
                module_init(chr_dev_init);
                module_exit(chr_dev_exit);
        
        2,在模块加载入口函数中
            a, 申请主设备号  (内核中用于区分和管理不同字符设备)
                     register_chrdev(dev_major, "chr_dev_test", &my_fops);

            b,创建设备节点文件 (为用户提供一个可操作到文件接口--open())
                    struct  class *class_create(THIS_MODULE, "chr_cls");
                    struct  device *device_create(devcls, NULL, MKDEV(dev_major, 0), NULL, "chr2");

            c,  硬件的初始化
                   1,地址的映射
                        gpx2conf = ioremap(GPX2_CON, GPX2_SIZE);
                   2,中断的申请
                   3,实现硬件的寄存器到初始化
                        // 需要配置gpio功能为输出
                        *gpx2conf &= ~(0xf<<28);
                        *gpx2conf |= (0x1<<28);
            d,实现file_operations ---------   核心工作量:file_operations及其元素填充、注册驱动
                    const struct file_operations my_fops = {
                            .open = chr_drv_open,
                            .read = chr_drv_read,
                            .write = chr_drv_write,
                            .release = chr_drv_close,
                    };

由上面的步骤可以看出,驱动开发是有一定的思路和框架的。说到底,就是给空模块添加驱动壳子,再具体实现功能细节。

2.2 实践写驱动代码

脑海里先有框架,要知道目标是什么。
细节代码不需要一个字一个字敲,可以到内核中去寻找参考代码复制过来改。
写下的所有代码必须心里清楚明白,不能似懂非懂。

(1) 定义file_operations结构体变量
在这里插入图片描述
(2) open和close函数原型确定、内容填充
在这里插入图片描述
(3) 注册驱动

  • 主设备号的选择 (cat /proc/devices 查看当前可用的主设备号,确定没用过的先随便定一个)
  • 返回值的检查

在这里插入图片描述
(4) 卸载驱动

在这里插入图片描述

2.3 代码实现

#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>           // 记得包含头文件<linux/fs.h>

#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");			// 描述模块的别名信息

2.4 驱动测试

(1) 编译等 make && make cp
(2) insmod 查看设备注册的现象
(3) rmmod 查看设备注销的现象
在这里插入图片描述

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

3.1 驱动设备文件的创建

  1. 手动创建–缺点/dev/目录中文件都是在内存中,断电后/dev/文件就会消失
1)设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。
2)使用mknod创建设备文件:mknod  /dev/xxx c 主设备号 次设备号

在这里插入图片描述

  1. 自动创建(通过udev/mdev机制)
struct class *class_create()  // 创建一个类
struct device *device_create() // 创建一个设备文件

3.2 添加读写接口

(1) 在驱动代码中添加(注意截图代码注释)
在这里插入图片描述
(2)在应用代码中添加

	write(fd, "helloworld2222", 14);
	read(fd, buf, 100);

3.3 应用和驱动之间的数据交换

(1) copy_from_user,用来将数据从用户空间复制到内核空间。
(2) copy_to_user,用来将数据从内核空间复制到用户空间。

kernel-4.19/include/linux/uaccess.h

static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
	if (likely(check_copy_size(to, n, false)))
		n = _copy_from_user(to, from, n);
	return n;
}

static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
	if (likely(check_copy_size(from, n, true)))
		n = _copy_to_user(to, from, n);
	return n;
}

注意:

  1. copy_from_user函数的返回值定义,和常规有点不同。返回值如果成功复制则返回0,如果复制失败,则返回没有成功复制剩下的字节数。
  2. linux在系统调用进入内核时,为什么要将参数从用户空间拷贝到内核空间?不能直接访问,或使用memcpy吗?非要使用copy_from_user才行吗?
      原因是cpu运行指令时,运行级别分为用户态和数据态,这样可以避免内核数据被污染。

3.4 读写接口实践

(1)完成write和read函数
在这里插入图片描述
在这里插入图片描述
(2)读写回环测试

	// 读写文件
	write(fd, "helloworld2222", 14);
	read(fd, buf, 100);
	printf("读出来的内容是:%s.\n", buf);

3.5 代码实现

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

3.5.1 驱动源码

#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>
#include <asm/uaccess.h>

#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");			    // 描述模块的别名信息

3.5.2 应用层源码

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


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

3.5.3 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

3.6 测试结果

在这里插入图片描述

四、驱动中如何操控硬件

4.1 硬件物理原理不变

  • 硬件操作接口(寄存器)不变
  • 硬件操作代码不变

4.2 寄存器地址、编程方法改变

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

4.3 内核的虚拟地址映射方法

4.3.1 为什么需要虚拟地址映射

更好维护,可移植。因为虚拟地址映射能够解决安全隐患、地址不确定问题,并缓解了效率的问题。
所谓虚拟地址映射就是从虚拟地址映射到物理地址,经由MMU内存管理单元映射到实际的物理地址。
注: 硬件 --> 物理地址(寄存器) --> MMU (MMU是实际管理内存的硬件) --> 虚拟地址 --> 驱动程序

4.3.2 内核中有2套虚拟地址映射方法

动态和静态

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

4.4 如何选择虚拟地址映射方法

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

4.5 操作寄存器地址的方式

在这里插入图片描述

   0,     
		led-- GPX2_7 —GPX2CON == 0x1100C40----------------gpio的输出功能配置寄存器
		GPX2DAT == 0x1100C44---------------------------------------数据寄存器
		将0x11000C40映射成虚拟地址
		对虚拟地址中的[31:28] = 0x1
    1, volatile unsigned long *gpxcon;
        *gpxcon &= ~(0xf<<28);
    
    2, readl/writel();
        u32 readl(const volatile void __iomem *addr)//从地址中读取地址空间的值
        void writel(unsigned long value , const volatile void __iomem *add) // 将value的值写入到addr地址
    
        例子:
            // gpio的输出功能配置寄存器
            u32 value = readl(led_dev->reg_virt_base);
            value &= ~(0xf<<28);
            value |= (0x1<<28)
            writel(value, led_dev->reg_virt_bas);    

        或者:
                *gpx2dat |= (1<<7);    // 数据寄存器
        替换成:
                writel( readl(led_dev->reg_virt_base + 4) | (1<<7),   led_dev->reg_virt_base + 4 );  // 操作地址时,+1 与+4的区别是因为数据类型不同。

五、静态映射操作LED

  1. 关于静态映射
  • 不同版本内核中静态映射表位置、文件名可能不同
  • 不同SoC的静态映射表位置、文件名可能不同
  • 所谓映射表其实就是头文件中的宏定义
  1. 三星版本内核中的静态映射表
    (1) 主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h
      CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。
      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
它们的层级关系如下:
在这里插入图片描述

  1. 裸机中的操作方法添加LED操作代码
    在这里插入图片描述
  2. 添加驱动中的写函数
  • 先定义好应用和用户之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭,应用向驱动写"flash"则驱动让LED闪烁。
  • 应用和驱动的接口定义做的尽量简单,譬如用1个字目来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。
    注:应用层一般用来满足用户需求,写更多的功能函数;驱动层一般只简单实现硬件的功能,比如控制LED的亮灭。
简言之, 上层玩策略,底层玩机制。
  1. 写应用来测试写函数
  2. 驱动和应用中来添加读功能
    注:内核中也有自己的memset()和strcmp(); 函数用法跟应用层一样,在驱动中使用时要包含头文件#include <linux/string.h>

5.1 应用层app源代码

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

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

5.2 驱动源代码

#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>		// arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include <linux/string.h>

#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");			// 描述模块的别名信息

六、动态映射操作LED

6.1 如何建立动态映射

(1) request_mem_region,向内核申请(报告)需要映射的内存资源。
(2) ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址

6.2 如何销毁动态映射

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

6.3 代码实践

(1) 2个寄存器分开独立映射
在这里插入图片描述
(2) 多个寄存器在一起映射
在这里插入图片描述

七、总结

常常认为,struct inode描述的是文件的静态信息,即这些信息很少会改变,而struct file描述的是动态信息,即对文件的操作的时候,struct file里面的信息经常会发生变化。典型的是struct file结构体里面的f_ops(记录当前文件的位移量),每次读写一个普通文件时f_ops的值都会发生改变。此处借用kavin.zhu大佬制作的图,就不重复造轮子了。
在这里插入图片描述
从上图可以知道,如果想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来。

(1) open()函数打开设备文件时,根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。

kernel-4.19/include/linux/fs.h

struct inode {
	umode_t			i_mode;-------------------- 记录设备类型
	unsigned short		i_opflags;
	kuid_t			i_uid;
	kgid_t			i_gid;
	unsigned int		i_flags;

	dev_t			i_rdev;  ------------------------------- 记录设备号
	loff_t			i_size;

	/* Misc */
	const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
	struct file_lock_context	*i_flctx;
	struct address_space	i_data;
	struct list_head	i_devices;
	union {
		struct pipe_inode_info	*i_pipe;
		struct block_device	*i_bdev;
		struct cdev		*i_cdev; ----------------------------记录描述字符设备的结构体
		char			*i_link;
		unsigned		i_dir_seq;
	};

	__u32			i_generation;

	void			*i_private; /* fs or device private pointer */
}

struct file {
    ...
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;
	......
} 

(2) 根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口。
(3) 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体的i_cdev成员中,将struct cdev结构体中的函数操作接口地址记录在struct file结构体的f_ops成员中。
(4) 任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层应用程序就可以通过fd找到struct file,然后在有struct file找到操作字符设备的函数接口了。

注:大部分接口都是成对使用的,比如 : register_chrdev与unregister_chrdev。类似的学习博文

KERN_ERR、PTR_ERR 与 IS_ERR接口在编写驱动中的应用。

if(IS_ERR(led_dev->dev))
{
	printk(KERN_ERR "device_create error\n");  // KERN_ERR 
	ret = PTR_ERR(led_dev->dev);  //将指针出错的具体原因转换成一个出错码
	goto err_2;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/383058.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Matplotlib精品学习笔记001——绘制3D图形详解+实例讲解

3D图片更生动&#xff0c;或许在时间序列数据的展示上更胜一筹 想法&#xff1a; 学习3D绘图的想法来自科研绘图中。我从事的专业是古植物学&#xff0c;也就是和植物化石打交道。化石有三大信息&#xff1a;1.物种信息&#xff0c;也就是它的分类学价值&#xff1b;2.时间信息…

RealBasicVSR模型转成ONNX以及用c++推理

文章目录安装RealBasicVSR的环境1. 新建一个conda环境2. 安装pytorch(官网上选择合适的版本)版本太低会有问题3. 安装 mim 和 mmcv-full4. 安装 mmedit下载RealBasicVSR源码下载模型文件写一个模型转换的脚步测试生成的模型安装RealBasicVSR的环境 1. 新建一个conda环境 cond…

第一次做性能测试,有亿点点紧张

确认需求 确定性能需求和性能测试方案、需要确定性能测试范围&#xff08;覆盖哪些场景&#xff09;、性能测试策略、并发用户数和加压方式。 时间、人员、任务的分配安排&#xff0c;一般在总体测试计划中会预留性能测试的时间。性能测试方案是开展性能测试前的核心内容&…

时序预测 | MATLAB实现Rmsprop算法优化LSTM长短期记忆神经网络时间序列多步预测(滚动预测未来,多指标,含验证Loss曲线)

时序预测 | MATLAB实现Rmsprop算法优化LSTM长短期记忆神经网络时间序列多步预测(滚动预测未来,多指标,含训练和验证Loss曲线) 目录 时序预测 | MATLAB实现Rmsprop算法优化LSTM长短期记忆神经网络时间序列多步预测(滚动预测未来,多指标,含训练和验证Loss曲线)效果一览基本描…

Flume使用入门

目录 一. Flume简单介绍 1. Agent 2. Source 3. Sink 4. Channel 5. Event 二. 环境安装 1. 创建日志目录 2. 修改日志配置文件 3.修改运行堆内存 4. 确定日志打印的位置 5. 修改flume使用内存 内存调大 三. 校验flume 1. 安装netcat工具和net-tools工具 2. 判…

MySQL服务端与客户端之间的连接过程

服务器程序和客户端程序本质上是两个进程&#xff0c;所以连接过程的本质就是两个进程直接的通信&#xff0c;MySQL支持下面三种方式来进行通信。一&#xff1a;TCP/IP数据库服务器进程和客户端进程可能运行在不同的主机中&#xff0c;需要通过网络来进行通信二&#xff1a;命名…

初识scrapy

认识scrapyscrapy是一个为了爬取网站数据&#xff0c;提取结构性数据而编写的应用框架&#xff0c;我们只需实现少量的代码&#xff0c;就能实现数据的快速抓取scrapy使用了Twisted异步网络架构&#xff0c;可以加快下载速度 pip install twisted安装&#xff1a;pip install s…

进程控制(详解)

进程控制上篇文章介绍了进程的相关概念&#xff0c;形如进程的内核数据结构task_struct 、进程是如何被操作系统管理的、进程的查看、进程标识符、进程状态、进程优先级、已经环境变量和进程地址空间等知识点&#xff1b; 本篇文章接着上篇文章继续对进程的控制进行展开&#x…

Spark 内存运用

RDD Cache 当同一个 RDD 被引用多次时&#xff0c;就可以考虑进行 Cache&#xff0c;从而提升作业的执行效率 // 用 cache 对 wordCounts 加缓存 wordCounts.cache // cache 后要用 action 才能触发 RDD 内存物化 wordCounts.count// 自定义 Cache 的存储介质、存储形式、副本…

【OJ比赛日历】快周末了,不来一场比赛吗? #03.04-03.10 #12场

CompHub 实时聚合多平台的数据类(Kaggle、天池…)和OJ类(Leetcode、牛客…&#xff09;比赛。本账号同时会推送最新的比赛消息&#xff0c;欢迎关注&#xff01;更多比赛信息见 CompHub主页 或 点击文末阅读原文以下信息仅供参考&#xff0c;以比赛官网为准目录2023-03-04&…

从100%进口到自主可控,从600块降到10块,中科院攻克重要芯片

前言 2月28日&#xff0c;“20多位中科院专家把芯片价格打到10块”冲上微博热搜&#xff0c;据河南省官媒大象新闻报道&#xff0c;热搜中提到的中科院专家所在企业为全球最大的PLC分路器芯片制造商仕佳光子&#xff0c;坐落于河南鹤壁。 为实现芯片技术自主可控自立自强&#…

根据栅格数据的范围和像元大小生成等比例的矢量数据

为啥会有这么一个需求呢&#xff0c;后面要是继续写的话会详细说&#xff0c;首先是有一个栅格数据&#xff0c;比如这样的&#xff1a;我的目标是这样的&#xff08;矢量&#xff09;&#xff0c;就是这样&#xff0c;上面的栅格数据像大小是2*2的&#xff0c;直接上代码&…

【JAVA程序设计】【C00109】基于SSM(非maven)的员工工资管理系统

基于SSM&#xff08;非maven&#xff09;的员工工资管理系统项目简介项目获取开发环境项目技术运行截图项目简介 基于ssm框架非maven开发的企业工资管理系统共分为二个角色&#xff1a;系统管理员、员工 管理员角色包含以下功能&#xff1a; 系统后台登陆、管理员管理、员工信…

Linux基础命令-nice调整进程的优先级

文章目录 Nice 命令介绍 语法格式 常用参数 参考实例 1 调整bash的优先级为-10 2 调整脚本的优先级为6 3 调整指令的优先级 4 默认使用nice命令调整优先级 命令总结 Nice 命令介绍 nice命令的主要功能是用于调整进程的优先级&#xff0c;合理分配系统资源。Linux系…

torchaudio的I/O函数

info、load、save1.1 infotorchaudio.info(filepath: str, ...)Fetch meta data of an audio file. Refer to torchaudio.backend for the detail.返回音频文件的meta信息这里的meta元信息包括采样率、帧数、通道数、量化位数、音频格式info torchaudio.info(rE:\adins\data\2…

Java使用DFA算法实现敏感词过滤

1 前言敏感词过滤就是你在项目中输入某些字&#xff08;比如输入xxoo相关的文字时&#xff09;时要能检测出来&#xff0c;很多项目中都会有一个敏感词管理模块&#xff0c;在敏感词管理模块中你可以加入敏感词&#xff0c;然后根据加入的敏感词去过滤输入内容中的敏感词并进行…

测试软件5

一 css基础 css定义&#xff1a;可以设置网页中的样式&#xff0c;外观&#xff0c;美化 css中文名字&#xff1a;级联样式表&#xff0c;层叠样式表&#xff0c;样式表 二 css基础语法 1.style标签写在title标签后面 2.选择器{属性名1&#xff1a;属性值1&#xff1b;属性名…

ChatGPT最牛应用,让它帮你更新网站新闻吧!

谁能想到&#xff0c;ChatGPT火了&#xff01;既能对话入流&#xff0c;又能写诗歌论文、出面试题、编代码&#xff0c;甚至还通过了谷歌面试拿到L3工程师offer&#xff0c;放在一年之前&#xff0c;没人相信这是当前AI能够达到的水平。ChatGPT自面世以来&#xff0c;凭借其极为…

【数据结构初阶】手把手带你实现栈

前言 在进入数据结构初阶的学习之后&#xff0c;我们学习了顺序表和链表&#xff0c;当然栈也是一种特殊的数据结构&#xff0c;他的特点是后进先出。 栈的概念及结构 栈&#xff08;stack&#xff09;又名堆栈&#xff0c;它是一种运算受限的线性表。限定仅在表尾进行插入和删…

iptables的介绍

iptables简介 1、 什么是iptables&#xff1f; iptables是linux防火墙工作在用户空间的管理工具&#xff0c;是 netfilter/iptables IP信息包过滤系统的一部分&#xff0c;用来设置、维护和检查Linux内核的IP数据包过滤规则 2、 iptables特点 iptables是基于内核的防火墙&…