ELADCMSecondEditionChapterFive

来自百问网嵌入式Linux wiki

目录

第五篇. 嵌入式Linux驱动开发基础知识

嵌入式后Linux驱动开发基础知识的引导与说明

打算讲什么、怎么讲?

以几个简单的驱动程序,讲解嵌入式Linux驱动的框架,了解驱动开发的流程、方法,掌握从APP到驱动的调用流程。
会涉及很多种开发板,让你明白“Linux驱动 = 软件框架 + 硬件操作”,让你“一通百通”,掌握了普适性的原理之后,在工作中很容易在其他板子使用这些知识。
以LED驱动为例,会如下讲解:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 001.png

需要做什么准备工作

驱动程序依赖于Linux内核,你为开发板A开发驱动,那就先在Ubuntu中得到、配置、编译开发板A所使用的Linux内核。
请使用git下载本教程的文档、源码,查看如下目录中你所用开发板的高级用户使用手册(有些开发板的手册我们还没编写完,持续更新):
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 002.png
根据手册完成下面操作:
硬件部分:
① 开发板接线:串口线、电源线、网线
② 开发板烧写系统
软件部分:
① 下载Linux内核,Windows和Ubuntu下各放一份
② Windows下:使用Source Insight创建内核源码的工程,这是用来浏览内核、编辑驱动
③ Ubuntu下:安装工具链,配置、编译Linux内核
注意:git的使用方法请参考http://wiki.100ask.net中的“初学者学习路线”:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 003.png

Hello驱动(不涉及硬件操作)

我们选用的内核都是4.x版本,操作都是类似的:
	rk3399   linux 4.4.154
	rk3288   linux 4.4.154
	imx6ul   linux 4.9.88
	am3358  linux 4.9.168

APP打开的文件在内核中如何表示

APP打开文件时,可以得到一个整数,这个整数被称为文件句柄。对于APP的每一个文件句柄,在内核里面都有一个“struct file”与之对应。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 004.png
可以猜测,我们使用open打开文件时,传入的flags、mode等参数会被记录在内核中对应的struct file结构体里(f_flags、f_mode):
	int open(const char *pathname, int flags, mode_t mode);
去读写文件时,文件的当前偏移地址也会保存在struct file结构体的f_pos成员里。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 005.png

打开字符设备节点时,内核中也有对应的struct file

注意这个结构体中的结构体:struct file_operations *f_op,这是由驱动程序提供的。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 006.png
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 007.png
结构体struct file_operations的定义如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 008.png

请猜猜怎么编写驱动程序

① 确定主设备号,也可以让内核分配
② 定义自己的file_operations结构体
③ 实现对应的drv_open/drv_read/drv_write等函数,填入file_operations结构体
④ 把file_operations结构体告诉内核:register_chrdev
⑤ 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
⑥ 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
⑦ 其他完善:提供设备信息,自动创建设备节点:class_create, device_create

请不要啰嗦,表演你的代码吧

写驱动程序

参考driver/char中的程序,包含头文件,写框架,传输数据:
A. 驱动中实现open, read, write, release,APP调用这些函数时,都打印内核信息
B. APP调用write函数时,传入的数据保存在驱动中
C. APP调用read函数时,把驱动中保存的数据返回给APP
使用GIT下载所有源码后,本节源码位于如下目录:
		01_all_series_quickstart\04_快速入门(正式开始)\
			02_嵌入式Linux驱动开发基础知识\source\01_hello_drv\hello_drv.c
hello_drv.c源码如下:
	#include <linux/module.h>

	#include <linux/fs.h>
	#include <linux/errno.h>
	#include <linux/miscdevice.h>
	#include <linux/kernel.h>
	#include <linux/major.h>
	#include <linux/mutex.h>
	#include <linux/proc_fs.h>
	#include <linux/seq_file.h>
	#include <linux/stat.h>
	#include <linux/init.h>
	#include <linux/device.h>
	#include <linux/tty.h>
	#include <linux/kmod.h>
	#include <linux/gfp.h>

	/* 1. 确定主设备号 */
	static int major = 0;
	static char kernel_buf[1024];
	static struct class *hello_class;


	#define MIN(a, b) (a < b ? a : b)

	/* 3. 实现对应的open/read/write等函数,填入file_operations结构体 */ 
	static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
	{
	     int err;
	     printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	     err = copy_to_user(buf, kernel_buf, MIN(1024, size));
	     return MIN(1024, size);
	}

	static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
	{
	     int err;
	     printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	     err = copy_from_user(kernel_buf, buf, MIN(1024, size));
	     return MIN(1024, size);
	}

	static int hello_drv_open (struct inode *node, struct file *file)
	{
	     printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	     return 0;
	}

	static int hello_drv_close (struct inode *node, struct file *file)
	{
	     printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	     return 0;
	}

	/* 2. 定义自己的file_operations结构体 */
	static struct file_operations hello_drv = {
	     .owner   = THIS_MODULE,
	     .open    = hello_drv_open,
	     .read    = hello_drv_read,
	     .write   = hello_drv_write,
	     .release = hello_drv_close,
	};

	/* 4. 把file_operations结构体告诉内核:注册驱动程序 */
	/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
	static int __init hello_init(void)
	{
	     int err;

	     printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	     major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello */


	     hello_class = class_create(THIS_MODULE, "hello_class");
	     err = PTR_ERR(hello_class);
	     if (IS_ERR(hello_class)) {
	             printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	             unregister_chrdev(major, "hello");
	             return -1;
	     }

	     device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */

	     return 0;
	}

	/* 6. 有入口函数就有出口函数:卸载驱动程序时就会去调用这个出口函数 */
	static void __exit hello_exit(void)
	{
	     printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	     device_destroy(hello_class, MKDEV(major, 0));
	     class_destroy(hello_class);
	     unregister_chrdev(major, "hello");
	}


	/* 7. 其他完善:提供设备信息,自动创建设备节点 */

	module_init(hello_init);
	module_exit(hello_exit);

	MODULE_LICENSE("GPL");
阅读一个驱动程序,从它的入口函数开始,第66行就是入口函数。它的主要工作就是第71行,向内核注册一个file_operations结构体:hello_drv,这就是字符设备驱动程序的核心。
file_operations结构体hello_drv在第56行定义,里面提供了open/read/write/release成员,应用程序调用open/read/write/close时就会导致这些成员函数被调用。
file_operations结构体hello_drv中的成员函数都比较简单,大多数只是打印而已。要注意的是,驱动程序和应用程序之间传递数据要使用copy_from_user/copy_to_user函数。

写测试程序

测试程序要实现写、读功能:
	A.  ./hello_drv_test  -w  wiki.100ask.net  // 把字符串“wiki.100ask.net”发给驱动程序
	B.  ./hello_drv_test  -r                  // 把驱动中保存的字符串读回来
使用GIT下载所有源码后,本节源码位于如下目录:
	01_all_series_quickstart\04_快速入门(正式开始)\
		02_嵌入式Linux驱动开发基础知识\source\01_hello_drv\hello_drv_test.c
hello_drv_test.c源码如下:
	#include <sys/types.h>
	#include <sys/stat.h>
	#include <fcntl.h>
	#include <unistd.h>
	#include <stdio.h>
	#include <string.h>
	
	/*
	 * ./hello_drv_test -w abc
	 * ./hello_drv_test -r
	 */
	int main(int argc, char **argv)
	{
	     int fd;
	     char buf[1024];
	     int len;
	
	     /* 1. 判断参数 */
	     if (argc < 2)
	     {
	             printf("Usage: %s -w <string>\n", argv[0]);
	             printf("       %s -r\n", argv[0]);
	             return -1;
	     }
	
	     /* 2. 打开文件 */
	     fd = open("/dev/hello", O_RDWR);
	     if (fd == -1)
	     {
	             printf("can not open file /dev/hello\n");
	             return -1;
	     }
	
	     /* 3. 写文件或读文件 */
	     if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
	     {
	             len = strlen(argv[2]) + 1;
	             len = len < 1024 ? len : 1024;
	             write(fd, argv[2], len);
	     }
	     else
	     {
	             len = read(fd, buf, 1024);
	             buf[1023] = '\0';
	             printf("APP read : %s\n", buf);
	     }
	
	     close(fd);
	
	     return 0;
	}

测试

A. 编写驱动程序的Makefile
驱动程序中包含了很多头文件,这些头文件来自内核,不同的ARM板它的某些头文件可能不同。所以编译驱动程序时,需要指定板子所用的内核的源码路径。
要编译哪个文件?这也需要指定,设置obj-m变量即可
怎么把.c文件编译为驱动程序.ko?这要借助内核的顶层Makefile。
本驱动程序的Makefile内容如下:
	01
	02 # 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
	03 # 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
	04 # 2.1 ARCH,          比如: export ARCH=arm64
	05 # 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
	06 # 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
	07 # 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
	08 #       请参考各开发板的高级用户使用手册
	09
	10 KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.4
	11
	12 all:
	13      make -C $(KERN_DIR) M=`pwd` modules
	14      $(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c
	15
	16 clean:
	17      make -C $(KERN_DIR) M=`pwd` modules clean
	18      rm -rf modules.order
	19      rm -f hello_drv_test
	20
	21 obj-m        += hello_drv.o
先设置好交叉编译工具链,编译好你的板子所用的内核,然后修改Makefile指定内核源码路径,最后即可执行make命令编译驱动程序和测试程序。
B. 上机实验
注意:我们是在Ubuntu中编译程序,但是需要在ARM板子上测试。所以需要把程序放到ARM板子上。
启动单板后,可以通过NFS挂载Ubuntu的某个目录,访问该目录中的程序。
测试示例:
① 在Ubuntu上编译好驱动,并它复制到NFS目录:
	$ cp *.ko hello_drv_test ~/nfs_rootfs/
② 在ARM板上测试:
	# echo "7 4 1 7" > /proc/sys/kernel/printk  // 打开内核的打印信息,有些板子默认打开了
	# ifconfig eth0 192.168.1.100   // 配置ARM板IP,下面是挂载NFS文件系统
	# mount -t nfs -o nolock,vers=3  192.168.1.137:/home/book/nfs_rootfs  /mnt
	# cd  /mnt
	# insmod hello_drv.ko    // 安装驱动程序
	[  293.594910] hello_drv: loading out-of-tree module taints kernel.
	[  293.616051] /home/book/source/01_hello_drv/hello_drv.c hello_init line 70
	# ls /dev/hello -l        // 驱动程序会生成设备节点
	crw-------    1 root     root      236,   0 Jan 18 08:55 /dev/hello
	# ./hello_drv_test        // 查看测试程序的用法
	Usage: ./hello_drv_test -w <string>
	       ./hello_drv_test -r
	# ./hello_drv_test -w wiki.100ask.net    // 往驱动程序中写入字符串
	[  318.360800] /home/book/source/01_hello_drv/hello_drv.c hello_drv_open line 45
	[  318.372570] /home/book/source/01_hello_drv/hello_drv.c hello_drv_write line 38
	[  318.382854] /home/book/source/01_hello_drv/hello_drv.c hello_drv_close line 51
	# ./hello_drv_test -r                  // 从驱动程序中读出字符串
	[  326.177890] /home/book/source/01_hello_drv/hello_drv.c hello_drv_open line 45
	[  326.198304] /home/book/source/01_hello_drv/hello_drv.c hello_drv_read line 30
	APP read : wiki.100ask.net
	[  326.214782] /home/book/source/01_hello_drv/hello_drv.c hello_drv_close line 51
注意:如果安装驱动时提示version magic不匹配,请看本文档最后的“常见问题”。

Hello驱动中的一些补充知识

module_init/module_exit的实现

register_chrdev的内部实现

class_destroy/device_create浅析

硬件知识_LED原理图

当我们学习C语言的时候,我们会写个Hello程序。
那当我们写ARM程序,也该有一个简单的程序引领我们入门,这个程序就是点亮LED。
我们怎样去点亮一个LED呢?
分为三步:
1.看原理图,确定控制LED的引脚;
2.看主芯片的芯片手册,确定如何设置控制这个引脚;
3.写程序;

先来讲讲怎么看原理图

LED样子有很多种,像插脚的,贴片的。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 009.png
它们长得完全不一样,因此我们在原理图中将它抽象出来。
点亮LED需要通电源,同时为了保护LED,加个电阻减小电流。
控制LED灯的亮灭,可以手动开关LED,但在电子系统中,不可能让人来控制开关,通过编程,利用芯片的引脚去控制开关。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 010.png
LED的驱动方式,常见的有四种。
方式1:使用引脚输出3.3V点亮LED,输出0V熄灭LED。
方式2:使用引脚拉低到0V点亮LED,输出3.3V熄灭LED。
有的芯片为了省电等原因,其引脚驱动能力不足,这时可以使用三极管驱动。
方式3:使用引脚输出1.2V点亮LED,输出0V熄灭LED。
方式4:使用引脚输出0V点亮LED,输出1.2V熄灭LED。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 011.png
由此,主芯片引脚输出高电平/低电平,即可改变LED状态,而无需关注GPIO引脚输出的是3.3V还是1.2V。
所以简称输出1或0:
逻辑1-->高电平
逻辑0-->低电平

普适的GPIO引脚操作方法

GPIO: General-purpose input/output,通用的输入输出口

GPIO模块一般结构

a.有多组GPIO,每组有多个GPIO
b.使能:电源/时钟
c.模式(Mode):引脚可用于GPIO或其他功能
d.方向:引脚Mode设置为GPIO时,可以继续设置它是输出引脚,还是输入引脚
e.数值:对于输出引脚,可以设置寄存器让它输出高、低电平
对于输入引脚,可以读取寄存器得到引脚的当前电平

GPIO寄存器操作

a.芯片手册一般有相关章节,用来介绍:power/clock
可以设置对应寄存器使能某个GPIO模块(Module)
有些芯片的GPIO是没有使能开关的,即它总是使能的
b.一个引脚可以用于GPIO、串口、USB或其他功能,
有对应的寄存器来选择引脚的功能
c.对于已经设置为GPIO功能的引脚,有方向寄存器用来设置它的方向:输出、输入
d.对于已经设置为GPIO功能的引脚,有数据寄存器用来写、读引脚电平状态
GPIO寄存器的2种操作方法:
原则:不能影响到其他位
a.直接读写:读出、修改对应位、写入
要设置bit n:
		val = data_reg;
		val = val | (1<<n);
		data_reg = val;
要清除bit n:
		val = data_reg;
		val = val & ~(1<<n);
		data_reg = val;
b.set-and-clear protocol:
set_reg, clr_reg, data_reg 三个寄存器对应的是同一个物理寄存器,
要设置bit n:set_reg = (1<<n);
要清除bit n:clr_reg = (1<<n);

GPIO的其他功能:防抖动、中断、唤醒

后续章节再介绍

具体单板的GPIO操作方法

请使用GIT下载文档后,看下图红框所示目录中各板子对应的文档及图片。
网盘中相同名字的目录下也有对应的视频。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 012.png
为方便学习,在本文档中也把上述GIT目录中的文档添加进来了。

AM335X的GPIO操作方法

GPIO: General-purpose input/output,通用的输入输出口
PRCM: Power, Reset, and Clock Management (电源、复位、时钟管理器)
CM: Control Module(控制模块) 或 Clock Module (时钟模块)
PRM_PER: Power Reset Module Peripheral Registers(电源/复位模块中关于外设的寄存器)
CM_PER: Clock Module Peripheral Registers (时钟模块中关于外设的寄存器)

AM335X的GPIO模块结构

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 013.png
有4组GPIO(GPIO0~3),每组有32个GPIO。
GPIO的控制涉及3大模块:PRCM、Control Module、GPIO模块本身。
① PRCM用于使能模块:
GPIO0永远都是使能的,GPIO1~3可单独控制。
PRCM模块给GPIO模块常供电,只需要使能GPIO模块的时钟。
② Control Module用于设置模式(Mode):
设置引脚的Mode(即选择功能)、上下拉电阻等;
每一个GPIO引脚在Control Module中都有一个寄存器,怎么找到这个寄存器?
a. 根据pin number确定pin name
b. 根据pin name在Control Module中确定寄存器
③ GPIO模块内部:
方向:引脚Mode设置为GPIO时,可以继续设置它是输出引脚,还是输入引脚。
数值:对于输出引脚,可以设置寄存器让它输出高、低电平;
对于输入引脚,可以读取寄存器得到引脚的当前电平。

AM335X的GPIO相关寄存器

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 014.png

set-and-clear协议

假设某个GPIO被设置为输出,怎么设置它的输出电平呢?AM335X中对于每个GPIO模块有一个GPIO_DATAOUT寄存器,其中的每一位对应一个引脚,如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 015.png
要设置某一位时,不能影响到其他位,操作方法是:读出原来的值,修改某一位,把新值写回去。需要3个步骤才可以设置某一位的值,这效率太低了!
使用set-and-clear可以只用一个步骤即可修改某一位的值。
当想设置某一位为1时,往DATA_SETDATAOUT寄存器中某位写入1即可,芯片内部会把对应引脚的电平设置为1,并且不会影响其他引脚:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 016.png
当想清除某一位为0时,往DATA_CLEARDATAOUT寄存器中某位写入1即可,芯片内部会把对应引脚的电平设置为0,并且不会影响其他引脚:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 017.png
并非所有的芯片都有set-and-clear功能,TI的AM335X系列芯片有这功能。

RK3288的GPIO操作方法

GPIO: General-purpose input/output,通用的输入输出口
CRU: Clock & Reset Unit (时钟和复位单元)
PMU: Power Managerment Unit (电源管理单元)
GRF: General Register Files (通用寄存器文件)

RK3288的GPIO模块结构

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 018.png
有9组GPIO(GPIO0~8),每组分为最多4个小组port A/B/C/D,每小组最多8个GPIO。理论上每组GPIO的引脚有32个,但是实际上并没有那么多。比如GPIO0只有GPIO0_A0~A7、GPIO0_B0~B7、GPIO0_C0~C2这些引脚。
GPIO的控制涉及4大模块:CRU、PMU、GRF、GPIO模块本身。
① CRU用于设置是否向GPIO模块提供时钟:
CRU的内部结构如下图所示:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 019.png
可以设置寄存器使能GPIOx的时钟:
a. CRU_CLKGATE17_CON用于控制GPIO0;
b. CRU_CLKGATE14_CON用于控制GPIO1~8
② PMU用于控制电源:
电源管理单元里,有多个电源域(power domain,简称为PM),在一个域下有多个设备。
比如PD_ALIVE,它下面有这些设备:CRU、GRF、GPIO 1~8、TIMER或WDT。
比如PD_PMU,它下面有这些设备:PMU、SRAM(4K)、Secure GRF、GPIO0。
可见,GPIO0、GPIO1~8分属不同的PM。
GPIO0、GPIO1~8都是常供电的,它们是否工作取决于其时钟是否使能。
③ 设置引脚的模式(Mode、功能):
GPIO0比较特殊,为了让其引脚用于GPIO功能,要设置PMU里的相关寄存器。
GPIO1~8类似,为了让其引脚用于GPIO功能,要设置GRF里的相关寄存器。
④ GPIO模块内部:
方向:引脚设置为GPIO时,可以继续设置寄存器GPIO_SWPORTA_DDR确定它是输出引脚,还是输入引脚。
数值:对于输出引脚,可以设置寄存器GPIO_SWPORTA_DR让它输出高、低电平;
对于输入引脚,可以读取寄存器GPIO_EXT_PORTA得到引脚的当前电平。

RK3288的GPIO相关寄存器

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 020.png

RK3399的GPIO操作方法

GPIO: General-purpose input/output,通用的输入输出口
CRU: Clock & Reset Unit (时钟和复位单元)
PMU: Power Managerment Unit (电源管理单元)
GRF: General Register Files (通用寄存器文件)

RK3399的GPIO模块结构

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 021.png
有5组GPIO(GPIO0~4),每组分为最多4个小组port A/B/C/D,每小组最多8个GPIO。理论上每组GPIO的引脚有32个,但是实际上并没有那么多。比如GPIO0只有GPIO0_A0~A7、GPIO0_B0~B5这些引脚。
GPIO的控制涉及4大模块:CRU、PMU、GRF、GPIO模块本身。
① CRU用于设置是否向GPIO模块提供时钟
a. PMUCRU_CLKGATE_CON1用于控制GPIO0~1;
b. CRU_CLKGATE_CON31用于控制GPIO2~4
② PMU用于控制电源:
电源管理单元里,有多个电源域(power domain,简称为PM),在一个域下有多个设备。
比如PD_ALIVE,它下面有这些设备:CRU、GRF、GPIO 1~4、TIMER或WDT。
比如PD_PMU,它下面有这些设备:cm0、PMU、SRAM(8K)、Secure GRF、GPIO0、PVTM、I2C。
可见,GPIO0、GPIO1~4分属不同的PM。
GPIO0、GPIO1~4都是常供电的。
③ 设置引脚的模式(Mode、功能):
GPIO0~1比较特殊,为了让其引脚用于GPIO功能,要设置PMU里的相关寄存器。
GPIO2~4类似,为了让其引脚用于GPIO功能,要设置GRF里的相关寄存器。
④ GPIO模块内部:
方向:引脚设置为GPIO时,可以继续设置寄存器GPIO_SWPORTA_DDR确定它是输出引脚,还是输入引脚。
数值:对于输出引脚,可以设置寄存器GPIO_SWPORTA_DR让它输出高、低电平;
对于输入引脚,可以读取寄存器GPIO_EXT_PORTA得到引脚的当前电平。

RK3399的GPIO相关寄存器

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 022.png

IMX6ULL的GPIO操作方法

CCM: Clock Controller Module (时钟控制模块)
IOMUXC : IOMUX Controller,IO复用控制器
GPIO: General-purpose input/output,通用的输入输出口

IMX6ULL的GPIO模块结构

参考资料:芯片手册《Chapter 26: General Purpose Input/Output (GPIO)》
有5组GPIO(GPIO1~GPIO5),每组引脚最多有32个,但是可能实际上并没有那么多。
GPIO1有32个引脚:GPIO1_IO0~GPIO1_IO31;
GPIO2有22个引脚:GPIO2_IO0~GPIO2_IO21;
GPIO3有29个引脚:GPIO3_IO0~GPIO3_IO28;
GPIO4有29个引脚:GPIO4_IO0~GPIO4_IO28;
GPIO5有12个引脚:GPIO5_IO0~GPIO5_IO11;
GPIO的控制涉及4大模块:CCM、IOMUXC、GPIO模块本身,框图如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 023.png

CCM用于设置是否向GPIO模块提供时钟

参考资料:芯片手册《Chapter 18: Clock Controller Module (CCM)》
GPIOx要用CCM_CCGRy寄存器中的2位来决定该组GPIO是否使能。哪组GPIO用哪个CCM_CCGR寄存器来设置,请看上图红框部分。
CCM_CCGR寄存器中某2位的取值含义如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 024.png
① 00:该GPIO模块全程被关闭
② 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭
③ 10:保留
④ 11:该GPIO模块全程使能
GPIO2时钟控制:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 025.png
GPIO1、GPIO5时钟控制:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 026.png
GPIO3时钟控制:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 027.png
GPIO4时钟控制:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 028.png

IOMUXC:引脚的模式(Mode、功能)

参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC)》。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 029.png
对于某个/某组引脚,IOMUXC中有2个寄存器用来设置它:
① 选择功能:
IOMUXC_SW_MUX_CTL_PAD_<PADNAME> :Mux pad xxx,选择某个pad的功能
IOMUXC_SW_MUX_CTL_GRP_<GROUP NAME>:Mux grp xxx,选择某组引脚的功能
某个引脚,或是某组预设的引脚,都有8个可选的模式(alternate (ALT) MUX_MODE)。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 030.png
比如:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 031.png
② 设置上下拉电阻等参数:
IOMUXC_SW_PAD_CTL_PAD_<PADNAME> : pad pad xxx,设置某个pad的参数
IOMUXC_SW_PAD_CTL_GRP_<GROUP NAME>:pad grp xxx,设置某组引脚的参数
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 032.png
比如:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 033.png

