2018-04-13 13:48:20 qq_27997595 阅读数 847
  • NandFlash和iNand-1.11.ARM裸机第十一部分

    本期课程主要讲述了2种常见的块存储设备:NandFlash和iNand。分别从物理接口、协议、芯片内部存储原理、SoC中的控制器、代码实践分析等几个方面详细讲述这两种存储设备。本课程的目标是让大家对块设备及其接口协议有个框架性的了解,给将来学习linux驱动时的块设备驱动打下基础。

    6921 人正在学习 去看看 朱有鹏

驱动分类

1   常规分类

1.1       字符设备:以字节为最小访问单位的设备,通常支持open,close,read,write系统调用。如串口、LED、按键

1.2       块设备:以块为最小访问单位的设备(块一般为512字节或512字节的倍数),linux中允许块设备传送任意字节数。如硬盘、flash、SD卡。

1.3       网络接口:负责发送和接收数据报文,可以是硬件设备,如网卡,也可以是纯软件设备,如回环接口(lo)

2   总线分类法

               1.1             USB设备

               1.2             PCI设备

               1.3             平台总线设备

学习方法

1   构建驱动模型

2   添加硬件操作

3   驱动测试

硬件访问

1   地址映射

1.1 静态映射

void *ioremap(physaddr,size)

physaddr:待映射的物理地址

size:映射的区域长度

返回值:映射后的虚拟地址

1.2 动态映射

用户事先指定映射关系,在内核启动时自动将物理地址映射为虚拟地址。

struct map_desc

{

     unsigned long virtual;/映射后的虚拟地址

     unsigned long pfn;/物理地址所在的页帧号

     unsigned long length;/映射长度

     unsigned int type;/映射的设备类型

}

2   寄存器读写

unsigned ioread8(void *addr)

unsigned ioread16(void *addr)

unsigned readb(address)

unsigned readw(address)

unsigned readl(address)

 

void iowrite8(u8 value,void *addr)

void iowrite16(u16 value,void *addr)

void iowrite32(u32 value,void *addr)

void writeb(unsigned value, address)

void writew(unsigned value, address)

void writel(unsigned value, address)

驱动的运用

1   编译安装驱动

1.1 创建Makefile

obj-m := memdev.o

KDIR := /home/…/linux-tq2440

all:

