ArduLite
v2.0.0 GitHub ↗

Modern C++ Framework for Arduino AVR.

Beginner-friendly on the surface. Register-level underneath.

ArduLite is a lightweight, header-driven C++ framework for bare-metal AVR programming on the Arduino Uno, Nano, and Mega 2560. No pinMode, no digitalWrite, no hidden runtime — every call is designed to fold away at compile time into plain register access. New to microcontrollers? Start with Quick Start — you don't need to understand registers to use it.

Digital led(13, OUT);→sbi 0x04, 5DDRB |= 1<<5 — set as output
led.on();→sbi 0x05, 5PORTB |= 1<<5
led.off();→cbi 0x05, 5PORTB &= ~(1<<5)
led.toggle();→ldi r24,0x20 / out 0x03,r24write-1-to-PINB toggles PORTB, 2 cycles
led.read();→sbic 0x03, 5test PINB bit 5 directly

01 · Design Philosophy

Why ArduLite exists

The Arduino Core is friendly, but its abstractions — digitalWrite's pin lookup table, String's heap churn, delay()'s Timer0 dependency — cost cycles and bytes you don't always get to see. ArduLite is what you get if you rebuild the beginner-friendly vocabulary on nothing but avr-libc, and refuse anything that isn't zero-cost.

Goals

GoalHow ArduLite delivers it
Zero heapnew, malloc, std::vector, std::string are all off the table. Every object lives on the stack or in static storage with a size known at compile time.
Zero collisionArduLite never defines pinMode, digitalWrite, delay, millis, HIGH/LOW, or byte. It can be mixed into a project that still uses the Arduino Core without a single symbol clash.
Zero overheadPin-facing methods are marked always_inline. With a literal pin number, the whole lookup folds away at compile time into one sbi/cbi/sbic instruction — see the strip above.
Opt-in everythingThe master header only pulls in Digital, Analog, PWM and Utils. Timers, UART, SPI, I2C, interrupts and every component are separate headers you include only when you use them, so an unused peripheral is never initialized and never costs flash.

Explicit non-goals

ArduLite 1.x/2.x is scoped deliberately narrow. It does not attempt to support ESP8266, ESP32, STM32 or RP2040, and it does not ship WiFi, Bluetooth, Ethernet, MQTT, HTTP, OTA, or an RTOS. Those belong to separate projects built for those chips — bolting them onto an AVR-only, no-heap framework would break every guarantee above.

Who it's for. Students and teachers who want to see what a microcontroller is actually doing; makers and hobbyists who want Arduino-level readability without giving up register-level performance; embedded developers who want a sane starting point instead of writing raw avr/io.h by hand every time.

02 · Setup

Installation

Arduino IDE — via Library Manager (easiest)

  1. Open the Arduino IDE, then go to Sketch → Include Library → Manage Libraries… (or press Ctrl+Shift+I).
  2. Search for ArduLite in the Library Manager search box.
  3. Click Install on the ArduLite entry by Ajang Rahmat.
  4. Select Arduino Uno, Arduino Nano, or Arduino Mega 2560 under Tools → Board — ArduLite only targets these three.
  5. Include the master header in your sketch: #include <ArduLite.h>.

Arduino IDE — manual install

  1. Download the latest release archive from github.com/ArduLite/ArduLite.
  2. Extract it and rename the folder to ArduLite if needed, then place it inside your Arduino libraries directory (Documents/Arduino/libraries on most systems).
  3. Restart the Arduino IDE so it re-scans the libraries folder.
  4. Select your board and include ArduLite.h, same as above.

PlatformIO

Add it as a library dependency from the command line — PlatformIO fetches it straight from the GitHub repository, no manual copying needed.

terminal
# from inside your PlatformIO project folder
pio pkg install --library "https://github.com/ArduLite/ArduLite.git"

# or, for an older PlatformIO CLI version:
pio lib install "https://github.com/ArduLite/ArduLite.git"

Or add it directly to platformio.ini and let PlatformIO install it on the next build:

platformio.ini
[env:uno]
platform = atmelavr
board = uno
framework = arduino
lib_deps =
    https://github.com/ArduLite/ArduLite.git

; ArduLite does not need the Arduino framework at runtime, but PlatformIO's
; "arduino" framework is the simplest way to get an avr-gcc toolchain wired up.

Then run pio run to build, or pio run -t upload to build and flash.

No Arduino.h required. ArduLite sketches use a plain int main() entry point instead of setup()/loop(). If you're on the Arduino IDE, this still compiles fine — the IDE just needs one .ino file with a main() in it, which every ArduLite example provides.

03 · First Sketch

Quick Start

Three progressively richer examples: a blink, a PWM fade, and a sensor-driven brightness loop — the same three examples from the project README, shown here with a line-by-line breakdown.

