changeset 2061:7c1760b5b3e5 segacd

Implemented basic TOC functionality of CDD MCU
author Michael Pavone <pavone@retrodev.com>
date Thu, 27 Jan 2022 00:33:41 -0800
parents f1c2415f4d1d
children 07ed42bd7b4c
files Makefile blastem.c cdd_mcu.c cdd_mcu.h cue.c segacd.c segacd.h
diffstat 7 files changed, 652 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Sun Jan 23 11:11:07 2022 -0800
+++ b/Makefile	Thu Jan 27 00:33:41 2022 -0800
@@ -214,11 +214,11 @@
 
 MAINOBJS=blastem.o system.o genesis.o debug.o gdb_remote.o vdp.o $(RENDEROBJS) io.o romdb.o hash.o menu.o xband.o \
 	realtec.o i2c.o nor.o sega_mapper.o multi_game.o megawifi.o $(NET) serialize.o $(TERMINAL) $(CONFIGOBJS) gst.o \
-	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o zip.o bindings.o jcart.o gen_player.o segacd.o lc8951.o cue.o
+	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o zip.o bindings.o jcart.o gen_player.o segacd.o lc8951.o cue.o cdd_mcu.o
 
 LIBOBJS=libblastem.o system.o genesis.o debug.o gdb_remote.o vdp.o io.o romdb.o hash.o xband.o realtec.o \
 	i2c.o nor.o sega_mapper.o multi_game.o megawifi.o $(NET) serialize.o $(TERMINAL) $(CONFIGOBJS) gst.o \
-	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o jcart.o rom.db.o gen_player.o segacd.o lc8951.o $(LIBZOBJS)
+	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o jcart.o rom.db.o gen_player.o segacd.o lc8951.o cue.o cdd.o cdd_mcu.o $(LIBZOBJS)
 
 ifdef NONUKLEAR
 CFLAGS+= -DDISABLE_NUKLEAR
