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);
}
}
}