1. Blink

Led_Blink.ino
#include <ArduLite.h>

int main() {
    Digital led(13, OUT);

    forever() {
        led.on();
        wait(1000);
        led.off();
        wait(1000);
    }
}

Digital led(13, OUT) configures pin 13 as an output once, at construction. forever() expands to while(1). wait(1000) blocks for 1000 ms using a calibrated _delay_loop_2 — no Timer0, no millis() dependency.

2. PWM fade

Led_PWM.ino
#include <ArduLite.h>

int main() {
    PWM led(9);

    forever() {
        for (uint8_t i = 0; i < 255; i++) { led.write(i); wait(5); }
        for (uint8_t i = 255; i > 0; i--) { led.write(i); wait(5); }
    }
}

3. Sensor-driven brightness

Analog_Read_Control_Led_Brightness.ino
#include <ArduLite.h>

int main() {
    PWM led(9);
    Analog sensor(A0);

    forever() {
        int sensorValue = sensor.read();
        float brightness = sensorValue * (255.0 / 1023.0);
        led.write(brightness);
    }
}

All three sketches are in examples/, importable directly from File → Examples → ArduLite in the Arduino IDE.

04 · Core Concepts

The zero-overhead model

Two techniques make this possible almost everywhere in ArduLite:

1. always_inline + compile-time constant pins

Every pin-facing helper (digitalWriteHigh, digitalTogglePin, ...) is marked __attribute__((always_inline)). Without it, GCC would leave the call as a real function at -Os, and the pin number would stop being a compile-time constant — forcing a runtime bit-shift to compute (1 << pin). With the attribute, and a literal pin like Digital led(13, OUT), the entire register lookup folds away and one method call becomes one AVR instruction.

2. Hardware toggle instead of read-modify-write

toggle() exploits a real AVR feature: writing a logic 1 to a bit in the PINx register flips that bit of PORTx, leaving every other bit untouched — 2 cycles, no read-modify-write race. Older chips without this feature (ATmega8/16/32) automatically fall back to PORTx ^= mask.

ArduLite callCompiles to (ATmega328P, literal pin)
Digital led(13, OUT)sbi 0x04, 5
led.on()sbi 0x05, 5
led.off()cbi 0x05, 5
led.read()sbic 0x03, 5
led.toggle()out 0x03, r24 (PINB write-1-to-toggle)
This only holds when the pin number is a literal or a constexpr. Passing a runtime variable as the pin still works correctly, but the compiler can no longer fold the branch chain, so you pay for the port lookup at runtime — the same trade-off you'd hit writing the register logic by hand.

Timer & resource map

ArduLite is careful never to let two modules fight over the same hardware timer. Know this map before combining modules in one sketch.

ResourceOwnerNotes
Timer0UntouchedReserved for the Arduino Core's own millis()/delay() if you ever mix them in — ArduLite never touches it.
Timer1PWM (pins 9/10 Uno), Servo, PulseDo not use Servo, Pulse, and PWM on pin 9/10 at the same time — they all reconfigure Timer1.
Timer2 — COMPATime / Timer / Stopwatch / Button / Interrupt debounceThe single 1 ms tick source (initTimer()), shared safely by every module that needs a clock.
Timer2 — COMPBcreateTimer() (TimerInterrupt)Only enabled when initTimerInterrupt()/createTimer() is actually called — a sketch that never uses it pays nothing.
ADCAnalogInitialized lazily on first read() call, AVcc reference, default prescaler 64.
TWI/I2C hardwarei2c global (ArduLiteI2C)Bit-rate register fixed at construction time (TWBR=72) — roughly 100 kHz at 16 MHz.
SPI hardwarespi global (ArduLiteSPI)Master mode only. The SS/CS pin is not managed automatically — pick it yourself via Digital.
UART hardwareuart / uart1..3 globalsMega 2560 exposes all four hardware UARTs; Uno/Nano only uart (UART0).
EEPROMeeprom global1024 bytes on ATmega328P, 4096 bytes on ATmega2560.

Core header — ArduLite.h

The master header is intentionally small. It defines short integer aliases, the forever() macro, and the two blocking-wait functions, then pulls in only Digital, Analog, PWM and Utils. Everything else is opt-in.

SymbolWhat it is
int8 / int16 / int32
uint8 / uint16 / uint32
Aliases for int8_t … uint32_t, in ArduLite's own naming style.
forever() { }Macro for while(1) { } — the main program loop, used instead of Arduino's loop() callback.
wait(uint32_t ms)Blocking millisecond delay. A real inline function, deliberately not a macro — a macro would rewrite every wait(...) substring across the whole program, including unrelated method names.
waitMicros(uint16_t us)Blocking microsecond delay, built on _delay_loop_2 (4 CPU cycles/iteration). One call covers up to ~16383 µs at 16 MHz.
wait()/waitMicros() never touch a timer — they're busy-loops calibrated purely from F_CPU. That means they can be called safely even before initTimer() has run, and they never collide with ArduLiteTime's Timer2-based clock.

