My new emulator

Emulator and emulator development specific topics
Thommy
Posts: 52
Joined: Wed Sep 28, 2011 6:37 pm

Re: My new emulator

Post by Thommy »

Andy Rea wrote:looking at the youtube vid when doing the loading, see those vertical bars... Well that is classic symtoms of Hsync counter been reset by the Vsync ( which occur albeit very short, but a vsync signal non-the less during loading). re jig your code to reset the Hsync counter on IntAck (M! low and IORQ low) and then The Hsync and NMI occurs 16 clock cycles after IntAck (either M1 or IORQ going high)

See if that cures it.
I don't have the code to hand so can't be definitive either way, but I'm confident that — the very real possibility of mistakes in my code aside — the two sync sources are entirely uncoupled until output. Obviously I'm going to take your advice and carefully review what I have but because I'm not otherwise going to be able to ask again until tomorrow: can you think of a way that incorrect triggering of the wait line (being based on hsync, after all) might give the same effect? Possibly I'm grasping too much for a single explanation that ties the low clock count to the character-by-character 25th Anniversary demo scroll to the incorrect tape loading display (which I didn't even spot — thanks in any event for the tip off).

Obviously I'm just wondering if your hardware experience suggests anything — please don't think I'm trying to offload the task of debugging my code, especially since I haven't shown it yet.
User avatar
Andy Rea
Posts: 1606
Joined: Fri May 09, 2008 2:48 pm
Location: Planet Earth
Contact:

Re: My new emulator

Post by Andy Rea »

probably not of much use, but i wrote a ZX81 emulator many years ago, never finished it, it was more of a personal challenge than anything else, but here is what i did for the waits which can only occur during an NMI = low
// fix for the NMI waits

int T_waits = 11; // start off with 11 clocks for the NMI RESPONSE
// t1 and t2 always happen before any waits
if ((Get_H207_Counter()+2) < 206) // is NMI PULSE STILL ACTIVE ?
{
T_waits += (206 - (Get_H207_Counter()+2));
}

// mmust have been pat end of NMI pulse already
this was before I discovered the correct timings for Hsync counter and was generating NMI at the end of the count.

this is assuming that the CPU isn't halted too.... (hmm seems like i've learnt a lot since then) which i appear to have been taking no account of whatsoever !

Andy
what's that Smell.... smells like fresh flux and solder fumes...
Thommy
Posts: 52
Joined: Wed Sep 28, 2011 6:37 pm

Re: My new emulator

Post by Thommy »

Thanks for that. Obviously the structure is very different from my emulator; my ULA code simply does this:

Code: Select all

if(NMIs are active)
{
    set NMI line to same value as the HSYNC line;
    if(!halt line)
        set wait line to same value as the HSYNC line;
    else
        set wait line to inactive;
}
else
    set wait and NMI lines to inactive;
Where same value means if one is active then so is the other. I've preferred semantics to the direct voltage metaphor. The simplicity of the logic involved is why suspicion is turned on how my Z80 sets the halt line and reacts to the wait line.

That said, the fact that I haven't implemented flywheel sync in my CRT yet (which is definitely required to give the proper sinusoidals; I'm temporarily using the normal vertical synchronisation test for both vertical and horizontal) makes it hard even to say authoritatively that the difference in loading pattern is the fault of the emulated computer. It's very easy to imagine that the inaccurate CRT sync is to blame for that one thing, given that both types of ULA sync look like a horizontal sync at the far end of the wire — if I'm being too lenient or too flexible in responding to mistimed horizontal sync pulses then you'd get much the same effect. It feels like I'm grasping though.

Thanks again for your help and suggestions; it's only because we're discussing it that I'm even slightly confident I can get a decent run at the problem when I'm next in possession of the code. Fingers crossed it's something simple.
Thommy
Posts: 52
Joined: Wed Sep 28, 2011 6:37 pm

Re: My new emulator

Post by Thommy »

Right, I can confirm that I had NMI timing slightly askew, which has fixed the scroller in the 25th Anniversary Demo. Based on random sampling, I think I also have something going wrong in my Z80's scheduling of read-modify-write (ix+d) type instructions, which I hope will ultimately explain the slow count for clkfreq.81 while almost all demos I've tested seem to run perfectly — the ROM needs to be compact but the demos are likely doing whatever is necessary to avoid the expensive opcodes.

However, I have vertical and horizontal functions running entirely independently and I'm still seeing vertical bars on loading. The temporarily inaccurate sync after the video signal has reached the CRT is the current suspect.

