Embedded systems have been a popular topic for a long time, therefore we decided to share some insight into one of our applications. According to Wikipedia, an embedded system is a computer system – a combination of a computer processor, computer memory, and input/output peripheral devices – that has a dedicated function within a larger mechanical or electrical system.
This blog post aims to describe the process of developing a simple Python application to control the hardware, along with a deep dive into embedded Linux boot optimization for a custom embedded application. For all of this, we used a build system – Yocto. It allows building a fully functional system, including bootloader, DTB (device tree), Linux kernel, root file system (with required apps), and a custom app as a service.
Embedded Linux boot optimization process
In this project, the Linux boot time is understood as the time from plugging the power connector to observing a blinking LED. Achieving effective embedded Linux boot optimization requires analyzing each layer of the startup sequence, and this analysis runs in parallel with application development to ensure both the system and the custom app meet timing requirements. The boot process includes (in short):
- hardware delay: voltage regulators, capacitors, reset supervisory circuit
- booting ROM Code (inside the processor)
- bootloader (U-Boot) loading and initialization
- Linux kernel image loading into memory
- Linux kernel booting
- systemd booting – system and service manager
- custom application loading time (especially for languages like Python)
Some of these times can be observed from system logs, but to get a full picture one needs an oscilloscope or logic analyzer connected to pins (UART TX, 3V3, LED).
The hardware that we used:
For this embedded systems project, we selected the following hardware components to support our application development goals:
-
- SOM: NXP® i.MX 7 Computer on Module – Colibri iMX7 by Toradex (with eMMC)
- Carrier Board: VIOLA carrier board by Toradex
- Debugging Tools: UART -> USB Converter & Logic Analyzer
- Peripherals: LED and a resistor
Software & distribution baseline
Toradex provides a Yocto build process, with all BSPs (Board Support Package) layers and a lot of own tunings and support, so it was our starting point. On top of that, we added a custom layer. We placed our demo application there as well, because it is really small, but ideally (according to the Yocto spirit) we should have downloaded our application from a git repository during the build.
Toradex build provides a lot of functionality with multiple utilities. It is really nice, as it is a ready-to-go system for most applications and development. But if you have a custom application with specific requirements — as is common in embedded systems application development — you can strip a lot of functionality at almost every stage of the build. Also, for some applications, like our use of CAN Bus, you need to modify the DTS (Device Tree Source).
Our application controls a device via CAN Bus interface, but for simplicity, this blog post describes only control of the GPIOs. The process is quite similar, but definitely more approachable for a single article. We would note the difference between GPIO and CAN control in some places.
Default Console Distribution (at the moment of development 3.0b3) uses systemd as the system and service manager. It is one of the most popular managers, providing great flexibility, but unfortunately, it is also slow… Regardless, we decided to go with it as our requirement. For the application layer, we decided to go with Python.
Our optimization and application include changes in multiple components of the build process, described in the following paragraphs.
Step-by-Step optimization guide
1. Linux kernel modules selection
One of the most popular tools to configure the kernel is menuconfig. To run it in the bitbake environment run:
bitbake linux-toradex -c menuconfig -f
It brings a new terminal, where one can get rid of (or include) some modules.
But the changes made with menuconfig are applied only in the build directory. One can prepare a patch from the changes or take the whole .config file. The first approach seems to be cleaner, but for simplicity, we went with the latter, and copied the file to our layer – in an appropriate directory, as:
layers/meta-canboard/recipes-kernel/linux/linux-toradex/colibri-imx7-emmc/defconfig
2. Device Tree (DTS) modification
The device tree allows the kernel to know the hardware. It is written in a readable form (in a file called DTS) and then compiled to a binary file for a specific kernel version (DTB).
Toradex provides a DTB file for its evaluation board, with a lot of additional hardware and a standard configuration of pins (according to Toradex standard). Fortunately, the DTS files are created as a composition, allowing to include base files. E.g. processor manufacturer (here NXP) may prepare a basic DTS for the chip, Toradex can provide next DTS layer for its COM, and one can include all of this and add only peripherals required for a custom board.
DTB files are created during Linux build, according to a Makefile in /arch/arm/boot/dts. So to add a custom DTS in Yocto, one can patch the kernel – here just include the new file. To achieve this, one needs to create linux-toradex_%.bbappend file, as well as the DTS file. See its content for our GPIO demo below:
/dts-v1/; #include "imx7d-colibri-emmc.dtsi" / { model = "Toradex Colibri iMX7D on Colibri with CAN"; compatible = "toradex,colibri-imx7d-emmc-eval-v3", "toradex,colibri-imx7d-emmc-canboard", "toradex,colibri-imx7d-emmc", "fsl,imx7d"; chosen { stdout-path = "serial0:115200n8"; }; }; &iomuxc { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_gpio1 &pinctrl_gpio2 &pinctrl_gpio3 &pinctrl_gpio7 &pinctrl_usbc_det>; }; /* &pinctrl_gpio4 same pins as pinctrl_flexcan2. pinctrl_gpio7 same as pinctrl_flexcan1. Therefore removed above */ &uart1 { status = "okay"; };
Note: But for CAN Bus application we needed to enable 2 CAN interfaces (and disable appropriate GPIOs).
3. Removing unnecessary DISTRO-, MACHINE- and IMAGE_FEATURES
The default Toradex image includes a lot of excessive modules (for our application). We definitely don’t need a Bluetooth stack nor X11 server. See example snippet below:
MACHINE_FEATURES_remove = "bluetooth" MACHINE_FEATURES_remove = "touchscreen" ... DISTRO_FEATURES_remove = "irda" DISTRO_FEATURES_remove = "opengl" ...
Tip: One can also use default images from poky, e.g. core-image-minimal.
4. Removing unnecessary application and utils, and installing a custom application
Toradex provides also a lot of utilities and tools, but we can strip them in the final product, to save the rootfs space. To achieve this, one can remove these applications from IMAGE_INSTALL.
But we also need to include some new dependencies – Python3, required python3 libraries, and our custom application:
IMAGE_INSTALL += " \ python3 \ python3-gpio \ gpio-demo \ "
5. U-Boot Bootloader Optimization
The most visible optimization here is just changing of the CONFIG_BOOTDELAY to 0. It is the time how long the U-Boot allows to enter the boot menu before moving to the default boot command. Since some version, U-Boot wait also for one key even if the delay is set to 0. In the past, one had to set CONFIG_ZERO_BOOTDELAY_CHECK to keep the opportunity to enter the boot menu.
One can also save some time on the output text written to the serial terminal, either reducing the number of logs and making it asynchronous with CONFIG_UBI_SILENCE_MSG.
By default, U-Boot supports a lot of interfaces to boot the system, but we can also save some time on removing some of them (Ethernet, USB, …). We didn’t have a play with this, because our COM board (Toradex iMX7) allows to run U-Boot ONLY from eMMC (due to eFUSES set by Toradex), therefore one needs to be careful while tuning the U-Boot. In case of misconfiguration, one would need to enter a recovery mode (this includes soldering a bridge onto the board and connecting to the PC via USB). On most boards, one can run the system (including U-Boot) from ana SD-Card, but it is considered as less stable and unrecommended for the production version. It is most probably a business decision, to prevent people from using SD-Card, as it may lead to a worse perception of the Toradex products, but of course, it is only my guess…
Anyway, the U-Boot configuration was made in Yocto as a patch for the U-Boot – colibri_imx7_emmc_defconfig.patch:
From d578b7452451d15bce87f006c0ddea5833119231 Mon Sep 17 00:00:00 2001 From: Przemyslaw <przemyslaw@wizzdev.pl> Date: Tue, 9 Jun 2020 11:45:05 +0200 Subject: [PATCH] some fixes --- configs/colibri_imx7_emmc_defconfig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/configs/colibri_imx7_emmc_defconfig b/configs/colibri_imx7_emmc_defconfig index 75732925ab..255c7bbed8 100644 --- a/configs/colibri_imx7_emmc_defconfig +++ b/configs/colibri_imx7_emmc_defconfig @@ -11,7 +11,7 @@ CONFIG_IMX_BOOTAUX=y CONFIG_DISTRO_DEFAULTS=y CONFIG_FIT=y CONFIG_SYS_EXTRA_OPTIONS="IMX_CONFIG=board/toradex/colibri_imx7/imximage.cfg,MX7D" -CONFIG_BOOTDELAY=1 +CONFIG_BOOTDELAY=0 # CONFIG_USE_BOOTCOMMAND is not set # CONFIG_CONSOLE_MUX is not set CONFIG_SYS_CONSOLE_IS_IN_ENV=y @@ -72,3 +72,4 @@ CONFIG_CI_UDC=y CONFIG_DM_VIDEO=y CONFIG_FAT_WRITE=y CONFIG_OF_LIBFDT_OVERLAY=y +CONFIG_UBI_SILENCE_MSG=y \ No newline at end of file -- 2.17.1
6. Creating the custom Python application recipe
For the blog purpose, we created a gpio_demo.py application, to control the LED. The application uses Python gpio library. It is not a standard package, therefore we need to create a bb file for it. fortunately, there is a tool to create it automatically for a Python package. To create the bb file, issue:
pipoe -p cantools –python python3
We also want to run our application automatically at boot (as a service). Because we want to run it as fast as possible, without optimizing of the systemd itself, we can set fields WantedBy and DefaultDependencies, as below:
[Unit] Description=gpio-demo service DefaultDependencies=no [Service] Type=simple ExecStart=python3 /usr/bin/gpio_demo.py PIDFile=/tmp/gpio_demo.pid [Install] WantedBy=swap.target
We also need to install our application file (e.g. in /usr/bin) and enable the service.
Deployment: flashing the image to the eMMC
Finally, we need to flash the image to the built-in eMMC memory, as it is most probably the fastest available interface. One can flash the U-Boot with the environment, kernel, dts and rootfs manually to the eMMC, but Toradex provides a great tool to do it automatically – Toradex Easy Installer. It requires all these components to be packed in an appropriate format, but our Yocto build already provides it.
We then flash the eMMC over the Ethernet, connecting to the Toradex Easy Installer with a VNC client – see screenshot below.
Benchmarks & results analysis
After flashing and restarting, our custom Linux distributions start and work well.
The time from plugging the power to the second LED blink is ~3.4 seconds. After disabling the kernel output to the serial port, it goes down to 2.9 s, including all the boot steps described earlier in the article.
The time was measured with a logic analyzer. The time we are talking about is from the rising edge of channel 0 to the second rising edge of channel 2.
It is worth noting, that the first LED toggle takes place 1.7 s after powering the board. This comes from the bash script – wrapper around the Python script..
Logic analyzer pin mapping
channel 0 – power 3.3V
channel 1 – serial TX
channel 2 – LED GPIO.
For the version with serial output enabled, we can see that the kernel itself already boots in 1 second.
U-Boot 2019.07-3.0.3+g26d926eda0 (Jun 09 2020 - 09:45:47 +0000) CPU: Freescale i.MX7D rev1.3 1000 MHz (running at 792 MHz) CPU: Extended Commercial temperature grade (-20C to 105C) at 36C Reset cause: POR DRAM: 1 GiB PMIC: RN5T567 LSIVER=0x01 OTPVER=0x0d MMC: FSL_SDHC: 1, FSL_SDHC: 0 Loading Environment from MMC... OK In: serial Out: serial Err: serial Model: Toradex Colibri iMX7 Dual 1GB (eMMC) V1.1A, Serial# 06597040 SEC0: RNG instantiated Net: FEC0 Hit any key to stop autoboot: 0 Booting from internal eMMC chip... 42387 bytes read in 9 ms (4.5 MiB/s) 3571456 bytes read in 74 ms (46 MiB/s) Kernel image @ 0x81000000 [ 0x000000 - 0x367f00 ] ## Flattened Device Tree blob at 82000000 Booting using the fdt blob at 0x82000000 Using Device Tree in place at 82000000, end 8200d592 Starting kernel ... [ 0.000000] Booting Linux on physical CPU 0x0 ... [ 0.959055] VFS: Mounted root (ext4 filesystem) readonly on device 179:2. ... [ 1.117189] systemd[1]: Detected architecture arm. Welcome to TDX WizzDev 2.6-snapshot-20200608 (thud)! [ 1.156436] systemd[1]: Set hostname to <colibri-imx7-emmc>. ... [ OK ] Reached target Swap. Mounting Temporary Directory (/tmp)... [ OK ] Started gpio-demo service. Starting udev Coldplug all Devices... ... [ OK ] Started Journal Service. [ 2.125964] EXT4-fs (mmcblk0p2): re-mounted. Opts: (null) ... TDX WizzDev 2.6-snapshot colibri-imx7-emmc ttymxc0 Colibri-iMX7-eMMC_Console-Image 3.0b3 20200609 colibri-imx7-emmc login:
Conclusion
Optimizing embedded Linux boot time is a multi-layered challenge, and this project demonstrates that meaningful gains are achievable at every stage — from U-Boot configuration to the application layer. Using Yocto as the build system proved essential throughout this process, enabling fine-grained control over the bootloader, kernel, device tree, root filesystem, and custom application packaging, all within a single, reproducible workflow.
The results we achieved were sufficient for our application, but there is certainly room to go further. Stripping additional U-Boot functionality, removing unused kernel modules, and replacing systemd with a lighter init system such as System V or a Busybox-based script would all contribute to a faster startup. As the timing data shows, the Python application alone accounts for 1.2 seconds of the total 2.9-second boot time — switching to a C application would reduce this significantly, at the cost of some development flexibility.
Ultimately, how far you push boot time optimization depends on your specific requirements. Yocto gives you the tools to make those trade-offs precisely and deliberately, making it an ideal foundation for any embedded Linux project where startup performance matters.







