This is an old revision of the document!
No new lectures today ;) some demos, maybe!
You are probably be tired of using u-boot CLI to load the Linux FIT uImage into RAM and booting it manually. Surely, there must be something to be done to automate this (yep, there is!).
First, let's learn about u-boot environment more… Start your board to a u-boot prompt and enter env print…
Notice various environment variables? There are various addresses, strings and even scripts! Can you figure out the command executed automatically at boot?
We hope you're getting the same idea… what if we can modify this environment when compiling U-Boot such that it executes our own script?
The KConfig item for doing this is called something like DEFAULT_ENV, try searching for it!
It allows us to specify a custom file that contains our default environment…
First, let's create this file, let's say mydefault.env inside the uboot source directory. Let's start from a minimal example:
/* default u-boot environment variables */ /* this file is passed through the C preprocessor (so we can use C-style macros!) */ /* someone in uboot source code re-defined `linux`... */ #undef linux arch=arm baseboard=autodetect baudrate=115200 mmcdev=0 mmcpart=1 emmc_dev=0 console=ttyLP0 bootargs=console=ttyLP0,115200 earlycon,115200 rdinit=/linuxrc clk_ignore_unused # This is the command executed automatically when uboot starts... bootcmd=echo Fastboot mode... press Ctrl-C to exit; fastboot auto # This runs instead of bootcmd when booted using `uuu` via USB bootcmd_mfg=run bootcmd bootdelay=3 # TODO: enter a valid uImage file DRAM load address (the one used with 'bootm') loadaddr=TODO # TODO: enter the 'load mmc 0 ...' script (will be executed using 'run loadimage') loadimage=TODO image=linux.itb # this can be executed using 'run linux' to boot your Linux kernel! linux=echo Booting Linux ...; run loadimage; bootm ${loadaddr};
Notice in the original environment that you may use the run command to run scripts from other defined variables, here's its reference:
run - run commands in an environment variable
We can also use ${varname} expressions to do variable expansion like in Linux shells!
Enter your boot script inside the bootcmd var and let's proceed with overwriting the default environment.
Recall the ENV_DEFAULT_ENV_TEXT_FILE menuconfig item? Modify it to point to your mydefault.env (you can simply use a relative path to u-boots source dir). Note that you need to enable ENV_USE_DEFAULT_ENV_TEXT_FILE checkbox first to let you modify that value!
For advanced use cases / build automation, you can override any config items by creating extra .config files and using the included ./scripts/kconfig/merge_config.sh u-boot host script!
Afterwards, [re]compile u-boot, copy the newly compiled u-boot .bin files to imx-mkimage directory again (if not doing it automatically using a Makefile) and regenerate your flash.bin.
Test it by booting your new firmware using uuu.
Always check the UBoot version and build time printed on the serial console during the boot process to ensure you loaded your latest firmware image!
Try using run linux to run the linux script (if you used that), otherwise edit the bootcmd to do that automatically after the timeout!
Did it work? if not, you may need to repeat this process (this is where a script comes in handy!).
Note that real devices have their uboot configuration proceed automatically with booting the OS if a physical button is NOT pressed (remember those old Android phones? you could stop this process by holding several volume keys together!). You can script this using GPIO (try the gpio command!), but it's out of scope for today (we don't have any physical buttons).
If you wish to change without uboot's environment without recompiling u-boot itself, it's also possible (and very easy to do).
Since this is usually desirable for flexibility during upgrades, there are many ways to do this: activate one of CONFIG_ENV_IS_IN_* config options or use a startup script which loads the env from a .txt file on the FAT32 partition. We'll prefer the latter since it's more fun (& failproof on most boards!).
Basically, we will write a custom script to be executed by u-boot at startup. To do this, search for the PREBOOT config item inside u-boot's menuconfig.
Since we added a custom default.env file, we need to activate USE_PREBOOT and specify our script as a hardcoded preboot= environment variable.
Sample script:
preboot=if fatload mmc 0:1 ${loadaddr} uboot.txt; then env import -t ${loadaddr} ${filesize}; fi
That's it! Create this file on your board's FAT32 partition (using the same key=value assignments separated by newlines syntax) and boot it! You should now see your new environment values (env print)!
Notice that the FRDM-iMX93 has two Ethernet ports. But if you boot your previous Buildroot distro and try to see the available network: ip link show.
In this task, you will have to debug the problem and try to fix it or at least find a workaround that lets you use your network interface.
Here are a few suggestions to get you started:
ethernet nodelinux/scripts/dtc/dtcdtc on Arch or device-tree-compiler on Ubuntugrep -rn parts of any interesting message in the kernel's source to determine the context./scripts/clang-tools/gen_compile_commands.py and it will generate a compile_commands.json file. This file can be used by your language server to allow you to *go to definition* or highlight code sections included in #ifdefs.compatible string from the DTB
Once you are done, enable the iperf3, iproute2 and ethtool network packages in Buildroot and build them. The compilation should not take more than 1-2 minutes. Re-generate linux.itb and copy it to the board's eMMC.
Connect to a colleague's board with an Ethernet cable.
Use the ip command to add a static IP to your network interface (man ip-address).
Then, use the iperf3 tool to test the throughput and compare it to what ethtool advertises.
Why is it not a full 1Gbps?
The steps above might seem a bit hackish… that's because they are a ugly HACK working around the real problem!
And the real problem is: since the kernel was compiled with external modules (and they take ~300MB),
they were not included inside the initial ramdisk (obviously, since it's less than 80MB)!
Due to missing modules, the ethernet driver and its dependencies could not be properly initialized (actually, as we've debugged above, it's just missing its MAC address provider) and thus fails!
The real solution would be to install the buildroot-obtained rootfs (or any other method, tbh.), together with the external kernel modules (usually sitting inside /var/modules/7.x – your kernel version).
This can be easily done by using the kernel's built-in install target:
# enter linux kernel source directory cd linux/ # create a target directory where external modules will be installed mkdir -p "../linux-modules" # install the modules make ARCH=arm64 INSTALL_MOD_PATH="../linux-overlay" modules modules_install # explore it! ls -l ../linux-modules/lib/modules/*
Next, you will want to add the linux-modules directory into your Buildroot filesystem.
Fortunately, this is the easiest thing: enter buildroot's menuconfig, search for ROOTFS_OVERLAY configuration item and set it to the absolute path of your linux-modules created above.
Do a make and voila! The rootfs archive will have a huge size (>~350MB), meaning we successfully added all available kernel modules.
There is one problem remaining, though: the rootfs won't fit anymore inside the uImage / small FAT32 partition…
So change of plans: delete the initrd node from linux.its (together with its ramdisk configuration node property) and rebuild the uImage.
Next, on the second partition inside your disk image, you must unpack the rootfs.tar file outputted by buildroot. Simply use tar xf ./path/to/rootfs.tar -C /mnt/your-ext4-partition/ (check out the provided mk-disk-image.sh script!).
Still not finished: we need to add the following parameter to the kernel command line BEFORE booting Linux: root=/dev/mmcblk0p2. This can be done inside u-boot, e.g., by using the setenv command:
# it's recommended to supply the whole cmdline value: setenv bootargs "console=ttyLP0,115200 earlycon,115200 root=/dev/mmcblk0p2 clk_ignore_unused"
Otherwise, the kernel will complain that it cannot find the root block device and panic. Check out the kernel boot log if you have any problems (it will also print its command line parameters it receives from the bootloader)!
If you successfully do all of this, you should see both eth interfaces: first one is 100Mbps, the second one is 1Gbps!
An embedded device is designed to interface with the physical world via sensors and actuators.
The most common way to do this is by means of Generic Purpose Input/Output (GPIO): electrical connectors exposed by the SoC that can be freely controlled by software to be either input or output, read/write a logical signal (0-3.3/5V) with whatever they're connected too.
Moreover, the FRDM-iMX93 board has some GPIOs exposed to a Raspbery PI-like 40-pin header and even has some on-board soldered I/O components (e.g., two user-controllable buttons and a RGB LED). Check out its the manual!
Today, we'll learn how to write a simple Linux application to control GPIOs and integrate it into Buildroot's automatic packaging system!
First, check out the Buildroot manual's table of contents.
Read the 9.9. Adding project-specific packages section for an overview of the process involved in adding new custom packages to our Linux distribution.
Basically, we have two options: either create a subdirectory inside the buildroot's internal packages directory or use a BR2_EXTERNAL directory. Although it will be a tad more work to do, we'll choose the second approach as it has one advantage: we are able to include / exclude the directory at will (useful if you don't intend to finish the task, as it would leave your buildroot source broken!) and being able to share your work with others (e.g., using your own Git repository), i.m.h.o. separation of concerns is always a nice thing to do!
So we'll proceed with the tutorial on Using BR2_EXTERNAL to keep customizations outside of the root.
First, create a directory, let's say, gpio-external somewhere in your home.
In there, you must create three files inside this directory:
1.external.desc, containing a name and a description:
name: ASS_EXTRA desc: ARM Summer School Extras
The name is the most important, must be uppercase [A-Z0-9_], as it will be used to prefix your custom packages!
2. Config.in, that will be used to load KConfig menus from your packages:
# BR2_EXTERNAL_<NAME>_PATH is automatically defined for your external dir! # we will create our package later, let's name it `myleds`: source "$BR2_EXTERNAL_ASS_EXTRA_PATH/package/myleds/Config.in"
3. … and external.mk, also designed to include packages' build scripts:
include $(sort $(wildcard $(BR2_EXTERNAL_ASS_EXTRA_PATH)/package/*/*.mk))
As you probably figured it out, you must also create the package/myleds (or whatever name you want for your Buildroot package be called) inside your external root, containing two important files:
Config.in (uppercase C!) – KConfig menu entries;myleds.mk: A makefile included by Buildroot, guess why: to compile your program!
Let's start with the package configuration. Recall that, by invoking menuconfig make target, you are presented with lots of BR2_PACKAGE_* offerings. All packages append to this menu by the means of a KConfig file using a custom definition language. For buildroot, it must be called Config.in and must be located inside each package's and external dir directory (you saw it included there!). Let's make one for our package:
config BR2_PACKAGE_MYLEDS
bool "myleds"
# we can have some dependencies:
depends on BR2_PACKAGE_LIBGPIOD2
help
ARM Summer School LEDS App.
We have given MYLEDS name for our package (the directory name must also be a lower-cased version of this). This is important, as we will need to use it as prefix for all of our Makefile variables defined in our *.mk:
################################################################################ # ASS Leds Package ################################################################################ MYLEDS_VERSION = 1.0 MYLEDS_SITE = $(BR2_EXTERNAL_ASS_EXTRA_PATH)/package/myleds/src MYLEDS_SITE_METHOD = local MYLEDS_LICENSE = GPL-3.0+ MYLEDS_INSTALL_STAGING = NO MYLEDS_INSTALL_TARGET = YES MYLEDS_DEPENDENCIES = libgpiod2 define MYLEDS_BUILD_CMDS $(MAKE) $(TARGET_CONFIGURE_OPTS) -C $(@D) all endef define MYLEDS_INSTALL_TARGET_CMDS $(INSTALL) -D -m 0755 $(@D)/myleds $(TARGET_DIR)/usr/bin endef define MYLEDS_PERMISSIONS /usr/bin/myleds f 0755 root root - - - - - endef $(eval $(generic-package))
We've started from the Generic Package tutorial with our makefile.
Notice that there are ways to specify where to get the source code (it can be downloaded, but we will write our code locally), how to compile the package (by invoking an inner Makefile inside our source directory) and how to install it (we are currently copying the myleds executable to the target rootfs's /usr/bin path, though feel free to custimize it further!) plus permissions (we'll giving anyone the right to execute it!).
The $(generic-package) is a Buildroot macro that will take all of our package name-prefixed variables and generate the final makefile build rules.
For now, create a dummy source directory at package/myleds/src with a main.cpp (or .c if you wish to stay oldschool) file simply printing something:
#include <cstdlib>
#include <iostream>
int main()
{
::std::cout << "Hello from MyLeds!"
<< ::std::endl;
return 0;
}
And its compilation script:
SRCS = main.cpp # all is the first target, so it will be invoked when calling `make` simply all: myleds # myleds is the resulting executable name using `-o $(@)` # depend on source files (so it will be recompiled everytime the .cpp changes) myleds: $(SRCS) $(CXX) $(CXXFLAGS) $(SRCS) $(LDFLAGS) -o $(@) # good practice to have a `clean` rule: clean: rm -f myleds
$(CXX) specifying the path to the compiler (it will actually be set to ${CROSS_COMPILE}g++ by Buildroot)!
Same for $(CXXFLAGS) and $(LDFLAGS), they contain compiler arguments supplied by the make caller (we will set them later, we'll leave them empty for now).
Those are actually defined as a convention by GNU Make's manual and used throughout the Open Source ecosystem, so just behave and use them too ;))
We can now do a preliminary test if it builds by first enabling our external directory using:
# activate our external dir (do this inside buildroot's source dir!) make BR2_EXTERNAL="../gpio-external"
.br2-external.mk file in the output directory!
If you ever wish to disable it, simply invoke a make clearing this variable, e.g.:
make BR2_EXTERNAL=
Now enter menuconfig and search for your package: MYLEDS or however you called it. It should exist! Enable it (together with its declared libgpiod2 dependency! recall: all KConfig names are UPPERCASE!).
After enabling it, invoke make again and check the logs: you should see your makefile being executed!
It may complain about missing C++ compiler, though, with a cryptic error: “make[1]: no: No such file or directory
”. If you look more closely, you'll find that buildroot passes CXX=no to our Makefile script. To fix this, simply enable TOOLCHAIN_BUILDROOT_CXX. Unfortunately, you must 'make clean' in order for Buildroot to make changes to the toolchain :(
Find your executable inside the buildroot's output directory:
find output/ -name 'myleds'
Was is correctly installed? Can you execute it (hah, gotcha!)?
We need to use a library for this, modern distributions use libgpiod2 which you saw we added as dependency earlier!
Time to finish our code: check out a C++ gpiod2 example here. This sets a GPIO for input, we want it to output. Change the code accordingly. Make a specific GPIO blink every second.
Find the RGB LED's GPIO index inside the board's manual. Either choose a single color or write code to cycle through them all. One more thing: there are multiple gpiochips defined by the board's device tree. The one useable to control the LEDs and RPI GPIO header is /dev/gpiochip0 (the first one, as expected).
Also check more LibGPIOd2 examples on its official repository!
When finally finished, try to build it. It may give you some errors (libgpiod functions/classes not found). This is because you're missing the appropiate LDFLAGS, so be sure to include them and re-try.
Good luck ;)