Skip to content →

Timings – state machine, bus sharing and oddities

So, for our C64 to work properly, we need to be very precise. A lot more precise than our 6502 for q00.nes. The Commodore 64 was pushed to its limits, that is why we need to account for that.

Double Raster Interrupt

Rasterbars are an effect that is often used in C64 games and demos, that makes use of a technique called double raster interrupt.
This method will be explained in more details later on. For now, it is important to know that this technique makes use of cycle counting, to be able to change colors at a time, where the raster beam is out of sight, so we don’t see the [color] break in the line.
If we use this multiple times, with selected colors, we are able to create the neat raster bars you can see in the image above.

We know, a complete row of the raster beam takes 63 cycles, 8 pixels per cycle, totalling in 504 pixels per row. To be able to count cycles, we need to be able to predict at which cycle we are. Long story short, the double raster interrupt comes to a point, where it checks if we are still in line N or already in line N+1 (and adds, or doesn’t add an additional cycle, predicting the same cycle position every time after the instruction is done).

For this check we read the current raster line and compare it to the line we are targeting. The CMP operation in this method takes up 4 cycles. If we start in line N though, the old 6502/6510 implementation shows its flaws – because there we execute the opcode immediately, thus comparing to the wrong row. The row changes while the CMP is being executed, therefore we need to have subinstruction accuracy in our CPU, to be able to detect the rowchange.

Double raster interrupt – CMP going over raster line change

State Machine

This means, our 6510 is supposed to be a state machine, thus representing the appropriate reads and writes in the according cycles.

void _RTI(uint8_t state) {
	switch (state)
	{
	case 1:	break;
	case 2:	break;
	case 3:	SP_++; break;
	case 4:	status.setStatus(readFromMem(SP_ + 0x100));	SP_++; break;
	case 5:	PC_L = readFromMem(SP_ + 0x100); SP_++;	break;
	case 6:	PC_H = readFromMem(SP_ + 0x100); PC = (PC_H << 8) | PC_L; fr = 0; break;
	default: break;
	}
}

In case you are lazy, you might even get away with just waiting the appropriate amount of cycles, before executing the whole block – even though there may be edge cases where this will not work out.

void _LAX(uint8_t state, ADDR_MODE mode) {
	switch (state)
	{
	case 1:	break;
	case 2:	break;
	case 3: if		(mode == ADDR_MODE::ZEROPAGE)	{ _mLAX(getZeropage(PC)); PC += 2; fr = 0; } break;
	case 4: if		(mode == ADDR_MODE::ABSOLUT)	{ _mLAX(getAbsolute(PC)); PC += 2; fr = 0; }
			else if (mode == ADDR_MODE::ZEROPAGE_Y) { _mLAX(getZeropageYIndex(PC, registers.Y)); PC += 2; fr = 0; } 
			else if (mode == ADDR_MODE::ABSOLUT_Y)	{ 
				uint16_t _t = ((readFromMem(PC + 2) << 8) | readFromMem(PC + 1));
				if ((_t & 0xff00) == ((_t + registers.Y) & 0xff00)) {	//	PB not crossed
					_mLAX(getAbsoluteYIndex(PC, registers.Y));
					PC += 2;
					fr = 0;
				}
			}
	break;
	case 5: if		(mode == ADDR_MODE::ABSOLUT_Y)	{ _mLAX(getAbsoluteYIndex(PC, registers.Y)); PC += 2; fr = 0; } break;
	default: break;
	}
}

VIC and 6510 bus sharing

At some point, when the CPU is accurate enough, the next thing, timing-wise, we need to account for is that the VIC and the 6510 share a bus – and this comes with some oddities.

Normal scan line, 0 sprites 

012345678901234567890123456789012345678901234567890123456789012 cycles  
ggggggggggggggggggggggggggggggggggggggggrrrrr  p p p p p p p p  phi-1 VIC                                                                 
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx phi-2 6510 
63 cycles available 

g = grafix data fetch (character images or graphics data) 
r = refresh 
p = sprite image pointer fetch 
c = character and color CODE fetch during a bad scan line 
s = sprite data fetch 
x = processor executing instructions 
X = processor executing an instruction, bus request pending 

You can see, the VIC and the CPU have access to the bus in the same cycle, just in different phases. Phi-1 (first phase) for the VIC, and Phi-2 (second phase) for the 6510. With the benefits of both elements having access to the bus in the same cycle, come some limitations as well.

Bad lines

The (infamous) badlines of the C64 appear (generally speaking) every 8 lines. On each first rasterline of a new line of characters, the character pointers have to be fetched. And since this fetching takes up additional 40 cycles, the VIC wouldn’t be able to do the fetching, and the rest of his tasks in a single row – therefore it steals cycles from the CPU!

The CPU is being stalled as can be seen in the following diagram:

Bad scan line, 0 sprites 

ggggggggggggggggggggggggggggggggggggggggrrrrr  p p p p p p p p  phi-1 VIC cccccccccccccccccccccccccccccccccccccccc                        phi-2 VIC
                                        xxxxxxxxxxxxxxxxxxxxxxx phi-2 6510 
23 cycles available 

As you can see, the VIC steals the phi-2 phase for additional 40 cycles from the 6510. So in these lines we have less time for our code, and this will effect cycle counting and other effects. So this needs to be representend in the emulator as well.

if (c >= 0 && c <= 10) {
	if (cycles_left == 0) {
		cycles_left = CPU_executeInstruction();
	}
	cycles_left--;
}
else if (c >= 11 && c <= 53) {
	if (!isBadline) {
		if (cycles_left == 0) {
			cycles_left = CPU_executeInstruction();
		}
		cycles_left--;
	}
}
else if (c >= 54 && c <= 62) {
	if (cycles_left == 0) {
		cycles_left = CPU_executeInstruction();
	}
	cycles_left--;
}

There are even more special cases we need to take care of, but this will be discussed once we talk about sprites (sprites steal additional cycles from the CPU as well).

Comments

Leave a Reply