diff --git a/CMakeLists.txt b/CMakeLists.txt
index 32b5926..e20e176 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -19,7 +19,7 @@ add_executable(
   main.cpp
   memory.cpp
   rk05.cpp
-  rx02.cpp
+  rl02.cpp
   terminal.cpp
   tests.cpp
   tm-11.cpp
diff --git a/bus.cpp b/bus.cpp
index e08e8c8..4730419 100644
--- a/bus.cpp
+++ b/bus.cpp
@@ -1,4 +1,4 @@
-// (C) 2018 by Folkert van Heusden
+// (C) 2018-2022 by Folkert van Heusden
 // Released under Apache License v2.0
 #include <assert.h>
 #include <stdio.h>
@@ -19,6 +19,7 @@ constexpr int n_pages = 12;
 constexpr int n_pages = 16;
 #endif
 
+
 bus::bus()
 {
 	m = new memory(n_pages * 8192);
@@ -33,7 +34,7 @@ bus::~bus()
 	delete c;
 	delete tm11;
 	delete rk05_;
-	delete rx02_;
+	delete rl02_;
 	delete tty_;
 	delete m;
 }
@@ -280,6 +281,9 @@ uint16_t bus::read(const uint16_t a, const bool word_mode, const bool use_prev,
 		if (rk05_ && a >= RK05_BASE && a < RK05_END)
 			return word_mode ? rk05_ -> readByte(a) : rk05_ -> readWord(a);
 
+		if (rl02_ && a >= RL02_BASE && a < RL02_END)
+			return word_mode ? rl02_ -> readByte(a) : rl02_ -> readWord(a);
+
 		if (tty_ && a >= PDP11TTY_BASE && a < PDP11TTY_END)
 			return word_mode ? tty_ -> readByte(a) : tty_ -> readWord(a);
 
@@ -546,6 +550,11 @@ uint16_t bus::write(const uint16_t a, const bool word_mode, uint16_t value, cons
 			return value;
 		}
 
+		if (rl02_ && a >= RL02_BASE && a < RL02_END) {
+			word_mode ? rl02_ -> writeByte(a, value) : rl02_ -> writeWord(a, value);
+			return value;
+		}
+
 		if (tty_ && a >= PDP11TTY_BASE && a < PDP11TTY_END) {
 			word_mode ? tty_ -> writeByte(a, value) : tty_ -> writeWord(a, value);
 			return value;
diff --git a/bus.h b/bus.h
index 0ce7138..a1c1260 100644
--- a/bus.h
+++ b/bus.h
@@ -7,7 +7,7 @@
 
 #include "tm-11.h"
 #include "rk05.h"
-#include "rx02.h"
+#include "rl02.h"
 
 class cpu;
 class memory;
@@ -24,7 +24,7 @@ private:
 	cpu     *c     { nullptr };
 	tm_11   *tm11  { nullptr };
 	rk05    *rk05_ { nullptr };
-	rx02    *rx02_ { nullptr };
+	rl02    *rl02_ { nullptr };
 	tty     *tty_  { nullptr };
 
 	memory  *m     { nullptr };
@@ -51,7 +51,7 @@ public:
 	void add_cpu(cpu *const c) { this -> c = c; }
 	void add_tm11(tm_11 *tm11) { this -> tm11 = tm11; } 
 	void add_rk05(rk05 *rk05_) { this -> rk05_ = rk05_; } 
-	void add_rx02(rx02 *rx02_) { this -> rx02_ = rx02_; }
+	void add_rl02(rl02 *rl02_) { this -> rl02_ = rl02_; }
 	void add_tty(tty *tty_) { this -> tty_ = tty_; }
 
 	cpu *getCpu() { return this->c; }
diff --git a/main.cpp b/main.cpp
index 508aa7a..5408588 100644
--- a/main.cpp
+++ b/main.cpp
@@ -21,6 +21,8 @@
 #include "utils.h"
 
 
+typedef enum { BL_NONE, BL_RK05, BL_RL02 } bootloader_t;
+
 bool              withUI       { false };
 uint32_t          event        { 0 };
 std::atomic_bool  terminate    { false };
@@ -37,32 +39,85 @@ void loadbin(bus *const b, uint16_t base, const char *const file)
 	fclose(fh);
 }
 
-void setBootLoader(bus *const b)
+void setBootLoader(bus *const b, const bootloader_t which)
 {
-	cpu *const c = b -> getCpu();
+	cpu *const c      = b -> getCpu();
 
-	const uint16_t offset = 01000;
-	constexpr uint16_t bootrom[] = {
-		0012700,
-		0177406,
-		0012710,
-		0177400,
-		0012740,
-		0000005,
-		0105710,
-		0100376,
-		0005007
-	};
+	uint16_t         offset = 0;
+	const uint16_t  *bl     = nullptr;
+	int              size   = 0;
 
-	FILE *fh = fopen("boot.dat", "wb");
+	if (which == BL_RK05) {
+		offset = 01000;
 
-	for(size_t i=0; i<sizeof bootrom / 2; i++) {
-		b -> writeWord(offset + i * 2, bootrom[i]);
-		fputc(bootrom[i] & 255, fh);
-		fputc(bootrom[i] >> 8, fh);
+		static uint16_t rk05_code[] = {
+			0012700,
+			0177406,
+			0012710,
+			0177400,
+			0012740,
+			0000005,
+			0105710,
+			0100376,
+			0005007
+		};
+
+		bl = rk05_code;
+
+		size = 9;
+	}
+	else if (which == BL_RL02) {
+		offset = 01000;
+
+		/* from https://www.pdp-11.nl/peripherals/disk/rl-info.html
+		static uint16_t rl02_code[] = {
+			0012701,
+			0174400,
+			0012761,
+			0000013,
+			0000004,
+			0012711,
+			0000004,
+			0105711,
+			0100376,
+			0005061,
+			0000002,
+			0005061,
+			0000004,
+			0012761,
+			0177400,
+			0000006,
+			0012711,
+			0000014,
+			0105711,
+			0100376,
+			0005007
+		};
+
+		size = 21;
+		*/
+
+		// from http://gunkies.org/wiki/RL11_disk_controller
+		static uint16_t rl02_code[] = {
+			0012700,
+			0174400,
+			0012760,
+			0177400,
+			0000006,
+			0012710,
+			0000014,
+			0105710,
+			0100376,
+			0005007,
+		};
+
+		size = 10;
+
+		bl = rl02_code;
 	}
 
-	fclose(fh);
+	for(int i=0; i<size; i++)
+		b -> writeWord(offset + i * 2, bl[i]);
 
 	c -> setRegister(7, offset);
 }
@@ -130,6 +185,44 @@ uint16_t loadTape(bus *const b, const char *const file)
 	return start;
 }
 
+void load_p11_x11(bus *const b, const std::string & file)
+{
+	FILE *fh = fopen(file.c_str(), "rb");
+	if (!fh)
+		error_exit(true, "Cannot open %s", file.c_str());
+
+	uint16_t addr    = 0;
+	int      n = 0;
+
+	while(!feof(fh)) {
+		char buffer[4096];
+
+		if (!fgets(buffer, sizeof buffer, fh))
+			break;
+
+		if (n) {
+			uint8_t byte = strtol(buffer, NULL, 16);
+
+			b->writeByte(addr, byte);
+
+			n--;
+
+			addr++;
+		}
+		else {
+			std::vector<std::string> parts = split(buffer, " ");
+
+			addr = strtol(parts[0].c_str(), NULL, 16);
+			n    = strtol(parts[1].c_str(), NULL, 16);
+		}
+	}
+
+	fclose(fh);
+
+	cpu *const c      = b -> getCpu();
+	c -> setRegister(7, 0);
+}
+
 volatile bool sw = false;
 void sw_handler(int s)
 {
@@ -150,7 +243,7 @@ void help()
 	printf("-R d.rk  load file as a RK05 disk device\n");
 	printf("-p 123   set CPU start pointer to decimal(!) value\n");
 	printf("-L f.bin load file into memory at address given by -p (and run it)\n");
-	printf("-b       enable bootloader (build-in)\n");
+	printf("-b x     enable bootloader (build-in), parameter must be \"rk05\" or \"rl02\"\n");
 	printf("-n       ncurses UI\n");
 	printf("-d       enable debugger\n");
 	printf("-t       enable tracing (disassemble to stderr, requires -d as well)\n");
@@ -170,12 +263,16 @@ int main(int argc, char *argv[])
 	c -> setEmulateMFPT(true);
 
 	std::vector<std::string> rk05_files;
+	std::vector<std::string> rl02_files;
+
 	bool testCases    = false;
 	bool run_debugger = false;
 	bool tracing      = false;
-	bool bootloader   = false;
+
+	bootloader_t  bootloader = BL_NONE;
+
 	int  opt          = -1;
-	while((opt = getopt(argc, argv, "hm:T:R:p:ndtL:b")) != -1)
+	while((opt = getopt(argc, argv, "hm:T:r:R:p:ndtL:b:")) != -1)
 	{
 		switch(opt) {
 			case 'h':
@@ -183,7 +280,13 @@ int main(int argc, char *argv[])
 				return 1;
 
 			case 'b':
-				bootloader   = true;
+				if (strcasecmp(optarg, "rk05") == 0)
+					bootloader = BL_RK05;
+				else if (strcasecmp(optarg, "rl02") == 0)
+					bootloader = BL_RL02;
+				else
+					error_exit(false, "Bootload \"%s\" not recognized", optarg);
+
 				break;
 
 			case 'd':
@@ -216,6 +319,10 @@ int main(int argc, char *argv[])
 				rk05_files.push_back(optarg);
 				break;
 
+			case 'r':
+				rl02_files.push_back(optarg);
+				break;
+
 			case 'p':
 				c->setRegister(7, atoi(optarg));
 				break;
@@ -247,8 +354,11 @@ int main(int argc, char *argv[])
 	if (rk05_files.empty() == false)
 		b->add_rk05(new rk05(rk05_files, b, cnsl->get_disk_read_activity_flag(), cnsl->get_disk_write_activity_flag()));
 
-	if (bootloader)
-		setBootLoader(b);
+	if (rl02_files.empty() == false)
+		b->add_rl02(new rl02(rl02_files, b, cnsl->get_disk_read_activity_flag(), cnsl->get_disk_write_activity_flag()));
+
+	if (bootloader != BL_NONE)
+		setBootLoader(b, bootloader);
 
 	running = cnsl->get_running_flag();
 
@@ -275,6 +385,8 @@ int main(int argc, char *argv[])
 //	loadbin(b, 0, "test.dat");
 //	c->setRegister(7, 0);
 
+//load_p11_x11(b, "/home/folkert/Projects/PDP-11/work/p11-2.10i/Tests/mtpi.x11");
+
 	cnsl->start_thread();
 
 	if (run_debugger)
@@ -306,5 +418,7 @@ int main(int argc, char *argv[])
 
 	delete b;
 
+	delete lf;
+
 	return 0;
 }
diff --git a/rk05.cpp b/rk05.cpp
index eb30ba5..cb76b86 100644
--- a/rk05.cpp
+++ b/rk05.cpp
@@ -11,7 +11,7 @@
 #include "utils.h"
 
 
-const char * const regnames[] = { 
+static const char * const regnames[] = { 
 	"RK05_DS drivestatus",
 	"RK05_ERROR        ",
 	"RK05_CS ctrlstatus",
diff --git a/rl02.cpp b/rl02.cpp
new file mode 100644
index 0000000..9048057
--- /dev/null
+++ b/rl02.cpp
@@ -0,0 +1,218 @@
+// (C) 2022 by Folkert van Heusden
+// Released under Apache License v2.0
+#include <errno.h>
+#include <string.h>
+
+#include "bus.h"
+#include "cpu.h"
+#include "error.h"
+#include "gen.h"
+#include "rl02.h"
+#include "utils.h"
+
+
+constexpr int sectors_per_track = 40;
+constexpr int bytes_per_sector  = 256;
+
+static const char * const regnames[] = { 
+	"control status",
+	"bus address   ",
+	"disk address  ",
+	"multipurpose  "
+	};
+
+rl02::rl02(const std::vector<std::string> & files, bus *const b, std::atomic_bool *const disk_read_acitivity, std::atomic_bool *const disk_write_acitivity) :
+	b(b),
+	disk_read_acitivity(disk_read_acitivity),
+	disk_write_acitivity(disk_write_acitivity)
+{
+	memset(registers, 0x00, sizeof registers);
+	memset(xfer_buffer, 0x00, sizeof xfer_buffer);
+
+	for(auto file : files) {
+#if defined(ESP32)
+		File32 *fh = new File32();
+
+		if (!fh->open(file.c_str(), O_RDWR)) {
+			delete fh;
+			error_exit(true, "rl02: cannot open \"%s\"", file.c_str());
+		}
+#else
+		FILE *fh = fopen(file.c_str(), "rb");
+		if (!fh)
+			error_exit(true, "rl02: cannot open \"%s\"", file.c_str());
+#endif
+
+		fhs.push_back(fh);
+	}
+}
+
+rl02::~rl02()
+{
+	for(auto fh : fhs) {
+#if defined(ESP32)
+		fh->close();
+		delete fh;
+#else
+		fclose(fh);
+#endif
+	}
+}
+
+uint8_t rl02::readByte(const uint16_t addr)
+{
+	uint16_t v = readWord(addr & ~1);
+
+	if (addr & 1)
+		return v >> 8;
+
+	return v;
+}
+
+uint16_t rl02::readWord(const uint16_t addr)
+{
+	const int reg = (addr - RL02_BASE) / 2;
+
+	if (addr == RL02_CSR) {  // control status
+		setBit(registers[reg], 0, true);  // drive ready (DRDY)
+		setBit(registers[reg], 7, true);  // controller ready (CRDY)
+	}
+
+	uint16_t value = registers[reg];
+
+	// TODO
+
+	D(fprintf(stderr, "RL02 read %s/%o: %06o\n", regnames[reg], addr, value);)
+
+	return value;
+}
+
+void rl02::writeByte(const uint16_t addr, const uint8_t v)
+{
+	uint16_t vtemp = registers[(addr - RL02_BASE) / 2];
+
+	if (addr & 1) {
+		vtemp &= ~0xff00;
+		vtemp |= v << 8;
+	}
+	else {
+		vtemp &= ~0x00ff;
+		vtemp |= v;
+	}
+
+	writeWord(addr, vtemp);
+}
+
+uint32_t rl02::calcOffset(const uint16_t da)
+{
+	int           sector            = da & 63;
+	int           track             = (da >> 6) & 1023;
+
+	uint32_t      offset            = (sectors_per_track * track + sector) * bytes_per_sector;
+
+	return offset;
+}
+
+void rl02::writeWord(const uint16_t addr, uint16_t v)
+{
+#if defined(ESP32)
+	digitalWrite(LED_BUILTIN, LOW);
+#endif
+
+	fprintf(stderr, "RL02 write %06o: %06o\n", addr, v);
+
+	const int reg = (addr - RL02_BASE) / 2;
+
+        registers[reg] = v;
+
+	if (addr == RL02_CSR) {  // control status
+		const uint8_t command = (v >> 1) & 7;
+
+		const bool    do_exec = !(v & 128);
+
+		fprintf(stderr, "RL02 set command %d, exec: %d\n", command, do_exec);
+
+		uint32_t disk_offset = calcOffset(registers[(RL02_DAR - RL02_BASE) / 2] & ~1);
+		int      device      = 0;  // TODO
+
+		if (command == 2) {  // get status
+			registers[(RL02_MPR - RL02_BASE) / 2] = 0;
+#if 0
+			if (registers[(RL02_DAR - RL02_BASE) / 2] & 2) {  // get status -> load status word in MPR
+				registers[(RL02_MPR - RL02_BASE) / 2] = 5 |  // lock on
+					(1 << 3) |  // brushes are home
+					(1 << 7) |  // this is an RL02
+					0;
+
+				fprintf(stderr, "RL02 set MPR for status to %06o\n", registers[(RL02_MPR - RL02_BASE) / 2]);
+			}
+#endif
+		}
+		else if (command == 6 || command == 7) {  // read data / read data without header check
+			*disk_read_acitivity = true;
+
+			bool proceed = true;
+
+#if defined(ESP32)
+			File32 *fh = fhs.at(device);
+
+			if (!fh->seek(disk_offset)) {
+				fprintf(stderr, "RL02 seek to %d error %s\n", disk_offset, strerror(errno));
+
+				proceed = false;
+			}
+#else
+			FILE *fh = nullptr;
+
+			if (size_t(device) >= fhs.size())
+				proceed = false;
+			else {
+				fh = fhs.at(device);
+
+				if (fseek(fh, disk_offset, SEEK_SET) == -1) {
+					fprintf(stderr, "RL02 seek error %s\n", strerror(errno));
+					proceed = false;
+				}
+			}
+#endif
+
+			uint32_t memory_address = registers[(RL02_BAR - RL02_BASE) / 2];
+
+			uint32_t count          = (65536 - registers[(RL02_MPR - RL02_BASE) / 2]) * 2;
+
+			fprintf(stderr, "RL02 read %d bytes (dec) from %d (dec) to %06o (oct)\n", count, disk_offset, memory_address);
+
+			uint32_t p    = memory_address;
+			while(proceed && count > 0) {
+				uint32_t cur = std::min(uint32_t(sizeof xfer_buffer), count);
+
+#if defined(ESP32)
+				yield();
+
+				if (fh->read(xfer_buffer, cur) != size_t(cur))
+					D(fprintf(stderr, "RL02 fread error: %s\n", strerror(errno));)
+#else
+				if (fread(xfer_buffer, 1, cur, fh) != size_t(cur))
+					D(fprintf(stderr, "RL02 fread error: %s\n", strerror(errno));)
+#endif
+
+				for(uint32_t i=0; i<cur; i++, p++)
+					b->writeByte(p, xfer_buffer[i]);
+
+				count -= cur;
+			}
+
+			if (registers[(RL02_CSR - RL02_BASE) / 2] & 64) {  // interrupt enable?
+				fprintf(stderr, "RL02 triggering interrupt\n");
+
+				b->getCpu()->queue_interrupt(5, 0254);
+			}
+
+			*disk_read_acitivity = false;
+		}
+	}
+
+#if defined(ESP32)
+	digitalWrite(LED_BUILTIN, HIGH);
+#endif
+}
diff --git a/rl02.h b/rl02.h
new file mode 100644
index 0000000..d0da43e
--- /dev/null
+++ b/rl02.h
@@ -0,0 +1,51 @@
+// (C) 2022 by Folkert van Heusden
+// Released under Apache License v2.0
+#pragma once
+
+#include <atomic>
+#include <stdint.h>
+#include <stdio.h>
+#include <string>
+#include <vector>
+
+#if defined(ESP32)
+#include "esp32.h"
+#endif
+
+#define RL02_CSR 0174400  // control status register
+#define RL02_BAR 0174402  // bus address register
+#define RL02_DAR 0174404  // disk address register
+#define RL02_MPR 0174406  // multi purpose register
+#define RL02_BASE  RL02_CSR
+#define RL02_END  (RL02_MPR + 2)
+
+class bus;
+
+class rl02
+{
+private:
+	bus      *const b;
+	uint16_t        registers[4];
+	uint8_t         xfer_buffer[512];
+
+#if defined(ESP32)
+	std::vector<File32 *> fhs;
+#else
+	std::vector<FILE *> fhs;
+#endif
+
+	std::atomic_bool *const disk_read_acitivity  { nullptr };
+	std::atomic_bool *const disk_write_acitivity { nullptr };
+
+	uint32_t calcOffset(uint16_t);
+
+public:
+	rl02(const std::vector<std::string> & files, bus *const b, std::atomic_bool *const disk_read_acitivity, std::atomic_bool *const disk_write_acitivity);
+	virtual ~rl02();
+
+	uint8_t  readByte(const uint16_t addr);
+	uint16_t readWord(const uint16_t addr);
+
+	void writeByte(const uint16_t addr, const uint8_t  v);
+	void writeWord(const uint16_t addr, const uint16_t v);
+};
diff --git a/rx02.cpp b/rx02.cpp
deleted file mode 100644
index 6b5767f..0000000
--- a/rx02.cpp
+++ /dev/null
@@ -1,72 +0,0 @@
-// (C) 2018 by Folkert van Heusden
-// Released under Apache License v2.0
-#include <errno.h>
-#include <string.h>
-
-#include "rx02.h"
-#include "gen.h"
-#include "memory.h"
-#include "utils.h"
-
-rx02::rx02(const std::string & file, memory *const m) : m(m)
-{
-	offset = 0;
-	memset(registers, 0x00, sizeof registers);
-
-	fh = fopen(file.c_str(), "rb");
-}
-
-rx02::~rx02()
-{
-	fclose(fh);
-}
-
-uint8_t rx02::readByte(const uint16_t addr)
-{
-	uint16_t v = readWord(addr & ~1);
-
-	if (addr & 1)
-		return v >> 8;
-
-	return v;
-}
-
-uint16_t rx02::readWord(const uint16_t addr)
-{
-	const int reg = (addr - RX02_BASE) / 2;
-	uint16_t vtemp = registers[reg];
-
-	D(printf("RX02 read addr %o: ", addr);)
-
-	// FIXME
-
-	D(printf("%o\n", vtemp);)
-
-	return vtemp;
-}
-
-void rx02::writeByte(const uint16_t addr, const uint8_t v)
-{
-	uint16_t vtemp = registers[(addr - RX02_BASE) / 2];
-
-	if (addr & 1) {
-		vtemp &= ~0xff00;
-		vtemp |= v << 8;
-	}
-	else {
-		vtemp &= ~0x00ff;
-		vtemp |= v;
-	}
-
-	writeWord(addr, vtemp);
-}
-
-void rx02::writeWord(const uint16_t addr, uint16_t v)
-{
-	D(printf("RX02 write %o: %o\n", addr, v);)
-
-	// FIXME
-
-	D(printf("set register %o to %o\n", addr, v);)
-	registers[(addr - RX02_BASE) / 2] = v;
-}
diff --git a/rx02.h b/rx02.h
deleted file mode 100644
index 36146db..0000000
--- a/rx02.h
+++ /dev/null
@@ -1,33 +0,0 @@
-// (C) 2018 by Folkert van Heusden
-// Released under Apache License v2.0
-#pragma once
-
-#include <stdint.h>
-#include <stdio.h>
-#include <string>
-
-// FIXME namen van defines
-#define RX02_BASE	0
-#define RX02_END	(1 + 2)
-
-class memory;
-
-class rx02
-{
-private:
-	memory *const m;
-	uint16_t registers[7];
-	uint8_t xfer_buffer[512];
-	int offset;
-	FILE *fh;
-
-public:
-	rx02(const std::string & file, memory *const m);
-	virtual ~rx02();
-
-	uint8_t readByte(const uint16_t addr);
-	uint16_t readWord(const uint16_t addr);
-
-	void writeByte(const uint16_t addr, const uint8_t v);
-	void writeWord(const uint16_t addr, uint16_t v);
-};