1 unstable release
0.9.0 | Oct 9, 2024 |
---|
#620 in Hardware support
400KB
9K
SLoC
A modular emulator for the P101
This crate provides the building blocks of the emulator. Such blocks are indipendent components that communicates via a system bus emitting and consuming events. Each component is completely isolated and only knows about the system bus.
This loosely coupled architecture brings a lot of flexibility:
- Plugging in different components allows to implement more functionalities and toolings. For istance, a log component can record logs and events for the entire emulator
- Replaying the event stream easily recreates the emulator behavior, greatly simplifying the debug
- It easily enables to mock up devices in unit tests
- On the opposite side, multiple components may provide the same functionality. For instance, printing may be implemented by a headless device for unit tests, by a CLI device to render on a terminal or using a graphical toolkit to build a GUI
- Compiling the sys crate to WASM you can write a UI for the web
- The sys crate is UI-agnostic. You can implement a UI in any language/toolkit able to consume system events
- The emulator logic could be built into an API to publicly expose it on the Internet.
Architecture
- Components have a common interface. Each one can subscribe for and receive any event implemented by the Message enum
- A single component has a focus on its own task. It manages its own private data and communicates consuming and producing events
- All components implement the Component trait. Thus a component have a name (for logging purpose), defines the list of events it wants to receive, expose its input channel and a run method
- Each component runs in its own thread
- Events are propagated via mpsc channels to keep things simple.
Event bus
The event bus is implemented by System. Components register with the system, which keeps track of interested events in order to route messages. System is responsible to start all components, communicate events and coordinate shutdown.
Events
Events, or messages, can be roughly grouped in two kinds.
- Events emitted as consequence of a physical action. For instance, Message::KeyPressed(Key::Power)
- Logical events that communicate a state transition. For instance, Message::SystemStatusChanged(SystemStatus)
A component can consume any event it requires. For instance the UI could register for KeyPressed events to provide a visual feedback.
Message::Halt
This message instructs the receiving component to stop. All components must properly handle halt. The event is emitted by any component that wants to stop the emulator; the system broadcasts the halt event to all components and waits all to stop before shutting down.
This is the only message that is explicitly broadcasted and do not require the component to register for it.
All components must properly handle the halt message, completing or aborting any running tasks then shutting down. A component that does not stops prevents the system to shutdown.
- Emitted by: any component that want to stop the system. It could by UI, a keyboard or the HaltComponent
- Consumed by: any component in the system.
- Kind: logical
Message::SystemStatusChanged(SystemStatus)
New system status.
- Emitted by: the status component
- Consumed by: the UI to update its representation and components that change behavior when turned on
- Kind: logical
Message::SetPrecision(u8)
Comunicates the new value set on the precision wheel.
- Emitted by: status component
- Consumed by: UI, ALU
- Kind: logical
Message::RecordProgramSwitch(bool)
Status of the record program switch.
- Emitted by: status component
- Consumed by: UI, ALU (send memory to storage. TODO not yet implemented)
- Kind: logical
Message::PrintProgramSwitch(bool)
Status of the print program switch.
- Emitted by: status component
- Consumed by: UI, TODO(who prints the program)
- Kind: logical
Message::Print(String) and Message::PrintLine(String) messages
These messages instruct the consuming device to output the string payload.
- Emitted by: ALU component
- Consumed by: UI
- Kind: logical
Message::CardInserted(Card)
The message notifies the storage that a card has been inserted and it is available for read/write operations.
- Emitted by: UI
- Consumed by: storage component
- Kind: logical
Message::CardRemoved
The message notifies the storage that a card has been removed from the reader.
- Emitted by: UI
- Consumed by: storage component
- Kind: logical
Message::ReadTextFromCard
This message instructs the storage to read data from the card.
- Emitted by: ???
- Consumed by: storage component
- Kind: logical
Message::TextRead(String)
This message is emitted by storage devices when data has been read from the card.
- Emitted by: storage component
- Consumed by: ALU
- Kind: logical
Message::StoreTextOnCard(String)
The message instructs the storage component to write memory to the card inserted in the reader. The event is emitted by the UI that also pass the data to save.
- Emitted by: ALU
- Consumed by: storage component
- Kind: logical
Message::ProgramStarted
Program execution has just started.
- Emitted by: ALU
- Consumed by: keyboard (locks), UI (performance light flicks)
- Kind: logical
Message::ProgramStopped
The ALU is waiting for user input.
- Emitted by: ALU
- Consumed by: keyboard (unlocks), UI (steady performance light)
- Kind: logical
Message::ProgramEnded
Program execution is ended. If the emulator has been started in non-interactive way then all components have to shutdown.
- Emitted by: ALU
- Consumed by: keyboard (unlocks), UI (steady performance light)
- Kind: logical
Message::KeyPressed(Key)
A key has been pressed.
- Emitted by: keyboard
- Consumed by: virtually any component. Some react to keypress, other maps keypress to logical events
- Kind: physical
Components in detail
*** TODO: provide a generic component description and how to implement a new one ***
Status component
The status component manages the status of the power, record and print program switches and the precision wheel. Any switch has its own model (a byte or a u8) and maps low-level input events into higher-level events that represent status transitions.
Any switch could have been implemented as an indipendent component. I found out that would be overkill and implemented all in the status component.
The distributed and event based architecture requires a status component to guarantee a single source of truth for system status.
Power switch
This switch turns on the real computer and the emulator too. When the power key is pressed a SystemStatusChanged message will be sent to the system; the new status can be On or Off, depending on previous status. The message in relevant to the UI (switch rendering and lights) and to the status component itself. Consumes: Message::KeyPressed(Key::Power) Produces: Message::SystemStatusChanged(SystemStatus)
Reset key
The reset key restore the system to an initial status. It is used
- when the system powers up because it is in a random statuse
- when the user wants to restore to a known status. Consumes: Message::KeyPressed(Key::Power) Produces: Message::SystemStatusChanged(SystemStatus::Ready)
Print program switch
The print program switch, when on (in), directs the computer to print out the instructions stored in memory from its present location in the program to the next Stop instruction, whenever the Print key is depressed. Consumes: Message::KeyPressed(Key::PrintProgram) Produces: Message::PrintProgramSwitch(Status: bool)
Record program switch
The record program switch, when on (in), directs the computer to store instructions either in the memory from the keyboard, or onto a magnetic program card from the memory. The record program switch must be off (out) to load instructions from a magnetic program card into the memory.
Consumes: Message::KeyPressed(Key::RecordProgram)
Produces: Message::RecordProgramSwitch(Status: bool)
Decimal wheel
The decimal wheel determines the number of decimal places to which computations will be carried out in the A register and the decimal places in the printed output, except for results from the R register. Rolling up the wheel increase the precision. Rolling down decrease it. Consumes: Message::KeyPressed(Key::DecimalUp) Message::KeyPressed(Key::DecimalDown) Produces: Message::SetPrecision(u8)
Event routing
The following table lists events managed by the status component, emitted events and related consumers.
| Originator | Event | Event | Consumer | | --- | --- | --- | --- | --- | | Keyboard component | Message::KeyPressed (Key::Power) | Message::SystemStatusChanged (SystemStatus) (1) | ALU, GUI | | Keyboard component | Message::KeyPressed (Key::Reset) | Message::SystemStatusChanged (SystemStatus::Ready) (2) | ALU, GUI, most components | | Keyboard component | Message::KeyPressed (Key::PrintProgram) | Message::PrintProgramSwitch (bool) | ALU (?), GUI | | Keyboard component | Message::KeyPressed (Key::RecordProgram) | Message::RecordProgramSwitch (bool) | ALU (?), GUI | | Keyboard component | Message::KeyPressed (Key::DecimalUp) | Message::SetPrecision (u8) | ALU, GUI | | Keyboard component | Message::KeyPressed (Key::DecimalDown) | Message::SetPrecision (u8) | ALU, GUI |
(1) Flips system status between on and off (2) If system is on then promote status to ready
Halt component
This component is purely functional and does not map to an actual, physical device. It is used to stop the emulator when a program ends. It is meant to be used in tests or in batch scenarios. It must not be used when running in interactive way.
The component listen for the ProgramEnded message then sends an halt event to the system.
- Consumes: Message::ProgramEnded
- Produces: Message::Halt
Printer component
A Printer consumes a Print or PrintLine message and writes text somewhere.
- Consumes: Message::Print(String), Message::PrintLine(String)
- Produces: no events.
The printer is split in two halves:
- A provider that actually prints (or mimics printing)
- A printer component, which is responsible for communication between the system bus and the provider.
Available providers are:
- A test printer whose output is a vector of strings. It is intended to be used only for testing
- A CLI printer which outputs to the standard output. It can be used when running the emulator from the shell.
To define a new provider it is necessary to implement the PrintProvider trait.
Depending on the UI architecture, a separate print component may not be required at all. UI could implement the PrintProvider trait and consume print events directly.
Storage component
Programs are stored on magnetic cards. Storage components allow to read and write external memory.
- Consumes: Message::CardInserted, Message::CardRemoved, Message::StoreTextOnCard, Message::ReadTextFromCard
- Produces: Message::TextRead(String).
The emulator supports two kinds of cards:
- Volatile memory backed by RAM. Most useful for tests
- Persistent memory, backed by files. Used for loading and storing programs.
The component provides two kinds of cards in the Card enum:
- Card::MemoryCard, a volatile memory mainly useful for unit test
- Card::FileCard, a persistent memory base on files.
Following the idea that UI can dynamycally change card type, the component does not have the internal architecture of others. To be dynamic, cards are implemented in the Card enum. This architecture is required because which kind of card to use it is an option of the UI and could vary at runtime. As such it seems more natural than a static component/provider split.
Depending on the UI architecture, a storage component may not be required at all. The UI may consume storage events directly. In the future it may be replaced by a static omponent-provider architecture.
Keyboard component
Keyboard component provides input to the emulator. It has a component-provider architecture that allow to statically set the required provider. To write a new provider you must implement the KeyboardProvider trait.
The component maps input (scripted keys, real keyboard input, UI clicks, whatever) to Message::KeyPressed(...) events.
Consumed event | Behavior |
---|---|
Message::ProgramStarted | Lock the keyboard |
Message::ProgramStopped | Unlock the keyboard |
Message::ProgramEnded | Unlock the keyboard |
Available providers
Provider | Input source | Applicability |
---|---|---|
CliKeyboard | Command line | Interactive command line emulator |
ScriptKeyboard | Text file | Batch emulator |
TestKeyboard | Array of keys | Unit tests |
ALU component
TODO
System behavior
On-off switch
When the switch is in the off position, any input is disabled and the emulator is turned off; in order to turn the P101 on you need to move the switch to the ON position.
General reset key
The general reset key erases all data and instructions from the computer and turns off the error light.
Tape advance
Tape advance advances the paper tape. This event is managed for emulator completeness but shold have no effect in a virtual environment.
Tape release
The tape release lever enables precise finger-tip adjustment when changing tape rolls. This event is managed for emulator completeness but shold have no effect in a virtual environment.
Routine selection keys
The routine selection keys V, W, Y and Z direct the computer to the proper program or subroutine.
Numeric keyboard
The numeric keyboard uses the ten-key entry system (1 2 3 4 5 6 7 8 9 0 . -) with provision for entry of a decimal point and a negative sign. Keyboard entries are automatically stored in the M register.
Clear entry key
The clear entry key clears the entire keyboard entry. When keying in a program, a depression of the clear key will erase the last instruction that has been entered.
Start key
The start key restarts the computer in programmed operation and is used to code a stop instruction when keying in programs.
Register address keys
The register address keys A, B, C, D, E, F and R identify the corresponding registers. The operating register M has no keyboard identification since the computer automatically relates all instructions to the M register unless instructed otherwise.
Print key
The print key prints the contents of an addressed register.
Clear key
The clear key clears the contents of an addressed register. When the computer is operated manually, a depression of this key will print the number in that register and clear it.
Transfer keys
The transfer keys perform transfer operations between the storage registers and the operating registers. (Refer to discussion of transfer operations for the function of each transfer key.)
Arithmetic keys
The arithmetic keys + - * / perform their indicated arithmetic function.
Keyboard release key
The keyboard release key reactivates a locked keyboard. If two or more keys are depressed simultaneously, the keyboard will lock to indicate a misoperation. Because the operator does not know what entry was accepted by the computer, after touching the Keyboard Release key the Clear Entry key (16) must next be depressed and the complete figure re-entered.
Loading a program, workflow
- Both ALU and storage receive a Message::CardInserted(Card) message. The event may be generated by a GUI, the keyboard or tests
- Storage forcibly ejects any card data present in the reader
- If "Record Program" status is off then ALU emits a Message::ReadTextFromCard event
- Storage receives the Message::ReadTextFromCard event and reads the program from the card
- Storage emits a Message::TextRead(String) message
- The Message::TextRead(String) message is consumed by the ALU which updates the memomry
Saving a program, workflow
- Both ALU and storage receive a Message::CardInserted(Card) event. The event may be generated by a GUI, the keyboard or tests
- If "Record Program" status is on then ALU emits a Message::StoreTextOnCard(String) message
- Storage receives the "Store Text" message and updates card's data
Ejecting a card, workflow
- The GUI or a test emits a Message::CardRemoved message
- The storage receives a Message::CardRemoved and ejects any inserted card.
Writing a GUI
TODO
Status
Status components represent computer status to the user. They are an abstraction to be used as base for GUI-specific components. The system does not have any expectations for the GUI. In the following paragraphs is described the behavior that GUI components should have to conform to the actual P101. System by itself is GUI-implementation agnostic.
Error light
The error light indicator is on is two cases:
- when the computer is turned on. Any input is ignored except the GENERAL RESET key, which resets ALU and memory; finallly the computer reach the full operating status
- when a processing error occurs. This event is produced by the ALU.
Correct performance light
The correct performance (green) light indicates the computer is functioning properly. A steady light indicates that the computer is ready for an operator decision; a flickering light indicates the computer is executing programmed instructions and that the keyboard is locked.
Dependencies
~2.9–4MB
~74K SLoC