嵌入式内核&驱动(中下)

并发控制

上下文和并发场合

执行流:有开始有结束总体顺序执行的一段代码 又称上下文

应用编程:任务上下文 内核编程:

  1. 任务上下文:五状态 可阻塞 a. 应用进程或线程运行在用户空间 b. 应用进程或线程运行在内核空间(通过调用syscall来间接使用内核空间) c. 内核线程始终在内核空间

  2. 异常上下文:不可阻塞 中断上下文

竞态:多任务 并行执行时,如果在一个时刻 同时操作 同一个资源,会引起资源的错乱,这种错乱情形被称为竞态

共享资源:可能会被多个任务同时使用的资源

临界区:操作共享资源的代码段

为了解决竞态,需要提供一种控制机制,来避免在同一时刻使用共享资源,这种机制被称为并发控制机制

并发控制机制分类:

  1. 原子操作类

  2. 忙等待类

  3. 阻塞类

通用并发控制机制的一般使用套路:

/*互斥问题:*/
并发控制机制初始化为可用
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();          与中断低半部有关,关闭、打开软中断

  1. 禁止中断
  2. 临界区 //临界区代码不能占用太长时间,需要很快完成
  3. 打开中断

适用场合:中断上下文与某任务共享资源时,或多个不同优先级的中断上下文间共享资源时

原子变量(掌握)

原子变量:存取不可被打断的特殊整型变量

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

适用场合:

  1. 异常上下文之间或异常上下文与任务上下文之间共享资源时

  2. 任务上下文之间且临界区执行时间很

  3. 互斥问题

互斥锁

信号量:基于阻塞的并发控制机制

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

  1. 定义对应类型的变量

  2. 初始化对应变量

  • P/加锁
  • 临界区
  • V/解锁

#include <linux/mutex.h>

适用场合:任务上下文之间且临界区执行时间较长时的互斥问题

选择并发控制机制的原则

  1. 不允许睡眠的上下文需要采用忙等待类,可以睡眠的上下文可以采用阻塞类。在异常上下文中访问的竞争资源一定采用忙等待类。

  2. 临界区操作较长的应用建议采用阻塞类,临界区很短的操作建议采用忙等待类。

  3. 中断屏蔽仅在有与中断上下文共享资源时使用。

  4. 共享资源仅是一个简单整型量时用原子变量

内核定时器

时钟中断

硬件有一个时钟装置,该装置每隔一定时间发出一个时钟中断(称为一次时钟嘀嗒-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. 异常上下文中只能采用忙等待类
  2. 任务上下文短延迟采用忙等待类,长延迟采用阻塞类

定时器

(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 类型的值。

您的代码中可能使用了旧的函数签名。下面是如何修正这个问题的步骤:

  1. 修改定时器回调函数的签名
    修改了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 宏用于从定时器结构体获取到包含它的更大结构体的指针。它是一个安全的方法,用于在定时器回调中获取到包含定时器的自定义结构体的指针。

  2. 使用 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.functiontimer.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 是如何传入的:

  1. 定义和初始化定时器
    使用 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);
    }
    
  2. 定时器回调函数
    在定时器回调函数中,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(分配内存的方法):

  1. GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;
  2. GFP_KERNEL —— 正常分配内存;
  3. 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() 的共同特点是:

  1. 用于申请内核空间的内存;
  2. 内存以字节为单位进行分配;
  3. 所分配的内存虚拟地址上连续;

kmalloc()、kzalloc()、vmalloc() 的区别是:

  1. kzalloc 是强制清零的 kmalloc 操作;(以下描述不区分 kmalloc 和 kzalloc)
  2. kmalloc 分配的内存大小有限制(128KB),而 vmalloc 没有限制
  3. kmalloc 可以保证分配的内存物理地址是连续的,但是 vmalloc 不能保证;
  4. kmalloc 分配内存的过程可以是原子过程(使用 GFP_ATOMIC),而 vmalloc 分配内存时则可能产生阻塞;
  5. kmalloc 分配内存的开销小,因此 kmalloc 比 vmalloc 要快;
  6. vmalloc不能用于异常上下文。

一般情况下,内存只有在要被 DMA 访问的时候才需要物理上连续,但为了性能上的考虑,内核中一般使用 kmalloc(),而只有在需要获得大块内存时才使用 vmalloc()。

2.4 分配选择原则:

  1. 小内存(< 128k)用kmalloc,大内存用vmalloc或get_free_page
  2. 如果需要比较大的内存,并且要求使用效率较高时用get_free_page,否则用vmalloc

一般最常用的就是kmalloc

三、IO访问-------访问外设控制器的寄存器

两种方式:

  1. IO端口:X86上用IO指令访问
  2. 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设计

用来记录硬件平台中各种硬件设备的属性信息

二、基本组成

两种源文件:

  1. xxxxx.dts dts是device tree source的缩写

  2. 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 属性设置套路

一般来说,每一种设备的节点属性设置都会有一些套路,比如可以设置哪些属性?属性值怎么设置?那怎么知道这些套路呢,有两种思路:

  1. 抄类似的dts,比如我们自己项目的平台是4412,那么就可以抄exynos4412-tiny4412.dts、exynos4412-smdk4412.dts这类相近的dts

  2. 查询内核中的文档,比如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发生了某种需要立即处理的事件

分为:

  1. 内部中断:CPU执行程序的过程中,发生的一些硬件出错、运算出错事件(如分母为0、溢出等等),不可屏蔽
  2. 外部中断:外设发生某种情况,通过一个引脚的高、低电平变化来通知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;
};

中断上半部与下半部机制

上半部与下半部

起源:

  1. 中断处理程序执行时间过长引起的问题
  2. 有些设备的中断处理程序必须要处理一些耗时操作
  • 上半部:紧急  但 不耗时  
  • 下半部:耗时  但 不紧急

下半部机制之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 ----- 无具体延后时间要求时

​ 定时器 -----有具体延后时间要求时

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值