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
- UARTs: Many older mainframe, mini, and microcomputers use some form of serial I/O (RS-232 or current loop, for example) to communicate with a teletype, teleprinter, or terminal.
- Parallel / GPIO: Some systems have general purpose parallel I/O used for data acquisition or other requirements.
- Keyboard and Mouse: Some systems will want input via keyboard and mouse attached directly to the system, or possibly through serial bus.
- Network: Local area networking is very popular on many systems. There must be a way to connect the simulated system to a real network OR to a virtual network.
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
- Universal
- Lowest common denominator
- Lots of developer support
- Speed
Cons
- Unsafe
- Vulnerabilities
- Lack of namespacing
- Lousy tooling
C++
Pros
- Universal
- Lots of developer support
- Speed
- Plenty of cross platform toolkits (Qt, GTK, etc.)
Cons
- Unsafe
- Complex
- Easy to misuse
- Not great tooling
Rust
Pros
- Safe
- Growing community
- Excellent type system
- Excellent tooling
Cons
- Lack of developers
- Steep learning curve
- No true cross-platform UI framework
- (GTK or SDL2 may work?)
Go
Pros
- Growing community
- Familiarity for C programmers
- Good tooling
Cons
- Lousy type system
- Garbage collection
- No true cross-platform UI framework
JavaScript
Pros
- Huge developer base
- Enormous community
- Ease of development
- Cross platform development is easy
- True cross platform UI (web browser / Electron)
Cons
- Dynamic typing
- Type coercion
- Needs a runtime
- Garbage collection
C#
Pros
- Excellent ecosystem
- Good general purpose language
- Lots of developer support
Cons
- Possibly encumbered
- No true cross platform UI framework
- Garbage collection