SO2 实验 11——ARM 内核开发

实验目标

  • 初步了解片上系统(SoC)
  • 熟悉使用 ARM 作为支持架构的嵌入式世界
  • 理解什么是板级支持包(BSP)
  • 使用 i.MX6UL 平台作为示例,编译和引导 ARM 内核
  • 熟悉使用设备树进行硬件描述

片上系统

片上系统 (SoC) 是一块集成电路 (IC),整个系统都集成在上面。通常在 SoC 上可以找到的组件,包括中央处理单元 (CPU)、内存、输入/输出端口、存储设备,以及更复杂的模块,如音频数字接口、神经处理单元 (NPU) 或图形处理单元 (GPU)。

SoC 可用于各种应用领域,最常见的包括: - 消费电子产品(电视机、手机、游戏机) - 工业计算机(医学成像等) - 汽车 - 家用电器

SoC 的主要架构是 ARM。值得一提的是,还有基于 x86 的 SoC 平台。我们还需要关注 RISC-V,这是一种开放的标准指令集架构。

下图展示了 ARM 平台的简化视图:

../_images/schematic1.png

我们将以 NXP 的 i.MX6UL 平台作为参考平台,但通常所有的 SoC 都包含以下构建模块:

  • 一个或多个 CPU 核心
  • 系统总线
  • 时钟和复位模块
    • 相位锁定环(PLL)
    • 晶振(OSC)
    • 复位控制器
  • 中断控制器
  • 定时器
  • 内存控制器
  • 外设控制器

下图是 i.MX6UL 平台的完整框图:

IMX6UL-BD

i.MX6UL 评估套件板的外观如下:

imx6ul-evk

其他流行的 SoC 开发板包括:

板级支持软件包

板级支持软件包(BSP)是支持演示特定硬件平台的功能的情况下,最小的一组软件包。这包括:

  • 工具链
  • 引导加载程序
  • Linux 内核映像、设备树文件和驱动程序
  • 根文件系统

半导体制造商通常会提供一个评估板和相应的 BSP。BSP 通常使用 Yocto 进行打包。

工具链

由于我们的开发机主要基于 x86 架构,我们需要交叉编译器,这样可以生成针对 ARM 平台的可执行代码。

我们可以使用 https://crosstool-ng.github.io/ 从头开始构建自己的交叉编译器,或者我们可以安装一个

$ sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf # 用于 arm32
$ sudo apt-get install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu     # 用于 arm64

根据配置,有几个工具链二进制文件可用:

  • 对于“arm-eabi-gcc”,你有 Linux 系统 C 库,该库将调用内核的 IOCTL,例如为进程分配内存页面。
  • 对于“arm-eabi-none-gcc”,你运行在没有任何操作系统的平台上,所以 C 库与之不同。

编译 ARM 版 Linux 内核

为 32 位 ARM 板编译内核:

# 根据你的平台选择 defconfig
$ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx_v6_v7_defconfig
# 编译内核
$ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make -j8

为 64 位 ARM 板编译内核:

# 对于 64 位 ARM,有一个适用于所有支持的板的单个配置
$ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make defconfig
# 编译内核
$ ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make -j8

Linux 内核镜像

内核镜像二进制文件被命名为 vmlinux,可以在内核树的根目录下找到。用于引导的压缩镜像可以在以下位置找到:

  • arch/arm/boot/Image,适用于 arm32
  • arch/arm64/boot/Image,适用于 arm64
$ file vmlinux
  vmlinux: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped

$ file vmlinux
  vmlinux: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), statically linked, not stripped

Rootfs

根文件系统 (rootfs) 是挂载在文件层次结构顶部 (/) 的文件系统。它应该至少包含允许系统引导到某个 shell 的关键文件。

root@so2$ tree -d -L 2
├── bin
├── boot
├── dev
├── etc
├── home
│   └── root
├── lib
│   └── udev
├── mnt
├── proc
├── sbin
│   └── init
├── sys
├── usr
│   ├── bin
│   ├── include
│   ├── lib
└── var