make–C $( KDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux- ARCH=arm

1.2 拷贝驱动(内核模块)到/…/rootfs

1.3 安装驱动程序

insmod  memdev.ko

1.4 查看 lsmod

2   字符设备文件

2.1       查看主设备号

cat /proc/devices

主设备号   设备名

2.2       创建设备文件

mknod /dev/文件名 c 设备号 次设备号(0~255)

3   应用程序

3.1 打开字符设备文件

fd = open(“/dev/memdev0”,O_RDWR);

3.2 操作字符设备文件

write(fd,&buf,sizeof(int));

read(fd,&buf,sizeof(int));

3.3 关闭字符设备文件

close(fd);

3.4 编译(静态编译)

arm-linux-gcc –static 文件名.c –O文件名

字符设备驱动模型

1    设备描述结构cdev

1.1 结构定义

struct cdev

{

struct kobject kobl;

struct module *owner;

const struct file operations *ops ;//设备操作集

struct lis thead list;

dev_t dev;//设备号。

unsigned int count;设备数

}

1.2 设备号类型dev_t

1.2.1 dev_t介绍:实质为32位的unsigned int,高十二位为主设备号,低二十位为次设备号。

1.2.2 操作设备号:

dev_t dev=MKDEV(主设备号,次设备号)

主设备号=MAJOR(dev_t dev)

次设备号=MINOR(dev_t dev)

1.2.3 申请设备号:

静态申请:intregister_chrdev_region(dev_t first, unsigned int count, char *name);

first: 待分配的起始设备编号,通常为0;

count: 连续设备编号的总数

name:设备名(cat /proc/devices

动态申请:intalloc_chrdev_region(dev_t *dev,unsigned int -firstminor,unsigned int -count,char*name)

dev:得到的设备号保存位置

-firstminor: 待分配的起始设备编号,通常为0;

-count: 连续设备编号的总数

name: 设备名(cat /proc/devices

1.2.4 注销设备号:unregister_chardev_region

1.3 操作集struct file operations

1.3.1 structfile operations介绍:函数指针的集合,定义能在设备上进行的操作。对于不支持的操作设置为NULL

1.3.2 例:struct file operations dev_fops =

{

.llseek  = NULL;

.read= dev_read,

.write= dev_write,

.ioctl= dev_inctl,

.open= dev_open,

.release= dev_release,

};

2    驱动初始化

2.1 分配设备描述结构

    静态分配:structcdev mdev;

动态分配:structcdev *pdev = cdev_alloc();

2.2 初始化设备描述结构

cdev_init(struct cdev*cdev,const struct file_operations *fops)

*cdev:待初始化的cdev结构/*fops:设备对应的操作函数集

2.3 注册设备描述结构

    cdev_add(struct cdev *p,dev_t dev,unsignedcount)

p:待添加到内核的字符设备结构

dev:设备号

count:该类设备的设备个数

2.4 硬件初始化

3         设备操作(设备方法)

3.1 int  (*open) (struct inode *,struct file *)  打开设备,响应open系统

3.1.1 structfile:

3.1.1.1   linux系统中,每个打开的文件,都会关联一个structfile,由内核在文件打开时创建,文件关闭后释放

3.1.1.2   重要成员

loff_t f_pos   //文件读写指针

struct file_operation*f_op  //文件对应的操作

3.1.2 structinode

3.1.2.1   每个存在于文件系统的文件都会关联一个inode结构,记录文件物理信息。

3.1.2.2   重要成员:

dev_t i_rdev   //设备号

3.2 int  (*release) (struct inode *,struct file*)  关闭设备,响应close系统

3.3 loff_t  (*llseek) (struct file *,loff_t,int)  重定位读写指针,响应lseek系统调用

3.4 ssize_t(*read) (struct file *,char _user *,size_t,loff_t *)  从设备读取数据,响应read系统调用

3.4.1 参数分析:

filp:与字符设备文件关联的file结构指针,由内核创建

buff:从设备读到的数据需要保存的位置,由read系统调用提供

count:请求传输的数据量,由read系统调用提供

offp:文件读写位置,由内核从file结构中取出传递进来

3.4.2 从设备中读取数据(硬件访问类操作)

3.4.3 将读到的数据返回给应用程序

3.5 ssize_t(*write) (struct file *,const char _user *,size_t,loff_t *)  向设备写入数据,响应write系统调用

3.5.1 参数分析:

filp:与字符设备文件关联的file结构指针,由内核创建

buff:从设备读到的数据需要保存的位置,由read系统调用提供

count:请求传输的数据量,由read系统调用提供

offp:文件读写位置,由内核从file结构中取出传递进来

3.5.2 从应用程序提供的地址中取出数据

3.5.3 将数据写入设备(硬件访问类操作)

4         驱动注销

4.1       用cdev_del函数卸载驱动程序

2020-03-16 17:49:23 muchunpeng 阅读数 17
  • NandFlash和iNand-1.11.ARM裸机第十一部分

    本期课程主要讲述了2种常见的块存储设备:NandFlash和iNand。分别从物理接口、协议、芯片内部存储原理、SoC中的控制器、代码实践分析等几个方面详细讲述这两种存储设备。本课程的目标是让大家对块设备及其接口协议有个框架性的了解,给将来学习linux驱动时的块设备驱动打下基础。

    6921 人正在学习 去看看 朱有鹏

一、Linux三大驱动类型:

  1. 字符设备驱动:字符设备驱动最多,从最简单的点灯到 I2C、 SPI、音频等都属于字符设备驱动的类型。
  2. 块设备驱动:块设备驱动就是存储器设备的驱动,比如 EMMC、 NAND、 SD 卡和 U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。
  3. 网络设备驱动:不管是有线还是无线,都属于网络设备驱动。

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

二、字符设备驱动开发

1.字符驱动设备介绍

字符设备是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IC、SPI,LCD等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
Linux应用程序对驱动程序的调用流程
在Linux中一切皆为文件,驱动加载成功以后会在"/dev"目录下生成一个相应的文件,应用程序通过对这个名为"/dev/xxx"(xxx是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。

比如现在有个叫做/dev/led的驱动文件,此文件是led灯的驱动文件。应用程序使用open函数来打开文件/devled,使用完成以后使用close函数关闭/dev/led这个文件。open和close就是打开和关闭led驱动的函数,如果要点亮或关闭led,那么就使用write函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开led的控制参数。如果要获取led灯的状态,就用read函数从驱动中读取相应的状态。

应用程序运行在用户空间,驱动运行于内核空间。
当我们在用户空间想要实现对内核的操作,比如使用open函数打开/dev/led这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open.close,write和read等这些函数是由C库提供的,在Linux系统中,系统调用作为C库的一部分。
在这里插入图片描述

2.字符驱动开发步骤
2.1 Linux驱动有两种运行方式:

第一种就是将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序。
第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用"insmod"命令加载驱动模块。
在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个Linux代码。
而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。

2.2 模块有加载和卸载两种操作
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

“insmod”命令加载驱动的时候, xxx_init 这个函数就会被调用。
“rmmod”命令卸载具体驱动的时候, xxx_exit 函数就会被调用。

2.3 字符设备驱动模块加载和卸载模板
/* 驱动入口函数 */
static int __init xxx_init(void)
{
	/* 入口函数具体内容 */
	return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
	/* 出口函数具体内容 */
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

驱动编译完成以后扩展名为.ko,
驱动模块的加载使用命令“modprobe ”,比如要加载 drv.ko,命令如下:
modprobe drv.ko”
驱动模块的卸载使用命令“rmmod”即可,比如要卸载 drv.ko,命令如下:
rmmod drv.ko
也可以使用“modprobe -r”命令卸载驱动,比如要卸载 drv.ko,命令如下:
modprobe -r drv.ko

3.字符驱动注册于注销
/*
功能:注册字符设备
参数:
major:主设备号
name:设备名字,指向字符串
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
*/
static inline int register_chrdev(unsigned int major, const char *name,
									const struct file_operations *fops)
/*
功能:注销字符设备
参数:
major:主设备号
name:设备名字,指向字符串
*/
static inline void unregister_chrdev(unsigned int major, const char *name)

在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。

struct file_operations {
......
}
  1. owner拥有该结构体的模块的指针,一般设置为THISMODULE.
  2. Ilsek函数用于修改文件当前的读写位置。
  3. read函数用于读取设备文件。
  4. write函数用于向设备文件写入(发送)数据。
  5. poll是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
  6. unlocked ioctl函数提供对于设备的控制功能,与应用程序中的ioctl函数对应。
  7. compat ioctl函数与unlocked ioctl函数功能一样,区别在于在64位系统上,32位的应用程序调用将会使用此函数。在32位的系统上运行32位的应用程序调用的是unlocked ioctl
  8. mmap函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如LCD驱动的显存,将帧缓冲(LCD显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
  9. open函数用于打开设备文件
  10. release函数用于释放(关闭)设备文件,与应用程序中的close函数对应。
  11. fasync函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
  12. aio_fsync函数与fasync函数的功能类似,只是aio-fsync是异步刷新待处理的数据。
4.字符驱动框架实现模板
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>


#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("muchunpeng");

2018-12-22 11:29:00 Archar_Saber 阅读数 99
  • NandFlash和iNand-1.11.ARM裸机第十一部分

    本期课程主要讲述了2种常见的块存储设备:NandFlash和iNand。分别从物理接口、协议、芯片内部存储原理、SoC中的控制器、代码实践分析等几个方面详细讲述这两种存储设备。本课程的目标是让大家对块设备及其接口协议有个框架性的了解,给将来学习linux驱动时的块设备驱动打下基础。

    6921 人正在学习 去看看 朱有鹏

1.11 字符设备驱动模型

在任何一种驱动模型中,设备都会用内核中的一种结构来描述。字符设备在内核中使用struct cdev来描述。

struct cdev{
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;//设备操作函数集
    struct list_head list;
    dev_t dev;//设备号
    unsigned int count;//设备数
};

字符设备驱动模型的整体框架:

                    

1.11.1 设备号

1,主设备号和次设备号

字符设备文件与字符驱动程序通过主设备号建立起对应关系;驱动程序通过次设备号区分同类型的设备。

2,分配设备号

(1)静态申请

开发者自己选择一个数字作为主设备号,然后通过函数register_chrdev_region向内核申请使用。缺点:如果申请使用的设备号已经被内核中的其他驱动使用了,则申请失败。

int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char*name)
//参数
//dev:存放分配的设备号
//baseminor:次设备号的起始值
//count:设备数
//name:设备名

(2)动态申请

使用alloc_chrdev_region由内核分配一个可用的主设备号。优点:因为内核知道哪些号已经被使用了,所以不会导致分配到已经被使用的号。

int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)
//参数:
//dev:设备号
//baseminor:次设备起始值
//count:设备数
//name:设备名

3,操作设备号

linux内核中使用dev_t类型来定义设备号,dev_t这种类型其实质为32位的unsigned int ,其中高12位为主设备号,低20位次设备号。

(1)由主设备号和次设备号组合成dev_t(类型)

dev_t dev=MKDEV(主设备号,次设备号)

(2)从dev_t中分解出主设备号

主设备号=MAJOR(dev_t dev)

(3)从dev_t中分解出次设备号

次设备号=MINOR(dev_t dev)

4,注销设备号

不论使用什么方法分配设备号,都应该在驱动退出时,使用unregister_chrdev_region函数释放这些设备号。

1.11.2 操作函数集

struct file_operations是一个函数指针的集合,定义能在设备上进行的操作。

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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*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 *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,             
unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
	long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
};

结构中的函数指针指向驱动程序中的函数,这些函数实现一个针对设备的操作,对于不支持的操作则设置函数指针为NULL。例如

struct file_operation dev_fops={
    .llseek=NULL,
    .read=dev_read,
    .write=dev_write,
    .ioctl=dev_ioctl,
    .open=dev_open,
    .release=dev_release,
};

1.11.3 字符设备初始化

1,分配描述结构cdev

(1)静态分配

struct cdev mdev;//直接定义一个struct cdev类型的变量。

(2)动态分配

struct cdev *pdev=cdev_alloc();//使用cdev_alloc()动态分配

2,初始化描述结构 cdev

struct cdv的初始化使用cdev_init函数来完成。

cdev_init(struct cdev *cdev,const struct file_opertations *fops)
//参数:
//cdev:待初始化的cdev结构 ; fops:设备对应的操作函数集

3,注册描述结构cdev

字符设备的注册使用cdev_add函数来完成。

cdev_add(struct cdev*p,dev_t dev,unsigned count)
//参数:
//p:待添加到内核的字符设备结构
//dev:设备号
//count:该类设备的设备个数

 1.11.4 实现设备操作

struct file:在linux系统中,每一个打开的文件,在内核中都会关联一个struct file,它由内核在打开文件时创建,在文件关闭后释放。

loff_t f_pos;//文件读写指针
struct file_operation *f_op;//该文件所对应的操作

struct inode:每一个存在于文件系统里面的文件都会关联一个inode结构,该结构主要用来记录文件物理上的信息。因此,它和代表打开文件的file结构是不同的。一个文件没有被打开时不会关联file结构,但是却会关联一个inode结构。

dev_t i_rdev;//设备号

注意:(1)从应用程序提供的地址中取出数据;(2)将数据写入设备中;

要使用专门的函数:(1)int copy_from_user(void *to,const void __user *from,int n)

(2)int copy_to_user(void __user*to,const void *from,int n)

1.11.5 驱动注销

当我们从内核中卸载驱动程序时候,需要使用cdev_del函数来完成字符设备的注销

1.11.6 使用字符驱动程序

1,编译/安装字符设备

 在Linux系统中,驱动程序通常采用内核模块的程序结构来进行编码。因此,编译/安装一个驱动程序,其实质就是编译/安装一个内核模块。

2, 字符设备文件

通过字符设备文件,应用程序可以使用相应的字符设备驱动程序来控制字符设备。创建字符设备文件的方法一般有两种:
(1)使用mknod命令

mknod /dev/文件名 c 主设备号 次设备号

(2)使用函数在驱动程序中创建

                                                   

1.11.7 内存操作驱动实例

    /**********************************/
   /*                                */
  /*            驱动程序             */
 /*                                */
/**********************************/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>


int dev1_registers[5];
int dev2_registers[5];

struct cdev cdev; 
dev_t devno;

/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
    
    /*获取次设备号*/
    int num = MINOR(inode->i_rdev);
    
    if (num==0)
        filp->private_data = dev1_registers;
    else if(num == 1)
        filp->private_data = dev2_registers;
    else
        return -ENODEV;  //无效的次设备号
    
    return 0; 
}

/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
  return 0;
}

