Embedded Linux Development——Note (2)——字符设备驱动的开发流程

本文是Linux设备驱动开发笔记,梳理了字符设备驱动程序开发流程,包括模块加载卸载、设备注册注销、编写驱动函数等。以LED驱动为例介绍开发细节,还阐述了新版驱动程序编写的优化,如设备号分配、注册方法、自动创建设备节点等。

Copyright Mentioned Ahead

这篇文章是我学习Linux设备驱动开发的笔记,是在阅读正点原子的教程中的摘要,所以版权将会被标记为转载。我们首先梳理一下开发一款字符设备驱动程序的流程,然后在此基础上分析一下最简单的设备驱动程序LED的开发流程。最后作为补充,再加入一些关于新版驱动程序编写的流程。

吐槽一下,正点原子的教程,逻辑太乱了,看的时候绕来绕去。

1.字符设备驱动的重要步骤——自顶向下看

1.1 驱动模块的加载与卸载

假设我们我们已经有了一个字符设备驱动模块,驱动编译完成之后的模块后缀是.ko,有两种方法可以加载驱动模块:insmod和modprobe,它们的基本用法如下:

# insmod加载模块
insmod drv.ko
# rmmod卸载模块
rmmod drv.ko

# -------------------------------

# modprobe加载模块
modprobe drv.ko
# modprobe 卸载模块
modprobe -r drv.ko

insmod和rmmod是配对的,它们在加载模块时不能自动分析模块依赖关系,而modprobe可以,所以在加载驱动模块时推荐modprobe。而使用modprobe -r卸载模块时必须首先卸载依赖它的其他模块,所以在卸载模块时推荐rmmod命令

1.2 字符设备的注册与注销

我们在编写驱动代码时,要分别使用module_init、module_exit来向Linux内核注册模块入口和出口函数。当使用上述的insmod或modprobe命令加载或者卸载驱动模块时,使用module_init和module_exit注册的入口与出口函数就会被自动执行,以完成设备的初始化或者注销,所以下面仔细研究一下入口和出口函数的写法。

人口和出口函数中要做的事就是对字符设备进行初始化和注册,或者注销。在老版驱动程序中,完成注册和注销对应功能的函数分别是register_chrdev和unregister_chrdev

// @param major:要注册的设备的主设备号
// @param name:要注册的设备名,帮助debug
// @param fops:设备驱动包含的动作结构体
static inline int register_chrdev(
									unsigned int major, 
									const char *name,
									const struct file_operations *fops);

//---------分割线-----------
// @param major: 要注销的设备的主设备号
// @param name: 要注销的设备的名字
static inline void unregister_chrdev(
									unsigned int major, 
									const char *name);

1.3 编写设备的具体驱动函数

我们在上面注册设备时用到了register_chrdev函数,这个函数要接收一个描述设备支持的操作的结构体fops来描述这个设备驱动程序中需要支持的操作。所以这里就需要我们自己编写代码,来实现去驱动程序中例如打开文件、读写文件等等一系列的操作,这是开发驱动设备时最困难和千变万化的部分,我们需要自己阅读手册并正确地配置寄存器完成这些函数

在完成具体函数的编写和实现之后,使用如下类似的语法将函数注册到一个file_operations类型的结构体中,随后在调用register_chrdev函数时将此结构体传入即可,下面是虚拟的字符设备驱动的函数注册代码,作为参考。

static struct file_operations test_fops = {
	.owner = THIS_MODULE, 
	.open = chrtest_open,
	.read = chrtest_read,
	.write = chrtest_write,
	.release = chrtest_release,
};

1.4 添加LICENSE和作者信息

在开发完上述流程之后,其实我们大部分工作已经完成了,但是最后还要添加一下此驱动遵循的LICENSE信息和作者的相关信息,这些信息使用如下宏来定义:

// 给驱动程序附加LICENSE和作者相关信息,其中LICENSE是必须的
MODULE_LICENSE();
MODULE_AUTHOR();

