RK3568,国产瑞芯微的CPU,支持多种操作系统,如Linux,Android等
底层硬件和上层软件的桥梁,让硬件动起来
使用驱动框架编程,提供统一接口给应用程序调用
Linux将驱动分为字符设备,网络设备,块设备
字符设备:必须以串行顺序依次进行访问的设备,如鼠标
块设备:按照任意顺序进行访问,如硬盘
网络设备:面向数据包的接收和发送,如网卡
arch:存放CPU架构的代码,如arm,X86,MIPS等
block:存放块设备的相关代码,如硬盘,SD卡
crypto:存放加密算法目录
Documentation:存放官方Linux内核文档
drivers:驱动目录,存放了Linux系统支持的硬件设备驱动源码
firmware:存放固件目录
fs:存放支持的文件系统的代码目录,如fat,ext2,ext3
include:存放公共的头文件目录,arch下也有一个include文件夹,这个arch下的是针对某个硬件的
init:存放Linux内核启动初始化的代码
ipc:存放进程间通信代码
kernel:存放内核本身的代码文件夹
lib:存放库函数的文件夹
mm:存放内存管理的目录,是memory management的缩写
net:存放网络相关的代码,如TCP/IP协议
scripts:存放脚本的代码
security:存放安全相关代码
sound:存放音频相关代码
tools:存放Linux用到的工具文件夹
usr:和Linux内核的启动有关代码
virt:内核虚拟机相关代码
组成:
头文件:
#include
#include
驱动模块函数,入口出口
module_init();驱动加载时自动执行
moudule_exit();驱动卸载时自动执行
声明信息
MODULE_LICENSE(“GPL”);
功能实现
static int hello_init(void)
{
printk("hello world \n");
return 0;
}
static void hello_exit(void)
{
printk("byb byb \n");
}
TIPS:驱动里面无法使用C库函数,所有无法使用printf,可以用printk函数替代,两者用法一样
1,将驱动放在Linux内核里面,然后编译Linux内核,将驱动编译到内核里面
2,将驱动编译成内核模块,独立于Linux内核外,最终生成的是.ko模块文件
内核模块是Linux系统中的一个特殊机制,可以将一些使用频率很少或者暂时用不到的功能编译成内核模块,在需要的时候再动态加载到内核里
obj-m += helloworld.o
KDIR:=/usr/src/linux-source-5.19.0 #内核源码
PWD?=$(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
echo $(PWD)
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order
obj-m += hello_world.o
KDIR
前提:内核编译通过,使用交叉编译器前设置环境变量,除非在内核的顶层Makefile设置设置ARCH(内核架构)和CROSS_COMPILE(编译器路径)
insmod命令
modprobe命令,这个命令加载Linux内核模块的同时把这个模块所依赖的模块也同时加载进去
这两个命令的语法都是:cmd hello_world.ko
rmmod hello_world.ko,移除已经载入Linux的内核模块
lsmod:列出已经加载进内核的模块,也可以用cat /proc/modules
modinfo 模块名:查看内核模块信息
该界面可以对内核进行配置,比如是否把某个模块编译进内核。
export ARCH=arm,然后进入Linux内核源码的顶层目录下通过make menuconfig命令打开该界面
搜索功能:/
驱动状态:
把驱动编译成模块:M
把驱动编译进内核:*
不编译
[ ]:选中(编译进内核)或不选中(不编译)两种状态
< >:有驱动的三种状态,选中,不选中,编译成模块
():存放字符串或者16进制数
进入内核源码的驱动文件夹下面的字符设备驱动,然后创建一个自己的文件夹来存放驱动程序源码
在驱动源码文件夹下编写Kconfig文件
config helloworld
bool "hellowworld support"
default y
help
helloworld
然后在上一级目录的Kconfig文件里面通过"source"命令包含自己写的Kconfig文件
在驱动程序目录下面创建Makefile文件,通过判断源码顶级目录下面的.config文件里的helloworld变量的值来决定是否要进行编译
obj -$(CONFIG_helloworld) :=helloworld.o
修改驱动程序上一级目录下面的Makefile文件,使得它包含驱动程序目录下的Makefile文件
最后要把原来的Kconfig文件用修改后的.config文件覆盖掉
通过module_param(name,type,perm)函数
name:传递给驱动代码中的参数的名字
type:参数类型
perm:参数的读写权限
Tips:参数的读写权限在Linux内核源码中有定义,\include\uapi\linux\stat.h,
/*忽略宏定义S_I,U/USR代表文件的拥有者,G/GRP代表与文件拥有者同组用户,O/OTH代表其它与文件所有者不同组的用户
#define S_IRWXU 00700 //文件拥有者具有读写可执行权限
#define S_IRUSR 00400 //文件拥有者具有可读权限
#define S_IWUSR 00200 //文件拥有者具有可写权限
#define S_IXUSR 00100 //文件拥有者具有可执行权限
#define S_IRWXG 00070 //同组用户具有读写可执行权限
#define S_IRGRP 00040 //同组用户具有可读权限
#define S_IWGRP 00020 //同组用户具有可写权限
#define S_IXGRP 00010 //同组用户具有可执行权限
#define S_IRWXO 00007 //不同组用户具有读写可执行权限
#define S_IROTH 00004 //不同组用户具有可读权限
#define S_IWOTH 00002 //不同组用户具有可写权限
#define S_IXOTH 00001 //不同组用户具有可执行权限
#define S_IRWXUGO (S_IRWXU|S_IRWXG|S_IRWXO) //所有用户具有读写可执行权限
#define S_IALLUGO (S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO) //所有用户具有读写可执行权限
#define S_IRUGO (S_IRUSR|S_IRGRP|S_IROTH) //所有用户具有可读可执行权限
#define S_IWUGO (S_IWUSR|S_IWGRP|S_IWOTH) //所有用户具有可写可执行权限
#define S_IXUGO (S_IXUSR|S_IXGRP|S_IXOTH) //所有用户具有可执行权限
通过module_param_array(name,type,nump,perm)函数,nump代表数组的长度,另外三个参数和上一个函数类似
通过module_param_string(name,string,len,perm)函数
name:要传递给驱动代码中的变量的名字
string:驱动程序中变量的名字
len:字符串的大小
perm:参数的读写可执行权限
描述模块参数的信息,_parm是要描述的参数的参数名称,desc是具体的描述信息,在include/linux/moduleparam.h中定义
系统调用是操作系统提供给编程人员的接口,当编程任意写程序时,因为上层应用无法直接访问底层硬件,需要利用系统调用接口来请求操作系统的服务,如访问硬件。
每个系统有唯一一个系统调用号来标识对应的系统调用接口函数,以init_module为例,该函数的系统调用号是105,可以在\linux-5.15.10\include\uapi\asm-generic\unistd.h
文件中找到所有的系统调用号
#define __NR_init_module 105 //系统调用号
__SYSCALL(__NR_init_module, sys_init_module)
Linux规定字符设备或者块设备都必须有一个专属的设备号,一个设备号由主设备号和次设备号组成,主设备号表示某一类驱动,如USB驱动设备,而次设备号表示这个驱动下的各个设备。
include\linux\types.h文件下定义了dev_t数据类型来表示设备号,是unsigned int类型的数据,是u32类型的数据,其中高12位表示主设备号,低12位表示次设备号。
typedef u32 __kernel_dev_t;
typedef __kernel_fd_set fd_set;
typedef __kernel_dev_t dev_t;
typedef __kernel_ulong_t ino_t;
typedef __kernel_mode_t mode_t;
typedef unsigned short umode_t;
在include\linux\kdev_t.h
文件中提供了几个操作设备号的宏定义。
#define MINORBITS 20 //次设备号的位数,一共是20位
#define MINORMASK ((1U << MINORBITS) - 1) //次设备号的掩码,用来计算次设备号
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //在dev_t 里面获取我们的主设备号,本质上是将dev_t右移20位
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //在dev_t 里面获取我们的次设备号,本质上是取低20位的值
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //将我们的主设备号和次设备号组成一个dev_t类型。第一个参数是主设备号,第二个参数是次设备号
分配方法有静态分配和动态分配,操作函数可以在include\linux\fs.h找到,分别是
register_chrdev_region(dev_t, unsigned, const char *);
需要明确知道我们的系统里面那些设备号没有用,可以使用cat /proc/devices 查看当前系统中使用了哪些主设备号
参数:
第一个:设备号的起始值。类型是dev_t类型,比如以宏定义MKDEV(100,0)作为参数,表示主设备号是100,次设备号是0
第二个:次设备号的个数,主设备号相同的情况下可以有多个次设备号
第三个:设备的名称
返回值:成功返回0,失败返回负数
alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
参数:
第一个:保存生成的设备号
第二个:我们请求的第一个次设备号,通常是从0开始
第三个:要申请的设备号的个数。
第四个:设备名称
返回值:成功返回0,失败返回负数
使用动态分配会优先使用255到234
unregister_chrdev_region(dev_t, unsigned);
参数:
第一个:要释放的设备号
第二个:释放的设备号的数量
在Linux中,使用cdev结构体描述字符设备,该结构体定义在include\linux\cdev.h里面:
struct cdev {
struct kobject kobj;
struct module *owner; //所属模块,一般是填宏定义TIHIS_MODULE
const struct file_operations *ops;//文件操作结构体,系统调用和驱动程序的桥梁!!!
struct list_head list;
dev_t dev; //设备号
unsigned int count;
};
cdev_init函数用于初始化cdev结构体成员变量,建立cdev和file_operations之间的联系,函数定义在fs\char_dev.c里面
/**
* cdev_init() - initialize a cdev structure
* @cdev: the structure to initialize字符设备描述结构体
* @fops: the file_operations for this device文件操作结构体
*
* Initializes @cdev, remembering @fops, making it ready to add to the
* system with cdev_add().
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
向系统添加一个cdev结构体,也就是添加一个字符设备,把字符设备函数注册到内核。函数定义在fs\char_dev.c里面
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device字符设备结构体
* @dev: the first device number for which this device is responsible设备号
* @count: the number of consecutive minor numbers corresponding to this要添加的设备数量
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
if (WARN_ON(dev == WHITEOUT_DEV))
return -EBUSY;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
注销字符设备函数
/**
* cdev_del() - remove a cdev from the system
* @p: the cdev structure to be removed
*
* cdev_del() removes @p from the system, possibly freeing the structure
* itself.
*
* NOTE: This guarantees that cdev device will no longer be able to be
* opened, however any cdevs already open will remain and their fops will
* still be callable even after cdev_del returns.
*/
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
这是将系统调用和驱动程序连接起来的结构体,定义在include\linux\fs.h中
struct file_operations {
struct module *owner;//拥有该结构体的模块指针,一般设置为THIS_MODULE
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 (*iopoll)(struct kiocb *kiocb, bool spin);
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);//与unlocked_ioctl功能一样
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 *);
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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#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);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
设备节点是应用程序和驱动程序沟通的一个桥梁,被创建在dev目录下,以设备文件的形式存在,设备文件就是设备节点。
设备节点有两种创建方法,手动创建和在注册设备的时候自动创建。
mknod 名称 类型(字符设备用c,块设备用b) 主设备号 次设备号
举例:
mknod /dev/test c 247 0
使用 mdev 来实现设备节点文件的自动创建和删除。
mdev 是 udev 的简化版本,是 busybox 中所带的程序,最适合用在嵌入式系统。
udev 是一种工具,它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。设备文件通常放在/dev 目录下。使用 udev 后,在/dev 目录下就只包含系统中真正存在的设备。udev 一般用在 PC 上的 linux中,相对 mdev 来说要复杂些。
自动创建设备节点分为俩个步骤:
使用class_create函数创建一个class的类,这个函数会在/sys/class下创建文件,函数定义在include\linux\device\class.h
/**
* class_create - create a struct class structure
* @owner: pointer to the module that is to "own" this struct class
* @name: pointer to a string for the name of this class.
*
* This is used to create a struct class pointer that can then be used
* in calls to device_create().
*
* Returns &struct class pointer on success, or ERR_PTR() on error.
*
* Note, the pointer created here is to be destroyed when finished by
* making a call to class_destroy().
*/
#define class_create(owner, name)
({
static struct lock_class_key __key;
__class_create(owner, name, &__key);
})
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。返回值是个指向结构体 class 的指针,也就是创建的类。
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
使用device_create函数在我们创建的类下面创建一个设备。
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
device_create 是个可变参数函数,
参数 class 就是设备要创建哪个类下面;
参数 parent 是父设备,一般为 NULL,也就是没有父设备;
参数 devt 是设备号;
参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;
参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件。返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原型如下:
void device_destroy(struct class *class, dev_t devt)
参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号。
Linux系统将可访问的内存空间分为了两部分,一部分是内核空间,一部分是用户空间,操作系统和驱动程序运行在内核空间(内核态),应用程序运行在用户空间(用户态)。
用户空间的代码只能通过内核暴露的系统调用接口来使用系统中的硬件资源,保证了操作系统自身的稳定性和安全性。
内核空间的代码更偏向于系统管理,用户空间的代码更偏向于业务逻辑实现,两者的分工不同。
从用户空间进入内核空间的方式有三种:系统调用,软中断,硬中断
用户空间和内核空间数据交换的两个系统调用函数:
unsigned long 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;
}
作用:将内核空间的数据复制到用户空间
参数:*to 用户空间的指针。
*from内核空间的指针,n是内核空间向用户空间拷贝的字节数。
函数返回值:成功返回0;
unsigned long 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;
}
作用:将用户空间的数据复制到内核空间
参数:*to 内核空间的指针。
*from用户空间的指针,n是交换数据的长度。
函数返回值:成功返回0;
无法归类的五花八门的设备定义为杂项设备,其主设备号固定为10,由字符设备不管是动态分配还是静态分配设备号都会消耗一个主设备号,比较浪费主设备号。
杂项设备使用miscdevice结构体描述,该结构体定义在include\linux\miscdevice.h中,
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};
次设备号一般使用MISC_DYNAMIC_MINOR,表示自动分配次设备号。
extern int misc_register(struct miscdevice *misc); //注册杂项设备
extern void misc_deregister(struct miscdevice *misc);//卸载杂项设备
并发:一个CPU在这个时间片执行任务一,在下个时间片执行任务二,由于CPU切换的时间非常快,所以很像是一个CPU同时执行任务一和任务二
并行:是并发的理想状态,两个CPU分别同时执行两个不同的任务。所以有时候把并发和并行统称为并发。
并发会造成多个程序同时访问一个共享资源,这种情况产生的问题就是竞争
Linux是一个多任务的操作系统,并发和竞争在Linux中非常的常见,如果不考虑并发与竞争,在访问共享资源时容易出现问题,而且这些问题往往不容易排查,很难定位。
(1)中断程序并发访问。中断是可以随时产生的,一旦产生中断,就会放下手头的工作,去执行中断中的任务,如果在执行中断中的任务的时候修改了共享资源,就会产生刚才我们讲的问题。
(2)抢占式并发访问。在Linux内核2.6版本以后,Linux内核支持了抢占,在支持抢占的情况下,正在执行的进程随时都有可能被抢占。
(3)多处理器(SMP)并发访问。多核处理器之间存在核间并发访问。
在编写驱动程序的时候,我们要尽量避免让驱动程序存在并发和竞争,Linux内核里面给我们提供了几种处理并发与竞争的方法,分别是:**原子操作,自旋锁,信号量,互斥体。**第一个是用来保护一个变量,后面三个保护一段代码
在Linux上用原子形容一个操作或者一个函数是最小执行单位,是不可以被打断的。所以原子操作指的是该操作在执行完之前不会被任何事物打断。
原子操作一般用于整形变量或者位的保护。比我,我们定义一个变量a,如果程序A正在给变量a赋值,此时程序B也要来操作变量a,这时候就发生了并发与竞争。程序A的操作就有可能会被程序B打断。如果我们使用原子操作对变量a进行保护,就可以避免这种问题。
Linux中定义了一个叫做atomic_t和atomic64_t的结构体来描述原子变量,其中atomic_t是用到32位系统中,atomic64_t是用在64位系统中。代码如下所示:
typedef struct {
int counter;
} atomic_t;
#ifdef CONFIG_64BIT
typedef struct{long counter;
} atomic64_t;#endif
自旋锁是以原地等待的方式解决资源冲突,是为了实现保护共享资源提出的一种锁机制。
即当线程A获取到自旋锁后,此时B也想获得自旋锁,但是线程B获取不到,只能原地打转(仍然占用CPU,不会休眠),不断尝试获取自旋锁,直到获取成功,然后才退出循环。
typedef struct spinlock {
union {
struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
函数 | 描述 |
---|---|
DEFINE_SPINLOCK(spinlock_t lock) | 定义并初始化一个变量 |
int spin_lock_init(spinlock_t *lock) | 初始化自旋锁 |
void spin_lock(spinlock_t *lock) | 获取自旋锁,也叫加锁 |
void spin_unlock(spinlock_t *lock) | 释放自旋锁,也交解锁 |
void spin_trylock(spinlock_t *lock) | 尝试获取自旋锁,如果没有获取到就返回0 |
void spin_is_lock(spinlock_t *lock) | 检查自旋锁是否被获取,如果没有被获取就返回非0,否则返回0 |
(1)在访问临界资源时先申请自旋锁
(2)获取到自旋锁以后就进入临界区,获取不到就原地等待
(3)退出临界区的时候释放自旋锁
临界区:加锁和解锁之间的区域
临界资源是加锁和解锁之间的代码
(1)由于自旋锁会原地等待,会继续占用CPU,会消耗CPU资源,所以锁的时间不能太长,也就是临界区的代码不能太多。
(2)在自旋锁保护的临界区里面不能调用可能导致线程休眠的函数,否则会发生死锁。
(3)自旋锁一般用在多核的SOC上
在多核CPU或者支持抢占的单核CPU中,被自旋锁保护的临界区不能调用任何能够引起睡眠或者阻塞的函数,否则可能会发生死锁。
如何避免
(1)如果在中断服务函数里面使用自旋锁,需要在驱动程序中使用spin_lock_irqsave和spin_unlock_irqstore来申请自旋锁,防止在执行临界区里的代码时被中断打断。
(2)避免在一个函数里多次获取自旋锁
(3)临界区代码不能太长
(4)临界区不能调用任何能够引起睡眠或者阻塞的函数
和自旋锁相比,信号量不需要原地等待,会引起调用者的睡眠,所以信号量也叫睡眠锁。
信号量本质是一个全局变量,其值可以根据实际情况来自行设置,当有线程来访问资源时,信号量执行“减一”操作,访问完以后执行“加一”操作。
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
函数 | 描述 |
---|---|
DEFINE_SEAMPHORE(name) | 定义信号量,并设置信号量的值为1 |
void sema_init(struct semaphore *sem, int val) | 初始化信号量sem并设置信号的值val |
void down(struct semaphore *sem) | 获取信号量,-1,不能被信号打断,如ctrl+c |
int down_interruptiable(struct semaphore *sem) | 获取信号量,-1,能被信号打断,如ctrl+c |
void up(struct semaphore *sem) | 释放信号量,+1 |
int down_trylock(struct semaphore *sem) | 尝试获取信号量,如果获取到信号量就返回0,获取不到就返回非0 |
(1)信号量的值不能小于0
(2)访问共享资源时,信号量执行“减一”操作,访问完成后再执行“加一”操作
(3)当访问信号量的值为0时,想访问共享资源的线程必须等待,直到信号量大于0时,等待的线程才可以访问
(4)因为信号量会引起休眠,所以中断里不能用信号量
(5)共享资源持有时间较长,一般用信号量而不用自旋锁
(6)在同时使用信号量和自旋锁时,要先获取信号量,再使用自旋锁,因为信号量会导致休眠
同一个资源同一时间只有一个访问者在进行访问,其他的访问者访问结束后才能访问这个资源,这就是互斥。
互斥锁和信号量值为1的情况很类似,但是互斥锁更简洁,更高效。
struct mutex {
atomic_long_t owner;
raw_spinlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
函数 | 描述 |
---|---|
DEFINE_MUTEX(name) | 定义并初始化一个互斥锁 |
void mutex_init(mutex *lock) | 初始化互斥锁 |
void mutex_lock(mutex *lock) | 上锁,如果不可以用则睡眠 |
void mutex_unlock(mutex *lock) | 解锁 |
void mutex_is_lock(mutex *lock) | 如果锁已经被使用则返回1,否则返回0 |
(1)互斥锁会导致休眠,所以在中断里面不能用互斥锁
(2)同一时刻只能有一个线程持有互斥锁,并且只有持有者可以解锁
(3)不允许递归上锁和解锁
输入输出模型,就是用户空间和内核空间进行数据交换的模型
应用程序运行在操作系统上,处在用户空间的应用程序不能直接对硬件进行操作,一个完整的IO过程包含以下几个步骤
(1)应用程序向操作系统发起IO调用请求(系统调用)。
(2)操作系统准备数据,把IO设备的数据加载到内核缓冲区。
(3)操作系统拷贝数据,把内核缓冲区的数据从内核空间拷贝到应用空间
IO执行过程中,CPU和内存的速度远远高于外设的速度,存在速度严重不匹配的情况。
阻塞IO,非阻塞IO,信号驱动IO,IO多路复用,异步IO,前四个被称为同步IO。
同步和异步的区别在于是否等待IO的执行结果,等待的是同步IO
内核数据没有准备好,一直等待着,等待数据准备好之后再把数据从内核空间拷贝到用户空间,如scanf函数
优点:及时获取数据
缺点:效率不高,等待过程不能干其它事情
数据没有准备好时,直接返回错误,系统会不断轮询查询数据是否准备好,准备好了再把数据从内核空间拷贝到用户空间。
优点:CPU可以去做其它事情
缺点:对CPU产生比较大的浪费,因为在不断轮询。
建立SIGIO的信号处理函数,不阻塞,一旦数据准备好,操作系统会以信号的方式通知用户来处理数据。
多了一个Select函数,函数里面有个参数是文件描述符的集合,这个函数会对这些文件进行一个监听,当某个文件描述符就绪的时候,就对这个文件描述符进行处理。类似多个非阻塞IO
当数据没有准备好,直接返回,不轮询,去做其它事情,当数据准备好之后,操作系统将数据拷贝到用户空间(系统调用提供的用户缓冲区),然后返回信号处理函数处理数据。
等待队列是内核实现阻塞和唤醒的内核机制。
等待队列以循环链表为基础结构,链表头和链表项分别为等待的队列头和等待队列元素。整个等待队列由等待队列头进行管理。
等待队列头使用结构体wait_queue_head_t来表示,这个结构体定义在文件include/linux/wait里面,结构体内容如下:
struct __wait_queue_head {
spinlock_t lock; //自旋锁
struct list_head task_list; //链表头
};
typedef struct __wait_queue_head wait_queue_head_t;
等待队列项的结构体描述如下:
struct wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head entry;
};
typedef struct __wait_queue wait_queue_t;
(1)定义一个等待队列头:
wait_queue_head_t test_wq; //定义一个等待队列的头
(2)初始化等待队列头
可以使用init_waitqueue_head函数初始化等待队列头, 函数原型如下:
void init_waitqueue_head(wait_queue_head_t *q)
使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义和初始化。
DECLARE_WAIT_QUEUE_HEAD (wait_queue_head_t *q);
一般使用宏定义DECLARE_WAITQUEUE(name,tsk)创建等待队列项
name是等待队列项的名字
task表示这个等待队列属于哪个任务(进程),一般设置为current。
在Linux内核中current相当于一个全局变量,表示当前进程。
举例:
DECLARE_WAITQUEUE(wait,current);//给当前正在运行的进程创建一个名为wait的等待队列项
add_wait_queue(wq,&wait);//将wait这个等待队列项加到wq这个等待队列当中
当等待队列不可访问的时候就需要将进程对应的等待队列添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠状态。当设备可以访问以后再将进程对应的等待队列从等待队列头中移除即可。
等待队列添加队列函数如下:
函数 | void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait) |
---|---|
q | 等待队列项要加入的等待队列头 |
wait | 要加入的等待队列项 |
返回值 | 无 |
功能 | 从等待队列头中添加队列 |
等待队列项移除队列函数如下:
函数 | void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait) |
---|---|
q | 要删除的等待队列项的等待队列头 |
wait | 要删除的等待队列项 |
返回值 | 无 |
初始化等待队列头,并将条件设置成假(condition=0)
在需要阻塞的地方调用wait_event(),使进程进入休眠
当条件满足时,需要解除休眠,先将条件置成真(condition=1),然后调用wake_up函数幻想等待队列中的休眠进程
wait_event宏
原型:wait_event(wq, condition)
功能:不可中断的阻塞等待,让调用进程进入不可中断的睡眠状态,在等待队列里面睡眠直到condition变成真,被内核唤醒。
wq :wait_queue_head_t 类型变量。
condition: 为等待的条件,为假时才可以进入休眠
注意:调用的时要确认condition 值是真还是假,如果调用condition为真,则不会休眠。
wait_event_interruptible宏
原型:wait_event_interruptible(wq, condition)
功能:可中断的阻塞等待,让调用进程进入可中断的睡眠状态,直到condition变成真被内核唤醒或被信号打断唤醒。
wq :wait_queue_head_t 类型变量。
condition为等待的条件,为假时才可以进入休眠,如果是真则不会休眠
原型:wake_up(x)
功能:唤醒所有休眠进程
原型:wake_up_interruptible(x)
功能:唤醒可中断的休眠进程
(1)初始化等待队列头,并将条件设置成假(condition=0)
(2)在需要阻塞的地方调用wait_event()使进程进入休眠
(3)当条件满足时,需要解除休眠,先将条件置成真(condition=1),然后调用wake_up函数唤醒等待队列中的休眠进程
要实现非阻塞,不仅驱动程序要支持非阻塞,应用程序也要支持非阻塞。
应用程序可以使用如下所示示例代码实现阻塞访问:
fd=open("/dev/xxx_dev",O_RDWR); /*阻塞方式打开*/
ret=read(fd, &data, sizeof(data));/*读取数据*/
设备驱动文件的默认读取方式就是非阻塞的
如果应用程序要采取非阻塞的方式来访问驱动设备文件,可以使用如下代码
fd=open("/dev/xxx_dev",O_RDWR|O_NONBLOCK);/*非阻塞方式打开*/
ret=read(fd,&data,sizeof(data));/*读取数据*/
使用Open函数打开设备文件时,如果添加了O_NONBLOCK参数,这样从设备驱动中读取数据的时候就是非阻塞方式的
IO多路复用可以实现一个进程监视多个文件描述符,一旦其中一个文件描述符准备就绪,就通知应用程序进行相应的操作。
在应用层提供了三种IO复用的API函数,分别是select,poll,epoll,其中poll和select基本一样,都可以监听多个文件描述符,通过轮询文件描述符来获取已经准备好的文件描述符。epoll是将主动轮询变成了被动通知,当事件发生时,被动的接收通知,效率更高,编程复杂一些。这三个函数Linux已经写好了,我们直接用就行了
在驱动中,需要实现file_operations结构体的poll函数
应用层中使用select和poll等系统调用会触发设备驱动中的poll()函数被执行,所以需要完善驱动中的poll函数,驱动中的poll函数原型:
unsigned int (*poll)(struct file *filp, struct poll_table *wait);
该函数要完成两项工作
poll_wait函数原型:
void poll_wait(struct file *flip, wait_queue_head_t *queue, poll_table *wait);
poll_wait函数不会引起阻塞
int poll(struct pollfd *fds, nfds_t, nfds, int timeout);
功能:监视并等待多个文件描述符的属性变化
参数:
fds:指向struct pollfd结构体,用于指定给定的fd条件
struct pollfd {
int fd;//被监视的文件描述符
short events;//等待的事件
short revents;//实际发生的事件
};
事件 | 常值 | 作为events的值 | 作为revents的值 | 说明 |
---|---|---|---|---|
读事件 | POLLIN | √ | √ | 可读 |
读事件 | POLLRDNORM | √ | √ | 可读 |
读事件 | POLLRDBAND | √ | √ | 可读 |
读事件 | POLLPRI | √ | √ | 可读 |
写事件 | POLLOUT | √ | √ | 可写 |
写事件 | POLLWRNORM | √ | √ | 可写 |
写事件 | POLLWRBAND | √ | √ | 可写 |
错误事件 | POLLERR | √ | 发生错误 | |
错误事件 | POLLHUB | √ | 错误挂起 | |
错误事件 | POLLNVAL | √ | 描述不是打开的文件 |
nfds:指定fds的个数
timeout:指定等待时间,单位是ms,无论I/O是否准备好,到时间poll都会返回。如果timeout等于0,立即返回。如果timeout等于-1,事件发生以后才返回。
返回值:失败返回-1,成功返回revents不为0的文件描述符个数
信号驱动IO不需要应用程序去查询设备的状态,一旦设备准备就绪,就触发SIGIO信号,该信号会通知应用程序数据已经到来。
注册信号处理函数,应用程序使用signal函数来注册SIGIO信号的信号处理函数。
设置能够接收这个信号的进程,通过fcntl函数来实现
开启信号驱动IO,也通常使用fcntl的F_SETFL命令来打开FASYNC标志
fcntl函数原型:
int fcntl(int fd, int cmd, ... /*arg*/)
功能:fcntl函数可以用来操作文件描述符
参数:
fd:被操作的文件描述符
cmd:操作文件描述符的命令,该参数决定了要如何操作文件描述符fd。
…:根据cmd 的参数来决定是否需要使用第三个参数
命令名 | 描述 |
---|---|
F_DUPFD | 复制文件描述符 |
F_GETFD | 获取文件描述符的标志 |
F_SETFD | 设置文件描述符的标志 |
F_GETFL | 获取文件状态标志 |
F_SETFL | 设置文件状态标志 |
F_GETLK | 获取文件锁 |
F_SETLK | 设置文件锁 |
F_SETLKW | 类似F_SETLK,但等待返回 |
F_GETOWN | 获取当前接收SIGIO和SIGURG信号的进程ID和进程组ID |
F_SETOWN | 设置当前接收SIGIO和SIGURG信号的进程ID和进程组ID |
步骤一:
当应用程序开启信号驱动中IO时,会触发驱动中的fasync函数,所以首先在file_operations结构体中实现fasync函数,函数原型如下:
int (*fasync) (int fd, struct file *filp, int on)
步骤二:
在驱动的fasync函数中调用fasync_helper函数来操作fasync_struct结构体,fasync_helper函数原型如下:
int fasync_helper(int fd, struct *filp,int on, struct fasync_struct **fapp)
步骤三:
当设备准备好后,驱动程序需要调用kill_fasync函数通知应用程序,此时应用程序的SIGIO信号处理函数就会被执行。kill_fasync负责发送指定的信号,函数原型如下:
void kill_fasync(struct fasync_struct **fp, int sig, int band)
函数参数:
fp:要操作的fasync_struct
sig:发送的信号
band:可读的时候设置成POLLIN,可写的时候设置成POLLOUT
Linux内核定时器是一种基于未来时间点的计时方式,以当前时刻为启动的时间点,以未来的某一时刻为终止点。
需要注意的是,内核定时器定时精度不高,不能作为高精度定时器使用。并且内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
Linux 内核使用 timer_list 结构体表示内核定时器, timer_list 定义在文件include/linux/timer.h 中,定义如下:
struct timer_list {
struct list_head entry;
unsigned long expires; /* 定时器超时时间,不是时长,单位是节拍数 */
struct tvec_base *base;
void (*function)(unsigned long); /* 定时处理函数 */
unsigned long data; /* 要传递给 function 函数的参数 */
int slack;
};
expires为计时终点时间,单位是节拍数。等于定时的当前的时钟节拍计数(存储在系统的全局变量jiffies)+定时时长对应的时钟节拍数量。内核中有一个宏HZ,表示一秒对应的时钟节拍数,那么我们就可以通过这个宏来把时间转换成节拍数。所以,定时1秒就是:expires = jiffies + 1*HZ。
jiffies是用来记录自系统启动以来产生的节拍的总数,启动时内核将该变量设置为0,此后每次时钟中断处理程序都会增加该变量的值。
定义在文件/include/linux/jiffies.h中
extern u64 __cacheline_aligned_in_smp jiffies_64;
extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;
Linux内核提供了几个jiffies和ms、us、ns之间的转换函数
函数 | 作用 |
---|---|
int jiffies_to_msecs(const unsigned long j) | 将jiffies类型的参数j转换成毫秒 |
int jiffies_to_usecs(const unsigned long j) | 将jiffies类型的参数j转换成微秒 |
u64 jiffies_to_nsecs(const unsigned long j) | 将jiffies类型的参数j转换成纳秒 |
long msecs_to_jiffies(const unsigned int m) | 将毫秒转换成jffies类型 |
long usecs_to_jiffies(const unsigned int u) | 将微秒转换成jffies类型 |
unsigned long nsecs_to_jiffies(u64 n) | 将纳秒转换成jffies类型 |
原型:#define DEFINE_TIMER(_name, _function, _expires, _data)
作用:静态定义结构体变量并且初始化初始化function, expires, data 成员。
参数:
_name 变量名,是timer_list结构体;
_function 超时处理函数 ;
_data 传递给超时处理函数的参数
_expires到点时间,一般在启动定时前需要重新初始化。
原型:void add_timer(struct timer_list *timer)
作用:add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行
在驱动出口函数使用del_timers函数删除定时器
原型:int del_timer(struct timer_list * timer)
作用:del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。
函数原型:int mod_timer(struct timer_list *timer, unsigned long expires)
作用:mod_timer 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时器!
dmesg命令
作用:显示内核打印信息
用法:dmesg[参数]
常用参数:
-C ,清除内核环形缓冲区
-c,读取并清除所有信息
-T,显示时间戳
可以和grep命令组合使用,dmseg |grep usb 查找带有usb关键字的打印信息
cat /proc/kmsg
此方法需要开两个终端,会阻塞等待内核打印信息
内核日志的打印是有打印等级的,可以通过调整内核的打印等级来控制打印日志的输出,使用命令cat /proc/sys/kernel/printk可以查看默认的的打印等级信息
打印有等级有四个等级,分别有
console_loglevel(控制台日志等级)、4
default_message_logevel(默认消息等级)、4
minimum_console_loglevel(最低控制台日志等级)、1
default_console_logevel(默认控制台日志等级),7
这四个等级定义在kernel/printk/printk.c文件
int console_printk[4] = {
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel */
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel */
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel */
};
内核提供了8种不同的日志级别,分别对应0~7,数字越小级别越高,定义在include/linux/kern_levels.h文件当中。
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
在内核打印的时候,只有数值小于(级别高)当前系统设置的打印等级,打印信息才可以被显示到控制台上,大于或者等级的打印信息不会被显示到系统上。
①通过make menuconfig图新配置化界面配置
②在调用printk的时候设置打印等级
printk(KERN_EMERG “hello!”);
③使用echo直接修改打印等级
查看内核打印等级:cat /proc/sys/kernel/printk
修改控制台打印等级:
屏蔽所有打印:echo 0 4 1 7 > /proc/sys/kernel/printk
打开控制台所有打印: echo 7 4 1 7 > /proc/sys/kernel/printk
函数原型:off_t(int fd, off_t offset, int whence);
函数参数:
fd:要操作的文件描述符。
offset:以whence为基准的偏移量(单位是字节)。
whenece:可以为SEEK_SET(文件指针开头),SEEK_CUR(文件指针当前位置),SEEK_END(文件指针末尾)
返回值:成功返回文件读写指针距离文件开头的字节大小,失败返回-1.
举例:获取文件的长度
ret=lseek(fd, 0, SEEK_END);
举例:
ret =lseek(fd, 10, SEEK_CUR);
完善file_operations结构体中的llseek函数
设备驱动不仅要具备读写的能力,还要具备对硬件的控制能力,这些控制操作通常需要非数据的操作,即通过ioctl操作来实现。
函数原型:
long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long arg);
参数:
struct file *file,打开字符设备进程创建的结构体
unsigned int cmd,用户空间传递的命令
unsigned long arg,配合cmd用的参数
返回值:成功返回0,失败返回负值
头文件:#include
函数原型:
int ioctl(int fd, unsigned long request, ...);
参数:
fd,打开设备节点获得的文件描述符
cmd,给驱动传递的命令
第三个参数,可变参数
返回值:成功返回0,失败返回-1
设备类型(0~7)bit | 代表一类设备,一般用一个字母或者一个8bit的数字来表示 |
---|---|
序列号(8~15)bit | 代表是这个设备的第几个命令 |
方向(16~17)bit | 表示命令的方向,如,只读(10),只写(01),写读(11),无数据(00) |
数据大小(18~31)bit | 用户数据的大小,注意这里传递的不是数字,而是数据类型,比如要传递四个字节,就可以写int |
ioctl命令合成宏
_IO(type,nr) //用来定义没有数据传递的命令
_IOR(type,nr,size) //用来定义从驱动中读取数据的命令
_IOW(type,nr,size) //用来定义向驱动写入数据的命令
_IOWR(type,nr,size) //用来定义数据交换类型的命令,先写入数据,再读取数据这类命令。
/*参数:
type:表示设备的类型,上面表格的0~7bit
nr:表示命令组成的序列号,8~15bit
size:表示命令组成的参数传递大小,注意这里不是传递数字,而是数据类型,如要传递4字节,就可以写成int。18~31bit。*/
//举例
#define CMD_TEST1 _IO('A', 0)
#define CMD_TEST0 _IO('L', 0)
#define CMD_TEST2 _IOW('A',1,int)
ioctl命令分解宏
_IOC_DIR(cmd) //分解命令的方向,也就是上面说16~17位的值
_IOC_TYPE(cmd) //分解命令的类型,也就是上面说0~7位的值
_IOC_NR(cmd) //分解命令的序列号,也就是上面说8~15位
_IOC_SIZE(cmd) //分解命令的复制数据大小,也就是上面说的18~31位
//举例
printf("CMD_TEST2 type is %ld\n", _IOC_TYPE(CMD_TEST2));
printf("CMD_TEST2 dir is %ld\n", _IOC_DIR(CMD_TEST2));
printf("CMD_TEST2 nr is %ld\n", _IOC_NR(CMD_TEST2));
printf("CMD_TEST2 size is %ld\n", _IOC_SIZE(CMD_TEST2));
//分别打印65(A的ASCII码),1(只写01),1(序列号是1),4(int是四个字节)
相比应用程序,驱动程序直接和底层硬件打交道,如果驱动程序出现问题相较于应用程序出现问题后果会非常严重,所以应尽量减小驱动程序出现问题的概率,主要思想是再一些关键的语句加上if条件判断语句,如果执行结果正确则继续往下执行,不正确则打印相关信息并return来结束程序。但是使用太多if条件判断语句会影响程序的效率,可以使用下面提到的unlikely和likely函数来提高效率
作用:
对代码运行效率有要求的if-else或if分支就应该使用likely或unlikely优化选项,其中:
if(likely(value))等价于if(value)
if(unlikely(value))等价于if(value)
使用场合:
现在的CPU都有ICache和流水线机制,运行当前的指令时,ICache会预读取后面的指令,从而提升效率,但是如果条件分支的结果是跳转到了其他指令,那预取下一条指令就浪费时间了,如果使用likely和unlikely来让编译器总是将大概率执行的代码放在靠前的位置,就可以提高效率了。
举例:
if(unlikely(value))
{
A;
}
else
{
B;
}
A代码是小概率执行的,B代码是大概率执行地方,这样设置编译器就会将大概率执行的代码B放在考前的地方,这样预读取的时候就会先预读取B代码的分支,从而提高了效率。
作用:检查用户空间的指针是否可用
函数原型:access_ok(addr, size);
函数参数:
addr,用户空间的指针变量,其指向一个要检查的内存块的开始
size,要检查的内存块的大小
函数返回值:如果检查用户空间的内存块可用,则返回真,否则返回假
除了printk函数,Linux内核还提供了下面几个函数来进行调试
作用:打印内核调用堆栈,并打印函数的调用关系
作用:打印函数的调用关系
作用:触发内核的OOPS,输出打印
作用:造成系统死机并输出打印
CPU在正常运行期间,由外部或者内部引起的事件,让CPU停下当前正在运行的程序,转而去执行触发他的中断所对应的程序,这个就是中断。
中断的存在可以极大的提高CPU的运行效率,但是中断会打断内核进程中的正常调度和运行,所以为保证系统实时性,中断服务程序必须足够简短,但实际应用中某些时候发生中断时必须处理大量的事物,这时候如果都在中断服务程序中完成,则会严重降低中断的实时性,基于这个原因,linux 系统提出了一个概念:把中断服务程序分为两部分:中断上文和中断下文。
中断上文:完成尽可能少且比较急的任务,中断上文的特点就是响应速度快。
中断下文:处理中断剩余的大量比较耗时间的任务,而且可以被新的中断打断。
Linux系统中断源数量远多于51单片机的,CPU直接去通过配置寄存器来控制中断会很低效,Linux系统的CPU通过中断控制器来控制中段
中断控制器可以来控制中断的打开,关闭,优先级控制等等,ARM架构的中断控制器一般叫做GIC,中断控制器之间可以级联来满足不同的需求。
IRQ number也叫软中断号,在Linux系统中是唯一的,也是我们在编程时要用到的中断号;
HW interrupt ID硬件中断号,中断控制器需要对外设中断进行编号,中断控制器使用HW interrupt ID来标识外设中断。
IRQ domain负责实现硬件中断号与软件中断号的映射
中断号在0~15之间,用于core之间通信,也叫软件中断
中断号在16~31之间,此类中断是每个core私有的,所以也叫做私有中断,比如每个core上有一个tick中断,用于进程调度使用。
中断号在32~1020之间,此类中断是由外设触发的中断信号,比如按键,串口等中断,也叫做共享中断
基于消息的中断,此类中断不支持GIC-v1,GIC-v2
发展历史,GIC-v1,GIC-v2,GIC-v3,GIC-v4,
GIC是CPU请来管理中断的帮手,GIC里面有两个单元,分别是Distributor(仲裁器)和 CPU Interface(CPU接口),外设通过AMBA接口连接到仲裁器上。
Distributor:系统中所有中断源都与之相连,它有寄存器可以控制每个中断的属性,比如优先级,状态,安全状态,路由信息以及使能状态等。Distributor通过连接的CPU接口2,可以决定将哪个中断转发到指定的core中
CPU接口:core通过接口来结束中断,CPU通过寄存器来屏蔽、确认以及控制转发到指定core的中断,在系统中每个core有个独立的CPU接口。
GIC通过CPU接口向CPU汇报时,只有快速中断(FIQ),外部中断(IRQ),虚拟化快速中断(virtual FIQ),虚拟化的外部中断(virtual IRQ)这四种窗口。
函数原型:
int request_irq( unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev)
函数作用:向Linux内核申请一个中断
参数:
irq:要申请中断的中断号。软件中断号
handler:指向中断处理函数的指针,当中断发生以后就会执行此中断处理函数。
flags:中断标志。
name: 中断名字, 设置以后可以在/proc/irq 文件中看到对应的中断名字。
dev: 中断发生后调用中断处理函数传递给中断处理函数的参数,如果中断设置标志为共享(IRQF_SHARED)的话,此参数用来区分具体的中断,共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:成功返回0,失败返回非0
函数原型:
int gpio_to_irq(unsigned int gpio)
函数作用:获取gpio中断号
函数参数:gpio的编号,不同开发板的计算方法不同
返回值:Linux系统分配的gpio对应的中断号
函数原型:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。 第二个参数是一个指向 void 的指针, 也就是个通用指针, 需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。
include/linux/interrupt.h 文件里可以找到
#define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010
函数原型:
void free_irq(unsigned int irq,void *dev)
参数:
irq: 要释放的中断的中断号。
dev: 如果中断设置为共享(IRQF_SHARED)的话, 此参数用来区分具体的中断。 共享中断
只有在释放最后中断处理函数的时候才会被禁止掉。
request_irq
申请的中断服务函数只是中断的上文,中断分成中断上文和中断下文,在中断上文中做比较着急的事情(比如申请标志位),下文做比较费时的事情(比如读取寄存器的值)。
Linux系统将中断分为中断上下文可以保证系统的实时性,中断下文的实现方式有很多种,tasklet是一种最常用的。
tasklet是处理中断下文常用的一种方法,tasklet是一种特殊的软中断。处理中断下文的机制还有工作队列和软中断。
tasklet绑定的函数在同一时间只能在一个CPU上运行(每个CPU会维护一个tasklet链表),所有在多核处理系统上不会出现并发的问题,在tasklet绑定的函数中不能调用任何可能引起休眠的函数,否则会导致内核异常。
tasklet由tasklet_struct结构表示,每个结构体单独代表一个tasklet,在
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
//激活tasklet
#define DECLARE_TASKLET(name, _callback)
struct tasklet_struct name = {
.count = ATOMIC_INIT(0),
.callback = _callback,
.use_callback = true,
}
//不激活tasklet
#define DECLARE_TASKLET_DISABLED(name, _callback)
struct tasklet_struct name = {
.count = ATOMIC_INIT(1),
.callback = _callback,
.use_callback = true,
}
1.next:链表中的下一个tasklet,方便管理和设置tasklet;
2.state: tasklet的状态。表示当前tasklet是否被调度
3.count:表示tasklet是否出在激活状态,如果是0,就处在激活状态,如果非0,就处在非激活状态
4.void (*func)(unsigned long):结构体中的func成员是tasklet的绑定函数,data是它唯一的参数。即中断下文的函数
5.date:传递给中断下文函数的参数
#define DECLARE_TASKLET(name, func, data)
struct tasklet_struct name = {NULL, 0, ATOMIC_INIT(0),func,data
}
功能:定义一个名为name的tasklet,初始化状态为使能状态
参数:
func,tasklet绑定的函数;data,函数执行时传递的参数
#define DECLARE_TASKLET_DISABLED(name, func, data)
struct tasklet_struct name = {NULL, 0, ATOMIC_INIT(0),func,data
}
功能:定义一个名为name的tasklet,初始化状态为非使能状态
参数:
func,tasklet绑定的函数;data,函数执行时传递的参数
函数原型:
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
参数:
*t:指向tasklet_struct结构的指针。
func:tasklet绑定的中断下文函数。
data:中断下文函数执行的时候传递的参数。
void tasklet_disable(struct tasklet_struct *t)
功能:把tasklet变成非使能状态,本质上是将count成员的值加一
void tasklet_enable(struct tasklet_struct *t)
功能:把tasklet变成使能状态,本质上是将count成员的值减一
void tasklet_schedule(struct tasklet_struct *t)
在tasklet处于使能状态时,调用tasklet调度函数,绑定的调度函数会在不确定的时间后被调度。如果tasklet处于非使能状态,执行tasklet_schedule调度函数不会执行绑定的中断下文函数
tasklet_kill(struct tasklet_struct *t)
取消一个已经调度的tasklet,但是这个函数会等待已经绑定的函数执行完成
软中断也是实现中断下半部分的方法之一,但是软中断的资源有限,对应的中断号不多,一般用在网络设备驱动,块设备驱动中。
void open_softirq(int nr, void (*action)(struct softorq_action *));
nr是软中断号,第二个参数是中断下文函数
void raise_softirq(unsigned int nr);
void raise_softirq_irqoff(unsigned int nr)
当我们使用tasklet调度函数的时候,会把用户之间定义的tasklet放在链表上,触发软中断。
tasklet和软中断的区别,tasklet可以动态添加中断号,软中断不建议添加中断号
工作队列(workqueue)是实现中断下文的机制之一,是一种将工作推后执行的形式。
和tasklet相比,他们俩个最主要的区别是tasklet不能休眠,而工作队列是可以休眠的。所以,tasklet可以用来处理比较耗时间的事情,而工作队列可以处理非常复杂并且更耗时间的事情。
工作队列将工作推后以后,会交给内核线程去执行,Linux在启动过程种会创建一个工作者内核线程,这个线程创建以后处于sleep状态,当有工作需要处理的时候,会唤醒这个线程去处理这个工作。
内核工作队列分为共享工作队列(Linux内核已经创建好了)和自定义工作队列两种。
不需要自己创建,但是如果前面的工作比较耗时间,就会影响后面的工作。
需要自己创建,系统开销大。优点是不会受到其他工作的影响。
结构体work_struct来描述的,定义在Linux\work\queue.h里面。
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;//中断下文要执行的函数,即工作函数
};
typedef void (*work_func_t)(struct work_struct *work);//工作函数
原型:
#define DECLARE_WORK(n,f)
struct work_struct n = _WORK_INITIALIZER(n,f)
作用:静态定义并初始化要推迟的工作队列work_struct
#define INIT_WORK(_work, _func)
__INIT_WORK((_work), (_func), 0)
作用:动态定义并初始化要推迟的工作队列work_struct
static inline bool_schedule_work(struct work_struct *work);
作用:调度工作,把work_struct挂到CPU相关的工作结构队列链表上,等待工作者线程处理。
bool cancel_work_sync(struct work_struct *work);
作用:取消一个已经调度的工作队列,如果被取消的工作已经在执行,则会等待它执行完再返回
内核使用struct workqueue_struct结构体描述一个工作队列,定义include/linux/workqueue.h
struct workqueue_struct {
struct list_head pwqs; /* WR: all pwqs of this wq */
struct list_head list; /* PR: list of all workqueues */
struct mutex mutex; /* protects this wq */
int work_color; /* WQ: current work color */
int flush_color; /* WQ: current flush color */
atomic_t nr_pwqs_to_flush; /* flush in progress */
struct wq_flusher *first_flusher; /* WQ: first flusher */
struct list_head flusher_queue; /* WQ: flush waiters */
struct list_head flusher_overflow; /* WQ: flush overflow list */
struct list_head maydays; /* MD: pwqs requesting rescue */
struct worker *rescuer; /* MD: rescue worker */
int nr_drainers; /* WQ: drain in progress */
int saved_max_active; /* WQ: saved pwq max_active */
struct workqueue_attrs *unbound_attrs; /* PW: only for unbound wqs */
struct pool_workqueue *dfl_pwq; /* PW: only for unbound wqs */
#ifdef CONFIG_SYSFS
struct wq_device *wq_dev; /* I: for sysfs interface */
#endif
#ifdef CONFIG_LOCKDEP
char *lock_name;
struct lock_class_key key;
struct lockdep_map lockdep_map;
#endif
char name[WQ_NAME_LEN]; /* I: workqueue name */
/*
* Destruction of workqueue_struct is RCU protected to allow walking
* the workqueues list without grabbing wq_pool_mutex.
* This is used to dump all workqueues from sysrq.
*/
struct rcu_head rcu;
/* hot fields used during command issue, aligned to cacheline */
unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */
struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */
struct pool_workqueue __rcu *numa_pwq_tbl[]; /* PWR: unbound pwqs indexed by node */
};
原型:
#define create_workqueue(name)
alloc_workqueue("%s", __WQ_LEGACY|WQ_MEM_RECLAIM, 1, (name))
作用:给每个CPU都创建一个CPU相关的自定义工作队列
参数:name,创建的工作队列的名字
返回值:创建成功返回一个struct workqueue_struct类型指针,创建失败返回NULL。
原型:
#define create_singlethread_workqueue(name)
alloc_workqueue("%s", __WQ_LEGACY|WQ_MEM_RECLAIM, (name))
作用:只给一个CPU创建一个CPU相关的自定义工作队列
参数:name,创建的工作队列的名字
返回值:创建成功返回一个struct workqueue_struct类型指针,创建失败返回NULL。
extern bool queue_work_on(int cpum struct workqueue_struct *wq, struct work_struct *work);
作用:把要延迟执行的工作放在工作队列上,调度工作队列
bool cancel_work_sync(struct work_struct *work);
作用:取消一个已经调度的工作,如果被取消的工作已经正在执行,则会等待它执行完再返回
extern void flush_workqueue(struct workqueue_struct *wq);
作用:刷新工作队列,告诉内核尽快处理工作队列上的工作
extern void destroy_workqueue(struct workqueue_struct *wq);
作用:删除自定义的工作队列
内核使用struct delayed_work结构体描述一个延迟工作,定义再include/linux/workqueue.h当中
//共享工作队列和定时器结构体的封装
struct delayed_work
{
struct work_struct work;
struct timer_list timer;
};
#define DECLARE_DELAYED_WORK(n,f)
struct delayed_work n=_DELAYED_WORK_INITILAIZER(n,f,0)
作用:静态定义并初始化延迟工作结构体
n是要延迟的工作,f是中断下文函数
#define INIT_DELAYED_WORK(_work,_func)
_INIT_DELAYED_WORK(_work,_func,0)
作用:动态定义并初始化延迟工作结构体
static inline bool schedule_delayed_work(struct delayed_work *dwork, unsigned long delay)
作用:在共享工作队列上调度延迟工作
参数:
*dwork,延迟的工作。delay,要延迟的时间,单位是节拍
static inline bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay)
作用:在自定义工作队列上调度延迟工作
参数:
*wq,自定义的工作队列。
*dwork,延迟的工作
delay,要延迟的时间,单位是节拍
extern bool cancel_delayed_work_sync(struct delayed_work *dwork);
作用:取消已经调度的延迟工作
函数原型:
alloc_workqueue(fmt, flags, max_active)
功能:创建一个工作队列
参数:
fmt,创建的工作队列的名称
flags,参数可以选中WQ_UNBOUND(处理工作队列上的工作的线程是不和CPU绑定的),WQ_FREEZABLE,WQ_HIGHPRI(把工作交给高优先级的线程做),WQ_CPU_INTENSIVE(耗时的工作),WQ_MEM_RECLAIM(避免死锁)。
max_active:线程池里最大的线程数量
中断线程化的处理仍然可以看作是将原来的中断分成中断上半部分和中断下半部分,上半部分还是用来处理紧急的事情,下半部分也是处理比较耗时的操作,但是下半部分会交给一个专门的内核线程来处理,这个内核线程只用于这个中断,当发生中断的时候,会唤醒这个内核线程,然后由这个内核线程来执行中断下半部分的函数。
有多少个中断下半部分就有多少个内核线程。
函数原型
//实际上是对request_irq函数进行了封装
int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev);
函数参数:
irq,中断号
handler,中断上半部分函数
thread_fn,中断线程,如果此处设置为NULL,则表示没有使用中断线程化,需要通过中断上半部分函数去唤醒该线程,通过返回值的类型来唤醒
flags,中断标志位
name,中断的名称
dev,共享中断时使用
函数返回值:成功返回0,失败返回非0
平台总线模型也叫platform总线模型,是Linux系统虚拟出来的总线,没有实际的物理硬件的接口。
平台总线模型将一个驱动拆分成了两个部分,分别是device.c和driver.c,
device.c用来描述硬件,里面存放如gpio寄存器的地址,中断号等信息(设备)
driver.c用来控制硬件,如何操作gpio,如何申请中断等等(驱动)
平台总线通过字符串比较,将name相同的device.c和driver.c匹配到一起来控制硬件,name不是指这两个.c的文件名,而是这两个文件里面结构体的名字。
减少编写重复代码,提高效率,提高代码的利用率。
device.c里面写的是硬件资源,这里的硬件资源指的是寄存器地址,中断号以及其他硬件资源,在Linux里面通过struct platform_device结构体来描述硬件资源,这个结构体定义在include/linux/platform_device.h文件当中
struct platform_device {
const char *name;//设备名字,平台总线进行匹配的时候用到的name
int id;//设备id,用来区分不同的设备,一般设置成-1,没有后缀
bool id_auto;//自动设置id,一般不使用
struct device dev;//设备通用属性部分,必须实现dev的release函数,不然无法编译驱动
u64 platform_dma_mask;
struct device_dma_parameters dma_parms;
u32 num_resources;//用到的资源的个数
struct resource *resource;//存放device描述设备的硬件资源,可以有多个资源,数量由num_resources
const struct platform_device_id *id_entry;
char *driver_override; /* Driver name to force a match */
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
struct pdev_archdata archdata;
};
struct resource {
resource_size_t start;//资源的起始信息,如寄存器的起始地址
resource_size_t end;//资源的终止信息,如寄存器的终止地址
const char *name;//资源的名字,存储什么资源起什么名字,如果存放中断,可以起irq,可以不写
unsigned long flags;/*存储的资源的类型,种类类型的宏定义在include\linux\ioport.h下,如 #define IORESOURCE_IO IO的内存
#define IORESOURCE_MEM 表述一段物理内存
#define IORESOURCE_IRQ 表示中断*/
unsigned long desc;
struct resource *parent, *sibling, *child;
};
int platform_device_register(struct platform_device *device)//把device加载到总线
void platform_device_unregister(struct platform_device *device)//把device从总线卸载
platform设备驱动(driver.c)里面写的是软件驱动。在driver.c里面需要定义一个platform_driver结构体,然后去实现这个结构体中各个成员的变量,当driver.c和device.c匹配成功以后,会执行driver.c里面的probe函数.
platform_driver这个结构体定义在include/linux/platform_device.h文件当中
struct platform_driver {
int (*probe)(struct platform_device *);//device和driver匹配成功后自动执行的函数
int (*remove)(struct platform_device *);//移除一个设备要执行函数
void (*shutdown)(struct platform_device *);//和电源管理相关,关掉一个设备
int (*suspend)(struct platform_device *, pm_message_t state);//和电源管理相关,挂起一个设备
int (*resume)(struct platform_device *);//和电源管理相关,恢复一个设备
struct device_driver driver;//设备共用的属性,该结构体里的name成员的值和device.c文件中platform_device结构体中的name成员的值保持相同才能匹配成功
const struct platform_device_id *id_table;//设备的id表,该结构体里面也有一个name成员,在device.c和driver.c匹配时,会优先匹配这个id表里面的name,如果没有匹配到在匹配device_driver结构体里面的name成员
bool prevent_deferred_p,robe;
};
int platform_driver_register(struct platform_driver *driver)//把driver加载到总线
void platform_driver_unregister(struct platform_driver *driver)//把driver从总线卸载
驱动是要控制硬件的,但是平台总线模型对硬件的描述是在设备(device)中的,所以在驱动(driver)中,需要得到设备(device)的硬件资源。当device和driver匹配成功以后,会执行driver中的probe函数,所以我们要在probe函数中拿到device中的硬件资源。
struct resource *platform_get_resource(struct platform_device *dev,
unsigned int type, unsigned int num);
作用:获取device中的硬件资源(resource)
参数:
dev,设备结构体
type,资源的类型,是resource结构体里面的flags
num,索引号,资源处在同类型资源的哪个位置上,同类资源指的是和flags是一样的同类资源,并不是resource的数组号,从0开始
设备树是一种描述硬件资源的数据结构,因为语法结构像树一样,所以叫设备树。
它用来替代原来平台总线模型中的device部分的代码。虽然用设备树替换了原来的device部分,但是平台总线模型的匹配和使用基本不变,并且对硬件修改以后不必重新编译内核,只需要将设备树文件编译成二进制文件,再通过bootloader传递给内核即可。
它通过bootloader将硬件资源传给内核,使得内核和硬件资源描述相对独立。
<1>DT:Device Tree //设备树
<2>FDT:Flattened Device Tree //展开设备树//开放固件,设备树起源于OF,所以我们在设备树中可以看到很多有of字母的函数
<3>dts:device tree source//设备树源码
<4>dtsi:device tree source include//更通用的设备树源码,也就是相同芯片但不能平台都可以使用的代码
<5>dtb:device tree blob//DTS编译后得到的DTB文件
<6>dtc:device tree compiler//设备树编译器
相关文件路径:arch\arm\boot\dts
dtc编译器路径:scripts\dtc,源码编译完成后会生成dtc可执行文件
编译设备树:dtc -I dts -O dtb -o xxx.dtb xxx.dts(将dts编译成dtb文件)
反编译设备树:dtc -I dtb -O dts -o xxx.dts xxx.dtb(将dtb编译成dts文件)
根节点是设备树必须要包含的节点,根节点的名字是/。
/dts-v1/; //第一行表示dts文件的版本
/
{
//根节点
};
[label:]node-name[@unit-address]
{
[properties definitions]
[child nodes]
};
[ ]里的内容可用省略
同级节点下面的节点名称不能相同,不同级节点下的名称可以相同
格式:[标签]:<名称>[@<设备地址>]
[标签]和[@<设备地址>]是可选项,<名称>是必选项
这里的节点地址没有实际意义,只是让节点名称更人性化,更方便阅读
例子:
uart8: serial@02288000
uart8就是这个节点名称的别名,serial@02288000就是节点名称。
reg属性可以来描述地址信息,比如寄存器的地址
格式:reg=
add代表起始地址,length代表要描述的这段地址的长度
#address-cells用来设置子节点中reg地址的数量
#size-cells用来设置子节点中reg地址长度的数量。
model属性的值是一个字符串,一般用model描述一些信息,比如设备的名称,名字等等
举例:
model = “This is Linux board”;
status属性是和设备状态有关的,值是字符串,有下面几个状态可选
属性值 | 描述 |
---|---|
okay | 设备是可用状态 |
disabled | 设备是不可用状态 |
fail | 设备是不可用状态并且设备检测到了错误 |
fail-sss | 设备是不可用状态且设备检测到了错误,sss是错误内容 |
compatiable属性是非常重要的一个属性,compitable是用来和驱动进行匹配的,匹配成功以后会执行驱动中的probe函数。
举例:
compatiable = “xunwei”, “xunwei-board”;
在匹配的时候会先使用第一个值xunwei进行匹配,如果没有匹配成功就会使用第二个值进行匹配
特殊节点aliases用来定义别名,定义别名的目的就是为了方便引用节点。除了使用aliases来命名别名,也可以对节点添加标签命名别名。aliases可以批量定义别名。
举例:
aliases{
mmc0=&sdmmc0;
mmc1=&sdmmc1;
mmc2=&sdhci;
serial0="/simple@fe00000/serial@llc500";
};
uboot可以给内核传递bootargs参数,而特殊节点chosen可以设置bootargs参数。chosen节点必须是根节点的子节点
举例:
chosen
{
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
device_type属性
在某些设备树文件中,可以看到device_type属性,该属性的值是字符串,只用于cpu节点或者memory节点进行描述。
举例:
memory@30000
{
device_type="memory";
reg=<0x30000 0x40000>;
};
自定义属性
设备树中规定的属性有时候不能满足我们的需求,这时候我们可以自定义属性。
举例:
自定义一个管脚标号的属性pinnum.
pinnum=<0 1 2 3 4>;
1,在中断控制器中,必须有一个属性#interrupt-cells,表示其他节点如果使用这个中断控制器需要几个cell来表示使用哪个中断
2,在中断控制器中,必须有一个属性interrupt-controller,表示他是中断控制器
3,在设备树中使用中断,需要使用属性interrupt-parent=<&XXX>表示中断信号链接的是哪个中断控制器,接着使用interrupts属性来表示中断引脚和触发方式
Interrupt里有几个cell是由interrupt-parent对应的中断控制器里的#interrupt-cells属性决定的
在设备树中,时钟分为消费者(使用时钟信号)和生产者(产生时钟信号)。
1,#clock-cells,该属性代表时钟输出的路数,当该属性的值为0时,代表仅有1路时钟输出,当该属性的值大于等于1时,代表输出多路时钟
2,clock-output-names,该属性定义了输出时钟的名字
3,clock-frequency,该属性指定时钟的大小
4,assigned-clocks和assigned-clock-rates一般承兑使用,当输出多路时钟时,为每路时钟进行编号
举例:
cru:clock-controller@fdd20000
{
#clock-cells=<1?
assigned-clocks=<&pmucru CLK_RTC_32K>,<&cru ACLK_RKVDEC_PRE>; //使用pmucru模块输出CLK_RTC_32K时钟信号,频率为32769;使用cru模块输出ACLK_RKVDEC_PRE时钟信号,频率为3000000
assigned-clock-rates=<32769>,<3000000>;
}
5,clock-indices属性可以指定索引号(index),如果不使用这个属性,那么clock-output-names和index的对应关系就是0,1,2…,如果这个对应关系不是线性的,那么可用通过clock-indices属性来定义映射到clock-output-names的index,索引号可以在引用的时候使用
6,assigned-clock-parents属性可以用来设置时钟的父时钟
clock:clock
{
assigned-clocks=<&clkcon 0>, <&p11 2>;//0和2就是索引号index
assigned-clock-parents=<&p11 2>;//把clckcon,0挂载到p11
assigned-clock-rates=<115200>,<9600>;
}
clocks属性和clock-names属性分别用来指定使用的时钟源和消费者中时钟的名字
举例:
clock:clock
{
clocks=<&cru CLKVOP>; //cru是clock&reset_unit的缩写,CLKVOP是用宏定义来表示索引号
clock-names="clk_vop",;
}
CPU的层次结构是通过不同的节点来描述系统中物理CPU的布局
里面包含物理CPU的布局,CPU的布局全部在此节点下描述
描述单核处理器不需要使用cpu-map节点,该节点主要是用在描述大小核架构处理器中,该节点的名称必须是cpu-map,该节点的父节点必须是cpus节点,子节点必须是一个或多个的cluster和socket节点
该节点描述的是主板上CPU插槽,主板上有几个CPU插槽就有几个socket节点,该节点的子节点必须是一个或多个cluster节点,当有多个cpu插槽时,socket节点的命名方式必须是socketN,N=0,1,2…
该节点用来描述CPU的集群,比如RK399的架构是双核A72+四核A53,双核是A72是一个集群,用一个cluster节点来描述,四核A53也是一个节点,用一个cluster节点来描述,cluster节点的命名方式必须是clusterN,N=0,1,2…,cluster的子节点必须是一个或者多个cluster节点或一个或者多个的core节点
core节点用来描述一个CPU,如果是单核CPU,则core节点就是cpus节点的子节点,core节点的命名方式必须是coreN,N=0,1,2…,core的子节点必须是一个或者多个thread节点
该节点用来描述处理的线程(超线程技术),节点的命名方式必须是threadN,N=0,1,2…
1,在GPIO控制器中,必须有一个属性#gpio-cells,表示其他节点如果使用这个GPIO控制器需要几个cell来表示使用哪个GPIO
2,在GPIO控制器中,必须有一个属性gpio-controller,表示他是GPIO控制器
3,在设备树中使用GPIO,需要使用属性data-gpios=<&gpio1 12 0>来指定具体的GPIO引脚,data-gpios属性可用为自定义属性
Linux内核提供了pinctrl子系统,pinctrl是pincontroller的缩写,目的是为了统一各芯片原厂的Pin脚管理,所以一般pinctrl子系统的驱动是由芯片原厂的BSP工程师实现。
有了pinctrl子系统以后,驱动工程师就可用通过配置设备树使用pinctrl子系统区设置管脚的复用以及管脚的电气属性
pinctrl语法由两个部分构成,一部分是客户端(client),一部分是服务端(service)。客户端源码是固定格式的,服务器代码格式不固定,跟平台相关。
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
含义:
<1>pinctrl-names = “default”;
设备的状态,可以有多个状态,default为状态0
<2>pinctrl-0 = <&pinctrl_hog_1>;
第0个状态所对应的引脚配置,也就是default状态对应的引脚在pin controller里面定义好的节点pinctrl_hog_1里面的管脚配置
pinctrl-names = "default","wake up";
pinctrl-0 = <&pinctrl_hog_1>;
pinctrl-1 = <&pinctrl_hog_2>;
含义
<1>pinctrl-names = “default”,“wake up”;
设备的状态,可以有多个状态,default为状态0,wake up为状态1
<2>pinctrl-0 = <&pinctrl_hog_1>;
第0个状态所对应的引脚配置,也就是default状态对应的引脚在pin controller里面定义好的节点pinctrl_hog_1里面的管脚配置
<3>pinctrl-1 = <&pinctrl_hog_2>;
第1个状态所对应的引脚配置,也就是wake up状态对应的引脚在pin controller里面定义好的节点pinctrl_hog_2里面的管脚配置
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1
&pinctrl_hog_2>;
含义
<1>pinctrl-names = “default”;
设备的状态,可以有多个状态,default为状态0
<2>pinctrl-0 = <&pinctrl_hog_1 &pinctrl_hog_2>;
第0个状态所对应的引脚配置,也就是default状态对应的引脚在pin controller里面定义好的节点,对应pinctrl_hog_1和pinctrl_hog_2这俩个节点的管脚配置
跟平台有关
dtb文件格式主要分为四个部分,small header(头部),memory reservation block(内存预留块),structure block(结构块),strings block(字符串块)。free space(自由空间,为了对齐的,不一定存在)
devicetree
的头布局由以下 C
结构定义。所有的头字段都是 32
位整数,以大端格式存储
struct fdt_header {
uint32_t magic;
uint32_t totalsize;
uint32_t off_dt_struct;
uint32_t off_dt_strings;
uint32_t off_mem_rsvmap;
uint32_t version;
uint32_t last_comp_version;
uint32_t boot_cpuid_phys;
uint32_t size_dt_strings;
uint32_t size_dt_struct;
};
字段 | 描述 |
---|---|
magic | 该字段应包含值 0xd00dfeed(大端)。该值是固定的 |
totalsize | dtb文件的总大小(以字节为单位)。 此大小应包含结构的所有部分:标题、内存保留块、结构块和字符串块,以及块之间或最后一个块之后的任何空闲空间间隙。 |
off_dt_struct | 该字段应包含structure block(结构块)从头开始的以字节为单位的偏移量。 |
off_dt_strings | 该字段应包含strings block(字符串块)从头开始的以字节为单位的偏移量。 |
off_mem_rsvmap | 该字段应包含从头开始的内存保留块的字节偏移量。 |
version | 该字段应包含设备树数据结构的版本。 如果使用本文档中定义的结构,则版本为 17。 DTSpec 引导程序可能会提供更高版本的设备树,在这种情况下,该字段应包含在提供该版本详细信息的较晚文档中定义的版本号。 |
last_comp_version | 该字段应包含使用的版本向后兼容的设备树数据结构的最低版本。 因此,对于本文档(版本 17)中定义的结构,该字段应包含 16,因为版本 17 向后兼容版本 16,但不兼容早期版本。 根据第 5.1 节,DTSpec 引导程序应以向后兼容版本 16 的格式提供设备树,因此该字段应始终包含 16 |
size_dt_strings | 该字段应包含设备树 strings block(字符串块)部分的字节长度。 |
size_dt_struct | 该字段应包含设备树structure block(结构块)部分的字节长度。 |
boot_cpuid_phys | cpu的ID值,与设备树 文件中CPU节点下的reg属性值相等 |
内存保留块为客户端程序提供物理内存中保留的区域列表;也就是说,不应将其用于一般内存分配。它用于保护重要数据结构不被客户端程序覆盖
内存预留块由一组 64
位大端整数对的列表组成,每对由以下 C
结构表示。
struct fdt_reserve_entry {
uint64_t address;
uint64_t size;
};
结构快描述的是设备树的结构,也就是设备树的节点,使用0x00000001表示节点的开始,然后跟上节点的名字(根节点名字用0表示),然后使用0x00000003表示属性的开始(每表示一个节点,都要用0x00000003表示开始),属性的名字和值用结构体表示如下:
struct {
uint32_t len;
uint32_t nameoff;
}
len
以字节为单位给出属性值的长度。nameoff
给出了字符串块的偏移量,在该块中属性的名称存储为以空字符结尾的字符串。使用0x00000002表示节点的结束,使用0x00000009表示根节点的结束,也是整个结构块的结束
字符串块用来存放属性的名字,比如compatible,reg等
dts源码文件经过dtc编译成dtb文件后,有的平台会把dtb打包进内核镜像,有的不会打包到一起。
但在系统启动的时候,uboot会将内核和设备树加载到内存的某个地址上,然后通过寄存器告诉内核dtb文件加载到了具体哪个地址,内核读这个地址获取到dtb文件后会在初始化的时候会将加载到内存里的dtb文件展开成内核可以识别的设备树。
struct device_node {
const char *name; //节点中的name属性
const char *type; //节点中的device_type属性
phandle phandle;
const char *full_name; //节点的名字
struct fwnode_handle fwnode;
struct property *properties; //指向该设备节点下的第一个属性,其它属性与该属性链表相连
struct property *deadprops; /* removed properties */
struct device_node *parent; //节点的父节点
struct device_node *child; //节点的子节点
struct device_node *sibling; //节点的同级节点,也叫兄弟节点
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj;
#endif
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
struct property {
char *name; //属性名字
int length; //属性长度
void *value; //属性值
struct property *next; //指向该节点的下一个属性
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)
unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr;
#endif
};
该函数是内核启动阶段的入口,在执行该函数之前虽然已经完成了一些初始化,但该函数之前是汇编代码,跳到start_kernel函数就是c语言代码了,所以该函数类似写程序的主函数main,该函数有许多根内核初始化有关的函数,这里只关注展开dtb相关的
setup_arch(&command_line);
根据开发板的不同架构,以arm64为例,这里跳到arch/arm64/kernel/setup.c
中的setup_arch
函数定义
boot_command_line
是一个4096大小的数组,记录的是uboot传递给内核的command_line
setup_arch
函数中的setup_machine_fdt(__fdt_pointer)
__fdt_pointer是dtb位于内存的地址,该值是由寄存器传递过来的,具体汇编代码在linux-5.15.10\arch\arm64\kernel\head.S
中
dtb的内存地址由X0传递给X1,X1再将内存地址保存到__fdt_pointer
void *dt_virt = fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL);
然后通过memblock_reserve(dt_phys, size);
保存dtb位于内存的地址,防止其它程序使用该地址
通过early_init_dt_scan(dt_virt)
函数来扫描dtb文件,dtb文件不能超过2MB
early_init_dt_scan(dt_virt)
函数中该函数early_init_dt_scan定义在drivers/of/fdt.c中,在early_init_dt_scan函数中先调用early_init_dt_verify进行校验,然后调用early_init_dt_scan_nodes
bool __init early_init_dt_scan(void *params)
{
bool status;
status = early_init_dt_verify(params);
if (!status)
return false;
early_init_dt_scan_nodes();
return true;
}
bool __init early_init_dt_verify(void *params)
{
if (!params)
return false;
/* check device tree validity */
if (fdt_check_header(params))//校验dtb文件头部
return false;
/* Setup flat device-tree pointer */
initial_boot_params = params; //保存dtb位于内存的虚拟地址
of_fdt_crc32 = crc32_be(~0, initial_boot_params,
fdt_totalsize(initial_boot_params));
return true;
}
void __init early_init_dt_scan_nodes(void)
{
int rc = 0;
/* Initialize {size,address}-cells info */
of_scan_flat_dt(early_init_dt_scan_root, NULL);
/* Retrieve various information from the /chosen node */
rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);//扫描chosen节点,把chosen节点的bootargs值拷贝给boot_command_line
if (!rc)
pr_warn("No chosen node found, continuing without\n");
/* Setup memory, calling early_init_dt_add_memory_arch */
of_scan_flat_dt(early_init_dt_scan_memory, NULL);
/* Handle linux,usable-memory-range property */
memblock_cap_memory_range(cap_mem_addr, cap_mem_size);
}
该函数调用了unflatten_device_tree函数,这个是展开dtb的核心函数,该函数继续调用__unflatten_device_tree去进行展开,相关代码如下
/* First pass, scan for size |第一次扫描统计设备树需要的内存的大小,第二个参数是NULL代表只统计大小不做其它事情*/
size = unflatten_dt_nodes(blob, NULL, dad, NULL);
if (size <= 0)
return NULL;
size = ALIGN(size, 4);
pr_debug(" size is %d, allocating...\n", size);
/* Allocate memory for the expanded device tree |一次性为展开的设备上分配内存*/
mem = dt_alloc(size + 4, __alignof__(struct device_node));
if (!mem)
return NULL;
memset(mem, 0, size);
/* Second pass, do actual unflattening */
ret = unflatten_dt_nodes(blob, mem, dad, mynodes);//第二次扫描调用函数!!!!!
if (be32_to_cpup(mem + size) != 0xdeadbeef)
pr_warn("End of tree marker overwritten: %08x\n",
be32_to_cpup(mem + size));
if (ret <= 0)
return NULL;
if (detached && mynodes && *mynodes) {
of_node_set_flag(*mynodes, OF_DETACHED);
pr_debug("unflattened tree is detached\n");
}
pr_debug(" <- unflatten_device_tree()\n");
for (offset = 0;
offset >= 0 && depth >= initial_depth;
offset = fdt_next_node(blob, offset, &depth)) { //查找设备树当中的每一个节点
if (WARN_ON_ONCE(depth >= FDT_MAX_DEPTH))
continue;
if (!IS_ENABLED(CONFIG_OF_KOBJ) &&
!of_fdt_device_is_available(blob, offset))
continue;
ret = populate_node(blob, offset, &mem, nps[depth], //查找到节点信息后构造device_node结构体
&nps[depth+1], dryrun);
if (ret < 0)
return ret;
if (!dryrun && nodepp && !*nodepp)
*nodepp = nps[depth+1];
if (!dryrun && !root)
root = nps[depth+1];
}
设备树替换了平台总线模型当中对硬件资源描述的device部分,所以设备树也是对硬件资源进行描述的文件。
在平台总线模型中,device部分是用platform_device结构体来描述的,所以内核最终会将内核认识的device_node树转换成platform_device。
并不是所有的节点都会被转换成platform_device,只有满足要求的才会转换成platform_device,转换成platform_device的节点可以在/sys/bus/platform/devices下查看
1,根节点下包含compatiable属性的子节点
2,节点中compatible属性包含simple-bus,simple-mfd,isa其中之一,那么该节点下包含compatible属性的子节点也会被转换
3,如果节点的compatible属性包含”arm,primecell“值,则对应的节点不会被转换成platform_device,而是会被转换成amba设备
device_node转换成platform_device过程
系统启动时会执行of_platform_default_populate_init
函数,这个函数是用arch_initcall_sync();
来修饰的。
编译内核源码时,会自动编译设备树,并且会将编译好的dtb设备树镜像打包到内核镜像里面,再更新开发板镜像即可成功将设备树节点添加到内核,可用通过/proc/device-tree/
来查看是否能看到添加成功的节点
设备树转换成的platform_device中的compatible属性值会和platform_driver结构体中的struct device_driver driver
成员中的const struct of_device_id *of_match_table;
进行匹配。
匹配优先级:of_match_table>id_table>name
Linux内核使用device_node结构体来描述一个节点,该节点定义在include/linux/of.h
struct device_node {
const char *name;
phandle phandle;
const char *full_name;
struct fwnode_handle fwnode;
struct property *properties;
struct property *deadprops; /* removed properties */
struct device_node *parent;
struct device_node *child;
struct device_node *sibling;
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj;
#endif
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
所有获取设备树节点的操作函数的返回值类型是struct device_node* 类型
函数原型:
struct device_node *of_find_node_by_name(struct device_node *from, const char *name)
函数作用:通过节点的名字来查找指定的节点,注意不是compitble的属性值
参数:
from,从哪个节点开始查找,如果是NULL表示从根节点开始查找
name,要查找的节点的名字
返回值:成功返回查找到的节点,失败返回NULL
函数原型:
inline struct device_node *of_find_node_by_path(const char *path)
函数作用:通过路径来查找指定的节点
参数:
path,带有全路径的节点名,可用使用节点的别名,比如“/backlight"就是backlight这个节点的全路径
返回值:
成功:返回找到的节点,失败返回NULL。
函数原型:
struct device_node *of_get_parent(const struct device_node *node)
作用:用于获取指定节点的父节点(如果有父节点的话),
函数参数:
node:要查找的父节点的节点。
返回值:找到的父节点。
函数原型:
struct device_node *of_get_next_child(const struct device_node *node struct device_node *prev)
参数:
node:父节点。
prev:从哪一个子节点开始迭代的查找下一个子节点。可以设置为 NULL,表示从第一个子节点开始。
返回值:找到的下一个子节点。
函数原型:
struct device_node *of_find_compatible_node(
struct device_node *from,
const char *type,
const char *compat)
函数作用:通过device_type和compatible属性来查找指定的节点
参数:
from,从哪个节点开始找,如果是NULL表示从根节点开始查找
type,要查找的节点的device_type属性的属性值,可以为NULL,代表忽略device_type属性
compatible,要查找的节点对应的compatible属性列表
函数原型:
static inline struct device_node *of_find_matching_node_and_match(
struct device_node *from,
const struct of_device_id *matches,
const struct of_device_id **match)
函数作用:
通过compatible属性列表来查找指定的节点
参数:
from,从哪个节点开始找,如果是NULL表示从根节点开始查找
matches,of_device_id匹配表,从这个匹配表里查找节点
match,查找的节点的of_device_id
Linux使用该结构体来描述一个节点的属性,定义在include/linux/of.h
Struct property
{
char *name;
int length;
void *value;
struct property *next;
};
函数原型:
struct property *of_find_property(const struct device_node *np,
const char *name,
int *lenp);
作用:查找指定节点下的指定的属性
参数:
np,要查找的节点
name,要查找的节点的属性名
lenp,属性值的字节数
返回值:成功返回查找到的属性,失败返回NULL
函数原型:
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname,
int elem_size)
作用:获取属性中元素的数量
参数:
np,要查找的设备树节点
propname,需要获取哪个属性的数量
elem_size,单个元素尺寸
返回值:成功返回获取到的元素的数量
函数原型:
int of_property_read_u8_index(const struct device_node *np,const char *propname,u8 index, u8 *out_value)
int of_property_read_u16_index(const struct device_node *np,const char *propname,u16 index, u16 *out_value)
int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value)
int of_property_read_u64_index(const struct device_node *np,const char *propname,u64 index, u64 *out_value)
函数作用:从指定属性中获取指定标号的uxx类型的数据值
参数:
np,设备节点
propname,要读取的属性的名字
index,要读取这个属性下的第结构值,index从0开始
out_value,读到的值
返回值:成功返回0
int of_property_read_variable_u8_array(const struct device_node *np,
const char *propname,
u8 *out_values,
size_t sz_min,
size_t sz_max)
int of_property_read_variable_u16_array(const struct device_node *np,
const char *propname,
u16 *out_values,
size_t sz_min,
size_t sz_max)
int of_property_read_variable_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values,
size_t sz_min,
size_t sz_max)
int of_property_read_variable_u64_array(const struct device_node *np,
const char *propname,
u64 *out_values,
size_t sz_min,
size_t sz_max)
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值,分别为 u8、u16、u32 和 u64。
sz_min:要读取的数组的最小值。
sz_max:要读取的数组的最大值
返回值:0,读取成功,负值,读取失败
extern int of_property_read_string(const struct device_node *np,
const char *propname,
const char **out_string);
作用:读取属性中的字符串
参数:
np,设备节点
propname,要读取的属性的名字
out_string,读取到的字符串
返回值:成功返回0
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
函数作用:从Interrupt属性中获取到对应的中断号
参数:
dev,设备节点
index,索引号
返回值:成功返回对应的中断号
static inline u32 irqd_get_trigger_type(struct irq_data *d)
作用:从interrupts属性中获取到对应的中断标志
参数:iqr_data结构体,可用从下面3这个函数获取
返回值:成功返回对应的中断的标志(高电平触发,低电平触发)
struct irq_data *irq_get_irq_data(unsigned int irq)
作用:获取iqr_data结构体
参数:irq中断号
int gpio_to_irq(unsigned int gpio)
作用:根据gpio编号获取中断号
参数:gpio编号
返回值:成功返回对应的中断号
int of_irq_get(struct device_node *dev, int index)
作用:获取中断号
参数:
dev,设备节点
index,索引号
返回值:成功返回中断号
int platform_get_irq(struct platfform_device *dev, unsigned int num)
作用:根据平台总线的device获取中断号
参数:
dev,平台总线模型的device结构体
num,索引号
返回值:成功返回中断号
Linux4.4版本以后引入了设备树新技术Dynamic DeviceTree,翻译为动态设备树或者设备树插件,这个技术可用实现在系统运行期间动态的修改设备树。
/dts-v1/;
/plugin/; //相比设备树的语法在这里要加上这句标识
&{/rk-485-ctl} //路径名
{
//在系统运行期间,在rk-485-ctl节点下添加一个overlay_node节点
overlay_node
{
status="okay";
}
}
/
{
fragment@0 //固定格式,0代表要修改的第0个节点,修改多个节点时往后依次递增序号
{
target-path="/rk-485-ctl";//固定格式,等式右边是要修改的节点的路径,也可以用别名来替代路径别名写法:target=<&rk-485-ctl>
__overlay__ //固定格式,里面跟要修改的具体内容
{
overlay_node
{
status="okay";
};
};
};
};
编译设备树插件的方法和编译设备树的方法一样,都是用dtc,不过为了和原来的dtb文件进行区别,把后缀改成了.dtbo