Skip to content

YagirProtect/Rust-Arduino-Basics

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rust Arduino Basics (Arduino Uno / ATmega328P)

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.


Quick start

cd Arduino
# edit Ravedude.toml -> set your serial port (COM5, /dev/ttyACM0, ...)
cargo run --release

Notes:

  • Arduino/.cargo/config.toml sets target = "avr-none" and uses ravedude as the runner.
  • This project uses build-std = ["core"] for the AVR target.
  • GlobalTimer reconfigures Timer0 (like Arduino core does). If you rely on Arduino-core millis()/delay(), don’t.
  • Use wrapping_sub for time deltas (the millis counter is u32 and wraps).
  • Arduino/build.rs invokes Python during build to stamp date/time env vars.

Prerequisites

  1. Rust nightly pinned in Arduino/rust-toolchain.toml.

  2. AVR tooling:

  • avr-gcc
  • avr-libc
  • avrdude
  1. Build helper:
  • python (in PATH)
  1. Flasher/runner:
  • ravedude

Canonical setup docs: https://github.com/Rahix/avr-hal#readme


Wiring cheatsheet

I²C bus (shared by LCD + sensors)

On Uno/Nano (ATmega328P):

  • SDAA4
  • SCLA5

All I²C devices connect in parallel to the same SDA/SCL and share GND. Each device must have its own I²C address.

LCD1602 + I²C backpack (PCF8574)

  • VCC5V
  • GNDGND
  • SDAA4
  • SCLA5

Common addresses: 0x27, 0x3F If backlight is on but no text: adjust the contrast potentiometer.


Firmware modules (expand)

The firmware crate exports modules under Arduino/src/modules/ and helpers under Arduino/src/std/.

Tiny “mini-std” layer

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.
  • u32 wraps naturally → use wrapping_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 + newline
Math 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.


Drivers (“modules”)

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 a heapless::String<64> scratch buffer.
  • ScreenLCD1602::print(&mut i2c) — prints the current buffer, interpreting \n as the second row.
  • EMode::Linear — blocking (bytes sent immediately).
  • EMode::Async — enqueues commands/data; you must call update(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)

  • VRxA0, VRyA1, 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);
    }
}

Tools (expand)

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 256

Args:

  1. PORTCOM5, /dev/ttyACM0, ...
  2. FILE – path to *.raw
  3. BAUD – default 250000
  4. RATE – samples/sec, default 8000
  5. CHUNK – bytes per write, default 256

Troubleshooting

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 (0x270x3F).
  • 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).

Licenses

Dual-licensed under either of:

  • Apache License 2.0 — LICENSE-APACHE
  • MIT License — LICENSE-MIT

Contributing

PRs/issues welcome. By default, contributions are dual-licensed under Apache-2.0 OR MIT.

About

Arduino basic module drivers on Rust

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages