1. 驱动章节实验环境搭建

本章的主要目的是搭建驱动章节所需的实验环境,后续章节将不在实验环境搭建上耗费太多的版面, 而是主要讲解设备驱动的原理。本小节内容涉及的知识点较多,需要有一定的内核基础才可理解相关内容,若不理解部分知识点,跳过即可,可等到接触到相关知识再回头学习。

首先我们要明白程序最终是运行在板卡上,可以在板卡上编译或者在pc上使用交叉编译器进行编译, 需要下载内核源码或对应内核的头文件(Kernel Headers),之后编译源码、编译驱动模块以及设备树等,最终将驱动模块和设备树拷贝到开发板上运行。

另外,驱动模块是具有独立功能的程序,它可以被单独编译,但不能独立运行, 在运行时它被链接到内核作为内核的一部分在内核空间运行。也因此想要我们写的内核模块在某个版本的内核上运行,那么就必须在该内核版本上编译它。

提示

本章节不需要烧录我们编译的内核,编译内核可以在板卡上或者个人PC上,编译内核是为了辅助编译驱动程序。 另外,也可以编译出内核头文件deb包,安装后同样可以编译驱动模块。如果有更新替换内核的需求请参考 镜像构建与部署 系列章节。

1.1. 搭建编译环境

编译内核可以在pc虚拟机上进行交叉编译也在板卡上进行本地编译。由于pc虚拟机性能远远强于板卡,一般建议在虚拟机上编译,如果不想用虚拟机,希望直接在板卡上开发,不介意编译时间过长也可以在板卡上编译。

在PC上,可以使用VirtualBox或者VMWare,搭建ubuntu虚拟机,建议使用Ubuntu18.04或者ubuntu20.04版本,详细搭建请参考 LubanCat-RK系列板卡应用开发手册 系列章节。

安装相关库和工具搭建编译环境,执行以下命令:

sudo apt update
sudo apt install gcc make  git  bc libssl-dev liblz4-tool device-tree-compiler bison flex u-boot-tools gcc-aarch64-linux-gnu

1.2. 获取内核源码

板卡使用的内核版本,可以使用命令 uname -a 查看。获取内核源码,建议直接git克隆野火官方提供的内核源码,或者下载Lubancat-SDK源码,SDK源码中包含内核源码。

1.2.1. 直接克隆野火官方提供的内核源码

RK356x系列板卡用户执行以下命令获取内核源码:

#-b参数指定stable-4.19-rk356x分支
git clone -b stable-4.19-rk356x https://github.com/LubanCat/kernel.git

RK3588系列板卡用户执行以下命令获取内核源码:

#-b参数指定develop-5.10分支
git clone -b develop-5.10 https://github.com/LubanCat/kernel.git

1.2.2. 通过SDK获取内核源码

访问百度网盘资源介绍页面获取SDK源码压缩包:资料网盘/8-SDK源码压缩包

根据鲁班猫板卡的型号下载对应版本的最近日期的压缩包即可。

重要

由于源码压缩包体积很大,仅在有大量修改的稳定版本时更新,其发布日期可能和镜像发布日期无法对应。当我们在本地解压压缩包以后,只需要借助Github做少量更新,即可同步到最新版本。

以下过程以LubanCat_RK356x_Linux_SDK进行演示,实际文件名称以自己下载的SDK为准。

# 安装7z压缩工具
sudo apt install p7zip-full

# 在用户家目录创建LubanCat_SDK目录
mkdir ~/LubanCat_SDK

# 将下载的SDK源码移动到LubanCat_SDK目录下,xxx为日期
mv LubanCat_RK356x_Linux_SDK_xxx.7z ~/LubanCat_SDK

# 进入LubanCat_SDK目录
cd ~/LubanCat_SDK

# 解压SDK压缩包
7z x LubanCat_RK356x_Linux_SDK_xxx.7z

# 检出.repo目录下的git仓库
.repo/repo/repo sync -l

#进入内核目录
cd ~/LubanCat_SDK/kernel

#更新内核
git pull

#也可以将SDK全部更新
#将所有的源码仓库同步到最新版本
cd ~/LubanCat_SDK/ && .repo/repo/repo sync -c

注意

如果git pull或者repo sync -c执行时提示网络连接超时,请检查并能否通畅访问github。 确认可以正常访问github的话,可以重复多次执行git pull或者repo sync -c命令来进行同步。 若无法访问github,可以忽略同步源码仓库到最新版本这一步骤。