/*读函数*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  int *register_addr = filp->private_data; /*获取设备的寄存器基地址*/

  /*判断读位置是否有效*/
  if (p >= 5*sizeof(int))
    return 0;
  if (count > 5*sizeof(int) - p)
    count = 5*sizeof(int) - p;

  /*读数据到用户空间*/
  if (copy_to_user(buf, register_addr+p, count))
  {
    ret = -EFAULT;
  }
  else
  {
    *ppos += count;
    ret = count;
  }

  return ret;
}

/*写函数*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  int *register_addr = filp->private_data; /*获取设备的寄存器地址*/
  
  /*分析和获取有效的写长度*/
  if (p >= 5*sizeof(int))
    return 0;
  if (count > 5*sizeof(int) - p)
    count = 5*sizeof(int) - p;
    
  /*从用户空间写入数据*/
  if (copy_from_user(register_addr + p, buf, count))
    ret = -EFAULT;
  else
  {
    *ppos += count;
    ret = count;
  }

  return ret;
}

/* seek文件定位函数 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{ 
    loff_t newpos;

    switch(whence) {
      case SEEK_SET: 
        newpos = offset;
        break;

      case SEEK_CUR: 
        newpos = filp->f_pos + offset;
        break;

      case SEEK_END: 
        newpos = 5*sizeof(int)-1 + offset;
        break;

      default: 
        return -EINVAL;
    }
    if ((newpos<0) || (newpos>5*sizeof(int)))
    	return -EINVAL;
    	
    filp->f_pos = newpos;
    return newpos;

}

/*文件操作结构体*/
static const struct file_operations mem_fops =
{
  .llseek = mem_llseek,
  .read = mem_read,
  .write = mem_write,
  .open = mem_open,
  .release = mem_release,
};

/*设备驱动模块加载函数*/
static int memdev_init(void)
{
  /*初始化cdev结构*/
  cdev_init(&cdev, &mem_fops);
  
  /* 注册字符设备 */
  alloc_chrdev_region(&devno, 0, 2, "memdev");
  cdev_add(&cdev, devno, 2);
}

/*模块卸载函数*/
static void memdev_exit(void)
{
  cdev_del(&cdev);   /*注销设备*/
  unregister_chrdev_region(devno, 2); /*释放设备号*/
}

MODULE_LICENSE("GPL");

module_init(memdev_init);
module_exit(memdev_exit);


    /**********************************/
   /*                                */
  /*            应用程序             */
 /*                                */
/**********************************/
//write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	int fd = 0;
	int src = 2013;
	
	/*打开设备文件*/
	fd = open("/dev/memdev0",O_RDWR);
	
	/*写入数据*/
	write(fd, &src, sizeof(int));
	
	/*关闭设备*/
	close(fd);
	
	return 0;	

}

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

