Linux设备驱动开发学习笔记(等待队列,锁,字符驱动程序,设备树,i2C...)

1. 内核工具和辅助函数

1.1宏container_of

container_of函数可以通过结构体的成员变量检索出整个结构体

函数原型:

c++ 复制代码
/*
pointer 指向结构体字段的指针
container_type 结构体类型
container_field 结构体字段名称
返回值是一个指针
*/
container_of(pointer, container_type,container_field);  

struct mcp23016 {
	struct i2c_client *client;
	struct gpio_chip chip;
};
static inline struct mcp23016* to_mcp23016(struct gpio_chip *gc)
{
    return container_of(gc,struct mcp23016,chip);
}

1.2 链表

内核开发者只实现了循环双链表,因为这个结构能够实现FIFO和LIFO,并且内核开发者要保持最少代码。 为了支持链表,代码中要添加的头文件是<linux/list.h>。内核中链表实现核心部分的数据结构

是struct list_head,其定义如下 :

c++ 复制代码
struct list_head {
	struct list_head *next, *prev;
};

实例代码:

c++ 复制代码
#include <linux/list.h>
struct car {
	int door_number;
	char *color;
	char *model;
	struct list_head list; /*内核的表结构 */
};

struct car *redcar = kmalloc(sizeof(*car),GFP_KERNEL);
struct car *bluecar = kmalloc(sizeof(*car),GFP_KERNEL);
/* 初始化每个节点的列表条目*/
INIT_LIST_HEAD(&bluecar->list);
INIT_LIST_HEAD(&redcar->list);
/* 为颜色和模型字段分配内存,并填充每个字段 */
list_add(&redcar->list, &carlist) ;
list_add(&bluecar->list, &carlist) ;

链表初始化:

C++ 复制代码
struct list_head mylist;
INIT_LIST_HEAD(&mylist);

static inline void INIT_LIST_HEAD(struct list_head *list)
{
	list->next = list;
	list->prev = list;
}

添加链表节点:

c++ 复制代码
static inline void list_add(struct list_head *new, struct list_head *head)
{
	__list_add(new, head, head->next);
}

/*新节点都是添加在head的后面而不是最后*/
static inline void __list_add(struct list_head *new,struct list_head *prev, struct list_head *next)
{
	next->prev = new;
	new->next = next;
	new->prev = prev;
	prev->next = new;
}

删除链表节点:

C++ 复制代码
void list_del(struct list_head *entry);
list_del(&redcar->list);

链表遍历:

C++ 复制代码
list_for_each_entry(pos, head, member)

1.3等待队列

等待队列=链表+锁

想要其入睡的每个进程都在该链表中排队(因此被称作等待队列)并进入睡眠状态,直到条件变为真。等待队列可以被看作简单的进程链表和锁。

wait_event_interruptible不会持续轮询,而只是在被调用时评估条件。如果条件为假,则进程将进入TASK_INTERRUPTIBLE状态并从运行队列中删除。之后,当每次在等待队列中调用wake_up_interruptible时,都会重新检查条件。如果wake_up_interruptible运行时发现条件为真,则等待队列中的进程将被唤醒,并将其状态设置为TASK_RUNNING。进程按照它们进入睡眠的顺序唤醒。要唤醒在队列中等待的所有进程,应该使用wake_up_interruptible_all。

如果调用了wake_up或wake_up_interruptible,并且条件仍然是FALSE,则什么都不会发生。如果没有调用wake_up(或wake_up_interuptible),进程将永远不会被唤醒。下面是一个等待队列的例子:

C++ 复制代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/time.h>
#include <linux/delay.h>
#include<linux/workqueue.h>

static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static int condition = 0;
/* 声明一个工作队列*/
static struct work_struct wrk;
static void work_handler(struct work_struct *work)
{
	printk("Waitqueue module handler %s\n",__FUNCTION__);
	msleep(5000);
	printk("Wake up the sleeping module\n");
	condition = 1;
	wake_up_interruptible(&my_wq);
} 
static int __init my_init(void)
{
	printk("Wait queue example\n");
	INIT_WORK(&wrk, work_handler);
	schedule_work(&wrk);//将work_handler加入工作队列,等待调用(系统自动调)
	printk("Going to sleep %s\n", __FUNCTION__);
	wait_event_interruptible(my_wq, condition !=0);//卡在这里,直到work_handler被调用才继续执行
	pr_info("woken up by the work job\n");
	return 0;
}
void my_exit(void)
{
	printk("waitqueue example cleanup\n");
} 
module_init(my_init);
module_exit(my_exit);
MODULE_AUTHOR("John Madieu<john.madieu@foobar.com>");
MODULE_LICENSE("GPL");


等待队列定义:
动态定义:
wait_queue_head_t my_wait_queue;
init_waitqueue_head(&my_wait_queue);
静态定义:
DECLARE_WAIT_QUEUE_HEAD(name)
阻塞:
/*
* 如果条件为false,则阻塞等待队列中的当前任务(进程)
*/
int wait_event_interruptible(wait_queue_head_t q, CONDITION);
解除阻塞:
/*
* 如果上述条件为true,则唤醒在等待队列中休眠的进程
*/
void wake_up_interruptible(wait_queue_head_t *q);

