Skip to content

Commit

Permalink
[#314] Z80 frequency updates, ZXspectr contention fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
vbmacher committed May 5, 2023
1 parent 27ce6e3 commit ff2de41
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public final class ContextZ80Impl implements ContextZ80 {
private final TimedEventsProcessor tep = new TimedEventsProcessor();

private volatile EmulatorEngine engine;
private volatile int clockFrequency = DEFAULT_FREQUENCY_KHZ;
private volatile int clockFrequencyKHz = DEFAULT_FREQUENCY_KHZ;

public void setEngine(EmulatorEngine engine) {
this.engine = engine;
Expand Down Expand Up @@ -98,15 +98,15 @@ public void signalInterrupt(byte[] data) {

@Override
public int getCPUFrequency() {
return clockFrequency;
return clockFrequencyKHz;
}

@Override
public void setCPUFrequency(int frequency) {
if (frequency <= 0) {
throw new IllegalArgumentException("Invalid CPU frequency (expected > 0): " + frequency);
}
clockFrequency = frequency;
clockFrequencyKHz = frequency;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
Expand Down Expand Up @@ -203,8 +204,11 @@ public CPU.RunState run(CPU cpu) {
// in 1 millisecond time slice = 1 / 0.00025 = 4000 t-states are executed uncontrollably

long timeSliceNanos = SleepUtils.SLEEP_PRECISION;
int timeSliceMillis = (int) Math.ceil(timeSliceNanos / 1000000.0);
int cyclesPerTimeSlice = timeSliceMillis * context.getCPUFrequency();
double timeSliceMicros = timeSliceNanos / 1_000.0;
int cyclesPerTimeSlice = (int) (timeSliceMicros * context.getCPUFrequency() / 1000.0); // frequency in kHZ -> MHz

//System.out.println("Time slice millis: " + timeSliceMillis);
//System.out.println("Cycles per time slice: " + cyclesPerTimeSlice);

currentRunState = CPU.RunState.STATE_RUNNING;
while (!Thread.currentThread().isInterrupted() && (currentRunState == CPU.RunState.STATE_RUNNING)) {
Expand Down Expand Up @@ -248,7 +252,13 @@ private int dispatch() throws Throwable {
lastQ = Q;
Q = 0;
if (pendingNonMaskableInterrupt.getAndSet(false)) {
writeWord((SP - 2) & 0xFFFF, PC);
if (memory.read(PC) == 0x76) {
// jump over HALT
writeWord((SP - 2) & 0xFFFF, (PC + 1) & 0xFFFF);
} else {
writeWord((SP - 2) & 0xFFFF, PC);
}

SP = (SP - 2) & 0xffff;
PC = 0x66;
return 12;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,18 @@

// Pulse length:
// https://worldofspectrum.org/faq/reference/48kreference.htm
// https://softspectrum48.weebly.com/notes/tape-loading-routines

//Machine Pilot pulse Length Sync1 Sync2 Bit 0 Bit 1
//ZX Spectrum 2168 (1) 667 735 855 1710
//ZX Spectrum 2168 (1) 667 735 855 1710

// Tape data is encoded as two 855 T-state pulses for binary zero, and two 1,710 T-state pulses for binary one.

// To distinguish header blocks from data blocks, a sequence of leader pulses precedes each type of block.
// The leader pulse is 2,168 T-states long and is repeated 8,063 times for header blocks and 3,223 times for data blocks.
//
// After the leader pulses, two sync pulses (667 T-states plus 735 T-states long) follow to signal the beginning of the
// actual data.
public class PlaybackListenerImpl implements Loader.PlaybackListener {
private final DeviceContext<Byte> bus;
private final AtomicReference<CassettePlayerGui> gui = new AtomicReference<>();
Expand Down Expand Up @@ -81,4 +90,8 @@ public void onStateChange(CassetteController.CassetteState state) {
private void log(String message) {
Optional.ofNullable(gui.get()).ifPresent(g -> g.setMetadata(message));
}

private void pulse(boolean active, int howLong) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,36 +64,47 @@
* tstates and continues for all 192 lines of screen data. After this, there is no delay until the end of the
* frame as the bottom border and vertical refresh happen, and no delay until 14335 tstates after the start of
* the next frame as the top border is drawn.
* <p>
* Contended I/O
* High byte | |
* in 40 - 7F? | Low bit | Contention pattern
* ------------+---------+-------------------
* No | Reset | N:1, C:3
* No | Set | N:4
* Yes | Reset | C:1, C:3
* Yes | Set | C:1, C:1, C:1, C:1
*/
@NotThreadSafe
public class ZxSpectrumBusImpl extends AbstractMemoryContext<Byte> implements ZxSpectrumBus {
private final static int SCREEN_LINES = 192;
private final static int CONTENTION_TSTATE_START = 14335;
private static final int CPU_INTERRUPT_TSTATES = 69888;

private ContextZ80 cpu;
private MemoryContext<Byte> memory;
private volatile byte busData; // data on the bus
private TimedEventsProcessor ted;
private boolean lastMemoryContended = false;
private TimedEventsProcessor tep;
private boolean isContended = false;

private final Map<Integer, Context8080.CpuPortDevice> deferredAttachments = new HashMap<>();


public void initialize(ContextZ80 cpu, MemoryContext<Byte> memory) {
this.cpu = Objects.requireNonNull(cpu);
this.memory = Objects.requireNonNull(memory);
this.ted = cpu.getTimedEventsProcessor().orElseThrow(() -> new RuntimeException("CPU must provide TimedEventProcessor"));
this.tep = cpu.getTimedEventsProcessor().orElseThrow(() -> new RuntimeException("CPU must provide TimedEventProcessor"));

for (Map.Entry<Integer, Context8080.CpuPortDevice> attachment : deferredAttachments.entrySet()) {
// TODO: contended device proxy if needed
if (!cpu.attachDevice(attachment.getKey(), attachment.getValue())) {
if (!cpu.attachDevice(attachment.getKey(), new ContendedDeviceProxy(attachment.getValue()))) {
throw new RuntimeException("Could not attach device " + attachment.getValue().getName() + " to CPU");
}
}

// 17 x from 14335 to 14463, then 96 tstates pause to reach "end of line", then repeat.
// the t-state 14335 is the first screen line to be read.
int cycles = 14335;
for (int line = 0; line < 192; line++) {
int cycles = CONTENTION_TSTATE_START + CPU_INTERRUPT_TSTATES;
for (int line = 0; line < SCREEN_LINES; line++) {
scheduleMemoryContention(cycles);
cycles = cycles + 16 * 8 + 96;
cycles = cycles + 17 * 8 + 96 - 1;
}
}

Expand All @@ -104,7 +115,7 @@ public void attachDevice(int port, Context8080.CpuPortDevice device) {
deferredAttachments.put(port, device);
} else {
// TODO: contended device proxy if needed
if (!cpu.attachDevice(port, device)) {
if (!cpu.attachDevice(port, new ContendedDeviceProxy(device))) {
throw new RuntimeException("Could not attach device " + device.getName() + " to CPU");
}
}
Expand Down Expand Up @@ -195,18 +206,48 @@ public MemoryContextAnnotations annotations() {
}

private void setContended(int location) {
this.lastMemoryContended = location >= 0x4000 && location <= 0x7FFF;
this.isContended = location >= 0x4000 && location <= 0x7FFF;
}

private void scheduleMemoryContention(int startCycles) {
// 17 x from 14335 to 14463, then 96 tstates pause to reach "end of line", then repeat.
Runnable slowDownByOneCycle = () -> {
if (isContended) {
cpu.addCycles(1);
}
};

for (int i = 0; i < 17; i++) {
for (int j = 0; j < 6; j++) {
ted.schedule(startCycles + i * 8 + j, () -> {
if (lastMemoryContended) {
cpu.addCycles(1);
}
});
tep.schedule(startCycles + i * 8 + j, slowDownByOneCycle);
}
}
}

private class ContendedDeviceProxy implements Context8080.CpuPortDevice {
private final Context8080.CpuPortDevice device;

private ContendedDeviceProxy(Context8080.CpuPortDevice device) {
this.device = Objects.requireNonNull(device);
}

@Override
public byte read(int portAddress) {
setContended(portAddress);
// TODO: portAddress & 1 == 0 ==> contended for 3 cycles only
return device.read(portAddress);
}

@Override
public void write(int portAddress, byte data) {
setContended(portAddress);
// TODO: portAddress & 1 == 0 ==> contended for 3 cycles only
device.write(portAddress, data);
}

@Override
public String getName() {
return device.getName();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@ public class DisplayCanvas extends Canvas implements AutoCloseable {
private volatile Dimension size = new Dimension(0, 0);

private final ULA ula;
private final TimedEventsProcessor ted;
private final TimedEventsProcessor tep;
private final PaintCycle paintCycle = new PaintCycle();
private int interrupts = 0;

public DisplayCanvas(ULA ula) {
this.ula = Objects.requireNonNull(ula);
this.ted = ula.getTimedEventsProcessor();
this.tep = ula.getTimedEventsProcessor();
this.screenImage.setAccelerationPriority(1.0f);
this.screenImageData = ((DataBufferInt) this.screenImage.getRaster().getDataBuffer()).getData();
}
Expand All @@ -96,11 +96,11 @@ public void start() {
if (painting.compareAndSet(false, true)) {
createBufferStrategy(2);

ted.schedule(REPAINT_CPU_TSTATES, this::triggerCpuInterrupt);
ted.schedule(REPAINT_CPU_TSTATES, ula::onNextFrame);
tep.schedule(REPAINT_CPU_TSTATES, this::triggerCpuInterrupt);
tep.schedule(REPAINT_CPU_TSTATES, ula::onNextFrame);
for (int i = 0; i < SCREEN_IMAGE_HEIGHT; i++) {
int finalI = i;
ted.schedule(i * LINE_CPU_TSTATES + 1, () -> drawNextLine(finalI));
tep.schedule(i * LINE_CPU_TSTATES + 1, () -> drawNextLine(finalI));
}
}
}
Expand Down Expand Up @@ -187,10 +187,10 @@ public void setBounds(Rectangle r) {

@Override
public void close() {
ted.remove(REPAINT_CPU_TSTATES, paintCycle);
tep.remove(REPAINT_CPU_TSTATES, paintCycle);
for (int i = 0; i < SCREEN_IMAGE_HEIGHT; i++) {
int finalI = i;
ted.remove(i * LINE_CPU_TSTATES, () -> drawNextLine(finalI));
tep.remove(i * LINE_CPU_TSTATES, () -> drawNextLine(finalI));
}
painting.set(false);
}
Expand Down

0 comments on commit ff2de41

Please sign in to comment.