int main()
{
	int fd = 0;
	int dst = 0;
	
	/*打开设备文件*/
	fd = open("/dev/memdev0",O_RDWR);
	
	/*写入数据*/
	read(fd, &dst, sizeof(int));
	
	printf("dst is %d\n",dst);
	
	/*关闭设备*/
	close(fd);
	
	return 0;	

}


 

 

 

 

2020-03-30 22:12:45 qq_33866593 阅读数 19
  • NandFlash和iNand-1.11.ARM裸机第十一部分

    本期课程主要讲述了2种常见的块存储设备:NandFlash和iNand。分别从物理接口、协议、芯片内部存储原理、SoC中的控制器、代码实践分析等几个方面详细讲述这两种存储设备。本课程的目标是让大家对块设备及其接口协议有个框架性的了解,给将来学习linux驱动时的块设备驱动打下基础。

    6921 人正在学习 去看看 朱有鹏

学习规划

linux ARM驱动开发
ARM体系结构和汇编
ARM寄存器 ,工作方式
汇编,冒泡排序
ARM外设硬件
时钟
内存
flash
串口
看芯片手册(英文)
看电路原理图
ARM Bootloader
相当于PC机上BIOS
BOOT
把一个开发板上的相关小硬件设备驱动起来
Loader
把硬盘(flash)上面的操作系统(linux/android),加载到内存,然后去执行操作系统
U BOOT
tiny210开发板,选用的处理器核心cortexA8(1G Hz)
ARM中断编程*
软件中断
硬件中断
linux kernel 核心数据结构分析
VFS虚拟文件系统
file
kobject
数据结构
双向链表
红黑树(二叉树)
队列
linux驱动简介
字符设备*(牵扯到很多linux内核问题)
PC机模拟字符设备驱动编程
led灯字符设备驱动
块设备
网络设备
USB设备
linux中断
上半部
下半部
linux串口驱动
linux中断编程
字符设备设计
2016-05-21 12:52:36 z1106609486 阅读数 2818
  • NandFlash和iNand-1.11.ARM裸机第十一部分

    本期课程主要讲述了2种常见的块存储设备:NandFlash和iNand。分别从物理接口、协议、芯片内部存储原理、SoC中的控制器、代码实践分析等几个方面详细讲述这两种存储设备。本课程的目标是让大家对块设备及其接口协议有个框架性的了解,给将来学习linux驱动时的块设备驱动打下基础。

    6921 人正在学习 去看看 朱有鹏

18.1 ARM设备树简介

  • 设备舒适一种描述印鉴的数据结构,它起源于OpenFirmware(OF)
    • 采用设备树前后对比:
      • 采用设备树之前:ARM架构的板极硬件细节过多的被硬编码在arch/arm/plat-xxx和arch/arm/mach-xxx中
      • 采用设备树之后:许多硬件细节可以直接通过它传递给Linux,而不再需要在讷河中进行大量的冗余编码
    • 设备树的组成:由一系列被命名的节点(Node)和属性(Property)组成
      • 节点本身有包含子节点
      • 属性:成对出现的名称和值
    • 设备树中可描述的信息包括:
      • CPU的数量和类别
      • 内存基地址和大小
      • 总线和桥
      • 外设链接
      • 中断控制器和中断使用情况
      • GPIO控制器和GPIO使用情况
      • 时钟控制器核实中使用情况
    • 通俗描述:
      • 它基本上就是画一颗电路板上CPU、总线、设备组成的树
      • bootloader会将这棵树传递给内核
      • 内核可以识别这棵树,并根据他站开出Linux内核中的platform_device、i2c_client、spi_device等设备
      • 这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备

18.2 设备树的组成和结构

整个设备数牵涉买你比较广,既增加了新的用于描述设备硬件信息的文件格式,又增加了编译这个文本的工具,同时bootloader也需要支持将编译后的设备数传递给Linux内核

18.2.1 DTS、DTC和DTB等

18.2.1.1 DTS

  • DTS:文件.dts是一种ASCII文本格式的设备树描述,人性化适合阅读
    • .dts的基本元素为节点和属性
    • ARM Linux中,一个.dts文件对应一个ARM的设备,一般放置在内核的arch/arm/boot/dts目录中
    • DTS不是ARM独有的:在arch/powerpc/boot/dts、arch/openrisc/boot/dts等目录中,也有大量的.dts文件
    • SOC公用的部分或多个设备共同的部分一般提炼为.dtsi,类似C的头文件,其他设备对应的.dts就包含.dtsi
      • 和C语言的头文件类似,.dtsi也可以包括其他的.dtsi,譬如几乎所有的ARM SOC的.dtsi都引用了skeleton.dtsi
    • 如下:设备数的模板
      • 他基本表征了一个设备树源文件的结构
        • 1个root节点”/”;
        • root节点下面含一系列子节点,如代码中的child-node1和child-node2
        • 节点node1和下又含有一系列子节点,如本例中的child-node1和child-node2
        • 各个节点都有一系列属性
          • 这些属性可能为空,如an-empty-property
          • 可能为字符串,如a-string-property
          • 可能为字符串树组,如a-string-list-property
          • 可能为Cells(由u32整数组成),如second-child-property
          • 可能为二进制数,如a-byte-data-property
/ {
    nodel {
        a-string-property = "A string";
        a-string-list-property = "first string", "second string"
        a-byte-data-property = {0x01 0x23 0x34 0x56};
        child-node {
            first-child-property;
            second-child-property = <1>
            a-string-property = "Hello, world"
        };
        child-node2 {
        };
    };
   node2 {
        an-enpty-property;
        a-cell-property = <1 2 3 4>; /* each munber (cell) is a uint32 */
        child-node1 {
        };
    };
};
  • 简单的设备数文件实例
    • 假设此machine的配置如下:
      • 1个双核ARM Cortex-A9 32位处理器;
      • ARM的local bus上的内存映射区域分布了
        • 2个串口(分别位于0x101F1000 和 0x101F2000)
        • GPIO控制器(位于0x101F3000)
        • SPI控制器(位于0x10170000)
        • 中断控制器(位于0x10140000)
        • 一个external bus桥,该桥上又连接了
          • SMC SMC91111 Ethernet(位于0x10100000)
          • 64MB NOR Flash(位于0x30000000)
          • I2C控制器(位于0x10160000),该I2C控制器所对应的I2C总线上又连接了
            • Maxim DS1338实时钟(I2C地址为0x58)
    • 其对应的.dts文件如下:
      • 从代码可知,external-bus是根节点的子节点,I2C又是external-bus的子节点,RTC有进一步是I2C的子节点