// 例子:添加模块LICENSE信息和作者信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tom");

2.设备号及其分配

在Linux系统中,Linux 中每个设备都有一个设备号,设备号由主设备号和从设备号两部分组成,主设备号表示某一个具体的驱动程序,从设备号表示使用这个驱动的各个设备。设备号的本质就是一个无符号整型数据(unsigned int32),其中高12位为主设备号,低20位为从设备号。所以,主设备号的取值范围是0~4095,在选择主设备号时一定不能超过这个范围

在分配主设备号时(如上面我们注册设备时,register_chrdev函数中要传入的第一个参数)可以有如下两种方法:

  • 静态分配:使用cat /proc/devices命令可以列出当前所有已经被占用的设备号,然后手动选一个没有被占用的主设备号
  • 动态分配(推荐!):我们可以使用alloc_chrdev_region和unregister_chrdev_region这两个函数来让Linux自动帮我们申请和回收设备号。这两个函数的原型如下,注意它们都可以用来分配和回收一段范围内的设备号
// @param dev: 申请到的主设备号,带出参数
// @param baseminor: 从设备号开始值,一般从0开始
// @param count: 要申请的设备号数量
// @param name: 设备名称 
int alloc_chrdev_region( 	dev_t *dev, 
							unsigned baseminor, 
							unsigned count, 
							const char *name);
// -------------------------------------------------
// @param from: 要释放的初始主设备号
// @param count: 要释放的设备号数量
void unregister_chrdev_region(dev_t from, unsigned count);

3.实例与更多细节——以led驱动的开发为例

3.1 ioremap和iounmap函数

上面我们简单陈述了字符设备驱动的开发流程,现在以一个例子将上述过程串起来,这个例子就是最简单的字符设备:LED灯。我们在板子上点亮LED灯,本质是通过GPIO的一个引脚输出高电平而做到的,那么现在要做的事情其实非常简单:就是向MMIO指定的地址写入值(电平)即可

但是因为有MMU的存在,就势必绕不开虚拟地址到物理地址的映射这个关键环节,这是由MMU自动完成的,现在的问题是我们知道端口的物理地址(就像裸机开发的时候那样),但是不知道它对应的虚拟地址,也就无法在程序中通过指针去访问这个地址。也就是反映射的过程我们无法实现, 为了帮助我们解决这个问题,Linux系统中提供了两个函数帮助我们将IO的物理地址映射到虚拟地址空间:ioremap和iounmap

前者用于获取指定物理地址空间对应的虚拟地址空间,后者用于释放掉ioremap所做的映射

// ioremap完成物理地址到虚拟地址的反映射
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)

// ioremap函数本质上是以下__arm_ioremap函数的简单封装
// @param phys_addr : 要反映射的物理地址起始位置
// @param size : 要反映射的地址空间大小
// @param mtype : ioremap的类型,默认传入MT_DEVICE
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
{
	return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
}

以下是iounmap的函数声明:

// 解除ioremap构建的映射关系,只需要将ioremap返回的指针传入即可
// @param addr : 要释放的IO设备在虚拟地址空间中的首地址
void iounmap (volatile void __iomem *addr)

可能你注意到了__iomem这个奇怪的修饰符,这个符号背后的文章不少。大体来说,它说明当前指向的地址空间属于IO地址空间。同时,含有__iomem修饰符的指针不允许解引用。那么,既然不能解引用,我们如何去访问这块地址呢?Linux要求我们必须通过以下这些函数来访问含有__iomem修饰的指针,否则编译器可能会报错或警告:

// 以不同大小来读取__iomem指针
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

// 以不同大小来写入__iomem指针
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

3.2 实例-浅析设备的初始化代码

至于驱动程序的编写,其实非常简单,这里比较有趣的是初始化函数,这里贴出源码。可以看到首先代码中使用ioremap将物理地址映射到虚拟地址上,然后就和裸机开发一样,进行GPIO寄存器的配置,注意对寄存器的读写都通过上述特制的读写函数来完成,这些都是很常规的操作,不再展开说明。

