linux下驱动的三种编写方法
驱动:主要目的就是为用户空间在内核提供一个调用接口,在接口中提供硬件资源,最终用户可以操作具体的硬件
对于linux下的驱动编写来说,最终需要做的核心就是注册一个file_operations结构体进内核,这个结构体中定义的函数就是当我们应用进行具体调用时候提供的函数支持,最终在这个结构体下的具体函数中使用到具体的硬件资源
struct 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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, 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 (*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);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
那么linux下驱动的编写方法中,一般有三种方法:
普通设备 驱动模型,
总线设备驱动模型,
设备树下的驱动模型
这三种方法的核心就是如上所说,最终注册一个file_operations结构体,为用户提供具体函数支持,那么三者的区别在哪里?差别就在于如何提供具体的硬件资源
普通设备驱动模型
对于刚步入嵌入式linux的初学者来说,最新接触的应该是这种驱动模型,这是一种比较直观而简单的驱动模型,因为没有复杂的匹配过程,设备驱动的注册和硬件资源的指定都在一个文件中完成,比如在file_operation结构体中的init等函数中指定并映射硬件资源。
#define GPX2CON 0x11000c40
#define GPX2DAT 0x11000c44
static void fs4412_led_init(void)
{
pgpx2con = ioremap(GPX2CON,4);
pgpx2dat = ioremap(GPX2DAT,4);
}
这样的驱动模型好处是直观,简单,复杂之处在于,每当我们需要的硬件资源多时,程序会很乱,且只要改动一个硬件资源,程序就需要重新编译。略麻烦,当然它也无法适应其他更强大的驱动。
平台总线设备驱动模型
这种驱动模型是设备资源和驱动分开,粗暴理解就是file_operations结构体的注册和硬件资源的提供分开来写,软件和硬件资源分开管理,这样的好处是不需要频繁的进行程序的编译操作,那么下面就分析硬件资源是如何和具体的驱动联系到一起。
驱动模型叫总线设备驱动模型,可以有个简单的框架:
device–bus–drvier
书写驱动的时候,一般会有一个dev.ko和drv.ko驱动,当二者被加载的时候(加载顺序不分先后),会自动进行匹配绑定,下面分析如何进行绑定。
1.dev提供一个核心的结构体:platform_device
struct platform_device {
const char * name;
int id;
struct device dev;
u32 num_resources;
struct resource * resource ;
struct platform_device_id *id_entry;
};
在这个结构体中
name成员用于标识这个结构体,在与具体驱动匹配绑定的时候关键的标识符,
resource成员用于指定我们具体需要的资源,这也就是我们写这个文件的目的,将硬件资源放到这里。提供给具体的platform_driver使用
2.drv也提供一个核心结构体:platform_driver
struct 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_device结构体会被加入到内核中的一个dev链表中进行集中维护,platform_driver结构体会被加入到内核中的一个driver链表中集中维护,在匹配过程中,总线bus会提供一个match函数,这个函数会首先去用driver结构体下的id_tale成员中的name去和platform_device结构体下的name成员 比较,会遍历整个dev链表,如果相同,代表匹配成功,这时候就会调用platform_driver下的probe函数,这个函数里就可以去做任何我们想做的事情。
至此,在driver驱动下就可以调用到具体设备dev下的硬件资源,也就是可以获得platform_device下的resource结构体的信息。
在driver中通过platform_get_resource;函数,获得platform_device提供的硬件信息,也就是resouce指定的信息。
这样的驱动更加强大,但是也有缺点,就是每次需要编写两个ko文件。需要加载两个文件进内核。操作繁琐。
设备树下的驱动模型(自己DIY起名)
本质上没有这个名字,但是便于理解,个人这个叫,这样的驱动模型,是将硬件信息整合到设备树中去,取消了dev.ko文件的编写,看似取消了一个dev驱动文件,其实不是,而是这部分工作由内核自己完成,它从设备树中获得信息,自己转化为了platform_device结构体,然后与我们的驱动进行匹配。
大体的转换流程:dts/dtsi -->dtb–>dev_node–>dev–>platform_device
下面粗略说一下如何转换:
1.首先罗列一个简单是设备树dts文件如下
/ {
model = "Atmel AT91SAM9x5 family SoC";
compatible = "atmel,at91sam9x5";
interrupt-parent = <&aic>;
cpus {
#address-cells = <0>;
#size-cells = <0>;
cpu {
compatible = "arm,arm926ej-s";
device_type = "cpu";
};
};
ssc0: ssc@f0010000 {
compatible = "atmel,at91sam9g45-ssc";
reg = <0xf0010000 0x4000>;
interrupts = <28 IRQ_TYPE_LEVEL_HIGH 5>;
dmas = <&dma0 1 AT91_DMA_CFG_PER_ID(13)>,
<&dma0 1 AT91_DMA_CFG_PER_ID(14)>;
dma-names = "tx", "rx";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ssc0_tx &pinctrl_ssc0_rx>;
clocks = <&ssc0_clk>;
clock-names = "pclk";
status = "disabled";
};
}
这是部分的设备树文件,可以简单理解每一个大括号代表着一个节点,大节点中也可以包含小节点,形成父子节点,今天不讲解设备树的具体格式。而是粗略讲解如何进行转换匹配
1.首先,dts或dtsi文件会被转换为dtb文件,然后dtb文件会转换为dev_node节点,这个节点里就可以提取到设备树提供的所有信息,比如compatible属性信息等,然后这个节点会被放到dev结构体下,dev结构体会被放到platform_device下参与构造这个结构体,所以,当我们要调用设备树资源的时候,一路调用下来就能找到。
2.在匹配过程中,platform_driver下的driver下的of_match_table会去和platform_device下的dev下的dev_node下的compatible匹配(实质上就是设备树中的compatible属性),匹配成功后就会得到这个节点提供的所有硬件资源,绑定成功后就可以通过一些函数来调用到具体设备树提供的硬件资源,具体是函数可以到内核下的of.h文件下去看。
分析完毕