/ {
    compatible = "acme,coyotes-revenge";
    #address-cells = <1>;
    #size-cells = <1>;
    interrupt-parent = <&intc>;

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a9";
            reg = <0>;
        };
        cpu@1 {
            compatible = "arm,cortex-a9";
            reg = <1>;
        };
    };

    serial@101f0000 {
        compatible = "arm,pl011";
        reg = <0x101f0000 0x1000 >;
        interrupts = < 1 0 >;
    };

    serial@101f2000 {
        compatible = "arm,pl011";
        reg = <0x101f2000 0x1000 >;
        interrupts = < 2 0 >;
    };

    gpio@101f3000 {
        compatible = "arm,pl061";
        reg = <0x101f3000 0x1000
               0x101f4000 0x0010>;
        interrupts = < 3 0 >;
    };

    intc: interrupt-controller@10140000 {
        compatible = "arm,pl190";
        reg = <0x10140000 0x1000 >;
        interrupt-controller;
        #interrupt-cells = <2>;
    };

    spi@10115000 {
        compatible = "arm,pl022";
        reg = <0x10115000 0x1000 >;
        interrupts = < 4 0 >;
    };

    external-bus {
        #address-cells = <2>
        #size-cells = <1>;
        ranges = <0 0  0x10100000   0x10000     // Chipselect 1, Ethernet
                  1 0  0x10160000   0x10000     // Chipselect 2, i2c controller
                  2 0  0x30000000   0x1000000>; // Chipselect 3, NOR Flash

        ethernet@0,0 {
            compatible = "smc,smc91c111";
            reg = <0 0 0x1000>;
            interrupts = < 5 2 >;
        };

        i2c@1,0 {
            compatible = "acme,a1234-i2c-bus";
            #address-cells = <1>;
            #size-cells = <0>;
            reg = <1 0 0x1000>;
            interrupts = < 6 2 >;
            rtc@58 {
                compatible = "maxim,ds1338";
                reg = <58>;
                interrupts = < 7 3 >;
            };
        };

        flash@2,0 {
            compatible = "samsung,k8f1315ebm", "cfi-flash";
            reg = <2 0 0x4000000>;
        };
    };
};

18.2.1.2 DTC(Device Tree Compiler)

  • DTC是将.dts编译为.dtb的工具
    • DTC的源代码位于内核的scripts/dtc目录中
    • 在Linux内核使能了Device Tree的情况下,编译内核的时候主机工具dtc会被编译出来,对应scripts/dtc/Makefile中的“hostprogs-y := dtc”这一hostprogs编译target
    • Ubuntu中单独安装DTC:sudo apt-get install device-tree-compiler
    • 在Linux内核的arch/arm/boot/dts/Makefile中,描述了当某种SOC被选中后,哪些.dtb文件会被编译出来
      • 如与VEXPRESS对应的.dtb如下:
        • 在Linux下可以单独编译设备树文件,当在Linux内核下运行make dtbs时,若之前选择了ARCH_VEXPRESS,上述.dtb都会有对应的.dts编译出来,因为arch/arm/Makefile中包含有一个.dtbs编译目标项目
dtb-$(CONFIG_ARCH_VEXPRESS) += vexpress-v2p-ca5s.dtb \
        vexpress-v2p-ca9.dtb \
        vexpress-v2p-ca15-tc1.dtb \
        vexpress-v2p-ca15_a7.dtb \
        xenvm-4.2.dtb

18.2.1.3 DTB(Device Tree Blob)

  • 文件.dtb是.dts被DTC编译后的二进制格式的设备树描述,可由Linux内核解析,UBOOT也可以识别
    • 使用.dtb两种方式:
      • 在制作NAND、SD启动映像时会留有一小片区域给.dtb文件单独存放,bootloader在引导内核时会先读取该.dtb到内存
      • 将.dtb直接和zImage绑定在一起做成一个映像文件,此时内核编译时需要使能CONFIG_ARM_APPENDED_DTB选项

18.2.1.4 绑定(Bingding)

  • 设备树绑定文档(.txt):描述对应节点的兼容性、必要的属性和可选的属性
    • 文档位置:内核的Documentation/devicetree/bindings目录
      • 其下又分为很多子目录,譬如,Documentation/devicetree/bindings/i2c/i2c-xiic.txt描述了Xilinx的I2C控制器
    • 文档主要内容:
      • 关于该模块的最基本描述
      • 必需属性(Required Properties)的描述
      • 可选属性(Optional properties)的描述
      • 一个实例

18.2.1.5 Bootloader

  • 使能设备树的bootloader配置
    • 编译UBOOT的时候在config文件中加入:#define CONFIG_OF_LIBFDT
    • 在UBOOT中,可以从NAND、SD或者TFTP等任意介质中将.dtb读入内存
      • 例如:假设.dtb放入的内存地址为0x71000000,之后可在UBOOT中运行fdt addr命令设置.dtb的地址
        • fdt的其他命令就变得可以使用,如fdt resize、fdt print等
        • 对于ARM来讲,可以通过bootz kernel_addr initrd_address dtb_address的命令来启动内核
          • dtb_address作为bootz或者bootm的最后一次参数,第一个参数为内核映像的地址,第二个参数为initrc的地址
UBoot > fdt addr 0x71000000

18.2.2 根节点兼容性

  • Linux内核通过根节点“/”的兼容性即可判断它启动的是什么设备
    • 组织形式:,
    • 兼容性的包含成员
      • 首个兼容性的字符串是板子级别的名字
      • 后面一个兼容性是芯片级别的名字
    • 两个字符串兼容性实例如例1:
      • 代码中各个电路板的共性是兼容于arm,vexpress,尔特性分别兼容于:arm,vexpress,v2p-ca9/ca5s/ca15_a7
    • 多个字符串兼容性实例如例2:
      • 第一个字符串是板子名字(很特定),第二个是芯片名字(比较特定),第三个是芯片系列的名字(比较通用)
//例1
//板子arch/arm/boot/dts/vexpress-v2p-ca9.dts兼容于"arm,vexpress,v2p-ca9"和“arm,vexpress”
compatible = "arm,vexpress,v2p-ca9", "arm, vexpress"
//板子arch/arm/boot/dts/vexpress-v2p-ca5s.dts兼容性如下:
compatible = "arm,vexpress,v2p-ca5s", "arm, vexpress"
//板子arch/arm/boot/dts/vexpress-v2p-ca15_a7.dts兼容性如下:
compatible = "arm,vexpress,v2p-ca15_a7", "arm, vexpress"

