While working on Snow, my vintage Macintosh emulator, I came across an interesting conflict in expectations of applications with regards to open bus behavior on these systems. In this article I’ll explain what open bus behavior is, how a compact Macintosh (the 128K, 512K, Plus, SE and Classic) behave and how this affects some applications.

The basics: what is a bus?

In digital electronics, a bus is a series of traces or wires that connect several peripherals together. The topology of a bus is basically one long line on which devices ‘hook in’.

By NetworkTopologies.png: Maksim / derivative work: Malyszkz - Own work based on: NetworkTopologies.png, Public Domain, https://commons.wikimedia.org/w/index.php?curid=15006915

On an electronic circuit board, such as the compact Mac’s logic board, the devices are usually different ICs. The CPU (the Motorola 68000) initiates all communication in the case of the Macintosh, but this is not necessarily always the case (in a ‘multi-master’ bus concept, multiple devices may initiate communication, usually gated by a mechanism to prevent multiple devices initiating at the same time).

The compact Mac makes use of what is called an ‘address/data bus’. On the PCB level, this is basically a set of 23 address lines and 16 data lines. When the processor wants to read/write to an address in memory or mapped to a device, it outputs the requested address on the address lines. When writing, the CPU puts the data on the 16 data lines. Then, it sends a read or write strobe on a different pin. The device mapped to that address will then respond on the data bus, or will store the data the CPU is presenting on the data bus.

This process is subject to timing constraints as described in the MC68000 User Manual:

MC68000 Read and Write-Cycle Timing Diagram

In the compact Macintosh, all components are so-called ‘memory mapped’ to the CPU. This means that on the address/databus there is not just memory, but also other devices, such as the VIA, floppy drive controller, serial controller, SCSI controller, etc.

To communicate with these devices, the CPU reads and writes from specific addresses. The address regions to which the devices are mapped are documented in so-called address maps. For example, here’s the address map for the Macintosh Plus:

Macintosh Plus address map

What if noone responds?

You will notice that there are holes in the address map, saying either ‘No device assigned’ or ‘Reserved’. This is where things get interesting.

The MC68000 has a mechanism for devices to acknowledge bus transfers, using the /DTACK and /VPA pins, as seen in the timing diagram above as well. If a device does not acknowledge the transfer, the CPU can generate an exception which the software can act upon.

However, on the compact Macintosh, the electronics around the CPU contain logic that generates these signals automatically, for every address:

DTACK/VPA note in guide to the Macintosh Family Hardware

So in this case, the CPU will always receive the ‘transfer successful’ signal, regardless of whether a device actually produced data, or not.

A digital signal has three different states: low, high and tri-state. If a device wants to signal a 1, it will pull that signal high (towards 3.3V or 5V, usually). If a device wants to signal a 0, it will pull that signal low (towards ground). If noone does anything, the signal is in a state we call ‘tri-state’. If the CPU requests an address in an unallocated region, the data bus remains tri-stated, but the computer’s logic will still acknowledge the bus transaction and the CPU will read these tri-stated signals as data.

In the digital world, there is no such thing as ‘tri-state’. There is only 1 or 0. To understand this, we need to look at what happens to these signals on the analog level. In the analog world, there’s no 1 or 0 reaching the CPU pin, it is a voltage. The CPU will read it as 0 or 1 based on voltage thresholds. These are specified in the MC68000 User Manual:

MC68000 input DC characteristics

You’ll notice there’s a band which is neither high, nor low. If the voltage is in this band, whatever you read in the digital world is undefined and can either be 1 or 0.

One last characteristic of any piece of wire or trace we need to take into account is parasitic capacitance. A copper signal trace on a circuit board generally has some capacitance (it can store an electric charge, like a capacitor, for a short amount of time). This means that when the signal is tri-stated, it may still retain some voltage if it was driven high shortly before!

What does this look like in the digital world?

This is easy to observe on a Mac using the Microbug debugger which is built into all the compact Macs and can be invoked by pushing the ‘programmers key’ or ‘interrupt key’ on the side of the machine.

Macintosh Classic II Programmers key - by ksfan, CC BY 2.5

When the debugger pops up, you can make the CPU read from the address/data bus using the DM command. If you read some data from an unassigned region, it comes up with this:

DM FFFFC0 in Microbug on the Macintosh 512K

Whoa! What are those 6116 sequences? Earlier we learned, that this is the data that was on the bus from the last bus transaction, still present due to parasitic capacitance.

But why 6116 here, specifically? To figure that out, we need to take a look at the routine in Microbug, which is located in the Macintosh Plus ROM, that retrieves and prints the data it reads (P_DbgCmd_DM):

