第一课:设备树的引入与体验

来自百问网嵌入式Linux wiki
Hceng讨论 | 贡献2018年11月3日 (六) 15:46的版本

本套视频面向如下三类学员: 1.有Linux驱动开发基础的人, 可以挑感兴趣的章节观看; 2.没有Linux驱动开发基础但是愿意学习的人,请按顺序全部观看,我会以比较简单的LED驱动为例讲解; 3.完全没有Linux驱动知识,又不想深入学习的人, 比如应用开发人员,不得已要改改驱动, 等全部录完后,我会更新本文档,那时再列出您需要观看的章节。

第01节_字符设备的三种写法

  • 怎么写驱动?

①看原理图:

 a.确定引脚;

 b.看芯片手册,确定如何操作引脚;

②写驱动程序;

 起封装作用;

③写测试程序;

如下原理图,VCC经过一个限流电阻到达LED的一端,再通向芯片的引脚上。

Ldd devicetree chapter1 1 001.jpg


当芯片引脚输出低电平时,电流从高电平流向低电平,LED灯点亮;

当芯片引脚输出高电平时,没有电势差,没有电流流过,LED灯不亮;

从原理图可以看出,控制了芯片引脚,就等于控制了灯。


在Linux里,操作硬件都是统一的接口,比如操作LED灯,需要先open,如果要读取LED状态就调用read,如果要操作LED就调用write函数,也可以通过ioctl去实现。

在驱动里,针对前面应用的每个调用函数,都写一个对应的函数,实现对硬件的操作。

Ldd devicetree chapter1 1 002.jpg


可以看出驱动程序起封装作用,它让应用程序访问硬件变得简单,屏蔽了硬件更加复杂的操作。


* 如何写驱动程序?

①分配一个file_operations结构体;

②设置:

 a. .open=led_open;把led引脚设置为输出引脚

 b. .read=led_read;根据APP传入的值设置引脚状态

③注册(告诉内核),register_chrdev(主设备号,file_operations,name)

④入口函数

⑤出口函数


*在驱动中如何指定LED引脚? 有如下三种方法:

①传统方法:在代码led_drv.c中写死;

②总线设备驱动模型:

 a. 在led_drv.c里分配、注册、入口、出口等

 b. 在led_dev.c里指定引脚

③使用设备树指定引脚

 a. 在led_drv.c里分配、注册、入口、出口等

 b. 在jz2440.dts里指定引脚


可以看到,无论何种方法,驱动写法的核心不变,差别在于如何指定硬件资源

对比下三种方法的优缺点。

假设这样一个情况,某公司用同一个芯片做了两款产品,其中一款是TV(电视盒子),使用Pin1作为LED的指示灯控制引脚,其中一款是Cam(监控摄像头),使用Pin2作为LED的指示灯控制引脚。

TV设备 Cam设备 优缺点
1.传统方法 led_drv.c
①分配一个file_operations结构体;
②设置:
 a .open=led_open;设置Pin1为输出引脚
 b .read=led_read;根据APP传入的值设置引脚状态
③注册(告诉内核)
④入口函数
⑤出口函数
led_drv.c

①分配一个file_operations结构体;
②设置:
 a. .open=led_open;设置Pin2为输出引脚
 b. .read=led_read;根据APP传入的值设置引脚状态
③注册(告诉内核)
④入口函数
⑤出口函数
优点:简单

缺点:不易扩展,需要重新编译
2.总线设备驱动模型 led_drv.c
①分配/设置/注册 platform_driver;
② .probe:
 a 分配一个file_operations结构体;
b .open=led_open;设置平台设备总指定的引脚为输出引脚
  .read=led_read;根据APP传入的值设置引脚状态
c注册
③ .driver{ .name }

led_dev.c
①分配/设置/注册 platform_device;
② .resource:指定引脚;,name为Pin1

led_dev.c

①分配/设置/注册 platform_driver;
② .resource:指定引脚;,name为Pin2
优点:易扩展

缺点:稍复杂,冗余代码太多,需要重新编译
3.设备树 led_drv.c
①分配/设置/注册 platform_driver;
② .probe:
 a 分配一个file_operations结构体;
b .open=led_open;设置平台设备总指定的引脚为输出引脚
  .read=led_read;根据APP传入的值设置引脚状态
c注册
③ .driver{ .name }

.dts指定资源
内核根据dts生成的dtb文件分配/设置/注册platform_device
.dts指定资源

内核根据dts生成的dtb文件分配/设置/注册platform_device
优点:易扩展

缺点:稍复杂,冗余代码太多,需要重新编译

第04节_总线设备驱动模型

总线驱动模型是为了解决什么问题呢?

  • 使用之前的驱动模型,编写一个led驱动程序,如果需要修改gpio引脚,则需要修改驱动源码,重新编译驱动文件,假如驱动放在内核中,则需要重新编译内核

Ldd devicetree chapter1 4 001.png
bus总线是虚拟的概念,并非硬件,dev注册设置某个结构体,这个设备也就是平台设备

    struct platform_device {
	const char	*name;
	int		id;
	bool		id_auto;
	struct device	dev;
	u32		num_resources;
	/*resource 里面确定使用那些资源*/
	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;
	};

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

