Skip to content

Commit

Permalink
Documentation: port "Device Drivers vs. Bus Drivers and GPIO Drivers"…
Browse files Browse the repository at this point in the history
  • Loading branch information
raiden00pl committed Sep 28, 2024
1 parent 5f91a9a commit ccf73a8
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Documentation/components/drivers/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ Drivers in NuttX generally work in two distinct layers:
* A "lower half" which is typically hardware-specific. This is
usually implemented at the architecture or board level.

Details about drivers implementation can be found in
:doc:`../../implementation/drivers_design` and :doc:`../../implementation/device_drivers`.

Subdirectories of ``nuttx/drivers``
===================================

Expand Down
209 changes: 209 additions & 0 deletions Documentation/implementation/device_drivers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
==============
Device Drivers
==============

Standard Device Drivers
=======================

Device drivers should be implemented in the RTOS and used by applications.
Drivers provide access to device functionality for applications. This is a
necessary part of the modular RTOS design. In the NuttX directory structure,
share-able device drivers reside under ``drivers/`` and custom drivers reside in
the board-specific directories at ``nuttx/boards/<arch>/<chip>/<board>/src`` or
``nuttx/boards/<arch>/<chip>/drivers`` that are built into the RTOS.

Bus Drivers
===========

There a many things that get called drivers in OS; NuttX makes a distinction
between device drivers and bus drivers. For example, SPI, PCI, PCMCIA, USB,
Ethernet, etc. are buses and not devices. You will never find a device driver
for a bus in the NuttX architecture.

In most devices architectures, devices reside on a bus. A bus is a transport
layer that connects the device residing on the bus to a device driver.
The bus is managed by a bus driver. The device driver uses the facilities of
the bus driver transport layer to interact with the device.

Consider SPI. SPI is a bus. It provides a serial bus to which many devices may
be connected. An SPI device resides on the SPI bus in the sense that is shares
the same MISO, MOSI, and clock lines with other devices on the SPI bus (but in
SPI, it will have its own dedicated chip select discrete).

Although we typically use the same term driver to refer to both bus drivers and
device drivers, there is one big, fundamental difference: applications interact
only with devices drivers and never with bus drivers. Applications never talk
directly to PCI, PCMCIA, USB, Ethernet, nor with I2C, SPI, or GPIOs. Applications
interface through device drivers that use PCI, PCMCIA, USB, Ethernet, I2C, or SPI.
Bus drivers only exist to support the communication between the device driver and
the device on the bus.

Back to SPI... There will never be an application accessible interface to SPI.
If your application were to use SPI directly, then you would have have embedded
a device driver in your application and would have violated the RTOS functional
partition.

Test Drivers
============

It would be possible to provide character driver, such as SPI driver, that could
perform bus level accesses on behalf of an application. There are not many cases
where this would be acceptable, however. One possibility would be to support
support testing of bus drivers.
There is, an example for I2S here: ``drivers/audio/i2schar.c`` with a test case
here ``apps/examples/i2schar``. I2S is, of course, very similar to SPI.
This interface exists only for testing purposes and would probably not be
possible to build any meaningless application with it.

The I2C Tool
------------

Of course, like most rules, there are lots of violations. I2C is another bus and
the the I2C "driver" is another transport similar in many ways to SPI. For I2C,
there is an application at ``apps/system/i2c`` alled the "I2C tool" that will allow
you access I2C devices from the command line. This is not really just a test tool
and not a real part of an application.

And there is a fundamental flaw in the I2C tool: it uses NuttX internal interfaces
and violates the functional partitioning. NuttX has three build mode: (1) A flat
build where there is no enforcement of RTOS boundaries. In that flat build,
the I2C tool works fine. And (2) a kernel build mode and (3) a protected build mode.
In bothof these latter cases, the OS interfaces are strictly enforced. In the kernel
pand protected build modes, the I2C tool is not available because it cannot access
those NuttX internal interfaces.

User Space Drivers
==================

Above, it was stated that if your application were to use a bus directly, then you
would have have embedded a device driver in your application and would violate
the RTOS functional partition. Such device built into user applications are
referred to as user space drivers in some contexts. There is no plan or intent
to support user space drivers in NuttX.

Communication Devices
=====================

What about interface like CAN and UARTs? Why are those exposed as drivers when
SPI and I2C are not?

Semantics are difficult. The general principles that are maintain in
the RTOS are clear, but sometimes applying principles in a black and white way
is not easy in a world with shades of grey. (And if the principles get in the
way of good design then the principles should change).

In the case of true buses that support generic devices, the principle
is a good one. But there are grey areas too.

CAN seems similar to Ethernet. Both are network interfaces of sorts. You
wouldn't interface directly with Ethernet driver because you need to go
through a network stack of some type. The OSI model prevents it.

UARTs are communication devices. There is no RS-232 bus with devices connected
to it. Rather there are peers on the bus that you communicate with. This does not
preclude a UART from being used as a low level transfer for a device driver
(as with the driver for a wireless modules). Nor does it preclude a stack layer
like Modbus from being inserted in the path.

