summaryrefslogtreecommitdiffstats
path: root/bsp-howto/console.rst
blob: 0453b37a1a626814bd8931da1d37af5455d4cafa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
.. comment SPDX-License-Identifier: CC-BY-SA-4.0

.. COMMENT: COPYRIGHT (c) 1988-2002.
.. COMMENT: On-Line Applications Research Corporation (OAR).
.. COMMENT: All rights reserved.

Console Driver
**************

.. warning::
    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:

    - :c:func:`rtems_termios_open()`

    - :c:func:`rtems_termios_close()`

    This manual describes the new API.

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 drivers.
`Termios <http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html>`_
is defined by IEEE Std 1003.1-2008 (POSIX.1-2008).  It supports various modes
of operations at application level.  This chapter focuses on the low-level
serial device driver.  Additional Termios information can be found in the
`Linux TERMIOS(3) <http://man7.org/linux/man-pages/man3/termios.3.html>`_
manpage or the
`FreeBSD TERMIOS(4) <https://www.freebsd.org/cgi/man.cgi?query=termios&sektion=4>`_
manpage.

There are the following software layers.

+-------------------------+
| Application             |
+-------------------------+
| Termios                 |
+-------------------------+
| Low-Level Device Driver |
+-------------------------+

In the default application configuration RTEMS opens during system
initialization a :file:`/dev/console` device file to create the file
descriptors 0, 1 and 2 used for standard input, output and error, respectively.
The corresponding device driver is usually a Termios serial device driver
described here.  The standard file descriptors are used by standard C library
calls such as :c:func:`printf` or :c:func:`scanf` or directly via the
:c:func:`read` or :c:func:`write` system calls.

Build System and Files
======================

A new serial device driver should consist of three parts.

- A section in the BSPs Makefile.am:

.. code-block:: makefile

      [...]
      libbsp_a_SOURCES += ../../shared/console-termios.c
      libbsp_a_SOURCES += console/console.c
      [...]

- A general serial device specific low-level driver providing the handler table
  and the device context specialization for the Termios
  :c:func:`rtems_termios_device_install()` function.  This low-level driver
  could be used for more than one BSP.

- A BSP-specific initialization routine :c:func:`console_initialize()`, that calls
  :c:func:`rtems_termios_device_install()` providing a low-level driver context for
  each installed device.  This is usually defined in the file
  :file:`console/console.c` relative to the BSP base directory.

The low-level driver should provide a specialization of the Termios device
context.  The initialization routine must provide a context for each installed
device via :c:func:`rtems_termios_device_install()`.  Here is an example header
file for a low-level serial device driver.

.. code-block:: c

    #ifndef MY_DRIVER_H
    #define MY_DRIVER_H

    #include <some-chip/serial.h>

    #include <rtems/termiostypes.h>

    /* My low-level driver specialization of Termios device context */
    typedef struct {
      rtems_termios_device_context base;
      const char *device_name;
      volatile some_chip_registers *regs;
      /* More stuff */
    } my_driver_context;

    extern const rtems_termios_device_handler my_driver_handler_polled;

    extern const rtems_termios_device_handler my_driver_handler_interrupt;

    #endif /* MY_DRIVER_H */

Driver Functioning Modes
========================

There are four main functioning modes for a Termios serial device driver.  The
mode must be set during device creation and cannot be changed afterwards.

Polled Mode (`TERMIOS_POLLED`)
    In polled mode, the processor blocks on sending/receiving characters.  This
    mode is not the most efficient way to utilize the serial device. 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
    device is to be used primarily as a debug console.  In a simple polled
    driver, the software will continuously check the status of the serial
    device when it is reading or writing to the serial device.  Termios
    improves on this by delaying the caller for one clock tick between
    successive checks of the serial device on a read operation.

Interrupt Driven Mode (`TERMIOS_IRQ_DRIVEN`)
    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 serial device.
    The interrupt routine 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 serial device (the exact number depends on the
    serial device) 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 serial device is idle, it is typically necessary to prime the
    transmitter before the first interrupt will occur.

Interrupt Server Driven Mode (`TERMIOS_IRQ_SERVER_DRIVEN`)
    The interrupt server driven mode is identical to the interrupt driven mode,
    except that a mutex is used to protect the low-level device state instead
    of an interrupt lock (disabled interrupts).  Use this mode in case the
    serial device is connected via I2C or SPI and the I2C or SPI framework is
    used.

Task Driven Mode (`TERMIOS_TASK_DRIVEN`)
    The task driven mode is similar to interrupt driven mode, but the actual
    data processing is done in dedicated tasks instead of interrupt routines.
    This mode is not available in SMP configurations.  It has some
    implementation flaws and it is not well tested.

Polled Mode
===========

The handler table for the polled mode should look like the following.

.. code-block:: c

    const rtems_termios_device_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,
      .ioctl = my_driver_ioctl, /* optional, may be NULL */
      .mode = TERMIOS_POLLED
    }

The :c:func:`my_driver_poll_write()` routine is responsible for writing ``n``
characters from ``buf`` to the serial device specified by ``base``.

.. code-block:: c

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

      ctx = (my_driver_context *) base;

      for ( i = 0 ; i < n ; ++i ) {
        my_driver_write_char( ctx, buf[ i ] );
      }
    }

The :c:func:`my_driver_poll_read` routine is responsible for reading a single
character from the serial device specified by ``base``.  If no character is
available, then the routine should immediately return minus one.

.. code-block:: c

    static int my_driver_poll_read( rtems_termios_device_context *base )
    {
      my_driver_context *ctx;

      ctx = (my_driver_context *) base;

      if ( my_driver_can_read_char( ctx ) ) {
        /* Return the character (must be unsigned) */
        return my_driver_read_char( ctx );
      } else {
        /* Return -1 to indicate that no character is available */
        return -1;
      }
    }

Interrupt Driven Mode
=====================

The handler table for the interrupt driven mode should look like the following.

.. code-block:: c

    const rtems_termios_device_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,
      .ioctl = my_driver_ioctl, /* optional, may be NULL */
      .mode = TERMIOS_IRQ_DRIVEN
    };

There is no device driver read handler to be passed to Termios.  Indeed a
:c:func:`read()` call returns the contents of Termios input buffer.  This
buffer is filled in the driver interrupt routine.

A serial device generally generates interrupts when it is ready to accept or to
emit a number of characters.  In this mode, the interrupt routine is the core
of the driver.

The :c:func:`my_driver_interrupt_handler` is responsible for processing
asynchronous interrupts from the serial device.  There may be multiple
interrupt handlers for a single serial device.  Some serial devices 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 :c:func:`my_driver_interrupt_handler` will have to
check the status of the serial device and determine what caused the interrupt.
The following describes the operation of an
:c:func:`my_driver_interrupt_handler` which has to do this:

.. code-block:: c

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

      tty = arg;
      ctx = rtems_termios_get_device_context( tty );

      /*
       * 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 :c:func:`my_driver_interrupt_write()` handler 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.

.. code-block:: c

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

      ctx = (my_driver_context *) base;

      if ( n > 0 ) {
        /*
         * 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.
         */
      } else {
        /*
         * 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.
         */
      }
    }

First Open
==========

Upon first open of the device, the :c:func:`my_driver_first_open` handler is
called by Termios.  The device registered as :file:`/dev/console` (or
``CONSOLE_DEVICE_NAME``) is opened automatically during RTEMS initialization.

.. code-block:: c

    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;
      rtems_status_code  sc;
      bool               ok;

      ctx = (my_driver_context *) base;

      /*
       * 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;
    }

Last Close
==========

Termios will call the :c:func:`my_driver_last_close` handler if the last close
happens on the device.

.. code-block:: c

    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;

      ctx = (my_driver_context *) base;

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

Set Attributes
==============

Termios will call the :c:func:`my_driver_set_attributes` handler if a serial
line configuration parameter changed, e.g. baud, character size, number of stop
bits, parity, etc.

.. code-block:: c

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

      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;
    }

IO Control
==========

Optionally, the :c:func:`my_driver_ioctl()` routine may be provided for
arbitrary device-specific functions.

.. code-block:: c

    static int my_driver_ioctl(
      rtems_termios_device_context *base,
      ioctl_command_t               request,
      void                         *buffer
    )
    {
      my_driver_context *ctx;

      ctx = (my_driver_context *) base;

      switch ( request ) {
        case MY_DRIVER_DO_XYZ:
          my_driver_do_xyz(ctx, buffer);
          break;
        default:
          rtems_set_errno_and_return_minus_one( EINVAL );
      }

      return 0;
    }

Flow Control
============

You can also provide handler for remote transmission control.  This is not
covered in this manual.

General Initialization
======================

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

The :c:func:`console_initialize()` function may look like this:

.. code-block:: c

    #include <my-driver.h>

    #include <rtems/console.h>

    #include <bsp.h>
    #include <bsp/fatal.h>

    static my_driver_context driver_context_table[] = {
      { /* Some values for device 0 */ },
      { /* Some values for device 1 */ }
    };

    rtems_device_driver console_initialize(
      rtems_device_major_number  major,
      rtems_device_minor_number  minor,
      void                      *arg
    )
    {
      const rtems_termios_device_handler *handler;
      rtems_status_code                   sc;
      size_t                              i;

      #ifdef SOME_BSP_USE_INTERRUPTS
        handler = &my_driver_handler_interrupt;
      #else
        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 ( i = 0; i < RTEMS_ARRAY_SIZE( driver_context_table ) ; ++i ) {
        my_driver_context *ctx;

        ctx = &driver_context_table[ i ];

        /*
         * 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, handler, NULL, ctx );
        if ( sc != RTEMS_SUCCESSFUL ) {
          bsp_fatal( SOME_BSP_FATAL_CONSOLE_DEVICE_INSTALL );
        }
      }

      return RTEMS_SUCCESSFUL;
    }