4.2. The C I/O Functions

The C I/O functions make it possible to access the host’s operating system to perform I/O. The capability to perform I/O on the host gives you more options when debugging and testing code.

The I/O functions are logically divided into layers: high level, low level, and device-driver level.

With properly written device drivers, the C-standard high-level I/O functions can be used to perform I/O on custom user-defined devices. This provides an easy way to use the sophisticated buffering of the high-level I/O functions on an arbitrary device.

The formatting rules for long long data types require ll (lowercase LL) in the format string. For example:

printf("%lld", 0x0011223344556677);
printf("llx", 0x0011223344556677);

Note

Debugger Required for Default HOST For the default HOST device to work, there must be a debugger to handle the C I/O requests; the default HOST device cannot work by itself in an embedded system. To work in an embedded system, you will need to provide an appropriate driver for your system.

Note

C I/O Mysteriously Fails If there is not enough space on the heap for a C I/O buffer, operations on the file will silently fail. If a call to printf() mysteriously fails, this may be the reason. The heap needs to be at least large enough to allocate a block of size BUFSIZ (defined in stdio.h) for every file on which I/O is performed, including stdout, stdin, and stderr, plus allocations performed by the user’s code, plus allocation bookkeeping overhead. Alternately, declare a char array of size BUFSIZ and pass it to setvbuf to avoid dynamic allocation. To set the heap size, use the --heap_size option when linking (refer to Linker Description).

Note

Open Mysteriously Fails The run-time support limits the total number of open files to a small number relative to general-purpose processors. If you attempt to open more files than the maximum, you may find that the open will mysteriously fail. You can increase the number of open files by extracting the source code from rts.src and editing the constants controlling the size of some of the C I/O data structures. The macro _NFILE controls how many FILE (fopen) objects can be open at one time (stdin, stdout, and stderr count against this total). (See also FOPEN_MAX.) The macro _NSTREAM controls how many low-level file descriptors can be open at one time (the low-level files underlying stdin, stdout, and stderr count against this total). The macro _NDEVICE controls how many device drivers are installed at one time (the HOST device counts against this total).

4.2.1. High-Level I/O Functions

The high-level functions are the standard C library of stream I/O routines (printf, scanf, fopen, getchar, and so on). These functions call one or more low-level I/O functions to carry out the high-level I/O request. The high-level I/O routines operate on FILE pointers, also called streams.

Portable applications should use only the high-level I/O functions.

To use the high-level I/O functions:

  • Include the header file stdio.h for each module that references a function.

  • Allow for 320 bytes of heap space for each I/O stream used in your program. A stream is a source or destination of data that is associated with a peripheral, such as a terminal or keyboard. Streams are buffered using dynamically allocated memory that is taken from the heap. More heap space may be required to support programs that use additional amounts of dynamically allocated memory (calls to malloc()). To set the heap size, use the --heap_size option when linking; see Define Heap Size (--heap_size Option).

For example, given the following C program in a file named main.c:

#include <stdio.h>

void main()
{
    FILE *fid;

    fid = fopen("myfile","w");
    fprintf(fid,"Hello, world\n");
    fclose(fid);

    printf("Hello again, world\n");
}

Issuing the following compiler command compiles, links, and creates the file main.out from the run-time-support library:

tiarmclang main.c -Xlinker --heap_size=400 -Xlinker --library=rtsv4_A_be_eabi.lib -Xlinker --output_file=main.out

Executing main.out results in

Hello, world

being output to a file and

Hello again, world

being output to your host’s stdout window.

4.2.1.1. Formatting and the Format Conversion Buffer

The internal routine behind the C I/O functions—such as printf(), vsnprintf(), and snprintf()—reserves stack space for a format conversion buffer. The buffer size is set by the macro FORMAT_CONVERSION_BUFSIZE, which is defined in format.h. Consider the following issues before reducing the size of this buffer:

  • The default buffer size is 510 bytes. If MINIMAL is defined, the size is set to 32, which allows integer values without width specifiers to be printed.

  • Each conversion specified with %xxxx (except %s) must fit in FORMAT_CONVERSION_BUFSIZE. This means any individual formatted float or integer value, accounting for width and precision specifiers, needs to fit in the buffer. Since the actual value of any representable number should easily fit, the main concern is ensuring the width and/or precision size meets the constraints.

  • The length of converted strings using %s are unaffected by any change in FORMAT_CONVERSION_BUFSIZE. For example, you can specify printf("%s value is %d",some_really_long_string,intval) without a problem.

  • The constraint is for each individual item being converted. For example, a format string of %d item1 %f item2 %e item3 does not need to fit in the buffer. Instead, each converted item specified with a % format must fit.

  • There is no buffer overrun check.