05 · GPIO Reference

Digital

src/ArduLiteDigital.h

Digital pin control with true zero-cost abstraction. Holds a single byte of RAM (the pin number) and nothing else — every method is always_inline.

Header-only1 byte RAMNo heap
Constructor / MethodDescription
Digital(pin, dir)Configures pin as IN or OUT. Direction is applied once, immediately.
on()Drives the pin HIGH.
off()Drives the pin LOW.
write(bool value)HIGH when true, LOW when false.
read() constReturns the current logic level as bool.
toggle()Flips the pin — uses the hardware PINx-toggle trick where available.
pullUp()Enables the internal pull-up resistor. Only meaningful on an IN pin.
number() constReturns the stored pin number.
Led_Toggle_With_Push_Button.ino (excerpt)
#include <ArduLite.h>

int main() {
    Digital button(2, IN);
    Digital led(13, OUT);
    button.pullUp();   // button wired to GND, no external resistor needed

    forever() {
        if (!button.read()) {   // active-low: pressed == LOW
            led.toggle();
            wait(300);          // crude debounce
        }
    }
}
Direction constants are the enum Direction { IN, OUT } — ArduLite's own vocabulary, never Arduino's INPUT/OUTPUT, so both can coexist in one project.

Analog

src/ArduLiteAnalog.h / .cpp

10-bit ADC reads through the AVR's built-in analog-to-digital converter. Arduino-style A0–A15 pin constants are defined automatically if not already present.

Constructor / MethodDescription
Analog(pin)Binds to an analog pin (e.g. A0). The ADC hardware itself is initialized lazily, on the first read() call across the whole program.
read()Blocking single conversion. Returns a 10-bit value, 0–1023.
setPrescaler(uint8_t factor)Sets the ADC clock prescaler: 2, 4, 8, 16, 32, 64, or 128. Default is 64 (a safe, accurate speed at 16 MHz). Lower prescalers read faster but lose precision.
Analog_Coding_Test_Speed.ino (excerpt)
Analog sensor(A0);
sensor.setPrescaler(16);   // faster reads, slightly less accurate

int raw = sensor.read();      // 0 - 1023

PWM

src/ArduLitePWM.h / .cpp

Hardware PWM output using each chip's fast-PWM timer channels directly (Timer0/1/2/3/4 depending on pin and board) — no analogWrite(), no software bit-banging.

Constructor / MethodDescription
PWM(pin)Attaches to a PWM-capable pin and configures the matching timer in fast-PWM mode, non-inverted. Pins that aren't PWM-capable on the target board are silently ignored (no-op).
write(uint8_t value)Duty cycle 0-255. 0 disables PWM output entirely and forces the pin LOW (rather than leaving a 0%-duty PWM signal running).
Uno / NanoPWM pins: 3, 5, 6, 9, 10, 11
Mega 2560PWM pins: 2–13, 44–46
Led_PWM.ino
PWM led(9);
led.write(128);   // ~50% brightness
led.write(0);     // off, pin forced LOW

Pulse

src/ArduLitePulse.h

Measures pulse width in microseconds via Timer1, equivalent to Arduino's pulseIn() but implemented directly on the register.

Uses Timer1
Constructor / MethodDescription
Pulse(pin, mode=IN, timeoutUs=30000)Binds to a pin (D0-D19 range) with a busy-wait timeout in microseconds.
read()Waits for a full HIGH pulse and returns its duration in microseconds. Returns 0 on timeout.
readLow()Same, but measures a LOW pulse instead.
setTimeout(uint32_t timeoutUs)Changes the timeout without recreating the object.
Simple_Pulse_Read.ino (pattern)
Pulse input(7);

forever() {
    unsigned long width = input.read();  // microseconds, 0 = timeout
}
Shares Timer1 with PWM (pins 9/10) and Servo — don't combine them in the same sketch.

06 · Timing Reference

Time, Timer & Stopwatch

src/ArduLiteTime.h

ArduLite never defines millis(), micros() or delay() as free functions — that vocabulary lives inside objects (time.millis()) so it can never collide with the Arduino Core. Timer2 is the single 1 ms tick source shared by everything in this file, plus Button and Interrupt debounce.

Time — reads the running clock

Every method is static and the class holds no data — time costs no RAM and adds no call overhead beyond the read itself.

