diff --git a/Machines/Acorn/Archimedes/Archimedes.cpp b/Machines/Acorn/Archimedes/Archimedes.cpp
index 3cb706e42..348094fc7 100644
--- a/Machines/Acorn/Archimedes/Archimedes.cpp
+++ b/Machines/Acorn/Archimedes/Archimedes.cpp
@@ -8,6 +8,12 @@
 
 #include "Archimedes.hpp"
 
+#include "HalfDuplexSerial.hpp"
+#include "InputOutputController.hpp"
+#include "Keyboard.hpp"
+#include "MemoryController.hpp"
+#include "Sound.hpp"
+
 #include "../../AudioProducer.hpp"
 #include "../../KeyboardMachine.hpp"
 #include "../../MediaTarget.hpp"
@@ -28,1091 +34,10 @@ namespace {
 
 Log::Logger<Log::Source::Archimedes> logger;
 
-enum class Zone {
-	LogicallyMappedRAM,
-	PhysicallyMappedRAM,
-	IOControllers,
-	LowROM,
-	HighROM,
-	VideoController,
-	DMAAndMEMC,
-	AddressTranslator,
-};
-constexpr std::array<Zone, 0x20> zones(bool is_read) {
-	std::array<Zone, 0x20> zones{};
-	for(size_t c = 0; c < zones.size(); c++) {
-		const auto address = c << 21;
-		if(address < 0x200'0000) {
-			zones[c] = Zone::LogicallyMappedRAM;
-		} else if(address < 0x300'0000) {
-			zones[c] = Zone::PhysicallyMappedRAM;
-		} else if(address < 0x340'0000) {
-			zones[c] = Zone::IOControllers;
-		} else if(address < 0x360'0000) {
-			zones[c] = is_read ? Zone::LowROM : Zone::VideoController;
-		} else if(address < 0x380'0000) {
-			zones[c] = is_read ? Zone::LowROM : Zone::DMAAndMEMC;
-		} else {
-			zones[c] = is_read ? Zone::HighROM : Zone::AddressTranslator;
-		}
-	}
-	return zones;
-}
-
-template <int start, int end> struct BitMask {
-	static_assert(start >= end);
-	static constexpr uint32_t value = ((1 << (start + 1)) - 1) - ((1 << end) - 1);
-};
-static_assert(BitMask<0, 0>::value == 1);
-static_assert(BitMask<1, 1>::value == 2);
-static_assert(BitMask<15, 15>::value == 32768);
-static_assert(BitMask<15, 0>::value == 0xffff);
-static_assert(BitMask<15, 14>::value == 49152);
-
 }
 
 namespace Archimedes {
 
-struct CMOSRAM: public I2C::Peripheral {
-
-};
-
-/// Models a half-duplex serial link between two parties, framing bytes with one start bit and two stop bits.
-struct HalfDuplexSerial {
-	static constexpr uint16_t ShiftMask = 0b1111'1110'0000'0000;
-
-	/// Enqueues @c value for output.
-	void output(int party, uint8_t value) {
-		parties_[party].output_count = 11;
-		parties_[party].input = 0x7ff;
-		parties_[party].output = uint16_t((value << 1) | ShiftMask);
-	}
-
-	/// @returns The last observed input.
-	uint8_t input(int party) const {
-		return uint8_t(parties_[party].input >> 1);
-	}
-
-	static constexpr uint8_t Receive = 1 << 0;
-	static constexpr uint8_t Transmit = 1 << 1;
-
-	/// @returns A bitmask of events that occurred during the last shift.
-	uint8_t events(int party) {
-		const auto result = parties_[party].events;
-		parties_[party].events = 0;
-		return result;
-	}
-
-	bool is_outputting(int party) const {
-		return parties_[party].output_count != 11;
-	}
-
-	/// Updates the shifters on both sides of the serial link.
-	void shift() {
-		const uint16_t next = parties_[0].output & parties_[1].output & 1;
-
-		for(int c = 0; c < 2; c++) {
-			if(parties_[c].output_count) {
-				--parties_[c].output_count;
-				if(!parties_[c].output_count) {
-					parties_[c].events |= Transmit;
-					parties_[c].input_count = -1;
-				}
-				parties_[c].output = (parties_[c].output >> 1) | ShiftMask;
-			} else {
-				// Check for a start bit.
-				if(parties_[c].input_count == -1 && !next) {
-					parties_[c].input_count = 0;
-				}
-
-				// Shift in if currently observing.
-				if(parties_[c].input_count >= 0 && parties_[c].input_count < 11) {
-					parties_[c].input = uint16_t((parties_[c].input >> 1) | (next << 10));
-
-					++parties_[c].input_count;
-					if(parties_[c].input_count == 11) {
-						parties_[c].events |= Receive;
-						parties_[c].input_count = -1;
-					}
-				}
-			}
-		}
-	}
-
-private:
-	struct Party {
-		int output_count = 0;
-		int input_count = -1;
-		uint16_t output = 0xffff;
-		uint16_t input = 0;
-		uint8_t events = 0;
-	} parties_[2];
-};
-
-static constexpr int IOCParty = 0;
-static constexpr int KeyboardParty = 1;
-
-// Resource for the keyboard protocol: https://github.com/tmk/tmk_keyboard/wiki/ACORN-ARCHIMEDES-Keyboard
-struct Keyboard {
-	Keyboard(HalfDuplexSerial &serial) : serial_(serial) {}
-
-	void update() {
-		if(serial_.events(KeyboardParty) & HalfDuplexSerial::Receive) {
-			const uint8_t input = serial_.input(KeyboardParty);
-			switch(input) {
-				case HRST:
-					// TODO:
-				case RAK1:
-				case RAK2:
-					serial_.output(KeyboardParty, input);
-				break;
-
-				case RQID:
-					serial_.output(KeyboardParty, 0x81);	// TODO: what keyboard type?
-				break;
-
-				default:
-					printf("Keyboard declines to respond to %02x\n", input);
-				break;
-			}
-		}
-	}
-
-private:
-	HalfDuplexSerial &serial_;
-
-	static constexpr uint8_t HRST	= 0b1111'1111;	// Keyboard reset.
-	static constexpr uint8_t RAK1	= 0b1111'1110;	// Reset response #1.
-	static constexpr uint8_t RAK2	= 0b1111'1101;	// Reset response #2.
-
-	static constexpr uint8_t RQID	= 0b0010'0000;	// Request for keyboard ID.
-	static constexpr uint8_t RQMP	= 0b0010'0010;	// Request for mouse data.
-
-	static constexpr uint8_t BACK	= 0b0011'1111;	// Acknowledge for first keyboard data byte pair.
-	static constexpr uint8_t NACK	= 0b0011'0000;	// Acknowledge for last keyboard data byte pair, selects scan/mouse mode.
-	static constexpr uint8_t SACK	= 0b0011'0001;	// Last data byte acknowledge.
-	static constexpr uint8_t MACK	= 0b0011'0010;	// Last data byte acknowledge.
-	static constexpr uint8_t SMAK	= 0b0011'0011;	// Last data byte acknowledge.
-	static constexpr uint8_t PRST	= 0b0010'0001;	// Does nothing.
-};
-
-struct Audio {
-	void set_next_end(uint32_t value) {
-		next_.end = value;
-	}
-
-	void set_next_start(uint32_t value) {
-		next_.start = value;
-		next_buffer_valid_ = true;
-	}
-
-	bool interrupt() const {
-		return !next_buffer_valid_;
-	}
-
-	void swap() {
-		current_.start = next_.start;
-		std::swap(current_.end, next_.end);
-		next_buffer_valid_ = false;
-		halted_ = false;
-	}
-
-	bool tick() {
-		if(halted_) {
-			return false;
-		}
-
-		current_.start += 16;
-		if(current_.start == current_.end) {
-			if(next_buffer_valid_) {
-				swap();
-				return true;
-			} else {
-				halted_ = true;
-				return false;
-			}
-		}
-		return false;
-	}
-
-private:
-	bool next_buffer_valid_ = false;
-	bool halted_ = true;				// This is a bit of a guess.
-
-	struct Buffer {
-		uint32_t start = 0, end = 0;
-	};
-	Buffer current_, next_;
-};
-struct Video {
-	void write(uint32_t value) {
-		const auto target = (value >> 24) & 0xfc;
-
-		switch(target) {
-			case 0x00:	case 0x04:	case 0x08:	case 0x0c:
-			case 0x10:	case 0x14:	case 0x18:	case 0x1c:
-			case 0x20:	case 0x24:	case 0x28:	case 0x2c:
-			case 0x30:	case 0x34:	case 0x38:	case 0x3c:
-				logger.error().append("TODO: Video palette logical colour %d to %03x", (target >> 2), value & 0x1fff);
-			break;
-
-			case 0x40:
-				logger.error().append("TODO: Video border colour to %03x", value & 0x1fff);
-			break;
-
-			case 0x44:	case 0x48:	case 0x4c:
-				logger.error().append("TODO: Cursor colour %d to %03x", (target - 0x44) >> 2, value & 0x1fff);
-			break;
-
-			case 0x60:	case 0x64:	case 0x68:	case 0x6c:
-			case 0x70:	case 0x74:	case 0x78:	case 0x7c:
-				logger.error().append("TODO: Stereo image register %d to %03x", (target - 0x60) >> 2, value & 0x7);
-			break;
-
-			case 0x80:
-				logger.error().append("TODO: Video horizontal period: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0x84:
-				logger.error().append("TODO: Video horizontal sync width: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0x88:
-				logger.error().append("TODO: Video horizontal border start: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0x8c:
-				logger.error().append("TODO: Video horizontal display start: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0x90:
-				logger.error().append("TODO: Video horizontal display end: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0x94:
-				logger.error().append("TODO: Video horizontal border end: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0x98:
-				logger.error().append("TODO: Video horizontal cursor end: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0x9c:
-				logger.error().append("TODO: Video horizontal interlace: %d", (value >> 14) & 0x3ff);
-			break;
-
-			case 0xa0:
-				logger.error().append("TODO: Video vertical period: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0xa4:
-				logger.error().append("TODO: Video vertical sync width: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0xa8:
-				logger.error().append("TODO: Video vertical border start: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0xac:
-				logger.error().append("TODO: Video vertical display start: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0xb0:
-				logger.error().append("TODO: Video vertical display end: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0xb4:
-				logger.error().append("TODO: Video vertical border end: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0xb8:
-				logger.error().append("TODO: Video vertical cursor start: %d", (value >> 14) & 0x3ff);
-			break;
-			case 0xbc:
-				logger.error().append("TODO: Video vertical cursor end: %d", (value >> 14) & 0x3ff);
-			break;
-
-			case 0xc0:
-				logger.error().append("TODO: Sound frequency: %d", value & 0x7f);
-			break;
-
-			case 0xe0:
-				logger.error().append("TODO: video control: %08x", value);
-			break;
-
-			default:
-				logger.error().append("TODO: unrecognised VIDC write of %08x", value);
-			break;
-		}
-	}
-};
-
-// IRQ A flags
-namespace IRQA {
-	// The first four of these are taken from the A500 documentation and may be inaccurate.
-	static constexpr uint8_t PrinterBusy		= 0x01;
-	static constexpr uint8_t SerialRinging		= 0x02;
-	static constexpr uint8_t PrinterAcknowledge	= 0x04;
-	static constexpr uint8_t VerticalFlyback	= 0x08;
-	static constexpr uint8_t PowerOnReset		= 0x10;
-	static constexpr uint8_t Timer0				= 0x20;
-	static constexpr uint8_t Timer1				= 0x40;
-	static constexpr uint8_t SetAlways			= 0x80;
-}
-
-// IRQ B flags
-namespace IRQB {
-	// These are taken from the A3010 documentation.
-	static constexpr uint8_t PoduleFIQRequest		= 0x01;
-	static constexpr uint8_t SoundBufferPointerUsed	= 0x02;
-	static constexpr uint8_t SerialLine				= 0x04;
-	static constexpr uint8_t IDE					= 0x08;
-	static constexpr uint8_t FloppyDiscInterrupt	= 0x10;
-	static constexpr uint8_t PoduleIRQRequest		= 0x20;
-	static constexpr uint8_t KeyboardTransmitEmpty	= 0x40;
-	static constexpr uint8_t KeyboardReceiveFull	= 0x80;
-}
-
-// FIQ flags
-namespace FIQ {
-	// These are taken from the A3010 documentation.
-	static constexpr uint8_t FloppyDiscData			= 0x01;
-	static constexpr uint8_t SerialLine				= 0x10;
-	static constexpr uint8_t PoduleFIQRequest		= 0x40;
-	static constexpr uint8_t SetAlways				= 0x80;
-}
-
-namespace InterruptRequests {
-	static constexpr int IRQ = 0x01;
-	static constexpr int FIQ = 0x02;
-};
-
-struct InputOutput {
-	int interrupt_mask() const {
-		return
-			((irq_a_.request() | irq_b_.request()) ? InterruptRequests::IRQ : 0) |
-			(fiq_.request() ? InterruptRequests::FIQ : 0);
-	}
-
-	template <int c>
-	bool tick_timer() {
-		if(!counters_[c].value && !counters_[c].reload) {
-			return false;
-		}
-
-		--counters_[c].value;
-		if(!counters_[c].value) {
-			counters_[c].value = counters_[c].reload;
-
-			switch(c) {
-				case 0:	return irq_a_.apply(IRQA::Timer0);
-				case 1:	return irq_a_.apply(IRQA::Timer1);
-				case 3: {
-					serial_.shift();
-					keyboard_.update();
-
-					const uint8_t events = serial_.events(IOCParty);
-					bool did_interrupt = false;
-					if(events & HalfDuplexSerial::Receive) {
-						did_interrupt |= irq_b_.apply(IRQB::KeyboardReceiveFull);
-					}
-					if(events & HalfDuplexSerial::Transmit) {
-						did_interrupt |= irq_b_.apply(IRQB::KeyboardTransmitEmpty);
-					}
-
-					return did_interrupt;
-				}
-				default: break;
-			}
-			// TODO: events for timers 2 (baud).
-		}
-
-		return false;
-	}
-
-	bool tick_timers() {
-		bool did_change_interrupts = false;
-		did_change_interrupts |= tick_timer<0>();
-		did_change_interrupts |= tick_timer<1>();
-		did_change_interrupts |= tick_timer<2>();
-		did_change_interrupts |= tick_timer<3>();
-		return did_change_interrupts;
-	}
-
-	static constexpr uint32_t AddressMask = 0x1f'ffff;
-
-	bool read(uint32_t address, uint8_t &value) {
-		const auto target = address & AddressMask;
-		value = 0xff;
-		switch(target) {
-			default:
-				logger.error().append("Unrecognised IOC read from %08x", address);
-			break;
-
-			case 0x3200000 & AddressMask:
-				value = control_ | 0xc0;
-				value &= ~(i2c_.clock() ? 2 : 0);
-				value &= ~(i2c_.data() ? 1 : 0);
-//				logger.error().append("IOC control read: C:%d D:%d", !(value & 2), !(value & 1));
-			return true;
-
-			case 0x3200004 & AddressMask:
-				value = serial_.input(IOCParty);
-				irq_b_.clear(IRQB::KeyboardReceiveFull);
-				logger.error().append("IOC keyboard receive: %02x", value);
-			return true;
-
-			// IRQ A.
-			case 0x3200010 & AddressMask:
-				value = irq_a_.status;
-//				logger.error().append("IRQ A status is %02x", value);
-			return true;
-			case 0x3200014 & AddressMask:
-				value = irq_a_.request();
-				logger.error().append("IRQ A request is %02x", value);
-			return true;
-			case 0x3200018 & AddressMask:
-				value = irq_a_.mask;
-				logger.error().append("IRQ A mask is %02x", value);
-			return true;
-
-			// IRQ B.
-			case 0x3200020 & AddressMask:
-				value = irq_b_.status;
-//				logger.error().append("IRQ B status is %02x", value);
-			return true;
-			case 0x3200024 & AddressMask:
-				value = irq_b_.request();
-				logger.error().append("IRQ B request is %02x", value);
-			return true;
-			case 0x3200028 & AddressMask:
-				value = irq_b_.mask;
-				logger.error().append("IRQ B mask is %02x", value);
-			return true;
-
-			// FIQ.
-			case 0x3200030 & AddressMask:
-				value = fiq_.status;
-				logger.error().append("FIQ status is %02x", value);
-			return true;
-			case 0x3200034 & AddressMask:
-				value = fiq_.request();
-				logger.error().append("FIQ request is %02x", value);
-			return true;
-			case 0x3200038 & AddressMask:
-				value = fiq_.mask;
-				logger.error().append("FIQ mask is %02x", value);
-			return true;
-
-			// Counters.
-			case 0x3200040 & AddressMask:
-			case 0x3200050 & AddressMask:
-			case 0x3200060 & AddressMask:
-			case 0x3200070 & AddressMask:
-				value = counters_[(target >> 4) - 0x4].output & 0xff;
-//				logger.error().append("%02x: Counter %d low is %02x", target, (target >> 4) - 0x4, value);
-			return true;
-
-			case 0x3200044 & AddressMask:
-			case 0x3200054 & AddressMask:
-			case 0x3200064 & AddressMask:
-			case 0x3200074 & AddressMask:
-				value = counters_[(target >> 4) - 0x4].output >> 8;
-//				logger.error().append("%02x: Counter %d high is %02x", target, (target >> 4) - 0x4, value);
-			return true;
-		}
-
-		return true;
-	}
-
-	bool write(uint32_t address, uint8_t value) {
-		const auto target = address & AddressMask;
-		switch(target) {
-			default:
-				logger.error().append("Unrecognised IOC write of %02x at %08x", value, address);
-			break;
-
-			case 0x320'0000 & AddressMask:
-				// TODO: does the rest of the control register relate to anything?
-//				logger.error().append("TODO: IOC control write: C:%d D:%d", !(value & 2), !(value & 1));
-
-				control_ = value;
-				i2c_.set_clock_data(!(value & 2), !(value & 1));
-			return true;
-
-			case 0x320'0004 & AddressMask:
-				logger.error().append("IOC keyboard transmit %02x", value);
-				serial_.output(IOCParty, value);
-				irq_b_.clear(IRQB::KeyboardTransmitEmpty);
-			return true;
-
-			case 0x320'0014 & AddressMask:
-				// b2: clear IF.
-				// b3: clear IR.
-				// b4: clear POR.
-				// b5: clear TM[0].
-				// b6: clear TM[1].
-				irq_a_.clear(value & 0x7c);
-			return true;
-
-			// Interrupts.
-			case 0x320'0018 & AddressMask:	irq_a_.mask = value;	return true;
-			case 0x320'0028 & AddressMask:	irq_b_.mask = value;	return true;
-			case 0x320'0038 & AddressMask:	fiq_.mask = value;		return true;
-
-			// Counters.
-			case 0x320'0040 & AddressMask:
-			case 0x320'0050 & AddressMask:
-			case 0x320'0060 & AddressMask:
-			case 0x320'0070 & AddressMask:
-				counters_[(target >> 4) - 0x4].reload = uint16_t(
-					(counters_[(target >> 4) - 0x4].reload & 0xff00) | value
-				);
-			return true;
-
-			case 0x320'0044 & AddressMask:
-			case 0x320'0054 & AddressMask:
-			case 0x320'0064 & AddressMask:
-			case 0x320'0074 & AddressMask:
-				counters_[(target >> 4) - 0x4].reload = uint16_t(
-					(counters_[(target >> 4) - 0x4].reload & 0x00ff) | (value << 8)
-				);
-			return true;
-
-			case 0x320'0048 & AddressMask:
-			case 0x320'0058 & AddressMask:
-			case 0x320'0068 & AddressMask:
-			case 0x320'0078 & AddressMask:
-				counters_[(target >> 4) - 0x4].value = counters_[(target >> 4) - 0x4].reload;
-			return true;
-
-			case 0x320'004c & AddressMask:
-			case 0x320'005c & AddressMask:
-			case 0x320'006c & AddressMask:
-			case 0x320'007c & AddressMask:
-				counters_[(target >> 4) - 0x4].output = counters_[(target >> 4) - 0x4].value;
-			return true;
-
-			case 0x327'0000 & AddressMask:
-				logger.error().append("TODO: exteded external podule space");
-			return true;
-
-			case 0x331'0000 & AddressMask:
-				logger.error().append("TODO: 1772 / disk write");
-			return true;
-
-			case 0x335'0000 & AddressMask:
-				logger.error().append("TODO: LS374 / printer data write");
-			return true;
-
-			case 0x335'0018 & AddressMask:
-				logger.error().append("TODO: latch B write: %02x", value);
-			return true;
-
-			case 0x335'0040 & AddressMask:
-				logger.error().append("TODO: latch A write: %02x", value);
-			return true;
-
-			case 0x335'0048 & AddressMask:
-				logger.error().append("TODO: latch C write: %02x", value);
-			return true;
-
-			case 0x336'0000 & AddressMask:
-				logger.error().append("TODO: podule interrupt request");
-			return true;
-
-			case 0x336'0004 & AddressMask:
-				logger.error().append("TODO: podule interrupt mask");
-			return true;
-
-			case 0x33a'0000 & AddressMask:
-				logger.error().append("TODO: 6854 / econet write");
-			return true;
-
-			case 0x33b'0000 & AddressMask:
-				logger.error().append("TODO: 6551 / serial line write");
-			return true;
-		}
-
-		return true;
-	}
-
-	InputOutput() : keyboard_(serial_) {
-		irq_a_.status = IRQA::SetAlways | IRQA::PowerOnReset;
-		irq_b_.status = 0x00;
-		fiq_.status = 0x80;				// 'set always'.
-
-		i2c_.add_peripheral(&cmos_, 0xa0);
-	}
-
-	Audio &audio() {
-		return audio_;
-	}
-
-	bool tick_audio() {
-		if(audio_.tick()) {
-			if(audio_.interrupt()) {
-				irq_b_.apply(IRQB::SoundBufferPointerUsed);
-			} else {
-				irq_b_.clear(IRQB::SoundBufferPointerUsed);
-			}
-			return true;
-		}
-		return false;
-	}
-
-	void swap_audio() {
-		audio_.swap();
-		irq_b_.clear(IRQB::SoundBufferPointerUsed);
-	}
-
-private:
-	// IRQA, IRQB and FIQ states.
-	struct Interrupt {
-		uint8_t status, mask;
-		uint8_t request() const {
-			return status & mask;
-		}
-		bool apply(uint8_t value) {
-			status |= value;
-			return status & mask;
-		}
-		void clear(uint8_t bits) {
-			status &= ~bits;
-		}
-	};
-	Interrupt irq_a_, irq_b_, fiq_;
-
-	// The IOCs four counters.
-	struct Counter {
-		uint16_t value;
-		uint16_t reload;
-		uint16_t output;
-	};
-	Counter counters_[4];
-
-	// The KART and keyboard beyond it.
-	HalfDuplexSerial serial_;
-	Keyboard keyboard_;
-
-	// The control register.
-	uint8_t control_ = 0xff;
-
-	// The I2C bus.
-	I2C::Bus i2c_;
-	CMOSRAM cmos_;
-
-	// Audio.
-	Audio audio_;
-};
-
-/// Primarily models the MEMC.
-template <typename IOCWriteDelegateT>
-struct Memory {
-	Memory(IOCWriteDelegateT &ioc_write_delegate) : ioc_write_delegate_(ioc_write_delegate) {}
-
-	int interrupt_mask() const {
-		return ioc_.interrupt_mask();
-	}
-
-	void set_rom(const std::vector<uint8_t> &rom) {
-		std::copy(
-			rom.begin(),
-			rom.begin() + static_cast<ptrdiff_t>(std::min(rom.size(), rom_.size())),
-			rom_.begin());
-	}
-
-	template <typename IntT>
-	uint32_t aligned(uint32_t address) {
-		if constexpr (std::is_same_v<IntT, uint32_t>) {
-			return address & static_cast<uint32_t>(~3);
-		}
-		return address;
-	}
-
-	template <typename IntT>
-	bool write(uint32_t address, IntT source, InstructionSet::ARM::Mode mode, bool) {
-		// User mode may only _write_ to logically-mapped RAM (subject to further testing below).
-		if(mode == InstructionSet::ARM::Mode::User && address >= 0x200'0000) {
-			return false;
-		}
-
-		switch(write_zones_[(address >> 21) & 31]) {
-			case Zone::DMAAndMEMC: {
-				const auto buffer_address = [](uint32_t source) -> uint32_t {
-					return (source & 0x1fffc0) << 2;
-				};
-
-				// The MEMC itself isn't on the data bus; all values below should be taken from `address`.
-				switch((address >> 17) & 0b111) {
-					case 0b000:
-						logger.error().append("TODO: DMA/MEMC Vinit = %04x", address & 0x1fffc0);
-					return true;
-
-					case 0b001:
-						logger.error().append("TODO: DMA/MEMC Vstart = %04x", address & 0x1fffc0);
-					return true;
-
-					case 0b010:
-						logger.error().append("TODO: DMA/MEMC Vend = %04x", address & 0x1fffc0);
-					return true;
-
-					case 0b011:
-						logger.error().append("TODO: DMA/MEMC Cinit = %04x", address & 0x1fffc0);
-					return true;
-
-					case 0b100:
-						logger.error().append("TODO: DMA/MEMC Sstart = %04x", address & 0x1fffc0);
-						ioc_.audio().set_next_start(buffer_address(address));
-					return true;
-
-					case 0b101:
-						logger.error().append("TODO: DMA/MEMC SendN = %04x", address & 0x1fffc0);
-						ioc_.audio().set_next_end(buffer_address(address));
-					return true;
-
-					case 0b110:
-						logger.error().append("TODO: DMA/MEMC Sptr");
-						ioc_.swap_audio();
-					return true;
-
-					case 0b111:
-						os_mode_ = address & (1 << 12);
-						audio_dma_enable_ = address & (1 << 11);
-						video_dma_enable_ = address & (1 << 10);
-						switch((address >> 8) & 3) {
-							default:
-								dynamic_ram_refresh_ = DynamicRAMRefresh::None;
-							break;
-							case 0b01:
-							case 0b11:
-								dynamic_ram_refresh_ = DynamicRAMRefresh((address >> 8) & 3);
-							break;
-						}
-						high_rom_access_time_ = ROMAccessTime((address >> 6) & 3);
-						low_rom_access_time_ = ROMAccessTime((address >> 4) & 3);
-						page_size_ = PageSize((address >> 2) & 3);
-
-						logger.info().append("MEMC Control: %08x -> OS:%d audio:%d video:%d refresh:%d high:%d low:%d size:%d", address, os_mode_, audio_dma_enable_, video_dma_enable_, dynamic_ram_refresh_, high_rom_access_time_, low_rom_access_time_, page_size_);
-						map_dirty_ = true;
-					return true;
-				}
-			} break;
-
-			case Zone::LogicallyMappedRAM: {
-				const auto item = logical_ram<IntT, false>(address, mode);
-				if(!item) {
-					return false;
-				}
-				*item = source;
-				return true;
-			} break;
-
-			case Zone::IOControllers:
-				// TODO: have I overrestricted the value type for the IOC area?
-				ioc_.write(address, uint8_t(source));
-				ioc_write_delegate_.did_write_ioc();
-			return true;
-
-			case Zone::VideoController:
-				// TODO: handle byte writes correctly.
-				vidc_.write(source);
-			break;
-
-			case Zone::PhysicallyMappedRAM:
-				physical_ram<IntT>(address) = source;
-			return true;
-
-			case Zone::AddressTranslator:
-//				printf("Translator write at %08x; replaces %08x\n", address, pages_[address & 0x7f]);
-				pages_[address & 0x7f] = address;
-				map_dirty_ = true;
-			break;
-
-			default:
-//				printf("TODO: write of %08x to %08x [%lu]\n", source, address, sizeof(IntT));
-			break;
-		}
-
-		return true;
-	}
-
-	template <typename IntT>
-	bool read(uint32_t address, IntT &source, InstructionSet::ARM::Mode mode, bool) {
-		// User mode may only read logically-maped RAM and ROM.
-		if(mode == InstructionSet::ARM::Mode::User && address >= 0x200'0000 && address < 0x380'0000) {
-			return false;
-		}
-
-		switch (read_zones_[(address >> 21) & 31]) {
-			case Zone::PhysicallyMappedRAM:
-				source = physical_ram<IntT>(address);
-			return true;
-
-			case Zone::LogicallyMappedRAM: {
-				if(!has_moved_rom_) {	// TODO: maintain this state in the zones table.
-					source = high_rom<IntT>(address);
-					return true;
-				}
-
-				const auto item = logical_ram<IntT, true>(address, mode);
-				if(!item) {
-					return false;
-				}
-				source = *item;
-				return true;
-			} break;
-
-			case Zone::LowROM:
-//				logger.error().append("TODO: Low ROM read from %08x", address);
-				source = IntT(~0);
-			return true;
-
-			case Zone::HighROM:
-				// Real test is: require A24=A25=0, then A25=1.
-				// TODO: as above, move this test into the zones tables.
-				has_moved_rom_ = true;
-				source = high_rom<IntT>(address);
-			return true;
-
-			case Zone::IOControllers:	{
-				if constexpr (std::is_same_v<IntT, uint8_t>) {
-					ioc_.read(address, source);
-					return true;
-				} else {
-					// TODO: generalise this adaptation of an 8-bit device to the 32-bit bus, which probably isn't right anyway.
-					uint8_t value;
-					ioc_.read(address, value);
-					source = value;
-					return true;
-				}
-			}
-
-			default:
-				logger.error().append("TODO: read from %08x", address);
-			break;
-		}
-
-		source = 0;
-		return false;
-	}
-
-	bool tick_timers() {
-		return ioc_.tick_timers();
-	}
-
-	bool tick_audio() {
-		// TODO: does disabling audio DMA pause output, or leave it ticking and merely
-		// stop allowing it to use the bus?
-		return ioc_.tick_audio();
-	}
-
-	private:
-		bool has_moved_rom_ = false;
-		std::array<uint8_t, 4*1024*1024> ram_{};
-		std::array<uint8_t, 2*1024*1024> rom_;
-		InputOutput ioc_;
-		Video vidc_;
-		IOCWriteDelegateT &ioc_write_delegate_;
-
-		template <typename IntT>
-		IntT &physical_ram(uint32_t address) {
-			address = aligned<IntT>(address);
-			address &= (ram_.size() - 1);
-			return *reinterpret_cast<IntT *>(&ram_[address]);
-		}
-
-		template <typename IntT>
-		IntT &high_rom(uint32_t address) {
-			address = aligned<IntT>(address);
-			return *reinterpret_cast<IntT *>(&rom_[address & (rom_.size() - 1)]);
-		}
-
-		static constexpr std::array<Zone, 0x20> read_zones_ = zones(true);
-		static constexpr std::array<Zone, 0x20> write_zones_ = zones(false);
-
-		// Control register values.
-		bool os_mode_ = false;
-		bool audio_dma_enable_ = false;
-		bool video_dma_enable_ = false;	// "Unaffected" by reset, so here picked arbitrarily.
-
-		enum class DynamicRAMRefresh {
-			None = 0b00,
-			DuringFlyback = 0b01,
-			Continuous = 0b11,
-		} dynamic_ram_refresh_ = DynamicRAMRefresh::None;	// State at reset is undefined; constrain to a valid enum value.
-
-		enum class ROMAccessTime {
-			ns450 = 0b00,
-			ns325 = 0b01,
-			ns200 = 0b10,
-			ns200with60nsNibble = 0b11,
-		} high_rom_access_time_ = ROMAccessTime::ns450, low_rom_access_time_ = ROMAccessTime::ns450;
-
-		enum class PageSize {
-			kb4 = 0b00,
-			kb8 = 0b01,
-			kb16 = 0b10,
-			kb32 = 0b11,
-		} page_size_ = PageSize::kb4;
-
-		// Address translator.
-		//
-		// MEMC contains one entry per a physical page number, indicating where it goes logically.
-		// Any logical access is tested against all 128 mappings. So that's backwards compared to
-		// the ideal for an emulator, which would map from logical to physical, even if a lot more
-		// compact — there are always 128 physical pages; there are up to 8192 logical pages.
-		//
-		// So captured here are both the physical -> logical map as representative of the real
-		// hardware, and the reverse logical -> physical map, which is built (and rebuilt, and rebuilt)
-		// from the other.
-
-		// Physical to logical mapping.
-		std::array<uint32_t, 128> pages_{};
-
-		// Logical to physical mapping.
-		struct MappedPage {
-			uint8_t *target = nullptr;
-			uint8_t protection_level = 0;
-		};
-		std::array<MappedPage, 8192> mapping_;
-		bool map_dirty_ = true;
-
-		template <typename IntT, bool is_read>
-		IntT *logical_ram(uint32_t address, InstructionSet::ARM::Mode mode) {
-			// Possibly TODO: this recompute-if-dirty flag is supposed to ameliorate for an expensive
-			// mapping process. It can be eliminated when the process is improved.
-			if(map_dirty_) {
-				update_mapping();
-				map_dirty_ = false;
-			}
-
-			address = aligned<IntT>(address);
-			address &= 0x1ff'ffff;
-			size_t page;
-
-			// TODO: eliminate switch here.
-			switch(page_size_) {
-				default:
-				case PageSize::kb4:
-					page = address >> 12;
-					address &= 0x0fff;
-				break;
-				case PageSize::kb8:
-					page = address >> 13;
-					address &= 0x1fff;
-				break;
-				case PageSize::kb16:
-					page = address >> 14;
-					address &= 0x3fff;
-				break;
-				case PageSize::kb32:
-					page = address >> 15;
-					address &= 0x7fff;
-				break;
-			}
-
-			if(!mapping_[page].target) {
-				return nullptr;
-			}
-
-			// TODO: eliminate switch here.
-			// Top of my head idea: is_read, is_user and is_os_mode make three bits, so
-			// keep a one-byte bitmap of permitted accesses rather than the raw protection
-			// level?
-			switch(mapping_[page].protection_level) {
-				case 0b00:	break;
-				case 0b01:
-					if(!is_read && mode == InstructionSet::ARM::Mode::User) {
-						return nullptr;
-					}
-				break;
-				default:
-					if(mode == InstructionSet::ARM::Mode::User) {
-						return nullptr;
-					}
-					if(!is_read && !os_mode_) {
-						return nullptr;
-					}
-				break;
-			}
-
-			return reinterpret_cast<IntT *>(mapping_[page].target + address);
-		}
-
-		void update_mapping() {
-			// For each physical page, project it into logical space.
-			switch(page_size_) {
-				default:
-				case PageSize::kb4:		update_mapping<PageSize::kb4>();	break;
-				case PageSize::kb8:		update_mapping<PageSize::kb8>();	break;
-				case PageSize::kb16:	update_mapping<PageSize::kb16>();	break;
-				case PageSize::kb32:	update_mapping<PageSize::kb32>();	break;
-			}
-		}
-
-		template <PageSize size>
-		void update_mapping() {
-			// Clear all logical mappings.
-			std::fill(mapping_.begin(), mapping_.end(), MappedPage{});
-
-			// For each physical page, project it into logical space
-			// and store it.
-			for(const auto page: pages_) {
-				uint32_t physical, logical;
-
-				switch(size) {
-					case PageSize::kb4:
-						// 4kb:
-						//		A[6:0] -> PPN[6:0]
-						//		A[11:10] -> LPN[12:11];	A[22:12] -> LPN[10:0]	i.e. 8192 logical pages
-						physical = page & BitMask<6, 0>::value;
-
-						physical <<= 12;
-
-						logical = (page & BitMask<11, 10>::value) << 1;
-						logical |= (page & BitMask<22, 12>::value) >> 12;
-					break;
-
-					case PageSize::kb8:
-						// 8kb:
-						//		A[0] -> PPN[6]; A[6:1] -> PPN[5:0]
-						//		A[11:10] -> LPN[11:10];	A[22:13] -> LPN[9:0]	i.e. 4096 logical pages
-						physical = (page & BitMask<0, 0>::value) << 6;
-						physical |= (page & BitMask<6, 1>::value) >> 1;
-
-						physical <<= 13;
-
-						logical = page & BitMask<11, 10>::value;
-						logical |= (page & BitMask<22, 13>::value) >> 13;
-					break;
-
-					case PageSize::kb16:
-						// 16kb:
-						//		A[1:0] -> PPN[6:5]; A[6:2] -> PPN[4:0]
-						//		A[11:10] -> LPN[10:9];	A[22:14] -> LPN[8:0]	i.e. 2048 logical pages
-						physical = (page & BitMask<1, 0>::value) << 5;
-						physical |= (page & BitMask<6, 2>::value) >> 2;
-
-						physical <<= 14;
-
-						logical = (page & BitMask<11, 10>::value) >> 1;
-						logical |= (page & BitMask<22, 14>::value) >> 14;
-					break;
-
-					case PageSize::kb32:
-						// 32kb:
-						//		A[1] -> PPN[6]; A[2] -> PPN[5]; A[0] -> PPN[4]; A[6:3] -> PPN[3:0]
-						//		A[11:10] -> LPN[9:8];	A[22:15] -> LPN[7:0]	i.e. 1024 logical pages
-						physical = (page & BitMask<1, 1>::value) << 5;
-						physical |= (page & BitMask<2, 2>::value) << 3;
-						physical |= (page & BitMask<0, 0>::value) << 4;
-						physical |= (page & BitMask<6, 3>::value) >> 3;
-
-						physical <<= 15;
-
-						logical = (page & BitMask<11, 10>::value) >> 2;
-						logical |= (page & BitMask<22, 15>::value) >> 15;
-					break;
-				}
-
-//				printf("%08x => physical %d -> logical %d\n", page, (physical >> 15), logical);
-
-				// TODO: consider clashes.
-				// TODO: what if there's less than 4mb present?
-				mapping_[logical].target = &ram_[physical];
-				mapping_[logical].protection_level = (page >> 8) & 3;
-			}
-		}
-};
-
 class ConcreteMachine:
 	public Machine,
 	public MachineTypes::MediaTarget,
@@ -1139,36 +64,14 @@ class ConcreteMachine:
 			//	* timers: 2;
 			//	* audio: 1.		[TODO]
 
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
-
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
-
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
+			tick_cpu();		tick_cpu();		tick_cpu();		tick_cpu();
+			tick_cpu();		tick_cpu();		tick_cpu();		tick_cpu();
+			tick_cpu();		tick_cpu();		tick_cpu();		tick_cpu();
 			tick_timers();
 
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
-
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
-
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
-			tick_cpu();
+			tick_cpu();		tick_cpu();		tick_cpu();		tick_cpu();
+			tick_cpu();		tick_cpu();		tick_cpu();		tick_cpu();
+			tick_cpu();		tick_cpu();		tick_cpu();		tick_cpu();
 			tick_timers();
 			tick_audio();
 		}
@@ -1192,8 +95,16 @@ class ConcreteMachine:
 			insert_media(target.media);
 		}
 
-		void did_write_ioc() {
-			test_interrupts();
+		void update_interrupts() {
+			using Exception = InstructionSet::ARM::Registers::Exception;
+
+			const int requests = executor_.bus.interrupt_mask();
+			if((requests & InterruptRequests::FIQ) && executor_.registers().interrupt<Exception::FIQ>()) {
+				return;
+			}
+			if(requests & InterruptRequests::IRQ) {
+				executor_.registers().interrupt<Exception::IRQ>();
+			}
 		}
 
 	private:
@@ -1290,29 +201,8 @@ class ConcreteMachine:
 //			}
 		}
 
-		void test_interrupts() {
-			using Exception = InstructionSet::ARM::Registers::Exception;
-
-			const int requests = executor_.bus.interrupt_mask();
-			if((requests & InterruptRequests::FIQ) && executor_.registers().interrupt<Exception::FIQ>()) {
-				return;
-			}
-			if(requests & InterruptRequests::IRQ) {
-				executor_.registers().interrupt<Exception::IRQ>();
-			}
-		}
-
-		void tick_timers() {
-			if(executor_.bus.tick_timers()) {
-				test_interrupts();
-			}
-		}
-
-		void tick_audio() {
-			if(executor_.bus.tick_audio()) {
-				test_interrupts();
-			}
-		}
+		void tick_timers()	{	executor_.bus.tick_timers();	}
+		void tick_audio()	{	executor_.bus.tick_audio();		}
 
 		// MARK: - MediaTarget
 		bool insert_media(const Analyser::Static::Media &) override {
@@ -1328,7 +218,7 @@ class ConcreteMachine:
 
 		// MARK: - ARM execution
 		static constexpr auto arm_model = InstructionSet::ARM::Model::ARMv2;
-		InstructionSet::ARM::Executor<arm_model, Memory<ConcreteMachine>> executor_;
+		InstructionSet::ARM::Executor<arm_model, MemoryController<ConcreteMachine>> executor_;
 };
 
 }
diff --git a/Machines/Acorn/Archimedes/CMOSRAM.hpp b/Machines/Acorn/Archimedes/CMOSRAM.hpp
new file mode 100644
index 000000000..4b2ff6102
--- /dev/null
+++ b/Machines/Acorn/Archimedes/CMOSRAM.hpp
@@ -0,0 +1,19 @@
+//
+//  CMOSRAM.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 20/03/2024.
+//  Copyright © 2024 Thomas Harte. All rights reserved.
+//
+
+#pragma once
+
+#include "../../../Components/I2C/I2C.hpp"
+
+namespace Archimedes {
+
+struct CMOSRAM: public I2C::Peripheral {
+	// All TODO.
+};
+
+}
diff --git a/Machines/Acorn/Archimedes/HalfDuplexSerial.hpp b/Machines/Acorn/Archimedes/HalfDuplexSerial.hpp
new file mode 100644
index 000000000..e47a0a17d
--- /dev/null
+++ b/Machines/Acorn/Archimedes/HalfDuplexSerial.hpp
@@ -0,0 +1,88 @@
+//
+//  HalfDuplexSerial.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 20/03/2024.
+//  Copyright © 2024 Thomas Harte. All rights reserved.
+//
+
+#pragma once
+
+namespace Archimedes {
+
+/// Models a half-duplex serial link between two parties, framing bytes with one start bit and two stop bits.
+struct HalfDuplexSerial {
+	static constexpr uint16_t ShiftMask = 0b1111'1110'0000'0000;
+
+	/// Enqueues @c value for output.
+	void output(int party, uint8_t value) {
+		parties_[party].output_count = 11;
+		parties_[party].input = 0x7ff;
+		parties_[party].output = uint16_t((value << 1) | ShiftMask);
+	}
+
+	/// @returns The last observed input.
+	uint8_t input(int party) const {
+		return uint8_t(parties_[party].input >> 1);
+	}
+
+	static constexpr uint8_t Receive = 1 << 0;
+	static constexpr uint8_t Transmit = 1 << 1;
+
+	/// @returns A bitmask of events that occurred during the last shift.
+	uint8_t events(int party) {
+		const auto result = parties_[party].events;
+		parties_[party].events = 0;
+		return result;
+	}
+
+	bool is_outputting(int party) const {
+		return parties_[party].output_count != 11;
+	}
+
+	/// Updates the shifters on both sides of the serial link.
+	void shift() {
+		const uint16_t next = parties_[0].output & parties_[1].output & 1;
+
+		for(int c = 0; c < 2; c++) {
+			if(parties_[c].output_count) {
+				--parties_[c].output_count;
+				if(!parties_[c].output_count) {
+					parties_[c].events |= Transmit;
+					parties_[c].input_count = -1;
+				}
+				parties_[c].output = (parties_[c].output >> 1) | ShiftMask;
+			} else {
+				// Check for a start bit.
+				if(parties_[c].input_count == -1 && !next) {
+					parties_[c].input_count = 0;
+				}
+
+				// Shift in if currently observing.
+				if(parties_[c].input_count >= 0 && parties_[c].input_count < 11) {
+					parties_[c].input = uint16_t((parties_[c].input >> 1) | (next << 10));
+
+					++parties_[c].input_count;
+					if(parties_[c].input_count == 11) {
+						parties_[c].events |= Receive;
+						parties_[c].input_count = -1;
+					}
+				}
+			}
+		}
+	}
+
+private:
+	struct Party {
+		int output_count = 0;
+		int input_count = -1;
+		uint16_t output = 0xffff;
+		uint16_t input = 0;
+		uint8_t events = 0;
+	} parties_[2];
+};
+
+static constexpr int IOCParty = 0;
+static constexpr int KeyboardParty = 1;
+
+}
diff --git a/Machines/Acorn/Archimedes/InputOutputController.hpp b/Machines/Acorn/Archimedes/InputOutputController.hpp
new file mode 100644
index 000000000..94c617144
--- /dev/null
+++ b/Machines/Acorn/Archimedes/InputOutputController.hpp
@@ -0,0 +1,380 @@
+//
+//  InputOutputController.h
+//  Clock Signal
+//
+//  Created by Thomas Harte on 20/03/2024.
+//  Copyright © 2024 Thomas Harte. All rights reserved.
+//
+
+#pragma once
+
+#include "../../../Outputs/Log.hpp"
+
+#include "CMOSRAM.hpp"
+#include "Keyboard.hpp"
+#include "Sound.hpp"
+
+namespace Archimedes {
+
+// IRQ A flags
+namespace IRQA {
+	// The first four of these are taken from the A500 documentation and may be inaccurate.
+	static constexpr uint8_t PrinterBusy		= 0x01;
+	static constexpr uint8_t SerialRinging		= 0x02;
+	static constexpr uint8_t PrinterAcknowledge	= 0x04;
+	static constexpr uint8_t VerticalFlyback	= 0x08;
+	static constexpr uint8_t PowerOnReset		= 0x10;
+	static constexpr uint8_t Timer0				= 0x20;
+	static constexpr uint8_t Timer1				= 0x40;
+	static constexpr uint8_t SetAlways			= 0x80;
+}
+
+// IRQ B flags
+namespace IRQB {
+	// These are taken from the A3010 documentation.
+	static constexpr uint8_t PoduleFIQRequest		= 0x01;
+	static constexpr uint8_t SoundBufferPointerUsed	= 0x02;
+	static constexpr uint8_t SerialLine				= 0x04;
+	static constexpr uint8_t IDE					= 0x08;
+	static constexpr uint8_t FloppyDiscInterrupt	= 0x10;
+	static constexpr uint8_t PoduleIRQRequest		= 0x20;
+	static constexpr uint8_t KeyboardTransmitEmpty	= 0x40;
+	static constexpr uint8_t KeyboardReceiveFull	= 0x80;
+}
+
+// FIQ flags
+namespace FIQ {
+	// These are taken from the A3010 documentation.
+	static constexpr uint8_t FloppyDiscData			= 0x01;
+	static constexpr uint8_t SerialLine				= 0x10;
+	static constexpr uint8_t PoduleFIQRequest		= 0x40;
+	static constexpr uint8_t SetAlways				= 0x80;
+}
+
+namespace InterruptRequests {
+	static constexpr int IRQ = 0x01;
+	static constexpr int FIQ = 0x02;
+};
+
+template <typename InterruptObserverT>
+struct InputOutputController {
+	int interrupt_mask() const {
+		return
+			((irq_a_.request() | irq_b_.request()) ? InterruptRequests::IRQ : 0) |
+			(fiq_.request() ? InterruptRequests::FIQ : 0);
+	}
+
+	template <int c>
+	bool tick_timer() {
+		if(!counters_[c].value && !counters_[c].reload) {
+			return false;
+		}
+
+		--counters_[c].value;
+		if(!counters_[c].value) {
+			counters_[c].value = counters_[c].reload;
+
+			switch(c) {
+				case 0:	return irq_a_.set(IRQA::Timer0);
+				case 1:	return irq_a_.set(IRQA::Timer1);
+				case 3: {
+					serial_.shift();
+					keyboard_.update();
+
+					const uint8_t events = serial_.events(IOCParty);
+					bool did_interrupt = false;
+					if(events & HalfDuplexSerial::Receive) {
+						did_interrupt |= irq_b_.set(IRQB::KeyboardReceiveFull);
+					}
+					if(events & HalfDuplexSerial::Transmit) {
+						did_interrupt |= irq_b_.set(IRQB::KeyboardTransmitEmpty);
+					}
+
+					return did_interrupt;
+				}
+				default: break;
+			}
+			// TODO: events for timers 2 (baud).
+		}
+
+		return false;
+	}
+
+	bool tick_timers() {
+		bool did_change_interrupts = false;
+		did_change_interrupts |= tick_timer<0>();
+		did_change_interrupts |= tick_timer<1>();
+		did_change_interrupts |= tick_timer<2>();
+		did_change_interrupts |= tick_timer<3>();
+		return did_change_interrupts;
+	}
+
+	static constexpr uint32_t AddressMask = 0x1f'ffff;
+
+	bool read(uint32_t address, uint8_t &value) {
+		const auto target = address & AddressMask;
+		value = 0xff;
+		switch(target) {
+			default:
+				logger.error().append("Unrecognised IOC read from %08x", address);
+			break;
+
+			case 0x3200000 & AddressMask:
+				value = control_ | 0xc0;
+				value &= ~(i2c_.clock() ? 2 : 0);
+				value &= ~(i2c_.data() ? 1 : 0);
+//				logger.error().append("IOC control read: C:%d D:%d", !(value & 2), !(value & 1));
+			return true;
+
+			case 0x3200004 & AddressMask:
+				value = serial_.input(IOCParty);
+				irq_b_.clear(IRQB::KeyboardReceiveFull);
+				logger.error().append("IOC keyboard receive: %02x", value);
+			return true;
+
+			// IRQ A.
+			case 0x3200010 & AddressMask:
+				value = irq_a_.status;
+//				logger.error().append("IRQ A status is %02x", value);
+			return true;
+			case 0x3200014 & AddressMask:
+				value = irq_a_.request();
+				logger.error().append("IRQ A request is %02x", value);
+			return true;
+			case 0x3200018 & AddressMask:
+				value = irq_a_.mask;
+				logger.error().append("IRQ A mask is %02x", value);
+			return true;
+
+			// IRQ B.
+			case 0x3200020 & AddressMask:
+				value = irq_b_.status;
+//				logger.error().append("IRQ B status is %02x", value);
+			return true;
+			case 0x3200024 & AddressMask:
+				value = irq_b_.request();
+				logger.error().append("IRQ B request is %02x", value);
+			return true;
+			case 0x3200028 & AddressMask:
+				value = irq_b_.mask;
+				logger.error().append("IRQ B mask is %02x", value);
+			return true;
+
+			// FIQ.
+			case 0x3200030 & AddressMask:
+				value = fiq_.status;
+				logger.error().append("FIQ status is %02x", value);
+			return true;
+			case 0x3200034 & AddressMask:
+				value = fiq_.request();
+				logger.error().append("FIQ request is %02x", value);
+			return true;
+			case 0x3200038 & AddressMask:
+				value = fiq_.mask;
+				logger.error().append("FIQ mask is %02x", value);
+			return true;
+
+			// Counters.
+			case 0x3200040 & AddressMask:
+			case 0x3200050 & AddressMask:
+			case 0x3200060 & AddressMask:
+			case 0x3200070 & AddressMask:
+				value = counters_[(target >> 4) - 0x4].output & 0xff;
+//				logger.error().append("%02x: Counter %d low is %02x", target, (target >> 4) - 0x4, value);
+			return true;
+
+			case 0x3200044 & AddressMask:
+			case 0x3200054 & AddressMask:
+			case 0x3200064 & AddressMask:
+			case 0x3200074 & AddressMask:
+				value = counters_[(target >> 4) - 0x4].output >> 8;
+//				logger.error().append("%02x: Counter %d high is %02x", target, (target >> 4) - 0x4, value);
+			return true;
+		}
+
+		return true;
+	}
+
+	bool write(uint32_t address, uint8_t value) {
+		const auto target = address & AddressMask;
+		switch(target) {
+			default:
+				logger.error().append("Unrecognised IOC write of %02x at %08x", value, address);
+			break;
+
+			case 0x320'0000 & AddressMask:
+				// TODO: does the rest of the control register relate to anything?
+//				logger.error().append("TODO: IOC control write: C:%d D:%d", !(value & 2), !(value & 1));
+
+				control_ = value;
+				i2c_.set_clock_data(!(value & 2), !(value & 1));
+			return true;
+
+			case 0x320'0004 & AddressMask:
+				logger.error().append("IOC keyboard transmit %02x", value);
+				serial_.output(IOCParty, value);
+				irq_b_.clear(IRQB::KeyboardTransmitEmpty);
+			return true;
+
+			case 0x320'0014 & AddressMask:
+				// b2: clear IF.
+				// b3: clear IR.
+				// b4: clear POR.
+				// b5: clear TM[0].
+				// b6: clear TM[1].
+				irq_a_.clear(value & 0x7c);
+			return true;
+
+			// Interrupts.
+			case 0x320'0018 & AddressMask:	irq_a_.mask = value;	return true;
+			case 0x320'0028 & AddressMask:	irq_b_.mask = value;	return true;
+			case 0x320'0038 & AddressMask:	fiq_.mask = value;		return true;
+
+			// Counters.
+			case 0x320'0040 & AddressMask:
+			case 0x320'0050 & AddressMask:
+			case 0x320'0060 & AddressMask:
+			case 0x320'0070 & AddressMask:
+				counters_[(target >> 4) - 0x4].reload = uint16_t(
+					(counters_[(target >> 4) - 0x4].reload & 0xff00) | value
+				);
+			return true;
+
+			case 0x320'0044 & AddressMask:
+			case 0x320'0054 & AddressMask:
+			case 0x320'0064 & AddressMask:
+			case 0x320'0074 & AddressMask:
+				counters_[(target >> 4) - 0x4].reload = uint16_t(
+					(counters_[(target >> 4) - 0x4].reload & 0x00ff) | (value << 8)
+				);
+			return true;
+
+			case 0x320'0048 & AddressMask:
+			case 0x320'0058 & AddressMask:
+			case 0x320'0068 & AddressMask:
+			case 0x320'0078 & AddressMask:
+				counters_[(target >> 4) - 0x4].value = counters_[(target >> 4) - 0x4].reload;
+			return true;
+
+			case 0x320'004c & AddressMask:
+			case 0x320'005c & AddressMask:
+			case 0x320'006c & AddressMask:
+			case 0x320'007c & AddressMask:
+				counters_[(target >> 4) - 0x4].output = counters_[(target >> 4) - 0x4].value;
+			return true;
+
+			case 0x327'0000 & AddressMask:
+				logger.error().append("TODO: exteded external podule space");
+			return true;
+
+			case 0x331'0000 & AddressMask:
+				logger.error().append("TODO: 1772 / disk write");
+			return true;
+
+			case 0x335'0000 & AddressMask:
+				logger.error().append("TODO: LS374 / printer data write");
+			return true;
+
+			case 0x335'0018 & AddressMask:
+				logger.error().append("TODO: latch B write: %02x", value);
+			return true;
+
+			case 0x335'0040 & AddressMask:
+				logger.error().append("TODO: latch A write: %02x", value);
+			return true;
+
+			case 0x335'0048 & AddressMask:
+				logger.error().append("TODO: latch C write: %02x", value);
+			return true;
+
+			case 0x336'0000 & AddressMask:
+				logger.error().append("TODO: podule interrupt request");
+			return true;
+
+			case 0x336'0004 & AddressMask:
+				logger.error().append("TODO: podule interrupt mask");
+			return true;
+
+			case 0x33a'0000 & AddressMask:
+				logger.error().append("TODO: 6854 / econet write");
+			return true;
+
+			case 0x33b'0000 & AddressMask:
+				logger.error().append("TODO: 6551 / serial line write");
+			return true;
+		}
+
+		return true;
+	}
+
+	InputOutputController(InterruptObserverT &observer) :
+		observer_(observer),
+		keyboard_(serial_),
+		sound_(*this)
+	{
+		irq_a_.status = IRQA::SetAlways | IRQA::PowerOnReset;
+		irq_b_.status = 0x00;
+		fiq_.status = 0x80;				// 'set always'.
+
+		i2c_.add_peripheral(&cmos_, 0xa0);
+		update_sound_interrupt();
+	}
+
+	Sound<InputOutputController> &sound() {
+		return sound_;
+	}
+
+	void update_sound_interrupt() {
+		if(sound_.interrupt()) {
+			irq_b_.set(IRQB::SoundBufferPointerUsed);
+		} else {
+			irq_b_.clear(IRQB::SoundBufferPointerUsed);
+		}
+		observer_.update_interrupts();
+	}
+
+private:
+	Log::Logger<Log::Source::ARMIOC> logger;
+	InterruptObserverT &observer_;
+
+	// IRQA, IRQB and FIQ states.
+	struct Interrupt {
+		uint8_t status, mask;
+		uint8_t request() const {
+			return status & mask;
+		}
+		bool set(uint8_t value) {
+			status |= value;
+			return status & mask;
+		}
+		void clear(uint8_t bits) {
+			status &= ~bits;
+		}
+	};
+	Interrupt irq_a_, irq_b_, fiq_;
+
+	// The IOCs four counters.
+	struct Counter {
+		uint16_t value;
+		uint16_t reload;
+		uint16_t output;
+	};
+	Counter counters_[4];
+
+	// The KART and keyboard beyond it.
+	HalfDuplexSerial serial_;
+	Keyboard keyboard_;
+
+	// The control register.
+	uint8_t control_ = 0xff;
+
+	// The I2C bus.
+	I2C::Bus i2c_;
+	CMOSRAM cmos_;
+
+	// Audio.
+	Sound<InputOutputController> sound_;
+};
+
+}
+
diff --git a/Machines/Acorn/Archimedes/Keyboard.hpp b/Machines/Acorn/Archimedes/Keyboard.hpp
new file mode 100644
index 000000000..5140e772b
--- /dev/null
+++ b/Machines/Acorn/Archimedes/Keyboard.hpp
@@ -0,0 +1,59 @@
+//
+//  Keyboard.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 20/03/2024.
+//  Copyright © 2024 Thomas Harte. All rights reserved.
+//
+
+#pragma once
+
+#include "HalfDuplexSerial.hpp"
+
+namespace Archimedes {
+
+// Resource for the keyboard protocol: https://github.com/tmk/tmk_keyboard/wiki/ACORN-ARCHIMEDES-Keyboard
+struct Keyboard {
+	Keyboard(HalfDuplexSerial &serial) : serial_(serial) {}
+
+	void update() {
+		if(serial_.events(KeyboardParty) & HalfDuplexSerial::Receive) {
+			const uint8_t input = serial_.input(KeyboardParty);
+			switch(input) {
+				case HRST:
+					// TODO:
+				case RAK1:
+				case RAK2:
+					serial_.output(KeyboardParty, input);
+				break;
+
+				case RQID:
+					serial_.output(KeyboardParty, 0x81);	// TODO: what keyboard type?
+				break;
+
+				default:
+					printf("Keyboard declines to respond to %02x\n", input);
+				break;
+			}
+		}
+	}
+
+private:
+	HalfDuplexSerial &serial_;
+
+	static constexpr uint8_t HRST	= 0b1111'1111;	// Keyboard reset.
+	static constexpr uint8_t RAK1	= 0b1111'1110;	// Reset response #1.
+	static constexpr uint8_t RAK2	= 0b1111'1101;	// Reset response #2.
+
+	static constexpr uint8_t RQID	= 0b0010'0000;	// Request for keyboard ID.
+	static constexpr uint8_t RQMP	= 0b0010'0010;	// Request for mouse data.
+
+	static constexpr uint8_t BACK	= 0b0011'1111;	// Acknowledge for first keyboard data byte pair.
+	static constexpr uint8_t NACK	= 0b0011'0000;	// Acknowledge for last keyboard data byte pair, selects scan/mouse mode.
+	static constexpr uint8_t SACK	= 0b0011'0001;	// Last data byte acknowledge.
+	static constexpr uint8_t MACK	= 0b0011'0010;	// Last data byte acknowledge.
+	static constexpr uint8_t SMAK	= 0b0011'0011;	// Last data byte acknowledge.
+	static constexpr uint8_t PRST	= 0b0010'0001;	// Does nothing.
+};
+
+}
diff --git a/Machines/Acorn/Archimedes/MemoryController.hpp b/Machines/Acorn/Archimedes/MemoryController.hpp
new file mode 100644
index 000000000..ff200b8f5
--- /dev/null
+++ b/Machines/Acorn/Archimedes/MemoryController.hpp
@@ -0,0 +1,467 @@
+//
+//  MemoryController.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 20/03/2024.
+//  Copyright © 2024 Thomas Harte. All rights reserved.
+//
+
+#pragma once
+
+#include "InputOutputController.hpp"
+#include "Video.hpp"
+
+#include "../../../InstructionSets/ARM/Registers.hpp"
+#include "../../../Outputs/Log.hpp"
+
+namespace Archimedes {
+
+/// Provides the mask with all bits set in the range [start, end], where start must  be >= end.
+template <int start, int end> struct BitMask {
+	static_assert(start >= end);
+	static constexpr uint32_t value = ((1 << (start + 1)) - 1) - ((1 << end) - 1);
+};
+static_assert(BitMask<0, 0>::value == 1);
+static_assert(BitMask<1, 1>::value == 2);
+static_assert(BitMask<15, 15>::value == 32768);
+static_assert(BitMask<15, 0>::value == 0xffff);
+static_assert(BitMask<15, 14>::value == 49152);
+
+/// Models the MEMC, making this the Archimedes bus. Owns various other chips on the bus as a result.
+template <typename InterruptObserverT>
+struct MemoryController {
+	MemoryController(InterruptObserverT &delegate) : ioc_(delegate) {}
+
+	int interrupt_mask() const {
+		return ioc_.interrupt_mask();
+	}
+
+	void set_rom(const std::vector<uint8_t> &rom) {
+		std::copy(
+			rom.begin(),
+			rom.begin() + static_cast<ptrdiff_t>(std::min(rom.size(), rom_.size())),
+			rom_.begin());
+	}
+
+	template <typename IntT>
+	uint32_t aligned(uint32_t address) {
+		if constexpr (std::is_same_v<IntT, uint32_t>) {
+			return address & static_cast<uint32_t>(~3);
+		}
+		return address;
+	}
+
+	template <typename IntT>
+	bool write(uint32_t address, IntT source, InstructionSet::ARM::Mode mode, bool) {
+		// User mode may only _write_ to logically-mapped RAM (subject to further testing below).
+		if(mode == InstructionSet::ARM::Mode::User && address >= 0x200'0000) {
+			return false;
+		}
+
+		switch(write_zones_[(address >> 21) & 31]) {
+			case Zone::DMAAndMEMC: {
+				const auto buffer_address = [](uint32_t source) -> uint32_t {
+					return (source & 0x1fffc0) << 2;
+				};
+
+				// The MEMC itself isn't on the data bus; all values below should be taken from `address`.
+				switch((address >> 17) & 0b111) {
+					case 0b000:
+						logger.error().append("TODO: DMA/MEMC Vinit = %04x", address & 0x1fffc0);
+					return true;
+
+					case 0b001:
+						logger.error().append("TODO: DMA/MEMC Vstart = %04x", address & 0x1fffc0);
+					return true;
+
+					case 0b010:
+						logger.error().append("TODO: DMA/MEMC Vend = %04x", address & 0x1fffc0);
+					return true;
+
+					case 0b011:
+						logger.error().append("TODO: DMA/MEMC Cinit = %04x", address & 0x1fffc0);
+					return true;
+
+					case 0b100:	ioc_.sound().set_next_start(buffer_address(address));	return true;
+					case 0b101:	ioc_.sound().set_next_end(buffer_address(address));		return true;
+					case 0b110:	ioc_.sound().swap();									return true;
+
+					case 0b111:
+						os_mode_ = address & (1 << 12);
+						audio_dma_enable_ = address & (1 << 11);
+						video_dma_enable_ = address & (1 << 10);
+						switch((address >> 8) & 3) {
+							default:
+								dynamic_ram_refresh_ = DynamicRAMRefresh::None;
+							break;
+							case 0b01:
+							case 0b11:
+								dynamic_ram_refresh_ = DynamicRAMRefresh((address >> 8) & 3);
+							break;
+						}
+						high_rom_access_time_ = ROMAccessTime((address >> 6) & 3);
+						low_rom_access_time_ = ROMAccessTime((address >> 4) & 3);
+						page_size_ = PageSize((address >> 2) & 3);
+
+						logger.info().append("MEMC Control: %08x -> OS:%d audio:%d video:%d refresh:%d high:%d low:%d size:%d", address, os_mode_, audio_dma_enable_, video_dma_enable_, dynamic_ram_refresh_, high_rom_access_time_, low_rom_access_time_, page_size_);
+						map_dirty_ = true;
+					return true;
+				}
+			} break;
+
+			case Zone::LogicallyMappedRAM: {
+				const auto item = logical_ram<IntT, false>(address, mode);
+				if(!item) {
+					return false;
+				}
+				*item = source;
+				return true;
+			} break;
+
+			case Zone::IOControllers:
+				// TODO: have I overrestricted the value type for the IOC area?
+				ioc_.write(address, uint8_t(source));
+			return true;
+
+			case Zone::VideoController:
+				// TODO: handle byte writes correctly.
+				vidc_.write(source);
+			break;
+
+			case Zone::PhysicallyMappedRAM:
+				physical_ram<IntT>(address) = source;
+			return true;
+
+			case Zone::AddressTranslator:
+//				printf("Translator write at %08x; replaces %08x\n", address, pages_[address & 0x7f]);
+				pages_[address & 0x7f] = address;
+				map_dirty_ = true;
+			break;
+
+			default:
+//				printf("TODO: write of %08x to %08x [%lu]\n", source, address, sizeof(IntT));
+			break;
+		}
+
+		return true;
+	}
+
+	template <typename IntT>
+	bool read(uint32_t address, IntT &source, InstructionSet::ARM::Mode mode, bool) {
+		// User mode may only read logically-maped RAM and ROM.
+		if(mode == InstructionSet::ARM::Mode::User && address >= 0x200'0000 && address < 0x380'0000) {
+			return false;
+		}
+
+		switch (read_zones_[(address >> 21) & 31]) {
+			case Zone::PhysicallyMappedRAM:
+				source = physical_ram<IntT>(address);
+			return true;
+
+			case Zone::LogicallyMappedRAM: {
+				if(!has_moved_rom_) {	// TODO: maintain this state in the zones table.
+					source = high_rom<IntT>(address);
+					return true;
+				}
+
+				const auto item = logical_ram<IntT, true>(address, mode);
+				if(!item) {
+					return false;
+				}
+				source = *item;
+				return true;
+			} break;
+
+			case Zone::LowROM:
+//				logger.error().append("TODO: Low ROM read from %08x", address);
+				source = IntT(~0);
+			return true;
+
+			case Zone::HighROM:
+				// Real test is: require A24=A25=0, then A25=1.
+				// TODO: as above, move this test into the zones tables.
+				has_moved_rom_ = true;
+				source = high_rom<IntT>(address);
+			return true;
+
+			case Zone::IOControllers:	{
+				if constexpr (std::is_same_v<IntT, uint8_t>) {
+					ioc_.read(address, source);
+					return true;
+				} else {
+					// TODO: generalise this adaptation of an 8-bit device to the 32-bit bus, which probably isn't right anyway.
+					uint8_t value;
+					ioc_.read(address, value);
+					source = value;
+					return true;
+				}
+			}
+
+			default:
+				logger.error().append("TODO: read from %08x", address);
+			break;
+		}
+
+		source = 0;
+		return false;
+	}
+
+	void tick_timers() {	ioc_.tick_timers();		}
+	void tick_audio() {
+		// TODO: does disabling audio DMA pause output, or leave it ticking and merely
+		// stop allowing it to use the bus?
+		ioc_.sound().tick();
+	}
+
+	private:
+		Log::Logger<Log::Source::ARMIOC> logger;
+
+		enum class Zone {
+			LogicallyMappedRAM,
+			PhysicallyMappedRAM,
+			IOControllers,
+			LowROM,
+			HighROM,
+			VideoController,
+			DMAAndMEMC,
+			AddressTranslator,
+		};
+		static std::array<Zone, 0x20> zones(bool is_read) {
+			std::array<Zone, 0x20> zones{};
+			for(size_t c = 0; c < zones.size(); c++) {
+				const auto address = c << 21;
+				if(address < 0x200'0000) {
+					zones[c] = Zone::LogicallyMappedRAM;
+				} else if(address < 0x300'0000) {
+					zones[c] = Zone::PhysicallyMappedRAM;
+				} else if(address < 0x340'0000) {
+					zones[c] = Zone::IOControllers;
+				} else if(address < 0x360'0000) {
+					zones[c] = is_read ? Zone::LowROM : Zone::VideoController;
+				} else if(address < 0x380'0000) {
+					zones[c] = is_read ? Zone::LowROM : Zone::DMAAndMEMC;
+				} else {
+					zones[c] = is_read ? Zone::HighROM : Zone::AddressTranslator;
+				}
+			}
+			return zones;
+		}
+
+		bool has_moved_rom_ = false;
+		std::array<uint8_t, 4*1024*1024> ram_{};
+		std::array<uint8_t, 2*1024*1024> rom_;
+		InputOutputController<InterruptObserverT> ioc_;
+		Video vidc_;
+
+		template <typename IntT>
+		IntT &physical_ram(uint32_t address) {
+			address = aligned<IntT>(address);
+			address &= (ram_.size() - 1);
+			return *reinterpret_cast<IntT *>(&ram_[address]);
+		}
+
+		template <typename IntT>
+		IntT &high_rom(uint32_t address) {
+			address = aligned<IntT>(address);
+			return *reinterpret_cast<IntT *>(&rom_[address & (rom_.size() - 1)]);
+		}
+
+		const std::array<Zone, 0x20> read_zones_ = zones(true);
+		const std::array<Zone, 0x20> write_zones_ = zones(false);
+
+		// Control register values.
+		bool os_mode_ = false;
+		bool audio_dma_enable_ = false;
+		bool video_dma_enable_ = false;	// "Unaffected" by reset, so here picked arbitrarily.
+
+		enum class DynamicRAMRefresh {
+			None = 0b00,
+			DuringFlyback = 0b01,
+			Continuous = 0b11,
+		} dynamic_ram_refresh_ = DynamicRAMRefresh::None;	// State at reset is undefined; constrain to a valid enum value.
+
+		enum class ROMAccessTime {
+			ns450 = 0b00,
+			ns325 = 0b01,
+			ns200 = 0b10,
+			ns200with60nsNibble = 0b11,
+		} high_rom_access_time_ = ROMAccessTime::ns450, low_rom_access_time_ = ROMAccessTime::ns450;
+
+		enum class PageSize {
+			kb4 = 0b00,
+			kb8 = 0b01,
+			kb16 = 0b10,
+			kb32 = 0b11,
+		} page_size_ = PageSize::kb4;
+
+		// Address translator.
+		//
+		// MEMC contains one entry per a physical page number, indicating where it goes logically.
+		// Any logical access is tested against all 128 mappings. So that's backwards compared to
+		// the ideal for an emulator, which would map from logical to physical, even if a lot more
+		// compact — there are always 128 physical pages; there are up to 8192 logical pages.
+		//
+		// So captured here are both the physical -> logical map as representative of the real
+		// hardware, and the reverse logical -> physical map, which is built (and rebuilt, and rebuilt)
+		// from the other.
+
+		// Physical to logical mapping.
+		std::array<uint32_t, 128> pages_{};
+
+		// Logical to physical mapping.
+		struct MappedPage {
+			uint8_t *target = nullptr;
+			uint8_t protection_level = 0;
+		};
+		std::array<MappedPage, 8192> mapping_;
+		bool map_dirty_ = true;
+
+		template <typename IntT, bool is_read>
+		IntT *logical_ram(uint32_t address, InstructionSet::ARM::Mode mode) {
+			// Possibly TODO: this recompute-if-dirty flag is supposed to ameliorate for an expensive
+			// mapping process. It can be eliminated when the process is improved.
+			if(map_dirty_) {
+				update_mapping();
+				map_dirty_ = false;
+			}
+
+			address = aligned<IntT>(address);
+			address &= 0x1ff'ffff;
+			size_t page;
+
+			// TODO: eliminate switch here.
+			switch(page_size_) {
+				default:
+				case PageSize::kb4:
+					page = address >> 12;
+					address &= 0x0fff;
+				break;
+				case PageSize::kb8:
+					page = address >> 13;
+					address &= 0x1fff;
+				break;
+				case PageSize::kb16:
+					page = address >> 14;
+					address &= 0x3fff;
+				break;
+				case PageSize::kb32:
+					page = address >> 15;
+					address &= 0x7fff;
+				break;
+			}
+
+			if(!mapping_[page].target) {
+				return nullptr;
+			}
+
+			// TODO: eliminate switch here.
+			// Top of my head idea: is_read, is_user and is_os_mode make three bits, so
+			// keep a one-byte bitmap of permitted accesses rather than the raw protection
+			// level?
+			switch(mapping_[page].protection_level) {
+				case 0b00:	break;
+				case 0b01:
+					if(!is_read && mode == InstructionSet::ARM::Mode::User) {
+						return nullptr;
+					}
+				break;
+				default:
+					if(mode == InstructionSet::ARM::Mode::User) {
+						return nullptr;
+					}
+					if(!is_read && !os_mode_) {
+						return nullptr;
+					}
+				break;
+			}
+
+			return reinterpret_cast<IntT *>(mapping_[page].target + address);
+		}
+
+		void update_mapping() {
+			// For each physical page, project it into logical space.
+			switch(page_size_) {
+				default:
+				case PageSize::kb4:		update_mapping<PageSize::kb4>();	break;
+				case PageSize::kb8:		update_mapping<PageSize::kb8>();	break;
+				case PageSize::kb16:	update_mapping<PageSize::kb16>();	break;
+				case PageSize::kb32:	update_mapping<PageSize::kb32>();	break;
+			}
+		}
+
+		template <PageSize size>
+		void update_mapping() {
+			// Clear all logical mappings.
+			std::fill(mapping_.begin(), mapping_.end(), MappedPage{});
+
+			// For each physical page, project it into logical space
+			// and store it.
+			for(const auto page: pages_) {
+				uint32_t physical, logical;
+
+				switch(size) {
+					case PageSize::kb4:
+						// 4kb:
+						//		A[6:0] -> PPN[6:0]
+						//		A[11:10] -> LPN[12:11];	A[22:12] -> LPN[10:0]	i.e. 8192 logical pages
+						physical = page & BitMask<6, 0>::value;
+
+						physical <<= 12;
+
+						logical = (page & BitMask<11, 10>::value) << 1;
+						logical |= (page & BitMask<22, 12>::value) >> 12;
+					break;
+
+					case PageSize::kb8:
+						// 8kb:
+						//		A[0] -> PPN[6]; A[6:1] -> PPN[5:0]
+						//		A[11:10] -> LPN[11:10];	A[22:13] -> LPN[9:0]	i.e. 4096 logical pages
+						physical = (page & BitMask<0, 0>::value) << 6;
+						physical |= (page & BitMask<6, 1>::value) >> 1;
+
+						physical <<= 13;
+
+						logical = page & BitMask<11, 10>::value;
+						logical |= (page & BitMask<22, 13>::value) >> 13;
+					break;
+
+					case PageSize::kb16:
+						// 16kb:
+						//		A[1:0] -> PPN[6:5]; A[6:2] -> PPN[4:0]
+						//		A[11:10] -> LPN[10:9];	A[22:14] -> LPN[8:0]	i.e. 2048 logical pages
+						physical = (page & BitMask<1, 0>::value) << 5;
+						physical |= (page & BitMask<6, 2>::value) >> 2;
+
+						physical <<= 14;
+
+						logical = (page & BitMask<11, 10>::value) >> 1;
+						logical |= (page & BitMask<22, 14>::value) >> 14;
+					break;
+
+					case PageSize::kb32:
+						// 32kb:
+						//		A[1] -> PPN[6]; A[2] -> PPN[5]; A[0] -> PPN[4]; A[6:3] -> PPN[3:0]
+						//		A[11:10] -> LPN[9:8];	A[22:15] -> LPN[7:0]	i.e. 1024 logical pages
+						physical = (page & BitMask<1, 1>::value) << 5;
+						physical |= (page & BitMask<2, 2>::value) << 3;
+						physical |= (page & BitMask<0, 0>::value) << 4;
+						physical |= (page & BitMask<6, 3>::value) >> 3;
+
+						physical <<= 15;
+
+						logical = (page & BitMask<11, 10>::value) >> 2;
+						logical |= (page & BitMask<22, 15>::value) >> 15;
+					break;
+				}
+
+//				printf("%08x => physical %d -> logical %d\n", page, (physical >> 15), logical);
+
+				// TODO: consider clashes.
+				// TODO: what if there's less than 4mb present?
+				mapping_[logical].target = &ram_[physical];
+				mapping_[logical].protection_level = (page >> 8) & 3;
+			}
+		}
+};
+
+}
diff --git a/Machines/Acorn/Archimedes/Sound.hpp b/Machines/Acorn/Archimedes/Sound.hpp
new file mode 100644
index 000000000..aad9bd1f0
--- /dev/null
+++ b/Machines/Acorn/Archimedes/Sound.hpp
@@ -0,0 +1,69 @@
+//
+//  Audio.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 20/03/2024.
+//  Copyright © 2024 Thomas Harte. All rights reserved.
+//
+
+#pragma once
+
+namespace Archimedes {
+
+template <typename InterruptObserverT>
+struct Sound {
+	Sound(InterruptObserverT &observer) : observer_(observer) {}
+
+	void set_next_end(uint32_t value) {
+		next_.end = value;
+	}
+
+	void set_next_start(uint32_t value) {
+		next_.start = value;
+		set_buffer_valid(true);	// My guess: this is triggered on next buffer start write.
+	}
+
+	bool interrupt() const {
+		return !next_buffer_valid_;
+	}
+
+	void swap() {
+		current_.start = next_.start;
+		std::swap(current_.end, next_.end);
+		set_buffer_valid(false);
+		halted_ = false;
+	}
+
+	void tick() {
+		if(halted_) {
+			return;
+		}
+
+		current_.start += 16;
+		if(current_.start == current_.end) {
+			if(next_buffer_valid_) {
+				swap();
+			} else {
+				halted_ = true;
+			}
+		}
+	}
+
+private:
+	void set_buffer_valid(bool valid) {
+		next_buffer_valid_ = valid;
+		observer_.update_sound_interrupt();
+	}
+
+	bool next_buffer_valid_ = false;
+	bool halted_ = true;				// This is a bit of a guess.
+
+	struct Buffer {
+		uint32_t start = 0, end = 0;
+	};
+	Buffer current_, next_;
+
+	InterruptObserverT &observer_;
+};
+
+}
diff --git a/Machines/Acorn/Archimedes/Video.hpp b/Machines/Acorn/Archimedes/Video.hpp
new file mode 100644
index 000000000..3a69d0c03
--- /dev/null
+++ b/Machines/Acorn/Archimedes/Video.hpp
@@ -0,0 +1,110 @@
+//
+//  Video.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 20/03/2024.
+//  Copyright © 2024 Thomas Harte. All rights reserved.
+//
+
+#pragma once
+
+#include "../../../Outputs/Log.hpp"
+
+#include <cstdint>
+
+namespace Archimedes {
+
+struct Video {
+	void write(uint32_t value) {
+		const auto target = (value >> 24) & 0xfc;
+
+		switch(target) {
+			case 0x00:	case 0x04:	case 0x08:	case 0x0c:
+			case 0x10:	case 0x14:	case 0x18:	case 0x1c:
+			case 0x20:	case 0x24:	case 0x28:	case 0x2c:
+			case 0x30:	case 0x34:	case 0x38:	case 0x3c:
+				logger.error().append("TODO: Video palette logical colour %d to %03x", (target >> 2), value & 0x1fff);
+			break;
+
+			case 0x40:
+				logger.error().append("TODO: Video border colour to %03x", value & 0x1fff);
+			break;
+
+			case 0x44:	case 0x48:	case 0x4c:
+				logger.error().append("TODO: Cursor colour %d to %03x", (target - 0x44) >> 2, value & 0x1fff);
+			break;
+
+			case 0x60:	case 0x64:	case 0x68:	case 0x6c:
+			case 0x70:	case 0x74:	case 0x78:	case 0x7c:
+				logger.error().append("TODO: Stereo image register %d to %03x", (target - 0x60) >> 2, value & 0x7);
+			break;
+
+			case 0x80:
+				logger.error().append("TODO: Video horizontal period: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0x84:
+				logger.error().append("TODO: Video horizontal sync width: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0x88:
+				logger.error().append("TODO: Video horizontal border start: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0x8c:
+				logger.error().append("TODO: Video horizontal display start: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0x90:
+				logger.error().append("TODO: Video horizontal display end: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0x94:
+				logger.error().append("TODO: Video horizontal border end: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0x98:
+				logger.error().append("TODO: Video horizontal cursor end: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0x9c:
+				logger.error().append("TODO: Video horizontal interlace: %d", (value >> 14) & 0x3ff);
+			break;
+
+			case 0xa0:
+				logger.error().append("TODO: Video vertical period: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0xa4:
+				logger.error().append("TODO: Video vertical sync width: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0xa8:
+				logger.error().append("TODO: Video vertical border start: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0xac:
+				logger.error().append("TODO: Video vertical display start: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0xb0:
+				logger.error().append("TODO: Video vertical display end: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0xb4:
+				logger.error().append("TODO: Video vertical border end: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0xb8:
+				logger.error().append("TODO: Video vertical cursor start: %d", (value >> 14) & 0x3ff);
+			break;
+			case 0xbc:
+				logger.error().append("TODO: Video vertical cursor end: %d", (value >> 14) & 0x3ff);
+			break;
+
+			case 0xc0:
+				logger.error().append("TODO: Sound frequency: %d", value & 0x7f);
+			break;
+
+			case 0xe0:
+				logger.error().append("TODO: video control: %08x", value);
+			break;
+
+			default:
+				logger.error().append("TODO: unrecognised VIDC write of %08x", value);
+			break;
+		}
+	}
+
+private:
+	Log::Logger<Log::Source::ARMIOC> logger;
+};
+
+}
diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index f74c8fc91..54a36d96d 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -1808,6 +1808,13 @@
 		4BA9C3CF1D8164A9002DDB61 /* MediaTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MediaTarget.hpp; sourceTree = "<group>"; };
 		4BAA167B21582B1D008A3276 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = "<group>"; };
 		4BAB1E522BA9D9950002C9B9 /* Disassembler.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Disassembler.hpp; sourceTree = "<group>"; };
+		4BAB1E532BAB5B040002C9B9 /* Sound.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Sound.hpp; sourceTree = "<group>"; };
+		4BAB1E542BAB5B3F0002C9B9 /* Keyboard.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Keyboard.hpp; sourceTree = "<group>"; };
+		4BAB1E552BAB5B6D0002C9B9 /* HalfDuplexSerial.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = HalfDuplexSerial.hpp; sourceTree = "<group>"; };
+		4BAB1E562BAB5BC60002C9B9 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = "<group>"; };
+		4BAB1E582BAB5C210002C9B9 /* MemoryController.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MemoryController.hpp; sourceTree = "<group>"; };
+		4BAB1E592BAB5CB90002C9B9 /* CMOSRAM.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CMOSRAM.hpp; sourceTree = "<group>"; };
+		4BAB1E5A2BAB5F400002C9B9 /* InputOutputController.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = InputOutputController.hpp; sourceTree = "<group>"; };
 		4BAB62AC1D3272D200DF5BA0 /* Disk.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Disk.hpp; sourceTree = "<group>"; };
 		4BAB62AE1D32730D00DF5BA0 /* Storage.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Storage.hpp; sourceTree = "<group>"; };
 		4BAF2B4C2004580C00480230 /* DMK.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DMK.cpp; sourceTree = "<group>"; };
@@ -4394,6 +4401,13 @@
 			children = (
 				4BB505842B9634F30031C43C /* Archimedes.cpp */,
 				4BB505852B9634F30031C43C /* Archimedes.hpp */,
+				4BAB1E592BAB5CB90002C9B9 /* CMOSRAM.hpp */,
+				4BAB1E552BAB5B6D0002C9B9 /* HalfDuplexSerial.hpp */,
+				4BAB1E5A2BAB5F400002C9B9 /* InputOutputController.hpp */,
+				4BAB1E542BAB5B3F0002C9B9 /* Keyboard.hpp */,
+				4BAB1E582BAB5C210002C9B9 /* MemoryController.hpp */,
+				4BAB1E532BAB5B040002C9B9 /* Sound.hpp */,
+				4BAB1E562BAB5BC60002C9B9 /* Video.hpp */,
 			);
 			path = Archimedes;
 			sourceTree = "<group>";
diff --git a/Outputs/Log.hpp b/Outputs/Log.hpp
index a0827d4fa..22b02f8d0 100644
--- a/Outputs/Log.hpp
+++ b/Outputs/Log.hpp
@@ -25,6 +25,9 @@ enum class Source {
 	AmigaBlitter,
 	AppleIISCSICard,
 	Archimedes,
+	ARMIOC,
+	ARMMEMC,
+	ARMVIDC,
 	AtariST,
 	AtariSTDMAController,
 	CommodoreStaticAnalyser,
@@ -90,6 +93,9 @@ constexpr const char *prefix(Source source) {
 		case Source::AmigaDisk:					return "Disk";
 		case Source::AppleIISCSICard:			return "SCSI card";
 		case Source::Archimedes:				return "Archimedes";
+		case Source::ARMIOC:					return "IOC";
+		case Source::ARMMEMC:					return "MEMC";
+		case Source::ARMVIDC:					return "VIDC";
 		case Source::AtariST:					return "AtariST";
 		case Source::AtariSTDMAController:		return "DMA";
 		case Source::CommodoreStaticAnalyser:	return "Commodore Static Analyser";