A DMOD module library that provides a standardized interface for device drivers, enabling uniform access to hardware devices in embedded systems.
dmdrvi is a driver interface module designed for embedded systems using the DMOD framework. It provides a consistent API for device driver operations including open, close, read, write, ioctl, and other standard device operations. The module uses device numbers (major/minor) to identify and manage devices.
- Standardized Device Interface: Uniform API for all device drivers
- Device Identification: Major/minor device number system
- Flexible Access Modes: Read-only, write-only, and read-write support
- Standard Operations: open, close, read, write, ioctl, flush, stat
- 64-bit Addressing: Explicit offset, size, and signed I/O-result types for devices larger than 4 GiB
- Block Device Controls: Standard geometry, erase, and discard ioctls
- Input Controls: Standard info / state / wait-for-event ioctls for touch panels, mice and buttons, so higher layers use every input device the same way
- Monitor Controls: Class-independent
GET_POLICY/EVENT/REFRESHioctls that let a monitor service handle presence detection, hot-plug and polling for a driver, so drivers never create threads for it - Configuration Support: Integration with dmini for device configuration
- Friends Groups: Group related driver configurations and let drivers discover one another's device paths
- Dynamic Device Notifications: Drivers can inform dmdevfs when a device becomes available or unavailable at runtime within an existing context
- Network Driver Ioctls: Built-in ioctl commands for MAC address configuration, link status, and interface start/stop (see
dmdrvi_ioctl.h) - Audio Ioctls: Codec controls (identity, format and output, volume, mute) and one stream contract (layout, start/stop, status, I/O timeout, drain) shared by codecs and bare serial audio interfaces (see
dmdrvi_ioctl.h) - SAL-Compatible: Uses only DMOD SAL functions
- Lightweight: Minimal memory footprint suitable for embedded systems
dmdrvi_create(config, dev_num)- Create driver context with configurationdmdrvi_free(context)- Free driver context
dmdrvi_open(context, flags, dev_num)- Open device with specified flagsdmdrvi_close(context, handle)- Close device handledmdrvi_read(context, handle, buffer, size, offset)- Read at a 64-bit byte offset; returns bytes, EOF, or a negative errordmdrvi_write(context, handle, buffer, size, offset)- Write at a 64-bit byte offset; returns bytes or a negative errordmdrvi_ioctl(context, handle, command, arg)- Device control operationsdmdrvi_flush(context, handle)- Flush device buffersdmdrvi_stat(context, path, stat)- Get device status
dmdrvi_device_available(context, dev_num)- Driver informs dmdevfs that a new device is available within an existing contextdmdrvi_device_unavailable(context, dev_num)- Driver informs dmdevfs that a device is no longer available
dmdrvi_friend_changed(context, info)- Notify a driver about a device belonging to the same friends group (optional driver callback)
DMDRVI_O_RDONLY- Open for read onlyDMDRVI_O_WRONLY- Open for write onlyDMDRVI_O_RDWR- Open for read and write
#include "dmdrvi.h"
// Create driver context - driver assigns device numbers
dmdrvi_dev_num_t dev_num; // Output parameter
dmdrvi_context_t ctx = dmdrvi_create(NULL, &dev_num);
// Check what numbering scheme the driver uses
if (dev_num.flags == DMDRVI_NUM_NONE) {
// Device file: /dev/dmclk
Dmod_Printf("Device: /dev/dmclk\n");
} else if (dev_num.flags & DMDRVI_NUM_MINOR) {
// Device file: /dev/dmspi0/0, /dev/dmspi0/1, etc.
// Driver uses both major and minor (directory structure)
Dmod_Printf("Device: /dev/dmspi%d/%d\n", dev_num.major, dev_num.minor);
} else if (dev_num.flags & DMDRVI_NUM_MAJOR) {
// Device file: /dev/dmuart0, /dev/dmuart1, etc.
// Driver uses major number only
Dmod_Printf("Device: /dev/dmuart%d\n", dev_num.major);
}
// Check if driver provides a human-friendly alternative file name
if (dev_num.flags & DMDRVI_NUM_ALT_NAME) {
Dmod_Printf("Alternative name: /dev/%s\n", dev_num.alt_name);
}
// Open device for reading and writing
void* handle = dmdrvi_open(ctx, DMDRVI_O_RDWR, &dev_num);
// Write data to device
char write_buffer[] = "Hello Device";
dmdrvi_ssize_t written = dmdrvi_write(ctx, handle, write_buffer, sizeof(write_buffer), 0);
// Read data from device
char read_buffer[256];
dmdrvi_ssize_t read = dmdrvi_read(ctx, handle, read_buffer, sizeof(read_buffer), 0);
// Get device status (does not require opening the device)
dmdrvi_stat_t stat;
int result = dmdrvi_stat(ctx, "/dev/dmuart0", &stat);
Dmod_Printf("Device size: %llu bytes\n", (unsigned long long)stat.size);
// Flush buffers
dmdrvi_flush(ctx, handle);
// Close device
dmdrvi_close(ctx, handle);
// Free context
dmdrvi_free(ctx);dmdrvi_ioctl.h defines a set of ioctl commands intended for network (e.g. Ethernet) drivers implemented on top of dmdrvi. Packet data itself is transferred with the regular dmdrvi_read/dmdrvi_write calls - these ioctls only cover control operations that don't fit the read/write model:
DMDRVI_IOCTL_NET_SET_MAC_ADDR- set the device MAC address (const dmdrvi_net_mac_addr_t*in)DMDRVI_IOCTL_NET_GET_MAC_ADDR- read the device MAC address (dmdrvi_net_mac_addr_t*out)DMDRVI_IOCTL_NET_GET_LINK_STATUS- read the current link state (dmdrvi_net_link_status_t*out)DMDRVI_IOCTL_NET_START- start the interface (begin packet reception/transmission)DMDRVI_IOCTL_NET_STOP- stop the interface
A typical bring-up sequence is: set the MAC address, start the interface, then poll (or wait for) the link status before sending/receiving packets:
#include "dmdrvi.h"
#include "dmdrvi_ioctl.h"
void* handle = dmdrvi_open(ctx, DMDRVI_O_RDWR, &dev_num);
// Configure the MAC address before starting the interface
dmdrvi_net_mac_addr_t mac = { .addr = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 } };
dmdrvi_ioctl(ctx, handle, DMDRVI_IOCTL_NET_SET_MAC_ADDR, &mac);
// Start the interface
dmdrvi_ioctl(ctx, handle, DMDRVI_IOCTL_NET_START, NULL);
// Check link status
dmdrvi_net_link_status_t link;
dmdrvi_ioctl(ctx, handle, DMDRVI_IOCTL_NET_GET_LINK_STATUS, &link);
if (link == DMDRVI_NET_LINK_UP) {
// Send a packet
uint8_t frame[64] = { /* ... */ };
dmdrvi_write(ctx, handle, frame, sizeof(frame), 0);
// Receive a packet
uint8_t rx_buffer[1518];
dmdrvi_ssize_t received = dmdrvi_read(ctx, handle, rx_buffer, sizeof(rx_buffer), 0);
}
// Stop the interface when done
dmdrvi_ioctl(ctx, handle, DMDRVI_IOCTL_NET_STOP, NULL);
dmdrvi_close(ctx, handle);For guidance on implementing these commands inside a network driver's dmdrvi_ioctl() DIF, see docs/dmdrvi.md.
DMDRVI_IOCTL_BLOCK_GET_INFOreturnsdmdrvi_block_info_tgeometry and capability flags.DMDRVI_IOCTL_BLOCK_ERASEperforms a physical erase over admdrvi_block_range_t.DMDRVI_IOCTL_BLOCK_DISCARDmarks admdrvi_block_range_tunused without promising its read-back contents.
All block ranges use 64-bit byte offsets and lengths. See the API reference for alignment and capability rules.
Any device exposing a framebuffer (LCD-TFT controller, SPI display, ...)
implements the standard DMDRVI_IOCTL_GFX_* commands (0x300 range), so a
generic graphics library only needs the device path: GET_INFO returns a
dmdrvi_gfx_info_t (resolution, dmdrvi_gfx_pixel_format_t, bytes per pixel,
stride, buffer count). The others are GET_FRAMEBUFFER, SWAP_BUFFERS,
WAIT_VSYNC, FILL_RECT (0xAARRGGBB, converted by the driver),
SET/GET_DISPLAY_ENABLED, SET/GET_BACKLIGHT and PRESENT. PRESENT
makes the drawing visible whatever the buffer count: double buffered, it shows
the drawing buffer at the next frame and copies the area drawn into the other
buffer, so a client that redraws only what changed draws without tearing and
without redrawing the rest; single buffered, it writes the area back from the
data cache. Reading/writing the node accesses the drawing buffer at the given
byte offset. Commands a driver cannot
honour return -ENOTSUP; driver-specific ones use DMDRVI_IOCTL_CUSTOM_BASE.
Any device a user touches, moves or presses (touch panel, mouse, buttons)
implements the standard DMDRVI_IOCTL_INPUT_* commands (0x400 range), so a
GUI library or a test tool only needs the device path, whatever the driver:
DMDRVI_IOCTL_INPUT_GET_INFO- returns admdrvi_input_info_t: name,dmdrvi_input_type_t,DMDRVI_INPUT_CAP_*capability bits, coordinate range, maximum number of contacts and buttons.DMDRVI_IOCTL_INPUT_GET_STATE- returns the currentdmdrvi_input_state_t(buttons, relative motion, wheel, contacts in screen coordinates).read()of the node returns the same.DMDRVI_IOCTL_INPUT_WAIT_EVENT- blocks until the state changes (const uint32_t*timeout in ms,NULL= forever),-ETIMEDOUTon timeout.
See docs/dmdrvi.md for the state rules, the recommended ini keys and an example.
Audio nodes use DMDRVI_IOCTL_AUDIO_* (0x500 range). GET_INFO,
CONFIGURE, SET_VOLUME and SET_MUTE are a codec's controls: hardware ID,
format and output, a 0..100 volume the driver maps to its gain, mute.
The stream commands - GET_STREAM, START, STOP, GET_STATUS,
SET_TIMEOUT / GET_TIMEOUT and DRAIN - are implemented by every node
that carries samples through read/write: a codec (samples in channels,
e.g. stereo) and a bare serial audio interface such as SAI, I2S or TDM
(samples in its wire slots). An application plays the same way on both -
CONFIGURE (optional), START, write, DRAIN, STOP - and learns the
buffer layout from GET_STREAM. A serial interface accepts CONFIGURE only
for its fixed format and answers the analog controls with -ENOTSUP. See
docs/dmdrvi.md.
DMDRVI_IOCTL_DEVFS_GET_FRIEND (0xF00 range, answered by dmdevfs, never by
the driver) returns the members of a node's friends_group to any module -
e.g. a display service finding the touch panel of its display. See
docs/dmdrvi.md.
Drivers whose devices need work done over time (card insertion/removal, USB port changes, media or link polling) implement three class-independent commands instead of creating threads. A monitor service calls them:
DMDRVI_IOCTL_MONITOR_GET_POLICY- returns admdrvi_monitor_policy_t: the dmhaman handler whose calls are the node's events, the settle time after events and the poll interval. Called once when the monitor starts.DMDRVI_IOCTL_MONITOR_EVENT- called immediately after every event. Fast and non-blocking; must not wait for in-flight I/O. It only records state or sets flags.DMDRVI_IOCTL_MONITOR_REFRESH- called at monitor start, after events once they settled, and every poll interval. May block; announces or withdraws nodes throughdmdrvi_device_available()/dmdrvi_device_unavailable().
Drivers without monitoring needs answer -ENOTTY. See docs/dmdrvi.md for the exact calling rules, examples and the recommended ini keys.
mkdir build
cd build
cmake .. -DDMOD_MODE=DMOD_MODULE
cmake --build .This generates:
dmf/dmdrvi.dmf- The driver interface library moduledmf/dmdrvi_version.txt- Version information
dmdrvi can work with dmini module for device configuration:
#include "dmini.h"
#include "dmdrvi.h"
// Parse device configuration
dmini_context_t config = dmini_create();
dmini_parse_file(config, "device.ini");
// Create driver context - driver assigns device numbers
dmdrvi_dev_num_t dev_num; // Output parameter
dmdrvi_context_t driver = dmdrvi_create(config, &dev_num);
// The driver has now assigned major/minor numbers and set the flags
// The filesystem layer can use dev_num to create appropriate device files
// Use driver...
dmdrvi_free(driver);
dmini_destroy(config);Friends groups connect device configurations that together implement one logical peripheral. This is useful when one driver depends on devices exposed by other drivers. For example, an SPI controller can use GPIO devices for its SCK, MISO, MOSI, and software-controlled chip-select pins.
Add the same friends_group value to every related INI section. dmdevfs then calls the optional dmdrvi_friend_changed() callback when information about a member of that group changes. The callback receives a dmdrvi_friend_info_t containing:
group_name- the value offriends_groupfriend_role- an optional application-defined role from the member'sfriend_rolesettingalt_name- the member's alternative device name, when presentnode_path- the full path of the device node; a dependent driver can save this path and open the device when neededstate- the current device state (unknown,ready,sleeping, ordead)dev_num- the member's device-number information
friend_role is useful when a group contains several devices of the same type. Instead of depending on a section name or a generated path, the consuming driver can find a member by its logical role, such as chip_select.
The following configuration groups an SPI2 controller and its GPIO pins under arduino_spi. The SPI driver can identify the CS GPIO by its chip_select role and obtain its path (on this board, /dev/dmgpio8/arduino_spi_cs) from info->node_path:
; STM32F746G-DISCO SPI2 on the Arduino Uno V3 connector:
; D13/SCK=PI1, D12/MISO=PB14, D11/MOSI=PB15, D10/CS=PI0.
[arduino_spi_sck]
driver_name=dmgpio
friends_group=arduino_spi
pin=PI1
mode=alternate
alternate_function=5
speed=maximum
output_circuit=push_pull
pull=none
[arduino_spi_miso]
driver_name=dmgpio
friends_group=arduino_spi
pin=PB14
mode=alternate
alternate_function=5
pull=none
[arduino_spi_mosi]
driver_name=dmgpio
friends_group=arduino_spi
pin=PB15
mode=alternate
alternate_function=5
speed=maximum
output_circuit=push_pull
pull=none
[arduino_spi_cs]
driver_name=dmgpio
friends_group=arduino_spi
friend_role=chip_select
pin=PI0
mode=output
pull=up
speed=maximum
output_circuit=push_pull
[arduino_spi]
driver_name=dmspi
friends_group=arduino_spi
instance=2
role=master
baudrate=1000000
mode=0
bit_order=msb_first
nss_mode=soft
cs_active_level=lowA driver that consumes a friend can implement the callback as follows:
void dmdrvi_friend_changed(dmdrvi_context_t context,
const dmdrvi_friend_info_t* info)
{
struct dmspi_context* spi = (struct dmspi_context*)context;
if (info->group_name != NULL &&
info->friend_role != NULL &&
strcmp(info->friend_role, "chip_select") == 0) {
if (info->state == dmdrvi_dev_state_ready && info->node_path != NULL) {
/* Copy node_path into storage owned by the driver. */
save_cs_path(spi, info->node_path);
} else {
clear_cs_path(spi);
}
}
}The callback is optional. Drivers that do not need to discover or track other members of their group do not have to implement it.
The driver manages device numbering within its own namespace. Each driver decides whether it uses numbering and which numbering scheme to employ:
-
No Numbering (DMDRVI_NUM_NONE):
- Driver doesn't use device numbers
- Device file uses driver name only
- Example:
/dev/dmclk
-
Major Number Only (DMDRVI_NUM_MAJOR):
- Driver uses major number to identify channels
- Device files named with major number suffix
- Example:
/dev/dmuart0,/dev/dmuart1
-
Major and Minor Numbers (DMDRVI_NUM_MAJOR | DMDRVI_NUM_MINOR):
- Driver uses both major and minor numbers
- Creates directory for major number, files for each minor number
- Example:
/dev/dmspi0/0,/dev/dmspi0/1,/dev/dmspi1/0
-
Alternative File Name (DMDRVI_NUM_ALT_NAME):
- Driver provides a human-friendly alternative name for the device file (max 32 characters)
- The
alt_namefield indmdrvi_dev_num_tcontains the alternative name - Can be combined with other flags
- Example:
/dev/my_sensor
- Major number: Identifies the device channel within a driver (e.g., UART0, UART1, SPI0)
- Minor number: Identifies specific configuration for the same channel (e.g., different SPI speeds for different CS lines)
- Each driver has its own namespace: Different drivers can use the same major/minor numbers independently
The driver assigns device numbers when creating a context. The filesystem layer receives the assigned numbers and creates appropriate device files.
Example device numbers:
Driver Flags Major Minor Device Path
-------------------------------------------------------------
CLK NUM_NONE - - /dev/dmclk
UART NUM_MAJOR 0 - /dev/dmuart0
UART NUM_MAJOR 1 - /dev/dmuart1
SPI NUM_MAJOR|NUM_MINOR 0 0 /dev/dmspi0/0
SPI NUM_MAJOR|NUM_MINOR 0 1 /dev/dmspi0/1
SPI NUM_MAJOR|NUM_MINOR 1 0 /dev/dmspi1/0
Most functions return error codes compatible with standard errno values:
0- Success- Negative values - Error codes (implementation-specific)
For read/write operations:
- Returns number of bytes transferred on success
- Returns 0 or negative value on error
The driver interface module has a minimal footprint suitable for embedded systems. Actual memory usage depends on the underlying driver implementation.
MIT License - see LICENSE file for details