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.
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.
Since drivers run in userspace, they need special permission to access hardware registers. CMRX provides this through MMIO (Memory-Mapped I/O) ranges.
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:
start_address is inclusive (can access this address)end_address is exclusive (cannot access this address)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
Let’s create a complete I2C driver that can handle multiple I2C devices. We’ll reuse the CharacterDevVtable interface from the RPC example.
#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:
vtable - RPC interface (read/write/close functions)device_address - The I2C address of the specific devicei2c_phy - Pointer to the physical I2C peripheral instance// 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:
this pointer to the specific device// 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
};
// 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);
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);
i2c_driver)sensor_app)SHARED keyword for data buffersThis driver pattern forms the foundation for building robust, secure embedded systems where hardware access is controlled and applications remain isolated from driver implementation details.