IIO Framework
Most no-OS example projects expose their device through an IIO (Industrial
I/O) interface, and the Getting Started tutorial drives one end to end.
This page explains what that interface is, the object model behind it, and how a
driver plugs into it — so that when you read an iio_* driver or add IIO
support to your own, the pieces make sense.
If you have not yet built and run an IIO application, do Getting Started first.
What IIO is, and why no-OS uses it
IIO is the same device model the Linux kernel uses for ADCs, DACs, sensors, and other data-converter-style hardware. no-OS implements the device side of that model in firmware: a small server (IIOD) runs on the target and speaks the IIO protocol over a transport (UART, USB CDC-ACM, or network). Any standard IIO client on a host can then discover and drive the device without knowing anything device-specific:
the libiio command-line tools —
iio_infoandiio_attrto enumerate the device and read/write attributes,iio_readdev/iio_writedevto stream buffered samples in and out, andiio_regfor register-level debug access,the IIO Oscilloscope or Scopy GUIs,
or any application linked against libiio.
The payoff is uniformity: every no-OS device that ships an IIO driver is inspected, configured, and streamed the same way, with tools that already exist. You get attribute discovery, buffered acquisition, and triggering "for free" once your driver fills in the model below.
The object model
An IIO device is described by a handful of nested structures, all defined in
iio/iio_types.h. From the top down:
iio_device
├── channels[] (iio_channel) the data streams: voltage0, temp, ...
│ └── attributes[] (iio_attribute) per-channel: scale, offset, ...
├── attributes[] (iio_attribute) device-global: sampling_frequency, ...
├── debug_attributes[] register-level debug access
├── buffer_attributes[] buffer configuration
└── callbacks read/write, buffer, trigger hooks
Device
struct iio_device is the root descriptor. It lists the device's channels
and attribute arrays, and provides the function pointers that IIOD calls to do
work — attribute access, buffer lifecycle, and (optionally) register debug and
trigger handling. A real example, from drivers/adc/adc_demo/iio_adc_demo.c:
struct iio_device adc_demo_iio_descriptor = {
.num_ch = TOTAL_ADC_CHANNELS,
.channels = iio_adc_channels,
.attributes = iio_adc_global_attributes,
.debug_attributes = NULL,
.buffer_attributes = NULL,
.pre_enable = update_adc_channels, /* before a capture */
.post_disable = close_adc_channels, /* after a capture */
.trigger_handler = adc_demo_trigger_handler,/* on a trigger event */
.submit = adc_submit_samples, /* move buffered data */
.debug_reg_read = iio_adc_demo_reg_read,
.debug_reg_write = iio_adc_demo_reg_write,
};
Channel
struct iio_channel describes one data stream the device presents — a single
ADC input, a DAC output, a temperature reading. Key fields:
ch_type— the kind of quantity (IIO_VOLTAGE,IIO_TEMP,IIO_ACCEL, ...); it determines the client-visible name prefix.channel/indexed— the numeric index (voltage0,voltage1).ch_out— input vs output (an ADC channel vs a DAC channel).scan_index/scan_type— the channel's position and sample format (bits, storage size, sign, endianness) within a buffered capture.attributes— the per-channel attribute array.
Attribute
struct iio_attribute is a named, readable/writable property exposed to the
client, implemented by a pair of callbacks:
struct iio_attribute {
const char *name;
/* ... */
int (*show)(void *device, char *buf, uint32_t len,
const struct iio_ch_info *channel, intptr_t priv);
int (*store)(void *device, char *buf, uint32_t len,
const struct iio_ch_info *channel, intptr_t priv);
};
show renders the current value into buf when the client reads the
attribute; store parses buf and applies it when the client writes.
Attributes are what surface things like sampling_frequency, scale, and
offset. Every attribute array is NULL-terminated with the
END_ATTRIBUTES_ARRAY marker ({.name = NULL}) so IIOD knows where it
ends.
Buffers
Buffered acquisition streams many samples at once instead of reading one
attribute at a time. When a client starts a capture, IIOD calls the device's
pre_enable hook, then repeatedly invokes submit to move sample data
between the device and the IIO buffer, and finally post_disable when the
capture stops. The application supplies the backing memory through a
struct iio_data_buffer (a size and a buff pointer) when it registers
the device.
Triggers
A trigger paces buffered acquisition. no-OS supports both hardware triggers
(a timer or GPIO interrupt, via iio/iio_trigger.*) and software
triggers. When a trigger fires, IIOD invokes the device's
trigger_handler, which performs one round of sampling. The IIO Demo's
iio_timer_trigger and iio_wifi_with_sw_trig variants exercise these two
paths; see Getting Started for how to build them.
How it runs: the iio_app helper
Most projects do not talk to IIOD directly. They use the thin iio_app layer
(iio/iio_app/iio_app.h), which wires the IIO devices to a transport and runs
the server loop. The application:
Builds an array of
struct iio_app_device, each binding a name, the device instance, itsiio_devicedescriptor, and optional read/write buffers:struct iio_app_device iio_devices[] = { { .name = "adc_demo", .dev = adc_demo_desc, .dev_descriptor = &adc_demo_iio_descriptor, .read_buff = &adc_buff, }, };
Fills a
struct iio_app_init_paramwith that array (plus the UART/transport configuration) and callsiio_app_init().Calls
iio_app_run(), which starts IIOD and services client requests until the program ends.
That is the entire application-side contract: describe your devices, hand them
to iio_app, and run. The transport (plain UART, USB CDC-ACM, or Wi-Fi) is
chosen by the project variant, which is why the same device is reachable over a
serial cable in the iio variant and over the network in the iio_wifi
variant without any change to the device model.
Software architecture
The runtime is a small stack of modules. The application drives iio_app,
which owns the iio core; the iio core in turn owns and drives iiod
(the IIO-protocol engine), which decodes each request and calls back into
iio through the iiod_ops callbacks. The whole thing talks to a host
client over the chosen transport:
┌────────────────────────────────────────────────────────┐
│ Application │
│ builds iio_app_device[], then calls │
│ iio_app_init() and iio_app_run() │
└────────────────────────────┬───────────────────────────┘
│ iio_app_init() / iio_app_run()
▼
┌────────────────────────────────────────────────────────┐
│ iio_app │
│ iio_app_init(): init the UART, build the │
│ device list, then call iio_init() │
│ iio_app_run(): loop over iio_step(), then │
│ the optional post_step_callback (custom app code) │
└────────────────────────────┬───────────────────────────┘
│ iio_step()
▼
┌────────────────────────────────────────────────────────┐
│ iio — iio_init() creates iio_desc (devices, │
│ XML, triggers), sets up the transport, │
│ creates the iiod, and wires the │
│ iiod_ops callbacks │
│ │
│ iio_step() ──► iiod_conn_step() │
│ │ │
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ iiod │ │
│ │ parses the IIO protocol and │ │
│ │ dispatches each request via │ │
│ │ iiod_ops callbacks │ │
│ └────────────────────────────────┘ │
│ │
│ iiod invokes the iiod_ops callbacks that iio │
│ implements: attribute I/O, buffer ops, and │
│ send/recv over the transport │
└────────────────────────────┬───────────────────────────┘
│ send / recv over transport
▼
┌────────────────────────────────────────────────────────┐
│ transport UART · USB CDC-ACM · TCP/IP │
└────────────────────────────┬───────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ Client (host) │
│ iio_info, iio_readdev, iio_writedev, │
│ IIO Oscilloscope, Scopy, pyadi-iio, … │
└────────────────────────────────────────────────────────┘
iio_app — the application interface (
iio/iio_app/iio_app.c).iio_app_init()initializes the UART, assembles the device list, and callsiio_init();iio_app_run()then drives the main loop, callingiio_step()repeatedly. Every IIO project uses it. The loop is cooperative, not blocking: if the application supplies apost_step_callback(with anarg) in itsiio_app_init_param,iio_app_run()calls it once after eachiio_step()— so your own application code runs in parallel with the IIOD server, without an RTOS, on the same single thread.iio — the core (
iio/iio.c).iio_init()creates theiio_desc, registers theiio_devicedescriptors, sets up the transport, creates theiiodinstance, and wires theiiod_opscallbacks to its own handlers. At runtime it walks the registered devices to read/write attributes and move buffered data, and generates the XML that describes the devices to the client.iio_step()services one connection by callingiiod_conn_step(), and theiiod_opscallbacks (attribute I/O, buffer ops,send/recv) are its implementation of whatiiodinvokes.iiod — the IIO-protocol engine (
iio/iiod.c, formerly the externallibtinyiiod). It parses the wire protocol and dispatches each request back intoiiothrough the matchingiiod_opscallback.transport — the byte pipe between target and host. Which one is used is a project-variant choice, not a code change: the
iiovariant runs over UART/USB CDC-ACM, theiio_wifivariant over TCP/IP.
Request lifecycle
Once iio_app_run() is looping, a single client request flows through the
stack like this:
The client (say
iio_readdev) sends a protocol command over the transport.iio_step()hands the received bytes to iiod, which parses the command and identifies the target device, channel, and operation.iiod calls the corresponding iio operation — an attribute
show/store, or a bufferpre_enable→submit→post_disablesequence for a capture (see Buffers and Triggers above).iio invokes the device's callbacks from its
iio_devicedescriptor, which read or write the actual hardware through the core driver.The result (attribute value, sample buffer, or the device XML) travels back out through iiod and the transport to the client.
Attribute discovery works the same way: on connect the client asks for the
context description, and iio generates the XML from the registered
iio_device descriptors — which is how a generic tool like iio_info or
IIO Oscilloscope can present your device without knowing anything about it in
advance.
Connecting a client
Once a project is built and running, connect any libiio client over the transport the project exposes. To enumerate a device and its attributes:
# serial (USB CDC-ACM / UART)
iio_info -u serial:/dev/ttyACM0,57600,8n1n
# network (Wi-Fi / Ethernet variants, Linux host)
iio_info -u ip:<host>
Anatomy of an IIO driver
A no-OS device driver and its IIO layer are kept separate, by convention in two files:
drivers/<class>/<part>/
├── <part>.c / <part>.h the core driver (register access, no IIO)
└── iio_<part>.c / iio_<part>.h the IIO wrapper: channels, attributes,
descriptor, and callbacks
The core driver knows nothing about IIO — it is the same
init_param/descriptor-style driver described in no-OS drivers guide. The
iio_<part> layer builds the object model on top of it: it declares the
channel array, the attribute arrays with their show/store callbacks that
call into the core driver, and the iio_device descriptor that ties them
together (like adc_demo_iio_descriptor above). This separation means the
core driver stays usable in a non-IIO application, and the IIO model is added
only when a project wants it.
Where to go next
Getting Started — build and run the IIO Demo, then stream data with
iio_readdev/iio_writedev.Architecture — how the IIO framework sits on top of the
no_os_*HAL and the rest of the stack.no-OS drivers guide — writing the core driver that an
iio_<part>layer wraps.The
adc_demo/dac_demodrivers (drivers/adc/adc_demo,drivers/dac/dac_demo) — the smallest complete IIO drivers to read as a template.IIO No-OS, IIO Oscilloscope, and Scopy — the client-side documentation.