设备和驱动如何进行通信呢?

  • 通过bus进行匹配 platform_match函数确定(dev,drv)若匹配则调用drv中的probe函数
    struct bus_type platform_bus_type = {
    	.name		= "platform",
    	.dev_groups	= platform_dev_groups,
    	.match		= platform_match,
    	.uevent		= platform_uevent,
    	.pm		= &platform_dev_pm_ops,
    };

这种模型只是一种编程技巧一种机制 并不是驱动程序的核心

platform_match是如何判断dev drv是匹配的?

判断方法是比较dev 和drv 各自的name来进行匹配

  • 平台设备platform_device这面有name
  • platform_driver这面有 driver (里面含有name) 还有id_table(包含 name driver_data)
  • id_table里面的内容表示所支持一个或多个的设备名
static int platform_match(struct device *dev, struct device_driver *drv)
{		
	/*省略部分无用代码*/
	/* Then try to match against the id table */
	if (pdrv->id_table)
		return platform_match_id(pdrv->id_table, pdev) != NULL;

	/* fall-back to driver name match */
	return (strcmp(pdev->name, drv->name) == 0);
}

也就是优先比较 id_table中名字,如果没有则对比driver中名字

  • 根据二期视频led代码进行修改
/* 分配/设置/注册一个platform_device */
/*设置资源*/
static struct resource led_resource[] = {
    [0] = {
		/*指明了使用那个引脚*/
        .start = S3C2440_GPF(5),
		/*end并不重要,可以随意指定*/
        .end   = S3C2440_GPF(5),
        .flags = IORESOURCE_MEM,
    },
};

static void led_release(struct device * dev)
{
}


static struct platform_device led_dev = {
    .name         = "myled",
    .id       = -1,
    .num_resources    = ARRAY_SIZE(led_resource),
    .resource     = led_resource,
    .dev = { 
    	.release = led_release, 
	},
};

/*入口函数去注册平台设备*/
static int led_dev_init(void)
{
	platform_device_register(&led_dev);
	return 0;
}
/*出口函数去释放这个平台设备*/
static void led_dev_exit(void)
{
	platform_device_unregister(&led_dev);
}

module_init(led_dev_init);
module_exit(led_dev_exit);
  • led_drv驱动文件
static int led_probe(struct platform_device *pdev)
{
	struct resource		*res;

	/* 根据platform_device的资源进行ioremap */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	led_pin = res->start;

	major = register_chrdev(0, "myled", &myled_oprs);

	led_class = class_create(THIS_MODULE, "myled");
	device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
	
	return 0;
}


struct platform_driver led_drv = {
	.probe		= led_probe,
	.remove		= led_remove,
	.driver		= {
		.name	= "myled",
	}
};


static int myled_init(void)
{
	platform_driver_register(&led_drv);
	return 0;
}

static void myled_exit(void)
{
	platform_driver_unregister(&led_drv);
}

Makefile文件

KERN_DIR = /work/system/linux-4.19-rc3

all:
	make -C $(KERN_DIR) M=`pwd` modules 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m	+= led_drv.o
obj-m	+= led_dev.o

执行测试程序


如果我需要更换一个led 则只需要修改 led_dev led_resource结构体中的引脚即可

static struct resource led_resource[] = {
    [0] = {
        .start = S3C2440_GPF(6),
        .end   = S3C2440_GPF(6),
        .flags = IORESOURCE_MEM,
    },
};

设备和驱动的匹配是如何完成的?

  • dev这面有设备链表
  • drv这面也有驱动的结构体链表
  • 通过match函数进行对比,如果相同,则调用drv中的probe函数

第05节_使用设备树时对应的驱动编程

  • 本节介绍怎么使用设备树怎么编写对应的驱动程序
  • 只是平台设备的构建区别,以前构造平台设备是在.c文件中,使用设备树构造设备节点原本不存在,需要在dts文件中构造节点,节点中含有资源
  • dts被编译成dtb文件传给内核,内核会处理解析dtb文件得到device_node结构体,之后变成platform_device结构体,里面含有资源(资源来自dts文件)
  • 我们定义的led设备节点
    	led {
    		compatible = "jz2440_led";
    		reg = <S3C2410_GPF(5) 1>;
    	};
  • 以后就使用compatible找到内核支持这个设备节点的平台driver reg = <S3C2410_GPF(5) 1>; 就是寄存器地址的映射

修改好后编译 设备树文件 make dtb

拷贝到tftp文件夹,开发板启动

  • 进入 /sys/devices/platform 目录查看是否有5005.led平台设备文件夹

Ldd devicetree chapter1 5 001.png
Ldd devicetree chapter1 5 002.png

  • 查看 reg 的地址,这里面是以大字节须来描述这些值的

Ldd devicetree chapter1 5 003.png

  • 这个属性有8个字节,对应两个数值
    • 第一个值S3C2410_GPF(5)是我们的起始地址,对应 #define S3C2410_GPF(_nr) ((5<<16) + (_nr))
    • 第二个值1 本意是指寄存器的大小

