BSPACM  20150113
Board Support Package for ARM Cortex-M Microcontrollers
Interfacing with newlib (libc)

Table of Contents

Newlib is an open-source implementation of the standard C library, and is used as the libc implementation in BSPACM's primary toolchain, GNU Tools for ARM Embedded Processors. It was designed for use in embedded systems, using a small set of externally-supplied functions to interface with the system.

The classic reference (in my experience) for porting newlib to a new platform is: http://neptune.billgatliff.com/newlib.html

Space Optimization with newlib-nano

Although newlib was designed for embedded systems, it has historically been unusable on microcontrollers due to its relatively large memory requirements, e.g. 1 kiB SRAM to support malloc, 25 kiB FLASH to support printf, etc.

With the addition of a variant called newlib-nano, this is no longer the case. newlib-nano was implemented specifically for ARM within the GNU Tools for ARM Embedded Processors, and some of its enhancements have been merged upstream.

By adding -specs=nano.specs to the gcc link command, a reduced-size libc is linked in. The effect of this is significant. The size of the classic "hello" program compiled for the EFM32GG-STK3700 kit with traditional newlib under 4_8-2013q4 is:

llc[56]$ rm app.axf ; make WITH_NANO=0 app.axf
arm-none-eabi-gcc -o app.axf -L/prj/arm/bspacm//board/efm32gg-stk3700 -ggdb -Wl,-gc-sections  -Wl,-T,/prj/arm/bspacm//toolchain/GCC/gcc_arm.ld -L/prj/arm/bspacm//device/efm32/efm32gg/efm32gg990f1024 -mthumb -mcpu=cortex-m3   system_efm32gg.o main.o startup_ARMCM3.o periph_config.o -Wl,--start-group -lc -lemlib -lbspacm-fdops -lbspacm  -Wl,--end-group
arm-none-eabi-size app.axf
   text    data     bss     dec     hex filename
  27832    2340     116   30288    7650 app.axf

The same program linked with newlib-nano is one third the FLASH, and one tenth the SRAM:

llc[57]$ rm app.axf ; make WITH_NANO=1 app.axf
arm-none-eabi-gcc -o app.axf -L/prj/arm/bspacm//board/efm32gg-stk3700 -ggdb -Wl,-gc-sections  -Wl,-T,/prj/arm/bspacm//toolchain/GCC/gcc_arm.ld -L/prj/arm/bspacm//device/efm32/efm32gg/efm32gg990f1024 -specs=nano.specs -mthumb -mcpu=cortex-m3   system_efm32gg.o main.o startup_ARMCM3.o periph_config.o -Wl,--start-group -lc_s -lemlib -lbspacm-fdops -lbspacm  -Wl,--end-group
arm-none-eabi-size app.axf
   text    data     bss     dec     hex filename
   8720     232      72    9024    2340 app.axf

BSPACM defaults to use WITH_NANO=1 but this can be overridden on the command line if necessary.

If text formatting functions alone are required alternative libraries such as embtextf may be used. Initial experimentation suggests that embtextf saves only about 1 kiB flash and 50 bytes SRAM relative to newlib-nano. At this time, there is no built-in support to incorporate embtextf support in BSPACM.

The following behavioral differences/limitations are known to exist with newlib-nano:

System Interface

newlib maps standard C functions to a specific implementation environment through a chain of functions, for example:

If nothing provides an implementation of _write() but the application requires one, the application will fail to link.

The standard solution for newlib is to add -specs=nosys.specs to the gcc linker command line. This links in a separate library with implementations for all required system functions. Most of these simple return an error; some (like _sbrk()) provide a usable definition.

To simplify customization, BSPACM provides weak definitions of all the system functions in its core library libbspacm.a, eliminating the need to add -specs=nosys.specs. The following function and data object definitions may be overridden by the application:

char ** environ;
int _chown (const char * path, uid_t owner, gid_t group);
int_execve (const char * filename, char * const argv[], char * const envp[]);
pid_t _fork (void);
pid_t _getpid (void);
int _gettimeofday (struct timeval * tv, struct timezone * tz);
int _kill (pid_t pid, int sig);
int _link (const char * oldpath, const char * newpath);
ssize_t _readlink (const char * path, char * buf, size_t bufsiz);
int _stat (const char * path, struct stat * buf);
int _symlink (const char * oldpath, const char * newpath);
clock_t _times (struct tms *buf);
int _unlink (const char * pathname);
pid_t _wait (int * status);
void _exit (int status);

File Descriptor Operations provides an framework for the following functions, which may instead be supplied by the application:

int _close (int fd);
int _fstat (int fd, struct stat * buf);
int _isatty (int fd);
off_t _lseek (int fd, off_t offset, int whence);
int _open (const char * pathname, int flags);
ssize_t _read (int fd, void * buf, size_t count);
ssize_t _write (int fd, const void * buf, size_t count);

Heap Management describes how to override this function:

void * _sbrk (ptrdiff_t increment);

Heap Management

The standard ARM SRAM layout comprises initialized data objects followed by heap space and stack space. The stack grows downward as material is pushed and popped; the heap grows upward only when dynamic memory is allocated through the _sbrk() system call. BSPACM allows significant customization of the policies that control heap allocation.

