Simple Driver

Now that you understand applications and RPC communication, let’s explore how to create device drivers in CMRX. This example shows how to build a driver that safely controls hardware peripherals while maintaining system security.

What You’ll Learn

Understanding CMRX Drivers

In CMRX, drivers are userspace processes - they don’t run with kernel privileges. This is different from traditional embedded systems where drivers often run in kernel space.

Benefits of Userspace Drivers

How Drivers Access Hardware

Since drivers run in userspace, they need special permission to access hardware registers. CMRX provides this through MMIO (Memory-Mapped I/O) ranges.

Granting Hardware Access

Single Memory Region

For most peripherals, you need access to one memory region:

OS_APPLICATION_MMIO_RANGE(app_name, start_address, end_address);

Example:

// Grant access to UART0 registers (example addresses)
OS_APPLICATION_MMIO_RANGE(uart_driver, 0x40000000, 0x40001000);

Important Notes:

Multiple Memory Regions

Some peripherals have separate control and data regions:

OS_APPLICATION_MMIO_RANGES(app_name, 
    region1_start, region1_end,
    region2_start, region2_end);

Example:

// Grant access to both I2C0 and I2C1 peripherals
OS_APPLICATION_MMIO_RANGES(i2c_driver, 
    0x40044000, 0x40048000,  // I2C0 registers
    0x40048000, 0x4004C000); // I2C1 registers

Complete Driver Example: I2C Driver

Let’s create a complete I2C driver that can handle multiple I2C devices. We’ll reuse the CharacterDevVtable interface from the RPC example.

Step 1: Define the Driver Object

#include <cmrx/application.h>
#include <cmrx/rpc/implementation.h>
#include <hardware/i2c.h>

// Define our I2C device object
struct I2cDevice {
    const struct CharacterDevVtable * vtable;  // Must be first for RPC
    char device_address;                       // I2C slave address
    i2c_inst_t * i2c_phy;                     // Physical I2C peripheral
};

// Forward declare device instances so other apps can use them
extern struct I2cDevice display;
extern struct I2cDevice temp_sensor;

What This Structure Contains:

Step 2: Implement the Driver Functions

// Tell CMRX we're implementing CharacterDevVtable for I2cDevice
IMPLEMENTATION_OF(struct I2cDevice, struct CharacterDevVtable);

// Read data from I2C device
static int i2c_read(INSTANCE(this), char * buffer, unsigned char bufsize) {
    // Use SDK function to read from I2C device
    return i2c_read_blocking(this->i2c_phy, this->device_address, 
                           buffer, bufsize, false);
}

// Write data to I2C device
static int i2c_write(INSTANCE(this), const char * buffer, unsigned char datasize) {
    // Use SDK function to write to I2C device
    return i2c_write_blocking(this->i2c_phy, this->device_address, 
                            buffer, datasize, false);
}

// Close the I2C device (nothing to do for I2C)
static void i2c_close(INSTANCE(this)) {
    // I2C doesn't need explicit closing, so do nothing
    return;
}

Key Points:

Step 3: Create the Driver Infrastructure

// Define our own I2C peripheral instance
// (The SDK's global instance isn't accessible in our process)
static i2c_inst_t phy_i2c0 = { i2c0_hw, false };

// Create the vtable with our function implementations
VTABLE struct CharacterDevVtable i2c_device_vtable = {
    i2c_read,
    i2c_write,
    i2c_close
};

// Create specific device instances
struct I2cDevice display = {
    &i2c_device_vtable,
    0x3C,        // SSD1306 OLED display default address
    &phy_i2c0    // Connected to I2C0 peripheral
};

struct I2cDevice temp_sensor = {
    &i2c_device_vtable,
    0x48,        // TMP117 temperature sensor default address
    &phy_i2c0    // Also connected to I2C0 peripheral
};

Step 4: Configure Hardware Access and Create Application

// Grant access to I2C0 and I2C1 hardware registers
// These addresses are specific to RP2040 microcontroller
OS_APPLICATION_MMIO_RANGES(i2c_driver, 
    0x40044000, 0x40048000,  // I2C0 peripheral registers
    0x40048000, 0x4004C000); // I2C1 peripheral registers

// Create the driver application
OS_APPLICATION(i2c_driver);

Using the Driver from Applications

Now other applications can use the I2C devices through RPC calls:

#include "i2c_driver.h"  // Include device declarations
#include <cmrx/ipc/rpc.h>

// Data must be marked as SHARED for RPC access
SHARED char display_buffer[16] = "Hello, World!";
SHARED char temp_data[2];

void use_i2c_devices(void * data) {
    // Write data to the display
    int bytes_written = rpc_call(display, write, display_buffer, sizeof(display_buffer));
    if (bytes_written > 0) {
        printf("Sent %d bytes to display\n", bytes_written);
    }
    
    // Read temperature data
    int bytes_read = rpc_call(temp_sensor, read, temp_data, sizeof(temp_data));
    if (bytes_read > 0) {
        printf("Read %d bytes from temperature sensor\n", bytes_read);
    }
}

OS_APPLICATION(sensor_app);
OS_THREAD_CREATE(sensor_app, use_i2c_devices, NULL, 64);

How This Architecture Works

Driver Process (i2c_driver)

  1. Hardware Access: Has permission to access I2C peripheral registers
  2. Device Management: Manages multiple I2C devices on the same bus
  3. RPC Server: Exposes devices through RPC interface
  4. Abstraction: Hides I2C implementation details from applications

Application Process (sensor_app)

  1. RPC Client: Makes calls to driver’s RPC interface
  2. Data Sharing: Uses SHARED keyword for data buffers
  3. Device Agnostic: Doesn’t need to know about I2C specifics
  4. Safe Access: Cannot directly access hardware or corrupt driver state

CMRX Kernel

  1. Memory Protection: Enforces MMIO range restrictions
  2. RPC Routing: Routes calls between applications and driver
  3. Security: Validates RPC calls and shared data access

Advantages of This Approach

Security Benefits

Design Benefits

Development Benefits

This driver pattern forms the foundation for building robust, secure embedded systems where hardware access is controlled and applications remain isolated from driver implementation details.

← Remote Procedure Call Interrupts →