//例2
//arch/arm/boot/dts/exymos4210-origen.dts的兼容性:
compatible = "insignal,origen", "samsung,exynos4210", "samsung,exynos4"
//arch/arm/boot/dts/exymos4210-universal_c210.dts的兼容性:
compatible = "insignal,universal_c210", "samsung,exynos4210", "samsung,exynos4"
  • ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围起来的针对这个machine的一系列callback
    • ARM Linux3.x引入Device Tree之后,MACHINE_START变更为DT_MACHINE_START
      • DT_MACHINE_START中含有一个.dt_compat成员,用于表明相关的machine与.dts中root结点的compatible属性兼容关系
      • 如果Bootloader传递给内核的Device Tree中root结点的compatible属性出现在某machine的.dt_compat表中,相关的machine就与对应的Device Tree匹配,从而引发这一machine的一系列初始化函数被执行。
 static const char * const v2m_dt_match[] __initconst = {
         "arm,vexpress",
         "xen,xenvm",
         NULL,
 };
 DT_MACHINE_START(VEXPRESS_DT, "ARM-Versatile Express")
         .dt_compat      = v2m_dt_match,
         .smp            = smp_ops(vexpress_smp_ops),
         .map_io         = v2m_dt_map_io,
         .init_early     = v2m_dt_init_early,
         .init_irq       = v2m_dt_init_irq,
         .timer          = &v2m_dt_timer,
         .init_machine   = v2m_dt_init,
         .handle_irq     = gic_handle_irq,
         .restart        = vexpress_restart,
 MACHINE_END
  • Linux倡导针对多个SoC、多个电路板的通用DT machine,即一个DT machine的.dt_compat表含多个电路板.dts文件的root结点compatible属性字符串
    • 如果这么多电路板的初始化序列不一样,可以透过以下API判断具体的电路板是什么
      • 此API判断目前运行的板子或者SOC的兼容性,它匹配的是设备树根节点下的兼容性
    • 如果一个兼容包含多个字符串,譬如对于前面介绍的根节点兼容compatible = “samsung, universal_c210”, “samsung,exynos4210”,”samsung,exynos4”的情况,如下表达式都成立
int of_machine_is_compatible(const char *compat) 

of_machine_is_compatible("samsung,universal_c210")
of_machine_is_compatible("samsung,exynos4210")
of_machine_is_compatible("samsung,exynos4")

18.2.3 设备节点兼容性

  • 在.dts文件的每个设备节点中,都有一个兼容性属性,用于驱动和设备的绑定
    • 兼容属性是一个字符串的列表,形式为”,”
      • 第一个字符串表征节点代表的确切设备,是个特指
      • 第二个字符串表征可兼容的其他设备,涵盖更广的范围
    • 例如:在vexpress-v2m.dtsi中Flash节点如下
      • 兼容属性的第2个字符串“cfi-flash”明显比第1个字符串“arm,vexpress-flash”涵盖范围更广
flash@0,00000000 {
compatible = "arm,vexpress-flash","cfi-flash";
reg = <0 0x00000000 0x04000000>
<1 0x00000000 0x04000000>;
bank-width = <4>
};
  • 使用设备树后,驱动需要与.dts中描述的设备节点进行匹配,从而使驱动的probe()函数执行
    • 对于platform_driver而言,需要添加一个OF匹配表,如前文的.dts文件的“acme,a1234-i2c-bus”兼容I2C控制节点的OF匹配表,代码如下:
    • 对于i2c和spi从设备而言,同样也可以通过of_match_table添加匹配的.dts中的相关节点的兼容属性
    • i2c和spi外设备驱动和设备树中设备节点的兼容属性还有一种弱势匹配方法——“别名”匹配
      • 别名就是去掉兼容属性中manufacturer前缀后的部分
//platform设备驱动中的of_match_table
 static const struct of_device_id a1234_i2c_of_match[] = {
         { .compatible = "acme,a1234-i2c-bus ", },
         {},
 };
 MODULE_DEVICE_TABLE(of, a1234_i2c_of_match);

 static struct platform_driver i2c_a1234_driver = {
         .driver = {
                 .name = "a1234-i2c-bus ",
                 .owner = THIS_MODULE,
                 .of_match_table = a1234_i2c_of_match,
         },
         .probe = i2c_a1234_probe,
         .remove = i2c_a1234_remove,
 };
 module_platform_driver(i2c_a1234_driver);

//i2c和spi设备驱动中的of_match_table
 static const struct of_device_id wm8753_of_match[] = {
         { .compatible = "wlf,wm8753", },
         { }
 };
 MODULE_DEVICE_TABLE(of, wm8753_of_match);
 static struct spi_driver wm8753_spi_driver = {
         .driver = {
                 .name   = "wm8753",
                .owner  = THIS_MODULE,
                 .of_match_table = wm8753_of_match,
        },
         .probe          = wm8753_spi_probe,
         .remove         = wm8753_spi_remove,
 };
 static struct i2c_driver wm8753_i2c_driver = {
         .driver = {
                 .name = "wm8753",
                 .owner = THIS_MODULE,
                 .of_match_table = wm8753_of_match,
         },
         .probe =    wm8753_i2c_probe,
         .remove =   wm8753_i2c_remove,
         .id_table = wm8753_i2c_id,
 };

//spi的别名匹配
   static int spi_match_device(struct device *dev, struct device_driver *drv) 
   { 
           const struct spi_device *spi = to_spi_device(dev); 
          const struct spi_driver *sdrv = to_spi_driver(drv); 

           /* Attempt an OF style match */ 
           if (of_driver_match_device(dev, drv)) 
                   return 1; 

          /* Then try ACPI */ 
          if (acpi_driver_match_device(dev, drv)) 
                  return 1; 

          if (sdrv->id_table) 
                  return !!spi_match_id(sdrv->id_table, spi); 

          return strcmp(spi->modalias, drv->name) == 0; 
  } 
  static const struct spi_device_id *spi_match_id(const struct spi_device_id *id,
 const struct spi_device *sdev) 
  { 
          while (id->name[0]) { 
                  if (!strcmp(sdev->modalias, id->name)) 
                          return id; 
                  id++; 
          }
          return NULL; 
  }  
  • 当一个驱动支持两个或多个设备的时候,这些不同的.dts文件中设备的兼容属性都会写入驱动OF匹配表
    • 驱动可以通过bootloader传递给内核设备树中的真正节点的兼容属性以确定究竟是哪一种设备,从而不同设备不同处理
    • Linux内核通过如下of_device_is_compatible的API来判断具体的设备是什么
      • 除了上面方法,还可以采用在驱动的of_device_id表中填充.data成员的形式
        • 例如,arch/arm/mm/cache-12x0.c支持“arm,1210-cache” “arm,p1310-cache” “arm,1220-cache”等多种设备