For the curious, my current 'ZX80 ULA' implementation is below. I'm fully aware of the fiction of the name. The self-imposed rules are that individual, sealed components must accurate reproduce and react to signals on the connections that they have in the real hardware. Within those rules I've settled on modelling both the ZX80 and 81 as three components: the Z80 CPU, the rest of the hardware inside the box and the CRT. In code I've named 'the rest of the hardware inside the box' the ULA even though the ZX80 doesn't have a ULA at all and the ZX81's ULA doesn't include the memory.

The CRT is conceptually signalled via a PCM wave, but you need only partially specify it. The output luminance byte method is a helper to specify the next 8 PCM samples as single bits; setLevel and setSyncLevel (which set the level as of the specified time) are the other CRT output methods in use. So it isn't unrealistic that the helper therefore allows you just to throw a video byte directly at the CRT if you've set the input PCM sampling rate appropriately in advance.

As before, comments are primarily for my own benefit and may no longer be accurate, or indeed may never have been accurate.

Code: Select all

static void llzx80ula_considerSync(LLZX80ULAState *ula)
{
	// decide the new output sync level
	ZX80ULA_BOOL newSyncLevel;
	if(ula->vsyncIsActive | ula->hsyncIsActive)
		newSyncLevel = ZX80ULA_YES;
	else
		newSyncLevel = ZX80ULA_NO;

	// if this is a leading edge of hsync then increment
	// the line counter
	if(ula->hsyncIsActive && !ula->lastHSyncLevel)
		ula->lineCounter++;

	ula->lastHSyncLevel = ula->hsyncIsActive;

	// set the current output level on the CRT
	unsigned int currentTime = llz80_monitor_getInternalValue(ula->CPU, LLZ80MonitorValueHalfCyclesToDate);
	if(newSyncLevel)
		llcrt_setSyncLevel(ula->CRT, currentTime);
	else
		llcrt_setLuminanceLevel(ula->CRT, currentTime, 0xff);
}


static void llzx80ula_observeIntAck(void *z80, unsigned int changedLines, void *context)
{
	// This triggers on IO request + M1 active, i.e. interrupt acknowledge;
	// we need to arrange for horizontal sync to occur; the ZX81 has a
	// well-documented counter for the purpose and we'll need to count M1
	// cycles on a ZX80 so the action is the same either way
	LLZX80ULAState *ula = (LLZX80ULAState *)context;
	ula->hsyncCounter = 0;
}

static void llzx80ula_observeRefresh(void *z80, unsigned int changedLines, void *context)
{
	// if this is a memory refresh cycle then make a note
	// of the address and echo bit 6 to the INT line,
	// recalling that it's active low
	LLZX80ULAState *ula = (LLZX80ULAState *)context;

	ula->lastRefreshAddress = llz80_getSignal(z80, LLZ80SignalAddress);
	llz80_setSignal(z80, LLZ80SignalInterruptRequest, (ula->lastRefreshAddress&0x40) ? LLZ80_INACTIVE : LLZ80_ACTIVE);

	// are we meant to be fetching a byte this refresh cycle?
	if(ula->fetchVideoByte)
	{
		ula->fetchVideoByte = ZX80ULA_NO;
		uint8_t videoByte;

		// if so, would the ROM actually serve this address?
		if(ula->videoFetchAddress < ula->ROMTop)
		{
			videoByte = ula->ROM[ula->videoFetchAddress & ula->ROMMask];
		}
		else
		{
			// if not then the ROM loads nothing and the response from
			// the RAM to the refresh request ends up being the video
			// byte. Internal RAM is static so it responds to refresh
			// requests by loading the relevant byte onto the bus just
			// like a normal read; external RAM packs can be modified
			// to do the same, and we'll emulate one that has
			videoByte = (ula->lastRefreshAddress < ula->RAMTop) ? ula->RAM[ula->lastRefreshAddress] : 0xff;
		}

		// this byte might be intended to be inverted
		videoByte ^= ula->videoByteXorMask;

		// and push it out to the CRT
		llcrt_output1BitLuminanceByte(
			ula->CRT,
			llz80_monitor_getInternalValue(z80, LLZ80MonitorValueHalfCyclesToDate),
			videoByte);
	}
}

/* This one is hooked up for the ZX80 only */
static void llzx80ula_observeMachineCycleOne(void *z80, unsigned int changedLines, void *context)
{
	// M1 cycles clock the horizontal sync generator, but
	// only when we're in an hsync cycle
	LLZX80ULAState *ula = (LLZX80ULAState *)context;
	if(ula->hsyncCounter < 4)
	{
		if(ula->hsyncCounter == 1)
		{
			ula->hsyncIsActive = ZX80ULA_YES;
			llzx80ula_considerSync(ula);
		}

		if(ula->hsyncCounter == 3)
		{
			ula->hsyncIsActive = ZX80ULA_NO;
			llzx80ula_considerSync(ula);
		}

		ula->hsyncCounter++;
	}
}