如何去写平台驱动? 通过bus总线去匹配设备驱动 在 platform_match函数中,通过

	/* Attempt an OF style match first */
	if (of_driver_match_device(dev, drv))
		return 1;

进入 of_device.h中

/**
 * of_driver_match_device - Tell if a driver's of_match_table matches a device.
 * @drv: the device_driver structure to test
 * @dev: the device structure to match against
 */
static inline int of_driver_match_device(struct device *dev,
					 const struct device_driver *drv)
{
	return of_match_device(drv->of_match_table, dev) != NULL;
}

of_match_table结构体

include\linux\mod_devicetable.h
/*
 * Struct used for matching a device
 */
struct of_device_id {
	char	name[32];
	char	type[32];
	char	compatible[128];
	const void *data;
};
  • compatible 也就是从dts得到的platform_device里有compatible 属性,两者进行对比,一样就表示匹配
  • 写led驱动,修改led_drv.c
  • 添加
static const struct of_device_id of_match_leds[] = {
	{ .compatible = "jz2440_led", .data = NULL },
	{ /* sentinel */ }
};
  • 修改
struct platform_driver led_drv = {
	.probe		= led_probe,
	.remove		= led_remove,
	.driver		= {
		.name	= "myled",
		.of_match_table = of_match_leds, /* 能支持哪些来自于dts的platform_device */
	}
};
  • 修改Makefile并编译
  • 如果修改灯怎么办?
    • 直接修改设备树中的led设备节点
    	led {
    		compatible = "jz2440_led";
    		reg = <S3C2410_GPF(6) 1>;
    	};

上传编译,直接使用新的dtb文件

我们使用另外一种方法指定引脚

	led {
		compatible = "jz2440_led";
		pin = <S3C2410_GPF(5)>;
	};

修改led_drv中的probe函数

在of.h中找到获取of属性的函数 of_property_read_s32

static int led_probe(struct platform_device *pdev)
{
	struct resource		*res;

	/* 根据platform_device的资源进行ioremap */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (res) {
		led_pin = res->start;
	}
	else {
		/* 获得pin属性 */
		of_property_read_s32(pdev->dev.of_node, "pin", &led_pin);
	}

	if (!led_pin) 
	{
		printk("can not get pin for led\n");
		return -EINVAL;
	}
		

	major = register_chrdev(0, "myled", &myled_oprs);

	led_class = class_create(THIS_MODULE, "myled");
	device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
	
	return 0;
}
  • 从新编译设备树 和led驱动文件

在platform_device结构体中的struct device dev;中对于dts生成的platform_device这里含有of_node

of_node中含有属性,这取决于设备树,比如compatible属性 让后注册/配置/file_operation

第06节_只想使用设备树不想深入研究怎么办

寄希望于写驱动程序的人,提供了文档/示例/程序写得好适配性强
根据之前写的设备树

   	led {
   		compatible = "jz2440_led";
   		reg = <S3C2410_GPF(6) 1>;
   	};

led { compatible = "jz2440_led"; pin = <S3C2410_GPF(5)>; };

可以通过reg指定引脚也可以通过pin指定引脚,我们在设备树中如何指定引脚完全取决于驱动程序 既可以获取pin属性值也可以获取reg属性值

	/* 根据platform_device的资源进行ioremap */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (res) {
		led_pin = res->start;
	}
	else {
		/* 获得pin属性 */
		of_property_read_s32(pdev->dev.of_node, "pin", &led_pin);
	}

	if (!led_pin) 
	{
		printk("can not get pin for led\n");
		return -EINVAL;
	}

我们通过驱动程序再次验证了设备树的属性完全却决于写驱动程序的人
commpatible属性必须是 jz2440_led 才可以和驱动匹配成功

我们写驱动的人应该写一个文档,告诉写应用程序的人设备树的节点应该怎么编写

对于内核自带的驱动文件,对应的设备树的文档一般放在Documentation\devicetree\bindings目录中,里面有各种架构的说明文档以及各种协议的说明文档, 这些驱动都能在 drivers 目录下找到对应的驱动程序. 比如查看Documentation\devicetree\bindings\arm\samsung\exynos-chipid.txt里面的内容如下

 SAMSUNG Exynos SoCs Chipid driver.
 Required properties:(必须填写的内容)
 - compatible : Should at least contain "samsung,exynos4210-chipid".
 - reg: offset and length of the register set
 Example:
	chipid@10000000 {
		compatible = "samsung,exynos4210-chipid";
		reg = <0x10000000 0x100>;
	};

我们自己写的驱动说明文档自然没有适配到内核中去,所以只能期盼商家给你提供相应的说明文档

参考同类型单板的设备树文件 进入 arch\arm\boot\dts 目录下里面是各种单板的设备树文件 比如

am335x-boneblack.dts和am335x-boneblack-wireless.dts

发现多了wifi的信息,通过对比设备树文件,我们可以看出怎么写wifi设备节点,就知道如何添加设备节点.

网上搜索 实在不行就研究驱动源码 一个好的驱动程序,它会尽量确定所用资源,只把不能确定的资源留给设备树,让设备树来指定。