MethodDescription
time.millis()Milliseconds since the timer started. Wraps to 0 after ~49 days.
time.micros()Microseconds since start, 4 µs resolution @16 MHz. Wraps after ~71 minutes.
time.seconds()Whole seconds since start.
time.now()Alias for millis().
Time::wait(uint32_t ms)Blocking wait based on the Timer2 clock rather than a busy-loop — stays accurate even when ms comes from a variable.
Time::waitMicros(uint16_t us)Blocking microsecond wait, same underlying primitive as the core header's waitMicros().

Timer — non-blocking periodic task

9 bytes of RAM per instance. No interrupt, no callback — just a polled check you call from inside forever().

Constructor / MethodDescription
Timer(uint32_t intervalMs)Starts counting immediately from construction.
ready()Returns true exactly once each time the interval elapses. Advances by one interval rather than snapping to "now" — the rhythm doesn't drift, but resynchronizes automatically if it falls far behind.
reset()Restarts counting from the current time.
setInterval(ms)Changes the interval and restarts counting.
start() / stop()Pauses/resumes. ready() always returns false while stopped.
isRunning() constCurrent run state.
remaining() constMilliseconds left before the next ready() fires true.
Periodic_Task.ino
#include <ArduLite.h>
#include <ArduLiteTime.h>
#include <ArduLiteUART.h>

int main() {
    Digital led(13, OUT);
    Timer blink(250);    // fast task
    Timer report(1000);  // slow task, runs independently
    uart.begin(9600);

    uint16_t blinkCount = 0;

    forever() {
        if (blink.ready())  { led.toggle(); blinkCount++; }
        if (report.ready()) {
            uart.send("Uptime "); uart.send(time.seconds());
            uart.send(" seconds, blinks: "); uart.sendLine(blinkCount);
        }
    }
}

Stopwatch — measures a duration

MethodDescription
start()Starts, or resumes after stop().
stop()Pauses. The accumulated duration is kept.
reset()Back to zero, stopped.
restart()reset() then start() in one call.
elapsed() constTotal duration in milliseconds.
elapsedSeconds() constTotal duration in whole seconds.
isRunning() constCurrent run state.
Stopwatch.ino (pattern)
Stopwatch watch;
watch.start();
// ... work ...
watch.stop();
uart.sendLine(watch.elapsed());   // duration in ms

07 · Components Reference

LED

src/ArduLiteLED.h

A semantic wrapper around Digital for LEDs, with common-anode (active-low) support.

Constructor / MethodDescription
LED(pin, activeLow=false)Set activeLow=true for common-anode LEDs that light up when the pin is driven LOW.
on() / off() / toggle()Standard state control, already accounting for activeLow.
write(bool on)Explicit on/off.
isOn() constCurrent logical state (not raw pin level).
blink(uint16_t delayMs, uint16_t count=1)Blocking blink, count times, delayMs on and off each cycle.
Component_LED.ino
LED led(13);
led.blink(500, 5);   // blink 5 times, 500 ms on/off

Button

src/ArduLiteButton.h

Time-based debounce with edge detection, for a button that connects the pin to GND (active-low) with the internal pull-up enabled automatically.

Constructor / MethodDescription
Button(pin, debounceMs=50)Enables the pull-up automatically at construction.
isPressed()True while the button is held, after debounce settles.
pressed()True only on the rising edge — the single instant it's first pressed.
released()True only on the falling edge — the instant it's first released.
reset()Clears stored state back to "not pressed".
setDebounce(uint16_t ms)Changes the debounce window.
Component_Button.ino
Button btn(2);

forever() {
    if (btn.pressed())   { /* fires exactly once per press */ }
    if (btn.isPressed()) { /* true the whole time it's held  */ }
}
Call pressed()/isPressed()/released() regularly from inside forever() — debounce and edge detection depend on it ticking.

Relay

src/ArduLiteRelay.h

Controls a relay module, defaulting to active-low (the common wiring on cheap relay boards, where a LOW signal energizes the coil).

Constructor / MethodDescription
Relay(pin, activeLow=true)Set activeLow=false for boards where HIGH energizes the relay.
on() / off() / toggle()State control, accounting for activeLow.
write(bool on)Explicit on/off.
isOn() constCurrent logical state.
Component_Relay.ino
Relay lamp(7);   // active-low module, the common case
lamp.on();
wait(1000);
lamp.off();

Buzzer

src/ArduLiteBuzzer.h

Drives an active buzzer — one that sounds continuously when given HIGH, as opposed to a passive buzzer that needs a driven frequency.

Constructor / MethodDescription
Buzzer(pin)Starts silent.
on() / off() / toggle()Direct state control.
beep(uint16_t ms)Blocking beep for ms milliseconds, then turns off.
Component_Buzzer.ino
Buzzer buz(8);
buz.beep(200);   // sounds for 200 ms

Servo

src/ArduLiteServo.h

