并发控制
上下文和并发场合
执行流:有开始有结束总体顺序执行的一段代码 又称上下文
应用编程:任务上下文 内核编程:
-
任务上下文:五状态 可阻塞 a. 应用进程或线程运行在用户空间 b. 应用进程或线程运行在内核空间(通过调用syscall来间接使用内核空间) c. 内核线程始终在内核空间
-
异常上下文:不可阻塞 中断上下文
竞态:多任务 并行执行时,如果在一个时刻 同时操作 同一个资源,会引起资源的错乱,这种错乱情形被称为竞态
共享资源:可能会被多个任务同时使用的资源
临界区:操作共享资源的代码段
为了解决竞态,需要提供一种控制机制,来避免在同一时刻使用共享资源,这种机制被称为并发控制机制
并发控制机制分类:
-
原子操作类
-
忙等待类
-
阻塞类
通用并发控制机制的一般使用套路:
/*互斥问题:*/
并发控制机制初始化为可用
P操作
临界区
V操作
/*同步问题:*/
//并发控制机制初始化为不可用
//先行方:
。。。。。
V操作
//后行方:
P操作
。。。。。
中断屏蔽(了解)
一种同步机制的辅助手段
禁止本cpu中断 使能本cpu中断
local_irq_disable(); local_irq_enable();
local_irq_save(flags); local_irq_restore(flags); 与cpu的中断位相关
local_bh_disable(); local_bh_enable(); 与中断低半部有关,关闭、打开软中断
- 禁止中断
- 临界区 //临界区代码不能占用太长时间,需要很快完成
- 打开中断
适用场合:中断上下文与某任务共享资源时,或多个不同优先级的中断上下文间共享资源时
原子变量(掌握)
原子变量:存取不可被打断的特殊整型变量
a.设置原子量的值
void atomic_set(atomic_t *v,int i); //设置原子量的值为i
atomic_t v = ATOMIC_INIT(0); //定义原子变量v并初始化为0
v = 10;//错误 不能直接赋值
b.获取原子量的值
atomic_read(atomic_t *v); //返回原子量的值
c.原子变量加减
void atomic_add(int i,atomic_t *v);//原子变量增加i
void atomic_sub(int i,atomic_t *v);//原子变量减少i
d.原子变量自增自减
void atomic_inc(atomic_t *v);//原子变量增加1
void atomic_dec(atomic_t *v);//原子变量减少1
e.操作并测试:运算后结果为0则返回真,否则返回假
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);
原子位操作方法:
- a.设置位 void set_bit(nr, void *addr); //设置addr的第nr位为1
- b.清除位 void clear_bit(nr , void *addr); //清除addr的第nr位为0
- c.改变位 void change_bit(nr , void *addr); //改变addr的第nr位为1
- d.测试位 void test_bit(nr , void *addr); //测试addr的第nr位是否为1
适用场合:共享资源为单个整型变量的互斥场合
练习:设置一个字符设备只能被打开一次
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/poll.h>
#include <asm/uaccess.h>
#include <asm/atomic.h>
int major = 11;
int minor = 0;
int openonce_num = 1;
struct openonce_dev
{
struct cdev mydev;
atomic_t openflag;//1 can open, 0 can not open
};
struct openonce_dev gmydev;
int openonce_open(struct inode *pnode,struct file *pfile)
{
struct openonce_dev *pmydev = NULL;
pfile->private_data =(void *) (container_of(pnode->i_cdev,struct openonce_dev,mydev));
pmydev = (struct openonce_dev *)pfile->private_data;
if(atomic_dec_and_test(&pmydev->openflag))//如果-1后结果为0 为真 可以打开
{
return 0;
}
else//如果-1后结果为-1 为假 不可以打开
{
atomic_inc(&pmydev->openflag);
printk("The device is opened already\n");
return -1;
}
}
int openonce_close(struct inode *pnode,struct file *pfile)
{
struct openonce_dev *pmydev = (struct openonce_dev *)pfile->private_data;
atomic_set(&pmydev->openflag,1);
return 0;
}
struct file_operations myops = {
.owner = THIS_MODULE,
.open = openonce_open,
.release = openonce_close,
};
int __init openonce_init(void)
{
int ret = 0;
dev_t devno = MKDEV(major,minor);
/*申请设备号*/
ret = register_chrdev_region(devno,openonce_num,"openonce");
if(ret)
{
ret = alloc_chrdev_region(&devno,minor,openonce_num,"openonce");
if(ret)
{
printk("get devno failed\n");
return -1;
}
major = MAJOR(devno);//容易遗漏,注意
}
/*给struct cdev对象指定操作函数集*/
cdev_init(&gmydev.mydev,&myops);
/*将struct cdev对象添加到内核对应的数据结构里*/
gmydev.mydev.owner = THIS_MODULE;
cdev_add(&gmydev.mydev,devno,openonce_num);
atomic_set(&gmydev.openflag,1); //原子变量初始设为1
return 0;
}
void __exit openonce_exit(void)
{
dev_t devno = MKDEV(major,minor);
cdev_del(&gmydev.mydev);
unregister_chrdev_region(devno,openonce_num);
}
MODULE_LICENSE("GPL");
module_init(openonce_init);
module_exit(openonce_exit);
四、自旋锁:基于忙等待的并发控制机制
a.定义自旋锁 spinlock_t lock;
b.初始化自旋锁 spin_lock_init(spinlock_t *);
c.获得自旋锁 spin_lock(spinlock_t *); //成功获得自旋锁立即返回,否则自旋在那里直到该自旋锁的保持者释放 p操作
spin_trylock(spinlock_t *); //成功获得自旋锁立即返回真,否则返回假,而不是像上一个那样"在原地打转” p操作
d.释放自旋锁 spin_unlock(spinlock_t *); v操作
解决互斥问题
#include <linux/spinlock.h>
定义spinlock_t类型的变量lock
spin_lock_init(&lock)后才能正常使用spinlock
spin_lock(&lock);
临界区
spin_unlock(&lock);
适用场合:
-
异常上下文之间或异常上下文与任务上下文之间共享资源时
-
任务上下文之间且临界区执行时间很短时
-
互斥问题
互斥锁
信号量:基于阻塞的并发控制机制
a.定义信号量
struct semaphore sem;
b.初始化信号量
void sema_init(struct semaphore *sem, int val);
c.获得信号量P
int down(struct semaphore *sem);//深度睡眠
int down_interruptible(struct semaphore *sem);//浅度睡眠
d.释放信号量V
void up(struct semaphore *sem);
#include <linux/semaphore.h>
适用场合:任务上下文之间且临界区执行时间较长时的互斥或同步问题
互斥锁:基于阻塞的互斥机制
a.初始化
struct mutex my_mutex;
mutex_init(&my_mutex);
b.获取互斥体
void mutex_lock(struct mutex *lock);
c.释放互斥体
void mutex_unlock(struct mutex *lock);
-
定义对应类型的变量
-
初始化对应变量
- P/加锁
- 临界区
- V/解锁
#include <linux/mutex.h>
适用场合:任务上下文之间且临界区执行时间较长时的互斥问题
选择并发控制机制的原则
-
不允许睡眠的上下文需要采用忙等待类,可以睡眠的上下文可以采用阻塞类。在异常上下文中访问的竞争资源一定采用忙等待类。
-
临界区操作较长的应用建议采用阻塞类,临界区很短的操作建议采用忙等待类。
-
中断屏蔽仅在有与中断上下文共享资源时使用。
-
共享资源仅是一个简单整型量时用原子变量
内核定时器
时钟中断
硬件有一个时钟装置,该装置每隔一定时间发出一个时钟中断(称为一次时钟嘀嗒-tick),对应的中断处理程序就将全局变量jiffies_64加1
jiffies_64 是一个全局64位整型, jiffies全局变量为其低32位的全局变量,程序中一般用jiffies
HZ:可配置的宏,表示1秒钟产生的时钟中断次数,一般设为100或200
延时机制
有忙等待 与 阻塞 两种
1.短延迟:忙等待
1. void ndelay(unsigned long nsecs)
2. void udelay(unsigned long usecs)
3. void mdelay(unsigned long msecs)
2.长延迟:忙等待
使用jiffies比较宏来实现
time_after(a,b) //a > b
time_before(a,b) //a < b//延迟100个jiffies
unsigned long delay = jiffies + 100; //100tick
while(time_before(jiffies,delay))
{
;
}//延迟2s
unsigned long delay = jiffies + 2*HZ;
while(time_before(jiffies,delay))
{
;
}
2.睡眠延迟----阻塞类
void msleep(unsigned int msecs); //深度睡眠
unsigned long msleep_interruptible(unsigned int msecs); //浅度睡眠
延时机制的选择原则:
- 异常上下文中只能采用忙等待类
- 任务上下文短延迟采用忙等待类,长延迟采用阻塞类
定时器
(1)定义定时器结构体
struct timer_list
{
struct list_head entry;
unsigned long expires; // 期望的时间值 jiffies + x * HZ
void (*function)(unsigned long); // 时间到达后,执行的回调函数,软中断异常上下文
unsigned long data;
};
(2)初始化定时器
init_timer(struct timer_list *)
(3)增加定时器 ------ 定时器开始计时
void add_timer(struct timer_list *timer);
(4)删除定时器 -------定时器停止工作
int del_timer(struct timer_list * timer);
(5)修改定时器:如设置每隔一会调用一次
int mod_timer(struct timer_list *timer, unsigned long expires);
//定义struct timer_list tl类型的变量
init_timer(...);//模块入口函数
//模块入口函数或open或希望定时器开始工作的地方
tl.expires = jiffies + n * HZ //n秒
tl.function = xxx_func;
tl.data = ...;
add_timer(....);
//不想让定时器继续工作时
del_timer(....);
void xxx_func(unsigned long arg)
{
......
mod_timer(....);//如需要定时器继续隔指定时间再次调用本函数
}
注意:在Linux内核4.15版本开始,定时器API发生了变化。
在Linux内核中,定时器的回调函数需要遵循特定的函数签名。具体来说,从Linux内核4.15版本开始,定时器API发生了变化,定时器回调函数的类型从 void (*)(unsigned long)
变更为 void (*)(struct timer_list *)
。这意味着回调函数需要接收一个指向 struct timer_list
的指针,而不再是一个 unsigned long
类型的值。
您的代码中可能使用了旧的函数签名。下面是如何修正这个问题的步骤:
-
修改定时器回调函数的签名:
修改了timer_func
函数,使其接收一个指向struct timer_list
的指针,而不是unsigned long
。例如:void timer_func(struct timer_list *t) { struct mysecond_dev *pmydev = from_timer(pmydev, t, timer); pmydev->second++; mod_timer(t, jiffies + HZ); }
在这里,
from_timer
宏用于从定时器结构体获取到包含它的更大结构体的指针。它是一个安全的方法,用于在定时器回调中获取到包含定时器的自定义结构体的指针。 -
使用
timer_setup()
替代init_timer_key()
:
在旧的Linux内核版本中使用init_timer_key()
或init_timer()
已不再推荐。应使用timer_setup()
设置定时器,如之前建议的修改。
这些改动将使代码符合新的Linux内核API,并解决类型不兼容的编译错误。如果您的代码基于一个特定的旧内核版本,请确保在适当的内核版本上进行修改。
修改 mysecond_init
函数中的定时器初始化部分:
// 旧的初始化方式:
// init_timer(&gmydev.timer);
// 应改为:
timer_setup(&gmydev.timer, timer_func, (unsigned long)&gmydev);
在 mysecond_open
函数中,不再需要设置 timer.function
和 timer.data
:
// 删除或注释掉以下行:
// pmydev->timer.function = timer_func;
// pmydev->timer.data = (unsigned long)pmydev;
// 直接设置定时器超时:
pmydev->timer.expires = jiffies + HZ * 1;
add_timer(&pmydev->timer);
在Linux内核中使用定时器时,mod_timer()
函数用于修改一个已经初始化和启动的定时器的超时时间。这个函数的第一个参数是一个指向 struct timer_list
的指针,它代表要修改的定时器。在定时器回调函数中使用 mod_timer()
时,这个参数通常是回调函数本身接收的参数。
这是一个典型的定时器设置和修改过程的示例,帮助您理解 t
是如何传入的:
-
定义和初始化定时器:
使用timer_setup()
初始化定时器,并设置回调函数和关联的数据。这个初始化过程通常在设备驱动的初始化函数中完成。static void timer_func(struct timer_list *t); struct mydevice { struct timer_list timer; // 其他设备相关的数据 }; struct mydevice my_dev; void mydevice_init(void) { timer_setup(&my_dev.timer, timer_func, 0); my_dev.timer.expires = jiffies + HZ; // 设定1秒后过期 add_timer(&my_dev.timer); }
-
定时器回调函数:
在定时器回调函数中,t
参数是指向struct timer_list
的指针,它指向触发这个回调的定时器。您可以通过这个指针修改定时器或访问与定时器相关联的数据。static void timer_func(struct timer_list *t) { struct mydevice *dev = from_timer(dev, t, timer); // 增加某些处理逻辑 // 重新设置或修改定时器以使其在未来再次触发 mod_timer(t, jiffies + HZ); // 再次设置1秒后触发 }
在这个示例中,t
是由内核定时器管理系统在定时器到期时传给回调函数的。在调用 mod_timer(t, new_expires)
时,t
是从回调函数的参数直接获取的,它指向了你在设置定时器时用 timer_setup()
初始化的那个 struct timer_list
实例。这样,您可以根据需要重新设置定时器的触发时间。
修改过后的代码如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/wait.h>
#include <linux/device.h>
#include <linux/sched.h>
#include <linux/poll.h>
#include <asm/uaccess.h>
#include <asm/atomic.h>
#include <linux/jiffies.h>
#include <linux/timer.h>
int major = 11;
int minor = 0;
int mysecond_num = 1;
struct mysecond_dev
{
struct cdev mydev;
int second;
struct timer_list timer;
atomic_t openflag;//1 can open, 0 can not open
struct class *pcls;
struct device *pdev;
};
struct mysecond_dev gmydev;
#if 0
void timer_func(unsigned long arg)
{
struct mysecond_dev *pmydev = (struct mysecond_dev *)arg;
pmydev->second++;
mod_timer(&pmydev->timer,jiffies + HZ * 1);
}
#endif
void timer_func(struct timer_list *t)
{
struct mysecond_dev *pmydev = from_timer(pmydev, t, timer);
pmydev->second++;
mod_timer(t, jiffies + HZ);
}
int mysecond_open(struct inode *pnode,struct file *pfile)
{
struct mysecond_dev *pmydev = NULL;
pfile->private_data =(void *) (container_of(pnode->i_cdev,struct mysecond_dev,mydev));
pmydev = (struct mysecond_dev *)pfile->private_data;
if(atomic_dec_and_test(&pmydev->openflag))
{
#if 0
pmydev->timer.function = timer_func;
pmydev->timer.data = (unsigned long)pmydev;
#endif
pmydev->timer.expires = jiffies + HZ * 1;
add_timer(&pmydev->timer);
return 0;
}
else
{
atomic_inc(&pmydev->openflag);
printk("The device is opened already\n");
return -1;
}
}
int mysecond_close(struct inode *pnode,struct file *pfile)
{
struct mysecond_dev *pmydev = (struct mysecond_dev *)pfile->private_data;
del_timer(&pmydev->timer);
atomic_set(&pmydev->openflag,1);
return 0;
}
ssize_t mysecond_read(struct file *pfile,char __user *puser,size_t size,loff_t *p_pos)
{
struct mysecond_dev *pmydev = (struct mysecond_dev *)pfile->private_data;
int ret = 0;
if(size < sizeof(int))
{
printk("the expect read size is invalid\n");
return -1;
}
if(size >= sizeof(int))
{
size = sizeof(int);
}
ret = copy_to_user(puser,&pmydev->second,size);
if(ret)
{
printk("copy to user failed\n");
return -1;
}
return size;
}
struct file_operations myops = {
.owner = THIS_MODULE,
.open = mysecond_open,
.release = mysecond_close,
.read = mysecond_read,
};
int __init mysecond_init(void)
{
int ret = 0;
dev_t devno = MKDEV(major,minor);
/*申请设备号*/
ret = register_chrdev_region(devno,mysecond_num,"mysecond");
if(ret)
{
ret = alloc_chrdev_region(&devno,minor,mysecond_num,"mysecond");
if(ret)
{
printk("get devno failed\n");
return -1;
}
major = MAJOR(devno);//容易遗漏,注意
}
/*给struct cdev对象指定操作函数集*/
cdev_init(&gmydev.mydev,&myops);
/*将struct cdev对象添加到内核对应的数据结构里*/
gmydev.mydev.owner = THIS_MODULE;
cdev_add(&gmydev.mydev,devno,mysecond_num);
#if 0
init_timer(&gmydev.timer);// old init timer
#endif
timer_setup(&gmydev.timer, timer_func, (unsigned long)&gmydev); //new kernal
atomic_set(&gmydev.openflag,1);
gmydev.pcls = class_create(THIS_MODULE,"mysecond");
if(IS_ERR(gmydev.pcls))
{
printk("class_create failed\n");
cdev_del(&gmydev.mydev);
unregister_chrdev_region(devno,mysecond_num);
return -1;
}
gmydev.pdev = device_create(gmydev.pcls,NULL,devno,NULL,"mysec");
if(NULL == gmydev.pdev)
{
printk("device_create failed\n");
class_destroy(gmydev.pcls);
cdev_del(&gmydev.mydev);
unregister_chrdev_region(devno,mysecond_num);
return -1;
}
return 0;
}
void __exit mysecond_exit(void)
{
dev_t devno = MKDEV(major,minor);
device_destroy(gmydev.pcls,devno);
class_destroy(gmydev.pcls);
cdev_del(&gmydev.mydev);
unregister_chrdev_region(devno,mysecond_num);
}
MODULE_LICENSE("GPL");
module_init(mysecond_init);
module_exit(mysecond_exit);
内核内存管理、动态分配及IO访问、LED驱动
一、内核内存管理框架
内核将物理内存等分成N块4KB,称之为一页,每页都用一个struct page来表示,采用伙伴关系算法维护。
内核地址空间划分图:
3G~3G+896M:低端内存,直接映射 虚拟地址 = 3G + 物理地址
细分为:ZONE_DMA、ZONE_NORMAL
分配方式:
1. kmalloc:小内存分配,slab算法
2. get_free_page:整页分配,2的n次方页,n最大为10
虚拟地址连续,物理地址连续
大于3G+896M:高端内存
细分为:vmalloc区、持久映射区、固定映射区
分配方式:vmalloc:虚拟地址连续,物理地址不连续
二、内核中常用动态分配
2.1 kmalloc
函数原型:
void *kmalloc(size_t size, gfp_t flags);
kmalloc() 申请的内存位于直接映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因为存在较简单的转换关系,所以对申请的内存大小有限制,不能超过128KB。
较常用的 flags(分配内存的方法):
- GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;
- GFP_KERNEL —— 正常分配内存;
- GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)。
flags 的参考用法:
- 进程上下文,可以睡眠 GFP_KERNEL
- 异常上下文,不可以睡眠 GFP_ATOMIC
- 中断处理程序 GFP_ATOMIC
- 软中断 GFP_ATOMIC
- Tasklet GFP_ATOMIC
- 用于DMA的内存,可以睡眠 GFP_DMA | GFP_KERNEL
- 用于DMA的内存,不可以睡眠 GFP_DMA | GFP_ATOMIC
对应的内存释放函数为:
void kfree(const void *objp);
void *kzalloc(size_t size, gfp_t flags)
2.2 vmalloc
void *vmalloc(unsigned long size);
vmalloc() 函数则会在虚拟内存空间给出一块连续的内存区,但这片连续的虚拟内存在物理内存中并不一定连续。由于 vmalloc() 没有保证申请到的是连续的物理内存,因此对申请的内存大小没有限制,如果需要申请较大的内存空间就需要用此函数了。
对应的内存释放函数为:
void vfree(const void *addr);
注意:vmalloc() 和 vfree() 可以睡眠,因此不能从异常上下文调用。
2.3 kmalloc & vmalloc 的比较
kmalloc()、kzalloc()、vmalloc() 的共同特点是:
- 用于申请内核空间的内存;
- 内存以字节为单位进行分配;
- 所分配的内存虚拟地址上连续;
kmalloc()、kzalloc()、vmalloc() 的区别是:
- kzalloc 是强制清零的 kmalloc 操作;(以下描述不区分 kmalloc 和 kzalloc)
- kmalloc 分配的内存大小有限制(128KB),而 vmalloc 没有限制;
- kmalloc 可以保证分配的内存物理地址是连续的,但是 vmalloc 不能保证;
- kmalloc 分配内存的过程可以是原子过程(使用 GFP_ATOMIC),而 vmalloc 分配内存时则可能产生阻塞;
- kmalloc 分配内存的开销小,因此 kmalloc 比 vmalloc 要快;
- vmalloc不能用于异常上下文。
一般情况下,内存只有在要被 DMA 访问的时候才需要物理上连续,但为了性能上的考虑,内核中一般使用 kmalloc(),而只有在需要获得大块内存时才使用 vmalloc()。
2.4 分配选择原则:
- 小内存(< 128k)用kmalloc,大内存用vmalloc或get_free_page
- 如果需要比较大的内存,并且要求使用效率较高时用get_free_page,否则用vmalloc
一般最常用的就是kmalloc
三、IO访问-------访问外设控制器的寄存器
两种方式:
- IO端口:X86上用IO指令访问
- IO内存:外设寄存器在SOC芯片手册上都有相应物理地址(ARM)
IO内存访问接口:
static inline void __iomem *ioremap(unsigned long offset, unsigned long size)
/*
功能:实现IO管脚的映射(物理->虚拟)
参数:offset:该管脚的偏移地址
Size:该管脚映射空间的大小
返回值:成功返回映射的虚拟地址,失败NULL
*/
static inline void iounmap(volatile void __iomem *addr)
/*
功能:解除io管脚的映射
参数:addr:io管脚映射的地址
*/
unsigned readb(void *addr);//1字节 或ioread8(void *addr)
unsigned readw(void *addr);//2字节 或ioread16(void *addr)
unsigned readl(void *addr);//4字节 或ioread32(void *addr)
/*
功能:读取寄存器的值
参数:addr 地址(虚拟)
返回值:读到的数据
*/
void writeb(unsigned value, void *addr);//1字节 或iowrite8(u8 value, void *addr)
void writew(unsigned value, void *addr);//2字节 或iowrite16(u16 value, void *addr)
void writel(unsigned value, void *addr);//4字节 或iowrite32(u32 value, void *addr)
/*
功能:向指定的寄存器中,写入数据。
参数:value:待写入寄存器中的数据
Address:寄存器的虚拟地址
*/
四、led驱动
1. 读原理图
2.查阅SOC芯片手册
GPX2_7 led2 GPX2CON----0x11000C40---28~31-----0001 GPX2DAT-----0x11000C44-----7
GPX1_0 led3 GPX1CON----0x11000C20---0~3-----0001 GPX1DAT----0x11000C24-----0
GPF3_4 led4 GPF3CON----0x114001E0---16~19-----0001 GPF3DAT----0x114001E4-----4
GPF3_5 led5 GPF3CON----0x114001E0---20~23-----0001 GPF3DAT----0x114001E4-----5
3.编写驱动
a. 设计设备数据类型
struct myled_dev
{
struct cdev mydev;
unsigned long * led2con;
unsigned long * led2dat;
unsigned long * led3con;
unsigned long * led3dat;
unsigned long * led4con;
unsigned long * led4dat;
unsigned long * led5con;
unsigned long * led5dat;
};
b. 考虑需要支持的函数(open、close)
c. 模块入口:ioremap + 设置成输出
d. 模块出口:iounmap
e. 编写关灯函数和开灯函数,实现ioctl
设备树
一、起源
减少垃圾代码
减轻驱动开发工作量
驱动代码和设备信息分离
参考Open Fireware设计
用来记录硬件平台中各种硬件设备的属性信息
二、基本组成
两种源文件:
-
xxxxx.dts dts是device tree source的缩写
-
xxxxx.dtsi dtsi是device tree source include的缩写,意味着这样源文件用于被dts文件包含用
实际使用时,需要把dts文件编译成对应的二进制文件(.dtb文件,dtb是device tree binary的缩写 )便于运行时存放在内存加快读取信息的速度
三、基本语法
dts文件主体内容由多个节点组成
每个节点可以包含0或多个子节点,形成树状关系
每个dts文件都有一个根节点,其它节点都是它的子孙
根节点一般来描述整个开发板硬件平台,其它节点用来表示具体设备、总线的属性信息
各个节点可以有多个属性,每个属性用key-value键值对来表示
节点语法: ( [] 代表可选项 )
[label:] node-name[@unit-address] {
[properties definitions];
[child nodes];
};
label: 可选项,节点别名,为了缩短节点访问路径,后续节点中可以使用 &label 来表示引用指定节点
node-name: 节点名
unit-address: 设备地址,一般填写该设备寄存器组或内存块的首地址
properties definitions:属性定义
child nodes:子节点
属性语法:
[label:] property-name = value;
[label:] property-name;
属性可以无值
有值的属性,可以有三种取值:
1. arrays of cells(1个或多个32位数据, 64位数据使用2个32位数据表示,空格分隔),用尖括号表示(< >)
2. string(字符串), 用双引号表示(" ")
3. bytestring(1个或多个字节,空格分隔),用方括号表示([])
4. 用,分隔的多值
示例:
四、特殊节点
4.1 根节点
根节点表示整块开发板的信息
#address-cells // 在子节点的reg(region)属性中, 使用多少个u32整数来描述地址(address)
#size-cells // 在子节点的reg属性中, 使用多少个u32整数来描述大小(size)
compatible // 定义一系列的字符串, 用来指定内核中哪个machine_desc可以支持本设备,即描述其兼容哪些平台
model // 比如有2款板子配置基本一致, 它们的compatible是一样的,那么就通过model来分辨这2款板子
4.2 /memory
所有设备树文件的必需节点,它定义了系统物理内存的 layout
device_type = "memory";
reg //用来指定内存的地址、大小
4.3 /chosen
传递内核启动时使用的参数parameter
bootargs //字符串,内核启动参数, 跟u-boot中设置的bootargs作用一样
4.4 /cpus 多核CPU支持
/cpus节点下有1个或多个cpu子节点, cpu子节点中用reg属性用来标明自己是哪一个cpu
所以 /cpus 中有以下2个属性:
#address-cells // 在它的子节点的reg属性中, 使用多少个u32整数来描述地址(address)
#size-cells // 在它的子节点的reg属性中, 使用多少个u32整数来描述大小(size) 必须设置为0
五、常用属性
5.1 phandle
数字形式的节点标识,在后续节点中属性值性质表示某节点时,可以引用对应节点
如:
pic@10000000 {
phandle = <1>;
interrupt-controller;
};
another-device-node {
interrupt-parent = <1>; // 使用phandle值为1来引用上述节点
};
5.2 地址 --------------- 重要
reg属性:表示内存区域region,语法:起址+大小
reg = <address1 length1 [address2 length2] [address3 length3]>;
#address-cells:reg属性中, 使用多少个u32整数来描述地址(address),语法:
#address-cells = <数字>;
#size-cells:reg属性中, 使用多少个u32整数来描述大小(size),语法:
#size-cells = <数字>;
5.3 compatible --------------- 重要
驱动和设备(设备节点)的匹配依据,compatible(兼容性)的值可以有不止一个字符串以满足不同的需求,语法:
compatible = "字符串1","字符串2",...;
5.4 中断 --------------- 重要
a. 中断控制器节点用的属性:
interrupt-controller 一个无值空属性用来声明这个node接收中断信号,表示该节点是一个中断控制器
#interrupt-cells 这是中断控制器节点的属性,用来标识这个控制器需要几个单位做中断描述符
b. 中断源设备节点用的属性:
interrupt-parent:标识此设备节点属于哪一个中断控制器,如果没有设置这个属性,会自动依附父节点的,语法:
interrupt-parent = <引用某中断控制器节点>
interrupts 一个中断标识符列表,表示每一个中断输出信号,语法:
interrupts = <中断号 触发方式>
1 low-to-high 上升沿触发
2 high-to-low 下降沿触发
4 high level 高电平触发
8 low level 低电平触发
5.5 gpio --------------- 重要
gpio也是最常见的IO口,常用的属性有:
a. 对于GPIO控制器:
gpio-controller,无值空属性,用来说明该节点描述的是一个gpio控制器
#gpio-cells,用来表示要用几个cell描述一个 GPIO引脚
b. 对于GPIO使用者节点:
gpio使用节点的属性
xxx-gpio = <&引用GPIO控制器 GPIO标号 工作模式>
工作模式: 1 低电平有效 GPIO_ACTIVE_HIGH
0 高电平有效 GPIO_ACTIVE_LOW
例:fs4412-gpio = <GPX1 0 1>
5.6 属性设置套路
一般来说,每一种设备的节点属性设置都会有一些套路,比如可以设置哪些属性?属性值怎么设置?那怎么知道这些套路呢,有两种思路:
-
抄类似的dts,比如我们自己项目的平台是4412,那么就可以抄exynos4412-tiny4412.dts、exynos4412-smdk4412.dts这类相近的dts
-
查询内核中的文档,比如Documentation/devicetree/bindings/i2c/i2c-imx.txt就描述了imx平台的i2c属性设置方法;Documentation/devicetree/bindings/fb就描述了lcd、lvds这类属性设置方法
六、常用接口
struct device_node 对应设备树中的一个节点
struct property 对应节点中一个属性
6.1 of_find_node_by_path
/**
include/of.h
of_find_node_by_path - 通过路径查找指定节点
@path - 带全路径的节点名,也可以是节点的别名
成功:得到节点的首地址;失败:NULL
*/
struct device_node * of_find_node_by_path(const char *path);
6.2 of_find_property
/*
include/of.h
of_find_property - 提取指定属性的值
@np - 设备节点指针
@name - 属性名称
@lenp - 属性值的字节数
成功:属性值的首地址;失败:NULL
*/
struct property *of_find_property(const struct device_node *np, const char *name, int *lenp);
6.3 of_get_named_gpio
/**
* include/of_gpio.h
* of_get_named_gpio - 从设备树中提取gpio口
* @np - 设备节点指针
* @propname - 属性名
* @index - gpio口引脚标号
* 成功:得到GPIO口编号;失败:负数,绝对值是错误码
*/
int of_get_named_gpio(struct device_node *np, const char *propname, int index);
6.4 irq_of_parse_and_map
/*
功能:获得设备树中的中断号并进行映射
参数:node:设备节点
index:序号
返回值:成功:中断号 失败:错误码
*/
unsigned int irq_of_parse_and_map(struct device_node *node, int index);
6.5 读属性值
of_property_read_string
/*
of_property_read_string - 提取字符串(属性值)
@np - 设备节点指针
@propname - 属性名称
@out_string - 输出参数,指向字符串(属性值)
成功:0;失败:负数,绝对值是错误码
*/
int of_property_read_string(struct device_node *np, const char *propname, const char **out_string);
读数值
int of_property_read_u8(const struct device_node *np,const char *propname,u8 *out_value)
int of_property_read_u16(const struct device_node *np,const char *propname,u16 *out_value)
int of_property_read_u32(const struct device_node *np,const char *propname,u32 *out_value)
判断属性是否存在
int of_property_read_bool(const struct device_node *np,const char *propname)
读数组
int of_property_read_u32_array(const struct device_node *np,const char *propname,u32 *out_value,size_t sz)
七、GPIO接口
7.1 向内核申请GPIO
int gpio_request(unsigned gpio,const char *label)
功能:其实就是让内核检查一下该GPIO引脚是否被其它设备占用,如果没有占用则返回0并用label做一下标记,表示被本设备占用,否则返回负数
void gpio_free(unsigned gpio)
功能:去除本设备对该GPIO的占用标记,表示本设备向内核归还对该GPIO引脚的使用权,此后其它设备可占用该GPIO引脚
7.2 设置GPIO方向
int gpio_direction_input(unsigned gpio)
int gpio_direction_output(unsigned gpio,int value)
7.3 读写GPIO数据
int gpio_get_value(unsigned gpio)
int gpio_set_value(unsigned gpio,int value)
八、led驱动设备树版
1.在设备树源文件的根节点下添加本设备的节点(该节点中包含本设备用到的资源信息)
..../linux3.14/arch/arm/boot/dts/exynos4412-fs4412.dts
fs4412-leds {
compatible = "fs4412,led2-5";
led2-gpio = <&gpx2 7 0>;
led3-gpio = <&gpx1 0 0>;
led4-gpio = <&gpf3 4 0>;
led5-gpio = <&gpf3 5 0>;
};
2.在linux内核源码的顶层目录下执行:make dtbs (生成对应的dtb文件)
3.cp ?????.dtb /tftpboot
4.编写驱动代码:
a. 通过本设备在设备树中的路径找到对应节点(struct device_node类型的地址值)
b. 调用 of_get_named_gpio 函数得到某个GPIO的编号
c. struct leddev结构体中记录所有用到的GPIO编号
d. 使用某个GPIO引脚前需先通过gpio_request函数向内核申请占用该引脚,不用该引脚时可通过gpio_free归还给内核
e. 通过gpio_direction_input和gpio_direction_output函数来设置某个GPIO的作用
f. 通过gpio_get_value函数可以获取某个GPIO引脚的当前电平
g. 通过gpio_set_value函数可以改变某个GPIO引脚的电平
中断
一、什么是中断
一种硬件上的通知机制,用来通知CPU发生了某种需要立即处理的事件
分为:
- 内部中断:CPU执行程序的过程中,发生的一些硬件出错、运算出错事件(如分母为0、溢出等等),不可屏蔽
- 外部中断:外设发生某种情况,通过一个引脚的高、低电平变化来通知CPU (如外设产生了数据、某种处理完毕等等)
二、中断处理原理
任何一种中断产生,CPU都会暂停当前执行的程序,跳转到内存固定位置执行一段程序,该程序被称为总的中断服务程序,在该程序中区分中断源,然后进一步调用该中断源对应的处理函数。
中断源对应的处理函数被称为分中断处理程序,一般每一个分中断处理程序对应一个外设产生的中断
写驱动时,如果外设有中断,则需要编写一个函数(分中断处理程序)来处理这种中断
三、中断接口
3.1 中断申请
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)
/*
参数:
irq:所申请的中断号
handler:该中断号对应的中断处理函数
flags:中断触发方式或处理方式
触发方式:IRQF_TRIGGER_NONE //无触发
IRQF_TRIGGER_RISING //上升沿触发
IRQF_TRIGGER_FALLING //下降沿触发
IRQF_TRIGGER_HIGH //高电平触发
IRQF_TRIGGER_LOW //低电平触发
处理方式:
IRQF_DISABLED //用于快速中断,处理中屏蔽所有中断
IRQF_SHARED //共享中断
name:中断名 /proc/interrupts
dev:传递给中断例程的参数,共享中断时用于区分那个设备,一般为对应设备的结构体地址,无共享中断时写NULL
返回值:成功:0 失败:错误码
*/
3.2 中断释放
void free_irq(unsigned int irq, void *dev_id);
/*
功能:释放中断号
参数:
irq:设备号
dev_id:共享中断时用于区分那个设备一般强转成设备号,无共享中断时写NULL
*/
3.3 中断处理函数原型
typedef irqreturn_t (*irq_handler_t)(int, void *);
/*
参数:
int:中断号
void*:对应的申请中断时的dev_id
返回值:
typedef enum irqreturn irqreturn_t; //中断返回值类型
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
返回IRQ_HANDLED表示处理完了,返回IRQ_NONE在共享中断表示不处理
*/
四、按键驱动
按键原理图:
exynos4412-fs4412.dts中增加节点
mykey2_node {
compatible = "mykey2,key2";
key2-gpio = <&gpx1 1 0>;
interrupt-parent = <&gpx1>;
interrupts = <1 3>;
};
mykey3_node {
compatible = "mykey3,key3";
key3-gpio = <&gpx1 2 0>;
interrupt-parent = <&gpx1>;
interrupts = <2 3>;
};
struct fs4412key2_dev
{
struct cdev mydev;
int gpio;
int irqno;
struct keyvalue data;
int newflag;
spinlock_t lock;
wait_queue_head_t rq;
};
中断上半部与下半部机制
上半部与下半部
起源:
- 中断处理程序执行时间过长引起的问题
- 有些设备的中断处理程序必须要处理一些耗时操作
- 上半部:紧急 但 不耗时
- 下半部:耗时 但 不紧急
下半部机制之tasklet ---- 基于软中断
1 结构体
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
2 定义tasklet的中断底半部处理函数
void tasklet_func(unsigned long data);
3 初始化tasklet
DECLARE_TASKLET(name, func, data);
/*
定义变量并初始化
参数:name:中断底半部tasklet的名称
Func:中断底半部处理函数的名字
data:给中断底半部处理函数传递的参数
*/
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
4 调度tasklet (在上半部函数的最后调用)
void tasklet_schedule(struct tasklet_struct *t)
//参数:t:tasklet的结构体
下半部机制之workqueue ----- 基于内核线程
1 工作队列结构体:
typedef void (*work_func_t)(struct work_struct *work)
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
2 定义工作队列底半部处理函数
void work_queue_func(struct work_struct *work);
3 初始化工作队列
struct work_struct work_queue;
初始化:绑定工作队列及工作队列的底半部处理函数
INIT_WORK(struct work_struct * pwork, _func) ;
参数:pwork:工作队列
func:工作队列的底半部处理函数
4 工作队列的调度函数
bool schedule_work(struct work_struct *work);
下半部机制比较
任务机制
workqueue ----- 内核线程 能睡眠 运行时间无限制
异常机制 ------- 不能睡眠 下半部执行时间不宜太长( < 1s)
软中断 ---- 接口不方便
tasklet ----- 无具体延后时间要求时
定时器 -----有具体延后时间要求时