Skip to content

RR16X Extension API

Alex edited this page Aug 27, 2026 · 7 revisions

Lifetime requirements:

  • APList (A std::vector<AbstractPeripheral*>) contains non-owning pointers.
  • The bus does not manage peripheral lifetime.
  • Peripheral instances are created and owned externally (normally by main).
  • Peripheral lifetime must equal or exceed the lifetime of the bus and APList usage.

Rules:

  • Peripheral tick() calls occur in APList order.

  • Address collisions between peripherals are not supported. If two peripherals claim the same address, behavior is unspecified. This is programmer error.
  • A call to tick() represents one emulator clock cycle from the peripheral's perspective.
  • A peripheral must not directly access CPU state.
  • Communication with the CPU occurs through bus-visible registers and optional interrupt lines.
  • Should a device require an interrupt line, the class InterruptEnhancer is the interrupt system.
  • tick is called AFTER interrupt evaluation and BEFORE the CPU itself

Emulator Cycle:
  1. InterruptEnhancer evaluates lines
  2. Peripheral::tick()  ← your code runs here
  3. CPU executes instruction
  • You may implement more than the minimum tick(), read(), write(). Just beware that the system is only guaranteed to call read(),tick(),write(), Any additional public methods must be invoked from within read(), write(), or tick(). The emulator will not call them directly.

What you must implement and expose as public, provided by AbstractPeripheral

  • uint16_t read(uint32_t) override;

  • void write(uint32_t, uint16_t) override;

  • void tick() override;

  • Address space is addressed in 16-bit words w/ 32-bit address space

  • On an unhandled read address, you may return 0, 0xFFFF, or return AbstractPeripheral::read(address) to get a debug message.

Interrupt-capable peripherals

Peripherals requiring interrupts communicate through InterruptEnhancer.

A peripheral:

  • owns acknowledgement of its own interrupt condition.
  • raises an interrupt by calling InterruptEnhancer::raise_interrupt().
  • it is safe to call raise_interrupt more than once while the condition remains true. The line remains asserted until clear_interrupt is called
  • clears its interrupt by calling InterruptEnhancer::clear_interrupt().

Interrupts are level triggered. A peripheral must keep its interrupt condition asserted until serviced.

The CPU does not acknowledge peripheral interrupts directly.

A peripheral must respond to all of the addresses listed in readableAddresses/writableAddresses. The bus uses these lists to determine which peripheral receives a memory access. please use the examples to illustrate how to add a new peripheral:

TEMPLATE -> USE TO START A NEW PERIPHERAL

NewPeripheral.h

STEP 1

#pragma once
#include "AbstractPeripheral.h"
class NewPeripheral: public AbstractPeripheral
{
private:
// local vars go here
public:
NewPeripheral();
uint16_t read(uint32_t address) override;
void write(uint32_t address, uint16_t value) override;
void tick() override; // called every cycle
};

STEP 2

NewPeripheral.cpp

#include "NewPeripheral.h"
NewPeripheral::NewPeripheral()
{
readableAddresses = {}; // put the addresses where you'll respond to a read call here, each element is a uint32_t
writableAddresses = {}; // put the addresses where you'll respond to a write call here, each element is a uint32_t
}
uint16_t NewPeripheral::read(uint32_t address)
{
// put your logic to respond to a read call here
}
void NewPeripheral::write(uint32_t address, uint16_t value)
{
// put your logic to respond to a write call here
}
void NewPeripheral::tick()
{
// put logic to be called every cycle here
}

RR16X_EMULATOR.cpp

STEP 3

#include <iostream>
#include <fstream>
#include <string>
#include "CPU.h"
#include "bus.h"
#include "AbstractPeripheral.h"

#include "interruptEnhancer.h"
#include "Timer.h"
#include "Multiplier.h"
#include "UART.h"
#include "WideIntCoprocessor.h"
#include "FP32Coprocessor.h"
#include "DMA.h"

#include "NewPeripheral.h" // <- new stuff here


STEP 4

InterruptEnhancer IE;

  
    Timer timer(IE, 0);
    Multiplier multiplier;
    UART uart(IE, 0);
    WideIntCoprocessor WIC;
    FP32Coprocessor FC;
    DMA dma(myBus, IE, 0);

    NewPeripheral NP; // init here

STEP 5

std::vector<AbstractPeripheral*> APList = { &timer,&dma,&uart,&IE,&multiplier,&WIC,&FC, &NP}; // add the new peripheral to APList by reference

worked example: countdown timer -> use to see a ready-to-use example.

STEP 1:

#pragma once
#include "AbstractPeripheral.h"
#include "interruptEnhancer.h"

class CountdownTimerPeripheral : public AbstractPeripheral
{
private:
    uint16_t counter;
    bool interruptRaised;
    InterruptEnhancer& IE;
    uint32_t irqLine;

public:
    CountdownTimerPeripheral(InterruptEnhancer& enhancer, uint32_t irq);

    uint16_t read(uint32_t address) override;
    void write(uint32_t address, uint16_t value) override;
    void tick() override;
};

STEP 2:

#include "CountdownTimerPeripheral.h"

CountdownTimerPeripheral::CountdownTimerPeripheral(InterruptEnhancer& enhancer, uint32_t irq)
    : IE(enhancer), irqLine(irq), counter(0), interruptRaised(false)
{
    readableAddresses = { 0x2000, 0x2002 };   // counter, status
    writableAddresses = { 0x2000, 0x2004 };   // load counter, clear interrupt
}

uint16_t CountdownTimerPeripheral::read(uint32_t address)
{
    switch(address)
    {
        case 0x2000: return counter;               // read current counter
        case 0x2002: return interruptRaised ? 1 : 0; // read interrupt status
        default: return 0;
    }
}

void CountdownTimerPeripheral::write(uint32_t address, uint16_t value)
{
    switch(address)
    {
        case 0x2000:                                // load counter
            counter = value;
            interruptRaised = false;
            IE.clear_interrupt(irqLine);
            break;

        case 0x2004:                                // clear interrupt command
            interruptRaised = false;
            IE.clear_interrupt(irqLine);
            break;
    }
}
void CountdownTimerPeripheral::tick()
{
    if(counter > 0)
    {
        if(--counter == 0 && !interruptRaised)
        {
            interruptRaised = true;
            IE.raise_interrupt(irqLine);
        }
    }
}

STEP 3:

#include <iostream>
#include <fstream>
#include <string>
#include "CPU.h"
#include "bus.h"
#include "AbstractPeripheral.h"

#include "interruptEnhancer.h"
#include "Timer.h"
#include "Multiplier.h"
#include "UART.h"
#include "WideIntCoprocessor.h"
#include "FP32Coprocessor.h"
#include "DMA.h"
#include "CountdownTimerPeripheral.h"

STEP 4

// ...

InterruptEnhancer IE;

Timer timer(IE, 0);
Multiplier multiplier;
UART uart(IE, 0);
WideIntCoprocessor WIC;
FP32Coprocessor FC;
DMA dma(myBus, IE, 0);

CountdownTimerPeripheral CTP(IE, 1); // new peripheral

STEP 5:

std::vector<AbstractPeripheral*> APList =
{
    &timer,
    &dma,
    &uart,
    &IE,
    &multiplier,
    &WIC,
    &FC,
    &CTP   // add new device
};

Clone this wiki locally