In this
article:
The Machine Specific Part
Putting it all together
Navigation:
HomeHardware
Software
Techniques
Controllers
Reviews
Index
Introduction
So in the blink article I mentioned how the lack of an operating system meant that many embedded programs ran with little use of the C library. However, many is not all, and it is convienient to use them if you can provide enough scaffolding for the C library in order to call those functions. This process of providing the scaffolding is called retargeting.
Retargeting GCC Embedded
The version of the C library that is used by the GCC Embedded project is called
Newlib, and they use a variant called Newlib-nano. The goal of this
particular article will be to get the libc functions of printf
and fgets
to work
so that we might think about writing some code which interacts with us via a serial
port. There is an piece of example code in the gcc distribution in the
arm-none-eabi-4.8-xxx/share/gcc-arm-none-eabi/samples/src/retarget
directory which
we will start with.
The Easy Part
The simple part is the generic code, that is just a variant of hello.c
which is shown
below:
#include <stdio.h> void main() { printf("Hello, world!"); for (;;); }
As you can see it includes the standard I/O header stdio.h
and it calls printf
on the
string Hello Word!, then goes into an infinite loop. Compile and run this on any Linux
system or OS X system and it prints out the string and hangs until you hit ^C.
The Less Easy Part
That program works because there is an operating system and its bound into the implementation of the subroutine printf which has other routines it calls and an entire I/O eco-system that it uses to put those things together. When retargeting embedded systems you have to fake all of that, and that is done with some ‘well known’ (basically documented) routines that are weakly bound to null routines in the embedded version of nanolib but you can implement to make real versions. The sample retarget code starts out pretty bare bones.
void retarget_init() { // Initialize UART } int _write (int fd, char *ptr, int len) { /* Write "len" of char from "ptr" to file id "fd" * Return number of char written. * Need implementing with UART here. */ return len; } int _read (int fd, char *ptr, int len) { /* Read "len" of char to "ptr" from file id "fd" * Return number of char read. * Need implementing with UART here. */ return len; } void _ttywrch(int ch) { /* Write one char "ch" to the default console * Need implementing with UART here. */ } /* SystemInit will be called before main */ void SystemInit() { retarget_init(); }
It too is pretty simple, but what you see is that there are functions like _write
which
use the C notion of a leading underscore means “private” and some key functions like read
and write.
The Machine Specific Part
Now we step away from being able to write generic code that runs anywhere, and like our
blink example we need to provide our own startup script with interrupt
vectors, and a linker script tuned to our board or system. Finally we will need to add
code to the retarget.c
file that initializes a serial port and then sends characters to
that serial port.
The linker script can be the same, this example targets the same Butterfly board as the blink example, but the startup needs to be updated if you are going to use interrupts from the STM32F4 series.
Adding in the GPIO definitions
In the blink code, I hand coded the bits that were set when configuring GPIOD. For small
cases you can do that, for more complex cases you need some previously validated #define
definitions.
For this example there are two choices, you can use the ones provided by ST Micro as part of their Firmware Package, but those come with the MCD-ST Liberty SW License which basically says you can’t use anything in their software (including #defines) for any processor other than ones made by ST Micro, and you don’t get a warranty, and they can sue you if you try to sell software you make that uses their software, in whole or in part. Or you can use an open source library like libopencm3 which is covered under an LGPL3 license and is actually ok with you selling software you made with it as long you give back any changes you make to the core libraries. I picked option #2.
This decision has two direct effects, one the symbol names for the interrupt vectors ideally need to be compatible with the weak references in libopencm3, (if they aren’t and you use the default loc3 startup code you won’t bind to the ISRs correctly), and two you will need to be sure that libopencm3 and its include files are available during compile and linking by using specific options in the Makefile.
A slightly altered flow
With these changes when the program is loaded it will have a slightly altered flow. The
startup code will initialize memory that needs to be initialized, then it will call
SystemInit, and then it will call whatever you defined as _START
when you compiled the
startup code (defaults to _start
, I change it in the
Makefile to main
).
Using Libopencm3 to initialize the USART
The USART that is connected to the DB–9 port on the Embest baseboard is USART6. The GPIO pins are Port C, pins 6 and 7. There is no hardware flow control available. So initialization is simple.
#include <stdint.h> #include <libopencm3/stm32/rcc.h> #include <libopencm3/stm32/gpio.h> #include <libopencm3/stm32/usart.h> /* This is the original retarget.c but with code filled in for * the STM32F4 and the Butterfly board. */ void retarget_init() { // Initialize UART /* *** Added Code Begin *** */ /* MUST enable the GPIO clock in ADDITION to the USART clock */ rcc_periph_clock_enable(RCC_GPIOC); /* This example uses PD5 and PD6 for Tx and Rx respectively * but other pins are available for this role on USART6 (our chosen * USART) as well, such as PA2 and PA3. You can also split them * so PA2 for Tx, PD6 for Rx but you would have to enable both * the GPIOA and GPIOD clocks in that case */ gpio_mode_setup(GPIOC, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO6 | GPIO7); /* Actual Alternate function number (in this case 7) is part * depenedent, CHECK THE DATA SHEET for the right number to * use. */ gpio_set_af(GPIOC, GPIO_AF8, GPIO6 | GPIO7); /* This then enables the clock to the USART6 peripheral which is * attached inside the chip to the APB2 bus. Different peripherals * attach to different buses, and even some UARTS are attached to * APB1 and some to APB2, again the data sheet is useful here. */ rcc_periph_clock_enable(RCC_USART6); /* Set up USART/UART parameters using the libopencm3 helper functions */ usart_set_baudrate(USART6, 115200); usart_set_databits(USART6, 8); usart_set_stopbits(USART6, USART_STOPBITS_1); usart_set_mode(USART6, USART_MODE_TX_RX); usart_set_parity(USART6, USART_PARITY_NONE); usart_set_flow_control(USART6, USART_FLOWCONTROL_NONE); usart_enable(USART6); /* *** Added Code End *** */ }
The include files for the libopencm3 are pulled in, the interesting ones are the gpio.h
and usart.h
, and using the definitions USART6 is initialized and enabled. Further the
GPIO pins PC6 and PC7 are converted to their alternate function (USART6). The baud rate
is 115200, 8 bits, one stop bit, and no parity. I use the GNU screen program to connect
my Linux host to the USB serial adapter, the command is screen /dev/ttyUSB0 115200
.
Now if you were doing more I/O than just serial I/O you would need to know which number
was in the fd
paramater being passed to _write
and _read
. The important ones for
serial I/O are 0 is the standard input, 1 is the standard output, and 2 is the standard
error output. In this case everything is going out to the same port so I don’t check the
value.
Typical _write
implementation is shown below.
int _write (int fd, char *ptr, int len) { /* Write "len" of char from "ptr" to file id "fd" * Return number of char written. * Need implementing with UART here. */ int i = 0; while (*ptr && (i < len)) { usart_send_blocking(USART6, *ptr); if (*ptr == '\n') { usart_send_blocking(USART6, '\r'); } i++; ptr++; } return i; }
The implementation is simply, given a pointer to characters and a length, send that many characters to the USART.
The other only interesting bit is that when SystemInit
is called I take that opportunity
to set up the system clock. That is shown here:
/* SystemInit will be called before main */ void SystemInit() { /* *** Added code *** */ /* Base board frequency, set to 168Mhz */ rcc_clock_setup_hse_3v3(&hse_8mhz_3v3[CLOCK_3V3_168MHZ]); /* *** End Added Code *** */ retarget_init(); }
And yes, presumably I could put the USART intialization here as well, I didn’t because
the original sample code had call to retarget_init
and I was following it closely.
Putting it all together
Putting it all together is pretty easy, the Makefile compiles the startup code, the retarget code, and main then links it all together with the same ldscript used for blink. The complete project is on Github if you want to fork it an play with it but I recommend doing it from scratch as it will help put that information deeper into your brain.