最后使用register_chrdev函数将设备驱动进行注册,这样就完成了整个初始化过程。led_init函数将会作为入口函数使用module_init进行注册,这样我们在加载驱动模块时这个函数就会被连带执行。

static int __init led_init(void)
{
	int retvalue = 0;
	u32 val = 0;

	/* 初始化LED */
	/* 1、寄存器地址映射 */
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

	/* 2、使能GPIO1时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	/* 清楚以前的设置 */
	val |= (3 << 26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 3、设置GPIO1_IO03的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	/* 清除以前的设置 */
	val |= (1 << 3);	/* 设置为输出 */
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

	/* 6、注册字符设备驱动 */
	retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
	if(retvalue < 0){
		printk("register chrdev failed!\r\n");
		return -EIO;
	}
	return 0;
}

与上述过程对应,我们也得有个出口函数,代码如下所示,首先使用iounmap取消映射,最后注销驱动模块

static void __exit led_exit(void)
{
	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

	/* 注销字符设备驱动 */
	unregister_chrdev(LED_MAJOR, LED_NAME);
}

3.3 上板验证

我们将上述编写的代码(其实也就是正点原子写好的代码…汗)进行编译得到以.ko(kernel object)为后缀的驱动模块。得到这个模块之后通过nfs直接映射到开发板上,并放到可加载的内核模块文件夹中,这个文件夹的名称因内核版本而异,我的路径是/lib/modules/4.1.15-g3dc0a4b,注意要保证之前编译内核驱动模块时使用的Linux内核源码版本也必须是/4.1.15-g3dc0a4b,否则就会报错,详情可见这篇微信文章,我在上板验证的过程中就踩了坑。

验证的步骤如下:

# 1.此命令用于检测模块之间的相互依赖性,应该在调用modprobe之前执行
depmod

# 2.然后调用modprobe进行驱动模块加载
modprobe led.ko

# 这里可能会报错
# root@ATK-IMX6U:/lib/modules/4.1.15-g3dc0a4b# modprobe led.ko
# modprobe: FATAL: Module led.ko not found in directory /lib/modules/4.1.15-g3dc0a4b
# 这种情况只需要去掉.ko的后缀即可

# 3.为设备创建设备节点
# c表示当前设备是字符设备,200和0分别是主设备号和从设备号
mknod /dev/led c 200 0

其实在执行完modprobe指令之后,驱动模块就已经加载好了,我们可以通过cat /proc/devices命令来确认,如下图所示。而创建设备节点只是为了让我们更方便地去使用这个设备,就好像读写一个文件一样
在这里插入图片描述

4.新版驱动程序的编写

这里正点原子引入了新的字符设备驱动开发流程,这里只简单写出和传统流程不同的地方,我总结有以下几点:

4.1 优化1——设备号的分配过程

在传统的开发流程中,我们使用register_chrdev函数来手工指定设备号,在此之前我们必须通过cat /proc/devices来确定某一个设备号有没有被使用,相对来说不方便也不灵活。

retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);

在新版设备驱动程序中,我们引入了alloc_chrdev_region和register_chrdev_region这样两个函数来向Linux系统申请设备号。这两个函数的区别在于alloc_chrdev_region函数是让Linux系统自动分配一个空闲设备号出来,而register_chrdev_region则是由用户向Linux系统申请指定的设备号。在新版驱动代码中是这样做的:

if (newchrled.major) {												/*  如果指定了设备号 */
		newchrled.devid = MKDEV(newchrled.major, 0);
		register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
	} else {														/* 没有指定设备号 */
		alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);	
		// ...
	}

也就是说,如果用户提前指定好了设备号,那么就直接用register_chrdev_region函数申请,否则使用alloc_chrdev_region函数让Linux系统自动帮助我们分配一个设备好,并通过newchrled.devid这个量直接带出