--- a/blastem.c	Sun Jan 23 11:11:07 2022 -0800
+++ b/blastem.c	Thu Jan 27 00:33:41 2022 -0800
@@ -239,7 +239,9 @@
 	romclose(f);
 	if (!strcasecmp(dst->extension, "cue")) {
 		if (parse_cue(dst)) {
-			*stype = SYSTEM_SEGACD;
+			if (stype) {
+				*stype = SYSTEM_SEGACD;
+			}
 		}
 	}
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cdd_mcu.c	Thu Jan 27 00:33:41 2022 -0800
@@ -0,0 +1,403 @@
+#include "cdd_mcu.h"
+#include "backend.h"
+
+#define SCD_MCLKS 50000000
+#define CD_BLOCK_CLKS 16934400
+#define CDD_MCU_DIVIDER 8
+#define SECTOR_CLOCKS (CD_BLOCK_CLKS/75)
+#define NIBBLE_CLOCKS (CDD_MCU_DIVIDER * 77)
+
+//lead in start max diameter 46 mm
+//program area start max diameter 50 mm
+//difference 4 mm = 4000 um
+//radius difference 2 mm = 2000 um
+//track pitch 1.6 um
+//1250 physical tracks in between
+//linear speed 1.2 m/s - 1.4 m/s
+// 1.3 m = 1300 mm
+// circumference at 46 mm ~ 144.51 mm
+// circumference at 50 mm ~ 157.08 mm
+// avg is 150.795
+// 75 sectors per second
+// 17.3333 mm "typical" length of a sector
+// ~8.7 sectors per track in lead-in area
+#define LEADIN_SECTORS 10875
+
+static uint32_t cd_block_to_mclks(uint32_t cycles)
+{
+	return ((uint64_t)cycles) * ((uint64_t)SCD_MCLKS) / ((uint64_t)CD_BLOCK_CLKS);
+}
+
+static uint32_t mclks_to_cd_block(uint32_t cycles)
+{
+	return ((uint64_t)cycles) * ((uint64_t)CD_BLOCK_CLKS) / ((uint64_t)SCD_MCLKS);
+}
+
+void cdd_mcu_init(cdd_mcu *context, system_media *media)
+{
+	context->next_int_cycle = CYCLE_NEVER;
+	context->last_subcode_cycle = CYCLE_NEVER;
+	context->last_nibble_cycle = CYCLE_NEVER;
+	context->requested_format = SF_NOTREADY;
+	context->media = media;
+	context->current_status_nibble = -1;
+	context->current_cmd_nibble = -1;
+}
+
+enum {
+	GAO_CDD_CTRL,
+	GAO_CDD_STATUS,
+	GAO_CDD_CMD = GAO_CDD_STATUS+5
+};
+
+static uint8_t checksum(uint8_t *vbuffer)
+{
+	uint8_t *buffer = vbuffer;
+	uint8_t sum = 0;
+	for (int i = 0; i < 9; i++)
+	{
+		sum += buffer[i];
+	}
+	return (~sum) & 0xF;
+}
+#define SEEK_SPEED 2200 //made up number
+static void handle_seek(cdd_mcu *context)
+{
+	if (context->seeking) {
+		if (context->seek_pba == context->head_pba) {
+			context->seeking = 0;
+		} else if (context->seek_pba > context->head_pba) {
+			context->head_pba += SEEK_SPEED;
+			if (context->head_pba > context->seek_pba) {
+				context->head_pba = context->seek_pba;
+			}
+		} else {
+			context->head_pba -= SEEK_SPEED;
+			if (context->head_pba < context->seek_pba) {
+				context->head_pba = context->seek_pba;
+			}
+		}
+	}
+}
+
+static void lba_to_status(cdd_mcu *context, uint32_t lba)
+{
+	uint32_t seconds = lba / 75;
+	uint32_t frames = lba % 75;
+	uint32_t minutes = seconds / 60;
+	seconds = seconds % 60;
+	context->status_buffer.b.time.min_high = minutes / 10;
+	context->status_buffer.b.time.min_low = minutes % 10;
+	context->status_buffer.b.time.sec_high = seconds / 10;
+	context->status_buffer.b.time.sec_low = seconds % 10;
+	context->status_buffer.b.time.frame_high = frames / 10;
+	context->status_buffer.b.time.frame_low = frames % 10;
+}
+
+static void update_status(cdd_mcu *context)
+{
+	switch (context->status)
+	{
+	case DS_PLAY:
+		handle_seek(context);
+		if (!context->seeking) {
+			context->head_pba++;
+		}
+		break;
+	case DS_PAUSE:
+		handle_seek(context);
+		break;
+	case DS_TOC_READ:
+		handle_seek(context);
+		if (!context->seeking) {
+			context->head_pba++;
+			if (context->media && context->media->type == MEDIA_CDROM && context->media->num_tracks) {
+				if (context->head_pba > 3*context->media->num_tracks + 1) {
+					context->toc_valid = 1;
+					context->seeking = 1;
+					context->seek_pba = LEADIN_SECTORS + context->media->tracks[0].start_lba + context->media->tracks[0].fake_pregap;
+					context->status = DS_PAUSE;
+				}
+
+			} else {
+				context->status = DS_NO_DISC;
+			}
+		}
+		break;
+
+	}
+	switch (context->requested_format)
+	{
+	case SF_ABSOLUTE:
+		if (context->toc_valid) {
+			lba_to_status(context, context->head_pba - LEADIN_SECTORS);
+			context->status_buffer.format = SF_ABSOLUTE;
+		} else {
+			context->status_buffer.format = SF_NOTREADY;
+		}
+		break;
+	case SF_RELATIVE:
+		if (context->toc_valid) {
+			uint32_t lba =context->head_pba - LEADIN_SECTORS;
+			for (uint32_t i = 0; i < context->media->num_tracks; i++)
+			{
+				if (lba < context->media->tracks[i].end_lba) {
+					if (context->media->tracks[i].fake_pregap) {
+						if (lba > context->media->tracks[i].fake_pregap) {
+							lba -= context->media->tracks[i].fake_pregap;
+						} else {
+							//relative time counts down to 0 in pregap
+							lba = context->media->tracks[i].fake_pregap - lba;
+							break;
+						}
+					}
+					if (lba < context->media->tracks[i].start_lba) {
+						//relative time counts down to 0 in pregap
+						lba = context->media->tracks[i].start_lba - lba;
+					} else {
+						lba -= context->media->tracks[i].start_lba;
+					}
+					break;
+				} else if (context->media->tracks[i].fake_pregap) {
+					lba -= context->media->tracks[i].fake_pregap;
+				}
+			}
+			lba_to_status(context, lba);
+			context->status_buffer.format = SF_ABSOLUTE;
+		} else {
+			context->status_buffer.format = SF_NOTREADY;
+		}
+		break;
+	case SF_TOCO:
+		if (context->toc_valid) {
+			lba_to_status(context, context->media->tracks[context->media->num_tracks - 1].end_lba + context->media->tracks[0].fake_pregap);
+			context->status_buffer.format = SF_TOCO;
+		} else {
+			context->status_buffer.format = SF_NOTREADY;
+		}
+		break;
+	case SF_TOCT:
+		if (context->toc_valid) {
+			context->status_buffer.b.toct.first_track_high = 0;
+			context->status_buffer.b.toct.first_track_low = 1;
+			context->status_buffer.b.toct.last_track_high = (context->media->num_tracks + 1) / 10;
+			context->status_buffer.b.toct.last_track_low = (context->media->num_tracks + 1) % 10;
+			context->status_buffer.b.toct.version = 0;
+			context->status_buffer.format = SF_TOCT;
+		} else {
+			context->status_buffer.format = SF_NOTREADY;
+		}
+		break;
+	case SF_TOCN:
+		if (context->toc_valid) {
+			lba_to_status(context, context->media->tracks[context->requested_track].start_lba + context->media->tracks[0].fake_pregap);
+			context->status_buffer.b.tocn.track_low = context->requested_track % 10;
+			context->status_buffer.format = SF_TOCN;
+		} else {
+			context->status_buffer.format = SF_NOTREADY;
+		}
+		break;
+	case SF_NOTREADY:
+		memset(&context->status_buffer, 0, sizeof(context->status_buffer) - 1);
+		context->status_buffer.format = SF_NOTREADY;
+		break;
+	}
+	if (context->error_status == DS_STOP) {
+		context->status_buffer.status = context->status;
+	} else {
+		context->status_buffer.status = context->error_status;
+		context->error_status = DS_STOP;
+	}
+	if (context->requested_format != SF_TOCN) {
+		context->status_buffer.b.time.flags = 0; //TODO: populate these
+	}
+	context->status_buffer.checksum = checksum((uint8_t *)&context->status_buffer);
+	if (context->status_buffer.format != SF_NOTREADY) {
+		printf("CDD Status %d%d.%d%d%d%d%d%d.%d%d\n",
+			context->status_buffer.status, context->status_buffer.format,
+			context->status_buffer.b.time.min_high, context->status_buffer.b.time.min_low,
+			context->status_buffer.b.time.sec_high, context->status_buffer.b.time.sec_low,
+			context->status_buffer.b.time.frame_high, context->status_buffer.b.time.frame_low,
+			context->status_buffer.b.time.flags, context->status_buffer.checksum
+		);
+	}
+}
+
+static void run_command(cdd_mcu *context)
+{
+	uint8_t check = checksum((uint8_t*)&context->cmd_buffer);
+	if (check != context->cmd_buffer.checksum) {
+		context->error_status = DS_SUM_ERROR;
+		return;
+	}
+	if (context->cmd_buffer.must_be_zero) {
+		context->error_status = DS_CMD_ERROR;
+		return;
+	}
+	switch (context->cmd_buffer.cmd_type)
+	{
+	case CMD_NOP:
+		break;
+	case CMD_STOP:
+		puts("CDD CMD: STOP");
+		context->status = DS_STOP;
+		break;
+	case CMD_REPORT_REQUEST:
+		switch (context->cmd_buffer.b.format.status_type)
+		{
+		case SF_ABSOLUTE:
+		case SF_RELATIVE:
+		case SF_TRACK:
+			context->requested_format = context->cmd_buffer.b.format.status_type;
+			break;
+		case SF_TOCO:
+			if (context->toc_valid) {
+				context->requested_format = SF_TOCO;
+			} else {
+				context->error_status = DS_CMD_ERROR;
+				context->requested_format = SF_ABSOLUTE;
+			}
+			break;
+		case SF_TOCT:
+			if (context->toc_valid) {
+				if (context->status == DS_STOP) {
+					context->status = DS_TOC_READ;
+					context->seeking = 1;
+					context->seek_pba = 0;
+				}
+			} else {
+				context->status = DS_TOC_READ;
+				context->seeking = 1;
+				context->seek_pba = 0;
+			}
+			context->requested_format = SF_TOCT;
+			break;
+		case SF_TOCN:
+			context->requested_track = context->cmd_buffer.b.format.track_high * 10;
+			context->requested_track += context->cmd_buffer.b.format.track_low;
+			context->status = DS_TOC_READ;
+			context->seeking = 1;
+			context->seek_pba = 0;
+			context->requested_format = SF_TOCN;
+			break;
+		}
+		printf("CDD CMD: REPORT REQUEST(%d), format set to %d\n", context->cmd_buffer.b.format.status_type, context->requested_format);
+		break;
+	default:
+		printf("CDD CMD: Unimplemented(%d)\n", context->cmd_buffer.cmd_type);
+	}
+}
+
+#define BIT_HOCK 0x4
+#define BIT_DRS  0x2
+#define BIT_DTS  0x1
+
+void cdd_mcu_run(cdd_mcu *context, uint32_t cycle, uint16_t *gate_array)
+{
+	uint32_t cd_cycle = mclks_to_cd_block(cycle);
+	if (!(gate_array[GAO_CDD_CTRL] & BIT_HOCK)) {
+		//it's a little unclear if this gates the actual cd block clock or just handshaking
+		//assum it's actually the clock for now
+		context->cycle = cycle;
+		return;
+	}
+	uint32_t next_subcode = context->last_subcode_cycle + SECTOR_CLOCKS;
+	uint32_t next_nibble = context->current_status_nibble >= 0 ? context->last_nibble_cycle + NIBBLE_CLOCKS : CYCLE_NEVER;
+	uint32_t next_cmd_nibble = context->current_cmd_nibble >= 0 ? context->last_nibble_cycle + NIBBLE_CLOCKS : CYCLE_NEVER;
+	for (; context->cycle < cd_cycle; context->cycle += CDD_MCU_DIVIDER)
+	{
+		if (context->cycle >= next_subcode) {
+			context->last_subcode_cycle = context->cycle;
+			next_subcode = context->cycle + SECTOR_CLOCKS;
+			update_status(context);
+			next_nibble = context->cycle;
+			context->current_status_nibble = 0;
+			gate_array[GAO_CDD_STATUS] |= BIT_DRS;
+		}
+		if (context->cycle >= next_nibble) {
+			if (context->current_status_nibble == sizeof(cdd_status)) {
+				context->current_status_nibble = -1;
+				gate_array[GAO_CDD_STATUS] &= ~BIT_DRS;
+				if (context->cmd_recv_pending) {
+					context->cmd_recv_pending = 0;
+					context->current_cmd_nibble = 0;
+					gate_array[GAO_CDD_STATUS] |= BIT_DTS;
+					next_nibble = context->cycle + NIBBLE_CLOCKS;
+				} else {
+					context->cmd_recv_wait = 1;
+					next_nibble = CYCLE_NEVER;
+				}
+			} else {
+				uint8_t value = ((uint8_t *)&context->status_buffer)[context->current_status_nibble];
+				int ga_index = GAO_CDD_STATUS + (context->current_status_nibble >> 1);
+				if (context->current_status_nibble & 1) {
+					gate_array[ga_index] = value | (gate_array[ga_index]  & 0xFF00);
+				} else {
+					gate_array[ga_index] = (value << 8) | (gate_array[ga_index]  & 0x00FF);
+				}
+				if (context->current_status_nibble == 7) {
+					context->int_pending = 1;
+					context->next_int_cycle = cd_block_to_mclks(cycle + SECTOR_CLOCKS);
+				}
+				context->current_status_nibble++;
+				context->last_nibble_cycle = context->cycle;
+				next_nibble = context->cycle + NIBBLE_CLOCKS;
+			}
+		} else if (context->cycle >= next_cmd_nibble) {
+			if (context->current_cmd_nibble == sizeof(cdd_cmd)) {
+				next_cmd_nibble = CYCLE_NEVER;
+				context->current_cmd_nibble = -1;
+				gate_array[GAO_CDD_STATUS] &= ~BIT_DTS;
+				run_command(context);
+			} else {
+				int ga_index = GAO_CDD_CMD + (context->current_cmd_nibble >> 1);
+				uint8_t value = (context->current_cmd_nibble & 1) ? gate_array[ga_index] : gate_array[ga_index] >> 8;
+				((uint8_t *)&context->cmd_buffer)[context->current_cmd_nibble] = value;
+				context->current_cmd_nibble++;
+				context->last_nibble_cycle = context->cycle;
+				next_cmd_nibble = context->cycle + NIBBLE_CLOCKS;
+			}
+		}
+	}
+}
+
+void cdd_mcu_start_cmd_recv(cdd_mcu *context, uint16_t *gate_array)
+{
+	if (context->cmd_recv_wait) {
+		context->current_cmd_nibble = 0;
+		gate_array[GAO_CDD_STATUS] |= BIT_DTS;
+		context->last_nibble_cycle = context->cycle;
+		context->cmd_recv_wait = 0;
+	} else {
+		context->cmd_recv_pending = 1;
+	}
+}
+
+void cdd_hock_enabled(cdd_mcu *context)
+{
+	context->last_subcode_cycle = context->cycle;
+	context->next_int_cycle = cd_block_to_mclks(context->cycle + SECTOR_CLOCKS + 7 * NIBBLE_CLOCKS);
+}
+
+void cdd_hock_disabled(cdd_mcu *context)
+{
+	context->last_subcode_cycle = CYCLE_NEVER;
+	context->next_int_cycle = CYCLE_NEVER;
+	context->last_nibble_cycle = CYCLE_NEVER;
+	context->current_status_nibble = -1;
+	context->current_cmd_nibble = -1;
+}
+
+void cdd_mcu_adjust_cycle(cdd_mcu *context, uint32_t deduction)
+{
+	uint32_t cd_deduction = mclks_to_cd_block(deduction);
+	if (context->next_int_cycle != CYCLE_NEVER) {
+		context->next_int_cycle -= deduction;
+	}
+	if (context->last_subcode_cycle != CYCLE_NEVER) {
+		context->last_subcode_cycle -= cd_deduction;
+	}
+	if (context->last_nibble_cycle != CYCLE_NEVER) {
+		context->last_nibble_cycle -= cd_deduction;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cdd_mcu.h	Thu Jan 27 00:33:41 2022 -0800
@@ -0,0 +1,164 @@
+#ifndef CDD_MCU_H_
+#define CDD_MCU_H_
+#include "system.h"
+
+
+typedef enum {
+	SF_ABSOLUTE,
+	SF_RELATIVE,
+	SF_TRACK,
+	SF_TOCO,
+	SF_TOCT,
+	SF_TOCN,
+	SF_E,
+	SF_NOTREADY = 0xF
+} status_format;
+
+typedef enum {
+	CMD_NOP,
+	CMD_STOP,
+	CMD_REPORT_REQUEST,
+	CMD_READ,
+	CMD_SEEK,
+	CMD_INVALID,
+	CMD_PAUSE,
+	CMD_PLAY,
+	CMD_FFWD,
+	CMD_RWD,
+	CMD_TRACK_SKIP,
+	CMD_TRACK_CUE,
+	CMD_DOOR_CLOSE,
+	CMD_DOOR_OPEN
+} host_cmd;
+
+typedef enum {
+	DS_STOP,
+	DS_PLAY,
+	DS_SEEK,
+	DS_SCAN,
+	DS_PAUSE,
+	DS_DOOR_OPEN,
+	DS_SUM_ERROR,
+	DS_CMD_ERROR,
+	DS_FUNC_ERROR,
+	DS_TOC_READ,
+	DS_TRACKING,
+	DS_NO_DISC,
+	DS_DISC_LEADOUT,
+	DS_DISC_LEADIN,
+	DS_TRAY_MOVING,
+} drive_status;
+
+
+typedef struct {
+	uint8_t status;
+	uint8_t format;
+	union {
+		struct {
+			uint8_t min_high;
+			uint8_t min_low;
+			uint8_t sec_high;
+			uint8_t sec_low;
+			uint8_t frame_high;
+			uint8_t frame_low;
+			uint8_t flags;
+		} time;
+		struct {
+			uint8_t track_high;
+			uint8_t track_low;
+			uint8_t padding0;
+			uint8_t padding1;
+			uint8_t control;
+			uint8_t padding2;
+			uint8_t flags;
+		} track;
+		struct {
+			uint8_t first_track_high;
+			uint8_t first_track_low;
+			uint8_t last_track_high;
+			uint8_t last_track_low;
+			uint8_t version;
+			uint8_t padding;
+			uint8_t flags;
+		} toct;
+		struct {
+			uint8_t min_high;
+			uint8_t min_low;
+			uint8_t sec_high;
+			uint8_t sec_low;
+			uint8_t frame_high;
+			uint8_t frame_low;
+			uint8_t track_low;
+		} tocn;
+		struct {
+		} error;
+	} b;
+	uint8_t checksum;
+} cdd_status;
+
+typedef struct {
+	uint8_t cmd_type;
+	uint8_t must_be_zero;
+	union {
+		struct {
+			uint8_t min_high;
+			uint8_t min_low;
+			uint8_t sec_high;
+			uint8_t sec_low;
+			uint8_t frame_high;
+			uint8_t frame_low;
+		} time;
+		struct {
+			uint8_t padding;
+			uint8_t status_type;
+			uint8_t track_high;
+			uint8_t track_low;
+		} format;
+		struct {
+			uint8_t padding;
+			uint8_t direction;
+			uint8_t tracks_highest;
+			uint8_t tracks_midhigh;
+			uint8_t tracks_midlow;
+			uint8_t tracks_lowest;
+		} skip;
+		struct {
+			uint8_t track_high;
+			uint8_t track_low;
+		} track;
+	} b;
+	uint8_t padding;
+	uint8_t checksum;
+} cdd_cmd;
+
+typedef struct {
+	system_media  *media;
+	uint32_t      cycle;          //this is in CD block CLKS
+	uint32_t      next_int_cycle; //this is in SCD MCLKS
+	uint32_t      last_subcode_cycle;
+	uint32_t      last_nibble_cycle;
+	int           current_status_nibble;
+	int           current_cmd_nibble;
+	uint32_t      head_pba;
+	uint32_t      seek_pba;
+	cdd_status    status_buffer;
+	cdd_cmd       cmd_buffer;
+	status_format requested_format;
+	drive_status  status;
+	drive_status  error_status;
+	uint8_t       requested_track;
+	uint8_t       cmd_recv_wait;
+	uint8_t       cmd_recv_pending;
+	uint8_t       int_pending;
+	uint8_t       toc_valid;
+	uint8_t       seeking;
+} cdd_mcu;
+
+void cdd_mcu_init(cdd_mcu *context, system_media *media);
+void cdd_mcu_run(cdd_mcu *context, uint32_t cycle, uint16_t *gate_array);
+void cdd_hock_enabled(cdd_mcu *context);
+void cdd_hock_disabled(cdd_mcu *context);
+void cdd_mcu_start_cmd_recv(cdd_mcu *context, uint16_t *gate_array);
+void cdd_mcu_adjust_cycle(cdd_mcu *context, uint32_t deduction);
+
+#endif //CD_MCU_H_
--- a/cue.c	Sun Jan 23 11:11:07 2022 -0800
+++ b/cue.c	Thu Jan 27 00:33:41 2022 -0800
@@ -144,5 +144,7 @@
 		fseek(media->f, 16, SEEK_SET);
 		media->size = fread(media->buffer, 1, 2048, media->f);
 	}
-	return tracks > 0 && media->f != NULL;
+	uint8_t valid = tracks > 0 && media->f != NULL;
+	media->type = valid ? MEDIA_CDROM : MEDIA_CART;
+	return valid;
 }