工作队列定义:
INIT_WORK(&wrk, work_handler);
工作队列调度:
schedule_work(&wrk);//等待系统调用

工作队列和等待队列的区别:工作队列会绑定函数执行,等待队列不会绑定函数,单纯的当进程同步使用。

1.4内核延迟与定时器

Jiffy是在<linux/jiffies.h>中声明的内核时间单位。为了理解Jiffy,需要引入一个新的常量HZ,它是jiffies在1s内递增的次数。每个增量被称为一个Tick。换句话说,HZ代表Jiffy的大小。HZ取决于硬件和内核版本,也决定了时钟中断触发的频率。

1.4.1标准定时器

C++ 复制代码
定时器API
定时器在内核中表示为timer_list的一个实例
#include <linux/timer.h>
struct timer_list {
	struct list_head entry;  //双向链表
	unsigned long expires;  //以jiffies为单位绝对值
	struct tvec_t_base_s *base;
	void (*function)(unsigned long);
	unsigned long data;
);
1.设置定时器
void setup_timer( struct timer_list *timer, void (*function)(unsigned long),
                 unsigned long data);
也可以使用这个函数
void init_timer(struct timer_list *timer);
    
2.设置过期时间。当定时器初始化时,需要在启动回调之前设置它的过期时间:    
int mod_timer( struct timer_list *timer,unsigned long expires);
    
3.释放定时器。定时器用过之后需要释放
void del_timer(struct timer_list *timer);
int del_timer_sync(struct timer_list *timer);
C++ 复制代码
样例:
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/timer.h>
static struct timer_list my_timer;
void my_timer_callback(unsigned long data)
{
	printk("%s called (%ld).\n", __FUNCTION__,jiffies);
} 
static int __init my_init(void)
{
	int retval;
	printk("Timer module loaded\n");
	setup_timer(&my_timer, my_timer_callback,0);
	printk("Setup timer to fire in 300ms(%ld)\n", jiffies);
	retval = mod_timer( &my_timer, jiffies + msecs_to_jiffies(300) );
	if (retval)
		printk("Timer firing failed\n");
	return 0;
} 
static void my_exit(void)
{
	int retval;
	retval = del_timer(&my_timer);
	/* 定时器仍然是活动的(1)或没有(0)*/
	if (retval)
	printk("The timer is still inuse...\n");
	pr_info("Timer module unloaded\n");
} 
module_init(my_init);
module_exit(my_exit);

1.4.2高精度定时器

高精度定时器由内核配置中的CONFIG_HIGH_RES_TIMERS选项启用,其精度达到微秒(取决于平台,最高可达纳秒),而标准定时器的精度则为毫秒。标准定时器取决于HZ(因为它们依赖于jiffies),而HRT实现是基于ktime。

C++ 复制代码
#include <linux/hrtimer.h>
struct hrtimer {
	struct timerqueue_node node;
	ktime_t _softexpires;
	enum hrtimer_restart (*function)(structhrtimer *);
	struct hrtimer_clock_base *base;
	u8 state;
	u8 is_rel;
};

1.初始化hrtimer。hrtimer初始化之前,需要设置ktime,它代表持续时间。
void hrtimer_init(struct hrtimer *time,clockid_t which_clock,enum hrtimer_mode mode);
2.启动hrtimer
int hrtimer_start( struct hrtimer *timer,ktime_t time,const enum hrtimer_mode mode);
/*mode代表到期模式。对于绝对时间值,它应该是HRTIMER_MODE_ABS,对于相对于现在的时间值,应该是HRTIMER_MODE_REL。*/
3.取消hrtimer。可以取消定时器或者查看是否可能取消它
int hrtimer_cancel( struct hrtimer *timer);
int hrtimer_try_to_cancel(struct hrtimer *timer);
/*这两个函数当定时器没被激活时都返回0,激活时返回1。这两个函数之间的区别是,如果定时器处于激活状态或其回调函数正在运行,则hrtimer_try_to_cancel会失败,返回-1,而hrtimer_cancel将等待回调完成。*/
用下面的函数可以独立检查hrtimer的回调函数是否仍在运行:
int hrtimer_callback_running(struct hrtimer *timer);

1.5内核锁机制

1.5.1互斥锁

C++ 复制代码
struct mutex {
/* 
1: 解锁, 0: 锁定, negative: 锁定, 可能的等待
其结构中有一个链表类型字段:wait_list,睡眠的原理是一样的
*/
	atomic_t count;
	spinlock_t wait_lock;
	struct list_head wait_list;
[...]
};
1.声明
静态声明:
DEFINE_MUTEX(my_mutex);
动态声明:
struct mutex my_mutex;
mutex_init(&my_mutex);

2.上锁
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock); //驱动程序可以被所有信号中断,推荐
int mutex_lock_killable(struct mutex *lock); //只有杀死进程的信号才能中断驱动程序

