A modular, fully testable, asynchronous core for 8-bit AVR — built to use all of the silicon you already own.
There are tens of millions of perfectly capable ATmega328 boards in the world, sold as "blink an LED" toys, with most of their real capability left dormant behind a beginner-friendly abstraction. AvrAsyncCore exists to hand that capability back.
Somewhere in a drawer you probably have an Arduino UNO or Nano from years ago. It still works. It is still, by any reasonable measure, a capable little computer: a 16–20 MHz core, hardware timers able to synthesize multi-MHz PWM for power electronics, an ADC, I2C, and more — all on inexpensive, robust silicon with an energy envelope that is genuinely hard to beat.
The point of this project is not to chase newer, shinier parts with smaller process nodes. There will always be something faster. The point is to do more with what is already on your desk: to push the hardware to its limits, to leverage every bit of the die, and to find the beauty in doing a lot with a few kilobytes.
It is also a deliberate reaction against bloat. Modern vendor stacks and code generators produce mountains of indirection in the name of "drivers." Constraint breeds craft: when you only have ~2 KB of RAM, every architectural decision has to mean something. This codebase tries to honour that.
If that resonates with you — minimalism, craft, sustainability through reuse, and squeezing real performance out of humble hardware — you're in the right place.
AvrAsyncCore is a layered, non-blocking Hardware Abstraction Layer and a cooperative "async core" for AVR microcontrollers (primary target: the ATmega328P, i.e. the Arduino UNO / Nano, at 16 MHz). Instead of an RTOS (often too heavy and too much overhead for tiny parts) or the Arduino stack (easy, but leaving most performance and determinism on the table), it occupies the middle ground:
- Bare-metal performance and determinism, with
- Structured, asynchronous, cooperative drivers, that are
- Modular, multi-instance, and — unusually for embedded C — genuinely unit-testable.
You trade some of Arduino's hand-holding for performance, modularity, and control.
Drivers never busy-wait. Each exposes a state machine driven either by a
*_process() call in your main loop or by the relevant hardware interrupt.
The CPU is free to do useful work instead of spinning. A typical application loop
is a clean cooperative scheduler:
while (true)
{
i2c_process(0U);
adc_read_values();
hd44780_lcd_process();
/* ... */
}This is the project's standout feature. The drivers are compiled and unit-tested on the host (PC) with GoogleTest — no microcontroller required. That includes a full I2C bus simulator with virtual devices and a simulated TWI peripheral, exercising master/slave, transmit/receive, and mode-transition scenarios that are nearly impossible to cover reliably on real silicon.
Most embedded HALs are untestable by construction. This one was designed the other way around.
Drivers don't hard-code register addresses; they receive them through a handle:
config.handle.TWCR = &TWCR;
config.handle.TWBR = &TWBR;
/* ... */This is what enables both multiple peripheral instances (e.g. several TWI units on an ATmega328PB driven by a single driver) and host-side testing (inject pointers to fake registers).
Every public function returns an explicit error enum, output parameters are
* const, and each driver ships a get_default_config() plus a unitary
get/set API for fine-grained control. The whole codebase reads as one library,
not a pile of experiments.
Each driver and module is an independent CMake target with its own tests.
Cross-compilation for AVR is handled by a dedicated toolchain file
(Toolchain/avr8-gcc-toolchain.cmake); the test suite builds natively for your
host.
The codebase is organised in clear layers:
Drivers/ Hardware-facing peripheral drivers (touch registers directly)
Modules/ Higher-level features built on top of drivers (no direct HW access)
Sensors/ Sensor logic built on drivers/modules
Utils/ Small shared helpers (e.g. memutils)
App/ Reference application wiring it all together
Tests/ Host-side GoogleTest build aggregating every component's tests
Toolchain/ AVR8 GCC CMake toolchain file
The distinction is principled: drivers speak to hardware; modules compose drivers into higher-level capabilities (e.g. a software timebase built on a hardware timer).
| Component | Type | Status |
|---|---|---|
| ADC | Driver | Implemented + tested |
| I2C / TWI (master & slave, async) | Driver | Implemented + tested (with bus simulator) |
| IO (GPIO) | Driver | Implemented + tested |
| HD44780 LCD | Driver | Implemented + tested |
| Timer 8-bit / 16-bit / 8-bit async / generic | Driver | Implemented + tested |
| Timebase | Module | Implemented + tested |
| Thermistor | Sensor | Implemented + tested |
| PWM (software + hardware) | Module | In progress — see the pwm_driver branch / PR #6 |
AvrAsyncCore grew out of the LabBenchPowerSupply project — a bench power
supply built around an Arduino Nano, converting mains-derived voltage down to a
regulated 0–16 V with voltage and current control. It is the intended embodiment
of this project's philosophy: a real, useful piece of power electronics driven
by a humble 8-bit part pushed to its potential. (The firmware CMake project is
still named LabBenchPowerSupply_Firmware for this reason.)
- An AVR toolchain on
PATH:avr-gcc,avr-g++,avr-objcopy,avr-size,avr-objdump(plusavr-libc). On Windows, an Atmel Studio 7 installation is auto-detected from the registry. - CMake ≥ 3.20
- For the tests: a host C/C++ compiler and GoogleTest (
find_package(GTest)).
The root build is configured for the ATmega328P at 16 MHz and selects the AVR toolchain automatically.
cmake -B build -DCMAKE_BUILD_TYPE=MinSizeRel
cmake --build buildOn Linux you may need to point CMake at your AVR sysroot, e.g.:
export AVR_FIND_ROOT_PATH=/usr/avr # or /usr/lib/avr, /usr/local/avr, ...The test suite is a separate CMake project under Tests/. It builds the drivers
for the host with UNIT_TESTING defined and links against GoogleTest.
cd Tests
# On Linux, AVR register headers are taken from /usr/avr/include by default.
# On Windows, pass the path explicitly, e.g. -DAVR_INCLUDES=C:/path/to/avr/include
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
./run_all_tests.shrun_all_tests.sh walks build/bin/, runs every test executable it finds, and
prints a summary of any failures. No microcontroller required to run the tests —
that's the point.
- Finish the PWM module (software-generated PWM with user callbacks, and
hardware PWM exploiting the timers' full waveform-generation capability,
including complementary outputs with dead-time generation). Work in progress on
the
pwm_driverbranch. - Formalise the cooperative "core" into a proper, reusable scheduler so the name fully earns itself.
- Continuous Integration to build and run the full test suite on every push.
- Explore portability of the architecture beyond AVR (the testable, async, dependency-injected design is largely silicon-agnostic).
This is a long-running, lovingly built personal project, intentionally incomplete in places rather than rushed. The foundations (drivers, modules, testing infrastructure) are solid and tested; some higher-level features are still in progress. Contributions, questions, and curiosity are welcome.
Released under the GNU General Public License v3.0. See LICENSE.