对于 x86,我们将使用 Yocto rootfs 镜像。为了下载 arm32ext4 rootfs 镜像,需要运行以下命令:

$ cd tools/labs/
$ ARCH=arm make core-image-minimal-qemuarm.ext4

设备树

设备树 (DT) 是一种用于描述系统中硬件设备的树状结构。树中的每个节点描述一个设备,因此这些节点被称为 设备节点。之引入设备树,是为了提供一种发现不可发现硬件的方式(例如,I2C 总线上的设备)。此前,这些信息是存储在 Linux 内核的源代码中的。这意味着每当我们需要修改某个设备的节点时,都需要重新编译内核。现在情况已经改变,因为设备树和内核镜像是独立的二进制文件。

设备树以设备树源文件 (.dts) 的形式存储,并编译成设备树二进制文件 (.dtb)。

# 编译 dtbs
$ make dtbs

# arm32 设备树源文件位置
$ ls arch/arm/boot/dts/
  imx6ul-14x14-evk.dtb imx6ull-14x14-evk.dtb bcm2835-rpi-a-plus.dts

# arm64 设备树源文件位置
$ ls arch/arm64/boot/dts/<vendor>
  imx8mm-evk.dts imx8mp-evk.dts

下面的图片是简单的设备树示例,描述了板型、CPU 和内存。

../_images/dts_node1.png

请注意,可以使用 label: name@address 来定义设备树节点:

  • label,用于从其他位置引用该节点的标识符
  • name,节点标识符
  • address,用于区分具有相同名称(name)的节点。

节点可以包含多个以 name = value 格式排列的属性。name 是一个字符串,value 可以是字节、字符串或字符串数组。

以下是一个示例:

/ {
     node@0 {
          empty-property;
          string-property = "string value";
          string-list-property = "string value 1", "string value 2";
          int-list-property = <value1 value2>;

          child-node@0 {
                  child-empty-property;
                  child-string-property = "string value";
                  child-node-reference = <&child-node1>;
          };

          child-node1: child-node@1 {
                  child-empty-property;
                  child-string-property = "string value";
          };
   };
};

Qemu

我们将使用 qemu-system-arm 启动 32 位 ARM 平台。虽然可以从官方发行版仓库中安装,例如:

sudo apt-get install -y qemu-system-arm

我们依然强烈推荐从源代码构建最新版本的 qemu-system-arm

$ git clone https://gitlab.com/qemu-project/qemu.git
$ ./configure --target-list=arm-softmmu --disable-docs
$ make -j8
$ ./build/qemu-system-arm

练习

重要

我们强烈建议你使用 这个仓库 中的配置。

要解决练习问题,你需要执行以下步骤:

  • 用模板来准备骨架
  • 构建模块
  • 启动虚拟机并在虚拟机中测试模块。

当前实验名称为 arm_kernel_development。请参阅任务名称的练习。

骨架代码是从位于 tools/labs/templates 的完整源代码示例中生成的。要解决任务,首先要为所有实验生成骨架代码:

tools/labs $ make clean
tools/labs $ LABS=<实验名称> make skels

你还可以使用以下命令为单个任务生成骨架代码:

tools/labs $ LABS=<实验名称>/<任务名称> make skels

生成骨架驱动程序后,构建源代码:

tools/labs $ make build

然后,启动虚拟机:

tools/labs $ make console

模块将放置在 /home/root/skels/arm_kernel_development/<任务名称> 目录中。

重新构建模块时,无需停止虚拟机!本地 skels 目录与虚拟机共享。

请查看 练习 部分以获取更详细的信息。

警告

在开始练习或生成骨架之前,请在 Linux 仓库中运行 git pull 命令,以确保你拥有最新版本的练习。

如果你有本地更改,pull 命令将失败。使用 git status 检查本地更改。如果要保留更改,在 pull 之前运行 git stash,之后运行 git stash pop。要放弃更改,请运行 git reset --hard master

如果你在 git pull 之前已经生成了骨架,你需要再次生成骨架。

警告

以下是针对 ARM 虚拟机操作的新规则