3.解锁
void mutex_unlock(struct mutex *lock);

有时,可能需要检查互斥锁是否锁定。为此使用int mutex_is_locked(struct mutex *lock)函数
这个函数只是检查互斥锁的所有者是否为空(NULL)。还有一个函数int mutex_trylock(struct mutex *lock),如果还没有锁定,则它获取互斥锁,并返回1;否则返回0。

实例:
struct mutex my_mutex;
mutex_init(&my_mutex);
/* 在工作或线程内部*/
mutex_lock(&my_mutex);
access_shared_memory();
mutex_unlock(&my_mutex)

互斥锁使用须知:

  • 一次只能有一个任务持有互斥锁;这其实不是规则,而是事实。
  • 多次解锁是不允许的。
  • 它们必须通过API初始化。
  • 持有互斥锁的任务不可能退出,因为互斥锁将保持锁定,可能的竞争者会永远等待(将睡眠)。
  • 不能释放锁定的内存区域。
  • 持有的互斥锁不得重新初始化。
  • 由于它们涉及重新调度,因此互斥锁不能用在原子上下文中,如Tasklet和定时器。

与wait_queue一样,互斥锁也没有轮询机制。每次在互斥锁上调用mutex_unlock时,内核都会检查wait_list中的等待者。如果有等待者,则其中的一个(且只有一个)将被唤醒和调度;它们唤醒的顺序与它们入睡的顺序相同。

1.5.2自旋锁

C++ 复制代码
spinlock_t my_spinlock;
spin_lock_init(my_spinlock);
static irqreturn_t my_irq_handler(int irq, void *data)
{
	unsigned long status, flags;
    /*
    spin_lock_irqsave()函数会在获取自旋锁之前,禁止当前处理器(调用该函数的处理器)上中断。
    spin_lock_irqsave在内部调用local_irq_save (flags)和preempt_disable(),前者是一个依赖于体系结构的函数,用于保存IRQ状态,后者禁止在相关CPU上发生抢占。
    */
	spin_lock_irqsave(&my_spinlock, flags);
	status = access_shared_resources();
	spin_unlock_irqrestore(&gpio->slock, flags); //释放锁
	return IRQ_HANDLED;
}

自旋锁与互斥锁的区别:

  • 互斥锁保护进程的关键资源,而自旋锁保护IRQ处理程序的关键部分。

  • 互斥锁让竞争者在获得锁之前睡眠,而自旋锁在获得锁之前一直自旋循环(消耗CPU)。

  • 鉴于上一点,自旋锁不能长时间持有,因为等待者在等待取锁期间会浪费CPU时间;而互斥锁则可以长

    时间持有,只要保护资源需要,因为竞争者被放入等待队列中进入睡眠状态。

1.6工作延时机制

延迟是将所要做的工作安排在将来执行的一种方法,这种方法推后发布操作。显然,内核提供了一些功能来实现这种机制;它允许延迟调用和执行任何类型函数。下面是内核中的3项功能。

SoftIRQ(执行在原子上下文) ,Tasklet (执行在原子上下文),工作队列 (执行在进程上下文)

1.6.1 Tasklet

Tasklet构建在Softirq之上的下半部(稍后将会看到这意味着什么)机制。它们在内核中表示为struct

tasklet_struct的实例:

C++ 复制代码
struct tasklet_struct
{
	struct tasklet_struct *next;
	unsigned long state;
	atomic_t count;
	void (*func)(unsigned long);
	unsigned long data;
};

1、声明:
动态声明:
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);
静态声明:
DECLARE_TASKLET( tasklet_example,tasklet_function, tasklet_data );
DECLARE_TASKLET_DISABLED(name, func, data);

这两个函数有一个区别,前者创建的Tasklet已经启用,并准备好在没有任何其他函数调用的情况下被调度,这通过将count字段设置为0来实现;而后者创建的Tasklet被禁用(通过将count设置为1来实现),必须在其上调用tasklet_enable ()之后,才可以调度这一Tasklet。
    
2、启动和禁用TaskLet
void tasklet_enable(struct tasklet_struct *);
void tasklet_disable(struct tasklet_struct *); //本次tasklet执行后返回
void tasklet_disable_nosync(struct tasklet_struct *); //直接终止执行立刻返回

3、TaskLet调度
void tasklet_schedule(struct tasklet_struct *t);
void tasklet_hi_schedule(struct tasklet_struct *t);
内核把普通优先级和高优先级的Tasklet维护在两个不同的链表中。tasklet_schedule将Tasklet添加到普通优先级链表中,用TASKLET_SOFTIRQ标志调度相关的Softirq。tasklet_hi_schedule将Tasklet添加到高优先级链表中,并用HI_SOFTIRQ标志调度相关的Softirq。高优先级Tasklet旨在用于具有低延迟要求的软中断处理程序。
    