4.2.2. Overview of Low-Level I/O Implementation

The low-level functions are comprised of seven basic I/O functions: open, read, write, close, lseek, rename, and unlink. These low-level routines provide the interface between the high-level functions and the device-level drivers that actually perform the I/O command on the specified device.

The low-level functions are designed to be appropriate for all I/O methods, even those which are not actually disk files. Abstractly, all I/O channels can be treated as files, although some operations (such as lseek) may not be appropriate. See Device-Driver Level I/O Functions for more details.

The low-level functions are inspired by, but not identical to, the POSIX functions of the same names.

The low-level functions operate on file descriptors. A file descriptor is an integer returned by open, representing an opened file. Multiple file descriptors may be associated with a file; each has its own independent file position indicator.

4.2.2.1. open – Open File for I/O

Syntax

#include <file.h>

int open (const char * path,
          unsigned     flags,
          int          file_descriptor );

Description The open function opens the file specified by path and prepares it for I/O.

  • The path is the filename of the file to be opened, including an optional directory path and an optional device specifier (see The device Prefix).

  • The flags are attributes that specify how the file is manipulated. Low-level I/O routines allow or disallow some operations depending on the flags used when the file was opened. Some flags may not be meaningful for some devices, depending on how the device implements files. The flags are specified using the following symbols:

O_RDONLY (0x0000) /* open for reading */
O_WRONLY (0x0001) /* open for writing */
O_RDWR (0x0002) /* open for read & write */
O_APPEND (0x0008) /* append on each write */
O_CREAT (0x0200) /* open with file create */
O_TRUNC (0x0400) /* open with truncation */
O_BINARY (0x8000) /* open in binary mode */
  • The file_descriptor is assigned by open to an opened file. The next available file descriptor is assigned to each new file opened.

Return Value The function returns one of the following values:

  • non-negative value is file descriptor if successful

  • -1 on failure

4.2.2.2. close – Close File for I/O

Syntax

#include <file.h>

int close (int file_descriptor);

Description The close function closes the file associated with file_descriptor. The file_descriptor is the number assigned by open to an opened file.

Return Value The return value is one of the following:

  • 0 if successful

  • -1 on failure

4.2.2.3. read – Read Characters from a File

Syntax

#include <file.h>

int read ( int      file_descriptor,
           char *   buffer,
           unsigned count );

Description The read function reads count characters into the buffer from the file associated with file_descriptor.

  • The file_descriptor is the number assigned by open to an opened file.

  • The buffer is where the read characters are placed.

  • The count is the number of characters to read from the file.

Return Value The function returns one of the following values:

  • 0 if EOF was encountered before any characters were read

  • positive value to indicate number of characters read (may be less than count)

  • -1 on failure

4.2.2.4. write – Write Characters to a File

Syntax

#include <file.h>

int write ( int          file_descriptor,
            const char * buffer,
            unsigned     count );

Description The write function writes the number of characters specified by count from the buffer to the file associated with file_descriptor.

  • The file_descriptor is the number assigned by open to an opened file.

  • The buffer is where the characters to be written are located.

  • The count is the number of characters to write to the file.

Return Value The function returns one of the following values:

  • positive value to indicate number of characters written if successful (may be less than count)

  • -1 on failure

4.2.2.5. lseek – Set File Position Indicator

Syntax for C

#include <file.h>

off_t lseek ( int   file_descriptor,
              off_t offset,
              int   origin );

Description The lseek function sets the file position indicator for the given file to a location relative to the specified origin. The file position indicator measures the position in characters from the beginning of the file.

  • The file_descriptor is the number assigned by open to an opened file.

  • The offset indicates the relative offset from the origin in characters.

  • The origin is used to indicate which of the base locations the offset is measured from. The origin must be one of the following macros:

    • SEEK_SET (0x0000) Beginning of file

    • SEEK_CUR (0x0001) Current value of the file position indicator

    • SEEK_END (0x0002) End of file

