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

1. 内核工具和辅助函数

1.1宏container_of

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

函数原型:

/*
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,其定义如下 :

struct list_head {
	struct list_head *next, *prev;
};

实例代码:

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

链表初始化:

struct list_head mylist;
INIT_LIST_HEAD(&mylist);

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

添加链表节点:

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

删除链表节点:

void list_del(struct list_head *entry);
list_del(&redcar->list);

链表遍历:

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),进程将永远不会被唤醒。下面是一个等待队列的例子:

#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标准定时器

定时器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);
样例:
#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。

#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互斥锁

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自旋锁

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的实例:

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);
#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工作队列

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

#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内核中断

注册中断函数

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中定义:

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数据结构:

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

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来调用自定义函数

/*
* 用户空间代码还需要包括定义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:

#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,并用专用函数把驱动程序注册到平台总线核,如下所示:

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%确定设备存在于系统中,请使用此方法:

ret = platform_driver_probe(&mypdrv,my_pdrv_probe);

平台驱动程序简单样例:

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

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

·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的实例,如下所示:

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中,定义如下:

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,必须添加以下头文件:

#include <linux/of.h>
#include <linux/of_device.h>

4.1.1命名约定

每个节点都必须有 [@

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

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

4.1.2处理中断

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

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结构

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设备

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通信

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作为第一个参数。第二个参数包含要读取或写入的字节,第三个参数表示要读取或写入的字节数。像任何读/写函数一样,返回值是读/写的字节数。也可以使用以下方式处理消息传输:

int i2c_transfer(struct i2c_adapter *adap,struct i2c_msg *msg, int num);

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

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

样例:

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中最重要的结构。

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是对象数组。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

智慧水务平台有哪些应用场景?

随着城市化进程的加速&#xff0c;城市水资源管理面临着越来越多的挑战。如何实现高效、智能的水资源管理&#xff0c;成为了城市管理者亟待解决的问题。智慧水务平台作为新一代信息技术在水务领域的深度应用&#xff0c;为城市水资源管理提供了全新的解决方案。 智慧水务平台的…

Tensorflow2.0笔记 - 基础数学运算

本笔记主要记录基于元素操作的,-,*,/,//,%,**,log,exp等运算&#xff0c;矩阵乘法运算&#xff0c;多维tensor乘法相关运算 import tensorflow as tf import numpy as nptf.__version__#element-wise运算&#xff0c;对应元素的,-,*,/,**,//,% tensor1 tf.fill([3,3], 4) ten…

动手学深度学习5 矩阵计算

矩阵计算--矩阵怎么求导数 1. 导数和微分2. 偏导数3. 梯度1. 向量-标量求导2. 向量-向量求导3. 拓展到矩阵 4. 链式法则5. 小结QA练习 课程安排&#xff1a; 视频&#xff1a;https://www.bilibili.com/video/BV1eZ4y1w7PY/?spm_id_fromautoNext&vd_sourceeb04c9a33e87ce…

如何选择第三方电子合同平台?

说起第三方电子合同平台大家可能有点不了解&#xff0c;陕西CA来给大家解释解释&#xff1a;第三方电子合同平台是一种提供电子合同服务的第三方机构&#xff0c;可以提供电子合同的签署、管理、存储、公证等服务&#xff0c;保障电子合同的有效性和合法性。 第三方电子合同平台…

vue 里 props 类型为 Object 时设置 default: () => {} 返回的是 undefined 而不是 {}?

问题 今天遇到个小坑&#xff0c;就是 vue 里使用 props 传参类型为 Object 的时候设置 default: () > {} 报错&#xff0c;具体代码如下 <template><div class"pre-archive-info"><template v-if"infoData.kaimo ! null">{{ infoD…

如何使用Docker本地部署Wiki.js容器并结合内网穿透实现知识库共享

文章目录 1. 安装Docker2. 获取Wiki.js镜像3. 本地服务器打开Wiki.js并添加知识库内容4. 实现公网访问Wiki.js5. 固定Wiki.js公网地址 不管是在企业中还是在自己的个人知识整理上&#xff0c;我们都需要通过某种方式来有条理的组织相应的知识架构&#xff0c;那么一个好的知识整…

用冒泡排序谈默认参数应用

前面在调用函数提到为了将信息打印到ofil中&#xff0c;前面提到的办法是 ofstream ofil("text_out1"); void bubble_sort(vector<int> vec){ } 在file scope中定义ofil&#xff0c;这是一个不受欢迎的举动。这样比较难在其他环境重用 一般的程序编写法则是&…

组态王软件安装教程6.51/6.53/5.55/6.60/7.5SP2版本组态软件

组态王软件是一款功能强大的工业自动化软件&#xff0c;以下是各个版本的主要特点&#xff1a; 组态王6.51&#xff1a;该版本是亚控科技在组态王6.0x系列版本成功应用后&#xff0c;广泛征询数千家用户的需求和使用经验&#xff0c;采取先进软件开发模式和流程&#xff0c;由…

私有仓库工具Nexus Maven如何部署并实现远程访问管理界面

文章目录 1. Docker安装Nexus2. 本地访问Nexus3. Linux安装Cpolar4. 配置Nexus界面公网地址5. 远程访问 Nexus界面6. 固定Nexus公网地址7. 固定地址访问Nexus Nexus是一个仓库管理工具&#xff0c;用于管理和组织软件构建过程中的依赖项和构件。它与Maven密切相关&#xff0c;可…

服务器运维小技巧(一)——如何进行远程协助

服务器运维中经常会遇到一些疑难问题&#xff0c;需要安全工程师&#xff0c;或者其他大神远程协助。很多人会选择使用todesk或者向日葵等一些远控软件。但使用这些软件会存在诸多问题&#xff1a; 双方都需要安装软件&#xff0c;太麻烦需要把服务器的公钥/密码交给对方不知道…

.NetCore Flurl.Http 4.0.0 以上管理客户端

参考原文地址&#xff1a;Managing Clients - Flurl 管理客户端 Flurl.Http 构建在堆栈之上System.Net.Http。如果您熟悉HttpClient&#xff0c;那么您可能听说过这个建议&#xff1a;不要为每个请求创建一个新客户端&#xff1b;重复使用它们&#xff0c;否则将面临后…

C++ //练习 1.15 编写程序,包含第14页”再探编译“中讨论的常见错误。熟悉编译器生成的错误信息。

C Primer&#xff08;第5版&#xff09; 练习 1.15 练习 1.15 编写程序&#xff0c;包含第14页”再探编译“中讨论的常见错误。熟悉编译器生成的错误信息。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代码块 /******************…

Django关联已有数据库中已有的数据表

Django关联已有数据库中已有的数据表 兜兜转转&#xff0c;发现自己还得用Python写后端&#xff0c;无语。。。 在写Django项目时&#xff0c;一般是通过模型来创建表&#xff0c;以及通过ORM框架来实现数据的crud&#xff0c;现在的情况是&#xff0c;如果我们的数据表已经存…

在js文件中引入外部变量

需求背景: 有个ip地址需要在项目部署后修改为客户自己的,所以就把这个ip放到了外部进行管理,方便直接修改 实现方法: 第一步:在public文件夹下创建一个json文件,里面放的就是需要在外部进行管理,随时都可以修改的变量 第二步:在需要引变量的js文件中写入如下代码 结合第一步…

Javascript,到底要不要写分号?

小白随机在互联网上乱丢一些赛博垃圾&#xff0c;还望拨冗批评斧正。 要不要加分号&#xff1f; 先说结论&#xff1a;“不引起程序出错的前提下&#xff0c;加不加都可以&#xff0c;按自身习惯来。” 为什么JS可以不加分号&#xff1f; 实际上&#xff0c;行尾使用分号的风…

命令行参数环境变量和进程空间地址

文章目录 命令行参数环境变量进程地址空间 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的 人工智能学习网站&#xff0c; 通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。 点击跳转到网站。 命令行参数 什么是命令行参数&#xff1f; 我…

【JavaEE进阶】 关于应用分层

文章目录 &#x1f38b;序言&#x1f343;什么是应⽤分层&#x1f38d;为什么需要应⽤分层&#x1f340;如何分层(三层架构)&#x1f384;MVC和三层架构的区别和联系&#x1f333;什么是高内聚低耦合⭕总结 &#x1f38b;序言 在我们进行项目开发时我们如果一股脑将所有代码都…

限流算法之固定窗口算法

文章目录 原理示例图 优缺点优点缺点 示例代码java 适用场景不推荐原因如下&#xff1a; 原理 固定窗口算法是一种常见的限流算法&#xff0c;它通过在固定长度的时间窗口内限制请求数量来实现限流。这种算法的原理是在每个时间窗口内&#xff0c;对到达的请求进行计数&#x…

操作系统课程设计-内存管理

目录 前言 1 实验题目 2 实验目的 3 实验内容 3.1 步骤 3.2 关键代码 3.2.1 显示虚拟内存的基本信息 3.2.2 遍历当前进程的虚拟内存 4 实验结果与分析 5 代码 前言 本实验为课设内容&#xff0c;博客内容为部分报告内容&#xff0c;仅为大家提供参考&#xff0c;请勿直…

【白皮书下载】GPU计算在汽车中的应用

驾驶舱域控制器 (CDC) 是汽车 GPU 的传统应用领域。在这里&#xff0c;它可以驱动仪表板上的图形&#xff0c;与车辆保持高度响应和直观的用户界面&#xff0c;甚至为乘客提供游戏体验。随着车辆屏幕数量的增加和分辨率的提高&#xff0c;对汽车 GPU 在 CDC 中进行图形处理的需…