8. Console Driver

8.1. Introduction

This chapter describes the operation of a console driver using the RTEMS POSIX Termios support. Traditionally RTEMS has referred to all serial device drivers as console device drivers. A console driver can be used to do raw data processing in addition to the “normal” standard input and output device functions required of a console.

The serial driver may be called as the consequence of a C Library call such as printf or scanf or directly via the``read`` or write system calls. There are two main functioning modes:

  • console: formatted input/output, with special characters (end of line, tabulations, etc.) recognition and processing,
  • raw: permits raw data processing.

One may think that two serial drivers are needed to handle these two types of data, but Termios permits having only one driver.

8.2. Termios

Termios is a standard for terminal management, included in the POSIX 1003.1b standard. As part of the POSIX and Open Group Single UNIX Specification, is commonly provided on UNIX implementations. The Open Group has the termios portion of the POSIX standard online at http://opengroup.org/onlinepubs/007908775/xbd/termios.html. The requirements for the <termios.h> file are also provided and are at http://opengroup.org/onlinepubs/007908775/xsh/termios.h.html.

Having RTEMS support for Termios is beneficial because:

  • from the user’s side because it provides standard primitive operations to access the terminal and change configuration settings. These operations are the same under UNIX and RTEMS.
  • from the BSP developer’s side because it frees the developer from dealing with buffer states and mutual exclusions on them. Early RTEMS console device drivers also did their own special character processing.
  • it is part of an internationally recognized standard.
  • it makes porting code from other environments easier.

Termios support includes:

  • raw and console handling,
  • blocking or non-blocking characters receive, with or without Timeout.

At this time, RTEMS documentation does not include a thorough discussion of the Termios functionality. For more information on Termios, type man termios on a Unix box or point a web browser athttp://www.freebsd.org/cgi/man.cgi.

8.3. Driver Functioning Modes

There are generally three main functioning modes for an UART (Universal Asynchronous Receiver-Transmitter, i.e. the serial chip):

  • polled mode
  • interrupt driven mode
  • task driven mode

In polled mode, the processor blocks on sending/receiving characters. This mode is not the most efficient way to utilize the UART. But polled mode is usually necessary when one wants to print an error message in the event of a fatal error such as a fatal error in the BSP. This is also the simplest mode to program. Polled mode is generally preferred if the serial port is to be used primarily as a debug console. In a simple polled driver, the software will continuously check the status of the UART when it is reading or writing to the UART. Termios improves on this by delaying the caller for 1 clock tick between successive checks of the UART on a read operation.

In interrupt driven mode, the processor does not block on sending/receiving characters. Data is buffered between the interrupt service routine and application code. Two buffers are used to insulate the application from the relative slowness of the serial device. One of the buffers is used for incoming characters, while the other is used for outgoing characters.

An interrupt is raised when a character is received by the UART. The interrupt subroutine places the incoming character at the end of the input buffer. When an application asks for input, the characters at the front of the buffer are returned.

When the application prints to the serial device, the outgoing characters are placed at the end of the output buffer. The driver will place one or more characters in the UART (the exact number depends on the UART) An interrupt will be raised when all the characters have been transmitted. The interrupt service routine has to send the characters remaining in the output buffer the same way. When the transmitting side of the UART is idle, it is typically necessary to prime the transmitter before the first interrupt will occur.

The task driven mode is similar to interrupt driven mode, but the actual data processing is done in dedicated tasks instead of interrupt routines.

8.4. Serial Driver Functioning Overview

The following Figure shows how a Termios driven serial driver works: Figure not included in ASCII version

The following list describes the basic flow.

  • the application programmer uses standard C library call (printf, scanf, read, write, etc.),
  • C library (ctx.g. RedHat (formerly Cygnus) Newlib) calls the RTEMS system call interface. This code can be found in the:file:cpukit/libcsupport/src directory.
  • Glue code calls the serial driver entry routines.

8.4.1. Basics

The low-level driver API changed between RTEMS 4.10 and RTEMS 4.11. The legacy callback API is still supported, but its use is discouraged. The following functions are deprecated:

  • rtems_termios_open() - use rtems_termios_device_open() in combination with rtems_termios_device_install() instead.
  • rtems_termios_close() - use rtems_termios_device_close() instead.

