第五篇. 嵌入式Linux驱动开发基础知识¶
嵌入式后Linux驱动开发基础知识的引导与说明¶
打算讲什么、怎么讲?¶
以几个简单的驱动程序,讲解嵌入式Linux驱动的框架,了解驱动开发的流程、方法,掌握从APP到驱动的调用流程。
会涉及很多种开发板,让你明白“Linux驱动 = 软件框架 + 硬件操作”,让你“一通百通”,掌握了普适性的原理之后,在工作中很容易在其他板子使用这些知识。
以LED驱动为例,会如下讲解:
<600px>
需要做什么准备工作¶
驱动程序依赖于Linux内核,你为开发板A开发驱动,那就先在Ubuntu中得到、配置、编译开发板A所使用的Linux内核。
请使用git下载本教程的文档、源码,查看如下目录中你所用开发板的高级用户使用手册(有些开发板的手册我们还没编写完,持续更新):
<600px>
根据手册完成下面操作:
硬件部分:
① 开发板接线:串口线、电源线、网线
② 开发板烧写系统
软件部分:
① 下载Linux内核,Windows和Ubuntu下各放一份
② Windows下:使用Source Insight创建内核源码的工程,这是用来浏览内核、编辑驱动
③ Ubuntu下:安装工具链,配置、编译Linux内核
注意:git的使用方法请参考http://wiki.100ask.net中的“初学者学习路线”:
<400px>
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”与之对应。
<400px>
可以猜测,我们使用open打开文件时,传入的flags、mode等参数会被记录在内核中对应的struct file结构体里(f_flags、f_mode):
int open(const char *pathname, int flags, mode_t mode);
去读写文件时,文件的当前偏移地址也会保存在struct file结构体的f_pos成员里。
<400px>
打开字符设备节点时,内核中也有对应的struct file¶
注意这个结构体中的结构体:struct file_operations *f_op,这是由驱动程序提供的。
<400px>
<600px>
结构体struct file_operations的定义如下:
<600px>
请猜猜怎么编写驱动程序¶
① 确定主设备号,也可以让内核分配
② 定义自己的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. 上机实验
{{redtext|注意:}}我们是在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不匹配,请看本文档最后的“常见问题”。
硬件知识_LED原理图¶
当我们学习C语言的时候,我们会写个Hello程序。
那当我们写ARM程序,也该有一个简单的程序引领我们入门,这个程序就是点亮LED。
我们怎样去点亮一个LED呢?
分为三步:
1.看原理图,确定控制LED的引脚;
2.看主芯片的芯片手册,确定如何设置控制这个引脚;
3.写程序;
先来讲讲怎么看原理图¶
LED样子有很多种,像插脚的,贴片的。
<400px>
它们长得完全不一样,因此我们在原理图中将它抽象出来。
点亮LED需要通电源,同时为了保护LED,加个电阻减小电流。
控制LED灯的亮灭,可以手动开关LED,但在电子系统中,不可能让人来控制开关,通过编程,利用芯片的引脚去控制开关。
<400px>
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。
<600px>
由此,主芯片引脚输出高电平/低电平,即可改变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下载文档后,看下图红框所示目录中各板子对应的文档及图片。
网盘中相同名字的目录下也有对应的视频。