当然,向系统申请了设备号就必须要释放设备号,所以出口函数代码如下,我们可以看到有释放设备号的过程,这个操作是使用unregister_chrdev_region函数完成的,此函数专门用来释放通过alloc_chrdev_region和register_chrdev_region函数分配的设备号

static void __exit led_exit(void)
{
	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

	/* 注销字符设备驱动 */
	cdev_del(&newchrled.cdev);/*  删除cdev */
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */

	device_destroy(newchrled.class, newchrled.devid);
	class_destroy(newchrled.class);
}

4.2 优化2——新的字符设备注册方法

在之前的版本中,我们使用register_chrdev函数向内核注册一个字符设备,在新版驱动程序的编写中这个过程变为了使用cdev这个特殊结构串连起来的一个过程。也就是说在Linux内核中有一个专门的数据结构来表示字符设备,即cdev:

struct cdev {
	struct kobject kobj;
	struct module *owner;				// 一般是THIS_MODULE
	const struct file_operations *ops;	// 字符设备的各个动作都记录在此
	struct list_head list;		
	dev_t dev;							// 设备号
	unsigned int count;
};

我们向内核注册一个设备的动作变成了如下几步:

// 1.使用cdev_init函数初始化一个cdev设备
// @param cdev : 指向设备的指针
// @param fops : 设备支持的驱动操作
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

// 2.使用cdev_add函数将上面初始化好的字符设备添加到内核中
// @param p : 指向字符设备的指针
// @param dev : 设备号
// @param count : 要添加的设备数量
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

// 3.在出口函数中,删除掉这个字符设备,将设备指针传入即可
// @param p : 指向字符设备的指针
void cdev_del(struct cdev *p)

4.3 优化3——自动创建设备节点

在老版本的驱动代码中,我们在得到.ko模块之后还得自己使用mknod命令来创建设备节点,而在新版代码中完全可以使用mdev机制来自动创建节点。这里不深入了解mdev机制,而是直接给出开发的流程,我们要在cdev_add操作之后使用class_create函数和device_create函数创建这个设备节点

// 1.在cdev_add操作之后创建一个类
// @param owner :此模块属于什么机构,一般写为THIS_MODULE
// @param name : 类的名称
struct class *class_create (struct module *owner, const char *name)

// 2.使用device_create函数创建一个设备
// @param class : 设备所属的类,就用上面创建出来的类
// @param parent : 该设备的父设备,如果是单一模块开发,则设为NULL
// @param devt : 设备号
// @param drvdata : 设备的一些附加数据,一般设为NULL
// @param fmt : 设备名字,传入“xxx”时,会生成名为/dev/xxx的设备
struct device *device_create(	struct class *class, 
 								struct device *parent,
 								dev_t devt, 
 								void *drvdata, 
 								const char *fmt, ...)

// 2.与上述函数对应的,在出口函数中应该将创建的设备和类都销毁
void class_destroy(struct class *cls);
void device_destroy(struct class *class, dev_t devt);

4.4 对数据的保护

经过上面引入新的函数与结构体,现在一个函数相关联的信息有很多,首先一个字符设备被抽象成了cdev这样一个结构体,它还属于一个类class,同时它本身作为一个设备device,也有自己的相关信息。所以为了保证这些信息逻辑上的相关性,我们一般将其声明为一个结构体,并使用,如下所示:

struct newchrled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};

// 使用上面的结构体声明一个设备,这就是我们要操作的设备实例
// 注意,最好将它注册进flip->private_data
struct newchrled_dev newchrled;	/* led设备 */

这样一个结构体其实就抽象了我们定义的整个设备,在设备的open函数中,一般要将其地址注册到flip->private_data字段中,比如在新版驱动代码中有如下实现:

// 将led设备对应的结构体注册到private_data字段
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrled; /* 设置私有数据 */
	return 0;
}

4.5 总结