GPIO模块内部

框图如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 034.png
我们暂时只需要关心3个寄存器:
① GPIOx_GDIR:设置引脚方向,每位对应一个引脚,1-output,0-input
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 035.png
② GPIOx_GDIR:设置输出引脚的电平,每位对应一个引脚,1-高电平,0-低电平
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 036.png
③ GPIOx_PSR:读取引脚的电平,每位对应一个引脚,1-高电平,0-低电平
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 037.png

怎么编程

读GPIO
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 038.png
翻译一下:
① 设置CCM_CCGRx寄存器中某位使能对应的GPIO模块 // 默认是使能的,上图省略了
② 设置IOMUX来选择引脚用于GPIO
③ 设置GPIOx_GDIR中某位为0,把该引脚设置为输入功能
④ 读GPIOx_DR或GPIOx_PSR得到某位的值(读GPIOx_DR返回的是GPIOx_PSR的值)
写GPIO
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 039.png
翻译一下:
① 设置CCM_CCGRx寄存器中某位使能对应的GPIO模块 // 默认是使能的,上图省略了
② 设置IOMUX来选择引脚用于GPIO
③ 设置GPIOx_GDIR中某位为1,把该引脚设置为输出功能
④ 写GPIOx_DR某位的值
需要注意的是,你可以设置该引脚的loopback功能,这样就可以从GPIOx_PSR中读到引脚的有实电平;你从GPIOx_DR中读回的只是上次设置的值,它并不能反应引脚的真实电平,比如可能因为硬件故障导致该引脚跟地短路了,你通过设置GPIOx_DR让它输出高电平并不会起效果。

LED驱动程序框架

注意:如果做实验安装驱动时提示version magic不匹配,请看本文档最后的“常见问题”。

回顾字符设备驱动程序框架

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 040.png

对于LED驱动,我们想要什么样的接口?

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 041.png

LED驱动要怎么写,才能支持多个板子?分层。

1. 把驱动拆分为通用的框架(leddrv.c)、具体的硬件操作(board_X.c):
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 042.png
2. 以面向对象的思想,改进代码:
抽象出一个结构体:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 043.png
每个单板相关的board_X.c实现自己的led_operations结构体,供上层的leddrv.c调用:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 044.png

写代码

使用GIT下载所有源码后,本节源码位于如下目录:
	01_all_series_quickstart\04_快速入门(正式开始)\
		02_嵌入式Linux驱动开发基础知识\source\02_led_drv\01_led_drv_template

驱动程序

驱动程序分为上下两层:leddrv.c、board_demo.c。
leddrv.c负责注册file_operations结构体,它的open/write成员会调用board_demo.c中提供的硬件led_opr中的对应函数。

把LED的操作抽象出一个led_operations结构体

首先看看led_opr.h,它定义了一个led_operations结构体,把LED的操作抽象为这个结构体:
	#ifndef _LED_OPR_H
	#define _LED_OPR_H

	struct led_operations {
	     int (*init) (int which); /* 初始化LED, which-哪个LED */
	     int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
	};

	struct led_operations *get_board_led_opr(void);


	#endif


驱动程序的上层:file_operations结构体

上层是leddrv.c,它的核心是file_operations结构体,首先看看入口函数:
	80 /* 4. 把file_operations结构体告诉内核:注册驱动程序 */
	81 /* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
	82 static int __init led_init(void)
	83 {
	84      int err;
	85      int i;
	86
	87      printk(“%s %s line %d\n”, __FILE__, __FUNCTION__, __LINE__);
	88      major = register_chrdev(0, “100ask_led”, &led_drv);  /* /dev/led */
	89
	90
	91      led_class = class_create(THIS_MODULE, “100ask_led_class”);
	92      err = PTR_ERR(led_class);
	93      if (IS_ERR(led_class)) {
	94              printk(“%s %s line %d\n”, __FILE__, __FUNCTION__, __LINE__);
	95              unregister_chrdev(major, “led”);
	96              return -1;
	97      }
	98
	99      for (i = 0; i < LED_NUM; i++)
	100             device_create(led_class, NULL, MKDEV(major, i), NULL, “100ask_led%d”, i); /* /dev/100ask_led0,1,… */
	101
	102     p_led_opr = get_board_led_opr();
	103
	104     return 0;
	105 }
第88行向内核注册一个file_operations结构体。
第102行从底层硬件相关的代码board_demo.c中获得led_operaions结构体。
再来看看leddrv.c中file_operations结构体的成员函数:
	37 /* write(fd, &val, 1); */
	38 static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
	39 {
	40      int err;
	41      char status;
	42      struct inode *inode = file_inode(file);
	43      int minor = iminor(inode);
	44
	45      printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	46      err = copy_from_user(&status, buf, 1);
	47
	48      /* 根据次设备号和status控制LED */
	49      p_led_opr->ctl(minor, status);
	50
	51      return 1;
	52 }
	53
	54 static int led_drv_open (struct inode *node, struct file *file)
	55 {
	56      int minor = iminor(node);
	57
	58      printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	59      /* 根据次设备号初始化LED */
	60      p_led_opr->init(minor);
	61
	62      return 0;
	63 }
	64
	65 static int led_drv_close (struct inode *node, struct file *file)
	66 {
	67      printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	68      return 0;
	69 }
	70
	71 /* 2. 定义自己的file_operations结构体 */ 
	72 static struct file_operations led_drv = {
	73      .owner   = THIS_MODULE,
	74      .open    = led_drv_open,
	75      .read    = led_drv_read,
	76      .write   = led_drv_write,
	77      .release = led_drv_close,
	78 };
第49行、第60行,会调用led_operations结构体中对应的函数。

测试程序

测试程序为ledtest.c:
	#include <sys/types.h>
	#include <sys/stat.h>
	#include <fcntl.h>
	#include <unistd.h>
	#include <stdio.h>
	#include <string.h>

	/*
	 * ./ledtest /dev/100ask_led0 on
	 * ./ledtest /dev/100ask_led0 off
	 */
	int main(int argc, char **argv)
	{
	     int fd;
	     char status;

	     /* 1. 判断参数 */
	     if (argc != 3)
	     {
	             printf("Usage: %s <dev> <on | off>\n", argv[0]);
	             return -1;
	     }

	     /* 2. 打开文件 */
	     fd = open(argv[1], O_RDWR);
	     if (fd == -1)
	     {
	             printf("can not open file %s\n", argv[1]);
	             return -1;
	     }

	     /* 3. 写文件 */
	     if (0 == strcmp(argv[2], "on"))
	     {
	             status = 1;
	             write(fd, &status, 1);
	     }
	     else
	     {
	             status = 0;
	             write(fd, &status, 1);
	     }

	     close(fd);

	     return 0;
	}
第26行打开设备节点。
如果用户想点亮LED,第37行会把值“1”通过write函数写入驱动程序。
如果用户想熄灭LED,第42行会把值“0”通过write函数写入驱动程序。

上机测试

这只是一个示例程序,还没有真正操作硬件。测试程序操作驱动程序时,只会导致驱动程序中打印信息。
首先设置交叉工具链,修改驱动Makefile中内核的源码路径,编译驱动和测试程序。
启动开发板后,通过NFS访问编译好驱动程序、测试程序,就可以在开发板上如下操作了:
	# insmod 100ask_led.ko   // 装载驱动程序
	[13449.134044] /home/book/source/02_led_drv/01_led_drv_template/leddrv.c led_init line 87
	# ls /dev/100ask_led* -l   // 可以得到2个设备节点
	crw-------    1 root     root      235,   0 Jan 18 12:34 /dev/100ask_led0
	crw-------    1 root     root      235,   1 Jan 18 12:34 /dev/100ask_led1
	# ./ledtest /dev/100ask_led0 on    // 点亮LED
	[13463.176987] /home/book/source/02_led_drv/01_led_drv_template/leddrv.c led_drv_open line 58
	[13463.197877] /home/book/source/02_led_drv/01_led_drv_template/board_demo.c board_demo_led_init line 22, led 0
	[13463.216232] /home/book/source/02_led_drv/01_led_drv_template/leddrv.c led_drv_write line 45
	[13463.232889] /home/book/source/02_led_drv/01_led_drv_template/board_demo.c board_demo_led_ctl line 28, led 0, on   // 可以看到这句“on”打印
	[13463.247977] /home/book/source/02_led_drv/01_led_drv_template/leddrv.c led_drv_close line 67
	# ./ledtest /dev/100ask_led0 off     // 熄灭LED
	[13464.540637] /home/book/source/02_led_drv/01_led_drv_template/leddrv.c led_drv_open line 58
	[13464.554380] /home/book/source/02_led_drv/01_led_drv_template/board_demo.c board_demo_led_init line 22, led 0
	[13464.569671] /home/book/source/02_led_drv/01_led_drv_template/leddrv.c led_drv_write line 45
	[13464.580615] /home/book/source/02_led_drv/01_led_drv_template/board_demo.c board_demo_led_ctl line 28, led 0, off    // 可以看到这句“off”打印
	[13464.593397] /home/book/source/02_led_drv/01_led_drv_template/leddrv.c led_drv_close line 67

课后作业

实现读LED状态的功能:涉及APP和驱动。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 045.png

具体单板的LED驱动程序

我们选用的内核都是4.x版本,操作都是类似的:
	rk3399   linux 4.4.154
	rk3288   linux 4.4.154
	imx6ul   linux 4.9.88
	am3358  linux 4.9.168
录制视频时,我的source insight里总是使用某个版本的内核。这没有关系,驱动程序中调用的内核函数,在这些4.x版本的内核里都是一样的。

怎么写LED驱动程序?

详细步骤如下:
① 看原理图确定引脚,确定引脚输出什么电平才能点亮/熄灭LED
② 看主芯片手册,确定寄存器操作方法:哪些寄存器?哪些位?地址是?
③ 编写驱动:先写框架,再写硬件操作的代码
注意:在芯片手册中确定的寄存器地址被称为物理地址,在Linux内核中无法直接使用。
需要使用内核提供的ioremap把物理地址映射为虚拟地址,使用虚拟地址
ioremap函数的使用:
① 函数原型:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 046.png
使用时,要包含头文件:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 047.png
② 它的作用:
把物理地址phys_addr开始的一段空间(大小为size),映射为虚拟地址;返回值是该段虚拟地址的首地址。
	virt_addr  = ioremap(phys_addr, size);
实际上,它是按页(4096字节)进行映射的,是整页整页地映射的。
假设phys_addr = 0x10002,size=4,ioremap的内部实现是:
a. phys_addr按页取整,得到地址0x10000
b. size按页取整,得到4096
c. 把起始地址0x10000,大小为4096的这一块物理地址空间,映射到虚拟地址空间,
假设得到的虚拟空间起始地址为0xf0010000
d. 那么phys_addr = 0x10002对应的virt_addr = 0xf0010002
③ 不再使用该段虚拟地址时,要iounmap(virt_addr):
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 048.png
volatile的使用:
① 编译器很聪明,会帮我们做些优化,比如:
	int   a;
	a = 0;   // 这句话可以优化掉,不影响a的结果
	a = 1;
② 有时候编译器会自作聪明,比如:
	int *p = ioremap(xxxx, 4);  // GPIO寄存器的地址
	*p = 0;   // 点灯,但是这句话被优化掉了
	*p = 1;   // 灭灯
③ 对于上面的情况,为了避免编译器自动优化,需要加上volatile,告诉它“这是容易出错的,别乱优化”:
	volatile  int *p = ioremap(xxxx, 4);  // GPIO寄存器的地址
	*p = 0;   // 点灯,这句话不会被优化掉
	*p = 1;   // 灭灯

AM335X的LED驱动程序

原理图

100ask_AM335X开发板结构为:底板+核心板,其中一个LED原理图如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 049.png
它使用GPIO1_16这个引脚,当它输出低电平时,LED被点亮;当它输出高电平时,LED被熄灭。

所涉及的寄存器操作

a. 使能GPIO1
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 050.png
	/* set PRCM to enalbe GPIO1 
	 * set CM_PER_GPIO1_CLKCTRL (0x44E00000 + 0xAC)
	 * val: (1<<18) | 0x2
	 */
b. 设置GPIO1_16的功能,让它工作于GPIO模式
根据原理图可以找到GPIO1_16这个引脚接到AM3358的R13引脚,根据下图知道pin name为GPMC_A0,并且知道要设置这个引脚为Mode 7。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 051.png
在芯片手册中搜“conf_gpmc_a0”,可得:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 052.png
	/* set Control Module to set GPIO1_16 (R13) used as GPIO 
	 * conf_gpmc_a0 as mode 7
	 * addr : 0x44E10000 + 0x840
	 * val  : 7
	 */
c. 设置GPIO1_16的方向,让它作为输出引脚
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 053.png
	/* set GPIO1's registers , to set GPIO1_16'S dir (output) 
	 * GPIO_OE 
	 * addr : 0x4804C000 + 0x134
	 * clear bit 16
	 */
d. 设置GPIO1_16的数据,让它输出高电平
AM335X芯片支持set-and-clear protocol,设置GPIO_SETDATAOUT的bit 16为1即可让引脚输出1:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 054.png
	/* set GPIO1_16's registers , to output 1 
	 * GPIO_SETDATAOUT
	 * addr : 0x4804C000 + 0x194
	 */
e. 清除GPIO1_16的数据,让它输出低电平
AM335X芯片支持set-and-clear protocol,设置GPIO_CLEARDATAOUT的bit 16为1即可让引脚输出0:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 055.png
	/* set GPIO1_16's registers , to output 0 
	 * GPIO_CLEARDATAOUT
	 * addr : 0x4804C000 + 0x190
	 */

写程序

使用GIT下载所有源码后,本节源码位于如下目录:
	01_all_series_quickstart\04_快速入门(正式开始)\
		02_嵌入式Linux驱动开发基础知识\source\02_led_drv\
	  	02_led_drv_for_boards\am335x_src_bin


硬件相关的文件是board_am335x.c,其他文件跟LED框架驱动程序完全一样。
它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
	100 static struct led_operations board_demo_led_opr = {
	101     .num  = 1,
	102     .init = board_demo_led_init,
	103     .ctl  = board_demo_led_ctl,
	104 };
	105
led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
值得关注的是第33~37行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器。
	19 #include "led_opr.h"
	20
	21 static volatile unsigned int *CM_PER_GPIO1_CLKCTRL;
	22 static volatile unsigned int *conf_gpmc_a0;
	23 static volatile unsigned int *GPIO1_OE;
	24 static volatile unsigned int *GPIO1_CLEARDATAOUT;
	25 static volatile unsigned int *GPIO1_SETDATAOUT;
	26
	27 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
	28 {
	29     if (which == 0)
	30     {
	31         if (!CM_PER_GPIO1_CLKCTRL)
	32         {
	33             CM_PER_GPIO1_CLKCTRL = ioremap(0x44E00000 + 0xAC, 4);
	34             conf_gpmc_a0 = ioremap(0x44E10000 + 0x840, 4);
	35             GPIO1_OE = ioremap(0x4804C000 + 0x134, 4);
	36             GPIO1_CLEARDATAOUT = ioremap(0x4804C000 + 0x190, 4);
	37             GPIO1_SETDATAOUT = ioremap(0x4804C000 + 0x194, 4);
	38         }
	39
	40         //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
	41         /* a. 使能GPIO1
	42          * set PRCM to enalbe GPIO1
	43          * set CM_PER_GPIO1_CLKCTRL (0x44E00000 + 0xAC)
	44          * val: (1<<18) | 0x2
	45          */
	46         *CM_PER_GPIO1_CLKCTRL = (1<<18) | 0x2;
	47
	48         /* b. 设置GPIO1_16的功能,让它工作于GPIO模式
	49          * set Control Module to set GPIO1_16 (R13) used as GPIO
	50          * conf_gpmc_ad0 as mode 7
	51          * addr : 0x44E10000 + 0x800
	52          * val  : 7
	53          */
	54         *conf_gpmc_a0 = 7;
	55
	56         /* c. 设置GPIO1_16的方向,让它作为输出引脚
	57          * set GPIO1's registers , to set GPIO1_16'S dir (output)
	58          * GPIO_OE
	59          * addr : 0x4804C000 + 0x134
	60          * clear bit 16
	61          */
	62
	63         *GPIO1_OE &= ~(1<<16);
	64     }
	65
	66     return 0;
	67 }
	68
led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
	69 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
	70 {
	71     //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
	72
	73     if (which == 0)
	74     {
	75         if (status) /* on: output 0 */
	76         {
	77             /* e. 清除GPIO1_16的数据,让它输出低电平
	78              * AM335X芯片支持set-and-clear protocol,设置GPIO_CLEARDATAOUT的bit 16为1即可让引脚输出0:
	79              * set GPIO1_16's registers , to output 0
	80              * GPIO_CLEARDATAOUT
	81              * addr : 0x4804C000 + 0x190
	82              */
	83             *GPIO1_CLEARDATAOUT = (1<<16);
	84         }
	85         else
	86         {
	87             /* d. 设置GPIO1_16的数据,让它输出高电平
	88              * AM335X芯片支持set-and-clear protocol,设置GPIO_SETDATAOUT的bit 16为1即可让引脚输出1:
	89              * set GPIO1_16's registers , to output 1
	90              * GPIO_SETDATAOUT
	91              * addr : 0x4804C000 + 0x194
	92              */
	93             *GPIO1_SETDATAOUT = (1<<16);
	94         }
	95     }
	96
	97     return 0;
	98 }
	99
下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
	106 struct led_operations *get_board_led_opr(void)
	107 {
	108     return &board_demo_led_opr;
	109 }
	110

配置内核去掉原有LED驱动

不需要重新配置内核,只需要在开发板上执行以下3条命令关闭内核对LED的使用即可:
	# echo none > /sys/class/leds/am335x:green:cpu0/trigger
	# echo none > /sys/class/leds/am335x:green:mmc0/trigger
	# echo none > /sys/class/leds/am335x:green:nand/trigger
然后就可以去安装驱动程序,执行测试程序了,操作过程跟LED框架驱动程序的测试是一样的。

课后作业

a. 在board_am335x.c里有ioremap,什么时候执行iounmap?请完善程序
b. 视频里我们只实现了点一个LED,请修改代码实现操作4个LED

RK3288和RK3399的LED驱动程序

原理图

fireflye RK3288的LED原理图
RK3288开发板上有2个LED,原理图如下,其中的WORK_LED使用引脚GPIO8_A1:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 056.png
这些LED引脚输出低电平时,LED被点亮;输出高电平时,LED被熄灭。
firefly RK3399的LED原理图
RK3399开发板上有3个LED,原理图如下,其中的WORK_LED使用引脚GPIO2_D3:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 057.png
这些LED引脚输出低电平时,LED被点亮;输出高电平时,LED被熄灭。


所涉及的寄存器操作

截图便于对比,后面有文字便于复制:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 058.png
RK3288的GPIO8_A1引脚
a. 使能GPIO8
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 059.png
设置CRU_CLKGATE14_CON的b[8]为0使能GPIO8,要修改b[8]的前提是把b[24]设置为1。
	/* rk3288 GPIO8_A1 */
	/* a. 使能GPIO8
	 * set CRU to enable GPIO8
	 * CRU_CLKGATE14_CON 0xFF760000 + 0x198
	 * (1<<(8+16)) | (0<<8)
	 */
