Skip to content

Driver Infrastructure

Sergio Benitez edited this page Jan 13, 2015 · 2 revisions

This document provides a detailed overview of the proposed TockOS driver infrastructure.

Overview

The TockOS driver infrastructure aims to be a transparent, magic-free mechanism for connecting device drivers to devices. A device driver offers a (usually high-level) interface to a specific device connected to the platform. For example, a SPI-based flash storage device driver could offer a key-value store interface for flash-based storage connected via an on-board SPI controller.

Device drivers do not communicate directly with the platform's device control hardware. Such an approach would be fragile and unsafe as it would require the device driver to tie its implementation to a specific architecture and program for the low-level details of the control hardware. Further, two device drivers utilizing the same control hardware would be required to implement similar logic. As a result, device drivers in TockOS communicate with the hardware through controller interfaces implemented by platform drivers.

A platform driver offers an interface to platform control hardware. These interfaces are fixed and platform-independent. A device driver targeting a controller interface can be used on any architecture with platform drivers supplying the necessary interface. Platform drivers and device drivers operate independently; device drivers need not have knowledge of a platform driver's implementation and vice versa.

Device Drivers

Device drivers are instantiated declaratively via a device tree. Device trees in TockOS are similar to device trees in Linux, except that they are verified at compile time to ensure that device drivers exist, that the drivers' resource requirements are satisfied exclusively, that the driver is instantiated with all necessary parameters of the correct type, and that no race conditions can occur when one driver uses another driver.

A device tree is written inside of a device_tree! macro. The following is an example of a device tree that instantiates five devices: (1, 2) GPIO based LEDs, (3) a bit-banged temperature sensor, (4) a UART based serial console, and (5) a SPI flash device.

device_tree!(
  red_led: led::GPIOLed(GPIO@5);
  green_led: led::GPIOLed(GPIO@4);

  temperature: bit_bang::Temperature(GPIO@0, GPIO@1, GPIO@2);

  console: uart::Console(UART(USART@3)) {
    baud_rate = 115200;
    data_bits = 8;
    parity = 0;
    stop_bits = 1;
  }

  flash: spi::MTDFlashM25P80(SPIMaster(USART@0)) {
    # 128mbit/16MB n25q128 SPI flash storage
    max_tx_lines = 2;
    max_rx_lines = 2;
    max_frequency = 50e6;

    # 16 * 2^20 == 16MB == 128mbit
    size = 0x1000000;
    status_indicator = &green_led;
  }
);

The first device declared is the red_led device. The name red_led is arbitrary; a handle to the device with that name will be available to the user after device initialization. The device is declared with type led::GPIOLed. This type is a path to the driver's type rooted at the OS driver path, which is ::drivers. The led::GPIOLed device driver requires a GPIO resource to function. This resource is declared in parenthesis as GPIO@5, indicating that the platform GPIO at location 5 will be allocated to this driver.

At compile time, the path and resource availability are verified. The OS will not compile if the path is incorrect or the resource requested is unavailable or previously requested and allocated.

The green_led device is declared similarly, albeit with the GPIO at location 4 as its resource.

The temperature device has a similar declaration. It is of type bit_bang::Temperature. This example illustrates a driver requiring multiple resources to function. Here, it requires three GPIO resources, and the GPIOs at locations 0, 1, and 2 are requested.

The console device describes a device named console of type uart::Console. The Console driver requires a UART resource to function. Here we illustrate a scenario where a dedicated UART resource is not available, but a polymorphic resource, a USART, is. Polymorphic resources can be requested to act as given variant. This declaration requests that the USART controller at location 3 be configured as a UART. The device driver need not be modified to reflect this; the USART will expose the same interface a dedicated UART would.

The console device driver requires properties to be instantiated. These properties are declared inside of a block. Here, four properties, baud_rate, data_bits, parity, and stop_bits with values 115200, 8, 0, and 1, respectively, are declared. These properties are used by the driver for device configuration. It is a compile time error to supply properties of incorrect type, and to omit or inject extra properties.

Finally, a flash device is declared similarly to the console device. The status_indicator property is of particular interest here as it references the previously declared green_led device. References to other devices must be unique if the device being referenced modifies any state. If the device is read-only, then multiple references can be created.

Compiled Value

The device tree declared above is transformed at compile time into the following Rust code:

let red_led = drivers::led::GPIOLed::init(gpio_4);

