Traditionally, computer memory was only accessible by a single part of the computer’s architecture at any one time. If your display device needed to access the video data in RAM, then your architecture had to arrange that the CPU didn’t access memory at the same time.
This was usually performed by ensuring that the CPU could only access memory at very specific times that were guaranteed not to clash with the video circuitry. This had the effect that the CPU in the system typically ran at a speed half that of the main system clock.
Sinclair, in their usual innovative way, came up with an ingenious solution to this problem, which was to have the ULA generate the CPU clock. When the ULA needed to do a video read and it detected that the CPU was reading memory from the lower RAM, the ULA simply halted the CPU clock until its video fetch cycle was complete.
This approach has two main benefits, firstly that the CPU is only slowed down by the minimum amount of time that is needed for the ULA to do its job, secondly when code is running or data is being fetched from the upper RAM area (which the ULA does not need to touch), the CPU can run at full speed.
You’ll often hear the lower and upper RAM being described as ‘contended’ and ‘uncontended’ respectively. This simply refers to the fact that the ULA overrides the CPU for access to the lower RAM at times, whereas the CPU has exclusive access to the upper RAM.
So, onto the floating bus itself. When the CPU needs to perform an I/O access (either read or write), the ULA stops the CPU if the ULA port at 0xFE (or any even numbered port) is accessed, as the ULA would need to use the data bus to perform I/O at the same time it also needs it to read video data. Odd addresses are uncontended as the ULA is not involved.
There is always an exception though – on Issue 1 and 2 machines that do not have the ‘spider mod’ installed, the behaviour of the ULA is to contend all I/O access. This is important, as we’ll see later.
So when the CPU tries to read from a non-existent I/O port (let’s say 0xFF), there is no hardware to provide a result, so the CPU would expect to read 0xFF, and indeed this is what happens on the +2A and +3 models as the video and CPU buses are kept separate by proper tri-state buffers.
Back when the original 48K Spectrum was designed though, such expense was frowned upon, so the buses were kept separate by 470 ohm resistors on each data line. This was enough to allow the data lines to be properly driven by devices on each side of the bus without causing any inputs or outputs to be shorted low or high.
In this case, if the CPU tries to read a value from 0xFF, instead of reading nothing, if the ULA is fetching data from RAM for use by the ULA’s video output then this data will ‘leak’ across the resistors and will be read by the CPU!
Some games took advantage of this quirk – instead of synchronising to the interrupt, they read from port 0xFF until, thanks to the floating bus, they started reading the contents of video RAM. They could then start rendering content to screen top to bottom, safe in the knowledge that the render would be happening behind the screen update, and so avoid screen ‘tearing’.
Several games ended up being broken by the changes to the +2A/+3 bus separation circuits – as the usual floating bus behaviour was not present on these machines, the game code ended up looping forever, waiting for video data that would never appear. Fixed versions had to be released in some cases (most notably Arkanoid).
Finally, I noted earlier the overly aggressive I/O contention on Issue 1 and 2 machines without the spider mod installed. This has the effect of removing the floating bus effect entirely, as in this case an I/O read from any port will be contended until the ULA has finished its video read. Therefore there will never be any spurious video data on the data bus to read!
A small footnote about floating bus behaviour on +2A and +3 machines – while the effect is no longer present on unused ports, it is still evident when reading from ports which match a particular addressing pattern, expressed as (1+n*4), or in binary, 0000 xxxx xxxx xx01. This is only evident when the memory paging ports are unlocked, otherwise they will always return FF.