Debugging with PlatformIO: Part 4. Using Semihosting on ARM Targets
A brief introduction to the semihosting mechanism and configuration steps for PlatformIO projects
When developing embedded software, it’s essential to have not only a full-featured debug probe that significantly improves the overall debugging experience but also a convenient way of sending status messages to a debug console on a host machine. This possibility can greatly help to less invasively monitor the behavior of an embedded application without stopping the whole program flow. Just imagine how useful it might be to use the “printf” function on a target device and see the output directly in the debug console on your machine without any additional hardware except a debug probe. It comes especially handy in the early development stages when there are no final I/O facilities available.
Nowadays, one of the most commonly used techniques for such tasks is called “Semihosting”. It’s a powerful debug mechanism designed to simplify the tunneling of I/O requests from a target device to a host machine. In the previous post, we reviewed the debugging capabilities of the PlatformIO ecosystem in the CLI mode. In this article, we will try to take a deeper look at the semihosting capabilities and configure a simple PlatformIO project to use the I/O facilities of a computer running the PlatformIO IDE.
Throughout this post, we will be using a low-cost ARM-based board ST Nucleo-F401RE and STM32Cube as the development framework.
Table of Contents
- Prerequisites
- What is Semihosting?
- Getting Started
- Enabling Semihosting
- Adding Support for Floating-point Numbers
- Conclusion
- Stay in touch with us
Prerequisites
This article uses the following components:
What is Semihosting?
In the Arm documentation, the term semihosting
is defined as a mechanism for communicating I/O requests from application code to a host computer running a debugger. To put it simply, it means that we can delegate some parts of application functionality (e.g. working with a keyboard, printing on a display, file I/O, etc.) to a host machine. A typical semihosting setup can be visualized as the following:
The implementation of the semihosting mechanism depends on the target architecture. In this article, we use ST Nucleo-F401RE with a Cortex-M4 MCU on-board. In our case, the semihosting implementation utilizes a special BKPT (0xAB)
instruction which forces the CPU to enter the debug state. In this state, the control is transferred to a debugger so it can investigate whether that breakpoint should trigger a semihosting operation. Given that semihosting supports many different operations (SYS_OPEN
, SYS_SEEK
, SYS_WRITE
, etc.), the processor has to specify what operation is requested and what parameters should be passed to the host machine. It’s done via the R0
and R1
registers. The processor stores the semihosting operation type in R0
and prepares other parameters in a memory block pointed by R1
. Once the processor executes the BKPT instruction and enters its debug state, the debugger checks the values in the R0
and R1
registers, executes the requested semihosting operation accordingly, returns the result value in R0
, and skips the BKPT instruction to let the target continue the execution.
It’s also worth mentioning that the semihosting technique cannot offer high-speed I/O operations. Each time a semihosting operation is executed, the processor is simply stopped while the data is being transferred. The required time depends on many aspects including the type of a debug probe, target CPU speed, and even the performance of the host machine, all of that can affect the speed of semihosting operations.
Now that we know how semihosting works behind the scenes, we need to instruct the internal system calls to utilize the mechanism described above. Fortunately, the GNU Arm Embedded toolchain offers a special semihosted version of the syscalls implemented in the library called rdimon
, we just need to properly configure the linker command with several special flags. The following project will show all the steps required to enable semihosting support.
Getting Started
For a quick start, let’s create s basic example that should print the “Hello, World!” string to the debug console. For the sake of brevity, I will assume you already have the ST STM32 platform installed in your system.
First of all, let’s create a new project using PlatformIO Home Page:
As discussed above, we need to select “ST Nucleo-F401RE” as the board and “STM32Cube” as the framework and press the “Finish” button:
Now, let’s add some actual code that should print the “Hello world!” string. Create a new file main.c
in the src
folder with the following content:
#include <stdio.h>
#include <string.h>
#include <stm32f4xx_hal.h>
int main(void) {
HAL_Init();
while(1) {
// Messages are buffered, so "\n" is required
// to flush the internal buffer immediately
printf("Hello world!\n");
HAL_Delay(1000);
}
}
void SysTick_Handler(void)
{
HAL_IncTick();
}
If we start a new debug session after this step, then the “Hello world!” string won’t be printed in the debug console. That’s expected behavior because by default STM32Cube-based projects are linked with the nosys
library. This library implements system calls as simple stubs which don’t provide any semihosting support. That’s why we need to slightly modify our project to link appropriate libraries.
Enabling Semihosting
Let’s go through the steps required to enable semihosting support. First of all, we need to disable the default system calls implementation using the build_unflags option:
[env:nucleo_f401re]
platform = ststm32
framework = stm32cube
board = nucleo_f401re
; Remove stub implementations
build_unflags =
-lnosys
--specs=nosys.specs
In the next step, we need to instruct the linker to use the above-mentioned rdimon
implementations. The easiest way is to create an extra script in the root of the project (for example enable_semihosting.py
) and add the semihosting flags directly to the default build environment:
Import ("env")
env.Append(
LINKFLAGS=["--specs=rdimon.specs"],
LIBS=["rdimon"]
)
In order to run that script we need to specify it in our platformio.ini
file using the extra-scripts option:
[env:nucleo_f401re]
platform = ststm32
framework = stm32cube
board = nucleo_f401re
; Remove stub implementations
build_unflags =
-lnosys
--specs=nosys.specs
; Add semihosting flags
extra_scripts =
enable_semihosting.py
In the third step, we need to configure the debug server so it will redirect the semihosting output to the debug console. It can be done in the platformio.ini
file using the debug_extra_cmds option and two special commands:
monitor arm semihosting enable
enables semihosting supportmonitor arm semihosting_fileio
redirects the semihosting I/O to the debug console
After all those steps, the final platformio.ini
file should look like this:
[env:nucleo_f401re]
platform = ststm32
framework = stm32cube
board = nucleo_f401re
; Remove stub implementations
build_unflags =
-lnosys
--specs=nosys.specs
; Add semihosting flags
extra_scripts =
enable_semihosting.py
; Enable semihosting
debug_extra_cmds =
monitor arm semihosting enable
monitor arm semihosting_fileio enable
Finally, we need to declare and call a special function initialise_monitor_handles
at the very beginning of the main
function:
#include <stdio.h>
#include <string.h>
#include <stm32f4xx_hal.h>
extern void initialise_monitor_handles(void);
int main(void) {
initialise_monitor_handles();
HAL_Init();
while(1) {
printf("Hello world!\n");
HAL_Delay(1000);
}
}
void SysTick_Handler(void)
{
HAL_IncTick();
}
Now we can start a new debug session (F5
) and see the output in the debug console:
In order to better understand how it works, let’s take a closer look at the assembly listing of an internal system call when the “Hello World!” string is passed to the printf
function:
The listing shows that before executing the BKPT (0x00ab
) instruction, the register R0
is loaded (via R4
) with the value 5
which stands for SYS_WRITE
. The register R1
is loaded with a memory address from R5
which was previously populated with the value of the Stack Pointer increased by four. The value in R1
points to a three-word data block that contains the following items:
- Handle to a previously opened stream or file
- Memory address with the data to write
- Size in bytes of the data to write
In our case, the value in R1
points to the address 0x20017fc7
, so let’s take a closer look at the real example of the data block located at that address:
By default, the ARM processors use the little-endian order, so all values should be read backward. The first block contains the value 0x01
which is the default value for the STDOUT
stream, the second block contains a pointer to the address 0x20000310
and if we read the memory at that address we will see our Hello World!
string. The final block contains the value 0x0D
which is equal to 13 bytes of the data.
Adding Support for Floating-point Numbers
By default, the STM32Cube framework is linked against a special variant of standard libraries called newlib-nano
. It’s an optimized version of the newlib
libraries with simplified logic and without unnecessary features. Additionally, rarely used functionality is declared using the weak symbol technique which greatly helps reduce the memory footprint. The formatted I/O operations with floating-point support are one of those features that are implemented as weak symbols. Fortunately, the process of enabling comes down to adding two flags to the linker command.
The support for floating-point I/O operations can be enabled in the same enable_semihosting.py
file from the “Getting Started” section. Simply add two new flags -Wl,-u,_printf_float
and -Wl,-u,_printf_scanf
which will force the linker to pull those symbols to the final binary:
Import ("env")
env.Append(
LINKFLAGS=[
"--specs=rdimon.specs",
"-Wl,-u,_printf_float",
"-Wl,-u,_scanf_float"
],
LIBS=["rdimon"]
)
Conclusion
Semihosting is an extremely useful feature for testing and debugging purposes. Having it in your toolbox can greatly help you out when there are no other communication channels available. But developers should be aware of the several limitations that semihosting may bring:
- Due to implementation trade-offs semihosting may significantly impact the performance of an application that can result in unexpected side effects in hard real-time system applications
- Semihosting only works during a debug session which means that applications compiled for semihosted development environment won’t run correctly when the target is detached from a debugger
- Memory-constrained devices may not have enough resources to handle semihosting calls (especially when
printf
with floats is used)
It’s also worth mentioning that there are alternatives like retargeting the printf
output via ITM channels or even solutions from commercial companies like Segger Real-Time Transfer.
This is the final post in the “Debugging with PlatformIO” series. If you are new to the series, you may find Debugging with PlatformIO: Part 1. Back to the Basics a useful starting point.
If you liked this series, you are sure to enjoy our posts on other topics:
Stay in touch with us
Stay tuned to this blog or follow us on LinkedIn and Twitter @PlatformIO_Org to keep up to date with the latest news, articles and tips!