Following my previous exploration with the Adafruit RP2040 Feather and custom ThinkInk e-paper drivers, I wanted to take the next logical step in embedded Rust: building cross-platform, hardware-agnostic drivers that can jump between entirely different architectures without breaking a sweat.
I recently received the Adafruit ESP32-C6 Feather (PID 5933), featuring a single-core RISC-V SoC. To pair with it, I developed the modulino Rust library—a pure no_std driver that currently supports all 12 modules in the Arduino Modulino family.
Thanks to the Feather's built-in STEMMA QT / Qwiic connector, getting this setup running requires absolutely zero soldering. You can hardware-verify your embedded apps by simply daisy-chaining the modules together over a standard 100kHz I2C bus.
1. Setting Up the RISC-V Environment
Unlike ARM Cortex-M or RP2040 targets, the ESP32-C6 utilizes a 32-bit RISC-V core. We can easily configure our environment using standard Rust toolchains and espflash:
# Add the bare-metal RISC-V target toolchain rustup target add riscv32imac-unknown-none-elf # Install the flashing tool for Espressif chips cargo install espflash
Make sure your project's .cargo/config.toml is configured to leverage espflash for seamless flashing and serial monitoring over the built-in USB-JTAG connection:
[target.riscv32imac-unknown-none-elf] runner = "espflash flash --monitor"
2. Cargo Dependencies (Cargo.toml)
Here is the setup for your dependencies. Note that we are using the modern 0.2.0 release of the modulino crate, which natively interfaces with any hardware layer implementing the embedded-hal 1.0 I2C traits.
[package]
name = "esp32c6-modulino-examples"
version = "0.1.0"
edition = "2021"
[dependencies]
# Espressif bare-metal hardware abstraction layer
esp-hal = { version = "0.22.0", features = ["esp32c6"] }
esp-backtrace = { version = "0.14.0", features = ["esp32c6", "panic-handler", "print-uart"] }
esp-println = { version = "0.12.0", features = ["esp32c6", "log"] }
# The hardware-agnostic Modulino driver crate
modulino = "0.2.0"
3. Initializing the Shared I2C Bus on the C6
On the Adafruit ESP32-C6 Feather, the hardware layout maps the STEMMA QT connector directly to GPIO 19 (SDA) and GPIO 20 (SCL).
Because the modulino driver is fully decoupled from the hardware, you simply initialize the standard I2c peripheral via esp-hal at 100kHz and hand it directly over to the driver constructors.
Below is an operational template showcasing how to spin up a couple of different modules sequentially or concurrently from the same hardware initialization block:
#![no_std]
#![no_main]
use esp_backtrace as _;
use esp_hal::{
clock::ClockControl,
gpio::Io,
i2c::I2c,
peripherals::Peripherals,
prelude::*,
system::SystemControl,
delay::Delay,
};
use esp_println::println;
// Import your chosen modules from the crate
use modulino::{Pixels, Buttons, Thermo, Color};
#[entry]
fn main() -> ! {
// 1. Core peripheral and clock configuration
let peripherals = Peripherals::take();
let system = SystemControl::new(peripherals.SYSTEM);
let clocks = ClockControl::boot_defaults(system.clock_control).freeze();
let mut delay = Delay::new(&clocks);
// 2. Configure I2C Pins via IO MUX
let io = Io::new(peripherals.GPIO, peripherals.IO_MUX);
let sda = io.pins.gpio19; // Adafruit Feather C6 STEMMA QT SDA
let scl = io.pins.gpio20; // Adafruit Feather C6 STEMMA QT SCL
// 3. Initialize the I2C Bus at the standard 100 kHz Modulino frequency
let i2c = I2c::new(
peripherals.I2C0,
sda,
scl,
100_u32.kHz(),
&clocks,
);
// 4. Instantiating drivers is plug-and-play!
// For single-ownership testing, move the bus directly in.
// (Note: For multi-module setups, wrap the bus in a shared manager like embedded-hal-bus)
let mut pixels = Pixels::new(i2c).unwrap();
println!("Modulino initialization successful! Starting main loop...");
loop {
// Blink the 8 RGB LEDs on the Pixels module
pixels.set_all_color(Color::RED, 25).unwrap();
pixels.show().unwrap();
delay.delay_ms(500_u32);
pixels.set_all_color(Color::BLUE, 25).unwrap();
pixels.show().unwrap();
delay.delay_ms(500_u32);
}
}
4. Supporting the Complete 12-Module Ecosystem
The ultimate strength of the modulino architecture is that it brings uniform, strongly typed Rust patterns to all 12 modules over the daisy-chained STEMMA QT line.
Depending on which hardware you chain to your Feather, swapping modules into your logic takes only a few lines of clean code:
-
Buttons (
Buttons): Instantly intercept tactile input and toggle the three integrated hardware indicator LEDs cleanly viabuttons.read()?andbuttons.update_leds()?. -
Thermo (
Thermo): Reads environment data directly via an underlyinghs3003implementation using a provided hardware delay. -
Movement (
Movement): Exposes 3-axis accelerometer and gyroscope metrics from the on-board LSM6DSOX IMU sensor mapped directly into clear coordinate structs. -
Knob, Joystick, Buzzer, Distance, Vibro, LatchRelay, and more: Every single one maps directly to native structs implementing zero-overhead drivers.
Why This Architecture Wins
By targeting the embedded-hal ecosystem abstraction layer rather than pinning code to a specific register layout, we achieve true portability. The exact same application layer code can be compiled for an RP2040, a bare-metal Cortex-M microcontroller, or an Espressif RISC-V core like the ESP32-C6.
Check out the full suite of hardware-tested examples, browse the source code, or grab the crate for your next plug-and-play project:
-
Crates.io: crates.io/crates/modulino
-
GitHub Repository: github.com/melastmohican/modulino-rs
- Github Repository: github.com/melastmohican/adafruit-feather-esp32c6-discovery
This page (Rust on Adafruit ESP32-C6 Feather: Plug-and-Play I2C with 12 Arduino Modulino Modules) was last updated on May 30, 2026.