BMOW title
Floppy Emu banner

Phantom Reads

I discovered an interesting BMOW hardware bug today– one that I’m amazed I never noticed before. It involves what I call “phantom reads” causing unexpected system behavior. I only stumbled across the problem by accident, while trying to troubleshoot why the audio system didn’t work in certain circumstances.

In brief, a phantom read operation is being performed on every cycle that memory isn’t explicitly written or read, and the result is thrown away. 99% of the time this is unimportant, because reading from memory or a device doesn’t have any side-effects. Except, in a few cases reads do have side-effects, and bad things happen due to phantom reads.

Every BMOW clock cycle, the CPU will be doing one of three operations:

  • reading from memory into a register
  • writing from a register into memory
  • moving data from one register to another

Memory and registers are on separate busses, connected by a bidirectional bus driver. For reads and writes, the CPU sets the required direction for the bus driver, and puts the desired address on the address bus, activating the needed memory chip or device. But what happens during register-to-register transfers? The bus driver is disabled, completely isolating the register bus from the memory bus. But some value will still be on the address bus, selecting a memory chip or device. What? I had to dig through the schematics and microcode to find the answer.

It turns out that during register-to-register transfers, the address bus will contain whatever’s in the general-purpose address register. Most of the time this will be the address of the most-recently referenced memory location, but because of the way the microcode works, it will sometimes be a combination of bytes from previous addresses and microcode temporary values. So in effect, during register-to-register transfers, a semi-random memory location ends up being read, and the result thrown away. This causes problems in two ways:

  • Interrupt flags for the keyboard, USB, and real-time clock are cleared as a side-effect when they are read. Normally this is a good thing, but a phantom read to one of these locations will clear the interrupt flag without ever servicing the interrupt.
  • Some write-only devices (audio, LCD, video registers) don’t actually distinguish between reads and writes, and perform a write whenever they’re accessed. I made the assumption that the software would only ever write to these locations, so I saved one wire by not bothering to connect the READ/WRITE line. A phantom read to one of these locations will perform a spurious write. Yikes!

So how can I fix this? At this point, I’m planning to leave the hardware as-is, and try to work-around it in software. Now that I know what to look for, I can predict what types of accesses are likely to cause problematic phantom reads, and avoid them by modifying the code slightly. I was able to do this fairly easily to solve the audio problem that led to this discovery in the first place. I’ll apply the same reasoning to some of the code where I’ve experienced intermittent crashes, and see what I can find.

If I had it all to do over again, I’d probably add a new control line from the CPU called MEMORY_ACCESS or something, that was asserted during any read or write operation. Then the address decoding logic could use that to disable all memory and devices when needed, making a clear distinction between intentional reads and phantom reads.

Read 8 comments and join the conversation 

8 Comments so far

  1. Bill Buzbee - March 14th, 2009 6:18 pm

    Wow – Deja Vu. I had the exact same problem with Magic-1. I only realized it when trying to debug problem with the UART – my spurious reads were acknowledging incoming chars before I had a chance to read them. I ended up adding an additional signal (_IOCLK) to suppress the spurious memory accesses. It was a real pain because I had to rework the backplane to convert a spare ground line to a signal line.

  2. Steve - March 14th, 2009 7:45 pm

    Hehe, quite a coincidence! Come to think of it, most commercial CPUs I’m familiar with have a signal like this already. I guess the need for it isn’t at all obvious, until you run into this sort of problem.

    I assume a software work-around wasn’t possible in your case? I’m pretty reluctant to take out the wire wrap tools again. I may be able to craft a better work-around with a microcode change, but I need to research that a little further.

  3. Martin Piper - March 15th, 2009 7:31 pm

    Yup, that’s one of the reasons why I added a tri-state external memory bus. 🙂 When the CPU doesn’t want memory it’s all tri-stated so the RAM/ROM enable and read/write lines are floating to hi which disables them. Of course, being tri-state anything external to the CPU could take-over the bus at that point and read/write its own memory.

  4. Steve - March 16th, 2009 6:09 pm

    I was able to implement a better work-around using microcode, and remove the software work-around I added earlier. It’s still not perfect, but I think it’ll cover 99% of the problems without any further work. I took advantage of the fact that all the devices and interrupt locations are mapped into memory in the address range $003FXX. I modified the microcode to guarantee that 3F is never left in the high byte of the address register after a read or write operation. Unfortunately there’s performance penalty, but not as bad as there was with the software work-around.

    @Martin: I just read through your web site documenting your DIY processor (http://www.wellytop.com/Fnagaton/DIYComputer.html). Very nice! You hit a lot of the areas I hope to work on with my next design, including custom PCBs. Great work! I’ll be following your future progress.

  5. Erik Petrich - March 17th, 2009 9:03 pm

    The HC11 has a similar bus that also assumes that a memory location is being read/written every cycle. Most of the time, that’s true. But for example, if you execute IDIV (integer division) there’s a read to fetch the opcode for the instruction and then 40 something cycles of internal arithmetic. Since there’s no mechanism to indicate that the bus is idle, the processor sets the bus as if it were reading memory location $ffff (the LSB of the reset vector) and just ignores any result. The assumption is that it’s always safe to read the reset vector.

    In simpler cases when it just needs to wait a single cycle before continuing, it usually just uses the current PC as the address and reads what’s (usually) the first byte of the next instruction (but it’s not smart about this, so it executes the same read a second time when it’s actually ready to start the next instruction).

    Lots of interesting activity that shows up on the logic analyzer that you don’t usually think about when just looking at or writing code.

  6. Erik Petrich - March 18th, 2009 1:13 pm

    It looks like on the schematics you could assert /ADRSEL_PC instead of /ADRSEL_AR with just a change to the microcode; this way you wouldn’t have to lose a cycle tweaking ADRHI to avoid the I/O space. However, I couldn’t find any reference to what you are using to compile your microcode. I think you would just have to change what defaults are for the bits that control the various /ADRSEL_* if the microcode does not explicity specify a memory transfer.

  7. Steve - March 18th, 2009 7:00 pm

    Thanks Erik! Schematics? Nobody really reads those, do they? 🙂

    You’re right, and that’s a great idea. I had some vague feeling there was a reason I didn’t do that in the first place, but it was probably just accidental.

    FYI, the microcde .usrc files are compiled by a custom program I wrote in C++ .NET. As you guessed, the address select defaults to AR when nothing else is explicitly or implicitly referenced. It should be a one-line change to make PC the default instead.

  8. Michael K. - April 20th, 2009 6:20 am

    Steve,

    Erik explained what I was just about to. That method of keeping the address bus up at $FFFF during non-bus cycles comes from the MC6809 which did the same thing. It is assumed that this address is in ROM and happens to be the lower-byte (big-endian) of the RESET vector.
    Prior to this method, the MC6800 had a signal called /VMA (Valid Memory Address) which would then be used with address decoding logic to generate the chip-select signals to the individual devices. I believe that it was dropped in the MC6809 in order to use that pin for other purposes as the MC6809 was in a 40-pin package (as was the MC6800).

Leave a reply. For customer support issues, please use the Customer Support link instead of writing comments.