1.3. 编译内核

1.3.1. 在PC上交叉编译内核(建议)

按照前面小节搭建编译环境并下载源码之后,进入内核源码根目录,根据具体的板卡设置配置文件。

RK356x系列板卡用户执行以下命令编译内核源码:

#清除之前生成的所有文件和配置
make mrproper

# 加载lubancat2_defconfig配置文件,rk356x系列均是该配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig

# 编译内核,指定平台,指定交叉编译工具,使用8线程进行编译,线程可根据电脑性能自行确定
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8

RK3588系列板卡用户执行以下命令编译内核源码:

#清除之前生成的所有文件和配置
make mrproper

# 加载lubancat_linux_rk3588_defconfig配置文件,rk3588系列均是该配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat_linux_rk3588_defconfig

# 编译内核,指定交叉编译工具,使用8线程进行编译,线程可根据电脑性能自行确定
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8

如果没有交叉编译工具,或者编译工具版本不匹配也可以使用Lubancat-SDK源码的gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu版本的编译工具链。

执行以下命令获取并配置编译工具的环境变量:

#获取编译工具链
git clone https://github.com/LubanCat/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu.git

#导出环境变量,需要根据实际指定编译工具链的绝对路径
export PATH=/root/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin:$PATH

#查看编译工具链,如果COLLECT_LTO_WRAPPER变量为指定的路径,即配置成功
aarch64-linux-gnu-gcc -v

以上配置为临时导出环境变量,打开其他终端或者重启都需要重新导出环境变量,如需永久保存需要将导出环境变量的命令写入~/.bashrc文件末尾,并执行source ~/.bashrc 重新加载配置。

1.3.2. 在板卡上本地编译内核

按照前面小节搭建编译环境并下载源码之后,进入内核源码根目录,根据具体的板卡设置配置文件。

RK356x系列板卡用户执行以下命令编译内核源码:

#清除之前生成的所有文件和配置
make mrproper

# 加载lubancat2_defconfig配置文件,rk356x系列均是该配置文件
make lubancat2_defconfig

# 编译内核,使用4线程进行编译
make -j4

RK3588系列板卡用户执行以下命令编译内核源码:

#清除之前生成的所有文件和配置
make mrproper

# 加载lubancat_linux_rk3588_defconfig配置文件,rk3588系列均是该配置文件
make lubancat_linux_rk3588_defconfig

# 编译内核,使用4线程进行编译
make -j4

板卡上本地编译需要的时间较长,rk3588系列可能需要1个多小时,rk356x系列可能需要半小时。

内核编译成功后方可以继续学习后续内容。

1.4. 如何编译和加载内核驱动模块

1.4.1. 编译内核驱动模块

内核模块加载到内核,可以将内核模块编译成单独的模块,在内核启动后由用户手动动态加载, 也可以将模块直接编译进内核,在内核启动时就自动加载。测试一般是单独编译成内核模块,然后手动加载,方便调试,同时也节省时间。

野火提供了驱动教程的源码,可以执行以下命令获取:

# 从github获取
git clone https://github.com/LubanCat/lubancat_rk_code_storage

# 或者从gitee获取
git clone https://gitee.com/LubanCat/lubancat_rk_code_storage

获取到源码后,源码目录下的linux_driver文件夹就是存放驱动教程的例程文件,将其配套驱动程序代码放置到 内核代码同级目录 ,原因是编译内核模块时, 驱动程序需要依赖编译好的Linux内核,驱动模块中的Makefile中指定了内核的路径,为方便使用例程,请放至同一目录结构下。

放置例程源码

内核驱动模块对象所需的构建步骤和编译很复杂,它利用了linux内核构建系统的强大功能, 目前我们还不需要深入了解这部分知识,利用简单的Make工具就能编译出我们想要的内核驱动模块。 这里以编译hellomodule内核模块为例,使用命令进入hellomodule目录,然后使用make:

cd linux_driver/module/hellomodule/
make

重要

Makefile中指定的目录 “KERNEL_DIR=../../../kernel/”要和实际编译内核时指定的输出目录一致,如果编译内核时没有指定特定输出目录,那么就将这个变量指定到内核源码的根目录,可以用绝对路径或者相对路径。 这里的环境是在PC上,使用交叉编译工具编译内核模块,在板卡上编译内核模块类似,使用板卡系统的gcc工具,Makefile不指定CROSS_COMPILE 、ARCH也可以。