4、终止TaskLet
void tasklet_kill(struct tasklet_struct *t);
C++ 复制代码
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h> 
char tasklet_data[]="We use a string; but it could be pointer to a structure";
/* Tasklet处理程序,只打印数据 */
void tasklet_work(unsigned long data)
{
	printk("%s\n", (char *)data);
} 
DECLARE_TASKLET(my_tasklet, tasklet_work,(unsigned long)tasklet_data);

static int __init my_init(void)
{
/*
* 安排处理程序
* 从中断处理程序调度Tasklet arealso
*/
	tasklet_schedule(&my_tasklet);
	return 0;
} 
void my_exit(void)
{
	tasklet_kill(&my_tasklet);
} 
module_init(my_init);
module_exit(my_exit);
MODULE_AUTHOR("John Madieu<john.madieu@gmail.com>");
MODULE_LICENSE("GPL");

1.6.2工作队列

作为延迟机制,工作队列采用的方法与我们之前介绍的方法相反,它只能运行在抢占上下文中。如果需要在中断下半部睡眠,工作队列则是唯一的选择 。

C++ 复制代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched.h> /* 睡眠 */
#include <linux/wait.h> /* 等待列队 */
#include <linux/time.h>
#include <linux/delay.h>
#include <linux/slab.h> /* kmalloc() */
#include <linux/workqueue.h>
//static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static int sleep = 0;
struct work_data {
	struct work_struct my_work;
	wait_queue_head_t my_wq;
	int the_data;
};
static void work_handler(struct work_struct *work)
{
	struct work_data *my_data =container_of(work, struct work_data, my_work);
	printk("Work queue module handler: %s, data is %d\n", __FUNCTION__,my_data->the_data);
	msleep(2000);
	wake_up_interruptible(&my_data->my_wq);
	kfree(my_data);
} 
static int __init my_init(void)
{
	struct work_data * my_data;
	my_data = kmalloc(sizeof(struct work_data),GFP_KERNEL);
	my_data->the_data = 34;
	INIT_WORK(&my_data->my_work, work_handler);
	init_waitqueue_head(&my_data->my_wq);
	schedule_work(&my_data->my_work);
	printk("I'm goint to sleep ...\n");
	wait_event_interruptible(my_data->my_wq,sleep != 0);
	printk("I am Waked up...\n");
	return 0;
} 
static void __exit my_exit(void)
{
	printk("Work queue module exit: %s %d\n",
	__FUNCTION__, __LINE__);
} 
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu<john.madieu@gmail.com> ");
MODULE_DESCRIPTION("Shared workqueue");

1.7内核中断

注册中断函数

C++ 复制代码
int request_irq(unsigned int irq, irq_handler_t handler,unsigned long flags, const char *name, void *dev);
flag表示掩码:
    IRQF_TIMER:通知内核这个处理程序是由系统定时器中断触发的。
    IRQF_SHARED:用于两个或多个设备共享的中断线。共享这个中断线的所有设备都必须设置该标志。如果被忽略,将只能为该中断线注册一个处理程序。
    IRQ_ONESHOT:主要在线程中断中使用,它要求内核在硬中断处理程序没有完成之前,不要重新启用该中断。在线程处理程序运行之前,中断会一直保持禁用状态。
name:内核用来标识/proc/interrupts和/proc/irq中的驱动程序    
dev:其主要用途是作为参数传递给中断处理程序,这对每个中断处理程序都是唯一的,因为它用来标识这个设备。对于非共享中断,它可以是NULL,但共享中断不能为NULL。使用它的常见方法是提供设备结构,因为它既独特,又可能对处理程序有用。也就是说,指向有一个指向设备数据结构的指针就足够了。

struct my_data {
	struct input_dev *idev;
	struct i2c_client *client;
	char name[64];
	char phys[32];
};
static irqreturn_t my_irq_handler(int irq, void*dev_id)
{
	struct my_data *md = dev_id;
	unsigned char nextstate = read_state(lp);
	/* Check whether my device raised the irq or no */
	[...]
	return IRQ_HANDLED;
}
/* 在probe函数的某些位置 */
int ret;
struct my_data *md = kzalloc(sizeof(*md), GFP_KERNEL);
ret = request_irq(client->irq, my_irq_handler,IRQF_TRIGGER_LOW |IRQF_ONESHOT,DRV_NAME, md);
/* 在释放函数中*/
free_irq(client->irq, md);

中断处理函数:
static irqreturn_t my_irq_handler(int irq, void *dev);
中断返回值:
IRQ_NONE:设备不是中断的发起者(在共享中断线上尤其会出现这种情况)。
IRQ_HANDLED:设备引发中断

中断上半部处理紧急事件,中断下半部用工作队列等延迟机制处理不紧急事件,并且上半部处理的时候需要将所有中断全部紧张,避免中断嵌套,等上半部处理完再开启中断。

2.字符设备驱动程序

2.1设备文件操作

内核把文件描述为inode结构(不是文件结构)的实例,inode结构在include/linux/fs.h中定义:

C++ 复制代码
struct inode {
[...]
	struct pipe_inode_info *i_pipe; /* 如果这是Linux内核管道,则设置并使用 */
	struct block_device *i_bdev; /* 如果这是块设备,则设置并使用 */
	struct cdev *i_cdev; /* 如果这是字符设备,则设置并使用 */
[...]
}

struct inode是文件系统的数据结构,它只与操作系统相关,用于保存文件(无论它的类型是字符、块、管道等)或目录(从内核的角度来看,目录也是文件,是其他文件的入口点)信息。

struct file结构(也在include/linux/fs.h中定义)是更高级的文件描述,它代表内核中打开的文件,依赖于低层的struct inode数据结构:

C++ 复制代码
struct file {
[...]
	struct path f_path; /* 文件路径 */
	struct inode *f_inode; /* 与此文件相关的inode */
	const struct file_operations *f_op; /* 可以在此文件上执行的操作 */
	loff_t f_pos; /* 此文件中光标的位置 */
	/* 需要tty驱动程序等 */
	void *private_data; /* 驱动程序可以设置的私有数据,以便在文件操作之间共享,这可以指向任何结构*/
[...]
}

struct inode和struct file的区别在于,inode不跟踪文件的当前位置和当前模式,它只是帮助操作系统找到底层文件结构的内容(管道、目录、常规磁盘文件、块/字符设备文件等)。而struct file则是一个基本结构(它实际上持有一个指向struct inode的指针),它代表打开的文件,并且提供一组函数,它们与底层文件结构上执行的方法相关,这些方法包括open、write、seek、read、select等。所有这一切都强化了UNIX系统的哲学:一切皆是文件。

2.2分配和注册设备

字符设备在内核中表示为struct cdev的实例。在编写字符设备驱动程序时,目标是最终创建并注册与struct file_operations关联的结构实例,为用户空间提供一组可以在该设备上执行的操作(函数)。为了实现这个目标,必须执行以下几个步骤。

(1)使用alloc_chrdev_region()保留一个主设备号和一定范围的次设备号。

(2)使用class_create()创建自己的设备类,该函数在/sys/class中定义。

(3)创建一个struct file_operation(传递给cdev_init),每一个设备都需要创建,并调用call_init和cdev_add()注册这个设备。

(4)调用device_create()创建每个设备,并给它们一个合适的名字。这样,就可在/dev目录下创建出设备。

2.3 ioctrl

c++ 复制代码
ioctrl原型:
_IO(MAGIC, SEQ_NO)
_IOW(MAGIC, SEQ_NO, TYPE)
_IOR(MAGIC, SEQ_NO, TYPE)
_IORW(MAGIC, SEQ_NO, TYPE)

eep_ioctl.h:
#ifndef PACKT_IOCTL_H
#define PACKT_IOCTL_H/*
* 需要为驱动选择一个数字,以及每个命令的序列号
*/
#define EEP_MAGIC 'E'
#define ERASE_SEQ_NO 0x01
#define RENAME_SEQ_NO 0x02
#define ClEAR_BYTE_SEQ_NO 0x03
#define GET_SIZE 0x04
/*
* 分区名必须是最大32字节
*/
#define MAX_PART_NAME 32
/*
* 定义ioctl编号
*/
#define EEP_ERASE _IO(EEP_MAGIC, ERASE_SEQ_NO)
#define EEP_RENAME_PART _IOW(EEP_MAGIC,RENAME_SEQ_NO, unsigned long)
#define EEP_GET_SIZE _IOR(EEP_MAGIC, GET_SIZE,int *)
#endif
    
long ioctl(struct file *f, unsigned int cmd,unsigned long arg);

ioctrl步骤:使用switch-case来调用自定义函数

C++ 复制代码
/*
* 用户空间代码还需要包括定义ioctls的头文件,这里是eep_iocl.h
*/
#include "eep_ioctl.h"
static long eep_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
	int part;
	char *buf = NULL;
	int size = 1300;
	switch(cmd){
		case EEP_ERASE:
			erase_eepreom();
			break;
		case EEP_RENAME_PART:
			buf = kmalloc(MAX_PART_NAME,GFP_KERNEL);
			copy_from_user(buf, (char *)arg,MAX_PART_NAME);
			rename_part(buf);
			break;
		case EEP_GET_SIZE:
			copy_to_user((int*)arg, &size,sizeof(int));
			break;
		default:
			return -ENOTTY;
	return 0;
}

用户程序调ioctrl:

C++ 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include "eep_ioctl.h" /* our ioctl header file*/
int main()
{
	int size = 0;
	int fd;
	char *new_name = "lorem_ipsum"; /* 不超过MAX_PART_NAME */
	fd = open("/dev/eep-mem1", O_RDWR);
	if (fd == -1){
		printf("Error while opening the eeprom\n");
		return -1;
	} 
	ioctl(fd, EEP_ERASE); /* 调用ioctl来擦除分区*/
	ioctl(fd, EEP_GET_SIZE, &size); /* 调用ioctl获取分区大小 */
	ioctl(fd, EEP_RENAME_PART, new_name); /*调用ioctl来重命名分区 */close(fd);
	return 0;
}

