Emulator Design Notes

Status

Early Draft

If you have any feedback on this document, please contact the author at web@loomcom.com.

Introduction

I’ve been interested in computer emulation for as long as I can remember. I wrote my first emulator for the 6502 in 2009, and since then have implemented emulators for several other much more complex systems.

There are a number of popular computer emulation frameworks, both open and closed source. They all have their pros and cons, but I think there’s still room for yet another emulation framework that does things a little differently. I have a few goals in mind.

Library Oriented

I think that a standalone library is a desirable goal. In other words, an emulation framework should stay away from being a monolithic collection of emulators all sharing code with each other, and instead lean toward offering a library of common functionality that can be used by an emulator author to create a standalone project or product.

Safe

Current emulation libraries written in C are a potential security nightmare. Safer practices and safer languages can help mitigate the low hanging fruit of security vulnerabilities, such as buffer overflows and race conditions.

Easy To Use

An emulator framework should offer a clear, usable API that’s easy to understand and well documented. It should offer a wide range of hooks and functions to help implement very common (and maybe even not so common) computer architectures.

Opinionated

A good framework is by its very nature opinionated. It expects users to conform to a certain architecture and tries to offer the best way to do a common task.

Balanced

At the same time, no framework should try to be everything for everyone. There HAS to be a way to use parts of the framework that make sense, while ignoring those that don’t for any particular special case.

Design

Most of this section is highly speculative, and at the moment there are more questions than answers. Expect this to get fleshed out considerably before this document is considered final.

Memory Map/Buses

There is a question of whether these devices should be treated as separate types, or all essentially variations of the same type.

There is also a question of whether a “Bus” should exist as a first-class type, or whether it’s merely an afterthought based on how the emulator does address decoding.

RAM

Every (?) computer has to have a main store of read-write memory. The memory may be byte addressable or word addressable, depending on the architecture. Word size is variable. Byte order is variable. Bit order is variable. How much of this needs to be supported is an open question.

ROM

A great many (but by no means all) systems have some kind of read-only storage that contains firmware or some other boot code. The same addressing, byte, and bit-order variations exist for ROM as well.

Memory-Mapped I/O

It’s a toss-up whether this belongs under “Memory Map/Buses” or under “Devices”. A lot of systems use memory-mapped IO to read and write peripherals at certain fixed addresses.

CPU

Clock

The clock controls the stepping of the emulated CPU. Some emulators may want more fine-grained control here. Does a clock step mean a full fetch/decode/execute cycle, or do you want a more fine-grained approach where each tick is one machine cycle? The latter provides better timing accuracy at the cost of performance, perhaps.

Microcode

Many machines are microcoded. Simulation of microcode could increase flexibility and accuracy at the cost of performance.

Registers and Internal State

Naturally, every CPU has internal state. Support for register-level access is essential for debugging.

Instruction Fetch

It seems unlikely that there will need to be any kind of special support for instruction fetch. Whatever routine is responsible for reading main memory is likely to be good enough.

Instruction Dispatch

Dispatch means taking a decoded instruction and calling the right code to execute it. Most CPUs in SIMH (for example) do this with an enormous switch statement in C. It may be desirable to use a different system for this in another language.

Instruction Disassembly

Maybe not essential, but having support for disassembly seems like a good idea for debugging.

Interrupt Handling

External devices (see below) must have a way of interrupting the CPU. Virtually every CPU supports some kind of interrupt. Many support interrupts at multiple priority levels, including maskable and non-maskable interrupts.

Exception Handling

Many CPUs provide some kind of exception handling. For example, illegal instructions, memory access or execution level violations, and so on. Depending on the complexity, perhaps there should be framework support for this in some kind of lifecycle hook.

Devices

Simulated I/O

Delays

Devices may want to simulate delays in I/O to make timing more accurate. This is likely to need a clock calibration of some kind on the emulator to ensure that triggered events actually happen at the right wall clock times.

This is especially important when creating a real-time clock simulation used, for example, to keep the time of day in the OS of a simulated system (see below).

It might also come in the form of I/O callbacks, for example scheduling when a disk seek or disk write will return.

Interrupt Generation

Simulated devices need to be able to signal an interrupt to the CPU. Interrupts might be latched, or they might be transient.

DMA

DMA allows devices to execute memory and memory-mapped I/O outside of the CPU’s interaction. A lot of existing emulators just handle all DMA in a single CPU step or fetch-decode-execute cycle. It would be more realistic to do DMA one step at a time in the normal clock tick handler, but it might impede performance.

Timers and Scheduled Events

Time-of-Day Clock

Accurate time keeping and saving of real-time clock state between emulator runs is important.

System Timers

A great many systems use periodic interrupts from one or more system timers to do things like housekeeping and process switching. Keeping an accurate timer is important.

Display

Any system with video hardware will need to have a display of some kind.

Bitmapped Display

Vector Display

Front Panel Display

Debugging

Debugging is an essential part of developing an emulator.

Testing

Testing here means testing of the framework and emulators written with it. Traditional emulator frameworks often do not have any testing of the framework itself, nor do they offer explicit hooks for unit testing of emulator code. Instead, they rely on machine-specific diagnostic code (for example, offline diagnostic programs written for the real hardware) to test the emulated hardware.

I think that any framework should both be well tested itself, and offer hooks for emulator authors to do complete unit, acceptance, and integration testing.

Configuration

Every emulator needs to be configured at runtime, for example to specify hardware details, set up devices on the bus, add or remove removable media, and so forth.

Of primary importance to me, it must be convenient and simple to change removable media at runtime without disturbing or halting the emulator.

That may lead into the next section.

User Interface

How a user interacts with the emulator should be uniform across platforms. A great deal can be accomplished simply through a command line and a configuration file. However, for machines that have a video display of any kind, a UI framework of some kind must be included so that the video display can be shown. Additionally, changing removable media such as diskettes and tapes at runtime could be made simpler through a UI. And, finally, a UI could offer support for debugging by showing the states of registers, memory, and so forth.

Language Choices

As a brainstorming exercise, I have very quickly and rather flippantly come up with some pros and cons of using various languages for the framework.

C

Pros

Cons

C++

Pros

Cons

Rust

Pros

Cons

Go

Pros

Cons

JavaScript

Pros

Cons

C#

Pros

Cons

Other?