嵌入式内核及驱动开发中级(下)

目录

第一部分

一.IO模型:阻塞和非阻塞

# 一、五种IO模型------读写外设数据的方式

# 二、阻塞与非阻塞

二. Code exercise

三. 知识补充

第二部分

一. IO模型:多路复用及信号驱动

# 一、多路复用

## 1.1 应用层:三套接口select、poll、epoll

## 1.2 驱动层:实现poll函数

# 二、信号驱动

## 2.1 应用层:信号注册+fcntl

## 2.2 驱动层:实现fasync函数

二. Code exercise 

三. 知识补充

第三部分

一. 上下文、并发控制机制中断屏蔽, 原子变量、自旋锁

# 一、上下文和并发场合

# 二、中断屏蔽(了解)

# 三、原子变量(掌握)

# 四、自旋锁:基于忙等待的并发控制机制

二. Code exercise 

三. 知识补充

第四部分

一. 信号量、互斥锁、并发机制的选择原则

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

# 二、互斥锁:基于阻塞的互斥机制

# 三、选择并发控制机制的原则

二. Code exercise

三. 知识补充

第五部分

一. linux内核定时器

# 一、时钟中断

# 二、延时机制

1. 短延迟:忙等待

2. 长延迟:忙等待

3. 睡眠延迟----阻塞类

# 三、定时器

# 四、课堂练习—秒设备

二. Code exercise

三. 知识补充

第六部分

一. 内核内存管理 、动态分配

# 一、内核内存管理框架

# 二、内核中常用动态分配

## 2.1 kmalloc

## 2.2 vmalloc

## 2.3 kmalloc & vmalloc 的比较

2.4 分配选择原则:

二. IO访问、 LED驱动

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

# 二、led驱动

三. 知识补充

第七部分

一.设备树的相关知识

# 一、设备树的起源

# 二、基本组成

# 三、基本语法

# 四、特殊节点

## 4.1 根节点

## 4.2 /memory

## 4.3  /chosen

## 4.4 /cpus  多核CPU支持

# 五、常用属性

# 5.1 phandle(读取整数):数字形式的节点标识

## 5.2 地址   ---------------  重要

## 5.3 compatible ---------------  重要

## 5.5 gpio ---------------  重要

## 5.6 属性设置套路

二. LED驱动设备树版

# 六、常用接口

## 6.1 of_find_node_by_path

## 6.2 of_find_property

## 6.3 of_get_named_gpio

## 6.4 irq_of_parse_and_map

of_property_read_string

# 七、GPIO接口

## 7.1 向内核申请GPIO

## 7.2 设置GPIO方向

## 7.3 读写GPIO数据

# 八、led驱动设备树版

三. 知识补充


第一部分



一.IO模型:阻塞和非阻塞

# 一、五种IO模型------读写外设数据的方式

1. 阻塞: 不能操作就睡觉

发现为阻塞, 就把该任务从就绪队列中拿出来加到等待队列中去,这样节省cpu的时间片轮转资源, 提高cpu的运行效率。

2. 非阻塞:不能操作就返回错误

3. 多路复用:委托中介监控

4. 信号驱动:让内核如果能操作时发信号,在信号处理函数中操作

5. 异步IO:向内核注册操作请求,内核完成操作后发通知信号

# 二、阻塞与非阻塞

应用层:

​open时由O_NONBLOCK指示read、write时是否阻塞, 在flags参数会保存在对应文件描述符的file结构里面。

​open以后可以由fcntl函数来改变是否阻塞:

# 一、五种IO模型------读写外设数据的方式

1. 阻塞: 不能操作就睡觉
发现为阻塞, 就把该任务从就绪队列中拿出来加到等待队列中去,这样节省cpu的时间片轮转资源, 提高cpu的运行效率。


2. 非阻塞:不能操作就返回错误

3. 多路复用:委托中介监控

 
4. 信号驱动:让内核如果能操作时发信号,在信号处理函数中操作


5. 异步IO:向内核注册操作请求,内核完成操作后发通知信号