Return Value The return value is one of the following:

  • positive value to indicate new value of the file position indicator if successful

  • (off_t)-1 on failure

4.2.2.7. rename – Rename File

Syntax for C

#include {<stdio.h> \| <file.h>}

int rename ( const char * old_name,
             const char * new_name );

Syntax for C++

#include {<cstdio> \| <file.h>}

int std::rename ( const char * old_name,
                  const char * new_name );

Description The rename function changes the name of a file.

  • The old_name is the current name of the file.

  • The new_name is the new name for the file.

Note

The optional device specified in the new name must match the device of the old name. If they do not match, a file copy would be required to perform the rename, and rename is not capable of this action.

Return Value The function returns one of the following values:

  • 0 if successful

  • -1 on failure

Note

Although rename is a low-level function, it is defined by the C standard and can be used by portable applications.

4.2.3. Device-Driver Level I/O Functions

At the next level are the device-level drivers. They map directly to the low-level I/O functions. The default device driver is the HOST device driver, which uses the debugger to perform file operations. The HOST device driver is automatically used for the default C streams stdin, stdout, and stderr.

The HOST device driver shares a special protocol with the debugger running on a host system so that the host can perform the C I/O requested by the program. Instructions for C I/O operations that the program wants to perform are encoded in a special buffer named _CIOBUF_ in the .cio section. The debugger halts the program at a special breakpoint (C$$IO$$), reads and decodes the target memory, and performs the requested operation. The result is encoded into _CIOBUF_, the program is resumed, and the target decodes the result.

The HOST device is implemented with seven functions, HOSTopen, HOSTclose, HOSTread, HOSTwrite, HOSTlseek, HOSTunlink, and HOSTrename, which perform the encoding. Each function is called from the low-level I/O function with a similar name.

A device driver is composed of seven required functions. Not all function need to be meaningful for all devices, but all seven must be defined. Here we show the names of all seven functions as starting with DEV, but you may choose any name except for HOST.

4.2.3.1. DEV_open – Open File for I/O

Syntax

int DEV_open ( const char * path,
               unsigned     flags ,
               int          llv_fd );

Description This function finds a file matching path and opens it for I/O as requested by flags.

  • The path is the filename of the file to be opened. If the name of a file passed to open has a device prefix, the device prefix will be stripped by open, so DEV_open will not see it. (See The device Prefix for details on the device prefix.)

  • The flags are attributes that specify how the file is manipulated. See POSIX for further explanation of the flags. The flags are specified using the following symbols:

O_RDONLY (0x0000) /* open for reading */
O_WRONLY (0x0001) /* open for writing */
O_RDWR   (0x0002) /* open for read & write */
O_APPEND (0x0008) /* append on each write */
O_CREAT  (0x0200) /* open with file create */
O_TRUNC  (0x0400) /* open with truncation */
O_BINARY (0x8000) /* open in binary mode */
  • The llv_fd is treated as a suggested low-level file descriptor. This is a historical artifact; newly-defined device drivers should ignore this argument. This differs from the low-level I/O open function.

This function must arrange for information to be saved for each file descriptor, typically including a file position indicator and any significant flags. For the HOST version, all the bookkeeping is handled by the debugger running on the host machine. If the device uses an internal buffer, the buffer can be created when a file is opened, or the buffer can be created during a read or write.

Return Value This function must return -1 to indicate an error if for some reason the file could not be opened; such as the file does not exist, could not be created, or there are too many files open. The value of errno may optionally be set to indicate the exact error (the HOST device does not set errno). Some devices might have special failure conditions; for instance, if a device is read-only, a file cannot be opened O_WRONLY.

On success, this function must return a non-negative file descriptor unique among all open files handled by the specific device. The file descriptor need not be unique across devices. The device file descriptor is used only by low-level functions when calling the device-driver-level functions. The low-level function open allocates its own unique file descriptor for the high-level functions to call the low-level functions. Code that uses only high-level I/O functions need not be aware of these file descriptors.

4.2.3.2. DEV_close – Close File for I/O

Syntax

int DEV_close ( int dev_fd );

Description This function closes a valid open file descriptor.

On some devices, DEV_close may need to be responsible for checking if this is the last file descriptor pointing to a file that was unlinked. If so, it is responsible for ensuring that the file is actually removed from the device and the resources reclaimed, if appropriate.