这里给出出口函数和入口函数的代码实现,注意和老版本的代码实现进行对比:

4.5.1 新版入口函数的实现

/* newchrled设备结构体 */
struct newchrled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};

struct newchrled_dev newchrled;	/* led设备 */

static int __init led_init(void)
{
	u32 val = 0;

	/* 初始化LED */
	/* 1、寄存器地址映射 */
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

	/* 2、使能GPIO1时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	/* 清楚以前的设置 */
	val |= (3 << 26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 3、设置GPIO1_IO03的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	/* 清除以前的设置 */
	val |= (1 << 3);	/* 设置为输出 */
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);
	
	// ----------------------------以下代码和老版本实现不一致----------------------
	// ---------------------------4.1 优化1——设备号的分配过程----------------------------
	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (newchrled.major) {			/*  定义了设备号 */
		newchrled.devid = MKDEV(newchrled.major, 0);
		register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);	/* 申请设备号 */
		newchrled.major = MAJOR(newchrled.devid);	/* 获取分配号的主设备号 */
		newchrled.minor = MINOR(newchrled.devid);	/* 获取分配号的次设备号 */
	}
	printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor);	
	
	// ---------------------------4.2 优化2——新的字符设备注册方法------------------------
	/* 2、初始化cdev */
	newchrled.cdev.owner = THIS_MODULE;
	cdev_init(&newchrled.cdev, &newchrled_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
	
	// ---------------------------4.3 优化3——自动创建设备节点------------------------
	/* 4、创建类 */
	newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
	if (IS_ERR(newchrled.class)) {
		return PTR_ERR(newchrled.class);
	}

	/* 5、创建设备 */
	newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
	if (IS_ERR(newchrled.device)) {
		return PTR_ERR(newchrled.device);
	}
	
	return 0;
}

4.5.2 新版出口函数的实现

static void __exit led_exit(void)
{
	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);
	
	// ---------------新版出口函数要做的事情-------------
	// 1.注销字符设备
	// 2.注销设备号
	// 3.注销设备节点
	/* 注销字符设备驱动 */
	cdev_del(&newchrled.cdev);/*  删除cdev */
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */

	device_destroy(newchrled.class, newchrled.devid);
	class_destroy(newchrled.class);
}

Linux Driver Development for Embedded Processors – Second Edition 版本: Learn to develop Linux embedded drivers with kernel 4.9 LTS The flexibility of Linux embedded, the availability of powerful, energy efficient processors designed for embedded computing and the low cost of new processors are encouraging many industrial companies to come up with new developments based on embedded processors. Current engineers have in their hands powerful tools for developing applications previously unimagined, but they need to understand the countless features that Linux offers today. This book will teach you how to develop device drivers for Device Tree Linux embedded systems. You will learn how to write different types of Linux drivers, as well as the appropriate APIs (Application Program Interfaces) and methods to interface with kernel and user spaces. This is a book is meant to be practical, but also provides an important theoretical base. More than twenty drivers are written and ported to three different processors. You can choose between NXP i.MX7D, Microchip SAMA5D2 and Broadcom BCM2837 processors to develop and test the drivers, whose implementation is described in detail in the practical lab sections of the book. Before you start reading, I encourage you to acquire any of these processor boards whenever you have access to some GPIOs, and at least one SPI and I2C controllers. One of the boards used to implement the drivers is the famous Raspberry PI 3 Model B board. You will learn how to develop drivers, from the simplest ones that do not interact with any external hardware, to drivers that manage different kind of devices: accelerometers, DACs, ADCs, RGB LEDs, Multi-Display LED controllers, I/O expanders, and Buttons. You will also develop DMA drivers, drivers that manage interrupts, and drivers that write/read on the internal registers of the processor to control external devices. To easy the development of some of these drivers, you will use different types of Frameworks: Miscellaneous framework, LED framework, UIO framework, Input framework and the IIO industrial one. This second edition has been updated to the v4.9 LTS kernel.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值