# 二、阻塞与非阻塞

应用层:

​open时由O_NONBLOCK指示read、write时是否阻塞, 在flags参数会保存在对应文件描述符的file结构里面。

​open以后可以由fcntl函数来改变是否阻塞:


```c
flags = fcntl(fd,F_GETFL,0);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
```

驱动层:通过等待队列

```c
wait_queue_head_t //等待队列头数据类型

init_waitqueue_head(wait_queue_head_t *pwq) //初始化等待队列头
    
wait_event_interruptible(wq,condition)
/*
功能:条件不成立则让任务进入浅度睡眠,直到条件成立醒来, 浅度睡眠可以被信号唤醒。
    wq:等待队列头
    condition:C语言表达式, 条件不成立等待
返回:正常唤醒返回0,信号唤醒返回非0(此时读写操作函数应返回-ERESTARTSYS)
*/
        
wait_event(wq,condition) //深度睡眠

wake_up_interruptible(wait_queue_head_t *pwq)         //唤醒浅度睡眠
        
wake_up(wait_queue_head_t *pwq)
    
    
/*
1. 读、写用不同的等待队列头rq、wq
2. 无数据可读、可写时调用wait_event_interruptible(rq、wq,条件)
3. 写入数据成功时唤醒rq,读出数据成功唤醒wq
*/

```

二. Code exercise

1.在设备对象中加入这两个成员

2.在模块的入口函数的地方初始化等待队列头
3. 在需要实现读写阻塞的地方加上驱动阻塞的支持

三. 知识补充

1.小于32的信号不可靠, 不可靠的原因在于当有多个进程向一个进程发送相同的信号的时候, 后面的信号会被丢掉, 也就是会有进程产生的信号得不到相应的处理操作。

2.任务的五种状态和CPU时间片调度任务

就绪态, 运行态, 睡眠态, 暂停态, 僵尸态

3.深度睡眠只有需要的那个条件成立了处于等待队列里面的任务才会被加到就绪独立额里面去,浅度睡眠除了上述说的,当接受到一个信号也会醒来。

4.输出重定向

往对应的设备文件里面写内容

5.同步IO和异步IO的理解

对于一个线程的请求调用来讲,同步和异步的区别在于是否要等这个请求出最终结果

对于多个线程而言,同步或异步就是线程间的步调是否要一致、是否要协调

同步也经常用在一个线程内先后两个函数的调用上

异步就是一个请求返回时一定不知道结果,还得通过其他机制来获知结果,如:主动轮询或被动通

6.cpu执行运行态和就绪态的任务

第二部分



一. IO模型:多路复用及信号驱动

# 一、多路复用

描述符:

1. 文件描述符:设备文件、管道文件, struct file*类型的数组

2. socket描述符, 会创建一个用于网络通信的套接字文件

## 1.1 应用层:三套接口select、poll、epoll

select:位运算实现 监控的描述符数量有限(32位机1024,64位机2048) 效率差

poll:链表实现,监控的描述符数量不限  效率差

epoll:效率最高,监控的描述符数量不限

```c

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

/*        功能:监听多个描述符,阻塞等待有一个或者多个文件描述符,准备就绪。

内核将没有准备就绪的文件描述符,从集合中清掉了。

参数:     nfds                       最大文件描述符数 ,加1

readfds                读文件描述符集合

writefds                写文件描述符集合

exceptfds                其他异常的文件描述符集合

timeout                超时时间(NULL)

返回值:当timeout为NULL时返回0,成功:准备好的文件描述的个数  出错:-1

   当timeout不为NULL时,如超时设置为0,则select为非阻塞,超时设置 > 0,则无描述符可被操作的情况下阻塞指定长度的时间

*/

void FD_CLR(int fd, fd_set *set);

//功能:将fd 从集合中清除掉



int  FD_ISSET(int fd, fd_set *set);

//功能:判断fd 是否存在于集合中



void FD_SET(int fd, fd_set *set);

//功能:将fd 添加到集合中



void FD_ZERO(fd_set *set);

//功能:将集合清零



//使用模型:



while(1)

{

    /*得到最大的描述符maxfd*/

    /*FD_ZERO清空描述符集合*/

/*将被监控描述符加到相应集合rfds里  FD_SET*/

    /*设置超时*/

    ret = select(maxfd+1,&rfds,&wfds,NULL,NULL);

    if(ret < 0)

    {

        if(errno == EINTR)//错误时信号引起的

        {

                continue;  

        }

        else

        {

            break;

        }

    }

    else if(ret == 0)

    {//超时

        //.....

    }

    else

    { //> 0 ret为可被操作的描述符个数

        if(FD_ISSET(fd1,&rfds))

        {//读数据

            //....

        }

        if(FD_ISSET(fd2,&rfds))

        {//读数据

            //....

        }

        ///.....

        if(FD_ISSET(fd1,&wfds))

        {//写数据

            //....

        }

    }

}

  

```