切换到module/hellomodule目录下,直接执行make命令,即可编译程序。

Makefile(module/hellomodule/Makefile)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
KERNEL_DIR=../../../kernel/
ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
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行: arm64体系结构.

  • 第3行: 指定交叉编译工具链,可以使用Lubancat-SDK的交叉编译工具gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu。

  • 第4行: 导出环境变量。

  • 第6行: 表示以模块进行编译。

  • 第8行: all只是个标号,可以自行定义,是make的默认执行目标。

  • 第9行: $(MAKE)的MAKE是Makefile中的宏变量,要引用宏变量要使用符号。这里实际上就是指向make程序,所以这里也可以把$(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文件,这就是编译生成的内核驱动模块。

1.4.2. 加载内核驱动模块

编译好内核驱动模块,可以通过多种方式将hellomodule.ko拷贝到开发板,我们可以使用 NFS网络文件系统scp命令sftp命令 等。 其中NFS环境请搭建请参考Linux系列章节之 挂载NFS网络文件系统 章节。

scp 命令用于 Linux 之间复制文件和目录,该命令基于ssh,需要搭建好ssh环境,scp命令格式如下:

scp local_file remote_username@remote_ip:remote_folder

例如:

scp hellomodule.ko cat@192.168.103.129:/home/cat/

将hellomodule.ko发送到192.168.103.129的/home/cat/目录下,192.168.103.129是开发板ip,需根据实际而定,开发板用户名为cat, 输入yes,然后输入密码进行验证,等待传输完成,这个时候我们开发板就有了hellomodule.ko 这个文件。如果是在开发板进行本地编译则不需要再进行传输。

实验环境

安装卸载内核驱动模块使用insmod和rmmod命令:

#进入家目录
cd /home/cat/

# 加载内核模块
sudo insmod hellomodule.ko

#查看当前加载的内核模块
lsmod

# 卸载内核模块
sudo rmmod hellomodule.ko
加载内核模块

查看加载的内核模块,可以使用命令lsmod,其他信息也可以到/sys/module目录下查看,例如:加载成功hellomodule.ko模块后, 可以到/sys/module/hellomodule下查看。

1.5. 如何编译和加载设备树

1.5.1. 编译设备树

Linux 3.x 之后的版本引入了设备树(Device Tree)的概念和机制。设备树是一种用于描述硬件平台的静态数据结构,包含了有关硬件设备、总线、中断控制器等信息的详细描述。 后面我们写的驱动需要依赖设备树,所以在这里先介绍如何编译设备树、加载设备树。 这里不做代码讲解,具体原理请参考后续 Linux设备树 章节

1.5.1.1. 使用内核工具编译设备树

在编译 Linux 内核时,会生成名为 dtc(Device Tree Compiler)的工具,该工具用于自动编译设备树源文件(.dts 或 .dtsi 文件)为二进制的设备树文件(.dtb 文件)。我们也可以使用命令 sudo apt install device-tree-compiler 下载dtc编译工具。 dtc工具使用示例如下:

# 编译 dts 为 dtb
内核目录/scripts/dtc/dtc -I dts -O dtb -o xxx.dtb xxx.dts

#也可以下载dtc工具进行编译
sudo apt install device-tree-compiler
dtc -I dts -O dtb -o xxx.dtb xxx.dts

内核使用dtc工具的命令大致如上所示,实际上设备树中有非常多的依赖关系,所以一般情况下, 设备树不仅仅只是通过一个dtc命令就能将编译出来的。以上为伪代码,仅供参考使用,了解即可。

1.5.1.2. 使用内核的构建脚本编译设备树

我们可以尝试着通过内核的构建脚本去编译设备树,我们所要用到的设备树文件都存放在 内核源码/arch/arm64/boot/dts/rockchip 里面。

前面提到了编译内核时会自动去编译设备树,但是编译内核很耗时,所以我们推荐使用如下命令只编译设备树。

rk356x系列板卡执行以下命令:

#加载配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
#使用dtbs参数单独编译设备树
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

rk3588系列板卡执行以下命令:

#加载配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat_linux_rk3588_defconfig
#使用dtbs参数单独编译设备树
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

前面我们提到了编译内核时会去编译设备树,那么此时在没有修改、添加设备树的情况下使用构建脚本单独编译设备树是不会有任何输出的,我们可以手动修改设备树文件再进行编译测试。

例如修改 内核源码/arch/arm64/boot/dts/rockchip 下的 xxx.dts 文件再进行编译。找到已经编译成dtb文件对应的dts源文件,可简单在dts文件中加个空格,以防错误修改导致编译报错。以下以修改rk3568-lubancat-2.dts为例。

编译设备树

执行命令后会编译修改过的设备树,编译成功后生成的设备树文件(.dtb)位于源码目录下的 内核源码/arch/arm64/boot/dts/rockchip

1.5.2. 加载设备树

加载设备树,将编译好的新设备树文件,替换对应板卡的设备树,替换 /boot/dtb/ 目录下的设备树文件即可。

1.5.2.1. 确定板卡使用的设备树文件

由于rk-lubancat系列板卡有众多型号,不同型号使用的设备树很可能不同,并且同一型号也可能有不同的版本,导致设备树很可能不同。我们可以启动并登录板卡,查看 /boot/ 目录下的软连接从而确认当前板卡使用的设备树文件。

在板卡中执行以下命令:

ls -l /boot/
编译设备树

笔者此处使用的板卡为鲁班猫1N,可以从上图中看到rk-kernel.dtb链接到了dtb/rk3566-lubancat-1n.dtb,在系统启动过程中会读取rk-kernel.dtb作为系统设备树,那么笔者使用的鲁班猫1N板卡实际读取的设备树为/boot/dtb/rk3566-lubancat-1n.dtb 。因此,如果需要修改和替换系统加载的设备树,那么就要修改rk-kernel.dtb软链接的设备树。

1.5.2.2. 替换设备树

以鲁班猫1N板卡为例,该板卡的设备树文件是 rk3566-lubancat-1n.dtb,通过SCP或NFS将 内核源码/arch/arm64/boot/dts/rockchip/ 目录下编译生成的设备树拷贝到开发板上,替换/boot/dtb/目录下的 rk3566-lubancat-1n.dtb。

在系统上查看设备树加载情况,比如我们设备树根目录下有设备树节点 leds,我们可以通过以下的方式加载并查看新的设备树是否生效了。 设备树中的设备树节点在文件系统中有与之对应的文件,位于“/proc/device-tree”目录。查看“/proc/device-tree”目录如下所示。

查看leds节点

接着进入led文件夹,可以发现led节点中定义的属性以及它的子节点,如下所示。

leds子节点

在节点属性中有一个name属性,我们查看dts源码并没有发现leds节点中定义了name属性,这个name属性自动生成的,为保存节点名。

这里的属性是一个文件,而子节点是一个文件夹,我们进入“sys-status-led”文件夹。 里面有compatible、name、status等属性文件。 我们可以使用“cat”命令查看这些属性文件,如下所示。

查看子节点属性文件

以上,设备树替换并加载成功。

1.6. 如何编译和加载设备树插件

1.6.1. 编译设备树插件

Linux4.4以后引入了动态设备树(Dynamic DeviceTree)。设备树插件可以被动态的加载到系统中,供被内核识别。

重要

设备树插件和设备树不是互相替代的关系,而是互补的关系。设备树插件可以在主设备树定型的情况下, 再对主设备树未描述的功能进行动态的拓展。 比如A板的设备树没有开启串口1的功能,但B板需要开启串口1的功能,那么可以直接沿用A板的设备树, 并用设备树插件拓展出串口1,满足B板的需求。

1.6.1.1. 使用内核工具编译设备树插件

编译设备树插件和编译设备树类似,这里介绍内核中的dtc工具编译编译设备树插件的过程。

内核中将xxx.dts 编译为 xxx.dtbo的过程示例,仅供参考:

内核构建目录/scripts/dtc/dtc -I dts -O dtb -o xxx.dtbo xxx.dts

例如,将内核源码arch/arm64/boot/dts/rockchip/overlay/目录下的rk356x-lubancat-uart3-m0-overlay.dts编译为rk356x-lubancat-uart3-m0-overlay.dtbo

scripts/dtc/dtc -I dts -O dtb -o arch/arm64/boot/dts/rockchip/overlay/rk356x-lubancat-uart3-m0-overlay.dtbo arch/arm64/boot/dts/rockchip/overlay/rk356x-lubancat-uart3-m0-overlay.dts

执行编译命令后可以内核源码arch/arm64/boot/dts/rockchip/overlay/找到相应的dtbo文件。

当然和编译设备树一样,设备树插件的编译也涉及到依赖关系,所以编译过程也比较复杂。 不仅仅是使用一条命令就可以完成编译的,一般我们在内核目录下,执行make指定dtb选项,即可自动编译添加的设备树插件。

1.6.1.2. 使用内核的构建脚本编译设备树插件

设备树插件与设备树一样都是使用DTC工具编译,只不过设备树编译为.dtb。而设备树插件需要编译为.dtbo。 我们可以使用DTC编译命令编译生成.dtbo,但是这样比较繁琐、容易出错。

鲁班猫系列开发板许多外设硬件描述都是以dtbo插件的形式提供的,使用设备树插件配置硬件外设使用起来非常灵活。

02

如上图,这是一个设备树插件文件列表,用于指定在编译内核时要编译的设备树插件文件。在条件CONFIG_CPU_RK3568被启用时,鲁班猫系列板卡,将编译CONFIG_CPU_RK3568包含的设备树插件。

野火rk356x系列板卡的设备树插件均添加到CONFIG_CPU_RK3568的条件判断中,野火rk3588系列板卡的设备树插件均添加到CONFIG_CPU_RK3588的条件判断中。

当大家尝试写设备树插件的时候,可以将自己的设备树插件添加到内核源码 arch/arm64/boot/dts/rockchip/overlays 目录下, 并修改 arch/arm64/boot/dts/rockchip/overlays/Makefile 文件, 添加编译选项,例如图片中添加lubancat-test-overlay.dtbo。对应的dts源码如下:

/dts-v1/;
/plugin/;

/ {
   compatible = "rockchip,rk3568";

   fragment@0 {
      target = <&uart3>;

      __overlay__ {
         status = "okay";
      };
   };
};

以上实际是rk356x-lubancat-uart3-m0-overlay.dts里面的内容,作用是将UART3设置为”okay”状态,表示将UART3外设接口启用,实际也可以复制其他插件内容进行测试,可以查看板卡的配置文件选择一个没有冲突的插件,在板卡中使用 cat /boot/uEnv/uEnv.txt 进行查看。

添加设备树插件源文件到overlays目录下后,执行设备树的编译命令,设备树插件的编译也会同步完成。

rk356x系列板卡执行以下命令:

#加载配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
#使用dtbs参数单独编译设备树
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

rk3588系列板卡执行以下命令:

#加载配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat_linux_rk3588_defconfig
#使用dtbs参数单独编译设备树
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

如果修改或添加了新的设备树插件,在执行命令后可以在终端看到有相应的编译输出,修改或添加的dts编译为dtbo文件。

1.6.2. 加载设备树插件

野火Lubancat_RK系列板卡支持通过uboot加载设备树插件,供内核识别使用。 通过SCP或NFS将.dtbo设备树插件拷贝到开发板 /boot/dtb/overlays/ 目录下,下面操作都在开发板上进行。

以Lubancat2为例,如下图所示:

02|

和设备树一样,设备树插件的配置文件也进行了软连接,方便不同板卡的配置文件进行加载,可以执行以下命令确认系统实际加载的配置文件。

ls -l /boot/uEnv/
02|

可以从上图看到,当前系统实际加载的配置文件为/boot/uEnv/uEnvLubanCat2.txt。

将要加载的设备树插件写入到/boot/uEnv/uEnvLubanCat2.txt配置文件中,系统启动过程中会自动读取uEnv.txt文件,从而加载指定的设备树插件, 打开位于“/boot/uEnv/”目录下的uEnvLubanCat2.txt文件,如下所示:

02|

要将设备树插件写入uEnvLubanCat2.txt,使用vim或者nano编辑器打开文件,参照着红框内容写即可,书写格式为“dtoverlay=<设备树插件路径>”。

修改完成后保存、退出,执行reboot命令重启系统。 重启后正常情况下我们可以在“/proc/device-tree”或者“/sys/firmware/devicetree/base/”目录下找到设备节点同名的文件夹,证明设备树插件已经加载成功。如果没有出现相应的节点,很可能是与其他节点冲突,可以修改lubancat-test-overlay.dts的内容为配置文件中其他插件的内容再进行测试。