view lc8951.c @ 2069:8e51c0c3f2e3 segacd

Initial attempt at implementing the Sega CD graphics hardware
author Michael Pavone <pavone@retrodev.com>
date Sun, 30 Jan 2022 19:55:33 -0800
parents f22e04b69272
children c69e42444f96
line wrap: on
line source

#include "lc8951.h"
#include "backend.h"

enum {
	COMIN,
	IFSTAT,
	DBCL,
	DBCH,
	HEAD0,
	HEAD1,
	HEAD2,
	HEAD3,
	PTL,
	PTH,
	WAL,
	WAH,
	STAT0,
	STAT1,
	STAT2,
	STAT3,

	SBOUT = COMIN,
	IFCTRL = IFSTAT,
	DACL = HEAD0,
	DACH = HEAD1,
	DTTRG = HEAD2,
	DTACK = HEAD3,
	WAL_WRITE = PTL,
	WAH_WRITE = PTH,
	CTRL0 = WAL,
	CTRL1 = WAH,
	PTL_WRITE = STAT0,
	PTH_WRITE = STAT1,
	RESET = STAT3
};

//IFCTRL
#define BIT_CMDIEN 0x80
#define BIT_DTEIEN 0x40
#define BIT_DECIEN 0x20
#define BIT_CMDBK  0x10
#define BIT_DTWAI  0x08
#define BIT_STWAI  0x04
#define BIT_DOUTEN 0x02
#define BIT_SOUTEN 0x01

//IFSTAT
#define BIT_CMDI   0x80
#define BIT_DTEI   0x40
#define BIT_DECI   0x20
#define BIT_DTBSY  0x08
#define BIT_STBSY  0x04
#define BIT_DTEN   0x02
#define BIT_STEN   0x01

//CTRL0
#define BIT_DECEN 0x80
#define BIT_WRRQ  0x04

//STAT0
#define BIT_CRCOK 0x80

//STAT3
#define BIT_VALST 0x80

//datasheet timing info
//3 cycles for memory operation
//6 cycles min for DMA-mode host transfer

void lc8951_init(lc8951 *context, lcd8951_byte_recv_fun byte_handler, void *handler_data)
{
	//This seems to vary somewhat between Sega CD models
	//unclear if the difference is in the lc8951 or gate array
	context->regs[IFSTAT] = 0xFF;
	context->ar_mask = 0x1F;
	context->clock_step = (2 + 2) * 6; // external divider, internal divider + DMA period
	context->byte_handler = byte_handler;
	context->handler_data = handler_data;
	context->decode_end = CYCLE_NEVER;
	context->transfer_end = CYCLE_NEVER;
}

void lc8951_reg_write(lc8951 *context, uint8_t value)
{
	switch (context->ar)
	{
	case SBOUT:
		context->regs[context->ar] = value;
		if (context->ifctrl & BIT_SOUTEN) {
			context->regs[IFSTAT] &= ~BIT_STBSY;
		}
		break;
	case IFCTRL:
		context->ifctrl = value;
		if (!(value & BIT_SOUTEN)) {
			context->regs[IFSTAT] |= BIT_STBSY;
		}
		if (!(value & BIT_DOUTEN)) {
			context->regs[IFSTAT] |= BIT_DTBSY|BIT_DTEI;
			context->transfer_end = CYCLE_NEVER;
		}
		break;
	case DBCL:
		context->regs[context->ar] = value;
		break;
	case DBCH:
		context->regs[context->ar] = value & 0xF;
		break;
	case DACL:
		context->dac &= 0xFF00;
		context->dac |= value;
		break;
	case DACH:
		context->dac &= 0xFF;
		context->dac |= value << 8;
		break;
	case DTTRG:
		if (context->ifctrl & BIT_DOUTEN) {
			context->regs[IFSTAT] &= ~BIT_DTBSY;
			uint16_t transfer_size = context->regs[DBCL] | (context->regs[DBCH] << 8);
			context->transfer_end = context->cycle + transfer_size * context->clock_step;
			printf("DTTRG: size %u, cycle %u, end %u\n", transfer_size, context->cycle, context->transfer_end);
		}
		break;
	case DTACK:
		context->regs[IFSTAT] |= BIT_DTEI;
		break;
	case WAL_WRITE:
		context->regs[WAL] = value;
		break;
	case WAH_WRITE:
		context->regs[WAH] = value;
		break;
	case CTRL0:
		context->ctrl0 = value;
		break;
	case CTRL1:
		context->ctrl1 = value;
		break;
	case PTL_WRITE:
		context->regs[PTL] = value;
		break;
	case PTH_WRITE:
		context->regs[PTH] = value;
		//TODO: Datasheet says any write to PT triggers a decode, but initial tests suggest that's not the case
		//Need to do more tests with other CTRL0/CTRL1 settings
		//context->decode_end = context->cycle + 2352 * context->clock_step * 4;
		break;
	case RESET:
		context->comin_count = 0;
		context->regs[IFSTAT] = 0xFF;
		break;
	default:
		break;
	}
	if (context->ar != SBOUT) {
		context->ar++;
		context->ar &= context->ar_mask;
	}
}

