Context: I am working on porting Plasma Mobile and EDK II to my Nokia 8 (codename: NB1) device. There are many things to do and to learn.
In this post, I want to write down what I have learnt from the ABL Bootloader and the Linux kernel, about how the Device Tree(DT, which describes the periphicals in an embedded device) is processed by the ABL and the kernel.
Introduction
The Nokia 8 device(and its successor Nokia 8 Sirocco) uses the MSM8998 SoC platform(or say Qualcomm 835) as a base. In fact, its company HMD just owns the Nokia trademark for mobile phone manufacture. The main design and development is by FIH Mobile, which is a subsidiary of Foxconn. So, we will say some modules named by the FIH in this post.
The model that I own is marked as “Qualcomm Technologies, Inc. MSM8998 v2.1 MTP, FIH NB1 PVT1 SS” in the DT. However, there are many DTs that are appended to the kernel, where it also has the ones for A1N(Nokia 8 Sirocco) and the evaluation version of these devices. These result in more than 80 different DTs, even including the different versions of MSM8998.
So, I wonder how the DT is actually choosed, by the Linux kernel, or by the Bootloader. And in which stage, during the early stage, the XBL or the ABL?
At very first, I thought that the DTs are appended to the kernel, so that the Bootloader(s) will not get involved very deeply. The best DT might be found and choosed by the kernel. However, I found that it is the ABL which is in charge of choosing, fixing and patching the DT after studying. Although the device is kind of outdated, this page talking about “Using Mutiple DTs” from Android documentation might be still helpful for someone.
DT loading in ABL Bootloader
In my previous posts such as Android bootloader analysis – ABL(1), I analyse the Bootloader in a coarse-grained manner. As mentioned in Android bootloader analysis – Aboot, ABL is actually an EFI application that loaded by the XBL. The application is a module named after LinuxLoader, which is in charge of loading Linux kernel and the related entities in an ABoot(Android Boot) image.
DT in LinuxLoader EFI application
As mentioned in Android bootloader analysis – ABL(1), the EFI application entry is declared in QcomModulePkg/Application/ LinuxLoader/LinuxLoader.inf
, as LinuxLoaderEntry
. The application can either boot into Android fastboot mode, or boot into Linux kernel(normal boot or recovery).
Normally, the ABL loads the image in the boot
partition, in which the ANDROIDBOOT format image is used. The image usually contains a kernel and a ramdisk for basic initialization. Depending on the devices, the DTs can be appended to the kernel or stored in a standalone partition:
My Nokia 8 uses the first solution. And there are many DTs. So, before booting, the Bootloader needs to purge them.
If the image is validated, BootLinux (&Info)
is called to actually try to process the image so as to boot it. In that function, DTBImgCheckAndAppendDT
is called to choose the best DT and append it to the kernel.
To do this, DeviceTreeAppended
in QcomModulePkg/Library/BootLib/LocateDeviceTree.c
is called. It checks all the possible DTs from the begin address of the appended DT, to the kernel end. For each DT, the DeviceTreeCompatible
is called to find the best DT.
The standard matching process contains the retrievals of qcom,msm-id
, qcom,board-id
and qcom,pmic-id
properties in the DT. There is a special structure to describe such information from the hardware:
1 | typedef struct DtInfo { |
The matching process is as follows:
1 | if (CurDtbInfo->DtMatchVal & BIT (ExactMatch)) { |
However, for different models of Nokia 8, they do share the same msm
information because they are using the same platform and the same board, which are:
1 | compatible = "qcom,msm8998-mtp", "qcom,msm8998", "qcom,mtp"; |
I found, by mysterious way, that my phone uses a special fih,hw-id
field to find the correct DT. I will discuss this later.
Finally, a pointer named tags
is returned and be passed as the actual DT to the kernel.
FIH modules
As aforementioned, there is a special fih,hw-id
field in addition to qcom,board-id
and compatible
fields to match the DT in the ABL Bootloader.
Such id is stored in a special memory area, which is also declared as reseved memory in the DT:
1 | fih_mem: fih_region@a0000000 { /* for FIH feature */ |
We can see that the memory region starts at 0xa0000000
and has a size of 0xb00000
. The detailed information is not accessible here because the XBL seems not to be open-sourced.
However, when I unpacked the XBL from the factory image, I saw there are FIHDxe
EFI driver and FIHHWIDApp
to load the hardware id into the EFI environment. So that, the LinuxLoader
EFI application can eventually read the information and match the DT.
I cannot provide more details because the reverse-engineering is performed. But the address to store the hardware id is 0x000160f1
in the EFI environment under my 5150 image. It is written by the FIHHWIDApp
using the methods provided by the FIHDxe
.
Anyway, the DT choosed by my device is with:
1 | model = "Qualcomm Technologies, Inc. MSM8998 v2.1 MTP, FIH NB1 PVT1 SS"; |
in which I guess that it is a Product version 1, with Single Slot. This DT is finally the only DT passed to the Linux kernel.
DT processing in Linux kernel
In ARM64, when kernel is loaded and executed, the address pointing to DT is passed in X5
register. The first executable code is from arch/arm64/kernel/head.S
, where we can see the device hardware initialization and the Flatten DT(FDT) pointer is saved from X5
register to __fdt_pointer
:
1 | str_l x21, __fdt_pointer, x5 // Save FDT pointer |
It is a physical address in arch/arm64/kernel/setup.c
:
1 | phys_addr_t __fdt_pointer __initdata; |
where __initdata
indicates that it should be stored in a special section for data used during initialization.
The head.S
does not do many things and then just passes the control(without return) to start_kernel
function. This function is usually generic for all platforms, which performs Linux initialization step-by-step. The DT-related function calls are as follows:
1 | setup_arch(&command_line); |
where setup_arch
is definitly platform-related and architecture-related. And after all initializations are finished, the rest part (non-essential) of the kernel needs to be initialized.
DT in architecture-related setup
Each platform/architecture has its own setup function. The one for ARM64 is arch/arm64/kernel/setup.c
.
It first print the CPU information(the information is visualized in dmesg
) and establish the mappings of virtual addresses:
1 | pr_info("Boot CPU: AArch64 Processor [%08x]\n", read_cpuid_id()); |
Then the boot-related functions are:
1 | setup_machine_fdt(__fdt_pointer); |
Note that both the DT and the EFI-ACPI boot modes are supported. In our case, we only consider the DT mode. So, the topic remains on setup_machine_fdt
, and the unflatten_device_tree
, psci_dt_init
when acpi_disabled
is true.
ARM64 Machine FDT setuping
The setup_machine_fdt
accepts a DT pointer. Note that now the DT has been chosen by the ABL, so we are safe to load and verify the only DT.
1 | static void __init setup_machine_fdt(phys_addr_t dt_phys) |
It first get the virtual address of FDT from the physical address using the memory mapping. Then, perform a basic scan to validate the DT(early_init_dt_scan
in drivers/of/fdt.c
). At the end, the machine name is gotten and printed, which can also be seen in dmesg
.
FDT processing
Then, the FDT is parsed in unflatten_device_tree()
to construct a tree of device_nodes
, which can be used to probe the peripherals.
The first use is to discover the Power State Coordination Interface(PSCI) in psci_dt_init()
. The interface should be compatible with one of the following values:
1 | { .compatible = "arm,psci", .data = psci_0_1_init}, |
Kernel can use the similar way to discover other devices.
DT processing in sysfs
In an ARM Linux with sysfs, we can usually see the devicetree
node and the fdt
node under /sysfs/firmware
directory. These nodes are actually the visualization of the corresponding kernel objects. The Linux kernel just adds them into the kernel object sets.
Such function is implemented in rest_init
. The kernel runs a kernel thread to start the non-critical initilization part of the kernel:
1 | kernel_thread(kernel_init, NULL, CLONE_FS); |
The kernel_init
executes kernel_init_freeable
and then run the init
command. The command can be passed from the kernel commanline in ramdisk_execute_command
or execute_command
. Otherwise, the kernel will try /sbin/init
, /etc/init
, /bin/init
and /bin/sh
. If this still fails, the kernel is in panic.
In kernel_init_freeable
, the kernel still calls many function. The do_basic_setup
and then driver_init
are associated to the DT processing in sysfs.
The driver_init
calls several functions to initialize the different parts as follows:
1 | /* These are the core pieces */ |
In firmware_init
, the firmware_kobj
is created to host the firmware-related kernel objects:
1 | int __init firmware_init(void) |
At the end, of_core_init
creates /sys/firmware/devicetree
and the nodes under the tree:
1 | void __init of_core_init(void) |
In addtion, late_initcall(of_fdt_raw_init)
in driver/of/fdt.c
can create the /sys/firmware/fdt
to host the FDT binary contents:
1 | static int __init of_fdt_raw_init(void) |
If we search the firmware_kobj
, there are also many other firmware types that can be initialized, such as:
- EFI
kobject_create_and_add("efi", firmware_kobj);
underdrivers/firmware/efi/efi.c
- ACPI
kobject_create_and_add("efi", firmware_kobj);
underdrivers/acpi/bus.c
- DMI
kobject_create_and_add("dmi", firmware_kobj);
underdrivers/firmware/dmi_scan.c
which are common for EFI system.
For those who are intrested in EFI and ACPI, there are some function calls during kernel init:
1 | acpi_early_init(); |
FIH modules
Some nodes in the DT are really device-wise and their drivers are not mainlined. The customized kernel provides a driver module to bring up the devices and read the related information. As mentioned before, there is also a similar close-sourced module in XBL. Here we can see the related information.
The hardware id can be read by the following structure, from the reserved memory region at 0xA0A80000
1 | struct st_hwid_table { |
Such information is read and explosed to a file under procfs(/proc
directory), which can be directly read by the userspace program:
1 | static int __init fih_info_init(void) |
There are also information of cpu, dram, battery, gpio, touch, etc. Here we can see the related information.
1 | /************************************************************** |
Conclusion
In this post, I analyzed the how the ABL Bootloader and the Linux kernel deal with the DT. It is interesting to know some details about the exact processing on the Nokia 8(NB1) platform anyway.