This manual describes the new API. A new console driver should consist of three parts.

  • The basic console driver functions using the Termios support. Add this the BSPs Makefile.am:
[...]
libbsp_a_SOURCES += ../../shared/console-termios.c
[...]
  • A general serial module specific low-level driver providing the handler table for the Termios rtems_termios_device_install() function. This low-level driver could be used for more than one BSP.
  • A BSP specific initialization routine console_initialize(), that calls rtems_termios_device_install() providing a low-level driver context for each installed device.

You need to provide a device handler structure for the Termios device interface. The functions are described later in this chapter. The first open and set attributes handler return a boolean status to indicate success (true) or failure (false). The polled read function returns an unsigned character in case one is available or minus one otherwise.

If you want to use polled IO it should look like the following. Termios must be told the addresses of the handler that are to be used for simple character IO, i.e. pointers to the my_driver_poll_read() and my_driver_poll_write() functions described later in Termios and Polled IO.

const rtems_termios_handler my_driver_handler_polled = {
  .first_open = my_driver_first_open,
  .last_close = my_driver_last_close,
  .poll_read = my_driver_poll_read,
  .write = my_driver_poll_write,
  .set_attributes = my_driver_set_attributes,
  .stop_remote_tx = NULL,
  .start_remote_tx = NULL,
  .mode = TERMIOS_POLLED
}

For an interrupt driven implementation you need the following. The driver functioning is quite different in this mode. There is no device driver read handler to be passed to Termios. Indeed a console_read() call returns the contents of Termios input buffer. This buffer is filled in the driver interrupt subroutine, see also Termios and Interrupt Driven IO. The driver is responsible for providing a pointer to the``my_driver_interrupt_write()`` function.

const rtems_termios_handler my_driver_handler_interrupt = {
  .first_open = my_driver_first_open,
  .last_close = my_driver_last_close,
  .poll_read = NULL,
  .write = my_driver_interrupt_write,
  .set_attributes = my_driver_set_attributes,
  .stopRemoteTx = NULL,
  .stop_remote_tx = NULL,
  .start_remote_tx = NULL,
  .mode = TERMIOS_IRQ_DRIVEN
};

You can also provide hander for remote transmission control. This is not covered in this manual, so they are set to NULL in the above examples.

The low-level driver should provide a data structure for its device context. The initialization routine must provide a context for each installed device via rtems_termios_device_install(). For simplicity of the console initialization example the device name is also present. Here is an example header file.

#ifndef MY_DRIVER_H
#define MY_DRIVER_H

#include <rtems/termiostypes.h>
#include <some-chip-header.h>

/* Low-level driver specific data structure */
typedef struct {
  rtems_termios_device_context base;
  const char *device_name;
  volatile module_register_block *regs;
  /* More stuff */
} my_driver_context;

extern const rtems_termios_handler my_driver_handler_polled;
extern const rtems_termios_handler my_driver_handler_interrupt;

#endif /* MY_DRIVER_H */

8.4.2. Termios and Polled IO

The following handler are provided by the low-level driver and invoked by Termios for simple character IO.

The my_driver_poll_write() routine is responsible for writing n characters from buf to the serial device specified by tty.

static void my_driver_poll_write(
  rtems_termios_device_context *base,
  const char                   *buf,
  size_t                        n
)
{
  my_driver_context *ctx = (my_driver_context *) base;
  size_t i;
  /* Write */
  for (i = 0; i < n; ++i) {
    my_driver_write_char(ctx, buf[i]);
  }
}

The my_driver_poll_read routine is responsible for reading a single character from the serial device specified by tty. If no character is available, then the routine should return minus one.

static int my_driver_poll_read(rtems_termios_device_context *base)
{
  my_driver_context *ctx = (my_driver_context *) base;
  /* Check if a character is available */
  if (my_driver_can_read_char(ctx)) {
    /* Return the character */
    return my_driver_read_char(ctx);
  } else {
    /* Return an error status */
    return -1;
  }
}