## 1.2 驱动层:实现poll函数
```c

void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);

/*功能:将等待队列头添加至poll_table表中

  参数:struct file :设备文件

  Wait_queue_head_t :等待队列头

  Poll_table :poll_table表

*/



/*该函数与select、poll、epoll_wait函数相对应,协助这些多路监控函数判断本设备是否有数据可读写*/

unsigned int xxx_poll(struct file *filp, poll_table *wait) //函数名初始化给struct file_operations的成员.poll

{

    unsigned int mask = 0;

    /*

            1. 将所有等待队列头加入poll_table表中

            2. 判断是否可读,如可读则mask |= POLLIN | POLLRDNORM;

            3. 判断是否可写,如可写则mask |= POLLOUT | POLLWRNORM;

    */

   

    return mask;

}

```

# 二、信号驱动

## 2.1 应用层:信号注册+fcntl

驱动程序帮助我们监测对应的设备是否可读, 发现可以执行相应的操作之后向应用进程发送一个信号, 触发应用程序的信号处理函数。

```c

signal(SIGIO, input_handler); //注册信号处理函数

fcntl(fd, F_SETOWN, getpid());//将描述符设置给对应进程,好由描述符获知PID, 需要知道具体给哪一个进程发送信号。

oflags = fcntl(fd, F_GETFL);

fcntl(fd, F_SETFL, oflags | FASYNC);//将该设备的IO模式设置成信号驱动模式

void input_handler(int signum)//应用自己实现的信号处理函数,在此函数中完成读写

{

    //读数据

}



//应用模板

int main()

{

int fd = open("/dev/xxxx",O_RDONLY);

fcntl(fd, F_SETOWN, getpid());

oflags = fcntl(fd, F_GETFL);

//将该设备的信号驱动模式改为信号驱动模式

fcntl(fd, F_SETFL, oflags | FASYNC);
signal(SIGIO,xxxx_handler);


//......

}
  
void xxxx_handle(int signo)

{//读写数据
  

}



```

## 2.2 驱动层:实现fasync函数
```c

/*设备结构中添加如下成员*/

struct fasync_struct *pasync_obj;



/*应用调用fcntl设置FASYNC时调用该函数产生异步通知结构对象,并将其地址设置到设备结构成员中*/

static int hello_fasync(int fd, struct file *filp, int mode) //函数名初始化给struct file_operations的成员.fasync

{

struct hello_device *dev = filp->private_data;

return fasync_helper(fd, filp, mode, &dev->pasync_obj);

}



/*写函数中有数据可读时向应用层发信号*/

if (dev->pasync_obj)

       kill_fasync(&dev->pasync_obj, SIGIO, POLL_IN);

      

/*release函数中释放异步通知结构对象*/

if (dev->pasync_obj)

fasync_helper(-1, filp, 0, &dev->pasync_obj);



int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **pp);

/*

功能:产生或释放异步通知结构对象

参数:回收, fd = -1, mode = 0

返回值:成功为>=0,失败负数

*/



void kill_fasync(struct fasync_struct **, int, int);

/*        

功能:发信号

参数:

struct fasync_struct ** 指向保存异步通知结构地址的指针

int         信号 SIGIO/SIGKILL/SIGCHLD/SIGCONT/SIGSTOP

int         读写信息POLLIN、POLLOUT

*/

```