uint8_t lc8951_reg_read(lc8951 *context)
{
	uint8_t value;
	if (context->ar == COMIN) {
		if (!context->comin_count) {
			return 0xFF;
		}
		value = context->comin[(context->comin_write - context->comin_count)&sizeof(context->comin)];
		context->comin_count--;
		if (!context->comin_count) {
			context->regs[IFSTAT] |= BIT_CMDI;
		}
		return value;
	}
	if (context->ar == STAT3) {
		context->regs[IFSTAT] |= BIT_DECI;
	}
	if (context->ar >= sizeof(context->regs)) {
		value = 0xFF;
	} else {
		value = context->regs[context->ar];
	}
	printf("CDC read %X: %X\n", context->ar, value);
	context->ar++;
	context->ar &= context->ar_mask;
	return value;
}

void lc8951_ar_write(lc8951 *context, uint8_t value)
{
	context->ar = value & context->ar_mask;
}

//25 MHz clock input (1/2 SCD MCLK)
//internal /2 divider
//3 cycles for each SRAM access (though might be crystal frequency rather than internal frequency)
//6 cycle period for DMA transfer out
//

void lc8951_run(lc8951 *context, uint32_t cycle)
{
	for(; context->cycle < cycle; context->cycle += context->clock_step)
	{
		if (context->cycle >= context->decode_end) {
			context->decode_end = CYCLE_NEVER;
			context->regs[IFSTAT] &= ~BIT_DECI;
			context->regs[STAT3] &= ~BIT_VALST;
			if (context->ctrl0 & BIT_WRRQ) {
				uint16_t block_start = (context->regs[PTL] | (context->regs[PTH] << 8)) & (sizeof(context->buffer)-1);
				for (int reg = HEAD0; reg < PTL; reg++)
				{
					printf("Setting HEAD%d to buffer[%X]\n", reg - HEAD0, block_start);
					context->regs[reg] =context->buffer[block_start++];
					block_start &= (sizeof(context->buffer)-1);
				}
			}
			printf("Decode done %X:%X:%X mode %X\n", context->regs[HEAD0], context->regs[HEAD1], context->regs[HEAD2], context->regs[HEAD3]);
			context->regs[STAT0] |= BIT_CRCOK;
		}
		if (context->transfer_end != CYCLE_NEVER) {
			if (context->byte_handler(context->handler_data, context->buffer[context->dac & (sizeof(context->buffer)-1)])) {
				context->dac++;
				context->regs[DBCL]--;
				if (context->regs[DBCL] == 0xFF) {
					context->regs[DBCH]--;
					if (context->regs[DBCH] == 0xFF) {
						context->regs[IFSTAT] &= ~BIT_DTEI;
						context->regs[IFSTAT] |= BIT_DTBSY;
						if (context->cycle != context->transfer_end) {
							printf("Expected transfer end at %u but ended at %u\n", context->transfer_end, context->cycle);
						}
						context->transfer_end = CYCLE_NEVER;
					}
				}
			} else {
				// pause transfer
				context->transfer_end = CYCLE_NEVER;
			}
		}
	}
}

