diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp
index c7e2578bc..d0d91bbdf 100644
--- a/Analyser/Static/StaticAnalyser.cpp
+++ b/Analyser/Static/StaticAnalyser.cpp
@@ -53,6 +53,7 @@
 #include "../../Storage/Disk/DiskImage/Formats/MSA.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/NIB.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp"
+#include "../../Storage/Disk/DiskImage/Formats/PCBooter.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/SSD.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/STX.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/WOZ.hpp"
@@ -179,9 +180,10 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
 			TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric | TargetPlatform::ZXSpectrum)
 			// HFE (TODO: switch to AllDisk once the MSX stops being so greedy)
 	Format("ima", result.disks, Disk::DiskImageHolder<Storage::Disk::FAT12>, TargetPlatform::PCCompatible)			// IMG (Enterprise/MS-DOS style)
-	Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)		// IMG (DiskCopy 4.2)
 	Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)	// IMG (DiskCopy 4.2)
+	Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)		// IMG (DiskCopy 4.2)
 	Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::FAT12>, TargetPlatform::Enterprise)			// IMG (Enterprise/MS-DOS style)
+	Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::PCBooter>, TargetPlatform::PCCompatible)		// IMG (PC raw booter)
 	Format(	"ipf",
 			result.disks,
 			Disk::DiskImageHolder<Storage::Disk::IPF>,
diff --git a/Machines/PCCompatible/DMA.hpp b/Machines/PCCompatible/DMA.hpp
index d31942752..3e9795bbc 100644
--- a/Machines/PCCompatible/DMA.hpp
+++ b/Machines/PCCompatible/DMA.hpp
@@ -136,9 +136,9 @@ class i8237 {
 		///
 		/// @returns Either a 16-bit address or @c NotAvailable if the requested channel isn't set up to perform a read or write at present.
 		uint32_t access(size_t channel, bool is_write) {
-			if(channels_[channel].transfer_complete) {
-				return NotAvailable;
-			}
+//			if(channels_[channel].transfer_complete) {
+//				return NotAvailable;
+//			}
 			if(is_write && channels_[channel].transfer != Channel::Transfer::Write) {
 				return NotAvailable;
 			}
diff --git a/Machines/PCCompatible/PCCompatible.cpp b/Machines/PCCompatible/PCCompatible.cpp
index 8bc233219..bf4d0842d 100644
--- a/Machines/PCCompatible/PCCompatible.cpp
+++ b/Machines/PCCompatible/PCCompatible.cpp
@@ -134,7 +134,7 @@ class FloppyController {
 					break;
 
 					case Command::ReadData: {
-						printf("FDC: Read %d:%d at %d/%d\n", decoder_.target().drive, decoder_.target().head, decoder_.geometry().cylinder, decoder_.geometry().head);
+						printf("FDC: Read %d:%d at %d/%d/%d\n", decoder_.target().drive, decoder_.target().head, decoder_.geometry().cylinder, decoder_.geometry().head, decoder_.geometry().sector);
 //						log = true;
 
 						status_.begin(decoder_);
@@ -142,7 +142,7 @@ class FloppyController {
 						// Search for a matching sector.
 						const auto target = decoder_.geometry();
 						bool found_sector = false;
-						for(auto &pair: drives_[decoder_.target().drive].cached_track) {
+						for(auto &pair: drives_[decoder_.target().drive].sectors(decoder_.target().head)) {
 							if(
 								(pair.second.address.track == target.cylinder) &&
 								(pair.second.address.sector == target.sector) &&
@@ -154,6 +154,7 @@ class FloppyController {
 
 								for(int c = 0; c < 128 << target.size; c++) {
 									if(!dma_.write(2, pair.second.samples[0].data()[c])) {
+										printf("FDC: DMA not permitted\n");
 										wrote_in_full = false;
 										break;
 									}
@@ -167,6 +168,7 @@ class FloppyController {
 										decoder_.geometry().sector,
 										decoder_.geometry().size);
 								} else {
+									printf("FDC: didn't write in full\n");
 									// TODO: Overrun, presumably?
 								}
 
@@ -175,6 +177,7 @@ class FloppyController {
 						}
 
 						if(!found_sector) {
+							printf("FDC: sector not found\n");
 							// TODO: there's more than this, I think.
 							status_.set(Intel::i8272::Status0::AbnormalTermination);
 							results_.serialise(
@@ -193,8 +196,6 @@ class FloppyController {
 					case Command::Seek:
 						printf("FDC: Seek %d:%d to %d\n", decoder_.target().drive, decoder_.target().head, decoder_.seek_target());
 						drives_[decoder_.target().drive].track = decoder_.seek_target();
-						drives_[decoder_.target().drive].side = decoder_.target().head;
-						drives_[decoder_.target().drive].cache_track();
 
 						drives_[decoder_.target().drive].raised_interrupt = true;
 						drives_[decoder_.target().drive].status = decoder_.drive_head() | uint8_t(Intel::i8272::Status0::SeekEnded);
@@ -203,7 +204,6 @@ class FloppyController {
 					case Command::Recalibrate:
 						printf("FDC: Recalibrate\n");
 						drives_[decoder_.target().drive].track = 0;
-						drives_[decoder_.target().drive].cache_track();
 
 						drives_[decoder_.target().drive].raised_interrupt = true;
 						drives_[decoder_.target().drive].status = decoder_.target().drive | uint8_t(Intel::i8272::Status0::SeekEnded);
@@ -316,38 +316,54 @@ class FloppyController {
 
 		Intel::i8272::CommandDecoder::SpecifySpecs specify_specs_;
 		struct DriveStatus {
-			bool raised_interrupt = false;
-			uint8_t status = 0;
-			uint8_t track = 0;
-			bool side = false;
-			bool motor = false;
-			bool exists = true;
+			public:
+				bool raised_interrupt = false;
+				uint8_t status = 0;
+				uint8_t track = 0;
+				bool motor = false;
+				bool exists = true;
 
-			std::shared_ptr<Storage::Disk::Disk> disk;
-			Storage::Encodings::MFM::SectorMap cached_track;
-			void cache_track() {
-				if(!disk) {
-					return;
-				}
-				cached_track.clear();
+				std::shared_ptr<Storage::Disk::Disk> disk;
 
-				auto raw_track = disk->get_track_at_position(
-					Storage::Disk::Track::Address(
-						side,
-						Storage::Disk::HeadPosition(track)
-					)
-				);
-				if(!raw_track) {
-					return;
+				Storage::Encodings::MFM::SectorMap &sectors(bool side) {
+					if(cached.track == track && cached.side == side) {
+						return cached.sectors;
+					}
+
+					cached.track = track;
+					cached.side = side;
+					cached.sectors.clear();
+
+					if(!disk) {
+						return cached.sectors;
+					}
+
+					auto raw_track = disk->get_track_at_position(
+						Storage::Disk::Track::Address(
+							side,
+							Storage::Disk::HeadPosition(track)
+						)
+					);
+					if(!raw_track) {
+						return cached.sectors;
+					}
+
+					const bool is_double_density = true;	// TODO: use MFM flag here.
+					auto serialisation = Storage::Disk::track_serialisation(
+						*raw_track,
+						is_double_density ? Storage::Encodings::MFM::MFMBitLength : Storage::Encodings::MFM::FMBitLength
+					);
+					cached.sectors = Storage::Encodings::MFM::sectors_from_segment(std::move(serialisation), is_double_density);
+					return cached.sectors;
 				}
 
-				const bool is_double_density = true;	// TODO: use MFM flag here.
-				auto serialisation = Storage::Disk::track_serialisation(
-					*raw_track,
-					is_double_density ? Storage::Encodings::MFM::MFMBitLength : Storage::Encodings::MFM::FMBitLength
-				);
-				cached_track = Storage::Encodings::MFM::sectors_from_segment(std::move(serialisation), is_double_density);
-			}
+			private:
+				struct {
+					uint8_t track = 0xff;
+					bool side;
+					Storage::Encodings::MFM::SectorMap sectors;
+				} cached;
+
 		} drives_[4];
 
 		static std::string drive_name(int c) {
@@ -696,7 +712,7 @@ class i8255PortHandler : public Intel::i8255::PortHandler {
 			switch(port) {
 				case 0:
 //					printf("PPI: from keyboard\n");
-					return enable_keyboard_ ? keyboard_.read() : 0b0011'1100;
+					return enable_keyboard_ ? keyboard_.read() : 0b0011'1101;
 						// Guesses that switches is high and low combined as below.
 
 				case 2:
@@ -715,7 +731,7 @@ class i8255PortHandler : public Intel::i8255::PortHandler {
 							// b3, b2: RAM on motherboard (64 * bit pattern)
 							// b1: 1 => FPU present; 0 => absent;
 							// b0: 1 => floppy drive present; 0 => absent.
-							0b0000'1100;
+							0b0000'1101;
 			}
 			return 0;
 		};
@@ -1104,13 +1120,25 @@ class ConcreteMachine:
 					context.flow_controller.begin_instruction();
 				}
 
-//				if(log) {
-//					const auto next = to_string(decoded, InstructionSet::x86::Model::i8086);
+/*				if(decoded_ip_ >= 0x7c00 && decoded_ip_ < 0x7c00 + 1024) {
+					const auto next = to_string(decoded, InstructionSet::x86::Model::i8086);
 //					if(next != previous) {
-//						std::cout << decoded_ip_ << " " << next << std::endl;
+						std::cout << std::hex << decoded_ip_ << " " << next;
+
+						if(decoded.second.operation() == InstructionSet::x86::Operation::INT) {
+							std::cout << " dl:" << std::hex << +context.registers.dl() << "; ";
+							std::cout << "ah:" << std::hex << +context.registers.ah() << "; ";
+							std::cout << "ch:" << std::hex << +context.registers.ch() << "; ";
+							std::cout << "cl:" << std::hex << +context.registers.cl() << "; ";
+							std::cout << "dh:" << std::hex << +context.registers.dh() << "; ";
+							std::cout << "es:" << std::hex << +context.registers.es() << "; ";
+							std::cout << "bx:" << std::hex << +context.registers.bx();
+						}
+
+						std::cout << std::endl;
 //						previous = next;
 //					}
-//				}
+				}*/
 
 				// Execute it.
 				InstructionSet::x86::perform(
diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index 95bd725c0..b77825539 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -9,6 +9,8 @@
 /* Begin PBXBuildFile section */
 		423820112B17CBC800964EFE /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 423820102B17CBC800964EFE /* StaticAnalyser.cpp */; };
 		423820122B17CBC800964EFE /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 423820102B17CBC800964EFE /* StaticAnalyser.cpp */; };
+		423820442B1A90BE00964EFE /* PCBooter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 423820422B1A90BE00964EFE /* PCBooter.cpp */; };
+		423820452B1A90BE00964EFE /* PCBooter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 423820422B1A90BE00964EFE /* PCBooter.cpp */; };
 		423BDC4A2AB24699008E37B6 /* 8088Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 423BDC492AB24699008E37B6 /* 8088Tests.mm */; };
 		42437B332AC70833006DFED1 /* HDV.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B6FD0342923061300EC4760 /* HDV.cpp */; };
 		425739382B051EA800B7D1E4 /* PCCompatible.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 425739372B051EA800B7D1E4 /* PCCompatible.cpp */; };
@@ -1136,6 +1138,8 @@
 		423820132B1A235200964EFE /* Memory.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Memory.hpp; sourceTree = "<group>"; };
 		423820142B1A23C200964EFE /* Registers.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Registers.hpp; sourceTree = "<group>"; };
 		423820152B1A23E100964EFE /* Segments.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Segments.hpp; sourceTree = "<group>"; };
+		423820422B1A90BE00964EFE /* PCBooter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PCBooter.cpp; sourceTree = "<group>"; };
+		423820432B1A90BE00964EFE /* PCBooter.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PCBooter.hpp; sourceTree = "<group>"; };
 		423BDC492AB24699008E37B6 /* 8088Tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = 8088Tests.mm; sourceTree = "<group>"; };
 		42437B342ACF02A9006DFED1 /* Flags.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Flags.hpp; sourceTree = "<group>"; };
 		42437B352ACF0AA2006DFED1 /* Perform.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Perform.hpp; sourceTree = "<group>"; };
@@ -3047,6 +3051,7 @@
 				4BC131782346DF2B00E4FF3D /* MSA.cpp */,
 				4B0F94FC208C1A1600FE41D9 /* NIB.cpp */,
 				4B4518971F75FD1B00926311 /* OricMFMDSK.cpp */,
+				423820422B1A90BE00964EFE /* PCBooter.cpp */,
 				4B4518991F75FD1B00926311 /* SSD.cpp */,
 				4B7BA03323C58B1E00B98D9E /* STX.cpp */,
 				4B6ED2EE208E2F8A0047B343 /* WOZ.cpp */,
@@ -3066,6 +3071,7 @@
 				4BC131792346DF2B00E4FF3D /* MSA.hpp */,
 				4B0F94FD208C1A1600FE41D9 /* NIB.hpp */,
 				4B4518981F75FD1B00926311 /* OricMFMDSK.hpp */,
+				423820432B1A90BE00964EFE /* PCBooter.hpp */,
 				4B45189A1F75FD1B00926311 /* SSD.hpp */,
 				4B7BA03223C58B1E00B98D9E /* STX.hpp */,
 				4B6ED2EF208E2F8A0047B343 /* WOZ.hpp */,
@@ -5822,6 +5828,7 @@
 				4B055A9C1FAE85DA0060FFFF /* CPCDSK.cpp in Sources */,
 				4B2BF19223DCC6A800C3AD60 /* STX.cpp in Sources */,
 				4B0ACC2723775819008902D0 /* AtariST.cpp in Sources */,
+				423820452B1A90BE00964EFE /* PCBooter.cpp in Sources */,
 				4B8318B922D3E56D006DB630 /* MemoryPacker.cpp in Sources */,
 				4B055ABA1FAE86170060FFFF /* Commodore.cpp in Sources */,
 				4B0ACC2F23775819008902D0 /* TIA.cpp in Sources */,
@@ -6122,6 +6129,7 @@
 				4B051CA826781D6500CA44E8 /* StaticAnalyser.cpp in Sources */,
 				4B69FB3D1C4D908A00B5F0AA /* Tape.cpp in Sources */,
 				4B4518841F75E91A00926311 /* UnformattedTrack.cpp in Sources */,
+				423820442B1A90BE00964EFE /* PCBooter.cpp in Sources */,
 				4B65086022F4CF8D009C1100 /* Keyboard.cpp in Sources */,
 				4B47770B268FBE4D005C2340 /* FAT.cpp in Sources */,
 				4B894528201967B4007DE474 /* Disk.cpp in Sources */,
diff --git a/Storage/Disk/DiskImage/Formats/FAT12.hpp b/Storage/Disk/DiskImage/Formats/FAT12.hpp
index b3a91ffa0..66e9692c8 100644
--- a/Storage/Disk/DiskImage/Formats/FAT12.hpp
+++ b/Storage/Disk/DiskImage/Formats/FAT12.hpp
@@ -6,8 +6,8 @@
 //  Copyright 2018 Thomas Harte. All rights reserved.
 //
 
-#ifndef MSXDSK_hpp
-#define MSXDSK_hpp
+#ifndef FAT12_hpp
+#define FAT12_hpp
 
 #include "MFMSectorDump.hpp"
 
@@ -36,4 +36,4 @@ class FAT12: public MFMSectorDump {
 
 }
 
-#endif /* MSXDSK_hpp */
+#endif /* FAT12_hpp */
diff --git a/Storage/Disk/DiskImage/Formats/PCBooter.cpp b/Storage/Disk/DiskImage/Formats/PCBooter.cpp
new file mode 100644
index 000000000..617ec16c8
--- /dev/null
+++ b/Storage/Disk/DiskImage/Formats/PCBooter.cpp
@@ -0,0 +1,61 @@
+//
+//  FAT12.cpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 07/01/2018.
+//  Copyright 2018 Thomas Harte. All rights reserved.
+//
+
+#include "PCBooter.hpp"
+
+#include "Utility/ImplicitSectors.hpp"
+
+using namespace Storage::Disk;
+
+PCBooter::PCBooter(const std::string &file_name) :
+	MFMSectorDump(file_name) {
+	// The only sanity check here is whether a sensible
+	// geometry is encoded in the first sector, or can be guessed.
+	const auto file_size = file_.stats().st_size;
+
+	// Check that file size is one of the accepted options.
+	switch(file_size) {
+		default: throw Error::InvalidFormat;
+
+		case 512 * 8 * 40:
+			head_count_ = 1;
+			track_count_ = 40;
+			sector_count_ = 8;
+		break;
+
+		case 512 * 9 * 40:
+			head_count_ = 1;
+			track_count_ = 40;
+			sector_count_ = 9;
+		break;
+
+		case 512 * 9 * 40 * 2:
+			head_count_ = 2;
+			track_count_ = 40;
+			sector_count_ = 9;
+		break;
+	}
+
+	set_geometry(sector_count_, 2, 1, true);
+
+	// TODO: check that an appropriate INT or similar is in the boot sector?
+	// Should probably factor out the "does this look like a PC boot sector?" test,
+	// as it can also be used to disambiguate FAT12 disks.
+}
+
+HeadPosition PCBooter::get_maximum_head_position() {
+	return HeadPosition(track_count_);
+}
+
+int PCBooter::get_head_count() {
+	return head_count_;
+}
+
+long PCBooter::get_file_offset_for_position(Track::Address address) {
+	return (address.position.as_int() * head_count_ + address.head) * 512 * sector_count_;
+}
diff --git a/Storage/Disk/DiskImage/Formats/PCBooter.hpp b/Storage/Disk/DiskImage/Formats/PCBooter.hpp
new file mode 100644
index 000000000..1ca5a0e9c
--- /dev/null
+++ b/Storage/Disk/DiskImage/Formats/PCBooter.hpp
@@ -0,0 +1,38 @@
+//
+//  PCBooter.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 07/01/2018.
+//  Copyright 2018 Thomas Harte. All rights reserved.
+//
+
+#ifndef PCBooter_hpp
+#define PCBooter_hpp
+
+#include "MFMSectorDump.hpp"
+
+#include <string>
+
+namespace Storage::Disk {
+
+/*!
+	Provides a @c DiskImage holding a raw IBM PC booter disk image: a sector dump of one of a few fixed sizes
+	with what looks like a meaningful boot sector.
+*/
+class PCBooter: public MFMSectorDump {
+	public:
+		PCBooter(const std::string &file_name);
+		HeadPosition get_maximum_head_position() final;
+		int get_head_count() final;
+
+	private:
+		long get_file_offset_for_position(Track::Address address) final;
+
+		int head_count_;
+		int track_count_;
+		int sector_count_;
+};
+
+}
+
+#endif /* PCBooter_hpp */
diff --git a/Storage/TargetPlatforms.hpp b/Storage/TargetPlatforms.hpp
index eeed8657b..9351d034b 100644
--- a/Storage/TargetPlatforms.hpp
+++ b/Storage/TargetPlatforms.hpp
@@ -39,11 +39,12 @@ enum Type: IntType {
 	ZX81			=	1 << 20,
 	ZXSpectrum		=	1 << 21,
 	PCCompatible	=	1 << 22,
+	FAT12			=	1 << 23,
 
 	Acorn			=	AcornAtom | AcornElectron | BBCMaster | BBCModelA | BBCModelB,
 	ZX8081			=	ZX80 | ZX81,
 	AllCartridge	=	Atari2600 | AcornElectron | Coleco | MSX,
-	AllDisk			=	Acorn | AmstradCPC | Commodore | Oric | MSX | ZXSpectrum | Macintosh | AtariST | DiskII | Amiga | PCCompatible,
+	AllDisk			=	Acorn | AmstradCPC | Commodore | Oric | MSX | ZXSpectrum | Macintosh | AtariST | DiskII | Amiga | PCCompatible | FAT12,
 	AllTape			=	Acorn | AmstradCPC | Commodore | Oric | ZX8081 | MSX | ZXSpectrum,
 };