# 模块构建
tools/labs $ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make build
# 模块拷贝
tools/labs $ ARCH=arm make copy
# 内核构建
$ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make -j8

0. 简介

检查 Linux 内核代码中以下位置,并识别使用 ARM 架构的平台和供应商:

  • 32位: arch/arm/boot/dts
  • 64位: arch/arm64/boot/dts

使用 qemu 查看支持的平台:

../qemu/build/arm-softmmu/qemu-system-arm -M ?

注解

我们使用了自己编译的 arm32 版本的 Qemu。有关详细信息,请参见 Qemu 部分。

1. 启动

使用 qemu 启动 i.MX6UL 平台。为了启动,我们首先需要编译内核。请查看 编译 ARM 版 Linux 内核 部分。

成功编译将生成以下二进制文件:

  • arch/arm/boot/Image,针对 ARM 架构编译的内核镜像
  • arch/arm/boot/dts/imx6ul-14x14-evk.dtb,适用于 i.MX6UL 开发板的设备树二进制文件

请查看 Rootfs 部分,并下载 core-image-minimal-qemuarm.ext4 根文件系统。然后使用以下命令运行 qemu

../qemu/build/arm-softmmu/qemu-system-arm -M mcimx6ul-evk -cpu cortex-a7 -m 512M \
  -kernel arch/arm/boot/zImage -nographic  -dtb arch/arm/boot/dts/imx6ul-14x14-evk.dtb \
  -append "root=/dev/mmcblk0 rw console=ttymxc0 loglevel=8 earlycon printk" -sd tools/labs/core-image-minimal-qemuarm.ext4

注解

Qemu 对 LCDIF 和 ASRC 设备的支持不够完善。请从编译中移除它们。

$ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make menuconfig
# 设置 FSL_ASRC=n 和 DRM_MXSFB=n
$ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make -j8

内核启动后,请检查内核版本和 CPU 信息:

$ cat /proc/cpuinfo
$ cat /proc/version

2. CPU 信息

检查 NXP i.MX6UL 开发板的 CPU 配置。从 arch/arm/boot/dts/imx6ul-14x14-evk.dts 开始。

  • 找到 cpu@0 设备树节点,并查找 operating-points 属性。
  • 读取处理器可以运行的最大和最小操作频率
$ cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq
$ cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq

3. I/O 内存

检查 NXP i.MX6UL 开发板的 I/O 空间配置。从 arch/arm/boot/dts/imx6ul-14x14-evk.dts 开始,并确定下面提到的每个设备。

$ cat /proc/iomem
  00900000-0091ffff : 900000.sram sram@900000
  0209c000-0209ffff : 209c000.gpio gpio@209c000
  021a0000-021a3fff : 21a0000.i2c i2c@21a0000
  80000000-9fffffff : System RAM

识别与以下设备对应的设备树节点:

  • System RAM,在 arch/arm/boot/dts/imx6ul-14x14-evk.dtsi 中查找 memory@80000000 节点。系统 RAM 的大小是多少?
  • GPIO1,在 arch/arm/boot/dts/imx6ul.dtsi 中查找 gpio@209c000 节点。该设备的 I/O 空间大小是多少?
  • I2C1,在 arch/arm/boot/dts/imx6ul.dtsi 中查找 i2c@21a0000 节点。该设备的 I/O 空间大小是多少?

4. Hello World

实现一个简单的内核模块,在加载和卸载时打印一条消息。编译该模块并加载到 i.MX6UL 模拟平台上。

# 模块构建
tools/labs $ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make build
# 模块复制
tools/labs $ ARCH=arm make copy
# 内核构建
$ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make -j8

5. 简单设备

为一个简单的平台设备实现一个驱动程序。找到 TODO 1,注意如何声明并注册 simple_driver 作为平台驱动程序。按照 TODO 2,在 simple_device_ids 数组中添加 so2,simple-device-v1so2,simple-device-v2 兼容字符串。

arch/arm/boot/dts/imx6ul.dtsisoc 节点下创建两个设备树节点,分别使用兼容字符串 so2,simple-device-v1so2,simple-device-v2。然后观察加载 simple_driver 模块时的行为。