README.md

Mcucpp — Bare-Metal C++20 Framework for RISC-V MCUs

Modern C++20 framework and project template for constrained RISC-V microcontrollers. Primary target: K1921VG015 (NIIET v.2 board, RV32IMFC, 50 MHz, 1 MiB flash / 256 KiB RAM).

Status: experimental, under active development.

Features:

  • Async/await coroutine framework for bare-metal I/O (timers, hardware flags, tasks)
  • Zero dynamic allocation — stack-first, pre-allocated containers
  • Hardware drivers: UART, ADC, I2C, DMA, timers, internal flash, GPIO, PLIC interrupts
  • Device drivers: MPU-6050 (IMU), SSD1306 (OLED)
  • Protocols: Modbus RTU slave, 1-Wire (DS18B20)
  • printf-like formatting, CRC (16/32), binary streams
  • No exceptions, no RTTI

Table of Contents


Prerequisites

Dependency Version Notes
Python 3.8+ SCons runtime
SCons 4.0+ Build system
PyYAML any Target config parsing
pyserial any UART test output reading
riscv-none-elf-gcc latest RISC-V cross-compiler toolchain
OpenOCD k1921vk branch Flash programming and debugging
VSCode optional Recommended IDE

Platform-Specific Notes

Windows:

  • Install Python from python.org, ensure python and pip are on PATH
  • Download RISC-V toolchain from riscv-collab/riscv-gnu-toolchain — add bin/ to PATH
  • Download OpenOCD for K1921 from DCVostok/openocd-k1921vk — add bin/ to PATH
  • Install Python packages: pip install scons pyyaml pyserial

Linux:

  • sudo apt install python3 python3-pip scons python3-yaml python3-serial
  • Download and extract RISC-V toolchain, add to PATH or use full paths
  • Download OpenOCD for K1921, add to PATH

Installation

# 1. Clone the repository
git clone <repo-url>
cd niiet_test

# 2. Install Python dependencies
pip install scons pyyaml pyserial

# 3. Verify toolchain is on PATH
riscv-none-elf-gcc --version
openocd --version

# 4. Connect hardware (one of):
#    - NIIET dev board v.2 with built-in FTDI debugger
#    - BlueBird VG015 board + BlueProg debugger (FTDI, VID:PID 0x16c0:05dc)
#    UART console on the debugger's USB-serial port

Verifying the Setup

# Build and run a simple test (requires hardware connected)
scons blink UART_PORT=COM30          # Windows
scons blink UART_PORT=/dev/ttyUSB0   # Linux

Quick Start

scons                  # Build and flash all tests (flash ops serialized via file lock)
scons -j 4             # Parallel build
scons blink            # Build, flash, and run a single test
scons ftoa             # Float-to-string conversion tests
scons uart             # UART driver tests (Poll / Dma / Irq / Async)
scons async_io         # Coroutine / async I/O tests
scons CONFIGURATION=debug   # Debug build (-Og, MY_DEBUG=1)
scons UART_PORT=COM30  # Override UART console port
scons -c               # Clean

Each test is compiled, flashed to the board via OpenOCD, and its UART output is read back to report pass/fail/timeout.


Project Structure

