Linux驱动开发和裸机开发的区别**
- 裸机开发:
- 底层,和寄存器打交道,有些MCU提供库
- 驱动开发思维:
- 直接操纵寄存器不太现实
- 根据Linux下的各种驱动框架进行开发,一定要满足框架,也就是Linux各种驱动框架的掌握
- 驱动最终表现就是/dev/xxx文件。打开,关闭,读写。。。
- 现在新的内核支持设备树,这是一个.dts文件,此文件描述了板子的设备信息
- Linux驱动开发分类:
- 字符设备驱动:最多的
- 块设备驱动:存储
- 网络设备驱动
==一个设备不一定只属于摸某一个类型==
字符设备驱动开发基础实验
- 驱动就是获取外设或者传感器数据,控制外设。数据会提交给应用程序,Linux驱动编写既要编写一个驱动,还要我们编写一个简单的测试应用程序,APP。单片机下,驱动和应用都是放到一个文件里面,也就是杂糅到一起。Linux下驱动和应用是完全分开的
字符驱动开发的具体流程:
- Linux里面的一切皆文件,驱动设备就是一个/dev/下的文件,/dev/led。应用程序调用open函数打开一个设备的时候,比如led。应用程序通过write函数向/dev/led写数据,比如写1表示打开,写0表示关闭,如果要关闭就是close函数
- 编写驱动的时候也需要编写驱动对应的open,close,write函数,字符设备驱动有一个file_operations结构体
第一个Linux驱动实验
字符设备驱动框架:字符设备驱动的编写主要就是驱动对应的close,read等函数的编写,其实就是file_operations结构体成员变量的编写
驱动模块的加载与卸载:
- linux驱动可以编译到kernel里面,也就是zImage,也可以编译成模块.ko,测试的时候只需要加载.ko模块就可以
- 将编译出来的.ko文件放到根文件系统中,加载驱动会用到加载命令insmod,modprobe。移除驱动使用命令rmmod
- 驱动模块加载成功以后可以使用lsmod查看一下
- 卸载模块使用rmmod命令
字符设备的注册与注销:
- 我们需要向系统注册一个字符设备,使用函数register_chrdev
- 注销驱动的时候需要注销掉前面注册的字符设备,使用函数unregister_chrdev注销字符设备
设备号:
- linux内核里面使用typedef __kernel_dev_t dev_t;
- linux内核将设备号分成两部分,也即是主设备号和次设备号,主设备号占用前12位,次设备号占用低20位
- 设备号的操作函数或宏,从dev_t获取主设备号和次设备号MAJOR(dev_t),MINOR(dev_t),也可以使用主设备号和次设备号构成dev_t,通过MKDEV(major,minor)
file_operation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
unsigned (*mmap_capabilities)(struct file *);
};应用程序编写:
Linux下一切接文件,首先要openchedevbase虚拟设备驱动的编写
完善:
- chrdevbase_read驱动函数的编写,驱动给应用传递数据的时候需要用到copy_to_user函数
==总结==
首先要编写注册和退出函数,传入的参数有主设备号,设备姓名,函数表。还有退出函数,
接着还要设置open,write,read,close等函数,同时在应用程序也要设置对应的调用,在设置例如write调用的时候,内核的值不能直接赋给应用,应用的值也不可以直接赋值给内核,都要通过相应的函数如copy_to_user copy_from_user等
LED驱动实验(直接操作寄存器版)
- 地址映射:
- 裸机的LED实验就是操作6ULL的寄存器
- linux驱动开发也可以操作寄存器,但是Linux下不能直接对寄存器的物理地址进行读写操作,例如寄存器A物理地址为0x01010101,裸机的时候可以直接对0x01010101这个物理地址进行操作,但是在linux不行,因为linux会使能mmu
在linux中操作的都是虚拟地址,所以需要先得到0x01010101对应的物理地址 - 获得物理地址对应的虚拟地址使用ioremap函数,第一个参数就是物理地址起始大小,第二个参数就是要转换的字节数量,例如va=ioremap(0x01010101,10)
- 卸载驱动的时候iounmap(va)
- 驱动程序的编写:
- 初始化时钟,IO,GPIO编写
- 初始化完进行测试,如果烧写用的是正点原子提供的linux内核,这个时候LED默认被配置为心跳灯,必须要先关闭心跳灯
Linux新字符设备驱动实验
以前的缺点:
使用register_chrdev函数注册字符设备,浪费了很多设备号,而且需要手动指定设备号
新申请函数:==int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)==
释放:==unregister_chrdev_region(dev_t from,unsigned count)==
如果想要指定设备号使用:register_chredev_region(dev_t from,unsigned count,const char *name)一般是给定主设备号,然后用MKDEV构建完整的dev_t,一般次设备号选择为0所以在编写时要考虑两种情况,
字符设备注册:cdev结构体表示字符设备,然后使用cdev_init函数来初始化cdev:cdev_init(struct cdev *cdev, const struct file_operations *fops)
初始化完cdev以后使用cdev_add向 Linux 系统添加字符设备 :int cdev_add(struct cdev *p, dev_t dev, unsigned count)
自动创建设备节点
在以前的实验中都需要手动调用mknod设备节点,为此2.6内核引入了udev机制替换devfs,udev机制提供了一个热插拔管理,可以在加载驱动的时候自动创建devxxx设备文件,busybox提供了udev的简化版本mdev
struct class *class_create (struct module *owner, const char *name) 创建类
void class_destroy(struct class *cls); 删除类
创建完类之后还要用device_creat函数创建设备,函数原型如下:
1
2
3
4
5struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)同样卸载需要使用函数void device_destroy(struct class *class, dev_t devt)
==总结:==
首先创建设备结构体
使用分配设备号函数来分配设备:
- 注册:alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)函数
- 注销:unregister_chrdev_region(dev_t from, unsigned count)
接着再在设备结构体下创建cdev结构体
- cdev结构体创建好后通过cdev_init初始化
- 初始化:cdev_init(struct cdev *cdev, const struct file_operations *fops)
- 通过 cdev_add函数将结构体添加到linux中:
- 初始化:cdev_add(struct cdev *p, dev_t dev, unsigned count)
- 删除结构体:cdev_del(struct cdev *p)
- cdev结构体创建好后通过cdev_init初始化
接着要配置自动创建设备树节点,首先要在设备结构体下创建class类:
通过class_create (struct module *owner, const char *name) 创建类
还要通过device_create(struct class *class,
struct device *parent,
dev_t devt
void *drvdata,
const char *fmt,… )在class类下创建设备还要删除设备device_destroy(struct class *class, dev_t devt)
删除类class_destroy(struct class *cls);
Linux设备树
设备树就是设备和树,描述设备树的文件叫DTS,这个DTS文件采用树形结构描述板级设备,在单片机驱动中,比如W25Q,SPI,速度都是在. c文件里面写死的,板级信息都写到.c里面导致linux内核臃肿,因此将板子信息做成独立的格式,文件后缀为.dts。一个平台或者机器对应一个.dts
- .dts相当于.c就是DTS源码文件,DTC工具相当于gcc编译器,将.dts编译成.dtb文件,dtb相当于bin文件
- 通过make dtbs编译所有的dts文件
dts基本语法
- dts也有头文件,扩展名为.dtsi,可以将一款SOC的其他所有设备/平台共有的信息提出来作为一个通用的.dtsi文件
- dts也是/开始
- 从根节点开始描述设备信息
- 在根节点外有一些&cpu0这样的语句是追加
- 节点名字完整的要求:node-name@unit-address(unit-adress一般是寄存器的起始地址,有时候是iic的设备地址或者其他含义,具体含义具体分析)设备树里面常常遇到如下所示节点名字:intc:interrupt-control@00a01000(:前面是标签,后面才是名字)
设备树在系统里面的体现
系统启动以后可以看在根文件系统里面看到设备树的节点信息,在/proc/device-tree目录下存放着设备树信息,内核启动的时候会解析设备树然后在/proc/device-tree目录下呈现出来。
==特殊节点== :
- aliases子节点意思是别名
- chosen子节点节点,主要目的是将uboot中bootargs环境变量传递给linux内核作为命令行参数cmd line。chosen节点中包含bootargs属性,属性值和uboot的bootargs一致,uboot拥有bootargs这个环境变量和dtb
==特殊属性==
compatimble属性,值是字符串,用来描述兼容性
- 根节点下的compatimble属性。内核启动的时候会检查是否支持此平台,不使用设备树的时候通过machine id来判断是否支持此id,使用设备树的时候通过compatimble属性
model属性:描述模块信息
status属性:描述设备状态
#address-cells和#size-cells属性: a用于表示地址信息所占字长,s表示长度信息所占字长
reg属性:reg 属性的值一般是(address, length)对。 reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息
Linux内核的OF操作函数
在驱动中使用OF函数获取设备树属性内容
驱动要想获取到设备树节点内容,首先要找到节点,
of_find_node_by_name(struct device_node *from, const char *name)
函数参数和返回值含义如下:from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值: 找到的节点,如果为 NULL 表示查找失败
of_find_node_by_type(struct device_node *from, const char *type)
of_find_compatible_node (struct device_node *from,const char *type,const char *compatible)
inline struct device_node *of_find_node_by_path(const char *path)
函数参数和返回值含义如下:
- path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径。
- 返回值: 找到的节点,如果为 NULL 表示查找失败
获取节点中参数的值
pinctl和gpio子系统
- 6ull的gpio使用
- 设置pin的复用和电器属性
- 配置gpio
- pinctrl子系统:借助pinctrl来设置一个pin的复用和电器属性,根据设备的类型创建对应的子节点,然后设备所用PIN都放到此节点
- 设备树里面的设备节点是如何根据驱动匹配的呢?:通过compatible属性,此属性是字符串列表,驱动文件里面有一个描述驱动兼容性的东西,当设备树节点的compatible属性和驱动里面的兼容性字符串匹配的时候表示设备和驱动匹配了,所以只需要全局搜索设备节点里面的compatible属性的值,看看在哪个.c文件里面有,那么此文件就是驱动文件。
- GPIO子系统:使用gpio子系统来使用gpio
- gpio的使用方式:定义了一个cd-gpios属性。
- 如何从设备树中获取使用的GPIO信息:of函数
- 首先获取到GPIO所处的节点 of_find_node_by_path
- 获取gpio ,of_get_name_gpio,返回值就是gpio编号
- 请求此编号的gpio:
- gpio_request请求
- gpio_free释放
- 设置gpio输入或输出:
- gpio_direction_input
- gpio_direction_output
- 如果是输入,通过gpio_get_value读取,如果是输出通过gpio_set_value设置gpio值
beep蜂鸣器实验
1 |
|
linux并发与竞争
原子操作:原子变量与原子位
函数 描述 ATOMIC_INIT(int i) 定义原子变量的时候对其初始化。 int atomic_read(atomic_t *v) 读取 v 的值,并且返回。 void atomic_set(atomic_t *v, int i) 向 v 写入 i 值。 void atomic_add(int i, atomic_t *v) 给 v 加上 i 值。 void atomic_sub(int i, atomic_t *v) 从 v 减去 i 值。 void atomic_inc(atomic_t *v) 给 v 加 1,也就是自增。 void atomic_dec(atomic_t *v) 从 v 减 1,也就是自减 int atomic_dec_return(atomic_t *v) 从 v 减 1,并且返回 v 的值。 int atomic_inc_return(atomic_t *v) 给 v 加 1,并且返回 v 的值。 int atomic_sub_and_test(int i, atomic_t *v) 从 v 减 i,如果结果为 0 就返回真,否则返回假 int atomic_dec_and_test(atomic_t *v) 从 v 减 1,如果结果为 0 就返回真,否则返回假 int atomic_inc_and_test(atomic_t *v) 给 v 加 1,如果结果为 0 就返回真,否则返回假 int atomic_add_negative(int i, atomic_t *v) 给 v 加 i,如果结果为负就返回真,否则返回假 自旋锁:用于多核SMP,使用自旋锁要注意死锁现象的发生
函数 描述 DEFINE_SPINLOCK(spinlock_t lock) 定义并初始化一个自选变量。 int spin_lock_init(spinlock_t *lock) 初始化自旋锁。 void spin_lock(spinlock_t *lock) 获取指定的自旋锁,也叫做加锁。 void spin_unlock(spinlock_t *lock) 释放指定的自旋锁。 int spin_trylock(spinlock_t *lock) 尝试获取指定的自旋锁,如果没有获取到就返回 0 int spin_is_locked(spinlock_t *lock) 检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0。 自旋锁要防止中断占用导致死锁发生所以有相应的API函数在获取锁之前关闭中断:
函数 描述 void spin_lock_irq(spinlock_t *lock) 禁止本地中断,并获取自旋锁。 void spin_unlock_irq(spinlock_t *lock) 激活本地中断,并释放自旋锁。 void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) 保存中断状态,禁止本地中断,并获取自旋锁。 void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。 信号量:semaphore
函数 描述 DEFINE_SEAMPHORE(name) 定义一个信号量,并且设置信号量的值为 1。 void sema_init(struct semaphore *sem, int val) 初始化信号量 sem,设置信号量值为 val。 void down(struct semaphore *sem) 获取信号量,因为会导致休眠,因此不能在中断中使用。 int down_trylock(struct semaphore *sem); 尝试获取信号量,如果能获取到信号量就获取,并且返回 0。如果不能就返回非 0,并且不会进入休眠。 int down_interruptible(struct semaphore *sem) 获取信号量,和 down 类似,只是使用 down 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。 void up(struct semaphore *sem) 释放信号量 互斥体:
函数 描述 DEFINE_MUTEX(name) 定义并初始化一个 mutex 变量。 void mutex_init(mutex *lock) 初始化 mutex。 void mutex_lock(struct mutex *lock) 获取 mutex,也就是给 mutex 上锁。如果获取不到就进休眠。 void mutex_unlock(struct mutex *lock) 释放 mutex,也就给 mutex 解锁。 int mutex_trylock(struct mutex *lock) 尝试获取 mutex,如果成功就返回 1,如果失败就返回 0。 int mutex_is_locked(struct mutex *lock) 判断 mutex 是否被获取,如果是的话就返回1,否则返回 0。 int mutex_lock_interruptible(struct mutex *lock) 使用此函数获取信号量失败进入休眠以后可以被信号打断。
按键输入实验(直接读io)
1 |
|
linux内核定时器实验
- 软件定时器原理是依靠系统定时器来驱动
- linux内核频率可以通过图形化界面配置
内核定时器:
软件定时器不像硬件定时器一样直接给周期值。设置期满以后时间点,
定时处理函数
内核定时器不是周期性的,一次定时时间到了以后就会关闭,除非重新打开
内核定时器API函数:
处理绕回:
函数 描述 time_after(unkown, known) unkown 通常为 jiffies, known 通常是需要对比的值。 time_before(unkown, known) time_after_eq(unkown, known) time_before_eq(unkown, known) jiffies函数与ms,us,ns之间的转换:
函数 描述 int jiffies_to_msecs(const unsigned long j) 将 jiffies 类型的参数 j 分别转换为对应的毫秒、微秒、纳秒。 int jiffies_to_usecs(const unsigned long j) u64 jiffies_to_nsecs(const unsigned long j) long msecs_to_jiffies(const unsigned int m) 将毫秒、微秒、纳秒转换为 jiffies 类型。 long usecs_to_jiffies(const unsigned int u) unsigned long nsecs_to_jiffies(u64 n) 内核定时器函数:首先要定义一个timer_list变量表示定时器,timer_list结构体的expires成员变量表示超时时间单位为节拍数。比如我们现在需要定义一个周期为 2 秒的定时器,那么这个定时器的超时时间就是 jiffies+(2HZ),因此 expires=jiffies+(2HZ)。 function 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定时处理函数。
init_timer 函数init_timer 函数负责初始化 timer_list 类型变量,当我们定义了一个 timer_list 变量以后一定要先用 init_timer 初始化一下。 init_timer 函数原型如下:void init_timer(struct timer_list *timer)
add_timer 函数add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer
函数向内核注册定时器以后,定时器就会开始运行,函数原型如下:void add_timer(struct timer_list *timer)
del_timer 函数del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。 del_timer 函数原型如下:int del_timer(struct timer_list * timer)
del_timer_sync 函数del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除, del_timer_sync 不能使用在中断上下文中。 del_timer_sync 函数原型如下所示:int del_timer_sync(struct timer_list *timer)
mod_timer 函数mod_timer 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时器!函数原型如下:int mod_timer(struct timer_list *timer, unsigned long expires)
步骤:
先定义定时器结构体 struct timer_list timer
接着初始化定时器:init_timer(&timer);
设置定时器处理函数:timer.function = function;
编写定时器处理函数
设置定时器定时时间:timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
设置要传递给定时器函数的参数:
timer.data = (unsigned long)&dev; /* 将设备结构体作为参数 */
启动定时器:add_timer(&timer);
删除定时器:del_timer(&timer);
linux中断实验
进程,线程,中断的核心:栈
- 程序被中断时通过栈保存现场:
- 程序被中断时通过栈保存现场:
Linux中断:
- 先知道要使用的中断对应的中断号
- 申请request_irq,此函数会激活中断
- 如果不用中断那就要用free_irq释放:void free_irq(unsigned int irq,void *dev)
- 中断处理函数:irqreturn_t (*irq_handler_t) (int, void *)
- 使能和禁止中断:
- void enable_irq(unsigned int irq)
- void disable_irq(unsigned int irq)
中断的上半部和下半部:==中断一定要处理的越快越好==
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用长时间处理就可以放在上半部完成,就是上面的中断处理就是上半部处理
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来交给下半部去执行
软中断: Linux 内核使用结构体 softirq_action {
void (*action)(struct softirq_action *);
};表示软中断 在 kernel/softirq.c 文件中一共定义了 10 个软中断,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14enum
{
HI_SOFTIRQ=0, /* 高优先级软中断 */
TIMER_SOFTIRQ, /* 定时器软中断 */
NET_TX_SOFTIRQ, /* 网络数据发送软中断 */
NET_RX_SOFTIRQ, /* 网络数据接收软中断 */
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, /* tasklet 软中断 */
SCHED_SOFTIRQ, /* 调度软中断 */
HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */
RCU_SOFTIRQ, /* RCU 软中断 */
NR_SOFTIRQS
};- 要使用软中断要写注册:使用函数:void open_softirq(int nr, void (*action)(struct softirq_action *))
- 注册以后使用void raise_softirq(unsigned int nr) 触发
下半部分耗时不是很长:tasklet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37假设硬件中断A的上半部函数为irq_top_half_A,下半部为irq_bottom_half_A。
使用情景化的分析,才能理解上述代码的精华。
a. 硬件中断A处理过程中,没有其他中断发生:
一开始,preempt_count = 0;
上述流程图①~⑨依次执行,上半部、下半部的代码各执行一次。
b. 硬件中断A处理过程中,又再次发生了中断A:
一开始,preempt_count = 0;
执行到第⑥时,一开中断后,中断A又再次使得CPU跳到中断向量表。
注意:这时preempt_count等于1,并且中断下半部的代码并未执行。
CPU又从①开始再次执行中断A的上半部代码:
在第①步preempt_count等于2;
在第③步preempt_count等于1;
在第④步发现preempt_count等于1,所以直接结束当前第2次中断的处理;
注意:重点来了,第2次中断发生后,打断了第一次中断的第⑦步处理。当第2次中断处理完毕,CPU会继续去执行第⑦步。
可以看到,发生2次硬件中断A时,它的上半部代码执行了2次,但是下半部代码只执行了一次。
所以,同一个中断的上半部、下半部,在执行时是多对一的关系。
c. 硬件中断A处理过程中,又再次发生了中断B:
一开始,preempt_count = 0;
执行到第⑥时,一开中断后,中断B又再次使得CPU跳到中断向量表。
注意:这时preempt_count等于1,并且中断A下半部的代码并未执行。
CPU又从①开始再次执行中断B的上半部代码:
在第①步preempt_count等于2;
在第③步preempt_count等于1;
在第④步发现preempt_count等于1,所以直接结束当前第2次中断的处理;
注意:重点来了,第2次中断发生后,打断了第一次中断A的第⑦步处理。当第2次中断B处理完毕,CPU会继续去执行第⑦步。
在第⑦步里,它会去执行中断A的下半部,也会去执行中断B的下半部。
所以,多个中断的下半部,是汇集在一起处理的。
总结:
a. 中断的处理可以分为上半部,下半部
b. 中断上半部,用来处理紧急的事,它是在关中断的状态下执行的
c. 中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行的
d. 中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断
e. 中断上半部执行完后,触发中断下半部的处理
f. 中断上半部、下半部的执行过程中,不能休眠:中断休眠的话,以后谁来调度进程啊?下半部要做的事情太多并且很复杂:工作队列:
在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但是毕竟整个中断的处理还没走完,这期间APP是无法执行的。所以,如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和APP都一样竞争执行,APP有机会执行,系统不会卡顿。这个内核线程是系统帮我们创建的,一般是kworker线程,内核中有很多这样的线程,kworker线程要去“工作队列”(work queue)上取出一个一个“工作”(work),来执行它里面的函数。使用:
a. 创建work:
你得先写出一个函数,然后用这个函数填充一个work结构体。比如:
b. 要执行这个函数时,把work提交给work queue就可以了:
c. 谁来执行work中的函数?
不用我们管,schedule_work函数不仅仅是把work放入队列,还会把kworker线程唤醒。此线程抢到时间运行时,它就会从队列中取出work,执行里面的函数。d. 谁把work提交给work queue?
在中断场景中,可以在中断上半部调用schedule_work函数。
新技术:threaded irq:使用线程来处理中断,并不是什么新鲜事。使用work就可以实现,但是需要定义work、调用schedule_work,较麻烦,所以内核提供了函数:
- 可以只提供thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。
- 以前用work来线程化地处理中断,一个worker线程只能由一个CPU执行,多个中断的work都由同一个worker线程来处理,在单CPU系统中也只能忍着了。但是在SMP系统中,明明有那么多CPU空着,你偏偏让多个中断挤在这个CPU上?新技术threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个CPU上执行,这提高了效率。
设备树中断节点信息:
#interrupt-cells指定interrupt的cells数量
interrupts属性第一个cells就是gpio编号,假如父节点是&gpio5,那就是gpio5_io00,第二个就是触发方式:
- 低电平触发:1
- 高电平触发:2
- 上升沿触发:4
- 下降沿触发:8
interr-parent指定父中断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16i2c@7000c000 {
gpioext: gpio-adnp@41 {
compatible = "ad,gpio-adnp";
interrupt-parent = <&gpio>;
interrupts = <160 1>;
gpio-controller;
interrupt-controller;
};
......
};
在代码中获得中断:
对于platform_device一个节点能被转换为platform_device,如果它的设备树里指定了中断属性,那么可以从platform_device中获得“中断资源”:
1
2
3
4
5
6
7
8
9
10/**
* platform_get_resource - get a resource for a device
* @dev: platform device
* @type: resource type // 取哪类资源?IORESOURCE_MEM、IORESOURCE_REG
* // IORESOURCE_IRQ等
* @num: resource index // 这类资源中的哪一个?
*/
struct resource *platform_get_resource(struct platform_device *dev,
unsigned int type, unsigned int num);对于I2C,SPI设备:
- 对于I2C设备节点,I2C总线驱动在处理设备树里的I2C子节点时,也会处理其中的中断信息。一个I2C设备会被转换为一个i2c_client结构体,中断号会保存在i2c_client的irq成员里,代码如下:
- 对于SPI设备节点,SPI总线驱动在处理设备树里的SPI子节点时,也会处理其中的中断信息。一个SPI设备会被转换为一个spi_device结构体,中断号会保存在spi_device的irq成员里,代码如下:
调用of_irq_get获得中断号
如果你的设备节点既不能转换为platform_device,它也不是I2C设备,不是SPI设备,那么在驱动程序中可以自行调用of_irq_get函数去解析设备树,得到中断号。对于GPIO:可以使用gpio_to_irq或gpiod_to_irq获得中断号。:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18举例,假设在设备树中有如下节点:
gpio-keys {
compatible = "gpio-keys";
pinctrl-names = "default";
user {
label = "User Button";
gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;
gpio-key,wakeup;
linux,code = <KEY_1>;
};
};
那么可以使用下面的函数获得引脚和flag:
button->gpio = of_get_gpio_flags(pp, 0, &flags);
bdata->gpiod = gpio_to_desc(button->gpio);
再去使用gpiod_to_irq获得中断号:
irq = gpiod_to_irq(bdata->gpiod);
步骤:
先通过imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd,i );或者gpio_to_irq(unsigned int gpio)获取中断号
接着通过imx6uirq.irqkeydesc[0].handler = key0_handler设置中断处理函数
再通过request_irq(imx6uirq.irqkeydesc[i].irqnum,imx6uirq.irqkeydesc[i].handler,IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,imx6uirq.irqkeydesc[i].name, &imx6uirq);申请中断
再编写中断处理函数即可
linux阻塞和非阻塞实验
- 阻塞和非阻塞io
- 当资源不可用的时候,引用程序就会挂起,当资源可用的时候唤醒任务,应用程序使用open打开驱动文件,默认是以阻塞方式打开
- 当资源不可用,应用程序就会轮询查看或放弃,会有超时处理机制,应用程序在使用open打开驱动文件的时候使用O_NOBLOCK。
- 等待队列:
- wait_queue_head_t 需要定义一个,定义以后使用int_waitqueue_head函数初始化,或者使用宏DECLARE_WAIT_QUEUE_HEAD
- 等待队列项:wait_queue_t表示等待队列项,或者使用宏DECLARE_WAITQUEUE(name,tsk);
- 添加队列项到等待队列头add_wait_queue
- 移除等待队列项:资源可用的时候使用remove_wait_queue函数移除
- 唤醒wake_up
linux异步通知实验
- 软件层次上对中断机制的模拟
plantform设备驱动实验
linux驱动的分离与分层:
- 分离
- 将驱动分离,主机控制器驱动和设备驱动,主机控制器由半导体厂商提供,在linux驱动框架下编写具体的设备驱动
- 中间联系就是核心层
- 分层:
- 核心层
- 设备层
- 分离
驱动-总线-设备:
总线:总线代码由linux内核提供,数据类型为bus_type向内核注册总线使用bus_register
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
struct device_attribute *dev_attrs; /* use dev_groups instead */
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*online)(struct device *dev);
int (*offline)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct dev_pm_ops *pm;
const struct iommu_ops *iommu_ops;
struct subsys_private *p;
struct lock_class_key lock_key;
};- 总线主要工作就是完成总线下的设备和驱动之间的匹配。通过:int (*match)(struct device *dev, struct device_driver *drv);函数
- 向linux内核注册总线,使用bus_register函数
- 卸载使用bus_unregister
驱动:驱动就是具体的设备驱动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const char *mod_name; /* used for built-in modules */
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
struct driver_private *p;
};- 驱动和设备匹配以后,驱动里面的probe函数就会执行
- 使用driver_register注册驱动
设备:设备属性,包括地址范围,如果是IIC话还有IIC器件地址,速度设备的数据类型为device通过device_register向内核注册设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71struct device {
struct device *parent;
struct device_private *p;
struct kobject kobj;
const char *init_name; /* initial name of the device */
const struct device_type *type;
struct mutex mutex; /* mutex to synchronize calls to
* its driver.
*/
struct bus_type *bus; /* type of bus device is on */
struct device_driver *driver; /* which driver has allocated this
device */
void *platform_data; /* Platform specific data, device
core doesn't touch it */
void *driver_data; /* Driver data, set and get with
dev_set/get_drvdata */
struct dev_pm_info power;
struct dev_pm_domain *pm_domain;
struct dev_pin_info *pins;
int numa_node; /* NUMA node this device is close to */
u64 *dma_mask; /* dma mask (if dma'able device) */
u64 coherent_dma_mask;/* Like dma_mask, but for
alloc_coherent mappings as
not all hardware supports
64 bit addresses for consistent
allocations such descriptors. */
unsigned long dma_pfn_offset;
struct device_dma_parameters *dma_parms;
struct list_head dma_pools; /* dma pools (if dma'ble) */
struct dma_coherent_mem *dma_mem; /* internal for coherent mem
override */
struct cma *cma_area; /* contiguous memory area for dma
allocations */
/* arch specific additions */
struct dev_archdata archdata;
struct device_node *of_node; /* associated device tree node */
struct fwnode_handle *fwnode; /* firmware device node */
dev_t devt; /* dev_t, creates the sysfs "dev" */
u32 id; /* device instance */
spinlock_t devres_lock;
struct list_head devres_head;
struct klist_node knode_class;
struct class *class;
const struct attribute_group **groups; /* optional groups */
void (*release)(struct device *dev);
struct iommu_group *iommu_group;
bool offline_disabled:1;
bool offline:1;
};- 向总线注册设备的时候使用设备注册函数device_register
platform平台驱动模型:
对于SOC内部的rtc,timer这种不好归结为具体的总线,为此linux内核提出了一个虚拟的总线:plantform,plantform设备和驱动
plantform总线注册:plantform_bus_init,对于platform平台,plantform_match负责驱动和设备的匹配
platform驱动结构体为:
1
2
3
4
5
6
7
8
9
10struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};- platform_driver_register向内核注册platform驱动
platform设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17struct platform_device {
const char *name;
int id;
bool id_auto;
struct device dev;
u32 num_resources;
struct resource *resource;
const struct platform_device_id *id_entry;
char *driver_override; /* Driver name to force a match */
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
struct pdev_archdata archdata;
};- 有设备树,修改设备树设备节点即可
- 向内核注册platform驱动的时候,如果驱动和设备匹配成功,最终会执行plantform_driver下的probe函数
platform匹配过程:根据前面的分析,驱动和设备的匹配是通过bus下面的match函数实现的,platform下面的match函数就是platform_match函数
- 如果使用设备树通过of_driver_match_device匹配
- 然后根据platform_dirver下的id_table匹配:platform_match_id
- 最终就是比较字符串,就是platform_device和platform_driver下的name,无设备树形况下使用
设备树platform平台驱动流程:
- 先在设备树中创建节点,重点是设置好compatible的值
- 创建platform驱动结构体,
- .driver中的.name自己设置
- 定义of_match_name结构体数组,数组中填入设备的compatible信息然后将.driver中的of_match_table与其匹配
- .probe就是匹配后自动执行的函数,可以在这里初始化设备
- .remove就是移除 platform 驱动的时候此函数会执行 ,在这里注销设备
linux自带led驱动
通过menuconfig配置后修改设备树即可,主要是设备树的编写需要注意规范,主要通过Documentation/devicetree/bindings/ 中的文档标准编写,例如led:
1 | ①、创建一个节点表示 LED 灯设备,比如 dtsleds,如果板子上有多个 LED 灯的话每个 LED |
linux杂项misc驱动实验
- misc设备的主设备号为10
- misc设备会自动创建cdev,不需要手动创建
- misc驱动是基于platform
- misc驱动编写的核心就是初始化miscdevice结构体变量
1 | 以前我们需要自己调用一堆的函数去创建设备,比如在以前的字符设备驱动中我们会使用 |
流程:
- 首先基于platform基础上
- 定义MISC结构体设置 minor、 name 和 fops 这三个成员变量。 minor 表示子设备号, MISC 设备的主设备号为 10 :struct miscdevice
- 注册MISC设备:int misc_register(struct miscdevice * misc)
- 注销设备:misc_deregister(&beep_miscdev);
linux INPUT子系统驱动实验
应用编程:
- 轮询查询
- 休眠-唤醒
- poll方式
- 异步通知
具体参考代码
input子系统也是字符设备,input核心层会帮我们注册input字符设备驱动,既然内核以及帮我们写好了input驱动,那我们要干嘛呢?需要我们去完善具体的输入设备,完善输入设备的时候就要按照input子系统驱动框架的要求来:
- 申请并注册input_dev,使用input_allocate_device()evbit表示输入事件,比如按键对应的事件就是EV_KEY,如果要连按还要加EV_REP。
- 设置按键对应的键值,也就是keybit,初始化完成input_dev以后,需要向内核注册,使用input_register_device()
- 按键按下以后上报事件,比如对于按键而言就是在按键中断服务函数,或者消抖定时器函数里面获取按键按下情况并且上报,可以使用input_event()
- 报告输入事件以后还要使用input_sync()做同步。
- 应用程序可以通过input_event来获取输入事件数据,包括按键值,input_event是一个结构体
流程:
定义input设备结构体:struct input_dev *inputdev;
通过inputdev = input_allocate_device(struct input_dev *dev ); 函数申请一个 input_dev
设置事件和事件值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/*********第一种设置事件和事件值的方法***********/
__set_bit(EV_KEY, inputdev->evbit); /* 设置产生按键事件 */
__set_bit(EV_REP, inputdev->evbit); /* 重复事件 */
__set_bit(KEY_0, inputdev->keybit); /*设置产生哪些按键值 */
/************************************************/
/*********第二种设置事件和事件值的方法***********/
keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) |
BIT_MASK(EV_REP);
keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |=
BIT_MASK(KEY_0);
/************************************************/
/*********第三种设置事件和事件值的方法***********/
keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) |
BIT_MASK(EV_REP);
input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
/************************************************/使用input_register_device(struct input_dev *dev ); 注册input_dev
在按键按下时上报
- input_report_key(struct input_dev *dev,unsigned int code, int value);
- void input_event(struct input_dev *dev,unsigned int type,unsigned int code,int value)
- dev:需要上报的 input_dev。
- type: 上报的事件类型,比如 EV_KEY。
- code: 事件码,也就是我们注册的按键值,比如 KEY_0、 KEY_1 等等
- value:事件值,比如 1 表示按键按下, 0 表示按键松开。
上报完后要同步:input_sync(struct input_dev *dev );
注销:input_unregister_device(struct input_dev *dev );
释放:input_free_device(struct input_dev *dev );
编写应用程序:
- 定义input_event变量,存放输入事件信息:static struct input_event inputevent;
- 通过read函数读取
linux lcd驱动实验
- framebuffer设备:RGBLCD屏幕,frambuffer是一种机制,应用程序操作驱动LCD显存的一种机制,因为应用程序需要通过操作显存来在LCD上显示字符,图片等信息,当我们编写好LCD驱动后会生成一个名为/dev/fbX的设备,应用程序通过访问这个设备就可以访问LCD
linux RTC驱动实验
- rtc是一个标准的字符设备驱动,在内核中被内核抽象为rtc_device结构体
linux iic驱动实验
IIC传输数据格式:
- 写操作:
- 主设备发出start信号
- 接着发出设备地址(用来确定是往哪个芯片写数据)方向(读/写,0表示写,1表示读)
- 从设备回应ACK信号
- 主设备发送==一个字节==的数据给从设备,等待回应
- 每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。
- 数据发送完之后,主芯片就会发送一个停止信号
- 读操作:
- 主设备发出start信号
- 接着发出设备地址(用来确定是往哪个芯片写数据)方向(读/写,0表示写,1表示读)
- 从设备回应ACK信号
- 从设备发送一个字节的数据给主设备,并等待回应
- 每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。
- 数据发送完之后,主芯片就会发送一个停止信号。
- 写操作:
IIC传输流程:
- start信号:SCL保持高电平,SDA从高变低表示传输开始
- 结束信号: SCL为高电平时,SDA从低变高
- SCL保持低电平,SDA在SCL低电平期间将电平拉高或者拉低
- SCL电平升高,此时SDA电平保持稳定,为高发出1,为低发出0
裸机下的IIC驱动框架:
- 首先编写IIC控制器驱动,向外提供一个i2c_master_transfer函数
- 不管是什么IIC芯片
SMBus协议:是基于IIC协议的,但是SMBus要求更严格,属于IIC协议的子集
使用一句话概括I2C传输:APP通过I2C Controller与I2C Device传输数据。
在linux内核中用i2c_adapter表示一个IIC BUS或者称为I2C Controller
该结构体如下所示:重要成员变量:
- nr:第几个I2C BUS(I2C Controller)
- i2c_algorithm,里面有该I2C BUS的传输函数,用来收发I2C数据
使用使用i2c_client来表示一个I2C Device
在上面的i2c_algorithm结构体中可以看到要传输的数据被称为:i2c_msg:
一个i2c_msg要么是读,要么是写
举例:设备地址为0x50的EEPROM,要读取它里面存储地址为0x10的一个字节,应该构造几个i2c_msg?
要构造2个i2c_msg
第一个i2c_msg表示写操作,把要访问的存储地址0x10发给设备
第二个i2c_msg表示读操作
代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13u8 data_addr = 0x10;
i8 data;
struct i2c_msg msgs[2];
msgs[0].addr = 0x50;
msgs[0].flags = 0;
msgs[0].len = 1;
msgs[0].buf = &data_addr;
msgs[1].addr = 0x50;
msgs[1].flags = I2C_M_RD;
msgs[1].len = 1;
msgs[1].buf = &data;
内核传输数据:
APP通过I2C Controller与I2C Device传输数据
APP通过i2c_adapter与i2c_client传输i2c_msg
内核函数i2c_transfer
- i2c_msg里含有addr,所以这个函数里不需要i2c_client
i2c设备驱动编写流程:
先创建i2c设备:
通过I2C bus number来创建
1
int i2c_register_board_info(int busnum, struct i2c_board_info const *info, unsigned len);
通过设备树来创建:在设备树中添加,IIC设备挂到哪个IIC控制器上就在哪个控制器下添加对应的节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16i2c1: i2c@400a0000 {
/* ... master properties skipped ... */
clock-frequency = <100000>;
flash@50 {
compatible = "atmel,24c256";
reg = <0x50>;
};
pca9532: gpio@60 {
compatible = "nxp,pca9532";
gpio-controller;
#gpio-cells = <2>;
reg = <0x60>;
};
};通过用户空间(user-space)生成调试时、或者不方便通过代码明确地生成i2c_client时,可以通过用户空间来生成。
1
2
3
4
5// 创建一个i2c_client, .name = "eeprom", .addr=0x50, .adapter是i2c-3
// 删除一个i2c_client
先在入口函数中注册:i2c_add_driver(ap3216c_driver)
创建设备i2c驱动结构体static struct i2c_driver ap3216c_driver
- 设置probe函数
- 设置remove函数
- 设置.driver中的信息如owner,name,==of_match_table==
- 编写of_match_table:static const struct of_device_id ap3216c_of_match[] ,主要是匹配compatible、
- 如果没有设备树就设置.id_table
编写probe函数,可以在这初始化设备,并且设置client值,一般在设备结构体中定义一个void指针变量然后将该指针变量指向probe函数自己传进来的client变量,也就是设备结构体
接着编写读和写函数,但是linux内核也提供了API函数:
int i2c_master_send(const struct i2c_client *client,const char *buf,int count)
client: I2C 设备对应的 i2c_client。
buf:要发送的数据。
count: 要发送的数据字节数,要小于 64KB, 因为 i2c_msg 的 len 成员变量是一个 u16(无符号 16 位)类型的数据。
返回值: 负值,失败,其他非负值,发送的字节数
int i2c_master_recv(const struct i2c_client *client,char *buf,int count)
- client: I2C 设备对应的 i2c_client。
- buf:要接收的数据。
- count: 要接收的数据字节数,要小于 64KB, 因为 i2c_msg 的 len 成员变量是一个 u16(无符号 16 位)类型的数据。
- 返回值: 负值,失败,其他非负值,发送的字节数。
进行读写和应用程序的编写
IIC适配器驱动编写流程:
首先要在设备树中构建I2C BUS节点
接着通过plantform总线注册一个plantform_driver结构体
在probe函数中分配、设置、注册i2c_apdater
设置i2c_apdater结构体中的algo结构体:
1
2
3
4const struct i2c_algorithm i2c_bus_virtual_algo = {
.master_xfer = i2c_bus_virtual_master_xfer,
.functionality = i2c_bus_virtual_func,
};核心是master_xfer函数,它的实现取决于硬件,大概代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33static int xxx_master_xfer(struct i2c_adapter *adapter,
struct i2c_msg *msgs, int num)
{
for (i = 0; i < num; i++) {
struct i2c_msg *msg = msgs[i];
{
// 1. 发出S信号: 设置寄存器发出S信号
CTLREG = S;
// 2. 根据Flag发出设备地址和R/W位: 把这8位数据写入某个DATAREG即可发出信号
// 判断是否有ACK
if (!ACK)
return ERROR;
else {
// 3. read / write
if (read) {
STATUS = XXX; // 这决定读到一个数据后是否发出ACK给对方
val = DATAREG; // 这会发起I2C读操作
} else if(write) {
DATAREG = val; // 这会发起I2C写操作
val = STATUS; // 判断是否收到ACK
if (!ACK)
return ERROR;
}
}
// 4. 发出P信号
CTLREG = P;
}
}
return i;
}
linux SPI驱动实验
传输实例:首先CS0先拉低选中SPI Flash,0x56的二进制就是0b0101 0110,因此在每个SCK时钟周期,DO输出对应的电平。
SPI Flash会在每个时钟周期的上升沿读取D0上的电平。spi模式:
在SPI协议中,有两个值来确定SPI的模式。
CPOL:表示SPICLK的初始电平,0为电平,1为高电平
CPHA:表示相位,即第一个还是第二个时钟沿采样数据,0为第一个时钟沿,1为第二个时钟沿CPOL CPHA 模式 含义 0 0 0 SPICLK初始电平为低电平,在第一个时钟沿采样数据 0 1 1 SPICLK初始电平为低电平,在第二个时钟沿采样数据 1 0 2 SPICLK初始电平为高电平,在第一个时钟沿采样数据 1 1 3 SPICLK初始电平为高电平,在第二个时钟沿采样数据 我们常用的是模式0和模式3,因为它们都是在上升沿采样数据,不用去在乎时钟的初始电平是什么,只要在上升沿采集数据就行。
极性选什么?格式选什么?通常去参考外接的模块的芯片手册。比如对于OLED,查看它的芯片手册时序部分:
SCLK的初始电平我们并不需要关心,只要保证在上升沿采样数据就行。
linux下spi驱动框架:
- 主机控制驱动:SOC的SPI外设驱动,此驱动时半导体厂商编写好的,
- spi控制器驱动核心就是spi_master的构建,该结构体里就有如何通过spi控制器与spi外设进行通信的函数,此函数是原厂编写的
SPI设备树处理过程
SPI Master:在设备树中,对于SPI Master,必须的属性如下:
- #address-cells:这个SPI Master下的SPI设备,需要多少个cell来表述它的片选引脚
- #size-cells:必须设置为0
- compatible:根据它找到SPI Master驱动
可选的属性如下:
- cs-gpios:SPI Master可以使用多个GPIO当做片选,可以在这个属性列出那些GPIO
- num-cs:片选引脚总数
SPI Device:在SPI Master对应的设备树节点下,每一个子节点都对应一个SPI设备,这个SPI设备连接在该SPI Master下面。
- compatible:根据它找到SPI Device驱动
- reg:用来表示它使用哪个片选引脚
- spi-max-frequency:必选,该SPI设备支持的最大SPI时钟
可选的属性如下:
- spi-cpol:这是一个空属性(没有值),表示CPOL为1,即平时SPI时钟为低电平
- spi-cpha:这是一个空属性(没有值),表示CPHA为1),即在时钟的第2个边沿采样数据
- spi-cs-high:这是一个空属性(没有值),表示片选引脚高电平有效
- spi-3wire:这是一个空属性(没有值),表示使用SPI 三线模式
- spi-lsb-first:这是一个空属性(没有值),表示使用SPI传输数据时先传输最低位(LSB)
- spi-tx-bus-width:表示有几条MOSI引脚;没有这个属性时默认只有1条MOSI引脚
- spi-rx-bus-width:表示有几条MISO引脚;没有这个属性时默认只有1条MISO引脚
- spi-rx-delay-us:单位是毫秒,表示每次读传输后要延时多久
- spi-tx-delay-us:单位是毫秒,表示每次写传输后要延时多久
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20spi@f00 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,mpc5200b-spi","fsl,mpc5200-spi";
reg = <0xf00 0x20>;
interrupts = <2 13 0 2 14 0>;
interrupt-parent = <&mpc5200_pic>;
ethernet-switch@0 {
compatible = "micrel,ks8995m";
spi-max-frequency = <1000000>;
reg = <0>;
};
codec@1 {
compatible = "ti,tlv320aic26";
spi-max-frequency = <100000>;
reg = <1>;
};
};处理过程看内核源码:
drivers\spi\spi.c
spi设备驱动:就是具体的SPI芯片驱动,比如ICM20608
- spi_device:每个spi_device都有一个spi_master,每个SPI设备肯定挂载到了一个SPI控制器,比如ICM20608挂载到了6ULL的ECSPI3接口上。有设备树时内核启动的时候会自动解析出来
- spi_driver:非常重要,申请或者定义一个spi_driver然后初始化spi_driver中的各个成员变量,当SPI设备和驱动匹配以后,spi_driver下的probe函数就会执行
- spi_driver初始化成功以后需要向内核注册,函数为spi_register_driver
编写SPI设备驱动程序
编写设备树
查看原理图,确定这个设备链接在哪个SPI控制器下
在设备树里,找到SPI控制器的节点
在这个节点下,创建子节点,用来表示SPI设备
示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14&ecspi1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi1>;
fsl,spi-num-chipselects = <2>;
cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>, <&gpio4 24 GPIO_ACTIVE_LOW>;
status = "okay";
dac: dac {
compatible = "100ask,dac";
reg = <0>;
spi-max-frequency = <10000000>;
};
}
注册spi_driver
SPI设备的设备树节点,会被转换为一个spi_device结构体。
我们需要编写一个spi_driver来支持它。
示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14static const struct of_device_id dac_of_match[] = {
{.compatible = "100ask,dac"},
{}
};
static struct spi_driver dac_driver = {
.driver = {
.name = "dac",
.of_match_table = dac_of_match,
},
.probe = dac_probe,
.remove = dac_remove,
//.id_table = dac_spi_ids,
};接口函数:接口函数都在这个内核文件里:
include\linux\spi\spi.h
- 简易函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82/**
* SPI同步写
* @spi: 写哪个设备
* @buf: 数据buffer
* @len: 长度
* 这个函数可以休眠
*
* 返回值: 0-成功, 负数-失败码
*/
static inline int
spi_write(struct spi_device *spi, const void *buf, size_t len);
/**
* SPI同步读
* @spi: 读哪个设备
* @buf: 数据buffer
* @len: 长度
* 这个函数可以休眠
*
* 返回值: 0-成功, 负数-失败码
*/
static inline int
spi_read(struct spi_device *spi, void *buf, size_t len);
/**
* spi_write_then_read : 先写再读, 这是一个同步函数
* @spi: 读写哪个设备
* @txbuf: 发送buffer
* @n_tx: 发送多少字节
* @rxbuf: 接收buffer
* @n_rx: 接收多少字节
* 这个函数可以休眠
*
* 这个函数执行的是半双工的操作: 先发送txbuf中的数据,在读数据,读到的数据存入rxbuf
*
* 这个函数用来传输少量数据(建议不要操作32字节), 它的效率不高
* 如果想进行高效的SPI传输,请使用spi_{async,sync}(这些函数使用DMA buffer)
*
* 返回值: 0-成功, 负数-失败码
*/
extern int spi_write_then_read(struct spi_device *spi,
const void *txbuf, unsigned n_tx,
void *rxbuf, unsigned n_rx);
/**
* spi_w8r8 - 同步函数,先写8位数据,再读8位数据
* @spi: 读写哪个设备
* @cmd: 要写的数据
* 这个函数可以休眠
*
*
* 返回值: 成功的话返回一个8位数据(unsigned), 负数表示失败码
*/
static inline ssize_t spi_w8r8(struct spi_device *spi, u8 cmd);
/**
* spi_w8r16 - 同步函数,先写8位数据,再读16位数据
* @spi: 读写哪个设备
* @cmd: 要写的数据
* 这个函数可以休眠
*
* 读到的16位数据:
* 低地址对应读到的第1个字节(MSB),高地址对应读到的第2个字节(LSB)
* 这是一个big-endian的数据
*
* 返回值: 成功的话返回一个16位数据(unsigned), 负数表示失败码
*/
static inline ssize_t spi_w8r16(struct spi_device *spi, u8 cmd);
/**
* spi_w8r16be - 同步函数,先写8位数据,再读16位数据,
* 读到的16位数据被当做big-endian,然后转换为CPU使用的字节序
* @spi: 读写哪个设备
* @cmd: 要写的数据
* 这个函数可以休眠
*
* 这个函数跟spi_w8r16类似,差别在于它读到16位数据后,会把它转换为"native endianness"
*
* 返回值: 成功的话返回一个16位数据(unsigned, 被转换为本地字节序), 负数表示失败码
*/
static inline ssize_t spi_w8r16be(struct spi_device *spi, u8 cmd);- 复杂的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44/**
* spi_async - 异步SPI传输函数,简单地说就是这个函数即刻返回,它返回后SPI传输不一定已经完成
* @spi: 读写哪个设备
* @message: 用来描述数据传输,里面含有完成时的回调函数(completion callback)
* 上下文: 任意上下文都可以使用,中断中也可以使用
*
* 这个函数不会休眠,它可以在中断上下文使用(无法休眠的上下文),也可以在任务上下文使用(可以休眠的上下文)
*
* 完成SPI传输后,回调函数被调用,它是在"无法休眠的上下文"中被调用的,所以回调函数里不能有休眠操作。
* 在回调函数被调用前message->statuss是未定义的值,没有意义。
* 当回调函数被调用时,就可以根据message->status判断结果: 0-成功,负数表示失败码
* 当回调函数执行完后,驱动程序要认为message等结构体已经被释放,不能再使用它们。
*
* 在传输过程中一旦发生错误,整个message传输都会中止,对spi设备的片选被取消。
*
* 返回值: 0-成功(只是表示启动的异步传输,并不表示已经传输成功), 负数-失败码
*/
extern int spi_async(struct spi_device *spi, struct spi_message *message);
/**
* spi_sync - 同步的、阻塞的SPI传输函数,简单地说就是这个函数返回时,SPI传输要么成功要么失败
* @spi: 读写哪个设备
* @message: 用来描述数据传输,里面含有完成时的回调函数(completion callback)
* 上下文: 能休眠的上下文才可以使用这个函数
*
* 这个函数的message参数中,使用的buffer是DMA buffer
*
* 返回值: 0-成功, 负数-失败码
*/
extern int spi_sync(struct spi_device *spi, struct spi_message *message);
/**
* spi_sync_transfer - 同步的SPI传输函数
* @spi: 读写哪个设备
* @xfers: spi_transfers数组,用来描述传输
* @num_xfers: 数组项个数
* 上下文: 能休眠的上下文才可以使用这个函数
*
* 返回值: 0-成功, 负数-失败码
*/
static inline int
spi_sync_transfer(struct spi_device *spi, struct spi_transfer *xfers,
unsigned int num_xfers);SPI传输示例:
linux 多点电容屏触摸实验
- 电容触摸屏,通过触摸芯片上报多点触摸信息,比如FT5426,这是一个IIC,多点电容触摸屏本质是一个 IIC驱动
- 触摸IC一般都是有INT,当检测到触摸信息以后就会触发中断,就要在中断处理函数中读取信息
- 得到触摸点信息,linux系统下有触摸屏上报的流程,涉及到input子系统下触摸信息的上报。
触摸协议分为TypeA和TypeB:- TypeA:一次上报全部触摸点信息,系统去判断这些信息属于哪个触摸点
- TypeB:适用于触摸芯片有硬件追踪能力的,上报触摸信息是通过不同的事件上报的:ABS_MT_XXX
驱动:
- 驱动主框架是IIC设备,会用到中断,在中断处理函数里面上报触摸点信息,要用到input子系统框架
USB驱动
概念
硬件框架:必须通过usbhoste访问设备,最多六级hub(可以通过hub访问usb设备)
软件框架:应用程序通过调用usb设备的驱动程序来收发数据,usb设备的驱动程序会调用usbhoste的驱动程序来访问到usbhoste通过usbhoste访问usbdev
传输类型:
- 批量传输:就是使用批量事务实现数据传输,比如U盘。
- 中断传输:就是使用中断事务实现数据传输,比如鼠标。
- 实时传输:就是使用实时事务实现数据传输,比如摄像头。
- 控制传输:由建立事务、批量事务组成,所有的USB设备都必须支持控制传输,用于”识别/枚举”
设备枚举过程:
使用控制传输,读取设备信息(设备描述符):第一次读取时,它只需要得到8字节数据,因为第8个数据表示端点0能传输的最大数据长度。
Host分配地址给设备,然后把新地址发给设备:
使用新地址,重新读取设备描述符,设备描述符长度是18:
读取配置描述符:它传入的长度是255,想一次性把当前配置描述符、它下面的接口描述符、端点描述符全部读出来
读取字符描述符
libusb
linux网络设备驱动框架
net_device结构体
1 | struct net_device { |
net_device_ops:
1 | struct net_device_ops { |
napi:
Linux 里面的网络数据接收也轮询和中断两种,中断的好处就是响应快,数据量小的时候处理及时,速度快,但是一旦当数据量大,而且都是短帧的时候会导致中断频繁发生,消耗大量的 CPU 处理时间在中断自身处理上。轮询恰好相反,响应没有中断及时,但是在处理大量数据的时候不需要消耗过多的 CPU 处理时间。 Linux 在这两个处理方式的基础上提出了另外一种网络数据接收的处理方法: NAPI(New API), NAPI 是一种高效的网络处理技术。NAPI 的核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程序,在接收服务程序中采用 POLL 的方法来轮询处理数据。这种方法的好处就是可以提高短数据包的接收效率,减少中断处理的时间。