Small Rust + arduino-hal playground for Arduino Uno (ATmega328P @ 16 MHz).
Repo layout:
- Firmware:
Arduino/—#![no_std]examples + a tiny project-local “mini-std” layer (timer + serial logging). - PC tools:
Tools/— helper utilities (e.g. a serial streamer for raw 8‑bit audio).
Target board: Arduino Uno / ATmega328P @ 16 MHz.
cd Arduino
# edit Ravedude.toml -> set your serial port (COM5, /dev/ttyACM0, ...)
cargo run --releaseNotes:
Arduino/.cargo/config.tomlsetstarget = "avr-none"and uses ravedude as the runner.- This project uses
build-std = ["core"]for the AVR target. GlobalTimerreconfigures Timer0 (like Arduino core does). If you rely on Arduino-coremillis()/delay(), don’t.- Use
wrapping_subfor time deltas (the millis counter isu32and wraps). Arduino/build.rsinvokes Python during build to stamp date/time env vars.
-
Rust nightly pinned in
Arduino/rust-toolchain.toml. -
AVR tooling:
avr-gccavr-libcavrdude
- Build helper:
python(inPATH)
- Flasher/runner:
ravedude
Canonical setup docs: https://github.com/Rahix/avr-hal#readme
On Uno/Nano (ATmega328P):
SDA→A4SCL→A5
All I²C devices connect in parallel to the same SDA/SCL and share GND. Each device must have its own I²C address.
VCC→5VGND→GNDSDA→A4SCL→A5
Common addresses: 0x27, 0x3F
If backlight is on but no text: adjust the contrast potentiometer.
The firmware crate exports modules under Arduino/src/modules/ and helpers under Arduino/src/std/.
GlobalTimer — Timer0 millis (CTC + Compare A ISR)
What: global millisecond counter backed by Timer0 interrupt.
Why: schedule periodic tasks without blocking delay().
Notes
- ATmega328P @ 16 MHz, prescaler 64,
OCR0A=249→ ~1 ms tick. - Requires interrupts enabled after init.
u32wraps naturally → usewrapping_sub.
Example
use crate::std::global_timer::GlobalTimer;
use crate::std::std::enable_interrupts;
let timer = GlobalTimer::new(&dp.TC0);
enable_interrupts();
let mut last = timer.millis();
loop {
let now = timer.millis();
if now.wrapping_sub(last) >= 200 {
last = now;
// do something every 200 ms
}
}IoUno — UART logger (heapless buffer + newline send)
What: serial logger without std, using a heapless::String<64> scratch buffer and sending it over USART0.
Example
use core::fmt::Write;
use crate::std::io::IoUno;
let mut io = IoUno::new(dp.USART0, pins.d0, pins.d1, 115200);
let x = 123;
let y = 456;
writeln!(io.str(), "x={}, y={}", x, y).ok();
io.log(); // sends the buffer + newlineMath helpers — small no_std utilities (internal)
What: inverse_lerp, lerp, normalize (float helpers).
Important: in the current repo std/math.rs is a private submodule (mod math;), so it is intended for internal use / future re-exporting.
LCD1602 (HD44780) over I²C (PCF8574) — blocking + queued (“async”)
What: character LCD driver through an I²C backpack (PCF8574).
Why “slow”: HD44780 has slow commands (clear/home ~1.5ms), and with PCF8574 each character becomes multiple I²C writes (send high nibble + low nibble, each latched by E).
API in this repo
ScreenLCD1602::get_line()— clears and returns aheapless::String<64>scratch buffer.ScreenLCD1602::print(&mut i2c)— prints the current buffer, interpreting\nas the second row.EMode::Linear— blocking (bytes sent immediately).EMode::Async— enqueues commands/data; you must callupdate(now_ms, &mut i2c)in the main loop.
Blocking example (Linear)
use core::fmt::Write;
use crate::modules::screen_lcd1602::screen_lcd1602::{EMode, ScreenLCD1602};
let mut lcd = ScreenLCD1602::new(0x27, &mut i2c, EMode::Linear);
write!(lcd.get_line(), "Hello!\nWorld").unwrap();
lcd.print(&mut i2c);Queued example (Async)
use core::fmt::Write;
use crate::modules::screen_lcd1602::screen_lcd1602::{EMode, ScreenLCD1602};
let mut lcd = ScreenLCD1602::new(0x27, &mut i2c, EMode::Async);
write!(lcd.get_line(), "pressed!!!").unwrap();
lcd.print(&mut i2c); // enqueues clear + cursor + bytes
loop {
let now = timer.millis();
lcd.update(now, &mut i2c); // executes queued ops (one per call)
}Tips
- Right now
print()always clears the display first. If you call it often, it will feel slow. - If backlight is on but no text: adjust contrast pot + confirm address (
0x27/0x3F).
Joystick HW-504 — analog X/Y + optional SW button
What: reads joystick axes via ADC and optional SW button via pull-up.
Wiring (typical)
VRx→A0,VRy→A1,SW→ e.g.D7(use pull-up)
Example
use crate::modules::joystick_hw504::JoystickHW504;
let mut adc = arduino_hal::Adc::new(dp.ADC, Default::default());
let x = pins.a0.into_analog_input(&mut adc);
let y = pins.a1.into_analog_input(&mut adc);
let sw = pins.d7.into_pull_up_input();
let mut js = JoystickHW504::new(Some(x), Some(y), Some(sw), 8);
loop {
let now = timer.millis();
js.update(now, &mut adc);
let _x = js.x_raw();
let _y = js.y_raw();
let _pressed = js.button_pressed();
}Light sensor (LDR) — analog read + optional power gating
What: reads an LDR divider via ADC; can optionally power the sensor from a GPIO.
Constructor in this repo
LightSensorResistor::new(power_pin: Option<OutputPin>, output_pin: AnalogPin, read_rate_ms: u32)
Example (with power gating)
use crate::modules::light_sensor_resistor::LightSensorResistor;
let mut adc = arduino_hal::Adc::new(dp.ADC, Default::default());
let power = pins.d7.into_output();
let analog = pins.a0.into_analog_input(&mut adc);
let mut ldr = LightSensorResistor::new(Some(power), analog, 50);
ldr.set_power(true);
loop {
let now = timer.millis();
ldr.update(now, &mut adc);
if ldr.is_read() {
let raw = ldr.last_data();
let pct = ldr.percent();
let _ = (raw, pct);
}
}Note: this module’s MAX_INPUT_VALUE is currently 512 (project-specific calibration).
BFS Water Sensor — analog + power-gating (anti-corrosion)
What: powers the probe only during read: HIGH → ADC → LOW.
Why: reduces electrolysis/corrosion and noise on exposed-trace sensors.
Example
use crate::modules::water_sensor_bfs::WaterSensorBFS;
let mut adc = arduino_hal::Adc::new(dp.ADC, Default::default());
let power = pins.d7.into_output();
let analog = pins.a0.into_analog_input(&mut adc);
let mut water = WaterSensorBFS::new(power, analog, 500);
loop {
let now = timer.millis();
water.update(now, &mut adc);
if water.is_read() {
let raw = water.last_data();
let pct = water.percent(); // f32
let _ = (raw, pct);
}
}Analog temperature sensor (LM25/LM35-style) — ADC to °C / °F without floats
What: reads analog temperature and converts using integer math (assumes 10 mV/°C).
API: to_celsius() -> (int, frac) and to_fahrenheit() -> (int, frac).
Example
use crate::modules::temperature_sensor_lm25::TemperatureSensorLM25;
let mut adc = arduino_hal::Adc::new(dp.ADC, Default::default());
let analog = pins.a0.into_analog_input(&mut adc);
let mut t = TemperatureSensorLM25::new(analog, 250);
loop {
let now = timer.millis();
t.update(now, &mut adc);
if t.is_read() {
let (c_int, c_frac) = t.to_celsius();
let (f_int, f_frac) = t.to_fahrenheit();
let _ = (c_int, c_frac, f_int, f_frac);
}
}Serial RAW audio streamer — Tools/MP3_SERIAL_STREAM
Streams a *.raw file (unsigned u8 mono PCM) to a serial port at real-time speed.
Run
cd Tools/MP3_SERIAL_STREAM
cargo run --release -- COM5 bad_apple.raw 250000 8000 256Args:
PORT–COM5,/dev/ttyACM0, ...FILE– path to*.rawBAUD– default250000RATE– samples/sec, default8000CHUNK– bytes per write, default256
I²C “no response” / device not detected
- Check GND (loose ground is #1).
- Verify SDA/SCL are on A4/A5 (Uno/Nano).
- Try the other LCD address (
0x27↔0x3F). - Keep wires short; if unstable, reduce I²C speed (for example from 100 kHz to 50 kHz).
LCD backlight on but no text
- Adjust contrast potentiometer.
- Confirm correct I²C address.
- Some PCF8574 backpacks use a different pin mapping (rare).
Dual-licensed under either of:
- Apache License 2.0 —
LICENSE-APACHE - MIT License —
LICENSE-MIT
PRs/issues welcome. By default, contributions are dual-licensed under Apache-2.0 OR MIT.