/* This one is hooked up for the ZX81 only */
static void llzx80ula_observeClock(void *z80, unsigned int changedLines, void *context)
{
	LLZX80ULAState *ula = (LLZX80ULAState *)context;

	// increment the hsync counter, check whether sync output is
	// currently active as a result
	ula->hsyncCounter = (ula->hsyncCounter+1)%207;
	ula->hsyncIsActive = (ula->hsyncCounter >= 16) && (ula->hsyncCounter < 32);

	if(ula->nmiIsEnabled)
	{
		llz80_setSignal(z80, LLZ80SignalNonMaskableInterruptRequest, ula->hsyncIsActive);

		if(!llz80_getSignal(z80, LLZ80SignalHalt))
			llz80_setSignal(z80, LLZ80SignalWait, ula->hsyncIsActive);
		else
			llz80_setSignal(z80, LLZ80SignalWait, ZX80ULA_NO);
	}
	else
	{
		llz80_setSignal(z80, LLZ80SignalWait, ZX80ULA_NO);
		llz80_setSignal(z80, LLZ80SignalNonMaskableInterruptRequest, ZX80ULA_NO);
	}

	// determine what to do about sync level output as a result
	llzx80ula_considerSync(ula);
}


static void llzx80ula_observeMemoryRead(void *z80, unsigned int changedLines, void *context)
{
	// a memory read request
	LLZX80ULAState *ula = (LLZX80ULAState *)context;
	uint16_t address = llz80_getSignal(z80, LLZ80SignalAddress);

	// if this is a read in the ROM area then just serve it
	if(address < ula->ROMTop)
	{
		llz80_setSignal(z80, LLZ80SignalData, ula->ROM[address&ula->ROMMask]);
		return;
	}

	// if this is an instruction fetch and the highest address
	// bit is set then we want to consider stealing the byte
	// for TV output purposes
	if(llz80_getSignal(z80, LLZ80SignalMachineCycleOne) && (address & 0x8000))
	{
		// mask off the relevant bit of the address
		address &= 0x7fff;

		// a value is reported by the RAM, or possibly
		// no value turns up at all
		uint8_t value = (address < ula->RAMTop) ? ula->RAM[address] : 0xff;

		// if bit 6 is set then we let this value proceed
		// to the CPU
		if(value & 0x40)
		{
			llz80_setSignal(z80, LLZ80SignalData, value);
			return;
		}
		else
		{
			// otherwise this value is used to set up some video
			// output momentarily. We combine with the buffered
			// refresh address and our internal line counter to
			// get a read address
			ula->videoFetchAddress = 
				((value&0x3f) << 3) | 
				(ula->lineCounter&7) | 
				(ula->lastRefreshAddress&0xff00);

			// make a note to fetch a video byte at the next
			// refresh cycle. Record whether we're going to
			// invert that thing
			ula->fetchVideoByte = ZX80ULA_YES;
			ula->videoByteXorMask = (value&0x80) ? 0x00 : 0xff;

			// and lie to the Z80 by forcing this read to
			// return a NOP
			llz80_setSignal(z80, LLZ80SignalData, 0);
			return;
		}
	}

	// if we're here then see whether RAM wants to
	// jump in for a normal read operation
	if(address < ula->RAMTop)
	{
		llz80_setSignal(z80, LLZ80SignalData, ula->RAM[address]);
		return;
	}
}

static void llzx80ula_observeMemoryWrite(void *z80, unsigned int changedLines, void *context)
{
	// a memory write request
	LLZX80ULAState *ula = (LLZX80ULAState *)context;
	uint16_t address = llz80_getSignal(z80, LLZ80SignalAddress);

	// if this is to the RAM area, then store it;
	// actually it might be to the ROM area too, but
	// if so it'll be filtered on read. 
	if(address < ula->RAMTop)
	{
		ula->RAM[address] = llz80_getSignal(z80, LLZ80SignalData);
	}
}