void lc8951_resume_transfer(lc8951 *context, uint32_t cycle)
{
	if (context->transfer_end == CYCLE_NEVER && (context->ifctrl & BIT_DOUTEN)) {
		uint16_t transfer_size = context->regs[DBCL] | (context->regs[DBCH] << 8);
		if (transfer_size != 0xFFFF) {
			//HACK!!! Work around Sub CPU running longer than we would like and dragging other components with it
			uint32_t step_diff = (context->cycle - cycle) / context->clock_step;
			if (step_diff) {
				context->cycle -= step_diff * context->clock_step;
			}
			context->transfer_end = context->cycle + transfer_size * context->clock_step;
			printf("RESUME: size %u, cycle %u, end %u\n", transfer_size, context->cycle, context->transfer_end);
			if (step_diff) {
				lc8951_run(context, cycle);
			}
		}
	}
}

void lc8951_write_byte(lc8951 *context, uint32_t cycle, int sector_offset, uint8_t byte)
{
	lc8951_run(context, cycle);
	uint16_t current_write_addr = context->regs[WAL] | (context->regs[WAH] << 8);
	if (sector_offset == 12) {
		//we've recevied the sync pattern for the next block

		//header/status regs no longer considered "valid"
		context->regs[STAT3] |= BIT_VALST;
		//!DECI is set inactive at the same time as !VALST
		context->regs[IFSTAT] |= BIT_DECI;
		if (context->ctrl0 & BIT_DECEN) {
			if (context->ctrl0 & BIT_WRRQ) {
				uint16_t block_start = current_write_addr - 2352;
				context->regs[PTL] = block_start;
				context->regs[PTH] = block_start >> 8;
			}
			printf("Decoding block starting at %X\n", context->regs[PTL] | (context->regs[PTH] << 8));
			//TODO: Datasheet has some hints about how long decoding takes in the form of how long DECI is asserted
			context->decode_end = context->cycle + 2352 * context->clock_step * 4;
		}
	}
	if (sector_offset >= 12 && sector_offset < 16) {
		//TODO: Handle SHDREN = 1
		if ((context->ctrl0 & (BIT_DECEN|BIT_WRRQ)) == (BIT_DECEN)) {
			//monitor only mode
			context->regs[HEAD0 + sector_offset - 12] = byte;
		}
	}
	if ((context->ctrl0 & (BIT_DECEN|BIT_WRRQ)) == (BIT_DECEN|BIT_WRRQ)) {
		context->buffer[current_write_addr & (sizeof(context->buffer)-1)] = byte;
		context->regs[WAL]++;
		if (!context->regs[WAL]) {
			context->regs[WAH]++;
		}
	}
}

uint32_t lc8951_next_interrupt(lc8951 *context)
{
	if ((~context->regs[IFSTAT]) & context->ifctrl & (BIT_CMDI|BIT_DTEI|BIT_DECI)) {
		//interrupt already pending
		return context->cycle;
	}
	uint32_t deci_cycle = CYCLE_NEVER;
	if (context->ifctrl & BIT_DECI) {
		deci_cycle = context->decode_end;
	}
	uint32_t dtei_cycle = CYCLE_NEVER;
	if (context->ifctrl & BIT_DTEI) {
		dtei_cycle = context->transfer_end;
	}
	return deci_cycle < dtei_cycle ? deci_cycle : dtei_cycle;
}

void lc8951_adjust_cycles(lc8951 *context, uint32_t deduction)
{
	if (context->decode_end != CYCLE_NEVER) {
		context->decode_end -= deduction;
	}
	if (context->transfer_end != CYCLE_NEVER) {
		context->transfer_end -= deduction;
	}
}