Generates a standard 50 Hz hobby-servo signal (1.0–2.0 ms pulse) via Timer1's Fast PWM mode 14, with zero extra interrupts and zero heap.

Uses Timer1
Uno / NanoPin 9 (OC1A) or 10 (OC1B)
Mega 2560Pin 11 (OC1A) or 12 (OC1B)
MethodDescription
Servo()Constructs unattached — no timer activity yet.
attach(pin)Binds to a supported OC1 pin and starts the 50 Hz frame. Silently does nothing on an unsupported pin. Moves to the middle position automatically.
detach()Releases the OC1 output, freeing Timer1 for other modules.
isAttached() constCurrent attach state.
write(uint8_t angle)Position, 0-180 degrees.
writeMicroseconds(uint16_t us)Direct pulse-width control, clamped to [usMin, usMax].
setPulseRange(minUs, maxUs)Recalibrates the 0°/180° endpoints (default 1000/2000 µs) for servos that don't follow the standard range.
Component_Servo.ino
Servo servo;
servo.attach(9);

forever() {
    for (uint8_t ang = 0; ang <= 180; ang++) { servo.write(ang); wait(15); }
    for (uint8_t ang = 180; ang > 0; ang--) { servo.write(ang); wait(15); }
}
Shares Timer1 with PWM (pins 9/10) and Pulse — don't run all three together.

Ultrasonic

src/ArduLiteUltrasonic.h

Driver for the HC-SR04 distance sensor: emits a 10 µs trigger pulse, then measures how long the echo line stays HIGH. Distance follows cm = time(µs) / 58.

TrigDigital output pin, e.g. 7
EchoDigital input pin, e.g. 6
Constructor / MethodDescription
Ultrasonic(trigPin, echoPin, timeout=30000)timeout is the maximum wait for the echo, in microseconds.
distanceCm()Blocking measurement. Returns 0 when there is no reflection within the timeout.
distanceInch()distanceCm() / 2.54.
Component_Ultrasonic.ino
Ultrasonic ultra(7, 6);   // trig pin 7, echo pin 6

forever() {
    float cm = ultra.distanceCm();
}

08 · Communication Reference

UART

src/ArduLiteUART.h

Interrupt-driven serial with type-safe overloads for every AVR integer and float type, plus non-blocking line receive via callback. Global instances are ready to use out of the box.

Global: uartMega 2560: + uart1, uart2, uart3
MethodDescription
begin(baud, callback=nullptr)Starts the UART at baud. If callback is given, it fires from the RX interrupt every time a full line (terminated by \r or \n) arrives.
send(...)Overloaded for char, const char*, every signed/unsigned integer width, float, and bool (sent as "true"/"false" from PROGMEM).
send(uint32_t data, uint8_t base)Sends a number in an arbitrary base, e.g. send(addr, 16) for hex.
sendLine(...)Same overload set as send(), followed by \r\n.

Blocking-style scan output

I2C_Scanner.ino (excerpt)
uart.begin(9600);
uart.sendLine("I2C Scanner ArduLite");

if (i2c.probe(addr)) {
    uart.send("Device found at 0x");
    uart.send(addr, 16);   // address in hex
    uart.sendLine("");
}

Interrupt-driven command receiver

UART_Interrupt.ino
#include <ArduLite.h>
#include <ArduLiteLED.h>
#include <ArduLiteUART.h>

LED led(13);

bool strEqual(const char *a, const char *b) {
    while (*a && *a == *b) { a++; b++; }
    return *a == *b;
}

void onData(const char *line) {   // called from the RX ISR on every full line
    if (strEqual(line, "on"))     led.on();
    else if (strEqual(line, "off"))  led.off();
    else if (strEqual(line, "toggle")) led.toggle();
}

int main() {
    uart.begin(9600, onData);
    forever() { }   // main loop can stay empty — RX is fully interrupt-driven
}
The receive callback runs inside an ISR: keep it short, mark shared variables volatile, and never call wait() or send long messages from inside it. The internal line buffer is 16 bytes.

I2C

src/ArduLiteI2C.h

Direct TWI (Two-Wire Interface) hardware driver — no software bit-banging. A ready-to-use global instance i2c is provided.

Uno / NanoSDA → A4, SCL → A5
Mega 2560SDA → 20, SCL → 21
MethodDescription
begin()Configures TWI hardware at roughly 100 kHz (16 MHz clock, TWBR=72).
probe(addr)Sends SLA+W and checks for an ACK. Returns true if a device answers at that address — the basis of an I2C bus scanner.
read(addr, reg, data*, len)Reads len bytes starting at register reg on device addr.
write(addr, reg, data*, len)Writes len bytes to register reg on device addr.

Two free helper functions are also declared alongside i2c: bcdToDec(uint8_t) and decToBcd(uint8_t), handy when talking to RTC modules (e.g. DS1307/DS3231) that store time fields in BCD.

