The standard functions that perform input and output are the printf and scanf functions.These functions convert between internal binary and external printable data. In some cases, though, you need to read and write formatted data on other channels, such as other RS232 ports. This section shows how you can extend the I/O library to best implement these function.

Classic custom printf-style output

Assume that we need to output formatted data to two UARTs, numbered 0 and 1, and we have a functions uart0_putc and __uart1_putc that do just that and whose prototypes are:

int uart0_putc(int ch);
int uart1_putc(int ch);

These functions return a positive value if there is no error outputting the character and EOF if there was an error.

Using a classic implementation, you would use sprintf to format the string for output and then output it:

void
uart0_printf(const char *fmt, ...)
{
  char buf[80], *p;
  va_list ap;
  va_start(ap, fmt);
  vsnprintf(buf, sizeof(buf), fmt, ap);
  for (p = buf; *p; ++p)
    uart0_putc(*p);
  va_end(ap);
}

We would, of course, need an identical routine for outputting to the other UART. This code is portable, but it requires an intermediate buffer of 80 characters. On small systems, this is quite an overhead, so we could reduce the buffer size to compensate. Of course, the trouble with that means that the maximum number of characters that can be output by a single call to printf_uart0 is also reduced. What would be good is a way to output characters to one of the UARTs without requiring an intermediate buffer.

CrossWorks printf-style output

CrossWorks provides a solution for just this case by using some internal functions and data types in the CrossWorks library. These functions and types are define in the header file <__vfprintf.h>.

The first thing to introduce is the __printf_t type:

typedef struct
{
  int is_string;
  size_t charcount;
  size_t maxchars;
  union
  {
    char *string;
    int (*output_fn)(int);
   } u;
} __printf_t;

This type is used by the library functions to direct what the formatting routines do with each character they need to output. The is_string member discriminates the union u and directs whether the character should be appended to the string pointed to by string or output using output_fn. The member charcount counts the number of characters currently output, and maxchars defines the maximum number of characters output by the formatting routine __vfprintf.

We can use this type and function to rewrite uart0_printf:

int
uart0_printf(const char *fmt, ...)
{
  int n;
  va_list ap;
  __printf_t iod;
  va_start(ap, fmt);
  iod.is_string = 0;
  iod.maxchars = INT_MAX;
  iod.u.output_fn = uart0_putc;
  n = __vfprintf(&iod, fmt, ap);
  va_end(ap);
  return n;
}

This function has no intermediate buffer: when a character is ready to be output by the formatting routine, it calls the output_fn function in the descriptor iod to output it immediately. The maximum number of characters isn’t limited as the maxchars member is set to INT_MAX. if you wanted to limit the number of characters output you can simply set the maxchars member to the appropriate value before calling __vfprintf.

We can adapt this function to take a UART number as a parameter:

int
uart_printf(int uart, const char *fmt, ...)
{
  int n;
  va_list ap;
  __printf_t iod;
  va_start(ap, fmt);
  iod.is_string = 0;
  iod.maxchars = INT_MAX;
  iod.u.output_fn = uart ? uart1_putc : uart0_putc;
  n = __vfprintf(&iod, fmt, ap);
  va_end(ap);
  return n;
}

Now we can use:

uart_printf(0, "This is uart %d\n...", 0);
uart_printf(1, "..and this is uart %d\n", 1);

__vfprintf returns the actual number of characters printed, which you may wish to dispense with and make the uart_printf routine return void.

Extending input functions

The formatted input functions would be implemented in the same manner as the output functions: read a string into an intermediate buffer and parse using sscanf. However, we can use the low-level routines in the CrossWorks library for formatted input without requiring the intermediate buffer.

The type __stream_scanf_t is:

typedef struct
{
  char is_string;
  int (*getc_fn)(void);
  int (*ungetc_fn)(int);
} __stream_scanf_t;

The function getc_fn reads a single character from the UART, and ungetc_fn pushes back a character to the UART. You can push at most one character back onto the stream.

Here’s an implementation of functions to read and write from a single UART:

static int uart0_ungot = EOF;
int
uart0_getc(void)
{
  if (uart0_ungot)
    {
      int c = uart0_ungot;
      uart0_ungot = EOF;
      return c;
    }
  else
    return read_char_from_uart(0);
}
int
uart0_ungetc{int c)
{
  uart0_ungot = c;
}

You can use these two functions to perform formatted input using the UART:

int
uart0_scanf(const char *fmt, ...)
{
  __stream_scanf_t iod;
  va_list a;
  int n;
  va_start(a, fmt);
  iod.is_string = 0;
  iod.getc_fn = uart0_getc;
  iod.ungetc_fn = uart0_ungetc;
  n = __vfscanf((__scanf_t *)&iod, (const unsigned char *)fmt, a);
  va_end(a);
  return n;
}

Using this template, we can add functions to do additional formatted input from other UARTs or devices, just as we did for formatted output.