int of_device_is_compatible(const struct device_node *device, const char *compat);

18.2.4 设备节点及label的命名

  • root结点”/”的cpus子节点下面又包含2个cpu子结点,描述了此machine上的2个CPU,且它们的compatible 属性为”arm,cortex-a9”。

    • cpus和cpus的2个cpu子结点的命名所遵循的组织形式为:[@]
      • <>中的内容是必选项,[]中的则为可选项
      • name:是一个ASCII字符串,用于描述结点对应的设备类型,如3com Ethernet适配器对应的结点name宜为ethernet,而不是3com509
      • @unit-address:如果一个结点描述的设备有地址,则应该给出
        • 设备的unit-address地址也经常在其对应结点的reg属性中给出
        • 多个相同类型设备节点的name可以一样,只要unit-address不同即可,如本例中含有cpu@0、cpu@1以及serial@101f0000与serial@101f2000这样的同名结点
    • ePAPR标准给出了节点命名的规范,具体可参考
  • 可以给一个设备节点添加label,之后可以通过&label的形式访问这个label,这种引用是通过phandle(pointer handle)进行的

18.2.5 地址编码

  • 可寻址的设备使用如下信息在设备树中编码地址信息
    • reg的组织形式为reg =
//代码1
reg
    #address-cells
    #size-cells
//代码2
ranges = <0 0  0x10100000   0x10000     // Chipselect 1, Ethernet
              1 0  0x10160000   0x10000     // Chipselect 2, i2c controller
              2 0  0x30000000   0x1000000>; // Chipselect 3, NOR Flash

18.2.6 中断连接

  • 对于中断控制器而言,它提供如下属性:
    • interrupt-controller – 这个属性为空,中断控制器应该加上此属性表明自己的身份
    • #interrupt-cells – 与#address-cells 和 #size-cells相似,它表明连接此中断控制器的设备的interrupts属性的cell大小
  • 在整个Device Tree中,与中断相关的属性还包括:
    • interrupt-parent – 设备结点透过它来指定它所依附的中断控制器的phandle,当节点没有指定interrupt-parent 时,则从父级节点继承
      • 对于本例而言,root节点指定了interrupt-parent = <&intc>;其对应于intc:interrupt-controller@10140000,而root节点的子节点并未指定interrupt-parent,因此它们都继承了intc,即位于0x10140000的中断控制器。
    • interrupts – 用到了中断的设备节点透过它指定中断号、触发方法等,具体这个属性含有多少个cell,由它依附的中断控制器节点的#interrupt-cells属性决定。而具体每个cell又是什么含义,一般由驱动的实现决定,而且也会在Device Tree的binding文档中说明
      • 譬如,对于ARM GIC中断控制器而言,#interrupt-cells为3,它3个cell的具体含义Documentation/devicetree/bindings/arm/gic.txt就有如下文字说明:
    The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI
    interrupts.

    The 2nd cell contains the interrupt number for the interrupt type.
    SPI interrupts are in the range [0-987].  PPI interrupts are in the
    range [0-15].

    The 3rd cell is the flags, encoded as follows:
          bits[3:0] trigger type and level flags.
                  1 = low-to-high edge triggered
                  2 = high-to-low edge triggered
                  4 = active high level-sensitive
                  8 = active low level-sensitive
          bits[15:8] PPI interrupt cpu mask.  Each bit corresponds to each of
          the 8 possible cpus attached to the GIC.  A bit set to '1' indicated
          the interrupt is wired to that CPU.  Only valid for PPI interrupts.
  • 一个设备还可能用到多个中断号
    • 对于ARM GIC而言,若某设备使用了SPI的168、169号2个中断,而且都是高电平触发,则该设备结点的interrupts属性可定义为:interrupts = <0 168 4>, <0 169 4>;

18.2.7 GPIO、时钟、pinmux连接

  • 除了中断以外,在ARM Linux中clock、GPIO、pinmux都可以透过.dts中的结点和属性进行描述

18.3 Device Tree引发的BSP和驱动变更

有了Device Tree后,大量的板级信息都不再需要,譬如过去经常在arch/arm/plat-xxx和arch/arm/mach-xxx实施的如下事情:

18.3.1. 注册platform_device,绑定resource,即内存、IRQ等板级信息。

  • 通过设备树后,形如代码1中的platform_device代码都不再需要,其中platform_device会由kernel自动展开
    • 这些resource实际来源于.dts中设备结点的reg、interrupts属性
    • 典型地,大多数总线都与“simple_bus”兼容,而在SoC对应的machine的.init_machine成员函数中,调用of_platform_bus_probe(NULL, xxx_of_bus_ids, NULL);即可自动展开所有的platform_device
      • 譬如,假设我们有个XXX SoC,则可在arch/arm/mach-xxx/的板文件中透过如下方式展开.dts中的设备结点对应的platform_device:如代码2
//代码1
90 static struct resource xxx_resources[] = {
91         [0] = {
92                 .start  = …,
93                 .end    = …,
94                 .flags  = IORESOURCE_MEM,
95         },
96         [1] = {
97                 .start  = …,
98                 .end    = …,
99                 .flags  = IORESOURCE_IRQ,
100         },
101 };
102
103 static struct platform_device xxx_device = {
104         .name           = "xxx",
105         .id             = -1,
106         .dev            = {
107                                 .platform_data          = &xxx_data,
108         },
109         .resource       = xxx_resources,
110         .num_resources  = ARRAY_SIZE(xxx_resources),
111 };
//代码2
18 static struct of_device_id xxx_of_bus_ids[] __initdata = {
19         { .compatible = "simple-bus", },
20         {},
21 };
22
23 void __init xxx_mach_init(void)
24 {
25         of_platform_bus_probe(NULL, xxx_of_bus_ids, NULL);
26 }
32
33 #ifdef CONFIG_ARCH_XXX
38
39 DT_MACHINE_START(XXX_DT, "Generic XXX (Flattened Device Tree)")
4145         .init_machine   = xxx_mach_init,
4649 MACHINE_END
50 #endif

18.3.2 注册i2c_board_info,指定IRQ等板级信息。

  • 形如代码1之类的i2c_board_info代码,目前不再需要出现