I2C_Scanner.ino
i2c.begin();

for (uint8_t addr = 1; addr < 128; addr++) {
    if (i2c.probe(addr)) {
        uart.send("Device found at 0x");
        uart.send(addr, 16);
        uart.sendLine("");
    }
}

SPI

src/ArduLiteSPI.h

Master-mode driver on the AVR's hardware SPI registers (SPCR/SPSR/SPDR). MOSI, SCK and MISO are handled automatically; the chip-select pin is deliberately not managed by this module, so you choose it freely through Digital.

Uno / NanoMOSI 11, MISO 12, SCK 13
Mega 2560MOSI 51, MISO 50, SCK 52
MethodDescription
begin()Enables SPI hardware as master, Mode 0, MSB-first, clock F_CPU/16. Safe to call repeatedly.
end()Disables the SPI hardware.
transfer(uint8_t data)Full-duplex: sends one byte, returns the byte received at the same time.
transfer16(uint16_t data)Sends/receives two bytes, high byte first.
setBitOrder(SPI_MSBFIRST / SPI_LSBFIRST)Bit transmission order.
setDataMode(SPI_MODE0..3)Selects clock polarity/phase.
setClockDivider(2..128)Clock speed = F_CPU / divider.
SPI_Demo.ino (loopback test)
Digital cs(10, OUT);   // our chosen SS/CS pin, held HIGH
cs.on();
spi.begin();

cs.off();                                 // select slave
uint8_t reply = spi.transfer(0x42);    // send and receive at once
cs.on();                                  // release slave
Master mode is only preserved if the SS pin is held as an output HIGH — begin() does this for you internally, in addition to whatever CS pin you drive yourself.

09 · Interrupts Reference

Interrupt (external)

src/ArduLiteInterrupt.h

Wraps AVR's two true external-interrupt lines (INT0/INT1), with optional built-in debounce timed off the Timer2 tick.

Uno / NanoINT0 = pin 2, INT1 = pin 3
Mega 2560INT0 = pin 21, INT1 = pin 20
ConstructorDescription
Interrupt(pin, callback, mode)No debounce. pin must be an INT0/INT1-capable pin.
Interrupt(pin, callback, mode, debounceDelayMs)Same, with a debounce window applied in the ISR itself.

mode maps to the AVR ISCn bits: 0 = LOW level, 1 = any logical CHANGE, 2 = FALLING edge, 3 = RISING edge.

Button_Interrupt.ino
volatile uint16_t pressCount = 0;
void onPress() { pressCount++; }

int main() {
    uart.begin(9600);
    Interrupt button(2, onPress, 3, 50);  // pin 2, RISING, 50 ms debounce

    forever() {
        // pressCount keeps incrementing in the background even while busy
    }
}

PinChange

src/ArduLitePinChange.h

Pin Change Interrupts fire on almost any digital pin — unlike the two dedicated external-interrupt lines above. Ideal for encoders, flow sensors, and revolution counters.

Max 4 handlers
Constructor / EnumDescription
PinChange(pin, callback, mode=CHANGE)Registers one pin and enables the matching PCICR/PCMSK bits automatically.
PinChangeMode::CHANGEFires on every level change, both directions.
PinChangeMode::RISINGFires only on LOW→HIGH.
PinChangeMode::FALLINGFires only on HIGH→LOW.
Uno / NanoAll of D0-D13 and A0-A5
Mega 2560D6-D9, D10-D13, D14-D15, D50-D53, A8-A15
Pulse_Counter.ino
volatile uint32_t pulses = 0;
void countPulse() { pulses++; }

int main() {
    PinChange counter(6, countPulse, PinChangeMode::RISING);
    forever() { /* pulses keeps growing via ISR */ }
}

TimerInterrupt

src/ArduLiteTimerInterrupt.h

A scheduler for periodic callbacks that keep their rhythm even while the main loop is busy — the interrupt-driven counterpart to the polling Timer class. Runs on Timer2's COMPB unit, a separate compare channel from the 1 ms tick in ArduLiteTime.h, so the two never fight over one vector.

Uses Timer2 COMPB11 bytes RAM / slot
FunctionDescription
createTimer(intervalMs, callback)Registers a periodic task, auto-calling initTimerInterrupt() on first use. Returns a task ID, or TIMER_INVALID if all slots are full.
setTimerEnabled(id, bool)Enables/disables a task without removing it.
setTimerInterval(id, newInterval)Changes a task's period and restarts its count.

Slot count defaults to MAX_TIMERS = 4. Define #define MAX_TIMERS 8 before including the header to trade RAM for more slots.

Periodic_Task_Interrupt.ino (pattern)
Digital led(13, OUT);
void blink() { led.toggle(); }