CAN differs from Ethernet in that it really is a direct peer-to-peer
communication, more like a UART. Although you can support a stack like CANOPen
on CAN. Currently CAN can be used as a simple character device, or as a network
interface using SocketCAN.

Communication devices support a fundamental peer-to-peer model. CAN and UARTs
are basically serial interfaces. But so are SPI, I2C, and USB. But those latter
serial interfaces clearly have a host/device, master/slave model associated with
them. It make perfectly good sense to think of them as buses that support device
interfaces.

I/O Expander
============

An I/O expander is device that interfaces with the MCU, usually via I2C, and
provides additional discrete inputs and outputs. The same rules apply:

* **GPIOS are Board-Specific**. Nothing in the system should now about GPIOs
except for board specific logic. GPIOs can change from board-toboard. They
can come and go. They can be replaced by GPIO expanders. Your (portable)
application should not have any knowledge about how any discrete I/O is
implemented on the board. There will never be GPIO drivers as a part of
the NuttX architecture.

* **Common Drivers are Board-Independent**. Nor should common drivers
(like those in ``drivers/``) know anything about GPIOs. In ALL cases,
the board specific implementation in the board directories creates
a "lower half" driver and binds that "lower half" driver with an common
"upper half" driver to initialize the driver. Only the board logic has
any kind of GPIO knowledge; not the application and not the common
"upper half driver".

* **I2C and SPI Drivers are Internal Bus Drivers**. Similarly I2C and SPI
drivers are not accessible to applications. These are NOT device drivers
but are bus drivers. They should not be accessed directly by applications.
Rather, again, the board-specific logic generates a "lower half" driver
that provides a common I2C or SPI interface and binds that with
an "upper half" driver to initialize the driver.

None of those rules change if you use an I/O expander, things just get
more convoluted.

Example Architecture
--------------------

Consider this case for some ``<board>``:

#. A discrete joystick is implemented as set of buttons: UP, DOWN, LEFT, RIGHT,
and CENTER. The state of each the buttons is sensed as a GPIO input.

#. The GPIO button inputs go to I2C I/O expander at say,
``drivers/ioexpander/myexpander.c``, and finally to

#. The discrete joystick driver "upper half" driver (``drivers/input/djoystick.c``).

Implementation Details
----------------------

These should be implemented in the following, flexible, portable, layered architecture:

#. In the end, the application would interact only with a joystick driver
interface via standard open/close/read/ioctl operations. It would receive
pjoystick information as described in ``include/nuttx/input/djoystick.h.``

#. The discrete joystick driver would have been initialized by logic in some
file like ``boards/<arch>/xyz/<board>/src/xyz_djoystick.c`` when the system
was initialized. ``zyz_joystick.c`` would have created instance of
the ``struct djoy_lowerhalf_s`` "lower half" interface as described in
``nuttx/include/nuttx/input/djoystick.h`` and would have passed that
interface instance to the ``drivers/input/djoystick.c`` "upper half" driver
to initialize it.

#. As part of the creation of the ``struct djoy_lowerhalf_s`` "lower half"
interface instance, logic in ``xyz_djoystick.c`` would have done the following:
It would have created an I2C driver instance by called MCU specific I2C initialization
logic then passed this I2C driver instance to the I/O expander initialization interface
in ``drivers/ioexpander/myexpander.c`` to create the I/O expander interface instance.

Note that the I/O expander interface should NOT be a normal character driver.
It should NOT be accessed via open/close/read/write/ioctl. Rather, it should return
an instance of a some ``struct ioexpander_s`` interface. That I/O expander interface
would be described in ``nuttx/include/ioexpander/ioexpander.h``. It is an internal
operating system interface and would never be available to application logic.

After receiving the I/O expander interface instance, the "lower half" discrete
joystick interface would retain this internally as private data. Nothing in the
system other than this "lower half" discrete joystick driver needs to know how
the joystick is connected on board.

#. After creating the "upper half" discrete joystick interface interface,
the "lower half" discrete joystick interface would enable interrupts from
the I/O expander device.

#. When a key is pressed, the "lower half" discrete joystick driver would receive
an interrupt from the I/O expander. It would then interact with the I/O driver
to obtain the current discrete button depressions. The I/O expander driver would
interact with I2C to obtain those button settings. Then the discrete joystick
interface callback will be called, providing the discrete joystick "upper half"
driver with the joystick input.

#. The "upper half" discrete joystick character driver would then return the encoded
joystick input to the application in response to a ``read()`` from application code.
1 change: 1 addition & 0 deletions Documentation/implementation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Implementation Details
:caption: Contents:

drivers_design.rst
device_drivers.rst
processes_vs_tasks.rst
critical_sections.rst
interrupt_controls.rst
Expand Down

0 comments on commit ccf73a8

Please sign in to comment.