--- a/segacd.c	Sun Jan 23 11:11:07 2022 -0800
+++ b/segacd.c	Thu Jan 27 00:33:41 2022 -0800
@@ -81,6 +81,9 @@
 #define BIT_MASK_IEN5  0x0020
 #define BIT_MASK_IEN6  0x0040
 
+//GA_CDD_CTRL
+#define BIT_HOCK       0x0004
+
 static void *prog_ram_wp_write16(uint32_t address, void *vcontext, uint16_t value)
 {
 	m68k_context *m68k = vcontext;
@@ -308,19 +311,28 @@
 	segacd_context *cd = context->system;
 	context->int_cycle = CYCLE_NEVER;
 	uint8_t mask = context->status & 0x7;
-	if (mask < 3) {
-		uint32_t next_timer;
-		if (cd->gate_array[GA_INT_MASK] & BIT_MASK_IEN3) {
-			uint32_t next_timer_cycle = next_timer_int(cd);
-			if (next_timer_cycle < context->int_cycle) {
-				context->int_cycle = next_timer_cycle;
-				context->int_num = 3;
+	if (mask < 4) {
+		if (cd->gate_array[GA_INT_MASK] & BIT_MASK_IEN4) {
+			uint32_t cdd_cycle = cd->cdd.int_pending ? context->current_cycle : cd->cdd.next_int_cycle;
+			if (cdd_cycle < context->int_cycle) {
+				context->int_cycle = cdd_cycle;
+				context->int_num = 4;
 			}
 		}
-		if (mask < 2) {
-			if (cd->int2_cycle < context->int_cycle && (cd->gate_array[GA_INT_MASK] & BIT_MASK_IEN2)) {
-				context->int_cycle = cd->int2_cycle;
-				context->int_num = 2;
+		if (mask < 3) {
+			uint32_t next_timer;
+			if (cd->gate_array[GA_INT_MASK] & BIT_MASK_IEN3) {
+				uint32_t next_timer_cycle = next_timer_int(cd);
+				if (next_timer_cycle < context->int_cycle) {
+					context->int_cycle = next_timer_cycle;
+					context->int_num = 3;
+				}
+			}
+			if (mask < 2) {
+				if (cd->int2_cycle < context->int_cycle && (cd->gate_array[GA_INT_MASK] & BIT_MASK_IEN2)) {
+					context->int_cycle = cd->int2_cycle;
+					context->int_num = 2;
+				}
 			}
 		}
 	}
@@ -363,6 +375,14 @@
 	case GA_TIMER:
 		timers_run(cd, m68k->current_cycle);
 		return cd->gate_array[reg];
+	case GA_CDD_STATUS0:
+	case GA_CDD_STATUS1:
+	case GA_CDD_STATUS2:
+	case GA_CDD_STATUS3:
+	case GA_CDD_STATUS4:
+		cdd_mcu_run(&cd->cdd, m68k->current_cycle, cd->gate_array + GA_CDD_CTRL);
+		return cd->gate_array[reg];
+		break;
 	case GA_FONT_DATA0:
 	case GA_FONT_DATA1:
 	case GA_FONT_DATA2:
@@ -499,6 +519,33 @@
 		cd->gate_array[reg] = value & (BIT_MASK_IEN6|BIT_MASK_IEN5|BIT_MASK_IEN4|BIT_MASK_IEN3|BIT_MASK_IEN2|BIT_MASK_IEN1);
 		calculate_target_cycle(m68k);
 		break;
+	case GA_CDD_CTRL: {
+		cdd_mcu_run(&cd->cdd, m68k->current_cycle, cd->gate_array + GA_CDD_CTRL);
+		uint16_t changed = cd->gate_array[reg] ^ value;
+		cd->gate_array[reg] &= ~BIT_HOCK;
+		cd->gate_array[reg] |= value & BIT_HOCK;
+		if (changed & BIT_HOCK) {
+			if (value & BIT_HOCK) {
+				cdd_hock_enabled(&cd->cdd);
+			} else {
+				cdd_hock_disabled(&cd->cdd);
+			}
+			calculate_target_cycle(m68k);
+		}
+		break;
+	}
+	case GA_CDD_CMD0:
+	case GA_CDD_CMD1:
+	case GA_CDD_CMD2:
+	case GA_CDD_CMD3:
+		cdd_mcu_run(&cd->cdd, m68k->current_cycle, cd->gate_array + GA_CDD_CTRL);
+		cd->gate_array[reg] = value & 0x0F0F;
+		break;
+	case GA_CDD_CMD4:
+		cdd_mcu_run(&cd->cdd, m68k->current_cycle, cd->gate_array + GA_CDD_CTRL);
+		cd->gate_array[reg] = value & 0x0F0F;
+		cdd_mcu_start_cmd_recv(&cd->cdd, cd->gate_array + GA_CDD_CTRL);
+		break;
 	case GA_FONT_COLOR:
 		cd->gate_array[reg] = value & 0xFF;
 		break;
@@ -536,6 +583,15 @@
 			cd->gate_array[reg] = value << 8;
 		}
 		return vcontext;
+	case GA_CDD_CMD4:
+		if (!address) {
+			//byte write to $FF804A should not trigger transfer
+			cdd_mcu_run(&cd->cdd, m68k->current_cycle, cd->gate_array + GA_CDD_CTRL);
+			cd->gate_array[reg] &= 0x0F;
+			cd->gate_array[reg] |= (value << 8 & 0x0F00);
+			return vcontext;
+		}
+		//intentional fallthrough for $FF804B
 	default:
 		if (address & 1) {
 			value16 = cd->gate_array[reg] & 0xFF00 | value;
@@ -555,6 +611,7 @@
 static void scd_peripherals_run(segacd_context *cd, uint32_t cycle)
 {
 	timers_run(cd, cycle);
+	cdd_mcu_run(&cd->cdd, cycle, cd->gate_array + GA_CDD_CTRL);
 }
 
 static m68k_context *sync_components(m68k_context * context, uint32_t address)
@@ -569,6 +626,8 @@
 	case 3:
 		cd->timer_pending = 0;
 		break;
+	case 4:
+		cd->cdd.int_pending = 0;
 	}
 	context->int_ack = 0;
 	calculate_target_cycle(context);
@@ -613,6 +672,7 @@
 	} else if (cd->periph_reset_cycle != CYCLE_NEVER) {
 		cd->periph_reset_cycle -= deduction;
 	}
+	cdd_mcu_adjust_cycle(&cd->cdd, deduction);
 }
 
 static uint16_t main_gate_read16(uint32_t address, void *vcontext)
@@ -849,6 +909,10 @@
 	cd->gate_array[1] = 1;
 	cd->gate_array[0x1B] = 0x100;
 	lc8951_init(&cd->cdc);
+	if (media->chain && media->type != MEDIA_CDROM) {
+		media = media->chain;
+	}
+	cdd_mcu_init(&cd->cdd, media);
 
 	return cd;
 }
--- a/segacd.h	Sun Jan 23 11:11:07 2022 -0800
+++ b/segacd.h	Thu Jan 27 00:33:41 2022 -0800
@@ -3,6 +3,7 @@
 #include <stdint.h>
 #include "genesis.h"
 #include "lc8951.h"
+#include "cdd_mcu.h"
 
 typedef struct {
 	m68k_context    *m68k;
@@ -27,6 +28,7 @@
 	uint8_t         need_reset;
 	uint8_t         memptr_start_index;
 	lc8951          cdc;
+	cdd_mcu         cdd;
 } segacd_context;
 
 segacd_context *alloc_configure_segacd(system_media *media, uint32_t opts, uint8_t force_region, rom_info *info);