int main() {
    createTimer(500, blink);   // calls blink() every 500 ms, automatically
    forever() { }                // free to do other work — blink keeps its rhythm
}
The callback runs inside an ISR: keep it minimal, mark shared state volatile, and never call wait() or send long UART messages from it.

10 · Storage & Utilities

EEPROM

src/ArduLiteEEPROM.h

A concise wrapper around avr/eeprom.h for data that survives power loss. A ready-to-use global instance eeprom is provided.

ATmega328P1024 bytes (Uno / Nano)
ATmega25604096 bytes (Mega)
MethodDescription
length() constEEPROM size in bytes for the current chip.
readByte / writeByte(addr, value)1-byte read/write. writeByte always writes, even if the value is unchanged.
updateByte(addr, value)Writes only when the stored value differs — the endurance-friendly choice for values that rarely change.
read/write/updateUInt16(addr, value)2-byte access, little-endian.
read/write/updateUInt32(addr, value)4-byte access, little-endian.
Persistent_Counter.ino
const uint16_t ADDR_COUNT = 0;

int main() {
    uart.begin(9600);
    uint32_t boots = eeprom.readUInt32(ADDR_COUNT) + 1;
    eeprom.updateUInt32(ADDR_COUNT, boots);   // endurance-friendly write

    uart.send("Powered on "); uart.send(boots); uart.sendLine(" times.");
    forever() { }
}

Utils

src/ArduLiteUtils.h / .cpp

Bit manipulation, math, pseudo-random numbers, and low-level board-aware pin register helpers — all zero-cost, mostly constexpr.

Bit utilities

FunctionDescription
bitReadFast(value, bit)constexpr. Returns 1 or 0.
bitSetFast / bitClearFast / bitToggleFast(value&, bit)In-place bit set/clear/flip.
bitReverse(uint8_t v)Reverses the order of all 8 bits.
bitCount(uint8_t v)Population count — number of bits set to 1.

Math helpers (templated, constexpr)

FunctionDescription
constrainFast(value, low, high)Clamps into [low, high].
scaleMap(value, inMin, inMax, outMin, outMax)Re-scales a value between two ranges — ArduLite's equivalent of Arduino's map().
minOf(a, b) / maxOf(a, b)Generic min/max.

Pseudo-random

FunctionDescription
randomSeed(uint32_t seed)Seeds the internal LCG state (falls back to 1 if seed==0).
randomInt(uint16_t max)Pseudo-random integer in [0, max).
randomRange(min, max)Pseudo-random integer in [min, max] inclusive.

Low-level pin helpers

Free functions used internally by Digital, exposed for anyone who wants direct register access without instantiating a class.

FunctionDescription
pinOutput(pin) / pinInput(pin)Sets direction directly.
pinPullup(pin, enable)Enables/disables the internal pull-up.
pinWrite(pin, value) / pinRead(pin)Direct level write/read.
pinToggle(pin)Hardware toggle via the PINx trick.
arduino_lite::pinRegs(pin)Returns the raw PinRegs{ ddr, port, pin, mask } struct for a pin — the same lookup Digital uses internally.
Utils in practice
int raw = sensor.read();                         // 0-1023
long angle = scaleMap(raw, 0, 1023, 0, 180);   // re-scale like Arduino's map()
angle = constrainFast(angle, 0L, 180L);

randomSeed(time.micros());
uint16_t dice = randomRange(1, 6);

11 · Appendix

Supported boards & pin map

Version 1.x/2.x scope is intentionally three boards. Everything in this document — Digital, PWM, Servo, PinChange, SPI, I2C — auto-selects the correct register set for whichever of these is targeted at compile time.

BoardMCUPWM pinsHardware serialI2C (SDA/SCL)SPI (MOSI/MISO/SCK)
UnoATmega328P3, 5, 6, 9, 10, 111 (uart)A4 / A511 / 12 / 13
NanoATmega328P3, 5, 6, 9, 10, 111 (uart)A4 / A511 / 12 / 13
Mega 2560ATmega25602–13, 44–464 (uart, uart1-3)20 / 2151 / 50 / 52
Older ATmega8/16/32 targets are also handled inside ArduLiteDigital.h and ArduLiteUtils.h at the register-mapping level (they lack the PINx hardware-toggle feature, so toggle() falls back to read-modify-write) — but they are not part of the officially supported board list in the PRD.

ArduLite vs. the Arduino Core