二. Code exercise 

1.多路复用的驱动代码实现

2.应用程序验证
3.在设备对象中添加一个异步信号通知对象的指针
4.回收异步信号通知对象

三. 知识补充

不同的任务在读写的时候会因为某些原因造成阻塞, 就可以将这些阻塞的任务的等待队列加入到poll_table表中

函数在定义的时候的声明与实现的地方不一样

  • 错误分析
  • 信号不可靠, 多个进程向一个进程发送信号会出现信号丢失
  • 多路复用本质是一种监管机制, 自动检测集合内部有哪些文件可以操作
  • ln - s 目标文件 新文件, 创建软连接文件
  • 多路复用的本质:
  • 快速定位

第三部分



一. 上下文、并发控制机制中断屏蔽, 原子变量、自旋锁

# 一、上下文和并发场合

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

应用编程:任务上下文

内核编程:

1. 任务上下文:五状态 可阻塞, 就绪态, 运行态, 睡眠态, 僵尸态, 暂停态

   a. 应用进程或线程运行在用户空间

   b. 应用进程或线程运行在内核空间(通过调用syscall来间接使用内核空间)

   c. 内核线程始终在内核空间

2. 异常上下文:不可阻塞, 不属于任务, 不可以阻塞

   a.中断上下文

竞态:多任务并行执行时,如果在一个时刻同时操作同一个资源,会引起资源的错乱,这种错乱情形被称为竞态, 这种情况发生的概率很低,但是还是要避免

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

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

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

并发控制机制分类:

1. 原子操作类 : 不能被打断的

2. 忙等待类 : 非阻塞, 采用循环的方式来判断一个资源可不可用, 执行的时间不宜过长

3. 阻塞类 : 发现临界资源不可用时,阻塞, 进入睡眠状态, 本质将任务加入到等待队列中去

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

```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;//错误, 不可以直接赋值, 原子变量只可以通过使用一些特殊的宏来完成向应的原子变量的赋值value

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

适用场合:共享资源为单个整型变量的互斥场合

# 四、自旋锁:基于忙等待的并发控制机制

a.定义自旋锁

spinlock_t  lock;

b.初始化自旋锁

spin_lock_init(spinlock_t *);

c.获得自旋锁

spin_lock(spinlock_t *);        //成功获得自旋锁立即返回,否则自旋(循环)在那里直到该自旋锁的保持者释放, 不会引起任务睡眠, 适用于任务上下文, 也适用异常上下文。适合运行时间短的程序, 占用cpu时间短。

spin_trylock(spinlock_t *);        //成功获得自旋锁立即返回真,否则返回假,而不是像上一个那样"在原地打转”

d.释放自旋锁

spin_unlock(spinlock_t *);

```

#include <linux/spinlock.h>

定义spinlock_t类型的变量lock

spin_lock_init(&lock)后才能正常使用spinlock

spin_lock(&lock);

临界区

spin_unlock(&lock);

```

适用场合:

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

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

3. 互斥问题

二. Code exercise 

三. 知识补充

1.任务上下文, 只要是任务都具有五种状态, 而异常上下文不隶属于任何一种任务,所以在异常上下文中不可以调用任何导致睡眠或者阻塞的函数。所以这种情况下就可以使用自旋锁.

2.原子变量, 用作共享资源中的单个整形变量, 对设备进行操作时可以用原子变量对其值判断分支做出不同的操作选择

3.上下文, 执行流, 代码段(任务上下文, 和异常上下文)

4.container_of的实现原理如下

总之相减代表二者相差多少个元素.

5.并发控制机制的分类

        原子类

        忙等待类

        阻塞类

6.原子变量,存取一些特殊的整形变量, 适用在共享资源为单个的整形变量

第四部分



一. 信号量、互斥锁、并发机制的选择原则

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

a.定义信号量

struct semaphore sem;(对应停车管理系统的提示牌和排队系统)

b.初始化信号量

void sema_init(struct semaphore *sem, int val);

参数:val为资源个数, 对应停车场内有多少个停车位

c.获得信号量P(资源数量减一)

int down(struct semaphore *sem);//深度睡眠

int down_interruptible(struct semaphore *sem);//浅度睡眠, 可被信号唤醒

d.释放信号量V

void up(struct semaphore *sem);

PV操作的理解, 对并发控制机制资源的数量的增加和减少

对信号量的理解:功能更强大的互斥锁, 并发控制机制资源数量比互斥锁更多

```

