Overview
This guide is the second in a collection of related guides:
- Building your first embedded image
- Retargeting output to UART (this guide)
- Creating an event-driven embedded image
- Changing Exception level and Security state in an embedded image
This guide shows you how to modify the output mechanism to use the UART capability of the target system.
In Building your first embedded image, we relied on semihosting to handle the output from our embedded image. In this guide, you will modify the output mechanism to send output to a UART serial port. This is useful to know, because embedded systems often have limited display capabilities, or no display capabilities. However, during the debug process it is often useful to be able to print diagnostic messages while a program is running.
Before you begin
To complete this guide, you'll need to have Arm Development Studio Gold Edition installed. If you don't have Arm Development Studio, you can download a 30-day free trial.
Arm Development Studio Gold Edition is a professional quality tool chain developed by Arm to accelerate your first steps in Arm software development. It includes both the Arm Compiler 6 toolchain and the Base_A76x1 model used in this guide. We will use the command-line tools for most of the guide, which means that you will need to configure your environment in order to run Arm Compiler 6 from the command-line.
The individual sections of this guide contain some code examples. These code examples are available to download as a ZIP file:
About semihosting
Semihosting enables code running on a target system, the model, to interface with a debugger running on a host system, the computer, and to use its input and output (I/O) facilities. This means that you can interact with a model or microcontroller that may not possess I/O functionality.
In Building your first embedded image, we used a printf()
call in the code to display the "Hello World" message. This printf()
call triggers a request to a connected debugger through the library function _sys_write
. To see how this works, you can use fromelf
to inspect the compiled code, as shown in the following instruction:
$ fromelf --text -c __image.axf --output=disasm.txt
This command generates a disassembly of __image.axf
in the file disasm.txt
. Within the disassembly, look at _sys_write
, which contains a HLT instruction:
_sys_write 0x00003a74: d100c3ff .... SUB sp,sp,#0x30 0x00003a78: a9027bfd .{.. STP x29,x30,[sp,#0x20] ... 0x00003a9c: d45e0000 ..^. HLT #0xf000 ... 0x00003aa8: d65f03c0 .._. RET
The debugger detects this halt as a semihosting operation, and interprets the _sys_write as a request to output to the console.
You can check if you are using semihosting by adding
__asm(".global __use_no_semihosting\n\t");
to main()
. Linking the image will now throw an error for any functions that use semihosting.
Retarget functions to use UART
Real embedded systems operate without sophisticated debuggers, but many library functions depend on semihosting. You must modify, or retarget, these functions to use the hardware of the target instead of the host system.
To retarget printf()
to use the PL011 UART of the model:
-
Write a driver for the UART. Copy and paste the following code into a new file with the filename
pl011_uart.c
:struct pl011_uart { volatile unsigned int UARTDR; // +0x00 volatile unsigned int UARTECR; // +0x04 const volatile unsigned int unused0[4]; // +0x08 to +0x14 reserved const volatile unsigned int UARTFR; // +0x18 - RO const volatile unsigned int unused1; // +0x1C reserved volatile unsigned int UARTILPR; // +0x20 volatile unsigned int UARTIBRD; // +0x24 volatile unsigned int UARTFBRD; // +0x28 volatile unsigned int UARTLCR_H; // +0x2C volatile unsigned int UARTCR; // +0x30 volatile unsigned int UARTIFLS; // +0x34 volatile unsigned int UARTIMSC; // +0x38 const volatile unsigned int UARTRIS; // +0x3C - RO const volatile unsigned int UARTMIS; // +0x40 - RO volatile unsigned int UARTICR; // +0x44 - WO volatile unsigned int UARTDMACR; // +0x48 }; // Instance of the dual timer struct pl011_uart* uart; // ------------------------------------------------------------ void uartInit(void* addr) { uart = (struct pl011_uart*) addr; // Ensure UART is disabled uart->UARTCR = 0x0; // Set UART 0 Registers uart->UARTECR = 0x0; // Clear the receive status (i.e. error) register uart->UARTLCR_H = 0x0 | PL011_LCR_WORD_LENGTH_8 | PL011_LCR_FIFO_DISABLE | PL011_LCR_ONE_STOP_BIT | PL011_LCR_PARITY_DISABLE | PL011_LCR_BREAK_DISABLE; uart->UARTIBRD = PL011_IBRD_DIV_38400; uart->UARTFBRD = PL011_FBRD_DIV_38400; uart->UARTIMSC = 0x0; // Mask out all UART interrupts uart->UARTICR = PL011_ICR_CLR_ALL_IRQS; // Clear interrupts uart->UARTCR = 0x0 | PL011_CR_UART_ENABLE | PL011_CR_TX_ENABLE | PL011_CR_RX_ENABLE; return; } // ------------------------------------------------------------ int fputc(int c, FILE *f) { // Wait until FIFO or TX register has space while ((uart->UARTFR & PL011_FR_TXFF_FLAG) != 0x0) {} // Write packet into FIFO/tx register uart->UARTDR = c; // Model requires us to manually send a carriage return if ((char)c == '\n') { while ((uart->UARTFR & PL011_FR_TXFF_FLAG) != 0x0){} uart->UARTDR = '\r'; } return 0; }
-
Modify
hello_world.c
to use the UART driver, so that the updated file contains:#include <stdio.h> #include "pl011_uart.h" int main (void) { uartInit((void*)(0x1C090000)); printf("hello world\n"); return 0; }
By redefining
fputc()
to use the UART you have retargetedprintf()
. This is becauseprintf()
ultimately callsfputc()
. -
Rebuild the image:
$ armclang -c -g --target=aarch64-arm-none-eabi startup.s $ armclang -c -g --target=aarch64-arm-none-eabi hello_world.c $ armclang -c -g --target=aarch64-arm-none-eabi pl011_uart.c $ armlink --scatter=scatter.txt --entry=start64 startup.o pl011_uart.o hello_world.o
-
Disassemble the image:
$ fromelf --text -c __image.axf --output=disasm.txt
The disassembly in
disasm.txt
now shows no calls to_sys_write
(although other semihosting functions such as_sys_exit
will be present).
Use Telnet to interface with the UART
All output is now directed to the model’s UART serial port.
To see this output, we are going to use a Telnet client to connect to the UART. We will use Arm Development Studio to help here, because it automatically starts the Telnet client and connects it to the model.
Note: If you want to start a Telnet client and connect to the model manually, you will need to use port 5000 instead of the default port 23. Timing the connection can be difficult, because you must start the client just before the server in the model starts listening.
To interface with the UART using Telnet:
-
Import your executable into Arm Development Studio.
Click File > Open Projects from File System…
-
Click Directory in the Import Projects from File System or Archive dialog to select the folder containing your executable, as this screenshot shows:
-
Click Finish. Your project files should appear in the Project Explorer tab.
-
Right-click the
__image.axf
file, then select Debug As > Debug Configurations to display the Debug Configuration dialog box. You can see the dialog box in the following screenshot: -
Select Generic Arm C/C++ Application, then click the New launch configuration button to create a new debug configuration.
-
In the Name field, give your debug configuration a name, for example
FVP_Base_Cortex-A76x1
. -
On the Connection tab select ARM FVP (Installed with Arm DS) > Base_A76x1 > Bare Metal Debug > Cortex-A76, as this screenshot shows:
-
On the Files tab, click File System and select your
__image.axf
file, as this screenshot shows: -
On the Debugger tab, select Debug from symbol: main as this screenshot shows:
-
Click Apply, then Close.
-
On the Debug Control tab, double-click your debug configuration to start the model and run your application. Execution pauses on entry to
main()
, and you should see the Fast Models window appear, as this screenshot shows: -
Click the Continue toolbar button to continue execution.
Arm Development Studio automatically starts the Telnet client and connects to the model.
The application output, “hello world”, will appear in the Telnet client window after it has been sent over the UART serial interface, as this screenshot shows:
Related information
Here are some resources related to material in this guide:
Next steps
This guide is the second in a series of four guides on the topic of building an embedded image. In this guide, you learned about semihosting, how to retarget functions to use UART and you to use Telnet to interface with the UART.
You can continue learning about building an embedded image in the next guides in the series:
- Creating an Event-Driven Embedded Image
- Changing Exception Level and Security State in an Embedded Image
In case you missed it, the first guide in the series is: