一、入门
1、字符设备驱动
1)注册字符设备
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
2)cdev_add 其实1)调用了cdev_add
int cdev_add(struct cdev *p, dev_t dev, unsigned count); /* 调用关系 */ register_chrdev __register_chrdev cdev_add
2、用户空间和内核空间的数据拷贝
1)copy_to_user/copy_from_user://拷贝一个空间 static __always_inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n); static __always_inline unsigned long __must_checkcopy_from_user(void *to, const void __user *from, unsigned long n) 2)put_user(x,p)/get_user://从p指针传单个值 #define put_user(x, ptr) \ ({ \ void __user *__p = (ptr); \ might_fault(); \ access_ok(VERIFY_WRITE, __p, sizeof(*ptr)) ? \ __put_user((x), ((__typeof__(*(ptr)) __user *)__p)) : \ -EFAULT; \ })
3、检测用户传来的空间是不是合法
access_ok(int ,const void *addr,ulong) ex:if(!access_ok(verify_write,buffer,count))return error;
4、异步通知
fasync_helper(int fd,struct file,int on,struct fasync_struct **);//on 0表示去除异步通知,1表示添加异步通知 kill_fasync(stuct fasync_struct **fp,int sig,int band);//当时间到达,将用来通知相关的进程
5、/proc
通过它可以在运行时访问内核的内部数据结构,改变内核设置,通过它发送信息。ps、top命令就是通过读取/PROC下的文件来后去信息。
一般情况proc自动加载,如果启动没有自动加载,可以用:mount -t proc proc /proc
内核还提供了一些/proc文件系统的接口函数:proc_mkdir;proc_create;proc_create_data;proc_remove;remove_proc_entry;
struct proc_dir_entry *proc_mkdir(const char *name,struct proc_dir_entry *parent);
6、内核makefile
kbuild Makefile
obj-y表示连接进内核,obj-m表示编译成可以加载的模块
1)目标定义
obj-(CONFIG_I2C_BOARDINFO)+=i2c-boardinfo.o
2)多文件模块定义
obj-(CONFIG_FB)+=fb.o fb-y:=fbmem.o fbmon.o..... fb-objs:=$(fb-y)
3)目录迭代
obj-$(CONFIG_FB_OMAP)+=OMAP/
如果CONFIG_FB_OMAP的值是y或者m,kbuid会将omap目录列入向相下迭代的目标中,但是其作用仅限于此,至于omap目录下文件是要作为模块编译还是连接进入内核,还要由omap目录下的makefile文件的内容来决定。
二、驱动模型
1、内核对象
1)kobject(内核对象,是内核设备管理机制的最高的层抽象):一个kobject对应sysfs文件系统一个目录,还负责设备热插拔等事件的处理工作。
对应有一些接口函数:kobject_init;kobject_add;将kobject加入到系统;kobject_init_and_add;。。。等
void kobject_init(struct kobject *kobj, struct kobj_type *ktype); int kobject_add(struct kobject *kobj, struct kobject *parent,const char *fmt, ...);
常见的kobject包括:
struct kobject *dev_kobh;//设备对象; kobject *sysfs_dev_char_kobj;//字符设备对象; struct kobject *sysfs_dev_block_kobj;//块设备对象; struct kobject *kernel_kobj;//sysfs下的kernel对象。
2、内核对象的类型:kobj_type{....sysfs_ops..};
sysfs_ops为内核对象在sysyfs文件系统中的接口:show(kobject。。)显示,store(kobject。。)存储
3、kset kobject 通过kset组织层次化结构
kset{ struct list_head list;//同一kset的链表 spinlock_t list_lock;//锁 struct kobject kobj;//自身的kobject struct kset_uevent_ops *uenent_ops;//uevent 相关操作,如事件过滤 }
常见的kset包括:
struct kset *bus,*class,*system
4、设备模型层次:模型包括device、device_driver、bus、class(设备类型)
设备和设备总线均挂载在总线上,总线完成设备、设备驱动的匹配
使用class_create可以创建一个类,系统注册的类可以在/sysfs/class目录下找到
5、sysfs文件系统
系统中每个kobject对应这sysyfs中的一个目录,而每一个sysyfs中的目录代表一个kobject对象,每个sysfs文件代表对应kobject属性。
sysfs文件系统最基本的函数包括:
sysfs_create_file创建文件,sysfs_create_dir_ns创建目录等
static inline int __must_check sysfs_create_file(struct kobject *kobj,const struct attribute *attr);
6、platform 平台概念的引入能更好的描述设备的资源信息,例如总线地址、中断、dma信息到呢个。也叫做虚拟总线。
7、attributes:设备、驱动、类均有自己的属性,这些属性在attribute结构的基础上,增加了显示与存储接口。
struct attribute{ const char *name; umode_t mode; #ifdef CONFIG_DEBUG_LOCK_ALLOC bool ignore_lockdep:1; struct lock_classs *key; strct loce_class_key skey: }
8、设备事件通知
1)kobject uevent 是内核中东发送给应用层设备事件。
kobject uevent包括
enum kobject_action { KOBJ_ADD, KOBJ_REMOVE, KOBJ_CHANGE, KOBJ_MOVE, KOBJ_ONLINE, KOBJ_OFFLINE, KOBJ_BIND, KOBJ_UNBIND, KOBJ_MAX };
通过netlink机制,内核通过kobject_uevent->kobject_uevent_env函数发送给netlink客户端;
2)uevent helper
如果内核支持uevent helper ,kobject_uevent_env就会调用应用层的uevent helper程序
Linux下的设备管理通常使用udev工具。mdev用来在嵌入式中替代udev。udev包含一个一直运行的后台进程。与udev不同,mdev不是一直运行的后台程序,它使用内核唤醒,则mdev要被设置成uevent_helper程序。
3)udev
它是用来监控udev客户端的控制信息,内核的hotplug事件,配置文件变化事件。当有设备插拔时,udev是会收到通知,它根据事件中参数和sysfs中的信息,调用合适的事件处理函数,创建和删除/dev节点。
udev是通过netlink机制获取内核的uevent事件。mdev是通过直接访问/sys/class/目录来获取设备信息。
udev按照规则文件中的规则处理uevent事件,udev规则文件在目录/etc/udev/rules.d下面。udev通过文件系统的inotify功能,监控其规则文件目录/etc/udev/rules.d,一旦该目录下的规则文件变化,它就重新加载规则文件。udev规则文件中一个不以“#”开头的行就是一条规则。每条规则包含匹配键和执行键。配置键以“==”号与值连接;执行用“=”
9、设备树
设备树用来描述板卡板级硬件信息。设备树位于Linux内核目录代码arch/arm/boot/dts下,dts文件为板级定义,dtsi危机为soc级定义。Linux设备树编译 make dtbs。
内核启动时会建立设备树节点:
setup_arch { mdesc=setup_machine_fdt(__atags_pointer);//建立设备树 unflagten_device_tree();//扫描设备树,转换成device_node 。。 }
bootloader将设备树的地址传给内核,放在R2寄存器中,在arch/arm/kernel/head-common.s文件同__mmap_switched赋值给__atags_pointer.
三、内核同步机制
1、原子操作
typefef stuct (volatile int counter;)atomic_t;
volatile修饰符告诉编译器不要对该类型的数据进行优化
2、自旋锁(一直循环直到条件满足)导致cpu效率降低
spin_lock_init; spin_lock; spin_trylock;s pin_unlock
spin_lock获取成功立即返回,否则原地打转。try函数尝试获取,如果立即获取则返回真,否则返回假。
中断安全的自旋锁函数:
硬件中断
spin_lock_irq; spin_unlock_irq; spin_lock_irqsave; spin_unlock_irqresore;
软件中断
spin_lock_bh; spin_unlock_bh;
禁止本地cpu上的中断与内核抢占。save保存本地中断状态,restore恢复中
3、读写锁
读写锁(rwlock)是一种特殊的自旋锁。允许同时有多个读者来访问共享资源。一个读写锁同时只能有一个写着和多个读者。
如果读写锁当前没有读着也没有写着,写者可以立即获取读写锁,否则自旋,直到没有任何写和读着。如果读写锁没有写者,那么读者可以立即获取读写锁,否则自旋,直到写着释放该读写锁。
rwlock_t x; rwlock_init(x);//动态初始化读写锁 rwlock_t x=RW+LOCK_UNLOCKED//静态初始化
读写尝试
read_lock;wirte_lock;read_trylock read_unlock;wirte_unlock; write_trylock
4、rcu(读-复制-修改) 之使用于读多写少的情况。
原理是对于被rcu保护的共享数据机构,读者不需要获取任何锁就可以访问它,但写者在访问它时需要先复制一个副本,然后对副本进行修改,最后调用一个函数在合适的时机修改。就是所有引用数据的任务都退出。
读者
#define rcu_read_lock() preempt_disable() //进入读操作临界区标记 #define rcu_read_unlock() preempt_enable() //退出读操作临界区
写者一般对副本操作,然后将副本设定成正本,最后同步或者异步的释放旧的。
struct rcu_head{ struct tcu_head*next;//下一个rcu_head void (*func)(stuct rcu_head*);//获取竞争条件后的处理函数 };
添加回调函数 同步rcu
void call_rcu(struc rcu_head*,rcu_callback_T func);\ void synchronize_rcu(void);
call_rcu函数调用后,直接返回,rcu软中断会调用回调汗死释放旧的数据指针。sysnchronize_rcu函数则原地等待,它被唤醒时,即可释放旧的数据指针。
5、信号量:是一种睡眠锁。如果信号量被占用,信号量将将会将其调用者加入等待队列。
自旋锁和信号量的第一个区别:前者不引起调用者睡眠。自旋锁和信号量的选用主要看锁被持有的时间长短,如果短,就用自旋锁。第二个区别:信号量有多个持有者,而自旋锁只能有一个持有者。
sema_init(struct semaphore *sem,int val);down()down_trylock()down_interruptible(能被信号打断);获取,up()释放,唤醒等待队列
6、读写信号量:与读写锁原理差不多。
7、互斥量:mutex,同一时间只允许一个访问者,互斥量加锁失败会进入睡眠等待唤醒。
mutex_init(mutex);void mutex_lock(mutex*);;int mutex_trylock();void mutex_unlock();
8、等待队列
等待队列用于异步通知和阻塞式访问。如果进程需要等待某些条件放生才能继续,则可以使用等待队列机制。在Linux内核中通常使用等待队列来实现阻塞式访问。
初始化一个等待队列
void init_waitqueue_head(wait_queue_head_t*q);
等待事件发生函数:
wait_event(wq,condition)//不可中断的等待 wait_event_interruptible(wq,condition)//可中断的等待 wait_event_timeout(wq,condition,timeout) wait_event_interruptible_timeout
唤醒等待队列
wake_up(wait_queue_head_t,*Q);//唤醒所有等待q的进程 wake_up_interruptible(*Q);//只唤醒可以中断休眠的进程
加入或退出等待队列
add_wait_queue(wait_queue_head_t *,wait_queue_t*) add_wait_queue_exclusive remove_wait_queue
加入等待队列的线程将等待唤醒。阻塞式字符驱动一般读函数中等待,并在中断或内核线程中使用wake_up函数唤醒等待队列。
四、内存管理和链表
1、物理地址和虚拟地址
如果cpu没有mmu则发出的地址就是直接传到芯片引脚,这个地址脚物理地址;如果有mmu,则发出的地址就是虚拟地址,mmu会将虚拟地址映射成物理地址。
mmu将虚拟地址映射到物理地址是以页为单位,对于32位cpu,通常一个页4KB。物理内存中的页称为物理页面或者页帧。mmu使用页表来记录虚拟地址页面与物理内存页面之间的映射关系。
2、内存分配
最长用的内存申请和释放函数:
void *kmalloc(size_t size,gfp_t flags); void *kzalloc(size_t size,gfp_t flags);//调用kmalloc分配内存并将内存清零 void kfree(const void*x);
Kmalloc函数分配的地址空间是线性映射的,它一般分配小于128kb的内存。
flags GFP_KERNEL内核空间进程使用。GFP_USER为用户空间分配空间,GFP_HIGHUSER从高端地址分配 。。。等
如果要分配大块内存,应使用面向页的技术
unsigned long get_zeored_page(gfp_t gfp_mask);//返回一个单个的,零填充的页 unsigned long __get_free_pages(gfp_t mask,unsigned int order);//直接获取整页的内存(页数是2 的幂) free_page(addr,order);
如果需要申请一块连续的虚拟地址内存,物理地址不是连续的,页表查询比较频繁,效率底:
void *vmalloc(size); void *vmalloc_user(size);为用户空间分配内存 void vfree(void *addr);
3、cache
高速缓存。Linux使用slab机制管理cache。kmem_cache_create创建slab缓存。
kmem_cache_alloc//从cache中分配内存 kmem_cache_free kmem_cache_destroy//销毁slab缓存
4、IO端口到虚拟地址映射
arm中,外设I/0端口具有和内存一样的物理地址,外设的i/O内存资源地址是已知的,有硬件的设计决定。Linux的驱动程序并不能直接通过物理地址访问I/0内存资源,而必须将物理地址转换成虚拟地址。
1)静态映射
在arm存储系统中,使用mmu实现虚拟地址到物理地址的映射。mmu的实现过程,实际上就是一个查表映射的过程。建立页表是实现mmu功能不可或缺的一步。页表位于系统的内存中,页表的每一项对应于一个虚拟地址到物理地址的映射。
Linux内存的create_mapping函数创建线性映射表。
stuct map_desc{ unsigned ling virtual;//虚拟地址 unsigned long pfn;//__phys_to_pfn(phy_addr) unsiged long length;//长度 unsiged int type; } void __init create_mapping(struct map_desc*md); /* 例: arm平台使用iotable_init来创建平台专用映射:*/ void __init iotable_init(struct map_desc *io_desc,int nr); static struct mcp_Desc smdk6410_iodesc[] = {};// 需要建立的映射在此添加 s3c64xx_init_io(smdk6410_iodesc,ARRAY_SIZE(smdk6410_iodesc)); { iotable_init(smdk6410_iodesc,ARRAY_SIZE(smdk6410_iodesc));; .. .. .. }
2)ioremap
如果需要在模块中动态映射IO,可以采用ioremap函数。此函数将i/o内存资源的物理地址映射到核心虚拟地址空间。
typedef phys_addr_t resource_size_t; void __iomem *ioremap(resource_size_t res_cookie/*物理地址*/,size_t size); void iounmap(volatile void __iomem *iomem_cookie);//取消映射
例:
reserve_virt_addr=ioremap(100*1024*1024,10*1024*1024);//将101MB开始的10MB地址映射到虚拟地址。
5、内核空间到用户空间的映射
mmap接口。将内核地址映射到用户地址,应用程序可以直接访问内存地址。
系统调用
unsigned long mmap(unsigned long addr,unsigned long len,int prot,int flags,int fd,long off);//取消映射munmap函数
驱动需要实现
memapmem_fops{ .. .mmap = memapmem_mmap; }
例:
fd=open("/dev/mmap",O_RDWR); addr=mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
6、DMA映射
1)建立一致性DMA映射:dma_alloc_coherent(禁止页表的Cacheable项和Bufferable)
2)建立非一致性DMA映射:dma_alloc_noncoherent
7、链表是双向链表:可以双向遍历
五、任务和调度
1、schedule
linux进程在等待资源就绪的过程中,可以主动让出cpu,自身进入休眠状态,等待唤醒后继续检查资源是否就绪。进程可以调用schedule函数让出cpu,进程被唤醒后将从schedule函数的下一条代码开始执行。
void _sched schedule(void) signed long _sched schedule_timeout(timeout)//带超时的调度 例: process a: set_current_state(TASK_INTERRUPTIBLE); spin_lock(&list_lock); if(list_empty(&list_head)){ spin_unlock(&list_lock); schedule(); spin_lock(&list_lock); } set_current_state(SASK_RUNNING); spin_unlock(&list_lock); process b: spin_lock(&list_lock); list_add_tail(&list_head,new_node); spin_lock(&list_lock); wake_up_process(process a);
2、内核线程kthread_create
kthread_cretate创建的线程不能立马运行,需要wake_up_process函数唤醒。kthread_run(先调用kthread_create,再调用wake_up_process)宏完成了kthread_create与wake_up_process两步。 kthread_stop结束内核线程,应保证线程函数尚未结束,否则会一直等待。
3、内核调用应用程序
int call_usermodehelper(char *path,char **argv,char **envp,int wait);
path程序路径,argv参数,envp环境变量,wait等待结束标志
4、软中断机制
1)原理
硬件中断是硬件产生的中断信号,软中断是软件模拟的中断。硬件产生中断后,会将中断通知给cpu,cpu查询向量表将中断映射成具体的程序。软中断完成在操作系统内部,内核运行一个守护进程来实现中断查询与执行,这个线程的功能类似处理器的中断控制器。构成软件中断机制的核心元素包含:软件中断状态(soft interrupt state)、软中断向量表(softirq_vec)、软中断线程(softirq thread)
系统在ksoftirqd内核进程中调用__do_softirq循环检测软中断是否处于pending状态,如果是,则执行相应处理函数。
在linux 4.5内核最多可以有10中软中断,包括定时器、网络软中断、tasklet。优先级从0-9,对应10 个已经定义好的函数。
内核将整个的中断处理流程分为了上半部和下半部。上半部就是之前所说的中断处理函数,它能最快的响应中断,并且做一些必须在中断响应之后马上要做的事情。而一些需要在中断处理函数后继续执行的操作,内核建议把它放在下半部执行。
2)tasklet
软中断是利用软件模拟的中断机制,常用来执行异步任务。tasklet是利用软中断实现的一种下半部机制。
软中断和tasklet优先级较高,性能较好,调度快,但不能睡眠。而工作队列是内核的进程调度,相对来说较慢,但能睡眠。所以,如果你的下半部需要睡眠,那只能选择工作队列。否则最好用tasklet。
三个步骤:
(1)编写tasklet处理程序 static void tasklet_callback(ulong data); (2)声明tasklet DECLEARE_TASKLET(tasklet,tasklet_callback,0); (3)调度tasklet static irqreturn_t irq_handler(int irq,void *arg) { tasklet_schedule(&tasklet); return IRQ_HANDLED; }
5、工作队列
1)原理
工作队列类似tasklet,允许调用者请求在将来某一个时间调用一个函数。tasklet在软中断上下文中允许,所以tasklet执行很快。工作队列在一个特殊内核进程上下文运行,有很多灵活性,并且能够休眠。工作队列包括一系列将要执行的任务和执行这些任务的内核线程。每个工作队列有一个专门的线程,所有的而任务必须在进程的上下文中运行,这样可以安全的休眠。Linux提供一系列全局work queue,包含system_wq、system_highpri_wq等。驱动程序可以创建并使用他们自己的工作队列。
2)延迟工作队列:延迟工作队列基于工作队列,可以实现延迟一段时间再将工作加入到工作队列
6、内核时间
1)时间概念
(1)时钟周期(clock cycle):晶振振荡器在1s内产生的时钟脉冲个数。Linux用宏CLOCK_TICK_RATE来表示计数器的输入时钟脉冲的频率。
(2)时钟滴答(clock tick):一次时钟中断产生一次时钟滴答。系统每个时钟周期产生一次时钟中断。
(3)时钟滴答频率:1s内的时钟滴答次数。Linux内核用HZ来表示时钟滴答的频率,而HZ通常就是1s。
(4)全局变量(jiffies):一个32为无符号整数,用来表示自内核上一次启动以来的时钟滴答次数。每滴答一次,内核的时钟中断处理函数timer_interrupt会将该变量加1.
(5)xtime:timeval结构全局变量,记载系统自开机以来的当前时间,基准为1970.1.1
(6)系统时钟:也是软件时钟,由软件根据时间中断计时。
内核可以应下面函数获取和设置系统时间:
void do_gettimeofday(struct timeval *tv);int do_settimeofday(struct timespec *tv) timeval和timespec与jiffies转换 timespec_to_jiffies;timeval_to_jiffies
2)Linux下的延迟
内核定义了一堆宏来实现延迟:
#define time_after(a,b) #define time_before #define time_after_eq(a,b) #define ndelay(n)//纳秒 #define udelay(n)//微秒 #define mdealy(n)//毫秒
以上都是忙等待,会导致其他任务此时间无法使用cpu,下面是不必忙等待的短延迟方法:
void msleep(u int);ulong msleep_interruptible(u int);单位是milliseconds。
3)内核定时器
timer_list{ struct list_head list; ulong ecpires;//定时器到期时间 ulong data;//传递给处理函数的 void (*fun)(ulong);//回调函数 }
操作:
增加:add_timer(timer_list *)
删除:del_timer
修改ecpire值:mod_timer
六、简单硬件设备驱动程序
1、处理器访问硬件设备主要通过下面几种方式:
(1)内存方式。外设的内存空间被映射到处理器的地址空间,处理器通过访问映射地址来访问硬件
(2)I/O接口。处理器与I/O设备之间通过一定的接口连接,这个接口就是I/O接口。I/O接口中包括一组寄存器以及控制电路。
(3)管脚(pin)。管脚可以用来对芯片进行复位,并接收来自设备的中断信号。另外有些芯片还可以通过管脚进行简单的模式配置。
在x86体系中,I/O地址空间与内存地址空间是分开的,寄存器位于I/O空间是,称为I/O端口。在arm等体系中,I/O通常是和内存统一编制的,也称为I/O内存,是系统中访问速度最快的内存。
2、嵌入式Linux系统构成
bootlader (传参,设备树(R2寄存器)等)-》kernel-》根文件系统-》其他文件系统挂载在根文件系统下面
3、硬件初始化
硬件初始化放在kernel下的arch目录下,如arch/arm/mach-xxx/mach-xxxx.c
DT_MACHINE_START(LS1021A, "Freescale LS1021A") .smp = smp_ops(ls1021a_smp_ops), .dt_compat = ls1021a_dt_compat, MACHINE_END
4、clk体系
时钟就像人的心跳,没有时钟,外设就无法运行。时钟相关代码在/driver/clk
5、dev/mem与dev/kmem
/dev/mem是物理内存的映射,可以用来访问物理I/O设备,例如接口控制器的寄存器。/dev/kmem是虚拟内存的映射,可以用来看下kernel的变量等信息。
例:
target = strtoul(argv[1],0,0);
打开内存设备:
fd=open("/dev/mem",O_RDWR|O_SYNC);
映射一个页面
map_base=mmcp(0,MAP_SIZE,PORT_READ|PORT_WRITE,MAP_SHARED,fd,target&~MAP_MASK);
根据数据类型获取内存的值
vir_addr = map_base+(target&map_mask);
然后就可以通过操作vir_addr来操作相应的寄存器。
6、寄存器访问
1)如S3C6410X处理器,支持32为物理地址空间,这些空间分为两个部分,一部分用于存储,一部分用于外设。
通过spine总线访问主存,主存范围i是0x00000000~0x6fffffff
引导镜像区:-0x07ffffff
内部存储区:-0x0fffffff
静态存储区:-0x3fffffff 用于访问SROM,SRAM NOR FLASH
动态存储区:-0x6fffffff
外设区域通过peri总线访问,范围0X70000000-0X7FFFFFFF.
Linux必须将外设的物理地址映射成虚拟地址才能使用。
地址映射可以采用固定地址映射
#define S3C_VA_IRQ S3C_ADDR(0X00000000) /*irq控制器*/
另一种方式采用ioremap函数。
当I/O寄存器与内存统一编址时,I/O寄存器也称I/O内存。当I/O寄存器与内存分开编址时,I/O寄存器也称I/O端口。在I/O内存资源地址映射成虚拟地址后,为了保证驱动程序的跨平台性,应该使用Linux中特定的函数访问I/O内存资源,而不应该通过指向虚拟地址的指针来访问。
void writew(u16,volatile void __iomem*addr); void iowrite16(u16,void __iomem*addr); void iorwrite16_rep(const volatile void __iomem*addr,void *buffer,uint cont);//连续的
2)看门狗
为保证系统出现异常时能自动启动,处理器均提供了看门狗功能。看门狗单元即可以产生复位信号,也可以被用作一个普通的16位间隔定时器来产生中断服务。
看门狗寄存器
WTCON 0x7e004000 r/w 看门狗定时器控制寄存器
WTDAT 0X7E004004 R/W 看门狗定时器数据寄存器
WTCNT 0X7E004008 R/W 看门狗计数器计数控制器
WTCLRINT 0X7E00400C W 中断清除寄存器
WTDAT 保存看门狗定时器重载计数值。WTCNT保存看门狗定时器当前的值。WTCLRINT 用来清除看门狗定时间中断,写入任意值将清除中断。
7、电平控制
一般电平包括高、底电平两种。常用的电平包括TTL电平、CMOS电平和RS232电平,各种电平的电压范围不同,TTL电平信号+5V等价于逻辑1,0V等价于0.一般输入,<1.2V为低电平,>2.0V为高,输出,<0.8低,>2.4高。电平控制离不开GPIO控制。
8、硬件中断处理
由硬件产生的一种电信号,并直接送入中断控制器输入引脚,再由中断控制器向处理器发送相应的信号。
如果中断处理过程非常复杂,可以分成两个部分:上半部和下半部。上半部完成一些紧急事物,下半部完成剩余的事物。上半部不可以中断,下半部可以。Linux中的下半部包括软中断、tasklet机制和工作队列、定时器等。
发生中断时:
cpu跳到"vector_irq", 保存现场, 调用C函数handle_arch_irq
handle_arch_irq:
a. 读 int controller, 得到hwirq
b. 根据hwirq得到virq
c. 调用 irq_desc[virq].handle_irq
驱动注册中断处理函数:驱动程序 request_irq(virq, my_handler)
9、看门狗驱动框架
在Linux/drivers/watchdog目录。
看门狗设备结构
struct watchdog_device
注册与注销看门狗:int watchdog_register_device(watchdog_device *);void watchdog_unregister_device();
看门狗有一个重要的参数,就是看门狗操作:
struct watchdog_ops{ int (*start)(struct watchdog_device*); ... }
watchdog_register_device会调用一个杂项设备驱动,注册一个字符设备驱动。
例:
static const struct watchdog_info s3c2410_wdt_ident = { .options = OPTIONS, .firmware_version = 0, .identity = "S3C2410 Watchdog", }; static const struct watchdog_ops s3c2410wdt_ops = {
.owner = THIS_MODULE, .start = s3c2410wdt_start, .stop = s3c2410wdt_stop, .ping = s3c2410wdt_keepalive, .set_timeout = s3c2410wdt_set_heartbeat, .restart = s3c2410wdt_restart, }; static const struct watchdog_device s3c2410_wdd = {
.info = &s3c2410_wdt_ident, .ops = &s3c2410wdt_ops, .timeout = S3C2410_WATCHDOG_DEFAULT_TIME, };
watchdog_register_device(&wdt->wdt_device);注册
10、RTC驱动
嵌入式系统一般有两个时间,一个是RTC时间,一个是Linux系统时间。RTC时间存储在RTC控制器中,系统断电后通过电池供电,保证系统下次重新上电都能读到正确的时间。通常在系统启动脚本中读取RTC时间,并将RTC时间设置为系统时间。Linux中的date命令是用来读取和设置系统时间;而hwclock命令是用来读取和设置RTC时间的。
注册与注销RTC驱动
devm_rtc_device_register(&pdev->dev, "s3c", &s3c_rtcops,THIS_MODULE);
RTC设备类的操作函数接口
struct rtc_class_ops { int (*ioctl)(struct device *, unsigned int, unsigned long); int (*read_time)(struct device *, struct rtc_time *); int (*set_time)(struct device *, struct rtc_time *); int (*read_alarm)(struct device *, struct rtc_wkalrm *); int (*set_alarm)(struct device *, struct rtc_wkalrm *); int (*proc)(struct device *, struct seq_file *); int (*set_mmss64)(struct device *, time64_t secs); int (*set_mmss)(struct device *, unsigned long secs); int (*read_callback)(struct device *, int data); int (*alarm_irq_enable)(struct device *, unsigned int enabled); int (*read_offset)(struct device *, long *offset); int (*set_offset)(struct device *, long offset); };
RTC驱动也包含一个通用的设备层,负责创建/dev/trc设备,并向应用层提供统一接口(调用devm_rtc_device_register注册RTC,该函数会调用创建设备节点函数)
11、LED类设备
Linux 内核定义了LED类设备专门的处理各种外设的LED灯。
struct led_classdev{ .. } #define led_classdev_register(parent, led_cdev) \ of_led_classdev_register(parent, NULL, led_cdev) void led_classdev_unregister(struct led_classdev *led_cdev)
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。