b. 设置GPIO8_A1用于GPIO
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 060.png
设置GRF_GPIO8A_IOMUX的b[3:2]为0b00把GPIO8_A1用作GPIO,要修改b[3:2]的前提是把b[19:18]设置为0b11。
	/* b. 设置GPIO8_A1用于GPIO
	 * set PMU/GRF to configure GPIO8_A1 as GPIO
	 * GRF_GPIO8A_IOMUX 0xFF770000 + 0x0080
	 * bit[3:2] = 0b00
	 * (3<<(2+16)) | (0<<2)
	 */
c. 设置GPIO8_A1作为output引脚
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 061.png
设置GPIO_SWPORTA_DDR 寄存器b[1]为1,把GPIO8_A1设置为输出引脚。
注意:
GPIO_A0~A7 对应bit0~bit7;GPIO_B0~B7 对应bit8~bit15;
GPIO_C0~C7 对应bit16~bit23;GPIO_D0~D7 对应bit24~bit31
/* c. 设置GPIO8_A1作为output引脚
 * set GPIO_SWPORTA_DDR to configure GPIO8_A1 as output
 * GPIO_SWPORTA_DDR 0xFF7F0000 + 0x0004
 * bit[1] = 0b1
 */
d. 设置GPIO8_A1输出高电平
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 062.png
设置GPIO_SWPORTA_DR 寄存器b[1]为1,让GPIO8_A1输出高电平。
注意:
GPIO_A0~A7 对应bit0~bit7;GPIO_B0~B7 对应bit8~bit15;
GPIO_C0~C7 对应bit16~bit23;GPIO_D0~D7 对应bit24~bit31
	/* d. 设置GPIO8_A1输出高电平
	 * set GPIO_SWPORTA_DR to configure GPIO8_A1 output 1
	 * GPIO_SWPORTA_DR 0xFF7F0000 + 0x0000
	 * bit[1] = 0b1
	 */
e. 设置GPIO8_A1输出低电平
同样是设置GPIO_SWPORTA_DR 寄存器,把b[1]设为0,让GPIO8_A1输出低电平。
	/* e. 设置GPIO8_A1输出低电平
	 * set GPIO_SWPORTA_DR to configure GPIO8_A1 output 0
	 * GPIO_SWPORTA_DR 0xFF7F0000 + 0x0000
	 * bit[1] = 0b0
	 */
RK3399的GPIO2_D3引脚
a. 使能GPIO2
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 063.png
设置CRU_CLKGATE_CON31的b[3]为0使能GPIO2,要修改b[3]的前提是把b[19]设置为1。
	/* rk3399 GPIO2_D3 */
	/* a. 使能GPIO2
	 * set CRU to enable GPIO2
	 * CRU_CLKGATE_CON31 0xFF760000 + 0x037c
	 * (1<<(3+16)) | (0<<3)
	 */
b. 设置GPIO2_D3用于GPIO
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 064.png
设置GRF_GPIO2D_IOMUX的b[7:6]为0b00把GPIO2_D3用作GPIO,要修改b[7:6]的前提是把b[23:22]设置为0b11。
	/* b. 设置GPIO2_D3用于GPIO
	 * set PMU/GRF to configure GPIO2_D3 as GPIO
	 * GRF_GPIO2D_IOMUX 0xFF770000 + 0x0e00c
	 * bit[7:6] = 0b00
	 * (3<<(6+16)) | (0<<6)
	 */
c. 设置GPIO2_D3作为output引脚
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 065.png
设置GPIO_SWPORTA_DDR 寄存器b[27]为1,把GPIO2_D3设置为输出引脚。
注意:
GPIO_A0~A7 对应bit0~bit7;GPIO_B0~B7 对应bit8~bit15;
GPIO_C0~C7 对应bit16~bit23;GPIO_D0~D7 对应bit24~bit31
	/* c. 设置GPIO2_D3作为output引脚
	 * set GPIO_SWPORTA_DDR to configure GPIO2_D3 as output
	 * GPIO_SWPORTA_DDR 0xFF780000 + 0x0004
	 * bit[27] = 0b1
	 */
d. 设置GPIO2_D3输出高电平
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 066.png
设置GPIO_SWPORTA_DR 寄存器b[27]为1,让GPIO2_D3输出高电平。
注意:
GPIO_A0~A7 对应bit0~bit7;GPIO_B0~B7 对应bit8~bit15;
GPIO_C0~C7 对应bit16~bit23;GPIO_D0~D7 对应bit24~bit31
	/* d. 设置GPIO2_D3输出高电平
	 * set GPIO_SWPORTA_DR to configure GPIO2_D3 output 1
	 * GPIO_SWPORTA_DR 0xFF780000 + 0x0000
	 * bit[27] = 0b1
	 */
e. 设置GPIO2_D3输出低电平
同样是设置GPIO_SWPORTA_DR 寄存器,把b[27]设为0,让GPIO2_D3输出低电平。
	/* e. 设置GPIO2_D3输出低电平
	 * set GPIO_SWPORTA_DR to configure GPIO2_D3 output 0
	 * GPIO_SWPORTA_DR 0xFF780000 + 0x0000
	 * bit[27] = 0b0
	 */

写程序

RK3288
使用GIT下载所有源码后,本节源码位于如下目录:
	01_all_series_quickstart\04_快速入门(正式开始)\
		02_嵌入式Linux驱动开发基础知识\source\02_led_drv\
	  	02_led_drv_for_boards\rk3288_src_bin
硬件相关的文件是board_rk3288.c,其他文件跟LED框架驱动程序完全一样。
它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
	91 static struct led_operations board_demo_led_opr = {
	92      .num  = 1,
	93      .init = board_demo_led_init,
	94      .ctl  = board_demo_led_ctl,
	95 };
	96
led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
值得关注的是第32~35行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器:
	20 static volatile unsigned int *CRU_CLKGATE14_CON;
	21 static volatile unsigned int *GRF_GPIO8A_IOMUX ;
	22 static volatile unsigned int *GPIO8_SWPORTA_DDR;
	23 static volatile unsigned int *GPIO8_SWPORTA_DR ;
	24
	25 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */    
	26 {
	27      //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
	28      if (which == 0)
	29      {
	30              if (!CRU_CLKGATE14_CON)
	31              {
	32                      CRU_CLKGATE14_CON = ioremap(0xFF760000 + 0x0198, 4);
	33                      GRF_GPIO8A_IOMUX  = ioremap(0xFF770000 + 0x0080, 4);
	34                      GPIO8_SWPORTA_DDR = ioremap(0xFF7F0000 + 0x0004, 4);
	35                      GPIO8_SWPORTA_DR  = ioremap(0xFF7F0000 + 0x0000, 4);
	36              }
	37
	38              /* rk3288 GPIO8_A1 */
	39              /* a. 使能GPIO8
	40               * set CRU to enable GPIO8
	41               * CRU_CLKGATE14_CON 0xFF760000 + 0x198
	42               * (1<<(8+16)) | (0<<8)
	43               */
	44              *CRU_CLKGATE14_CON = (1<<(8+16)) | (0<<8);
	45
	46              /* b. 设置GPIO8_A1用于GPIO
	47               * set PMU/GRF to configure GPIO8_A1 as GPIO
	48               * GRF_GPIO8A_IOMUX 0xFF770000 + 0x0080
	49               * bit[3:2] = 0b00
	50               * (3<<(2+16)) | (0<<2)
	51               */
	52              *GRF_GPIO8A_IOMUX =(3<<(2+16)) | (0<<2);
	53
	54              /* c. 设置GPIO8_A1作为output引脚
	55               * set GPIO_SWPORTA_DDR to configure GPIO8_A1 as output
	56               * GPIO_SWPORTA_DDR 0xFF7F0000 + 0x0004
	57               * bit[1] = 0b1
	58               */
	59              *GPIO8_SWPORTA_DDR |= (1<<1);
	60      }
	61              return 0;
	62 }
	63
led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
	64 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮, 0-灭*/
	65 {
	66      //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
	67      if (which == 0)
	68      {
	69              if (status) /* on: output 0 */
	70              {
	71                      /* e. 设置GPIO8_A1输出低电平
	72                       * set GPIO_SWPORTA_DR to configure GPIO8_A1 output 0
	73                       * GPIO_SWPORTA_DR 0xFF7F0000 + 0x0000
	74                       * bit[1] = 0b0
	75                       */
	76                      *GPIO8_SWPORTA_DR &= ~(1<<1);
	77              }
	78              else /* off: output 1 */
	79              {
	80                      /* d. 设置GPIO8_A1输出高电平
	81                       * set GPIO_SWPORTA_DR to configure GPIO8_A1 output 1
	82                       * GPIO_SWPORTA_DR 0xFF7F0000 + 0x0000
	83                       * bit[1] = 0b1
	84                       */
	85                      *GPIO8_SWPORTA_DR |= (1<<1);
	86              }
	87      }
	88      return 0;
	89 }
	90
下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
	97 struct led_operations *get_board_led_opr(void)
	98 {
	99      return &board_demo_led_opr;
	100 }
	101
RK3399
使用GIT下载所有源码后,本节源码位于如下目录:
	01_all_series_quickstart\04_快速入门(正式开始)\
		02_嵌入式Linux驱动开发基础知识\source\02_led_drv\
	  	02_led_drv_for_boards\rk3399_src_bin
硬件相关的文件是board_rk3399.c,其他文件跟LED框架驱动程序完全一样。
它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
	91 static struct led_operations board_demo_led_opr = {
	92     .num  = 1,
	93     .init = board_demo_led_init,
	94     .ctl  = board_demo_led_ctl,
	95 };
	96
led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
值得关注的是第32~35行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器:
	20 static volatile unsigned int *CRU_CLKGATE_CON31;
	21 static volatile unsigned int *GRF_GPIO2D_IOMUX ;
	22 static volatile unsigned int *GPIO2_SWPORTA_DDR;
	23 static volatile unsigned int *GPIO2_SWPORTA_DR ;
	24
	25 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */    
	26 {
	27     //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
	28     if (which == 0)
	29     {
	30         if (!CRU_CLKGATE_CON31)
	31         {
	32             CRU_CLKGATE_CON31 = ioremap(0xFF760000 + 0x037c, 4);
	33             GRF_GPIO2D_IOMUX  = ioremap(0xFF770000 + 0x0e00c, 4);
	34             GPIO2_SWPORTA_DDR = ioremap(0xFF780000 + 0x0004, 4);
	35             GPIO2_SWPORTA_DR  = ioremap(0xFF780000 + 0x0000, 4);
	36         }
	37
	38         /* rk3399 GPIO2_D3 */
	39         /* a. 使能GPIO2
	40          * set CRU to enable GPIO2
	41          * CRU_CLKGATE_CON31 0xFF760000 + 0x037c
	42          * (1<<(3+16)) | (0<<3)
	43          */
	44         *CRU_CLKGATE_CON31 = (1<<(3+16)) | (0<<3);
	45
	46         /* b. 设置GPIO2_D3用于GPIO
	47          * set PMU/GRF to configure GPIO2_D3 as GPIO
	48          * GRF_GPIO2D_IOMUX 0xFF770000 + 0x0e00c
	49          * bit[7:6] = 0b00
	50          * (3<<(6+16)) | (0<<6)
	51          */
	52         *GRF_GPIO2D_IOMUX = (3<<(6+16)) | (0<<6);
	53
	54         /* c. 设置GPIO2_D3作为output引脚
	55          * set GPIO_SWPORTA_DDR to configure GPIO2_D3 as output
	56          * GPIO_SWPORTA_DDR 0xFF780000 + 0x0004
	57          * bit[27] = 0b1
	58          */
	59         *GPIO2_SWPORTA_DDR |= (1<<27);
	60     }
	61     return 0;
	62 }
	63
led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
	64 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮, 0-灭*/
	65 {
	66     //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
	67     if (which == 0)
	68     {
	69         if (status) /* on: output 1 */
	70         {
	71             /* d. 设置GPIO2_D3输出高电平
	72              * set GPIO_SWPORTA_DR to configure GPIO2_D3 output 1
	73              * GPIO_SWPORTA_DR 0xFF780000 + 0x0000
	74              * bit[27] = 0b1
	75              */
	76             *GPIO2_SWPORTA_DR |= (1<<27);
	77         }
	78         else /* off : output 0 */
	79         {
	80             /* e. 设置GPIO2_D3输出低电平
	81              * set GPIO_SWPORTA_DR to configure GPIO2_D3 output 0
	82              * GPIO_SWPORTA_DR 0xFF780000 + 0x0000
	83              * bit[27] = 0b0
	84              */
	85             *GPIO2_SWPORTA_DR &= ~(1<<27);
	86         }
	87     }
	88     return 0;
	89 }
	90
下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
	97 struct led_operations *get_board_led_opr(void)
	98 {
	99     return &board_demo_led_opr;
	100 }
	101

上机实验

首先设置工具链,然后修改驱动程序Makefile指定内核源码路径,就可以编译驱动程序和测试程序了。
启动开发板,挂载NFS文件系统,这样就可以访问到Ubuntu中的文件。
最后,就可以在开发板上进行下列测试。
RK3288
	# insmod  100ask_led.ko
	# ./ledtest  /dev/100ask_led0  on
	# ./ledtest  /dev/100ask_led0  off
RK3399
要先禁止内核中原来的LED驱动,把“heatbeat”功能关闭,执行以下命令即可:
	# echo none > /sys/class/leds/firefly\:yellow\:heartbeat/trigger
	# echo none > /sys/class/leds/firefly\:yellow\:user/trigger
	# echo none > /sys/class/leds/firefly\:red\:power/trigger
这样就可以使用我们的驱动程序做实验了:
	# insmod  100ask_led.ko
	# ./ledtest  /dev/100ask_led0  on
	# ./ledtest  /dev/100ask_led0  off
如果想恢复原来的心跳功能,可以执行:
	# echo heartbeat > /sys/class/leds/firefly\:yellow\:heartbeat/trigger
	# echo heartbeat > /sys/class/leds/firefly\:yellow\:user/trigger
	# echo heartbeat > /sys/class/leds/firefly\:red\:power/trigger

课后作业

a. 在驱动里有ioremap,什么时候执行iounmap?请完善程序
b. 视频里我们只实现了点一个LED,请修改代码实现操作所有LED

野火/正点原子IMX6ULL的LED驱动程序

野火、正点原子用的内核版本是4.1.15
我们用的内核版本是 linux 4.9.88
都是4.x版本,在学习上没有任何差别
你拿到板子后,可以使用他们出厂的系统,
也可以根据我们提供的高级用户手册更改为我们的系统。

原理图

野火fire_imx6ull-pro开发板
LED原理图如下,它使用GPIO5_IO03,引脚输出低电平时LED被点亮,输出高电平时LED被熄灭:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 067.png
正点原子Atk_imx6ull-alpha开发板
LED原理图如下,它使用GPIO1_IO03,引脚输出低电平时LED被点亮,输出高电平时LED被熄灭:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 068.png

所涉及的寄存器操作

GPIO模块图如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 023.png
代码中对硬件的操作截图如下,截图便于对比,后面有文字便于复制:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 069.png
野火fire_imx6ull-pro 开发板
步骤1:使能GPIO5
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 070.png
设置b[31:30]就可以使能GPIO5,设置为什么值呢?
看下图,设置为0b11:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 024.png
① 00:该GPIO模块全程被关闭
② 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭
③ 10:保留
④ 11:该GPIO模块全程使能
	/* GPIO5_IO03 */
	/* a. 使能GPIO5
	 * set CCM to enable GPIO5
	 * CCM_CCGR1[CG15] 0x20C406C
	 * bit[31:30] = 0b11
	 */
步骤2:设置GPIO5_IO03为GPIO模式
设置如下寄存器:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 071.png
	/* b. 设置GPIO5_IO03用于GPIO
	 * set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3
	 *		to configure GPIO5_IO03 as GPIO
	 * IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3	0x2290014
	 * bit[3:0] = 0b0101 alt5
	 */
步骤3:设置GPIO5_IO03为输出引脚,设置其输出电平
寄存器地址为:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 072.png
设置方向寄存器,把引脚设置为输出引脚:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 073.png
设置数据寄存器,设置引脚的输出电平:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 074.png
	/* c. 设置GPIO5_IO03作为output引脚
	 * set GPIO5_GDIR to configure GPIO5_IO03 as output
	 * GPIO5_GDIR  0x020AC000 + 0x4
	 * bit[3] = 0b1
	 */

	/* d. 设置GPIO5_DR输出低电平
	 * set GPIO5_DR to configure GPIO5_IO03 output 0
	 * GPIO5_DR 0x020AC000 + 0
	 * bit[3] = 0b0
	 */
	
	/* e. 设置GPIO5_IO3输出高电平
	 * set GPIO5_DR to configure GPIO5_IO03 output 1
	 * GPIO5_DR 0x020AC000 + 0
	 * bit[3] = 0b1
	 */
正点原子Atk_imx6ull-alpha开发板
步骤1:使能GPIO1
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 075.png
设置b[27:26]就可以使能GPIO1,设置为什么值呢?
看下图,设置为0b11:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 024.png
① 00:该GPIO模块全程被关闭
② 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭
③ 10:保留
④ 11:该GPIO模块全程使能
	/* GPIO1_IO03 */
	/* a. 使能GPIO1
	 * set CCM to enable GPIO1
	 * CCM_CCGR1[CG13] 0x20C406C
	 * bit[27:26] = 0b11
	 */
步骤2:设置GPIO1_IO03为GPIO模式
设置如下寄存器:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 076.png
	/* b. 设置GPIO1_IO03用于GPIO
	 * set IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
	 *		to configure GPIO1_IO03 as GPIO
	 * IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03  0x20E0068
	 * bit[3:0] = 0b0101 alt5
	 */
步骤3:设置GPIO1_IO03为输出引脚,设置其输出电平
寄存器地址为:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 072.png
设置方向寄存器,把引脚设置为输出引脚:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 073.png
设置数据寄存器,设置引脚的输出电平:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 074.png
	/* c. 设置GPIO1_IO03作为output引脚
	 * set GPIO1_GDIR to configure GPIO1_IO03 as output
	 * GPIO1_GDIR  0x0209C000 + 0x4
	 * bit[3] = 0b1
	 */
	
	/* d. 设置GPIO1_DR输出低电平
	 * set GPIO1_DR to configure GPIO1_IO03 output 0
	 * GPIO1_DR 0x0209C000 + 0
	 * bit[3] = 0b0
	 */
	
	/* e. 设置GPIO1_IO03输出高电平
	 * set GPIO1_DR to configure GPIO1_IO03 output 1
	 * GPIO1_DR 0x0209C000 + 0
	 * bit[3] = 0b1
	 */

写程序

野火fire_imx6ull-pro开发板
使用GIT下载所有源码后,本节源码位于如下目录:
	01_all_series_quickstart\04_快速入门(正式开始)\
		02_嵌入式Linux驱动开发基础知识\source\02_led_drv\
	  	02_led_drv_for_boards\fire_imx6ull-pro_src_bin
硬件相关的文件是board_fire_imx6ull-pro.c,其他文件跟LED框架驱动程序完全一样。
它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
	100 static struct led_operations board_demo_led_opr = {
	101     .num  = 1,
	102     .init = board_demo_led_init,
	103     .ctl  = board_demo_led_ctl,
	104 };
	105
led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
值得关注的是第35~38行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器:
	21 static volatile unsigned int *CCM_CCGR1                              ;
	22 static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
	23 static volatile unsigned int *GPIO5_GDIR                             ;
	24 static volatile unsigned int *GPIO5_DR                               ;
	25
	26 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */    
	27 {
	28     unsigned int val;
	29
	30     //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
	31     if (which == 0)
	32     {
	33         if (!CCM_CCGR1)
	34         {
	35             CCM_CCGR1  = ioremap(0x20C406C, 4);
	36         IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);
	37             GPIO5_GDIR  = ioremap(0x020AC000 + 0x4, 4);
	38             GPIO5_DR    = ioremap(0x020AC000 + 0, 4);
	39         }
	40
	41         /* GPIO5_IO03 */
	42         /* a. 使能GPIO5
	43          * set CCM to enable GPIO5
	44          * CCM_CCGR1[CG15] 0x20C406C
	45          * bit[31:30] = 0b11
	46          */
	47         *CCM_CCGR1 |= (3<<30);
	48
	49         /* b. 设置GPIO5_IO03用于GPIO
	50          * set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3
	51          *      to configure GPIO5_IO03 as GPIO
	52          * IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3  0x2290014
	53          * bit[3:0] = 0b0101 alt5
	54          */
	55         val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
	56         val &= ~(0xf);
	57         val |= (5);
	58         *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;
	59
	60
	61         /* b. 设置GPIO5_IO03作为output引脚
	62          * set GPIO5_GDIR to configure GPIO5_IO03 as output
	63          * GPIO5_GDIR  0x020AC000 + 0x4
	64          * bit[3] = 0b1
	65          */
	66         *GPIO5_GDIR |= (1<<3);
	67     }
	68
	69     return 0;
	70 }
	71
led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
	72 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
	73 {
	74     //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
	75     if (which == 0)
	76     {
	77         if (status) /* on: output 0*/
	78         {
	79             /* d. 设置GPIO5_DR输出低电平
	80              * set GPIO5_DR to configure GPIO5_IO03 output 0
	81              * GPIO5_DR 0x020AC000 + 0
	82              * bit[3] = 0b0
	83              */
	84             *GPIO5_DR &= ~(1<<3);
	85         }
	86         else  /* off: output 1*/
	87         {
	88             /* e. 设置GPIO5_IO3输出高电平
	89              * set GPIO5_DR to configure GPIO5_IO03 output 1
	90              * GPIO5_DR 0x020AC000 + 0
	91              * bit[3] = 0b1
	92              */
	93             *GPIO5_DR |= (1<<3);
	94         }
	95
	96     }
	97     return 0;
	98 }
	99
