In this posting I will look at porting the C standard library output (e.g. puts / printf ) to use a UART rather than the default ARM/Keil semihosting.
In my last post, I looked at getting basic user I/O out from a native-mbed via UART0 to a terminal emulator (e.g. Tera Term). This was driven by the fact that, currently, neither
printf (via semihosting) or ITM_SendChar do not function on the mbed. Unfortunately, my solution uses a propriety API, such as init_serial0 and putchar0, etc., rather than puts, printf, etc.
ITM_SendChar uses the ITM (Instrumented Trace Macrocell) on the Cortex-M3 core, which in turn uses a Trace Port (either SWO or 4-pin) to send messages. To get output from a Trace Port you need a debug unit with trace capabilities, e.g. a ULINK or J-Link device. The current implementation of CMSIS-DAP does not support any trace capabilities; however I am led to believe that ARM are planning to add some trace capabilities in future versions or variants of CMSIS-DAP (no timeframes).
To reference the ARM website:
Semihosting is implemented by a set of defined software instructions, for example, SVCs, that generate exceptions from program control. The application invokes the appropriate semihosting call and the debug agent then handles the exception. The debug agent provides the required communication with the host.
On a Cortex-M3 (ARMv7–M) you’d typically see “BKPT 0xAB” opcode instead of SVC’s. For the same reason as the ITM_SendChar, currently semihosting is not supported on the mbed.
So, ideally, it would be nice to still be able to use puts/printf (the greatest debug tool of all) but redirect the output to our UART; i.e. rehosting.
Rehosting in the Keil environment is very easy, once you know how! It is easy to go down a couple of dead ends, which hopefully I’ll help you avoid.
First, in our main, where we’re using printf, we need to include the following pre-processor directive in main.c:
This guarantees that no functions using the semihosting are included in your application. If you build you will get the following three linker error messages:
linking... .\build\mbed.axf: Error: L6915E: Library reports error: __use_no_semihosting_swi was requested, but _sys_exit was referenced .\build\mbed.axf: Error: L6915E: Library reports error: __use_no_semihosting_swi was requested, but _sys_open was referenced .\build\mbed.axf: Error: L6915E: Library reports error: __use_no_semihosting_swi was requested, but _ttywrch was referenced
This is telling us that the linker cannot find definitions for the functions _sys_exit , _sys_open and _ttywrch, indicating that the standard I/O library is dependent on theses.
_sys_exit is the library exit function. All exits from the library eventually call
_sys_exit(). It must not return and has the prototype :
void _sys_exit(int return_code
_ttywrch writes a character to the console. This function is also used as an error handling routine. It has the prototype:
void _ttywrch(int ch
Finally, _sys_open opens a file and is required by fopen. It should return -1 on an error. The Prototype is:
FILEHANDLE _sys_open(const char *name, int openmode
The type FILEHANDLE is defined in <rt_sys.h> as a typedef for an int.
So, in theory, we should only need to implement those three functions for the application to link! simples…
But building results in the following linker error:
compiling retarget_1.c... linking... .\build\mbed.axf: Error: L6200E: Symbol _sys_open multiply defined (by sys_io.o and retarget_1.o). Target not created
Huh? If it was unresolved before how can it be a duplicate now? Well before you start pursuing this (and loosing the will to live) then don’t and move on…
As with most things in life, the simpler approach is easiest; in this case follow the steps in the manual!
From within the Keil toolchain, under the help menu open the Book Window.
Select the Book “Complete User’s Guide Selection”
Click on the “Contents” tab and navigate to the “Library Retarget File” notes
The Retarget.c file in my current build (version µVision V220.127.116.11) doesn’t exactly match the one in the Keil workbook, but the differences are minor.
The Retarget.c declares two functions:
extern int sendchar(int ch); /* in Serial.c */ extern int getkey(void); /* in Serial.c */
Rather than renaming these to putchar0 and getchar0 I decided it was better (i.e. more portable) to add those functions (sendchar and getkey) to my serial.c file, and let them call putchar0 and getchar0. Rebuilding the project it all linked fine and my previous main now looks like this:
However, on downloading this and running it I didn’t get any output? I’m sure you’re much sharper than me, but I had a moment of head scratching. The the lightbulb came on “Of course nowhere is init_serial0 getting invoked”.
There are a number of places we could make the call to init_serial0():
- As the first line of main
- At the end of the CMSIS function SystemInit
- From $Sub$$main
I discounted calling the serial init in main as it seems inconsistent with the principle of rehosting.
Calling the function at the end of SystemInit is possible the best solution, as the Peripheral Clock has just been configured via the PLL (Phase Lock Loop) in SystemInit. The only downside is that we now have a non-CMSIS-standard system_LPC17xx.c file. I guess it’s not a major issue, but nevertheless, it would be nice not to have to fiddle with this file. Alternatively, we could modify the Reset_Handler assembler in startup_LPC17xx.s to achieve the same results but with the same downsides.
The final option is to use a nice feature of the Keil compiler. Using CMSIS user code normally starts at main. However Keil supports the ability (through weak linkage) to define a function named $Sub$$main. This function, if defined, is called after the runtime library has been initialized, but before main. The function ($Sub$$main) must call $Super$$main, which actually calls the main function. This allows you to place both pre– and post-amble code around the call to main, e.g.
For simplicity, I added the init_serial function (that calls init_serial0) in to serial.c to keep the API clean.
The downsides of this approach is that:
- it is not integrated into CMSIS
- any error messages from the standard library initialization will call the function _ttywrch, which will call putchar before init_serial has been call, so won’t appear in the terminal window.
In reflection I’m not happy with any of the approaches. Ideally there should be a standard call as part of rehosting ( e.g. _sys_open or fopen with STDOUT ? ) where the init_serial could be located. Maybe for another day…
One final item, at the moment I still have to deal with the ‘\r’, the simplest option is to reconfigure Tera Term to map ‘\n’ to “\n\r” or modify putchar to add at ‘\r’ if a ‘\n’ is received.
It is worth noting that Rehosting will work on any ARM target using the ARM compiler and is not limited to the mbed.
- Disassembling a Cortex-M raw binary file with Ghidra - December 20, 2022
- Using final in C++ to improve performance - November 14, 2022
- Understanding Arm Cortex-M Intel-Hex (ihex) files - October 12, 2022
Co-Founder and Director of Feabhas since 1995.
Niall has been designing and programming embedded systems for over 30 years. He has worked in different sectors, including aerospace, telecomms, government and banking.
His current interest lie in IoT Security and Agile for Embedded Systems.
Pingback: Sticky Bits » Blog Archive » User I/O from mbed with CMSIS-DAP
Regarding "One final item, at the moment I still have to deal with the ‘\r’, the simplest option is to reconfigure Tera Term to map ‘\n’ to “\n\r” or modify putchar to add at ‘\r’ if a ‘\n’ is received."
You shouldn't map '\r' to "\r\n", this is because '\r' can and is used without '\n'. (like when one wants to display percentage of completion on the same line).
You should instead make sure that every line you write to the terminal ends with "\r\n".
Hi Asique, What I'm trying to convey is that there is a choice. When working on a hosted system people are used to just using '\n' which is mapped on to "\n\r". If you're trying to write portable code, or do a level of host testing before target testing, the issue of '\r' is just a bit of a pain. I've also seen people #define CR using #ifdef to map between host and target.