Search K
Appearance
Appearance
void atomic_set(atomic_t *v, int i); /* 设置原子变量的值为i */
atomic_t v = ATOMIC_INIT(0); /* 定义原子变量v并初始化为0 */
atomic_read(atomic_t *v); /* 返回原子变量的值*/
void atomic_add(int i, atomic_t *v); /* 原子变量增加i */
void atomic_sub(int i, atomic_t *v); /* 原子变量减少i */
void atomic_inc(atomic_t *v); /* 原子变量增加1 */
void atomic_dec(atomic_t *v); /* 原子变量减少1 */
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
上述操作对原子变量执行自增、自减和减操作后(注意没有加),测试其是否为0,为0返回true,否则返回false。
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
void set_bit(nr, void *addr);
上述操作设置addr地址的第nr位,所谓设置位即是将位写为1。
void clear_bit(nr, void *addr);
上述操作清除addr地址的第nr位,所谓清除位即是将位写为0。
void change_bit(nr, void *addr);
上述操作对addr地址的第nr位进行反置。
test_bit(nr, void *addr);
上述操作返回addr地址的第nr位。
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
上述test_and_xxx_bit(nr,voidaddr)操作等同于执行test_bit(nr,voidaddr)后再执行xxx_bit(nr,void*addr)。
spinlock_t lock;
spin_lock_init(lock)
spin_lock(lock)
该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则,它将在那里自旋,直到该自旋锁的保持者释放。
spin_trylock(lock)
该宏尝试获得自旋锁lock,如果能立即获得锁,它获得锁并返回true,否则立即返回false,实际上不再“在原地打转”。
spin_unlock(lock)
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()
/* 定义一个自旋锁*/
spinlock_t lock;
spin_lock_init(&lock);
spin_lock (&lock) ; /* 获取自旋锁,保护临界区 */
. . ./* 临界区*/
spin_unlock (&lock) ; /* 解锁*/
在多核编程的时候,如果进程和中断可能访问同一片临界资源,我们一般需要在进程上下文中调用spin_lock_irqsave()/spin_unlock_irqrestore(),在中断上下文中调用spin_lock()/spin_unlock。
自旋锁实际上是忙等锁,当锁不可用时,CPU一直循环执行“测试并设置”该锁直到可用而取得该锁,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。因此,只有在占用锁的时间极短的情况下,使用自旋锁才是合理的。当临界区很大,或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能。
自旋锁可能导致系统死锁。引发这个问题最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU将死锁。
在自旋锁锁定期间不能调用可能引起进程调度的函数。如果进程获得自旋锁之后再阻塞,如调用copy_from_user()、copy_to_user()、kmalloc()和msleep()等函数,则可能导致内核的崩溃。
在单核情况下编程的时候,也应该认为自己的CPU是多核的,驱动特别强调跨平台的概念。比如,在单CPU的情况下,若中断和进程可能访问同一临界区,进程里调用spin_lock_irqsave()是安全的,在中断里其实不调用spin_lock()也没有问题,因为spin_lock_irqsave()可以保证这个CPU的中断服务程序不可能执行。但是,若CPU变成多核,spin_lock_irqsave()不能屏蔽另外一个核的中断,所以另外一个核就可能造成并发问题。因此,无论如何,我们在中断服务程序里也应该调用spin_lock()。
自旋锁不关心锁定的临界区究竟在进行什么操作,不管是读还是写,它都一视同仁。即便多个执行单元同时读取临界资源也会被锁住。实际上,对共享资源并发访问时,多个执行单元同时读取它是不会有问题的,自旋锁的衍生锁读写自旋锁(rwlock)可允许读的并发。读写自旋锁是一种比自旋锁粒度更小的锁机制,它保留了“自旋”的概念,但是在写操作方面,只能最多有1个写进程,在读操作方面,同时可以有多个读执行单元。当然,读和写也不能同时进行。
rwlock_t my_rwlock;
rwlock_init(&my_rwlock); /* 动态初始化 */
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
在对共享资源进行读取之前,应该先调用读锁定函数,完成之后应调用读解锁函数。
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
rwlock_t lock; /* 定义rwlock */
rwlock_init(&lock); /* 初始化rwlock */
/* 读时获取锁*/
read_lock(&lock);
... /* 临界资源 */
read_unlock(&lock);
/* 写时获取锁*/
write_lock_irqsave(&lock, flags);
... /* 临界资源 */
write_unlock_irqrestore(&lock, flags);
顺序锁(seqlock)是对读写锁的一种优化,若使用顺序锁,读执行单元不会被写执行单元阻塞,也就是说,读执行单元在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。但是,写执行单元与写执行单元之间仍然是互斥的,即如果有写执行单元在进行写操作,其他写执行单元必须自旋在那里,直到写执行单元释放了顺序锁。
对于顺序锁而言,尽管读写之间不互相排斥,但是如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新读取数据,以便确保得到的数据是完整的。所以,在这种情况下,读端可能反复读多次同样的区域才能读到有效的数据。
void write_seqlock(seqlock_t *sl);
int write_tryseqlock(seqlock_t *sl);
write_seqlock_irqsave(lock, flags)
write_seqlock_irq(lock)
write_seqlock_bh(lock)
write_seqlock_irqsave() = loal_irq_save() + write_seqlock()
write_seqlock_irq() = local_irq_disable() + write_seqlock()
write_seqlock_bh() = local_bh_disable() + write_seqlock()
void write_sequnlock(seqlock_t *sl);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh(lock)
write_sequnlock_irqrestore() = write_sequnlock() + local_irq_restore()
write_sequnlock_irq() = write_sequnlock() + local_irq_enable()
write_sequnlock_bh() = write_sequnlock() + local_bh_enable()
write_seqlock(&seqlock_a);
.../* 写操作代码块 */
write_sequnlock(&seqlock_a);
unsigned read_seqbegin(const seqlock_t *sl);
read_seqbegin_irqsave(lock, flags)
读执行单元在对被顺序锁s1保护的共享资源进行访问前需要调用该函数,该函数返回顺序锁s1的当前顺序号。其中,
read_seqbegin_irqsave() = local_irq_save() + read_seqbegin()
int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)
读执行单元在访问完被顺序锁s1保护的共享资源后需要调用该函数来检查,在读访问期间是否有写操作。如果有写操作,读执行单元就需要重新进行读操作。其中,
read_seqretry_irqrestore() = read_seqretry() + local_irq_restore()
do {
seqnum = read_seqbegin(&seqlock_a);
/* 读操作代码块 */
...
} while (read_seqretry(&seqlock_a, seqnum));
不同于自旋锁,使用RCU的读端没有锁、内存屏障、原子指令类的开销,几乎可以认为是直接读(只是简单地标明读开始和读结束),而RCU的写执行单元在访问它的共享资源前首先复制一个副本,然后对副本进行修改,最后使用一个回调机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据,这个时机就是所有引用该数据的CPU都退出对共享数据读操作的时候。等待适当时机的这一时期称为宽限期(Grace Period)。
RCU可以看作读写锁的高性能版本,相比读写锁,RCU的优点在于既允许多个读执行单元同时访问被保护的数据,又允许多个读执行单元和多个写执行单元同时访问被保护的数据。但是,RCU不能替代读写锁,因为如果写比较多时,对读执行单元的性能提高不能弥补写执行单元同步导致的损失。因为使用RCU时,写执行单元之间的同步开销会比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制来同步并发的其他写执行单元的修改操作。
rcu_read_lock()
rcu_read_lock_bh()
rcu_read_unlock()
rcu_read_unlock_bh()
rcu_read_lock()
.../* 读临界区*/
rcu_read_unlock()
synchronize_rcu()
该函数由RCU写执行单元调用,它将阻塞写执行单元,直到当前CPU上所有的已经存在(Ongoing)的读执行单元完成读临界区,写执行单元才可以继续下一步操作。synchronize_rcu()并不需要等待后续(Subsequent)读临界区的完成,如图所示。
探测所有的rcu_read_lock()被rcu_read_unlock()结束的过程很类似Java语言垃圾回收的工作
void call_rcu(struct rcu_head *head,
void (*func)(struct rcu_head *rcu));
函数call_rcu()也由RCU写执行单元调用,与synchronize_rcu()不同的是,它不会使写执行单元阻塞,因而可以在中断上下文或软中断中使用。该函数把函数func挂接到RCU回调函数链上,然后立即返回。挂接的回调函数会在一个宽限期结束(即所有已经存在的RCU读临界区完成)后被执行。
rcu_assign_pointer(p, v)
给RCU保护的指针赋一个新的值。
rcu_dereference(p)
信号量(Semaphore)是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0、1或者n。信号量与操作系统中的经典概念PV操作对应。 P(S):①如果信号量S的值大于零,该进程继续执行。 ②如果S的值为零,将该进程置为等待状态,排入信号量的等待队列,直到V操作唤醒之。
struct semaphore sem;
void sema_init(struct semaphore *sem, int val);
该函数初始化信号量,并设置信号量sem的值为val。
void down(struct semaphore * sem);
该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文中使用。
int down_interruptible(struct semaphore * sem);
该函数功能与down类似,不同之处为,因为down()进入睡眠状态的进程不能被信号打断,但因为down_interruptible()进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非0。
int down_trylock(struct semaphore * sem);
该函数尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回非0值。它不会导致调用者睡眠,可以在中断上下文中使用。
void up(struct semaphore * sem);
struct mutex my_mutex;
mutex_init(&my_mutex);
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int mutex_trylock(struct mutex *lock);
mutex_lock()与mutex_lock_interruptible()的区别和down()与down_trylock()的区别完全一致,前者引起的睡眠不能被信号打断,而后者可以。mutex_trylock()用于尝试获得mutex,获取不到mutex时不会引起进程睡眠。
void mutex_unlock(struct mutex *lock);
mutex的使用方法和信号量用于互斥的场合完全一样
struct mutex my_mutex; /* 定义mutex */
mutex_init(&my_mutex); /* 初始化mutex */
mutex_lock(&my_mutex); /* 获取mutex */
... /* 临界资源*/
mutex_unlock(&my_mutex); /* 释放mutex */
自旋锁和互斥体都是解决互斥问题的基本手段,面对特定的情况,应该如何取舍这两种手段呢?选择的依据是临界区的性质和系统的特点。 从严格意义上说,互斥体和自旋锁属于不同层次的互斥手段,前者的实现依赖于后者。在互斥体本身的实现上,为了保证互斥体结构存取的原子性,需要自旋锁来互斥。所以自旋锁属于更底层的手段。
互斥体是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会发生进程上下文切换,当前进程进入睡眠状态,CPU将运行其他进程。鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间较长时,用互斥体才是较好的选择。 当所要保护的临界区访问时间比较短时,用自旋锁是非常方便的,因为它可节省上下文切换的时间。但是CPU得不到自旋锁会在那里空转直到其他执行单元解锁为止,所以要求锁不能在临界区里长时间停留,否则会降低系统的效率。
多用于多线程多进程间的同步
struct completion my_completion;
下列代码初始化或者重新初始化my_completion这个完成量的值为0(即没有完成的状态):
init_completion(&my_completion);
reinit_completion(&my_completion)
下列函数用于等待一个完成量被唤醒
void wait_for_completion(struct completion *c);
void complete(struct completion *c);
void complete_all(struct completion *c);
wait_queue_head_t my_queue;
wait_queue_head_t是__wait_queue_head结构体的一个typedef。
init_waitqueue_head(&my_queue);
而下面的DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头部的“快捷方式”。
DECLARE_WAIT_QUEUE_HEAD (name)
DECLARE_WAITQUEUE(name, tsk)
该宏用于定义并初始化一个名为name的等待队列元素。
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
add_wait_queue()用于将等待队列元素wait添加到等待队列头部q指向的双向链表中,而remove_wait_queue()用于将等待队列元素wait从由q头部指向的链表中移除。
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
等待第1个参数queue作为等待队列头部的队列被唤醒,而且第2个参数condition必须满足,否则继续阻塞。wait_event()和wait_event_interruptible()的区别在于后者可以被信号打断,而前者不能。加上_timeout后的宏意味着阻塞等待的超时时间,以jiffy为单位,在第3个参数的timeout到达时,不论condition是否满足,均返回。
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
上述操作会唤醒以queue作为等待队列头部的队列中所有的进程。 wake_up()应该与wait_event()或wait_event_timeout()成对使用,而wake_up_interruptible()则应与wait_event_interruptible()或wait_event_interruptible_timeout()成对使用。wake_up()可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程,而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE的进程。
sleep_on(wait_queue_head_t *q );
interruptible_sleep_on(wait_queue_head_t *q );
sleep_on()函数应该与wake_up()成对使用,interruptible_sleep_on()应该与wake_up_interruptible()成对使用。
unsigned int(*poll)(struct f ile * f ilp, struct poll_table* wait);
第1个参数为file结构体指针,第2个参数为轮询表指针。这个函数应该进行两项工作。 1)对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头部添加到poll_table中。 2)返回表示是否能对设备进行无阻塞读、写访问的掩码。 用于向poll_table注册等待队列的关键poll_wait()函数的原型如下:
void poll_wait(struct f ile *f ilp, wait_queue_heat_t *queue, poll_table * wait);
驱动程序poll()函数应该返回设备资源的可获取状态,即POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL等宏的位“或”结果。每个宏的含义都表明设备的一种状态,如POLLIN(定义为0x0001)意味着设备可以无阻塞地读,POLLOUT(定义为0x0004)意味着设备可以无阻塞地写。
1 static unsigned int xxx_poll(struct file *filp, poll_table *wait)
2 {
3 unsigned int mask = 0;
4 struct xxx_dev *dev = filp->private_data; /* 获得设备结构体指针*/
5
6 ...
7 poll_wait(filp, &dev->r_wait, wait); /* 加入读等待队列 */
8 poll_wait(filp, &dev->w_wait, wait); /* 加入写等待队列 */
9
10 if (...) /* 可读 */
11 mask |= POLLIN | POLLRDNORM; /* 标示数据可获得(对用户可读)*/
12
13 if (...) /* 可写 */
14 mask |= POLLOUT | POLLWRNORM; /* 标示数据可写入*/
15 ...
16 return mask;
17 }
所谓中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理,CPU必须暂停当前程序的执行,转去处理突发事件,处理完毕后又返回原程序被中断的位置继续执行。 根据中断的来源,中断可分为内部中断和外部中断,内部中断的中断源来自CPU内部(软件中断指令、溢出、除法错误等,例如,操作系统从用户态切换到内核态需借助CPU内部的软件中断),外部中断的中断源来自CPU外部,由外设提出请求。
根据中断是否可以屏蔽,中断可分为可屏蔽中断与不可屏蔽中断(NMI),可屏蔽中断可以通过设置中断控制器寄存器等方法被屏蔽,屏蔽后,该中断不再得到响应,而不可屏蔽中断不能被屏蔽。 根据中断入口跳转方法的不同,中断可分为向量中断和非向量中断。采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后,再通过软件判断中断标志来识别具体是哪个中断。也就是说,向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。
设备的中断会打断内核进程中的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。但是,这个良好的愿望往往与现实并不吻合。在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。
顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,从而可以服务更多的中断请求。
现在,中断处理工作的重心就落在了底半部的头上,需用它来完成中断事件的绝大多数任务。底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。
尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为Linux设备驱动中的中断处理一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。
在Linux中,查看/proc/interrupts文件可以获得系统中中断的统计信息,并能统计出每一个中断号上的中断在每个CPU上发生的次数。
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev);
irq是要申请的硬件中断号。 handler是向系统登记的中断处理函数(顶半部),是一个回调函数,中断发生时,系统调用这个函数,dev参数将被传递给它。
irqflags是中断处理的属性,可以指定中断的触发方式以及处理方式。在触发方式方面,可以是IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW等。在处理方式方面,若设置了IRQF_SHARED,则表示多个设备共享中断,dev是要传递给中断服务程序的私有数据,一般设置为这个设备的设备结构体或者NULL。 request_irq()返回0表示成功,返回-EINVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。
int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id);
此函数与request_irq()的区别是devm_开头的API申请的是内核“managed”的资源,一般不需要在出错处理和remove()接口里再显式的释放。有点类似Java的垃圾回收机制。
typedef irqreturn_t (*irq_handler_t)(int, void *);
typedef int irqreturn_t;
void free_irq(unsigned int irq,void *dev_id);
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
disable_irq_nosync()与disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。由于disable_irq()会等待指定的中断被处理完,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统的死锁,这种情况下,只能调用disable_irq_nosync(n)。
Linux实现底半部的机制主要有tasklet、工作队列、软中断和线程化irq。
tasklet的使用较简单,它的执行上下文是软中断,执行时机通常是顶半部返回的时候。我们只需要定义tasklet及其处理函数,并将两者关联则可,例如:
void my_tasklet_func(unsigned long); /*定义一个处理函数*/
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
/*定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联*/
代码DECLARE_TASKLET(my_tasklet,my_tasklet_func,data)实现了定义名称为my_tasklet的tasklet,并将其与my_tasklet_func()这个函数绑定,而传入这个函数的参数为data。 在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行:
tasklet_schedule(&my_tasklet);
1 /* 定义tasklet和底半部函数并将它们关联 */
2 void xxx_do_tasklet(unsigned long);
3 DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
4
5 /* 中断处理底半部 */
6 void xxx_do_tasklet(unsigned long)
7 {
8 ...
9 }
10
11 /* 中断处理顶半部 */
12 irqreturn_t xxx_interrupt(int irq, void *dev_id)
13 {
14 ...
15 tasklet_schedule(&xxx_tasklet);
16 ...
17 }
18
19 /* 设备驱动模块加载函数 */
20 int __init xxx_init(void)
21 {
22 ...
23 /* 申请中断 */
24 result = request_irq(xxx_irq, xxx_interrupt,
25 0, "xxx", NULL);
26 ...
27 return IRQ_HANDLED;
28 }
29
30 /* 设备驱动模块卸载函数 */
31 void __exit xxx_exit(void)
32 {
33 ...
34 /* 释放中断 */
35 free_irq(xxx_irq, xxx_interrupt);
36 ...
37 }
工作队列的使用方法和tasklet非常相似,但是工作队列的执行上下文是内核线程,因此可以调度和睡眠。
struct work_struct my_wq; /* 定义一个工作队列 */
void my_wq_func(struct work_struct *work); /* 定义一个处理函数 */
通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定:
INIT_WORK(&my_wq, my_wq_func);
/* 初始化工作队列并将其与处理函数绑定 */
schedule_work(&my_wq); /* 调度工作队列执行 */
1 /* 定义工作队列和关联函数 */
2 struct work_struct xxx_wq;
3 void xxx_do_work(struct work_struct *work);
4
5 /* 中断处理底半部 */
6 void xxx_do_work(struct work_struct *work)
7 {
8 ...
9 }
10
11 /*中断处理顶半部*/
12 irqreturn_t xxx_interrupt(int irq, void *dev_id)
13 {
14 ...
15 schedule_work(&xxx_wq);
16 ...
17 return IRQ_HANDLED;
18 }
19
20 /* 设备驱动模块加载函数 */
21 int xxx_init(void)
22 {
23 ...
24 /* 申请中断 */
25 result = request_irq(xxx_irq, xxx_interrupt,
26 0, "xxx", NULL);
27 ...
28 /* 初始化工作队列 */
29 INIT_WORK(&xxx_wq, xxx_do_work);
30 ...
31 }
32
33 /* 设备驱动模块卸载函数 */
34 void xxx_exit(void)
35 {
36 ...
37 /* 释放中断 */
38 free_irq(xxx_irq, xxx_interrupt);
39 ...
40 }
工作队列早期的实现是在每个CPU核上创建一个worker内核线程,所有在这个核上调度的工作都在该worker线程中执行,其并发性显然差强人意。在Linux 2.6.36以后,转而实现了“Concurrency-managed workqueues”,简称cmwq,cmwq会自动维护工作队列的线程池以提高并发性,同时保持了API的向后兼容。
软中断(Softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet是基于软中断实现的,因此也运行于软中断上下文。
在Linux内核中,用softirq_action结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递给该函数的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以触发一个软中断。
软中断和tasklet运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下文。因此,在软中断和tasklet处理函数中不允许睡眠,而在工作队列处理函数中允许睡眠。 local_bh_disable()和local_bh_enable()是内核中用于禁止和使能软中断及tasklet底半部机制的函数。
内核中采用softirq的地方包括HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、SCSI_SOFTIRQ、TASKLET_SOFTIRQ等,一般来说,驱动的编写者不会也不宜直接使用softirq。
总结一下硬中断、软中断和信号的区别:硬中断是外部设备对CPU的中断,软中断是中断底半部的一种处理机制,而信号则是由内核(或其他进程)对某个进程的中断。在涉及系统调用的场合,人们也常说通过软中断(例如ARM为swi)陷入内核,此时软中断的概念是指由软件指令引发的中断,和我们这个地方说的softirq是两个完全不同的概念,一个是software,一个是soft。
需要特别说明的是,软中断以及基于软中断的tasklet如果在某段时间内大量出现的话,内核会把后续软中断放入ksoftirqd内核线程中执行。总的来说,中断优先级高于软中断,软中断又高于任何一个线程。软中断适度线程化,可以缓解高负载情况下系统的响应。
在内核中,除了可以通过request_irq()、devm_request_irq()申请中断以外,还可以通过request_threaded_irq()和devm_request_threaded_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);
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id);
由此可见,它们比request_irq()、devm_request_irq()多了一个参数thread_fn。用这两个API申请中断的时候,内核会为相应的中断号分配一个对应的内核线程。注意这个线程只针对这个中断号,如果其他中断也通过request_threaded_irq()申请,自然会得到新的内核线程。
参数handler对应的函数执行于中断上下文,thread_fn参数对应的函数则执行于内核线程。如果handler结束的时候,返回值是IRQ_WAKE_THREAD,内核会调度对应线程执行thread_fn对应的函数。
request_threaded_irq()和devm_request_threaded_irq()支持在irqflags中设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上下文中屏蔽对应的中断号,而在内核调度thread_fn执行后,重新使能该中断号。对于我们无法在上半部清除中断的情况,IRQF_ONESHOT特别有用,避免了中断服务程序一退出,中断就洪泛的情况。
handler参数可以设置为NULL,这种情况下,内核会用默认的irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT标记。irq_default_primary_handler()定义为:
/*
* Default primary interrupt handler for threaded interrupts. Is
* assigned as primary handler when request_threaded_irq is called
* with handler == NULL. Useful for oneshot interrupts.
*/
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}
1 /* 中断处理顶半部 */
2 irqreturn_t xxx_interrupt(int irq, void *dev_id)
3 {
4 ...
5 int status = read_int_status(); /* 获知中断源 */
6 if(!is_myint(dev_id,status)) /* 判断是否为本设备中断 */
7 return IRQ_NONE; /* 不是本设备中断,立即返回 */
8
9 /* 是本设备中断,进行处理 */
10 ...
11 return IRQ_HANDLED; /* 返回IRQ_HANDLED表明中断已被处理 */
12 }
13
14 /* 设备驱动模块加载函数 */
15 int xxx_init(void)
16 {
17 ...
18 /* 申请共享中断 */
19 result = request_irq(sh_irq, xxx_interrupt,
20 IRQF_SHARED, "xxx", xxx_dev);
21 ...
22 }
23
24 /* 设备驱动模块卸载函数 */
25 void xxx_exit(void)
26 {
27 ...
28 /* 释放中断 */
29 free_irq(xxx_irq, xxx_interrupt);
30 ...
31 }
软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。
1 struct timer_list {
2 /*
3 * All fields that change during normal runtime grouped to the
4 * same cacheline
5 */
6 struct list_head entry;
7 unsigned long expires;
8 struct tvec_base *base;
9
10 void (*function)(unsigned long);
11 unsigned long data;
12
13 int slack;
14
15 #ifdef CONFIG_TIMER_STATS
16 int start_pid;
17 void *start_site;
18 char start_comm[16];
19 #endif
20 #ifdef CONFIG_LOCKDEP
21 struct lockdep_map lockdep_map;
22 #endif
23 };
当定时器期满后,其中第10行的function()成员将被执行,而第11行的data成员则是传入其中的参数,第7行的expires则是定时器到期的时间(jiffies)。
struct timer_list my_timer;
void init_timer(struct timer_list * timer);
上述init_timer()函数初始化timer_list的entry的next为NULL,并给base指针赋值。 TIMER_INITIALIZER(_function,_expires,_data)宏用于赋值定时器结构体的function、expires、data和base成员,这个宏等价于:
#define TIMER_INITIALIZER(_function, _expires, _data) { \
.entry = { .prev = TIMER_ENTRY_STATIC }, \
.function = (_function), \
.expires = (_expires), \
.data = (_data), \
.base = &boot_tvec_bases, \
}
DEFINE_TIMER(_name,_function,_expires,_data)宏是定义并初始化定时器成员的“快捷方式”,这个宏定义为:
#define DEFINE_TIMER(_name, _function, _expires, _data) \
struct timer_list _name = \
TIMER_INITIALIZER(_function, _expires, _data)
此外,setup_timer()也可用于初始化定时器并赋值其成员,其源代码为:
#define __setup_timer(_timer, _fn, _data, _flags) \
do { \
__init_timer((_timer), (_flags)); \
(_timer)->function = (_fn); \
(_timer)->data = (_data); \
} while (0)
void add_timer(struct timer_list * timer);
上述函数用于注册内核定时器,将定时器加入到内核动态定时器链表中
int del_timer(struct timer_list * timer);
上述函数用于删除定时器。 del_timer_sync()是del_timer()的同步版,在删除一个定时器时需等待其被处理完,因此该函数的调用不能发生在中断上下文中。
int mod_timer(struct timer_list *timer, unsigned long expires);
上述函数用于修改定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数。
//将 jiffies 类型的参数 j 分别转换为对应的毫秒、微秒、纳秒。
int jiffies_to_msecs(const unsigned long j)
int jiffies_to_usecs(const unsigned long j)
u64 jiffies_to_nsecs(const unsigned long j)
//将毫秒、微秒、纳秒转换为 jiffies 类型。
long msecs_to_jiffies(const unsigned int m)
long usecs_to_jiffies(const unsigned int u)
unsigned long nsecs_to_jiffies(u64 n)
1 /* xxx设备结构体 */
2 struct xxx_dev {
3 struct cdev cdev;
4 ...
5 timer_list xxx_timer; /* 设备要使用的定时器 */
6 };
7
8 /* xxx驱动中的某函数 */
9 xxx_func1(…)
10 {
11 struct xxx_dev *dev = filp->private_data;
12 ...
13 /* 初始化定时器 */
14 init_timer(&dev->xxx_timer);
15 dev->xxx_timer.function = &xxx_do_timer;
16 dev->xxx_timer.data = (unsigned long)dev;
17 /* 设备结构体指针作为定时器处理函数参数 */
18 dev->xxx_timer.expires = jiffies + delay;
19 /* 添加(注册)定时器 */
20 add_timer(&dev->xxx_timer);
21 ...
22 }
23
24 /* xxx驱动中的某函数 */
25 xxx_func2(…)
26 {
27 ...
28 /* 删除定时器 */
29 del_timer (&dev->xxx_timer);
30 ...
31 }
32
33 /* 定时器处理函数 */
34 static void xxx_do_timer(unsigned long arg)
35 {
36 struct xxx_device *dev = (struct xxx_device *)(arg);
37 ...
38 /* 调度定时器再执行 */
39 dev->xxx_timer.expires = jiffies + delay;
40 add_timer(&dev->xxx_timer);
41 ...
42 }
对于周期性的任务,除了定时器以外,在Linux内核中还可以利用一套封装得很好的快捷机制,其本质是利用工作队列和定时器实现,这套快捷机制就是delayed_work,delayed_work结构体的定义如代码清单所示
1 struct delayed_work {
2 struct work_struct work;
3 struct timer_list timer;
4
5 /* target workqueue and CPU ->timer uses to queue ->work */
6 struct workqueue_struct *wq;
7 int cpu;
8 };
我们可以通过如下函数调度一个delayed_work在指定的延时后执行:
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
当指定的delay到来时,delayed_work结构体中的work成员work_func_t类型成员func()会被执行。work_func_t类型定义为:
typedef void (*work_func_t)(struct work_struct *work);
其中,delay参数的单位是jiffies,因此一种常见的用法如下:
schedule_delayed_work(&work, msecs_to_jiff ies(poll_interval));
如果要周期性地执行任务,通常会在delayed_work的工作函数中再次调用schedule_delayed_work(),周而复始。 如下函数用来取消delayed_work:
int cancel_delayed_work(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);
Linux内核中提供了下列3个函数以分别进行纳秒、微秒和毫秒延迟:
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
上述延迟的实现原理本质上是忙等待,它根据CPU频率进行一定次数的循环。有时候,人们在软件中进行下面的延迟:
void delay(unsigned int time)
{
while(time--);
}
ndelay()、udelay()和mdelay()函数的实现方式原理与此类似。内核在启动时,会运行一个延迟循环校准(Delay Loop Calibration),计算出lpj(Loops Per Jiffy),内核启动时会打印如下类似信息:
Calibrating delay loop... 530.84 BogoMIPS (lpj=1327104)
如果我们直接在bootloader传递给内核的bootargs中设置lpj=1327104,则可以省掉这个校准的过程,节省约百毫秒级的开机时间。 毫秒时延(以及更大的秒时延)已经比较大了,在内核中,最好不要直接使用mdelay()函数,这将耗费CPU资源,对于毫秒级以上的时延,内核提供了下述函数:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
上述函数将使得调用它的进程睡眠参数指定的时间为millisecs,msleep()、ssleep()不能被打断,而msleep_interruptible()则可以被打断。 受系统Hz以及进程调度的影响,msleep()类似函数的精度是有限的。
在内核中进行延迟的一个很直观的方法是比较当前的jiffies和目标jiffies(设置为当前jiffies加上时间间隔的jiffies),直到未来的jiffies达到目标jiffies。代码清单给出了使用忙等待先延迟100个jiffies再延迟2s的实例。
1 /* 延迟100个jiffies */
2 unsigned long delay = jiffies + 100;
3 while(time_before(jiffies, delay));
4
5 /* 再延迟2s */
6 unsigned long delay = jiffies + 2*Hz;
7 while(time_before(jiffies, delay));
与time_before()对应的还有一个time_after(),它们在内核中定义为(实际上只是将传入的未来时间jiffies和被调用时的jiffies进行一个简单的比较):
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(b) - (long)(a) < 0))
#define time_before(a,b) time_after(b,a)
为了防止在time_before()和time_after()的比较过程中编译器对jiffies的优化,内核将其定义为volatile变量,这将保证每次都会重新读取这个变量。因此volatile更多的作用还是避免这种读合并。
睡着延迟无疑是比忙等待更好的方式,睡着延迟是在等待的时间到来之前进程处于睡眠状态,CPU资源被其他进程使用。schedule_timeout()可以使当前任务休眠至指定的jiffies之后再重新被调度执行,msleep()和msleep_interruptible()在本质上都是依靠包含了schedule_timeout()的schedule_timeout_uninterruptible()和schedule_timeout_interruptible()来实现的,如代码清单所示。
1 void msleep(unsigned int msecs)
2 {
3 unsigned long timeout = msecs_to_jiffies(msecs) + 1;
4
5 while (timeout)
6 timeout = schedule_timeout_uninterruptible(timeout);
7 }
8
9 unsigned long msleep_interruptible(unsigned int msecs)
10 {
11 unsigned long timeout = msecs_to_jiffies(msecs) + 1;
12
13 while (timeout && !signal_pending(current))
14 timeout = schedule_timeout_interruptible(timeout);
15 return jiffies_to_msecs(timeout);
16 }
实际上,schedule_timeout()的实现原理是向系统添加一个定时器,在定时器处理函数中唤醒与参数对应的进程。
dts是设备树源文件、dtsi是头文件、dtc是设备树编译器、dtb是编译出来的二进制文件
对于设备树中的节点和属性具体是如何来描述设备的硬件细节的,一般需要文档来进行讲解,文档的后缀名一般为.txt。在这个.txt文件中,需要描述对应节点的兼容性、必需的属性和可选的属性。 这些文档位于内核的Documentation/devicetree/bindings目录下,其下又分为很多子目录。
chosen 并不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重 点是 bootargs 参数。一般.dts 文件中 chosen 节点通常为空或者内容很少, chosen 节点内容如下所示:
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
struct device_node *of_f ind_compatible_node(struct device_node *from,
const char *type, const char *compatible);
根据兼容属性,获得设备节点。遍历设备树中的设备节点,看看哪个节点的类型、兼容属性与本函数的输入参数匹配,在大多数情况下,from、type为NULL,则表示遍历了所有节点。
int of_property_read_u8_array(const struct device_node *np,
const char *propname, u8 *out_values, size_t sz);
int of_property_read_u16_array(const struct device_node *np,
const char *propname, u16 *out_values, size_t sz);
int of_property_read_u32_array(const struct device_node *np,
const char *propname, u32 *out_values, size_t sz);
int of_property_read_u64(const struct device_node *np, const char
*propname, u64 *out_value);
读取设备节点np的属性名,为propname,属性类型为8、16、32、64位整型数组。对于32位处理器来讲,最常用的是of_property_read_u32_array()。
除了整型属性外,字符串属性也比较常用,其对应的API包括:
int of_property_read_string(struct device_node *np, const char
*propname,const char **out_string);
int of_property_read_string_index(struct device_node *np, const char
*propname,int index, const char **output);
除整型、字符串以外的最常用属性类型就是布尔型,其对应的API很简单,具体如下
static inline bool of_property_read_bool(const struct device_node *np,
const char *propname);
void __iomem *of_iomap(struct device_node *node, int index);
上述API可以直接通过设备节点进行设备内存区间的ioremap(),index是内存段的索引。若设备节点的reg属性有多段,可通过index标示要ioremap()的是哪一段,在只有1段的情况,index为0。采用设备树后,一些设备驱动通过of_iomap()而不再通过传统的ioremap()进行映射,当然,传统的ioremap()的用户也不少。
int of_address_to_resource(struct device_node *dev, int index,
struct resource *r);
上述API通过设备节点获取与它对应的内存资源的resource结构体。其本质是分析reg属性以获取内存基地址、大小等信息并填充到struct resource*r参数指向的结构体中。
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
通过设备树获得设备的中断号,实际上是从.dts中的interrupts属性里解析出中断号。若设备使用了多个中断,index指定中断的索引号。
struct platform_device *of_f ind_device_by_node(struct device_node *np);
在可以拿到device_node的情况下,如果想反向获取对应的platform_device,可使用上述API。 当然,在已知platform_device的情况下,想获取device_node则易如反掌,例如:
static int sirfsoc_dma_probe(struct platform_device *op)
{
struct device_node *dn = op->dev.of_node;
…
}
与“线程池线程”概念一样,按序处理其他线程/进程交付的批量工作
struct kthread_worker {
unsigned int flags;
spinlock_t lock;
struct list_head work_list;
struct list_head delayed_work_list;
struct task_struct *task;
struct kthread_work *current_work;
};
表示等待内核线程处理的具体工作
struct kthread_work {
struct list_head node;
kthread_work_func_t func;
struct kthread_worker *worker;
/* Number of canceling calls that are running at the moment. */
int canceling;
};
typedef void (*kthread_work_func_t)(struct kthread_work *work);
表示等待某个内核线程工人处理完所有工作
struct kthread_flush_work {
struct kthread_work work;
struct completion done;
};
struct kthread_worker hi_worker;
kthread_init_worker(&hi_worker);
struct task_struct *kworker_task;
kworker_task =kthread_run(kthread_worker_fn, &hi_worker, "nvme%d", 1);
struct kthread_work hi_work;
kthread_init_work(&hi_work, xxx_work_fn);
kthread_queue_work(&hi_worker, &hi_work);
刷新指定 kthread_worker上所有 work
kthread_flush_worker(&hi_worker);
kthread_stop(kworker_task);
Linux中有I2C和SPI这样的子系统,这些子系统用来连接依附在这些总线上的子设备。这些总线都有一个特点,就是需要对子设备进行寄存器的读写,这往往会导致具有寄存器读写的子系统代码中存在冗余。
为了避免这些问题发生,Linux将通用代码抽取出来,简化了驱动程序的开发和维护。从Linux3.1开始引入了新的API,称为regmap。regmap子系统负责调用相关的i2c和spi子系统来读写寄存器,本质上是对i2c读写的一层封装系统,下面将分析一个LT9611驱动来了解regmap的使用。
regmap相关文件:regmap.c、regmap-i2c.c、regmapcache.c、regmap-flat.c、regmap-mmio.c
regmap 是 Linux 内核为了减少慢速 I/O 在驱动上的冗余开销,提供了一种通用的接口来操 作硬件寄存器。另外, regmap 在驱动和硬件之间添加了 cache,降低了低速 I/O 的操作次数,提 高了访问效率,缺点是实时性会降低。
regmap 框架分为三层: ①、底层物理总线: regmap 就是对不同的物理总线进行封装,目前 regmap 支持的物理总线有 i2c、 i3c、 spi、 mmio、 sccb、 sdw、 slimbus、 irq、 spmi 和 w1。 ②、 regmap 核心层,用于实现 regmap,我们不用关心具体实现。 ③、 regmap API 抽象层, regmap 向驱动编写人员提供的 API 接口,驱动编写人员使用这些API 接口来操作具体的芯片设备,也是驱动编写人员重点要掌握的。
51 struct regmap {
52 union {
53 struct mutex mutex;
54 struct {
55 spinlock_t spinlock;
56 unsigned long spinlock_flags;
57 };
58 };
59 regmap_lock lock;
60 regmap_unlock unlock;
61 void *lock_arg; /* This is passed to lock/unlock functions */
62
63 struct device *dev; /* Device we do I/O on */
64 void *work_buf; /* Scratch buffer used to format I/O */
65 struct regmap_format format; /* Buffer format */
66 const struct regmap_bus *bus;
67 void *bus_context;
68 const char *name;
69
70 bool async;
71 spinlock_t async_lock;
72 wait_queue_head_t async_waitq;
73 struct list_head async_list;
74 struct list_head async_free;
75 int async_ret;
......
89 unsigned int max_register;
90 bool (*writeable_reg)(struct device *dev, unsigned int reg);
91 bool (*readable_reg)(struct device *dev, unsigned int reg);
92 bool (*volatile_reg)(struct device *dev, unsigned int reg);
93 bool (*precious_reg)(struct device *dev, unsigned int reg);
94 const struct regmap_access_table *wr_table;
95 const struct regmap_access_table *rd_table;
96 const struct regmap_access_table *volatile_table;
97 const struct regmap_access_table *precious_table;
98
99 int (*reg_read)(void *context, unsigned int reg,
unsigned int *val);
100 int (*reg_write)(void *context, unsigned int reg,
unsigned int val);
......
147 struct rb_root range_tree;
148 void *selector_work_buf; /* Scratch buffer used for selector */
149 };
regmap_config 结构体就是用来初始化 regmap 的
186 struct regmap_config {
187 const char *name;
188
189 int reg_bits;
190 int reg_stride;
191 int pad_bits;
192 int val_bits;
193
194 bool (*writeable_reg)(struct device *dev, unsigned int reg);
195 bool (*readable_reg)(struct device *dev, unsigned int reg);
196 bool (*volatile_reg)(struct device *dev, unsigned int reg);
197 bool (*precious_reg)(struct device *dev, unsigned int reg);
198 regmap_lock lock;
199 regmap_unlock unlock;
200 void *lock_arg;
201
202 int (*reg_read)(void *context, unsigned int reg, unsigned int*val);
203 int (*reg_write)(void *context, unsigned int reg, unsigned int val);
204
205 bool fast_io;
206
207 unsigned int max_register;
208 const struct regmap_access_table *wr_table;
209 const struct regmap_access_table *rd_table;
210 const struct regmap_access_table *volatile_table;
211 const struct regmap_access_table *precious_table;
212 const struct reg_default *reg_defaults;
213 unsigned int num_reg_defaults;
214 enum regcache_type cache_type;
215 const void *reg_defaults_raw;
216 unsigned int num_reg_defaults_raw;
217
218 u8 read_flag_mask;
219 u8 write_flag_mask;
220
221 bool use_single_rw;
222 bool can_multi_write;
223
224 enum regmap_endian reg_format_endian;
225 enum regmap_endian val_format_endian;
226
227 const struct regmap_range_cfg *ranges;
228 unsigned int num_ranges;
229 };
Linux 内核里面已经对 regmap_config 各个成员变量进行了详细的讲解,这里我们只看一些 比较重要的: 第 187 行 name:名字。 第 189 行 reg_bits:寄存器地址位数,必填字段。 第 190 行 reg_stride:寄存器地址步长。 第 191 行 pad_bits:寄存器和值之间的填充位数。 第 192 行 val_bits:寄存器值位数,必填字段。 第 194 行 writeable_reg:可选的可写回调函数,寄存器可写的话此回调函数就会被调用, 并返回 true。 第 195 行 readable_reg:可选的可读回调函数,寄存器可读的话此回调函数就会被调用,并 返回 true。 第 196 行 volatile_reg:可选的回调函数,当寄存器值不能缓存的时候此回调函数就会被调 用,并返回 true。 第 197 行 precious_reg:当寄存器值不能被读出来的时候此回调函数会被调用,比如很多中 断状态寄存器读清零,读这些寄存器就可以清除中断标志位,但是并没有读出这些寄存器内部 的值。 第 202 行 reg_read:可选的读操作回调函数,所有读寄存器的操作此回调函数就会执行。 第 203 行 reg_write:可选的写操作回调函数,所有写寄存器的操作此回调函数就会执行。 第 205 行 fast_io:快速 I/O,使用 spinlock 替代 mutex 来提升锁性能。 第 207 行 max_register:有效的最大寄存器地址,可选。 第 208 行 wr_table:可写的地址范围,为 regmap_access_table 结构体类型。后面的 rd_table、 volatile_table、 precious_table、 wr_noinc_table 和 rd_noinc_table 同理。 第 212 行 reg_defaults:寄存器模式值,为 reg_default 结构体类型,此结构体有两个成员变 量: reg 和 def, reg 是寄存器地址, def 是默认值。 第 216 行 num_reg_defaults:默认寄存器表中的元素个数。 第 218 行 read_flag_mask:读标志掩码。 第 219 行 write_flag_mask:写标志掩码。 关于 regmap_config 结构体成员变量就介绍这些,其他没有介绍的自行查阅 Linux 内核中的 相关描述。
Regmap 申请与初始化 前面说了, regmap 支持多种物理总线,比如 I2C 和 SPI,我们需要根据所使用的接口来选择合适的 regmap 初始化函数。 Linux 内核提供了针对不同接口的 regmap 初始化函数, SPI 接口初始化函数为 regmap_init_spi,函数原型如下:
struct regmap * regmap_init_spi(struct spi_device *spi,
const struct regmap_config *config)
函数参数和返回值含义如下:
spi: 需要使用 regmap 的 spi_device。
config: regmap_config 结构体,需要程序编写人员初始化一个 regmap_config 实例,然后将其地址赋值给此参数。 返回值:申请到的并进过初始化的 regmap。
I2C 接口的 regmap 初始化函数为 regmap_init_i2c,函数原型如下:
struct regmap * regmap_init_i2c(struct i2c_client *i2c,
const struct regmap_config *config)
函数参数和返回值含义如下: i2c: 需要使用 regmap 的 i2c_client。 config: regmap_config 结构体,需要程序编写人员初始化一个 regmap_config 实例,然后将 其地址赋值给此参数。 返回值:申请到的并进过初始化的 regmap。 还有很多其他物理接口对应的 regmap 初始化函数,这里就不介绍了,大家直接查阅 Linux 内核即可,基本和 SPI/I2C 的初始化函数相同 在退出驱动的时候需要释放掉申请到的 regmap,不管是什么接口,全部使用 regmap_exit 这 个函数来释放 regmap,函数原型如下:
void regmap_exit(struct regmap *map)
函数参数和返回值含义如下: map: 需要释放的 regmap 返回值:无。 我们一般会在 probe 函数中初始化 regmap_config,然后申请并初始化 regmap。
regmap 设备访问 API 函数
不管是 I2C 还是 SPI 等接口,还是 SOC 内部的寄存器,对于寄存器的操作就两种:读和 写。 regmap 提供了最核心的两个读写操作: regmap_read 和 regmap_write。这两个函数分别用来 读/写寄存器, regmap_read 函数原型如下:
int regmap_read(struct regmap *map,
unsigned int reg,
unsigned int *val)
函数参数和返回值含义如下: map: 要操作的 regmap。 reg: 要读的寄存器。 val:读到的寄存器值。 返回值: 0,读取成功;其他值,读取失败。 regmap_write 函数原型如下:
int regmap_write(struct regmap *map,
unsigned int reg,
unsigned int val)
函数参数和返回值含义如下: map: 要操作的 regmap。 reg: 要写的寄存器。 val:要写的寄存器值。 返回值: 0,写成功;其他值,写失败。 在 regmap_read 和 regmap_write 的基础上还衍生出了其他一些 regmap 的 API 函数,首先是 regmap_update_bits 函数,看名字就知道,此函数用来修改寄存器指定的 bit,函数原型如下:
int regmap_update_bits (struct regmap *map,
unsigned int reg,
unsigned int mask,
unsigned int val)
函数参数和返回值含义如下: map: 要操作的 regmap。
reg: 要操作的寄存器。 mask: 掩码,需要更新的位必须在掩码中设置为 1。 val:需要更新的位值。 返回值: 0,写成功;其他值,写失败。
regmap_bulk_read 函数,此函数用于读取多个寄存器的值,函数原型如下:
int regmap_bulk_read(struct regmap *map,
unsigned int reg,
void *val,
size_t val_count)
函数参数和返回值含义如下: map: 要操作的 regmap。 reg: 要读取的第一个寄存器。
val: 读取到的数据缓冲区。 val_count:要读取的寄存器数量。 返回值: 0,写成功;其他值,读失败。
多个寄存器写函数 regmap_bulk_write,函数原型如下:
int regmap_bulk_write(struct regmap *map,
unsigned int reg,
const void *val,
size_t val_count)
函数参数和返回值含义如下: map: 要操作的 regmap。 reg: 要写的第一个寄存器。 val: 要写的寄存器数据缓冲区。 val_count:要写的寄存器数量。 返回值: 0,写成功;其他值,读失败。
结构体 regmap_config 里面有三个关于掩码的成员变量: read_flag_mask 和 write_flag_mask,这二个掩码非常重要。
I2C,SPI使用regmap进行读写的时候,会把从机地址分别与这两个掩码相与。
配置初始化函数 devm_regmap_init_i2c(struct i2c_client *client, const struct regmap_config)
,第一个参数传入i2c的client,此结构体从i2c_driver的probe传入,第二个参数为regmap_config
结构体。
static const struct regmap_config xx_regmap_config = {
.reg_bits = 8,//子设备寄存器的位数
.val_bits = 8,//子设备寄存器中设置的值的位数
.read_flag_mask = 0x80//读掩码
};
regmap_init_spi(iic,&xx_regmap_config);
struct input_dev *input_allocate_device(void);
void input_free_device(struct input_dev *dev);
int __must_check input_register_device(struct input_dev *);
void input_unregister_device(struct input_dev *);
/* 报告指定type、code的输入事件 */
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);
/* 报告键值 */
void input_report_key(struct input_dev *dev, unsigned int code, int value);
/* 报告相对坐标 */
void input_report_rel(struct input_dev *dev, unsigned int code, int value);
/* 报告绝对坐标 */
void input_report_abs(struct input_dev *dev, unsigned int code, int value);
/* 报告同步事件 */
void input_sync(struct input_dev *dev);
121 struct input_dev {
122 const char *name;
123 const char *phys;
124 const char *uniq;
125 struct input_id id;
126
127 unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
128
129 unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; /* 事件类型的位图 */
130 unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; /* 按键值的位图 */
131 unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; /* 相对坐标的位图 */
132 unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; /* 绝对坐标的位图 */
133 unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; /* 杂项事件的位图 */
134 unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; /*LED 相关的位图 */
135 unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];/* sound 有关的位图 */
136 unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; /* 压力反馈的位图 */
137 unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; /*开关状态的位图 */
......
189 bool devres_managed;
190 };
#define EV_SYN 0x00 /* 同步事件 */
#define EV_KEY 0x01 /* 按键事件 */
#define EV_REL 0x02 /* 相对坐标事件 */
#define EV_ABS 0x03 /* 绝对坐标事件 */
#define EV_MSC 0x04 /* 杂项(其他)事件 */
#define EV_SW 0x05 /* 开关事件 */
#define EV_LED 0x11 /* LED */
#define EV_SND 0x12 /* sound(声音) */
#define EV_REP 0x14 /* 重复事件 */
#define EV_FF 0x15 /* 压力事件 */
#define EV_PWR 0x16 /* 电源事件 */
#define EV_FF_STATUS 0x17 /* 压力状态事件 */
1 struct input_event {
2 struct timeval time;//time:时间,也就是此事件发生的时间
3 __u16 type; //事件类型
4 __u16 code; //事件码
5 __s32 value;//值
6 };
struct timeval {
__kernel_time_t tv_sec; /* 秒 */
__kernel_suseconds_t tv_usec; /* 微秒 */
};
I2C协议可以工作在以下5种速率模式下,不同的器件可能支持不同的速率。bps:bit/s,即SCL的频率
其中超快模式是单向数据传输,通常用于LED、LCD等不需要应答的器件,和正常的I2C操作时序类似,但是只进行写数据,不需要考虑ACK应答信号。
I2C协议最基础的几种信号:起始、停止、应答和非应答信号。
I2C协议规定,SCL处于高电平时,SDA由高到低变化,这种信号是起始信号。
I2C协议规定,SCL处于高电平,SDA由低到高变化,这种信号是停止信号。
I2C协议对数据的采样发生在SCL高电平期间,除了起始和停止信号,在数据传输期间,SCL为高电平时,SDA必须保持稳定,不允许改变,在SCL低电平时才可以进行变化。
I2C最大的一个特点就是有完善的应答机制,从机接收到主机的数据时,会回复一个应答信号来通知主机表示“我收到了”。
应答信号出现在1个字节传输完成之后,即第9个SCL时钟周期内,此时主机需要释放SDA总线,把总线控制权交给从机,由于上拉电阻的作用,此时总线为高电平,如果从机正确的收到了主机发来的数据,会把SDA拉低,表示应答响应。
使用MCU、FPGA等控制器实现时,需要在第9个SCL时钟周期把SDA设置为高阻输入状态,如果读取到SDA为低电平,则表示数据被成功接收到,可以进行下一步操作。
当第9个SCL时钟周期时,SDA保持高电平,表示非应答信号。
非应答信号可能是主机产生也可能是从机产生,产生非应答信号的情况主要有以下几种:
主机通过 I2C 总线与从机之间进行通信不外乎两个操作:写和读, I2C 总线单字节写时序 如图 所示:
1)、开始信号。 2)、发送 I2C 设备地址,每个 I2C 器件都有一个设备地址,通过发送具体的设备地址来决 定访问哪个 I2C 器件。这是一个 8 位的数据,其中高 7 位是设备地址,最后 1 位是读写位,为 1 的话表示这是一个读操作,为 0 的话表示这是一个写操作。 3)、 I2C 器件地址后面跟着一个读写位,为 0 表示写操作,为 1 表示读操作。 4)、从机发送的 ACK 应答信号。 5)、重新发送开始信号。 6)、发送要写写入数据的寄存器地址。 7)、从机发送的 ACK 应答信号。 8)、发送要写入寄存器的数据。
9)、从机发送的 ACK 应答信号。
10)、停止信号。
I2C 总线单字节读时序如图 所示:
I2C 单字节读时序比写时序要复杂一点,读时序分为 4 大步,第一步是发送设备地址,第二步是发送要读取的寄存器地址,第三步重新发送设备地址,最后一步就是 I2C 从器件输出要读取的寄存器值,我们具体来看一下这几步。 1)、主机发送起始信号。 2)、主机发送要读取的 I2C 从设备地址。 3)、读写控制位,因为是向 I2C 从设备发送数据,因此是写信号。 4)、从机发送的 ACK 应答信号。 5)、重新发送 START 信号。 6)、主机发送要读取的寄存器地址。 7)、从机发送的 ACK 应答信号。 8)、重新发送 START 信号。 9)、重新发送要读取的 I2C 从设备地址。 10)、读写控制位,这里是读信号,表示接下来是从 I2C 从设备里面读取数据。 11)、从机发送的 ACK 应答信号。 12)、从 I2C 器件里面读取到的数据。 13)、主机发出 NO ACK 信号,表示读取完成,不需要从机再发送 ACK 信号了。 14)、主机发出 STOP 信号,停止 I2C 通信。
I2C 多字节读写时序 有时候我们需要读写多个字节,多字节读写时序和单字节的基本一致,只是在读写数据的时候可以连续发送多个自己的数据,其他的控制时序都是和单字节一样的。
Linux的I²C体系结构分为3个组成部分。 (1)I²C核心 I²C核心提供了I²C总线驱动和设备驱动的注册、注销方法,I²C通信方法(即Algorithm)上层的与具体适配器无关的代码以及探测设备、检测设备地址的上层代码等,如图所示。
(2)I²C总线驱动 I²C总线驱动是对I²C硬件体系结构中适配器端的实现,适配器可由CPU控制,甚至可以直接集成在CPU内部。
I²C总线驱动主要包含I²C适配器数据结构i2c_adapter、I²C适配器的Algorithm数据结构i2c_algorithm和控制I²C适配器产生通信信号的函数。
经由I²C总线驱动的代码,我们可以控制I²C适配器以主控方式产生开始位、停止位、读写周期,以及以从设备方式被读写、产生ACK等。
(3)I²C设备驱动 I²C设备驱动(也称为客户驱动)是对I²C硬件体系结构中设备端的实现,设备一般挂接在受CPU控制的I²C适配器上,通过I²C适配器与CPU交换数据。 I²C设备驱动主要包含数据结构i2c_driver和i2c_client,我们需要根据具体设备实现其中的成员函数。
161 struct i2c_driver {
162 unsigned int class;
163
164 /* Notifies the driver that a new bus has appeared. You should
165 * avoid using this, it will be removed in a near future.
166 */
167 int (*attach_adapter)(struct i2c_adapter *) __deprecated;
168
169 /* Standard driver model interfaces */
170 int (*probe)(struct i2c_client *, const struct i2c_device_id *);
171 int (*remove)(struct i2c_client *);
172
173 /* driver model interfaces that don't relate to enumeration */
174 void (*shutdown)(struct i2c_client *);
175
176 /* Alert callback, for example for the SMBus alert protocol.
177 * The format and meaning of the data value depends on the
178 * protocol.For the SMBus alert protocol, there is a single bit
179 * of data passed as the alert response's low bit ("event
180 flag"). */
181 void (*alert)(struct i2c_client *, unsigned int data);
182
183 /* a ioctl like command that can be used to perform specific
184 * functions with the device.
185 */
186 int (*command)(struct i2c_client *client, unsigned int cmd,
void *arg);
187
188 struct device_driver driver;
189 const struct i2c_device_id *id_table;
190
191 /* Device detection callback for automatic device creation */
192 int (*detect)(struct i2c_client *, struct i2c_board_info *);
193 const unsigned short *address_list;
194 struct list_head clients;
195 };
第 170 行,当 I2C 设备和驱动匹配成功以后 probe 函数就会执行,和 platform 驱动一样。 第 188 行, device_driver 驱动结构体,如果使用设备树的话,需要设置 device_driver 的of_match_table 成员变量,也就是驱动的兼容(compatible)属性。 第 189 行, id_table 是传统的、未使用设备树的设备匹配 ID 表
nt i2c_register_driver(struct module *owner,
struct i2c_driver *driver)
函数参数和返回值含义如下: owner: 一般为 THIS_MODULE。
driver:要注册的 i2c_driver。 返回值: 0,成功;负值,失败。
void i2c_del_driver(struct i2c_driver *driver)
函数参数和返回值含义如下: driver:要注销的 i2c_driver。 返回值: 无。
1 static int __init yyy_init(void)
2 {
3 return i2c_add_driver(&yyy_driver);
4 }
5 module_initcall(yyy_init);
6
7 static void __exit yyy_exit(void)
8 {
9 i2c_del_driver(&yyy_driver);
10 }
11 module_exit(yyy_exit);
int i2c_transfer(struct i2c_adapter *adap,
struct i2c_msg *msgs,
int num)
函数参数和返回值含义如下:
adap: 所使用的 I2C 适配器, i2c_client 会保存其对应的 i2c_adapter。 msgs: I2C 要发送的一个或多个消息。 num: 消息数量,也就是 msgs 的数量。 返回值: 负值,失败,其他非负值,发送的 msgs 数量。
68 struct i2c_msg {
69 __u16 addr; /* 从机地址 */
70 __u16 flags; /* 标志 */
71 #define I2C_M_TEN 0x0010
72 #define I2C_M_RD 0x0001
73 #define I2C_M_STOP 0x8000
74 #define I2C_M_NOSTART 0x4000
75 #define I2C_M_REV_DIR_ADDR 0x2000
76 #define I2C_M_IGNORE_NAK 0x1000
77 #define I2C_M_NO_RD_ACK 0x0800
78 #define I2C_M_RECV_LEN 0x0400
79 __u16 len; /* 消息(本 msg)长度 */
80 __u8 *buf; /* 消息数据 */
81 };
Type A:适用于触摸点不能被区分或者追踪,此类型的设备上报原始数据 Type B:适用于有硬件追踪并能区分触摸点的触摸设备,此类型设备通过 slot 更新某一个触摸点的信息, 触摸点的信息通过一系列的 ABS_MT 事件(有的资料也叫消息)上报给 linux 内核,只有ABS_MT 事件是用于多点触摸的 。
在 ABS_MT 事 件 中 , 我 们 最 常 用 的 就 是 ABS_MT_SLOT 、ABS_MT_POSITION_X 、 ABS_MT_POSITION_Y 和 ABS_MT_TRACKING_ID 。 其 中ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y 用 来 上报 触 摸点 的 (X,Y) 坐 标 信息 ,ABS_MT_SLOT 用 来 上 报 触 摸 点 ID , 对 于 Type B 类 型 的 设 备 , 需 要 用 到ABS_MT_TRACKING_ID 事件来区分触摸点 。
对于 TypeA 类型的设备,通过 input_mt_sync()函数来隔离不同的触摸点数据信息,此函数原型如下所示:
void input_mt_sync(struct input_dev *dev)
此函数只要一个参数,类型为 input_dev,用于指定具体的 input_dev 设备。 input_mt_sync()函数会触发 SYN_MT_REPORT 事件,此事件会通知接收者获取当前触摸数据,并且准备接收下一个触摸点数据。
对于 Type B 类型的设备,上报触摸点信息的时候需要通过 input_mt_slot()函数区分是哪一个触摸点, input_mt_slot()函数原型如下所示:
void input_mt_slot(struct input_dev *dev, int slot)
此函数有两个参数,第一个参数是 input_dev 设备,第二个参数 slot 用于指定当前上报的是哪个触摸点信息。 input_mt_slot()函数会触发 ABS_MT_SLOT 事件,此事件会告诉接收者当前正在更新的是哪个触摸点(slot)的数据。
不管是哪个类型的设备,最终都要调用 input_sync()函数来标识多点触摸信息传输完成,告诉接收者处理之前累计的所有消息,并且准备好下一次接收。
Type B 和 Type A 相比最大的区别就是 Type B 可以区分出触摸点, 因此可以减少发送到用户空间的数据。 Type B 使用 slot 协议区分具体的触摸点, slot 需要用到 ABS_MT_TRACKING_ID 消息,这个 ID 需要硬件提供,或者通过原始数据计算出来。对于 TypeA 设备,内核驱动需要一次性将触摸屏上所有的触摸点信息全部上报,每个触摸点的信息在本次上报事件流中的顺序不重要,因为事件的过滤和手指(触摸点)跟踪是在内核空间处理的。
Type B 设备驱动需要给每个识别出来的触摸点分配一个 slot,后面使用这个 slot 来上报触摸点信息。可以通过 slot 的 ABS_MT_TRACKING_ID 来新增、替换或删除触摸点。一个非负数的 ID 表示一个有效的触摸点, -1 这个 ID 表示未使用 slot。一个以前不存在的 ID 表示这是一个新加的触摸点,一个 ID 如果再也不存在了就表示删除了。有些设备识别或追踪的触摸点信息要比他上报的多,这些设备驱动应该给硬件上报的每个触摸点分配一个 Type B 的 slot。一旦检测到某一个 slot 关联的触摸点 ID 发生了变化,驱动就应该改变这个 slot 的 ABS_MT_TRACKING_ID,使这个 slot 失效。如果硬件设备追踪到了比他正在上报的还要多的触摸点,那么驱动程序应该发送 BTN_TOOL_*TAP 消息,并且调用input_mt_report_pointer_emulation()函数,将此函数的第二个参数 use_count 设置为 false。
对于 Type A 类型的设备,发送触摸点信息的时序如下所示,这里以 2 个触摸点为例:
1 ABS_MT_POSITION_X x[0]
2 ABS_MT_POSITION_Y y[0]
3 SYN_MT_REPORT
4 ABS_MT_POSITION_X x[1]
5 ABS_MT_POSITION_Y y[1]
6 SYN_MT_REPORT
7 SYN_REPORT
第 1 行,通过 ABS_MT_POSITION_X 事件上报第一个触摸点的 X 坐标数据,通过 input_report_abs 函数实现,下面同理。 第 2 行,通过 ABS_MT_POSITION_Y 事件上报第一个触摸点的 Y 坐标数据。 第 3 行,上报 SYN_MT_REPORT 事件,通过调用 input_mt_sync 函数来实现。 第 4 行,通过 ABS_MT_POSITION_X 事件上报第二个触摸点的 X 坐标数据。 第 5 行,通过 ABS_MT_POSITION_Y 事件上报第二个触摸点的 Y 坐标数据。 第 6 行,上报 SYN_MT_REPORT 事件,通过调用 input_mt_sync 函数来实现。 第 7 行,上报 SYN_REPORT 事件,通过调用 input_sync 函数实现
103 static irqreturn_t st1232_ts_irq_handler(int irq, void *dev_id)
104 {
......
111 ret = st1232_ts_read_data(ts);
112 if (ret < 0)
113 goto end;
114
115 /* multi touch protocol */
116 for (i = 0; i < MAX_FINGERS; i++) {
117 if (!finger[i].is_valid)
118 continue;
119
120 input_report_abs(input_dev, ABS_MT_TOUCH_MAJOR, finger[i].t);
121 input_report_abs(input_dev, ABS_MT_POSITION_X, finger[i].x);
122 input_report_abs(input_dev, ABS_MT_POSITION_Y, finger[i].y);
123 input_mt_sync(input_dev);
124 count++;
125 }
......
140
141 /* SYN_REPORT */
142 input_sync(input_dev);
143
144 end:
145 return IRQ_HANDLED;
146 }
第 111 行,获取所有触摸点信息。 第 116~125 行,按照 Type A 类型轮流上报所有的触摸点坐标信息,第 121 和 122 行分别上 报触摸点的(X,Y)轴坐标,也就是 ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y 事件。每 上报完一个触摸点坐标,都要在第 123 行调用 input_mt_sync 函数上报一个 SYN_MT_REPORT 信息。 第 142 行,每上报完一轮触摸点信息就调用一次 input_sync 函数,也就是发送一个 SYN_REPORT 事件
1 ABS_MT_SLOT 0
2 ABS_MT_TRACKING_ID 45
3 ABS_MT_POSITION_X x[0]
4 ABS_MT_POSITION_Y y[0]
5 ABS_MT_SLOT 1
6 ABS_MT_TRACKING_ID 46
7 ABS_MT_POSITION_X x[1]
8 ABS_MT_POSITION_Y y[1]
9 SYN_REPORT
第 1 行,上报 ABS_MT_SLOT 事件,也就是触摸点对应的 SLOT。每次上报一个触摸点坐 标之前要先使用input_mt_slot函数上报当前触摸点SLOT,触摸点的SLOT其实就是触摸点ID, 需要由触摸 IC 提供。 第 2 行,根据 Type B 的要求,每个 SLOT 必须关联一个 ABS_MT_TRACKING_ID,通过 修改 SLOT 关联的 ABS_MT_TRACKING_ID 来完成对触摸点的添加、替换或删除。具体用到 的函数就是 input_mt_report_slot_state,如果是添加一个新的触摸点,那么此函数的第三个参数 active 要设置为 true, linux 内核会自动分配一个 ABS_MT_TRACKING_ID 值,不需要用户去指 定具体的 ABS_MT_TRACKING_ID 值。 第 3 行,上报触摸点 0 的 X 轴坐标,使用函数 input_report_abs 来完成。 第 4 行,上报触摸点 0 的 Y 轴坐标,使用函数 input_report_abs 来完成。 第 5~8 行,和第 1~4 行类似,只是换成了上报触摸点 0 的(X,Y)坐标信息 第 9 行,当所有的触摸点坐标都上传完毕以后就得发送 SYN_REPORT 事件,使用 input_sync 函数来完成。 当一个触摸点移除以后,同样需要通过 SLOT 关联的 ABS_MT_TRACKING_ID 来处理, 时序如下所示:
1 ABS_MT_TRACKING_ID -1
2 SYN_REPORT
第 1 行,当一个触摸点(SLOT)移除以后,需要通过 ABS_MT_TRACKING_ID 事件发送一 个-1 给内核。方法很简单,同样使用 input_mt_report_slot_state 函数来完成,只需要将此函数的 第三个参数 active 设置为 false 即可,不需要用户手动去设置-1。 第 2 行,当所有的触摸点坐标都上传完毕以后就得发送 SYN_REPORT 事件。 当要编写 Type B 类型的多点触摸驱动的时候就需要按照示例代码 64.1.3.1 中的时序上报坐 标信息。 Linux 内核里面有大量的 Type B 类型的多点触摸驱动程序,我们可以参考这些现成的 驱动程序来编写自己的驱动代码。这里就以 ili210x 这个触摸驱动 IC 为例,看看是 Type B 类型 是 如 何 上 报 触 摸 点 坐 标 信 息 的 。 找 到 ili210x.c 这 个 驱 动 文 件 , 路 径 为 drivers/input/touchscreen/ili210x.c,找到 ili210x_report_events 函数,此函数就是用于上报 ili210x 触摸坐标信息的,函数内容如下所示:
78 static void ili210x_report_events(struct input_dev *input,
79 const struct touchdata *touchdata)
80 {
81 int i;
82 bool touch;
83 unsigned int x, y;
84 const struct finger *finger;
85
86 for (i = 0; i < MAX_TOUCHES; i++) {
87 input_mt_slot(input, i);
88
89 finger = &touchdata->finger[i];
90
91 touch = touchdata->status & (1 << i);
92 input_mt_report_slot_state(input, MT_TOOL_FINGER, touch);
93 if (touch) {
94 x = finger->x_low | (finger->x_high << 8);
95 y = finger->y_low | (finger->y_high << 8);
96
97 input_report_abs(input, ABS_MT_POSITION_X, x);
98 input_report_abs(input, ABS_MT_POSITION_Y, y);
99 }
100 }
101
102 input_mt_report_pointer_emulation(input, false);
103 input_sync(input);
104 }
第 86~100 行,使用 for 循环实现上报所有的触摸点坐标,第 87 行调用 input_mt_slot 函数 上 报 ABS_MT_SLOT 事 件 。 第 92 行 调 用 input_mt_report_slot_state 函 数 上 报 ABS_MT_TRACKING_ID 事件,也就是给 SLOT 关联一个 ABS_MT_TRACKING_ID。第 97 和 98 行使用 input_report_abs 函数上报触摸点对应的(X,Y)坐标值。 第 103 行,使用 input_sync 函数上报 SYN_REPORT 事件
input_mt_init_slots 函数用于初始化 MT 的输入 slots,编写 MT 驱动的时候必须先调用此函 数初始化 slots
int input_mt_init_slots( struct input_dev *dev,
unsigned int num_slots,
unsigned int flags)
函数参数和返回值含义如下:
dev: MT 设备对应的 input_dev,因为 MT 设备隶属于 input_dev。 num_slots:设备要使用的 SLOT 数量,也就是触摸点的数量。 flags: 其他一些 flags 信息,可设置的 flags 如下所示:
#define INPUT_MT_POINTER 0x0001 /* pointer device, e.g. trackpad */
#define INPUT_MT_DIRECT 0x0002 /* direct device, e.g. touchscreen */
#define INPUT_MT_DROP_UNUSED0x0004 /* drop contacts not seen in frame */
#define INPUT_MT_TRACK 0x0008 /* use in-kernel tracking */
#define INPUT_MT_SEMI_MT 0x0010 /* semi-mt device, finger count handled manually */
可以采用‘|’运算来同时设置多个 flags 标识。 返回值: 0,成功;负值,失败。
此函数用于 Type B 类型,此函数用于产生 ABS_MT_SLOT 事件,告诉内核当前上报的是 哪个触摸点的坐标数据
void input_mt_slot(struct input_dev *dev,
int slot)
函数参数和返回值含义如下: dev: MT 设备对应的 input_dev。 slot:当前发送的是哪个 slot 的坐标信息,也就是哪个触摸点。 返回值:无。
此函数用于 Type B 类型,用于产生 ABS_MT_TRACKING_ID 和 ABS_MT_TOOL_TYPE事 件 , ABS_MT_TRACKING_ID 事 件 给 slot 关 联 一 个 ABS_MT_TRACKING_ID ,ABS_MT_TOOL_TYPE 事 件 指 定 触 摸 类 型 ( 是 笔 还 是 手 指 等 )。
void input_mt_report_slot_state( struct input_dev *dev,
unsigned int tool_type,
bool active)
函数参数和返回值含义如下: dev: MT 设备对应的 input_dev。
tool_type:触摸类型,可以选择 MT_TOOL_FINGER(手指)、 MT_TOOL_PEN(笔)或 MT_TOOL_PALM(手掌),对于多点电容触摸屏来说一般都是手指。 active: true,连续触摸, input 子系统内核会自动分配一个 ABS_MT_TRACKING_ID 给 slot。 false,触摸点抬起,表示某个触摸点无效了, input 子系统内核会分配一个-1 给 slot,表示触摸 点溢出。 返回值:无。
初始化触摸 IC、中断和 input 子系统
添加 FT5426 所使用的 IO ,FT5426 触摸芯片用到了 4 个 IO,一个复位 IO、一个中断 IO、 I2C2 的 SCL 和 SDA 。
mipi data & clk, mclk, powerdown, reset, i2c
当发出一个HSYNC信号后,电子枪就会从最右边花费HBP时长移动到最左边,等到了最右边后,等待HFP时长HSYNC信号才回来。因此,HBP和HFP分别决定了左边和右边的黑框。 同理,当发出一个VSYNC信号后,电子枪就会从最下边花费VBP时长移动到最上边,等到了最下边后,等待VFP时长VSYNC信号才回来。因此,VBP和VFP分别决定了上边和下边的黑框。 中间灰色区域才是有效显示区域。
HSYNC:行同步信号,当此信号有效的话就表示开始显示新的一行数据,查阅所使用的LCD 数据手册可以知道此信号是低电平有效还是高电平有效,假设此时是低电平有效。 HSPW: 有些地方也叫做 thp,是 HSYNC 信号宽度,也就是 HSYNC 信号持续时间。
HSYNC信号不是一个脉冲,而是需要持续一段时间才是有效的,单位为 CLK。 HBP: 有些地方叫做 thb,前面已经讲过了,术语叫做行同步信号后肩,单位是 CLK。 HOZVAL:有些地方叫做 thd,显示一行数据所需的时间,假如屏幕分辨率为 1024*600,那么 HOZVAL 就是 1024,单位为 CLK。 HFP:有些地方叫做 thf,前面已经讲过了, 术语叫做行同步信号前肩,单位是 CLK。
当 HSYNC 信号发出以后,需要等待 HSPW+HBP 个 CLK 时间才会接收到真正有效的像素数据。当显示完一行数据以后需要等待 HFP 个 CLK 时间才能发出下一个 HSYNC 信号,所以显示一行所需要的时间就是: HSPW + HBP + HOZVAL + HFP。
VSYNC:帧同步信号,当此信号有效的话就表示开始显示新的一帧数据,查阅所使用的LCD 数据手册可以知道此信号是低电平有效还是高电平有效,假设此时是低电平有效。 VSPW: 有些地方也叫做 tvp,是 VSYNC 信号宽度,也就是 VSYNC 信号持续时间,单位为 1 行的时间。 VBP: 有些地方叫做 tvb,前面已经讲过了,术语叫做帧同步信号后肩,单位为 1 行的时间。 LINE: 有些地方叫做 tvd,显示一帧有效数据所需的时间,假如屏幕分辨率为 1024*600,那么 LINE 就是 600 行的时间。 VFP: 有些地方叫做 tvf,前面已经讲过了,术语叫做帧同步信号前肩,单位为 1 行的时间。 显示一帧所需要的时间就是: VSPW+VBP+LINE+VFP 个行时间,最终的计算公式: T = (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP) 因此我们在配置一款 RGB LCD 的时候需要知道这几个参数:
HOZVAL(屏幕有效宽度)、LINE(屏幕有效高度)、 HBP、 HSPW、 HFP、 VSPW、 VBP 和 VFP。
RGB888
eLCDIF 屏幕接口,用于连接 RGB LCD 接口的屏幕, eLCDIF 接口特性如下: ①、支持 RGB LCD 的 DE 模式。
②、支持 VSYNC 模式以实现高速数据传输。
③、支持 ITU-R BT.656 格式的 4:2:2 的 YCbCr 数字视频,并且将其转换为模拟 TV 信号。 ④、支持 8/16/18/24/32 位 LCD。
eLCDIF 支持三种接口: MPU 接口、 VSYNC 接口和 DOTCLK 接口,这三种接口区别如下:
1、 MPU 接口 显示数据写入DDRAM,控制简单方便,无需时钟和同步信号,常用于静止图片显示。
2、RGB接口 它包括 VSYNC、 HSYNC、 DOTCLK和 ENABLE(可选的)这四个信号。
显示数据不写入DDRAM,直接写屏,速度快,常用于显示视频或动画用。
3、VSYNC模式:该模式是在MCU模式下增加了一根VSYNC(帧同步)信号线而已,应用于运动画面更新。
Framebuffer(帧缓冲)是Linux系统为显示设备提供的一个接口,它将显示缓冲区抽象,屏蔽图像硬件的底层差异,允许上层应用程序在图形模式下直接对显示缓冲区进行读写操作。对于帧缓冲设备而言,只要在显示缓冲区中与显示点对应的区域内写入颜色值,对应的颜色会自动在屏幕上显示。
448 struct fb_info {
449 atomic_t count;
450 int node;
451 int flags;
452 struct mutex lock; /* 互斥锁 */
453 struct mutex mm_lock; /* 互斥锁,用于 fb_mmap 和 smem_*域*/
454 struct fb_var_screeninfo var; /* 当前可变参数 */
455 struct fb_fix_screeninfo fix; /* 当前固定参数 */
456 struct fb_monspecs monspecs; /* 当前显示器特性 */
457 struct work_struct queue; /* 帧缓冲事件队列 */
458 struct fb_pixmap pixmap; /* 图像硬件映射 */
459 struct fb_pixmap sprite; /* 光标硬件映射 */
460 struct fb_cmap cmap; /* 当前调色板 */
461 struct list_head modelist; /* 当前模式列表 */
462 struct fb_videomode *mode; /* 当前视频模式 */
463
464 #ifdef CONFIG_FB_BACKLIGHT /* 如果 LCD 支持背光的话 */
465 /* assigned backlight device */
466 /* set before framebuffer registration,
467 remove after unregister */
468 struct backlight_device *bl_dev; /* 背光设备 */
469
470 /* Backlight level curve */
471 struct mutex bl_curve_mutex;
472 u8 bl_curve[FB_BACKLIGHT_LEVELS];
473 #endif
......
479 struct fb_ops *fbops; /* 帧缓冲操作函数集 */
480 struct device *device; /* 父设备 */
481 struct device *dev; /* 当前 fb 设备 */
482 int class_flag; /* 私有 sysfs 标志 */
......
486 char __iomem *screen_base; /* 虚拟内存基地址(屏幕显存) */
487 unsigned long screen_size; /* 虚拟内存大小(屏幕显存大小) */
488 void *pseudo_palette; /* 伪 16 位调色板 */
......
507 };
struct fb_var_screeninfo {
__u32 xres; /* visible resolution */ // 水平分辨率
__u32 yres; // 垂直分辨率
__u32 xres_virtual; /* virtual resolution */ // 虚拟水平分辨率
__u32 yres_virtual; // 虚拟垂直分辨率
__u32 xoffset; /* offset from virtual to visible */// 当前显存水平偏移量
__u32 yoffset; /* resolution */ // 当前显存垂直偏移量
__u32 bits_per_pixel; /* guess what */ // 像素深度
__u32 grayscale; /* != 0 Graylevels instead of colors */
struct fb_bitfield red; /* bitfield in fb mem if true color, */
struct fb_bitfield green; /* else only length is significant */
struct fb_bitfield blue;
struct fb_bitfield transp; /* transparency */
__u32 nonstd; /* != 0 Non standard pixel format */
__u32 activate; /* see FB_ACTIVATE_* */
__u32 height; /* height of picture in mm */ // LCD的物理高度mm
__u32 width; /* width of picture in mm */ // LCD的物理宽度mm
__u32 accel_flags; /* (OBSOLETE) see fb_info.flags */
/* Timing: All values in pixclocks, except pixclock (of course) */
__u32 pixclock; /* pixel clock in ps (pico seconds) */ // 像素时钟
// 下面这六个就是LCD的时序参数
__u32 left_margin; /* time from sync to picture */
__u32 right_margin; /* time from picture to sync */
__u32 upper_margin; /* time from sync to picture */
__u32 lower_margin;
__u32 hsync_len; /* length of horizontal sync */
__u32 vsync_len; /* length of vertical sync */
//////////////////////////////////////////////////////
__u32 sync; /* see FB_SYNC_* */
__u32 vmode; /* see FB_VMODE_* */
__u32 rotate; /* angle we rotate counter clockwise */
__u32 reserved[5]; /* Reserved for future compatibility */
};
struct fb_fix_screeninfo {
char id[16]; /* identification string eg "TT Builtin" */
unsigned long smem_start; /* Start of frame buffer mem */ // LCD显存的起始地址(物理地址)
/* (physical address) */
__u32 smem_len; /* Length of frame buffer mem */ // LCD显存的字节大小
__u32 type; /* see FB_TYPE_* */ // 类型
__u32 type_aux; /* Interleave for interleaved Planes */
__u32 visual; /* see FB_VISUAL_* */
__u16 xpanstep; /* zero if no hardware panning */
__u16 ypanstep; /* zero if no hardware panning */
__u16 ywrapstep; /* zero if no hardware ywrap */
__u32 line_length; /* length of a line in bytes */ // LCD一行的长度 (以字节为单位)
unsigned long mmio_start; /* Start of Memory Mapped I/O */
/* (physical address) */
__u32 mmio_len; /* Length of Memory Mapped I/O */
__u32 accel; /* Indicate to driver which */
/* specific chip/card we have */
__u16 reserved[3]; /* Reserved for future compatibility */
};
fb_info 结构体的成员变量很多,我们重点关注 var、 fix、 fbops、 screen_base、 screen_size 和 pseudo_palette。 mxsfb_probe 函数的主要工作内容为: ①、申请 fb_info。 ②、初始化 fb_info 结构体中的各个成员变量。 ③、初始化 eLCDIF 控制器。 ④、使用 register_framebuffer 函数向 Linux 内核注册初始化好的 fb_info。register_framebuffer 函数原型如下:
int register_framebuffer(struct fb_info *fb_info)
函数参数和返回值含义如下: fb_info:需要上报的 fb_info。 返回值: 0,成功;负值,失败。
从设备树中获取寄存器首地址(res)进行内存映射,得到虚拟地址 。
填充fb_info结构体,如 var、 fix、 fbops、 screen_base、 screen_size。
fbops主要是实现open、release 、mmap 等函数。
总线宽度、时序信息
24根数据线
4 根控制线 ,包括 CLK、ENABLE、 VSYNC 和 HSYNC
背光 PWM 引脚
像素时钟
X 轴像素个数 、Y 轴像素个数
hfp 参数 、hbp 参数 、hspw 参数 、vbp 参数 、vfp 参数 、vspw 参数
优点
1):支持全双工通信
2):通信简单
3):数据传输速率快
缺点 没有指定的流控制,没有应答机制确认是否接收到数据,所以跟IIC总线协议比较在数据可靠性上有一定的缺陷。
SPI 规定了两个 SPI 设备之间通信必须由主设备 (Master) 来控制次设备 (Slave)。 一个 Master 设备可以通过提供 Clock 以及对 Slave 设备进行片选 (Slave Select) 来控制多个 Slave 设备,SPI 协议还规定 Slave 设备的 Clock 由 Master 设备通过 SCK 管脚提供给 Slave 设备, Slave 设备本身不能产生或控制 Clock,没有 Clock 则 Slave 设备不能正常工作。
Master 设备会根据将要交换的数据来产生相应的时钟脉冲(Clock Pulse),时钟脉冲组成了时钟信号(Clock Signal) ,时钟信号通过时钟极性 (CPOL) 和 时钟相位 (CPHA) 控制着两个 SPI 设备间何时数据交换以及何时对接收到的数据进行采样,来保证数据在两个设备之间是同步传输的。
SPI 设备间的数据传输之所以又被称为数据交换,是因为 SPI 协议规定一个 SPI 设备不能在数据通信过程中仅仅只充当一个 "发送者(Transmitter)" 或者 "接收者(Receiver)"。在每个 Clock 周期内,SPI 设备都会发送并接收一个 bit 大小的数据(不管主设备好还是从设备),相当于该设备有一个 bit 大小的数据被交换了。一个 Slave 设备要想能够接收到 Master 发过来的控制信号,必须在此之前能够被 Master 设备进行访问 (Access)。所以,Master 设备必须首先通过 SS/CS pin 对 Slave 设备进行片选, 把想要访问的 Slave 设备选上。 在数据传输的过程中,每次接收到的数据必须在下一次数据传输之前被采样。如果之前接收到的数据没有被读取,那么这些已经接收完成的数据将有可能会被丢弃,导致 SPI 物理模块最终失效。因此,在程序中一般都会在 SPI 传输完数据后,去读取 SPI 设备里的数据, 即使这些数据(Dummy Data)在我们的程序里是无用的(虽然发送后紧接着的读取是无意义的,但仍然需要从寄存器中读出来)。
SPI没有读和写的说法,因为实质上每次SPI是主从设备在交换数据。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。
SPI总线定义两个及以上设备间的数据传输,提供时钟的设备为主设备(Master),接收时钟的设备为从设备(Slave)。下图为单个Master与单个Slave的SPI连接:
SPI协议定义四根信号线,分别为:
SCK : Serial Clock 串行时钟 MOSI : Master Output, Slave Input 主发从收信号 MISO : Master Input, Slave Output 主收从发信号 SS : Slave Select 片选信号 其中MISO方向为从设备到主设备,其余三个信号均为主设备到从设备。
注意:
对于主设备,如果设置SS作为从设备的片选信号(最常用的场合),则它就不能用于多设备应用的模式错误检测 SPI单个数据管脚支持双向模式。在双向模式下,主设备的MOSI,从设备的MISO作为双向IO。
每个从设备都需要单独的片选信号,主设备每次只能选择其中一个从设备进行通信。因为所有从设备的SCK、MOSI、MISO都是连在一起的,未被选中从设备的MISO要表现为高阻状态(Hi-Z)以避免数据传输错误。由于每个设备都需要单独的片选信号,如果需要的片选信号过多,可以使用译码器产生所有的片选信号。
SPI 有四种工作模式,通过串行时钟极性(CPOL)和相位(CPHA)的搭配来得到四种工作模式: ①、 CPOL=0,串行时钟空闲状态为低电平。 ②、 CPOL=1,串行时钟空闲状态为高电平,此时可以通过配置时钟相位(CPHA)来选择具 体的传输协议。 ③、 CPHA=0,串行时钟的第一个跳变沿(上升沿或下降沿)采集数据。 ④、 CPHA=1,串行时钟的第二个跳变沿(上升沿或下降沿)采集数据。
CS 片选信号先拉低,选中要通信的从设备,然后通过 MOSI 和 MISO 这两根数据线进行收发数据, MOSI 数据线发出了0XD2 这个数据给从设备,同时从设备也通过 MISO 线给主设备返回了 0X66 这个数据。这个就是 SPI 时序图。
180 struct spi_driver {
181 const struct spi_device_id *id_table;
182 int (*probe)(struct spi_device *spi);
183 int (*remove)(struct spi_device *spi);
184 void (*shutdown)(struct spi_device *spi);
185 struct device_driver driver;
186 };
int spi_register_driver(struct spi_driver *sdrv)
函数参数和返回值含义如下: sdrv: 要注册的 spi_driver。 返回值: 0,注册成功;赋值,注册失败。
void spi_unregister_driver(struct spi_driver *sdrv)
函数参数和返回值含义如下: sdrv: 要注销的 spi_driver。
返回值: 无。
603 struct spi_transfer {
604 /* it's ok if tx_buf == rx_buf (right?)
605 * for MicroWire, one buffer must be null
606 * buffers must work with dma_*map_single() calls, unless
607 * spi_message.is_dma_mapped reports a pre-existing mapping
608 */
609 const void *tx_buf;
610 void *rx_buf;
611 unsigned len;
612
613 dma_addr_t tx_dma;
614 dma_addr_t rx_dma;
615 struct sg_table tx_sg;
616 struct sg_table rx_sg;
617
618 unsigned cs_change:1;
619 unsigned tx_nbits:3;
620 unsigned rx_nbits:3;
621 #define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */
622 #define SPI_NBITS_DUAL 0x02 /* 2bits transfer */
623 #define SPI_NBITS_QUAD 0x04 /* 4bits transfer */
624 u8 bits_per_word;
625 u16 delay_usecs;
626 u32 speed_hz;
627
628 struct list_head transfer_list;
629 };
第 609 行, tx_buf 保存着要发送的数据。 第 610 行, rx_buf 用于保存接收到的数据。 第 611 行, len 是要进行传输的数据长度, SPI 是全双工通信,因此在一次通信中发送和接收的字节数都是一样的,所以 spi_transfer 中也就没有发送长度和接收长度之分。 spi_transfer 需要组织成 spi_message, spi_message 也是一个结构体,内容如下:
660 struct spi_message {
661 struct list_head transfers;
662
663 struct spi_device *spi;
664
665 unsigned is_dma_mapped:1;
......
678 /* completion is reported through a callback */
679 void (*complete)(void *context);
680 void *context;
681 unsigned frame_length;
682 unsigned actual_length;
683 int status;
684
685 /* for optional use by whatever driver currently owns the
686 * spi_message ... between calls to spi_async and then later
687 * complete(), that's the spi_master controller driver.
688 */
689 struct list_head queue;
690 void *state;
691 };
在使用spi_message之前需要对其进行初始化, spi_message初始化函数为spi_message_init, 函数原型如下:
void spi_message_init(struct spi_message *m)
函数参数和返回值含义如下: m: 要初始化的 spi_message。 返回值: 无。 spi_message 初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中,这里我们要用 到 spi_message_add_tail 函数,此函数原型如下:
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
函数参数和返回值含义如下: t: 要添加到队列中的 spi_transfer。 m: spi_transfer 要加入的 spi_message。 返回值: 无。 spi_message 准备好以后既可以进行数据传输了,数据传输分为同步传输和异步传输,同步 传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync,函数原型如下:
int spi_sync(struct spi_device *spi, struct spi_message *message)
函数参数和返回值含义如下: spi: 要进行数据传输的 spi_device。 message:要传输的 spi_message。 返回值: 无。 异步传输不会阻塞的等到 SPI 数据传输完成,异步传输需要设置 spi_message 中的 complete 成员变量, complete 是一个回调函数,当 SPI 异步传输完成以后此函数就会被调用。 SPI 异步传 输函数为 spi_async,函数原型如下:
int spi_async(struct spi_device *spi, struct spi_message *message)
函数参数和返回值含义如下: spi: 要进行数据传输的 spi_device。 message:要传输的 spi_message。 返回值: 无。 在本章实验中,我们采用同步传输方式来完成 SPI 数据的传输工作,也就是 spi_sync 函数。 综上所述, SPI 数据传输步骤如下:
①、申请并初始化 spi_transfer,设置 spi_transfer 的 tx_buf 成员变量, tx_buf 为要发送的数 据。然后设置 rx_buf 成员变量, rx_buf 保存着接收到的数据。最后设置 len 成员变量,也就是 要进行数据通信的长度。 ②、使用 spi_message_init 函数初始化 spi_message。 ③、使用spi_message_add_tail函数将前面设置好的spi_transfer添加到spi_message队列中。 ④、使用 spi_sync 函数完成 SPI 数据同步传输。
字符设备与块设备I/O操作的不同如下。
1)块设备只能以块为单位接收输入和返回输出,而字符设备则以字节为单位。大多数设备是字符设备,因为它们不需要缓冲而且不以固定块大小进行操作。 2)块设备对于I/O请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无须缓冲且被直接读写。对于存储设备而言,调整读写的顺序作用巨大,因为在读写连续的扇区的存储速度比分离的扇区更快。 3)字符设备只能被顺序读写,而块设备可以随机访问。 虽然块设备可随机访问,但是对于磁盘这类机械设备而言,顺序地组织块设备的访问可以提高性能,如图13.1所示,对扇区1、10、3、2的请求被调整为对扇区1、2、3、10的请求。
int register_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下: major: 主设备号。 name: 块设备名字。 返回值: 如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1~255),如果返回负值那就表示注册失败。
void unregister_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下: major: 要注销的块设备主设备号。 name: 要注销的块设备名字。 返回值: 无。
1 struct block_device_operations {
2 int (*open) (struct block_device *, fmode_t);
3 void (*release) (struct gendisk *, fmode_t);
4 int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
5 int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
6 int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
7 int (*direct_access) (struct block_device *, sector_t,
8 void **, unsigned long *);
9 unsigned int (*check_events) (struct gendisk *disk,
10 unsigned int clearing);
11 /* ->media_changed() is DEPRECATED, use ->check_events() instead */
12 int (*media_changed) (struct gendisk *);
13 void (*unlock_native_capacity) (struct gendisk *);
14 int (*revalidate_disk) (struct gendisk *);
15 int (*getgeo)(struct block_device *, struct hd_geometry *);
16 /* this callback is with swap_lock and sometimes page table lock held */
17 void (*swap_slot_free_notify) (struct block_device *, unsigned long);
18 struct module *owner;
19 };
int (*open) (struct block_device *, fmode_t);
void (*release) (struct gendisk *, fmode_t);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
在Linux内核中,使用gendisk(通用磁盘)结构体来表示一个独立的磁盘设备(或分区)
1 struct gendisk {
2 /* major, first_minor and minors are input parameters only,
3 * don't use directly. Use disk_devt() and disk_max_parts().
4 */
5 int major; /* major number of driver */
6 int first_minor;
7 int minors; /* maximum number of minors, =1 for
8 * disks that can't be partitioned. */
9
10 char disk_name[DISK_NAME_LEN]; /* name of major driver */
11 char *(*devnode)(struct gendisk *gd, umode_t *mode);
12
13 unsigned int events; /* supported events */
14 unsigned int async_events; /* async events, subset of all */
15
16 /* Array of pointers to partitions indexed by partno.
17 * Protected with matching bdev lock but stat and other
18 * non-critical accesses use RCU. Always access through
19 * helpers.
20 */
21 struct disk_part_tbl __rcu *part_tbl;
22 struct hd_struct part0;
23
24 const struct block_device_operations *fops;
25 struct request_queue *queue;
26 void *private_data;
27
28 int flags;
29 struct device *driverfs_dev; // FIXME: remove
30 struct kobject *slave_dir;
31
32 struct timer_rand_state *random;
33 atomic_t sync_io; /* RAID */
34 struct disk_events *ev;
35 #ifdef CONFIG_BLK_DEV_INTEGRITY
36 struct blk_integrity *integrity;
37 #endif
38 int node_id;
39 };
major、first_minor和minors共同表征了磁盘的主、次设备号,同一个磁盘的各个分区共享一个主设备号,而次设备号则不同。fops为block_device_operations,即上节描述的块设备操作集合。queue是内核用来管理这个设备的I/O请求队列的指针。private_data可用于指向磁盘的任何私有数据,用法与字符设备驱动file结构体的private_data类似。hd_struct成员表示一个分区,而disk_part_tbl成员用于容纳分区表,part0和part_tbl两者的关系在于:
disk->part_tbl->part[0] = &disk->part0;
gendisk结构体是一个动态分配的结构体,它需要特别的内核操作来初始化,驱动不能自己分配这个结构体,而应该使用下列函数来分配gendisk:
struct gendisk *alloc_disk(int minors);
minors参数是这个磁盘使用的次设备号的数量,一般也就是磁盘分区的数量,此后minors不能被修改。
gendisk结构体被分配之后,系统还不能使用这个磁盘,需要调用如下函数来注册这个磁盘设备。
void add_disk(struct gendisk *disk);
当不再需要磁盘时,应当使用如下函数释放gendisk。
void del_gendisk(struct gendisk *gp);
每一个磁盘都有容量,所以在初始化 gendisk 的时候也需要设置其容量,使用函数set_capacity,函数原型如下:
void set_capacity(struct gendisk *disk, sector_t size)
函数参数和返回值含义如下: disk: 要设置容量的 gendisk。 size: 磁盘容量大小,注意这里是扇区数量。块设备中最小的可寻址单元是扇区,一个扇区一般是 512 字节,有些设备的物理扇区可能不是 512 字节。不管物理扇区是多少,内核和块设备驱动之间的扇区都是 512 字节。所以 set_capacity 函数设置的大小就是块设备实际容量除以512 字节得到的扇区数量。比如一个 2MB 的磁盘,其扇区数量就是(210241024)/512=4096。 返回值: 无。
通常一个bio对应上层传递给块层的I/O请求。每个bio结构体实例及其包含的bvec_iter、bio_vec结构体实例描述了该I/O请求的开始扇区、数据方向(读还是写)、数据放入的页
1 struct bvec_iter {
2 sector_t bi_sector; /* device address in 512 byte
3 sectors */
4 unsigned int bi_size; /* residual I/O count */
5
6 unsigned int bi_idx; /* current index into bvl_vec */
7
8 unsigned int bi_bvec_done; /* number of bytes completed
9 in current bvec */
10 };
11
12 /*
13 * main unit of I/O for the block layer and lower layers (ie drivers and
14 * stacking drivers)
15 */
16 struct bio {
17 struct bio *bi_next; /* request queue link */
18 struct block_device *bi_bdev;
19 unsigned long bi_flags; /* status, command, etc */
20 unsigned long bi_rw; /* bottom bits READ/WRITE,
21 * top bits priority
22 */
23
24 struct bvec_iter bi_iter;
25
26 /* Number of segments in this BIO after
27 * physical address coalescing is performed.
28 */
29 unsigned int bi_phys_segments;
30
31 ...
32
33 struct bio_vec *bi_io_vec; /* the actual vec list */
34
35 struct bio_set *bi_pool;
36
37 /*
38 * We can inline a number of vecs at the end of the bio, to avoid
39 * double allocations for a small number of bio_vecs. This member
40 * MUST obviously be kept at the very end of the bio.
41 */
42 struct bio_vec bi_inline_vecs[0];
43 };
与bio对应的数据每次存放的内存不一定是连续的,bio_vec结构体用来描述与这个bio请求对应的所有的内存,它可能不总是在一个页面里面,因此需要一个向量,定义如代码清单13.4所示。向量中的每个元素实际是一个[page,offset,len],我们一般也称它为一个片段。
1 struct bio_vec {
2 struct page *bv_page;
3 unsigned int bv_len;
4 unsigned int bv_offset;
5 };
I/O调度算法可将连续的bio合并成一个请求。请求是bio经由I/O调度进行调整后的结果,这是请求和bio的区别。因此,一个request可以包含多个bio。当bio被提交给I/O调度器时,I/O调度器可能会将这个bio插入现存的请求中,也可能生成新的请求。 每个块设备或者块设备的分区都对应有自身的request_queue,从I/O调度器合并和排序出来的请求会被分发(Dispatch)到设备级的request_queue。下图描述了request_queue、request、bio、bio_vec之间的关系。
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
该函数的第一个参数是请求处理函数的指针,第二个参数是控制访问队列权限的自旋锁,这个函数会发生内存分配的行为,它可能会失败,因此一定要检查它的返回值。这个函数一般在块设备驱动的初始化过程中调用。
void blk_cleanup_queue(request_queue * q);
这个函数完成将请求队列返回给系统的任务,一般在块设备驱动卸载过程中调用。
request_queue *blk_alloc_queue(int gfp_mask);
对于RAMDISK这种完全随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,可以直接“踢开”I/O调度器,使用如下函数来绑定请求队列和“制造请求”函数(make_request_fn)。
void blk_queue_make_request(request_queue * q, make_request_fn * mfn);
blk_alloc_queue()和blk_queue_make_request()结合起来使用的逻辑一般是:
xxx_queue = blk_alloc_queue(GFP_KERNEL);
blk_queue_make_request(xxx_queue, xxx_make_request);
struct request * blk_peek_request(struct request_queue *q);
上述函数用于返回下一个要处理的请求(由I/O调度器决定),如果没有请求则返回NULL。它不会清除请求,而是仍然将这个请求保留在队列上。原先的老的函数elv_next_request()已经不再存在。
void blk_start_request(struct request *req);
我们也可以使用 blk_fetch_request 函数来一次性完成请求的获取和开启
#define __rq_for_each_bio(_bio, rq) \
if ((rq->bio)) \
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
__rq_for_each_bio()遍历一个请求的所有bio。
#define __bio_for_each_segment(bvl, bio, iter, start) \
for (iter = (start); \
(iter).bi_size && \
((bvl = bio_iter_iovec((bio), (iter))), 1); \
bio_advance_iter((bio), &(iter), (bvl).bv_len))
#define bio_for_each_segment(bvl, bio, iter) \
__bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
rq_for_each_segment()迭代遍历一个请求所有bio中的所有segment。
#define rq_for_each_segment(bvl, _rq, _iter) \
__rq_for_each_bio(_iter.bio, _rq) \
bio_for_each_segment(bvl, _iter.bio, _iter.iter)
void __blk_end_request_all(struct request *rq, int error);
void blk_end_request_all(struct request *rq, int error);
上述两个函数用于报告请求是否完成,error为0表示成功,小于0表示失败。__blk_end_request_all()需要在持有队列锁的场景下调用。
若我们用blk_queue_make_request()绕开I/O调度,但是在bio处理完成后应该使用bio_endio()函数通知处理结束:
void bio_endio(struct bio *bio, int error);
如果是I/O操作故障,可以调用快捷函数bio_io_error(),它定义为:
#def ine bio_io_error(bio) bio_endio((bio), -EIO)
从上到下可以划分为4层,依次为网络协议接口层、网络设备接口层、提供实际功能的设备驱动功能层以及网络设备与媒介层,这4层的作用如下所示。
1)网络协议接口层向网络层协议提供统一的数据包收发接口,不论上层协议是ARP,还是IP,都通过dev_queue_xmit()函数发送数据,并通过netif_rx()函数接收数据。这一层的存在使得上层协议独立于具体的设备。 2)网络设备接口层向协议接口层提供统一的用于描述具体网络设备属性和操作的结构体net_device,该结构体是设备驱动功能层中各函数的容器。实际上,网络设备接口层从宏观上规划了具体操作硬件的设备驱动功能层的结构。
3)设备驱动功能层的各函数是网络设备接口层net_device数据结构的具体成员,是驱使网络设备硬件完成相应动作的程序,它通过hard_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接收操作。 4)网络设备与媒介层是完成数据包发送和接收的物理实体,包括网络适配器和具体的传输媒介,网络适配器被设备驱动功能层中的函数在物理上驱动。对于Linux系统而言,网络设备和媒介都可以是虚拟的。
在设计具体的网络设备驱动程序时,我们需要完成的主要工作是编写设备驱动功能层的相关函数以填充net_device数据结构的内容并将net_device注册入内核。
• cfg80211: 用于对无线设备进行配置管理。与FullMAC, mac80211和nl80211一起工作。(Kernel态)
• mac80211: 是一个driver开发者可用于为SoftMAC无线设备写驱动的框架 (Kernel态)。
• nl80211: 用于对无线设备进行配置管理,它是一个基本Netlink的用户态协议(User态)
• WNIC : Wireless Network Interface Controller, 它总是指望硬件执行协议(如IEEE802.11)描述的功能。
• MLME: 即MAC(Media Access Control ) Layer Management Entity,它管理物理层MAC状态机。
• SoftMAC: 其MLME由软件实现,mac80211为SoftMAC实现提供了一个driver API。 即:SoftMAC设备允许对硬件执行更好地控制,允许用软件实现对802.11的帧管理,包括解析和产生802.11无线帧。目前大多数802.11设备为SoftMAC,而FullMAC设备较少。
• FullMAC: 其MLME由硬件管理,当写FullMAC无线驱动时,不需要使用mac80211。
• wpa_supplicant: 是用户空间一个应用程序,主要发起MLME命令,然后处理相关结果。
cfg80211是Linux 802.11配置API。cfg80211用于代码wext(Wireless-Extensions),nl80211用于配置一个cfg80211设备,且用于kernel与userspace间的通信。wext现处理维护状态,没有新的功能被增加,只是修改bug。如果需要通过wext操作,则需要定义CONFIG_CFG80211_WEXT。
cfg80211 and nl80211: 基于消息机制,使用netlink接口
wext: 基于ioctl机制
• struct ieee80211_hw: 表示硬件信息和状态
• ieee80211_alloc_hw:每个driver调用ieee80211_alloc_hw分配ieee80211_hw,且以ieee80211_ops为参数
• ieee80211_register_hw:每个driver调用ieee80211_register_hw创建wlan0和 wmaster0,并进行各种初始化。
• struct ieee80211_ops:每个driver实现它的成员函数,且它的成员函数都以struct ieee80211_hw做为第一个参数。在struct ieee80211_ops中定义了24个方法,以下7个方法必须实现: tx,start,stop,add_interface,remove_interface,config和configure_filter。
它是一个driver开发者可用于为SoftMAC无线设备写驱动的框架,mac80211为SoftMAC设备实现了cfg80211回调函数,且mac80211通过cfg80211实现了向网络子系统注册和配置。配置由cfg80211通过nl80211和wext实现。
mac80211在体系结构中的位置如下图所示:
网络协议接口层最主要的功能是给上层协议提供透明的数据包发送和接收接口。当上层ARP或IP需要发送数据包时,它将调用网络协议接口层的dev_queue_xmit()函数发送该数据包,同时需传递给该函数一个指向struct sk_buff数据结构的指针。dev_queue_xmit()函数的原型为:
int dev_queue_xmit(struct sk_buff *skb);
同样地,上层对数据包的接收也通过向netif_rx()函数传递一个struct sk_buff数据结构的指针来完成。netif_rx()函数的原型为:
int netif_rx(struct sk_buff *skb);
sk_buff结构体非常重要,它定义于include/linux/skbuff.h文件中,含义为“套接字缓冲区”,用于在Linux网络子系统中的各层之间传递数据,是Linux网络子系统数据传递的“中枢神经”。
当发送数据包时,Linux内核的网络处理模块必须建立一个包含要传输的数据包的sk_buff,然后将sk_buff递交给下层,各层在sk_buff中添加不同的协议头直至交给网络设备发送。同样地,当网络设备从网络媒介上接收到数据包后,它必须将接收到的数据转换为sk_buff数据结构并传递给上层,各层剥去相应的协议头直至交给用户。
1 /**
2 * struct sk_buff - socket buffer
3 * @next: Next buffer in list
4 * @prev: Previous buffer in list
5 * @len: Length of actual data
6 * @data_len: Data length
7 * @mac_len: Length of link layer header
8 * @hdr_len: writable header length of cloned skb
9 * @csum: Checksum (must include start/offset pair)
10 * @csum_start: Offset from skb->head where checksumming should start
11 * @csum_offset: Offset from csum_start where checksum should be stored
12 * @priority: Packet queueing priority
13 * @protocol: Packet protocol from driver
14 * @inner_protocol: Protocol (encapsulation)
15 * @inner_transport_header: Inner transport layer header (encapsulation)
16 * @inner_network_header: Network layer header (encapsulation)
17 * @inner_mac_header: Link layer header (encapsulation)
18 * @transport_header: Transport layer header
19 * @network_header: Network layer header
20 * @mac_header: Link layer header
21 * @tail: Tail pointer
22 * @end: End pointer
23 * @head: Head of buffer
24 * @data: Data head pointer
25 */
26
27 struct sk_buff {
28 /* These two members must be first. */
29 struct sk_buff *next;
30 struct sk_buff *prev;
31
32 ...
33 unsigned int len,
34 data_len;
35 __u16 mac_len,
36 hdr_len;
37 ...
38 __u32 priority;
39 ...
40 __be16 protocol;
41
42 ...
43
44 __be16 inner_protocol;
45 __u16 inner_transport_header;
46 __u16 inner_network_header;
47 __u16 inner_mac_header;
48 __u16 transport_header;
49 __u16 network_header;
50 __u16 mac_header;
51 /* These elements must be at the end, see alloc_skb() for details. */
52 sk_buff_data_t tail;
53 sk_buff_data_t end;
54 unsigned char *head,
55 *data;
56 ...
57 };
尤其值得注意的是head和end指向缓冲区的头部和尾部,而data和tail指向实际数据的头部和尾部。每一层会在head和data之间填充协议头,或者在tail和end之间添加新的协议数据。.
下面我们来分析套接字缓冲区涉及的操作函数,Linux套接字缓冲区支持分配、释放、变更等功能函数。
Linux内核中用于分配套接字缓冲区的函数有:
struct sk_buff *alloc_skb(unsigned int len, gfp_t priority);
struct sk_buff *dev_alloc_skb(unsigned int len);
alloc_skb()函数分配一个套接字缓冲区和一个数据缓冲区,参数len为数据缓冲区的空间大小,通常以L1_CACHE_BYTES字节(对于ARM为32)对齐,参数priority为内存分配的优先级。dev_alloc_skb()函数以GFP_ATOMIC优先级进行skb的分配,原因是该函数经常在设备驱动的接收中断里被调用。
void kfree_skb(struct sk_buff *skb);
void dev_kfree_skb(struct sk_buff *skb);
void dev_kfree_skb_irq(struct sk_buff *skb);
void dev_kfree_skb_any(struct sk_buff *skb);
在Linux内核中可以用如下函数在缓冲区尾部增加数据:
unsigned char *skb_put(struct sk_buff *skb, unsigned int len);
它会导致skb->tail后移len(skb->tail+=len),而skb->len会增加len的大小(skb->len+=len)。通常,在设备驱动的接收数据处理中会调用此函数。 在Linux内核中可以用如下函数在缓冲区开头增加数据:
unsigned char *skb_push(struct sk_buff *skb, unsigned int len);
它会导致skb->data前移len(skb->data-=len),而skb->len会增加len的大小(skb->len+=len)。与该函数的功能完成相反的函数是skb_pull(),它可以在缓冲区开头移除数据,执行的动作是skb->len-=len、skb->data+=len。 对于一个空的缓冲区而言,调用如下函数可以调整缓冲区的头部:
static inline void skb_reserve(struct sk_buff *skb, int len);
它会将skb->data和skb->tail同时后移len,执行skb->data+=len、skb->tail+=len。内核里存在许多这样的代码:
skb=alloc_skb(len+headspace, GFP_KERNEL);
skb_reserve(skb, headspace);
skb_put(skb,len);
memcpy_fromfs(skb->data,data,len);
pass_to_m_protocol(skb);
上述代码先分配一个全新的sk_buff,接着调用skb_reserve()腾出头部空间,之后调用skb_put()腾出数据空间,然后把数据复制进来,最后把sk_buff传给协议栈。
网络设备接口层的主要功能是为千变万化的网络设备定义统一、抽象的数据结构net_device结构体,以不变应万变,实现多种硬件在软件层次上的统一。 net_device结构体在内核中指代一个网络设备,它定义于include/linux/netdevice.h文件中,网络设备驱动程序只需通过填充net_device的具体成员并注册net_device即可实现硬件操作函数与内核的挂接。
char name[IFNAMESIZ];
name是网络设备的名称。
unsigned long mem_end;
unsigned long mem_start;
mem_start和mem_end分别定义了设备所使用的共享内存的起始和结束地址。
unsigned long base_addr;
unsigned char irq;
unsigned char if_port;
unsigned char dma;
base_addr为网络设备I/O基地址。 irq为设备使用的中断号。
unsigned short hard_header_len;
hard_header_len是网络设备的硬件头长度,在以太网设备的初始化函数中,该成员被赋为ETH_HLEN,即14。
unsigned short type;
type是接口的硬件类型。
unsigned mtu;
mtu指最大传输单元(MTU)。
unsigned char *dev_addr;
用于存放设备的硬件地址,驱动可能提供了设置MAC地址的接口,这会导致用户设置的MAC地址等存入该成员
unsigned short flags;
flags指网络接口标志,以IFF_(Interface Flags)开头,部分标志由内核来管理,其他的在接口初始化时被设置以说明设备接口的能力和特性。接口标志包括IFF_UP(当设备被激活并可以开始发送数据包时,内核设置该标志)、IFF_AUTOMEDIA(设备可在多种媒介间切换)、IFF_BROADCAST(允许广播)、IFF_DEBUG(调试模式,可用于控制printk调用的详细程度)、IFF_LOOPBACK(回环)、IFF_MULTICAST(允许组播)、IFF_NOARP(接口不能执行ARP)和IFF_POINTOPOINT(接口连接到点到点链路)等。
const struct net_device_ops *netdev_ops;
该结构体是网络设备的一系列硬件操作行数的集合
1 struct net_device_ops {
2 int (*ndo_init)(struct net_device *dev);
3 void (*ndo_uninit)(struct net_device *dev);
4 int (*ndo_open)(struct net_device *dev);
5 int (*ndo_stop)(struct net_device *dev);
6 netdev_tx_t (*ndo_start_xmit) (struct sk_buff *skb,
7 struct net_device *dev);
8 u16 (*ndo_select_queue)(struct net_device *dev,
9 struct sk_buff *skb,
10 void *accel_priv,
11 select_queue_fallback_t fallback);
12 void (*ndo_change_rx_flags)(struct net_device *dev,
13 int flags);
14 void (*ndo_set_rx_mode)(struct net_device *dev);
15 int (*ndo_set_mac_address)(struct net_device *dev,
16 void *addr);
17 int (*ndo_validate_addr)(struct net_device *dev);
18 int (*ndo_do_ioctl)(struct net_device *dev,
19 struct ifreq *ifr, int cmd);
20 ...
21 };
ndo_open()函数的作用是打开网络接口设备,获得设备需要的I/O地址、IRQ、DMA通道等。stop()函数的作用是停止网络接口设备,与open()函数的作用相反。
int (*ndo_start_xmit) (struct sk_buff *skb,struct net_device *dev);
ndo_start_xmit()函数会启动数据包的发送,当系统调用驱动程序的xmit函数时,需要向其传入一个sk_buff结构体指针,以使得驱动程序能获取从上层传递下来的数据包。
void (*ndo_tx_timeout)(struct net_device *dev);
当数据包的发送超时时,ndo_tx_timeout()函数会被调用,该函数需采取重新启动数据包发送过程或重新启动硬件等措施来恢复网络设备到正常状态。
struct net_device_stats* (*ndo_get_stats)(struct net_device *dev);
ndo_get_stats()函数用于获得网络设备的状态信息,它返回一个net_device_stats结构体指针。net_device_stats结构体保存了详细的网络设备流量统计信息,如发送和接收的数据包数、字节数等
int (*ndo_do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);
int (*ndo_set_config)(struct net_device *dev, struct ifmap *map);
int (*ndo_set_mac_address)(struct net_device *dev, void *addr);
ndo_do_ioctl()函数用于进行设备特定的I/O控制。 ndo_set_config()函数用于配置接口,也可用于改变设备的I/O地址和中断号。 ndo_set_mac_address()函数用于设置设备的MAC地址。
const struct ethtool_ops *ethtool_ops;
const struct header_ops *header_ops;
ethtool_ops成员函数与用户空间ethtool工具的各个命令选项对应,ethtool提供了网卡及网卡驱动管理能力,能够为Linux网络开发人员和管理人员提供对网卡硬件、驱动程序和网络协议栈的设置、查看以及调试等功能。 header_ops对应于硬件头部操作,主要是完成创建硬件头部和从给定的sk_buff分析出硬件头部等操作。
net_device结构体的成员(属性和net_device_ops结构体中的函数指针)需要被设备驱动功能层赋予具体的数值和函数。对于具体的设备xxx,工程师应该编写相应的设备驱动功能层的函数,这些函数形如xxx_open()、xxx_stop()、xxx_tx()、xxx_hard_header()、xxx_get_stats()和xxx_tx_timeout()等。 由于网络数据包的接收可由中断引发,设备驱动功能层中的另一个主体部分将是中断处理函数,它负责读取硬件上接收到的数据包并传送给上层协议,因此可能包含xxx_interrupt()和xxx_rx()函数,前者完成中断类型判断等基本工作,后者则需完成数据包的生成及将其递交给上层等复杂工作。
int register_netdev(struct net_device *dev);
void unregister_netdev(struct net_device *dev);
IROM (Internal ROM):固化在CPU内部ROM里的一段代码,它的运行叫做BL0. IRAM: 因为IROM启动运行的时候,外置SDRAM还没有初始化好,而IRAM是可用的,因此必须要把BL1加载到IRAM中运行,由BL1对SDRAM进行初始化。ROM为什么不初始化SDRAM呢?那是因为支持的SDRAM规格是可变的,由固化代码来初始化显得不够灵活,而且固化代码往往代码量比较小,因为越多越容易出BUG,出BUG就会导致SOC芯片重新掩膜tapout,一次可要好几百万人民币呢。 BL0: (BootLoad 0阶段),BL0做了些什么? s5pc100芯片手册见2.2FUNCTIONAL SEQUENCE,翻译成中文如下 1.初始化PLL和时钟,将其设定为固定值; 2.初始化栈和堆区域; 3.初始化指令Cache 控制器; 4.从外部起动设备中加载BL1; 5.如果起动安全机制开启,则检查BL1数据完整性; 6.如果校验通过,则跳转到0x34010地址处运行; 7.如果校验失败则停止。 BL1: 从CPU上电起,把系统启动过程分为3个阶段BL0、BL1、BL2。BL0是固化在内部ROM上电就执行的一小段程序,BL0引导u-boot的第一个阶段称为BL1。通常加载到CPU 内的 IRAM中执行 BL2: 把u-boot的第二阶段代码用于引导内核的阶段称为BL2,通常加载到SDRAM中执行。
初始化 RAM
因为 Linux 内核一般都会在 RAM 中运行,所以在调用 Linux 内核之前 bootloader 必须设置和初始化 RAM,为调用 Linux内核做好准备。初始化 RAM 的任务包括设置 CPU 的控制寄存器参数,以便能正常使用 RAM 以及检测RAM 大小等。
初始化串口
串口在 Linux 的启动过程中有着非常重要的作用,它是 Linux内核和用户交互的方式之一。Linux 在启动过程中可以将信息通过串口输出,这样便可清楚的了解 Linux 的启动过程。虽然它并不是 bootloader 必须要完成的工作,但是通过串口输出信息是调试 bootloader 和Linux 内核的强有力的工具,所以一般的 bootloader 都会在执行过程中初始化一个串口做为调试端口。
检测处理器类型
Bootloader在调用 Linux内核前必须检测系统的处理器类型,并将其保存到某个常量中提供给 Linux 内核。Linux 内核在启动过程中会根据该处理器类型调用相应的初始化程序。
设置 Linux启动参数
Bootloader在执行过程中必须设置和初始化 Linux 的内核启动参数。目前传递启动参数主要采用两种方式:即通过 struct param_struct 和struct tag(标记列表,tagged list)两种结构传递。struct param_struct 是一种比较老的参数传递方式,在 2.4 版本以前的内核中使用较多。从 2.4 版本以后 Linux 内核基本上采用标记列表的方式。但为了保持和以前版本的兼容性,它仍支持 struct param_struct 参数传递方式,只不过在内核启动过程中它将被转换成标记列表方式。标记列表方式是种比较新的参数传递方式,它必须以 ATAG_CORE 开始,并以ATAG_NONE 结尾。中间可以根据需要加入其他列表。Linux内核在启动过程中会根据该启动参数进行相应的初始化工作。
调用 Linux内核映像
Bootloader完成的最后一项工作便是调用 Linux内核。如果 Linux 内核存放在 Flash 中,并且可直接在上面运行(这里的 Flash 指 Nor Flash),那么可直接跳转到内核中去执行。但由于在 Flash 中执行代码会有种种限制,而且速度也远不及 RAM 快,所以一般的嵌入式系统都是将 Linux内核拷贝到 RAM 中,然后跳转到 RAM 中去执行。不论哪种情况,在跳到 Linux 内核执行之前 CUP的寄存器必须满足以下条件:r0=0,r1=处理器类型,r2=标记列表在 RAM中的地址。
3、Linux内核的启动过程
在 bootloader将 Linux 内核映像拷贝到 RAM 以后,可以通过下例代码启动 Linux 内核:call_linux(0, machine_type, kernel_params_base)。
其中,machine_tpye 是 bootloader检测出来的处理器类型, kernel_params_base 是启动参数在 RAM 的地址。通过这种方式将 Linux 启动需要的参数从 bootloader传递到内核。Linux 内核有两种映像:一种是非压缩内核,叫 Image,另一种是它的压缩版本,叫zImage。根据内核映像的不同,Linux 内核的启动在开始阶段也有所不同。zImage 是 Image经过压缩形成的,所以它的大小比 Image 小。但为了能使用 zImage,必须在它的开头加上解压缩的代码,将 zImage 解压缩之后才能执行,因此它的执行速度比 Image 要慢。但考虑到嵌入式系统的存储空容量一般比较小,采用 zImage 可以占用较少的存储空间,因此牺牲一点性能上的代价也是值得的。所以一般的嵌入式系统均采用压缩内核的方式。
对于 ARM 系列处理器来说,zImage 的入口程序即为 arch/arm/boot/compressed/head.S。它依次完成以下工作:开启 MMU 和 Cache,调用 decompress_kernel()解压内核,最后通过调用 call_kernel()进入非压缩内核 Image 的启动。下面将具体分析在此之后 Linux 内核的启动过程。
(1)Linux内核入口
Linux 非压缩内核的入口位于文件/arch/arm/kernel/head-armv.S 中的 stext 段。该段的基地址就是压缩内核解压后的跳转地址。如果系统中加载的内核是非压缩的 Image,那么bootloader将内核从 Flash中拷贝到 RAM 后将直接跳到该地址处,从而启动 Linux 内核。不同体系结构的 Linux 系统的入口文件是不同的,而且因为该文件与具体体系结构有关,所以一般均用汇编语言编写[3]。对基于 ARM 处理的 Linux 系统来说,该文件就是head-armv.S。该程序通过查找处理器内核类型和处理器类型调用相应的初始化函数,再建立页表,最后跳转到 start_kernel()函数开始内核的初始化工作。
检测处理器内核类型是在汇编子函数__lookup_processor_type中完成的。通过以下代码可实现对它的调用:bl __lookup_processor_type。__lookup_processor_type调用结束返回原程序时,会将返回结果保存到寄存器中。其中r8 保存了页表的标志位,r9 保存了处理器的 ID 号,r10 保存了与处理器相关的 struproc_info_list 结构地址。
检测处理器类型是在汇编子函数 __lookup_architecture_type 中完成的。与__lookup_processor_type类似,它通过代码:“bl __lookup_processor_type”来实现对它的调用。该函数返回时,会将返回结构保存在 r5、r6 和 r7 三个寄存器中。其中 r5 保存了 RAM 的起始基地址,r6 保存了 I/O基地址,r7 保存了 I/O的页表偏移地址。当检测处理器内核和处理器类型结束后,将调用__create_page_tables 子函数来建立页表,它所要做的工作就是将 RAM 基地址开始的 4M 空间的物理地址映射到 0xC0000000 开始的虚拟地址处。对笔者的 S3C2410 开发板而言,RAM 连接到物理地址 0x30000000 处,当调用 __create_page_tables 结束后 0x30000000 ~ 0x30400000 物理地址将映射到0xC0000000~0xC0400000 虚拟地址处。
当所有的初始化结束之后,使用如下代码来跳到 C 程序的入口函数 start_kernel()处,开始之后的内核初始化工作:
b SYMBOL_NAME(start_kernel)
(2)start_kernel函数
start_kernel是所有 Linux 平台进入系统内核初始化后的入口函数,它主要完成剩余的与硬件平台相关的初始化工作,在进行一系列与内核相关的初始化后,调用第一个用户进程-init 进程并等待用户进程的执行,这样整个 Linux 内核便启动完毕。该函数所做的具体工作有[4][5]:
调用 setup_arch()函数进行与体系结构相关的第一个初始化工作;
对不同的体系结构来说该函数有不同的定义。对于 ARM 平台而言,该函数定义在arch/arm/kernel/Setup.c。它首先通过检测出来的处理器类型进行处理器内核的初始化,然后通过 bootmem_init()函数根据系统定义的 meminfo 结构进行内存结构的初始化,最后调用paging_init()开启 MMU,创建内核页表,映射所有的物理内存和 IO空间。
a、创建异常向量表和初始化中断处理函数;
b、初始化系统核心进程调度器和时钟中断处理机制;
c、初始化串口控制台(serial-console);
d、ARM-Linux 在初始化过程中一般都会初始化一个串口做为内核的控制台,这样内核在启动过程中就可以通过串口输出信息以便开发者或用户了解系统的启动进程。
e、创建和初始化系统 cache,为各种内存调用机制提供缓存,包括;动态内存分配,虚拟文件系统(VirtualFile System)及页缓存。
f、初始化内存管理,检测内存大小及被内核占用的内存情况;
g、初始化系统的进程间通信机制(IPC);
当以上所有的初始化工作结束后,start_kernel()函数会调用 rest_init()函数来进行最后的初始化,包括创建系统的第一个进程-init 进程来结束内核的启动。Init 进程首先进行一系列的硬件初始化,然后通过命令行传递过来的参数挂载根文件系统。最后 init 进程会执行用 户传递过来的“init=”启动参数执行用户指定的命令,或者执行以下几个进程之一:
1 execve("/sbin/init",argv_init,envp_init);
2 execve("/etc/init",argv_init,envp_init);
3 execve("/bin/init",argv_init,envp_init);
4 execve("/bin/sh",argv_init,envp_init)。
当所有的初始化工作结束后,cpu_idle()函数会被调用来使系统处于闲置(idle)状态并等待用户程序的执行。至此,整个 Linux 内核启动完毕。
Android的启动流程: 1. 硬件BOOT、加载Linux内核并挂载Rootfs 2. init进程启动及Native服务启动 3. SystemServer及Android服务启动 4. Home桌面启动
【1】Uboot的启动流程
Uboot的启动分为两个阶段。
第一阶段:设置异常向量表,设置ARM核为svc模式,关cache和关mmu,
关看门狗,初始化时钟,串口,内存,初始化栈空间,清bss。跳转到第二阶
段。
第二阶段:硬件的初始化,读取环境变量,将内核从emmc加载到内存中,
调用内核
【2】kernel的启动流程
设置cpu为svc模式,关中断
为内核的解压做准备(内存,中断等等)
内核完成自解压,调用__start_kernel来执行内核
挂载根文件系统
开启第一个用户进程init,执行linuxrc应用程序
【3】rootfs执行过程
Linuxrc调用inittab文件(规定操作系统行为文件)获取操作系统行为。
执行启动第一个脚本rcS,rcS中调用mount -a 挂载fstab中所有设备
当控制台登录的时候,执行bin/sh命令,进入shell命令行(在进入前,会执行profile,设置环境变量)
Target options(目标配置)
Toolchain(工具链)
Filesystems images(文件系统镜像选择)
-> Filesystem images
-> [*] ext2/3/4 root filesystem //如果是 EMMC 或 SD 卡的话就用 ext3/ext4
-> ext2/3/4 variant = ext4 //选择 ext4 格式
-> [*] ubi image containing an ubifs root filesystem //如果使用 NAND 的话就用 ubifs
配置 System configuration
System configuration
-> System hostname = alpha_imx6ull //平台名字,自行设置
-> System banner = Welcome to alpha i.mx6ull //欢迎语
-> Init system = BusyBox //使用 busybox
-> /dev management = Dynamic using devtmpfs + mdev //使用 mdev
-> [*] Enable root login with password (NEW) //使能登录密码
-> Root password = 123456 //登录密码为 123456
禁止编译 Linux 内核和 uboot
-> Kernel
-> [ ] Linux Kernel //不要选择编译 Linux Kernel 选项!
-> Bootloaders
-> [ ] U-Boot //不要选择编译 U-Boot 选项!
配置 Target packages
此选项用于配置要选择的第三方库或软件、比如 alsa-utils、 ffmpeg、 iperf 等工具,但是现在我们先不选择第三方库,防止编译不下去!先编译一下最基本的根文件系统,如果没有问题的话再重新配置选择第三方库和软件。否则一口吃太多会容易撑着的,编译出问题的时候都不知道怎么找问题