#include <linux/semaphore.h>

```

适用场合:任务上下文之间且临界区执行时间较长时的互斥或同步问题, 解决同步和异步问题的实现思路,对val参数的赋值为大于0的数, 和赋值成为0。

自旋锁也可以实现互斥和同步, 但只支持临界资源为一个的情况。

# 二、互斥锁:基于阻塞的互斥机制

只有一个互斥量

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. 临界区操作较长的应用建议采用阻塞类(采用忙阻塞类不占用cpu资源), 临界区很短的操作建议采用忙等待类。

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

4. 共享资源仅是一个简单整型量时用原子变量, 不可被打断, 阻塞和忙等待都可以。

二. Code exercise

三. 知识补充

停车场牌子为正数的时候,表示有多少个资源可用, 为负数的时候告诉我们有多少辆车在等待。

  • 信号是一种通信机制, 信号量是一种并发控制机制
  • 信号量, 停车场例子分析
  • 对于信号量这种机制的理解

第五部分



一. linux内核定时器

# 一、时钟中断

硬件有一个时钟装置,该装置每隔一定时间发出一个时钟中断(称为一次时钟嘀嗒-tick),对应的中断处理程序就将全局变量jiffies_64加1

jiffies_64   是一个全局64位整型, jiffies全局变量为其低32位的全局变量,程序中一般用jiffies

HZ:可配置的宏,表示1秒钟产生的时钟中断次数,一般设为100或200

# 二、延时机制

1. 短延迟:忙等待
   ```c

   1. void ndelay(unsigned long nsecs)

   2. void udelay(unsigned long usecs)

   3. void mdelay(unsigned long msecs)

   ```

  

2. 长延迟:忙等待

   使用jiffies比较宏来实现


 

  ```c

   time_after(a,b)    //a > b

   time_before(a,b)   //a < b

  
   //延迟100个jiffies

   unsigned long delay = jiffies + 100;

   while(time_before(jiffies,delay))

   {

       ;

   }

  
   //延迟2s

   unsigned long delay = jiffies + 2*HZ;

   while(time_before(jiffies,delay))

   {

       ;

   }

   ```

  

3. 睡眠延迟----阻塞类
   ```c

   void msleep(unsigned int msecs);

  

   unsigned long msleep_interruptible(unsigned int msecs);, 浅阻塞, 可以被信号唤醒

   ```

延时机制的选择原则:

1. 异常上下文中只能采用忙等待类, 异常不属于任务

2. 任务上下文短延迟采用忙等待类,长延迟采用阻塞类

# 三、定时器

(1)定义定时器结构体

在linux内核里面有一个定时器链表

```c

struct timer_list

{

struct list_head entry;         //entry进入

unsigned long expires;  // 期望的时间值 jiffies + x * HZ

void (*function)(unsigned long); // 时间到达后,执行的回调函数,软中断异常上下文

unsigned long data;      //传递给回调函数的参数

};

```

(2)初始化定时器

```c

init_timer(struct timer_list *)

```

(3)增加定时器 ------ 定时器开始计时

```c

void add_timer(struct timer_list *timer);            //将定时器加入到内核的定时器链表中去

```

(4)删除定时器 -------定时器停止工作


```c

int del_timer(struct timer_list * timer);

```

(5)修改定时器

```c

 int mod_timer(struct timer_list *timer, unsigned long expires);

```




/*使用定时器的套路*/

```c