//代码1
145 static struct i2c_board_info __initdata afeb9260_i2c_devices[] = {
146         {
147                 I2C_BOARD_INFO("tlv320aic23", 0x1a),
148         }, {
149                 I2C_BOARD_INFO("fm3130", 0x68),
150         }, {
151                 I2C_BOARD_INFO("24c64", 0x50),
152         },
153 };
  • 现在只需要把tlv320aic23、fm3130、24c64这些设备结点填充作为相应的I2C controller结点的子结点即可,类似于前面的代码2
    • Device Tree中的I2C client会透过I2C host驱动的probe()函数中调用of_i2c_register_devices(&i2c_dev->adapter);被自动展开。
      i2c@1,0 {
            compatible = "acme,a1234-i2c-bus";
            …
            rtc@58 {
                compatible = "maxim,ds1338";
                reg = <58>;
                interrupts = < 7 3 >;
            };
        };

18.3.3 注册spi_board_info,指定IRQ等板级信息。

  • 如下的spi_board_info代码,目前不再需要出现
79 static struct spi_board_info afeb9260_spi_devices[] = {
80         {       /* DataFlash chip */
81                 .modalias       = "mtd_dataflash",
82                 .chip_select    = 1,
83                 .max_speed_hz   = 15 * 1000 * 1000,
84                 .bus_num        = 0,
85         },
86 };
  • 与I2C类似,现在只需要把mtd_dataflash之类的结点,作为SPI控制器的子结点即可,SPI host驱动的probe函数透过spi_register_master()注册master的时候,会自动展开依附于它的slave。

18.3.4 多个针对不同电路板的machine,以及相关的callback。

  • 过去,ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围起来的针对这个machine的一系列callback,譬如:
    [cpp] view plain copy
    373 MACHINE_START(VEXPRESS, “ARM-Versatile Express”)
    374 .atag_offset = 0x100,
    375 .smp = smp_ops(vexpress_smp_ops),
    376 .map_io = v2m_map_io,
    377 .init_early = v2m_init_early,
    378 .init_irq = v2m_init_irq,
    379 .timer = &v2m_timer,
    380 .handle_irq = gic_handle_irq,
    381 .init_machine = v2m_init,
    382 .restart = vexpress_restart,
    383 MACHINE_END
  • Device Tree之后,MACHINE_START变更为DT_MACHINE_START

18.3.5 设备与驱动的匹配方式

  • 在上面介绍过

18.3.6 设备平台数据属性化

18.4 常用的OF API

在Linux的BSP和驱动代码中,还经常会使用到Linux中一组Device Tree的API,这些API通常被冠以of_前缀,它们的实现代码位于内核的drivers/of目录。

18.4.1 寻找节点

  • 根据compatible属性,获得设备结点
    • 遍历Device Tree中所有的设备结点,看看哪个结点的类型、compatible属性与本函数的输入参数匹配
    • 大多数情况下,from、type为NULL。
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible);

18.4.2 读取属性

int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz);
int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz);
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz);
int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value);
  • 读取设备结点np的属性名为propname,类型为8、16、32、64位整型数组的属性
    • 对于32位处理器来讲,最常用的是of_property_read_u32_array()
      • 如在arch/arm/mm/cache-l2x0.c中,透过如下语句读取L2 cache的”arm,data-latency”属性:
534         of_property_read_u32_array(np, "arm,data-latency",
535                                    data, ARRAY_SIZE(data));
  • 在arch/arm/boot/dts/vexpress-v2p-ca9.dts中,含有”arm,data-latency”属性的L2 cache结点如下:
137         L2: cache-controller@1e00a000 {
138                 compatible = "arm,pl310-cache";
139                 reg = <0x1e00a000 0x1000>;
140                 interrupts = <0 43 4>;
141                 cache-level = <2>;
142                 arm,data-latency = <1 1 1>;
143                 arm,tag-latency = <1 1 1>;
144         }

有些情况下,整形属性的长度可能为1,于是内核为了方便调用者,又在上述API的基础上封装出了更加简单的读单一整形属性的API,它们为int of_property_read_u8()、of_property_read_u16()等,实现于include/linux/of.h:

513 static inline int of_property_read_u8(const struct device_node *np,
514                                        const char *propname,
515                                        u8 *out_value)
516 {
517         return of_property_read_u8_array(np, propname, out_value, 1);
518 }
519
520 static inline int of_property_read_u16(const struct device_node *np,
521                                        const char *propname,
522                                        u16 *out_value)
523 {
524         return of_property_read_u16_array(np, propname, out_value, 1);
525 }
526
527 static inline int of_property_read_u32(const struct device_node *np,
528                                        const char *propname,
529                                        u32 *out_value)
530 {
531         return of_property_read_u32_array(np, propname, out_value, 1);
532 }

处整形属性外,字符串属性也比较常用,其对应的API包括:

int of_property_read_string(struct device_node *np, const char
*propname, const char **out_string);
int of_property_read_string_index(struct device_node *np, const char
    *propname, int index, const char **output);

前者读取字符串属性,后者读取字符串数组属性中的第index个字符串。如drivers/clk/clk.c中的of_clk_get_parent_name()透过of_property_read_string_index()遍历clkspec结点的所有”clock-output-names”字符串数组属性。

1759 const char *of_clk_get_parent_name(struct device_node *np, int index)
1760 {
1761         struct of_phandle_args clkspec;
1762         const char *clk_name;
1763         int rc;
1764
1765         if (index < 0)
1766                 return NULL;
1767
1768         rc = of_parse_phandle_with_args(np, "clocks", "#clock-cells", index,
1769                                         &clkspec);
1770         if (rc)
1771                 return NULL;
1772
1773         if (of_property_read_string_index(clkspec.np, "clock-output-names",
1774                                   clkspec.args_count ? clkspec.args[0] : 0,
1775                                           &clk_name) < 0)
1776                 clk_name = clkspec.np->name;
1777
1778         of_node_put(clkspec.np);
1779         return clk_name;
1780 }
1781 EXPORT_SYMBOL_GPL(of_clk_get_parent_name);

处整形外、字符串以外的最常用属性类就是布尔类型,其对应的API如下

static inline bool of_property_read_bool(const struct device_node *np, const char *propname);

如果设备结点np含有propname属性,则返回true,否则返回false。一般用于检查空属性是否存在。

18.4.3 内存映射

void __iomem *of_iomap(struct device_node *node, int index);

通过设备结点直接进行设备内存区间的 ioremap(),index是内存段的索引。若设备结点的reg属性有多段,可通过index标示要ioremap的是哪一段,只有1段的情况,index为0。采用Device Tree后,大量的设备驱动通过of_iomap()进行映射,而不再通过传统的ioremap。

unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

透过Device Tree或者设备的中断号,实际上是从.dts中的interrupts属性解析出中断号。若设备使用了多个中断,index指定中断的索引号。
还有一些OF API,这里不一一列举,具体可参考include/linux/of.h头文件。

没有更多推荐了,返回首页