下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
	106 struct led_operations *get_board_led_opr(void)
	107 {
	108     return &board_demo_led_opr;
	109 }
	110
正点原子Atk_imx6ull-alpha开发板
使用GIT下载所有源码后,本节源码位于如下目录:
	01_all_series_quickstart\04_快速入门(正式开始)\
		02_嵌入式Linux驱动开发基础知识\source\02_led_drv\
	      02_led_drv_for_boards\atk_imx6ull-alpha_src_bin
硬件相关的文件是board_atk_imx6ull-alpha.c,其他文件跟LED框架驱动程序完全一样。
它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
	100 static struct led_operations board_demo_led_opr = {
	101     .num  = 1,
	102     .init = board_demo_led_init,
	103     .ctl  = board_demo_led_ctl,
	104 };
	105
led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
值得关注的是第35~38行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器:
	26 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
	27 {
	28     unsigned int val;
	29
	30     //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
	31     if (which == 0)
	32     {
	33         if (!CCM_CCGR1)
	34         {
	35             CCM_CCGR1 = ioremap(0x20C406C, 4);
	36             IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = ioremap(0x20E0068, 4);
	37             GPIO1_GDIR = ioremap(0x0209C000 + 0x4, 4);
	38             GPIO1_DR  = ioremap(0x0209C000 + 0, 4);
	39         }
	40
	41         /* GPIO1_IO03 */
	42         /* a. 使能GPIO1
	43          * set CCM to enable GPIO1
	44          * CCM_CCGR1[CG13] 0x20C406C
	45          * bit[27:26] = 0b11
	46          */
	47         *CCM_CCGR1 |= (3<<26);
	48
	49         /* b. 设置GPIO1_IO03用于GPIO
	50          * set IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
	51          *      to configure GPIO1_IO03 as GPIO
	52          * IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03  0x20E0068
	53          * bit[3:0] = 0b0101 alt5
	54          */
	55         val = *IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
	56         val &= ~(0xf);
	57         val |= (5);
	58         *IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = val;
	59
	60
	61         /* c. 设置GPIO1_IO03作为output引脚
	62          * set GPIO1_GDIR to configure GPIO1_IO03 as output
	63          * GPIO1_GDIR  0x0209C000 + 0x4
	64          * bit[3] = 0b1
	65          */
	66         *GPIO1_GDIR |= (1<<3);
	67     }
	68
	69     return 0;
	70 }
	71
led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
	72 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
	73 {
	74     //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
	75     if (which == 0)
	76     {
	77         if (status) /* on: output 0*/
	78         {
	79             /* d. 设置GPIO1_DR输出低电平
	80              * set GPIO1_DR to configure GPIO1_IO03 output 0
	81              * GPIO1_DR 0x0209C000 + 0
	82              * bit[3] = 0b0
	83              */
	84             *GPIO1_DR &= ~(1<<3);
	85         }
	86         else  /* off: output 1*/
	87         {
	88             /* e. 设置GPIO1_IO03输出高电平
	89              * set GPIO1_DR to configure GPIO1_IO03 output 1
	90              * GPIO1_DR 0x0209C000 + 0
	91              * bit[3] = 0b1
	92              */
	93             *GPIO1_DR |= (1<<3);
	94         }
	95
	96     }
	97     return 0;
	98 }
	99
下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
	06 struct led_operations *get_board_led_opr(void)
	07 {
	08     return &board_demo_led_opr;
	09 }
	10

上机实验

首先设置工具链,然后修改驱动程序Makefile指定内核源码路径,就可以编译驱动程序和测试程序了。
启动开发板,挂载NFS文件系统,这样就可以访问到Ubuntu中的文件。
最后,就可以在开发板上进行下列测试。
野火fire_imx6ull-pro 开发板
注意:如果要使用板子自带的系统,关闭原有LED驱动的方法是类似的,也是进入开发板/sys/class/leds/目录,对于每一个LED在该目录下都有一个子目录,假设某个子目录名为XXX,则执行如下命令:
	# echo none  >  /sys/class/leds/XXX/trigger
使用我们的系统时,按如下操作。
要先禁止内核中原来的LED驱动,把“heatbeat”功能关闭,执行以下命令即可:
	# echo none > /sys/class/leds/cpu/trigger
这样就可以使用我们的驱动程序做实验了:
	# insmod  100ask_led.ko
	#./ledtest  /dev/100ask_led0  on
	#./ledtest  /dev/100ask_led0  off
如果想恢复原来的心跳功能,可以执行:
	# echo heartbeat > /sys/class/leds/cpu/trigger
正点原子Atk_imx6ull-alpha开发板
注意:如果要使用板子自带的系统,关闭原有LED驱动的方法是类似的,也是进入开发板/sys/class/leds/目录,对于每一个LED在该目录下都有一个子目录,假设某个子目录名为XXX,则执行如下命令:
	# echo none  >  /sys/class/leds/XXX/trigger
使用我们的系统时,按如下操作。
要先禁止内核中原来的LED驱动,把“heatbeat”功能关闭,执行以下命令即可:
	# echo none > /sys/class/leds/sys-led/trigger
这样就可以使用我们的驱动程序做实验了:
	# insmod  100ask_led.ko
	# ./ledtest  /dev/100ask_led0  on
	# ./ledtest  /dev/100ask_led0  off
如果想恢复原来的心跳功能,可以执行:
	# echo heartbeat > /sys/class/leds/sys-led/trigger

课后作业

a. 在驱动里有ioremap,什么时候执行iounmap?请完善程序
b. 视频里我们只实现了点一个LED,开发板上也只有一个LED,
所以,请修改代码操作蜂鸣器。

百问网IMX6ULL-QEMU的LED驱动程序

使用QEMU模拟的硬件,它的硬件资源可以随意扩展。
在IMX6ULL QEMU 虚拟开发板上,我们为它设计了4个 LED。

看原理图确定引脚及操作方法

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 077.png
从上图可知,这4个 LED 用到了GPIO5_3、GPIO1_3、GPIO1_5、GPIO1_6 共4个引脚。
在芯片手册里,这些引脚的名字是:GPIO5_IO03、GPIO1_IO03、GPIO1_IO05、GPIO1_IO06。可以根据名字搜到对应的寄存器。
当这些引脚输出低电平时,对应的LED被点亮;输出高电平时,LED熄灭。

所涉及的寄存器操作

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 023.png
步骤1:使能GPIO1、GPIO5
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 078.png
设置b[31:30]、b[27:26]就可以使能GPIO5、GPIO1,设置为什么值呢?
看下图,设置为0b11:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 024.png
① 00:该GPIO模块全程被关闭
② 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭
③ 10:保留
④ 11:该GPIO模块全程使能
步骤2:设置GPIO5_IO03、GPIO1_IO03、GPIO1_IO05、GPIO1_IO06为GPIO模式
① 对于GPIO5_IO03,设置如下寄存器:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 071.png
② 对于GPIO1_IO03,设置如下寄存器:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 076.png
③ 对于GPIO1_IO05,设置如下寄存器:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 079.png
④ 对于GPIO1_IO06,设置如下寄存器:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 080.png
步骤3:设置GPIO5_IO03、GPIO1_IO03、GPIO1_IO05、GPIO1_IO06为输出引脚,设置其输出电平
寄存器地址为:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 072.png
设置方向寄存器,把引脚设置为输出引脚:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 073.png
设置数据寄存器,设置引脚的输出电平:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 074.png

写程序

使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\04_快速入门(正式开始)\ 02_嵌入式Linux驱动开发基础知识\source\02_led_drv\ 02_led_drv_for_boards\100ask_imx6ull-qemu_src_bin

硬件相关的文件是board_100ask_imx6ull-qemu.c,其他文件跟LED框架驱动程序完全一样。
涉及的寄存器挺多,一个一个去执行ioremap效率太低。
先定义结构体,然后对结构体指针进行ioremap,这些结构体在。
对于IOMUXC,可以如下定义:
	struct iomux {
		volatile unsigned int unnames[23];
		volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00;
		volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO01;
		volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO02;
		volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
		volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04;
		volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05;
		volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06;
		volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO07;
		volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO08;
		volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO09;
	};

	struct iomux  *iomux = ioremap(0x20e0000,  sizeof(struct iomux));
对于GPIO,可以如下定义:
	struct imx6ull_gpio {
		volatile unsigned int dr;
		volatile unsigned int gdir;
		volatile unsigned int psr;
		volatile unsigned int icr1;
		volatile unsigned int icr2;
		volatile unsigned int imr;
		volatile unsigned int isr;
		volatile unsigned int edge_sel;
	};
	struct imx6ull_gpio *gpio1 = ioremap(0x209C000,  sizeof(struct imx6ull_gpio));
	struct imx6ull_gpio *gpio5 = ioremap(0x20AC000,  sizeof(struct imx6ull_gpio));


开始详细分析board_100ask_imx6ull-qemu.c。
它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
	176 static struct led_operations board_demo_led_opr = {
	177     .num  = 4,
	178     .init = board_demo_led_init,
	179     .ctl  = board_demo_led_ctl,
	180 };
	181
led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
值得关注的是第61~66行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器:
	57 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
	58 {
	59     if (!CCM_CCGR1)
	60     {
	61         CCM_CCGR1 = ioremap(0x20C406C, 4);
	62         IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);
	63
	64         iomux = ioremap(0x20e0000, sizeof(struct iomux));
	65         gpio1 = ioremap(0x209C000, sizeof(struct imx6ull_gpio));
	66         gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
	67     }
	68
	69     if (which == 0)
	70     {
	71         /* 1. enable GPIO5
	72          * CG15, b[31:30] = 0b11
	73          */
	74         *CCM_CCGR1 |= (3<<30);
	75
	76         /* 2. set GPIO5_IO03 as GPIO
	77          * MUX_MODE, b[3:0] = 0b101
	78          */
	79         *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = 5;
	80
	81         /* 3. set GPIO5_IO03 as output
	82          * GPIO5 GDIR, b[3] = 0b1
	83          */
	84         gpio5->gdir |= (1<<3);
	85     }
	86     else if(which == 1)
	87     {
	88         /* 1. enable GPIO1
	89          * CG13, b[27:26] = 0b11
	90          */
	91         *CCM_CCGR1 |= (3<<26);
	92
	93         /* 2. set GPIO1_IO03 as GPIO
	94          * MUX_MODE, b[3:0] = 0b101
	95          */
	96         iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 5;
	97
	98         /* 3. set GPIO1_IO03 as output
	99          * GPIO1 GDIR, b[3] = 0b1
	100          */
	101         gpio1->gdir |= (1<<3);
	102     }
	103     else if(which == 2)
	104     {
	105         /* 1. enable GPIO1
	106          * CG13, b[27:26] = 0b11
	107          */
	108         *CCM_CCGR1 |= (3<<26);
	109
	110         /* 2. set GPIO1_IO05 as GPIO
	111          * MUX_MODE, b[3:0] = 0b101
	112          */
	113         iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05 = 5;
	114
	115         /* 3. set GPIO1_IO05 as output
	116          * GPIO1 GDIR, b[5] = 0b1
	117          */
	118         gpio1->gdir |= (1<<5);
	119     }
	120     else if(which == 3)
	121     {
	122         /* 1. enable GPIO1
	123          * CG13, b[27:26] = 0b11
	124          */
	125         *CCM_CCGR1 |= (3<<26);
	126
	127         /* 2. set GPIO1_IO06 as GPIO
	128          * MUX_MODE, b[3:0] = 0b101
	129          */
	130         iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06 = 5;
	131
	132         /* 3. set GPIO1_IO06 as output
	133          * GPIO1 GDIR, b[6] = 0b1
	134          */
	135         gpio1->gdir |= (1<<6);
	136     }
	137
	138     //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
	139     return 0;
	140 }
	141
led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
	142 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
	143 {
	144     //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
	145     if (which == 0)
	146     {
	147         if (status)  /* on : output 0 */
	148             gpio5->dr &= ~(1<<3);
	149         else         /* on : output 1 */
	150             gpio5->dr |= (1<<3);
	151     }
	152     else if (which == 1)
	153     {
	154         if (status)  /* on : output 0 */
	155             gpio1->dr &= ~(1<<3);
	156         else         /* on : output 1 */
	157             gpio1->dr |= (1<<3);
	158     }
	159     else if (which == 2)
	160     {
	161         if (status)  /* on : output 0 */
	162             gpio1->dr &= ~(1<<5);
	163         else         /* on : output 1 */
	164             gpio1->dr |= (1<<5);
	165     }
	166     else if (which == 3)
	167     {
	168         if (status)  /* on : output 0 */
	169             gpio1->dr &= ~(1<<6);
	170         else         /* on : output 1 */
	171             gpio1->dr |= (1<<6);
	172     }
	173     return 0;
	174 }
	175
下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
	182 struct led_operations *get_board_led_opr(void)
	183 {
	184     return &board_demo_led_opr;
	185 }
	186

上机实验

先启动IMX6ULL QEMU模拟器,挂载NFS文件系统。
运行QEMU时,
QEMU内部为主机虚拟出一个网卡, IP为 10.0.2.2,
IMX6ULL有一个网卡, IP为 10.0.2.15,
它连接到主机的虚拟网卡。
这样IMX6ULL就可以通过10.0.2.2去访问Ubuntu了。
然后执行以下命令安装驱动、执行测试程序:
	# insmod  100ask_led.ko
	# ./ledtest  /dev/100ask_led0  on
	# ./ledtest  /dev/100ask_led0  off


课后作业

a. 在驱动里有ioremap,什么时候执行iounmap?请完善程序
b. 驱动程序中有太多的if判断,请优化程序减少if的使用

驱动设计的思想:面向对象/分层/分离

面向对象

字符设备驱动程序抽象出一个file_operations结构体;
我们写的程序针对硬件部分抽象出led_operations结构体。

分层

上下分层,比如我们前面写的LED驱动程序就分为2层:
① 上层实现硬件无关的操作,比如注册字符设备驱动:leddrv.c
② 下层实现硬件相关的操作,比如board_A.c实现单板A的LED操作
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 081.png

分离

还能不能改进?分离
在board_A.c中,实现了一个led_operations,为LED引脚实现了初始化函数、控制函数:
	static struct led_operations board_demo_led_opr = {
		.num  = 1,
		.init = board_demo_led_init,
		.ctl  = board_demo_led_ctl,
	};
如果硬件上更换一个引脚来控制LED怎么办?你要去修改上面结构体中的init、ctl函数。
实际情况是,每一款芯片它的GPIO操作都是类似的。比如:GPIO1_3、GPIO5_4这2个引脚接到LED:
① GPIO1_3属于第1组,即GPIO1。
有方向寄存器DIR、数据寄存器DR等,基础地址是addr_base_addr_gpio1。
设置为output引脚:修改GPIO1的DIR寄存器的bit3。
设置输出电平:修改GPIO1的DR寄存器的bit3。
② GPIO5_4属于第5组,即GPIO5。
有方向寄存器DIR、数据寄存器DR等,基础地址是addr_base_addr_gpio5。
设置为output引脚:修改GPIO5的DIR寄存器的bit4。
设置输出电平:修改GPIO5的DR寄存器的bit4。


既然引脚操作那么有规律,并且这是跟主芯片相关的,那可以针对该芯片写出比较通用的硬件操作代码。
比如board_A.c使用芯片chipY,那就可以写出:chipY_gpio.c,它实现芯片Y的GPIO操作,适用于芯片Y的所有GPIO引脚。
使用时,我们只需要在board_A_led.c中指定使用哪一个引脚即可。
程序结构如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 082.png
以面向对象的思想,在board_A_led.c中实现led_resouce结构体,它定义“资源”──要用哪一个引脚。
在chipY_gpio.c中仍是实现led_operations结构体,它要写得更完善,支持所有GPIO。


写示例代码

使用GIT下载所有源码后,本节源码位于如下目录:
	01_all_series_quickstart\04_快速入门(正式开始)\
		02_嵌入式Linux驱动开发基础知识\source\02_led_drv\03_led_drv_template_seperate
程序仍分为上下结构:上层leddrv.c向内核注册file_operations结构体;下层chip_demo_gpio.c提供led_operations结构体来操作硬件。
下层的代码分为2个:chip_demo_gpio.c实现通用的GPIO操作,board_A_led.c指定使用哪个GPIO,即“资源”。
led_resource.h中定义了led_resource结构体,用来描述GPIO:
	04 /* GPIO3_0 */
	05 /* bit[31:16] = group */
	06 /* bit[15:0]  = which pin */
	07 #define GROUP(x) (x>>16)
	08 #define PIN(x)   (x&0xFFFF)
	09 #define GROUP_PIN(g,p) ((g<<16) | (p))
	10
	11 struct led_resource {
	12      int pin;
	13 };
	14
	15 struct led_resource *get_led_resouce(void);
	16
board_A_led.c指定使用哪个GPIO,它实现一个led_resource结构体,并提供访问函数:
	02 #include "led_resource.h"
	03
	04 static struct led_resource board_A_led = {
	05      .pin = GROUP_PIN(3,1),
	06 };
	07
	08 struct led_resource *get_led_resouce(void)
	09 {
	10      return &board_A_led;
	11 }
	12