定义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(....);//如需要定时器继续隔指定时间再次调用本函数

}

```

# 四、课堂练习—秒设备

二. Code exercise

三. 知识补充

  • 任务之间的切换需要花费时间
  • 在内核中有很多定时器链表
  • 互斥锁效率较低

第六部分



一. 内核内存管理 、动态分配

# 一、内核内存管理框架

内核将物理内存等分成N块4KB,称之为一页,每页都用一个struct page来表示,采用伙伴关系算法维护

内核地址空间划分图:

3G~3G+896M:低端内存,直接映射  虚拟地址 = 3G + 物理地址

​细分为:ZONE_DMA、ZONE_NORMAL

​分配方式:

```c

1. kmalloc:小内存分配,slab算法, 得到的地址连续

2. get_free_page:整页分配,2的n次方页,n最大为10

```

大于3G+896M:高端内存

​          细分为:vmalloc区、持久映射区、固定映射区

​        分配方式:vmalloc:虚拟地址连续,物理地址不连续, 因为要得到很大的内存空间

​                                             

# 二、内核中常用动态分配

## 2.1 kmalloc

​        函数原型:

```c

void *kmalloc(size_t size, gfp_t flags);

```

  kmalloc() 申请的内存位于直接映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因为存在较简单的转换关系,因此kmalloc的效率也比较高, 所以对申请的内存大小有限制,不能超过128KB。

较常用的 flags(分配内存的方法):

- **GFP_ATOMIC** —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;

- **GFP_KERNEL** —— 正常分配内存;

- **GFP_DMA** —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)。DAM去相应的外设控制器里面去取出数据加载到内存中来, 不需要CPU的参与,提升CPU的工作效率。

flags 的参考用法:

 |– 进程上下文,可以睡眠     GFP_KERNEL

 |– 异常上下文,不可以睡眠    GFP_ATOMIC

 |  |– 中断处理程序       GFP_ATOMIC

 |  |– 软中断          GFP_ATOMIC

 |  |– Tasklet, 也是一种软中断  GFP_ATOMIC

 |– 用于DMA的内存,可以睡眠   GFP_DMA | GFP_KERNEL

 |– 用于DMA的内存,不可以睡眠  GFP_DMA |GFP_ATOMIC

  

对应的内存释放函数为:

```c

void kfree(const void *objp);

```

```c, 会对申请到的空间进行初始化, kmalloc完之后在memset也一样

void *kzalloc(size_t size, gfp_t flags)

```

## 2.2 vmalloc

```c

void *vmalloc(unsigned long size);

```

  vmalloc() 函数则会在虚拟内存空间给出一块连续的内存区,但这片连续的虚拟内存在物理内存中并不一定连续。由于 vmalloc() 没有保证申请到的是连续的物理内存,因此对申请的内存大小没有限制,如果需要申请较大的内存空间就需要用此函数了。

对应的内存释放函数为:

```c

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 要快;

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

2.4 分配选择原则:

1. 小内存(< 128k)用kmalloc,大内存用vmalloc或get_free_page

2. 如果需要比较大的内存,并且要求使用效率较高时用get_free_page,否则用vmalloc

总结:为什么小端内核内存申请的效率更高?

解答:因为小端内存(内核空间的虚拟内存)是连续的,在实际的物理地址上也是连续的, 算法简单。

/*

 功能:向指定的寄存器中,写入数据。

 参数:value:待写入寄存器中的数据

  Address:寄存器的虚拟地址

*/