TaskArduino CoreArduLite
Configure & write a pinpinMode(13, OUTPUT); digitalWrite(13, HIGH);Digital led(13, OUT); led.on();
Read a button (active-low)digitalRead(2) == LOWbutton.pressed()
Blocking delaydelay(500);wait(500);
Running clockmillis()time.millis()
Non-blocking periodic taskif (millis() - last >= interval) {...}if (myTimer.ready()) {...}
Re-scale a rangemap(v, 0, 1023, 0, 180)scaleMap(v, 0, 1023, 0, 180)
PWM outputanalogWrite(9, 128);PWM led(9); led.write(128);
Dynamic stringsString s = "x" + String(v); // heapNot supported by design — use uart.send() overloads
ArduLite is not a drop-in replacement for the Arduino Core — it's a parallel, non-conflicting vocabulary. Because it never defines Arduino's own symbols, the two can be mixed in one sketch if a project genuinely needs a library that expects Arduino.h.

Examples index

All 21 example sketches ship in examples/ and are importable from File → Examples → ArduLite.

GPIO

Led_Blink

Minimal Digital output blink.

GPIO

Led_Toggle_With_Push_Button

Digital input with pull-up + toggle.

GPIO

Led_PWM

PWM fade up and down.

GPIO

Analog_Read_Control_Led_Brightness

Analog + PWM brightness control.

GPIO

Analog_Coding_Test_Speed

ADC prescaler speed comparison.

GPIO

Simple_Pulse_Read

Basic Pulse width capture.

Timing

Simple_Timer_or_Millis

Timer class vs. manual millis math.

Timing

Periodic_Task

Two independent Timer-driven tasks.

Timing

Periodic_Task_Interrupt

createTimer() ISR-driven scheduling.

Timing

Stopwatch

Start/stop/elapsed duration measurement.

Components

Component_LED

LED wrapper with blink().

Components

Component_Button

Debounced press/release edges.

Components

Component_Relay

Active-low relay control.

Components

Component_Buzzer

Active buzzer beep().

Components

Component_Servo

0–180° sweep over Timer1.

Components

Component_Ultrasonic

HC-SR04 distance in cm.

Comms

UART_Interrupt

Serial command line receiver.

Comms

I2C_Scanner

Bus scan with probe().

Comms

SPI_Demo

MOSI/MISO loopback self-test.

Interrupts

Button_Interrupt

Debounced INT0 button.

Interrupts

Pulse_Counter

PinChange rising-edge counter.

Storage

Persistent_Counter

Boot counter surviving power loss.

Storage

Save_Configuration

Storing settings in EEPROM.

Design rules (project conventions)

ArduLite's internal docs/rules.md spells out the constraints every module in this reference follows — useful context if you're extending the library yourself.

RuleDetail
C++ standardTarget C++17. Required: constexpr, inline, enum class, explicit constructors, const-correctness.
ForbiddenExceptions, RTTI, virtual inheritance, std::thread, std::filesystem, and any heap allocation (new/delete/malloc/calloc/realloc/free).
Avoidstd::string, std::vector — anything that hides a dynamic allocation.
API readabilityDigital led(13); led.on(); is the goal, not digitalWrite(13, HIGH);. Every API must read more clearly than the Arduino API it replaces.
Opt-in modulesNo optional module is auto-included in the master header. No peripheral is initialized unless the sketch actually uses it.
NamingClasses: PascalCase. Variables and methods: camelCase.

License & authorship

ArduLite is released under the MIT License, © 2025 Ajang Rahmat.

Created by Ajang Rahmat — Robot Class, PT AViSha Inovasi Indonesia.
Source: github.com/ArduLite/ArduLite

Contributions are welcome — fork the repository and submit a pull request. New modules should follow the conventions in the Design rules section above: no heap, opt-in headers, and an API that reads more clearly than the Arduino equivalent it replaces.

12 · For Beginners

Frequently asked questions

New to Arduino or embedded C++? Start here.

What is ArduLite, in plain terms?

It's a free library that lets you control an Arduino board with short, readable code like led.on() instead of the longer digitalWrite(13, HIGH) style — while producing code that is just as fast (often faster) than writing the registers by hand yourself.

Do I need to know what a "register" is to use it?

No. You can build complete projects — blink an LED, read a button, sweep a servo — using only the object methods shown in this reference, exactly like the Quick Start examples above. The register-level explanations in Zero-Overhead Model are optional background reading for when you're curious how it works under the hood.

Which Arduino board should I use with ArduLite?

Any Arduino Uno or Nano is the easiest starting point — they're the cheapest, most common boards and every example in this documentation works on them unmodified. Use a Mega 2560 only if your project needs more pins or more than one hardware serial port.

How is this different from just using the normal Arduino language?

You don't have to choose — see the ArduLite vs. Arduino Core table. ArduLite never redefines Arduino's own functions, so you can use both in the same sketch if a library you need still expects the standard Arduino API.

I'm stuck — where do I start?

Read Installation, copy the first example from Quick Start exactly as written, upload it, and confirm the LED blinks before changing anything. Then open Examples Index and pick the next one closest to what you want to build.