linux驱动程序框架基础

时间:2023-03-08 17:26:12
linux驱动程序框架基础

============================      指引     =============================

第一节是最基础的驱动程序;

第二节是/dev应用层接口的使用;

第三节是/sys应用层接口的使用;

第四节是对硬件的操作;

第五节是旧版platform_driver的简易说明;

第六节是设备树与新版platform的简易说明;

===========================   简易驱动程序   ===========================

1.基本框架

这是一个.ko驱动程序最基本、也是最常见的框架,这种框架最大的特点是,它不依赖任何外部代码的限制,随时可以进行insmod操作,通常用于将高实时性的代码、或者某种接口的支持嵌入到内核中。这种框架是所有人必须掌握的。

static __init int ModuleInit(void) //装载时调用的函数

{

......

return 0; //完成初始化则返回0,否则应根据原因返回正确的ERR码

}

static __exit void ModuleExit(void) //卸载时调用的函数

{

......

}

module_init(ModuleInit); //规定ModuleInit为装载时调用的函数

module_exit(ModuleExit); //规定ModuleExit为卸载时调用的函数

MODULE_AUTHOR("your name"); //作者名以及额外的说明,用于维护

MODULE_LICENSE("GPL"); //声明开源or不开源,可选GPL和Proprietary

2.常用函数

1)printk:

printk和printf非常相似,它用于输出驱动程序的调试信息,但是printk的功能更强大,可以规定打印等级,显示打印时的系统时间,此外,printk仅用于输出调试信息,它输出的内容不会进入stdio的缓冲流内。

例子:

printk(KERN_INFO "Kernel message %d", 1);

输出:

[s.mmmμμμ] Kernel message 1

由于调试信息通常是通过串口输出的,因此printk占用的时间很多;

2)kzalloc

kzalloc用于申请内存空间,大部分驱动程序需要开辟一段内存空间作为临时的数据存储区,这个函数使用的频率就非常高了,它和malloc基本一样;

例子:

struct device_type *objet = NULL;

objet = kzalloc(sizeof(*objet), GFP_KERNEL);

3)kfree

与kzalloc相反,用于释放kzalloc申请的内存空间;

例子:

kfree(object);

=========================   /dev 应用层接口   ==========================

之所以将这个抽取出来讲,是因为/dev应用层接口是一个独立的框架,并不依赖于某种特定的驱动框架,任何驱动程序都可以使用/dev框架。

1.前提知识——linux文件系统基本操作

很多人都知道linux下面是将设备当成文件了,但具体的,信息是怎么通过文件从应用层下发到设备的,很多人都没有去了解,甚至很多人连标准的读写操作都分不太清楚,因此有必要在这里非常基本的说一下。

1)open和close

open和close函数是程序获得文件使用权限的途径,open的结果是返回一个文件句柄,close函数则用于释放文件;

int open (__const char *__file, int __oflag, ...);

int close (int __fd);

例:

int fd = open("abc", O_RDWR | O_NONBLOCK);

close(fd);

2)read和write

read和write用于向文件传入或传出数据,返回值为实际写入的数据的size;

ssize_t read (int __fd, void *__buf, size_t __nbytes);

ssize_t write (int __fd, __const void *__buf, size_t __n);

例:

length = write(fd, buffer, strlen(buffer));

length = read(fd, buffer, sizeof(buffer));

3)ioctl

ioctl用于稍复杂的控制,它原本是用于TCP/IP的,但很多驱动程序也使用这个接口实现多种功能共用一个接口,返回值为0表示成功,小于0;

int ioctl( int fd, int cmd, void *arg);

4)fread和fwrite

使用fopen打开的文件的句柄,需要用fread和fwrite操作,但需要注意的是f族函数采用了流控制,数据会在缓冲区进行缓存,到一定数量或者被fflush函数触发后再发送。对于时序严格的设备,是不宜使用f族函数的;

2.struct file_operations 结构体

file_operations 结构体存在于fs.h文件中,该结构体规定了驱动程序在/dev目录下对应用层程序会展现哪一些接口,这些接口与read、write等函数是严格匹配的;因此,在设计驱动程序时,如果想要/dev目录下使用标准文件接口向驱动程序传输数据,就必须在驱动程序中实现file_operations 结构体里对应的属性;

声明如下(节选):

struct file_operations {

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

int (*open) (struct inode *, struct file *);

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

......

};

例:实现向文件write后回显写入的数据

ssize_t Fops_Write(struct file *file_p, const char __user *buf, size_t size, loff_t *loff)

{

char data[PAGE_SIZE] = {'\0'};

copy_from_user(&data, buf, size);

data[size] = '\0';

printk(KERN_INFO "%s", data);

return size;

};

struct file_operations fops = {

.write = Fops_Write;

};

3.产生/dev下的设备接口

1)过程较为复杂,先给出实例代码:

int dev_id = 0;

struct class *test_class = NULL;

dev_t test_dev;

dev_id = register_chrdev(0, "test_dev", &fops);

test_dev = MKDEV(dev_id, 0);

test_class = class_create(THIS_MODULE, "test_class");

device_create(test_class, NULL, test_dev, NULL, "test_dev");

2)那么这段代码都干了什么?

这段代码涉及到了4个对象,简单来说就是:A、fops结构体;B、dev对象;C、/sys/class下的对象;D、/dev下的对象;

这段代码完成了以下操作:创建/sys/class/test_class文件夹;创建/dev/test_dev设备文件;