chip_demo_gpio.c中,首先获得board_A_led.c实现的led_resource结构体,然后再进行其他操作,请看下面第26行:
	20 static struct led_resource *led_rsc;
	21 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
	22 {
	23      //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
	24      if (!led_rsc)
	25      {
	26              led_rsc = get_led_resouce();
	27      }
	28


课后作业

使用“分离”的思想,去改造前面写的LED驱动程序:实现led_resouce,在里面可以指定要使用哪一个LED;改造led_operations,让它能支持更多GPIO。
注意:作为练习,led_operations结构体不需要写得很完善,不需要支持所有GPIO,你可以只支持若干个GPIO即可。

驱动进化之路:总线设备驱动模型

示例:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 083.png

驱动编写的3种方法

以LED驱动为例:

传统写法

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 084.png
使用哪个引脚,怎么操作引脚,都写死在代码中。
最简单,不考虑扩展性,可以快速实现功能。
修改引脚时,需要重新编译。

总线设备驱动模型

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 085.png
引入platform_device/platform_driver,将“资源”与“驱动”分离开来。
代码稍微复杂,但是易于扩展。
冗余代码太多,修改引脚时设备端的代码需要重新编译。
更换引脚时,上图中的led_drv.c基本不用改,但是需要修改led_dev.c

设备树

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 086.png
通过配置文件──设备树来定义“资源”。
代码稍微复杂,但是易于扩展。
无冗余代码,修改引脚时只需要修改dts文件并编译得到dtb文件,把它传给内核。
无需重新编译内核/驱动。

在Linux中实现“分离”:Bus/Dev/Drv模型

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 087.png

匹配规则

最先比较:platform_device. driver_override和platform_driver.driver.name

可以设置platform_device的driver_override,强制选择某个platform_driver。

然后比较:platform_device. name和platform_driver.id_table[i].name

Platform_driver.id_table是“platform_device_id”指针,表示该drv支持若干个device,它里面列出了各个device的{.name, .driver_data},其中的“name”表示该drv支持的设备的名字,driver_data是些提供给该device的私有数据。

最后比较:platform_device.name和platform_driver.driver.name

platform_driver.id_table可能为空,
这时可以根据platform_driver.driver.name来寻找同名的platform_device。

函数调用关系

platform_device_register
	platform_device_add
		device_add
			bus_add_device // 放入链表
			bus_probe_device  // probe枚举设备,即找到匹配的(dev, drv)
				device_initial_probe
					__device_attach
						bus_for_each_drv(...,__device_attach_driver,...)
							__device_attach_driver
								driver_match_device(drv, dev) // 是否匹配
								driver_probe_device         // 调用drv的probe

platform_driver_register
	__platform_driver_register
		driver_register
			bus_add_driver // 放入链表
				driver_attach(drv)
						bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
							__driver_attach
								driver_match_device(drv, dev) // 是否匹配
								driver_probe_device         // 调用drv的probe

常用函数

这些函数可查看内核源码:drivers/base/platform.c,根据函数名即可知道其含义。
下面摘取常用的几个函数。

注册/反注册

platform_device_register/ platform_device_unregister
platform_driver_register/ platform_driver_unregister
platform_add_devices // 注册多个device

获得资源

返回该dev中某类型(type)资源中的第几个(num):
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 088.png
返回该dev所用的第几个(num)中断:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 089.png
通过名字(name)返回该dev的某类型(type)资源:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 090.png
通过名字(name)返回该dev的中断号:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 091.png

怎么写程序

分配/设置/注册platform_device结构体

在里面定义所用资源,指定设备名字。

分配/设置/注册platform_driver结构体

在其中的probe函数里,分配/设置/注册file_operations结构体,
并从platform_device中确实所用硬件资源。
指定platform_driver的名字。

课后作业

在内核源码中搜索platform_device_register可以得到很多驱动,选择一个作为例子:
① 确定它的名字
② 根据它的名字找到对应的platform_driver
③ 进入platform_device_register/platform_driver_register内部,分析dev和drv的匹配过程

LED模板驱动程序的改造:总线设备驱动模型

原来的框架

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 092.png

要实现的框架

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 093.png

写代码

使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\04_快速入门(正式开始)\ 02_嵌入式Linux驱动开发基础知识\source\ 02_led_drv\04_led_drv_template_bus_dev_drv

注意事项

① 如果platform_device中不提供release函数,如下图所示不提供红框部分的函数:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 094.png
则在调用platform_device_unregister时会出现警告,如下图所示:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 095.png
你可以提供一个release函数,如果实在无事可做,把这函数写为空。
② EXPORT_SYMBOL
a.c编译为a.ko,里面定义了func_a;如果它想让b.ko使用该函数,那么a.c里需要导出此函数(如果a.c, b.c都编进内核,则无需导出):
EXPORT_SYMBOL(led_device_create);
并且,使用时要先加载a.ko。
如果先加载b.ko,会有类似如下“Unknown symbol”的提示:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 096.png


实现platform_device结构体

board_A.c作为一个可加载模块,里面也有入口函数、出口函数。在入口函数中注册platform_device结构体,在platform_device结构体中指定使用哪个GPIO引脚。
首先看入口函数,它调用platform_device_register函数,向内核注册board_A_led_dev结构体:
	50 static int __init led_dev_init(void)
	51 {
	52     int err;
	53
	54     err = platform_device_register(&board_A_led_dev);
	55
	56     return 0;
	57 }
	58
board_A_led_dev结构体定义如下。
在resouces数组中指定了2个引脚(第27~38行);
我们还提供了一个空函数led_dev_release(第23~25行),它被赋给board_A_led_dev结构体(第46行),这个函数在卸载platform_device时会被调用,如果不提供的话内核会打印警告信息。
	23 static void led_dev_release(struct device *dev)
	24 {
	25 }
	26
	27 static struct resource resources[] = {
	28         {
	29                 .start = GROUP_PIN(3,1),
	30                 .flags = IORESOURCE_IRQ,
	31                 .name = "100ask_led_pin",
	32         },
	33         {
	34                 .start = GROUP_PIN(5,8),
	35                 .flags = IORESOURCE_IRQ,
	36                 .name = "100ask_led_pin",
	37         },
	38 };
	39
	40
	41 static struct platform_device board_A_led_dev = {
	42         .name = "100ask_led",
	43         .num_resources = ARRAY_SIZE(resources),
	44         .resource = resources,
	45         .dev = {
	46                 .release = led_dev_release,
	47          },
	48 };
	49


实现platform_driver结构体

chip_demo_gpio.c中注册platform_driver结构体,它使用Bus/Dev/Drv模型,当有匹配的platform_device时,它的probe函数就会被调用。
在probe函数中所做的事情跟之前的代码没有差别。
先看入口函数。
第150行向内核注册一个platform_driver结构体;
这个结构体的核心在于第140行的chip_demo_gpio_probe函数。
	138 static struct platform_driver chip_demo_gpio_driver = {
	139     .probe      = chip_demo_gpio_probe,
	140     .remove     = chip_demo_gpio_remove,
	141     .driver     = {
	142         .name   = "100ask_led",
	143     },
	144 };
	145
	146 static int __init chip_demo_gpio_drv_init(void)
	147 {
	148     int err;
	149
	150     err = platform_driver_register(&chip_demo_gpio_driver);
	151     register_led_operations(&board_demo_led_opr);
	152
	153     return 0;
	154 }
	155
chip_demo_gpio_probe函数代码如下。
第107行:从匹配的platform_device中获取资源,确定GPIO引脚。
第111行:把引脚记录下来,在操作硬件时要用。
第112行:新发现了一个GPIO引脚,就调用上层驱动的代码创建设备节点。
	100 static int chip_demo_gpio_probe(struct platform_device *pdev)
	101 {
	102     struct resource *res;
	103     int i = 0;
	104
	105     while (1)
	106     {
	107         res = platform_get_resource(pdev, IORESOURCE_IRQ, i++);
	108         if (!res)
	109             break;
	110
	111         g_ledpins[g_ledcnt] = res->start;
	112         led_class_create_device(g_ledcnt);
	113         g_ledcnt++;
	114     }
	115     return 0;
	116
	117 }
	118
操作硬件的代码如下,第31、63行的代码里用到了数组g_ledpins,里面的值来自platform_device,在probe函数中根据platform_device的资源确定了引脚:
	23 static int g_ledpins[100];
	24 static int g_ledcnt = 0;
	25
	26 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
	27 {
	28     //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
	29
	30   printk("init gpio: group %d, pin %d\n", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
	31     switch(GROUP(g_ledpins[which]))
	32     {
	33         case 0:
	34         {
	35             printk("init pin of group 0 ...\n");
	36             break;
	37         }
	38         case 1:
	39         {
	40             printk("init pin of group 1 ...\n");
	41             break;
	42         }
	43         case 2:
	44         {
	45             printk("init pin of group 2 ...\n");
	46             break;
	47         }
	48         case 3:
	49         {
	50             printk("init pin of group 3 ...\n");
	51             break;
	52         }
	53     }
	54
	55     return 0;
	56 }
	57
	58 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
	59 {
	60     //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
	61    printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
	62
	63     switch(GROUP(g_ledpins[which]))
	64     {
	65         case 0:
	66         {
	67             printk("set pin of group 0 ...\n");
	68             break;
	69         }
	70         case 1:
	71         {
	72             printk("set pin of group 1 ...\n");
	73             break;
	74         }
	75         case 2:
	76         {
	77             printk("set pin of group 2 ...\n");
	78             break;
	79         }
	80         case 3:
	81         {
	82             printk("set pin of group 3 ...\n");
	83             break;
	84         }
	85     }
	86
	87     return 0;
	88 }
	89
	90 static struct led_operations board_demo_led_opr = {
	91     .init = board_demo_led_init,
	92     .ctl  = board_demo_led_ctl,
	93 };
	94
	95 struct led_operations *get_board_led_opr(void)
	96 {
	97     return &board_demo_led_opr;
	98 }
	99

课后作业

完善半成品程序:04_led_drv_template_bus_dev_drv_unfinished。
请仿照本节提供的程序(位于04_led_drv_template_bus_dev_drv目录),改造你所用的单板的LED驱动程序。

驱动进化之路:设备树的引入及简明教程

官方文档(可以下载到devicetree-specification-v0.2.pdf): https://www.devicetree.org/specifications/
内核文档:
	Documentation/devicetree/booting-without-of.txt
我录制“设备树视频”时写的文档:设备树详细分析.txt
这个txt文件也同步上传到wiki了:http://wiki.100ask.org/Linux_devicetree
我录制的设备树视频,它是基于s3c2440的,用的是linux 4.19;需要深入研究的可以看该视频(收费)。
注意,如果只是想入门,看本文档及视频即可。

设备树的引入与作用

以LED驱动为例,如果你要更换LED所用的GPIO引脚,需要修改驱动程序源码、重新编译驱动、重新加载驱动。
在内核中,使用同一个芯片的板子,它们所用的外设资源不一样,比如A板用GPIO A,B板用GPIO B。而GPIO的驱动程序既支持GPIO A也支持GPIO B,你需要指定使用哪一个引脚,怎么指定?在c代码中指定。
随着ARM芯片的流行,内核中针对这些ARM板保存有大量的、没有技术含量的文件。
Linus大发雷霆:"this whole ARM thing is a f*cking pain in the ass"。
于是,Linux内核开始引入设备树。
设备树并不是重新发明出来的,在Linux内核中其他平台如PowerPC,早就使用设备树来描述硬件了。
Linus发火之后,内核开始全面使用设备树来改造,神人就神人。
有一种错误的观点,说“新驱动都是用设备树来写了”。
设备树不可能用来写驱动
请想想,要操作硬件就需要去操作复杂的寄存器,如果设备树可以操作寄存器,那么它就是“驱动”,它就一样很复杂。
设备树只是用来给内核里的驱动程序,指定硬件的信息。比如LED驱动,在内核的驱动程序里去操作寄存器,但是操作哪一个引脚?这由设备树指定。
你可以事先体验一下设备树,板子启动后执行下面的命令:
	# ls /sys/firmware/
	devicetree  fdt
/sys/firmware/devicetree目录下是以目录结构程现的dtb文件, 根节点对应base目录, 每一个节点对应一个目录, 每一个属性对应一个文件。
这些属性的值如果是字符串,可以使用cat命令把它打印出来;对于数值,可以用hexdump把它打印出来。
一个单板启动时,u-boot先运行,它的作用是启动内核。U-boot会把内核和设备树文件都读入内存,然后启动内核。在启动内核时会把设备树在内存中的地址告诉内核。

设备树的语法

为什么叫“树”?
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 097.png
怎么描述这棵树?
我们需要编写设备树文件(dts: device tree source),它需要编译为dtb(device tree blob)文件,内核使用的是dtb文件。
dts文件是根本,它的语法很简单。
下面是一个设备树示例:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 098.png
它对应的dts文件如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 099.png

Devicetree格式

DTS文件的格式
DTS文件布局(layout):
	/dts-v1/;                // 表示版本
	[memory reservations]    // 格式为: /memreserve/ <address> <length>;
	/ {
	    [property definitions]
	    [child nodes]
	};
node的格式
设备树中的基本单元,被称为“node”,其格式为:
	[label:] node-name[@unit-address] {
	    [properties definitions]
	    [child nodes]
	};


label是标号,可以省略。label的作用是为了方便地引用node,比如:
	/dts-v1/;
	/ {
		uart0: uart@fe001000 {
	        compatible="ns16550";
	        reg=<0xfe001000 0x100>;
		};
	};
可以使用下面2种方法来修改uart@fe001000这个node:
	// 在根节点之外使用label引用node:
	&uart0 {
	    status = “disabled”;
	};
或在根节点之外使用全路径:
		&{/uart@fe001000}  {
		    status = “disabled”;
		};


properties的格式
简单地说,properties就是“name=value”,value有多种取值方式。
Property格式1:
	[label:] property-name = value;


Property格式2(没有值):
	[label:] property-name;
Property取值只有3种
		arrays of cells(1个或多个32位数据, 64位数据使用2个32位数据表示), 
		string(字符串), 
		bytestring(1个或多个字节)
示例
a. Arrays of cells : cell就是一个32位的数据,用尖括号包围起来
	interrupts = <17 0xc>;
b. 64bit数据使用2个cell来表示,用尖括号包围起来:
	clock-frequency = <0x00000001 0x00000000>;
c. A null-terminated string (有结束符的字符串),用双引号包围起来:
	compatible = "simple-bus";
d. A bytestring(字节序列) ,用中括号包围起来:
	local-mac-address = [00 00 12 34 56 78];  // 每个byte使用2个16进制数来表示
	local-mac-address = [000012345678];       // 每个byte使用2个16进制数来表示
e. 可以是各种值的组合, 用逗号隔开:
	compatible = "ns16550", "ns8250";
	example = <0xf00f0000 19>, "a strange property format";

dts文件包含dtsi文件

设备树文件不需要我们从零写出来,内核支持了某款芯片比如imx6ull,在内核的arch/arm/boot/dts目录下就有了能用的设备树模板,一般命名为xxxx.dtsi。“i”表示“include”,被别的文件引用的。
我们使用某款芯片制作出了自己的单板,所用资源跟xxxx.dtsi是大部分相同,小部分不同,所以需要引脚xxxx.dtsi并修改。
dtsi文件跟dts文件的语法是完全一样的。


dts中可以包含.h头文件,也可以包含dtsi文件,在.h头文件中可以定义一些宏。
示例:
	/dts-v1/;
	
	#include <dt-bindings/input/input.h>
	#include "imx6ull.dtsi"
	
	/ {
	……
	};


常用的属性

#address-cells、#size-cells
cell指一个32位的数值,
		address-cells:address要用多少个32位数来表示;
		size-cells:size要用多少个32位数来表示。
比如一段内存,怎么描述它的起始地址和大小?
下例中,address-cells为1,所以reg中用1个数来表示地址,即用0x80000000来表示地址;size-cells为1,所以reg中用1个数来表示大小,即用0x20000000表示大小:
		/ {
		#address-cells = <1>;
		#size-cells = <1>;
		memory {
		reg = <0x80000000 0x20000000>;
		    };
		};
compatible
“compatible”表示“兼容”,对于某个LED,内核中可能有A、B、C三个驱动都支持它,那可以这样写:
		led {
		compatible = “A”, “B”, “C”;
		};
内核启动时,就会为这个LED按这样的优先顺序为它找到驱动程序:A、B、C。
根节点下也有compatible属性,用来选择哪一个“machine desc”:一个内核可以支持machine A,也支持machine B,内核启动后会根据根节点的compatible属性找到对应的machine desc结构体,执行其中的初始化函数。
compatible的值,建议取这样的形式:"manufacturer,model",即“厂家名,模块名”。
注意:machine desc的意思就是“机器描述”,学到内核启动流程时才涉及。
model
model属性与compatible属性有些类似,但是有差别。
compatible属性是一个字符串列表,表示可以你的硬件兼容A、B、C等驱动;
model用来准确地定义这个硬件是什么。
比如根节点中可以这样写:
		/ {
			compatible = "samsung,smdk2440", "samsung,mini2440";
			model = "jz2440_v3";
		};
它表示这个单板,可以兼容内核中的“smdk2440”,也兼容“mini2440”。
从compatible属性中可以知道它兼容哪些板,但是它到底是什么板?用model属性来明确。
status
dtsi文件中定义了很多设备,但是在你的板子上某些设备是没有的。这时你可以给这个设备节点添加一个status属性,设置为“disabled”:
		&uart1 {
		      status = "disabled";
		};
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 100.png
reg
reg的本意是register,用来表示寄存器地址。
但是在设备树里,它可以用来描述一段空间。反正对于ARM系统,寄存器和内存是统一编址的,即访问寄存器时用某块地址,访问内存时用某块地址,在访问方法上没有区别。
reg属性的值,是一系列的“address size”,用多少个32位的数来表示address和size,由其父节点的#address-cells、#size-cells决定。
示例:
		/dts-v1/;
		/ {
				#address-cells = <1>;
				#size-cells = <1>; 
				memory {
				reg = <0x80000000 0x20000000>;
			};
		};
name(过时了,建议不用)
它的值是字符串,用来表示节点的名字。在跟platform_driver匹配时,优先级最低。
compatible属性在匹配过程中,优先级最高。
device_type(过时了,建议不用)
它的值是字符串,用来表示节点的类型。在跟platform_driver匹配时,优先级为中。
compatible属性在匹配过程中,优先级最高。


常用的节点(node)

根节点
dts文件中必须有一个根节点:
	/dts-v1/;
	/ {
			model = "SMDK24440";
			compatible = "samsung,smdk2440";

			#address-cells = <1>;
			#size-cells = <1>; 
	};
根节点中必须有这些属性:
		#address-cells // 在它的子节点的reg属性中, 使用多少个u32整数来描述地址(address)
		#size-cells   // 在它的子节点的reg属性中, 使用多少个u32整数来描述大小(size)
		compatible   // 定义一系列的字符串, 用来指定内核中哪个machine_desc可以支持本设备
		            // 即这个板子兼容哪些平台 
		            // uImage : smdk2410 smdk2440 mini2440     ==> machine_desc         
		                 
		model       // 咱这个板子是什么
		            // 比如有2款板子配置基本一致, 它们的compatible是一样的
		            // 那么就通过model来分辨这2款板子
CPU节点
一般不需要我们设置,在dtsi文件中都定义好了:
		cpus {
				#address-cells = <1>;
				#size-cells = <0>;
		
				cpu0: cpu@0 {
				    .......
		        }
		};
memory节点
芯片厂家不可能事先确定你的板子使用多大的内存,所以memory节点需要板厂设置,比如:
		memory {
			reg = <0x80000000 0x20000000>;
		};
chosen节点
我们可以通过设备树文件给内核传入一些参数,这要在chosen节点中设置bootargs属性:
		chosen {
			bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
		};


编译、更换设备树

我们一般不会从零写dts文件,而是修改。程序员水平有高有低,改得对不对?需要编译一下。并且内核直接使用dts文件的话,就太低效了,它也需要使用二进制格式的dtb文件。

在内核中直接make

设置ARCH、CROSS_COMPILE、PATH这三个环境变量后,进入ubuntu上板子内核源码的目录,执行如下命令即可编译dtb文件:

make dtbs V=1

这些操作步骤在各个开发板的高级用户使用手册,或是http://wiki.100ask.net中各个板子的页面里,都有说明。
以野火的IMX6UL为例,可以看到如下输出:
		mkdir -p arch/arm/boot/dts/ ; 
		arm-linux-gnueabihf-gcc -E 
		  -Wp,-MD,arch/arm/boot/dts/.imx6ull-14x14-ebf-mini.dtb.d.pre.tmp 
		  -nostdinc 
		  -I./arch/arm/boot/dts 
		  -I./arch/arm/boot/dts/include 
		  -I./drivers/of/testcase-data 
		  -undef -D__DTS__ -x assembler-with-cpp 
		  -o arch/arm/boot/dts/.imx6ull-14x14-ebf-mini.dtb.dts.tmp 
		  arch/arm/boot/dts/imx6ull-14x14-ebf-mini.dts ; 
		  
		./scripts/dtc/dtc -O dtb 
		  -o arch/arm/boot/dts/imx6ull-14x14-ebf-mini.dtb
		  -b 0 -i arch/arm/boot/dts/ -Wno-unit_address_vs_reg  
		  -d arch/arm/boot/dts/.imx6ull-14x14-ebf-mini.dtb.d.dtc.tmp 
		  arch/arm/boot/dts/.imx6ull-14x14-ebf-mini.dtb.dts.tmp ;
它首先用arm-linux-gnueabihf-gcc预处理dts文件,把其中的.h头文件包含进来,把宏展开。
然后使用scripts/dtc/dtc生成dtb文件。
可见,dts文件之所以支持“#include”语法,是因为arm-linux-gnueabihf-gcc帮忙。
如果只用dtc工具,它是不支持”#include”语法的,只支持“/include”语法。

手工编译

除非你对设备树比较了解,否则不建议手工使用dtc工具直接编译。
内核目录下scripts/dtc/dtc是设备树的编译工具,直接使用它的话,包含其他文件时不能使用“#include”,而必须使用“/incldue”。
编译、反编译的示例命令如下,“-I”指定输入格式,“-O”指定输出格式,“-o”指定输出文件:
		./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts  // 编译dts为dtb
		./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb  // 反编译dtb为dts

给开发板更换设备树文件

怎么给各个单板编译出设备树文件,它们的设备树文件是哪一个?
这些操作步骤在各个开发板的高级用户使用手册,或是http://wiki.100ask.net中各个板子的页面里,都有说明。
基本方法都是:设置ARCH、CROSS_COMPILE、PATH这三个环境变量后,在内核源码目录中执行:
	make  dtbs
对于100ask-am335x 单板
设备树文件是:内核源码目录中arch/arm/boot/dts/100ask-am335x.dtb
要更换板子上的设备树文件,启动板子后,更换这个文件:/boot/mx6ull-14x14-ebf.dtb
对于firefly-rk3288
设备树文件是:内核源码目录中arch/arm/boot/dts/rk3288-firefly.dtb
对于这款板子,本教程中我们使用SD卡上的系统。
要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/rk3288-firefly.dtb
对于firefly的roc-rk3399-pc
设备树文件是:内核源码目录中arch/arm64/boot/dts/rk3399-roc-pc.dtb
对于这款板子,本教程中我们使用SD卡上的系统。
要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/ rk3399-roc-pc.dtb
对于百问网使用QEMU模拟的IMX6ULL板子
设备树文件是:内核源码目录中arch/arm/boot/dts/100ask_imx6ul_qemu.dtb
它是执行qemu时直接在命令行中指定设备树文件的,你可以打开脚本文件qemu-imx6ul-gui.sh找到dtb文件的位置,然后使用新编译出来的dtb去覆盖老文件。
对于野火imx6ull-pro
设备树文件是:内核源码目录中arch/arm/boot/dts/imx6ull-14x14-ebf.dtb
对于这款板子,本教程中我们使用SD卡上的系统。
要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/imx6ull-14x14-ebf.dtb
对于正点原子imx6ull-alpha
设备树文件是:内核源码目录中arch/arm/boot/dts/imx6ull-14x14-alpha.dtb
对于这款板子,本教程中我们使用SD卡上的系统。
要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/arch/arm/boot/dts/imx6ull-14x14-alpha.dtb

板子启动后查看设备树

板子启动后执行下面的命令:
		# ls /sys/firmware/
		devicetree  fdt
/sys/firmware/devicetree目录下是以目录结构程现的dtb文件, 根节点对应base目录, 每一个节点对应一个目录, 每一个属性对应一个文件。
这些属性的值如果是字符串,可以使用cat命令把它打印出来;对于数值,可以用hexdump把它打印出来。
还可以看到/sys/firmware/fdt文件,它就是dtb格式的设备树文件,可以把它复制出来放到ubuntu上,执行下面的命令反编译出来(-I dtb:输入格式是dtb,-O dts:输出格式是dts):
		cd  板子所用的内核源码目录
		./scripts/dtc/dtc  -I  dtb  -O  dts   /从板子上/复制出来的/fdt  -o   tmp.dts

内核对设备树的处理

从源代码文件dts文件开始,设备树的处理过程为:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 101.png
① dts在PC机上被编译为dtb文件;
② u-boot把dtb文件传给内核;
③ 内核解析dtb文件,把每一个节点都转换为device_node结构体;
④ 对于某些device_node结构体,会被转换为platform_device结构体。

dtb中每一个节点都被转换为device_node结构体

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 102.png
根节点被保存在全局变量of_root中,从of_root开始可以访问到任意节点。

哪些设备树节点会被转换为platform_device

A. 根节点下含有compatile属性的子节点
B. 含有特定compatile属性的节点的子节点
如果一个节点的compatile属性,它的值是这4者之一:"simple-bus","simple-mfd","isa","arm,amba-bus",
那么它的子结点(需含compatile属性)也可以转换为platform_device。
C. 总线I2C、SPI节点下的子节点:不转换为platform_device
某个总线下到子节点,应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device。
比如以下的节点中:
/mytest会被转换为platform_device, 因为它兼容"simple-bus";
它的子节点/mytest/mytest@0 也会被转换为platform_device
/i2c节点一般表示i2c控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
/i2c/at24c02节点不会被转换为platform_device, 它被如何处理完全由父节点的platform_driver决定, 一般是被创建为一个i2c_client。
类似的也有/spi节点, 它一般也是用来表示SPI控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
/spi/flash@0节点不会被转换为platform_device, 它被如何处理完全由父节点的platform_driver决定, 一般是被创建为一个spi_device。
			/ {
				  mytest {
					  compatile = "mytest", "simple-bus";
					  mytest@0 {
							compatile = "mytest_0";
					  };
				  };
				  
				  i2c {
					  compatile = "samsung,i2c";
					  at24c02 {
							compatile = "at24c02";                      
					  };
				  };
			
				  spi {
					  compatile = "samsung,spi";              
					  flash@0 {
							compatible = "winbond,w25q32dw";
							spi-max-frequency = <25000000>;
							reg = <0>;
						  };
				  };
			  };

怎么转换为platform_device

内核处理设备树的函数调用过程,这里不去分析;我们只需要得到如下结论:
A. platform_device中含有resource数组, 它来自device_node的reg, interrupts属性;
B. platform_device.dev.of_node指向device_node, 可以通过它获得其他属性

platform_device如何与platform_driver配对

从设备树转换得来的platform_device会被注册进内核里,以后当我们每注册一个platform_driver时,它们就会两两确定能否配对,如果能配对成功就调用platform_driver的probe函数。
套路是一样的。
我们需要将前面讲过的“匹配规则”再完善一下:
先贴源码:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 103.png

最先比较:是否强制选择某个driver

比较platform_device. driver_override和platform_driver.driver.name
可以设置platform_device的driver_override,强制选择某个platform_driver。

然后比较:设备树信息

比较:platform_device. dev.of_node和platform_driver.driver.of_match_table。
由设备树节点转换得来的platform_device中,含有一个结构体:of_node。
它的类型如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 104.png
如果一个platform_driver支持设备树,它的platform_driver.driver.of_match_table是一个数组,类型如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 105.png
使用设备树信息来判断dev和drv是否配对时,
首先,如果of_match_table中含有compatible值,就跟dev的compatile属性比较,若一致则成功,否则返回失败;
其次,如果of_match_table中含有type值,就跟dev的device_type属性比较,若一致则成功,否则返回失败;
最后,如果of_match_table中含有name值,就跟dev的name属性比较,若一致则成功,否则返回失败。
而设备树中建议不再使用devcie_type和name属性,所以基本上只使用设备节点的compatible属性来寻找匹配的platform_driver。

接下来比较:platform_device_id

比较platform_device. name和platform_driver.id_table[i].name,id_table中可能有多项。
platform_driver.id_table是“platform_device_id”指针,表示该drv支持若干个device,它里面列出了各个device的{.name, .driver_data},其中的“name”表示该drv支持的设备的名字,driver_data是些提供给该device的私有数据。

最后比较:platform_device.name和platform_driver.driver.name

platform_driver.id_table可能为空,
这时可以根据platform_driver.driver.name来寻找同名的platform_device。

一个图概括所有的配对过程

概括出了这个图:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 106.png

没有转换为platform_device的节点,如何使用

任意驱动程序里,都可以直接访问设备树。
你可以使用“11.7”节中介绍的函数找到节点,读出里面的值。

内核里操作设备树的常用函数

内核源码中include/linux/目录下有很多of开头的头文件,of表示“open firmware”即开放固件。

内核中设备树相关的头文件介绍

内核源码中include/linux/目录下有很多of开头的头文件,of表示“open firmware”即开放固件。
设备树的处理过程是:dtb -> device_node -> platform_device
处理DTB
		of_fdt.h           // dtb文件的相关操作函数, 我们一般用不到, 
		// 因为dtb文件在内核中已经被转换为device_node树(它更易于使用)
处理device_node
		of.h               // 提供设备树的一般处理函数, 
		// 比如 of_property_read_u32(读取某个属性的u32值),
		// of_get_child_count(获取某个device_node的子节点数)
		of_address.h       // 地址相关的函数, 
		// 比如 of_get_address(获得reg属性中的addr, size值)
		// of_match_device (从matches数组中取出与当前设备最匹配的一项)
		of_dma.h           // 设备树中DMA相关属性的函数
		of_gpio.h          // GPIO相关的函数
		of_graph.h         // GPU相关驱动中用到的函数, 从设备树中获得GPU信息
		of_iommu.h         // 很少用到
		of_irq.h           // 中断相关的函数
		of_mdio.h          // MDIO (Ethernet PHY) API
		of_net.h           // OF helpers for network devices. 
		of_pci.h           // PCI相关函数
		of_pdt.h           // 很少用到
		of_reserved_mem.h  // reserved_mem的相关函数
处理 platform_device
		of_platform.h      // 把device_node转换为platform_device时用到的函数, 
		                   // 比如of_device_alloc(根据device_node分配设置platform_device), 
		                   // of_find_device_by_node (根据device_node查找到platform_device),
		                   //   of_platform_bus_probe (处理device_node及它的子节点)
		of_device.h        // 设备相关的函数, 比如 of_match_device

platform_device相关的函数

of_platform.h中声明了很多函数,但是作为驱动开发者,我们只使用其中的1、2个。其他的都是给内核自己使用的,内核使用它们来处理设备树,转换得到platform_device。
of_find_device_by_node
函数原型为:
		extern struct platform_device *of_find_device_by_node(struct device_node *np);
设备树中的每一个节点,在内核里都有一个device_node;你可以使用device_node去找到对应的platform_device。
platform_get_resource
这个函数跟设备树没什么关系,但是设备树中的节点被转换为platform_device后,设备树中的reg属性、interrupts属性也会被转换为“resource”。
这时,你可以使用这个函数取出这些资源。
函数原型为:
			/**
			 * platform_get_resource - get a resource for a device
			 * @dev: platform device
			 * @type: resource type   // 取哪类资源?IORESOURCE_MEM、IORESOURCE_REG
			*                      // IORESOURCE_IRQ等
			 * @num: resource index  // 这类资源中的哪一个?
			 */
			struct resource *platform_get_resource(struct platform_device *dev,
							       unsigned int type, unsigned int num);
对于设备树节点中的reg属性,它属性IORESOURCE_MEM类型的资源;
对于设备树节点中的interrupts属性,它属性IORESOURCE_IRQ类型的资源。

有些节点不会生成platform_device,怎么访问它们

内核会把dtb文件解析出一系列的device_node结构体,我们可以直接访问这些device_node。
内核源码incldue/linux/of.h中声明了device_node和属性property的操作函数,device_node和property的结构体定义如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 102.png
找到节点
a. of_find_node_by
根据路径找到节点,比如“/”就对应根节点,“/memory”对应memory节点。
函数原型:
	static inline struct device_node *of_find_node_by_path(const char *path);
b. of_find_node_by_name
根据名字找到节点,节点如果定义了name属性,那我们可以根据名字找到它。
函数原型:
		 extern struct device_node *of_find_node_by_name(struct device_node *from,
				const char *name);
参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
但是在设备树的官方规范中不建议使用“name”属性,所以这函数也不建议使用。
c. of_find_node_by_type
根据类型找到节点,节点如果定义了device_type属性,那我们可以根据类型找到它。
函数原型:
		 extern struct device_node *of_find_node_by_type(struct device_node *from,
		 	const char *type);
参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
但是在设备树的官方规范中不建议使用“device_type”属性,所以这函数也不建议使用。


d. of_find_compatible_node
根据compatible找到节点,节点如果定义了compatible属性,那我们可以根据compatible属性找到它。
函数原型:
		extern struct device_node *of_find_compatible_node(struct device_node *from,
			const char *type, const char *compat);
参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
参数compat是一个字符串,用来指定compatible属性的值;
参数type是一个字符串,用来指定device_type属性的值,可以传入NULL。
e. of_find_node_by_phandle
根据phandle找到节点。
dts文件被编译为dtb文件时,每一个节点都有一个数字ID,这些数字ID彼此不同。可以使用数字ID来找到device_node。这些数字ID就是phandle。
函数原型:
		extern struct device_node *of_find_node_by_phandle(phandle handle);
参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
f. of_get_parent
找到device_node的父节点。
函数原型:
		extern struct device_node *of_get_parent(const struct device_node *node);
参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
g. of_get_next_parent
这个函数名比较奇怪,怎么可能有“next parent”?
它实际上也是找到device_node的父节点,跟of_get_parent的返回结果是一样的。
差别在于它多调用下列函数,把node节点的引用计数减少了1。这意味着调用of_get_next_parent之后,你不再需要调用of_node_put释放node节点。
		of_node_put(node);
函数原型:
		extern struct device_node *of_get_next_parent(struct device_node *node);
参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。


h. of_get_next_child
取出下一个子节点。
函数原型:
			extern struct device_node *of_get_next_child(const struct device_node *node,
								     struct device_node *prev);
参数node表示父节点;
prev表示上一个子节点,设为NULL时表示想找到第1个子节点。
不断调用of_get_next_child时,不断更新pre参数,就可以得到所有的子节点。
i. of_get_next_available_child
取出下一个“可用”的子节点,有些节点的status是“disabled”,那就会跳过这些节点。
函数原型:
		 struct device_node *of_get_next_available_child(const struct device_node *node,
		 	struct device_node *prev);
参数node表示父节点;
prev表示上一个子节点,设为NULL时表示想找到第1个子节点。
j. of_get_child_by_name
根据名字取出子节点。
函数原型:
			extern struct device_node *of_get_child_by_name(const struct device_node *node,
								const char *name);
参数node表示父节点;
name表示子节点的名字。
找到属性
内核源码incldue/linux/of.h中声明了device_node的操作函数,当然也包括属性的操作函数。
a. of_find_property
找到节点中的属性。
函数原型:
		extern struct property *of_find_property(const struct device_node *np,
							 const char *name,
							 int *lenp);
参数np表示节点,我们要在这个节点中找到名为name的属性。
lenp用来保存这个属性的长度,即它的值的长度。
在设备树中,节点大概是这样:
	xxx_node {
	    xxx_pp_name = “hello”;
	};
上述节点中,“xxx_pp_name”就是属性的名字,值的长度是6。
获取属性的值
a. of_get_property
根据名字找到节点的属性,并且返回它的值。
函数原型:
		/*
		 * Find a property with a given name for a given node
		 * and return the value.
		 */
		const void *of_get_property(const struct device_node *np, const char *name,
					    int *lenp)
参数np表示节点,我们要在这个节点中找到名为name的属性,然后返回它的值。
lenp用来保存这个属性的长度,即它的值的长度。
b. of_property_count_elems_of_size
根据名字找到节点的属性,确定它的值有多少个元素(elem)。
函数原型:
		/* of_property_count_elems_of_size - Count the number of elements in a property
		 *
		 * @np:		device node from which the property value is to be read.
		 * @propname:	name of the property to be searched.
		 * @elem_size:	size of the individual element
		 *
		 * Search for a property in a device node and count the number of elements of
		 * size elem_size in it. Returns number of elements on sucess, -EINVAL if the
		 * property does not exist or its length does not match a multiple of elem_size
		 * and -ENODATA if the property does not have a value.
		 */
		int of_property_count_elems_of_size(const struct device_node *np,
						const char *propname, int elem_size)
参数np表示节点,我们要在这个节点中找到名为propname的属性,然后返回下列结果:
		return prop->length / elem_size;
在设备树中,节点大概是这样:
		xxx_node {
		    xxx_pp_name = <0x50000000 1024>  <0x60000000  2048>;
		};
调用of_property_count_elems_of_size(np, “xxx_pp_name”, 8)时,返回值是2;
调用of_property_count_elems_of_size(np, “xxx_pp_name”, 4)时,返回值是4。
c. 读整数u32/u64
函数原型为:
		static inline int of_property_read_u32(const struct device_node *np,
						       const char *propname,
						       u32 *out_value);

		extern int of_property_read_u64(const struct device_node *np,
	 				const char *propname, u64 *out_value);
在设备树中,节点大概是这样:
	xxx_node {
	    name1 = <0x50000000>;
	    name2 = <0x50000000  0x60000000>;
	};
调用of_property_read_u32 (np, “name1”, &val)时,val将得到值0x50000000;
调用of_property_read_u64 (np, “name2”, &val)时,val将得到值0x0x6000000050000000。
d. 读某个整数u32/u64
函数原型为:
		extern int of_property_read_u32_index(const struct device_node *np,
						       const char *propname,
						       u32 index, u32 *out_value);
在设备树中,节点大概是这样:
		xxx_node {
		    name2 = <0x50000000  0x60000000>;
		};
调用of_property_read_u32 (np, “name2”, 1, &val)时,val将得到值0x0x60000000。
e. 读数组
函数原型为:
		int of_property_read_variable_u8_array(const struct device_node *np,
							const char *propname, u8 *out_values,
							size_t sz_min, size_t sz_max);
		
		int of_property_read_variable_u16_array(const struct device_node *np,
							const char *propname, u16 *out_values,
							size_t sz_min, size_t sz_max);
		
		int of_property_read_variable_u32_array(const struct device_node *np,
					       const char *propname, u32 *out_values,
					       size_t sz_min, size_t sz_max);
		
		int of_property_read_variable_u64_array(const struct device_node *np,
					       const char *propname, u64 *out_values,
					       size_t sz_min, size_t sz_max);
在设备树中,节点大概是这样:
		xxx_node {
		    name2 = <0x50000012  0x60000034>;
		};
上述例子中属性name2的值,长度为8。
调用of_property_read_variable_u8_array (np, “name2”, out_values, 1, 10)时,out_values中将会保存这8个字节: 0x12,0x00,0x00,0x50,0x34,0x00,0x00,0x60。
调用of_property_read_variable_u16_array (np, “name2”, out_values, 1, 10)时,out_values中将会保存这4个16位数值: 0x0012, 0x5000,0x0034,0x6000。
总之,这些函数要么能取到全部的数值,要么一个数值都取不到;
如果值的长度在sz_min和sz_max之间,就返回全部的数值;否则一个数值都不返回。
f. 读字符串
函数原型为:
		 int of_property_read_string(const struct device_node *np, const char *propname,
		 				const char **out_string);
返回节点np的属性(名为propname)的值,(*out_string)指向这个值,把它当作字符串。

怎么修改设备树文件

一个写得好的驱动程序, 它会尽量确定所用资源。
只把不能确定的资源留给设备树, 让设备树来指定。
根据原理图确定"驱动程序无法确定的硬件资源", 再在设备树文件中填写对应内容。
那么, 所填写内容的格式是什么?

使用芯片厂家提供的工具

有些芯片,厂家提供了对应的设备树生成工具,可以选择某个引脚用于某些功能,就可以自动生成设备树节点。
你再把这些节点复制到内核的设备树文件里即可。

看绑定文档

内核文档 Documentation/devicetree/bindings/
做得好的厂家也会提供设备树的说明文档

参考同类型单板的设备树文件

网上搜索

实在没办法时, 只能去研究驱动源码

LED模板驱动程序的改造:设备树

总结3种写驱动程序的方法

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 107.png
核心永远是file_operations结构体。
上述三种方法,只是指定“硬件资源”的方式不一样。
从上图可以知道,platform_device/platform_driver只是编程的技巧,不涉及驱动的核心。

怎么使用设备树写驱动程序

设备树节点要与platform_driver能匹配

在我们的工作中,驱动要求设备树节点提供什么,我们就得按这要求去编写设备树。
但是,匹配过程所要求的东西是固定的:
① 设备树要有compatible属性,它的值是一个字符串
② platform_driver中要有of_match_table,其中一项的.compatible成员设置为一个字符串
③ 上述2个字符串要一致。
示例如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 108.png

设备树节点指定资源,platform_driver获得资源

如果在设备树节点里使用reg属性,那么内核生成对应的platform_device时会用reg属性来设置IORESOURCE_MEM类型的资源。
如果在设备树节点里使用interrupts属性,那么内核生成对应的platform_device时会用reg属性来设置IORESOURCE_IRQ类型的资源。对于interrupts属性,内核会检查它的有效性,所以不建议在设备树里使用该属性来表示其他资源。
在我们的工作中,驱动要求设备树节点提供什么,我们就得按这要求去编写设备树。驱动程序中根据pin属性来确定引脚,那么我们就在设备树节点中添加pin属性。
设备树节点中:
		#define GROUP_PIN(g,p) ((g<<16) | (p))
		100ask_led0 {
		   compatible = “100ask,led”;
		   pin = <GROUP_PIN(5, 3)>;
		};
驱动程序中,可以从platform_device中得到device_node,再用of_property_read_u32得到属性的值:
			struct  device_node* np = pdev->dev. of_node;
			int led_pin;
			int err = of_property_read_u32(np, “pin”, &led_pin);

开始编程

修改设备树添加led设备节点

在本实验中,需要添加的设备节点代码是一样的,你需要找到你的单板所用的设备树文件,在它的根节点下添加如下内容:
			#define GROUP_PIN(g,p) ((g<<16) | (p))
			100ask_led@0 {
				compatible = "100as,leddrv";
				pin = <GROUP_PIN(3, 1)>;
			};

			100ask_led@1 {
				compatible = "100as,leddrv";
				pin = <GROUP_PIN(5, 8)>;
			};
对于100ask-am335x 单板
设备树文件是:内核源码目录中arch/arm/boot/dts/100ask-am335x.dts
修改、编译后得到arch/arm/boot/dts/100ask-am335x.dtb文件。
要更换板子上的设备树文件,启动板子后,更换这个文件:/boot/mx6ull-14x14-ebf.dtb
对于firefly-rk3288
设备树文件是:内核源码目录中arch/arm/boot/dts/rk3288-firefly.dts
修改、编译后得到arch/arm/boot/dts/rk3288-firefly.dtb文件。
对于这款板子,本教程中我们使用SD卡上的系统。
要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/rk3288-firefly.dtb


对于firefly的roc-rk3399-pc
设备树文件是:内核源码目录中arch/arm64/boot/dts/rk3399-roc-pc.dts
修改、编译后得到arch/arm64/boot/dts/rk3399-roc-pc.dtb文件。
对于这款板子,本教程中我们使用SD卡上的系统。
要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/ rk3399-roc-pc.dtb
对于百问网使用QEMU模拟的IMX6ULL板子
设备树文件是:内核源码目录中arch/arm/boot/dts/100ask_imx6ul_qemu.dts
修改、编译后得到arch/arm/boot/dts/100ask_imx6ul_qemu.dtb文件。
它是执行qemu时直接在命令行中指定设备树文件的,你可以打开脚本文件qemu-imx6ul-gui.sh找到dtb文件的位置,然后使用新编译出来的dtb去覆盖老文件。
对于野火imx6ull-pro
设备树文件是:内核源码目录中arch/arm/boot/dts/imx6ull-14x14-ebf.dts
修改、编译后得到arch/arm/boot/dts/imx6ull-14x14-ebf.dtb文件。
对于这款板子,本教程中我们使用SD卡上的系统。
要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/imx6ull-14x14-ebf.dtb
对于正点原子imx6ull-alpha
设备树文件是:内核源码目录中arch/arm/boot/dts/imx6ull-14x14-alpha.dts
修改、编译后得到arch/arm/boot/dts/imx6ull-14x14-alpha.dtb文件。
对于这款板子,本教程中我们使用SD卡上的系统。
要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/arch/arm/boot/dts/imx6ull-14x14-alpha.dtb

修改platform_driver的源码=

使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\04_快速入门(正式开始)\ 02_嵌入式Linux驱动开发基础知识\source\ 02_led_drv\05_led_drv_template_device_tree

关键代码在chip_demo_gpio.c,主要看里面的platform_driver,代码如下。
第166行指定了of_match_table,它是用来跟设备树节点匹配的,如果设备树节点中有compatile属性,并且其值等于第157行的“100as,leddrv”,就会导致第162行的probe函数被调用。
		156 static const struct of_device_id ask100_leds[] = {
		157     { .compatible = "100as,leddrv" },
		158     { },
		159 };
		160
		161 static struct platform_driver chip_demo_gpio_driver = {
		162     .probe      = chip_demo_gpio_probe,
		163     .remove     = chip_demo_gpio_remove,
		164     .driver     = {
		165         .name   = "100ask_led",
		166         .of_match_table = ask100_leds,
		167     },
		168 };
		169
		170 static int __init chip_demo_gpio_drv_init(void)
		171 {
		172     int err;
		173
		174     err = platform_driver_register(&chip_demo_gpio_driver);
		175     register_led_operations(&board_demo_led_opr);
		176
		177     return 0;
		178 }
		179

上机实验

1.使用新的设备树dtb文件启动单板,查看/sys/firmware/devicetree/base下有无节点
2. 查看/sys/devices/platform目录下有无对应的platform_device
3. 加载驱动:
		# insmod  leddrv.ko
		# insmod  chip_demo_gpio.ko
4. 测试驱动:
		# ./ledtest   /dev/100ask_led0  on
		# ./ledtest   /dev/100ask_led0  off

调试技巧

/sys目录下有很多内核、驱动的信息:

设备树的信息

以下目录对应设备树的根节点,可以从此进去找到自己定义的节点。
		cd /sys/firmware/devicetree/base/
节点是目录,属性是文件。
属性值是字符串时,用cat命令可以打印出来;属性值是数值时,用hexdump命令可以打印出来。

platform_device的信息

以下目录含有注册进内核的所有platform_device:
		/sys/devices/platform
一个设备对应一个目录,进入某个目录后,如果它有“driver”子目录,就表示这个platform_device跟某个platform_driver配对了。
比如下面的结果中,平台设备“ff8a0000.i2s”已经跟平台驱动“rockchip-i2s”配对了:
		/sys/devices/platform/ff8a0000.i2s]# ls driver -ld
		lrwxrwxrwx    1 root     root             0 Jan 18 16:28 driver -> ../../../bus/platform/drivers/rockchip-i2s

platform_driver的信息

以下目录含有注册进内核的所有platform_driver:
		/sys/bus/platform/drivers
一个driver对应一个目录,进入某个目录后,如果它有配对的设备,可以直接看到。
比如下面的结果中,平台驱动“rockchip-i2s”跟2个平台设备“平台设备“ff890000.i2s”、“ff8a0000.i2s”配对了:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 109.png
注意:一个平台设备只能配对一个平台驱动,一个平台驱动可以配对多个平台设备。

课后作业

请仿照本节提供的程序(位于05_led_drv_template_device_tree目录),改造你所用的单板的LED驱动程序。

APP怎么读取按键值

APP读取按键值,需要有按键驱动程序。
为什么要讲按键驱动程序?
APP去读按键的方法有4种:
① 查询方式
② 休眠-唤醒方式
③ poll方式
④ 异步通知方式
通过这4种方式的学习,我们可以掌握如下知识:
① 驱动的基本技能:中断、休眠、唤醒、poll等机制。
这些基本技能是驱动开发的基础,其他大型驱动复杂的地方是它的框架及设计思想,但是基本技术就这些。
② APP开发的基本技能:阻塞 、非阻塞、休眠、poll、异步通知。

妈妈怎么知道孩子醒了

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 110.png
妈妈怎么知道卧室里小孩醒了?
① 时时进房间看一下:查询方式
简单,但是累
② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒
不累,但是妈妈干不了活了
③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式
要浪费点时间,但是可以继续干活。
妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知
妈妈、小孩互不耽误。
这4种方法没有优劣之分,在不同的场合使用不同的方法。

APP读取按键的4种方法

跟上述生活场景类似,APP去读取按键也有4种方法:
① 查询方式
② 休眠-唤醒方式
③ poll方式
④ 异步通知方式
第2、3、4种方法,都涉及中断服务程序。中断,就像小孩醒了会哭闹一样,中断不经意间到来,它会做某些事情:唤醒APP、向APP发信号。
所以,在按键驱动程序中,中断是核心。
实际上,中断无论是在单片机还是在Linux中都很重要。在Linux中,中断的知识还涉及进程、线程等。

查询方式

这种方法最简单:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 111.png
驱动程序中构造、注册一个file_operations结构体,里面提供有对应的open,read函数。APP调用open时,导致驱动中对应的open函数被调用,在里面配置GPIO为输入引脚。APP调用read时,导致驱动中对应的read函数被调用,它读取寄存器,把引脚状态直接返回给APP。

休眠-唤醒方式

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 112.png
驱动程序中构造、注册一个file_operations结构体,里面提供有对应的open,read函数。
APP调用open时,导致驱动中对应的open函数被调用,在里面配置GPIO为输入引脚;并且注册GPIO的中断处理函数。
APP调用read时,导致驱动中对应的read函数被调用,如果有按键数据则直接返回给APP;否则APP在内核态休眠。
当用户按下按键时,GPIO中断被触发,导致驱动程序之前注册的中断服务程序被执行。它会记录按键数据,并唤醒休眠中的APP。
APP被唤醒后继续在内核态运行,即继续执行驱动代码,把按键数据返回给APP(的用户空间)。


poll方式

上面的休眠-唤醒方式有个缺点:如果用户一直没操作按键,那么APP就会永远休眠。
我们可以给APP定个闹钟,这就是poll方式。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 113.png
驱动程序中构造、注册一个file_operations结构体,里面提供有对应的open,read,poll函数。
APP调用open时,导致驱动中对应的open函数被调用,在里面配置GPIO为输入引脚;并且注册GPIO的中断处理函数。
APP调用poll或select函数,意图是“查询”是否有数据,这2个函数都可以指定一个超时时间,即在这段时间内没有数据的话就返回错误。这会导致驱动中对应的poll函数被调用,如果有按键数据则直接返回给APP;否则APP在内核态休眠一段时间。
当用户按下按键时,GPIO中断被触发,导致驱动程序之前注册的中断服务程序被执行。它会记录按键数据,并唤醒休眠中的APP。
如果用户没按下按键,但是超时时间到了,内核也会唤醒APP。
所以APP被唤醒有2种原因:用户操作了按键,超时。被唤醒的APP在内核态继续运行,即继续执行驱动代码,把“状态”返回给APP(的用户空间)。
APP得到poll/select函数的返回结果后,如果确认是有数据的,则再调用read函数,这会导致驱动中的read函数被调用,这时驱动程序中含有数据,会直接返回数据。

异步通知方式

异步通知的原理:发信号
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 114.png
异步通知的实现原理是:内核给APP发信号。信号有很多种,这里发的是SIGIO。
驱动程序中构造、注册一个file_operations结构体,里面提供有对应的open,read,fasync函数。
APP调用open时,导致驱动中对应的open函数被调用,在里面配置GPIO为输入引脚;并且注册GPIO的中断处理函数。
APP给信号SIGIO注册自己的处理函数:my_signal_fun。
APP调用fcntl函数,把驱动程序的flag改为FASYNC,这会导致驱动程序的fasync函数被调用,它只是简单记录进程PID。
当用户按下按键时,GPIO中断被触发,导致驱动程序之前注册的中断服务程序被执行。它会记录按键数据,然后给进程PID发送SIGIO信号。
APP收到信号后会被打断,先执行信号处理函数:在信号处理函数中可以去调用read函数读取按键值。
信号处理函数返回后,APP会继续执行原先被打断的代码。


应用程序之间发信号示例代码
使用GIT下载所有源码后,本节源码位于如下目录:
	01_all_series_quickstart\04_快速入门(正式开始)\
		02_嵌入式Linux驱动开发基础知识\source\03_signal_example
代码并不复杂,如下。
第13行注册信号处理函数,第15行就是一个无限循环。在它运行期间,你可以用另一个APP发信号给它。
	01 #include <stdio.h>
	02 #include <unistd.h>
	03 #include <signal.h>
	04 void my_sig_func(int signo)
	05 {
	06     printf("get a signal : %d\n", signo);
	07 }
	08
	09 int main(int argc, char **argv)
	10 {
	11     int i = 0;
	12
	13     signal(SIGIO, my_sig_func);
	14
	15     while (1)
	16     {
	17         printf("Hello, world %d!\n", i++);
	18         sleep(2);
	19     }
	20
	21     return 0;
	22 }
在Ubuntu上的测试方法:
		$ gcc   -o   signal  signal.c   // 编译程序
		$ ./signal &                   // 后台运行
		$ ps  -A | grep signal          // 查看进程ID,假设是9527
		$ kill  -SIGIO  9527           // 给这个进程发信号

驱动程序提供能力,不提供策略

我们的驱动程序可以实现上述4种提供按键的方法,但是驱动程序不应该限制APP使用哪种方法。
这就是驱动设计的一个原理:提供能力,不提供策略。就是说,你想用哪种方法都行,驱动程序都可以提供;但是驱动程序不能限制你使用哪种方法。


查询方式的按键驱动程序_编写框架

LED驱动回顾

对于LED,APP调用open函数导致驱动程序的led_open函数被调用。在里面,把GPIO配置为输出引脚。安装驱动程序后并不意味着会使用对应的硬件,而APP要使用对应的硬件,必须先调用open函数。所以建议在驱动程序的open函数中去设置引脚。
APP继续调用write函数传入数值,在驱动程序的led_write函数根据该数值去设置GPIO的数据寄存器,从而控制GPIO的输出电平。
怎么操作寄存器?从芯片手册得到对应寄存器的物理地址,在驱动程序中使用ioremap函数映射得到虚拟地址。驱动程序中使用虚拟地址去访问寄存器。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 045.png

按键驱动编写思路

GPIO按键的原理图一般有如下2种:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 115.png
按键没被按下时,上图中左边的GPIO电平为高,右边的GPIO电平为低。
按键被按下后,上图中左边的GPIO电平为低,右边的GPIO电平为高。
编写按键驱动程序最简单的方法如下图所示:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 116.png
回顾一下编写驱动程序的套路:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 040.png
对于使用查询方式的按键驱动程序,我们只需要实现button_open、button_read。

编程:先写框架

我们的目的写出一个容易扩展到各种芯片、各种板子的按键驱动程序,所以驱动程序分为上下两层:
① button_drv.c分配/设置/注册file_operations结构体
起承上启下的作用,向上提供button_open,button_read供APP调用。
而这2个函数又会调用底层硬件提供的p_button_opr中的init、read函数操作硬件。
② board_xxx.c分配/设置/注册button_operations结构体
这个结构体是我们自己抽象出来的,里面定义单板xxx的按键操作函数。
这样的结构易于扩展,对于不同的单板,只需要替换board_xxx.c提供自己的button_operations结构体即可。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 117.png
使用GIT下载所有源码后,本节源码位于如下目录:
	01_all_series_quickstart\04_快速入门(正式开始)\
		02_嵌入式Linux驱动开发基础知识\source\
			04_button_drv\01_button_drv_template

把按键的操作抽象出一个button_operations结构体

首先看看button_drv.h,它定义了一个button_operations结构体,把按键的操作抽象为这个结构体:
		04 struct button_operations {
		05     int count;
		06     void (*init) (int which);
		07     int (*read) (int which);
		08 };
		09
		10 void register_button_operations(struct button_operations *opr);
		11 void unregister_button_operations(void);
		12
再看看board_xxx.c,它实现了一个button_operations结构体,代码如下。
第 45 行调用register_button_operations函数,把这个结构体注册到上层驱动中。
		37 static struct button_operations my_buttons_ops ={
		38     .count = 2,
		39     .init  = board_xxx_button_init_gpio,
		40     .read  = board_xxx_button_read_gpio,
		41 };
		42
		43 int board_xxx_button_init(void)
		44 {
		45     register_button_operations(&my_buttons_ops);
		46     return 0;
		47 }
		48

驱动程序的上层:file_operations结构体

上层是button_drv.c,它的核心是file_operations结构体,首先看看入口函数,代码如下。
第 83 行向内核注册一个file_operations结构体。
第 85 行创建一个class,但是该class下还没有device,在后面获得底层硬件的信息时再在class下创建device:这只是用来创建设备节点,它不是驱动程序的核心。
		81 int button_init(void)
		82 {
		83     major = register_chrdev(0, "100ask_button", &button_fops);
		84
		85     button_class = class_create(THIS_MODULE, "100ask_button");
		86     if (IS_ERR(button_class))
		87         return -1;
		88
		89     return 0;
		90 }
		91
再来看看button_drv.c中file_operations结构体的成员函数,代码如下。
第 34 、 44 行都用到一个button_operations指针,它是从何而来?
		28 static struct button_operations *p_button_opr;
		29 static struct class *button_class;
		30
		31 static int button_open (struct inode *inode, struct file *file)
		32 {
		33     int minor = iminor(inode);
		34     p_button_opr->init(minor);
		35     return 0;
		36 }
		37
		38 static ssize_t button_read (struct file *file, char __user *buf, size_t size, loff_t *off)
		39 {
		40     unsigned int minor = iminor(file_inode(file));
		41     char level;
		42     int err;
		43
		44     level = p_button_opr->read(minor);
		45     err = copy_to_user(buf, &level, 1);
		46     return 1;
		47 }
		48
		49
		50 static struct file_operations button_fops = {
		51     .open = button_open,
		52     .read = button_read,
		53 };
上面第34、44行都用到一个button_operations指针,来自于底层硬件相关的代码。
底层代码调用register_button_operations函数,向上提供这个结构体指针。
register_button_operations函数代码如下,它还根据底层提供的button_operations调用device_create,这是创建设备节点(第62行)。
		55 void register_button_operations(struct button_operations *opr)
		56 {
		57     int i;
		58
		59     p_button_opr = opr;
		60     for (i = 0; i < opr->count; i++)
		61     {
		62         device_create(button_class, NULL, MKDEV(major, i), NULL, "100ask_button%d", i);
		63     }
		64 }
		65

测试

这只是一个示例程序,还没有真正操作硬件。测试程序操作驱动程序时,只会导致驱动程序中打印信息。
首先设置交叉工具链,修改驱动Makefile中内核的源码路径,编译驱动和测试程序。
启动开发板后,通过NFS访问编译好驱动程序、测试程序,就可以在开发板上如下操作了:
		# insmod button_drv.ko   // 装载驱动程序
		[  435.276713] button_drv: loading out-of-tree module taints kernel.
		# insmod board_xxx.ko
		# ls /dev/100ask_button* -l     // 查看设备节点
		crw-------    1 root     root      236,   0 Jan 18 08:57 /dev/100ask_button0
		crw-------    1 root     root      236,   1 Jan 18 08:57 /dev/100ask_button1
		# ./button_test /dev/100ask_button0    // 读按键
		[  450.886180] /home/book/source/04_button_drv/01_button_drv_template/board_xxx.c board_xxx_button_init_gpio 28, init gpio for button 0
		[  450.910915] /home/book/source/04_button_drv/01_button_drv_template/board_xxx.c board_xxx_button_read_gpio 33, read gpio for button 0
		get button : 1    // 得到数据

课后怎业

合并LED、BUTTON框架驱动程序:01_led_drv_template、01_button_drv_template,合并为:gpio_drv_template

具体单板的按键驱动程序(查询方式)

GPIO操作回顾

参考第4章《普适的GPIO引脚操作方法》、第5章《具体单板的GPIO操作方法》。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 118.png

AM335X的按键驱动程序(查询方式)

先看原理图确定引脚及操作方法

AM335X是底板+核心板的结构,打开底板原理图100ask_am335x_v12_原理图.pdf,它有4个按键,本视频只操作一个按键,原理图如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 119.png
平时按键电平为高,按下按键后电平为低。
按键引脚为GPIO1_25。

再看芯片手册确定寄存器及操作方法

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 120.png
步骤1:使能GPIO1模块
设置CM_PER_GPIO1_CLKCTRL寄存器的bit[18]为1,bit[1:0]为0x2,该寄存器地址为0x44E00000+0xAC。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 121.png
步骤2:把GPIO1_25对应的引脚设置为GPIO模式
要用哪一个寄存器来把GPIO1_25对应的引脚设置为GPIO模式?
① 在核心板原理图ET-som335X原理图.pdf里搜“GPIO1_25”,可以看到下图,确定pin number为U16:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 122.png
② 在芯片手册AM335x Sitara™ Processors.pdf里搜“U16”,可得下图,引脚名为GPMC_A9,用作GPIO时要设置为mode 7:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 123.png
③ 在芯片手册AM335x_datasheet_spruh73p.pdf中搜gpmc_a9,
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 124.png
所以,要把GPIO1_25对应的引脚设置为GPIO模式,要设置conf_gpmc_a9寄存器的bit[5]为1,bit[2:0]为7,这个寄存器的地址是 0x44E10000+0x864。
步骤3:设置GPIO1内部寄存器,把GPIO1_25设置为输入引脚,读数据寄存器
GPIO_OE寄存器:地址为0x4804C000+0x134,bit[25]设置为1。
GPIO_DATAIN寄存器:地址为0x4804C000+0x138,读其bit[25]。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 125.png
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 126.png

编程

程序框架
使用GIT下载所有源码后,本节源码位于如下目录:
		01_all_series_quickstart\04_快速入门(正式开始)\
			02_嵌入式Linux驱动开发基础知识\source\
				04_button_drv\02_button_drv_for_boards\01_button_drv_for_am335x
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 127.png
硬件相关的代码
主要看board_am335x.c,先看它的入口函数,代码如下。
第84行向上层驱动注册一个button_operations结构体,该结构体在第76~80行定义。
		76 static struct button_operations my_buttons_ops = {
		77     .count = 1,
		78     .init = board_am335x_button_init,
		79     .read = board_am335x_button_read,
		80 };
		81
		82 int board_am335x_button_drv_init(void)
		83 {
		84     register_button_operations(&my_buttons_ops);
		85     return 0;
		86 }
		87
button_operations结构体中有init函数指针,它指向board_am335x_button_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。代码如下。
值得关注的是第32~35行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器。
		21 static volatile unsigned int *CM_PER_GPIO1_CLKCTRL;
		22 static volatile unsigned int *conf_gpmc_a9;
		23 static volatile unsigned int *GPIO1_OE;
		24 static volatile unsigned int *GPIO1_DATAIN;
		25
		26 static void board_am335x_button_init (int which) /* 初始化button, which-哪个button */
		27 {
		28     if (which == 0)
		29     {
		30         if (!CM_PER_GPIO1_CLKCTRL)
		31         {
		32             CM_PER_GPIO1_CLKCTRL = ioremap(0x44E00000 + 0xAC, 4);
		33             conf_gpmc_a9 = ioremap(0x44E10000 + 0x864, 4);
		34             GPIO1_OE = ioremap(0x4804C000 + 0x134, 4);
		35             GPIO1_DATAIN = ioremap(0x4804C000 + 0x138, 4);
		36         }
		37
		38         //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
		39         /* a. 使能GPIO1
		40          * set PRCM to enalbe GPIO1
		41          * set CM_PER_GPIO1_CLKCTRL (0x44E00000 + 0xAC)
		42          * val: (1<<18) | 0x2
		43          */
		44         *CM_PER_GPIO1_CLKCTRL = (1<<18) | 0x2;
		45
		46         /* b. 设置GPIO1_25的功能,让它工作于GPIO模式
		47          * set Control Module to set GPIO1_25 (U16) used as GPIO
		48          * conf_gpmc_a9 as mode 7
		49          * addr : 0x44E10000 + 0x864
		50          * bit[5]   : 1, Input enable value for the PAD
		51          * bit[2:0] : mode 7
		52          */
		53         *conf_gpmc_a9 = (1<<5) | 7;
		54
		55         /* c. 设置GPIO1_25的方向,让它作为输入引脚
		56          * set GPIO1's registers , to set 设置GPIO1_25的方向'S dir (input)
		57          * GPIO_OE
		58          * addr : 0x4804C000 + 0x134
		59          * set bit 25
		60          */
		61
		62         *GPIO1_OE |= (1<<25);
		63     }
		64
		65 }
		66
button_operations结构体中还有有read函数指针,它指向board_am335x_button_read函数,在里面将会读取并返回按键引脚的电平。代码如下。
		67 static int board_am335x_button_read (int which) /* 读button, which-哪个 */
		68 {
		69     printk("%s %s line %d, button %d, 0x%x\n", __FILE__, __FUNCTION__, __LINE__, which, *GPIO1_DATAIN);
		70     if (which == 0)
		71         return (*GPIO1_DATAIN & (1<<25)) ? 1 : 0;
		72     else
		73         return 0;
		74 }
		75

测试

安装驱动程序之后执行测试程序,观察它的返回值(执行测试程序的同时操作按键):
		# insmod button_drv.ko
		# insmod board_am335x.ko
		# ./button_test /dev/100ask_button0

课后作业

① 修改board_am335x.c,增加更多按键
② 修改button_test.c,使用按键来点灯

RK3288的按键驱动程序(查询方式)

先看原理图确定引脚及操作方法

Firefly的RK3288开发板上没有按键,我们为它制作的扩展板上有1个按键。在扩展板原理图rk3288_extend_v12_0715.pdf中可以看到按键,如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 128.png
平时按键电平为高,按下按键后电平为低。
按键引脚为GPIO7_B1。


再看芯片手册确定寄存器及操作方法

芯片手册为Rockchip_RK3288_TRM_V1.2_Part1-20170321.pdf,不过我们总结如下。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 129.png
步骤1:使能GPIO7模块
设置CRU_CLKGATE14_CON寄存器的bit[7]为0。
要设置bit7,必须同时设置bit23为1。
该寄存器地址为0xFF760000+0x198。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 130.png
步骤2:把GPIO7_B1对应的引脚设置为GPIO模式
设置GRF_GPIO7B_IOMUX寄存器的bit[3:2]为0b00。
要设置bit[3:2],必须同时设置bit[19:18]为0b11。
该寄存器地址为0xFF770000+0x0070。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 131.png
步骤3:设置GPIO7内部寄存器,把GPIO7_B1设置为输入引脚,读数据寄存器
GPIO_SWPORTA_DDR方向寄存器:地址为0xFF7E0000+ 0x0004,bit[9]设置为0。
GPIO_EXT_PORTA外部端口寄存器:地址为0xFF7E0000+ 0x0050,读其bit[9]。
注意:
GPIO_A0~A7 对应bit0~bit7;GPIO_B0~B7 对应bit8~bit15;
GPIO_C0~C7 对应bit16~bit23;GPIO_D0~D7 对应bit24~bit31
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 132.png

编程

程序框架
使用GIT下载所有源码后,本节源码位于如下目录:
		01_all_series_quickstart\04_快速入门(正式开始)\
			02_嵌入式Linux驱动开发基础知识\source\
				04_button_drv\02_button_drv_for_boards\02_button_drv_for_rk3288
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 133.png
硬件相关的代码
主要看board_rk3288.c,先看它的入口函数,代码如下。
第 81 行向上层驱动注册一个button_operations结构体,该结构体在第73~77行定义。
		73 static struct button_operations my_buttons_ops = {
		74     .count = 1,
		75     .init = board_rk3288_button_init,
		76     .read = board_rk3288_button_read,
		77 };
		78
		79 int board_rk3288_button_drv_init(void)
		80 {
		81     register_button_operations(&my_buttons_ops);
		82     return 0;
		83 }
button_operations结构体中有init函数指针,它指向board_rk3288_button_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。代码如下。
值得关注的是第32~35行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器。
		21 static volatile unsigned int *CRU_CLKGATE14_CON;
		22 static volatile unsigned int *GRF_GPIO7B_IOMUX ;
		23 static volatile unsigned int *GPIO7_SWPORTA_DDR;
		24 static volatile unsigned int *GPIO7_EXT_PORTA ;
		25
		26 static void board_rk3288_button_init (int which) /* 初始化button, which-哪个button */
		27 {
		28     if (which == 0)
		29     {
		30         if (!CRU_CLKGATE14_CON)
		31         {
		32             CRU_CLKGATE14_CON = ioremap(0xFF760000 + 0x0198, 4);
		33             GRF_GPIO7B_IOMUX  = ioremap(0xFF770000 + 0x0070, 4);
		34             GPIO7_SWPORTA_DDR = ioremap(0xFF7E0000 + 0x0004, 4);
		35             GPIO7_EXT_PORTA   = ioremap(0xFF7E0000 + 0x0050, 4);
		36         }
		37
		38         /* rk3288 GPIO7_B1 */
		39         /* a. 使能GPIO7
		40          * set CRU to enable GPIO7
		41          * CRU_CLKGATE14_CON 0xFF760000 + 0x198
		42          * (1<<(7+16)) | (0<<7)
		43          */
		44         *CRU_CLKGATE14_CON = (1<<(7+16)) | (0<<7);
		45
		46         /* b. 设置GPIO7_B1用于GPIO
		47          * set PMU/GRF to configure GPIO7_B1 as GPIO
		48          * GRF_GPIO7B_IOMUX 0xFF770000 + 0x0070
		49          * bit[3:2] = 0b00
		50          * (3<<(2+16)) | (0<<2)
		51          */
		52         *GRF_GPIO7B_IOMUX =(3<<(2+16)) | (0<<2);
		53
		54         /* c. 设置GPIO7_B1作为input引脚
		55          * set GPIO_SWPORTA_DDR to configure GPIO7_B1 as input
		56          * GPIO_SWPORTA_DDR 0xFF7E0000 + 0x0004
		57          * bit[9] = 0b0
		58          */
		59         *GPIO7_SWPORTA_DDR &= ~(1<<9);
		60     }
		61
		62 }
button_operations结构体中还有有read函数指针,它指向board_rk3288_button_read函数,在里面将会读取并返回按键引脚的电平。代码如下。
		64 static int board_rk3288_button_read (int which) /* 读button, which-哪个 */
		65 {
		66     //printk("%s %s line %d, button %d, 0x%x\n", __FILE__, __FUNCTION__, __LINE__, which, *GPIO1_DATAIN);
		67     if (which == 0)
		68         return (*GPIO7_EXT_PORTA & (1<<9)) ? 1 : 0;
		69     else
		70         return 0;
		71 }

测试

安装驱动程序之后执行测试程序,观察它的返回值(执行测试程序的同时操作按键):
		# insmod button_drv.ko
		# insmod board_rk3288.ko
		# ./button_test /dev/100ask_button0

课后作业

① 修改button_test.c,使用按键来点灯


RK3399的按键驱动程序(查询方式)

先看原理图确定引脚及操作方法

Firefly的RK3399开发板上没有按键,我们为它制作的扩展板上有3个按键。在扩展板原理图rk3399_extend_v12_0709final.pdf中可以看到按键,如下:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 134.png
平时按键电平为高,按下按键后电平为低。
按键引脚为GPIO0_B1、GPIO0_B2、GPIO0_B4。
本视频中,只操作一个按键:GPIO0_B1。


再看芯片手册确定寄存器及操作方法

芯片手册为Rockchip RK3399TRM V1.3 Part1.pdf和Rockchip RK3399TRM V1.3 Part2.pdf,不过我们总结如下。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 135.png
步骤1:使能GPIO0模块
设置PMUCRU_CLKGATE_CON1寄存器的bit[3]为0。
要设置bit3,必须同时设置bit19为1。
该寄存器地址为0xFF760000+ 0x0104。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 136.png
步骤2:把GPIO0_B1对应的引脚设置为GPIO模式
设置PMUGRF_GPIO0B_IOMUX寄存器的bit[3:2]为0b00。
要设置bit[3:2],必须同时设置bit[19:18]为0b11。
该寄存器地址为0xFF310000+0x0004。
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 137.png
步骤3:设置GPIO0内部寄存器,把GPIO0_B1设置为输入引脚,读数据寄存器
这些寄存器的介绍在芯片手册Rockchip RK3399TRM V1.3 Part2.pdf中。
GPIO_SWPORTA_DDR方向寄存器:地址为0xFF720000+ 0x0004,bit[9]设置为0。
GPIO_EXT_PORTA外部端口寄存器:地址为0xFF720000+ 0x0050,读其bit[9]。
注意:
GPIO_A0~A7 对应bit0~bit7;GPIO_B0~B7 对应bit8~bit15;
GPIO_C0~C7 对应bit16~bit23;GPIO_D0~D7 对应bit24~bit31
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 138.png

编程

程序框架
使用GIT下载所有源码后,本节源码位于如下目录:
		01_all_series_quickstart\04_快速入门(正式开始)\
			02_嵌入式Linux驱动开发基础知识\source\
				04_button_drv\02_button_drv_for_boards\03_button_drv_for_rk3399
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 139.png
硬件相关的代码
主要看board_rk3399.c,先看它的入口函数,代码如下。
第81行向上层驱动注册一个button_operations结构体,该结构体在第73~77行定义。
		73 static struct button_operations my_buttons_ops = {
		74     .count = 1,
		75     .init = board_rk3399_button_init,
		76     .read = board_rk3399_button_read,
		77 };
		78
		79 int board_rk3399_button_drv_init(void)
		80 {
		81     register_button_operations(&my_buttons_ops);
		82     return 0;
		83 }
button_operations结构体中有init函数指针,它指向board_rk3399_button_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。代码如下。
值得关注的是第32~35行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器。
		21 static volatile unsigned int *PMUCRU_CLKGATE_CON1;
		22 static volatile unsigned int *GRF_GPIO0B_IOMUX ;
		23 static volatile unsigned int *GPIO0_SWPORTA_DDR;
		24 static volatile unsigned int *GPIO0_EXT_PORTA ;
		25
		26 static void board_rk3399_button_init (int which) /* 初始化button, which-哪个button */
		27 {
		28     if (which == 0)
		29     {
		30         if (!PMUCRU_CLKGATE_CON1)
		31         {
		32             PMUCRU_CLKGATE_CON1 = ioremap(0xFF760000+ 0x0104, 4);
		33             GRF_GPIO0B_IOMUX  = ioremap(0xFF310000+0x0004, 4);
		34             GPIO0_SWPORTA_DDR = ioremap(0xFF720000 + 0x0004, 4);
		35             GPIO0_EXT_PORTA   = ioremap(0xFF720000 + 0x0050, 4);
		36         }
		37
		38         /* rk3399 GPIO0_B1 */
		39         /* a. 使能GPIO0
		40          * set CRU to enable GPIO0
		41          * PMUCRU_CLKGATE_CON1 0xFF760000+ 0x0104
		42          * (1<<(3+16)) | (0<<3)
		43          */
		44         *PMUCRU_CLKGATE_CON1 = (1<<(3+16)) | (0<<3);
		45
		46         /* b. 设置GPIO0_B1用于GPIO
		47          * set PMU/GRF to configure GPIO0_B1 as GPIO
		48          * GRF_GPIO0B_IOMUX 0xFF310000+0x0004
		49          * bit[3:2] = 0b00
		50          * (3<<(2+16)) | (0<<2)
		51          */
		52         *GRF_GPIO0B_IOMUX =(3<<(2+16)) | (0<<2);
		53
		54         /* c. 设置GPIO0_B1作为input引脚
		55          * set GPIO_SWPORTA_DDR to configure GPIO0_B1 as input
		56          * GPIO_SWPORTA_DDR 0xFF720000 + 0x0004
		57          * bit[9] = 0b0
		58          */
		59         *GPIO0_SWPORTA_DDR &= ~(1<<9);
		60     }
		61
		62 }
button_operations结构体中还有有read函数指针,它指向board_rk3399_button_read函数,在里面将会读取并返回按键引脚的电平。代码如下。
		64 static int board_rk3399_button_read (int which) /* 读button, which-哪个 */
		65 {
		66    //printk("%s %s line %d, button %d, 0x%x\n", __FILE__, __FUNCTION__, __LINE__, which, *GPIO1_DATAIN);
		67     if (which == 0)
		68         return (*GPIO0_EXT_PORTA & (1<<9)) ? 1 : 0;
		69     else
		70         return 0;
		71 }

测试

安装驱动程序之后执行测试程序,观察它的返回值(执行测试程序的同时操作按键):
		# insmod button_drv.ko
		# insmod board_rk3399.ko
		# ./button_test /dev/100ask_button0

课后作业

① 修改board_rk3399.c,增加更多按键
② 修改button_test.c,使用按键来点灯

百问网IMX6ULL-QEMU的按键驱动程序(查询方式)

使用QEMU模拟的硬件,它的硬件资源可以随意扩展。
在IMX6ULL QEMU 虚拟开发板上,我们为它设计了2个 按键。在QEMU的GUI上有4个按键,右边的2个留待以后用于电源管理。

先看原理图确定引脚及操作方法

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 140.png
平时按键电平为低,按下按键后电平为高。
按键引脚为GPIO5_IO01、GPIO1_IO18。

再看芯片手册确定寄存器及操作方法

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 023.png
步骤1:使能GPIO1、GPIO5
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 078.png
设置b[31:30]、b[27:26]就可以使能GPIO5、GPIO1,设置为什么值呢?
看下图,设置为0b11:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 141.png
① 00:该GPIO模块全程被关闭
② 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭
③ 10:保留
④ 11:该GPIO模块全程使能
步骤2:设置GPIO5_IO01、GPIO1_IO18为GPIO模式
① 对于GPIO5_IO01,设置如下寄存器:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 142.png
② 对于GPIO1_IO18,设置如下寄存器:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 001.png
步骤3:设置GPIO5_IO01、GPIO1_IO18为输入引脚,读取引脚电平
寄存器地址为:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 072.png
设置方向寄存器,把引脚设置为输出引脚:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 073.png
读取引脚状态寄存器,得到引脚电平:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 143.png

编程

程序框架
使用GIT下载所有源码后,本节源码位于如下目录:
	 01_all_series_quickstart\04_快速入门(正式开始)\
	 		02_嵌入式Linux驱动开发基础知识\source\
	 			04_button_drv\02_button_drv_for_boards\04_button_drv_for_100ask_imx6ull-qemu
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 144.png
硬件相关的代码
主要看board_100ask_imx6ull-qemu.c。
涉及的寄存器挺多,一个一个去执行ioremap效率太低。
先定义结构体,然后对结构体指针进行ioremap。
对于IOMUXC,可以如下定义:
		struct iomux {
			volatile unsigned int unnames[23];
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO01;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO02;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO07;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO08;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO09;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA;
			volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B;
		};
struct iomux *iomux = ioremap(0x20e0000, sizeof(struct iomux));
对于GPIO,可以如下定义:
		struct imx6ull_gpio {
			volatile unsigned int dr;
			volatile unsigned int gdir;
			volatile unsigned int psr;
			volatile unsigned int icr1;
			volatile unsigned int icr2;
			volatile unsigned int imr;
			volatile unsigned int isr;
			volatile unsigned int edge_sel;
		};
		struct imx6ull_gpio *gpio1 = ioremap(0x209C000,  sizeof(struct imx6ull_gpio));
		struct imx6ull_gpio *gpio5 = ioremap(0x20AC000,  sizeof(struct imx6ull_gpio));
看一个驱动程序,先看它的入口函数,代码如下。
第127行向上层驱动注册一个button_operations结构体,该结构体在第119~123行定义。
		119 static struct button_operations my_buttons_ops = {
		120     .count = 2,
		121     .init = board_imx6ull_button_init,
		122     .read = board_imx6ull_button_read,
		123 };
		124
		125 int board_imx6ull_button_drv_init(void)
		126 {
		127     register_button_operations(&my_buttons_ops);
		128     return 0;
		129 }
button_operations结构体中有init函数指针,它指向board_imx6ull_button_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。代码如下。
值得关注的是第65~70行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器。
		50 /* enable GPIO1,GPIO5 */
		51 static volatile unsigned int *CCM_CCGR1;
		52
		53 /* set GPIO5_IO03 as GPIO */
		54 static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1;
		55
		56 static struct iomux *iomux;
		57
		58 static struct imx6ull_gpio *gpio1;
		59 static struct imx6ull_gpio *gpio5;
		60
		61 static void board_imx6ull_button_init (int which) /* 初始化button, which-哪个button */
		62 {
		63     if (!CCM_CCGR1)
		64     {
		65         CCM_CCGR1 = ioremap(0x20C406C, 4);
		66         IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = ioremap(0x229000C, 4);
		67
		68         iomux = ioremap(0x20e0000, sizeof(struct iomux));
		69         gpio1 = ioremap(0x209C000, sizeof(struct imx6ull_gpio));
		70         gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
		71     }
		72
		73     if (which == 0)
		74     {
		75         /* 1. enable GPIO5
		76          * CG15, b[31:30] = 0b11
		77          */
		78         *CCM_CCGR1 |= (3<<30);
		79
		80         /* 2. set GPIO5_IO01 as GPIO
		81          * MUX_MODE, b[3:0] = 0b101
		82          */
		83         *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = 5;
		84
		85         /* 3. set GPIO5_IO01 as input
		86          * GPIO5 GDIR, b[1] = 0b0
		87          */
		88         gpio5->gdir &= ~(1<<1);
		89     }
		90     else if(which == 1)
		91     {
		92         /* 1. enable GPIO1
		93          * CG13, b[27:26] = 0b11
		94          */
		95         *CCM_CCGR1 |= (3<<26);
		96
		97         /* 2. set GPIO1_IO18 as GPIO
		98          * MUX_MODE, b[3:0] = 0b101
		99          */
		100         iomux->IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B = 5;
		101
		102         /* 3. set GPIO1_IO18 as input
		103          * GPIO1 GDIR, b[18] = 0b0
		104          */
		105         gpio1->gdir &= ~(1<<18);
		106     }
		107
		108 }
button_operations结构体中还有有read函数指针,它指向board_imx6ull_button_read函数,在里面将会读取并返回按键引脚的电平。代码如下。
		110 static int board_imx6ull_button_read (int which) /* 读button, which-哪个 */
		111 {
		112     //printk("%s %s line %d, button %d, 0x%x\n", __FILE__, __FUNCTION__, __LINE__, which, *GPIO1_DATAIN);
		113     if (which == 0)
		114         return (gpio5->psr & (1<<1)) ? 1 : 0;
		115     else
		116         return (gpio1->psr & (1<<18)) ? 1 : 0;
		117 }

测试

先启动IMX6ULL QEMU模拟器,挂载NFS文件系统。
运行QEMU时,
QEMU内部为主机虚拟出一个网卡, IP为 10.0.2.2,
IMX6ULL有一个网卡, IP为 10.0.2.15,
它连接到主机的虚拟网卡。
这样IMX6ULL就可以通过10.0.2.2去访问Ubuntu了。
安装驱动程序之后执行测试程序,观察它的返回值(执行测试程序的同时操作按键):
	# insmod button_drv.ko
	# insmod board_drv.ko
	# insmod board_100ask_imx6ull-qemu.ko
	# ./button_test  /dev/100ask_button0
	# ./button_test  /dev/100ask_button1

课后作业

① 修改button_test.c,使用按键来点灯


异常与中断的概念及处理流程

中断的引入

妈妈怎么知道孩子醒了

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 110.png
妈妈怎么知道卧室里小孩醒了?
① 时时进房间看一下:查询方式
简单,但是累
② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒
不累,但是妈妈干不了活了
③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式
要浪费点时间,但是可以继续干活。
妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知
妈妈、小孩互不耽误。
后面的3种方式,都需要“小孩来中断妈妈”:中断她的睡眠、中断她的工作。
实际上,能“中断”妈妈的事情可多了:
① 远处的猫叫:这可以被忽略
② 门铃、小孩哭声:妈妈的应对措施不一样
③ 身体不舒服:那要赶紧休息
④ 有蜘蛛掉下来了:赶紧跑啊,救命
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 145.png
妈妈当前正在看书,被“中断”后她会怎么做?流程如下:
① 妈妈正在看书
② 发生了各种声音
可忽略的远处猫叫
快递员按门铃
卧室中小孩哭了
③ 妈妈怎么办?
a. 先在书中放入书签,合上书
b. 去处理
对于不同的情况,处理方法不同:
对于门铃:开门取快递
对于哭声:照顾小孩
c. 回来继续看书

嵌入系统中也有类似的情况

EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 146.png
CPU在运行的过程中,也会被各种“异常”打断。这些“异常”有:
① 指令未定义
② 指令、数据访问有问题
③ SWI(软中断)
④ 快中断
⑤ 中断
中断也属于一种“异常”,导致中断发生的情况有很多,比如:
① 按键
② 定时器
③ ADC转换完成
④ UART发送完数据、收到数据
⑤ 等等
这些众多的“中断源”,汇集到“中断控制器”,由“中断控制器”选择优先级最高的中断并通知CPU。

中断的处理流程

arm对异常(中断)处理过程:
① 初始化:
a. 设置中断源,让它可以产生中断
b. 设置中断控制器(可以屏蔽某个中断,优先级)
c. 设置CPU总开关(使能中断)
② 执行其他程序:正常程序
③ 产生中断:比如按下按键--->中断控制器--->CPU
④ CPU 每执行完一条指令都会检查有无中断/异常产生
⑤ CPU发现有中断/异常产生,开始处理。
对于不同的异常,跳去不同的地址执行程序。
这地址上,只是一条跳转指令,跳去执行某个函数(地址),这个就是异常向量。
③④⑤都是硬件做的。
⑥ 这些函数做什么事情?
软件做的:
a. 保存现场(各种寄存器)
b. 处理异常(中断):
分辨中断源,再调用不同的处理函数
c. 恢复现场

异常向量表

u-boot或是Linux内核,都有类似如下的代码:
		 _start: b	reset
		 	ldr	pc, _undefined_instruction
		 	ldr	pc, _software_interrupt
		 	ldr	pc, _prefetch_abort
		 	ldr	pc, _data_abort
		 	ldr	pc, _not_used
		 	ldr	pc, _irq //发生中断时,CPU跳到这个地址执行该指令 **假设地址为0x18**
		 	ldr	pc, _fiq
这就是异常向量表,每一条指令对应一种异常。
发生复位时,CPU就去 执行第1条指令:b reset。
发生中断时,CPU就去执行“ldr pc, _irq”这条指令。
这些指令存放的位置是固定的,比如对于ARM9芯片中断向量的地址是0x18。
当发生中断时,CPU就强制跳去执行0x18处的代码。
在向量表里,一般都是放置一条跳转指令,发生该异常时,CPU就会执行向量表中的跳转指令,去调用更复杂的函数。
当然,向量表的位置并不总是从0地址开始,很多芯片可以设置某个vector base寄存器,指定向量表在其他位置,比如设置vector base为0x80000000,指定为DDR的某个地址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是0,中断是0x18。

参考资料

对于ARM的中断控制器,述语上称之为GIC (Generic Interrupt Controller),到目前已经更新到v4版本了。
各个版本的差别可以看这里:
https://developer.arm.com/ip-products/system-ip/system-controllers/interrupt-controllers
简单地说,GIC v3/v4用于 ARMv8 架构,即64位ARM芯片。
而GIC v2用于ARMv7和其他更低的架构。
以后在驱动大全里讲解中断时,我们再深入分析,到时会涉及单核、多核等知识。

常见问题

安装驱动时version magic不匹配

要想彻底了解内核的LOCALVERSION信息,可以看这个贴子:
https://blog.csdn.net/gatieme/article/details/78510497
总结一下:
① 开发板所用的内核版本:
在开发板上执行“uname -r”命令,可以得到开发板所用内核的版本,比如:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 147.png
② 在服务器中给开发板编译内核时,这个内核也有一个版本:
进入该内核源码目录,执行“make kernelrelease”命令,可以得到它的版本,比如:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 148.png
③ 编译驱动时,会用到服务器中开发板的内核源码,会带有它的版本信息。
如果①②③的版本信息不匹配,很可能导致驱动程序无法加载,比如:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 149.png
有2个解决方法:
A. 在Ubuntu上重新编译内核,让开发板使用新的内核启动;重新编译驱动,加载新驱动:
这样,①②③三者的内核版本就都一致了。
但是,这种方法有时候不好用,比如开发板上的内核无法更改(出厂固化了),或者你没有开发板上所用内核的全部源码无法编译出内核,这时就可以使用下面的方法。
B. 在Ubuntu上修改版本号,改为开发板上“uname -r”的结果,然后重新编译内核和驱动:
开发板就可以继续使用原来的内核,并且可以加载编译出来的驱动了。
步骤如下:
b.1 修改Ubuntu上开发板内核源码顶层目录Makefile,如下图:
EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive 150.png
b.2 重新编译内核,这会生成一些头文件,供驱动使用
b.3 重新编译驱动

__NOTITLE__