With semihosting I can use standard I/O function like printf()
and I can read and write data on the host through the debug connection. If used with care, this is a great feature especially for unit testing.
Outline
Semihosting is a technology originally defined by ARM to allow communication between the host and the target. It is named ‘semi’ because half of the work is on the target and half of the work is on the host. Basically with semihosting the target runs into a special breakpoint, the debugger catches the breakpoint, reads the target command and executes read and write operations, depending on the command. For details see my other article: Using Semihosting the direct Way.
With semihosting I can
- redirect standard I/O like printf() or scanf() to the host (telnet or gdb console)
- open/read/write files on the host
- Other operations as implemented in McuSemihost.c
In this article I show how I can use semihosting with the Raspberry Pi Pico (RP2040, for example the pico board or the pico-w board, Visual Studio Code, the Cortex-Debug extension and using a SEGGER J-Link.
Semihosting Standard I/O Library
To use semihosting, link your application with the pico_stdio_semihosting
library. One way is to add it with target_link_libraries
:
💡 You might notice the other stdio libraries I have added above: the nice thing with the RP2040 SDK is that I can use standard I/O redirection to USB CDC, UART and Semihosting, all the same time. If I have enabled standard I/O for all of them, e.g. text with printf() is written to all enabled output channels.
The other way is to add the following to the CMakeLists.txt
:
pico_enable_stdio_semihosting(${CMAKE_PROJECT_NAME} ENABLED)
Initialization
In the application initialization code, I have to init the library:
First, include the header file:
#include "pico/stdio_semihosting.h"
Then initialize the library:
stdio_semihosting_init();
Further, I can configure how CR-LF is treated:
stdio_set_translate_crlf(&stdio_semihosting, false);
Semihosting Hard Fault Handler
Semihosting works with debugger breakpoints. If no debugger is attached, the breakpoint will cause an exception and the application won’t run. For this I’m using a special hardfault handler which allows me to run the application with no active debug session. The handler is implemented in the McuLib and the module McuHardFault.
I include the header file:
#include "McuHardFault.h"
and initialize it, so it overwrites the default Hardfault handler of the SDK:
McuHardFault_Init();
With this, any semihosting exceptions will be skipped in the handler, allowing the application to run.
Cortex-Debug
The Cortex-Debug extension supports semihosting, and requires special launch settings to work. Below is my debug.json
using a J-Link:
{
"version": "0.2.0",
"configurations": [
{
"name": "TSM_PicoW_Blinky Cortex-Debug",
"type": "cortex-debug",
"request": "launch",
"servertype": "jlink",
"serverpath": "${env:PICO_SEGGER_PATH}/JLinkGDBServerCL.exe", // or: set in global json: cortex-debug.JLinkGDBServerPath
"cwd": "${workspaceRoot}",
"executable": "${command:cmake.launchTargetPath}",
"armToolchainPath": "${env:PICO_TOOLCHAIN_PATH}", // needed for the gdb
"device": "RP2040_M0_0",
"interface": "swd",
"serialNumber": "", // add J-Link serial number if having multiple attached the same time.
"runToEntryPoint": "main", // "_reset_handler" or for example "main"
"postLaunchCommands": [
"monitor semihosting enable",
"monitor semihosting ioclient 3", // 1: telnet (port 2333); 2: gdb; 3: both telnet and gdbclient output
],
"postRestartCommands": [],
"postResetCommands": [],
"rtos": "FreeRTOS",
"svdFile": "${env:PICO_SDK_PATH}/src/rp2040/hardware_regs/rp2040.svd",
"rttConfig": {
"enabled": false,
"address": "auto",
"decoders": [
{
"label": "",
"port": 0,
"type": "console"
}
]
},
"showDevDebugOutput": "none",
}
]
}
The important part is this:
"postLaunchCommands": [
"monitor semihosting enable",
"monitor semihosting ioclient 3", // 1: telnet (port 2333); 2: gdb; 3: both telnet and gdbclient output
],
This enables semihosting with the J-Link debug probe and uses telnet
and gdbclient
for the communication.
Debug
Time to check it: use some printf()
in your code and the output will be written to the Debug Console:
Summary
Semihosting is just another nice way for standard I/O communication with the target. Using the RP2040 SDK I need to link with a library and initialize it. I recommend to use the special McuHardFault handler to enable the application running without debugger too. Just keep in mind that standard I/O semihosting and semihosting in general can be intrusive. I recommend to use the McuSemihosting library which is faster and includes file I/O and other features.
Happy hosting 🙂
I confirm that I also run semihosted unit-tests on the Pico (for example https://github.com/micro-os-plus/micro-os-plus-iii/blob/xpack/tests/platform-raspberrypi-pico/CMakeLists.txt). It is a nice platform.
LikeLike
Hi Liviu,
yes, the RP2040 is a really nice MCU and platform. Compared to other devices it is rather simple (on the plus side: less complex), but very capable. Semihosting is something I usually avoid in the application because it is rather intrusive and slow (e.g. compared to RTT), but for unit testing it gives the needed flexibility and way to send and receive data to the target in an easy way.
Interesting to see uCOS in use: I have abandoned it years back with the uCOS ownership changes and troubles. Nice OS, but pretty much obsolete with the dominance of FreeRTOS now?
LikeLike
Yes, just that it’s not the nice µC/OS by Labrosse, it is µOS++, a C++ framework (http://micro-os-plus.github.io/user-manual/), even nicer, I would say 🙂
LikeLike
Ah, I see. Did not look close enough. Thanks for the clarification!
LikeLike
In the same context, to be noted that running semihosted unit tests does not require physical boards, I actually run most of my tests on QEMU emulated platforms, like Cortex-M0, Cortex-M7F, Cortex-A15, Cortex-A72, RISC-V RV32IMAC and RV64IMAFDC (for example https://github.com/micro-os-plus/utils-lists-xpack/blob/xpack/tests/platform-qemu-cortex-m7f/CMakeLists.txt), with the obvious advantage that this also works in CI environments, like GitHub Actions.
LikeLike
That approach with QEMU works fine for an RTOS testing and if you only need to core, an not much more. I might need to look at QEMU again, but I think it does not provide a FCS (full chip simulation)? I see that this should come from the silicon vendors, but I don’t see them providing it? On the other side: if the testing is on the application logic with no hardware required (or only mocks of it), QEMU is a viable solution.
LikeLike
Right, QEMU is great for unit-testing portable libraries. The challenge is to modularise a project into portable libraries, which should start at design time. Once this is done properly, and dependencies streamlined, there are many advantages, including simplified testing, which can also be done as native applications in a POSIX environment (an example from the same project is https://github.com/micro-os-plus/utils-lists-xpack/blob/xpack/tests/platform-native/CMakeLists.txt). To be noted that in all cases (native process, semihosted QEMU emulated or semihosted OpenOCD), the source code of the tests is exactly the same, the code itself is completely unaware of the environment it runs on. Big advantage!
LikeLike
Hi Liviu,
yes, that approach works fine for independent logic or something which is not really tied to the hardware. Unfortunately this is not always the case, especially for embedded targets. Yes, some part like the ‘business logic’ can be abstracted easily, but not if it needs to deal with the real hardware.
LikeLike
Hi Erich, enjoyed your write up.
Coming from an app-dev background I’d be curious what kind of challenges interfacing with hardware poses – and how much of the limitation is inherent vs tooling related.
Considering a project to cut my teeth on and thought developing tooling in the area could be fun. Things like recording/playing back signals and asserting the expected outputs. Thanks!
LikeLike
Hi Mike,
tooling is not that much of a problem. The problem are the limited communication ways of deep embedded systems compared to ‘normal’ applications on the host. For example in many cases all what you have is an UART connection (if any). So you need sophisticated ways on the target or in the target firmware to get the data in and out.
LikeLike
Pingback: Implementing File I/O Semihosting for the RP2040 and VS Code #RP2040 #RaspberryPi @raspberry_Pi @McuOnEclipse « Adafruit Industries – Makers, hackers, artists, designers and engineers!