这一切的原因是因为device_create创建/dev下的对象时,需要同时对设备驱动以及/sys/class中的一个对象进行绑定;最终在/dev下面出现代表设备的文件。

创建/dev应用层接口的代码一般放在驱动程序的初始化函数中。

=========================   /sys 应用层接口   ==========================

1./sys文件夹

该文件夹提供了整个linux的所有配置选项,对于驱动开发来说,我们需要重点关注的是/sys/class文件夹,因为该文件夹下的对象是创建/dev下设备文件的必要条件之一,并且它通常用于存放驱动程序的配置选项。

以下是该文件夹下的一个例子,以gpio为例:

/sys/class

┗ gpio # class

┣ export # class attribute

┣ unexport # class attribute

┣ ......

┗ pioA # device

┣ active_low   # device attribute

┣ direction   # device attribute

┣ edge # device attribute

┣ value # device attribute

┗ ......

2.和/dev的区别

1)接口差异:

/sys下只支持对read和write操作的定制;

2)读写差异:

/sys下,一旦执行open操作,那么show函数就会被执行并产生返回值,无论重复read多少次,都是同一个返回值,必须在close操作后才会被刷新;write没有经过严格测试,推荐这方面要了解一下。

3.代码样本

这一块的代码层次感是非常强的,但是涉及了非常多的变量,代码量也比较庞大,因此只能使用例子的形式来说明,希望大家能够仔细阅读分析,现在举以前做过的335x的eqep驱动的相应部分:

1)首先是class的实现

// 为class对象指定有哪些attribute,但该驱动不需要,因此为空

static struct class_attribute eqep_class_attrs_gs[] = {

__ATTR_NULL,

};

// 构造class对象

static struct class eqep_class_gs = {

.name = "eqep",

.owner = THIS_MODULE,

.class_attrs = eqep_class_attrs_gs,

};

/* 在设备初始化时,向系统注册class对象 */

static int __init EQEP_ClassInit(void) {

return class_register(&eqep_class_gs);

}

那么当这段程序执行完毕后,系统就会产生 /sys/class/eqep 这个类;

2)device的实现

//构造单个attribute

// 对应read函数

static ssize_t EQEP_ShowPosition(struct device *dev, struct device_attribute *attr, char *buf)

{

...

return sprintf(buf, "...");

}

// 对应write函数

static ssize_t EQEP_StorePosition(struct device *dev,  struct device_attribute *attr,  const char *buf, size_t len)

{

......

return len;

}

// 构造attribute,赋予读写权限和读写操作

static DEVICE_ATTR(position, S_IRUGO | S_IWUSR, EQEP_ShowPosition,        EQEP_StorePosition);

......

// 构造attribute列表,汇总所有的属性

static const struct attribute *eqep_attrs_gs[] = {

&dev_attr_all_regs.attr,

&dev_attr_position.attr,

&dev_attr_mode.attr,

&dev_attr_run.attr,

&dev_attr_timer_period.attr,

NULL,

};

static const struct attribute_group eqep_device_attr_group_gs = {

.attrs = (struct attribute **) eqep_attrs_gs,

};

3)device对象的注册,完成后将出现一系列属性

int EQEP_DeviceCreate(struct EQEP_Chip_t *eqep) {

......

// 创建device对象

sprintf(device_name, "%s.%d", eqep->pdev->name, eqep->pdev->id);   eqep_device = device_create(&eqep_class_gs, NULL, MKDEV(0, 0), NULL, device_name);

......

// 向device对象中注册attributes

ret = sysfs_create_group(&eqep_device->kobj, &eqep_device_attr_group_gs);

......

return 0;

}

=============================   对硬件的操作   ==========================

此处只讲片上设备的操作,因为对外部设备的操作都可以归结为对CPU的片上设备进行操作。本章节只讲片上设备操作的API接口。

1.基本概念——内存映射

内存映射,在这里主要指ioremap函数,作用是将物理地址映射到内核虚拟地址中,提供物理设备的访问途径。

2.申请硬件资源,即内存映射

struct resource  *r;

void __iomem *phy_addr = 0x********;

u32 size = ***;

void __iomem *virt_addr;

r = request_mem_region(phy_addr, size, "dev name");

virt_addr = ioremap(phy_addr, size);

3.读写

读写虚拟内存地址需要用特殊的API函数:

readb, readw, readl, writeb, writew, writel;

====================   旧版platform_driver的简易说明   ===================

待完善,现在这种方式已经逐渐被淘汰了

====================   设备树与新版platform的简易说明   ===================

1)平台设备驱动框架

这种框架的适用范围:依赖于硬件的驱动程序。

只要compatible匹配,那么节点的信息会被传递给驱动程序,若找不到匹配的设备树节点,则驱动不会被启动,这种框架的好处是携带多个驱动程序的同一个系统镜像可以兼容不一样的设备树,不会出现驱动程序找不到硬件的情况。

2)示例:

设备树创建节点:

/ {

new_dev {

compatible = "test,new_dev";

status = "okay";

};

};

编写简易驱动:

static int Test_Probe(struct platform_device *pdev)

{

return 0;

}

static int __devexit Test_Remove(struct platform_device *pdev)

{

return 0;

}

static const struct of_device_id test_of_match[] = {

{

.compatible = "test,new_dev",

},

{ }

};

static struct platform_driver test_driver = {

.driver = {

.name = "new_dev",

.owner = THIS_MODULE,

.of_match_table = of_match_ptr(test_of_match),

},

.probe =    Test_Probe,

.remove =   Test_Remove,

};