为方便学习,在本文档中也把上述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模块结构¶
<800px>
有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相关寄存器¶
<600px>
set-and-clear协议¶
假设某个GPIO被设置为输出,怎么设置它的输出电平呢?AM335X中对于每个GPIO模块有一个GPIO_DATAOUT寄存器,其中的每一位对应一个引脚,如下:
<600px>
要设置某一位时,不能影响到其他位,操作方法是:读出原来的值,修改某一位,把新值写回去。需要3个步骤才可以设置某一位的值,这效率太低了!
使用set-and-clear可以只用一个步骤即可修改某一位的值。
当想设置某一位为1时,往DATA_SETDATAOUT寄存器中某位写入1即可,芯片内部会把对应引脚的电平设置为1,并且不会影响其他引脚:
<600px>
当想清除某一位为0时,往DATA_CLEARDATAOUT寄存器中某位写入1即可,芯片内部会把对应引脚的电平设置为0,并且不会影响其他引脚:
<600px>
并非所有的芯片都有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模块结构¶
<800px>
有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的内部结构如下图所示:
<800px>
可以设置寄存器使能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相关寄存器¶
<600px>
RK3399的GPIO操作方法¶
GPIO: General-purpose input/output,通用的输入输出口
CRU: Clock & Reset Unit (时钟和复位单元)
PMU: Power Managerment Unit (电源管理单元)
GRF: General Register Files (通用寄存器文件)
RK3399的GPIO模块结构¶
<600px>
有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相关寄存器¶
<600px>
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模块本身,框图如下:
<600px>
CCM用于设置是否向GPIO模块提供时钟¶
参考资料:芯片手册《Chapter 18: Clock Controller Module (CCM)》
GPIOx要用CCM_CCGRy寄存器中的2位来决定该组GPIO是否使能。哪组GPIO用哪个CCM_CCGR寄存器来设置,请看上图红框部分。
CCM_CCGR寄存器中某2位的取值含义如下:
<400px>
① 00:该GPIO模块全程被关闭
② 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭
③ 10:保留
④ 11:该GPIO模块全程使能
GPIO2时钟控制:
<600px>
GPIO1、GPIO5时钟控制:
<600px>
GPIO3时钟控制:
<600px>
GPIO4时钟控制:
<600px>
IOMUXC:引脚的模式(Mode、功能)¶
参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC)》。
<100px>
对于某个/某组引脚,IOMUXC中有2个寄存器用来设置它:
① 选择功能:
IOMUXC_SW_MUXCTLPAD_<PADNAME> :Mux pad xxx,选择某个pad的功能
IOMUXC_SW_MUXCTLGRP_<GROUP NAME>:Mux grp xxx,选择某组引脚的功能
某个引脚,或是某组预设的引脚,都有8个可选的模式(alternate (ALT) MUX_MODE)。
<400px>
比如:
<600px>
② 设置上下拉电阻等参数:
IOMUXC_SW_PADCTLPAD_<PADNAME> : pad pad xxx,设置某个pad的参数
IOMUXC_SW_PADCTLGRP_<GROUP NAME>:pad grp xxx,设置某组引脚的参数
<400px>
比如:
<600px>
GPIO模块内部¶
框图如下:
<600px>
我们暂时只需要关心3个寄存器:
① GPIOx_GDIR:设置引脚方向,每位对应一个引脚,1-output,0-input
<600px>
② GPIOx_GDIR:设置输出引脚的电平,每位对应一个引脚,1-高电平,0-低电平
<600px>
③ GPIOx_PSR:读取引脚的电平,每位对应一个引脚,1-高电平,0-低电平
<600px>
怎么编程¶
读GPIO¶
<600px>
翻译一下:
① 设置CCM_CCGRx寄存器中某位使能对应的GPIO模块 // 默认是使能的,上图省略了
② 设置IOMUX来选择引脚用于GPIO
③ 设置GPIOx_GDIR中某位为0,把该引脚设置为输入功能
④ 读GPIOx_DR或GPIOx_PSR得到某位的值(读GPIOx_DR返回的是GPIOx_PSR的值)
写GPIO¶
<600px>
翻译一下:
① 设置CCM_CCGRx寄存器中某位使能对应的GPIO模块 // 默认是使能的,上图省略了
② 设置IOMUX来选择引脚用于GPIO
③ 设置GPIOx_GDIR中某位为1,把该引脚设置为输出功能
④ 写GPIOx_DR某位的值
需要注意的是,你可以设置该引脚的loopback功能,这样就可以从GPIOx_PSR中读到引脚的有实电平;你从GPIOx_DR中读回的只是上次设置的值,它并不能反应引脚的真实电平,比如可能因为硬件故障导致该引脚跟地短路了,你通过设置GPIOx_DR让它输出高电平并不会起效果。
LED驱动程序框架¶
注意:如果做实验安装驱动时提示version magic不匹配,请看本文档最后的“常见问题”。
回顾字符设备驱动程序框架¶
<600px>
对于LED驱动,我们想要什么样的接口?¶
<600px>
LED驱动要怎么写,才能支持多个板子?分层。¶
1. 把驱动拆分为通用的框架(leddrv.c)、具体的硬件操作(board_X.c):
<600px>
2. 以面向对象的思想,改进代码:
抽象出一个结构体:
<600px>
每个单板相关的board_X.c实现自己的led_operations结构体,供上层的leddrv.c调用:
<600px>
写代码¶
使用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驱动程序¶
我们选用的内核都是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函数的使用:
① 函数原型:
<600px>
使用时,要包含头文件:
<200px>
② 它的作用:
把物理地址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):
<400px>
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原理图如下:
<1200px>
它使用GPIO1_16这个引脚,当它输出低电平时,LED被点亮;当它输出高电平时,LED被熄灭。
所涉及的寄存器操作¶
a. 使能GPIO1
<600px>
/* 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。
<600px>
在芯片手册中搜“conf_gpmc_a0”,可得:
<600px>
/* 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的方向,让它作为输出引脚
<600px>
/* 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:
<600px>
/* 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:
<600px>
/* 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框架驱动程序的测试是一样的。
RK3288和RK3399的LED驱动程序¶
原理图¶
fireflye RK3288的LED原理图¶
RK3288开发板上有2个LED,原理图如下,其中的WORK_LED使用引脚GPIO8_A1:
<800px>
这些LED引脚输出低电平时,LED被点亮;输出高电平时,LED被熄灭。
firefly RK3399的LED原理图¶
RK3399开发板上有3个LED,原理图如下,其中的WORK_LED使用引脚GPIO2_D3:
<1200px>
这些LED引脚输出低电平时,LED被点亮;输出高电平时,LED被熄灭。
所涉及的寄存器操作¶
截图便于对比,后面有文字便于复制:
<800px>
RK3288的GPIO8_A1引脚¶
a. 使能GPIO8
<600px>
设置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
<600px>
设置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引脚

<600px>
  设置GPIO_SWPORTA_DDR 寄存器b[1]为1,把GPIO8_A1设置为输出引脚。
  <font color="# dd0000">注意:</font>
    GPIO_A0~A7 对应bit0~bit7;GPIO_B0~B7 对应bit8~bit15;
    GPIO_C0~C7 对应bit16~bit23;GPIO_D0~D7 对应bit24~bit31
``` C
/* 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输出高电平
<600px>
设置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
<600px>
设置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
<600px>
设置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引脚
<600px>
设置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输出高电平
<600px>
设置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
野火/正点原子IMX6ULL的LED驱动程序¶
野火、正点原子用的内核版本是4.1.15,
我们用的内核版本是 linux 4.9.88,
都是4.x版本,在学习上没有任何差别。
你拿到板子后,可以使用他们出厂的系统,
也可以根据我们提供的高级用户手册更改为我们的系统。
原理图¶
所涉及的寄存器操作¶
GPIO模块图如下:
<600px>
代码中对硬件的操作截图如下,截图便于对比,后面有文字便于复制:
<800px>
野火fire_imx6ull-pro 开发板¶
步骤1:使能GPIO5
<600px>
设置b[31:30]就可以使能GPIO5,设置为什么值呢?
看下图,设置为0b11:
<400px>
① 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模式
设置如下寄存器:
<600px>
/* 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为输出引脚,设置其输出电平
寄存器地址为:
<600px>
设置方向寄存器,把引脚设置为输出引脚:
<600px>
设置数据寄存器,设置引脚的输出电平:
<600px>
/* 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
<600px>
设置b[27:26]就可以使能GPIO1,设置为什么值呢?
看下图,设置为0b11:
<400px>
① 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模式
设置如下寄存器:
<600px>
/* 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为输出引脚,设置其输出电平
寄存器地址为:
<600px>
设置方向寄存器,把引脚设置为输出引脚:
<600px>
设置数据寄存器,设置引脚的输出电平:
<600px>
/* 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
百问网IMX6ULL-QEMU的LED驱动程序¶
使用QEMU模拟的硬件,它的硬件资源可以随意扩展。
在IMX6ULL QEMU 虚拟开发板上,我们为它设计了4个 LED。
看原理图确定引脚及操作方法¶
<400px>
从上图可知,这4个 LED 用到了GPIO5_3、GPIO1_3、GPIO1_5、GPIO1_6 共4个引脚。
在芯片手册里,这些引脚的名字是:GPIO5_IO03、GPIO1_IO03、GPIO1_IO05、GPIO1_IO06。可以根据名字搜到对应的寄存器。
当这些引脚输出低电平时,对应的LED被点亮;输出高电平时,LED熄灭。
所涉及的寄存器操作¶
<600px>
步骤1:使能GPIO1、GPIO5
<600px>
设置b[31:30]、b[27:26]就可以使能GPIO5、GPIO1,设置为什么值呢?
看下图,设置为0b11:
<400px>
① 00:该GPIO模块全程被关闭
② 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭
③ 10:保留
④ 11:该GPIO模块全程使能
步骤2:设置GPIO5_IO03、GPIO1_IO03、GPIO1_IO05、GPIO1_IO06为GPIO模式
① 对于GPIO5_IO03,设置如下寄存器:
<600px>
② 对于GPIO1_IO03,设置如下寄存器:
<600px>
③ 对于GPIO1_IO05,设置如下寄存器:
<600px>
④ 对于GPIO1_IO06,设置如下寄存器:
<600px>
步骤3:设置GPIO5_IO03、GPIO1_IO03、GPIO1_IO05、GPIO1_IO06为输出引脚,设置其输出电平
寄存器地址为:
<600px>
设置方向寄存器,把引脚设置为输出引脚:
<600px>
设置数据寄存器,设置引脚的输出电平:
<600px>
写程序¶
使用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
驱动设计的思想:面向对象/分层/分离¶
分层¶
上下分层,比如我们前面写的LED驱动程序就分为2层:
① 上层实现硬件无关的操作,比如注册字符设备驱动:leddrv.c
② 下层实现硬件相关的操作,比如board_A.c实现单板A的LED操作
<400px>
分离¶
还能不能改进?分离。
在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中指定使用哪一个引脚即可。
程序结构如下:
<600px>
以面向对象的思想,在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即可。
驱动进化之路:总线设备驱动模型¶
示例:
<800px>
驱动编写的3种方法¶
以LED驱动为例:
总线设备驱动模型¶
<600px>
引入platform_device/platform_driver,将“资源”与“驱动”分离开来。
代码稍微复杂,但是易于扩展。
冗余代码太多,修改引脚时设备端的代码需要重新编译。
更换引脚时,上图中的led_drv.c基本不用改,但是需要修改led_dev.c
设备树¶
<600px>
通过配置文件──设备树来定义“资源”。
代码稍微复杂,但是易于扩展。
无冗余代码,修改引脚时只需要修改dts文件并编译得到dtb文件,把它传给内核。
无需重新编译内核/驱动。
在Linux中实现“分离”:Bus/Dev/Drv模型¶
<1200px>
匹配规则¶
最先比较: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):
<600px>
返回该dev所用的第几个(num)中断:
<600px>
通过名字(name)返回该dev的某类型(type)资源:
<600px>
通过名字(name)返回该dev的中断号:
<600px>
怎么写程序¶
分配/设置/注册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模板驱动程序的改造:总线设备驱动模型¶
原来的框架¶
<800px>
要实现的框架===¶
<800px>
写代码¶
使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\
02_led_drv\04_led_drv_template_bus_dev_drv
注意事项¶
① 如果platform_device中不提供release函数,如下图所示不提供红框部分的函数:
<600px>
则在调用platform_device_unregister时会出现警告,如下图所示:
<600px>
你可以提供一个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”的提示:
<600px>
实现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/ https://www.devicetree.org/specifications/)
内核文档:
Documentation/devicetree/booting-without-of.txt
我录制“设备树视频”时写的文档:设备树详细分析.txt
这个txt文件也同步上传到wiki了:[](http://wiki.100ask.org/Linux_devicetree 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会把内核和设备树文件都读入内存,然后启动内核。在启动内核时会把设备树在内存中的地址告诉内核。
设备树的语法¶
为什么叫“树”?
<600px>
怎么描述这棵树?
我们需要编写设备树文件(dts: device tree source),它需要编译为dtb(device tree blob)文件,内核使用的是dtb文件。
dts文件是根本,它的语法很简单。
下面是一个设备树示例:
<800px>
它对应的dts文件如下:
<400px>
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";
};
<800px>
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>;
};
};
常用的节点(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 {
.......
}
};
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 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文件开始,设备树的处理过程为:
<600px>
① dts在PC机上被编译为dtb文件;
② u-boot把dtb文件传给内核;
③ 内核解析dtb文件,把每一个节点都转换为device_node结构体;
④ 对于某些device_node结构体,会被转换为platform_device结构体。
dtb中每一个节点都被转换为device_node结构体¶
<600px>
根节点被保存在全局变量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函数。
套路是一样的。
我们需要将前面讲过的“匹配规则”再完善一下:
先贴源码:
<600px>
最先比较:是否强制选择某个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。
它的类型如下:
<400px>
如果一个platform_driver支持设备树,它的platform_driver.driver.of_match_table是一个数组,类型如下:
<200px>
使用设备树信息来判断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。
内核里操作设备树的常用函数¶
内核源码中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的结构体定义如下:
<800px>
找到节点¶
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)指向这个值,把它当作字符串。
LED模板驱动程序的改造:设备树¶
总结3种写驱动程序的方法¶
<600px>
核心永远是file_operations结构体。
上述三种方法,只是指定“硬件资源”的方式不一样。
从上图可以知道,platform_device/platform_driver只是编程的技巧,不涉及驱动的核心。
怎么使用设备树写驱动程序¶
设备树节点要与platform_driver能匹配¶
在我们的工作中,驱动要求设备树节点提供什么,我们就得按这要求去编写设备树。
但是,匹配过程所要求的东西是固定的:
① 设备树要有compatible属性,它的值是一个字符串
② platform_driver中要有of_match_table,其中一项的.compatible成员设置为一个字符串
③ 上述2个字符串要一致。
示例如下:
<800px>
设备树节点指定资源,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”配对了:
<600px>
注意:一个平台设备只能配对一个平台驱动,一个平台驱动可以配对多个平台设备。
课后作业¶
请仿照本节提供的程序(位于05_led_drv_template_device_tree目录),改造你所用的单板的LED驱动程序。
APP怎么读取按键值¶
APP读取按键值,需要有按键驱动程序。
为什么要讲按键驱动程序?
APP去读按键的方法有4种:
① 查询方式
② 休眠-唤醒方式
③ poll方式
④ 异步通知方式
通过这4种方式的学习,我们可以掌握如下知识:
① 驱动的基本技能:中断、休眠、唤醒、poll等机制。
这些基本技能是驱动开发的基础,其他大型驱动复杂的地方是它的框架及设计思想,但是基本技术就这些。
② APP开发的基本技能:阻塞 、非阻塞、休眠、poll、异步通知。
妈妈怎么知道孩子醒了¶
<400px>
妈妈怎么知道卧室里小孩醒了?
① 时时进房间看一下:查询方式
简单,但是累
② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒
不累,但是妈妈干不了活了
③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式
要浪费点时间,但是可以继续干活。
妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知
妈妈、小孩互不耽误。
这4种方法没有优劣之分,在不同的场合使用不同的方法。
APP读取按键的4种方法¶
跟上述生活场景类似,APP去读取按键也有4种方法:
① 查询方式
② 休眠-唤醒方式
③ poll方式
④ 异步通知方式
第2、3、4种方法,都涉及中断服务程序。中断,就像小孩醒了会哭闹一样,中断不经意间到来,它会做某些事情:唤醒APP、向APP发信号。
所以,在按键驱动程序中,中断是核心。
实际上,中断无论是在单片机还是在Linux中都很重要。在Linux中,中断的知识还涉及进程、线程等。
查询方式¶
这种方法最简单:
<400px>
驱动程序中构造、注册一个file_operations结构体,里面提供有对应的open,read函数。APP调用open时,导致驱动中对应的open函数被调用,在里面配置GPIO为输入引脚。APP调用read时,导致驱动中对应的read函数被调用,它读取寄存器,把引脚状态直接返回给APP。
休眠-唤醒方式¶
<600px>
驱动程序中构造、注册一个file_operations结构体,里面提供有对应的open,read函数。
APP调用open时,导致驱动中对应的open函数被调用,在里面配置GPIO为输入引脚;并且注册GPIO的中断处理函数。
APP调用read时,导致驱动中对应的read函数被调用,如果有按键数据则直接返回给APP;否则APP在内核态休眠。
当用户按下按键时,GPIO中断被触发,导致驱动程序之前注册的中断服务程序被执行。它会记录按键数据,并唤醒休眠中的APP。
APP被唤醒后继续在内核态运行,即继续执行驱动代码,把按键数据返回给APP(的用户空间)。
poll方式¶
上面的休眠-唤醒方式有个缺点:如果用户一直没操作按键,那么APP就会永远休眠。
我们可以给APP定个闹钟,这就是poll方式。
<800px>
驱动程序中构造、注册一个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函数被调用,这时驱动程序中含有数据,会直接返回数据。
异步通知方式¶
异步通知的原理:发信号=====¶
<800px>
异步通知的实现原理是:内核给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函数映射得到虚拟地址。驱动程序中使用虚拟地址去访问寄存器。
<600px>
按键驱动编写思路¶
GPIO按键的原理图一般有如下2种:
<400px>
按键没被按下时,上图中左边的GPIO电平为高,右边的GPIO电平为低。
按键被按下后,上图中左边的GPIO电平为低,右边的GPIO电平为高。
编写按键驱动程序最简单的方法如下图所示:
<600px>
回顾一下编写驱动程序的套路:
<600px>
对于使用查询方式的按键驱动程序,我们只需要实现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结构体即可。
<800px>
使用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
具体单板的按键驱动程序(查询方式)¶
AM335X的按键驱动程序(查询方式)¶
先看原理图确定引脚及操作方法¶
AM335X是底板+核心板的结构,打开底板原理图100ask_am335x_v12_原理图.pdf,它有4个按键,本视频只操作一个按键,原理图如下:
<400px>
平时按键电平为高,按下按键后电平为低。
按键引脚为GPIO1_25。
再看芯片手册确定寄存器及操作方法¶
<600px>
步骤1:使能GPIO1模块
设置CM_PER_GPIO1_CLKCTRL寄存器的bit[18]为1,bit[1:0]为0x2,该寄存器地址为0x44E00000+0xAC。
<600px>
步骤2:把GPIO1_25对应的引脚设置为GPIO模式
要用哪一个寄存器来把GPIO1_25对应的引脚设置为GPIO模式?
① 在核心板原理图ET-som335X原理图.pdf里搜“GPIO1_25”,可以看到下图,确定pin number为U16:
<600px>
② 在芯片手册AM335x Sitara™ Processors.pdf里搜“U16”,可得下图,引脚名为GPMC_A9,用作GPIO时要设置为mode 7:
<600px>
③ 在芯片手册AM335x_datasheet_spruh73p.pdf中搜gpmc_a9,
<600px>
所以,要把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]。
<600px>
<600px>
编程¶
程序框架¶
使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\
04_button_drv\02_button_drv_for_boards\01_button_drv_for_am335x
<800px>
硬件相关的代码¶
主要看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
RK3288的按键驱动程序(查询方式)¶
先看原理图确定引脚及操作方法¶
Firefly的RK3288开发板上没有按键,我们为它制作的扩展板上有1个按键。在扩展板原理图rk3288_extend_v12_0715.pdf中可以看到按键,如下:
<400px>
平时按键电平为高,按下按键后电平为低。
按键引脚为GPIO7_B1。
再看芯片手册确定寄存器及操作方法¶
芯片手册为Rockchip_RK3288_TRM_V1.2_Part1-20170321.pdf,不过我们总结如下。
<600px>
步骤1:使能GPIO7模块
设置CRU_CLKGATE14_CON寄存器的bit[7]为0。
要设置bit7,必须同时设置bit23为1。
该寄存器地址为0xFF760000+0x198。
<600px>
步骤2:把GPIO7_B1对应的引脚设置为GPIO模式
设置GRF_GPIO7B_IOMUX寄存器的bit[3:2]为0b00。
要设置bit[3:2],必须同时设置bit[19:18]为0b11。
该寄存器地址为0xFF770000+0x0070。
<600px>
步骤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
<600px>
编程¶
程序框架¶
使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\
04_button_drv\02_button_drv_for_boards\02_button_drv_for_rk3288
<800px>
硬件相关的代码¶
主要看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中可以看到按键,如下:
<800px>
平时按键电平为高,按下按键后电平为低。
按键引脚为GPIO0_B1、GPIO0_B2、GPIO0_B4。
本视频中,只操作一个按键:GPIO0_B1。
再看芯片手册确定寄存器及操作方法¶
芯片手册为Rockchip RK3399TRM V1.3 Part1.pdf和Rockchip RK3399TRM V1.3 Part2.pdf,不过我们总结如下。
<600px>
步骤1:使能GPIO0模块
设置PMUCRU_CLKGATE_CON1寄存器的bit[3]为0。
要设置bit3,必须同时设置bit19为1。
该寄存器地址为0xFF760000+ 0x0104。
<600px>
步骤2:把GPIO0_B1对应的引脚设置为GPIO模式
设置PMUGRF_GPIO0B_IOMUX寄存器的bit[3:2]为0b00。
要设置bit[3:2],必须同时设置bit[19:18]为0b11。
该寄存器地址为0xFF310000+0x0004。
<600px>
步骤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
<600px>
编程¶
程序框架¶
使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\
04_button_drv\02_button_drv_for_boards\03_button_drv_for_rk3399
<600px>
硬件相关的代码¶
主要看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
百问网IMX6ULL-QEMU的按键驱动程序(查询方式)¶
使用QEMU模拟的硬件,它的硬件资源可以随意扩展。
在IMX6ULL QEMU 虚拟开发板上,我们为它设计了2个 按键。在QEMU的GUI上有4个按键,右边的2个留待以后用于电源管理。
再看芯片手册确定寄存器及操作方法¶
<600px>
步骤1:使能GPIO1、GPIO5
<600px>
设置b[31:30]、b[27:26]就可以使能GPIO5、GPIO1,设置为什么值呢?
看下图,设置为0b11:
<600px>
① 00:该GPIO模块全程被关闭
② 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭
③ 10:保留
④ 11:该GPIO模块全程使能
步骤2:设置GPIO5_IO01、GPIO1_IO18为GPIO模式
① 对于GPIO5_IO01,设置如下寄存器:
<600px>
② 对于GPIO1_IO18,设置如下寄存器:
<600px>
步骤3:设置GPIO5_IO01、GPIO1_IO18为输入引脚,读取引脚电平
寄存器地址为:
<600px>
设置方向寄存器,把引脚设置为输出引脚:
<600px>
读取引脚状态寄存器,得到引脚电平:
<600px>
编程¶
程序框架¶
使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\
04_button_drv\02_button_drv_for_boards\04_button_drv_for_100ask_imx6ull-qemu
<800px>
硬件相关的代码¶
主要看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,使用按键来点灯
异常与中断的概念及处理流程¶
中断的引入¶
妈妈怎么知道孩子醒了¶
<400px>
妈妈怎么知道卧室里小孩醒了?
① 时时进房间看一下:查询方式
简单,但是累
② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒
不累,但是妈妈干不了活了
③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式
要浪费点时间,但是可以继续干活。
妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知
妈妈、小孩互不耽误。
后面的3种方式,都需要“小孩来中断妈妈”:中断她的睡眠、中断她的工作。
实际上,能“中断”妈妈的事情可多了:
① 远处的猫叫:这可以被忽略
② 门铃、小孩哭声:妈妈的应对措施不一样
③ 身体不舒服:那要赶紧休息
④ 有蜘蛛掉下来了:赶紧跑啊,救命
<600px>
妈妈当前正在看书,被“中断”后她会怎么做?流程如下:
① 妈妈正在看书
② 发生了各种声音
可忽略的远处猫叫
快递员按门铃
卧室中小孩哭了
③ 妈妈怎么办?
a. 先在书中放入书签,合上书
b. 去处理
对于不同的情况,处理方法不同:
对于门铃:开门取快递
对于哭声:照顾小孩
c. 回来继续看书
嵌入系统中也有类似的情况¶
<600px>
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”命令,可以得到开发板所用内核的版本,比如:
<200px>
② 在服务器中给开发板编译内核时,这个内核也有一个版本:
;进入该内核源码目录,执行“make kernelrelease”命令,可以得到它的版本,比如:
<800px>
③ 编译驱动时,会用到服务器中开发板的内核源码,会带有它的版本信息。
如果①②③的版本信息不匹配,很可能导致驱动程序无法加载,比如:
<800px>
有2个解决方法:
A. 在Ubuntu上重新编译内核,让开发板使用新的内核启动;重新编译驱动,加载新驱动:
这样,①②③三者的内核版本就都一致了。
但是,这种方法有时候不好用,比如开发板上的内核无法更改(出厂固化了),或者你没有开发板上所用内核的全部源码无法编译出内核,这时就可以使用下面的方法。
B. 在Ubuntu上修改版本号,改为开发板上“uname -r”的结果,然后重新编译内核和驱动:
开发板就可以继续使用原来的内核,并且可以加载编译出来的驱动了。
步骤如下:
b.1 修改Ubuntu上开发板内核源码顶层目录Makefile,如下图:
<600px>
b.2 重新编译内核,这会生成一些头文件,供驱动使用
b.3 重新编译驱动
<400px>
<800px>
<600px>
使用哪个引脚,怎么操作引脚,都写死在代码中。
<1200px>
<600px>
<600px>