niiet_test/
├── inc/                    # Framework headers (namespace Mcucpp)
│   ├── async_io.h          #   io_task, co_await adapters
│   ├── queue_dispatcher.h  #   Coroutine queue with priority
│   ├── timer_dispatcher.h  #   Heap-based timer scheduler
│   ├── flag_dispatcher.h   #   Hardware flag/event dispatcher
│   ├── coro_concurrency.h  #   coro_mutex, manual_event, CO_LOCK
│   ├── vector_adapter.h    #   Stack-backed vector
│   ├── noalloc_function.h  #   Stack-based std::function replacement
│   ├── binary_stream.h     #   Byte-level I/O with endianness
│   ├── crc.h               #   Templated CRC (16/32)
│   ├── format/             #   printf-like formatting engine
│   ├── protocols/          #   Modbus RTU slave
│   ├── devices/            #   I2C device drivers (MPU-6050, SSD1306)
│   └── K1921VG015/         #   Target-specific headers (uart, adc, i2c, dma, tmr, ...)
├── src/
│   ├── main.cpp            # Application entry point
│   └── platform/
│       ├── K1921VG015/     #   Target implementations (startup, uart, adc, i2c, dma, tmr, ...)
│       └── ...             #   Shared framework implementations
├── tests/                  # Hardware tests (flash + UART pass/fail)
├── scons/tools/            # SCons build tools (target YAML, PlatformTest, flash_and_run)
├── targets/                # Target configuration (YAML)
├── ldscripts/              # Linker scripts
└── openocd/                # OpenOCD interface configs

Tests

Tests run on real hardware: the build system compiles, flashes via OpenOCD, and parses the UART console output for All tests passed! / FAILED markers.

Alias Description
blink GPIO blink
adc ADC driver (Poll / Dma / Irq / Async)
uart UART driver (Poll / Dma / Irq / Async)
i2c I2C driver (Poll / Irq)
timer Timers: period / PWM / capture
flash Internal flash (erase + write)
mti_irq Machine timer interrupt
async_io Async I/O coroutines, dispatchers, concurrency primitives
exception_handler Trap / exception handler
i2c_mpu6050 MPU-6050 IMU sensor over I2C
i2c_ssd1306 SSD1306 OLED over I2C
ssd1306_checkerboard SSD1306 framebuffer visual test
modbus Modbus RTU slave
ftoa Float-to-string conversion
format printf-like format engine
crc CRC16 / CRC32 computation
noalloc_function Stack-based function wrapper
dyna_pin_list Dynamic pin list management
allocators Bitmap / fixed-block / dispatch allocators + benchmarks

Writing Your First Program

Step 1: Create the Test Source File

Create tests/myapp/test.cpp:

#include <clock.h>
#include <iopins.h>
#include <ports.h>
#include <delay.h>
#include <test_harness.h>

using namespace Mcucpp;
using namespace std::chrono_literals;

void run_tests()
{
    test_cout << "My first program" << endl;

    // Blink an LED on PC0
    Pc0::Port().Enable();
    Pc0::SetConfiguration(PortCfg::Out);

    for (int i = 0; i < 5; i++)
    {
        Pc0::Set();
        delay(100ms);
        Pc0::Clear();
        delay(100ms);
    }

    test_cout << "All tests passed!" << endl;
}

Step 2: Create the SConscript

Create tests/myapp/SConscript:

import os
from SCons.Script import ARGUMENTS, Environment

target = ARGUMENTS.get("target", "k1921vg015")

env = Environment(
    toolpath=["../../scons/tools"],
    tools=["mcucpp_tools"],
    ENV=os.environ,
    MCU_TARGET=target,
)

env.Append(CPPPATH=["#tests/common"])

env.PlatformTest(
    test_name="myapp",
    sources=[
        "#tests/common/test_harness.cpp",
        "#tests/myapp/test.cpp",
    ],
    timeout=15,
)

Step 3: Register in SConstruct

SConscript("tests/myapp/SConscript")

And add "myapp" to the Alias("all_tests", ...) list.

Step 4: Build and Run

scons myapp

Using Coroutines (Async/Await)

#include <async_io.h>
#include <queue_dispatcher.h>
#include <timer_dispatcher.h>
#include <flag_dispatcher.h>
#include <test_harness.h>

using namespace Mcucpp;
using namespace std::chrono_literals;

io_task<void> BlinkLed()
{
    while (true)
    {
        Pc0::Toggle();
        co_await 500ms;  // Async wait — yields control to the dispatcher
    }
}

void run_tests()
{
    auto blink = BlinkLed();

    // Main loop — poll the dispatchers
    for (int i = 0; i < 20; i++)
    {
        ATOMIC
        {
            GetTimerDispatcher().Poll();
            GetFlagDispatcher().Poll();
            GetQueueDispatcher().Poll();
        }
    }

    test_cout << "All tests passed!" << endl;
}

Adding a New Test

  1. Create directory tests/<name>/
  2. Write test.cpp with void run_tests() entry point
  3. Write SConscript (see template above)
  4. Register in root SConstruct
  5. Add to alias in Alias("all_tests", ...)

Test output convention:

  • Output All tests passed! on success
  • The CHECK, CHECK_EQUAL, CHECK_RANGE macros (from test_harness.h) print FAILED: ... on a failed assertion
  • The test harness reads UART and reports pass/fail/timeout

Target-specific tests (e.g., tests/k1921vg015/<name>/) guard execution with if target == _target: and pass a short test_alias_name for the scons target.


Adding a New Target Platform

  1. Create YAML config targets/<target>.yaml:

    name: MyMCU
    manufacturer: Vendor
    memory:
     flash: 524288
     ram: 65536
    clock:
     f_osc: 16000000
     f_cpu: 48000000
    toolchain_tool: riscv_gcc
    toolchain:
     prefix: riscv-none-elf-
     arch: rv32imfc
     abi: ilp32
    linker_script: mymcu_flash.ld
    include_paths:
      - inc
    defines:
     F_CPU: 48000000
     DYNAMIC_MEMORY_SIZE: 8192
    platform_sources:
     always:
        - src/platform/MyMCU/startup.c
    openocd:
     interface: openocd/mydebugger.cfg
     target_cfg: target/mymcu.cfg
    
  2. Create platform sources under src/platform/MyMCU/

  3. Create target headers under inc/MyMCU/
  4. Create linker script under ldscripts/
  5. Create OpenOCD config under openocd/

Key Framework Concepts

Async/Await Coroutines

io_task<void> MyTask()
{
    co_await 100ms;                    // Wait 100ms
    co_await io_flag<MyRegister>;      // Wait for hardware flag
    auto result = co_await OtherTask(); // Wait for another task
    co_return;
}

Three dispatchers are polled in the main loop: TimerDispatcher, FlagDispatcher, CoroDispatcher.

Zero Dynamic Allocation

All containers use pre-allocated storage:

  • vector_adapter<T> — stack-backed vector
  • noalloc_function<R(Args...)> — stack-based function wrapper (SBO)
  • BinaryStream<Source> — zero-copy byte I/O

Memory Management

No dynamic heap by default. When enabled (DYNAMIC_MEMORY_SIZE in target YAML), a tiered DispatchBlockAllocator backs the global operator new/delete over a pre-allocated pool. Coroutine frames are allocated on this heap.

Concurrency

  • coro_mutex + CO_LOCK(mtx) for coroutine-safe locking
  • manual_event for signaling between coroutines
  • ATOMIC blocks for interrupt-safe dispatcher access

Hardware Drivers

Each peripheral is a single class covering all transfer mechanisms (Poll / Dma / Irq), selected by IOFlags, plus a coroutine API:

Peripheral Notes
UART Poll / Dma / Irq / Async
ADC Poll / Dma / Irq / Async
I2C Poll / Irq / Async; platform-independent I2cDriver contract
DMA Channel transfers for UART / ADC / I2C
Timers TMR0–2 (16-bit) + TMR32 (32-bit); period / PWM / capture
Flash 1 MiB internal flash, 256 × 4 KiB pages
GPIO Per-pin API + dynamic pin lists
PLIC External interrupts; core-local MTI/MSI bypass PLIC

Device drivers build on top of the I2C driver: MPU-6050 (6-axis IMU) and SSD1306 (OLED display), each with Poll / Async / Irq mechanisms.


License

BSD License. See individual source files for full license text.