8.4.3. Termios and Interrupt Driven IO

The UART generally generates interrupts when it is ready to accept or to emit a number of characters. In this mode, the interrupt subroutine is the core of the driver.

The my_driver_interrupt_handler() is responsible for processing asynchronous interrupts from the UART. There may be multiple interrupt handlers for a single UART. Some UARTs can generate a unique interrupt vector for each interrupt source such as a character has been received or the transmitter is ready for another character.

In the simplest case, the my_driver_interrupt_handler() will have to check the status of the UART and determine what caused the interrupt. The following describes the operation of an my_driver_interrupt_handler which has to do this:

static void my_driver_interrupt_handler(
  rtems_vector_number  vector,
  void                *arg
)
{
  rtems_termios_tty *tty = arg;
  my_driver_context *ctx = rtems_termios_get_device_context(tty);
  char buf[N];
  size_t n;

  /*
   * Check if we have received something.  The function reads the
   * received characters from the device and stores them in the
   * buffer.  It returns the number of read characters.
   */
  n = my_driver_read_received_chars(ctx, buf, N);
  if (n > 0) {
    /* Hand the data over to the Termios infrastructure */
    rtems_termios_enqueue_raw_characters(tty, buf, n);
  }

  /*
   * Check if we have something transmitted.  The functions returns
   * the number of transmitted characters since the last write to the
   * device.
   */
  n = my_driver_transmitted_chars(ctx);
  if (n > 0) {
    /*
     * Notify Termios that we have transmitted some characters.  It
     * will call now the interrupt write function if more characters
     * are ready for transmission.
     */
    rtems_termios_dequeue_characters(tty, n);
  }
}

The my_driver_interrupt_write() function is responsible for telling the device that the n characters at buf are to be transmitted. It the value n is zero to indicate that no more characters are to send. The driver can disable the transmit interrupts now. This routine is invoked either from task context with disabled interrupts to start a new transmission process with exactly one character in case of an idle output state or from the interrupt handler to refill the transmitter. If the routine is invoked to start the transmit process the output state will become busy and Termios starts to fill the output buffer. If the transmit interrupt arises before Termios was able to fill the transmit buffer you will end up with one interrupt per character.

static void my_driver_interrupt_write(
  rtems_termios_device_context  *base,
  const char                    *buf,
  size_t                         n
)
{
  my_driver_context *ctx = (my_driver_context *) base;

  /*
   * Tell the device to transmit some characters from buf (less than
   * or equal to n).  When the device is finished it should raise an
   * interrupt.  The interrupt handler will notify Termios that these
   * characters have been transmitted and this may trigger this write
   * function again.  You may have to store the number of outstanding
   * characters in the device data structure.
   */
  /*
   * Termios will set n to zero to indicate that the transmitter is
   * now inactive.  The output buffer is empty in this case.  The
   * driver may disable the transmit interrupts now.
   */
}

8.4.4. Initialization

The BSP specific driver initialization is called once during the RTEMS initialization process.

The console_initialize() function may look like this:

#include <my-driver.h>
#include <rtems/console.h>
#include <bsp.h>
#include <bsp/fatal.h>

static my_driver_context driver_context_table[M] = { /* Some values */ };

rtems_device_driver console_initialize(
  rtems_device_major_number  major,
  rtems_device_minor_number  minor,
  void                      *arg
)
{
  rtems_status_code sc;
  #ifdef SOME_BSP_USE_INTERRUPTS
    const rtems_termios_handler *handler = &my_driver_handler_interrupt;
  #else
    const rtems_termios_handler *handler = &my_driver_handler_polled;
  #endif

  /*
   * Initialize the Termios infrastructure.  If Termios has already
   * been initialized by another device driver, then this call will
   * have no effect.
   */
  rtems_termios_initialize();

  /* Initialize each device */
  for (
    minor = 0;
    minor < RTEMS_ARRAY_SIZE(driver_context_table);
    ++minor
  ) {
    my_driver_context *ctx = &driver_context_table[minor];

    /*
     * Install this device in the file system and Termios.  In order
     * to use the console (i.e. being able to do printf, scanf etc.
     * on stdin, stdout and stderr), one device must be registered as
     * "/dev/console" (CONSOLE_DEVICE_NAME).
     */
    sc = rtems_termios_device_install(
      ctx->device_name,
      major,
      minor,
      handler,
      NULL,
      ctx
    );
    if (sc != RTEMS_SUCCESSFUL) {
      bsp_fatal(SOME_BSP_FATAL_CONSOLE_DEVICE_INSTALL);
    }
  }

  return RTEMS_SUCCESSFUL;
}

8.4.5. Opening a serial device

The console_open() function provided by console-termios.c is called whenever a serial device is opened. The device registered as "/dev/console" (CONSOLE_DEVICE_NAME) is opened automatically during RTEMS initialization. For instance, if UART channel 2 is registered as "/dev/tty1", the console_open() entry point will be called as the result of an fopen("/dev/tty1", mode) in the application.

During the first open of the device Termios will call the my_driver_first_open() handler.

static bool my_driver_first_open(
  rtems_termios_tty             *tty,
  rtems_termios_device_context  *base,
  struct termios                *term,
  rtems_libio_open_close_args_t *args
)
{
  my_driver_context *ctx = (my_driver_context *) base;
  rtems_status_code sc;
  bool ok;

  /*
   * You may add some initialization code here.
   */

  /*
   * Sets the initial baud rate.  This should be set to the value of
   * the boot loader.  This function accepts only exact Termios baud
   * values.
   */
  sc = rtems_termios_set_initial_baud(tty, MY_DRIVER_BAUD_RATE);
  if (sc != RTEMS_SUCCESSFUL) {
    /* Not a valid Termios baud */
  }

  /*
   * Alternatively you can set the best baud.
   */
  rtems_termios_set_best_baud(term, MY_DRIVER_BAUD_RATE);

  /*
   * To propagate the initial Termios attributes to the device use
   * this.
  */
  ok = my_driver_set_attributes(base, term);
  if (!ok) {
    /* This is bad */
  }

  /*
   * Return true to indicate a successful set attributes, and false
   * otherwise.
   */
  return true;
}

8.4.6. Closing a Serial Device

The console_close() provided by console-termios.c is invoked when the serial device is to be closed. This entry point corresponds to the device driver close entry point.

Termios will call the my_driver_last_close() handler if the last close happens on the device.

static void my_driver_last_close(
  rtems_termios_tty             *tty,
  rtems_termios_device_context  *base,
  rtems_libio_open_close_args_t *args
)
{
  my_driver_context *ctx = (my_driver_context *) base;

  /*
   * The driver may do some cleanup here.
  */
}

8.4.7. Reading Characters from a Serial Device

The console_read() provided by console-termios.c is invoked when the serial device is to be read from. This entry point corresponds to the device driver read entry point.

8.4.8. Writing Characters to a Serial Device

The console_write() provided by console-termios.c is invoked when the serial device is to be written to. This entry point corresponds to the device driver write entry point.

8.4.9. Changing Serial Line Parameters

The console_control() provided by console-termios.c is invoked when the line parameters for a particular serial device are to be changed. This entry point corresponds to the device driver IO control entry point.

The application writer is able to control the serial line configuration with Termios calls (such as the ioctl() command, see the Termios documentation for more details). If the driver is to support dynamic configuration, then it must have the console_control() piece of code. Basically ioctl() commands call console_control() with the serial line configuration in a Termios defined data structure.

The driver is responsible for reinitializing the device with the correct settings. For this purpose Termios calls the my_driver_set_attributes() handler.

static bool my_driver_set_attributes(
  rtems_termios_device_context *base,
  const struct termios         *term
)
{
  my_driver_context *ctx = (my_driver_context *) base;

  /*
   * Inspect the termios data structure and configure the device
   * appropriately.  The driver should only be concerned with the
   * parts of the structure that specify hardware setting for the
   * communications channel such as baud, character size, etc.
   */
  /*
   * Return true to indicate a successful set attributes, and false
   * otherwise.
   */
  return true;
}