device-driver Toolkit

repository·master·Indexed 43 days ago

https://github.com/diondokter/device-driver

A toolkit for writing high-quality Rust device drivers using a domain-specific specification language (DDSL) and automated API generation. It includes the device-driver runtime and the device-driver-cli (ddc) for ahead-of-time code generation, supporting synchronous and asynchronous register access, command dispatching, and buffer transfers.

Tokens
58.5K
Snippets
186
Records
297
Agent score
54%

What's inside device-driver

  1. What is the device-driver toolkit?

    master

    The device-driver toolkit is a Rust-based framework designed to automate the creation of safe, documented hardware device drivers. It solves two primary pain points in driver development:

    1. Complex Data Structures: It provides battle-tested data structures for representing hardware interfaces.
    2. Manual Definition Work: It uses code generation (via a macro DSL or YAML configuration) so developers only need to define the unique aspects of their hardware.

    The toolkit focuses on correctness, ease of use, and high-quality documentation by providing a dense input language, support for various byte/bit orderings, and analysis steps to catch common mistakes.

  2. Overview of device-driver-lexer

    master

    The device-driver-lexer crate is part of the Device Driver Toolkit, designed to help developers write better device drivers more efficiently.

    Warning: This is an internal crate and is not intended for public usage. It does not follow Semantic Versioning (SemVer). If you choose to use it in your project, you must pin the version to prevent breaking changes from unexpected updates.

  3. What is a register in the device driver toolkit?

    master
    A register is a singular piece of addressable memory stored on the device. It is the fundamental unit for defining operations on a specific memory block. Functionality is exposed through the RegisterOperation struct, which provides the interface for reading, writing, or modifying the register's state.
  4. What is the device-driver toolkit and DDSL?

    master

    The device-driver toolkit is designed to help developers write hardware drivers more efficiently. It uses a custom domain-specific language called DDSL (device driver specification language) as input to define hardware registers and fields.

    Drivers generated with this toolkit are compatible with any no-std context and can be integrated with the embedded-hal crate or any custom hardware interfaces. It is particularly well-suited for peripherals communicating over buses like I2C, SPI, or parallel buses (e.g., accelerometers, radio transceivers, or displays).

    register SYNT {
        address: 0x05,
        reset: 0x42162762,
        fields: fieldset _ {
            size-bytes: 4,
            byte-order: BE,
            
            field PLL_CP_ISEL 31:29 -> uint,
            field BS 28 -> bool,
            field SYNT 27:0 -> uint
        }
    }
  5. What is a Ref and how does it work?

    master

    A Ref is a copy of an existing object (a register, command, or block) where specific parts of that object are overridden with new values.

    Key Characteristics

    • Purpose: Use refs to create variations of objects that share the same structure but differ in properties like address. This is preferred over using a repeat block when the objects are not logically repetitions.
    • Usage: Using a ref is functionally identical to using the original object, just under a new name.
    • API Impact: If you override a field's reset value, the toolkit provides an extra constructor to initialize that fieldset with the new overridden reset value.
    • Limitations:
      • Buffers cannot be reffed (they are too simple to require overrides).
      • Refs cannot target other refs (nesting refs is not supported).
      • Immutable properties: You cannot override properties that define the field set itself, such as size_bits, fields, or byte_order.

    Supported Targets

    • Registers
    • Commands
    • Blocks
  6. What is a Fieldset and how to use it

    master

    A Fieldset is a collection of fields that represent the data within a register, a command input, or a command output. It defines a specific type where individual fields are accessible via getter and setter methods named after the fields.

    While users typically do not need to construct fieldsets manually, you can do so for special cases. Fieldsets also support bitwise operators (e.g., !, ^, &) which operate on all underlying bits, including those not explicitly part of a defined field. Additionally, fieldsets implement Into and From for byte arrays of the same size.

    use device_driver::Fieldset;
    
    let mut reg = MyFieldSet::ZERO;
    reg.set_foo(1234);
    let foo = reg.foo();
  7. Define and use Fields within a FieldSet

    master

    A Field is a slice of bits defined within a fieldset. To define a field, you must specify:

    1. Bit Address: The position within the fieldset.
    2. Access Limits: Whether the field is Read-Write (RW), Read-Only (RO), or Write-Only (WO).
    3. Base Type: A type capable of being converted to and from a bit slice.

    Type Conversions

    While fields have a raw base type, you can convert these types into enums or externs.

    • Use the try keyword to mark a conversion as 'fallible'. This is required when the conversion from the raw bit slice to the target type might fail.

    Accessing Fields

    Interaction with fields in code is performed using generated setters and getters.

    • Setters: Used to write values to the field (e.g., set_field_name(value)).
    • Getters: Used to read values from the field (e.g., field_name()). If the conversion is marked as fallible, the getter will return a Result and requires handling (e.g., using ?).
    let mut reg = MyFieldSet::ZERO;
    
    // Using a setter
    reg.set_foo(1234);
    
    // Using a getter for a non-fallible type
    let foo = reg.foo();
    
    // Using a setter with an enum
    reg.set_bar(MyEnum::A);
    
    // Using a getter for a fallible conversion (requires ?)
    let bar = reg.bar()?;
  8. Use enums and conversions in DDSL fields

    master

    To make driver code more readable, you can map bit patterns to meaningful names using enums. This can be done inline within a field definition.

    • Infallible conversion: Use -> _ as enum _ when the enum covers all possible bit patterns.
    • Fallible conversion: Use -> _ as try enum _ when the enum might not cover all possible bit patterns.

    Example of an inline enum conversion:

    field tremolo_depth 7 -> _ as enum _ {
        Low: 0b0,
        High: 0b1,
    }
    field tremolo_depth 7 -> _ as enum _ {
        /// 1.0dB
        Low: 0b0,
        /// 4.8dB
        High: 0b1,
    }
  9. Work with Devices and Blocks

    master

    Devices and blocks are the primary containers for operations.

    • Devices: The root block. It always has an address-offset of 0. You can construct a device using its generated new function with an owned interface value. To release the device and reclaim the interface, call the free function.
    • Blocks: Nested structures within devices or other blocks.
    • The Block Trait: Both devices and blocks implement the Block trait, providing raw interface access and the ability to start bulk operations.

    Note on Bulk Operations: A bulk operation only has access to the specific device or block it was started on. If an operation requires access to the entire device hierarchy, ensure you start the bulk operation on the device itself.

  10. How device interface traits work

    master

    The device-driver library uses a trait-based system to define what operations an interface can perform. Every type of hardware operation has a corresponding trait. If your interface implements these traits, the generated driver can perform those operations.

    Core Trait Categories

    • Register Interface: For reading and writing registers.
    • Command Interface: For sending commands to the device.
    • Buffer Interface: For buffer-based operations.

    Async Support

    For every synchronous trait, there is a corresponding Async version. When you implement the async trait, you can use the _async suffix on the driver's methods (e.g., .read_async().await).

  11. Define a Command in DDSSL

    master

    In the Device Driver Specification Language (DDSSL), a command represents an operation. Commands are defined using an identifier followed by optional parameters in square brackets (like stride) and a body containing specific properties.

    Commands belong to the Operation identifier namespace and support being repeated, but they do not support basetype, conversion type, or subnodes directly within the command definition itself.

    /// doc comment line
    command Example[8 stride 4] {
        address: 0,
        address-overlap: allow,
        fields-in: MyFieldset,
        fields-out: MyFieldset,
    }