```

二. IO访问、 LED驱动

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

两种方式:

1. IO端口:X86上用IO指令访问

2. IO内存:ARM上使用的是,外设寄存器在SOC芯片手册上都有相应物理地址, 特殊功能寄存器

使用:在芯片手册查到硬件控制器的地址, 使用该函数映射出一个虚拟地址, 之后对该虚拟地址操作就是对该实际的物理内存地址进行操作。

IO内存访问接口:

将实际的物理地址和虚拟地址建立一个映射关系

```c

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寄存器](.\GPX2_7寄存器.jpg)

   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. 设计设备数据类型

   ```c

   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. 考虑需要支持的函数

   c. 模块入口:ioremap + 设置成输出

   d. 模块出口:iounmap

   e. 编写关灯函数和开灯函数,实现ioctl

三. 知识补充

1.模块中的init函数属于内核线程, 可以阻塞

2。使用arm的编译器

3.在开发板上的相应操作

4.字符, 一个字节, 块, 512个字节,  页,4kb

5.之前的malloc是在用户空间上申请内存, kmalloc kzlloc和get_free_page, vmalloc, 是在内核空间申请内存.

6.原子变量

原子变量需要各种处理器架构提供特殊的指令支持,ARM64处理器提供了以下指令。

(1)独占加载指令ldxr(Load Exclusive Register)。

(2)独占存储指令stxr(Store Exclusive Register)。

独占加载指令从内存加载32位或64位数据到寄存器中,把访问的物理地址标记为独占访问。

独占存储指令从寄存器存储32位或64位数据到内存中,检查目标内存地址是否被标记为独占访问。如果是独占访问,那么存储到内存中,并且返回状态值0来表示存储成功;否则不存储到内存中,并且返回1。

7.再次强调原子变量

有时候需要共享的资源可能只是一个简单的整型数值。例如在驱动程序中,需要对包含一个count的计数器。这个计数器表示有多少个应用程序打开了设备所对应的设备文件。

8.原子过程不会造成阻塞

第七部分



一.设备树的相关知识

# 一、设备树的起源

减少垃圾代码

减轻驱动开发工作量

驱动代码和设备信息分离

参考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键值对来表示(类似python中的字典)

节点语法:

```c

[label:] node-name[@unit-address] {   

[properties definitions];   

[child nodes];

};

label: 可选项,节点别名,为了缩短节点访问路径,后续节点中可以使用  &label 来表示引用指定节点

node-name: 节点名

unit-address: 设备地址,一般填写该设备寄存器组或内存块的首地址

properties definitions:属性定义

child nodes:子节点

```

属性语法:

```c

[label:] property-name = value;

[label:] property-name;

属性可以无值

有值的属性,可以有三种取值:

1. arrays of cells(1个或多个32位数据, 64位数据使用2个32位数据表示,空格分隔),用尖括号表示(< >)

2. string(字符串), 用双引号表示(" ")

3. bytestring(1个或多个字节,空格分隔),用方括号表示([])

4. 用,分隔的多值

   

```

# 四、特殊节点

## 4.1 根节点

根节点表示整块开发板的信息

```c

#address-cells   // 在子节点的reg属性中, 使用多少个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

```

## 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这类属性设置方法

二. LED驱动设备树版

# 六、常用接口

struct device_node  对应设备树中的一个节点

struct property 对应节点中一个属性

## 6.1 of_find_node_by_path

作用:通过在设备树源文件中相对于根节点的路径到对应的节点并返回节点地址

```c

/**

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

```c

/*

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

```c

/**

 * 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

```c

/*

功能:获得设备树中的中断号并进行映射

参数:node:设备节点

 index:序号

返回值:成功:中断号        失败:错误码

*/

unsigned int irq_of_parse_and_map(struct device_node *node, int index);

```

## 6.5 读属性值

of_property_read_string

```c

/*

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

```

读数值

```c

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)

```

判断属性是否存在

```c

int of_property_read_bool(const struct device_node *np,const char *propname)

```

读数组

```c

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引脚的电平

三. 知识补充

1.没有宏体的宏名通常将它使用成一个开关, 有没有定义着宏

2.<1 2>可以指定内存的起始地址和内存大小

3.交叉编译器的工作路径发生改变要修改相应的配置文件, 这样操作系统才找得到我们的编译器

bash一下, 刷新一下,启用bash

4.为什么可以找到设备树的地址?

设备树的本质也是一种文本文件, 里面记录着我们对硬件的补充依据,通过特殊的根据佳能设备树源文件编译成dtb文件加载到内存中去运行, 当然可以找到相应节点的地址喽.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@daiwei

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值