-
Notifications
You must be signed in to change notification settings - Fork 0
RR16X Extension API
NOTE:
-
APList 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.
-
Address collisions between peripherals are not supported. If two peripherals claim the same address, behavior is unspecified, and 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 current interrupt system.
-
You may implement more than the minimum tick(), read(), write(). Just beware that the system is only guaranteed to call read(),tick(),write(), so any unique functions should be called in read(), write(), or tick()
Peripherals requiring interrupts communicate through InterruptEnhancer.
A peripheral:
- owns acknowledgement of its own interrupt condition.
- raises an interrupt by calling InterruptEnhancer::raise_interrupt().
- 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.
To add a new extension, please observe the following requirements:
- Inherit from AbstractPeripheral
- expose and implement a write(), read(), tick() marked as override
- init in main, then put in APList as a reference. A peripheral should only respond to addresses listed in readableAddresses/writableAddresses.
The bus uses these lists to determine which peripheral receives a memory access. EXAMPLE:
NewPeripheral.h
STEP 1
#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
#include <filesystem>
STEP 4
InterruptEnhancer IE;
// std::cout << "[TRACER 2] Initializing Timer...\n";
Timer timer(IE, 0);
//std::cout << "[TRACER 3] Initializing Multiplier...\n";
Multiplier multiplier;
//std::cout << "[TRACER 4] Initializing UART...\n";
UART uart(IE, 0);
// std::cout << "[TRACER 5] Initializing WIC...\n";
WideIntCoprocessor WIC;
// std::cout << "[TRACER 6] Initializing FC...\n";
FP32Coprocessor FC;
// std::cout << "[TRACER 7] Initializing DMA...\n";
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
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)
{
counter--;
if(counter == 0)
{
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
};