The standard ARM startup file supports two preprocessor macros that specify memory to be reserved for the initial stack and heap. These can be controlled within BSPACM:

These macro values propagate to symbols that specify SRAM boundaries for heap and stack, following all application-declared data.

BSPACM provides a make variable NEWLIB_SBRK which selects among the following policies for dynamic memory management. Explicit selection of an allocation policy by assigning one of the following values to NEWLIB_SBRK will link the corresponding policy into the application even if dynamic memory allocation is not done. Each policy invokes _bspacm_sbrk_error() if allocation fails, allowing diagnosis and recovery to be customized.

Leaving NEWLIB_SBRK unset causes a weak reference to the unlimitedstack implementation, providing good default behavior without increasing application size if no dynamic memory allocation is performed. A user application may supply its own non-weak definition of _sbrk() if none of the following policies is acceptable.

unlimitedstack

An _sbrk() implementation that allows heap (growing up) and stack (growing down) to share a region of memory, with a minimum size reserved for the stack but allowing for the stack to grow below that point. An error is indicated if the new break point would encroach into the reserved stack space or the currently used stack space. This is the default policy for BSPACM, and is closely related to dynstack which is the policy implemented by newlib's -specs=nosys.specs option.

heap

An _sbrk() implementation that allocates within the reserved heap. An error is indicated if the reserved heap size would be exceeded. There is no check against the current stack pointer.

fatal

An _sbrk() implementation that rejects any attempt to allocate memory dynamically. The behavior is equivalent to the heap policy with a zero-sized heap.

fixedstack

An _sbrk() implementation that allows heap (growing up) to grow to the bottom of reserved stack region. An error is indicated if the new program break would encroach into the reserved stack space. There is no check against the current stack pointer. This policy is preferred to unlimitedstack when code may be executing in tasks where the stack frame is in previously allocated memory, making comparison with the stack pointer invalid.

dynstack

An _sbrk() implementation that allows heap (growing up) and stack (growing down) to share a region of memory. An error is indicated if the new break point would encroach into the current stack space. (This eliminates the minimum reserved stack that is checked by unlimitedstack.)

File Descriptor Operations

In <bspacm/newlib/fdops.h> BSPACM provides an infrastructure that implements various functions as well as the function below, so that standard operations on file descriptors are supported:

/* BSPACM extension, no weak definition provided */
int ioctl (int fd, int request, ...);

The libbspacm-fdops.a library implements the bulk of this, with the assistance of application-provided drivers. This feature is disabled by default, and is enabled by setting WITH_FDOPS=1 in make.

xBSPACMnewlibFDOPSdriver is a list of drivers that are supported by the application. Entries in this list are pointers to functions with the signature fBSPACMnewlibFDOPSdriver, taking a path string and some flags, and returning a handle to a file object. The list terminates with a null pointer. The newlib _open() system call invokes each driver function in turn until it finds one that recognizes the path and returns a file object.

Use of WITH_FDOPS=1 will provide a weak definition of this array that contains a single driver, which maps the path "/dev/console" to the default UART on the board. Applications override this by providing a strong definition in the application code.

The C API for file descriptors represents each file by an integer, numbered consecutively from zero. As drivers return file handles, they are stored in the array xBSPACMnewlibFDOPSfile_, with their index used as the file descriptor. The number of entries allowed in this array is provided by nBSPACMnewlibFDOPSfile.

Use of WITH_FDOPS=1 will provide a weak definition of the array and its length, containing three entries which is sufficient for the standard C I/O descriptors. Use of additional file descriptors requires that the application provide an alternative definition of the array and its length.

Application code itself should never need to access the xBSPACMnewlibFDOPSfile_ array. open() will fail if too many devices are opened.

Each file handle references an instance of sBSPACMnewlibFDOPSfile, which provides a pointer to device-specific data as well as a pointer to a sBSPACMnewlibFDOPSfileOps table that provides implementation for each file descriptor operation supported by the device. System calls that would attempt to invoke an unsupported operation return an error indicator and set errno to ENOSYS.

For an example of implementing devices, see src/newlib/uart.c, which provides hBSPACMnewlibFDOPSdriverUARTbind() which binds a generic UART handle to a file handle. This function in turn is used by fBSPACMnewlibFDOPSdriverCONSOLE(), a full driver implementation that provides access to the board-specific default UART as a console device.

Standard I/O Descriptors

The C library assumes that descriptors for stdin (0), stdout (1), and stderr(2) are provided by the system. BSPACM supports this by ensuring that vBSPACMnewlibFDOPSinitializeStdio_() is invoked prior to executing any library function that manipulates file descriptors.

Use of WITH_FDOPS=1 provides a weak definition of vBSPACMnewlibFDOPSinitializeStdio_() which opens "/dev/console" three times with appropriate flags to initialize the first three entries of the descriptor table.

Note
For console support to be complete, the application must also use:
AUX_CPPFLAGS+=-DBSPACM_CONFIG_ENABLE_UART=1
in its Makefile so that hBSPACMdefaultUART is provided.

Copyright 2014-2015, Peter A. Bigot