let green_led = drivers::led::GPIOLed::init(gpio_4);

let temperature = drivers::bit_bang::Temperature::init(gpio_0, gpio_1, gpio_2);

let console = drivers::uart::Console::init(usart_3.as_UART(),
  drivers::uart::Console::InitParams {
    baud_rate: 115200,
    data_bits: 8,
    parity: 0,
    stop_bits: 1
  }
);

let flash = drivers::spi::MTDFlashM25P80::init(usart_0.as_SPIMaster,
  drivers::spi::MTDFlashM25P80::InitParams {
    max_tx_lines: 2,
    max_rx_lines: 2,
    max_frequency: 50e6,
    size: 0x1000000,
    status_indicator: Some(&green_led);
  }
);

The following is an implementation aside:

Note: We could do something like:

flash.init(drivers::spi::MTDFlashM25P80::InitParams {
  max_tx_lines: 2,
  max_rx_lines: 2,
  max_frequency: 50e6,
  size: 0x1000000,
  .. Default::default()
});

So that the device tree implementors don't have to specify every parameter. In this case, when there is no LED, we can simply omit that parameter. I'm not sure this is the best way to go, though.

Device Tree Syntax

The following grammar defines the device tree syntax:

device_tree :=
  device+

device :=
  ID':' PATH'('resource_requirement[',' resource_requirement]*) [block | ';']

block :=
  '{' property* '}'

property :=
  ID '=' VALUE;

resource_requirement := [
  basic_resource_requirement
  | variant_resource_requitement
]

basic_resource_requirement :=
  ID'@'VALUE

variant_resource_requitement :=
  ID'('basic_resource_requirement')'

ID, PATH := { as defined by Rust }

Platform Drivers

Platform drivers are instantiated declaratively via the platform_tree! macro. They are declared similarly to device drivers except that the platform name is the first macro parameter (s4mlxx below) and no resource requirements are declared. The path name for the type is rooted at ::platform::platform_name:: where platform_name is the macro's first parameter. platform_name can be overridden on a per device basis by specifying the path.

The example below declares a platform with a CortexM4 processor, 27 GPIOs, 1 USART with SPI mode availability, and 3 USARTS without SPI mode.

platform_tree!(s4mlxx,
  cpu@0 = CortexM4 {
    clock = 12e6;
    mpu_present = true;
    nvic = &nvic;

    pll = PLL {
      m = 50;
      n = 3;
      divisor = 4;
    };
  };

  gpio@[0..26] = cortex_common::GPIO;

  usart@0 = SPI_USART {
    base_address = 0x40024000;

    _variants = [
      UART, RS485, Modem, LINMaster, LINSlave, SPIMaster, SPISlave
    ];
  };

  usart@[1..3] = USART {
    base_address = 0x40024000;

    _variants = [
      UART, RS485, Modem, LINMaster, LINSlave
    ];
  };
);

This platform tree is compliled to the following Rust code:

fn main() {
  use platform::s4mlxx;
  s4mlxx::init_start();

  let cpu_0 = s4mlxx::CortexM4::init(
    s4mlxx::CortexM4::InitParams {
      location: 0,
      clock: 12e6,
      mpu_present: true,
      nvic: &nvic,
      pll: s4mlxx::CortexM4::PLL {
        m: 50,
        n: 3,
        divisor: 4
      }
    }
  );

  let gpio_0 = cortex_common::GPIO::init(
    cortex_common::GPIO::InitParams {
      location: 0
    }
  };

  { gpio_1 ... gpio_25 }

  let gpio_26 = s4mlxx::GPIO::init(
    s4mlxx::GPIO::InitParams {
      location: 26
    }
  };

  let usart_0 = s4mlxx::SPI_USART::init(
    s4mlxx::SPI_USART::InitParams {
      location: 0,
      base_address: 0x40024000
    }
  );

  let usart_1 = s4mlxx::USART::init(
    s4mlxx::USART::InitParams {
      location: 1,
      base_address: 0x40024000
    }
  );

  { ... usart_2 ... }

  let usart_3 = s4mlxx::USART::init(
    s4mlxx::USART::InitParams {
      location: 3,
      base_address: 0x40024000
    }
  );

  /* device init inlined here */

  s4mlxx::init_done();
}

Note that the functions platform_name::init_start() and platform_name::init_done() are called before and after platform initialization, respectively. Properties beginning with a _ are ignored, and paths of nested items are rooted at the parent's path.