401592   2F3C 0070 01B0            Move.L    #$7001B0, -(A7)
401598   42A7                      Clr.L     -(A7)
40159A   61EC                      Bsr.B     P24
40159C   6100 011E                 Bsr       P30
4015A0   0880 0000                 BClr.B    #$0, D0
4015A4   2640                      Move.L    D0, A3
4015A6   23C0 003F FCF8            Move.L    D0, ($3FFCF8)
4015AC   7C05                      MoveQ.L   #$5, D6
4015AE   7A0F                      MoveQ.L   #$F, D5
4015B0   0645 000F      L195:      Add       #$F, D5
4015B4   3F3C 000A                 Move      #$A, -(A7)
4015B8   3F05                      Move      D5, -(A7)
4015BA   A893                      _MoveTo
4015BC   200B                      Move.L    A3, D0
4015BE   6122                      Bsr.B     P25
4015C0   7807                      MoveQ.L   #$7, D4
4015C2   7623                      MoveQ.L   #$23, D3
4015C4   0643 002D      L196:      Add       #$2D, D3
4015C8   3F03                      Move      D3, -(A7)
4015CA   3F05                      Move      D5, -(A7)
4015CC   A893                      _MoveTo
4015CE   301B                      Move      (A3)+, D0
4015D0   6116                      Bsr.B     P26
4015D2   51CC FFF0                 DBF       D4, L196
4015D6   51CE FFD8                 DBF       D6, L195
4015DA   23CB 003F FCFC            Move.L    A3, ($3FFCFC)
4015E0   4E75                      Rts

A3 contains the address that contains the next data word to be printed. The instruction at 4015CE loads the word into D0. In the test, A3 contains FFFFF0, which the CPU outputs as address during the read, noone responds, and the CPU reads back 6116 from the residual signals from the previous access on the data bus.

But why 6116? You’ll notice that 6116 is actually the encoded instruction on address 4015D0, which comes after the move instruction we’ve been looking at at 4015CE. The reason why the next instruction shows up here is because the MC68000 has a prefetch queue. The prefetch queue is two words long and the instructions at 4015CE and 4015D0 are both one word long, so the Bsr.B instruction at 4015D0 was read into the queue just before the Move instruction began executing, explaining why we’re seeing 6116 as open bus data.

How can this be emulated?

First of all, to properly emulate this behavior, an emulator should emulate the MC68000 with bus cycle accuracy - meaning the emulator needs to execute every bus transaction of every instruction in the exact same sequence and timing as a real MC68000. Prefetch also needs to be emulated accurately. Fortunately, Snow already achieves this level of accuracy and its CPU core passes the test suite based on a MC68000 microcode simulation.

Once that is achieved, it is just a matter of storing the data from the last successful bus transaction, whether it is read or write. If the CPU reads from an address where no device is present, simply return that stored last value, instead. With that implemented, Snow now generates the same pattern as real compact Mac hardware.

Open bus emulation in Snow

The effect on software

There are many documented examples of software on various systems that rely on open bus behavior, either intentionally or unintentionally. A known example is Donkey Kong Country 2 which broke on the ZSNES emulator due to incorrectly emulated open bus behavior.

On the Macintosh, I found two applications that presented contradicting behavior when I made changes to open bus behavior emulation..

The Print Shop 1.0

The Print Shop 1.0, running in Snow

This first version of The Print Shop by Broderbund came to me as an image of the original floppy, including the copy protection. At the time, neither Snow, nor MAME could run this version from the original floppy. The application would just exit back to the Finder. Initially, I was suspecting the copy protection.

The copy protection on this disk reads a specially crafted track on the floppy which is written with flux transitions at a faster interval. This track looks like this when viewed in Applesauce:

Print Shop 1.0 protection track

For reference, a normal track:

Print Shop 1.0 normal track

Note that the three lines (which are transition times that create bit sequences of 1, 01 and 001 on the disk) are lower (shorter transition times) on the protection track. When the copy protection reads the track, it first reconfigures the IWM (the Mac’s floppy controller) to 7 MHz mode (instead of 8 MHz). It then reads the bit sequences from the protection track. If the disk is a copy created using a normal copying tool, the bit sequences wouldn’t have been properly copied because they need the IWM configured to 7 MHz mode first, which a copy tool wouldn’t do.

This was the first time I had encountered this type of protection so I first suspected an issue in Snow’s IWM. However, when I investigated the IWM output, it seemed the bit sequences were coming out properly (identically as decoded by Applesauce).

Eventually, I noticed the following code running after the copy protection check:

01F52C  3280      MOVE.w D0,(A1) ; D0 = $0001, A1 = $FFFFFFFE
...
01D1C8  41F8FFFE  LEA $FFFE,A0
01D1CC  3E10      MOVE.w (A0),D7
01D1CE  66000004  BNE.w $01D1D4
01D1D2  A9F4      LINEA [$A9F4] ; ExitToShell

This code writes 1 to $FFFFFE, then reads from $FFFFFE and exits the application if the result is 0. Given that the $FFFFFE address is explicitly loaded into A0 by the LEA instruction, it looks like this is a deliberate check, rather than e.g. stack corruption due to an emulator bug. I don’t know the purpose of this check. It doesn’t seem to be an anti-debugging measure because it runs after the copy protection code.

