ELADCMSecondEditionChapterFive
来自百问网嵌入式Linux wiki
Zhouyuebiao(讨论 | 贡献)2019年12月17日 (二) 10:23的版本 (Zhouyuebiao移动页面EmbeddedLinuxApplicationDevelopmentCompleteManualSecondEditionChapterFive至ELADCMSecondEditionChapterFive)
目录
-
1 第五篇. 嵌入式Linux驱动开发基础知识
- 1.1 嵌入式后Linux驱动开发基础知识的引导与说明
- 1.2 Hello驱动(不涉及硬件操作)
- 1.3 硬件知识_LED原理图
- 1.4 普适的GPIO引脚操作方法
- 1.5 具体单板的GPIO操作方法
- 1.6 LED驱动程序框架
- 1.7 具体单板的LED驱动程序
- 1.8 驱动设计的思想:面向对象/分层/分离
- 1.9 驱动进化之路:总线设备驱动模型
- 1.10 LED模板驱动程序的改造:总线设备驱动模型
- 1.11 驱动进化之路:设备树的引入及简明教程
- 1.12 LED模板驱动程序的改造:设备树
- 1.13 APP怎么读取按键值
- 1.14 查询方式的按键驱动程序_编写框架
- 1.15 具体单板的按键驱动程序(查询方式)
- 1.16 异常与中断的概念及处理流程
- 1.17 常见问题
第五篇. 嵌入式Linux驱动开发基础知识
嵌入式后Linux驱动开发基础知识的引导与说明
打算讲什么、怎么讲?
需要做什么准备工作
- 驱动程序依赖于Linux内核,你为开发板A开发驱动,那就先在Ubuntu中得到、配置、编译开发板A所使用的Linux内核。
- 请使用git下载本教程的文档、源码,查看如下目录中你所用开发板的高级用户使用手册(有些开发板的手册我们还没编写完,持续更新):
- 根据手册完成下面操作:
- 硬件部分:
- ① 开发板接线:串口线、电源线、网线
- ② 开发板烧写系统
- 软件部分:
- ① 下载Linux内核,Windows和Ubuntu下各放一份
- ② Windows下:使用Source Insight创建内核源码的工程,这是用来浏览内核、编辑驱动
- ③ Ubuntu下:安装工具链,配置、编译Linux内核
- 硬件部分:
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”与之对应。
- 可以猜测,我们使用open打开文件时,传入的flags、mode等参数会被记录在内核中对应的struct file结构体里(f_flags、f_mode):
int open(const char *pathname, int flags, mode_t mode);
打开字符设备节点时,内核中也有对应的struct file
请猜猜怎么编写驱动程序
- ① 确定主设备号,也可以让内核分配
- ② 定义自己的file_operations结构体
- ③ 实现对应的drv_open/drv_read/drv_write等函数,填入file_operations结构体
- ④ 把file_operations结构体告诉内核:register_chrdev
- ⑤ 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
- ⑥ 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
- ⑦ 其他完善:提供设备信息,自动创建设备节点:class_create, device_create
请不要啰嗦,表演你的代码吧
写驱动程序
- 参考driver/char中的程序,包含头文件,写框架,传输数据:
- A. 驱动中实现open, read, write, release,APP调用这些函数时,都打印内核信息
- B. APP调用write函数时,传入的数据保存在驱动中
- C. APP调用read函数时,把驱动中保存的数据返回给APP
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\01_hello_drv\hello_drv.c
- hello_drv.c源码如下:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
/* 1. 确定主设备号 */
static int major = 0;
static char kernel_buf[1024];
static struct class *hello_class;
#define MIN(a, b) (a < b ? a : b)
/* 3. 实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_to_user(buf, kernel_buf, MIN(1024, size));
return MIN(1024, size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_open (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
static int hello_drv_close (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
/* 2. 定义自己的file_operations结构体 */
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};
/* 4. 把file_operations结构体告诉内核:注册驱动程序 */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init hello_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "hello", &hello_drv); /* /dev/hello */
hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
return -1;
}
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
return 0;
}
/* 6. 有入口函数就有出口函数:卸载驱动程序时就会去调用这个出口函数 */
static void __exit hello_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
unregister_chrdev(major, "hello");
}
/* 7. 其他完善:提供设备信息,自动创建设备节点 */
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
- 阅读一个驱动程序,从它的入口函数开始,第66行就是入口函数。它的主要工作就是第71行,向内核注册一个file_operations结构体:hello_drv,这就是字符设备驱动程序的核心。
- file_operations结构体hello_drv在第56行定义,里面提供了open/read/write/release成员,应用程序调用open/read/write/close时就会导致这些成员函数被调用。
- file_operations结构体hello_drv中的成员函数都比较简单,大多数只是打印而已。要注意的是,驱动程序和应用程序之间传递数据要使用copy_from_user/copy_to_user函数。
写测试程序
- 测试程序要实现写、读功能:
A. ./hello_drv_test -w wiki.100ask.net // 把字符串“wiki.100ask.net”发给驱动程序
B. ./hello_drv_test -r // 把驱动中保存的字符串读回来
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\01_hello_drv\hello_drv_test.c
- hello_drv_test.c源码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
* ./hello_drv_test -w abc
* ./hello_drv_test -r
*/
int main(int argc, char **argv)
{
int fd;
char buf[1024];
int len;
/* 1. 判断参数 */
if (argc < 2)
{
printf("Usage: %s -w <string>\n", argv[0]);
printf(" %s -r\n", argv[0]);
return -1;
}
/* 2. 打开文件 */
fd = open("/dev/hello", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/hello\n");
return -1;
}
/* 3. 写文件或读文件 */
if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
{
len = strlen(argv[2]) + 1;
len = len < 1024 ? len : 1024;
write(fd, argv[2], len);
}
else
{
len = read(fd, buf, 1024);
buf[1023] = '\0';
printf("APP read : %s\n", buf);
}
close(fd);
return 0;
}
测试
- A. 编写驱动程序的Makefile
- 驱动程序中包含了很多头文件,这些头文件来自内核,不同的ARM板它的某些头文件可能不同。所以编译驱动程序时,需要指定板子所用的内核的源码路径。
- 要编译哪个文件?这也需要指定,设置obj-m变量即可
- 怎么把.c文件编译为驱动程序.ko?这要借助内核的顶层Makefile。
- 本驱动程序的Makefile内容如下:
01
02 # 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
03 # 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
04 # 2.1 ARCH, 比如: export ARCH=arm64
05 # 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
06 # 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
07 # 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
08 # 请参考各开发板的高级用户使用手册
09
10 KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.4
11
12 all:
13 make -C $(KERN_DIR) M=`pwd` modules
14 $(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c
15
16 clean:
17 make -C $(KERN_DIR) M=`pwd` modules clean
18 rm -rf modules.order
19 rm -f hello_drv_test
20
21 obj-m += hello_drv.o
- 先设置好交叉编译工具链,编译好你的板子所用的内核,然后修改Makefile指定内核源码路径,最后即可执行make命令编译驱动程序和测试程序。
- B. 上机实验
- 注意:我们是在Ubuntu中编译程序,但是需要在ARM板子上测试。所以需要把程序放到ARM板子上。
- 启动单板后,可以通过NFS挂载Ubuntu的某个目录,访问该目录中的程序。
- 测试示例:
- ① 在Ubuntu上编译好驱动,并它复制到NFS目录:
$ cp *.ko hello_drv_test ~/nfs_rootfs/
- ② 在ARM板上测试:
# echo "7 4 1 7" > /proc/sys/kernel/printk // 打开内核的打印信息,有些板子默认打开了
# ifconfig eth0 192.168.1.100 // 配置ARM板IP,下面是挂载NFS文件系统
# mount -t nfs -o nolock,vers=3 192.168.1.137:/home/book/nfs_rootfs /mnt
# cd /mnt
# insmod hello_drv.ko // 安装驱动程序
[ 293.594910] hello_drv: loading out-of-tree module taints kernel.
[ 293.616051] /home/book/source/01_hello_drv/hello_drv.c hello_init line 70
# ls /dev/hello -l // 驱动程序会生成设备节点
crw------- 1 root root 236, 0 Jan 18 08:55 /dev/hello
# ./hello_drv_test // 查看测试程序的用法
Usage: ./hello_drv_test -w <string>
./hello_drv_test -r
# ./hello_drv_test -w wiki.100ask.net // 往驱动程序中写入字符串
[ 318.360800] /home/book/source/01_hello_drv/hello_drv.c hello_drv_open line 45
[ 318.372570] /home/book/source/01_hello_drv/hello_drv.c hello_drv_write line 38
[ 318.382854] /home/book/source/01_hello_drv/hello_drv.c hello_drv_close line 51
# ./hello_drv_test -r // 从驱动程序中读出字符串
[ 326.177890] /home/book/source/01_hello_drv/hello_drv.c hello_drv_open line 45
[ 326.198304] /home/book/source/01_hello_drv/hello_drv.c hello_drv_read line 30
APP read : wiki.100ask.net
[ 326.214782] /home/book/source/01_hello_drv/hello_drv.c hello_drv_close line 51
- 注意:如果安装驱动时提示version magic不匹配,请看本文档最后的“常见问题”。
Hello驱动中的一些补充知识
module_init/module_exit的实现
register_chrdev的内部实现
class_destroy/device_create浅析
硬件知识_LED原理图
- 当我们学习C语言的时候,我们会写个Hello程序。
- 那当我们写ARM程序,也该有一个简单的程序引领我们入门,这个程序就是点亮LED。
- 我们怎样去点亮一个LED呢?
- 分为三步:
- 1.看原理图,确定控制LED的引脚;
- 2.看主芯片的芯片手册,确定如何设置控制这个引脚;
- 3.写程序;
先来讲讲怎么看原理图
- LED的驱动方式,常见的有四种。
- 方式1:使用引脚输出3.3V点亮LED,输出0V熄灭LED。
- 方式2:使用引脚拉低到0V点亮LED,输出3.3V熄灭LED。
- 由此,主芯片引脚输出高电平/低电平,即可改变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种操作方法:
- 原则:不能影响到其他位
- 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操作方法
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模块结构
- 有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时,可以继续设置它是输出引脚,还是输入引脚。
- 数值:对于输出引脚,可以设置寄存器让它输出高、低电平;
- 对于输入引脚,可以读取寄存器得到引脚的当前电平。
set-and-clear协议
- 假设某个GPIO被设置为输出,怎么设置它的输出电平呢?AM335X中对于每个GPIO模块有一个GPIO_DATAOUT寄存器,其中的每一位对应一个引脚,如下:
- 要设置某一位时,不能影响到其他位,操作方法是:读出原来的值,修改某一位,把新值写回去。需要3个步骤才可以设置某一位的值,这效率太低了!
- 使用set-and-clear可以只用一个步骤即可修改某一位的值。
- 当想设置某一位为1时,往DATA_SETDATAOUT寄存器中某位写入1即可,芯片内部会把对应引脚的电平设置为1,并且不会影响其他引脚:
- 并非所有的芯片都有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模块结构
- 有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模块提供时钟:
- ② 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得到引脚的当前电平。
RK3399的GPIO操作方法
- GPIO: General-purpose input/output,通用的输入输出口
- CRU: Clock & Reset Unit (时钟和复位单元)
- PMU: Power Managerment Unit (电源管理单元)
- GRF: General Register Files (通用寄存器文件)
RK3399的GPIO模块结构
- 有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得到引脚的当前电平。
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;
CCM用于设置是否向GPIO模块提供时钟
- 参考资料:芯片手册《Chapter 18: Clock Controller Module (CCM)》
- GPIOx要用CCM_CCGRy寄存器中的2位来决定该组GPIO是否使能。哪组GPIO用哪个CCM_CCGR寄存器来设置,请看上图红框部分。
- CCM_CCGR寄存器中某2位的取值含义如下:
- ① 00:该GPIO模块全程被关闭
- ② 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭
- ③ 10:保留
- ④ 11:该GPIO模块全程使能
IOMUXC:引脚的模式(Mode、功能)
- 参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC)》。
- 对于某个/某组引脚,IOMUXC中有2个寄存器用来设置它:
- ① 选择功能:
GPIO模块内部
怎么编程
读GPIO
LED驱动程序框架
- 注意:如果做实验安装驱动时提示version magic不匹配,请看本文档最后的“常见问题”。
LED驱动要怎么写,才能支持多个板子?分层。
写代码
- 使用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把物理地址映射为虚拟地址,使用虚拟地址。
- ② 它的作用:
- 把物理地址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
- 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驱动程序
所涉及的寄存器操作
/* set PRCM to enalbe GPIO1
* set CM_PER_GPIO1_CLKCTRL (0x44E00000 + 0xAC)
* val: (1<<18) | 0x2
*/
- b. 设置GPIO1_16的功能,让它工作于GPIO模式
/* set Control Module to set GPIO1_16 (R13) used as GPIO
* conf_gpmc_a0 as mode 7
* addr : 0x44E10000 + 0x840
* val : 7
*/
/* set GPIO1's registers , to set GPIO1_16'S dir (output)
* GPIO_OE
* addr : 0x4804C000 + 0x134
* clear bit 16
*/
/* set GPIO1_16's registers , to output 1
* GPIO_SETDATAOUT
* addr : 0x4804C000 + 0x194
*/
/* set GPIO1_16's registers , to output 0
* GPIO_CLEARDATAOUT
* addr : 0x4804C000 + 0x190
*/
写程序
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\02_led_drv\
02_led_drv_for_boards\am335x_src_bin
- 硬件相关的文件是board_am335x.c,其他文件跟LED框架驱动程序完全一样。
- 它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
100 static struct led_operations board_demo_led_opr = {
101 .num = 1,
102 .init = board_demo_led_init,
103 .ctl = board_demo_led_ctl,
104 };
105
- led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
- 值得关注的是第33~37行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器。
19 #include "led_opr.h"
20
21 static volatile unsigned int *CM_PER_GPIO1_CLKCTRL;
22 static volatile unsigned int *conf_gpmc_a0;
23 static volatile unsigned int *GPIO1_OE;
24 static volatile unsigned int *GPIO1_CLEARDATAOUT;
25 static volatile unsigned int *GPIO1_SETDATAOUT;
26
27 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
28 {
29 if (which == 0)
30 {
31 if (!CM_PER_GPIO1_CLKCTRL)
32 {
33 CM_PER_GPIO1_CLKCTRL = ioremap(0x44E00000 + 0xAC, 4);
34 conf_gpmc_a0 = ioremap(0x44E10000 + 0x840, 4);
35 GPIO1_OE = ioremap(0x4804C000 + 0x134, 4);
36 GPIO1_CLEARDATAOUT = ioremap(0x4804C000 + 0x190, 4);
37 GPIO1_SETDATAOUT = ioremap(0x4804C000 + 0x194, 4);
38 }
39
40 //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
41 /* a. 使能GPIO1
42 * set PRCM to enalbe GPIO1
43 * set CM_PER_GPIO1_CLKCTRL (0x44E00000 + 0xAC)
44 * val: (1<<18) | 0x2
45 */
46 *CM_PER_GPIO1_CLKCTRL = (1<<18) | 0x2;
47
48 /* b. 设置GPIO1_16的功能,让它工作于GPIO模式
49 * set Control Module to set GPIO1_16 (R13) used as GPIO
50 * conf_gpmc_ad0 as mode 7
51 * addr : 0x44E10000 + 0x800
52 * val : 7
53 */
54 *conf_gpmc_a0 = 7;
55
56 /* c. 设置GPIO1_16的方向,让它作为输出引脚
57 * set GPIO1's registers , to set GPIO1_16'S dir (output)
58 * GPIO_OE
59 * addr : 0x4804C000 + 0x134
60 * clear bit 16
61 */
62
63 *GPIO1_OE &= ~(1<<16);
64 }
65
66 return 0;
67 }
68
- led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
69 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
70 {
71 //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
72
73 if (which == 0)
74 {
75 if (status) /* on: output 0 */
76 {
77 /* e. 清除GPIO1_16的数据,让它输出低电平
78 * AM335X芯片支持set-and-clear protocol,设置GPIO_CLEARDATAOUT的bit 16为1即可让引脚输出0:
79 * set GPIO1_16's registers , to output 0
80 * GPIO_CLEARDATAOUT
81 * addr : 0x4804C000 + 0x190
82 */
83 *GPIO1_CLEARDATAOUT = (1<<16);
84 }
85 else
86 {
87 /* d. 设置GPIO1_16的数据,让它输出高电平
88 * AM335X芯片支持set-and-clear protocol,设置GPIO_SETDATAOUT的bit 16为1即可让引脚输出1:
89 * set GPIO1_16's registers , to output 1
90 * GPIO_SETDATAOUT
91 * addr : 0x4804C000 + 0x194
92 */
93 *GPIO1_SETDATAOUT = (1<<16);
94 }
95 }
96
97 return 0;
98 }
99
- 下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
106 struct led_operations *get_board_led_opr(void)
107 {
108 return &board_demo_led_opr;
109 }
110
配置内核去掉原有LED驱动
- 不需要重新配置内核,只需要在开发板上执行以下3条命令关闭内核对LED的使用即可:
# echo none > /sys/class/leds/am335x:green:cpu0/trigger
# echo none > /sys/class/leds/am335x:green:mmc0/trigger
# echo none > /sys/class/leds/am335x:green:nand/trigger
- 然后就可以去安装驱动程序,执行测试程序了,操作过程跟LED框架驱动程序的测试是一样的。
课后作业
- a. 在board_am335x.c里有ioremap,什么时候执行iounmap?请完善程序
- b. 视频里我们只实现了点一个LED,请修改代码实现操作4个LED
RK3288和RK3399的LED驱动程序
原理图
fireflye RK3288的LED原理图
所涉及的寄存器操作
RK3288的GPIO8_A1引脚
/* rk3288 GPIO8_A1 */
/* a. 使能GPIO8
* set CRU to enable GPIO8
* CRU_CLKGATE14_CON 0xFF760000 + 0x198
* (1<<(8+16)) | (0<<8)
*/
/* 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引脚
- 设置GPIO_SWPORTA_DDR 寄存器b[1]为1,把GPIO8_A1设置为输出引脚。
-
注意:
- GPIO_A0~A7 对应bit0~bit7;GPIO_B0~B7 对应bit8~bit15;
- GPIO_C0~C7 对应bit16~bit23;GPIO_D0~D7 对应bit24~bit31
/* c. 设置GPIO8_A1作为output引脚
* set GPIO_SWPORTA_DDR to configure GPIO8_A1 as output
* GPIO_SWPORTA_DDR 0xFF7F0000 + 0x0004
* bit[1] = 0b1
*/
- d. 设置GPIO8_A1输出高电平
- 设置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引脚
/* rk3399 GPIO2_D3 */
/* a. 使能GPIO2
* set CRU to enable GPIO2
* CRU_CLKGATE_CON31 0xFF760000 + 0x037c
* (1<<(3+16)) | (0<<3)
*/
/* 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引脚
- 设置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输出高电平
- 设置GPIO_SWPORTA_DR 寄存器b[27]为1,让GPIO2_D3输出高电平。
-
注意:
- GPIO_A0~A7 对应bit0~bit7;GPIO_B0~B7 对应bit8~bit15;
- GPIO_C0~C7 对应bit16~bit23;GPIO_D0~D7 对应bit24~bit31
/* d. 设置GPIO2_D3输出高电平
* set GPIO_SWPORTA_DR to configure GPIO2_D3 output 1
* GPIO_SWPORTA_DR 0xFF780000 + 0x0000
* bit[27] = 0b1
*/
- e. 设置GPIO2_D3输出低电平
- 同样是设置GPIO_SWPORTA_DR 寄存器,把b[27]设为0,让GPIO2_D3输出低电平。
/* e. 设置GPIO2_D3输出低电平
* set GPIO_SWPORTA_DR to configure GPIO2_D3 output 0
* GPIO_SWPORTA_DR 0xFF780000 + 0x0000
* bit[27] = 0b0
*/
写程序
RK3288
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\02_led_drv\
02_led_drv_for_boards\rk3288_src_bin
- 硬件相关的文件是board_rk3288.c,其他文件跟LED框架驱动程序完全一样。
- 它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
91 static struct led_operations board_demo_led_opr = {
92 .num = 1,
93 .init = board_demo_led_init,
94 .ctl = board_demo_led_ctl,
95 };
96
- led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
- 值得关注的是第32~35行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器:
20 static volatile unsigned int *CRU_CLKGATE14_CON;
21 static volatile unsigned int *GRF_GPIO8A_IOMUX ;
22 static volatile unsigned int *GPIO8_SWPORTA_DDR;
23 static volatile unsigned int *GPIO8_SWPORTA_DR ;
24
25 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
26 {
27 //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
28 if (which == 0)
29 {
30 if (!CRU_CLKGATE14_CON)
31 {
32 CRU_CLKGATE14_CON = ioremap(0xFF760000 + 0x0198, 4);
33 GRF_GPIO8A_IOMUX = ioremap(0xFF770000 + 0x0080, 4);
34 GPIO8_SWPORTA_DDR = ioremap(0xFF7F0000 + 0x0004, 4);
35 GPIO8_SWPORTA_DR = ioremap(0xFF7F0000 + 0x0000, 4);
36 }
37
38 /* rk3288 GPIO8_A1 */
39 /* a. 使能GPIO8
40 * set CRU to enable GPIO8
41 * CRU_CLKGATE14_CON 0xFF760000 + 0x198
42 * (1<<(8+16)) | (0<<8)
43 */
44 *CRU_CLKGATE14_CON = (1<<(8+16)) | (0<<8);
45
46 /* b. 设置GPIO8_A1用于GPIO
47 * set PMU/GRF to configure GPIO8_A1 as GPIO
48 * GRF_GPIO8A_IOMUX 0xFF770000 + 0x0080
49 * bit[3:2] = 0b00
50 * (3<<(2+16)) | (0<<2)
51 */
52 *GRF_GPIO8A_IOMUX =(3<<(2+16)) | (0<<2);
53
54 /* c. 设置GPIO8_A1作为output引脚
55 * set GPIO_SWPORTA_DDR to configure GPIO8_A1 as output
56 * GPIO_SWPORTA_DDR 0xFF7F0000 + 0x0004
57 * bit[1] = 0b1
58 */
59 *GPIO8_SWPORTA_DDR |= (1<<1);
60 }
61 return 0;
62 }
63
- led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
64 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮, 0-灭*/
65 {
66 //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
67 if (which == 0)
68 {
69 if (status) /* on: output 0 */
70 {
71 /* e. 设置GPIO8_A1输出低电平
72 * set GPIO_SWPORTA_DR to configure GPIO8_A1 output 0
73 * GPIO_SWPORTA_DR 0xFF7F0000 + 0x0000
74 * bit[1] = 0b0
75 */
76 *GPIO8_SWPORTA_DR &= ~(1<<1);
77 }
78 else /* off: output 1 */
79 {
80 /* d. 设置GPIO8_A1输出高电平
81 * set GPIO_SWPORTA_DR to configure GPIO8_A1 output 1
82 * GPIO_SWPORTA_DR 0xFF7F0000 + 0x0000
83 * bit[1] = 0b1
84 */
85 *GPIO8_SWPORTA_DR |= (1<<1);
86 }
87 }
88 return 0;
89 }
90
- 下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
97 struct led_operations *get_board_led_opr(void)
98 {
99 return &board_demo_led_opr;
100 }
101
RK3399
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\02_led_drv\
02_led_drv_for_boards\rk3399_src_bin
- 硬件相关的文件是board_rk3399.c,其他文件跟LED框架驱动程序完全一样。
- 它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
91 static struct led_operations board_demo_led_opr = {
92 .num = 1,
93 .init = board_demo_led_init,
94 .ctl = board_demo_led_ctl,
95 };
96
- led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
- 值得关注的是第32~35行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器:
20 static volatile unsigned int *CRU_CLKGATE_CON31;
21 static volatile unsigned int *GRF_GPIO2D_IOMUX ;
22 static volatile unsigned int *GPIO2_SWPORTA_DDR;
23 static volatile unsigned int *GPIO2_SWPORTA_DR ;
24
25 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
26 {
27 //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
28 if (which == 0)
29 {
30 if (!CRU_CLKGATE_CON31)
31 {
32 CRU_CLKGATE_CON31 = ioremap(0xFF760000 + 0x037c, 4);
33 GRF_GPIO2D_IOMUX = ioremap(0xFF770000 + 0x0e00c, 4);
34 GPIO2_SWPORTA_DDR = ioremap(0xFF780000 + 0x0004, 4);
35 GPIO2_SWPORTA_DR = ioremap(0xFF780000 + 0x0000, 4);
36 }
37
38 /* rk3399 GPIO2_D3 */
39 /* a. 使能GPIO2
40 * set CRU to enable GPIO2
41 * CRU_CLKGATE_CON31 0xFF760000 + 0x037c
42 * (1<<(3+16)) | (0<<3)
43 */
44 *CRU_CLKGATE_CON31 = (1<<(3+16)) | (0<<3);
45
46 /* b. 设置GPIO2_D3用于GPIO
47 * set PMU/GRF to configure GPIO2_D3 as GPIO
48 * GRF_GPIO2D_IOMUX 0xFF770000 + 0x0e00c
49 * bit[7:6] = 0b00
50 * (3<<(6+16)) | (0<<6)
51 */
52 *GRF_GPIO2D_IOMUX = (3<<(6+16)) | (0<<6);
53
54 /* c. 设置GPIO2_D3作为output引脚
55 * set GPIO_SWPORTA_DDR to configure GPIO2_D3 as output
56 * GPIO_SWPORTA_DDR 0xFF780000 + 0x0004
57 * bit[27] = 0b1
58 */
59 *GPIO2_SWPORTA_DDR |= (1<<27);
60 }
61 return 0;
62 }
63
- led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
64 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮, 0-灭*/
65 {
66 //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
67 if (which == 0)
68 {
69 if (status) /* on: output 1 */
70 {
71 /* d. 设置GPIO2_D3输出高电平
72 * set GPIO_SWPORTA_DR to configure GPIO2_D3 output 1
73 * GPIO_SWPORTA_DR 0xFF780000 + 0x0000
74 * bit[27] = 0b1
75 */
76 *GPIO2_SWPORTA_DR |= (1<<27);
77 }
78 else /* off : output 0 */
79 {
80 /* e. 设置GPIO2_D3输出低电平
81 * set GPIO_SWPORTA_DR to configure GPIO2_D3 output 0
82 * GPIO_SWPORTA_DR 0xFF780000 + 0x0000
83 * bit[27] = 0b0
84 */
85 *GPIO2_SWPORTA_DR &= ~(1<<27);
86 }
87 }
88 return 0;
89 }
90
- 下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
97 struct led_operations *get_board_led_opr(void)
98 {
99 return &board_demo_led_opr;
100 }
101
上机实验
- 首先设置工具链,然后修改驱动程序Makefile指定内核源码路径,就可以编译驱动程序和测试程序了。
- 启动开发板,挂载NFS文件系统,这样就可以访问到Ubuntu中的文件。
- 最后,就可以在开发板上进行下列测试。
RK3288
# insmod 100ask_led.ko
# ./ledtest /dev/100ask_led0 on
# ./ledtest /dev/100ask_led0 off
RK3399
- 要先禁止内核中原来的LED驱动,把“heatbeat”功能关闭,执行以下命令即可:
# echo none > /sys/class/leds/firefly\:yellow\:heartbeat/trigger
# echo none > /sys/class/leds/firefly\:yellow\:user/trigger
# echo none > /sys/class/leds/firefly\:red\:power/trigger
- 这样就可以使用我们的驱动程序做实验了:
# insmod 100ask_led.ko
# ./ledtest /dev/100ask_led0 on
# ./ledtest /dev/100ask_led0 off
- 如果想恢复原来的心跳功能,可以执行:
# echo heartbeat > /sys/class/leds/firefly\:yellow\:heartbeat/trigger
# echo heartbeat > /sys/class/leds/firefly\:yellow\:user/trigger
# echo heartbeat > /sys/class/leds/firefly\:red\:power/trigger
课后作业
- a. 在驱动里有ioremap,什么时候执行iounmap?请完善程序
- b. 视频里我们只实现了点一个LED,请修改代码实现操作所有LED
野火/正点原子IMX6ULL的LED驱动程序
- 野火、正点原子用的内核版本是4.1.15,
- 我们用的内核版本是 linux 4.9.88,
- 都是4.x版本,在学习上没有任何差别。
- 你拿到板子后,可以使用他们出厂的系统,
- 也可以根据我们提供的高级用户手册更改为我们的系统。
原理图
所涉及的寄存器操作
野火fire_imx6ull-pro 开发板
- 步骤1:使能GPIO5
/* GPIO5_IO03 */
/* a. 使能GPIO5
* set CCM to enable GPIO5
* CCM_CCGR1[CG15] 0x20C406C
* bit[31:30] = 0b11
*/
/* 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为输出引脚,设置其输出电平
/* 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
/* GPIO1_IO03 */
/* a. 使能GPIO1
* set CCM to enable GPIO1
* CCM_CCGR1[CG13] 0x20C406C
* bit[27:26] = 0b11
*/
/* 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
*/
/* c. 设置GPIO1_IO03作为output引脚
* set GPIO1_GDIR to configure GPIO1_IO03 as output
* GPIO1_GDIR 0x0209C000 + 0x4
* bit[3] = 0b1
*/
/* d. 设置GPIO1_DR输出低电平
* set GPIO1_DR to configure GPIO1_IO03 output 0
* GPIO1_DR 0x0209C000 + 0
* bit[3] = 0b0
*/
/* e. 设置GPIO1_IO03输出高电平
* set GPIO1_DR to configure GPIO1_IO03 output 1
* GPIO1_DR 0x0209C000 + 0
* bit[3] = 0b1
*/
写程序
野火fire_imx6ull-pro开发板
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\02_led_drv\
02_led_drv_for_boards\fire_imx6ull-pro_src_bin
- 硬件相关的文件是board_fire_imx6ull-pro.c,其他文件跟LED框架驱动程序完全一样。
- 它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
100 static struct led_operations board_demo_led_opr = {
101 .num = 1,
102 .init = board_demo_led_init,
103 .ctl = board_demo_led_ctl,
104 };
105
- led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
- 值得关注的是第35~38行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器:
21 static volatile unsigned int *CCM_CCGR1 ;
22 static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
23 static volatile unsigned int *GPIO5_GDIR ;
24 static volatile unsigned int *GPIO5_DR ;
25
26 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
27 {
28 unsigned int val;
29
30 //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
31 if (which == 0)
32 {
33 if (!CCM_CCGR1)
34 {
35 CCM_CCGR1 = ioremap(0x20C406C, 4);
36 IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);
37 GPIO5_GDIR = ioremap(0x020AC000 + 0x4, 4);
38 GPIO5_DR = ioremap(0x020AC000 + 0, 4);
39 }
40
41 /* GPIO5_IO03 */
42 /* a. 使能GPIO5
43 * set CCM to enable GPIO5
44 * CCM_CCGR1[CG15] 0x20C406C
45 * bit[31:30] = 0b11
46 */
47 *CCM_CCGR1 |= (3<<30);
48
49 /* b. 设置GPIO5_IO03用于GPIO
50 * set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3
51 * to configure GPIO5_IO03 as GPIO
52 * IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 0x2290014
53 * bit[3:0] = 0b0101 alt5
54 */
55 val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
56 val &= ~(0xf);
57 val |= (5);
58 *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;
59
60
61 /* b. 设置GPIO5_IO03作为output引脚
62 * set GPIO5_GDIR to configure GPIO5_IO03 as output
63 * GPIO5_GDIR 0x020AC000 + 0x4
64 * bit[3] = 0b1
65 */
66 *GPIO5_GDIR |= (1<<3);
67 }
68
69 return 0;
70 }
71
- led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
72 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
73 {
74 //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
75 if (which == 0)
76 {
77 if (status) /* on: output 0*/
78 {
79 /* d. 设置GPIO5_DR输出低电平
80 * set GPIO5_DR to configure GPIO5_IO03 output 0
81 * GPIO5_DR 0x020AC000 + 0
82 * bit[3] = 0b0
83 */
84 *GPIO5_DR &= ~(1<<3);
85 }
86 else /* off: output 1*/
87 {
88 /* e. 设置GPIO5_IO3输出高电平
89 * set GPIO5_DR to configure GPIO5_IO03 output 1
90 * GPIO5_DR 0x020AC000 + 0
91 * bit[3] = 0b1
92 */
93 *GPIO5_DR |= (1<<3);
94 }
95
96 }
97 return 0;
98 }
99
- 下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
106 struct led_operations *get_board_led_opr(void)
107 {
108 return &board_demo_led_opr;
109 }
110
正点原子Atk_imx6ull-alpha开发板
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\02_led_drv\
02_led_drv_for_boards\atk_imx6ull-alpha_src_bin
- 硬件相关的文件是board_atk_imx6ull-alpha.c,其他文件跟LED框架驱动程序完全一样。
- 它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
100 static struct led_operations board_demo_led_opr = {
101 .num = 1,
102 .init = board_demo_led_init,
103 .ctl = board_demo_led_ctl,
104 };
105
- led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
- 值得关注的是第35~38行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器:
26 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
27 {
28 unsigned int val;
29
30 //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
31 if (which == 0)
32 {
33 if (!CCM_CCGR1)
34 {
35 CCM_CCGR1 = ioremap(0x20C406C, 4);
36 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = ioremap(0x20E0068, 4);
37 GPIO1_GDIR = ioremap(0x0209C000 + 0x4, 4);
38 GPIO1_DR = ioremap(0x0209C000 + 0, 4);
39 }
40
41 /* GPIO1_IO03 */
42 /* a. 使能GPIO1
43 * set CCM to enable GPIO1
44 * CCM_CCGR1[CG13] 0x20C406C
45 * bit[27:26] = 0b11
46 */
47 *CCM_CCGR1 |= (3<<26);
48
49 /* b. 设置GPIO1_IO03用于GPIO
50 * set IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
51 * to configure GPIO1_IO03 as GPIO
52 * IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 0x20E0068
53 * bit[3:0] = 0b0101 alt5
54 */
55 val = *IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
56 val &= ~(0xf);
57 val |= (5);
58 *IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = val;
59
60
61 /* c. 设置GPIO1_IO03作为output引脚
62 * set GPIO1_GDIR to configure GPIO1_IO03 as output
63 * GPIO1_GDIR 0x0209C000 + 0x4
64 * bit[3] = 0b1
65 */
66 *GPIO1_GDIR |= (1<<3);
67 }
68
69 return 0;
70 }
71
- led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
72 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
73 {
74 //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
75 if (which == 0)
76 {
77 if (status) /* on: output 0*/
78 {
79 /* d. 设置GPIO1_DR输出低电平
80 * set GPIO1_DR to configure GPIO1_IO03 output 0
81 * GPIO1_DR 0x0209C000 + 0
82 * bit[3] = 0b0
83 */
84 *GPIO1_DR &= ~(1<<3);
85 }
86 else /* off: output 1*/
87 {
88 /* e. 设置GPIO1_IO03输出高电平
89 * set GPIO1_DR to configure GPIO1_IO03 output 1
90 * GPIO1_DR 0x0209C000 + 0
91 * bit[3] = 0b1
92 */
93 *GPIO1_DR |= (1<<3);
94 }
95
96 }
97 return 0;
98 }
99
- 下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
06 struct led_operations *get_board_led_opr(void)
07 {
08 return &board_demo_led_opr;
09 }
10
上机实验
- 首先设置工具链,然后修改驱动程序Makefile指定内核源码路径,就可以编译驱动程序和测试程序了。
- 启动开发板,挂载NFS文件系统,这样就可以访问到Ubuntu中的文件。
- 最后,就可以在开发板上进行下列测试。
野火fire_imx6ull-pro 开发板
- 注意:如果要使用板子自带的系统,关闭原有LED驱动的方法是类似的,也是进入开发板/sys/class/leds/目录,对于每一个LED在该目录下都有一个子目录,假设某个子目录名为XXX,则执行如下命令:
# echo none > /sys/class/leds/XXX/trigger
- 使用我们的系统时,按如下操作。
- 要先禁止内核中原来的LED驱动,把“heatbeat”功能关闭,执行以下命令即可:
# echo none > /sys/class/leds/cpu/trigger
- 这样就可以使用我们的驱动程序做实验了:
# insmod 100ask_led.ko
#./ledtest /dev/100ask_led0 on
#./ledtest /dev/100ask_led0 off
- 如果想恢复原来的心跳功能,可以执行:
# echo heartbeat > /sys/class/leds/cpu/trigger
正点原子Atk_imx6ull-alpha开发板
- 注意:如果要使用板子自带的系统,关闭原有LED驱动的方法是类似的,也是进入开发板/sys/class/leds/目录,对于每一个LED在该目录下都有一个子目录,假设某个子目录名为XXX,则执行如下命令:
# echo none > /sys/class/leds/XXX/trigger
- 使用我们的系统时,按如下操作。
- 要先禁止内核中原来的LED驱动,把“heatbeat”功能关闭,执行以下命令即可:
# echo none > /sys/class/leds/sys-led/trigger
- 这样就可以使用我们的驱动程序做实验了:
# insmod 100ask_led.ko
# ./ledtest /dev/100ask_led0 on
# ./ledtest /dev/100ask_led0 off
- 如果想恢复原来的心跳功能,可以执行:
# echo heartbeat > /sys/class/leds/sys-led/trigger
课后作业
- a. 在驱动里有ioremap,什么时候执行iounmap?请完善程序
- b. 视频里我们只实现了点一个LED,开发板上也只有一个LED,
- 所以,请修改代码操作蜂鸣器。
百问网IMX6ULL-QEMU的LED驱动程序
- 使用QEMU模拟的硬件,它的硬件资源可以随意扩展。
- 在IMX6ULL QEMU 虚拟开发板上,我们为它设计了4个 LED。
看原理图确定引脚及操作方法
写程序
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\ 02_嵌入式Linux驱动开发基础知识\source\02_led_drv\ 02_led_drv_for_boards\100ask_imx6ull-qemu_src_bin
- 硬件相关的文件是board_100ask_imx6ull-qemu.c,其他文件跟LED框架驱动程序完全一样。
- 涉及的寄存器挺多,一个一个去执行ioremap效率太低。
- 先定义结构体,然后对结构体指针进行ioremap,这些结构体在。
- 对于IOMUXC,可以如下定义:
struct iomux {
volatile unsigned int unnames[23];
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO01;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO02;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO07;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO08;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO09;
};
struct iomux *iomux = ioremap(0x20e0000, sizeof(struct iomux));
- 对于GPIO,可以如下定义:
struct imx6ull_gpio {
volatile unsigned int dr;
volatile unsigned int gdir;
volatile unsigned int psr;
volatile unsigned int icr1;
volatile unsigned int icr2;
volatile unsigned int imr;
volatile unsigned int isr;
volatile unsigned int edge_sel;
};
struct imx6ull_gpio *gpio1 = ioremap(0x209C000, sizeof(struct imx6ull_gpio));
struct imx6ull_gpio *gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
- 开始详细分析board_100ask_imx6ull-qemu.c。
- 它首先构造了一个led_operations结构体,用来表示LED的硬件操作:
176 static struct led_operations board_demo_led_opr = {
177 .num = 4,
178 .init = board_demo_led_init,
179 .ctl = board_demo_led_ctl,
180 };
181
- led_operations结构体中有init函数指针,它指向board_demo_led_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。
- 值得关注的是第61~66行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器:
57 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
58 {
59 if (!CCM_CCGR1)
60 {
61 CCM_CCGR1 = ioremap(0x20C406C, 4);
62 IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);
63
64 iomux = ioremap(0x20e0000, sizeof(struct iomux));
65 gpio1 = ioremap(0x209C000, sizeof(struct imx6ull_gpio));
66 gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
67 }
68
69 if (which == 0)
70 {
71 /* 1. enable GPIO5
72 * CG15, b[31:30] = 0b11
73 */
74 *CCM_CCGR1 |= (3<<30);
75
76 /* 2. set GPIO5_IO03 as GPIO
77 * MUX_MODE, b[3:0] = 0b101
78 */
79 *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = 5;
80
81 /* 3. set GPIO5_IO03 as output
82 * GPIO5 GDIR, b[3] = 0b1
83 */
84 gpio5->gdir |= (1<<3);
85 }
86 else if(which == 1)
87 {
88 /* 1. enable GPIO1
89 * CG13, b[27:26] = 0b11
90 */
91 *CCM_CCGR1 |= (3<<26);
92
93 /* 2. set GPIO1_IO03 as GPIO
94 * MUX_MODE, b[3:0] = 0b101
95 */
96 iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 5;
97
98 /* 3. set GPIO1_IO03 as output
99 * GPIO1 GDIR, b[3] = 0b1
100 */
101 gpio1->gdir |= (1<<3);
102 }
103 else if(which == 2)
104 {
105 /* 1. enable GPIO1
106 * CG13, b[27:26] = 0b11
107 */
108 *CCM_CCGR1 |= (3<<26);
109
110 /* 2. set GPIO1_IO05 as GPIO
111 * MUX_MODE, b[3:0] = 0b101
112 */
113 iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05 = 5;
114
115 /* 3. set GPIO1_IO05 as output
116 * GPIO1 GDIR, b[5] = 0b1
117 */
118 gpio1->gdir |= (1<<5);
119 }
120 else if(which == 3)
121 {
122 /* 1. enable GPIO1
123 * CG13, b[27:26] = 0b11
124 */
125 *CCM_CCGR1 |= (3<<26);
126
127 /* 2. set GPIO1_IO06 as GPIO
128 * MUX_MODE, b[3:0] = 0b101
129 */
130 iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06 = 5;
131
132 /* 3. set GPIO1_IO06 as output
133 * GPIO1 GDIR, b[6] = 0b1
134 */
135 gpio1->gdir |= (1<<6);
136 }
137
138 //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
139 return 0;
140 }
141
- led_operations结构体中有ctl函数指针,它指向board_demo_led_ctl函数,在里面将会根据参数设置LED引脚的输出电平:
142 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
143 {
144 //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
145 if (which == 0)
146 {
147 if (status) /* on : output 0 */
148 gpio5->dr &= ~(1<<3);
149 else /* on : output 1 */
150 gpio5->dr |= (1<<3);
151 }
152 else if (which == 1)
153 {
154 if (status) /* on : output 0 */
155 gpio1->dr &= ~(1<<3);
156 else /* on : output 1 */
157 gpio1->dr |= (1<<3);
158 }
159 else if (which == 2)
160 {
161 if (status) /* on : output 0 */
162 gpio1->dr &= ~(1<<5);
163 else /* on : output 1 */
164 gpio1->dr |= (1<<5);
165 }
166 else if (which == 3)
167 {
168 if (status) /* on : output 0 */
169 gpio1->dr &= ~(1<<6);
170 else /* on : output 1 */
171 gpio1->dr |= (1<<6);
172 }
173 return 0;
174 }
175
- 下面的get_board_led_opr函数供上层调用,给上层提供led_operations结构体:
182 struct led_operations *get_board_led_opr(void)
183 {
184 return &board_demo_led_opr;
185 }
186
上机实验
- 先启动IMX6ULL QEMU模拟器,挂载NFS文件系统。
- 运行QEMU时,
- QEMU内部为主机虚拟出一个网卡, IP为 10.0.2.2,
- IMX6ULL有一个网卡, IP为 10.0.2.15,
- 它连接到主机的虚拟网卡。
- 这样IMX6ULL就可以通过10.0.2.2去访问Ubuntu了。
- 然后执行以下命令安装驱动、执行测试程序:
# insmod 100ask_led.ko
# ./ledtest /dev/100ask_led0 on
# ./ledtest /dev/100ask_led0 off
课后作业
- a. 在驱动里有ioremap,什么时候执行iounmap?请完善程序
- b. 驱动程序中有太多的if判断,请优化程序减少if的使用
驱动设计的思想:面向对象/分层/分离
面向对象
- 字符设备驱动程序抽象出一个file_operations结构体;
- 我们写的程序针对硬件部分抽象出led_operations结构体。
分层
分离
- 还能不能改进?分离。
- 在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。
- ① GPIO1_3属于第1组,即GPIO1。
- ② GPIO5_4属于第5组,即GPIO5。
- 有方向寄存器DIR、数据寄存器DR等,基础地址是addr_base_addr_gpio5。
- 设置为output引脚:修改GPIO5的DIR寄存器的bit4。
- 设置输出电平:修改GPIO5的DR寄存器的bit4。
- ② GPIO5_4属于第5组,即GPIO5。
- 以面向对象的思想,在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即可。
驱动进化之路:总线设备驱动模型
驱动编写的3种方法
- 以LED驱动为例:
总线设备驱动模型
匹配规则
最先比较: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
怎么写程序
分配/设置/注册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模板驱动程序的改造:总线设备驱动模型
写代码
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\ 02_嵌入式Linux驱动开发基础知识\source\ 02_led_drv\04_led_drv_template_bus_dev_drv
注意事项
- 你可以提供一个release函数,如果实在无事可做,把这函数写为空。
- ② EXPORT_SYMBOL
- a.c编译为a.ko,里面定义了func_a;如果它想让b.ko使用该函数,那么a.c里需要导出此函数(如果a.c, b.c都编进内核,则无需导出):
- EXPORT_SYMBOL(led_device_create);
实现platform_device结构体
- board_A.c作为一个可加载模块,里面也有入口函数、出口函数。在入口函数中注册platform_device结构体,在platform_device结构体中指定使用哪个GPIO引脚。
- 首先看入口函数,它调用platform_device_register函数,向内核注册board_A_led_dev结构体:
50 static int __init led_dev_init(void)
51 {
52 int err;
53
54 err = platform_device_register(&board_A_led_dev);
55
56 return 0;
57 }
58
- board_A_led_dev结构体定义如下。
- 在resouces数组中指定了2个引脚(第27~38行);
- 我们还提供了一个空函数led_dev_release(第23~25行),它被赋给board_A_led_dev结构体(第46行),这个函数在卸载platform_device时会被调用,如果不提供的话内核会打印警告信息。
23 static void led_dev_release(struct device *dev)
24 {
25 }
26
27 static struct resource resources[] = {
28 {
29 .start = GROUP_PIN(3,1),
30 .flags = IORESOURCE_IRQ,
31 .name = "100ask_led_pin",
32 },
33 {
34 .start = GROUP_PIN(5,8),
35 .flags = IORESOURCE_IRQ,
36 .name = "100ask_led_pin",
37 },
38 };
39
40
41 static struct platform_device board_A_led_dev = {
42 .name = "100ask_led",
43 .num_resources = ARRAY_SIZE(resources),
44 .resource = resources,
45 .dev = {
46 .release = led_dev_release,
47 },
48 };
49
实现platform_driver结构体
- chip_demo_gpio.c中注册platform_driver结构体,它使用Bus/Dev/Drv模型,当有匹配的platform_device时,它的probe函数就会被调用。
- 在probe函数中所做的事情跟之前的代码没有差别。
- 先看入口函数。
- 第150行向内核注册一个platform_driver结构体;
- 这个结构体的核心在于第140行的chip_demo_gpio_probe函数。
138 static struct platform_driver chip_demo_gpio_driver = {
139 .probe = chip_demo_gpio_probe,
140 .remove = chip_demo_gpio_remove,
141 .driver = {
142 .name = "100ask_led",
143 },
144 };
145
146 static int __init chip_demo_gpio_drv_init(void)
147 {
148 int err;
149
150 err = platform_driver_register(&chip_demo_gpio_driver);
151 register_led_operations(&board_demo_led_opr);
152
153 return 0;
154 }
155
- chip_demo_gpio_probe函数代码如下。
- 第107行:从匹配的platform_device中获取资源,确定GPIO引脚。
- 第111行:把引脚记录下来,在操作硬件时要用。
- 第112行:新发现了一个GPIO引脚,就调用上层驱动的代码创建设备节点。
100 static int chip_demo_gpio_probe(struct platform_device *pdev)
101 {
102 struct resource *res;
103 int i = 0;
104
105 while (1)
106 {
107 res = platform_get_resource(pdev, IORESOURCE_IRQ, i++);
108 if (!res)
109 break;
110
111 g_ledpins[g_ledcnt] = res->start;
112 led_class_create_device(g_ledcnt);
113 g_ledcnt++;
114 }
115 return 0;
116
117 }
118
- 操作硬件的代码如下,第31、63行的代码里用到了数组g_ledpins,里面的值来自platform_device,在probe函数中根据platform_device的资源确定了引脚:
23 static int g_ledpins[100];
24 static int g_ledcnt = 0;
25
26 static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
27 {
28 //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
29
30 printk("init gpio: group %d, pin %d\n", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
31 switch(GROUP(g_ledpins[which]))
32 {
33 case 0:
34 {
35 printk("init pin of group 0 ...\n");
36 break;
37 }
38 case 1:
39 {
40 printk("init pin of group 1 ...\n");
41 break;
42 }
43 case 2:
44 {
45 printk("init pin of group 2 ...\n");
46 break;
47 }
48 case 3:
49 {
50 printk("init pin of group 3 ...\n");
51 break;
52 }
53 }
54
55 return 0;
56 }
57
58 static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
59 {
60 //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
61 printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
62
63 switch(GROUP(g_ledpins[which]))
64 {
65 case 0:
66 {
67 printk("set pin of group 0 ...\n");
68 break;
69 }
70 case 1:
71 {
72 printk("set pin of group 1 ...\n");
73 break;
74 }
75 case 2:
76 {
77 printk("set pin of group 2 ...\n");
78 break;
79 }
80 case 3:
81 {
82 printk("set pin of group 3 ...\n");
83 break;
84 }
85 }
86
87 return 0;
88 }
89
90 static struct led_operations board_demo_led_opr = {
91 .init = board_demo_led_init,
92 .ctl = board_demo_led_ctl,
93 };
94
95 struct led_operations *get_board_led_opr(void)
96 {
97 return &board_demo_led_opr;
98 }
99
课后作业
- 完善半成品程序:04_led_drv_template_bus_dev_drv_unfinished。
- 请仿照本节提供的程序(位于04_led_drv_template_bus_dev_drv目录),改造你所用的单板的LED驱动程序。
驱动进化之路:设备树的引入及简明教程
- 官方文档(可以下载到devicetree-specification-v0.2.pdf): https://www.devicetree.org/specifications/
- 内核文档:
Documentation/devicetree/booting-without-of.txt
- 我录制“设备树视频”时写的文档:设备树详细分析.txt
- 这个txt文件也同步上传到wiki了:http://wiki.100ask.org/Linux_devicetree
- 我录制的设备树视频,它是基于s3c2440的,用的是linux 4.19;需要深入研究的可以看该视频(收费)。
- 注意,如果只是想入门,看本文档及视频即可。
设备树的引入与作用
- 以LED驱动为例,如果你要更换LED所用的GPIO引脚,需要修改驱动程序源码、重新编译驱动、重新加载驱动。
- 在内核中,使用同一个芯片的板子,它们所用的外设资源不一样,比如A板用GPIO A,B板用GPIO B。而GPIO的驱动程序既支持GPIO A也支持GPIO B,你需要指定使用哪一个引脚,怎么指定?在c代码中指定。
- 随着ARM芯片的流行,内核中针对这些ARM板保存有大量的、没有技术含量的文件。
- Linus大发雷霆:"this whole ARM thing is a f*cking pain in the ass"。
- 于是,Linux内核开始引入设备树。
- 设备树并不是重新发明出来的,在Linux内核中其他平台如PowerPC,早就使用设备树来描述硬件了。
- Linus发火之后,内核开始全面使用设备树来改造,神人就神人。
- 有一种错误的观点,说“新驱动都是用设备树来写了”。
- 设备树不可能用来写驱动。
- 请想想,要操作硬件就需要去操作复杂的寄存器,如果设备树可以操作寄存器,那么它就是“驱动”,它就一样很复杂。
- 设备树只是用来给内核里的驱动程序,指定硬件的信息。比如LED驱动,在内核的驱动程序里去操作寄存器,但是操作哪一个引脚?这由设备树指定。
- 你可以事先体验一下设备树,板子启动后执行下面的命令:
# ls /sys/firmware/
devicetree fdt
- /sys/firmware/devicetree目录下是以目录结构程现的dtb文件, 根节点对应base目录, 每一个节点对应一个目录, 每一个属性对应一个文件。
- 这些属性的值如果是字符串,可以使用cat命令把它打印出来;对于数值,可以用hexdump把它打印出来。
- 一个单板启动时,u-boot先运行,它的作用是启动内核。U-boot会把内核和设备树文件都读入内存,然后启动内核。在启动内核时会把设备树在内存中的地址告诉内核。
设备树的语法
- 怎么描述这棵树?
- 我们需要编写设备树文件(dts: device tree source),它需要编译为dtb(device tree blob)文件,内核使用的是dtb文件。
- dts文件是根本,它的语法很简单。
- 下面是一个设备树示例:
- 它对应的dts文件如下:
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";
};
reg
- reg的本意是register,用来表示寄存器地址。
- 但是在设备树里,它可以用来描述一段空间。反正对于ARM系统,寄存器和内存是统一编址的,即访问寄存器时用某块地址,访问内存时用某块地址,在访问方法上没有区别。
- reg属性的值,是一系列的“address size”,用多少个32位的数来表示address和size,由其父节点的#address-cells、#size-cells决定。
- 示例:
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
memory {
reg = <0x80000000 0x20000000>;
};
};
name(过时了,建议不用)
- 它的值是字符串,用来表示节点的名字。在跟platform_driver匹配时,优先级最低。
- compatible属性在匹配过程中,优先级最高。
device_type(过时了,建议不用)
- 它的值是字符串,用来表示节点的类型。在跟platform_driver匹配时,优先级为中。
- compatible属性在匹配过程中,优先级最高。
常用的节点(node)
根节点
- dts文件中必须有一个根节点:
/dts-v1/;
/ {
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <1>;
#size-cells = <1>;
};
- 根节点中必须有这些属性:
#address-cells // 在它的子节点的reg属性中, 使用多少个u32整数来描述地址(address)
#size-cells // 在它的子节点的reg属性中, 使用多少个u32整数来描述大小(size)
compatible // 定义一系列的字符串, 用来指定内核中哪个machine_desc可以支持本设备
// 即这个板子兼容哪些平台
// uImage : smdk2410 smdk2440 mini2440 ==> machine_desc
model // 咱这个板子是什么
// 比如有2款板子配置基本一致, 它们的compatible是一样的
// 那么就通过model来分辨这2款板子
CPU节点
- 一般不需要我们设置,在dtsi文件中都定义好了:
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
.......
}
};
memory节点
- 芯片厂家不可能事先确定你的板子使用多大的内存,所以memory节点需要板厂设置,比如:
memory {
reg = <0x80000000 0x20000000>;
};
chosen节点
- 我们可以通过设备树文件给内核传入一些参数,这要在chosen节点中设置bootargs属性:
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
编译、更换设备树
- 我们一般不会从零写dts文件,而是修改。程序员水平有高有低,改得对不对?需要编译一下。并且内核直接使用dts文件的话,就太低效了,它也需要使用二进制格式的dtb文件。
在内核中直接make
- 设置ARCH、CROSS_COMPILE、PATH这三个环境变量后,进入ubuntu上板子内核源码的目录,执行如下命令即可编译dtb文件:
make dtbs V=1
- 这些操作步骤在各个开发板的高级用户使用手册,或是http://wiki.100ask.net中各个板子的页面里,都有说明。
- 以野火的IMX6UL为例,可以看到如下输出:
mkdir -p arch/arm/boot/dts/ ;
arm-linux-gnueabihf-gcc -E
-Wp,-MD,arch/arm/boot/dts/.imx6ull-14x14-ebf-mini.dtb.d.pre.tmp
-nostdinc
-I./arch/arm/boot/dts
-I./arch/arm/boot/dts/include
-I./drivers/of/testcase-data
-undef -D__DTS__ -x assembler-with-cpp
-o arch/arm/boot/dts/.imx6ull-14x14-ebf-mini.dtb.dts.tmp
arch/arm/boot/dts/imx6ull-14x14-ebf-mini.dts ;
./scripts/dtc/dtc -O dtb
-o arch/arm/boot/dts/imx6ull-14x14-ebf-mini.dtb
-b 0 -i arch/arm/boot/dts/ -Wno-unit_address_vs_reg
-d arch/arm/boot/dts/.imx6ull-14x14-ebf-mini.dtb.d.dtc.tmp
arch/arm/boot/dts/.imx6ull-14x14-ebf-mini.dtb.dts.tmp ;
- 它首先用arm-linux-gnueabihf-gcc预处理dts文件,把其中的.h头文件包含进来,把宏展开。
- 然后使用scripts/dtc/dtc生成dtb文件。
- 可见,dts文件之所以支持“#include”语法,是因为arm-linux-gnueabihf-gcc帮忙。
- 如果只用dtc工具,它是不支持”#include”语法的,只支持“/include”语法。
手工编译
- 除非你对设备树比较了解,否则不建议手工使用dtc工具直接编译。
- 内核目录下scripts/dtc/dtc是设备树的编译工具,直接使用它的话,包含其他文件时不能使用“#include”,而必须使用“/incldue”。
- 编译、反编译的示例命令如下,“-I”指定输入格式,“-O”指定输出格式,“-o”指定输出文件:
./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts // 编译dts为dtb
./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb // 反编译dtb为dts
给开发板更换设备树文件
- 怎么给各个单板编译出设备树文件,它们的设备树文件是哪一个?
- 这些操作步骤在各个开发板的高级用户使用手册,或是http://wiki.100ask.net中各个板子的页面里,都有说明。
- 基本方法都是:设置ARCH、CROSS_COMPILE、PATH这三个环境变量后,在内核源码目录中执行:
make dtbs
对于100ask-am335x 单板
- 设备树文件是:内核源码目录中arch/arm/boot/dts/100ask-am335x.dtb
- 要更换板子上的设备树文件,启动板子后,更换这个文件:/boot/mx6ull-14x14-ebf.dtb
对于firefly-rk3288
- 设备树文件是:内核源码目录中arch/arm/boot/dts/rk3288-firefly.dtb
- 对于这款板子,本教程中我们使用SD卡上的系统。
- 要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/rk3288-firefly.dtb
对于firefly的roc-rk3399-pc
- 设备树文件是:内核源码目录中arch/arm64/boot/dts/rk3399-roc-pc.dtb
- 对于这款板子,本教程中我们使用SD卡上的系统。
- 要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/ rk3399-roc-pc.dtb
对于百问网使用QEMU模拟的IMX6ULL板子
- 设备树文件是:内核源码目录中arch/arm/boot/dts/100ask_imx6ul_qemu.dtb
- 它是执行qemu时直接在命令行中指定设备树文件的,你可以打开脚本文件qemu-imx6ul-gui.sh找到dtb文件的位置,然后使用新编译出来的dtb去覆盖老文件。
对于野火imx6ull-pro
- 设备树文件是:内核源码目录中arch/arm/boot/dts/imx6ull-14x14-ebf.dtb
- 对于这款板子,本教程中我们使用SD卡上的系统。
- 要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/imx6ull-14x14-ebf.dtb
对于正点原子imx6ull-alpha
- 设备树文件是:内核源码目录中arch/arm/boot/dts/imx6ull-14x14-alpha.dtb
- 对于这款板子,本教程中我们使用SD卡上的系统。
- 要更换板上的设备树文件,你可以使用SD卡启动开发板后,更换这个文件:/boot/arch/arm/boot/dts/imx6ull-14x14-alpha.dtb
板子启动后查看设备树
- 板子启动后执行下面的命令:
# ls /sys/firmware/
devicetree fdt
- /sys/firmware/devicetree目录下是以目录结构程现的dtb文件, 根节点对应base目录, 每一个节点对应一个目录, 每一个属性对应一个文件。
- 这些属性的值如果是字符串,可以使用cat命令把它打印出来;对于数值,可以用hexdump把它打印出来。
- 还可以看到/sys/firmware/fdt文件,它就是dtb格式的设备树文件,可以把它复制出来放到ubuntu上,执行下面的命令反编译出来(-I dtb:输入格式是dtb,-O dts:输出格式是dts):
cd 板子所用的内核源码目录
./scripts/dtc/dtc -I dtb -O dts /从板子上/复制出来的/fdt -o tmp.dts
内核对设备树的处理
- 从源代码文件dts文件开始,设备树的处理过程为:
哪些设备树节点会被转换为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函数。
- 套路是一样的。
- 我们需要将前面讲过的“匹配规则”再完善一下:
- 先贴源码:
最先比较:是否强制选择某个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。
- 它的类型如下:
- 如果一个platform_driver支持设备树,它的platform_driver.driver.of_match_table是一个数组,类型如下:
- 使用设备树信息来判断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。
没有转换为platform_device的节点,如何使用
- 任意驱动程序里,都可以直接访问设备树。
- 你可以使用“11.7”节中介绍的函数找到节点,读出里面的值。
内核里操作设备树的常用函数
- 内核源码中include/linux/目录下有很多of开头的头文件,of表示“open firmware”即开放固件。
内核中设备树相关的头文件介绍
- 内核源码中include/linux/目录下有很多of开头的头文件,of表示“open firmware”即开放固件。
- 设备树的处理过程是:dtb -> device_node -> platform_device
处理DTB
of_fdt.h // dtb文件的相关操作函数, 我们一般用不到,
// 因为dtb文件在内核中已经被转换为device_node树(它更易于使用)
处理device_node
of.h // 提供设备树的一般处理函数,
// 比如 of_property_read_u32(读取某个属性的u32值),
// of_get_child_count(获取某个device_node的子节点数)
of_address.h // 地址相关的函数,
// 比如 of_get_address(获得reg属性中的addr, size值)
// of_match_device (从matches数组中取出与当前设备最匹配的一项)
of_dma.h // 设备树中DMA相关属性的函数
of_gpio.h // GPIO相关的函数
of_graph.h // GPU相关驱动中用到的函数, 从设备树中获得GPU信息
of_iommu.h // 很少用到
of_irq.h // 中断相关的函数
of_mdio.h // MDIO (Ethernet PHY) API
of_net.h // OF helpers for network devices.
of_pci.h // PCI相关函数
of_pdt.h // 很少用到
of_reserved_mem.h // reserved_mem的相关函数
处理 platform_device
of_platform.h // 把device_node转换为platform_device时用到的函数,
// 比如of_device_alloc(根据device_node分配设置platform_device),
// of_find_device_by_node (根据device_node查找到platform_device),
// of_platform_bus_probe (处理device_node及它的子节点)
of_device.h // 设备相关的函数, 比如 of_match_device
platform_device相关的函数
- of_platform.h中声明了很多函数,但是作为驱动开发者,我们只使用其中的1、2个。其他的都是给内核自己使用的,内核使用它们来处理设备树,转换得到platform_device。
of_find_device_by_node
- 函数原型为:
extern struct platform_device *of_find_device_by_node(struct device_node *np);
- 设备树中的每一个节点,在内核里都有一个device_node;你可以使用device_node去找到对应的platform_device。
platform_get_resource
- 这个函数跟设备树没什么关系,但是设备树中的节点被转换为platform_device后,设备树中的reg属性、interrupts属性也会被转换为“resource”。
- 这时,你可以使用这个函数取出这些资源。
- 函数原型为:
/**
* platform_get_resource - get a resource for a device
* @dev: platform device
* @type: resource type // 取哪类资源?IORESOURCE_MEM、IORESOURCE_REG
* // IORESOURCE_IRQ等
* @num: resource index // 这类资源中的哪一个?
*/
struct resource *platform_get_resource(struct platform_device *dev,
unsigned int type, unsigned int num);
- 对于设备树节点中的reg属性,它属性IORESOURCE_MEM类型的资源;
- 对于设备树节点中的interrupts属性,它属性IORESOURCE_IRQ类型的资源。
有些节点不会生成platform_device,怎么访问它们
- 内核会把dtb文件解析出一系列的device_node结构体,我们可以直接访问这些device_node。
- 内核源码incldue/linux/of.h中声明了device_node和属性property的操作函数,device_node和property的结构体定义如下:
找到节点
- a. of_find_node_by
- 根据路径找到节点,比如“/”就对应根节点,“/memory”对应memory节点。
- 函数原型:
static inline struct device_node *of_find_node_by_path(const char *path);
- b. of_find_node_by_name
- 根据名字找到节点,节点如果定义了name属性,那我们可以根据名字找到它。
- 函数原型:
extern struct device_node *of_find_node_by_name(struct device_node *from,
const char *name);
- 参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
- 但是在设备树的官方规范中不建议使用“name”属性,所以这函数也不建议使用。
- c. of_find_node_by_type
- 根据类型找到节点,节点如果定义了device_type属性,那我们可以根据类型找到它。
- 函数原型:
extern struct device_node *of_find_node_by_type(struct device_node *from,
const char *type);
- 参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
- 但是在设备树的官方规范中不建议使用“device_type”属性,所以这函数也不建议使用。
- d. of_find_compatible_node
- 根据compatible找到节点,节点如果定义了compatible属性,那我们可以根据compatible属性找到它。
- 函数原型:
extern struct device_node *of_find_compatible_node(struct device_node *from,
const char *type, const char *compat);
- 参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
- 参数compat是一个字符串,用来指定compatible属性的值;
- 参数type是一个字符串,用来指定device_type属性的值,可以传入NULL。
- e. of_find_node_by_phandle
- 根据phandle找到节点。
- dts文件被编译为dtb文件时,每一个节点都有一个数字ID,这些数字ID彼此不同。可以使用数字ID来找到device_node。这些数字ID就是phandle。
- 函数原型:
extern struct device_node *of_find_node_by_phandle(phandle handle);
- 参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
- f. of_get_parent
- 找到device_node的父节点。
- 函数原型:
extern struct device_node *of_get_parent(const struct device_node *node);
- 参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
- g. of_get_next_parent
- 这个函数名比较奇怪,怎么可能有“next parent”?
- 它实际上也是找到device_node的父节点,跟of_get_parent的返回结果是一样的。
- 差别在于它多调用下列函数,把node节点的引用计数减少了1。这意味着调用of_get_next_parent之后,你不再需要调用of_node_put释放node节点。
of_node_put(node);
- 函数原型:
extern struct device_node *of_get_next_parent(struct device_node *node);
- 参数from表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
- h. of_get_next_child
- 取出下一个子节点。
- 函数原型:
extern struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev);
- 参数node表示父节点;
- prev表示上一个子节点,设为NULL时表示想找到第1个子节点。
- 不断调用of_get_next_child时,不断更新pre参数,就可以得到所有的子节点。
- i. of_get_next_available_child
- 取出下一个“可用”的子节点,有些节点的status是“disabled”,那就会跳过这些节点。
- 函数原型:
struct device_node *of_get_next_available_child(const struct device_node *node,
struct device_node *prev);
- 参数node表示父节点;
- prev表示上一个子节点,设为NULL时表示想找到第1个子节点。
- j. of_get_child_by_name
- 根据名字取出子节点。
- 函数原型:
extern struct device_node *of_get_child_by_name(const struct device_node *node,
const char *name);
- 参数node表示父节点;
- name表示子节点的名字。
找到属性
- 内核源码incldue/linux/of.h中声明了device_node的操作函数,当然也包括属性的操作函数。
- a. of_find_property
- 找到节点中的属性。
- 函数原型:
extern struct property *of_find_property(const struct device_node *np,
const char *name,
int *lenp);
- 参数np表示节点,我们要在这个节点中找到名为name的属性。
- lenp用来保存这个属性的长度,即它的值的长度。
- 在设备树中,节点大概是这样:
xxx_node {
xxx_pp_name = “hello”;
};
- 上述节点中,“xxx_pp_name”就是属性的名字,值的长度是6。
获取属性的值
- a. of_get_property
- 根据名字找到节点的属性,并且返回它的值。
- 函数原型:
/*
* Find a property with a given name for a given node
* and return the value.
*/
const void *of_get_property(const struct device_node *np, const char *name,
int *lenp)
- 参数np表示节点,我们要在这个节点中找到名为name的属性,然后返回它的值。
- lenp用来保存这个属性的长度,即它的值的长度。
- b. of_property_count_elems_of_size
- 根据名字找到节点的属性,确定它的值有多少个元素(elem)。
- 函数原型:
/* of_property_count_elems_of_size - Count the number of elements in a property
*
* @np: device node from which the property value is to be read.
* @propname: name of the property to be searched.
* @elem_size: size of the individual element
*
* Search for a property in a device node and count the number of elements of
* size elem_size in it. Returns number of elements on sucess, -EINVAL if the
* property does not exist or its length does not match a multiple of elem_size
* and -ENODATA if the property does not have a value.
*/
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname, int elem_size)
- 参数np表示节点,我们要在这个节点中找到名为propname的属性,然后返回下列结果:
return prop->length / elem_size;
- 在设备树中,节点大概是这样:
xxx_node {
xxx_pp_name = <0x50000000 1024> <0x60000000 2048>;
};
- 调用of_property_count_elems_of_size(np, “xxx_pp_name”, 8)时,返回值是2;
- 调用of_property_count_elems_of_size(np, “xxx_pp_name”, 4)时,返回值是4。
- c. 读整数u32/u64
- 函数原型为:
static inline int of_property_read_u32(const struct device_node *np,
const char *propname,
u32 *out_value);
extern int of_property_read_u64(const struct device_node *np,
const char *propname, u64 *out_value);
- 在设备树中,节点大概是这样:
xxx_node {
name1 = <0x50000000>;
name2 = <0x50000000 0x60000000>;
};
- 调用of_property_read_u32 (np, “name1”, &val)时,val将得到值0x50000000;
- 调用of_property_read_u64 (np, “name2”, &val)时,val将得到值0x0x6000000050000000。
- d. 读某个整数u32/u64
- 函数原型为:
extern int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index, u32 *out_value);
- 在设备树中,节点大概是这样:
xxx_node {
name2 = <0x50000000 0x60000000>;
};
- 调用of_property_read_u32 (np, “name2”, 1, &val)时,val将得到值0x0x60000000。
- e. 读数组
- 函数原型为:
int of_property_read_variable_u8_array(const struct device_node *np,
const char *propname, u8 *out_values,
size_t sz_min, size_t sz_max);
int of_property_read_variable_u16_array(const struct device_node *np,
const char *propname, u16 *out_values,
size_t sz_min, size_t sz_max);
int of_property_read_variable_u32_array(const struct device_node *np,
const char *propname, u32 *out_values,
size_t sz_min, size_t sz_max);
int of_property_read_variable_u64_array(const struct device_node *np,
const char *propname, u64 *out_values,
size_t sz_min, size_t sz_max);
- 在设备树中,节点大概是这样:
xxx_node {
name2 = <0x50000012 0x60000034>;
};
- 上述例子中属性name2的值,长度为8。
- 调用of_property_read_variable_u8_array (np, “name2”, out_values, 1, 10)时,out_values中将会保存这8个字节: 0x12,0x00,0x00,0x50,0x34,0x00,0x00,0x60。
- 调用of_property_read_variable_u16_array (np, “name2”, out_values, 1, 10)时,out_values中将会保存这4个16位数值: 0x0012, 0x5000,0x0034,0x6000。
- 总之,这些函数要么能取到全部的数值,要么一个数值都取不到;
- 如果值的长度在sz_min和sz_max之间,就返回全部的数值;否则一个数值都不返回。
- f. 读字符串
- 函数原型为:
int of_property_read_string(const struct device_node *np, const char *propname,
const char **out_string);
- 返回节点np的属性(名为propname)的值,(*out_string)指向这个值,把它当作字符串。
怎么修改设备树文件
- 一个写得好的驱动程序, 它会尽量确定所用资源。
- 只把不能确定的资源留给设备树, 让设备树来指定。
- 根据原理图确定"驱动程序无法确定的硬件资源", 再在设备树文件中填写对应内容。
- 那么, 所填写内容的格式是什么?
使用芯片厂家提供的工具
- 有些芯片,厂家提供了对应的设备树生成工具,可以选择某个引脚用于某些功能,就可以自动生成设备树节点。
- 你再把这些节点复制到内核的设备树文件里即可。
看绑定文档
- 内核文档 Documentation/devicetree/bindings/
- 做得好的厂家也会提供设备树的说明文档
参考同类型单板的设备树文件
网上搜索
实在没办法时, 只能去研究驱动源码
LED模板驱动程序的改造:设备树
总结3种写驱动程序的方法
怎么使用设备树写驱动程序
设备树节点要与platform_driver能匹配
设备树节点指定资源,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
课后作业
- 请仿照本节提供的程序(位于05_led_drv_template_device_tree目录),改造你所用的单板的LED驱动程序。
APP怎么读取按键值
- APP读取按键值,需要有按键驱动程序。
- 为什么要讲按键驱动程序?
- APP去读按键的方法有4种:
- ① 查询方式
- ② 休眠-唤醒方式
- ③ poll方式
- ④ 异步通知方式
- 通过这4种方式的学习,我们可以掌握如下知识:
- ① 驱动的基本技能:中断、休眠、唤醒、poll等机制。
- 这些基本技能是驱动开发的基础,其他大型驱动复杂的地方是它的框架及设计思想,但是基本技术就这些。
- ② APP开发的基本技能:阻塞 、非阻塞、休眠、poll、异步通知。
- ① 驱动的基本技能:中断、休眠、唤醒、poll等机制。
- 通过这4种方式的学习,我们可以掌握如下知识:
妈妈怎么知道孩子醒了
- 妈妈怎么知道卧室里小孩醒了?
- ① 时时进房间看一下:查询方式
- 简单,但是累
- ② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒
- 不累,但是妈妈干不了活了
- ③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式
- 要浪费点时间,但是可以继续干活。
- 妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
- ④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知
- 妈妈、小孩互不耽误。
- ① 时时进房间看一下:查询方式
- 这4种方法没有优劣之分,在不同的场合使用不同的方法。
APP读取按键的4种方法
- 跟上述生活场景类似,APP去读取按键也有4种方法:
- ① 查询方式
- ② 休眠-唤醒方式
- ③ poll方式
- ④ 异步通知方式
- 第2、3、4种方法,都涉及中断服务程序。中断,就像小孩醒了会哭闹一样,中断不经意间到来,它会做某些事情:唤醒APP、向APP发信号。
- 所以,在按键驱动程序中,中断是核心。
- 实际上,中断无论是在单片机还是在Linux中都很重要。在Linux中,中断的知识还涉及进程、线程等。
查询方式
- 驱动程序中构造、注册一个file_operations结构体,里面提供有对应的open,read函数。APP调用open时,导致驱动中对应的open函数被调用,在里面配置GPIO为输入引脚。APP调用read时,导致驱动中对应的read函数被调用,它读取寄存器,把引脚状态直接返回给APP。
休眠-唤醒方式
- 驱动程序中构造、注册一个file_operations结构体,里面提供有对应的open,read函数。
- APP调用open时,导致驱动中对应的open函数被调用,在里面配置GPIO为输入引脚;并且注册GPIO的中断处理函数。
- APP调用read时,导致驱动中对应的read函数被调用,如果有按键数据则直接返回给APP;否则APP在内核态休眠。
- 当用户按下按键时,GPIO中断被触发,导致驱动程序之前注册的中断服务程序被执行。它会记录按键数据,并唤醒休眠中的APP。
- APP被唤醒后继续在内核态运行,即继续执行驱动代码,把按键数据返回给APP(的用户空间)。
poll方式
- 驱动程序中构造、注册一个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函数被调用,这时驱动程序中含有数据,会直接返回数据。
异步通知方式
异步通知的原理:发信号
- 异步通知的实现原理是:内核给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驱动回顾
按键驱动编写思路
编程:先写框架
- 我们的目的写出一个容易扩展到各种芯片、各种板子的按键驱动程序,所以驱动程序分为上下两层:
- ① button_drv.c分配/设置/注册file_operations结构体
- 起承上启下的作用,向上提供button_open,button_read供APP调用。
- 而这2个函数又会调用底层硬件提供的p_button_opr中的init、read函数操作硬件。
- ② board_xxx.c分配/设置/注册button_operations结构体
- 这个结构体是我们自己抽象出来的,里面定义单板xxx的按键操作函数。
- ① button_drv.c分配/设置/注册file_operations结构体
- 使用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的按键驱动程序(查询方式)
先看原理图确定引脚及操作方法
再看芯片手册确定寄存器及操作方法
- 步骤2:把GPIO1_25对应的引脚设置为GPIO模式
- 要用哪一个寄存器来把GPIO1_25对应的引脚设置为GPIO模式?
编程
程序框架
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\
04_button_drv\02_button_drv_for_boards\01_button_drv_for_am335x
硬件相关的代码
- 主要看board_am335x.c,先看它的入口函数,代码如下。
- 第84行向上层驱动注册一个button_operations结构体,该结构体在第76~80行定义。
76 static struct button_operations my_buttons_ops = {
77 .count = 1,
78 .init = board_am335x_button_init,
79 .read = board_am335x_button_read,
80 };
81
82 int board_am335x_button_drv_init(void)
83 {
84 register_button_operations(&my_buttons_ops);
85 return 0;
86 }
87
- button_operations结构体中有init函数指针,它指向board_am335x_button_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。代码如下。
- 值得关注的是第32~35行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器。
21 static volatile unsigned int *CM_PER_GPIO1_CLKCTRL;
22 static volatile unsigned int *conf_gpmc_a9;
23 static volatile unsigned int *GPIO1_OE;
24 static volatile unsigned int *GPIO1_DATAIN;
25
26 static void board_am335x_button_init (int which) /* 初始化button, which-哪个button */
27 {
28 if (which == 0)
29 {
30 if (!CM_PER_GPIO1_CLKCTRL)
31 {
32 CM_PER_GPIO1_CLKCTRL = ioremap(0x44E00000 + 0xAC, 4);
33 conf_gpmc_a9 = ioremap(0x44E10000 + 0x864, 4);
34 GPIO1_OE = ioremap(0x4804C000 + 0x134, 4);
35 GPIO1_DATAIN = ioremap(0x4804C000 + 0x138, 4);
36 }
37
38 //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
39 /* a. 使能GPIO1
40 * set PRCM to enalbe GPIO1
41 * set CM_PER_GPIO1_CLKCTRL (0x44E00000 + 0xAC)
42 * val: (1<<18) | 0x2
43 */
44 *CM_PER_GPIO1_CLKCTRL = (1<<18) | 0x2;
45
46 /* b. 设置GPIO1_25的功能,让它工作于GPIO模式
47 * set Control Module to set GPIO1_25 (U16) used as GPIO
48 * conf_gpmc_a9 as mode 7
49 * addr : 0x44E10000 + 0x864
50 * bit[5] : 1, Input enable value for the PAD
51 * bit[2:0] : mode 7
52 */
53 *conf_gpmc_a9 = (1<<5) | 7;
54
55 /* c. 设置GPIO1_25的方向,让它作为输入引脚
56 * set GPIO1's registers , to set 设置GPIO1_25的方向'S dir (input)
57 * GPIO_OE
58 * addr : 0x4804C000 + 0x134
59 * set bit 25
60 */
61
62 *GPIO1_OE |= (1<<25);
63 }
64
65 }
66
- button_operations结构体中还有有read函数指针,它指向board_am335x_button_read函数,在里面将会读取并返回按键引脚的电平。代码如下。
67 static int board_am335x_button_read (int which) /* 读button, which-哪个 */
68 {
69 printk("%s %s line %d, button %d, 0x%x\n", __FILE__, __FUNCTION__, __LINE__, which, *GPIO1_DATAIN);
70 if (which == 0)
71 return (*GPIO1_DATAIN & (1<<25)) ? 1 : 0;
72 else
73 return 0;
74 }
75
测试
- 安装驱动程序之后执行测试程序,观察它的返回值(执行测试程序的同时操作按键):
# insmod button_drv.ko
# insmod board_am335x.ko
# ./button_test /dev/100ask_button0
课后作业
- ① 修改board_am335x.c,增加更多按键
- ② 修改button_test.c,使用按键来点灯
RK3288的按键驱动程序(查询方式)
先看原理图确定引脚及操作方法
- Firefly的RK3288开发板上没有按键,我们为它制作的扩展板上有1个按键。在扩展板原理图rk3288_extend_v12_0715.pdf中可以看到按键,如下:
- 平时按键电平为高,按下按键后电平为低。
- 按键引脚为GPIO7_B1。
再看芯片手册确定寄存器及操作方法
- 步骤2:把GPIO7_B1对应的引脚设置为GPIO模式
编程
程序框架
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\
04_button_drv\02_button_drv_for_boards\02_button_drv_for_rk3288
硬件相关的代码
- 主要看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中可以看到按键,如下:
- 平时按键电平为高,按下按键后电平为低。
- 按键引脚为GPIO0_B1、GPIO0_B2、GPIO0_B4。
- 本视频中,只操作一个按键:GPIO0_B1。
再看芯片手册确定寄存器及操作方法
- 步骤2:把GPIO0_B1对应的引脚设置为GPIO模式
- 步骤3:设置GPIO0内部寄存器,把GPIO0_B1设置为输入引脚,读数据寄存器
编程
程序框架
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\
04_button_drv\02_button_drv_for_boards\03_button_drv_for_rk3399
硬件相关的代码
- 主要看board_rk3399.c,先看它的入口函数,代码如下。
- 第81行向上层驱动注册一个button_operations结构体,该结构体在第73~77行定义。
73 static struct button_operations my_buttons_ops = {
74 .count = 1,
75 .init = board_rk3399_button_init,
76 .read = board_rk3399_button_read,
77 };
78
79 int board_rk3399_button_drv_init(void)
80 {
81 register_button_operations(&my_buttons_ops);
82 return 0;
83 }
- button_operations结构体中有init函数指针,它指向board_rk3399_button_init函数,在里面将会初始化LED引脚:使能、设置为GPIO模式、设置为输出引脚。代码如下。
- 值得关注的是第32~35行,对于寄存器要先使用ioremap得到它的虚拟地址,以后使用虚拟地址访问寄存器。
21 static volatile unsigned int *PMUCRU_CLKGATE_CON1;
22 static volatile unsigned int *GRF_GPIO0B_IOMUX ;
23 static volatile unsigned int *GPIO0_SWPORTA_DDR;
24 static volatile unsigned int *GPIO0_EXT_PORTA ;
25
26 static void board_rk3399_button_init (int which) /* 初始化button, which-哪个button */
27 {
28 if (which == 0)
29 {
30 if (!PMUCRU_CLKGATE_CON1)
31 {
32 PMUCRU_CLKGATE_CON1 = ioremap(0xFF760000+ 0x0104, 4);
33 GRF_GPIO0B_IOMUX = ioremap(0xFF310000+0x0004, 4);
34 GPIO0_SWPORTA_DDR = ioremap(0xFF720000 + 0x0004, 4);
35 GPIO0_EXT_PORTA = ioremap(0xFF720000 + 0x0050, 4);
36 }
37
38 /* rk3399 GPIO0_B1 */
39 /* a. 使能GPIO0
40 * set CRU to enable GPIO0
41 * PMUCRU_CLKGATE_CON1 0xFF760000+ 0x0104
42 * (1<<(3+16)) | (0<<3)
43 */
44 *PMUCRU_CLKGATE_CON1 = (1<<(3+16)) | (0<<3);
45
46 /* b. 设置GPIO0_B1用于GPIO
47 * set PMU/GRF to configure GPIO0_B1 as GPIO
48 * GRF_GPIO0B_IOMUX 0xFF310000+0x0004
49 * bit[3:2] = 0b00
50 * (3<<(2+16)) | (0<<2)
51 */
52 *GRF_GPIO0B_IOMUX =(3<<(2+16)) | (0<<2);
53
54 /* c. 设置GPIO0_B1作为input引脚
55 * set GPIO_SWPORTA_DDR to configure GPIO0_B1 as input
56 * GPIO_SWPORTA_DDR 0xFF720000 + 0x0004
57 * bit[9] = 0b0
58 */
59 *GPIO0_SWPORTA_DDR &= ~(1<<9);
60 }
61
62 }
- button_operations结构体中还有有read函数指针,它指向board_rk3399_button_read函数,在里面将会读取并返回按键引脚的电平。代码如下。
64 static int board_rk3399_button_read (int which) /* 读button, which-哪个 */
65 {
66 //printk("%s %s line %d, button %d, 0x%x\n", __FILE__, __FUNCTION__, __LINE__, which, *GPIO1_DATAIN);
67 if (which == 0)
68 return (*GPIO0_EXT_PORTA & (1<<9)) ? 1 : 0;
69 else
70 return 0;
71 }
测试
- 安装驱动程序之后执行测试程序,观察它的返回值(执行测试程序的同时操作按键):
# insmod button_drv.ko
# insmod board_rk3399.ko
# ./button_test /dev/100ask_button0
课后作业
- ① 修改board_rk3399.c,增加更多按键
- ② 修改button_test.c,使用按键来点灯
百问网IMX6ULL-QEMU的按键驱动程序(查询方式)
- 使用QEMU模拟的硬件,它的硬件资源可以随意扩展。
- 在IMX6ULL QEMU 虚拟开发板上,我们为它设计了2个 按键。在QEMU的GUI上有4个按键,右边的2个留待以后用于电源管理。
再看芯片手册确定寄存器及操作方法
编程
程序框架
- 使用GIT下载所有源码后,本节源码位于如下目录:
01_all_series_quickstart\04_快速入门(正式开始)\
02_嵌入式Linux驱动开发基础知识\source\
04_button_drv\02_button_drv_for_boards\04_button_drv_for_100ask_imx6ull-qemu
硬件相关的代码
- 主要看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,使用按键来点灯
异常与中断的概念及处理流程
中断的引入
妈妈怎么知道孩子醒了
- 妈妈怎么知道卧室里小孩醒了?
- ① 时时进房间看一下:查询方式
- 简单,但是累
- ② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒
- 不累,但是妈妈干不了活了
- ③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式
- 要浪费点时间,但是可以继续干活。
- 妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
- ④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知
- 妈妈、小孩互不耽误。
- ① 时时进房间看一下:查询方式
- 后面的3种方式,都需要“小孩来中断妈妈”:中断她的睡眠、中断她的工作。
- 妈妈当前正在看书,被“中断”后她会怎么做?流程如下:
- ① 妈妈正在看书
- ② 发生了各种声音
- 可忽略的远处猫叫
- 快递员按门铃
- 卧室中小孩哭了
- ③ 妈妈怎么办?
- a. 先在书中放入书签,合上书
- b. 去处理
- 对于不同的情况,处理方法不同:
- 对于门铃:开门取快递
- 对于哭声:照顾小孩
- c. 回来继续看书
中断的处理流程
- 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信息,可以看这个贴子:
- 有2个解决方法:
- A. 在Ubuntu上重新编译内核,让开发板使用新的内核启动;重新编译驱动,加载新驱动:
- 这样,①②③三者的内核版本就都一致了。
- 但是,这种方法有时候不好用,比如开发板上的内核无法更改(出厂固化了),或者你没有开发板上所用内核的全部源码无法编译出内核,这时就可以使用下面的方法。
- A. 在Ubuntu上重新编译内核,让开发板使用新的内核启动;重新编译驱动,加载新驱动:
- 有2个解决方法:
__NOTITLE__