├── .gitignore ├── .vscode ├── c_cpp_properties.json └── tasks.json ├── 00-hello ├── Makefile ├── README.md └── hello.c ├── 01-gpio_led ├── Makefile ├── README.md ├── rgbled.c └── rgbled_test.c ├── 02-gpio_key ├── Makefile ├── README.md └── gpiokey.c ├── 03-device_io ├── Makefile ├── README.md ├── gpiokey.c ├── select_test.c └── signal_test.c ├── 04-pwm_led ├── Makefile ├── README.md ├── pwmled.c ├── pwmled.fzz ├── pwmled.h └── pwmled_test.c ├── 05-pwm_musicbox ├── Makefile ├── README.md ├── music │ ├── 01-保卫黄河 │ ├── 02-我和我的祖国 │ ├── 03-FC马戏团 │ ├── 04-灌篮高手主题曲 │ └── 05-欢乐斗地主 ├── musicbox.c ├── musicbox.h ├── player_test.c └── pwm_musicbox.fzz ├── 06-infrared ├── Makefile ├── README.md ├── infrared.c ├── ir_test.c └── 红外接收头连接图.fzz ├── 07-pdd ├── Makefile ├── README.md ├── led.h ├── leddev.c ├── leddrv.c └── myled.dts ├── README.md ├── document ├── device-tree.cn.md └── rpi_SCH_3bplus_1p0_reduced.pdf └── rules.mk /.gitignore: -------------------------------------------------------------------------------- 1 | linux-rpi-4.19.y 2 | .DS_Store 3 | 4 | Module.symvers 5 | modules.order 6 | .tmp_versions 7 | *.cmd 8 | *.mod.* 9 | *.o 10 | *.ko 11 | *.dtbo -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Mac", 5 | "includePath": [ 6 | "${workspaceFolder}/linux-rpi-4.19.y/include", 7 | "${workspaceFolder}/linux-rpi-4.19.y/include/uapi", 8 | "${workspaceFolder}/linux-rpi-4.19.y/include/generated", 9 | "${workspaceFolder}/linux-rpi-4.19.y/include/generated/uapi", 10 | "${workspaceFolder}/linux-rpi-4.19.y/arch/arm/include", 11 | "${workspaceFolder}/linux-rpi-4.19.y/arch/arm/include/uapi", 12 | "${workspaceFolder}/linux-rpi-4.19.y/arch/arm/include/generated", 13 | "${workspaceFolder}/linux-rpi-4.19.y/arch/arm/include/generated/uapi" 14 | ], 15 | "defines": [ 16 | "ARCH=arm", 17 | "__KERNEL__", 18 | "CONFIG_SPARSE_IRQ" 19 | ], 20 | "macFrameworkPath": [ 21 | "Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks" 22 | ], 23 | "compilerPath": "/opt/arm-mac-linux-gnueabihf/bin/arm-mac-linux-gnueabihf-gcc", 24 | "cStandard": "c89", 25 | "cppStandard": "c++17", 26 | "intelliSenseMode": "clang-arm64" 27 | } 28 | ], 29 | "version": 4 30 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // 有关 tasks.json 格式的文档,请参见 3 | // https://go.microsoft.com/fwlink/?LinkId=733558 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "shell", 8 | "label": "Build", 9 | "command": "cd ${fileDirname} && make", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | }, 14 | "presentation": { 15 | "echo": true, 16 | "reveal": "always", 17 | "focus": true, 18 | "panel": "shared", 19 | "showReuseMessage": true, 20 | "clear": false 21 | }, 22 | "problemMatcher": [ 23 | "$gcc" 24 | ] 25 | }, { 26 | "type": "shell", 27 | "label": "clean", 28 | "command": "cd ${fileDirname} && make clean", 29 | "presentation": { 30 | "echo": true, 31 | "reveal": "always", 32 | "focus": true, 33 | "panel": "shared", 34 | "showReuseMessage": true, 35 | "clear": false 36 | } 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /00-hello/Makefile: -------------------------------------------------------------------------------- 1 | # 模块驱动,必须以obj-m=xxx形式编写 2 | obj-m = hello.o 3 | 4 | KDIR = /home/philon/linux-rpi-4.19.y 5 | CROSS = ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- 6 | 7 | all: 8 | $(MAKE) -C $(KDIR) M=$(PWD) $(CROSS) modules 9 | 10 | clean: 11 | $(MAKE) -C $(KDIR) M=`pwd` $(CROSS) clean -------------------------------------------------------------------------------- /00-hello/README.md: -------------------------------------------------------------------------------- 1 | # 00 HelloWorld 2 | 3 | 本文源码:https://github.com/philon/rpi-drivers/tree/master/00-hello 4 | 5 | 最近打算利用手里的树莓派3B+及各种丰富的扩展模块学些一下嵌入式Linux驱动开发。我计划实现LED、温湿度传感器、陀螺仪、PS遥感、红外模组、超声波模组、光敏传感器等驱动,其目的是为了学习嵌入式Linux驱动开发的思想,以及常见的接口及模块的原理。我会把整个学习过程整理为《树莓派驱动开发实战》。当然,我的初衷是学习ARM-Linux驱动开发,如果不是必须,我不会可以强调硬件平台,而是更侧重于驱动编程和电路设计。 6 | 7 | ![](https://i.loli.net/2019/07/12/5d28aa437927829767.jpg) 8 | 9 | 我的开发环境如下: 10 | 1. 宿主机:华硕A55VM | Ubuntu18.04 11 | 2. 开发板:Raspberry 3 Model B+ 2017 12 | 3. 编辑器:Visual Studio Code | C/C++扩展 13 | 14 | 由于文章中会存在大量命令脚本的引用,先做个声明: 15 | ```sh 16 | philon@a55v:~$ # 表示在宿主机敲出的命令 17 | 18 | philon@rpi:~$ # 表示在开发板敲出的命令 19 | ``` 20 | 21 | 此外,如果不是必须,我不会拿串口调试开发板,一律ssh! 22 | 23 | # 准备工作 24 | 25 | Linux驱动开发需要准备几样东西: 26 | - `Datasheet`:硬件手册,用于了解目标平台的规格/寄存器/内存地址/引脚定义等 27 | - `编译器`:用于编译目标平台的驱动源码,嵌入式开发,一般用交叉编译器 28 | - `内核源码`:编译驱动依赖于内核,**且必须与目标平台系统内核版本一致** 29 | - `外围电路原理图`:连怎么走线都不知道,还开发个球啊! 30 | 31 | 本文不涉及真实的模块驱动开发,因此具体的外围电路等实际开发的时候再看吧,先把前三样搞到手。 32 | 33 | ## 树莓派3B+硬件资料 34 | 35 | 由于官网没有配套的树莓派3B+Datasheet,我只能尽可能从官网其他地方把硬件资料凑齐。任何有关硬件的资料都优先从官网找:https://www.raspberrypi.org/documentation/hardware/raspberrypi/ 36 | 37 | 树莓派3B+硬件介绍 38 | 39 | - 处理器是BCM2837B0,64bit四核1.4GHz的Cortex-A53架构 40 | - 双频80211ac无线和蓝牙4.2 41 | - 千兆有线网卡,且支持PoE(网线供电) 42 | 43 | 电路原理图下载:https://www.raspberrypi.org/documentation/hardware/raspberrypi/schematics/rpi_SCH_3bplus_1p0_reduced.pdf 44 | 45 | ## 宿主机安装交叉编译器 46 | 47 | 交叉编译器选择是有套路的:`--` 48 | 49 | 例如: 50 | - arm-linux-gnueabi-gcc 表示arm32位架构,目标可执行文件依赖Linux+glibc 51 | - arm-linux-gnueabihf-gcc 表示arm32位/硬浮点数处理架构,其他同上 52 | - aarch64-linux-gnueabi-gcc 表示arm64位架构,其他同上 53 | 54 | 55 | 所以在选择交叉编译器之前要先确定开发板的处理器及操作系统环境: 56 | ```sh 57 | philon@rpi:~ $ uname -a 58 | Linux rpi 4.19.42-v7+ #1219 SMP Tue May 14 21:20:58 BST 2019 armv7l GNU/Linux 59 | ``` 60 | 61 | 注意最后的**armv7**,尽管RPi-3B+的处理器平台是64位4核,不过它的操作系统是32位的,所以应该安装arm32位glibc的交叉编译器。 62 | 63 | ```sh 64 | philon@a55v:~$ sudo apt install gcc-arm-linux-gnueabihf 65 | ``` 66 | 67 | ## 宿主机构建内核源码 68 | 69 | Linux驱动模块的编译依赖于内核源码,而且要注意版本问题。之前在开发板上查看Linux版本时已经知道其运行的内核是`Linux rpi 4.19.42-v7+`。所以最好是去官网下载对应版本号的内核源码。 70 | 71 | ```sh 72 | # 下载内核源码 73 | philon@a55v:~$ wget https://github.com/raspberrypi/linux/archive/rpi-4.19.y.tar.gz 74 | # 解压,进入内核目录 75 | philon@a55v:~$ tar xvf rpi-4.19.y.tar.gz && cd linux-rpi-4.19.y 76 | # 清理内核 77 | philon@a55v:~/linux-rpi-4.19.y$ make mrproper 78 | # 加载RPi-3B+的配置 79 | philon@a55v:~/linux-rpi-4.19.y$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- bcm2709_defconfig 80 | # 预编译 81 | philon@a55v:~/linux-rpi-4.19.y$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_prepare -j8 82 | ``` 83 | 84 | 【题外话】:很多教程都是先把内核完整编译一遍,再拷贝各种头/库文件到目标平台,其实没必要,预编译内核即可。从[内核官方文档](https://www.kernel.org/doc/Documentation/kbuild/modules.txt)可以看到这样一段话: 85 | 86 | > === 2. How to Build External Modules 87 | > To build external modules, you must have a prebuilt kernel available 88 | that contains the configuration and header files used in the build. 89 | > ... 90 | > An alternative is to use the "make" target "modules_prepare." This will 91 | make sure the kernel contains the information required. The target 92 | exists solely as a simple way to prepare a kernel source tree for 93 | building external modules. 94 | > NOTE: "modules_prepare" will not build Module.symvers even if 95 | CONFIG_MODVERSIONS is set; therefore, a full kernel build needs to be 96 | executed to make module versioning work. 97 | 98 | 简而言之,如果要构建外部驱动模块,内核必须有相关的配置及头文件。可以用“modules_prepare”准备内核源码树,这也仅仅用于构建外部驱动模块,但不会生成“Module.symvers”。 99 | 100 | 174 | 175 | 176 | # 第一个树莓派驱动 177 | 178 | 入门例子当然是留给HelloWorld啦! 179 | 180 | 创建工程目录: 181 | ```sh 182 | # 创建目录,进入 183 | philon@a55v:~$ mkdir -p drivers/00-hello && cd drivers/00-hello 184 | # 创建驱动模块源码及Makefile 185 | philon@a55v:~/drivers/00-hello$ touch hello.c Makefile 186 | ``` 187 | 188 | hello.c 189 | ```c 190 | #include 191 | #include 192 | 193 | static int __init hello_init(void) 194 | { 195 | printk("hello kernel\n"); 196 | return 0; 197 | } 198 | module_init(hello_init); 199 | 200 | static void __exit hello_exit(void) 201 | { 202 | printk("bye kernel\n"); 203 | } 204 | module_exit(hello_exit); 205 | 206 | MODULE_LICENSE("GPL v2"); // 开源许可证 207 | MODULE_DESCRIPTION("hello module for RPi 3B+"); // 模块描述 208 | MODULE_ALIAS("Hello"); // 模块别名 209 | MODULE_AUTHOR("Philon"); // 模块作者 210 | ``` 211 | 212 | Makefile 213 | ```Makefile 214 | # 模块驱动,必须以obj-m=xxx形式编写 215 | obj-m = hello.o 216 | 217 | KDIR = ../linux-rpi-4.19.y 218 | CROSS = ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- 219 | 220 | all: 221 | $(MAKE) -C $(KDIR) M=$(PWD) $(CROSS) modules 222 | 223 | clean: 224 | $(MAKE) -C $(KDIR) M=`pwd` $(CROSS) clean 225 | ```` 226 | 227 | ```sh 228 | # 编译内核 229 | philon@a55v:~/drivers/00-hello$ make 230 | # 将内核模块ko远程复制到开发板 231 | philon@a55v:~/drivers/00-hello$ scp hello.ko rpi.local:/home/philon/modules/ 232 | 233 | # 开发板加载hello模块 234 | philon@rpi:~/modules $ sudo insmod hello.ko 235 | # 卸载内核 236 | philon@rpi:~/modules $ sudo rmmod hello 237 | # 查看内核打印信息 238 | philon@rpi:~/modules $ dmesg | tail -5 239 | [ 1176.602268] Under-voltage detected! (0x00050005) 240 | [ 1182.842326] Voltage normalised (0x00000000) 241 | [ 4731.671023] hello: no symbol version for module_layout 242 | [ 4731.671042] hello: loading out-of-tree module taints kernel. 243 | [ 4731.672307] hello kernel 👈看到没,就是它了! 244 | [ 5010.908453] bye kernel 245 | ``` 246 | -------------------------------------------------------------------------------- /00-hello/hello.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static int __init hello_init(void) 5 | { 6 | printk("hello kernel\n"); 7 | return 0; 8 | } 9 | module_init(hello_init); 10 | 11 | static void __exit hello_exit(void) 12 | { 13 | printk("bye kernel\n"); 14 | } 15 | module_exit(hello_exit); 16 | 17 | MODULE_LICENSE("GPL v2"); // 开源许可证 18 | MODULE_DESCRIPTION("hello module for RPi 3B+"); // 模块描述 19 | MODULE_ALIAS("Hello"); // 模块别名 20 | MODULE_AUTHOR("Philon"); // 模块作者 -------------------------------------------------------------------------------- /01-gpio_led/Makefile: -------------------------------------------------------------------------------- 1 | # 模块驱动,必须以obj-m=xxx形式编写 2 | obj-m = rgbled.o 3 | 4 | KDIR = /home/philon/linux-rpi-4.19.y 5 | CROSS = ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- 6 | 7 | TEST := rgbled_test 8 | TEST_SRC := $(TEST).c 9 | TEST_OBJ := $(TEST).o 10 | 11 | all: module test 12 | 13 | module: 14 | $(MAKE) -C $(KDIR) M=$(PWD) $(CROSS) modules 15 | 16 | test: 17 | arm-linux-gnueabihf-gcc $(CFLAGS) -c $(TEST_SRC) -o $(TEST_OBJ) 18 | arm-linux-gnueabihf-gcc $(LFLAGS) -o $(TEST) $(TEST_OBJ) 19 | 20 | clean: 21 | $(MAKE) -C $(KDIR) M=`pwd` $(CROSS) clean 22 | rm -f $(TEST_OBJ) $(TEST) -------------------------------------------------------------------------------- /01-gpio_led/README.md: -------------------------------------------------------------------------------- 1 | # 树莓派驱动开发实战01:GPIO驱动之LED 2 | 3 | 本文源码:https://github.com/philon/rpi-drivers/tree/master/01-gpio-led 4 | 5 | GPIO可以说是驱动中最最最简单的部分了,但我上网查了下,绝大部分所谓《树莓派GPIO驱动》的教程全是python、shell等编程,或者调用第三方库,根本不涉及任何ARM底层、Linux内核相关的知识。显然,这根本不是什么驱动实现,只是调用了一两个别人实现好的库函数而已,跟着那种文章走一遍,你只知道怎么用,永远不知道为什么。 6 | 7 | 所以本文是希望从零开始,在Linux内核下实现一个真正的gpio-led驱动程序,初步体验一下Linux内核模块的开发思想,知其然,知其所以然。 8 | 9 | ## GPIO基础 10 | 11 | General-purpose input/output(通用输入/输出),其引脚可由软件控制,选择输入、输出、中断、时钟、片选等不同的功能模式。以树莓派为例,我们可以通过[pinout官网](https://pinout.xyz)查看板子预留的40pinGPIO分别是做什么的。 12 | 13 | ![FD10F639-BBC1-4AB8-938C-7C69F3D005B4.png](https://i.loli.net/2019/07/20/5d32e43bd2d9783386.png) 14 | 15 | 16 | 如上图,GPIO0-1、GPIO2-3脚,除了常规的输入/输出,还可作为I²C接口,GPIO14-15脚,可另作为TTL串口。 17 | 18 | 总之,GPIO平时就是个普通IO口,仅作为开关用,但开关只是为了掩人耳目,背后的复用功能才是它的真正职业。 19 | 20 | ## 三色LED电路 21 | 22 | 弄懂了GPIO原理,那就来实际操作一把,准备点亮LED灯吧! 23 | 24 | 先来看看原理图,为了区分三色灯不同颜色的LED,我特别用红绿蓝接入对应的RGB三个灯,黑线表示GND。 25 | 26 | ![0E846F15-1316-471A-8EB6-37AAC4B6E379.png](https://i.loli.net/2019/07/20/5d32ef5346d9193879.png) 27 | 28 | 如图所示,三色灯的R、G、B正极分别接到树莓派GPIO的2、3、4脚,灯的公共负极随便接一个GND脚。因此,想要点亮其中一个灯,对应GPIO脚输出高电平即可,是不是很简单呐! 29 | 30 | ![C181BD91-B00A-4781-AED7-F40932D97C81.png](https://i.loli.net/2019/07/20/5d32f0f3ee4d365881.png) 31 | 32 | ## BCM2837寄存器分配 33 | 34 | 基于上述,要点亮LED只需要做一件事——GPIO输出高电平。如果通过程序让GPIO口输出高电平呢? 35 | 36 | GPIO的控制其实是通过对应的CPU寄存器来实现的。在ARM架构的SoC中,所有的外围资源(寄存器)其实都是被映射到内存当中的,所以我们要读写寄存器,只需访问它映射到的内存地址即可。 37 | 38 | 那么问题来了,为什么不直接读写CPU的寄存器呢?因为现代的嵌入式系统往往都标配内存模块,处理器也带有MMU,所以其内部寄存器也就交由MMU来管理。 39 | 40 | 综上,我们现在要找出树莓派3B+这款芯片——BCM2837B0的GPIO物理内存地址。 41 | 42 | *这里不得不吐槽一下,我先是跑到[树莓派3B+官方网址](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2837b0/README.md)去找芯片资料,得知BCM2837其实就是BCM2836的主频升级版;我又去看BCM2836的资料,得知这只不过是BCM2835从32位到64位的升级版;我又去看BCM2835的芯片资料,然而里面说的内存映射地址根本就是错的......* 43 | 44 | 要确定BCM2837B0的内存映射需要参考两个地方: 45 | 1. [BCM2835 Datasheet](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2835/BCM2835-ARM-Peripherals.pdf),但要留意,里面坑很多,且并不完全适用于树莓派3B。 46 | 2. [国外热心网友的代码](https://github.com/Filkolev/LED-basic-driver),仅包含GPIO驱动,没有太多细节 47 | 48 | 官方文档在第6页和第90页有这样一句话和这样几张表: 49 | 50 | > Physical addresses range from 0x20000000 to 0x20FFFFFF for peripherals. The bus addresses for peripherals are set up to map onto the peripheral bus address range starting at 0x7E000000. Thus a peripheral advertised here at bus address 0x7Ennnnnn is available at physical address 0x20nnnnnn. 51 | 52 | ![BCM2835 GPIO 总线地址分配](https://i.loli.net/2019/07/20/5d3322e21519c31578.png) 53 | 54 | ![GPIO 复用功能选择](https://i.loli.net/2019/07/20/5d3323337992b55401.png) 55 | 56 | 我就直说吧,共6个关键要素: 57 | - 外围总线地址0x7E000000映射到ARM物理内存地址0x20000000,加上偏移,**GPIO物理地址为0x20200000** 58 | - GPIO操作需要先通过**GPFSEL选择复用功能**,再通过**GPSET/GPCLR对指定位拉高/拉低** 59 | - BMC2835共54个GPIO,分为两组BANK,第一组[0:31],第二组[32:53] 60 | - GPFSEL寄存器每3位表示一个GPIO的复用功能,因此一个寄存器可容纳10个GPIO,共6个GPFSEL 61 | - GPSET/GPCLR寄存器每1位表示一个GPIO的状态1/0,因此一个寄存器可容纳32个GPIO,共2个GPSET/GPCLR 62 | - ⚠️国外热心网友指出:**树莓派3B+的GPIO物理内存地址被映射到了0x3F200000!** 63 | 64 | 好了,现在结合电路图可推导出思路: 65 | 1. R、G、B分别对应GPIO2、3、4,需要操作的寄存器为GPFSEL0/GPSET0/GPCLR0 66 | 2. 要把三个脚全部设为“输出模式”,需要将GPFSEL0的第6、9、12都置为001 67 | 3. 要控制三个脚的输出状态,需要将GPSET0/GPCLR0的第2、3、4脚置1 68 | 69 | ## 先点亮红灯 70 | 71 | 现在开始写Linux驱动模块,先不着急完整实现,这一步只是把其中的红灯点亮,为此我甚至把绿蓝线给拔了! 72 | 73 | 代码实现非常简单,就是在加载驱动时红灯亮,卸载驱动时红灯灭。 74 | ```c 75 | #include 76 | #include 77 | #include 78 | 79 | #define BCM2837_GPIO_BASE 0x3F200000 80 | #define BCM2837_GPIO_FSEL0_OFFSET 0x0 // GPIO功能选择寄存器0 81 | #define BCM2837_GPIO_SET0_OFFSET 0x1C // GPIO置位寄存器0 82 | #define BCM2837_GPIO_CLR0_OFFSET 0x28 // GPIO清零寄存器0 83 | 84 | static void* gpio = 0; 85 | 86 | static int __init rgbled_init(void) 87 | { 88 | // 获取GPIO对应的Linux虚拟内存地址 89 | gpio = ioremap(BCM2837_GPIO_BASE, 0xB0); 90 | 91 | // 将GPIO bit2设置为“输出模式” 92 | int val = ioread32(gpio + BCM2837_GPIO_FSEL0_OFFSET); 93 | val &= ~(7 << 6); 94 | val |= 1 << 6; 95 | 96 | // GPIO bit2 输出1 97 | iowrite32(val, gpio); 98 | iowrite32(1 << 2, gpio + BCM2837_GPIO_SET0_OFFSET); 99 | 100 | return 0; 101 | } 102 | module_init(rgbled_init); 103 | 104 | static void __exit rgbled_exit(void) 105 | { 106 | // GPIO输出0 107 | iowrite32(1 << 2, gpio + BCM2837_GPIO_CLR0_OFFSET); 108 | iounmap(gpio); 109 | 110 | } 111 | module_exit(rgbled_exit); 112 | 113 | MODULE_LICENSE("Dual BSD/GPL"); 114 | MODULE_AUTHOR("Philon | ixx.life"); 115 | ``` 116 | 117 | 这段代码太简单了,以至于我觉得完全不需要解释,直接看效果吧。图中的命令: 118 | 119 | ```sh 120 | philon@rpi:~/modules$ insmod rgbled.ko # 亮 121 | philon@rpi:~/modules$ rmmod rgbled.ko # 灭 122 | ``` 123 | 124 | ![红灯点亮效果](https://i.loli.net/2019/07/20/5d3331f96425544265.gif) 125 | 126 | ## 再点亮全部 127 | 128 | 其实点亮红灯后,绿蓝灯无非是改改地址而已,没什么难度。本文的目的是学习Linux驱动,点亮LED不过是驱动开发的感性认识,所以我决定把简单的问题复杂化😄。驱动主要为用户层提供了几种设备控制方式: 129 | 1. 通过命令`echo [white|black|red|yellow...] > /dev/rgbled`直接控制灯的颜色 130 | 2. 通过命令`cat /dev/rgbled`查看当前灯的状态 131 | 3. 通过函数`ioctl(fd, 1, 0)`可独立控制每个灯的状态 132 | 133 | 说白了,用户层只须关心等的输出的颜色,屏蔽了具体的电路引脚及状态。 134 | 135 | 为此,我们需要把三色LED模块当作一个**字符设备**来实现,本文是驱动开发实战,所以更多的讲如何实现,有关字符设备的原理可以参考我的另一篇文章[《ARM-Linux驱动开发四:字符设备》](https://ixx.life/ArmLinuxDriver/chapter4/)。 136 | 137 | 驱动主要分为两大块:**设备的`read/write/ioctl`接口**以及**字符设备的注册**。 138 | 139 | 先看看驱动的读写控制是如何实现的: 140 | ```c 141 | // 三色LED灯不同状态组合 142 | static struct { const char* name; const bool pins[3]; } colors[] = { 143 | { "white", {1,1,1} }, // 白(全开) 144 | { "black", {0,0,0} }, // 黑(全关) 145 | { "red", {1,0,0} }, // 红 146 | { "green", {0,1,0} }, // 绿 147 | { "blue", {0,0,1} }, // 蓝 148 | { "yellow", {1,1,0} }, // 黄 149 | { "cyan", {0,1,1} }, // 青 150 | { "purple", {1,0,1} }, // 紫 151 | }; 152 | 153 | static void* gpio = 0; // GPIO起始地址映射 154 | static bool ledstate[3] = {0}; // 三个LED灯当前状态 155 | 156 | void gpioctl(int pin, bool stat) 157 | { 158 | void* reg = gpio + (stat ? BCM2837_GPIO_SET0_OFFSET : BCM2837_GPIO_CLR0_OFFSET); 159 | ledstate[pin-2] = stat; 160 | iowrite32(1 << pin, reg); 161 | } 162 | 163 | // 通过文件读取,得到当前颜色名称 164 | ssize_t rgbled_read(struct file* filp, char __user* buf, size_t len, loff_t* off) 165 | { 166 | int rc = 0; 167 | int i = 0; 168 | 169 | // 当文件已经读过一次,返回EOF,避免重复读 170 | if (*off > 0) { 171 | return 0; 172 | } 173 | 174 | // 根据当前三个LED的输出状态,找到对应颜色名,返回 175 | for (i = 0; i < sizeof(colors) / sizeof(colors[0]); i++) { 176 | const char* name = colors[i].name; 177 | const bool* pins = colors[i].pins; 178 | 179 | if (ledstate[0] == pins[0] && ledstate[1] == pins[1] && ledstate[2] == pins[2]) { 180 | char color[32] = {0}; 181 | sprintf(color, "%s\n", name); 182 | *off = strlen(color); 183 | rc = copy_to_user(buf, color, *off); 184 | return rc < 0 ? rc : *off; 185 | } 186 | } 187 | 188 | return -EFAULT; 189 | } 190 | 191 | // 通过向文件写入颜色名称,控制LED灯状态 192 | ssize_t rgbled_write(struct file* filp, const char __user* buf, size_t len, loff_t* off) 193 | { 194 | char color[32] = {0}; 195 | int rc = 0; 196 | int i = 0; 197 | 198 | rc = copy_from_user(color, buf, len); 199 | if (rc < 0) { 200 | return rc; 201 | } 202 | 203 | *off = 0; // 每次控制之后,文件索引都回到开始 204 | 205 | // 根据用户层传来的颜色名,找到对应引脚状态,输出 206 | for (i = 0; i < sizeof(colors) / sizeof(colors[0]); i++) { 207 | const char* name = colors[i].name; 208 | const bool* pins = colors[i].pins; 209 | if (!strncasecmp(color, name, strlen(name))) { 210 | gpioctl(LED_RED_PIN, pins[0]); 211 | gpioctl(LED_GREEN_PIN, pins[1]); 212 | gpioctl(LED_BLUE_PIN, pins[2]); 213 | return len; 214 | } 215 | } 216 | 217 | return -EINVAL; 218 | } 219 | 220 | // 通过ioctl函数控制每个灯的状态 221 | long rgbled_ioctl(struct file* filp, unsigned int cmd, unsigned long arg) 222 | { 223 | if (cmd >= 2 && cmd <= 4) { 224 | gpioctl(cmd, arg); 225 | } else { 226 | return -ENODEV; 227 | } 228 | 229 | return 0; 230 | } 231 | 232 | const struct file_operations fops = { 233 | .owner = THIS_MODULE, 234 | .read = rgbled_read, 235 | .write = rgbled_write, 236 | .unlocked_ioctl = rgbled_ioctl, 237 | }; 238 | ``` 239 | 240 | 关于读/写/控制这三种操作的代码实现,看似复杂,其实都很容易理解,无非就是通过`copy_to_user`和`copy_from_user`两个函数,实现内核层与用户层之间的数据交互,剩下的事情不过就是在`colors`结构体数组中进行遍历和比对而已。 241 | 242 | 然后是字符设备注册、GPIO功能配置等内容的实现。每种字符设备都需要唯一的主设备号和次设备号,设备号可以静态指定或动态分配,原则上建议由内核动态分配,避免冲突。 243 | 244 | 字符设备的创建有很多种思路,普通字符设备、混杂设备、平台设备等,它们都是内核提供的编程框架。例如GPIO这类设备,内核其实是有专门的gpio类,但为了更好的学习驱动开发,别着急,一步步来,先从最简单的开始(因为难的我也不会)。 245 | 246 | 下边的代码主要看`cdev_xxx`相关的部分即可,驱动加载时配置好GPIO映射,注册字符设备,获取设备号;驱动卸载时,取消GPIO映射,释放设备号,注销字符设备。 247 | 248 | ```c 249 | static dev_t devno = 0; // 设备编号 250 | static struct cdev cdev; // 字符设备结构体 251 | 252 | static int __init rgbled_init(void) 253 | { 254 | // 映射GPIO物理内存到虚拟地址,并将其置为“输出模式” 255 | // 代码写得比较丑,解释以下: 256 | // 就是先把三个GPIO的“功能选择位”全部置000 257 | // 然后再将其置为001 258 | int val = ~((7 << (LED_RED_PIN*3)) | (7 << (LED_GREEN_PIN*3)) | (7 << LED_BLUE_PIN*3)); 259 | gpio = ioremap(BCM2837_GPIO_BASE, 0xB0); 260 | val &= ioread32(gpio + BCM2837_GPIO_FSEL0_OFFSET); 261 | val |= (1 << (LED_RED_PIN*3)) | (1 << (LED_GREEN_PIN*3)) | (1 << (LED_BLUE_PIN*3)); 262 | iowrite32(val, gpio); 263 | 264 | // 将该模块注册为一个字符设备,并动态分配设备号 265 | if (alloc_chrdev_region(&devno, 0, 1, "rgbled")) { 266 | printk(KERN_ERR"failed to register kernel module!\n"); 267 | return -1; 268 | } 269 | cdev_init(&cdev, &fops); 270 | cdev_add(&cdev, devno, 1); 271 | 272 | printk(KERN_INFO"rgbled device major & minor is [%d:%d]\n", MAJOR(devno), MINOR(devno)); 273 | 274 | return 0; 275 | } 276 | module_init(rgbled_init); 277 | 278 | static void __exit rgbled_exit(void) 279 | { 280 | // 取消gpio物理内存映射 281 | iounmap(gpio); 282 | 283 | // 释放字符设备 284 | cdev_del(&cdev); 285 | unregister_chrdev_region(devno, 1); 286 | 287 | printk(KERN_INFO"rgbled free\n"); 288 | } 289 | module_exit(rgbled_exit); 290 | ``` 291 | 292 | 代码基本就是这样。来看看效果吧,操作指令如下 293 | 294 | ```sh 295 | # 编译驱动,拷贝至开发板 296 | philon@a55v:~/drivers/01-gpio_led$ make 297 | philon@a55v:~/drivers/01-gpio_led$ scp rgbled.ko rgbled_test rpi.local:/home/philon/modules 298 | 299 | #----------------------以下是开发板操作---------------------- 300 | # 加载驱动 & 查看模块的主从设备号 301 | philon@rpi:~/modules$ sudo insmod rgbled.ko 302 | philon@rpi:~/modules$ dmesg 303 | ... 304 | [ 106.818009] rgbled: no symbol version for module_layout 305 | [ 106.818028] rgbled: loading out-of-tree module taints kernel. 306 | [ 106.820307] rgbled device major&minor is [240:0] 👈主从设备号 307 | 308 | # 根据动态分配的设备号创建设备节点 309 | philon@rpi:~/modules$ sudo mknod /dev/rgbled c 240 0 310 | 311 | philon@rpi:~/modules$ sudo sh -c "echo green > /dev/rgbled" # 打开绿灯 312 | philon@rpi:~/modules$ sudo ./rgbled_test b 1 # 再打开蓝灯 313 | philon@rpi:~/modules$ sudo cat /dev/rgbled # 查看当前颜色 314 | cyan #青色 315 | ``` 316 | 317 | 动态图,只是被我设置得比较慢,别着急换台呀!😂 318 | 319 | ![三色LED驱动最终效果图](https://i.loli.net/2019/07/22/5d35cca94f89546830.gif) -------------------------------------------------------------------------------- /01-gpio_led/rgbled.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | MODULE_LICENSE("Dual BSD/GPL"); 10 | MODULE_AUTHOR("Philon | https://ixx.life"); 11 | 12 | #define BCM2837_GPIO_BASE 0x3F200000 13 | #define BCM2837_GPIO_FSEL0_OFFSET 0x0 // GPIO功能选择寄存器0 14 | #define BCM2837_GPIO_SET0_OFFSET 0x1C // GPIO置位寄存器0 15 | #define BCM2837_GPIO_CLR0_OFFSET 0x28 // GPIO清零寄存器0 16 | 17 | #define LED_RED_PIN 2 18 | #define LED_GREEN_PIN 3 19 | #define LED_BLUE_PIN 4 20 | 21 | static void* gpio = 0; // GPIO起始地址映射 22 | static bool ledstate[3] = {0}; // 三个LED的状态 23 | 24 | // 三色LED灯不同状态组合 25 | static struct { const char* name; const bool pins[3]; } colors[] = { 26 | { "white", {1,1,1} }, // 白(全开) 27 | { "black", {0,0,0} }, // 黑(全关) 28 | { "red", {1,0,0} }, // 红 29 | { "green", {0,1,0} }, // 绿 30 | { "blue", {0,0,1} }, // 蓝 31 | { "yellow", {1,1,0} }, // 黄 32 | { "cyan", {0,1,1} }, // 青 33 | { "purple", {1,0,1} }, // 紫 34 | }; 35 | 36 | void gpioctl(int pin, bool stat) 37 | { 38 | void* reg = gpio + (stat ? BCM2837_GPIO_SET0_OFFSET : BCM2837_GPIO_CLR0_OFFSET); 39 | ledstate[pin-2] = stat; 40 | iowrite32(1 << pin, reg); 41 | } 42 | 43 | // 向用户层返回当前设备颜色值 44 | ssize_t rgbled_read(struct file* filp, char __user* buf, size_t len, loff_t* off) 45 | { 46 | int rc = 0; 47 | int i = 0; 48 | 49 | // 当文件已经读过一次,返回EOF,避免重复读 50 | if (*off > 0) { 51 | return 0; 52 | } 53 | 54 | for (i = 0; i < sizeof(colors) / sizeof(colors[0]); i++) { 55 | const char* name = colors[i].name; 56 | const bool* pins = colors[i].pins; 57 | if (ledstate[0] == pins[0] && ledstate[1] == pins[1] && ledstate[2] == pins[2]) { 58 | char color[32] = {0}; 59 | sprintf(color, "%s\n", name); 60 | *off = strlen(color); 61 | rc = copy_to_user(buf, color, *off); 62 | return rc < 0 ? rc : *off; 63 | } 64 | } 65 | 66 | return -EFAULT; 67 | } 68 | 69 | // 通过向文件写入颜色名称,控制LED灯状态 70 | ssize_t rgbled_write(struct file* filp, const char __user* buf, size_t len, loff_t* off) 71 | { 72 | char color[32] = {0}; 73 | int rc = 0; 74 | int i = 0; 75 | 76 | rc = copy_from_user(color, buf, len); 77 | if (rc < 0) { 78 | return rc; 79 | } 80 | 81 | *off = 0; // 每次控制之后,文件索引都回到开始 82 | 83 | for (i = 0; i < sizeof(colors) / sizeof(colors[0]); i++) { 84 | const char* name = colors[i].name; 85 | const bool* pins = colors[i].pins; 86 | if (!strncasecmp(color, name, strlen(name))) { 87 | gpioctl(LED_RED_PIN, pins[0]); 88 | gpioctl(LED_GREEN_PIN, pins[1]); 89 | gpioctl(LED_BLUE_PIN, pins[2]); 90 | return len; 91 | } 92 | } 93 | 94 | return -EINVAL; 95 | } 96 | 97 | // 用户层通过ioctl函数单独控制灯的状态 98 | long rgbled_ioctl(struct file* filp, unsigned int cmd, unsigned long arg) 99 | { 100 | if (cmd >= 2 && cmd <= 4) { 101 | gpioctl(cmd, arg); 102 | } else { 103 | return -ENODEV; 104 | } 105 | 106 | return 0; 107 | } 108 | 109 | static const struct file_operations fops = { 110 | .owner = THIS_MODULE, 111 | .read = rgbled_read, 112 | .write = rgbled_write, 113 | .unlocked_ioctl = rgbled_ioctl, 114 | }; 115 | 116 | static dev_t devno = 0; // 设备编号 117 | static struct cdev cdev; // 字符设备结构体 118 | 119 | static int __init rgbled_init(void) 120 | { 121 | // 映射GPIO物理内存到虚拟地址,并将其置为“输出模式” 122 | // 代码写得比较丑,解释以下: 123 | // 就是先把三个GPIO的“功能选择位”全部置000 124 | // 然后再将其置为001 125 | int val = ~((7 << (LED_RED_PIN*3)) | (7 << (LED_GREEN_PIN*3)) | (7 << LED_BLUE_PIN*3)); 126 | gpio = ioremap(BCM2837_GPIO_BASE, 0xB0); 127 | val &= ioread32(gpio + BCM2837_GPIO_FSEL0_OFFSET); 128 | val |= (1 << (LED_RED_PIN*3)) | (1 << (LED_GREEN_PIN*3)) | (1 << (LED_BLUE_PIN*3)); 129 | iowrite32(val, gpio); 130 | 131 | // 将该模块注册为一个字符设备,并动态分配设备号 132 | if (alloc_chrdev_region(&devno, 0, 1, "rgbled")) { 133 | printk(KERN_ERR"failed to register kernel module!\n"); 134 | return -1; 135 | } 136 | cdev_init(&cdev, &fops); 137 | cdev_add(&cdev, devno, 1); 138 | 139 | printk(KERN_INFO"rgbled device major & minor is [%d:%d]\n", MAJOR(devno), MINOR(devno)); 140 | 141 | return 0; 142 | } 143 | module_init(rgbled_init); 144 | 145 | static void __exit rgbled_exit(void) 146 | { 147 | // 取消gpio物理内存映射 148 | iounmap(gpio); 149 | 150 | // 释放字符设备 151 | cdev_del(&cdev); 152 | unregister_chrdev_region(devno, 1); 153 | 154 | printk(KERN_INFO"rgbled free\n"); 155 | } 156 | module_exit(rgbled_exit); 157 | -------------------------------------------------------------------------------- /01-gpio_led/rgbled_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | int main(int argc, char* argv[]) 8 | { 9 | if (argc < 3) { 10 | fprintf(stderr, "\n./rgbled_test <0|1>\n\n"); 11 | exit(0); 12 | } 13 | 14 | int fd = open("/dev/rgbled", O_RDWR); 15 | if (fd < 0) { 16 | perror("open device"); 17 | return -1; 18 | } 19 | 20 | switch (argv[1][0]) { 21 | case 'r': 22 | case 'R': 23 | ioctl(fd, 2, atoi(argv[2])); 24 | break; 25 | case 'g': 26 | case 'G': 27 | ioctl(fd, 3, atoi(argv[2])); 28 | break; 29 | case 'b': 30 | case 'B': 31 | ioctl(fd, 4, atoi(argv[2])); 32 | break; 33 | } 34 | 35 | close(fd); 36 | return 0; 37 | } 38 | -------------------------------------------------------------------------------- /02-gpio_key/Makefile: -------------------------------------------------------------------------------- 1 | # 模块驱动,必须以obj-m=xxx形式编写 2 | obj-m = gpiokey.o 3 | 4 | KDIR = /home/philon/linux-rpi-4.19.y 5 | CROSS = ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- 6 | 7 | TEST := gpiokey_test 8 | TEST_SRC := $(TEST).c 9 | TEST_OBJ := $(TEST).o 10 | 11 | all: module 12 | 13 | module: 14 | $(MAKE) -C $(KDIR) M=$(PWD) $(CROSS) modules 15 | 16 | test: 17 | arm-linux-gnueabihf-gcc $(CFLAGS) -c $(TEST_SRC) -o $(TEST_OBJ) 18 | arm-linux-gnueabihf-gcc $(LFLAGS) -o $(TEST) $(TEST_OBJ) 19 | 20 | clean: 21 | $(MAKE) -C $(KDIR) M=`pwd` $(CROSS) clean 22 | rm -f $(TEST_OBJ) $(TEST) -------------------------------------------------------------------------------- /02-gpio_key/README.md: -------------------------------------------------------------------------------- 1 | # 树莓派驱动开发实战02:GPIO驱动之按键中断 2 | 3 | 在上一篇中主要学习了GPIO原理及Linux字符设备,其过程大致是这样子的: 4 | 5 | ``` 6 | 电路图“看引脚” --> 手册“看物理地址” --> 寄存器手册“看GPIO逻辑控制” --> 复用功能 --> 地址操作 7 | ``` 8 | 9 | 可以看到,整个过程是非常繁琐的,驱动程序必须精确到处理器的每一根引脚的状态,如果所有驱动都这么写估计当场就跪了。因此,本文除了学习GPIO中断原理之外,更重要是掌握以下知识: 10 | 11 | - 混杂设备机制 12 | - Linux内核GPIO接口 13 | - ARM中断基础 14 | - Linux内核中断接口 15 | 16 | # ARM中断基础 17 | 18 | 中断,就是由外部电路产生的一个电信号,强制CPU从当前执行代码区转移到中断处理函数。ARM架构的CPU中断硬件原理和这个差不多,注意这里说的是ARM的CPU,仅仅代表处理器当中的一个核,不要把CPU和SoC划等号。 19 | 20 | CPU能提供的中断资源是非常有限的,一般也就一两个“引脚”,但我们在看芯片手册的时候就会发现,几乎每个GPIO都具备中断功能,那可是几十上百个中断啊!这归功于内部继承的PIC——可编程中断控制器,它负责监听所有GPIO的中断信号,并在外设给出中断信号时真正去触发CPU中断,并告诉CPU是谁触发的。这种中断信号源被抽象为——中断号。 21 | 22 | 在现代多核处理器架构下,ARM用的是GIC(通用中断控制器),它能支持SGI(软件生成中断)、PPI(单核私有外设中断)、SPI(多核共享外设中断)。默认情况下,ARM处理器的外设中断总是先给到CPU0,如果其忙不过来才往后传递。 23 | 24 | 那么CPU收到中断信号后又如何处理呢?ARM共有7种工作模式,常规情况下会运行于用户模式(用户代码区),一旦中断触发,会立刻切换至中断模式(响应函数),中断模式分为IRQ(中断)和FIQ(快速中断),它们二者的区别是,FIQ可以进一步中断IRQ。 25 | 26 | 由于是实战操作,过于理论的东西就不往上放了,如果要进一步了解ARM中断,可以参考这篇文章👉:https://my.oschina.net/u/914989/blog/121585 27 | 28 | 这里只需要掌握两个重要概念: 29 | 1. 中断号本身可以看作一种独立的CPU资源,通过中断控制器监听真实的物理资源(引脚)状态 30 | 2. CPU的外设中断会直接触发PC跳转到指定代码区 31 | 32 | # Linux/IRQ基础 33 | 34 | 正如前文所说,中断是让CPU切换执行上下文,尽管Linux操作系统通过时间切片的方式实现多任务,但IRQ切换是硬件层级的,进入中断函数就意味着什么进程、调度、并发等软件概念将全部失效。举例来说,一个进程调用sleep只会让自身运行停止并让出CPU资源,但在内核中断函数当中sleep,那就真睡过去了——整个操作系统的调度机制都会崩溃掉。 35 | 36 | 所以,Linux将中断处理分为“`顶半部`”和“`底半部`”,可以简单粗暴地理解: 37 | - 顶半部,硬件级响应,处理内容必须快准狠,尽快将CPU资源交还操作系统 38 | - 底半部,交由系统任务队列调度,处理耗时的响应业务 39 | 40 | 打个比方,顶半部好比医院挂号,底半部好比排队就诊的过程。但不要死脑筋,如果响应业务本身并不耗时,就没必要再拆分为两个处理部分了,比如出院缴费,直接在顶半部搞定。 41 | 42 | 带着这个原则,看一下Linux中断编程接口: 43 | 44 | ```c 45 | #include 46 | 47 | // 根据GPIO引脚号获取对应中断号 48 | int gpio_to_irq(unsigned gpio); 49 | 50 | // 申请占用中断号,并绑定处理函数 51 | // - irq 中断号 52 | // - handler 顶半部中断处理函数 53 | // - flags 中断触发方式 54 | // - name 中断名称 55 | // - dev 中断参数传递 56 | int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev); 57 | 58 | // 释放中断号 59 | void *free_irq(unsigned int irq, void *dev); 60 | ``` 61 | 62 | 下面来实际操作一把——Linux中断的顶半部处理实现。 63 | 64 | # 最简单的GPIO中断 65 | 66 | 先来看个接线图,为了更好地展示中断,“继承”了上一篇文章的三色LED接线,预期要实现是“每按一次键改变一种颜色”。按键Key的两个脚接到了树莓派的`GPIO17`和`3V3`上,换句话说,就是用GPIO17接收上升沿中断信号。而LED的控制电路保持之前的不变。 67 | 68 | ![接线图](https://i.loli.net/2019/08/04/7EYIMusdiBWhv5Q.png) 69 | ![电路图](https://i.loli.net/2019/08/04/JlXWK7bcs4Igjxv.png) 70 | 71 | 实现GPIO上升沿中断大体分为4步: 72 | 1. 设置GPIO复用功能为输入模式 `gpio_request()` 73 | 2. 获取GPIO对应中断号 `gpio_to_irq()` 74 | 3. 申请中断号、中断类型、绑定处理函数 `request_irq()` 75 | 4. 释放中断(卸载驱动时) `free_irq()` 76 | 77 | ```c 78 | #include 79 | #include // 各种gpio的数据结构及函数 80 | #include // 内核中断相关接口 81 | 82 | MODULE_LICENSE("Dual BSD/GPL"); 83 | MODULE_AUTHOR("Philon | https://ixx.life"); 84 | 85 | // 稍后由内核分配的按键中断号 86 | static unsigned int key_irq = 0; 87 | 88 | // 定义按键的GPIO引脚功能 89 | static const struct gpio key = { 90 | .gpio = 17, // 引脚号为BCM - 17 91 | .flags = GPIOF_IN, // 功能复用为输入 92 | .label = "Key0" // 标示为Key0 93 | }; 94 | 95 | // 按键中断“顶半部”处理函数 96 | static irqreturn_t on_key_press(int irq, void* dev) 97 | { 98 | printk(KERN_INFO "key pressed\n"); 99 | return IRQ_HANDLED; 100 | } 101 | 102 | static int __init gpiokey_init(void) 103 | { 104 | int rc = 0; 105 | 106 | // 向内核申请GPIO 107 | if ((rc = gpio_request_one(key.gpio, key.flags, key.label)) < 0) { 108 | printk(KERN_ERR "ERROR%d: cannot request gpio\n", rc); 109 | return rc; 110 | } 111 | 112 | // 获取中断号 113 | key_irq = gpio_to_irq(key.gpio); 114 | if (key_irq < 0) { 115 | printk(KERN_ERR "ERROR%d:cannot get irq num\n", key_irq); 116 | return key_irq; 117 | } 118 | 119 | // 申请上升沿触发中断 120 | if (request_irq(key_irq, on_key_press, IRQF_TRIGGER_RISING, "onKeyPress", NULL) < 0) { 121 | printk(KERN_ERR "cannot request irq\n"); 122 | return -EFAULT; 123 | } 124 | 125 | return 0; 126 | } 127 | module_init(gpiokey_init); 128 | 129 | static void __exit gpiokey_exit(void) 130 | { 131 | // 释放中断号及GPIO 132 | free_irq(key_irq, NULL); 133 | gpio_free(key.gpio); 134 | } 135 | module_exit(gpiokey_exit); 136 | ``` 137 | 138 | 上述代码非常简单,就是在按下按键的时候,打印一条消息。可以通过`dmesg`命令查看内核打印消息: 139 | ```sh 140 | philon@rpi:~/modules $ sudo insmod gpiokey.ko 141 | philon@rpi:~/modules $ dmesg 142 | ... 143 | [ 77.238326] gpiokey: no symbol version for module_layout 144 | [ 77.238345] gpiokey: loading out-of-tree module taints kernel. 145 | [ 79.310635] key pressed 146 | [ 79.463206] key pressed 147 | [ 79.463262] key pressed # 我摸着右边的鼻孔对天发誓,我只按了一下! 148 | ``` 149 | 150 | 正如文章最开始所说,Linux对各种资源的调用是有相关API的,要尽量使用内核接口编写驱动程序,一能保证底层代码的质量,二能提高代码的移植性。关于GPIO资源的调用,要熟悉以下接口: 151 | 152 | ```c 153 | #include 154 | 155 | struct gpio { 156 | unsigned gpio; // GPIO编号 157 | unsigned long flags; // GPIO复用功能配置 158 | const char *label; // GPIO标签名 159 | }; 160 | 161 | // 单个GPIO资源申请/释放 162 | int gpio_request_one(unsigned gpio, unsigned long flags, const char *label); 163 | void gpio_free(unsigned gpio); 164 | 165 | // 多个GPIO资源申请/释放 166 | int gpio_request_array(const struct gpio *array, size_t num); 167 | void gpio_free_array(const struct gpio *array, size_t num); 168 | 169 | // GPIO状态读写 170 | int gpio_get_value(unsigned gpio); 171 | void gpio_set_value(unsigned gpio, int value); 172 | ``` 173 | 174 | # 按键防抖,中断的底半部与定时器接口 175 | 176 | 由于前边的代码没有做防抖,明明只按了一下按键,中断函数却被连续触发了3次。话说在单片机里实现按键防抖是非常简单的,无非就是睡个50毫秒,再确认是否真的按下即可。但是前文也明确说了,Linux是多任务系统,永远不要试图在中断函数里睡眠。因此,防抖只能放在Linux中断的底半部。 177 | 178 | 此外,慎用睡眠函数!除非你很清楚它不是忙等待。在多任务系统下,按键防抖的逻辑应该是——触发中断后,让出CPU资源50毫秒,然后再确认是否真的按下。 179 | 180 | 先来认识一下底半部机制,Linux内核提供的底半部机制主要有`软中断`、`tasklet`、`工作队列`、`线程IRQ`。 181 | 182 | - 软中断,是有内核软件模拟的一种中断机制,注意不要和ARM指令触发的中断混淆,后者本质上是硬中断 183 | - tasklet,基于软中断实现的中断调度机制,本质上还是中断,不允许在处理函数中sleep 184 | - 工作队列,类似于tasklet,区别在于工作队列底层基于线程,可以在处理函数中sleep 185 | - 线程IRQ,不用解释了,就是个线程 186 | 187 | 有关Linux底半部的知识不适合放在这里,建议参考此文:http://chinaunix.net/uid-20768928-id-5077401.html 188 | 189 | 这里了解底半部机制的目的,仅仅是为了挑选一种何时的响应方式,首先可以明确,软中断和tasklet不能睡,pass。线程维护麻烦,pass。就只剩工作队列了。尽管工作队列可以睡,但内核提供的`usleep/msleep`等接口本质上是忙等待,依旧占用CPU资源,pass。怎么办呢——工作队列+定时器。当中断来临后: 190 | 1. 顶半部迅速定义个工作队列,交由内核调度 191 | 2. 当工作队列被调度时,迅速定义个定时器——延时50ms 192 | 3. 当定时器到时中断,才真的去做防抖判断 193 | 194 | **工作队列**API: 195 | ```c 196 | #include 197 | 198 | // 工作队列原型 199 | struct work_struct { 200 | atomic_long_t data; 201 | struct list_head entry; 202 | work_func_t func; 203 | #ifdef CONFIG_LOCKDEP 204 | struct lockdep_map lockdep_map; 205 | #endif 206 | }; 207 | 208 | // 工作队列回调函数原型 209 | typedef void (*work_func_t)(struct work_struct *work); 210 | 211 | // 初始化一个工作队列,绑定回调 212 | INIT_WORK(work, func); 213 | // 启动队列,之后会由内核完成调度 214 | schedule_work(&my_wq); 215 | ``` 216 | 217 | **定时器**API: 218 | ```c 219 | #include 220 | 221 | // 全局变量 222 | // 记录上电后定时器中断次数,也就是开机时长,但不是微秒或纳秒的概念 223 | extern unsigned long volatile jiffies; 224 | // 表示CPU一秒钟有多少个定时器中断 225 | #define HZ 100 226 | 227 | // 简单来说,如果要定义一个100ms的延时,相当于以下公式: 228 | // jiffies + (HZ/10) 229 | // 相当于以现在的jiffies做偏移,而1s的十分之一就是100ms 230 | 231 | // 定时器原型 232 | struct timer_list { 233 | struct hlist_node entry; 234 | unsigned long expires; 235 | void (*function)(struct timer_list *); 236 | u32 flags; 237 | #ifdef CONFIG_LOCKDEP 238 | struct lockdep_map lockdep_map; 239 | #endif 240 | }; 241 | 242 | // 向内核注册一个定时器 243 | #define timer_setup(timer, callback, flags) 244 | void add_timer(struct timer_list *timer); 245 | 246 | // 向内核删除一个定时器 247 | int del_timer(struct timer_list *timer); 248 | 249 | // 修改定时器的下次的jiffies 250 | int mod_timer(struct timer_list *timer, unsigned long expires) 251 | ``` 252 | 253 | 下面是本文的完整代码,按一次按键,切换一次彩色led的颜色: 254 | ```c 255 | #include 256 | #include 257 | #include // 混杂设备相关结构 258 | #include // 各种gpio的数据结构及函数 259 | #include // 内核中断相关接口 260 | #include 261 | #include 262 | 263 | MODULE_LICENSE("Dual BSD/GPL"); 264 | MODULE_AUTHOR("Philon | https://ixx.life"); 265 | 266 | // 定义按键的GPIO引脚 267 | static const struct gpio key = { 268 | .gpio = 17, // 引脚号为BCM - 17 269 | .flags = GPIOF_IN, // 功能复用为输入 270 | .label = "Key0" // 标示为Key0 271 | }; 272 | 273 | // 定义三色LED的GPIO引脚 274 | static const struct gpio leds[] = { 275 | { 2, GPIOF_OUT_INIT_HIGH, "LED_RED" }, 276 | { 3, GPIOF_OUT_INIT_HIGH, "LED_GREEN" }, 277 | { 4, GPIOF_OUT_INIT_HIGH, "LED_BLUE" }, 278 | }; 279 | 280 | static unsigned int keyirq = 0; // GPIO按键中断号 281 | static struct work_struct keywork; // 按键工作队列 282 | static struct timer_list timer; // 定时器作为中断延时 283 | 284 | // 按键中断“顶半部”处理函数,启用工作队列 285 | static irqreturn_t on_key_press(int irq, void* dev) 286 | { 287 | schedule_work(&keywork); 288 | return IRQ_HANDLED; 289 | } 290 | 291 | // 按键中断“底半部”工作队列,启动一个50ms的延时定时器 292 | void start_timer(struct work_struct *work) 293 | { 294 | mod_timer(&timer, jiffies + (HZ/20)); 295 | } 296 | 297 | // 按键防抖定时器,及处理函数 298 | void on_delay_50ms(struct timer_list *timer) 299 | { 300 | static int i = 0; 301 | if (gpio_get_value(key.gpio)) { 302 | gpio_set_value(leds[i].gpio, 0); 303 | i = ++i == 3 ? 0 : i; 304 | gpio_set_value(leds[i].gpio, 1); 305 | } 306 | } 307 | 308 | static int __init gpiokey_init(void) 309 | { 310 | int rc = 0; 311 | 312 | // 向内核申请GPIO 313 | if ((rc = gpio_request_one(key.gpio, key.flags, key.label)) < 0 314 | || (rc = gpio_request_array(leds, 3)) < 0) { 315 | printk(KERN_ERR "ERROR%d: cannot request gpio\n", rc); 316 | return rc; 317 | } 318 | 319 | // 获取中断号 320 | keyirq = gpio_to_irq(key.gpio); 321 | if (keyirq < 0) { 322 | printk(KERN_ERR "can not get irq num.\n"); 323 | return -EFAULT; 324 | } 325 | 326 | // 申请上升沿触发 327 | if (request_irq(keyirq, on_key_press, IRQF_TRIGGER_RISING, "onKeyPress", NULL) < 0) { 328 | printk(KERN_ERR "can not request irq\n"); 329 | return -EFAULT; 330 | } 331 | 332 | // 初始化按键中断底半部(工作队列) 333 | INIT_WORK(&keywork, start_timer); 334 | 335 | // 初始化定时器 336 | timer_setup(&timer, on_delay_50ms, 0); 337 | add_timer(&timer); 338 | 339 | return 0; 340 | } 341 | module_init(gpiokey_init); 342 | 343 | static void __exit gpiokey_exit(void) 344 | { 345 | free_irq(keyirq, NULL); 346 | gpio_free_array(leds, 3); 347 | gpio_free(key.gpio); 348 | del_timer(&timer); 349 | } 350 | module_exit(gpiokey_exit); 351 | ``` 352 | 353 | # 小结 354 | 355 | - ARM有7种工作模式,其中IRQ和FIQ为中断模式,会导致CPU跳转到指定代码区 356 | - Linux/IRQ分为顶半部和底半部机制 357 | - 顶半部处理要快且不是睡眠 358 | - 底半部又分为4种机制,软中断、tasklet、工作队列、线程IRQ 359 | - 我们可以通过gpio_xxx函数访问CPU资源,而无需地操作底层寄存器 360 | - 如果有延时需求,最好采用内核提供的定时器接口 -------------------------------------------------------------------------------- /02-gpio_key/gpiokey.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include // 混杂设备相关结构 4 | #include // 各种gpio的数据结构及函数 5 | #include // 内核中断相关接口 6 | #include 7 | #include 8 | 9 | MODULE_LICENSE("Dual BSD/GPL"); 10 | MODULE_AUTHOR("Philon | https://ixx.life"); 11 | 12 | // 定义按键的GPIO引脚 13 | static const struct gpio key = { 14 | .gpio = 17, // 引脚号为BCM - 17 15 | .flags = GPIOF_IN, // 功能复用为输入 16 | .label = "Key0" // 标示为Key0 17 | }; 18 | 19 | // 定义三色LED的GPIO引脚 20 | static const struct gpio leds[] = { 21 | { 2, GPIOF_OUT_INIT_HIGH, "LED_RED" }, 22 | { 3, GPIOF_OUT_INIT_HIGH, "LED_GREEN" }, 23 | { 4, GPIOF_OUT_INIT_HIGH, "LED_BLUE" }, 24 | }; 25 | 26 | static unsigned int keyirq = 0; // GPIO按键中断号 27 | static struct work_struct keywork; // 按键工作队列 28 | static struct timer_list timer; // 定时器作为中断延时 29 | 30 | // 按键中断“顶半部”处理函数,启用工作队列 31 | static irqreturn_t on_key_press(int irq, void* dev) 32 | { 33 | schedule_work(&keywork); 34 | return IRQ_HANDLED; 35 | } 36 | 37 | // 按键中断“底半部”工作队列,启动一个50ms的延时定时器 38 | void start_timer(struct work_struct *work) 39 | { 40 | mod_timer(&timer, jiffies + (HZ/20)); 41 | } 42 | 43 | // 按键防抖定时器,及处理函数 44 | void on_delay_50ms(struct timer_list *timer) 45 | { 46 | static int i = 0; 47 | if (gpio_get_value(key.gpio)) { 48 | gpio_set_value(leds[i].gpio, 0); 49 | i = ++i == 3 ? 0 : i; 50 | gpio_set_value(leds[i].gpio, 1); 51 | } 52 | } 53 | 54 | static int __init gpiokey_init(void) 55 | { 56 | int rc = 0; 57 | 58 | // 向内核申请GPIO 59 | if ((rc = gpio_request_one(key.gpio, key.flags, key.label)) < 0 60 | || (rc = gpio_request_array(leds, 3)) < 0) { 61 | printk(KERN_ERR "ERROR%d: cannot request gpio\n", rc); 62 | return rc; 63 | } 64 | 65 | // 获取中断号 66 | keyirq = gpio_to_irq(key.gpio); 67 | if (keyirq < 0) { 68 | printk(KERN_ERR "can not get irq num.\n"); 69 | return -EFAULT; 70 | } 71 | 72 | // 申请上升沿触发 73 | if (request_irq(keyirq, on_key_press, IRQF_TRIGGER_RISING, "onKeyPress", NULL) < 0) { 74 | printk(KERN_ERR "can not request irq\n"); 75 | return -EFAULT; 76 | } 77 | 78 | // 初始化按键中断底半部(工作队列) 79 | INIT_WORK(&keywork, start_timer); 80 | 81 | // 初始化定时器 82 | timer_setup(&timer, on_delay_50ms, 0); 83 | add_timer(&timer); 84 | 85 | return 0; 86 | } 87 | module_init(gpiokey_init); 88 | 89 | static void __exit gpiokey_exit(void) 90 | { 91 | free_irq(keyirq, NULL); 92 | gpio_free_array(leds, 3); 93 | gpio_free(key.gpio); 94 | del_timer(&timer); 95 | } 96 | module_exit(gpiokey_exit); -------------------------------------------------------------------------------- /03-device_io/Makefile: -------------------------------------------------------------------------------- 1 | # 模块驱动,必须以obj-m=xxx形式编写 2 | obj-m = gpiokey.o 3 | 4 | KDIR = $(shell pwd)/../../linux-rpi-4.19.y 5 | 6 | TEST_SRC := $(wildcard *_test.c) 7 | TEST_OBJ := $(TEST_SRC:%.c=%.o) 8 | TEST_OUT := $(TEST_OBJ:%.o=%) 9 | 10 | export ARCH = arm 11 | export CROSS_COMPILE = /opt/arm-mac-linux-gnueabihf/bin/arm-mac-linux-gnueabihf- 12 | 13 | all: module test 14 | 15 | module: 16 | $(MAKE) -C $(KDIR) M=$(PWD) modules 17 | 18 | test: $(TEST_OUT) 19 | $(TEST_OUT): %:%.o 20 | $(CROSS_COMPILE)gcc $(LFLAGS) -o $@ $< 21 | $(TEST_OBJ): %.o:%.c 22 | $(CROSS_COMPILE)gcc $(CLFAGS) -c -o $@ $< 23 | 24 | clean: 25 | $(MAKE) -C $(KDIR) M=`pwd` clean 26 | rm -f $(TEST_OBJ) $(TEST_OUT) -------------------------------------------------------------------------------- /03-device_io/README.md: -------------------------------------------------------------------------------- 1 | # 树莓派驱动开发实战03:设备IO访问技术 2 | 3 | 本文是上一篇《GPIO驱动之按键中断》的扩展,之前的文章侧重于中断原理及Linux/IRQ基础,用驱动实现了一个按键切换LED灯色的功能。但涉及中断的场景,就会拔出萝卜带出泥要实现IO的同步/异步访问方式。归根结底,驱动要站在(用户层)接口调用的角度,除了实现自身的功能,还要为用户层提供一套良好的访问机制。本文主要涉及以下知识点: 4 | 5 | - 机制与策略原则 6 | - IO阻塞/非阻塞——read/write 7 | - IO多路复用——select/epoll 8 | - 信号异步通知——signal 9 | 10 | ## 机制与策略 11 | 12 | Linux的核心思想之一就是“一切皆文件”,而这其中最重要的原则便是“提供机制,而非策略”。文件——内核层与用户层分水岭,通过文件,用户可以用简单而统一的方式去访问复杂而多样的设备,且不必操心设备内部的具体细节。然而哪些部分该驱动实现,哪些部分又该留给用户实现,这是需要拿捏的,我个人理解: 13 | 14 | - 机制,相当于怎么做,提供某个功能范围的实现形式、框架、标准 15 | - 策略,相当于做什么,提供某种功能的具体实现方法和细节 16 | 17 | 以上一篇按键中断的驱动为例——“按一下切换一种灯色”,显然,这根本不符合驱动设计原则。首先,驱动把两种设备打包进一个程序;其次,驱动实现了“切换灯色”这个具体业务功能(策略);最后,驱动根本没有提供用户层的访问机制。 18 | 19 | 还是回到需求——“按一下切换一种灯色”,理想情况下应该是这样的: 20 | 1. LED驱动——向用户层提供灯色切换机制 21 | 2. 按键驱动——向用户层提供“按下事件”通知/获取机制 22 | 3. 由用户层自行决定收到按键事件后,如何切换灯色 23 | 24 | 从以上分析看,原本一个驱动代码分拆分成led驱动、按键驱动、切灯app三个部分:led驱动已经在第1章《GPIO驱动之LED》里实现了,因此现在还差两件事: 25 | - 按键驱动,砍掉原有的led功能实现,增加中断标志获取的访问机制 26 | - 切灯app,这个就无所谓了,只要有了机制,策略想怎么写就怎么写 27 | 28 | ## IO的阻塞/非阻塞访问 29 | 30 | 阻塞/非阻塞,是访问IO的两种不同机制,即同步和异步获取。 31 | 32 | 所谓阻塞访问,就是当用户层`read/write`设备文件时,如果预期的结果还没准备好,进程就会被挂起睡眠,让出CPU资源,直到要读写的资源准备好后,重新唤醒进程执行操作。以本文实际的按键中断来说,当用户层读按键设备节点时,只要按键没有被按下,进程就应该一直阻塞在read函数,直到触发中断后才返回。 33 | 34 | 所谓非阻塞访问,就是调用`open(file, O_NONBLOCK)`或者`ioctl(fd, F_SETFL, O_NONBLOCK)`,将文件句柄设置为非阻塞模式,此时,如果要读写的资源还没准备好,`read/write`会立刻返回`-EAGAIN`错误码,进程不会被挂起。 35 | 36 | 简而言之: 37 | - 阻塞模式:read/write时会被挂起,让出CPU,无法及时响应后续业务 38 | - 非阻塞模式:read/write时不会挂起,占有CPU,不影响后续程序执行 39 | 40 | 显然,通过以上介绍,驱动需要在自己的`xxx_read() xxx_write()`等文件接口里实现阻塞功能,在Linux内核中主要通过“等待队列”来实现进程的阻塞与唤醒的。 41 | 42 | ### 等待队列 43 | 44 | 等待队列是基于链表实现的数据结构,用来表示某个事件上所有等待的进程。内核接口如下: 45 | 46 | ```c 47 | #include 48 | 49 | // 等待队列链表数据结构 50 | struct wait_queue_head { 51 | spinlock_t lock; 52 | struct list_head head; 53 | }; 54 | typedef struct wait_queue_head wait_queue_head_t; 55 | 56 | // 初始化一个等待队列 57 | #define init_waitqueue_head(wq_head) 58 | 59 | // 声明一个等到节点(进程) 60 | #define DECLARE_WAITQUEUE(name, tsk) 61 | 62 | // 添加/删除节点到等待队列中 63 | void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry); 64 | void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry); 65 | 66 | /////////////////////////////////////////// 67 | // 往下看之前先记住两个函数 68 | // set_current_state(value) 👈设置当前进程的状态,比如阻塞 69 | // schedule() 👈调度其他进程 70 | /////////////////////////////////////////// 71 | 72 | // 【接收】开始等待事件到来 73 | // 以下宏,均是由上边的两个函数封装而言,不过是设置进程状态不同罢了 74 | #define wait_event(wq_head, condition) // 不可被中断 75 | #define wait_event_interruptible(wq_head, condition) // 可被信号中断 76 | #define wait_event_timeout(wq_head, condition, timeout) // 会超时 77 | #define wait_event_interruptible_timeout(wq_head, condition, timeout) // 可被信号中断和超时 78 | 79 | // 【发送】唤醒队列中的所有等待队列 80 | #define wake_up(x) 81 | #define wake_up_interruptible(x) 82 | ``` 83 | 84 | 上边的API比较难理解的就是`wait_event_xxx`和`wake_up_xxx`,其实很简单,一般在驱动的读写接口里调用`wait_xxx`让进程切换到阻塞状态等待唤醒,然后在中断或其他地方调用`wake_up_xxx`即可唤醒队列。再有,通常情况下建议使用`interruptible`模式,否则进程将无法被系统信号中断。 85 | 86 | ### 最简单的按键阻塞实现 87 | 88 | 下面来实现按键的阻塞访问,当用`cat /dev/key`命令去读按键是否按下时,应该会被阻塞,直到按键真的被按下。为此,需要实现: 89 | 1. 在`init`函数中初始化gpio按键相关,以及一个“等待队列” 90 | 2. 在`read`函数中创建一个“等待”,然后进入阻塞,直到被唤醒 91 | 3. 在中断响应函数中,唤醒整个“等待队列” 92 | 93 | PS:以下驱动也顺便实现了“非阻塞”访问模式,这个其实很简单,无非就是判断以下文件标识是否为`O_NONBLOCK`即可。 94 | 95 | ```c 96 | #include 97 | #include 98 | #include 99 | #include 100 | #include 101 | #include 102 | 103 | #define KEY_GPIO 17 104 | 105 | MODULE_LICENSE("Dual BSD/GPL"); 106 | MODULE_AUTHOR("Philon | https://ixx.life"); 107 | 108 | static wait_queue_head_t r_wait; 109 | 110 | // 读取阻塞 111 | static ssize_t gpiokey_read(struct file *filp, char __user *buf, size_t len, loff_t * off) 112 | { 113 | int data = 1; 114 | // 新建一个”等待“并加入队列 115 | DECLARE_WAITQUEUE(wait, current); 116 | add_wait_queue(&r_wait, &wait); 117 | 118 | if ((filp->f_flags & O_NONBLOCK) && !gpio_get_value(KEY_GPIO)) { 119 | // 如果是非阻塞访问,且按键没有按下时,直接返回错误 120 | return -EAGAIN; 121 | } 122 | 123 | // 进程进入阻塞状态,等待事件唤醒(可被信号中断) 124 | wait_event_interruptible(r_wait, gpio_get_value(KEY_GPIO)); 125 | remove_wait_queue(&r_wait, &wait); 126 | 127 | // 被唤醒后,进行业务处理,返回一个“按下”标志给用户进程 128 | len = sizeof(data); 129 | if ((data = copy_to_user(buf, &data, len)) < 0) { 130 | return -EFAULT; 131 | } 132 | *off += len; 133 | 134 | return 0; 135 | } 136 | 137 | static int press_irq = 0; 138 | static struct timer_list delay; 139 | 140 | // 按键中断顶半部响应及防抖延时判断 141 | static irqreturn_t on_key_pressed(int irq, void* dev) 142 | { 143 | mod_timer(&delay, jiffies + (HZ/20)); 144 | return IRQ_HANDLED; 145 | } 146 | static void on_delay50(struct timer_list* timer) 147 | { 148 | if (gpio_get_value(KEY_GPIO)) { 149 | // 按下按键后,唤醒阻塞队列 150 | wake_up_interruptible(&r_wait); 151 | } 152 | } 153 | 154 | struct file_operations fops = { 155 | .owner = THIS_MODULE, 156 | .read = gpiokey_read, 157 | }; 158 | 159 | struct miscdevice gpiokey = { 160 | .minor = 1, 161 | .name = "gpiokey", 162 | .fops = &fops, 163 | .nodename = "mykey", 164 | .mode = 0700, 165 | }; 166 | 167 | static int __init gpiokey_init(void) 168 | { 169 | // 初始化定时器,用于防抖延时 170 | timer_setup(&delay, on_delay50, 0); 171 | add_timer(&delay); 172 | 173 | // 初始化“读阻塞”等待队列 174 | init_waitqueue_head(&r_wait); 175 | 176 | // 向内核申请GPIO和IRQ并绑定中断处理函数 177 | gpio_request_one(KEY_GPIO, GPIOF_IN, "key"); 178 | press_irq = gpio_to_irq(KEY_GPIO); 179 | if (request_irq(press_irq, on_key_pressed, IRQF_TRIGGER_RISING, "onKeyPress", NULL) < 0) { 180 | printk(KERN_ERR "Failed to request irq for gpio%d\n", KEY_GPIO); 181 | } 182 | 183 | // 注册驱动模块并创建设备节点 184 | misc_register(&gpiokey); 185 | return 0; 186 | } 187 | module_init(gpiokey_init); 188 | 189 | static void __exit gpiokey_exit(void) 190 | { 191 | misc_deregister(&gpiokey); 192 | free_irq(0, NULL); 193 | gpio_free(KEY_GPIO); 194 | del_timer(&delay); 195 | } 196 | module_exit(gpiokey_exit); 197 | ``` 198 | 199 | ![按键中断阻塞访问效果](https://i.loli.net/2019/08/18/iX8BPOrq1pw74aH.gif) 200 | 201 | ## 多路复用IO模型-poll 202 | 203 | 如果程序只监听一个设备,那用阻塞或非阻塞足够了,但如果设备数量繁多呢?比如我们的键盘,有一百多个键,难道每次都要全部扫描一遍有没有被按下?(当然,键盘事件有另外的机制,这里只是举个例子) 204 | 205 | 这个时候轮询操作就非常有用了,据我所知有很多小型的网络服务正是用此机制实现的高性能并发访问,简单来说,就是把成千上万个socket句柄放到一种名叫`fd_set`的集合里,然后通过`select()/epoll()`同时监听集合里的句柄状态,其中任何一个socket可读写时就唤醒进程并及时响应。 206 | 207 | 综上,设备驱动要做的,便是实现`select/epoll`的底层接口。而有关select的应用层开发这里就不介绍了,网上一大堆。 208 | 209 | 驱动模块的多路复用实现其实非常简单,和读写接口一样,你只需实现`file_operations`里的poll接口: 210 | ```c 211 | #include 212 | 213 | // file_operations->poll 214 | // 由驱动自行实现多路复用功能 215 | __poll_t (*poll) (struct file *, struct poll_table_struct *); 216 | 217 | // 在具体实现poll接口是需要调用 218 | // 该函数本身不会引发阻塞,仅仅是把select的等待指向驱动模块 219 | // 睡眠是由用户层等select()自身完成的 220 | // 当它遍历完全部的设备文件后,相当于把自己的等待节点指向了每一个设备驱动 221 | // 任何一个设备唤醒时都会触发select唤醒 222 | void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p); 223 | 224 | // 既然poll接口不会阻塞,那就直接告诉用户层,设备当前的可操作状态 225 | // 便于select判断文件的读写状态 226 | #define POLLIN 0x0001 // 可读 227 | #define POLLPRI 0x0002 // 紧急数据可读 228 | #define POLLOUT 0x0004 // 可写 229 | #define POLLERR 0x0008 // 错误 230 | #define POLLHUP 0x0010 // 被挂起 231 | #define POLLNVAL 0x0020 // 非法 232 | 233 | #define POLLRDNORM 0x0040 // 普通数据可读 234 | #define POLLRDBAND 0x0080 // 优先数据可读 235 | #define POLLWRNORM 0x0100 // 普通数据可写 236 | #define POLLWRBAND 0x0200 // 优先数据可写 237 | #define POLLMSG 0x0400 // 有消息 238 | #define POLLREMOVE 0x1000 // 被移除 239 | #define POLLRDHUP 0x2000 // 读被挂起 240 | ``` 241 | 242 | 结合第二小结的等待队列,实现gpio按键的多路复用IO就非常简单了,只需要在原有的代码里加入下面这段: 243 | 244 | ```c 245 | // 实现内核的poll接口 246 | static __poll_t gpiokey_poll(struct file *filp, struct poll_table_struct *wait) 247 | { 248 | __poll_t mask = 0; // 设备的可操作状态 249 | 250 | // 加入等待队列 251 | poll_wait(filp, &r_wait, wait); 252 | if (gpio_get_value(KEY_GPIO)) { 253 | // 按键设备不存在写,所以总是返回可读,如果可以时 254 | mask = POLLIN | POLLRDNORM; 255 | } 256 | 257 | return mask; 258 | } 259 | 260 | // 注意接口要加入到文件操作描述里 261 | struct file_operations fops = { 262 | .owner = THIS_MODULE, 263 | .read = gpiokey_read, 264 | .poll = gpiokey_poll, 265 | }; 266 | ``` 267 | 268 | ## 异步通知-信号 269 | 270 | 不论IO阻塞、非阻塞还是多路复用,都是由应用程序主动向设备发起访问,有没有一种机制,就像邮箱一样,当有信息来临时再通知用户,用户仅仅是被动接收——答:信号! 271 | 272 | 信号本质上来说,就是软件层对中断的一种模拟。正如常见的`Ctrl+C`、`kill`等,都是向进程发送信号的手段。所以信号也可以理解为是一种特殊的中断号或事件ID。其实在Linux应用开发中,会涉及很多的“终止/定时/异常/掉电”等信号捕获,我们写的程序之所以能被`Ctrl+C`终止,就是因为在应用接口里已经实现了相关信号的捕获处理。 273 | 274 | 为了更好地理解设备驱动有关信号机制的实现,必须先站在用户层的角度看看信号是如何被调用的。有关Linux常用的标准信号这里也不展开讨论,请用好互联网。这里仅仅是看一个应用程序如何接收指定设备的`SIGIO`信号的: 275 | 276 | ```c 277 | #include 278 | #include 279 | #include 280 | #include 281 | 282 | // 按键信号的处理函数 283 | void on_keypress(int sig) 284 | { 285 | printf("key pressed!!\n"); 286 | } 287 | 288 | int main(int argc, char* argv[]) 289 | { 290 | // 第一步:将驱动的拥有者指向本进程,否则设备信号不知道发给谁 291 | int oflags = 0; 292 | int fd = open("/dev/mykey", O_RDONLY); 293 | fcntl(fd, F_SETOWN, getpid()); 294 | oflags = fcntl(fd, F_GETFL) | O_ASYNC; 295 | fcntl(fd, F_SETFL, oflags); 296 | 297 | // 第二步:捕获想要的信号,并绑定到相关处理函数 298 | signal(SIGIO, on_keypress); 299 | 300 | // 以下无关紧要,就是等到程序退出 301 | printf("I'm doing something ...\n"); 302 | getchar(); 303 | close(fd); 304 | return 0; 305 | } 306 | ``` 307 | 308 | 从以上代码来看,应用程序要实现信号捕获需要操作: 309 | 310 | 1. 用`F_SETOWN`让设备文件指向自己,确保信号的传输目的地 311 | 2. 用`O_ASYNC`或者`FASYNC`标志告诉驱动(即调用驱动的`xxx_fasync`接口),我要去做其他事了,有情况请主动通知我 312 | 3. 设置信号捕获及相关处理handler 313 | 314 | 所以对应的,内核模块也需要实现信号的发送也需要三个步骤: 315 | 316 | 1. `filp->f_onwer`指向进程ID,这点已经又内核完成,不用再实现 317 | 2. 实现`xxx_fasync()`接口,在里面初始化一个`fasync_struct`用于信号处理 318 | 3. 当有情况时,使用`kill_fasync()`发送信号给进程 319 | 320 | 内核具体接口如下: 321 | 322 | ```c 323 | // 设备文件的异步接口,当用户层标记了O_ASYNC或FASYNC时触发 324 | struct file_operations { 325 | int (*fasync) (int fd, struct file *filp, int mode); 326 | ... 327 | }; 328 | 329 | // 异步“小助手”,初始化用,一般在xxx_fasync()接口里调用 330 | // 前面三个参数由用户层传进来,最后一个是“异步队列”,该函数会为其分配内存初始化 331 | int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa); 332 | 333 | // 通过fa,发送信号到进程 334 | // 后两个参数为信号ID、可读/可写状态 335 | void kill_fasync(struct fasync_struct **fa, int sig, int band); 336 | 337 | // 最后务必注意,在xxx_close或xxx_release中让文件描述从异步队列中剥离 338 | // 否则用户进程挂了,驱动还一直向其发送信号,岂不有病 339 | static int xxx_close(struct inode *node, struct file *filp) 340 | { 341 | ... 342 | xxx_fasync(-1, filp, 0); 343 | } 344 | ``` 345 | 346 | 搞清楚了内核关于异步信号的机制,下面用让gpiokey支持SIGIO信号吧! 347 | 348 | ```c 349 | static struct { 350 | int irq; // 按键GPIO中断号 351 | struct timer_list delay; // 防抖延时 352 | wait_queue_head_t r_wait; // IO阻塞等待队列 353 | struct fasync_struct* fa; // 异步描述 354 | } dev; 355 | 356 | // 实现fops->gpiokey_fasync接口,支持用户层的FASYNC标记 357 | // 将用户进程文件描述添加到异步队列中 358 | static int gpiokey_fasync(int fd, struct file *filp, int mode) 359 | { 360 | return fasync_helper(fd, filp, mode, &dev.fa); 361 | } 362 | 363 | // 用户进程关闭设备时,务必将其从异步队列中剥离 364 | static int gpiokey_close(struct inode *node, struct file *filp) 365 | { 366 | gpiokey_fasync(-1, filp, 0); 367 | return 0; 368 | } 369 | 370 | // 当按键中断触发后,将信号发送至用户进程 371 | static irqreturn_t on_key_pressed(int irq, void* dev_id) 372 | { 373 | mod_timer(&dev.delay, jiffies + (HZ/20)); 374 | return IRQ_HANDLED; 375 | } 376 | static void on_delay50(struct timer_list* timer) 377 | { 378 | if (gpio_get_value(KEY_GPIO)) { 379 | wake_up_interruptible(&dev.r_wait); // 唤醒阻塞队列 380 | kill_fasync(&dev.fa, SIGIO, POLL_IN); // 发送SIGIO异步信号 381 | } 382 | } 383 | 384 | struct file_operations fops = { 385 | ... 386 | .fasync = gpiokey_fasync, 387 | .release = gpiokey_close, 388 | }; 389 | ``` 390 | 391 | 从输出结果中可以看到,程序启动并执行后续,完全没有监听设备,当按键被按下时,信号传回进程并触发了`on_keypress()`函数。 392 | 393 | ```sh 394 | philon@rpi:~/modules $ sudo insmod gpiokey.ko 395 | philon@rpi:~/modules $ ./signal_test 396 | I'm doing something ... 397 | key pressed!! 398 | key pressed!! 399 | ``` 400 | 401 | ## 异步IO 402 | 403 | 自Linux2.6以后,IO的异步访问又多了一种新方式——`aio`,此方式在实际开发中并不多见,尤其是嵌入式领域!因此本文不打算深入讨论,这里作为知识扩展仅做个简单介绍。 404 | 405 | 异步IO的核心思想就是——回调,例如`aio_read(struct aiocb *cb)`和`aio_write(truct aiocb *cb)`,程序调用该函数后不会阻塞,当文件读写就绪后,会自动根据`cb`描述进行回调。 406 | 407 | 此外,AIO有应用层基于线程的glibc实现,以及内核层的fops接口实现,甚至还有类型libuv、libevent这样的事件驱动的第三方框架可供使用。 408 | 409 | 就我个人而言,技术是把双刃剑,回调是一种看似美妙的骚操作,但如果你编写的业务具有强逻辑性,那回调在时序上的失控,以及返回状态的多样化,会随着代码的壮大而进入回调陷阱,深深地无法自拔。给我这种感受的并非C语言,而是JavaScript。 410 | 411 | 总之,没有最优秀的技术,只有最适用的场景!我的原则是:用回调,远离嵌套回调。 412 | 413 | ## 小结 414 | 415 | - Linux内核模块应当“提供机制,而非策略” 416 | - 阻塞IO是在用户层读写访问时,是进程睡眠,由驱动来唤醒 417 | - 非阻塞IO是有`IO_NONBLOCK`标记时,当资源不可访问时,直接返回`-EAGAIN` 418 | - 多路复用IO是通过`select/epoll`进行多个设备监听,驱动须实现对应的`fops->poll`接口 419 | - 异步IO即信号,由设备驱动作为信号源,主动向进程发送通知 420 | - 不同的IO同步/异步访问机制无优劣之分,而是取决于具体的应用场景 421 | - 务必搞懂`等待队列`,它贯穿以上几种IO访问机制 -------------------------------------------------------------------------------- /03-device_io/gpiokey.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | MODULE_LICENSE("Dual BSD/GPL"); 10 | MODULE_AUTHOR("Philon | https://ixx.life"); 11 | 12 | #define KEY_GPIO 17 13 | 14 | static struct { 15 | int irq; // 按键GPIO中断号 16 | struct timer_list delay; // 防抖延时 17 | wait_queue_head_t r_wait; // IO阻塞等待队列 18 | struct fasync_struct* fa; // 异步IO 19 | } dev; 20 | 21 | static int gpiokey_open(struct inode *node, struct file *filp) 22 | { 23 | return 0; 24 | } 25 | 26 | static __poll_t gpiokey_poll(struct file *filp, struct poll_table_struct *wait) 27 | { 28 | __poll_t mask = 0; 29 | 30 | // 加入等待队列 31 | poll_wait(filp, &dev.r_wait, wait); 32 | if (gpio_get_value(KEY_GPIO)) { 33 | mask = POLLIN | POLLRDNORM; 34 | } 35 | 36 | return mask; 37 | } 38 | 39 | static ssize_t gpiokey_read(struct file *filp, char __user *buf, size_t len, loff_t * off) 40 | { 41 | int data = 1; 42 | DECLARE_WAITQUEUE(wait, current); 43 | add_wait_queue(&dev.r_wait, &wait); 44 | 45 | if ((filp->f_flags & O_NONBLOCK) && !gpio_get_value(KEY_GPIO)) { 46 | // 如果是非阻塞访问,且按键没有按下时,直接返回错误 47 | return -EAGAIN; 48 | } 49 | 50 | // 等待事件唤醒(可被信号中断),进程进入阻塞状态 51 | wait_event_interruptible(dev.r_wait, gpio_get_value(KEY_GPIO)); 52 | remove_wait_queue(&dev.r_wait, &wait); 53 | 54 | // 被唤醒后,进行业务处理,返回一个“按下”标志给用户进程 55 | len = sizeof(data); 56 | if ((data = copy_to_user(buf, &data, len)) < 0) { 57 | return -EFAULT; 58 | } 59 | *off += len; 60 | 61 | return 0; 62 | } 63 | 64 | static int gpiokey_fasync(int fd, struct file *filp, int mode) 65 | { 66 | return fasync_helper(fd, filp, mode, &dev.fa); 67 | } 68 | 69 | static int gpiokey_close(struct inode *node, struct file *filp) 70 | { 71 | gpiokey_fasync(-1, filp, 0); 72 | return 0; 73 | } 74 | 75 | static long gpiokey_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) 76 | { 77 | return 0; 78 | } 79 | 80 | // 按键中断顶半部响应及防抖延时判断 81 | static irqreturn_t on_key_pressed(int irq, void* dev_id) 82 | { 83 | mod_timer(&dev.delay, jiffies + (HZ/20)); 84 | return IRQ_HANDLED; 85 | } 86 | static void on_delay50(struct timer_list* timer) 87 | { 88 | if (gpio_get_value(KEY_GPIO)) { 89 | // 按下按键后,唤醒阻塞队列 90 | wake_up_interruptible(&dev.r_wait); 91 | kill_fasync(&dev.fa, SIGIO, POLL_IN); 92 | } 93 | } 94 | 95 | struct file_operations fops = { 96 | .owner = THIS_MODULE, 97 | .open = gpiokey_open, 98 | .release = gpiokey_close, 99 | .read = gpiokey_read, 100 | .poll = gpiokey_poll, 101 | .fasync = gpiokey_fasync, 102 | .unlocked_ioctl = gpiokey_ioctl, 103 | }; 104 | 105 | struct miscdevice gpiokey = { 106 | .minor = 1, 107 | .name = "gpiokey", 108 | .fops = &fops, 109 | .nodename = "mykey", 110 | .mode = 0744, 111 | }; 112 | 113 | static int __init gpiokey_init(void) 114 | { 115 | // 初始化定时器,用于防抖延时 116 | timer_setup(&dev.delay, on_delay50, 0); 117 | add_timer(&dev.delay); 118 | 119 | // 初始化“读阻塞”等待队列 120 | init_waitqueue_head(&dev.r_wait); 121 | 122 | // 向内核申请GPIO和IRQ并绑定中断处理函数 123 | gpio_request_one(KEY_GPIO, GPIOF_IN, "key"); 124 | dev.irq = gpio_to_irq(KEY_GPIO); 125 | if (request_irq(dev.irq, on_key_pressed, IRQF_TRIGGER_RISING, "onKeyPress", NULL) < 0) { 126 | printk(KERN_ERR "Failed to request irq for gpio%d\n", KEY_GPIO); 127 | } 128 | 129 | // 注册驱动模块并创建设备节点 130 | misc_register(&gpiokey); 131 | return 0; 132 | } 133 | module_init(gpiokey_init); 134 | 135 | static void __exit gpiokey_exit(void) 136 | { 137 | misc_deregister(&gpiokey); 138 | free_irq(dev.irq, NULL); 139 | gpio_free(KEY_GPIO); 140 | del_timer(&dev.delay); 141 | } 142 | module_exit(gpiokey_exit); -------------------------------------------------------------------------------- /03-device_io/select_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main(int argc, char* argv[]) 9 | { 10 | int fd = open("/dev/mykey", O_RDONLY); 11 | if (fd < 0) { 12 | perror("open"); 13 | exit(0); 14 | } 15 | 16 | fd_set fds; 17 | while (1) { 18 | FD_ZERO(&fds); 19 | FD_SET(fd, &fds); 20 | struct timeval timeout = {1, 0}; 21 | int rc = select(fd + 1, &fds, NULL, NULL, &timeout); 22 | if (rc == 0) { 23 | printf("Timeout!!\n"); 24 | continue; 25 | } else if (rc < 0) { 26 | perror("select"); 27 | break; 28 | } else { 29 | if (FD_ISSET(fd, &fds)) printf("Key pressed!!"); 30 | } 31 | } 32 | 33 | close(fd); 34 | return 0; 35 | } -------------------------------------------------------------------------------- /03-device_io/signal_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | void on_keypress(int sig) 7 | { 8 | printf("key pressed!!\n"); 9 | } 10 | 11 | int main(int argc, char* argv[]) 12 | { 13 | // 第一步:将驱动的拥有者指向本进程,否则设备信号不知道发给谁 14 | int oflags = 0; 15 | int fd = open("/dev/mykey", O_RDONLY); 16 | fcntl(fd, F_SETOWN, getpid()); 17 | oflags = fcntl(fd, F_GETFL) | O_ASYNC; 18 | fcntl(fd, F_SETFL, oflags); 19 | 20 | // 第二步:捕获想要的信号,并绑定到相关处理函数 21 | signal(SIGIO, on_keypress); 22 | 23 | // 以下无关紧要,就是等到程序退出 24 | printf("I'm doing something ...\n"); 25 | getchar(); 26 | 27 | close(fd); 28 | return 0; 29 | } -------------------------------------------------------------------------------- /04-pwm_led/Makefile: -------------------------------------------------------------------------------- 1 | # 模块驱动,必须以obj-m=xxx形式编写 2 | obj-m = pwmled.o 3 | 4 | KDIR = $(shell pwd)/../../linux-rpi-4.19.y 5 | 6 | TEST_SRC := $(wildcard *_test.c) 7 | TEST_OBJ := $(TEST_SRC:%.c=%.o) 8 | TEST_OUT := $(TEST_OBJ:%.o=%) 9 | 10 | CLFAGS += -std=c99 11 | 12 | export ARCH = arm 13 | export CROSS_COMPILE = /opt/arm-mac-linux-gnueabihf/bin/arm-mac-linux-gnueabihf- 14 | 15 | all: module test install 16 | 17 | install: module 18 | scp $(obj-m:%.o=%.ko) rpi.local:~/modules 19 | scp $(TEST_OUT) rpi.local:~/modules 20 | 21 | module: 22 | $(MAKE) -C $(KDIR) M=$(PWD) modules 23 | 24 | test: $(TEST_OUT) 25 | $(TEST_OUT): %:%.o 26 | $(CROSS_COMPILE)gcc $(LFLAGS) -o $@ $< 27 | $(TEST_OBJ): %.o:%.c 28 | $(CROSS_COMPILE)gcc $(CLFAGS) -c -o $@ $< 29 | 30 | clean: 31 | $(MAKE) -C $(KDIR) M=`pwd` clean 32 | rm -f $(TEST_OBJ) $(TEST_OUT) -------------------------------------------------------------------------------- /04-pwm_led/README.md: -------------------------------------------------------------------------------- 1 | # 树莓派驱动开发实战04:PWM呼吸灯 2 | 3 | 如果你对硬件了解不是很深,看了标题可能会一脸懵逼,PWM是什么?别急,在回答这个问题之前,先看看PWM+一个普通的LED灯能实现什么效果: 4 | 5 | ![pwmled.gif](https://i.loli.net/2019/09/08/zYci2Z4kymaEbho.gif) 6 | 7 | (动图有点大,请稍等...) 8 | 9 | 从结果来看可能第一反应是这样的,普通的LED灯只能控制开/关,既然PWM可以控制LED的亮和暗,那它应该是一种电压控制器!其实根本不是那么回事,和电压毛关系都没有。它背后的原理恰恰是它最有意思的地方,它的本领可不仅仅是拿来控制个灯的明暗那么low。 10 | 11 | ## PWM基础 12 | 13 | PWM——脉冲宽度调制,顾名思义,就是一个脉冲信号输出源,且方波的周期(period)以及高电平持续时间(duty)是可调的。从软件角度来看,主要关注两个地方: 14 | 15 | - `period`,脉冲周期,就是发送一次1和0交替的完整时间 16 | - `duty`,占空比,就是一个脉冲周期内1占了多长时间。 17 | 18 | 有时候,还需要关注1秒钟发送多少个脉冲信号,即频率,不过对于本文要控制的led灯亮度而言,频率几乎可以忽略。关于PWM原理,如果还是云里雾里,就看一下这个视频介绍,非常简单。 19 | 20 | 不过先提醒以下,PWM的实际应用非常广,有红外遥控、音频控制、通信等,最常见的要数直流电机控制,以及手机上的背光灯亮度。总之,掌握pwm的原理及应用是灰常有必要滴。 21 | 22 | 23 | 24 | ## 树莓派上的PWM 25 | 26 | 树莓派上的PWM比较坑,明明很简单的东西我调了两天,网上99%的教程都是各种应用层的脚本或writingPi的函数调用,和Linux内核的边都沾不上,而且其中绝大多数还是相互复制粘贴。所以真正有用的资料寥寥无几,一直卡在`pwm_request`却获取不到pwm资源,当然最主要原因还是我对技术细节的不理解,好在发现了一个老外的博客:https://jumpnowtek.com/rpi/Using-the-Raspberry-Pi-Hardware-PWM-timers.html 27 | 28 | 在官方的[《BCM2835外围指南》](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2835/BCM2835-ARM-Peripherals.pdf)第9章说得很清楚,处理器内部集成了两个独立的PWM控制器,理论上可以达到100MHz的脉冲频率。**树莓派扩展接口共有4个GPIO引出PWM**,具体为: 29 | 30 | |PWM通道| GPIO号 |物理引脚| 复用功能 | 31 | |------|--------|------|-----------| 32 | | PWM0 | GPIO12 | 32 | Alt Fun 0 | 33 | | PWM1 | GPIO13 | 33 | Alt Fun 0 | 34 | | PWM0 | GPIO18 | 12 | Alt Fun 5 | 35 | | PWM1 | GPIO19 | 35 | Alt Fun 5 | 36 | 37 | **第一步,启用pwm(默认情况下未启用)** 38 | 39 | 简而言之,你无法通过Linux内核API获取到PWM资源,因为在树莓派官方的设备树配置(`/boot/config.txt`)里并没有通知内核要启用pwm。因此第一步自然是让内核支持pwm驱动,使用如下命令: 40 | 41 | ```sh 42 | # vim打开/boot/config.txt 43 | # 在最后一行加入: dtoverlay=pwm 44 | # 保存退出,重启 45 | 46 | philon@rpi:~ $ sudo vim /boot/config.txt 47 | philon@rpi:~ $ sudo reboot 48 | 49 | # 重启之后,有两种方式确认pwm已启用 50 | philon@rpi:~ $ lsmod | grep pwm 51 | pwm_bcm2835 16384 1 # 方式1: 加载了官方pwm驱动 52 | 53 | philon@rpi:~ $ ls /sys/class/pwm/ 54 | pwmchip0 # 方式2: sysfs里可以看到pwmchip0目录 55 | ``` 56 | 57 | **第二步,搭建硬件环境** 58 | 59 | 非常简单,LED的正极拉到一个PWM通道,负极随便找一个GND接上。 60 | 61 | ![PWMLED接线图](https://i.loli.net/2019/09/08/fKURQTq8O6rgSAp.png) 62 | 63 | 如图所示,将三色LED的绿灯脚接到GPIO18,也就是PWM0通道,再随便找一个GND接上即可,和第一章的点亮一个LED一样,关键看软件如何操作了。 64 | 65 | ![PWMLED原理图](https://i.loli.net/2019/09/08/IYM7C91OXikftjy.png) 66 | 67 | 68 | ## 使用命令行控制PWM 69 | 70 | 根据之前的硬件接线,LED与树莓派的PWM0通道相连,所以使能pwm0即可点亮led,大体步骤为: 71 | 72 | 1. 请求pwm0资源 73 | 2. 设置脉冲周期 74 | 3. 设置占空比 75 | 4. 打开pwm0 76 | 77 | 命令行控制pwm其实和gpio大同小异,都是通过sysfs这个虚拟文件系统完成的。 78 | 79 | ```sh 80 | philon@rpi:~ $ cd /sys/class/pwm/pwmchip0/ # 进入pwm资源目录 81 | 82 | philon@rpi:~ $ echo 0 > export # 加载pwm0资源 83 | philon@rpi:~ $ echo 10000000 > pwm0/period # 设置脉冲周期为10ms(100Hz) 84 | philon@rpi:~ $ echo 8000000 > pwm0/duty_cycle # 设置占空比为8ms 85 | philon@rpi:~ $ echo 1 > pwm0/enable # 开始输出 86 | 87 | # 可以自行调整脉冲周期和占空比,得到不同的亮度 88 | # 如果玩够了,记得释放资源 89 | philon@rpi:~ $ echo 0 > pwm0/enable # 关闭输出 90 | philon@rpi:~ $ echo 0 > unexport # 卸载pwm0资源 91 | ``` 92 | 93 | 经过上面这番犀利操作,只要你够虔诚,就会看见一束绿光闯入你的眼里,心里,脑海里。 94 | 95 | ## Linux驱动控制PWM 96 | 97 | 在实现pwm调光led之前,需要先交代清楚: 98 | 99 | 由于本驱动仅仅是调节led的亮度,关于pwm的`占空比`和`脉冲周期`两个参数,其实只调节占空比就够了。你想一下,脉冲周期长短,无非就是led闪烁频率,只要人眼看着不闪,再快有个毛用。因此本驱动将脉冲周期固定为1ms,即1KHz,而占空比的取值为0~1000us,说白了,就是led从最暗到最亮一共分为一千个档位。 100 | 101 | 内核为pwm提供了标准的API接口,需要掌握这几个: 102 | 103 | ```c 104 | // PWM channel object 105 | struct pwm_device { 106 | const char *label; // name of the PWM device 107 | unsigned long flags; // flags associated with the PWM device 108 | unsigned int hwpwm; // per-chip relative index of the PWM device 109 | unsigned int pwm; // global index of the PWM device 110 | struct pwm_chip *chip; // PWM chip providing this PWM device 111 | void *chip_data; // chip-private data associated with the PWM device 112 | struct pwm_args args; // PWM arguments 113 | struct pwm_state state; // curent PWM channel state 114 | }; 115 | 116 | /** 117 | * 通过pwm通道号获取pwm通道对象 118 | * @pwm_id 通道号 119 | * @label pwm通道别名 120 | */ 121 | struct pwm_device *pwm_request(int pwm_id, const char *label); 122 | 123 | /** 124 | * 释放pwm通道对象 125 | */ 126 | void pwm_free(struct pwm_device *pwm); 127 | 128 | /** 129 | * 设置pwm通道的相关参数 130 | * @duty_ns 以纳秒为单位的占空比 131 | * @period_ns 以纳秒为单位的脉冲周期 132 | */ 133 | int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns) 134 | 135 | /** 136 | * 打开pwm通道,开始输出脉冲 137 | */ 138 | int pwm_enable(struct pwm_device *pwm) 139 | 140 | /** 141 | * 关闭pwm通道,停止输出脉冲 142 | */ 143 | void pwm_disable(struct pwm_device *pwm) 144 | ``` 145 | 146 | 接着,实现pwmled的驱动吧: 147 | ```c 148 | /********************* pwmled.h *********************/ 149 | 150 | #define PWMLED_MAX_BRIGHTNESS 1000 151 | 152 | typedef enum { 153 | PWMLED_CMD_SET_BRIGHTNESS = 0x1, 154 | PWMLED_CMD_GET_BRIGHTNESS, 155 | } pwmled_cmd_t; 156 | 157 | /********************* pwmled.c *********************/ 158 | 159 | #include 160 | #include 161 | #include 162 | #include 163 | 164 | #include "pwmled.h" 165 | 166 | MODULE_LICENSE("Dual MIT/GPL"); 167 | MODULE_AUTHOR("Phlon | https://ixx.life"); 168 | 169 | #define PWMLED_PERIOD 1000000 // 脉冲周期固定为1ms 170 | 171 | static struct { 172 | struct pwm_device* pwm; 173 | unsigned int brightness; 174 | } pwmled; 175 | 176 | long pwmled_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { 177 | switch (cmd) { 178 | case PWMLED_CMD_SET_BRIGHTNESS: 179 | // 所谓调节亮度,就是配置占空比,然后使能pwm0 180 | pwmled.brightness = arg < PWMLED_MAX_BRIGHTNESS ? arg : PWMLED_MAX_BRIGHTNESS; 181 | pwm_config(pwmled.pwm, pwmled.brightness * 1000, PWMLED_PERIOD); 182 | if (pwmled.brightness > 0) { 183 | pwm_enable(pwmled.pwm); 184 | } else { 185 | pwm_disable(pwmled.pwm); 186 | } 187 | case PWMLED_CMD_GET_BRIGHTNESS: 188 | return pwmled.brightness; 189 | default: 190 | return -EINVAL; 191 | } 192 | 193 | return pwmled.brightness; 194 | } 195 | 196 | static struct file_operations fops = { 197 | .owner = THIS_MODULE, 198 | .unlocked_ioctl = pwmled_ioctl, 199 | }; 200 | 201 | static struct miscdevice dev = { 202 | .minor = 0, 203 | .name = "pwmled", 204 | .fops = &fops, 205 | .nodename = "pwmled", 206 | .mode = 0666, 207 | }; 208 | 209 | int __init pwmled_init(void) { 210 | // 请求PWM0通道 211 | struct pwm_device* pwm = pwm_request(0, "pwm0"); 212 | if (IS_ERR_OR_NULL(pwm)) { 213 | printk(KERN_ERR "failed to request pwm\n"); 214 | return PTR_ERR(pwm); 215 | } 216 | 217 | pwmled.pwm = pwm; 218 | pwmled.brightness = 0; 219 | 220 | misc_register(&dev); 221 | 222 | return 0; 223 | } 224 | module_init(pwmled_init); 225 | 226 | void __exit pwmled_exit(void) { 227 | misc_deregister(&dev); 228 | // 停止并释放PWM0通道 229 | pwm_disable(pwmled.pwm); 230 | pwm_free(pwmled.pwm); 231 | } 232 | module_exit(pwmled_exit); 233 | ``` 234 | 235 | 该驱动会自动创建`/dev/pwmled`设备节点,然后应用层通过ioctl即可设置和获取灯的亮度等级。 236 | 237 | ```c 238 | int main(int argc, char* argv[]) { 239 | int fd = open("/dev/pwmled", O_RDWR); 240 | int brightness = 0; 241 | char key = 0; 242 | 243 | while ((key = getchar()) != 'q') { 244 | switch (key) { 245 | case '=': 246 | brightness += brightness < PWMLED_MAX_BRIGHTNESS ? 10 : 0; 247 | break; 248 | case '-': 249 | brightness -= brightness > 0 ? 10 : 0; 250 | break; 251 | } 252 | 253 | if (ioctl(fd, PWMLED_CMD_SET_BRIGHTNESS, brightness) < 0) { 254 | perror("ioctl"); 255 | break; 256 | } 257 | } 258 | 259 | close(fd); 260 | return 0; 261 | } 262 | ``` 263 | 264 | 正如本文开头那张动图一样,用户只需要按`+/-`即可调节led的亮度,so easy~ 265 | 266 | ## 小结 267 | 268 | - PWM即脉冲宽度调制,可以实时改变脉冲源的占空比和周期时长 269 | - 树莓派Linux内核默认不加载pwm通道,需要在/boot/config.txt中增加一行dtoverlay=pwm 270 | - pwm可以像gpio一样通过命令行对sysfs虚拟文件系统操作,即可控制pwm资源 271 | - 内核提供了标准的pwm接口,注意通道值的配置是以纳秒为单位 272 | - pwm技术应用范围非常广,务必牢牢掌握 -------------------------------------------------------------------------------- /04-pwm_led/pwmled.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "pwmled.h" 7 | 8 | MODULE_LICENSE("Dual MIT/GPL"); 9 | MODULE_AUTHOR("Phlon | https://ixx.life"); 10 | 11 | #define PWMLED_PERIOD 1000000 // 脉冲周期固定为1ms 12 | 13 | static struct { 14 | struct pwm_device* pwm; 15 | unsigned int brightness; 16 | } pwmled; 17 | 18 | long pwmled_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { 19 | switch (cmd) { 20 | case PWMLED_CMD_SET_BRIGHTNESS: 21 | // 所谓调节亮度,就是配置占空比,然后使能pwm0 22 | pwmled.brightness = arg < PWMLED_MAX_BRIGHTNESS ? arg : PWMLED_MAX_BRIGHTNESS; 23 | pwm_config(pwmled.pwm, pwmled.brightness * 1000, PWMLED_PERIOD); 24 | if (pwmled.brightness > 0) { 25 | pwm_enable(pwmled.pwm); 26 | } else { 27 | pwm_disable(pwmled.pwm); 28 | } 29 | case PWMLED_CMD_GET_BRIGHTNESS: 30 | return pwmled.brightness; 31 | default: 32 | return -EINVAL; 33 | } 34 | 35 | return pwmled.brightness; 36 | } 37 | 38 | static struct file_operations fops = { 39 | .owner = THIS_MODULE, 40 | .unlocked_ioctl = pwmled_ioctl, 41 | }; 42 | 43 | static struct miscdevice dev = { 44 | .minor = 0, 45 | .name = "pwmled", 46 | .fops = &fops, 47 | .nodename = "pwmled", 48 | .mode = 0666, 49 | }; 50 | 51 | int __init pwmled_init(void) { 52 | // 请求PWM0通道 53 | struct pwm_device* pwm = pwm_request(0, "pwm0"); 54 | if (IS_ERR_OR_NULL(pwm)) { 55 | printk(KERN_ERR "failed to request pwm\n"); 56 | return PTR_ERR(pwm); 57 | } 58 | 59 | pwmled.pwm = pwm; 60 | pwmled.brightness = 0; 61 | 62 | misc_register(&dev); 63 | 64 | return 0; 65 | } 66 | module_init(pwmled_init); 67 | 68 | void __exit pwmled_exit(void) { 69 | misc_deregister(&dev); 70 | // 停止并释放PWM0通道 71 | pwm_disable(pwmled.pwm); 72 | pwm_free(pwmled.pwm); 73 | } 74 | module_exit(pwmled_exit); -------------------------------------------------------------------------------- /04-pwm_led/pwmled.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Philon/rpi-drivers/d1c55d06ef4bc42ff8dee4c4a9962266a36e752c/04-pwm_led/pwmled.fzz -------------------------------------------------------------------------------- /04-pwm_led/pwmled.h: -------------------------------------------------------------------------------- 1 | #ifndef RPI_DRIVER_INACTION_PWMLED_H 2 | #define RPI_DRIVER_INACTION_PWMLED_H 3 | 4 | // PWM输出的频率为 1k Hz 5 | #define PWMLED_MAX_BRIGHTNESS 1000 6 | 7 | typedef enum { 8 | PWMLED_CMD_SET_BRIGHTNESS = 0x1, 9 | PWMLED_CMD_GET_BRIGHTNESS, 10 | } pwmled_cmd_t; 11 | 12 | #endif // !RPI_DRIVER_INACTION_PWMLED_H -------------------------------------------------------------------------------- /04-pwm_led/pwmled_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "pwmled.h" 9 | 10 | #define PWM_LED "/dev/pwmled" 11 | 12 | void init_stdin() { 13 | struct termios term; 14 | tcgetattr(0, &term); 15 | term.c_lflag &= (~ICANON); 16 | term.c_cc[VTIME] = 0; 17 | term.c_cc[VMIN] = 1; 18 | tcsetattr(0, TCSANOW, &term); 19 | } 20 | 21 | int main(int argc, char* argv[]) { 22 | int fd = open(PWM_LED, O_RDWR); 23 | int brightness = 0; 24 | char key = 0; 25 | 26 | init_stdin(); 27 | while ((key = getchar()) != 'q') { 28 | switch (key) { 29 | case '=': 30 | brightness += brightness < PWMLED_MAX_BRIGHTNESS ? 10 : 0; 31 | break; 32 | case '-': 33 | brightness -= brightness > 0 ? 10 : 0; 34 | break; 35 | case '9': 36 | brightness = PWMLED_MAX_BRIGHTNESS; 37 | break; 38 | case '0': 39 | brightness = 0; 40 | break; 41 | default: 42 | // printf("unknown opt: '%c'\n", key); 43 | break; 44 | } 45 | 46 | if (ioctl(fd, PWMLED_CMD_SET_BRIGHTNESS, brightness) < 0) { 47 | perror("ioctl"); 48 | break; 49 | } 50 | } 51 | 52 | close(fd); 53 | return 0; 54 | } -------------------------------------------------------------------------------- /05-pwm_musicbox/Makefile: -------------------------------------------------------------------------------- 1 | # 模块驱动,必须以obj-m=xxx形式编写 2 | obj-m = musicbox.o 3 | 4 | KDIR = $(shell pwd)/../../linux-rpi-4.19.y 5 | 6 | TEST_SRC := $(wildcard *_test.c) 7 | TEST_OBJ := $(TEST_SRC:%.c=%.o) 8 | TEST_OUT := $(TEST_OBJ:%.o=%) 9 | 10 | CLFAGS += -std=gnu99 11 | 12 | export ARCH = arm 13 | export CROSS_COMPILE = /opt/arm-mac-linux-gnueabihf/bin/arm-mac-linux-gnueabihf- 14 | 15 | all: module test install 16 | 17 | install: module 18 | scp $(obj-m:%.o=%.ko) rpi.local:~/modules 19 | scp $(TEST_OUT) rpi.local:~/modules 20 | 21 | module: 22 | $(MAKE) -C $(KDIR) M=$(PWD) modules 23 | 24 | test: $(TEST_OUT) 25 | scp -r ./music/ rpi.local:~/modules/ 26 | $(TEST_OUT): %:%.o 27 | $(CROSS_COMPILE)gcc $(LFLAGS) -o $@ $< 28 | $(TEST_OBJ): %.o:%.c 29 | $(CROSS_COMPILE)gcc $(CLFAGS) -c -o $@ $< 30 | 31 | clean: 32 | $(MAKE) -C $(KDIR) M=`pwd` clean 33 | rm -f $(TEST_OBJ) $(TEST_OUT) -------------------------------------------------------------------------------- /05-pwm_musicbox/README.md: -------------------------------------------------------------------------------- 1 | # 树莓派驱动开发实战05:PWM音乐盒 2 | 3 | 上一篇用LED呼吸灯的方式,基本介绍了PWM原理以及在树莓派上的驱动开发,但总感觉意犹未尽,所以再写一篇PWM的应用场景——PWM+蜂鸣器,实现一个简易的音乐盒。 4 | 5 | 说明一下: 本篇纯粹是“玩”,并不涉及任何新的知识点,如果不感兴趣可以掠过。 6 | 7 | 先来看看我实现的效果:《保卫黄河》、《灌篮高手》、《欢乐斗地主》。这些谱子全是我从网上扒下来的,并根据蜂鸣器的效果修改过,自己不是专业搞音乐的,所以难免会有错误的地方。(反正我耳朵里听着没问题就行😁) 8 | 9 | ```sh 10 | philon@rpi:~ $ cd modules/ 11 | philon@rpi:~/modules $ sudo insmod musicbox.ko 12 | philon@rpi:~/modules $ ./player_test music/01-保卫黄河 # 🎵 13 | philon@rpi:~/modules $ ./player_test music/04-灌篮高手主题曲 # 🎵 14 | philon@rpi:~/modules $ ./player_test music/05-欢乐斗地主 # 🎵 15 | ``` 16 | 17 | 18 | 19 | 再来看看我是怎么实现的: 20 | 21 | 1. 实现PWM蜂鸣器驱动`musicbox`:通过`write`音符给设备节点,播放不同的声音;通过`ioctl`控制节拍 22 | 2. 实现应用层`player_test`,负责读取歌曲的乐谱 23 | 3. 编写乐谱,其实就是文本简谱,类似下面这首 24 | 25 | ``` 26 | C 3/4 27 | 28 | # ~前奏~ 29 | (5` (4`) 3` 2` 1` 7 1` 0 3 (2) 3 5 (6 5 6 1`) 5 0) 30 | (6` (5`) 4` 3` 2` 1` 7 6 5 6 1` 2` 5 6 2` 3` 5 6 3` 4` 5 6 4` 5`) 31 | 32 | # 风在吼,马在叫,黄河在咆哮,黄河在咆哮 33 | 1` (1` 3) 5- 1` (1` 3) 5- (3) 3 (5) 1` 1` (6) 6 (4) 2` 2` 34 | 35 | # 河西山岗万丈高,河东河北高粱熟了 36 | (5 (6) 5 4) (3 2 3 0) (5 (6) 5 4) (3 2 3 1) 37 | 38 | # 万山丛中,抗日英雄真不少 39 | 5 (6) 1` 3 (5 (3`) 2` 1`) 5 6 3- 40 | 41 | # 青纱帐里,游击健儿真不少 42 | 5 (6) 1` 3 (5 (3`) 2` 1`) 5 6 1`- 43 | 44 | # 端起了土枪洋枪,挥动着大刀长矛 45 | (5 (3 5) 6 5 1` 1`) 0 (5 (3 5) 6 5 2` 2`) 0 46 | 47 | # 保卫家乡,保卫黄河,保卫华北,保卫全中国 48 | (5 (6) 1` 1`) 0 (5 (6) 2` 2`) 0 (5 (6) 3` 3`) (5 (6) 3` 2` 1`----) 49 | ``` 50 | 51 | 好,具体实现且听我慢慢道来~ 52 | 53 | ## PWM蜂鸣器驱动 54 | 55 | 有关蜂鸣器硬件原理、有源、无源这里不展开讨论。总之本文采用的是树莓派上的PWM0+一个无源蜂鸣器。接线如下图所示: 56 | 57 | ![PWM蜂鸣器树莓派接线图](https://i.loli.net/2019/09/22/K1znqZhc6Bx4G3u.png) 58 | ![PWM蜂鸣器原理图](https://i.loli.net/2019/09/22/VqfoJl5IXT8uMwm.png) 59 | 60 | 根据上一篇《PWM呼吸灯》的学习,基本知道PWM对脉冲的控制主要有`占空比`和`脉冲周期`两部分。用来控制LED的时,占空比可以调节灯光的强弱,在脉冲周期似乎没什么乱用。 61 | 62 | 对于蜂鸣器声用作声乐,有三个基本要素:音调、节拍、音量大小。 63 | - 音调:由震动频率决定,对应PWM的脉冲周期 64 | - 音量:同样的频率,PWM占空比越高,声音越大 65 | - 节拍,声音的持续时长,和PWM毛关系都没有,做个定时开关即可 66 | 67 | 综上,其实在蜂鸣器驱动`musicbox`里重点实现两个接口: 68 | - `write`: 解析用户层写入的字符串,例如音调do的高中低音分别为'`` 1` ``'和'`` 1 ``'和'`` 1. ``',然后换算出对应的`频率`即可。 69 | - `ioctl`: 解析用户层发来的指令,有节拍、音调、音量等控制。 70 | 71 | ### 不同音调的蜂鸣器频率 72 | 73 | 注意:此部分涉及的乐理知识我不是很懂,基本是从网上抄来的,但我发现F和B调的发音不是很准,估计频率不对。 74 | 75 | 下表分别是`Do Re Mi Fa So La Ti`对应的蜂鸣器震动频率。 76 | 77 | wait-!7个音符,怎么会干出13种频率呢? 78 | 79 | 因为其中涵盖了A-G不同曲调,一首曲子可以由多个调子来演奏,比如我们经常听到的C小调,D大调之类的。其中的乐理只是更为复杂,这里只需要记住: 80 | 81 | 以`C调的Do`为基准,其他调子做相应偏移。例如E调的Do相当于C调的Mi,而A调的Do相当于C调的La。 82 | 83 | | 音域 | 1 | 2 | 3 | 4 | 5 | 6 | C7 | D7 | E7 | F7 | G7 | A7 | B7 | 84 | |-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----| 85 | | 低音 | 131 | 147 | 165 | 175 | 196 | 221 | 248 | 278 | 312 | 330 | 371 | 416 | 467 | 86 | | 中音 | 262 | 294 | 330 | 350 | 393 | 441 | 495 | 556 | 624 | 661 | 742 | 883 | 935 | 87 | | 高音 | 525 | 589 | 661 | 700 | 786 | 882 | 900 | 1112| 1248| 1322| 1484| 1665| 1869| 88 | 89 | ### 如何计算PWM的周期 90 | 91 | 有了不同音符的震动频率,也就得到了PWM的脉冲周期。举个例子,50Hz相当于1秒钟震动50次,那PWM的脉冲周期就应该为1s/50=0.02秒。因此周期的计算公式为: 92 | 93 | period = 1s / freq 94 | 95 | 其中的freq就是音符表中的频率,而1s可以由Linux中的`HZ`变量表示。 96 | 97 | ### 如何计算PWM占空比 98 | 99 | 有了脉冲周期,才能计算占空比。一个周期内高电平所占时间越大,输出声音也就越大。所以我们可以通过百分比来决定占空比大小。 100 | 101 | 假设现在要输出高音`` 3` ``,它对应的频率为661,并根据前面的公式求得脉冲周期为12345,而音量为75%,那占空比应该为12345*75/100 = 9528。因此占空比的计算公式为: 102 | 103 | duty = period * volume / 100 104 | 105 | ### 如何计算节拍 106 | 107 | 所谓节拍,如2/4拍,表示以4分音符为一拍,每小节有两拍。 108 | 109 | 但在程序里,节拍即每个音符输出的时长,这一点我并没有在驱动层实现,但做的做法非常简单。并没有引入“小节”和“动次打次”的概念。就是强制一个小节为4秒,如果是2/4拍,就相当于4000/4/2 = 500毫秒,即每个音符默认响0.5秒。如果存在半拍的情况(就是音符下有画线),那时间再减半。 110 | 111 | 程序中把半拍用圆括号`()`表示,遇到左括号就减半时间,遇到右括号就加倍时间,就是那么粗暴。 112 | 113 | ### 驱动程序实现 114 | 115 | 当加载以下驱动后,可以通过命令行`echo 1 > /dev/musicbox`来测试是否会响。 116 | 117 | ```c 118 | #include 119 | #include 120 | #include 121 | #include 122 | #include 123 | #include 124 | #include 125 | 126 | #define MUSICBOX_MAX_VOLUME 100 // 最大音量100% 127 | #define ONE_SECOND 1000000000 // 以纳秒为单位的一秒 128 | 129 | // 各音域音符对应震动频率 130 | static const int tones[][14] = { 131 | // C D E F G A B 132 | // 1. 2. 3. 4. 5. 6. 7. 133 | {0, 131, 147, 165, 175, 196, 221, 248, 278, 312, 330, 371, 416, 476}, 134 | // 1 2 3 4 5 6 7 135 | {0, 262, 294, 330, 350, 393, 441, 496, 556, 624, 661, 742, 833, 935}, 136 | // 1' 2' 3' 4' 5' 6' 7' 137 | {0, 525, 589, 661, 700, 786, 882, 990, 1112, 1248, 1322, 1484, 1665, 1869}, 138 | }; 139 | 140 | static struct { 141 | bool playing; // 是否正在播放 142 | wait_queue_head_t wwait; // 写等待 143 | struct pwm_device* buzzer; // 蜂鸣器 144 | struct timer_list timer; // 定时器 145 | char volume; // 音量 0-100 146 | char tonality; // 音调 A-G 147 | char beat; // 节拍 148 | char key; // 音调 149 | } musicbox; 150 | 151 | static void music_stop(struct timer_list* timer) { 152 | pwm_disable(musicbox.buzzer); 153 | musicbox.playing = false; 154 | wake_up(&musicbox.wwait); 155 | } 156 | 157 | static ssize_t musicbox_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) { 158 | // 获取音符 159 | int note = (buf[0] - '0') + musicbox.key; 160 | // 获取音域 161 | int pitch = (buf[1] == '`') ? 2 : (buf[1] == '.' ? 0 : 1); 162 | // 根据频率计算脉冲周期 163 | int tone = ONE_SECOND / tones[pitch][note]; 164 | // 根据脉冲周期计算音量 165 | int volume = tone * musicbox.volume / 100; 166 | // 当音符后跟着'-'就延长一倍时间 167 | int delay = HZ / musicbox.beat * (len - (pitch != 1)); 168 | 169 | // 写阻塞,一次只能播放一个音符 170 | if (musicbox.playing) { 171 | if (filp->f_flags & O_NONBLOCK) { 172 | return -EAGAIN; 173 | } else { 174 | DECLARE_WAITQUEUE(wq, current); 175 | add_wait_queue(&musicbox.wwait, &wq); 176 | wait_event(musicbox.wwait, !musicbox.playing); 177 | remove_wait_queue(&musicbox.wwait, &wq); 178 | } 179 | } 180 | 181 | pwm_config(musicbox.buzzer, volume, tone); 182 | if (buf[0] > '0') { 183 | pwm_enable(musicbox.buzzer); 184 | } 185 | mod_timer(&musicbox.timer, jiffies + delay); 186 | musicbox.playing = true; 187 | 188 | return len; 189 | } 190 | 191 | // 音量、音调、节拍控制 192 | static long musicbox_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { 193 | switch (cmd) { 194 | case MUSICBOX_SET_VOLUMN: 195 | if (arg >= 0 && arg <= MUSICBOX_MAX_VOLUME) { 196 | musicbox.volume = arg; 197 | } else { 198 | return -EINVAL; 199 | } 200 | break; 201 | case MUSICBOX_GET_VOLUMN: 202 | return musicbox.volume; 203 | case MUSICBOX_SET_BEAT: 204 | if (arg > 0 && arg <= 1000) { 205 | musicbox.beat = 1000 / arg; 206 | } else { 207 | return -EINVAL; 208 | } 209 | break; 210 | case MUSICBOX_GET_BEAT: 211 | return 1000 / musicbox.beat; 212 | case MUSICBOX_SET_KEY: 213 | if (arg < 'A' || arg > 'G') { 214 | return -EINVAL; 215 | } 216 | musicbox.key = arg >= 'C' ? (arg - 'C') : (arg - 'A' + 5); 217 | break; 218 | case MUSICBOX_GET_KEY: 219 | return musicbox.key; 220 | default: 221 | printk("error cmd = %d\n", cmd); 222 | return -EFAULT; 223 | } 224 | return 0; 225 | } 226 | 227 | // 以下是设备驱动注册/注销相关 228 | static const struct file_operations fops = { 229 | .owner = THIS_MODULE, 230 | .write = musicbox_write, 231 | .unlocked_ioctl = musicbox_ioctl, 232 | }; 233 | 234 | static struct miscdevice mdev = { 235 | .minor = MISC_DYNAMIC_MINOR, 236 | .name = "musicbox", 237 | .fops = &fops, 238 | .nodename = "musicbox", 239 | .mode = S_IRWXUGO, 240 | }; 241 | 242 | int __init musicbox_init(void) { 243 | musicbox.buzzer = pwm_request(0, "Buzzer"); 244 | if (IS_ERR_OR_NULL(musicbox.buzzer)) { 245 | printk(KERN_ERR "failed to request pwm0\n"); 246 | return PTR_ERR(musicbox.buzzer); 247 | } 248 | 249 | musicbox.volume = 50; 250 | musicbox.tonality = 'A'; 251 | musicbox.key = 0; 252 | musicbox.beat = 4; 253 | 254 | init_waitqueue_head(&musicbox.wwait); 255 | timer_setup(&musicbox.timer, music_stop, 0); 256 | add_timer(&musicbox.timer); 257 | 258 | misc_register(&mdev); 259 | 260 | return 0; 261 | } 262 | module_init(musicbox_init); 263 | 264 | void __exit musicbox_exit(void) { 265 | misc_deregister(&mdev); 266 | del_timer(&musicbox.timer); 267 | pwm_disable(musicbox.buzzer); 268 | pwm_free(musicbox.buzzer); 269 | } 270 | module_exit(musicbox_exit); 271 | ``` 272 | 273 | ## 应用层加载乐谱 274 | 275 | 应用层`player_test`程序的业务逻辑就简单得多了: 276 | 1. 加载指定的乐谱文件 277 | 2. 配置音乐盒的节拍、音调 278 | 3. 按行读取文件内容(跳过注释行和空行) 279 | 4. 提取每一行的音符、括号 280 | 5. 将音符、节拍写入驱动 281 | 6. 重复第3-5步,直至文件末尾 282 | 283 | ```c 284 | #include 285 | #include 286 | #include 287 | #include 288 | #include 289 | #include 290 | 291 | #include "musicbox.h" 292 | 293 | #define MUSIC_BOX_FILE "/dev/musicbox" 294 | 295 | int main(int argc, char* argv[]) { 296 | if (argc < 2) { 297 | printf("Usage: ./player "); 298 | return -1; 299 | } 300 | 301 | int fd = open(MUSIC_BOX_FILE, O_RDWR); 302 | if (fd < 0) { 303 | perror("open musicbox"); 304 | exit(0); 305 | } 306 | 307 | FILE* music = fopen(argv[1], "r"); 308 | if (music == NULL) { 309 | perror("open music"); 310 | exit(0); 311 | } 312 | 313 | // 初始化音乐盒 314 | char line[128] = {'\0'}; 315 | if (fgets(line, sizeof(line), music) == NULL) { 316 | perror("read music"); 317 | exit(0); 318 | } 319 | 320 | // 4秒为一节,计算每拍的长度,如2/4拍时,每拍长度为500ms 321 | int beat = 4000 / (line[4]-'0') / (line[2]-'0'); 322 | if (ioctl(fd, MUSICBOX_SET_BEAT, beat) < 0 323 | || ioctl(fd, MUSICBOX_SET_VOLUMN, 90) < 0 324 | || ioctl(fd, MUSICBOX_SET_KEY, line[0]) < 0) { 325 | perror("ioctl"); 326 | exit(0); 327 | } 328 | 329 | // 按行加载乐谱文件 330 | while (fgets(line, sizeof(line), music)) { 331 | printf("%s", line); 332 | if (line[0] == '#' || line[0] == '\0') { 333 | continue; 334 | } 335 | 336 | char* p = line; 337 | while (*p) { 338 | if (*p == '(') { 339 | ioctl(fd, MUSICBOX_SET_BEAT, ioctl(fd, MUSICBOX_GET_BEAT) / 2); 340 | } else if (*p == ')') { 341 | ioctl(fd, MUSICBOX_SET_BEAT, ioctl(fd, MUSICBOX_GET_BEAT) * 2); 342 | } else if (*p >= '0' && *p <= '7') { 343 | char* q = p+1; 344 | while (*q == '`') q++; 345 | while (*q == '.') q++; 346 | while (*q == '-') q++; 347 | write(fd, p, q-p); 348 | } 349 | p++; 350 | } 351 | } 352 | 353 | close(fd); 354 | fclose(music); 355 | return 0; 356 | } 357 | ``` 358 | 359 | ## 小结 360 | 361 | 由于本章没有新的知识点,就不做知识总结了,说说感受。 362 | 363 | 当蜂鸣器按照我的预期演奏音乐是还是挺开心的,仿佛一下子把我拉回了大学的那个暑假,一个人默默在宿舍鼓弄51单片机的日子。时光荏苒,尽管做的是同一件事,但我现在的软件架构、编程基础不可同日而语。或许我重拾底层技术的同时,也重拾了当年学习的热情吧😊。 -------------------------------------------------------------------------------- /05-pwm_musicbox/music/01-保卫黄河: -------------------------------------------------------------------------------- 1 | C 3/4 2 | 3 | # ~前奏~ 4 | (5` (4`) 3` 2` 1` 7 1` 0 3 (2) 3 5 (6 5 6 1`) 5 0) 5 | (6` (5`) 4` 3` 2` 1` 7 6 5 6 1` 2` 5 6 2` 3` 5 6 3` 4` 5 6 4` 5`) 6 | 7 | # 风在吼,马在叫,黄河在咆哮,黄河在咆哮 8 | 1` (1` 3) 5- 1` (1` 3) 5- (3) 3 (5) 1` 1` (6) 6 (4) 2` 2` 9 | 10 | # 河西山岗万丈高,河东河北高粱熟了 11 | (5 (6) 5 4) (3 2 3 0) (5 (6) 5 4) (3 2 3 1) 12 | 13 | # 万山丛中,抗日英雄真不少 14 | 5 (6) 1` 3 (5 (3`) 2` 1`) 5 6 3- 15 | 16 | # 青纱帐里,游击健儿真不少 17 | 5 (6) 1` 3 (5 (3`) 2` 1`) 5 6 1`- 18 | 19 | # 端起了土枪洋枪,挥动着大刀长矛 20 | (5 (3 5) 6 5 1` 1`) 0 (5 (3 5) 6 5 2` 2`) 0 21 | 22 | # 保卫家乡,保卫黄河,保卫华北,保卫全中国 23 | (5 (6) 1` 1`) 0 (5 (6) 2` 2`) 0 (5 (6) 3` 3`) (5 (6) 3` 2` 1`----) 24 | -------------------------------------------------------------------------------- /05-pwm_musicbox/music/02-我和我的祖国: -------------------------------------------------------------------------------- 1 | A 1/4 2 | 3 | # 我和我的祖国,一刻也不能分割! 4 | (5 6 5 4 3 2) 1 5. (1 3 1` 7 6 (3)) 5- 5 | 6 | # 无论我走到哪里,都留出一首赞歌。 7 | (6 7 6 5 4 3) 2 6. (7. 6. 5. 5. 1 (2)) 3- 8 | -------------------------------------------------------------------------------- /05-pwm_musicbox/music/03-FC马戏团: -------------------------------------------------------------------------------- 1 | C 4/4 2 | 3 | (5. 6. 7.) 1 1 (1 7. 1 2) 3 3 (3 2 3 4) 5 5 (5 4 5 1') 5- 4 | 3 4 (4 3) 2 4 3 (3 2) 1 3 2 6. 7. 1 2 5 | (5. 6. 7.) 1 1 (1 7. 1 2) 3 3 (3 2 3 4) 5 5 (5 4 5 1') 5- 6 | 1. 6 5 4 3 2 1 7. 1 2 (3 4) 3 2 1 7 | (5. 6. 7. 1 2 3) 4 4 4 4 (4 3)) 4 (4 5 5 (5 4 3 4)) 5 8 | (5 6 6 1' (6) 5 5 (5 4) 4 3 4 4 7. 4 3 3 (3 2)) 9 | (1 6` 6 1` (6) 5 5 (5 4) 3 4 4 7. 2 1 1 1) 10 | -------------------------------------------------------------------------------- /05-pwm_musicbox/music/04-灌篮高手主题曲: -------------------------------------------------------------------------------- 1 | C 3/4 2 | 3 | 2` (2`- 3` 0) (2` 2`) 2` (3` 0) (1` 2` 3` 2` 1` 7 6 6 5) 5 0 4 | (5 6 1` 1` 1` 1` 7 1` 1` 2` 3` 3` 2`) 2` 0 5 | (1` 1` 7 1` 2` 3`) 2` 3` 6 | (6. 5. 2. 6. 5. 2. 6. 5.) 2.- 7 | 8 | (1` 7) 1` 7 (1`) 5 (3 4 5 0) 9 | (1` 7) 1` 7 (1`) 1` (3 3 4 0) 10 | (6 5 5 6 5) 5- (3 3 5 5) 6 (4 4 0) 6 (5 5 0) 11 | (1` 7) 1` 7 (1`) 5 (3 4 5 0) 12 | (1` 7) 1` 7 (1`) 1` (3 3 4 0) 13 | (6 5 5 6 5) 5 1` (7 1`) 2` (1`) 7 (1` 1` 0) 14 | (1` 2` 3`) 1` (6) 1` (1` 7 1` 0) 15 | (1` 2` 3`) 1` (6) 1` (1` 7 1` 0) 16 | (1` 2` 3`) 1` (0) (1` 2` 3`) 1` (0) 1` 1` (2` 3`) 3`- 0 17 | 18 | (5 6 1` 2`) 2` (3` 0) (2` 2`) 2` (3` 0) 19 | (1` 2` 3` 2` 2` 1` 7 6 6 5 5 0) 20 | (5 6 1` 1`) 1` 7 1` (3`) 3` 2` 0 21 | (1` 1` 7) 1` (2` 3` 2`) 3` 0 22 | (5 5 6 1` 2`) 2` (3` 0) (2` 2`) 2` (3` 0) 23 | (1` 2` 3` 2` 2` 1` 7) 5 3`- 0 24 | (3` 3`) 3` (2` 2` 0) (2` 2` 3` 1` 1` 0) 25 | (6 6 7 1`) 7 (6) 7 1`--- 26 | -------------------------------------------------------------------------------- /05-pwm_musicbox/music/05-欢乐斗地主: -------------------------------------------------------------------------------- 1 | A 2/4 2 | 3 | (5 (5 6) 5 3) 5- 4 | (1 (1 2) 1 6.) 5.- 5 | (6. (6. 5.) 6. 1 2 1 2 3 5 (5 6) 5 3) 2- 6 | 7 | (5 (5 6) 5 3) 5- 8 | (2 (2 3) 2 1) 6.- 9 | (5. 6. 1 2 3 5 2 3 2 (2 3) 2 6.) 1- 10 | 11 | (6 (6 5) 3 5) 6- 12 | (5 (5 6) 5 3) 5- 13 | (2 (2 3) 2 1 2 1 6. 5.) 2 (1) 3- 14 | 15 | (6 (6 5) 3 5) 6- 16 | (5 (5 6) 5 3) 5- 17 | (2 (2 3) 2 1 2 1 6. 5.) 2 (1) 1- 18 | 19 | 0 3 (3 2) 1 (1 6.) (2 3 2 3) 5.- 20 | 6. (6. 5.) 6. 1 (5 6 3 5) 2- 21 | 3 (3 2) 3 5 (6 6 6 1`) 6 (5 3) 2 (2 3) 5 5. (2 3 2 3) 1- 22 | (2 3 2 3) 5 (5 6) 1` 5 1`- 23 | -------------------------------------------------------------------------------- /05-pwm_musicbox/musicbox.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "musicbox.h" 10 | 11 | MODULE_LICENSE("Dual BSD/GPL"); 12 | MODULE_AUTHOR("Philon | https://ixx.life"); 13 | 14 | #define MUSICBOX_MAX_VOLUME 100 // 最大音量100% 15 | #define ONE_SECOND 1000000000 // 以纳秒为单位的一秒 16 | 17 | static const int tones[][14] = { 18 | // C D E F G A B 19 | // 1. 2. 3. 4. 5. 6. 7. 20 | {0, 131, 147, 165, 175, 196, 221, 248, 278, 312, 330, 371, 416, 476}, 21 | // 1 2 3 4 5 6 7 22 | {0, 262, 294, 330, 350, 393, 441, 496, 556, 624, 661, 742, 833, 935}, 23 | // 1' 2' 3' 4' 5' 6' 7' 24 | {0, 525, 589, 661, 700, 786, 882, 990, 1112, 1248, 1322, 1484, 1665, 1869}, 25 | }; 26 | 27 | static struct { 28 | bool playing; // 是否正在播放 29 | wait_queue_head_t wwait; // 写等待 30 | struct pwm_device* buzzer; // 蜂鸣器 31 | struct timer_list timer; // 定时器 32 | char volume; // 音量 0-100 33 | char tonality; // 音调 A-G 34 | char beat; // 节拍 35 | char key; // 音调 36 | } musicbox; 37 | 38 | static void music_stop(struct timer_list* timer) { 39 | pwm_disable(musicbox.buzzer); 40 | musicbox.playing = false; 41 | wake_up(&musicbox.wwait); 42 | } 43 | 44 | static ssize_t musicbox_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) { 45 | int note = (buf[0] - '0') + musicbox.key; 46 | int pitch = (buf[1] == '`') ? 2 : (buf[1] == '.' ? 0 : 1); 47 | int tone = ONE_SECOND / tones[pitch][note]; 48 | int volume = tone * musicbox.volume / 100; 49 | int delay = HZ / musicbox.beat * (len - (pitch != 1)); // 但音符后跟着'-'就延长一倍,如2-- 50 | 51 | if (musicbox.playing) { 52 | if (filp->f_flags & O_NONBLOCK) { 53 | return -EAGAIN; 54 | } else { 55 | DECLARE_WAITQUEUE(wq, current); 56 | add_wait_queue(&musicbox.wwait, &wq); 57 | wait_event(musicbox.wwait, !musicbox.playing); 58 | remove_wait_queue(&musicbox.wwait, &wq); 59 | } 60 | } 61 | 62 | pwm_config(musicbox.buzzer, volume, tone); 63 | if (buf[0] > '0') { 64 | pwm_enable(musicbox.buzzer); 65 | } 66 | mod_timer(&musicbox.timer, jiffies + delay); 67 | musicbox.playing = true; 68 | 69 | return len; 70 | } 71 | 72 | static long musicbox_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { 73 | switch (cmd) { 74 | case MUSICBOX_SET_VOLUMN: 75 | if (arg >= 0 && arg <= MUSICBOX_MAX_VOLUME) { 76 | musicbox.volume = arg; 77 | } else { 78 | return -EINVAL; 79 | } 80 | break; 81 | case MUSICBOX_GET_VOLUMN: 82 | return musicbox.volume; 83 | case MUSICBOX_SET_BEAT: 84 | if (arg > 0 && arg <= 1000) { 85 | musicbox.beat = 1000 / arg; 86 | } else { 87 | return -EINVAL; 88 | } 89 | break; 90 | case MUSICBOX_GET_BEAT: 91 | return 1000 / musicbox.beat; 92 | case MUSICBOX_SET_KEY: 93 | if (arg < 'A' || arg > 'G') { 94 | return -EINVAL; 95 | } 96 | musicbox.key = arg >= 'C' ? (arg - 'C') : (arg - 'A' + 5); 97 | break; 98 | case MUSICBOX_GET_KEY: 99 | return musicbox.key; 100 | default: 101 | printk("error cmd = %d\n", cmd); 102 | return -EFAULT; 103 | } 104 | return 0; 105 | } 106 | 107 | static const struct file_operations fops = { 108 | .owner = THIS_MODULE, 109 | .write = musicbox_write, 110 | .unlocked_ioctl = musicbox_ioctl, 111 | }; 112 | 113 | static struct miscdevice mdev = { 114 | .minor = MISC_DYNAMIC_MINOR, 115 | .name = "musicbox", 116 | .fops = &fops, 117 | .nodename = "musicbox", 118 | .mode = S_IRWXUGO, 119 | }; 120 | 121 | int __init musicbox_init(void) { 122 | musicbox.buzzer = pwm_request(0, "Buzzer"); 123 | if (IS_ERR_OR_NULL(musicbox.buzzer)) { 124 | printk(KERN_ERR "failed to request pwm0\n"); 125 | return PTR_ERR(musicbox.buzzer); 126 | } 127 | 128 | musicbox.volume = 50; 129 | musicbox.tonality = 'A'; 130 | musicbox.key = 0; 131 | musicbox.beat = 4; 132 | 133 | init_waitqueue_head(&musicbox.wwait); 134 | timer_setup(&musicbox.timer, music_stop, 0); 135 | add_timer(&musicbox.timer); 136 | 137 | misc_register(&mdev); 138 | 139 | return 0; 140 | } 141 | module_init(musicbox_init); 142 | 143 | void __exit musicbox_exit(void) { 144 | misc_deregister(&mdev); 145 | del_timer(&musicbox.timer); 146 | pwm_disable(musicbox.buzzer); 147 | pwm_free(musicbox.buzzer); 148 | } 149 | module_exit(musicbox_exit); -------------------------------------------------------------------------------- /05-pwm_musicbox/musicbox.h: -------------------------------------------------------------------------------- 1 | #ifndef RPI_DRIVER_MUSICBOX_H 2 | #define RPI_DRIVER_MUSICBOX_H 3 | 4 | typedef enum { 5 | MUSICBOX_SET_VOLUMN = 0x12, // 音量,取值0-100 6 | MUSICBOX_GET_VOLUMN, 7 | MUSICBOX_SET_BEAT, // 节拍,取值1-1000毫秒 8 | MUSICBOX_GET_BEAT, 9 | MUSICBOX_SET_KEY, // 音调,取值'A'-'G' 10 | MUSICBOX_GET_KEY, 11 | } musicbox_cmd_t; 12 | 13 | #endif // !RPI_DRIVER_MUSICBOX_H -------------------------------------------------------------------------------- /05-pwm_musicbox/player_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "musicbox.h" 9 | 10 | #define MUSIC_BOX_FILE "/dev/musicbox" 11 | 12 | int main(int argc, char* argv[]) { 13 | if (argc < 2) { 14 | printf("Usage: ./player "); 15 | return -1; 16 | } 17 | 18 | int fd = open(MUSIC_BOX_FILE, O_RDWR); 19 | if (fd < 0) { 20 | perror("open musicbox"); 21 | exit(0); 22 | } 23 | 24 | FILE* music = fopen(argv[1], "r"); 25 | if (music == NULL) { 26 | perror("open music"); 27 | exit(0); 28 | } 29 | 30 | char line[128] = {'\0'}; 31 | if (fgets(line, sizeof(line), music) == NULL) { 32 | perror("read music"); 33 | exit(0); 34 | } 35 | 36 | // 4秒为一节,计算每拍的长度,如2/4拍时,每拍长度为500ms 37 | int beat = 4000 / (line[4]-'0') / (line[2]-'0'); 38 | if (ioctl(fd, MUSICBOX_SET_BEAT, beat) < 0 39 | || ioctl(fd, MUSICBOX_SET_VOLUMN, 90) < 0 40 | || ioctl(fd, MUSICBOX_SET_KEY, line[0]) < 0) { 41 | perror("ioctl"); 42 | exit(0); 43 | } 44 | 45 | while (fgets(line, sizeof(line), music)) { 46 | printf("%s", line); 47 | if (line[0] == '#' || line[0] == '\0') { 48 | continue; 49 | } 50 | 51 | char* p = line; 52 | while (*p) { 53 | if (*p == '(') { 54 | ioctl(fd, MUSICBOX_SET_BEAT, ioctl(fd, MUSICBOX_GET_BEAT) / 2); 55 | } else if (*p == ')') { 56 | ioctl(fd, MUSICBOX_SET_BEAT, ioctl(fd, MUSICBOX_GET_BEAT) * 2); 57 | } else if (*p >= '0' && *p <= '7') { 58 | char* q = p+1; 59 | while (*q == '`') q++; 60 | while (*q == '.') q++; 61 | while (*q == '-') q++; 62 | write(fd, p, q-p); 63 | } 64 | p++; 65 | } 66 | } 67 | 68 | close(fd); 69 | fclose(music); 70 | return 0; 71 | } -------------------------------------------------------------------------------- /05-pwm_musicbox/pwm_musicbox.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Philon/rpi-drivers/d1c55d06ef4bc42ff8dee4c4a9962266a36e752c/05-pwm_musicbox/pwm_musicbox.fzz -------------------------------------------------------------------------------- /06-infrared/Makefile: -------------------------------------------------------------------------------- 1 | # 模块驱动,必须以obj-m=xxx形式编写 2 | obj-m = infrared.o 3 | 4 | KDIR = $(shell pwd)/../../linux-rpi-4.19.y 5 | 6 | TEST_SRC := $(wildcard *_test.c) 7 | TEST_OBJ := $(TEST_SRC:%.c=%.o) 8 | TEST_OUT := $(TEST_OBJ:%.o=%) 9 | 10 | CLFAGS += -std=gnu99 11 | LFLAGS += -lpthread 12 | 13 | export ARCH = arm 14 | export CROSS_COMPILE = /opt/arm-mac-linux-gnueabihf/bin/arm-mac-linux-gnueabihf- 15 | 16 | all: module test install 17 | 18 | install: module 19 | scp $(obj-m:%.o=%.ko) rpi.local:~/modules 20 | scp $(TEST_OUT) rpi.local:~/modules 21 | 22 | module: 23 | $(MAKE) -C $(KDIR) M=$(PWD) modules 24 | 25 | test: $(TEST_OUT) 26 | $(TEST_OUT): %:%.o 27 | $(CROSS_COMPILE)gcc $(LFLAGS) -o $@ $< 28 | $(TEST_OBJ): %.o:%.c 29 | $(CROSS_COMPILE)gcc $(CLFAGS) -c -o $@ $< 30 | 31 | clean: 32 | $(MAKE) -C $(KDIR) M=`pwd` clean 33 | rm -f $(TEST_OBJ) $(TEST_OUT) -------------------------------------------------------------------------------- /06-infrared/README.md: -------------------------------------------------------------------------------- 1 | # 树莓派驱动开发实战06:红外接收 2 | 3 | 由于我手上只有一个1838红外接收头和一个CAR-MP3遥控器,所以本文主要基于Linux内核实现红外NEC协议的解码。 4 | 5 | 先来看看效果: 6 | 7 | 8 | 9 | ## 红外通信原理 10 | 11 | 呐,太专业的电路原理呢我就不展开讲了,反正也没人看。简单点说吧: 12 | 13 | 反正就是有一对红外发射管和接收管组成,通过产生脉冲信号来传递信息。脉冲信号是什么?你可以理解为摩尔斯电码那种样子,就是1和0。 14 | 15 | 红外通信在日常生活中主要应用于家电控制,例如电视、空调、投影等等。市面上比较常见的红外通信协议是NEC,所以就来研究以下NEC的解码。 16 | 17 | 18 | ## NEC协议 19 | 20 | 在讲述NEC协议之前,先来看看下面这几行数据打印。这是我随便按了几下遥控器,抓取的红外原始数据。“横杠”表示有红外信号,“下划线”表示无信号。 21 | 22 | ```sh 23 | philon@rpi:~/modules $ dmesg 24 | # 9ms 4.5ms 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 1 0 0 0 0 1 1 1 0 1 1 1 1 1 25 | [ 203.718032] -----------------_________-_-_-_-_-_-_-_-_-___-___-___-___-___-___-___-___-_-_-_-___-_-_-_-_-___-___-___-_-___-___-___-___-_ 26 | [ 207.647870] -----------------_________-_-_-_-_-_-_-_-_-___-___-___-___-___-___-___-___-_-_-___-___-___-_-_-_-___-___-_-_-_-___-___-___-_ 27 | [ 209.927802] -----------------_________-_-_-_-_-_-_-_-_-___-___-___-___-___-___-___-___-_-___-_-___-___-_-___-_-___-_-___-_-_-___-_-___-_ 28 | [ 214.557679] -----------------_________-_-_-_-_-_-_-_-_-___-___-___-___-___-___-___-___-_-___-_-_-_-_-___-_-___-_-___-___-___-___-_-___-_ 29 | [ 216.917629] -----------------_________-_-_-_-_-_-_-_-_-___-___-___-___-___-___-___-___-_-___-_-_-___-_-___-_-___-_-___-___-_-___-_-___-_ 30 | [ 219.457571] -----------------_________-_-_-_-_-_-_-_-_-___-___-___-___-___-___-___-___-_-___-_-___-_-_-___-_-___-_-___-_-___-___-_-___-_ 31 | ``` 32 | 33 | 从上边的原始数据可以看出来,每个NEC红外协议很相似,以9毫秒的高电平、4.5毫秒的低电平开始,之后跟上一堆1和0,最后一部分才是不相同的地方。简直可以总结出NEC的协议格式是这样的: 34 | 35 | `<帧头9ms高+4.5低><8位地址码><8位地址码取反><8位指令码><8位指令码取反>` 36 | 37 | 没错,就是这样的😁,不过需要注意,**NEC协议采用PWM(脉宽调制)编码,一个脉冲周期表示一个bit,是0还是1取决于占空比**。不信请看下图: 38 | 39 | ![NEC协议编码说明](https://i.loli.net/2019/10/08/BQPUXCGspiTumE6.jpg) 40 | 41 | ⚠️我在程序中对接收到的数据取反,所以原始数据和上图的逻辑刚好相反。 42 | 43 | 结合原始数据和图片可以总结出: 44 | 45 | 1. 协议帧头总是以9ms的高电平和4.5ms的低电平为一个脉冲周期 46 | 2. 协议内容的脉冲周期,‘`-___`’表示1,‘`-_`’表示0,且电平信号以560us为单位; 47 | 3. 9ms高电平和2.25ms的低电平表示重复码,即长按按键时触发 48 | 4. 帧间间隔为110ms 49 | 50 | ## 红外接收电路 51 | 52 | ![红外接收管树莓派接线图](https://i.loli.net/2019/10/08/aLufX8MJltyDCpF.png) 53 | 54 | 如上图所示,红外接收管从左到右一共3个脚,分别是:地、3.3V、数据输出。所以供电就用树莓派自身的3.3V即可,而数据输出脚,我这里接的是GPIO18。 55 | 56 | ## 驱动实现 57 | 58 | 正如前文所述,NEC红外协议是高频脉冲信号,所以我用GPIO的中断来记录每一次脉冲信号及其时长。实现起来没什么太复杂的地方,大致流程为: 59 | 60 | 1. 申请并注册GPIO18的中断,务必是双边沿触发 61 | 2. 申请一个定时器用于超时断帧处理 62 | 3. 每次中断触发,都记录上升或者下降沿的状态及时长 63 | 4. 每当经过一个完整脉冲后,通过占空比判断数据类型 64 | 5. 每当记录了32个数据(一帧)后,处理协议指令 65 | 6. 我是直接把地址和指令推给用户层处理 66 | 67 | ⚠️注意:以下代码有个很大的风险,为了简化程序,IRQ中断我并没有采取“底半部”来处理复杂的红外解码业务,如果业务逻辑进一步加大,可能会导致内核崩溃。 68 | 69 | ```c 70 | #include 71 | #include 72 | #include 73 | #include 74 | #include 75 | #include 76 | #include 77 | #include 78 | 79 | MODULE_LICENSE("Dual BSD/GPL"); 80 | MODULE_AUTHOR("Philon | https://ixx.life"); 81 | 82 | static struct { 83 | int gpio; 84 | int irq; 85 | wait_queue_head_t rwait; 86 | struct timer_list timer; 87 | u32 pulse; // 脉冲上升沿持续时长 88 | u32 space; // 脉冲下降沿持续时长 89 | size_t count; // 脉冲个数 90 | u32 data; // 脉冲解码后的值 91 | } ir; 92 | 93 | #define is_head(p, s) (p > 8900 && p < 9100 && s > 4400 && s < 4600) 94 | #define is_repeat(p, s) (p > 8900 && p < 9100 && s > 2150 && s < 2350) 95 | #define is_bfalse(p, s) (p > 500 && p < 650 && s > 500 && s < 650) 96 | #define is_btrue(p, s) (p > 500 && p < 650 && s > 1500 && s < 1750) 97 | 98 | // 红外接收函数(即GPIO18的双边沿中断处理函数) 99 | // 记录GPIO每次中断是“上升还是下降”,以及持续的时长 100 | static irqreturn_t ir_rx(int irq, void* dev) { 101 | static ktime_t last = 0; 102 | u32 duration = (u32)ktime_to_us(ktime_get() - last); 103 | 104 | // ⚠️注意:1838红外头高低电平逻辑取反 105 | if (!gpio_get_value(ir.gpio)) { 106 | ir.space = duration; 107 | } else { 108 | // 切换下降沿时,脉冲只有高电平部分,所以不做处理 109 | ir.pulse = duration; 110 | goto irq_out; 111 | } 112 | 113 | if (is_head(ir.pulse, ir.space)) { 114 | ir.count = ir.data = 0; 115 | } else if (is_repeat(ir.pulse, ir.space)) { 116 | ir.count = 32; 117 | } else if (is_btrue(ir.pulse, ir.space)) { 118 | ir.data |= 1 << ir.count++; 119 | } else if (is_bfalse(ir.pulse, ir.space)) { 120 | ir.data |= 0 << ir.count++; 121 | } else { 122 | goto irq_out; 123 | } 124 | 125 | if (ir.count >= 32) { 126 | wake_up(&ir.rwait); 127 | } 128 | 129 | irq_out: 130 | mod_timer(&ir.timer, jiffies + (HZ / 10)); 131 | last = ktime_get(); 132 | return IRQ_HANDLED; 133 | } 134 | 135 | // 定时清除红外协议帧的相关信息,便于接收下一帧 136 | static void clear_flag(struct timer_list *timer) { 137 | ir.pulse = 0; 138 | ir.space = 0; 139 | ir.count = 0; 140 | ir.data = 0; 141 | } 142 | 143 | static ssize_t ir_read(struct file *filp, char __user *buf, size_t len, loff_t *off) { 144 | int rc = 0; 145 | 146 | if ((filp->f_flags & O_NONBLOCK) && ir.count < 32) { 147 | return -EAGAIN; 148 | } else { 149 | DECLARE_WAITQUEUE(wq, current); 150 | add_wait_queue(&ir.rwait, &wq); 151 | wait_event(ir.rwait, ir.count == 32); 152 | remove_wait_queue(&ir.rwait, &wq); 153 | } 154 | 155 | rc = copy_to_user(buf, &ir.data, sizeof(u32)); 156 | if (rc < 0) { 157 | return rc; 158 | } 159 | 160 | ir.count = 0; 161 | *off += sizeof(u32); 162 | return sizeof(u32); 163 | } 164 | 165 | static const struct file_operations fops = { 166 | .owner = THIS_MODULE, 167 | .read = ir_read, 168 | }; 169 | 170 | static struct miscdevice irdev = { 171 | .minor = MISC_DYNAMIC_MINOR, 172 | .name = "IR1838-NEC", 173 | .fops = &fops, 174 | .nodename = "ir0", 175 | .mode = 0744, 176 | }; 177 | 178 | static int __init ir_init(void) { 179 | int rc = 0; 180 | 181 | // 初始化脉冲处理函数 182 | init_waitqueue_head(&ir.rwait); 183 | 184 | // 初始化定时器,用于断帧 185 | timer_setup(&ir.timer, clear_flag, 0); 186 | add_timer(&ir.timer); 187 | 188 | // 申请GPIO及其双边沿中断 189 | ir.gpio = 18; 190 | if ((rc = gpio_request_one(ir.gpio, GPIOF_IN, "IR")) < 0) { 191 | printk(KERN_ERR "ERROR%d: can not request gpio%d\n", rc, ir.gpio); 192 | return rc; 193 | } 194 | 195 | ir.irq = gpio_to_irq(ir.gpio); 196 | if ((rc = request_irq(ir.irq, ir_rx, 197 | IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, 198 | "IR", NULL)) < 0) { 199 | printk(KERN_ERR "ERROR%d: can not request irq\n", ir.irq); 200 | return rc; 201 | } 202 | 203 | if ((rc = misc_register(&irdev)) < 0) { 204 | return rc; 205 | } 206 | 207 | return 0; 208 | } 209 | module_init(ir_init); 210 | 211 | static void __exit ir_exit(void) { 212 | misc_deregister(&irdev); 213 | free_irq(ir.irq, NULL); 214 | gpio_free(ir.gpio); 215 | del_timer(&ir.timer); 216 | } 217 | module_exit(ir_exit); 218 | ``` 219 | 220 | 以下是应用层的测试代码,有关CAR-MP3遥控器的指令码网上一搜一大把,如果你不嫌烦,也可以一个一个的试出来。 221 | 222 | 由于驱动层是直接把原始数据的<地址><地址取反><指令><指令取反>高低位反转后,直接给到进程,所以进程read出来的数据,指令码应该在第3段(16-24位)。 223 | 224 | ```c 225 | #include 226 | #include 227 | #include 228 | #include 229 | 230 | // car-mp3遥控器指令码 231 | static const char* keyname[] = { 232 | [0x45] = "Channel-", [0x46] = "Channel", [0x47] = "Channel+", 233 | [0x44] = "Speed-", [0x40] = "Speed+", [0x43] = "Play/Pause", 234 | [0x15] = "Vol+", [0x07] = "Vol-", [0x09] = "EQ", 235 | [0x16] = "No.0", [0x19] = "100+", [0x0d] = "200+", 236 | [0x0c] = "No.1", [0x18] = "No.2", [0x5e] = "No.3", 237 | [0x08] = "No.4", [0x1c] = "No.5", [0x5a] = "No.6", 238 | [0x42] = "No.7", [0x52] = "No.8", [0x4a] = "No.9", 239 | }; 240 | 241 | int main(int argc, char* argv[]) { 242 | int ir = open("/dev/ir0", O_RDONLY); 243 | 244 | while (1) { 245 | int frame = 0; 246 | if (read(ir, &frame, sizeof(int)) < 0) { 247 | perror("read ir"); 248 | break; 249 | } 250 | 251 | int cmd = (frame >> 16) & 0xFF; 252 | printf("%s\n", keyname[cmd]); 253 | } 254 | 255 | close(ir); 256 | return 0; 257 | } 258 | ``` 259 | 260 | ## 小结 261 | 262 | - NEC协议采用PWM编码,一个完整的脉冲周期表示一个bit 263 | - 1838红外接收头状态取反 264 | - 别看我写的这么轻松,前几天刚接触红外时简直被搞疯😫 -------------------------------------------------------------------------------- /06-infrared/infrared.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | MODULE_LICENSE("Dual BSD/GPL"); 11 | MODULE_AUTHOR("Philon | https://ixx.life"); 12 | 13 | static struct { 14 | int gpio; 15 | int irq; 16 | wait_queue_head_t rwait; 17 | struct timer_list timer; 18 | u32 pulse; // 脉冲上升沿持续时长 19 | u32 space; // 脉冲下降沿持续时长 20 | size_t count; // 脉冲个数 21 | u32 data; // 脉冲解码后的值 22 | } ir; 23 | 24 | #define is_head(p, s) (p > 8900 && p < 9100 && s > 4400 && s < 4600) 25 | #define is_repeat(p, s) (p > 8900 && p < 9100 && s > 2150 && s < 2350) 26 | #define is_bfalse(p, s) (p > 500 && p < 650 && s > 500 && s < 650) 27 | #define is_btrue(p, s) (p > 500 && p < 650 && s > 1500 && s < 1750) 28 | 29 | // 红外接收函数(即GPIO18的双边沿中断处理函数) 30 | // 记录GPIO每次中断是“上升还是下降”,以及持续的时长 31 | static irqreturn_t ir_rx(int irq, void* dev) { 32 | static ktime_t last = 0; 33 | u32 duration = (u32)ktime_to_us(ktime_get() - last); 34 | 35 | // ⚠️注意:1838红外头高低电平逻辑取反 36 | if (!gpio_get_value(ir.gpio)) { 37 | ir.space = duration; 38 | } else { 39 | // 切换下降沿时,脉冲只有高电平部分,所以不做处理 40 | ir.pulse = duration; 41 | goto irq_out; 42 | } 43 | 44 | if (is_head(ir.pulse, ir.space)) { 45 | ir.count = ir.data = 0; 46 | } else if (is_repeat(ir.pulse, ir.space)) { 47 | ir.count = 32; 48 | } else if (is_btrue(ir.pulse, ir.space)) { 49 | ir.data |= 1 << ir.count++; 50 | } else if (is_bfalse(ir.pulse, ir.space)) { 51 | ir.data |= 0 << ir.count++; 52 | } else { 53 | goto irq_out; 54 | } 55 | 56 | if (ir.count >= 32) { 57 | wake_up(&ir.rwait); 58 | } 59 | 60 | irq_out: 61 | mod_timer(&ir.timer, jiffies + (HZ / 10)); 62 | last = ktime_get(); 63 | return IRQ_HANDLED; 64 | } 65 | 66 | // 定时清除红外协议帧的相关信息,便于接收下一帧 67 | static void clear_flag(struct timer_list *timer) { 68 | ir.pulse = 0; 69 | ir.space = 0; 70 | ir.count = 0; 71 | ir.data = 0; 72 | } 73 | 74 | static ssize_t ir_read(struct file *filp, char __user *buf, size_t len, loff_t *off) { 75 | int rc = 0; 76 | 77 | if ((filp->f_flags & O_NONBLOCK) && ir.count < 32) { 78 | return -EAGAIN; 79 | } else { 80 | DECLARE_WAITQUEUE(wq, current); 81 | add_wait_queue(&ir.rwait, &wq); 82 | wait_event(ir.rwait, ir.count == 32); 83 | remove_wait_queue(&ir.rwait, &wq); 84 | } 85 | 86 | rc = copy_to_user(buf, &ir.data, sizeof(u32)); 87 | if (rc < 0) { 88 | return rc; 89 | } 90 | 91 | ir.count = 0; 92 | *off += sizeof(u32); 93 | return sizeof(u32); 94 | } 95 | 96 | static const struct file_operations fops = { 97 | .owner = THIS_MODULE, 98 | .read = ir_read, 99 | }; 100 | 101 | static struct miscdevice irdev = { 102 | .minor = MISC_DYNAMIC_MINOR, 103 | .name = "IR1838-NEC", 104 | .fops = &fops, 105 | .nodename = "ir0", 106 | .mode = 0744, 107 | }; 108 | 109 | static int __init ir_init(void) { 110 | int rc = 0; 111 | 112 | // 初始化脉冲处理函数 113 | init_waitqueue_head(&ir.rwait); 114 | 115 | // 初始化定时器,用于断帧 116 | timer_setup(&ir.timer, clear_flag, 0); 117 | add_timer(&ir.timer); 118 | 119 | // 申请GPIO及其双边沿中断 120 | ir.gpio = 18; 121 | if ((rc = gpio_request_one(ir.gpio, GPIOF_IN, "IR")) < 0) { 122 | printk(KERN_ERR "ERROR%d: can not request gpio%d\n", rc, ir.gpio); 123 | return rc; 124 | } 125 | 126 | ir.irq = gpio_to_irq(ir.gpio); 127 | if ((rc = request_irq(ir.irq, ir_rx, 128 | IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, 129 | "IR", NULL)) < 0) { 130 | printk(KERN_ERR "ERROR%d: can not request irq\n", ir.irq); 131 | return rc; 132 | } 133 | 134 | if ((rc = misc_register(&irdev)) < 0) { 135 | return rc; 136 | } 137 | 138 | return 0; 139 | } 140 | module_init(ir_init); 141 | 142 | static void __exit ir_exit(void) { 143 | misc_deregister(&irdev); 144 | free_irq(ir.irq, NULL); 145 | gpio_free(ir.gpio); 146 | del_timer(&ir.timer); 147 | } 148 | module_exit(ir_exit); -------------------------------------------------------------------------------- /06-infrared/ir_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | static const char* keyname[] = { 8 | [0x45] = "Channel-", 9 | [0x46] = "Channel", 10 | [0x47] = "Channel+", 11 | [0x44] = "Speed-", 12 | [0x40] = "Speed+", 13 | [0x43] = "Play/Pause", 14 | [0x15] = "Vol+", 15 | [0x07] = "Vol-", 16 | [0x09] = "EQ", 17 | [0x16] = "No.0", 18 | [0x19] = "100+", 19 | [0x0d] = "200+", 20 | [0x0c] = "No.1", 21 | [0x18] = "No.2", 22 | [0x5e] = "No.3", 23 | [0x08] = "No.4", 24 | [0x1c] = "No.5", 25 | [0x5a] = "No.6", 26 | [0x42] = "No.7", 27 | [0x52] = "No.8", 28 | [0x4a] = "No.9", 29 | }; 30 | 31 | 32 | void* irrecv(void* param) { 33 | int ir = open("/dev/ir0", O_RDONLY); 34 | 35 | while (1) { 36 | int frame = 0; 37 | if (read(ir, &frame, sizeof(int)) < 0) { 38 | perror("read ir"); 39 | break; 40 | } 41 | 42 | int cmd = (frame >> 16) & 0xFF; 43 | printf("%s\n", keyname[cmd]); 44 | } 45 | 46 | close(ir); 47 | return NULL; 48 | } 49 | 50 | int main(int argc, char* argv[]) { 51 | pthread_t t = 0; 52 | pthread_create(&t, NULL, irrecv, NULL); 53 | printf("Press any key to quit!\n"); 54 | getchar(); 55 | pthread_detach(t); 56 | return 0; 57 | } -------------------------------------------------------------------------------- /06-infrared/红外接收头连接图.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Philon/rpi-drivers/d1c55d06ef4bc42ff8dee4c4a9962266a36e752c/06-infrared/红外接收头连接图.fzz -------------------------------------------------------------------------------- /07-pdd/Makefile: -------------------------------------------------------------------------------- 1 | obj-m += leddrv.o 2 | obj-m += leddev.o 3 | 4 | KDIR = $(shell pwd)/../../linux-rpi-4.19.y 5 | 6 | TEST_SRC := $(wildcard *_test.c) 7 | TEST_OBJ := $(TEST_SRC:%.c=%.o) 8 | TEST_OUT := $(TEST_OBJ:%.o=%) 9 | 10 | CLFAGS += -std=gnu99 11 | LFLAGS += -lpthread 12 | 13 | export ARCH = arm 14 | export CROSS_COMPILE = /opt/arm-mac-linux-gnueabihf/bin/arm-mac-linux-gnueabihf- 15 | 16 | all: module dtbo test install 17 | 18 | install: module dtbo 19 | scp $(obj-m:%.o=%.ko) myled.dtbo rpi.local:~/modules 20 | ifneq ($(TEST_OUT), ) 21 | scp $(TEST_OUT) rpi.local:~/modules 22 | endif 23 | 24 | module: 25 | $(MAKE) -C $(KDIR) M=$(PWD) modules 26 | 27 | dtbo: 28 | ../../linux-rpi-4.19.y/scripts/dtc/dtc -I dts -o myled.dtbo myled.dts 29 | 30 | test: $(TEST_OUT) 31 | $(TEST_OUT): %:%.o 32 | $(CROSS_COMPILE)gcc $(LFLAGS) -o $@ $< 33 | $(TEST_OBJ): %.o:%.c 34 | $(CROSS_COMPILE)gcc $(CLFAGS) -c -o $@ $< 35 | 36 | clean: 37 | $(MAKE) -C $(KDIR) M=`pwd` clean 38 | rm -f $(TEST_OBJ) $(TEST_OUT) -------------------------------------------------------------------------------- /07-pdd/README.md: -------------------------------------------------------------------------------- 1 | # 树莓派驱动开发实战07:PDD与设备树 2 | 3 | 从《树莓派驱动开发实战》的第一篇至今,都是在写单个字符设备,这其中不难发现个问题——如果我有10个LED灯就意味着我要写10个led字符设备驱动,而其中的大部分代码都是重复的,它们之间可能仅仅是控制引脚不同。 4 | 5 | 一是为了解决这个问题,二是之后的驱动开发更多会涉及USB、I²C、UART之类的总线设备,三是为了更好地理解Linux驱动架构。从本篇文章开始正式以`驱动-总线-设备`模型和`设备树`机制来编写设备驱动。我觉得以**平台驱动设备**模型作为切入点较好——可以不涉及真实的硬件。 6 | 7 | ## 驱动-总线-设备模型 8 | 9 | Linux2.6之后引入了全新驱动注册管理机制:**驱动、总线、设备**。一句话,为了高内聚低耦合! 10 | 11 | - 驱动部分:负责实现设备的控制逻辑及用户接口,并注册到内核 12 | - 设备部分:负责描述设备的硬件资源,并告知内核 13 | - 总线部分:负责实现设备与驱动之间的感知、识别、匹配规则 14 | 15 | 举例来说,如果有100个按键(比如键盘),我只需要实现`1个按键驱动`+`100个按键设备描述`,并把它们挂到`按键总线`上,总线会负责把二者匹配起来,所有的按键就都可以用了。 16 | 17 | 内核提供了相应的`bus、device、driver、class`等最为底层的API和数据结构,即`驱动、总线、设备`模型来管理系统设备。但在日常驱动开发中,一般是用不到的。因为常见物理总线都基于这些API封装了对应的如`usb_bus、usb_device、usb_driver、tty_device、tty_driver`等接口,日常的设备驱动开发更多以这一层打交道。 18 | 19 | 用面向对象的思想来说,驱动总线设备模型就是DeviceManage基类,由此派生出了USB、TTY、I²C、SPI、PCIe、GPIO等设备管理机制。当然,这其中也包括`platform`平台设备管理。 20 | 21 | ```sh 22 | philon@rpi:~ $ ls /sys/bus/ 23 | amba container genpd i2c media mmc_rpmb scsi usb 24 | clockevents cpu gpio iscsi_flashnode mipi-dsi nvmem sdio workqueue 25 | clocksource event_source hid mdio_bus mmc platform spi 26 | ``` 27 | 28 | 通过`/sys/bus`目录可以看到系统当前存在的总线。👆注意看倒数第二个,`platform`平台总线出现了!! 29 | 30 | ## 平台总线、平台驱动、平台设备 31 | 32 | **platform是一种虚拟总线**。它是“驱动-总线-设备”模型的一种实现,与`usb_bus tty_bus spi_bus`等物理总线平级。为了把那些不走总线架构的设备囊括进来。 33 | 34 | 回顾历史: 35 | 36 | 1. 编写字符设备`cdev`时需要关心主、次设备号,还要用mknod命令创建对应的设备节点。 37 | 2. 于是内核提供`misc`混杂设备,将所有不好管理的字符设备统一主设备号为10,自动分配和创建节点,本质上就是基于cdev再封装了一层。 38 | 3. 为了管理总线设备,内核又提出了`驱动总线设备`模型,并封装了各种USB、I²C、TTY等软件层。 39 | 4. 为了把非总线架构的设备也用总线思想来管理,内核提出了`平台驱动设备`层 40 | 41 | 所以,`platform`和`misc`一样,都是为了给`“其他”`设备找一个爸爸。 42 | 43 | ## 最简单的PDD实例 44 | 45 | 根据上述可推断,`platform_bus`(即总线部分)内核已经实现了,所以我们只需要实现两边的`platform_xxx_driver`和`platform_xxx_device`即可,然后把它们挂到平台总线上去,总线会自动进行匹配的。以下只是以led为例说明platform相关接口用法,并未真正实现led驱动。 46 | 47 | led_driver.c 48 | 49 | ```c 50 | #include 51 | #include 52 | 53 | MODULE_LICENSE("Dual BSD/GPL"); 54 | MODULE_AUTHOR("Philon | https://ixx.life"); 55 | 56 | // 当总线匹配到设备时调用该函数 57 | static int led_probe(struct platform_device* dev) { 58 | printk("led %s probe\n", dev->name); 59 | // todo: 字符设备注册、gpio申请之类的 60 | return 0; 61 | } 62 | 63 | // 当总线匹配到设备卸载时调用该函数 64 | static int led_remove(struct platform_device* dev) { 65 | printk("led %s removed\n", dev->name); 66 | // todo: 其他资源释放 67 | return 0; 68 | } 69 | 70 | // 平台驱动描述 71 | static struct platform_driver led_driver = { 72 | .probe = led_probe, 73 | .remove = led_remove, 74 | .driver = { 75 | .name = "my_led", // 👈务必注意,platform是以name比对来匹配的 76 | .owner = THIS_MODULE, 77 | }, 78 | }; 79 | 80 | // 【宏】将led驱动挂到平台总线上 81 | // 相当于同时定义了模块的入口和出口函数 82 | // module_init(platform_driver_register) 83 | // module_exit(platform_driver_unregister) 84 | module_platform_driver(led_driver); 85 | ``` 86 | 87 | led_device.c 88 | 89 | ```c 90 | #include 91 | #include 92 | 93 | MODULE_LICENSE("Dual BSD/GPL"); 94 | MODULE_AUTHOR("Philon | https://ixx.life"); 95 | 96 | // ⚠️最好实现该接口,否则在设备释放的时候内核会报错 97 | static void led_release(struct device* pdev) { 98 | printk("led release!\n"); 99 | } 100 | 101 | // 平台设备描述 102 | static struct platform_device led_device = { 103 | .name = "my_led", // 👈要确保与led_driver的定义一致,否则匹配不上 104 | .dev = { 105 | .release = led_release, 106 | }, 107 | }; 108 | 109 | // 将设备注册到平台总线 110 | static int leddev_init(void) { 111 | platform_device_register(&led_device); 112 | return 0; 113 | } 114 | module_init(leddev_init); 115 | 116 | static void leddev_exit(void) { 117 | platform_device_unregister(&led_device); 118 | } 119 | module_exit(leddev_exit); 120 | ``` 121 | 122 | 简述一下代码的逻辑: 123 | 124 | 1. platform_bus监听到有device注册时,会查看它的`device.name` 125 | 2. platform_bus会查找所有的`driver.name`,找到之后将设备和驱动进行绑定 126 | 3. 绑定成功后,`platform_driver.probe()`将触发,刚才的设备作为参数传递进去 127 | 4. 剩下的事情,就看你如何实现platform_driver了... 128 | 129 | 实际操作下,加载led_driver.ko模块后,可以在平台总线目录下看到`my_led`驱动了。然后,加载led_device.ko模块后,同样可以在平台总线设备里查看到`my_led.0`的设备。 130 | 131 | ```sh 132 | # 平台总线里查看my_led驱动 133 | philon@rpi:~/modules $ sudo insmod led_driver.ko 134 | philon@rpi:~/modules $ ls /sys/bus/platform/drivers/my_led/ 135 | bind module uevent unbind 136 | 137 | # 平台总线里查看my_led设备 138 | philon@rpi:~/modules $ sudo insmod led_device.ko 139 | philon@rpi:~/modules $ ls /sys/bus/platform/devices/my_led.0/ 140 | driver driver_override modalias power subsystem uevent 141 | 142 | # 看一下内核打印信息 143 | philon@rpi:~/modules $ dmesg 144 | [225668.547712] led my_led probe 145 | [225687.213336] led my_led removed 146 | [225687.213448] led release! 147 | ``` 148 | 149 | ⚠️温馨提示:不必操心driver/device模块的加载顺序,谁先谁后都一样,platform_bus会料理好一切。 150 | 151 | 以上,便是PDD模型的一个基本展示,如果你愿意,可以在led_device.c文件里多注册几个设备。不过在此之前——你内心难道不会充满疑惑吗:这tm怎么匹配上的呀?🤔️ 152 | 153 | ## 平台驱动和设备的匹配 154 | 155 | 看一下内核是如何实现平台匹配的,非常容易理解: 156 | 157 | ```c 158 | // linux-rpi-4.19.y/drivers/base/platform.c line:963 159 | static int platform_match(struct device *dev, struct device_driver *drv) 160 | { 161 | struct platform_device *pdev = to_platform_device(dev); 162 | struct platform_driver *pdrv = to_platform_driver(drv); 163 | 164 | /* When driver_override is set, only bind to the matching driver */ 165 | if (pdev->driver_override) 166 | return !strcmp(pdev->driver_override, drv->name); 167 | 168 | /* 首先尝试设备树匹配(OF - Open Firmware Standard) */ 169 | if (of_driver_match_device(dev, drv)) 170 | return 1; 171 | 172 | /* 然后尝试匹配高级配置和电源接口 173 | (ACPI - https://baike.baidu.com/item/ACPI/299421?fr=aladdin) 174 | */ 175 | if (acpi_driver_match_device(dev, drv)) 176 | return 1; 177 | 178 | /* 然后尝试匹配ID表 */ 179 | if (pdrv->id_table) 180 | return platform_match_id(pdrv->id_table, pdev) != NULL; 181 | 182 | /* 最后尝试匹配驱动和设备的名称 */ 183 | return (strcmp(pdev->name, drv->name) == 0); 184 | } 185 | ``` 186 | 187 | 从以上代码可以看出,平台总线的匹配经过`设备树 > ACPI > ID表 > 名称`等4种方式匹配,只要任意一种属性确认过眼神,就可以进行下一步。所以`led_driver`和`led_device`能够匹配上,正是因为它们内部的`name`值相同。 188 | 189 | 接着,看看平台驱动和平台设备的数据结构,一切就明朗了。 190 | 191 | 在平台驱动的数据结构中可以看到,它内部包含了底层的`device_driver`结构,如果驱动想要只是某些类型的设备,那就必须在相应的用于匹配的属性里事先声明。 192 | 193 | ```c 194 | struct platform_driver { 195 | int (*probe)(struct platform_device *); 196 | int (*remove)(struct platform_device *); 197 | void (*shutdown)(struct platform_device *); 198 | int (*suspend)(struct platform_device *, pm_message_t state); 199 | int (*resume)(struct platform_device *); 200 | struct device_driver driver; // 底层驱动数据结构 201 | const struct platform_device_id *id_table; // ID的匹配表👈 202 | bool prevent_deferred_probe; 203 | }; 204 | 205 | struct device_driver { 206 | const char *name; // 驱动名,用于名称匹配👈 207 | struct bus_type *bus; // 总线类型,如platform_bus 208 | struct module *owner; // 这就不解释了 209 | const char *mod_name; // 用于构建模块 210 | bool suppress_bind_attrs; /* disables bind/unbind via sysfs */ 211 | enum probe_type probe_type; 212 | 213 | const struct of_device_id *of_match_table; // 设备树的匹配表👈 214 | const struct acpi_device_id *acpi_match_table; // acpi的匹配表👈 215 | 216 | int (*probe) (struct device *dev); // 探测设备:当匹配成功时回调 217 | int (*remove) (struct device *dev); // 移除设备:当设备卸载时回调 218 | void (*shutdown) (struct device *dev); // 关闭设备 219 | int (*suspend) (struct device *dev, pm_message_t state); // 暂停 220 | int (*resume) (struct device *dev); // 恢复 221 | 222 | const struct attribute_group **groups; 223 | const struct dev_pm_ops *pm; // 电源管理 224 | void (*coredump) (struct device *dev); // 核心转储 225 | struct driver_private *p; // 私有数据 226 | }; 227 | ``` 228 | 229 | 和平台驱动很类似,其内部同样包含了底层的`device`结构体,如果设备想要被总线匹配上,同样要在自己的属性里配置好。 230 | 231 | ```c 232 | struct platform_device { 233 | const char *name; // 设备名,与驱动的name匹配 👈 234 | int id; // 设备ID 235 | bool id_auto; 236 | struct device dev; // 底层数据结构 237 | u32 num_resources; // 设备要用的资源数量 238 | struct resource *resource; // 设备的硬件资源描述 239 | 240 | const struct platform_device_id *id_entry; // 设备ID,与驱动的ID表匹配 👈 241 | char *driver_override; /* Driver name to force a match */ 242 | 243 | /* MFD cell pointer */ 244 | struct mfd_cell *mfd_cell; 245 | 246 | /* arch specific additions */ 247 | struct pdev_archdata archdata; 248 | }; 249 | ``` 250 | 251 | 好了,本文并不打算详细讨论有关平台、驱动、设备及其如何匹配的原理。如果对此感兴趣或想要深入研究,请用好互联网。我个人也查了很多资料,感觉这位作者写的[《Linux Platform驱动模型(一) _设备信息》](https://www.cnblogs.com/xiaojiang1025/p/6367061.html)和[《Linux Platform驱动模型(二) _驱动方法》](https://www.cnblogs.com/xiaojiang1025/archive/2017/02/06/6367910.html)还阔以,适合入门。 252 | 253 | 那么接下来,我们已经知道平台总线提供了4种匹配方法,`name`匹配就不说了,另外两种不提也罢,最最最重要的`设备树`匹配该登场了。 254 | 255 | ## 设备树 256 | 257 | 先声明,关于设备树的语法、树莓派的配置规则,不会涉及太多。本文侧重于实战,原理知识请用好互联网。 258 | 259 | 从历史上说,ARM-Linux引入设备树完全是被逼出来的。我们知道ARM以IP授权的商业模式运作,诞生了众多芯片厂商。它不像x86/x64架构只有Intel之类的寡头,产品大同小异比较好管理。arm的江湖可谓鱼龙混杂,每家都想在Linux内核种争的一席之地。可偏偏这帮家伙把又是硬件出生,对于兼容自家的不同产品只会用`if-else`,作为软件大神的Linus自然是怒了:“策略模式”难道不香么,你以为用C写一堆“电路板说明书”很高级么?如果你愿意,可以浏览下内核目录`arch/arm/mach-xxx`,非常多对吧。其实这些目录大多是SoC的硬件细节描述,用于适配各大厂商不同型号的处理器或开发板。 260 | 261 | 闲话就扯那么多,总之,设备树就是用类C的文本语言编写,用于描述Soc及其外围电路模块的配置文件。通常情况下它由bootloader传递给内核。这种做法,极大的降低了驱动的维护难度,也大大增加了系统设备管理的灵活性。 262 | 263 | ⚠️在阅读下文前,必须基本懂得两个知识点: 264 | 265 | 1. 设备树语法,我就不多嘴了,网上一搜一大堆 266 | 2. 树莓派overlay机制,这个网上几乎没有,我做个大概说明 267 | 268 | ### 树莓派的设备树配置 269 | 270 | 本小结是从👉[树莓派DeviceTree的官方介绍中](https://www.raspberrypi.org/documentation/configuration/device-tree.md)总结而来,如果想更全面地了解,可以看原文。 271 | 272 | 一个常规的Arm-Linux设备树,主要是由源文件`.dts`和头文件`.dtsi`共同编译出`.dtb`二进制,内核在初始化后会加载这个dtb,并把相关设备都注册好,就可以愉快地使用了。例如树莓派3B+,`/boot/bcm2710-rpi-3-b-plus.dtb`就是树莓派SoC和外围电路的默认配置。 273 | 274 | 对于大部分硬件产品来说这没什么问题,例如一部手机在出厂以后,它的硬件几乎是不会变的。但对于树莓派这种开发板来说,尤其是它的40pin扩展引脚,外围电路的变动可就大了去了,而内核加载`dtb`后是不能变的,所以需要一种**动态覆盖配置**的设备树机制,这就是树莓派的——dtoverlay(设备树覆盖)。 275 | 276 | dtoverlay同样是由dts源编译而来,语法几乎和设备树一样,不过输出文件扩展名为`dtbo`。树莓派提供了两种方式加载dtbo: 277 | 278 | 1. 将编译好的dtbo放到`/boot/overlays`下,并由`/boot/config.txt`配置和使能; 279 | 2. 通过命令`dtoverlay `动态覆盖设备树; 280 | 281 | 第1种方式会涉及更复杂的语法规则,本篇文章仅仅是对平台设备及设备树的知识入门,因此选择第2种命令行的方式,动态加载。 282 | 283 | ### 用设备树注册设备 284 | 285 | led_driver.c: 286 | 其他内容不变,仅仅是增加`of_device_id`属性。 287 | 288 | ```c 289 | // 首先用of_device_id声明了三种LED型号的表,支持设备树解析 290 | static const struct of_device_id of_leds_id[] = { 291 | { .compatible = "led_type_a" }, 292 | { .compatible = "led_type_b" }, 293 | { .compatible = "led_type_c" }, 294 | }; 295 | 296 | static struct platform_driver led_driver = { 297 | .probe = led_probe, 298 | .remove = led_remove, 299 | .driver = { 300 | .name = "my_led", 301 | .of_match_table = of_leds_id, // 👈在驱动种添加对应属性 302 | .owner = THIS_MODULE, 303 | }, 304 | }; 305 | ``` 306 | 307 | 接着新建一个设备树文件,并定义一个`led_type_a`的LED设备,并将其命名为`led_a1`。 308 | 309 | myled.dts 310 | 311 | ``` 312 | /dts-v1/; 313 | /plugin/; 314 | 315 | / { 316 | fragment@0 { 317 | target-path = "/"; 318 | __overlay__ { 319 | led_a1 { 320 | compatible = "led_type_a"; 321 | }; 322 | }; 323 | }; 324 | }; 325 | ``` 326 | 327 | `fragment`和`__overlay__`非常重要!!如果不这么写会导致动态加载失败,但其实以上的代码转化为标准的设备树语法为: 328 | 329 | ``` 330 | /led_a1 { 331 | compatible = "led_type_a"; 332 | }; 333 | ``` 334 | 335 | 最后用`dtc`编译器将`dts`编译为`dtbo`: 336 | 337 | ```sh 338 | linux-rpi-4.19.y/scripts/dtc -I dts -o myled.dtbo myled.dts 339 | ``` 340 | 341 | 万事俱备,看看效果吧: 342 | 343 | ```sh 344 | # 第一步:加载led驱动 345 | philon@rpi:~/modules $ sudo insmod led_driver.ko 346 | # 第二步:加载设备树覆盖 347 | philon@rpi:~/modules $ sudo dtoverlay myled.dtbo 348 | # 第三部:看看平台设备里是否注册了一个叫“led_a1”的设备 349 | philon@rpi:~/modules $ ls /sys/devices/platform/ 350 | alarmtimer Fixed MDIO bus.0 👉led_a1👈 power serial8250 uevent 351 | ... 352 | 353 | # 显然已经注册,根据led_driver的实现,设备注册后会在probe函数中打印一条消息 354 | philon@rpi:~/modules $ dmesg 355 | ... 356 | [ 429.359567] leddrv: no symbol version for module_layout 357 | [ 429.359577] leddrv: loading out-of-tree module taints kernel. 358 | [ 435.995744] led led_a1 probe 👈啊~我看到树上长了个灯 359 | ``` 360 | 361 | 再来回顾下流程: 362 | 363 | 1. 首先驱动要支持`of_device_id`属性,并且以`compatible`作为匹配对象 364 | 2. 然后通过编写设备树定义相应的设备资源 365 | 3. 最后通过加载驱动和dtoverlay即可 366 | 367 | ### 让设备开机自动注册 368 | 369 | 这就非常简单了,前面已经说过`/boot/overlays`其实是通过`config.txt`配置和使能的,所以我们只需要将`myled.dtbo`放到overlays目录下,并在config.txt添加一行使能即可。 370 | 371 | ```sh 372 | # 第一步:将自己的dtbo放到overlays下 373 | philon@rpi:~/modules $ sudo cp myled.dtbo /boot/overlays 374 | # 第二步:在config.txt最后一行添加myled 375 | philon@rpi:~/modules $ sudo echo "dtoverlay=myled" | sudo tee -a /boot/config.txt 376 | # 第三步:reboot... 377 | # 第四步:可以在/sys/device/platform下查看到设备已经注册 378 | philon@rpi:~/modules $ ls /sys/devices/platform/ 379 | alarmtimer Fixed MDIO bus.0 👉led_a1👈 power serial8250 uevent 380 | ... 381 | 382 | # ⚠️但是,设备树仅仅是定义了led_device,而led_driver.ko其实并没有开机加载,如果要更完善的话,应该把led_driver直接编译进内核! 383 | philon@rpi:~/modules $ sudo insmod leddrv.ko 384 | philon@rpi:~/modules $ dmesg 385 | ... 386 | [ 214.076752] leddrv: no symbol version for module_layout 387 | [ 214.076771] leddrv: loading out-of-tree module taints kernel. 388 | [ 214.077535] led led_a1 probe 389 | ``` 390 | 391 | ## 小结 392 | 393 | - Linux-2.6后引入了`驱动-总线-设备`的软件架构来管理系统设备; 394 | - `platform`设备和USB、TTY、UART一样,都是基于底层的抽象和封装; 395 | - `platform`是为了把那些没有总线的设备,以总线的思想管理起来,所以它算作一根虚拟总线; 396 | - 平台总线提供了多种驱动和设备的匹配规则:设备树、ACPI、ID表、名称等; 397 | - 设备树是由bootloader传递给内核,并且在初始化后基本不可修改; 398 | - 树莓派为了满足设备树动态修改的需求,引入了`dtoverlay`; 399 | - dtoverlay采用常规的设备树语法,但需要`fragment`和`__overlay__`属性; 400 | - 驱动必须定义`of_device_id`数据结构,才能与设备树匹配; 401 | - 务必掌握设备树语法! -------------------------------------------------------------------------------- /07-pdd/led.h: -------------------------------------------------------------------------------- 1 | #ifndef RPI_DRIVER_PDD_LED_H 2 | #define RPI_DRIVER_PDD_LED_H 3 | 4 | #include 5 | 6 | struct platform_device_id led_types[] = { 7 | { .name = "type0_led" }, 8 | { .name = "type1_led" }, 9 | {}, 10 | }; 11 | 12 | #endif //!RPI_DRIVER_PDD_LED_H -------------------------------------------------------------------------------- /07-pdd/leddev.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | MODULE_LICENSE("Dual BSD/GPL"); 5 | MODULE_AUTHOR("Philon | https://ixx.life"); 6 | 7 | static void led_release(struct device* pdev) { 8 | printk("led release!\n"); 9 | } 10 | 11 | static struct platform_device led_device = { 12 | .name = "type0_led", 13 | .dev = { 14 | .release = led_release, 15 | }, 16 | }; 17 | 18 | static int leddev_init(void) { 19 | platform_device_register(&led_device); 20 | return 0; 21 | } 22 | module_init(leddev_init); 23 | 24 | static void leddev_exit(void) { 25 | platform_device_unregister(&led_device); 26 | } 27 | module_exit(leddev_exit); -------------------------------------------------------------------------------- /07-pdd/leddrv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "led.h" 6 | 7 | MODULE_LICENSE("Dual BSD/GPL"); 8 | MODULE_AUTHOR("Philon | https://ixx.life"); 9 | 10 | // 当总线匹配到设备加载时调用该函数 11 | static int led_probe(struct platform_device* dev) { 12 | printk("led %s probe\n", dev->name); 13 | return 0; 14 | } 15 | 16 | // 当总线匹配到设备卸载时调用该函数 17 | static int led_remove(struct platform_device* dev) { 18 | printk("led %s removed\n", dev->name); 19 | return 0; 20 | } 21 | 22 | static const struct of_device_id of_leds_id[] = { 23 | { .compatible = "led_type_a" }, 24 | { .compatible = "led_type_b" }, 25 | { .compatible = "led_type_c" }, 26 | }; 27 | // MODULE_DEVICE_TABLE(of, of_leds_id); 28 | 29 | static struct platform_driver led_driver = { 30 | .probe = led_probe, 31 | .remove = led_remove, 32 | .driver = { 33 | .name = "my_led", 34 | .of_match_table = of_leds_id, 35 | .owner = THIS_MODULE, 36 | }, 37 | 38 | .id_table = led_types, 39 | }; 40 | 41 | // 【宏】向内核注册统一的led驱动 42 | // 相当于同时定义了模块的入口和出口函数 43 | // module_init(platform_driver_register) 44 | // module_exit(platform_driver_unregister) 45 | module_platform_driver(led_driver); -------------------------------------------------------------------------------- /07-pdd/myled.dts: -------------------------------------------------------------------------------- 1 | /dts-v1/; 2 | /plugin/; 3 | 4 | / { 5 | fragment@0 { 6 | target-path = "/"; 7 | __overlay__ { 8 | led_a1 { 9 | compatible = "led_type_a"; 10 | }; 11 | }; 12 | }; 13 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 树莓派驱动开发实战 2 | 3 | 本项目是基于Raspberry Pi 3B+平台学习Linux驱动开发的记录与分享,旨于对Linux内核模块机制的熟悉、常见接口的Linux驱动实现、常用模块的原理掌握。原则上,每个驱动模块我都会编写对应的教程(因为我相信掌握知识最有效的方式是理解并能转述)。 4 | 5 | ## 环境 6 | 7 | 详细的环境搭建已在[00-hello](./00-hello/README.md)中介绍,以下仅做补充说明。 8 | 9 | - 内核:务必按照树莓派所运行的Linux内核版本到官方仓库下载,比如[rpi-4.19.y.tar.gz](https://github.com/raspberrypi/linux/archive/rpi-4.19.y.tar.gz),通常情况下我会将其放在工程根目录 10 | - IDE:我个人喜欢用vscode,显然本项目的`.vscode目录`是我自己的环境配置,仅做参考 11 | - 交叉编译:建议采用官方提供的cross-toolchain,由于我个人宿主机为macOS环境,交叉编译器为自行构建的[arm-mac-linux-gnueabihf-10.3.0](https://github.com/Philon/macos-crosstools)版本 12 | - 接线图:文章中涉及的电路图采用[Fritzing](https://fritzing.org)绘制,即各目录下的.fzz文件 13 | 14 | ## 编译规则 15 | 16 | `rules.mk`是一套通用的驱动模块及测试用例构建规则,在各驱动源码目录下的`Makefile`中指定模块并将其包含即可。例如: 17 | 18 | ```Makefile 19 | # ./00-hello/Makefile 20 | obj-m := hello.o 21 | -include ../rules.mk 22 | ``` 23 | 24 | 如此这般,便可获得如下方式对项目进行构建: 25 | 26 | ```sh 27 | make # 生成全部(内核模块、测试用例、dtbo等) 28 | make clean # 清理项目 29 | make modules # 仅编译驱动模块及设备树(若有) 30 | make tests # 仅编译测试用例 31 | make install # 安装相应驱动模块、测试用例等至目标开发板 32 | ``` 33 | 34 | ### 文件类型 35 | 36 | - `xxx_test.c`会被视作测试用例,会生成对应的"xxx_test"程序 37 | - `xxx.dts`会被视作设备树,在编译模块是会同时将其编译"xxx.dtbo" 38 | 39 | ### Makefile变量 40 | 41 | 可以在Makefile中配置相关环境或参数,当然它不是必须的,如果觉得麻烦也可以直接去修改rules。不过要注意,任何配置都必须放在"-include ../rules.mk"语句之前,否则不生效。 42 | 43 | ```Makefile 44 | # ./00-hello/Makefile 45 | obj-m := hello.o 46 | 47 | # 指定交叉编译工具链前缀 48 | CROSS_COMPILE = /usr/local/bin/arm-linux- 49 | 50 | # 指定内核源码目录 51 | KDIR = /home/user/linux-rpi-4.19.y 52 | 53 | # 配置测试用例编译链接参数 54 | LDFLAGS = -lpthread -L/home/user/mylib -lmy 55 | 56 | # 指定安装路径(将通过scp命令远程拷贝,即scp :) 57 | INSTALL_PATH = 192.168.1.100:~/modules 58 | 59 | # 额外需要安装的文件指定(*.ko *.dtbo *_test将被自动检测并安装) 60 | INSTALL_FILES = file1 file2 file3 61 | 62 | -include ../rules.mk 63 | ``` 64 | -------------------------------------------------------------------------------- /document/device-tree.cn.md: -------------------------------------------------------------------------------- 1 | # Device Trees, overlays, and parameters 2 | 3 | Raspberry Pi的最新内核和固件(包括Raspbian和NOOBS版本)现在默认使用设备树(DT)来管理一些资源分配和模块加载。这样做是为了缓解多个驱动程序争用系统资源的问题,并允许HAT模块自动配置。 4 | 5 | 当前的实现不是纯粹的设备树系统-仍然存在创建某些平台设备的板级支持代码-但是外部接口(I2C,I2S,SPI)以及使用它们的音频设备现在必须使用设备实例化加载程序(start.elf)将Tree Blob(DTB)传递到内核。 6 | 7 | 除非DTB要求,否则使用设备树的主要影响是将所有内容(依靠模块黑名单来管理争用)更改为全部关闭。为了继续使用外部接口和与其连接的外围设备,您将需要在config.txt中添加一些新设置。第3部分中提供了完整的详细信息,但以下是一些示例: 8 | 9 | ```sh 10 | # 取消注释部分或全部这些行以启用可选硬件接口 11 | #dtparam=i2c_arm=on 12 | #dtparam=i2s=on 13 | #dtparam=spi=on 14 | 15 | # 取消注释这些行之一以启用音频接口 16 | #dtoverlay=hifiberry-amp 17 | #dtoverlay=hifiberry-dac 18 | #dtoverlay=hifiberry-dacplus 19 | #dtoverlay=hifiberry-digi 20 | #dtoverlay=iqaudio-dac 21 | #dtoverlay=iqaudio-dacplus 22 | #dtoverlay=audioinjector-wm8731-audio 23 | 24 | # 取消注释此选项以启用lirc-rpi模块 25 | #dtoverlay=lirc-rpi 26 | 27 | # 取消注释此选项以覆盖lirc-rpi模块的默认值 28 | #dtparam=gpio_out_pin=16 29 | #dtparam=gpio_in_pin=17 30 | #dtparam=gpio_in_pull=down 31 | ``` 32 | 33 | ## Device Trees 34 | 35 | 设备树(DT)是对系统中硬件的描述。 它应包括基本CPU的名称,其内存配置以及所有外围设备(片内的和片外的)。 DT不应用于描述软件,尽管通过列出硬件模块通常会导致加载驱动程序模块。 记住DT应该是与操作系统无关的,这可能有助于记住,所以任何Linux特定的内容都不应该存在。 36 | 37 | 设备树将硬件配置表示为节点的层次结构。 每个节点都可以包含属性和子节点。 属性被命名为字节数组,可以包含字符串,数字(大端),任意字节序列及其任意组合。 类似于文件系统,节点是目录,属性是文件。 树中节点和属性的位置可以使用路径来描述,以斜杠作为分隔符,并使用单个斜杠(`/`)表示根。 38 | 39 | ### 1.1: DTS基础语法 40 | 41 | 设备树通常以称为设备树源(DTS)的文本形式编写,并存储在带有`.dts`后缀的文件中。 DTS语法类似于C,每行的末尾都有用于分组的括号和分号。 请注意,DTS在右大括号后需要分号:相对于C的`struct`而不是函数。 编译后的二进制格式称为“扁平化设备树(FDT)”或“设备树Blob(DTB)”,并存储在`.dtb`文件中。 42 | 43 | 以下是.dts格式的简单树: 44 | 45 | ```sh 46 | /dts-v1/; 47 | /include/"common.dtsi"; 48 | 49 | /{ 50 | node1 { 51 | a-string-property = "A string"; 52 | a-string-list-property = "first string", "second string"; 53 | a-byte-data-property = [0x01 0x23 0x34 0x56]; 54 | cousin: child-node1 { 55 | first-child-property; 56 | second-child-property = <1>; 57 | a-string-property = "Hello, world"; 58 | }; 59 | child-node2 { 60 | }; 61 | }; 62 | node2 { 63 | an-empty-property; 64 | a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */ 65 | child-node1 { 66 | my-cousin = <&cousin>; 67 | }; 68 | }; 69 | }; 70 | 71 | /node2 { 72 | another-property-for-node2; 73 | }; 74 | ``` 75 | 76 | 该树包含: 77 | 78 | - 必需的标头:`/dts-v1/`。 79 | - 包含另一个DTS文件,该文件通常名为`*.dtsi`,类似于C中的.h头文件-请参见下面的/include/。 80 | - 单个根节点:`/` 81 | - 几个子节点:`node1`和`node2` 82 | - node1的一些子节点:`child-node1`和`child-node2` 83 | - 标签(`cousin`)和对该标签的引用(`&cousin`):请参阅下面的标签和引用。 84 | - 分散在树上的几个属性 85 | - 一个重复的节点(`/node2`)-参见下面的/include/。 86 | 87 | 属性是简单的键/值对,其中值可以为空或包含任意字节流。 尽管数据类型未在数据结构中编码,但是可以在设备树源文件中表达一些基本数据表示形式。 88 | 89 | 文本字符串(NUL终止)用双引号表示: 90 | 91 | ```sh 92 | string-property = "a string"; 93 | ``` 94 | 95 | 单元格是由尖括号分隔的32位无符号整数: 96 | 97 | ```sh 98 | cell-property = <0xbeef 123 0xabcd1234>; 99 | ``` 100 | 101 | 任意字节数据用方括号定界,并以十六进制输入: 102 | 103 | ``` 104 | binary-property = [01 23 45 67 89 ab cd ef]; 105 | ``` 106 | 107 | 可以使用逗号将不同表示形式的数据连接起来: 108 | 109 | ```sh 110 | mixed-property = "a string", [01 23 45 67], <0x12345678>; 111 | ``` 112 | 113 | 逗号还用于创建字符串列表: 114 | 115 | ```sh 116 | string-list = "red fish", "blue fish"; 117 | ``` 118 | 119 | ### 1.2: 关于/include/ 120 | 121 | `/include/`指令是简单的文本包含,就像C的`#include`指令一样,但是Device Tree编译器的功能导致了不同的使用模式。 给定节点的名称(可能带有绝对路径),则同一个节点可能在DTS文件(及其包含文件)中出现两次。 发生这种情况时,将节点和属性组合在一起,根据需要进行交织和覆盖属性(后面的值会覆盖前面的值)。 122 | 123 | 在上面的示例中,`/node2`的第二次出现导致将新属性添加到原始属性: 124 | 125 | ```sh 126 | /node2 { 127 | an-empty-property; 128 | a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */ 129 | another-property-for-node2; 130 | child-node1 { 131 | my-cousin = <&cousin>; 132 | }; 133 | }; 134 | ``` 135 | 136 | 因此,一个`.dtsi`可能会覆盖树中的多个位置或为其提供默认值。 137 | 138 | ### 1.3: 标签与引用 139 | 140 | 树往往会一部分引用另一部分,有四种方法可以做到这一点: 141 | 142 | 1. 路径字符串 143 | 144 | 类似于文件系统,路径应该是不言自明的 - `/soc/i2s@7e203000`是BCM2835和BCM2836中I2S设备的完整路径。请注意,尽管构造属性的路径很容易(例如,`/soc/i2s@7e203000/status`),但是标准API却不这样做。您首先找到一个节点,然后选择该节点的属性。 145 | 146 | 2. phandles(把手) 147 | 148 | 把手是在其`phandle`属性中分配给节点的唯一32位整数。由于历史原因,您可能还会看到一个冗余的,匹配`linux,phandle`。把手从1开始顺序编号; 0不是有效的。当它们在整数上下文中遇到对节点的引用时,通常由DT编译器分配它们,通常以标签的形式(请参见下文)。使用句柄对节点的引用被简单地编码为相应的整数(单元)值。没有标记指示应将它们解释为phandles,因为这是应用程序定义的。 149 | 150 | 3. Labels(标签) 151 | 152 | 就像C中的标签为代码中的位置提供名称一样,DT标签也为层次结构中的节点分配名称。编译器获取对标签的引用,并将其在字符串上下文(`&node`)中使用时转换为路径,在整数上下文(`<&node>`)中使用phandles;原始标签不会出现在编译输出中。请注意,标签不包含任何结构。它们只是一个统一的全局命名空间中的令牌。 153 | 154 | 4. Aliases(别名) 155 | 156 | 别名与标签类似,不同的是别名确实以索引形式出现在FDT输出中。它们存储为`/aliases`节点的属性,每个属性都将别名映射到路径字符串。尽管别名节点出现在源中,但路径字符串通常显示为对标签(`&node`)的引用,而不是全部写出。 DT API将节点的路径字符串解析为路径的首字符,通常将不以斜杠开头的路径视为别名,必须先使用`/aliases`表将其转换为路径。 157 | 158 | ### 1.4:设备树语义 159 | 160 | 如何构造设备树,以及如何最好地使用它来捕获某些硬件的配置是一个大而复杂的主题。有很多可用的资源,下面列出了其中的一些资源,但是在本文档中有几点需要提及: 161 | 162 | `compatible`(兼容性)是硬件描述和驱动程序软件之间的链接。当操作系统遇到具有`compatible`属性的节点时,它将在其设备驱动程序数据库中查找该节点以找到最佳匹配项。在Linux中,如果已正确标记驱动程序模块且未将其列入黑名单,这通常会导致驱动程序模块自动加载。 163 | 164 | `status`(状态)属性指示设备是启用还是禁用。如果`status`为`ok`、`okay`或不存在,则表示设备已启用。否则`status`应为`disabled`,以便禁用设备。将设备置于状态设置为`disabled`的`.dtsi`文件中可能很有用。然后,派生的配置可以包括该`.dtsi`并设置需要的设备状态。 165 | 166 | ## 第2部分:设备树覆盖 167 | 168 | 现代的SoC(片上系统)是非常复杂的设备。完整的设备树可能长达数百行。再往前走一步,将SoC与其他组件一起放置在板上,只会使情况变得更糟。为了保持可管理性,特别是在存在共享组件的相关设备的情况下,将公共元素放入`.dtsi`文件中是很有意义的,以便可能包含多个`.dts`文件。 169 | 170 | 但是,当像Raspberry Pi这样的系统支持可选的插件附件(例如HAT)时,问题就会加剧。最终,每种可能的配置都需要一个设备树来描述它,但是一旦您考虑到不同的基本硬件(模型A,B,A +和B +),并且小工具只需要使用几个可以共存的GPIO引脚,组合开始迅速繁殖。 171 | 172 | 所需要的是一种使用部分设备树描述这些可选组件的方法,然后通过采用基本DT并添加许多可选元素来构建完整的树。您可以执行此操作,这些可选元素称为“叠加层”。 173 | 174 | ### 2.1:Fragments(片段) 175 | 176 | DT覆盖包括多个片段,每个片段都针对一个节点及其子节点。尽管这个概念听起来很简单,但是语法一开始似乎很奇怪: 177 | 178 | ```sh 179 | //Enable the i2s interface 180 | /dts-v1/; 181 | /plugin/; 182 | 183 | /{ 184 | compatible = "brcm,bcm2708"; 185 | 186 | fragment@0 { 187 | target = <&i2s>; 188 | __overlay__ { 189 | status = "okay"; 190 | }; 191 | }; 192 | }; 193 | ``` 194 | 195 | `compatible`字符串将其标识为BCM2708,这是BCM2835部件的基本体系结构。 对于BCM2836部分,您可以使用兼容的字符串“ brcm,bcm2709”,但是除非您针对的是ARM CPU的功能,否则两种架构应该等效,因此坚持使用“ brcm,bcm2708”是合理的。 然后是第一个(仅在这种情况下)片段。 片段从零开始顺序编号。 不遵守此规定可能会导致丢失部分或全部片段。 196 | 197 | 每个片段由两部分组成:一个`target`属性,标识要应用叠加层的节点; 和`__overlay__`本身,其主体将添加到目标节点。 可以将上面的示例解释为如下所示: 198 | 199 | ``` 200 | /dts-v1/; 201 | 202 | / { 203 | compatible = "brcm,bcm2708"; 204 | }; 205 | 206 | &i2s { 207 | status = "okay"; 208 | }; 209 | ``` 210 | 211 | 如果覆盖层是在以后加载的,那么将覆盖层与标准的Raspberry Pi基本设备树(例如`bcm2708-rpi-b-plus.dtb`)合并,将会通过将I2S接口的状态更改为`okay`来启用I2S接口。 但是,如果您尝试使用以下命令编译此叠加层: 212 | 213 | ``` 214 | dtc -I dts -O dtb -o 2nd.dtbo 2nd-overlay.dts 215 | ``` 216 | 217 | 你会看到一个错误提示: 218 | 219 | ``` 220 | Label or path i2s not found 221 | ``` 222 | 223 | 这不应太出乎意料,因为没有引用基础`.dtb`或`.dts`文件来允许编译器找到`i2s`标签。 224 | 225 | 再试一次,这次使用原始示例并添加`-@`选项以允许未解析的引用: 226 | 227 | ``` 228 | dtc -@ -I dts -O dtb -o 1st.dtbo 1st-overlay.dts 229 | ``` 230 | 231 | 如果`dtc`返回有关第三行的错误,则它没有覆盖工作所需的扩展名。 运行`sudo apt install device-tree-compiler`并重试-这次,编译应该成功完成。 注意,在内核树中还可以使用`scripts/dtc/dtc`作为合适的编译器,该编译器是在使用`dtbs` make target时构建的: 232 | 233 | ``` 234 | make ARCH=arm dtbs 235 | ``` 236 | 237 | 转储DTB文件的内容以查看编译器生成的内容很有趣: 238 | 239 | ``` 240 | $ fdtdump 1st.dtbo 241 | 242 | /dts-v1/; 243 | // magic: 0xd00dfeed 244 | // totalsize: 0x106 (262) 245 | // off_dt_struct: 0x38 246 | // off_dt_strings: 0xe8 247 | // off_mem_rsvmap: 0x28 248 | // version: 17 249 | // last_comp_version: 16 250 | // boot_cpuid_phys: 0x0 251 | // size_dt_strings: 0x1e 252 | // size_dt_struct: 0xb0 253 | 254 | / { 255 | compatible = "brcm,bcm2708"; 256 | fragment@0 { 257 | target = <0xdeadbeef>; 258 | __overlay__ { 259 | status = "okay"; 260 | }; 261 | }; 262 | __fixups__ { 263 | i2s = "/fragment@0:target:0"; 264 | }; 265 | }; 266 | ``` 267 | 268 | 在文件结构的详细描述之后,是我们的片段。但是请仔细看-在我们写`&i2s`的地方,现在写着`0xdeadbeef`,这表明发生了奇怪的事情。片段之后有一个新节点`__fixups__`。这包含一个属性列表,该属性列表将未解析符号的名称映射到片段中的单元格的路径列表,一旦找到了目标节点,该片段就需要使用目标节点的模范进行修补。在这种情况下,路径是`target`的值`0xdeadbeef`,但是片段可以包含其他未解析的引用,这将需要其他修复。 269 | 270 | 如果编写更复杂的片段,则编译器可能会生成另外两个节点:`__local_fixups__`和`__symbols__`。如果片段中的任何节点都有一个phandle,则前者是必需的,因为执行合并的程序将必须确保phandle号是连续且唯一的。但是,后者是如何处理未解析符号的关键。 271 | 272 | 在第1.3节中,它说“原始标签不会出现在编译输出中”,但是在使用`-@`开关时情况并非如此。相反,每个标签都会在`__symbols__`节点中产生一个属性,将标签映射到路径,就像`aliases`节点一样。实际上,该机制是如此相似,以至于在解析符号时,Raspberry Pi加载器将在没有`__symbols__`节点的情况下搜索“ aliases”节点。这很有用,因为通过提供足够的别名,我们可以允许使用较旧的`dtc`来构建基本DTB文件。 273 | 274 | 更新:内核中对[动态设备树](https://www.raspberrypi.org/documentation/configuration/device-tree.md#part3.5)的支持要求覆盖中的“本地修正”格式不同。为了避免新旧样式的叠加样式存在问题,并与其他叠加样式用户匹配,从4.4版开始,旧的“ name-overlay.dtb”命名方案已替换为“ name.dtbo”。覆盖层应该仅通过名称来引用,加载它们的固件或实用程序将附加适当的后缀。例如: 275 | 276 | ``` 277 | dtoverlay=awesome-overlay # This is wrong 278 | dtoverlay=awesome # This is correct 279 | ``` 280 | 281 | ### 2.2: 设备树参数 282 | 283 | 为了避免需要大量的设备树覆盖,并减少外围设备用户修改DTS文件的需求,Raspberry Pi加载程序支持一项新功能-设备树参数。 这允许使用命名参数对DT进行少量更改,类似于内核模块从`modprobe`和内核命令行接收参数的方式。 基本DTB和覆盖(包括HAT覆盖)可以暴露参数。 284 | 285 | 通过在根目录中添加`__overrides__`节点在DTS中定义参数。 它包含一些属性,这些属性的名称是所选的参数名称,其值是一个序列,该序列包括目标节点的phandle(对标签的引用)和指示目标属性的字符串; 支持字符串,整数(单元格)和布尔属性。 286 | 287 | #### 2.2.1: 字符串 288 | 289 | 字符串的声明如下: 290 | 291 | ``` 292 | name = <&label>,"property"; 293 | ``` 294 | 295 | 其中`label`和`property`被适当的值替换。 字符串参数可以导致其目标属性增长,缩小或创建。 296 | 297 | 请注意,对称为`status`的属性进行了特殊处理。 non-zero/true/yes/on的值将转换为字符串`“okay”`,而zero/false/no/off将变为`“disabled”`。 298 | 299 | #### 2.2.2: 整型 300 | 301 | 整型参数声明如下: 302 | 303 | ``` 304 | name = <&label>,"property.offset"; // 8-bit 305 | name = <&label>,"property;offset"; // 16-bit 306 | name = <&label>,"property:offset"; // 32-bit 307 | name = <&label>,"property#offset"; // 64-bit 308 | ``` 309 | 310 | 用适当的值替换`label`、`property`以及`offset`;相对于属性的开头以字节为单位指定偏移量(默认为十进制),并且前面的分隔符指示参数的大小。 与以前的实现方式相比,整数参数可以引用不存在的属性,也可以引用超出现有属性结尾的偏移量。 311 | 312 | #### 2.2.3:布尔型 313 | 314 | 设备树将布尔值编码为零长度属性;如果存在,则该属性为true,否则为false。它们的定义如下: 315 | 316 | ``` 317 | boolean_property; //将'boolean_property'设置为true 318 | ``` 319 | 320 | 请注意,通过不定义属性,为属性分配了`false`值。布尔参数声明如下: 321 | 322 | ``` 323 | name = <&label>,"property?"; 324 | ``` 325 | 326 | 其中`label`和`property`被适当的值替换。布尔参数可以导致创建或删除属性。 327 | 328 | #### 2.2.4:叠加/片段 329 | 330 | 所描述的DT参数机制具有许多限制,包括当使用参数时无法更改节点名称以及将任意值写入任意属性。克服其中一些限制的一种方法是有条件地包括或排除某些片段。 331 | 332 | 通过将`__overlay__`节点重命名为`__dormant__`,可以将片段从最终合并过程中排除(禁用)。扩展了参数声明语法,以允许否则为非法的零目标对象指示以下字符串包含片段或覆盖范围内的操作。到目前为止,已实施了四个操作: 333 | 334 | ``` 335 | + //启用片段 336 | - //禁用片段 337 | = //如果分配的参数值为true,则启用片段,否则将其禁用 338 | ! //如果分配的参数值为false,则启用片段,否则将其禁用 339 | ``` 340 | 341 | 例子: 342 | 343 | ``` 344 | just_one = <0>,"+1-2"; //启用1,停用2 345 | conditional = <0>,"=3!4"; //启用3,如果值为true,则禁用4, 346 |                           //否则禁用3,启用4。 347 | ``` 348 | 349 | i2c-mux覆盖使用此技术。 350 | 351 | #### 2.2.5: 例子 352 | 353 | 以下是一些不同类型的属性的示例,并带有用于修改它们的参数: 354 | 355 | ``` 356 | / { 357 | fragment@0 { 358 | target-path = "/"; 359 | __overlay__ { 360 | 361 | test: test_node { 362 | string = "hello"; 363 | status = "disabled"; 364 | bytes = /bits/ 8 <0x67 0x89>; 365 | u16s = /bits/ 16 <0xabcd 0xef01>; 366 | u32s = /bits/ 32 <0xfedcba98 0x76543210>; 367 | u64s = /bits/ 64 < 0xaaaaa5a55a5a5555 0x0000111122223333>; 368 | bool1; // Defaults to true 369 | // bool2 defaults to false 370 | }; 371 | }; 372 | }; 373 | 374 | fragment@1 { 375 | target-path = "/"; 376 | __overlay__ { 377 | frag1; 378 | }; 379 | }; 380 | 381 | fragment@2 { 382 | target-path = "/"; 383 | __dormant__ { 384 | frag2; 385 | }; 386 | }; 387 | 388 | __overrides__ { 389 | string = <&test>,"string"; 390 | enable = <&test>,"status"; 391 | byte_0 = <&test>,"bytes.0"; 392 | byte_1 = <&test>,"bytes.1"; 393 | u16_0 = <&test>,"u16s;0"; 394 | u16_1 = <&test>,"u16s;2"; 395 | u32_0 = <&test>,"u32s:0"; 396 | u32_1 = <&test>,"u32s:4"; 397 | u64_0 = <&test>,"u64s#0"; 398 | u64_1 = <&test>,"u64s#8"; 399 | bool1 = <&test>,"bool1?"; 400 | bool2 = <&test>,"bool2?"; 401 | only1 = <0>,"+1-2"; 402 | only2 = <0>,"-1+2"; 403 | toggle1 = <0>,"=1"; 404 | toggle2 = <0>,"=2"; 405 | not1 = <0>,"!1"; 406 | not2 = <0>,"!2"; 407 | }; 408 | }; 409 | ``` 410 | 411 | #### 2.2.6:具有多个目标的参数 412 | 413 | 在某些情况下,能够在设备树中的多个位置设置相同的值很方便。可以使用串联多个目标的方法来将多个目标添加到单个参数中,而不是笨拙地创建多个参数的方法: 414 | 415 | ``` 416 | __overrides__ { 417 | gpiopin = <&w1>,"gpios:4", 418 | <&w1_pins>,"brcm,pins:0"; 419 | ... 420 | }; 421 | ``` 422 | (示例取自`w1-gpio`叠加层) 423 | 424 | 请注意,甚至可以使用单个参数来定位不同类型的属性。您可以合理地将“启用”参数连接到`status`字符串,包含零或一的单元格以及适当的布尔属性。 425 | 426 | #### 2.2.7:更多叠加示例 427 | 428 | Raspberry Pi/Linux [GitHub存储库](https://github.com/raspberrypi/linux/tree/rpi-4.4.y/arch/arm/boot/dts/overlays)中托管着越来越多的叠加源文件。 429 | 430 | ## 第三部分:在树莓派中使用设备树 431 | 432 | ### 3.1:叠加层和config.txt 433 | 434 | 在Raspberry Pi上,加载程序(`start.elf`映像之一)的工作是将覆盖层与适当的基本设备树结合在一起,然后将完全解析的设备树传递给内核。基本设备树位于FAT分区中的`start.elf`旁边(从Linux启动),名为`bcm2708-rpi-b.dtb`,`bcm2708-rpi-b-plus.dtb`,`bcm2708-rpi-cm.dtb`和`bcm2709 -rpi-2-b.dtb`。请注意,模型A和A+将分别使用“ b”和“ b-plus”变体。此选择是自动的,并允许在各种设备中使用相同的SD卡映像。 435 | 436 | 请注意,DT和ATAG是互斥的。结果,将DT blob传递给不了解它的内核会导致启动失败。为了防止这种情况,加载程序会检查内核映像是否具有DT兼容性,这由`mkknlimg`实用程序添加的预告片标记;这可以在最近的内核源代码树的`scripts`目录中找到。假定没有预告片的任何内核均不具备DT功能。 437 | 438 | 如果没有DTB,则从rpi-4.4.y树(及更高版本)构建的内核将无法运行,因此从4.4版本开始,任何不带预告片的内核都被视为具有DT功能。您可以通过添加不带DTOK标志的预告片或通过将`device_tree=`放在config.txt中来覆盖它,但是如果它不起作用,请不要感到惊讶。 N.B.结果是,如果内核具有指示DT功能的预告片,则`device_tree=`将被忽略。 439 | 440 | 加载程序现在支持使用bcm2835_defconfig进行的构建,该版本选择上游的BCM2835支持。此配置将导致构建`bcm2835-rpi-b.dtb`和`bcm2835-rpi-b-plus.dtb`。如果这些文件是随内核一起复制的,并且内核已被最近的`mkknlimg`标记,则加载程序将默认尝试加载其中一个DTB。 441 | 442 | 为了管理设备树和覆盖,加载程序支持许多新的config.txt指令: 443 | 444 | ``` 445 | dtoverlay = acme-board 446 | dtparam = foo = bar,level = 42 447 | ``` 448 | 449 | 这将使加载程序在固件分区中查找`overlays/acme-board.dtbo`,Raspbian将其安装在/boot上。然后它将搜索参数`foo`和`level`,并为其指定指示的值。 450 | 451 | 加载程序还将搜索带有已编程EEPROM的附加HAT,并从那里加载支持的覆盖图。这种情况无需任何用户干预即可完成。 452 | 453 | 有几种方法可以表明内核正在使用设备树: 454 | 455 | 1. 在启动过程中,“Machine model:”内核消息具有特定于板的值,例如“Raspberry Pi 2 Model B”,而不是“ BCM2709”。 456 | 2. 一段时间后,可能还会有另一条内核消息显示“没有ATAG?”。 -这是预期的。 457 | 3. `/proc/device-tree`存在,并且包含与DT的节点和属性完全相同的子目录和文件。 458 | 459 | 使用设备树,内核将自动搜索并加载支持指定的已启用设备的模块。结果,通过为设备创建适当的DT覆盖,可以使设备用户免于编辑`/etc/ modules`的麻烦;所有配置都放在`config.txt`中,对于HAT,甚至不需要该步骤。但是请注意,诸如`i2c-dev`之类的分层模块仍需要显式加载。 460 | 461 | 不利的一面是,除非DTB要求,否则不会创建平台设备,因此不再需要将由于板支持代码中定义的平台设备而被加载的模块列入黑名单。实际上,当前的Raspbian图像没有黑名单文件。 462 | 463 | ### 3.2:DT参数 464 | 465 | 如上所述,DT参数是对设备配置进行小的更改的便捷方法。当前的基本DTB支持无需启用专用覆盖即可启用和控制板载音频,I2C,I2S和SPI接口的参数。在使用中,参数如下所示: 466 | 467 | ``` 468 | dtparam=audio=on,i2c_arm=on,i2c_arm_baudrate=400000,spi=on 469 | ``` 470 | 471 | 请注意,可以将多个分配放在同一行上,但请确保您不超过80个字符的限制。 472 | 473 | 将来的默认`config.txt`可能包含以下部分: 474 | 475 | ```sh 476 | # 取消注释部分或全部注释以启用可选的硬件接口 477 | #dtparam=i2c_arm=on 478 | #dtparam=i2s=on 479 | #dtparam=spi=on 480 | ``` 481 | 482 | 如果您有一个定义了一些参数的覆盖,则可以在后续行中指定它们,如下所示: 483 | 484 | ``` 485 | dtoverlay=lirc-rpi 486 | dtparam=gpio_out_pin=16 487 | dtparam=gpio_in_pin=17 488 | dtparam=gpio_in_pull=down 489 | ``` 490 | 491 | 或附加到叠加线,如下所示: 492 | 493 | ``` 494 | dtoverlay=lirc-rpi:gpio_out_pin=16,gpio_in_pin=17,gpio_in_pull=down 495 | ``` 496 | 497 | 请注意,此处使用冒号(`:`)将叠加层名称与其参数分开,这是受支持的语法变体。 498 | 499 | 覆盖参数仅在作用域中,直到加载下一个覆盖。如果覆盖层和基础都导出了具有相同名称的参数,则覆盖层中的参数优先;为了清楚起见,建议您避免这样做。要公开由基本DTB导出的参数,请使用以下命令结束当前覆盖范围: 500 | 501 | ``` 502 | dtoverlay= 503 | ``` 504 | 505 | ### 3.3:特定于电路板的标签和参数 506 | 507 | Raspberry Pi板具有两个I2C接口。它们名义上是分开的:一个用于ARM,一个用于VideoCore(“ GPU”)。在几乎所有型号上,`i2c1`都属于ARM,而`i2c0`属于VC,用于控制相机和读取HAT EEPROM。但是,模型B有两个早期版本,其作用相反。 508 | 509 | 为了使所有Pi都能使用一组叠加层和参数,固件会创建一些特定于板的DT参数。这些是: 510 | 511 | ``` 512 | i2c/i2c_arm 513 | i2c_vc 514 | i2c_baudrate/i2c_arm_baudrate 515 | i2c_vc_baudrate 516 | ``` 517 | 518 | 这些是`i2c0`,`i2c1`,`i2c0_baudrate`和`i2c1_baudrate`的别名。建议仅在确实需要时才使用`i2c_vc`和`i2c_vc_baudrate`,例如,如果您正在编程HAT EEPROM。启用`i2c_vc`可以停止检测到Pi摄像机。 519 | 520 | 对于编写覆盖图的人员,相同的别名已应用于I2C DT节点上的标签。因此,您应该写: 521 | 522 | ``` 523 | fragment@0 { 524 | target = <&i2c_arm>; 525 | __overlay__ { 526 | status = "okay"; 527 | }; 528 | }; 529 | ``` 530 | 531 | 使用数字变体的所有叠加层都将被修改为使用新的别名。 532 | 533 | ### 3.4:HAT和设备树 534 | 535 | Raspberry Pi HAT是用于带有嵌入式EEPROM的“Plus”形(A +,B +或Pi 2 B)Raspberry Pi的附加板。 EEPROM包含使能电路板所需的任何DT覆盖层,并且该覆盖层还可以暴露参数。 536 | 537 | HAT覆盖由固件在基本DTB之后自动加载,因此在加载任何其他覆盖或使用`dtoverlay=`终止覆盖范围之前,可以访问其参数。如果出于某种原因要抑制HAT叠加层的加载,请将`dtoverlay=`放在任何其他`dtoverlay`或`dtparam`指令之前。 538 | 539 | ### 3.5:动态设备树 540 | 541 | 从Linux 4.4开始,RPi内核支持动态加载覆盖和参数。兼容的内核管理一叠叠加层,这些叠加层应用在基本DTB的顶部。更改会立即反映在`/proc/device-tree`中,并且可能导致模块加载以及平台设备被创建和销毁。 542 | 543 | 上面的“堆栈”一词很重要-叠加层只能在堆栈的顶部添加和删除;更改堆栈下方的内容需要首先除去其顶部的所有内容。 544 | 545 | 有一些用于管理覆盖图的新命令: 546 | 547 | #### 3.5.1 dtoverlay命令 548 | 549 | `dtoverlay`是一个命令行实用程序,可在系统运行时加载和删除覆盖,以及列出可用的覆盖并显示其帮助信息: 550 | 551 | ```sh 552 | pi@raspberrypi ~ $ dtoverlay -h 553 | Usage: 554 | dtoverlay [=...] 555 | 添加叠加层(带有参数) 556 | dtoverlay -r [] 删除覆盖(按名称,索引或最后一个) 557 | dtoverlay -R [] 从叠加层中删除(按名称,索引或全部) 558 | dtoverlay -l 列出活动的叠加层/参数 559 | dtoverlay -a 列出所有叠加层(标记为活动的) 560 | dtoverlay -h 显示此用法消息 561 | dtoverlay -h 在叠加层上显示帮助 562 | dtoverlay -h .. 或其参数 563 |   其中是dtparams的覆盖名称或'dtparam' 564 | 适用于大多数变体的选项: 565 | -d 指定overlays的替代位置 566 | (默认为/boot/overlays or /flash/overlays) 567 | -n 空运行 - 显示将执行的操作 568 | -v 详细操作 569 | ``` 570 | 571 | 与`config.txt`等效项不同,覆盖层的所有参数都必须包含在同一命令行中-`dtparam`命令仅适用于基本DTB的参数。 572 | 573 | 需要注意两点: 574 | 575 | 更改内核状态(添加和删除内容)的命令变体需要root特权,因此您可能需要在命令前加上`sudo`前缀。 576 | 577 | 只能卸载在运行时应用的覆盖和参数-固件应用的覆盖或参数被“烘焙”,因此`dtoverlay`不会列出它,也无法将其删除。 578 | 579 | #### 3.5.2 dtparam命令 580 | 581 | `dtparam`创建的覆盖与在`config.txt`中使用dtparam指令具有相同的效果。在用法上,它与覆盖名称为 `-` 的`dtoverlay`等效,但有一些小区别: 582 | 583 | 1. `dtparam`将列出基本DTB的所有已知参数的帮助信息。仍然可以使用`dtparam -h`获得有关dtparam命令的帮助。 584 | 2. 当指示要删除的参数时,只能使用索引号(不能使用名称)。 585 | 586 | #### 3.5.3 编写具有运行时能力的叠加层的准则 587 | 588 | 该区域的文档很少,但是这里有一些累积的技巧: 589 | 590 | - 设备对象的创建或删除由添加或删除的节点触发,或者由节点的状态从禁用变为启用,反之亦然。当心-缺少“状态”属性意味着该节点已启用。 591 | 592 | - 不要在片段内创建将覆盖基本DTB中现有节点的节点-内核将重命名新节点以使其唯一。如果要更改现有节点的属性,请创建一个针对它的片段。 593 | 594 | - ALSA不会阻止其编解码器和其他组件在使用时被卸载。如果删除覆盖,则删除声卡仍在使用的编解码器会导致内核异常。实验发现,设备会按照覆盖中的片段顺序相反的顺序删除,因此将卡的节点放置在组件的节点之后可以有序关闭。 595 | 596 | #### 3.5.4 注意事项 597 | 598 | 在运行时加载叠加层是内核的新增功能,到目前为止,尚无从用户空间执行此操作的方法。通过在命令后隐藏此机制的详细信息,目的是在其他内核接口标准化的情况下,使用户免受更改的影响。 599 | 600 | - 一些覆盖在运行时比其他覆盖要好。设备树的某些部分仅在引导时使用-使用覆盖更改它们不会有任何影响。 601 | 602 | - 应用或删除某些覆盖可能会导致意外的行为,因此应谨慎操作。这是它需要`sudo`的原因之一。 603 | 604 | - 如果某些东西正在使用ALSA,则卸载ALSA卡的覆盖层可能会停顿-LXPanel音量滑块插件演示了这种效果。为了能够删除声卡的叠加层,`lxpanelctl`实用程序提供了两个新选项 - `alsastop`和`alsastart` - 分别在加载或卸载叠加层之前和之后从辅助脚本dtoverlay-pre和dtoverlay-post调用了这些选项。 605 | 606 | - 删除覆盖不会导致已加载的模块被卸载,但是可能导致某些模块的引用计数降至零。两次运行`rmmod -a`将导致卸载未使用的模块。 607 | 608 | - 覆盖层必须以相反的顺序除去。这些命令将允许您删除一个较早的命令,但是所有中间的命令将被删除并重新应用,这可能会带来意想不到的后果。 609 | 610 | - 在运行时在`/clocks`节点下添加时钟不会导致注册新的时钟提供程序,因此`devm_clk_get`对于覆盖中创建的时钟将失败。 611 | 612 | ### 3.6:支持的叠加层和参数 613 | 614 | 由于在此处记录单个覆盖图非常耗时,因此请参阅`/boot/overlays`中覆盖图`.dtbo`文件旁边的[README](https://github.com/raspberrypi/firmware/blob/master/boot/overlays/README)。它通过添加和更改保持最新状态。 615 | 616 | ## 第4部分:故障排除和专业提示 617 | 618 | ### 4.1:调试 619 | 620 | 加载程序将跳过缺少的覆盖和错误的参数,但是如果存在严重错误,例如基本DTB丢失或损坏或覆盖合并失败,则加载程序将退回到非DT引导。如果发生这种情况,或者您的设置不符合预期,则值得检查加载程序中的警告或错误: 621 | 622 | ```sh 623 | sudo vcdbg log msg 624 | ``` 625 | 626 | 通过将`dtdebug=1`添加到`config.txt`可以启用额外的调试。 627 | 628 | 如果内核无法在DT模式下启动,**则可能是因为内核映像没有有效的尾部**。使用`knlinfo`检查一个,然后使用`mkknlimg`实用程序添加一个。这两个实用程序都包含在当前Raspberry Pi内核源代码树的`scripts`目录中。 629 | 630 | 您可以像这样创建人类可读的DT当前状态的表示形式: 631 | 632 | ``` 633 | dtc -I fs /proc/device-tree 634 | ``` 635 | 636 | 这对于查看将覆盖层合并到基础树上的效果很有用。 637 | 638 | 如果内核模块未按预期加载,请检查`/etc/modprobe.d/raspi-blacklist.conf`中是否未将其列入黑名单;使用设备树时,不必将其列入黑名单。如果没有任何问题,您还可以通过在`/lib/modules//modules.alias`中搜索`compatible`值来检查模块是否导出了正确的别名。否则,您的驱动程序可能丢失: 639 | 640 | ``` 641 | .of_match_table = xxx_of_match, 642 | ``` 643 | 644 | 要么: 645 | 646 | ``` 647 | MODULE_DEVICE_TABLE(of, xxx_of_match); 648 | ``` 649 | 650 | 如果失败,则`depmod`失败或在目标文件系统上未安装更新的模块。 651 | 652 | ### 4.2:使用dtmerge和dtdiff测试覆盖 653 | 654 | `dtoverlay`和`dtparam`命令旁边是一个实用程序,用于将覆盖图应用于DTB - `dtmerge`。要使用它,您首先需要获取基本的DTB,可以通过以下两种方式之一获得它: 655 | 656 | a)从`/proc/device-tree`中的实时DT状态生成它: 657 | 658 | ``` 659 | dtc -I fs -O dtb -o base.dtb /proc/device-tree 660 | ``` 661 | 662 | 这将包括您到目前为止已应用的所有叠加层和参数,无论是在`config.txt`中还是通过在运行时加载它们,都可能是您想要的,也可能不是。或者... 663 | 664 | b)从/boot中的源DTB复制它。这将不包括覆盖和参数,但也将不包括固件的任何其他修改。为了允许测试所有覆盖,`dtmerge`实用程序将创建一些特定于板的别名(“ i2c_arm”等),但这意味着合并的结果将包括与原始DTB相比更多的差异。解决方案是使用dtmerge进行复制: 665 | 666 | ``` 667 | dtmerge /boot/bcm2710-rpi-3-b.dtb base.dtb- 668 | ``` 669 | 670 | (`"-"`表示缺少覆盖名称)。 671 | 672 | 现在,您可以尝试应用叠加层或参数: 673 | 674 | ``` 675 | dtmerge base.dtb merged.dtb - sd_overclock=62 676 | dtdiff base.dtb merged.dtb 677 | ``` 678 | 679 | 它将返回: 680 | 681 | ``` 682 | --- /dev/fd/63 2016-05-16 14:48:26.396024813 +0100 683 | +++ /dev/fd/62 2016-05-16 14:48:26.396024813 +0100 684 | @@ -594,7 +594,7 @@ 685 | }; 686 | 687 | sdhost@7e202000 { 688 | - brcm,overclock-50 = <0x0>; 689 | + brcm,overclock-50 = <0x3e>; 690 | brcm,pio-limit = <0x1>; 691 | bus-width = <0x4>; 692 | clocks = <0x8>; 693 | ``` 694 | 695 | 您还可以比较不同的叠加层或参数。 696 | 697 | ``` 698 | dtmerge base.dtb merged1.dtb /boot/overlays/spi1-1cs.dtbo 699 | dtmerge base.dtb merged2.dtb /boot/overlays/spi1-2cs.dtbo 700 | dtdiff merged1.dtb merged2.dtb 701 | ``` 702 | 703 | 要得到: 704 | 705 | ``` 706 | --- /dev/fd/63 2016-05-16 14:18:56.189634286 +0100 707 | +++ /dev/fd/62 2016-05-16 14:18:56.189634286 +0100 708 | @@ -453,7 +453,7 @@ 709 | 710 | spi1_cs_pins { 711 | brcm,function = <0x1>; 712 | - brcm,pins = <0x12>; 713 | + brcm,pins = <0x12 0x11>; 714 | phandle = <0x3e>; 715 | }; 716 | 717 | @@ -725,7 +725,7 @@ 718 | #size-cells = <0x0>; 719 | clocks = <0x13 0x1>; 720 | compatible = "brcm,bcm2835-aux-spi"; 721 | - cs-gpios = <0xc 0x12 0x1>; 722 | + cs-gpios = <0xc 0x12 0x1 0xc 0x11 0x1>; 723 | interrupts = <0x1 0x1d>; 724 | linux,phandle = <0x30>; 725 | phandle = <0x30>; 726 | @@ -743,6 +743,16 @@ 727 | spi-max-frequency = <0x7a120>; 728 | status = "okay"; 729 | }; 730 | + 731 | + spidev@1 { 732 | + #address-cells = <0x1>; 733 | + #size-cells = <0x0>; 734 | + compatible = "spidev"; 735 | + phandle = <0x41>; 736 | + reg = <0x1>; 737 | + spi-max-frequency = <0x7a120>; 738 | + status = "okay"; 739 | + }; 740 | }; 741 | 742 | spi@7e2150C0 { 743 | ``` 744 | 745 | ### 4.3:强制特定的设备树 746 | 747 | 如果您有默认DTB不支持的非常特殊的需求(尤其是人们尝试ARCH_BCM2835项目使用的纯DT方法),或者您只是想尝试编写自己的DT,您可以告诉加载程序以加载备用DTB文件,如下所示: 748 | 749 | ``` 750 | device_tree=my-pi.dtb 751 | ``` 752 | 753 | ### 4.4:禁用设备树用法 754 | 755 | 由于切换到4.4内核并使用更多的上游驱动程序,因此在Pi内核中需要使用设备树。禁用DT使用的方法是添加: 756 | 757 | ``` 758 | device_tree= 759 | ``` 760 | 761 | 到`config.txt`。但是,如果内核具有指示DT功能的`mkknlimg`预告片,则该指令将被忽略。 762 | 763 | ### 4.5:快捷方式和语法变体 764 | 765 | 加载程序了解一些快捷方式: 766 | 767 | ``` 768 | dtparam=i2c_arm=on 769 | dtparam=i2s=on 770 | ``` 771 | 772 | 可以缩短为: 773 | 774 | ``` 775 | dtparam=i2c_arm=on 776 | dtparam=i2s=on 777 | ``` 778 | 779 | (`i2c`是`i2c_arm`的别名,并且假定`=on`)。它还仍然接受以下较长版本:`device_tree_overlay`和`device_tree_param`。 780 | 781 | 加载程序曾经接受使用空格和冒号作为分隔符,但是为简单起见,已不再支持它们,因此可以在不带引号的参数值中使用它们。 782 | 783 | ### 4.6:其他DT命令可在config.txt中使用 784 | 785 | `device_tree_address`用于覆盖固件加载设备树的地址(不是dt-blob)。默认情况下,固件将选择合适的位置。 786 | 787 | `device_tree_end`这为加载的设备树设置了(独占)限制。默认情况下,设备树可以增长到可用内存的末尾,几乎可以肯定这是必需的。 788 | 789 | `dtdebug`如果非零,请为固件的设备树处理打开一些额外的日志记录。 790 | 791 | `enable_uart`启用主/控制台UART(Pi 3上为ttyS0,否则为ttyAMA0-除非与诸如pi3-miniuart-bt的覆盖层交换)。如果主UART是ttyAMA0,则enable_uart默认为1(启用),否则默认为0(禁用)。这是因为必须停止更改ttyS0不可用的核心频率,因此`enable_uart=1`意味着core_freq = 250(除非force_turbo = 1)。在某些情况下,这会影响性能,因此默认情况下处于禁用状态。有关UART的更多详细信息,请参见[此处](https://www.raspberrypi.org/documentation/configuration/uart.md) 792 | 793 | `overlay_prefix`指定要从中加载覆盖的子目录/前缀-默认为“overlays/”。注意尾随“/”。如果需要,您可以在最后的“/”之后添加一些内容,以便为每个文件添加一个前缀,尽管这不是必需的。 794 | 795 | DT可以控制其他端口,有关更多详细信息,请参阅第3部分。 -------------------------------------------------------------------------------- /document/rpi_SCH_3bplus_1p0_reduced.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Philon/rpi-drivers/d1c55d06ef4bc42ff8dee4c4a9962266a36e752c/document/rpi_SCH_3bplus_1p0_reduced.pdf -------------------------------------------------------------------------------- /rules.mk: -------------------------------------------------------------------------------- 1 | # 指定交叉编译工具链前缀 2 | CROSS_COMPILE ?= /opt/arm-mac-linux-gnueabihf/bin/arm-mac-linux-gnueabihf- 3 | 4 | # 指定内核源码目录 5 | KDIR ?= ../linux-rpi-4.19.y 6 | 7 | # 指定安装路径(通过scp命令远程拷贝,即scp :) 8 | INSTALL_PATH ?= rpi.local:~/modules 9 | 10 | # 检测当前目录下所有测试用例 11 | TESTS ?= $(patsubst %.c, %, $(wildcard *_test.c)) 12 | 13 | # 检测当前目录下所有设备树 14 | DTBOS ?= $(patsubst %.dts, %.dtbo, $(wildcard *.dts)) 15 | 16 | # 添加安装文件 17 | INSTALL_FILES += $(obj-m:%.o=%.ko) 18 | INSTALL_FILES += $(DTBOS) 19 | INSTALL_FILES += $(TESTS) 20 | 21 | export CC=$(CROSS_COMPILE)gcc 22 | export CROSS_COMPILE 23 | export ARCH=arm 24 | 25 | # 各种构建规则 26 | .PHONY: all modules tests install clean 27 | 28 | all: modules tests install 29 | 30 | modules: $(DTBOS) 31 | make -C $(KDIR) M=$(PWD) modules 32 | 33 | tests: $(TESTS) 34 | 35 | install: $(INSTALL_FILES) 36 | scp -o ConnectTimeout=5 $^ $(INSTALL_PATH) 37 | 38 | clean: 39 | rm -f $(TESTS) $(TESTS:%=%.o) $(DTBOS) 40 | make -C $(KDIR) M=$(PWD) clean 41 | 42 | %_test:%_test.o 43 | $(CC) $(LDFLAGS) -o $@ $< 44 | %.o:%.c 45 | $(CC) $(CFLAGS) -c $< -o $@ 46 | %.dtbo:%.dts 47 | $(KDIR)/scripts/dtc/dtc -I dts -o $@ $< --------------------------------------------------------------------------------