static void llzx80ula_observeIORead(void *z80, unsigned int changedLines, void *context)
{
	// an IO read request
	LLZX80ULAState *ula = (LLZX80ULAState *)context;
	uint16_t address = llz80_getSignal(z80, LLZ80SignalAddress);

	switch(address&7)
	{
		default: break;

		case 6:
		{
			// start vertical sync 
			if(!ula->nmiIsEnabled)
			{
				ula->vsyncIsActive = ZX80ULA_YES;
				llzx80ula_considerSync(ula);
			}

			// do a keyboard read
			uint8_t result = 0x7f;
			if(!(address&0x0100)) result &= ula->keyLines[0];
			if(!(address&0x0200)) result &= ula->keyLines[1];
			if(!(address&0x0400)) result &= ula->keyLines[2];
			if(!(address&0x0800)) result &= ula->keyLines[3];
			if(!(address&0x1000)) result &= ula->keyLines[4];
			if(!(address&0x2000)) result &= ula->keyLines[5];
			if(!(address&0x4000)) result &= ula->keyLines[6];
			if(!(address&0x8000)) result &= ula->keyLines[7];

			// read the tape input too, if there is any
			void *tape = cstapePlayer_getTape(ula->tapePlayer);
			if(tape)
			{
				unsigned int currentTime = llz80_monitor_getInternalValue(z80, LLZ80MonitorValueHalfCyclesToDate);
				uint64_t tapeTime = cstapePlayer_getTapeTime(ula->tapePlayer, currentTime);

				if(cstape_getLevelAtTime(tape, tapeTime) == CSTapeLevelLow)
					result |= 0x80;
			}

			// and load the result
			llz80_setSignal(z80, LLZ80SignalData, result);
		}
		break;
	}
}

static void llzx80ula_observeIOWrite(void *z80, unsigned int changedLines, void *context)
{
	// an IO write request ...
	LLZX80ULAState *ula = (LLZX80ULAState *)context;
	uint16_t address = llz80_getSignal(z80, LLZ80SignalAddress);

	// determine whether activate or deactive the NMI generator;
	// this is relevant to the ZX81 only, and since NMI enabled
	// prevents output of 'vertical' sync, it would adversely
	// affect ZX80 emulation if we went ahead regardless
	if(ula->machineType == LLZX80ULAMachineTypeZX81)
		switch(address&7)
		{
			default: break;
			
			case 6:
				ula->nmiIsEnabled = ZX80ULA_YES;
			break;
			case 5:
				ula->nmiIsEnabled = ZX80ULA_NO;
			break;
		}

	// if all three of the lowest bits are set then this is 
	if((address&7) == 7)
	{
		if(!ula->nmiIsEnabled)
		{
			ula->lineCounter = 0;
			ula->vsyncIsActive = ZX80ULA_NO;
			llzx80ula_considerSync(ula);
		}
	}

}
sirmorris
Posts: 2811
Joined: Thu May 08, 2008 5:45 pm

Re: My new emulator

Post by sirmorris »

I believe that you'll have a much easier time when it comes to adding peripherals if the memory and IO systems get their own observers. Multiple memory observers need to be able to be present simultaneously.

C
Thommy
Posts: 52
Joined: Wed Sep 28, 2011 6:37 pm

Re: My new emulator

Post by Thommy »

sirmorris wrote:I believe that you'll have a much easier time when it comes to adding peripherals if the memory and IO systems get their own observers. Multiple memory observers need to be able to be present simultaneously.
There's no 1:1 mapping of observers; the Z80 (which can do partial bus management but would work equally well with an external agent driving everything) can accept arbitrarily many observers and observers simply nominate those lines they're interested in, which of those they want to be notified only upon specific values of and — in that case — the values they're interested in.

The ULA-as-defined-to-include-memory therefore installs observers but if anyone else were to install an observer as well then everything would just work. For example, note that the ULA simply fails to react to IO requests that it wouldn't react to. If there were also a ZonX agent or whatever that had installed agents to react to its IO requests then the natural hardware rules would take effect naturally — an IO request that both respond to will be responded to by both, an IO request that either responds to individually will be responded to by that one and an IO request that isn't relevant to either will be ignored by both.

