本章主要目的是搭建驱动章节的实验环境,方便后续章节不在实验环境上耗费太多版面,而是主要讲解设备驱动的原理。当进行实验的时候,不清楚具体细节时再跳转回来看。
首先我们要明白程序最终是运行在开发板上,我们开发板主要使用STM32MP1 系列处理器,STM32MP1 系列基于单核或双核Cortex-A7(ARM 的一种高能效处理器架构) 内核与Cortex-M4(传统微控制器架构) 组成的异构架构。开发板上已经移植好相关的环境,我们只需要将我们写的代码交叉编译成arm 架构下的可执行文件。
设备驱动是具有独立功能的程序,它可以被单独编译,但不能独立运行,在运行时它被链接到内核作为内核的一部分在内核空间运行。也因此想要我们写的内核模块在某个版本的内核上运行,那么就必须在该内核版本上编译它,如果我们编译的内核与我们运行的内核具备不相同的特性,设备驱动则可能无法运行。
首先我们需要知道内核版本,并准备好该版本的内核源码,使用交叉编译工具编译内核源码;其次,依赖编译的内核源码编译我们的驱动模块以及设备树文件。最终将驱动模块和设备树拷贝到开发板上运行。
重要: 本小节内容涉及的知识点非常多,需要有一定的基础才可理解相关内容。若不理解部分知识点,跳过即可,接触到相关知识可再回头学习。
本小节知识点是对linux 内核所支持的特性(比如设备树、设备树插件),再结合实例演示的,不针对某一款特定的开发板!大家不必纠结开发板环境。
准备对应内核环境
既然是编译内核模块,那么这项工作的开始,自然就是构建一遍完整的内核。当内核能够完整构建之后,我们构建内核模块的操作才可以顺利进行。
安装工具
在编译源码之前我们需要先准备好交叉编译的环境,安装必要的依赖和工具
- gcc-arm-linux-gnueabihf 交叉编译器
- bison 语法分析器
- flex 词法分析器
- libssl-dev OpenSSL 通用库
- lzop LZO 压缩库的压缩软件
执行下面的命令即可:
sudo apt install make gcc-arm-linux-gnueabihf gcc bison flex libssl-dev dpkg-dev lzop
编译内核
获取内核源码
开发板内核使用Linux npi 4.19.94-stm-r1 版本,可以使用命令uname -a 查看。
我们可以从github 克隆野火官方提供的Debian 镜像内核源码,当然首先需要安装git 工具。
git 工具安装命令如下:
sudo apt -y install git
我们可以从如下两个地址中的任意一个,开始执行克隆命令,命令如下(注:命令会将仓库clone到命令执行的目录下):
github:
git clone -b ebf_4.19_star https://github.com/Embedfire/ebf_linux_kernel.git
gitee:
git clone https://gitee.com/Embedfire/ebf_linux_kernel_mp157_depth1
提示:因为网络原因从githu 克隆困难的用户请使用下面gitee 的地址,gitee 仓库master 分支作为github 仓库ebf_4.19_star 分支的只保存了最新提交内容(git clone 使用–depth=1 参数),可以直接使用,如果需要查看历史提交差异内容可以从github 网页里面访问。
进行编译
我们可以单独新建一个工作目录,将内核源码放置在该目录下,切换到内核源码目录,我们可以找到make_deb.sh 脚本,里面有配置好的参数,只需要执行脚本便可编译内核。
编译出来的内核相关文件存放位置,由脚本make_deb.sh 中build_opts=“${build_opts}O=build_image/buil” 指定。示例源码指定编译好的内核存放在build_image/build,此处建议不修改此目录,方便后面编译驱动模块。
执行如下命令即可开始构建内核:
./make_deb.sh
构建过程此处我们忽略。
接下来我们不妨简单了解下构建内核的脚本make_deb.sh
make_deb.sh 脚本
列表1: make_deb.sh(内核源码/make_deb.sh)
deb_distro=bionic
DISTRO=stable
build_opts="-j 6"
build_opts="${build_opts} O=build_image/build"
build_opts="${build_opts} ARCH=arm"
build_opts="${build_opts} KBUILD_DEBARCH=${DEBARCH}"
build_opts="${build_opts} LOCALVERSION=-stm-r1"
build_opts="${build_opts} KDEB_CHANGELOG_DIST=${deb_distro}"
build_opts="${build_opts} KDEB_PKGVERSION=1${DISTRO}"
build_opts="${build_opts} CROSS_COMPILE=arm-linux-gnueabihf-"
build_opts="${build_opts} KDEB_SOURCENAME=linux-upstream"
make ${build_opts} stm32mp157_ebf_defconfig
#make ${build_opts} menuconfig
make ${build_opts}
make ${build_opts} bindeb-pkg
- 第4 行:指定编译好的内核放置位置
- 第5 行:编译出来的目标是针对ARM 体系结构的内核
- 第6 行:对于deb-pkg 目标,允许覆盖deb-pkg 部署的常规启发式
- 第7 行:使用内核配置选项“LOCALVERSION”为常规内核版本附加一个唯一的后缀。
- 第10 行:指定交叉编译器
- 第12 行:生成配置文件
- 第15 行:编译文件进行打包
更多内核编译可选的参数,可以参考linux 内核官方网址:linux 内核编译参数介绍。
构建好内核之后,我们可以进行下面的内容学习了。
如何编译和加载内核驱动模块
对于linux 内核,可以通过两种方式将内核模块添加到内核中:
- 将内核模块编译成内核模块文件,在内核启动后由用户手动动态加载
- 将模块直接编译到内核中去,内核启动时自动加载。
我们着重第一种方式的讲解。
首先我们需要,获取内核驱动模块示例源码。
github:
git clone 待添加
gitee:
git clone 待添加
获取到源码仓库后,将配套驱动程序代码放置到内核代码同级目录,原因是编译内核驱动模块的时候,驱动程序需要依赖编译好的Linux 内核。具体信息可参考驱动模块中的Makefile 文件内容。
图片中演示的实验代码位于:linux_driver/module/hellomodule这里不做代码讲解,具体原理请参考内核模块章节及后述的内容。
在内核源码外编译
内核驱动模块对象所需的构建步骤和编译很复杂,它利用了linux 内核构建系统的强大功能,当然我们不需要深入了解这部分知识,利用简单的Make 工具就能编译出我们想要的内核驱动模块。
cd hellomodule
make
重要: 该目录下的Makefile 中指定的目录“KERNEL_DIR=…/ebf_linux_kernel/build_image/build”要与前面编译的内核所在目录一致。
切换到module/hellomodule 目录下,直接执行make 命令,即可编译程序。
列表2: Makefile(module/hellomodule/Makefile)
KERNEL_DIR=../../ebf_linux_kernel/build_image/build
ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH CROSS_COMPILE
obj-m := hellomodule.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONE:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
- 第1 行:指定编译内核存放位置
- 第2 行:针对ARM 体系结构
- 第3 行:指定交叉编译工具链
- 第4 行:导入环境变量
- 第6 行:表示以模块编译
- 第8 行: all 只是个标号,可以自己定义,是make 的默认执行目标。
- 第9 行: $ (MAKE):MAKE 是Makefile 中的宏变量,要引用宏变量要使用符号。这里实际上就是指向make程序,所以这里也可以把$ (MAKE) 换成make.-C: 是make 命令的一个 选项,-C 作用是changedirectory. -C dir 就是转到dir 目录。M=$(CURDIR):返回当前目录。这句话的意思是:当make 执行默认的目标all 时,-C(KVDIR) 指明跳转到内核源码目录下去执行那里的Makefile,-C $(KERNEL_DIR)指明跳转到内核源码目录下去执行那里的Makefile,M=(CURDIR) 表示又返回到当前目录来执行当前的Makefile.
- 第11 行: clean 就是删除后面这些由make 生成的文件。
查看module/hellomodule/文件夹,新增hellomodule.ko,这就是我们自己编写、编译的内核驱动模块。
和内核源码一起编译
- 待完善
加载内核驱动模块
编译好内核驱动模块,可以通过多种方式将hellomodule.ko 拷贝到开发板,我们这里主要使用NFS网络文件系统或者SCP 命令。
NFS 环境请搭建请参考Linux 系列章节之挂载NFS 网络文件系统章节。
scp 命令用于Linux 之间复制文件和目录,scp 命令格式如下:
scp local_file remote_username@remote_ip:remote_folder
例如:
scp hellomodule.ko debian@192.168.0.2:/home/debian/
将hellomodule.ko 发送到192.168.0.2 这个IP 的Linux(这里是我的开发板IP) 的/home/debian/目录下,开发板用户名为debian,输入yes,然后验证密码,等待传输完成。这个时候我们开发板就有了hellomodule.ko 这个文件。
安装卸载内核驱动模块使用insmod 和rmmod,前面章节有对这两个工具的详细介绍,这里不做展开。
sudo insmod hellomodule.ko
sudo rmmod hellomodule.ko
如何编译和加载设备树
Linux3.x 以后的版本才引入了设备树,设备树用于描述一个硬件平台的板级细节。后面我们写的驱动需要依赖设备树,所以在这里先介绍如何编译设备树、加载设备树。
这里不做代码讲解,具体原理请参考Linux 设备树章节
设备树编译
内核编译设备树的方式
编译内核的时候会生成的dtc 工具,内核会使用dtc 工具去自动编译设备树。
dtc 工具使用示例如下:
# 编译dts 为dtb
内核目录/scripts/dtc/dtc -I dts -O dtb -o xxx.dtb xxx.dts
实际使用示例,此处为伪代码,仅供参考使用,了解即可:
内核目录/build_image/build/scripts/dtc/dtc -I dts -O dtb -o fire-dtb.dtb fire-dts.dts
内核使用dtc 工具的命令大致如上所示,实际上设备树中有非常多的依赖关系,这些依赖关系通过Makefile 文件去处理,所以一般情况下,设备树不仅仅只是通过一个dtc 命令就能将编译出来的。
我们可以尝试着通过内核的构建脚本去编译设备树,我们所要用到的设备树文件都存放在内核源码/arch/arm/boot/dts/ 里面。
前面提到了编译内核时会自动去编译设备树,但是编译内核很耗时,所以我们推荐使用如下命令只编译设备树。
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157_ebf_defconfig
make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs
如果在内核源码中执行了make distclean 则必须执行第一条命令,它用于生成默认配置文件,如果执行过一次就没有必要再次执行,当然再次执行也没有什么问题。第二条命令开始编译设备树,参数“-j4”指定多少个线程编译,根据自己电脑实际情况设置,越大编译越快,当然也可以不设置,设备树编译本来就很快。
编译成功后生成的设备树文件(.dtb)位于源码目录下的内核源码/arch/arm/boot/dts ,开发板适配的设备树文件名arch/arm/boot/dts/stm32mp157a-basic.dtb。
加载设备树
设备树加载方法
替换设备树有下面几种方法。
-
第一种,将我们编译好的设备树或者设备树插件替换掉开发板里面原有的。
-
第二种,设备树可以编译到内核中的,所以重新烧写内核这种方式肯定可行。但是烧写内核比较麻烦,可以参考制作系统镜像系列章节。不推荐也不做过多的讲解。
我们只介绍第一种,将编译好的新设备树文件,替换开发板/boot/dtbs/ 目录下的旧设备树文件即可。
检查设备树加载情况
假设我们在原来的设备树上添加了新的节点,led_test, 在该节点下有一个设备为rgb_led_red,我们可以通过以下的方式加载并查看新的设备树是否生效了,新节点是否添加。
通过SCP 或NFS 将刚才编译的设备树拷贝到开发板上。替换开发板/boot/dtbs/stm32mp157a-basic.dtb 。
uboot 在启动的时候负责该目录的设备文件加载到内存,供内核解析使用。重启开发板即可。
设备树中的设备树节点在文件系统中有与之对应的文件,位于“/proc/device-tree”目录。查看“/proc/device-tree”目录如下所示。
接着进入led 文件夹,可以发现led 节点中定义的属性以及它的子节点,如下所示。
在节点属性中多了一个name,我们在led 节点中并没有定义name 属性,这是自动生成的,保存节点名。
这里的属性是一个文件,而子节点是一个文件夹,我们再次进入“rgb_led_red@0x50002000”文件夹。里面有compatible、name、reg、status 四个属性文件。我们可以使用“cat”命令查看这些属性文件,如下所示。
至此,设备树加载成功。
如何编译和加载设备树插件
Linux4.4 以后引入了动态设备树(Dynamic DeviceTree)。设备树插件可以被动态的加载到系统中,供被内核识别。
注意设备树插件和设备树不是互相替代的关系,而是互补的关系。设备树插件可以在主设备树定型的情况下,再对主设备树未描述的功能进行动态的拓展。比如A 板的设备树没有开启串口1 的功能,但B 板需要开启串口1 的功能,那么可以直接沿用A 板的设备树,并用设备树插件拓展出串口1,满足B 板的需求。
在内核编译设备树插件
设备树插件与设备树一样都是使用DTC 工具编译,只不过设备树编译为.dtb。而设备树插件需要编译为.dtbo。我们可以使用DTC 编译命令编译生成.dtbo,但是这样比较繁琐、容易出错。
我们将设备树插件dtbo 的编译工作也放在了内核编译时来完成,当然我们单独编译设备树的时候也是可以编译出设备树插件dtbo 的。
我们在内核中提供了大量的设备树插件,野火的开发板许多外设硬件描述都是以dtbo 插件的形式提供的。这样使用起来非常灵活。
如上图,当大家尝试写设备树插件的时候,可以将自己的设备树插件添加到:arch/arm/boot/dts/overlays 目录下,并修改arch/arm/boot/dts/overlays/Makefile 文件,添加编译选项,形如stm-fire-qspi.dtbo 的添加形式。将stm-fireqspi.dtbo 追加到stm-fire-spi4.dtbo 后面即可。
添加好后,可以执行设备树的编译命令,设备树插件的编译也会同步完成。
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157_ebf_defconfig
make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs
可以看到,编译输出时,dtbo 文件也有对应的打印信息。
内核编译设备树插件过程
编译设备树插件和编译设备树类似,这里介绍内核中的dtc 工具编译编译设备树插件的过程。
内核中将xxx.dts 编译为xxx.dtbo 的过程示例,仅供参考:
内核构建目录/scripts/dtc/dtc -I dts -O dtb -o xxx.dtbo xxx.dts
例如,将fire-rgb-led-overlay.dts 编译为rgb.dtbo
../ebf_linux_kernel/build_image/build/scripts/dtc/dtc -I dts -O dtb -o rgb.→dtbo fire-rgb-led-overlay.dts
编译好的设备树插件为rgb.dtbo。
当然和编译设备树一样,设备树插件的编译也涉及到依赖关系,所以编译过程也比较复杂。不仅仅是使用一条命令就可以完成编译的。
加载设备树插件
uboot 加载(适用野火linux 开发板)
- 可以通过SCP 或NFS 将.dtbo设备树插件拷贝到开发板/usr/lib/linux-image-4.19.94-stm-r1/overlays/上,所以下面操作都在开发板上进行。
如下所示:
- 将对应的设备树插件加载配置,写入uEnv.txt 配置文件,系统启动过程中会自动从uEnv.txt 读取要加载的设备树插件,打开位于“/boot”目录下的uEnv.txt 文件,如下所示:
要将设备树插件写入uEnv.txt 也很简单,参照着红框内容写即可。书写格式为“dtoverlay=< 设备树插件路径>”。
从上图中可以看出在uEnv.txt 文件夹下有很多被屏蔽的设备树插件,这些设备树插件是烧写系统时自带的插件,为避免它们干扰我们的实验,这里可以把它们全部屏蔽掉。
修改完成后保存、退出。执行reboot 命令重启系统。重启后正常情况下我们可以在“/proc/devicetree”找与插入的设备节点同名的文件夹。
在“/proc/device-tree”目录下找到与插入的设备树节点同名的文件夹,进入该文件夹还可以看到该节点拥有的属性以及它的子节点,如下所示。
看到这些文件,证明已经加载成功了。
参考资料:嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列