Return Value This function should return -1 to indicate an error if the file descriptor is invalid in some way, such as being out of range or already closed, but this is not required. The user should not call close() with an invalid file descriptor.

4.2.3.3. DEV_read – Read Characters from a File

Syntax

int DEV_read ( int      dev_fd ,
               char *   buf ,
               unsigned count );

Description The read function reads count bytes from the input file associated with dev_fd.

  • The dev_fd is the number assigned by open to an opened file.

  • The buf is where the read characters are placed.

  • The count is the number of characters to read from the file.

Return Value This function must return -1 to indicate an error if for some reason no bytes could be read from the file. This could be because of an attempt to read from a O_WRONLY file, or for device-specific reasons.

If count is 0, no bytes are read and this function returns 0.

This function returns the number of bytes read, from 0 to count. 0 indicates that EOF was reached before any bytes were read. It is not an error to read less than count bytes; this is common if there are not enough bytes left in the file or the request was larger than an internal device buffer size.

4.2.3.4. DEV_write – Write Characters to a File

Syntax

int DEV_write ( int          dev_fd ,
                const char * buf ,
                unsigned     count );

Description This function writes count bytes to the output file.

  • The dev_fd is the number assigned by open to an opened file.

  • The buffer is where the write characters are placed.

  • The count is the number of characters to write to the file.

Return Value This function must return -1 to indicate an error if for some reason no bytes could be written to the file. This could be because of an attempt to read from a O_RDONLY file, or for device-specific reasons.

4.2.3.5. DEV_lseek – Set File Position Indicator

Syntax

off_t DEV_lseek ( int   dev_fd,
                  off_t offset,
                  int   origin );

Description This function sets the file’s position indicator for this file descriptor as lseek – Set File Position Indicator.

If lseek is supported, it should not allow a seek to before the beginning of the file, but it should support seeking past the end of the file. Such seeks do not change the size of the file, but if it is followed by a write, the file size will increase.

Return Value If successful, this function returns the new value of the file position indicator.

This function must return -1 to indicate an error if for some reason no bytes could be written to the file. For many devices, the lseek operation is nonsensical (e.g. a computer monitor).

4.2.3.7. DEV_rename – Rename File

Syntax

int DEV_rename ( const char * old_name,
                 const char * new_name );

Description This function changes the name associated with the file.

  • The old_name is the current name of the file.

  • The new_name is the new name for the file.

Return Value This function must return -1 to indicate an error if for some reason the file could not be renamed, such as the file doesn’t exist, or the new name already exists.

Note

It is inadvisable to allow renaming a file so that it is on a different device. In general this would require a whole file copy, which may be more expensive than you expect.

If successful, this function returns 0.

4.2.4. Adding a User-Defined Device Driver for C I/O

The function add_device allows you to add and use a device. When a device is registered with add_device, the high-level I/O routines can be used for I/O on that device.

You can use a different protocol to communicate with any desired device and install that protocol using add_device; however, the HOST functions should not be modified. The default streams stdin, stdout, and stderr can be remapped to a file on a user-defined device instead of HOST by using freopen() as in the following example. If the default streams are reopened in this way, the buffering mode will change to _IOFBF (fully buffered). To restore the default buffering behavior, call setvbuf on each reopened file with the appropriate value (_IOLBF for stdin and stdout, _IONBF for stderr).

The default streams stdin, stdout, and stderr can be mapped to a file on a user-defined device instead of HOST by using freopen() as shown in the following example. Each function must set up and maintain its own data structures as needed. Some function definitions perform no action and should just return.

#include <stdio.h>
#include <file.h>
#include "mydevice.h"

void main()
{
    add_device("mydevice", _MSA,
                MYDEVICE_open, MYDEVICE_close,
                MYDEVICE_read, MYDEVICE_write,
                MYDEVICE_lseek, MYDEVICE_unlink, MYDEVICE_rename);

    /*-----------------------------------------------------------------------*/
    /* Re-open stderr as a MYDEVICE file */
    /*-----------------------------------------------------------------------*/
if (!freopen("mydevice:stderrfile", "w", stderr))
{
    puts("Failed to freopen stderr");
    exit(EXIT_FAILURE);
}

/*-----------------------------------------------------------------------*/
/* stderr should not be fully buffered; we want errors to be seen as */
/* soon as possible. Normally stderr is line-buffered, but this example */
/* doesn't buffer stderr at all. This means that there will be one call */
/* to write() for each character in the message. */
/*-----------------------------------------------------------------------*/
if (setvbuf(stderr, NULL, _IONBF, 0))
{
    puts("Failed to setvbuf stderr");
    exit(EXIT_FAILURE);
}

/*-----------------------------------------------------------------------*/
/* Try it out! */
/*-----------------------------------------------------------------------*/
printf("This goes to stdout\n");
fprintf(stderr, "This goes to stderr\n"); }