In any case, if the result of the open bus read is 0, the application exits. This is the reason why The Print Shop 1.0 did not run on Snow (and MAME) before, but now runs on Snow after implementing proper open bus emulation.

Animation Toolkit 1.0

Animation Toolkit 1.0, running in Snow

For Animation Toolkit 1.0, I initially received a bug report about the application ‘bombing’ with ID=02 on Snow, which indicates an unhandled address error exception.

Snow showing an address error

This only occurred on the Macintosh Plus and newer. On the Macintosh 512K, for which the application was originally developed, the crash does not occur. To add to the puzzle, the crash only occurred on a Macintosh Plus with 4MB of RAM (the largest possible configuration and the default on Snow); reducing the amount of emulated RAM and the application starts without crashing. On MAME however, the application started up without issue, even on an emulated 4MB Plus, leading me to believe there was a bug in Snow.

The path of the crash is as follows:

3E0FB6  302DDD4A      MOVE.w ($FFFFDD4A,A5),D0 ; D0 = FFFFFF9A, A5 = 003FA400
3E0FBA  E540          ASL.w #2,D0 ; D0 = FFFFFF9A
3E0FBC  5840          ADDQ.w #$04,D0 ; D0 = FFFFFE68
3E0FBE  48C0          EXT.l D0 ; D0 = FFFFFE6C
3E0FC0  2F00          MOVE.l D0,-(A7) ; D0 = FFFFFE6C
3E0FC2  4EAD00BA      JSR ($00BA,A5) ; D0 = FFFFFE6C, A5 = 003FA400
3FA4BA  4EF90000E822  JMP $0000E822
00E822  225F          MOVEA.l (A7)+,A1
00E824  201F          MOVE.l (A7)+,D0 ; A1 = 003E0FC6
00E826  A122          LINEA [$A122] ; NewHandle, D0 = FFFFFE6C
...NewHandle runs...
401FAA  4E75          RTS ; NewHandle returns, D0 = -108, A0 = 00000000

Let’s look at the reference for NewHandle:

NewHandle, Inside Macintosh

Assuming LogicalSize is unsigned (signed wouldn’t make sense either), it’s trying to allocate almost 4 gigabytes of memory! That’s not going to fit on the Mac’s heap, so NewHandle properly returns memFullErr (-108). Applications are supposed to check this return value, but Animation Toolkit does not and happily stores A0, which is NIL, on the stack.

I don’t know how and why Animation Toolkit ends up requesting this amount of memory. I speculate it is a bug that remained unnoticed when the program was developed since it wasn’t until 1986 that the Macintosh Plus was released.

Initialization continues, until the NIL-references comes back and gets dereferenced to obtain a pointer, dereferenced again and then fatally passed into HLock:

3E06C6	206DFB98	    MOVEA.l ($FFFFFB98,A5),A0
3E06CA	2050	        MOVEA.l (A0),A0 ; A0 = 00000000
3E06CC	2F10	        MOVE.l (A0),-(A7) ; A0 = 00F80000
3E06CE	4EAD007A	    JSR ($007A,A5)
3FA47A	4EF90000E84C	JMP $0000E84C
00E84C	225F	        MOVEA.l (A7)+,A1
00E84E	205F	        MOVEA.l (A7)+,A0
00E850	A029	        LINEA [$A029] ; HLock, A0 = <open bus value>
...lots of stuff happens...
410270	2011	        MOVE.l (A1),D0

You’ll notice that at 3E06CC, the address F80000 gets dereferenced, producing the open bus value and that gets dereferenced again inside HLock. Let’s confirm first that F80000 is an open bus region on a real Mac:

F80000 debugger dump

Looks like it! Let’s also look at the reference for HLock:

HLock, Inside Macintosh

On MAME, open bus produces 00. This is why the application works there, because HLock on 000000 produces a proper error (nilHandleErr) after which the application startup stumbles forward and the application eventually ends up running.

Bug-accurate emulation

The final thing to do was to test what actually happens on real hardware. A real Macintosh Plus with 4MB RAM gave the final verdict:

Macintosh Plus showing an address error

Snow is actually emulating the application properly, including the fact it crashes when trying to run it on a Plus with 4MB RAM.

Snow showing an address error

There are likely many more examples of applications that crash or exhibit strange behavior or crashes under emulation. To be able to create an emulator as accurate as possible to real hardware, we need to keep verifying we’re recreating exactly what happens on that hardware, even if it means the behavior we’re recreating results in a crash that we could’ve prevented.

References

  • Motorola - MC68000 User Manual
  • Apple Computer, Inc. - Guide to the Macintosh Family Hardware
  • Apple Computer, Inc. - Inside Macintosh Volume II

Thanks to Doctorgianlu, Reza Fouladian, KenDesigns and Andy for providing bug reports, input and testing