用户空间ioctl传递fd和cmd给内核空间,内核空间根据cmd用switch-case调用对应的函数处理。cmd在头文件通过_IO(MAGIC, SEQ_NO)等io宏定义即可。

3.平台设备驱动程序

3.1平台驱动程序

I2C设备或SPI设备是平台设备,但分别依赖于I2C或SPI总线,而不是平台总线。

对于平台驱动程序一切都需要手动完成。平台驱动程序必须实现probe函数,在插入模块或设备声明时,内核调用它。在开发平台驱动程序时,必须填写主结构struct platform_driver,并用专用函数把驱动程序注册到平台总线核,如下所示:

C++ 复制代码
static struct platform_driver mypdrv = {
	.probe = my_pdrv_probe, /*设备匹配后声明驱动程序时所调用的函数。*/
	.remove = my_pdrv_remove,
	.driver = {
		.name = "my_platform_driver",
		.owner = THIS_MODULE,
	},
}

在内核中注册平台驱动程序很简单,只需在init函数中调用platform_driver_register()或platform_driver_probe()(模块加载时)。这两个函数之间的区别如下。

·platform_driver_register():注册驱动程序并将其放入由内核维护的驱动程序列表中,以便每当发现新的匹配时就可以按需调用其probe()函数。为防止驱动程序在该列表中插入和注册,请使用下一个函数。

·platform_driver_probe():调用该函数后,内核立即运行匹配循环,检查是否有平台设备名称匹配,如果匹配则调用驱动程序的probe(),这意味着设备存在;否则,驱动程序将被忽略。此方法可防止延迟探测,因为它不会在系统上注册驱动程序。在这里,probe函数被放置在__init部分,当内核启动完成时这个部分被释放,从而防止了延迟探测并减少驱动程序的内存占用。如果100%确定设备存在于系统中,请使用此方法:

C++ 复制代码
ret = platform_driver_probe(&mypdrv,my_pdrv_probe);

平台驱动程序简单样例:

C++ 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>
static int my_pdrv_probe (struct platform_device *pdev){
	pr_info("Hello! device probed!\n");
	return 0;
} 
static void my_pdrv_remove(struct platform_device *pdev){
	pr_info("good bye reader!\n");
} 
static struct platform_driver mypdrv = {
	.probe = my_pdrv_probe,
	.remove = my_pdrv_remove,
	.driver = {
		.name = KBUILD_MODNAME,
		.owner = THIS_MODULE,
	},
};
static int __init my_drv_init(void)
{
	pr_info("Hello Guy\n");/* 向内核注册*/
	platform_driver_register(&mypdrv);
	return 0;
}
static void __exit my_pdrv_remove (void)
{
    pr_info("Good bye Guy\n");
	/* 从内核注销 */
	platform_driver_unregister(&my_driver);
} 
module_init(my_drv_init);
module_exit(my_pdrv_remove);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu");
MODULE_DESCRIPTION("My platform Hello World module");

每个总线都有特定的宏来注册驱动程序,以下列表是其中的一部分。

C++ 复制代码
·module_platform_driver(struct platform_driver):用于平台驱动程序,专用于传统物理总线以外的设备
·module_spi_driver (struct spi_driver):用于SPI驱动程序。
·module_i2c_driver (struct i2c_driver):用于I2C驱动程序。
·module_pci_driver(struct pci_driver):用于PCI驱动程序。
·module_usb_driver(struct usb_driver):用于USB驱动程序。
·module_mdio_driver(struct mdio_driver):用于MDIO。

3.2平台设备

完成驱动程序后,必须向内核提供需要该驱动程序的设备。平台设备在内核中表示为struct platform_device的实例,如下所示:

C++ 复制代码
struct platform_device {
	const char *name;
	u32 id;
	struct device dev;
	u32 num_resources;
	struct resource *resource;
};

3.1设备驱动总线匹配

在匹配发生之前,Linux会调用platform_match(struct device * dev,structdevice_driver * drv)。平台设备通过字符串与驱动程序匹配。根据Linux设备模型,**总线元素是最重要的部分。每个总线都维护一个注册的驱动程序和设备列表。总线驱动程序负责设备和驱动程序的匹配。**每当连接新设备或者向总线添加新的驱动程序时,总线都会启动匹配循环。

内核通过以下方式触发I2C总线匹配循环:调用由I2C总线驱动程序注册的I2C核心匹配函数,以检查是否有已注册的驱动程序与该设备匹配。如果没有匹配,则什么都不会发生;如果发现匹配,则内核将通知(通过netlink套接字通信机制)设备管理器(udev/mdev),由它加载(如果尚未加载)与设备匹配的驱动程序。一旦驱动程序加载完成,其probe()函数就立即执行。

(1)内核设备与驱动程序匹配函数

内核中负责平台设备和驱动程序匹配功能的函数在/drivers/base/platform.c中,定义如下:

C++ 复制代码
static int platform_match(struct device *dev, struct device_driver *drv)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct platform_driver *pdrv = to_platform_driver(drv);
	/* 在设置driver_override时,只绑定到匹配的驱动程序*/
	if (pdev->driver_override)
		return !strcmp(pdev->driver_override,drv->name);
	/* 尝试一个样式匹配*/
	if (of_driver_match_device(dev, drv))
		return 1;
	/* 尝试ACPI样式匹配 */
	if (acpi_driver_match_device(dev, drv))
		return 1;
	/* 尝试匹配ID表 */
	if (pdrv->id_table)
		return platform_match_id(pdrv->id_table, pdev) != NULL;
	/* 回退到驱动程序名称匹配 */
	return (strcmp(pdev->name, drv->name) == 0);
}

static const struct platform_device_id *platform_match_id(const struct platform_device_id *id, struct platform_device *pdev)
{
	while (id->name[0]) 
    {
        if (strcmp(pdev->name, id->name)== 0) {
            pdev->id_entry = id;
            return id;
        } 
		id++;
	} 
	return NULL;
}

struct device_driver是每个设备驱动程序的基础。无论是I2C、SPI、TTY,还是其他设备驱动程序,它们都嵌入
struct device_driver元素。
    
struct device_driver {
	const char *name;
	[...]
	const struct of_device_id *of_match_table;
	const struct acpi_device_id *acpi_match_table;
};

4.设备树

4.1设备树机制

将选项CONFIG_OF设置为Y即可在内核中启用DT。要在驱动程序中调用DT API,必须添加以下头文件:

C++ 复制代码
#include <linux/of.h>
#include <linux/of_device.h>

4.1.1命名约定

每个节点都必须有 [@
]形式的名称,其中是一个字符串,其长度最多为31个字符,[@ ]是可选的,具体取决于节点代表是否为可寻址的设备。

C++ 复制代码
i2c@021a0000 {
	compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
	reg = <0x021a0000 0x4000>;
	[...]
};

4.1.2处理中断

中断接口实际上分为两部分,消费者端和控制器端。DT中用4个属性描述中断连接。控制器是为消费者提供中断线的设备。在控制器端有以下属性。·interrupt-controller:为了将设备标记为中断控制器而应该定义的空(布尔)属性。·#interrupt-cells:这是中断控制器的属性。它指出为该中断控制器指定一个中断要使用多少个单元。消费者是生成中断的设备。消费者绑定需要以下属性。·interrupt-parent:对于产生中断的设备节点,这个属性包含指向设备所连接的中断控制器节点的指针phandle。如果省略,则设备从其父节点继承该属性。

C++ 复制代码
interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>;
·0:共享外设中断(SPI),用于核间共享的中断信号,可由GIC路由至任意核。
·1:专用外设中断(PPI),专用于单核的中断信号。·第二个单元格保存中断号。该中断号取决于中断线是PPI还是SPI。
·第三个单元,这里的IRQ_TYPE_LEVEL_HIGH代表感知级别。所有可用的感知级别在include/linux/irq.h中定义。

5.I2C客户端驱动程序

I2C驱动程序在内核中表示为struct i2c_driver的实例。I2C客户端(代表设备本身)由struct i2c_client结构表示。

5.1i2c_driver结构

C++ 复制代码
struct i2c_driver {
	/* 标准驱动模型接口 */
	int (*probe)(struct i2c_client *, const struct i2c_device_id *);
	int (*remove)(struct i2c_client *);
	/* 与枚举无关的驱动类型接口 */
	void (*shutdown)(struct i2c_client *);
	struct device_driver driver;
	const struct i2c_device_id *id_table;
};

struct i2c_driver结构包含并描述通用访问例程,这些例程是处理声明驱动程序的设备所必需的,而struct i2c_client则包含设备特有的信息,如其地址。struct i2c_client结构表示和描述I2C设备

C++ 复制代码
struct i2c_client {
	unsigned short flags; /* div., 见下文 */
	unsigned short addr; /* chip address - NOTE:7bit */
	/* 地址被存储在 _LOWER_ 7 bits */
	char name[I2C_NAME_SIZE];
	struct i2c_adapter *adapter; /* 适配器 */
	struct device dev; /* 设备结构 */
	int irq; /* 由设备发出的IR */
	struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)i2c_slave_cb_t slave_cb; /* 回调从设备 */
#endif
};

5.2普通I2C通信

C++ 复制代码
int i2c_master_send(struct i2c_client *client,const char *buf, int count);
int i2c_master_recv(struct i2c_client *client,char *buf, int count);

几乎所有I2C通信函数都以struct i2c_client作为第一个参数。第二个参数包含要读取或写入的字节,第三个参数表示要读取或写入的字节数。像任何读/写函数一样,返回值是读/写的字节数。也可以使用以下方式处理消息传输:

C++ 复制代码
int i2c_transfer(struct i2c_adapter *adap,struct i2c_msg *msg, int num);

i2c_transfer发送一组消息,其中每个消息可以是读取操作或写入操作,也可以是它们的任意混合。请记住,每两个事务之间没有停止位。 i2c_msg结构描述和表示I2C消息。它必须包含每条消息的客户端地址、消息的字节数和消息有效载荷。

C++ 复制代码
struct i2c_msg {
	__u16 addr; /* 从设备地址 */
	__u16 flags; /* 信息标志 */
	__u16 len; /* msg长度 */
	__u8 *buf; /* 指向msg数据的指针 */
};

样例:

C++ 复制代码
ssize_t eep_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
[...]
	int _reg_addr = dev->current_pointer;
	u8 reg_addr[2];reg_addr[0] = (u8)(_reg_addr>> 8);
	reg_addr[1] = (u8)(_reg_addr& 0xFF);
	struct i2c_msg msg[2];
	msg[0].addr = dev->client->addr;
	msg[0].flags = 0; /* 写入*/
	msg[0].len = 2; /* 地址是2字节编码 */
	msg[0].buf = reg_addr;
	msg[1].addr = dev->client->addr;
	msg[1].flags = I2C_M_RD; /* 读取*/
	msg[1].len = count;
	msg[1].buf = dev->data;
if (i2c_transfer(dev->client->adapter, msg,2) < 0)
	pr_err("ee24lc512: i2c_transferfailed\n");
if (copy_to_user(buf, dev->data, count) !=0) {
	retval = -EIO;
	goto end_read;
} 
[...]
}

6.Regmap API---寄存器映射抽象

内核版本3.1中引入了Regmap API,用于分解和统一内核开发人员访问SPI/I2C设备的方式。接下来的问题是,无论它是SPI设备,还是I2C设备,只需要初始化、配置Regmap,并流畅地处理所有读/写/修改操作

6.1.1使用Regmap API编程

Regmap API非常简单,只需了解几个结构即可。这个API中的两个重要结构是struct regmap_config(代表Regmap配置)和struct regmap(Regmap实例本身)。

struct regmap_config在驱动程序的生命周期中存储Regmap配置,这里的设置会影响读/写操作,它是Regmap API中最重要的结构。

C++ 复制代码
struct regmap_config {
	const char *name;
	int reg_bits;//寄存器地址中的位数
	int reg_stride;
	int pad_bits;
	int val_bits;
	bool (*writeable_reg)(struct device *dev,unsigned int reg);
    /*回调函数。如果提供,则在需要写入寄存器时供Regmap子系统使用。*/
	bool (*readable_reg)(struct device *dev,unsigned int reg);
	bool (*volatile_reg)(struct device *dev,unsigned int reg);
    /*每当需要通过Regmap缓存读取或写入寄存器时调用它。*/
	bool (*precious_reg)(struct device *dev,unsigned int reg);
	regmap_lock lock;
	regmap_unlock unlock;
	void *lock_arg;
	int (*reg_read)(void *context, unsigned intreg,unsigned int *val);
    int (*reg_write)(void *context, unsigned intreg,unsigned int val);
	bool fast_io;
	unsigned int max_register;
	const struct regmap_access_table *wr_table;
	const struct regmap_access_table *rd_table;
	const struct regmap_access_table *volatile_table;
	const struct regmap_access_table *precious_table;
	const struct reg_default *reg_defaults;
	unsigned int num_reg_defaults;
	enum regcache_type cache_type;
	const void *reg_defaults_raw;
	unsigned int num_reg_defaults_raw;
	u8 read_flag_mask;
	u8 write_flag_mask;
	bool use_single_rw;
	bool can_multi_write;
	enum regmap_endian reg_format_endian;
	enum regmap_endian val_format_endian;
	const struct regmap_range_cfg *ranges;
	unsigned int num_ranges;
}

7.内核内存管理

7.1Sla分配器

Slab分配器是kmalloc()所依赖的分配器。其主要目的是消除小内存分配情况下由伙伴系统引起的内存分配/释放造成的碎片,加快常用对象的内存分配。

7.1.1伙伴系统

分配内存时,所请求的是大小被四舍五入为2的幂,伙伴分配器搜索相应的列表。如果请求列表中无项存在,则把下一个上部列表(其块大小为前一列表的两倍)的项拆分成两部分(称为伙伴)。分配器使用前半部分,而另一部分则向下添加到下一个列表中。这是一种递归方法,当伙伴分配器成功找到可以拆分的块或达到最大块大小并且没有可用的空闲块时,该递归方法停止。

7.1.2slab分配器概述

在介绍Slab分配器之前,先定义它使用的一些术语。

·Slab:这是由数个页面帧组成的一块连续的物理内存。每个Slab分成大小相同的块,用于存储特定类型的内核对象,例如inode、互斥锁等。每个Slab是对象数组。

相关推荐
0xDevNull1 小时前
Linux切换JDK版本详细教程
linux
进击的丸子1 小时前
虹软人脸服务器版SDK(Linux/ARM Pro)多线程调用及性能优化
linux·数据库·后端
Kapaseker3 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴3 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭13 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab14 小时前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe19 小时前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少1 天前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker1 天前
一杯美式搞定 Kotlin 空安全
android·kotlin