Note

Use Unique Function Names The function names open, read, write, close, lseek, rename, and unlink are used by the low-level routines. Use other names for the device-level functions that you write.

Use the low-level function add_device() to add your device to the device_table. The device table is a statically defined array that supports n devices, where n is defined by the macro _NDEVICE found in stdio.h/cstdio.

The first entry in the device table is predefined to be the host device on which the debugger is running. The low-level routine add_device() finds the first empty position in the device table and initializes the device fields with the passed-in arguments. For a complete description, see add_device – Add Device to Device Table.

4.2.5. The device Prefix

A file can be opened to a user-defined device driver by using a device prefix in the pathname. The device prefix is the device name used in the call to add_device followed by a colon. For example:

FILE *fptr = fopen("mydevice:file1", "r");
int fd = open("mydevice:file2, O_RDONLY, 0);

If no device prefix is used, the HOST device will be used to open the file.

4.2.5.1. add_device – Add Device to Device Table

Syntax for C

#include <file.h>

int add_device(
       char * name,
       unsigned flags,
       int   (* dopen )(const char *path, unsigned flags, int llv_fd),
       int   (* dclose )(int dev_fd),
       int   (* dread )(int dev_fd, char *buf, unsigned count),
       int   (* dwrite )(int dev_fd, const char *buf, unsigned count),
       off_t (* dlseek )(int dev_fd, off_t ioffset, int origin),
       int   (* dunlink )(const char * path),
       int   (* drename )(const char *old_name, const char *new_name) );

Defined in lowlev.c (in the lib/src subdirectory of the compiler installation)

Description The add_device function adds a device record to the device table allowing that device to be used for I/O from C. The first entry in the device table is predefined to be the HOST device on which the debugger is running. The function add_device() finds the first empty position in the device table and initializes the fields of the structure that represent a device.

To open a stream on a newly added device use fopen( ) with a string of the format devicename:filename as the first argument.

  • The name is a character string denoting the device name. The name is limited to 8 characters.

  • The flags are device characteristics. The defined flags are as follows, and more flags can be added by defining them in file.h:

    • _SSA Denotes that the device supports only one open stream at a time

    • _MSA Denotes that the device supports multiple open streams

  • The dopen, dclose, dread, dwrite, dlseek, dunlink, and drename specifiers are function pointers to the functions in the device driver that are called by the low-level functions to perform I/O on the specified device. You must declare these functions with the interface specified in Overview of Low-Level I/O Implementation. The device driver for the HOST that the Arm debugger is run on are included in the C I/O library.

Return Value The function returns one of the following values:

  • 0 if successful

  • -1 on failure

Example The following example illustrates adding and using a device for C I/O. It does the following:

  • Adds the device mydevice to the device table

  • Opens a file named test on that device and associates it with the FILE pointer fid

  • Writes the string Hello, world into the file

  • Closes the file

#include <file.h>
#include <stdio.h>
/****************************************************************************/
/* Declarations of the user-defined device drivers */
/****************************************************************************/
extern int MYDEVICE_open(const char *path, unsigned flags, int fno);
extern int MYDEVICE_close(int fno);
extern int MYDEVICE_read(int fno, char *buffer, unsigned count);
extern int MYDEVICE_write(int fno, const char *buffer, unsigned count);
extern off_t MYDEVICE_lseek(int fno, off_t offset, int origin);
extern int MYDEVICE_unlink(const char *path);
extern int MYDEVICE_rename(const char *old_name, char *new_name);

main()
{
    FILE *fid;
    add_device("mydevice", _MSA, MYDEVICE_open, MYDEVICE_close, MYDEVICE_read,
                MYDEVICE_write, MYDEVICE_lseek, MYDEVICE_unlink, MYDEVICE_rename);
    fid = fopen("mydevice:test","w");
    fprintf(fid,"Hello, world\n");
    fclose(fid);
}