The only deviation from real hardware expectations it that the Z80 explicitly isn't meant to be a simulation of a bus too, so it's just expecting to be told the latest values of its input pins. A real bus agent, as yet unwritten, would obviously require semantics according to which things sitting beyond the bus are either actively loading a value onto the bus or aren't doing anything at all. If multiple devices are loading at once then you'd want to combine the values as per the original hardware (which seems almost always to be open collector on machines I've read descriptions of).

So there's a very real extent to which what I have at present is a stepping stone but it isn't anywhere near as limited as you might assume. It's also capable of being a 100% accurate line-level emulation of the closed-box ZX80 and ZX81 and requiring the addition of only a couple of extra calls (to surrender the bus at appropriate moments) for adaptation to an explicit bus agent. It also directly and natively describes ZX80 and 81 logic as is — notice that the ULA isn't falsely required to have knowledge of Z80 timings and that the Z80 has no idea that it's acting as part of a Z80 and not, say, a contended-memory CPC or Spectrum, or cooperating with a 68000 as in a Mega Drive. I therefore think it's a reasonable way to proceed.

Further for the curious, this is a segment from my setup code for the ULA:

Code: Select all

		// observe interrupt acknowldge (which is M1 + IOREQ active)
		llz80_addSignalObserverWithActiveCondition(
				ula->CPU,
				llzx80ula_observeIntAck,
				LLZ80SignalMachineCycleOne | LLZ80SignalInputOutputRequest,
				ula);

		// observe entry into refresh
		llz80_addSignalObserverWithActiveCondition(
				ula->CPU,
				llzx80ula_observeRefresh,
				LLZ80SignalRefresh,
				ula);

		// observe memory read requests ...
		llz80_addSignalObserverWithActiveCondition(
				ula->CPU,
				llzx80ula_observeMemoryRead,
				LLZ80SignalMemoryRequest | LLZ80SignalRead,
				ula);

		// ... and write requests
		llz80_addSignalObserverWithActiveCondition(
				ula->CPU,
				llzx80ula_observeMemoryWrite,
				LLZ80SignalMemoryRequest | LLZ80SignalWrite,
				ula);

		// observe IO read requests ...
		llz80_addSignalObserverWithActiveCondition(
				ula->CPU,
				llzx80ula_observeIORead,
				LLZ80SignalInputOutputRequest | LLZ80SignalRead,
				ula);

		// ... and write requests
		llz80_addSignalObserverWithActiveCondition(
				ula->CPU,
				llzx80ula_observeIOWrite,
				LLZ80SignalInputOutputRequest | LLZ80SignalWrite,
				ula);
sirmorris
Posts: 2811
Joined: Thu May 08, 2008 5:45 pm

Re: My new emulator

Post by sirmorris »

I never assumed it was limited :)

How would you handle external hardware suppressing the internal ROM? Does the ula code provide and honour - for the sake of agument - ROMCS and RAMCS?

C
Thommy
Posts: 52
Joined: Wed Sep 28, 2011 6:37 pm

Re: My new emulator

Post by Thommy »

sirmorris wrote:I never assumed it was limited :)

How would you handle external hardware suppressing the internal ROM? Does the ula code provide and honour - for the sake of agument - ROMCS and RAMCS?

C
No, not yet. Following my own rules, the plan is to proceed as is to provide a 100% bus accurate simulation of a sealed ZX80 and 81. So at that point I'll be confident that the Z80 is overwhelmingly correct and that I have a full understanding and a valid expression of the ZX80 and 81 logic, albeit with bus releases implicit.

At that point I'll add an explicit bus actor and modify the ULA to explicitly stop loading the bus at appropriate moments, which will probably just mean it additionally observing when relevant lines go inactive. At that point it'll be an option to split the RAM and ROM off, but I'll see what effect that has from a performance point of view. At this point I'll add those signals that are present on the expansion connector but not on the Z80 bus.

Although the practicalities of such a thing would probably slow the emulator to a crawl because it'd require completely synchronous operation across some sort of connection with latency, since the idea has been suggested I've come around to the idea that anything short of being able to support a hardware add-on that exposes the ZX80 and ZX81's original bus for the connection of real hardware is a failure to reach the objectives I've set for myself.
sirmorris
Posts: 2811
Joined: Thu May 08, 2008 5:45 pm

Re: My new emulator

Post by sirmorris »

Although the practicalities of such a thing would probably slow the emulator to a crawl because it'd require completely synchronous operation across some sort of connection with latency, since the idea has been suggested I've come around to the idea that anything short of being able to support a hardware add-on that exposes the ZX80 and ZX81's original bus for the connection of real hardware is a failure to reach the objectives I've set for myself.
Eh? I'm just talking about emulating in software, nothing in hardware...
Thommy
Posts: 52
Joined: Wed Sep 28, 2011 6:37 pm

Re: My new emulator

Post by Thommy »

sirmorris wrote:Eh? I'm just talking about emulating in software, nothing in hardware...
But in this case the software is predicated on exactly simulating the physical lines between components, so the distinction isn't particularly relevant. I'm not saying it's a realistic or a practical thing to do, but it's a good intellectual test of accuracy and highlights the difference between this approach and that of previous emulators.

Your computer's activity monitor and/or fans will probably be the other thing that demonstrates the difference. Describing it as better emulation by throwing massively increased processing resources at the problem would be accurate, I think.
Post Reply