A few blog posts ago, I documented how I created a Linux System for the STM32MP157F-DK2
board. It wasn’t such a pleasant experience. Although tiny compared to desktop or server
Linux Systems it’s still huge and consumes a lot of resources just to be able to run. You
also get quickly lost in all the packages and scripts that let everything work together.
One doesn’t get the feeling you can understand the system as a whole anymore and when something goes wrong, a lot of head scratching is only the beginning of the journey of getting it fixed.
That’s in big contrast to our home computers of yesteryear where you really could understand, and master, every single bit of the system in front of you. Which is probably the biggest appeal of the current retro computing movement.
So, I decided to try something a bit more simple (while still being a very healthy upgrade compared to our retro computers). A few years ago, I did a first attempt to get NuttX compiled on a STM32 Discovery board. It was complex and I couldn’t get it to run reliably at that time. Nevertheless, after my embedded Linux System experience just a few weeks before, How hard could it be now?
And, oh my, have things improved in NuttX land. Setting up the build environment and getting
a first build running has become almost trivial. In this blog post I’ll document how I got a
minimal NuttX build running on the STM32F769-DISCO
board that I bought at the same time
with the STM32MP157F-DK2
as a backup in case embedded Linux would be a disappointment. And
how to setup VSCode for NuttX development.
Some useful pointers to get you started with NuttX
My host PC is running Manjaro Linux and, as it turns out, it’s now perfectly possible to cross compile NuttX on this Linux distribution. However, if you want to stick closer to the recommended way of working, a Docker container with Ubuntu works as well.
The native (Manjaro) solution
I’m not going to give a list of packages that need to be installed here. Depending on your preferred Manjaro install, it could be that you already have (part of) those packages. The best way is to just go ahead and see what errors occur along the way and install the missing package based on that.
For the ARM tool chain, you might consider downloading the official ARM toolchain instead of the one provided by the package manager of Manjaro. These tend to be more recent. The same remark goes for the Docker solution of course.
The docker solution
Setting up Docker
Use this Dockerfile
description to set up a suitable docker container with Ubuntu to build
NuttX. Save it under the filename Dockerfile
(no filename extension required):
FROM ubuntu
# The following 2 lines are added to avoid hanging the container creation.
# See <https://grigorkh.medium.com/fix-tzdata-hangs-docker-image-build-cdb52cc3360d>
ENV TZ=Europe/Brussels
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get update && apt-get install -y \
bison flex gettext texinfo libncurses5-dev locales \
libncursesw5-dev gperf automake libtool pkg-config \
build-essential genromfs libgmp-dev libmpc-dev \
libmpfr-dev libisl-dev binutils-dev libelf-dev git \
libexpat-dev gcc-multilib g++-multilib picocom \
u-boot-tools util-linux kconfig-frontends sudo \
gcc-arm-none-eabi binutils-arm-none-eabi
RUN groupadd -g 1000 dev \
&& useradd -u 1000 -g dev -d /home/dev dev \
&& mkdir /home/dev \
&& chown -R dev:dev /home/dev
# The following 3 lines allow the 'dev' user to run sudo (password is "dev").
# Useful when later on packages need to be installed that are missing.
RUN usermod -aG sudo dev
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN echo 'dev:dev' | chpasswd
RUN locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
USER dev
WORKDIR /home/dev
This Dockerfile
creates a user named dev
and password dev
. sudo
is also present so
that you can easily install extra packages if there would be a need for it.
Building the docker image
Build the docker image with this command (while standing in the directory with the above
Dockerfile
):
docker build -t ubuntu-nuttx .
Using a shared directory
Run the docker image with a shared directory which is, in my case, ~/Documents/Projects/NuttX/stm32f7/10.1.0
on the host PC and /data
in the docker container. I use this shared directory to install
the NuttX repo’s and resulting builds:
docker run -dit -P --name nuttx -v ~/Documents/Projects/NuttX/stm32f7/10.1.0:/data ubuntu-nuttx
We get this hash tag back as its identifier:
3de20d7e0c16b30056f60862a27febd5affc11f4b3453f91390d88d4aea05410
And we attach to the running docker image with:
docker attach 3de2
Download NuttX
You can either download a stable release in compressed tar format or use the GIT repositories. At the time of writing this, the latest stable release was 10.1.0
:
Download a stable release
mkdir nuttx-10.1.0
cd nuttx-10.1.0
curl -L https://www.apache.org/dyn/closer.lua/incubator/nuttx/10.1.0/apache-nuttx-10.1.0-incubating.tar.gz?action=download -o nuttx.tar.gz
curl -L https://www.apache.org/dyn/closer.lua/incubator/nuttx/10.1.0/apache-nuttx-apps-10.1.0-incubating.tar.gz?action=download -o apps.tar.gz
tar zxf nuttx.tar.gz
tar zxf apps.tar.gz
Clone from GIT repositories
mkdir nuttx-10.1.0
cd nuttx-10.1.0
git clone https://github.com/apache/incubator-nuttx.git nuttx -b releases/10.1
git clone https://github.com/apache/incubator-nuttx-apps apps -b releases/10.1
In both cases, your directory layout should be like this (It’s important that the apps
and
nuttx
directories are next to each other!):
└── nuttx-10.1.0
├── apps
└── nuttx
It’s also best to not have spaces in the path’s according to the documentation.
Related repo’s
Can be found here.
E.g. the NxWM
, the tiny NuttX Window Manager
can be found there.
Compiling NuttX for a supported board
Now we are ready to compile the source code into an executable binary file that can be run
on the embedded board. In my case a STM32F769-DISCO
.
First we need to configure the build environment
Initialize Configuration
To list all supported configurations, do (while being in the nuttx
directory):
./tools/configure.sh -L | less
The output is in the format <board name>:<board configuration>
. You will see that, in
general, all boards support the nsh
configuration which is a good starting point since it
enables booting into the interactive command line NuttShell (NSH
).
To choose a configuration, you pass the <board name>:<board configuration>
option to
configure.sh. For my board, I have two possibilities:
./tools/configure.sh stm32f769i-disco:nsh
or
./tools/configure.sh stm32f769i-disco:netnsh
You can expect this kind of output for the NSH
option:
nuttx$ ./tools/configure.sh stm32f769i-disco:nsh
Copy files
Select CONFIG_HOST_LINUX=y
Refreshing...
make[1]: Entering directory '/data/nuttx-10.1.0/nuttx'
...
make[2]: Leaving directory '/data/nuttx-10.1.0/apps/industry'
mkkconfig in /data/nuttx-10.1.0/apps
make[1]: Leaving directory '/data/nuttx-10.1.0/apps'
#
# configuration written to .config
#
This configuration can later on be modified by using a menu based configuration system.
Building
Now you just need to use the make
command to build NuttX with the configuration we
specified earlier.
make
If you want to compile a lot faster, add the -j
option and specify how many cores you
want to use to compile the code. E.g.:
make -j 24
After just a few seconds we already have a NuttX build for our board (compare this to over
half an hour for a Linux build for the STM32MP157F-DK2
board of my earlier blog posts…).
You can expect this kind of output:
nuttx$ make
make[1]: Entering directory '/data/nuttx-10.1.0/nuttx/tools'
...
make[1]: Entering directory '/data/nuttx-10.1.0/nuttx/sched'
CC: clock/clock_initialize.c
...
CC: wdog/wd_recover.c
AR (create): libsched.a clock_initialize.o clock_settime.o
...
make[1]: Leaving directory '/data/nuttx-10.1.0/nuttx/sched'
IN: sched/libsched.a -> staging/libsched.a
make[1]: Entering directory '/data/nuttx-10.1.0/nuttx/drivers'
...
make[2]: Leaving directory '/data/nuttx-10.1.0/nuttx/boards/arm/stm32f7/stm32f769i-disco/src'
LD: nuttx
make[1]: Leaving directory '/data/nuttx-10.1.0/nuttx/arch/arm/src'
CP: nuttx.hex
CP: nuttx.bin
As the above output already indicates, there are 2 files created in the nuttx
directory
that can be used to flash the board:
nuttx.bin
nuttx.hex
Other useful make
options are:
make clean
# make a clean startmake distclean
# make a clean start and clear out old configurations
Flashing the MCU with openOCD
This always needs to be done on the host PC with the openOCD
tool (in case you used a docker image to build the code). You will probably need to install
it first on the host PC.
Make sure you’re in the nuttx
directory which contain the nuttx.bin
and nuttx.hex
files. It takes this command line to let openOCD
flash the nuttx.bin
into the MCU:
openocd -f interface/stlink.cfg -f target/stm32f7x.cfg -c "program nuttx verify reset exit"
This is the output openOCD
gave me during its execution (which went very fast as well,
barely 2 seconds):
Open On-Chip Debugger 0.11.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
Info : clock speed 2000 kHz
Info : STLINK V2J35M26 (API v2) VID:PID 0483:374B
Info : Target voltage: 3.228947
Info : stm32f7x.cpu: hardware has 8 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f7x.cpu on 3333
Info : Listening on port 3333 for gdb connections
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x080001f8 msp: 0x20020d5c
Info : Unable to match requested speed 8000 kHz, using 4000 kHz
Info : Unable to match requested speed 8000 kHz, using 4000 kHz
** Programming Started **
Info : device id = 0x10016451
Info : flash size = 2048 kbytes
Info : Single Bank 2048 kiB STM32F76x/77x found
** Programming Finished **
** Verify Started **
** Verified OK **
** Resetting Target **
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
shutdown command invoked
The same ST-Link USB connection also functions as a serial port available at /dev/ttyACM0
.
Simply set up your favourite terminal emulator at 115.200
baud to see what goes on on the
board. E.g with picocom
:
picocom -b 115200 /dev/ttyACM0
And this is the output I saw. NuttShell
is already running. Typing in help
gives a list of available commands and installed applications:
nsh> help
help usage: help [-v] [<cmd>]
. cat dd help mkrd rmdir time usleep
[ cd echo hexdump mount set true xd
? cp exec kill mv sleep uname
basename cmp exit ls pwd source umount
break dirname false mkdir rm test unset
Builtin Apps:
sh nsh
nsh> help hexdump
hexdump usage: hexdump <file or device> [skip=<bytes>] [count=<bytes>]
nsh> help -v hexdump
NSH command forms:
[nice [-d <niceness>>]] <cmd> [> <file>|>> <file>] [&]
OR
if <cmd>
then
[sequence of <cmd>]
else
[sequence of <cmd>]
fi
OR
while <cmd>
do
[sequence of <cmd>]
done
OR
until <cmd>
do
[sequence of <cmd>]
done
hexdump usage: hexdump <file or device> [skip=<bytes>] [count=<bytes>]
Configuring Nuttx for a supported board
The canned NuttX configuration files are retained in:
.../nuttx/boards/arch-name/chip-name/board-name/configs/config-dir
For the STM32F769-DISCO I used, this is in:
.../nuttx/boards/arm/stm32f7/stm32f769i-disco
With the configuration tool it’s possible to configure NuttX further to your needs.
The NuttX Configuration Tool
Make sure you stand in the nuttx
directory and issue the following command:
make menuconfig
This brings up this text based GUI (if kconfig
is installed):
Another possibility to use this menu driven configuration tool is to use this command:
make nconfig
You should see something like this if the required packages are installed to support this menu driven configuration tool:
The basic configuration order is “bottom-up”:
- Select the build environment,
- Select the processor,
- Select the board,
- Select the supported peripherals,
- Configure the device drivers,
- Configure the application options on top of this.
Installing basic
A simple test that you can do is installing a basic interpreter with the configuration tool.
I’ll use nconfig
for this in this example:
Select Application Configuration
:
Select Interpreters
:
Select Basic Interpreter support
:
Save the new configuration and after a quick rebuild and reboot:
Debugging with VSCode
Official NuttX documentation on fast configuration changes
First of all, we need to make sure to have a debug build of our code. We can use the
configuration tool for that. But it’s faster to use the command line via the
kconfig-tweak
utility like so:
kconfig-tweak --enable CONFIG_DEBUG_SYMBOLS
And do a quick rebuild of NuttX.
Make sure you have the Cortex-Debug
extension installed in VSCode:
Now we need to make a new configuration:
Select Cortex-Debug
:
A launch.json
file is created inside the .vscode
directory that is created alongside the
apps
and nuttx
directories:
.
├── apps
├── nuttx
└── .vscode
The launch.json
is already filled in with a template:
Replace it with this content in launch.json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Cortex Debug (NuttX)",
"cwd": "${workspaceRoot}",
"executable": "${workspaceFolder}/nuttx/nuttx",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
"device": "stm32f7x",
"configFiles": [
"interface/stlink.cfg",
"target/stm32f7x.cfg"
]
}
]
}
And we’re ready for debugging with VSCode.
Debugging
I noticed there seems to be a bug when I want to do use the list
command in bas
:
With this setup it’s now easy to set a breakpoint where the error string is formed. Which is
the Program_PCtoError
function:
And from there on, we can search further for this bug. All of it directly on the stm32f769i-disco
board itself.
For this kind of debugging, there is generally no need to see the contents of all the
registers such a MCU has. But it can be useful for other kinds of applications. Luckily this
is something that can easily be accomplished by using the corresponding SVD
file that
matches the MCU in use.
For this particular case, I needed the STM32F769.SVD
file which I dropped in the .vscode
directory (next to the launch.json
and tasks.json
files). Now we only need to add this line to our launch.json
file to tell the Cortex-debug
extension where it can find the
SVD
file:
"svdFile": "${workspaceRoot}/.vscode/STM32F769.svd"
The contents of the launch.json
file then becomes:
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Cortex Debug (NuttX)",
"cwd": "${workspaceRoot}",
"executable": "${workspaceFolder}/nuttx/nuttx",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
"device": "stm32f7x",
"configFiles": [
"interface/stlink.cfg",
"target/stm32f7x.cfg"
],
"svdFile": "${workspaceRoot}/.vscode/STM32F769.svd"
}
]
}
After a restart of the debugging session we see the contents of the registers of the MCU in use:
Building NuttX from within VSCode
To make our lives even more easier, we can add a task to build NuttX again after we made some changes to the code.
Select Configure Tasks...
from the Terminal
menu:
You probably are going to see an empty template in the tasks.json
files. Copy the following contents in it:
{
"version": "2.0.0",
"tasks": [
{
"label": "make NuttX",
"command": "make",
"options": {
"cwd": "${workspaceFolder}/nuttx/"
},
"args": [
"-j12"
],
"type": "shell",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
If you now select Run Build Task...
from the Run
menu, you’ll see that make
is called
and the changed code is rebuild (using 12 threads because of the -j12
argument):
Debugging NuttX which is compiled in a Docker container
When your code is compiled in a Docker container, all debug info will contains paths to the source code as they were used in the Docker container. More than likely, these paths will be very different compared to the paths where the source code lives in the shared directory of the host PC where VSCode is running.
In that case, GDB
will not be able to locate your source code because of the different
paths for source code level debugging, which is very annoying of course. Luckily there is a
solution for that by telling GDB
where to look for the source code by using the
set substitute-path
From the GDB manual:
For example, if the file /foo/bar/baz.c
was moved to /mnt/cross/baz.c
, then the command
set substitute-path /foo/bar /mnt/cross
will tell GDB
to replace /foo/bar
with /mnt/cross
, which will allow GDB
to find
the file baz.c
in the /mnt/cross
directory even though it was moved from the /foo/bar
directory.
More debugging tips for NuttX:
A few more links that go deeper into debugging NuttX: