├── .github ├── actions │ ├── download-sdl2-ttf │ │ └── action.yaml │ └── download-sdl2 │ │ └── action.yaml └── workflows │ └── cmake.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake └── sdl2 │ ├── Copyright.txt │ ├── FindSDL2.cmake │ ├── FindSDL2_ttf.cmake │ └── README.md ├── fonts └── Hack-Regular.ttf ├── programs ├── add_two_numbers.asm ├── count_0_255.asm ├── count_0_255_stop.asm ├── count_255_0_stop.asm ├── fibonacci.asm ├── increase_by_three.asm ├── memory_test.asm ├── multiply_two_numbers.asm ├── nop_test.asm ├── subtract_two_numbers.asm └── test │ ├── all_instructions.asm │ ├── empty_test.asm │ ├── invalid_data_test.asm │ ├── invalid_instruction_test.asm │ ├── invalid_operand_test.asm │ ├── invalid_origin_test.asm │ ├── missing_operand_test.asm │ └── too_many_instructions_test.asm ├── resources ├── emulator-animation-subtract.gif └── emulator-screenshot.png ├── src ├── CMakeLists.txt ├── core │ ├── ArithmeticLogicUnit.cpp │ ├── ArithmeticLogicUnit.h │ ├── ArithmeticLogicUnitObserver.h │ ├── Assembler.cpp │ ├── Assembler.h │ ├── Bus.cpp │ ├── Bus.h │ ├── CMakeLists.txt │ ├── Clock.cpp │ ├── Clock.h │ ├── ClockListener.h │ ├── ClockObserver.h │ ├── Disassembler.cpp │ ├── Disassembler.h │ ├── Emulator.cpp │ ├── Emulator.h │ ├── FlagsRegister.cpp │ ├── FlagsRegister.h │ ├── FlagsRegisterObserver.h │ ├── GenericRegister.cpp │ ├── GenericRegister.h │ ├── InstructionDecoder.cpp │ ├── InstructionDecoder.h │ ├── InstructionDecoderObserver.h │ ├── InstructionRegister.cpp │ ├── InstructionRegister.h │ ├── Instructions.cpp │ ├── Instructions.h │ ├── MemoryAddressRegister.cpp │ ├── MemoryAddressRegister.h │ ├── OutputRegister.cpp │ ├── OutputRegister.h │ ├── ProgramCounter.cpp │ ├── ProgramCounter.h │ ├── RandomAccessMemory.cpp │ ├── RandomAccessMemory.h │ ├── RegisterListener.h │ ├── StepCounter.cpp │ ├── StepCounter.h │ ├── StepListener.h │ ├── TimeSource.cpp │ ├── TimeSource.h │ ├── Utils.cpp │ ├── Utils.h │ └── ValueObserver.h ├── main.cpp └── ui │ ├── ArithmeticLogicUnitModel.cpp │ ├── ArithmeticLogicUnitModel.h │ ├── CMakeLists.txt │ ├── ClockModel.cpp │ ├── ClockModel.h │ ├── FlagsRegisterModel.cpp │ ├── FlagsRegisterModel.h │ ├── InstructionDecoderModel.cpp │ ├── InstructionDecoderModel.h │ ├── InstructionModel.cpp │ ├── InstructionModel.h │ ├── Keyboard.cpp │ ├── Keyboard.h │ ├── RandomAccessMemoryModel.cpp │ ├── RandomAccessMemoryModel.h │ ├── UserInterface.cpp │ ├── UserInterface.h │ ├── ValueModel.cpp │ ├── ValueModel.h │ ├── Window.cpp │ └── Window.h └── test ├── CMakeLists.txt ├── core ├── ArithmeticLogicUnitTest.cpp ├── AssemblerTest.cpp ├── BusTest.cpp ├── ClockTest.cpp ├── DisassemblerTest.cpp ├── EmulatorIntegrationStepTest.cpp ├── EmulatorIntegrationTest.cpp ├── FlagsRegisterTest.cpp ├── GenericRegisterTest.cpp ├── InstructionDecoderTest.cpp ├── InstructionRegisterTest.cpp ├── MemoryAddressRegisterTest.cpp ├── OutputRegisterTest.cpp ├── ProgramCounterTest.cpp ├── RandomAccessMemoryTest.cpp ├── StepCounterTest.cpp ├── TimeSourceTest.cpp └── UtilsTest.cpp ├── include ├── doctest.h └── fakeit.hpp └── test_main.cpp /.github/actions/download-sdl2-ttf/action.yaml: -------------------------------------------------------------------------------- 1 | name: "Download SDL2_ttf" 2 | description: "Download SDL2_ttf sources for Windows, using MSVC." 3 | 4 | branding: 5 | icon: "download" 6 | color: "green" 7 | 8 | inputs: 9 | version: 10 | description: "The version of SDL2_ttf that will be downloaded" 11 | required: true 12 | 13 | destination: 14 | description: "Where the SDL2_ttf sources will be saved" 15 | required: true 16 | 17 | runs: 18 | using: "composite" 19 | steps: 20 | - name: Download sources 21 | run: Invoke-WebRequest -Uri "https://libsdl.org/projects/SDL_ttf/release/SDL2_ttf-devel-${{inputs.version}}-VC.zip" -OutFile ${{inputs.destination}}/SDL2_ttf-devel-${{inputs.version}}-VC.zip 22 | shell: powershell 23 | 24 | - name: Unzip sources 25 | run: | 26 | cd ${{inputs.destination}} 27 | 7z x -y SDL2_ttf-devel-${{inputs.version}}-VC.zip 28 | del SDL2_ttf-devel-${{inputs.version}}-VC.zip 29 | shell: cmd 30 | -------------------------------------------------------------------------------- /.github/actions/download-sdl2/action.yaml: -------------------------------------------------------------------------------- 1 | name: "Download SDL2" 2 | description: "Download SDL2 sources for Windows, using MSVC." 3 | 4 | branding: 5 | icon: "download" 6 | color: "green" 7 | 8 | inputs: 9 | version: 10 | description: "The version of SDL2 that will be downloaded" 11 | required: true 12 | 13 | destination: 14 | description: "Where the SDL2 sources will be saved" 15 | required: true 16 | 17 | runs: 18 | using: "composite" 19 | steps: 20 | - name: Download sources 21 | run: Invoke-WebRequest -Uri "https://libsdl.org/release/SDL2-devel-${{inputs.version}}-VC.zip" -OutFile ${{inputs.destination}}/SDL2-devel-${{inputs.version}}-VC.zip 22 | shell: powershell 23 | 24 | - name: Unzip sources 25 | run: | 26 | cd ${{inputs.destination}} 27 | 7z x -y SDL2-devel-${{inputs.version}}-VC.zip 28 | del SDL2-devel-${{inputs.version}}-VC.zip 29 | shell: cmd 30 | -------------------------------------------------------------------------------- /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: [push] 4 | 5 | env: 6 | BUILD_TYPE: Release 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | platform: [ubuntu-latest, macos-latest, windows-latest] 13 | # Don't cancel the other builds if one fails 14 | fail-fast: false 15 | runs-on: ${{matrix.platform}} 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Install dependencies on Ubuntu 21 | run: sudo apt-get install libsdl2-dev libsdl2-ttf-dev 22 | if: matrix.platform == 'ubuntu-latest' 23 | 24 | - name: Install dependencies on macOS 25 | run: brew install sdl2 sdl2_ttf 26 | if: matrix.platform == 'macos-latest' 27 | 28 | - name: Install SDL2 on Windows 29 | uses: ./.github/actions/download-sdl2 30 | if: matrix.platform == 'windows-latest' 31 | with: 32 | version: 2.0.14 33 | destination: ${{github.workspace}} 34 | 35 | - name: Install SDL2_ttf on Windows 36 | uses: ./.github/actions/download-sdl2-ttf 37 | if: matrix.platform == 'windows-latest' 38 | with: 39 | version: 2.0.15 40 | destination: ${{github.workspace}} 41 | 42 | - name: Create Build Environment 43 | run: cmake -E make_directory ${{github.workspace}}/build 44 | 45 | - name: Configure CMake 46 | shell: bash 47 | working-directory: ${{github.workspace}}/build 48 | run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE 49 | env: 50 | # Only needed for Windows. The others seem to ignore this. 51 | SDL2DIR: ${{github.workspace}}\SDL2-2.0.14 52 | SDL2TTFDIR: ${{github.workspace}}\SDL2_ttf-2.0.15 53 | 54 | - name: Build 55 | working-directory: ${{github.workspace}}/build 56 | shell: bash 57 | run: cmake --build . --config $BUILD_TYPE 58 | 59 | - name: Test 60 | working-directory: ${{github.workspace}}/build 61 | shell: bash 62 | run: ctest -C $BUILD_TYPE --output-on-failure 63 | 64 | - name: Archive binaries 65 | uses: actions/upload-artifact@v2 66 | with: 67 | name: 8-bit ${{matrix.platform}} 68 | path: | 69 | ${{github.workspace}}/build/src/8bit 70 | ${{github.workspace}}/build/src/Release/8bit.exe 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | cmake-build-debug 4 | cmake-build-debug-coverage 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.1) 2 | project(8-bit-computer-emulator) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | 6 | add_subdirectory(src) 7 | add_subdirectory(test) 8 | 9 | FILE(COPY fonts DESTINATION "${CMAKE_BINARY_DIR}/src") 10 | 11 | enable_testing() 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Christian Ihle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 8-bit-computer-emulator 2 | 3 | This is a C++ based emulator of my [8-bit-computer](https://github.com/blurpy/8-bit-computer). 4 | 5 | The goal is to make the emulator as realistic as possible. It's based on emulating the communication between the different parts of the computer so the state is accurate on every cycle. This means the instruction decoder does not change state of memory or registers, but rather directs which part can communicate over the bus at different points in time, like the real hardware do with the microcode in the EEPROMs. Programs that run on the real hardware will run unmodified on the emulator with the same result. 6 | 7 | Please see the link above for documentation on how the computer works. Since the emulator tries to replicate that, no additional documentation is provided here. 8 | 9 | ![Animation of emulator](resources/emulator-animation-subtract.gif) 10 | 11 | The column on the right displays the entire contents of the RAM, as well as where the MAR is pointing. 12 | 13 | 14 | ## Requirements 15 | 16 | The emulator builds on Linux, macOS and Windows. It is primarily tested on Linux. 17 | 18 | * cmake 19 | * gcc/clang/msvc 20 | * sdl2 21 | * sdl2_ttf 22 | 23 | 24 | ### Linux 25 | 26 | Ubuntu: 27 | 28 | ``` 29 | sudo apt-get install build-essential cmake libsdl2-dev libsdl2-ttf-dev 30 | ``` 31 | 32 | openSUSE: 33 | 34 | ``` 35 | sudo zypper install cmake-full libSDL2-devel libSDL2_ttf-devel 36 | ``` 37 | 38 | 39 | ### macOS 40 | 41 | Install xcode command line tools: 42 | 43 | ``` 44 | sudo xcode-select --install 45 | ``` 46 | 47 | Install the rest of the tools with [Homebrew](https://brew.sh/): 48 | 49 | ``` 50 | brew install cmake sdl2 sdl2_ttf 51 | ``` 52 | 53 | 54 | ## Build and run 55 | 56 | Clone the repo first with git or download a zip of the repo. Run the following inside the folder with the code: 57 | 58 | ``` 59 | $ mkdir build 60 | $ cd build 61 | $ cmake .. 62 | $ cmake --build . 63 | $ ctest (optional step) 64 | $ cd .. 65 | $ ./build/src/8bit programs/ 66 | ``` 67 | 68 | 69 | ## Programs 70 | 71 | Pre-made programs are available in the [programs](programs) directory. Programs are in assembly-format, and the emulator will assemble them into machine code at runtime. 72 | 73 | The assembly-format is the same as the real hardware, with a couple of additions to handle the case where you would input data directly into the memory using the DIP-switches. 74 | 75 | |Name|Full name|Operand|Description| 76 | |----|---------|-------|-----------| 77 | |ORG|Origin|4-bit memory reference|Changes memory location to the address in the parameter| 78 | |DB|Define byte|8-bit value|Sets the parameter as a byte in memory at the current memory location| 79 | 80 | Example of how to put the value 202 at memory location 15: 81 | 82 | ```asm 83 | ORG 15 84 | DB 202 85 | ``` 86 | 87 | This is useful to prepare the memory before the program runs, and also the only way to prepare larger numbers since `LDI` only takes a 4-bit value. 88 | 89 | 90 | ## Keyboard shortcuts 91 | 92 | The emulator starts in a stopped state. Use the keyboard to control the emulator. 93 | 94 | * `s` start / stop 95 | * `r` restart the program (_when stopped_) 96 | * `space` single step (_when stopped_) 97 | * `+` increase frequency 98 | * `-` decrease frequency 99 | 100 | 101 | ## Included files 102 | 103 | These are the external files included in this project: 104 | 105 | * [Hack font](https://github.com/source-foundry/Hack) 106 | * [doctest test framework](https://github.com/onqtam/doctest/) 107 | * [FakeIt mock framework](https://github.com/eranpeer/FakeIt) 108 | * [sdl2-cmake-modules](https://github.com/aminosbh/sdl2-cmake-modules) 109 | * [download-sdl2 GitHub Action](https://github.com/albin-johansson/download-sdl2) 110 | 111 | 112 | ## License 113 | 114 | This code is licensed under the MIT license. See [LICENSE](LICENSE) for details. 115 | -------------------------------------------------------------------------------- /cmake/sdl2/Copyright.txt: -------------------------------------------------------------------------------- 1 | CMake - Cross Platform Makefile Generator 2 | Copyright 2000-2019 Kitware, Inc. and Contributors 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | 16 | * Neither the name of Kitware, Inc. nor the names of Contributors 17 | may be used to endorse or promote products derived from this 18 | software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ------------------------------------------------------------------------------ 33 | 34 | The following individuals and institutions are among the Contributors: 35 | 36 | * Aaron C. Meadows 37 | * Adriaan de Groot 38 | * Aleksey Avdeev 39 | * Alexander Neundorf 40 | * Alexander Smorkalov 41 | * Alexey Sokolov 42 | * Alex Merry 43 | * Alex Turbov 44 | * Amine Ben Hassouna 45 | * Andreas Pakulat 46 | * Andreas Schneider 47 | * André Rigland Brodtkorb 48 | * Axel Huebl, Helmholtz-Zentrum Dresden - Rossendorf 49 | * Benjamin Eikel 50 | * Bjoern Ricks 51 | * Brad Hards 52 | * Christopher Harvey 53 | * Christoph Grüninger 54 | * Clement Creusot 55 | * Daniel Blezek 56 | * Daniel Pfeifer 57 | * Enrico Scholz 58 | * Eran Ifrah 59 | * Esben Mose Hansen, Ange Optimization ApS 60 | * Geoffrey Viola 61 | * Google Inc 62 | * Gregor Jasny 63 | * Helio Chissini de Castro 64 | * Ilya Lavrenov 65 | * Insight Software Consortium 66 | * Jan Woetzel 67 | * Julien Schueller 68 | * Kelly Thompson 69 | * Laurent Montel 70 | * Konstantin Podsvirov 71 | * Mario Bensi 72 | * Martin Gräßlin 73 | * Mathieu Malaterre 74 | * Matthaeus G. Chajdas 75 | * Matthias Kretz 76 | * Matthias Maennich 77 | * Michael Hirsch, Ph.D. 78 | * Michael Stürmer 79 | * Miguel A. Figueroa-Villanueva 80 | * Mike Jackson 81 | * Mike McQuaid 82 | * Nicolas Bock 83 | * Nicolas Despres 84 | * Nikita Krupen'ko 85 | * NVIDIA Corporation 86 | * OpenGamma Ltd. 87 | * Patrick Stotko 88 | * Per Øyvind Karlsen 89 | * Peter Collingbourne 90 | * Petr Gotthard 91 | * Philip Lowman 92 | * Philippe Proulx 93 | * Raffi Enficiaud, Max Planck Society 94 | * Raumfeld 95 | * Roger Leigh 96 | * Rolf Eike Beer 97 | * Roman Donchenko 98 | * Roman Kharitonov 99 | * Ruslan Baratov 100 | * Sebastian Holtermann 101 | * Stephen Kelly 102 | * Sylvain Joubert 103 | * Thomas Sondergaard 104 | * Tobias Hunger 105 | * Todd Gamblin 106 | * Tristan Carel 107 | * University of Dundee 108 | * Vadim Zhukov 109 | * Will Dicharry 110 | 111 | See version control history for details of individual contributions. 112 | 113 | The above copyright and license notice applies to distributions of 114 | CMake in source and binary form. Third-party software packages supplied 115 | with CMake under compatible licenses provide their own copyright notices 116 | documented in corresponding subdirectories or source files. 117 | 118 | ------------------------------------------------------------------------------ 119 | 120 | CMake was initially developed by Kitware with the following sponsorship: 121 | 122 | * National Library of Medicine at the National Institutes of Health 123 | as part of the Insight Segmentation and Registration Toolkit (ITK). 124 | 125 | * US National Labs (Los Alamos, Livermore, Sandia) ASC Parallel 126 | Visualization Initiative. 127 | 128 | * National Alliance for Medical Image Computing (NAMIC) is funded by the 129 | National Institutes of Health through the NIH Roadmap for Medical Research, 130 | Grant U54 EB005149. 131 | 132 | * Kitware, Inc. 133 | -------------------------------------------------------------------------------- /cmake/sdl2/README.md: -------------------------------------------------------------------------------- 1 | # SDL2 CMake modules 2 | 3 | This repository contains [CMake][] modules for finding and using the SDL2 4 | library as well as other related libraries: 5 | 6 | - [SDL2][] 7 | - [SDL2_image][] 8 | - [SDL2_ttf][] 9 | - [SDL2_net][] 10 | - [SDL2_mixer][] 11 | - [SDL2_gfx][] 12 | 13 | These modules are based on the SDL (1.2) modules, with the same names, 14 | distributed with the CMake project. The SDL2_gfx module is also based 15 | on the SDL_image module. 16 | 17 | ## Details and Improvements 18 | 19 | The improvements made to these modules are as follows: 20 | 21 | **FindSDL2.cmake** 22 | 23 | - Adapt `FindSDL.cmake` to `SDL2` (`FindSDL2.cmake`). 24 | - Add cache variables for more flexibility:
25 | `SDL2_PATH`, `SDL2_NO_DEFAULT_PATH` 26 | - Mark `Threads` as a required dependency for non-OSX systems. 27 | - Modernize the `FindSDL2.cmake` module by creating specific targets: 28 | - `SDL2::Core` : Library project should link to `SDL2::Core` 29 | - `SDL2::Main` : Application project should link to `SDL2::Main` 30 | 31 | *For more details, please see the embedded documentation in `FindSDL2.cmake` file.* 32 | 33 | **FindSDL2_<COMPONENT>.cmake** 34 | 35 | - Adapt `FindSDL_.cmake` to `SDL2_` (`FindSDL2_.cmake`). 36 | - Add cache variables for more flexibility:
37 | `SDL2__PATH`, `SDL2__NO_DEFAULT_PATH` 38 | - Add `SDL2` as a required dependency. 39 | - Modernize the `FindSDL2_.cmake` modules by creating specific targets:
40 | `SDL2::Image`, `SDL2::TTF`, `SDL2::Net`, `SDL2::Mixer` and `SDL2::GFX`. 41 | 42 | *For more details, please see the embedded documentation in 43 | `FindSDL2_.cmake` file.* 44 | 45 | ## Usage 46 | 47 | In order to use the SDL2 CMake modules, we have to clone this repository in a 48 | sud-directory `cmake/sdl2` in our project as follows: 49 | 50 | ```sh 51 | cd 52 | git clone https://gitlab.com/aminosbh/sdl2-cmake-modules.git cmake/sdl2 53 | rm -rf cmake/sdl2/.git 54 | ``` 55 | 56 | Or if we are using git for our project, we can add this repository as a 57 | submodule as follows: 58 | 59 | ```sh 60 | cd 61 | git submodule add https://gitlab.com/aminosbh/sdl2-cmake-modules.git cmake/sdl2 62 | git commit -m "Add SDL2 CMake modules" 63 | ``` 64 | 65 | Then we should specify the modules path in the main CMakeLists.txt file like 66 | the following: 67 | 68 | ```cmake 69 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/sdl2) 70 | ``` 71 | 72 | Finally, we can use the SDL2 modules. There is two approaches that can be 73 | adopted: A legacy approach and a modern approach. Both of them are supported. 74 | 75 | ### Modern CMake 76 | 77 | We can link to the SDL2:: targets like the following example:
78 | *This example requires the SDL2, SDL2_image and the SDL2_gfx libraries* 79 | 80 | ```cmake 81 | # Find SDL2, SDL2_image and SDL2_gfx libraries 82 | find_package(SDL2 REQUIRED) 83 | find_package(SDL2_image REQUIRED) 84 | find_package(SDL2_gfx REQUIRED) 85 | 86 | # Link SDL2::Main, SDL2::Image and SDL2::GFX to our project 87 | target_link_libraries(${PROJECT_NAME} SDL2::Main SDL2::Image SDL2::GFX) 88 | ``` 89 | 90 | *Use the appropriate packages for you project.*
91 | *Please see above, for the whole list of packages*
92 | *For more details, please see the embedded documentation in modules files* 93 | 94 | ### Legacy CMake 95 | 96 | We can also specify manually the include directories and libraries to link to: 97 | 98 | ```cmake 99 | # Find and link SDL2 100 | find_package(SDL2 REQUIRED) 101 | target_include_directories(${PROJECT_NAME} PRIVATE ${SDL2_INCLUDE_DIRS}) 102 | target_link_libraries(${PROJECT_NAME} ${SDL2_LIBRARIES}) 103 | 104 | # Find and link SDL2_image 105 | find_package(SDL2_image REQUIRED) 106 | target_include_directories(${PROJECT_NAME} PRIVATE ${SDL2_IMAGE_INCLUDE_DIRS}) 107 | target_link_libraries(${PROJECT_NAME} ${SDL2_IMAGE_LIBRARIES}) 108 | 109 | # Find and link SDL2_gfx 110 | find_package(SDL2_gfx REQUIRED) 111 | target_include_directories(${PROJECT_NAME} PRIVATE ${SDL2_GFX_INCLUDE_DIRS}) 112 | target_link_libraries(${PROJECT_NAME} ${SDL2_GFX_LIBRARIES}) 113 | 114 | ``` 115 | 116 | *For more details, please see the embedded documentation in modules files* 117 | 118 | ## Special customization variables 119 | 120 | Each module have special customization cache variables that can be used to help 121 | the modules find the appropriate libraries: 122 | 123 | - `SDL2_PATH` and `SDL2__PATH`:
124 | Can be specified to set the root search path for the `SDL2` and `SDL2_` 125 | - `SDL2_NO_DEFAULT_PATH` and `SDL2__NO_DEFAULT_PATH`:
126 | Disable search `SDL2/SDL2_` library in default path:
127 | If `SDL2[_]_PATH` is set, defaults to ON
128 | Else defaults to OFF 129 | - `SDL2_INCLUDE_DIR` and `SDL2__INCLUDE_DIR`:
130 | Set headers path. (Override) 131 | - `SDL2_LIBRARY` and `SDL2__LIBRARY`:
132 | Set the library (.dll, .so, .a, etc) path. (Override) 133 | - `SDL2MAIN_LIBRAY`:
134 | Set the `SDL2main` library (.a) path. (Override) 135 | 136 | These variables could be used in case of Windows projects, and when the 137 | libraries are not localized in a standard pathes. They can be specified when 138 | executing the `cmake` command or when using the [CMake GUI][] (They are marked 139 | as advanced). 140 | 141 | **cmake command example:** 142 | 143 | ```sh 144 | mkdir build 145 | cd build 146 | cmake .. -DSDL2_PATH="/path/to/sdl2" 147 | ``` 148 | 149 | **CMakeLists.txt example:** 150 | 151 | If we embed, for example, binaries of the SDL2_ttf in our project, we can 152 | specify the cache variables values just before calling the `find_package` 153 | command as follows: 154 | 155 | ```cmake 156 | set(SDL2_TTF_PATH "/path/to/sdl2_ttf" CACHE BOOL "" FORCE) 157 | find_package(SDL2_ttf REQUIRED) 158 | ``` 159 | 160 | ## License 161 | 162 | Maintainer: Amine B. Hassouna [@aminosbh](https://gitlab.com/aminosbh) 163 | 164 | The SDL2 CMake modules are based on the SDL (1.2) modules available with the 165 | CMake project which is distributed under the OSI-approved BSD 3-Clause License. 166 | 167 | The SDL2 CMake modules are also distributed under the OSI-approved BSD 168 | 3-Clause License. See accompanying file [Copyright.txt](Copyright.txt). 169 | 170 | 171 | 172 | [CMake]: https://cmake.org 173 | [CMake GUI]: https://cmake.org/runningcmake 174 | [SDL2]: https://www.libsdl.org 175 | [SDL2_image]: https://www.libsdl.org/projects/SDL_image 176 | [SDL2_ttf]: https://www.libsdl.org/projects/SDL_ttf 177 | [SDL2_net]: https://www.libsdl.org/projects/SDL_net 178 | [SDL2_mixer]: https://www.libsdl.org/projects/SDL_mixer 179 | [SDL2_gfx]: http://www.ferzkopp.net/wordpress/2016/01/02/sdl_gfx-sdl2_gfx 180 | -------------------------------------------------------------------------------- /fonts/Hack-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurpy/8-bit-computer-emulator/cc5fbdaccbe36c7be42b636d91837726f4e5b97c/fonts/Hack-Regular.ttf -------------------------------------------------------------------------------- /programs/add_two_numbers.asm: -------------------------------------------------------------------------------- 1 | LDA 14 ; Put the value from memory location 14 in the A-register 2 | ADD 15 ; Put the value from memory location 15 in the B-register, and store A+B in the A-register 3 | OUT ; Output the value of the A-register 4 | HLT ; Halt the computer 5 | ORG 14 ; Change memory location to 14 6 | DB 28 ; Define a byte with the value 28 at memory location 14 7 | DB 14 ; Define a byte with the value 14 at memory location 15 8 | -------------------------------------------------------------------------------- /programs/count_0_255.asm: -------------------------------------------------------------------------------- 1 | OUT ; Output the value of the A-register 2 | ADD 15 ; Put the value from memory location 15 in the B-register, and store A+B in the A-register 3 | JC 4 ; Jump to instruction 4 if the A-register is past 255 4 | JMP 0 ; Jump to instruction 0 5 | SUB 15 ; Put the value from memory location 15 in the B-register, and store A-B in the A-register 6 | OUT ; Output the value of the A-register 7 | JZ 0 ; Jump to instruction 0 if the A-register is 0 8 | JMP 4 ; Jump to instruction 4 9 | ORG 15 ; Change memory location to 15 10 | DB 1 ; Define a byte with the value 1 at memory location 15 11 | -------------------------------------------------------------------------------- /programs/count_0_255_stop.asm: -------------------------------------------------------------------------------- 1 | OUT ; Output the value of the A-register 2 | ADD 15 ; Put the value from memory location 15 in the B-register, and store A+B in the A-register 3 | JC 4 ; Jump to instruction 4 if the A-register is past 255 4 | JMP 0 ; Jump to instruction 0 5 | HLT ; Halt the computer 6 | ORG 15 ; Change memory location to 15 7 | DB 1 ; Define a byte with the value 1 at memory location 15 8 | -------------------------------------------------------------------------------- /programs/count_255_0_stop.asm: -------------------------------------------------------------------------------- 1 | SUB 15 ; Put the value from memory location 15 in the B-register, and store A-B in the A-register 2 | OUT ; Output the value of the A-register 3 | JZ 4 ; Jump to instruction 4 if the A-register is 0 4 | JMP 0 ; Jump to instruction 0 5 | HLT ; Halt the computer 6 | ORG 15 ; Change memory location to 15 7 | DB 1 ; Define a byte with the value 1 at memory location 15 8 | -------------------------------------------------------------------------------- /programs/fibonacci.asm: -------------------------------------------------------------------------------- 1 | LDI 0 ; Put the value 0 in the A-register 2 | STA 13 ; Store the value from the A-register in memory location 13 3 | OUT ; Output the value of the A-register 4 | LDI 1 ; Put the value 1 in the A-register 5 | STA 14 ; Store the value from the A-register in memory location 14 6 | OUT ; Output the value of the A-register 7 | ADD 13 ; Put the value from memory location 13 in the B-register, and store A+B in the A-register 8 | JC 0 ; Jump to instruction 0 if the A-register is past 255 9 | STA 15 ; Store the value from the A-register in memory location 15 10 | LDA 14 ; Put the value from memory location 14 in the A-register 11 | STA 13 ; Store the value from the A-register in memory location 13 12 | LDA 15 ; Put the value from memory location 15 in the A-register 13 | JMP 4 ; Jump to instruction 4 14 | DB 0 ; Define a byte with the value 0 at memory location 13 15 | DB 0 ; Define a byte with the value 0 at memory location 14 16 | DB 0 ; Define a byte with the value 0 at memory location 15 17 | -------------------------------------------------------------------------------- /programs/increase_by_three.asm: -------------------------------------------------------------------------------- 1 | LDI 3 ; Put the value 3 in the A-register 2 | STA 15 ; Store the value from the A-register in memory location 15 3 | LDI 0 ; Put the value 0 in the A-register 4 | ADD 15 ; Put the value from memory location 15 in the B-register, and store A+B in the A-register 5 | OUT ; Output the value of the A-register 6 | JMP 3 ; Jump to instruction 3 7 | -------------------------------------------------------------------------------- /programs/memory_test.asm: -------------------------------------------------------------------------------- 1 | LDA 14 ; Put the value from memory location 14 in the A-register 2 | ADD 15 ; Put the value from memory location 15 in the B-register, and store A+B in the A-register 3 | STA 14 ; Store the value from the A-register in memory location 14 4 | OUT ; Output the value of the A-register 5 | HLT ; Halt the computer 6 | ORG 14 ; Change memory location to 14 7 | DB 3 ; Define a byte with the value 3 at memory location 14 8 | DB 4 ; Define a byte with the value 4 at memory location 15 9 | -------------------------------------------------------------------------------- /programs/multiply_two_numbers.asm: -------------------------------------------------------------------------------- 1 | LDA 14 ; Put the value from memory location 14 in the A-register 2 | SUB 12 ; Put the value from memory location 12 in the B-register, and store A-B in the A-register 3 | JC 6 ; Jump to instruction 6 if the A-register is past 255 4 | LDA 13 ; Put the value from memory location 13 in the A-register 5 | OUT ; Output the value of the A-register 6 | HLT ; Halt the computer 7 | STA 14 ; Store the value from the A-register in memory location 14 8 | LDA 13 ; Put the value from memory location 13 in the A-register 9 | ADD 15 ; Put the value from memory location 15 in the B-register, and store A+B in the A-register 10 | STA 13 ; Store the value from the A-register in memory location 13 11 | JMP 0 ; Jump to instruction 0 12 | ORG 12 ; Change memory location to 12 13 | DB 1 ; Define a byte with the value 1 at memory location 12 14 | DB 0 ; Define a byte with the value 0 at memory location 13 15 | DB 7 ; Define a byte with the value 7 at memory location 14 16 | DB 8 ; Define a byte with the value 8 at memory location 15 17 | -------------------------------------------------------------------------------- /programs/nop_test.asm: -------------------------------------------------------------------------------- 1 | LDI 10 ; Put the value 10 in the A-register 2 | NOP ; Wait 3 | NOP ; Wait 4 | NOP ; Wait 5 | OUT ; Output the value of the A-register 6 | HLT ; Halt the computer 7 | -------------------------------------------------------------------------------- /programs/subtract_two_numbers.asm: -------------------------------------------------------------------------------- 1 | LDA 14 ; Put the value from memory location 14 in the A-register 2 | SUB 15 ; Put the value from memory location 15 in the B-register, and store A-B in the A-register 3 | OUT ; Output the value of the A-register 4 | HLT ; Halt the computer 5 | ORG 14 ; Change memory location to 14 6 | DB 30 ; Define a byte with the value 30 at memory location 14 7 | DB 12 ; Define a byte with the value 12 at memory location 15 8 | -------------------------------------------------------------------------------- /programs/test/all_instructions.asm: -------------------------------------------------------------------------------- 1 | ; Note: not runnable - only for unit tests 2 | HLT 3 | OUT 4 | JZ 5 5 | JC 4 6 | JMP 1 7 | LDI 3 8 | STA 10 9 | SUB 8 10 | ADD 15 11 | LDA 14 12 | NOP 13 | ORG 14 14 | DB 230 15 | DB 5 16 | -------------------------------------------------------------------------------- /programs/test/empty_test.asm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurpy/8-bit-computer-emulator/cc5fbdaccbe36c7be42b636d91837726f4e5b97c/programs/test/empty_test.asm -------------------------------------------------------------------------------- /programs/test/invalid_data_test.asm: -------------------------------------------------------------------------------- 1 | DB 0 1 2 | -------------------------------------------------------------------------------- /programs/test/invalid_instruction_test.asm: -------------------------------------------------------------------------------- 1 | ; 2 | ; 3 | ;well 4 | monkey see 5 | -------------------------------------------------------------------------------- /programs/test/invalid_operand_test.asm: -------------------------------------------------------------------------------- 1 | LDA 16 2 | -------------------------------------------------------------------------------- /programs/test/invalid_origin_test.asm: -------------------------------------------------------------------------------- 1 | ORG 16 2 | DB 129 3 | -------------------------------------------------------------------------------- /programs/test/missing_operand_test.asm: -------------------------------------------------------------------------------- 1 | JMP 2 | -------------------------------------------------------------------------------- /programs/test/too_many_instructions_test.asm: -------------------------------------------------------------------------------- 1 | LDI 0 2 | LDI 1 3 | LDI 2 4 | LDI 3 5 | LDI 4 6 | LDI 5 7 | LDI 6 8 | LDI 7 9 | LDI 8 10 | LDI 9 11 | LDI 10 12 | LDI 11 13 | LDI 12 14 | LDI 13 15 | LDI 14 16 | LDI 15 17 | LDI 16 18 | -------------------------------------------------------------------------------- /resources/emulator-animation-subtract.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurpy/8-bit-computer-emulator/cc5fbdaccbe36c7be42b636d91837726f4e5b97c/resources/emulator-animation-subtract.gif -------------------------------------------------------------------------------- /resources/emulator-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurpy/8-bit-computer-emulator/cc5fbdaccbe36c7be42b636d91837726f4e5b97c/resources/emulator-screenshot.png -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(core) 2 | add_subdirectory(ui) 3 | 4 | add_executable(8bit main.cpp) 5 | target_link_libraries(8bit 8bit-core) 6 | target_link_libraries(8bit 8bit-ui) 7 | -------------------------------------------------------------------------------- /src/core/ArithmeticLogicUnit.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Utils.h" 4 | 5 | #include "ArithmeticLogicUnit.h" 6 | 7 | Core::ArithmeticLogicUnit::ArithmeticLogicUnit(const std::shared_ptr &aRegister, 8 | const std::shared_ptr &bRegister, 9 | const std::shared_ptr &bus) { 10 | if (Utils::debugL2()) { 11 | std::cout << "ArithmeticLogicUnit construct" << std::endl; 12 | } 13 | 14 | this->aRegister = aRegister; 15 | this->bRegister = bRegister; 16 | this->bus = bus; 17 | this->value = 0; 18 | this->carry = false; 19 | this->zero = true; 20 | } 21 | 22 | Core::ArithmeticLogicUnit::~ArithmeticLogicUnit() { 23 | if (Utils::debugL2()) { 24 | std::cout << "ArithmeticLogicUnit destruct" << std::endl; 25 | } 26 | } 27 | 28 | void Core::ArithmeticLogicUnit::writeToBus() { 29 | bus->write(value); 30 | } 31 | 32 | void Core::ArithmeticLogicUnit::print() const { 33 | printf("ArithmeticLogicUnit: value - %d / 0x%02X / " BYTE_PATTERN " \n", value, value, BYTE_TO_BINARY(value)); 34 | std::cout << "ArithmeticLogicUnit: bits - C=" << carry << ", Z=" << zero << std::endl; 35 | } 36 | 37 | void Core::ArithmeticLogicUnit::reset() { 38 | value = 0; 39 | carry = false; 40 | zero = true; 41 | } 42 | 43 | void Core::ArithmeticLogicUnit::out() { 44 | if (Utils::debugL2()) { 45 | std::cout << "ArithmeticLogicUnit: out" << std::endl; 46 | } 47 | 48 | writeToBus(); 49 | } 50 | 51 | void Core::ArithmeticLogicUnit::registerValueChanged(const uint8_t registerValue) { 52 | add(); 53 | } 54 | 55 | void Core::ArithmeticLogicUnit::add() { 56 | uint8_t aValue = aRegister->readValue(); 57 | uint8_t bValue = bRegister->readValue(); 58 | uint16_t result = aValue + bValue; 59 | uint8_t newValue = result; 60 | bool newCarry = result > 255; 61 | bool newZero = newValue == 0; // Both can be active at once if result is 256 (0b100000000) / new value is 0 62 | 63 | if (Utils::debugL2()) { 64 | std::cout << "ArithmeticLogicUnit: add. changing value from " << (int) value << " to " << (int) result 65 | << " (" << (int) newValue << ")" << std::endl; 66 | std::cout << "ArithmeticLogicUnit: add. changing bits from C=" << carry << ", Z=" << zero 67 | << " to C=" << newCarry << ", Z=" << newZero << std::endl; 68 | } 69 | 70 | value = newValue; 71 | carry = newCarry; 72 | zero = newZero; 73 | 74 | notifyObserver(); 75 | } 76 | 77 | /** 78 | * Subtracts using two's compliment. 79 | * 80 | * Example: 30 - 12 81 | * 30 = 0001 1110 82 | * 12 = 0000 1100 83 | * 84 | * Since the computer only does addition, we can convert 12 to -12 using two's compliment, and then 85 | * think of the calculation as 30 + -12. 86 | * 87 | * Two's compliment of 12 is done by inverting the bits and adding 1. 88 | * Inverted 12 = 1111 0011 89 | * +1 = 1111 0100 90 | * = 244 91 | * 92 | * The calculation then becomes 30 + 244 = 274 93 | * 274 = 1 0001 0010 94 | * Or 18 (0001 0010) + the carry bit 95 | * 96 | * This is why the carry bit LED is often on when subtracting. 97 | * 98 | * The carry bit is not set when the result is 255 and less. 99 | * 100 | * Example: 0 - 1 101 | * 0 = 0000 0000 102 | * 1 = 0000 0001 103 | * Inverted 1 = 1111 1110 104 | * +1 = 1111 1111 105 | * = 255 106 | * 0 + 255 = 255 (no carry needed) 107 | */ 108 | void Core::ArithmeticLogicUnit::subtract() { 109 | uint8_t aValue = aRegister->readValue(); 110 | uint8_t bValue = -(unsigned int) bRegister->readValue(); // Two's complement conversion 111 | uint16_t result = aValue + bValue; 112 | uint8_t newValue = result; 113 | bool newCarry = result > 255; 114 | bool newZero = newValue == 0; // Both can be active at once if result is 256 (0b100000000) / new value is 0 115 | 116 | if (Utils::debugL2()) { 117 | std::cout << "ArithmeticLogicUnit: subtract. changing value from " << (int) value << " to " << (int) result 118 | << " (" << (int) newValue << ")" << std::endl; 119 | std::cout << "ArithmeticLogicUnit: subtract. changing bits from C=" << carry << ", Z=" << zero 120 | << " to C=" << newCarry << ", Z=" << newZero << std::endl; 121 | } 122 | 123 | value = newValue; 124 | carry = newCarry; 125 | zero = newZero; 126 | 127 | notifyObserver(); 128 | } 129 | 130 | bool Core::ArithmeticLogicUnit::isCarry() const { 131 | return carry; 132 | } 133 | 134 | bool Core::ArithmeticLogicUnit::isZero() const { 135 | return zero; 136 | } 137 | 138 | void Core::ArithmeticLogicUnit::notifyObserver() const { 139 | if (observer != nullptr) { 140 | observer->resultUpdated(value, carry, zero); 141 | } 142 | } 143 | 144 | void Core::ArithmeticLogicUnit::setObserver(const std::shared_ptr &newObserver) { 145 | observer = newObserver; 146 | } 147 | -------------------------------------------------------------------------------- /src/core/ArithmeticLogicUnit.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_ARITHMETICLOGICUNIT_H 2 | #define INC_8_BIT_COMPUTER_ARITHMETICLOGICUNIT_H 3 | 4 | #include 5 | 6 | #include "ArithmeticLogicUnitObserver.h" 7 | #include "GenericRegister.h" 8 | #include "RegisterListener.h" 9 | 10 | namespace Core { 11 | 12 | /** 13 | * An 8-bit ALU that can do addition and subtraction based on the values in the A- and B-registers, 14 | * and output the result to the bus. 15 | * 16 | * Addition is performed as A-register + B-register and stored as soon as any of the registers change value, 17 | * without waiting for the clock to tick. 18 | * 19 | * Subtraction can be invoked to perform a recalculation as A-register - B-register and stored, 20 | * also without waiting for the clock to tick. Subtraction is a one off operation and not a state change, 21 | * so the result will be overwritten using addition the next time the registers change value. 22 | * 23 | * Both types of calculations result in some status bits being set. 24 | * 25 | * The bits are: 26 | * - Carry: whether the calculation results in a number larger than 8 bit (255) and has wrapped around. 27 | * - Zero: whether the calculation results in 0. 28 | * 29 | * The bits change immediately after a calculation. 30 | */ 31 | class ArithmeticLogicUnit: public RegisterListener { 32 | 33 | public: 34 | ArithmeticLogicUnit(const std::shared_ptr &aRegister, 35 | const std::shared_ptr &bRegister, 36 | const std::shared_ptr &bus); 37 | ~ArithmeticLogicUnit(); 38 | 39 | /** Print current result to standard out. */ 40 | void print() const; 41 | 42 | /** Reset result to 0. */ 43 | void reset(); 44 | 45 | /** Output result to the bus. */ 46 | virtual void out(); 47 | 48 | /** Overwrite current result with a subtraction instead. See implementation for more detailed docs. */ 49 | virtual void subtract(); 50 | 51 | /** Is the carry bit set. */ 52 | [[nodiscard]] virtual bool isCarry() const; 53 | 54 | /** Is the zero bit set. */ 55 | [[nodiscard]] virtual bool isZero() const; 56 | 57 | /** Set an optional external observer of this arithmetic logic unit. */ 58 | void setObserver(const std::shared_ptr &newObserver); 59 | 60 | private: 61 | std::shared_ptr aRegister; 62 | std::shared_ptr bRegister; 63 | std::shared_ptr bus; 64 | std::shared_ptr observer; 65 | uint8_t value; 66 | bool carry; 67 | bool zero; 68 | 69 | void writeToBus(); 70 | void add(); 71 | void notifyObserver() const; 72 | 73 | void registerValueChanged(uint8_t newValue) override; 74 | }; 75 | } 76 | 77 | #endif //INC_8_BIT_COMPUTER_ARITHMETICLOGICUNIT_H 78 | -------------------------------------------------------------------------------- /src/core/ArithmeticLogicUnitObserver.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_ARITHMETICLOGICUNITOBSERVER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_ARITHMETICLOGICUNITOBSERVER_H 3 | 4 | #include 5 | 6 | namespace Core { 7 | 8 | /** 9 | * Interface for external observation of the arithmetic logic unit of the computer. 10 | */ 11 | class ArithmeticLogicUnitObserver { 12 | 13 | public: 14 | /** The result of a calculation is updated, including the bits. */ 15 | virtual void resultUpdated(uint8_t newValue, bool newCarryBit, bool newZeroBit) = 0; 16 | }; 17 | } 18 | 19 | #endif //INC_8_BIT_COMPUTER_EMULATOR_ARITHMETICLOGICUNITOBSERVER_H 20 | -------------------------------------------------------------------------------- /src/core/Assembler.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "Instructions.h" 7 | #include "Utils.h" 8 | 9 | #include "Assembler.h" 10 | 11 | Core::Assembler::Assembler() { 12 | if (Utils::debugL2()) { 13 | std::cout << "Assembler construct" << std::endl; 14 | } 15 | 16 | this->currentMemoryLocation = 0; 17 | } 18 | 19 | Core::Assembler::~Assembler() { 20 | if (Utils::debugL2()) { 21 | std::cout << "Assembler destruct" << std::endl; 22 | } 23 | } 24 | 25 | std::vector Core::Assembler::loadInstructions(const std::string &fileName) { 26 | std::vector lines = loadFile(fileName); 27 | std::vector instructions = interpret(lines); 28 | 29 | return instructions; 30 | } 31 | 32 | std::vector Core::Assembler::loadFile(const std::string &fileName) { 33 | std::cout << "Assembler: loading file: " << fileName << std::endl; 34 | 35 | std::ifstream file(fileName); 36 | 37 | if (!file.is_open()) { 38 | throw std::runtime_error("Assembler: failed to open file: " + fileName); 39 | } 40 | 41 | std::vector lines; 42 | std::string currentLine; 43 | 44 | while (std::getline(file, currentLine)) { 45 | if (!currentLine.empty()) { 46 | lines.push_back(currentLine); 47 | } 48 | } 49 | 50 | file.close(); 51 | 52 | return lines; 53 | } 54 | 55 | std::vector Core::Assembler::interpret(const std::vector &lines) { 56 | std::vector instructions; 57 | 58 | for (const auto &line : lines) { 59 | if (Utils::debugL1()) { 60 | std::cout << "Assembler: " << line << std::endl; 61 | } 62 | 63 | if (currentMemoryLocation > Utils::FOUR_BITS_MAX) { 64 | throw std::runtime_error("Assembler: address out of bounds " + std::to_string(currentMemoryLocation)); 65 | } 66 | 67 | std::vector tokens = tokenize(line); 68 | 69 | if (tokens.empty()) { 70 | continue; // Skip pure comment lines 71 | } 72 | 73 | std::string mnemonic = tokens[0]; 74 | 75 | // Supports 2 pseudo-instructions that can be used for adding data to the memory before the program runs. 76 | // This is to support the flexibility of the DIP switches in the memory module. 77 | if (mnemonic == "ORG") { 78 | // "Origin" - changes memory location to the address in the parameter 79 | std::string operand = tokens[1]; 80 | currentMemoryLocation = std::stoi(operand); 81 | } else if (mnemonic == "DB") { 82 | // "Define byte" - sets the parameter as a byte in memory at the current memory location 83 | addData(instructions, tokens); 84 | currentMemoryLocation++; 85 | } else { 86 | addInstruction(instructions, mnemonic, tokens); 87 | currentMemoryLocation++; 88 | } 89 | } 90 | 91 | return instructions; 92 | } 93 | 94 | void Core::Assembler::addData(std::vector &instructions, const std::vector &tokens) const { 95 | if (tokens.size() != 2) { 96 | throw std::runtime_error("Assembler: wrong number of arguments to data"); 97 | } 98 | 99 | uint8_t value = std::stoi(tokens[1]); 100 | std::bitset<4> msb = value >> 4; 101 | std::bitset<4> lsb = value & 0x0F; 102 | 103 | Assembler::Instruction instruction = {std::bitset<4>(currentMemoryLocation), msb, lsb}; 104 | instructions.push_back(instruction); 105 | 106 | if (Utils::debugL1()) { 107 | std::cout << "Assembler: " << instruction.address << " " << instruction.opcode << " " << instruction.operand 108 | << std::endl; 109 | } 110 | } 111 | 112 | void Core::Assembler::addInstruction(std::vector &instructions, const std::string &mnemonic, const std::vector &tokens) { 113 | const std::bitset<4> &opcodeBitset = interpretMnemonic(mnemonic); 114 | const std::bitset<4> &operandBitset = interpretOperand(mnemonic, tokens); 115 | 116 | Assembler::Instruction instruction = {std::bitset<4>(currentMemoryLocation), opcodeBitset, operandBitset}; 117 | instructions.push_back(instruction); 118 | 119 | if (Utils::debugL1()) { 120 | std::cout << "Assembler: " << instruction.address << " " << instruction.opcode << " " << instruction.operand 121 | << std::endl; 122 | } 123 | } 124 | 125 | std::bitset<4> Core::Assembler::interpretMnemonic(const std::string &mnemonic) { 126 | const Instructions::Instruction &instruction = Instructions::find(mnemonic); 127 | 128 | if (instruction == Instructions::UNKNOWN) { 129 | throw std::runtime_error("Assembler: interpret mnemonic - unknown mnemonic " + mnemonic); 130 | } 131 | 132 | return instruction.opcodeAsBitset(); 133 | } 134 | 135 | std::bitset<4> Core::Assembler::interpretOperand(const std::string &mnemonic, const std::vector &tokens) { 136 | const Instructions::Instruction &instruction = Instructions::find(mnemonic); 137 | 138 | if (instruction == Instructions::UNKNOWN) { 139 | throw std::runtime_error("Assembler: interpret operand - unknown mnemonic " + mnemonic); 140 | } 141 | 142 | if (instruction.hasOperand) { 143 | if (tokens.size() != 2) { 144 | throw std::runtime_error("Assembler: interpret operand - wrong number of arguments to " + mnemonic); 145 | } 146 | 147 | uint8_t operand = std::stoi(tokens[1]); 148 | 149 | if (operand > Utils::FOUR_BITS_MAX) { 150 | throw std::runtime_error("Assembler: interpret operand - out of bounds " + std::to_string(operand)); 151 | } 152 | 153 | return std::bitset<4>(operand); 154 | } else { 155 | return Instructions::noOperand(); 156 | } 157 | } 158 | 159 | std::vector Core::Assembler::tokenize(const std::string &line) const { 160 | std::stringstream stream(line); 161 | std::vector tokens; 162 | std::string token; 163 | 164 | while (stream >> token) { 165 | // Drop comments 166 | if (Utils::startsWith(token, ";")) { 167 | break; 168 | } 169 | 170 | tokens.push_back(token); 171 | 172 | if (Utils::debugL2()) { 173 | std::cout << "Token: " << token << std::endl; 174 | } 175 | } 176 | 177 | return tokens; 178 | } 179 | -------------------------------------------------------------------------------- /src/core/Assembler.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_ASSEMBLER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_ASSEMBLER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace Core { 9 | 10 | /** 11 | * Reads files of type .asm with assembly source code, and turns the lines into machine instructions 12 | * that can be programmed into memory. The machine instructions map to 8-bit values, in a 4-bit address space. 13 | * That gives at most 16 bytes of instructions and data. 14 | * 15 | * Supports all the official instructions, as well as 2 pseudo-instructions that can be used for adding 16 | * data to the memory before the program runs: 17 | * 18 | * - ORG: "Origin" - changes memory location to the address in the parameter 19 | * - DB: "Define byte" - sets the parameter as a byte in memory at the current memory location 20 | * 21 | * Comments in asm source files start with ';' 22 | */ 23 | class Assembler { 24 | 25 | public: 26 | Assembler(); 27 | ~Assembler(); 28 | 29 | struct Instruction { 30 | std::bitset<4> address; 31 | std::bitset<4> opcode; 32 | std::bitset<4> operand; 33 | }; 34 | 35 | /** Turns the assembly code in the file into machine instructions. */ 36 | std::vector loadInstructions(const std::string &fileName); 37 | 38 | private: 39 | uint8_t currentMemoryLocation; 40 | 41 | std::vector loadFile(const std::string &fileName); 42 | std::vector interpret(const std::vector &lines); 43 | std::bitset<4> interpretMnemonic(const std::string &mnemonic); 44 | std::bitset<4> interpretOperand(const std::string &mnemonic, const std::vector &tokens); 45 | void addInstruction(std::vector &instructions, const std::string &mnemonic, const std::vector &tokens); 46 | void addData(std::vector &instructions, const std::vector &tokens) const; 47 | [[nodiscard]] std::vector tokenize(const std::string &line) const; 48 | }; 49 | } 50 | 51 | #endif //INC_8_BIT_COMPUTER_EMULATOR_ASSEMBLER_H 52 | -------------------------------------------------------------------------------- /src/core/Bus.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Utils.h" 4 | 5 | #include "Bus.h" 6 | 7 | Core::Bus::Bus() { 8 | if (Utils::debugL2()) { 9 | std::cout << "Bus construct" << std::endl; 10 | } 11 | 12 | this->value = 0; 13 | } 14 | 15 | Core::Bus::~Bus() { 16 | if (Utils::debugL2()) { 17 | std::cout << "Bus destruct" << std::endl; 18 | } 19 | } 20 | 21 | uint8_t Core::Bus::read() const { 22 | return value; 23 | } 24 | 25 | void Core::Bus::write(const uint8_t newValue) { 26 | if (Utils::debugL2()) { 27 | std::cout << "Bus: changing value from " << (int) value << " to " << (int) newValue << std::endl; 28 | } 29 | 30 | value = newValue; 31 | 32 | notifyObserver(); 33 | } 34 | 35 | void Core::Bus::print() const { 36 | printf("Bus: %d / 0x%02X / " BYTE_PATTERN "\n", value, value, BYTE_TO_BINARY(value)); 37 | } 38 | 39 | void Core::Bus::reset() { 40 | value = 0; 41 | 42 | notifyObserver(); 43 | } 44 | 45 | void Core::Bus::notifyObserver() const { 46 | if (observer != nullptr) { 47 | observer->valueUpdated(value); 48 | } 49 | } 50 | 51 | void Core::Bus::setObserver(const std::shared_ptr &newObserver) { 52 | observer = newObserver; 53 | } 54 | -------------------------------------------------------------------------------- /src/core/Bus.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_BUS_H 2 | #define INC_8_BIT_COMPUTER_BUS_H 3 | 4 | #include 5 | #include 6 | 7 | #include "ValueObserver.h" 8 | 9 | namespace Core { 10 | 11 | /** 12 | * 8-bit bus that can be read and written to at any point. 13 | */ 14 | class Bus { 15 | 16 | public: 17 | Bus(); 18 | ~Bus(); 19 | 20 | /** Get current value on the bus. */ 21 | [[nodiscard]] uint8_t read() const; 22 | 23 | /** Write a new value to the bus. */ 24 | void write(uint8_t newValue); 25 | 26 | /** Print current value to standard out. */ 27 | void print() const; 28 | 29 | /** Reset the bus to 0. */ 30 | virtual void reset(); 31 | 32 | /** Set an optional external observer of this bus. */ 33 | void setObserver(const std::shared_ptr &newObserver); 34 | 35 | private: 36 | std::shared_ptr observer; 37 | uint8_t value; 38 | 39 | void notifyObserver() const; 40 | }; 41 | } 42 | 43 | #endif //INC_8_BIT_COMPUTER_BUS_H 44 | -------------------------------------------------------------------------------- /src/core/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Threads REQUIRED) 2 | 3 | add_library(8bit-core Emulator.cpp Emulator.h GenericRegister.cpp GenericRegister.h Bus.cpp Bus.h Utils.cpp Utils.h ArithmeticLogicUnit.cpp ArithmeticLogicUnit.h Clock.cpp Clock.h ClockListener.h RandomAccessMemory.cpp RandomAccessMemory.h MemoryAddressRegister.cpp MemoryAddressRegister.h ProgramCounter.cpp ProgramCounter.h InstructionRegister.cpp InstructionRegister.h OutputRegister.cpp OutputRegister.h StepCounter.cpp StepCounter.h InstructionDecoder.cpp InstructionDecoder.h StepListener.h Assembler.cpp Assembler.h RegisterListener.h FlagsRegister.cpp FlagsRegister.h Instructions.cpp Instructions.h TimeSource.cpp TimeSource.h ValueObserver.h ArithmeticLogicUnitObserver.h ClockObserver.h FlagsRegisterObserver.h InstructionDecoderObserver.h Disassembler.cpp Disassembler.h) 4 | 5 | target_link_libraries(8bit-core ${CMAKE_THREAD_LIBS_INIT}) 6 | -------------------------------------------------------------------------------- /src/core/Clock.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "Utils.h" 6 | 7 | #include "Clock.h" 8 | 9 | Core::Clock::Clock(const std::shared_ptr &timeSource) { 10 | if (Utils::debugL2()) { 11 | std::cout << "Clock construct" << std::endl; 12 | } 13 | 14 | this->timeSource = timeSource; 15 | this->counter = 0; 16 | this->frequency = 0; 17 | this->hz = 0; 18 | this->running = false; 19 | this->halted = false; 20 | this->rising = false; 21 | this->singleStepping = false; 22 | this->remainingTicks = 0; 23 | } 24 | 25 | Core::Clock::~Clock() { 26 | if (Utils::debugL2()) { 27 | std::cout << "Clock destruct" << std::endl; 28 | } 29 | 30 | running = false; 31 | listeners.clear(); 32 | } 33 | 34 | void Core::Clock::start() { 35 | std::cout << "Clock: starting clock" << std::endl; 36 | 37 | if (halted) { 38 | std::cerr << "Clock: halted" << std::endl; 39 | return; 40 | } 41 | 42 | if (frequency <= 0) { 43 | throw std::runtime_error("Clock: frequency must be set before start"); 44 | } 45 | 46 | counter = 0; 47 | running = true; 48 | rising = true; 49 | singleStepping = false; 50 | timeSource->reset(); 51 | clockThread = std::thread(&Clock::mainLoop, this); 52 | } 53 | 54 | void Core::Clock::stop() { 55 | running = false; 56 | std::cout << "Clock: stopped" << std::endl; 57 | } 58 | 59 | void Core::Clock::halt() { 60 | halted = true; 61 | stop(); 62 | } 63 | 64 | void Core::Clock::join() { 65 | if (clockThread.joinable()) { 66 | clockThread.join(); 67 | } 68 | } 69 | 70 | void Core::Clock::detach() { 71 | if (clockThread.joinable()) { 72 | clockThread.detach(); 73 | } 74 | } 75 | 76 | void Core::Clock::singleStep() { 77 | std::cout << "Clock: single stepping clock" << std::endl; 78 | 79 | if (halted) { 80 | std::cerr << "Clock: halted" << std::endl; 81 | return; 82 | } 83 | 84 | if (running) { 85 | std::cerr << "Clock: already running" << std::endl; 86 | return; 87 | } 88 | 89 | if (frequency <= 0) { 90 | throw std::runtime_error("Clock: frequency must be set before start"); 91 | } 92 | 93 | counter = 0; 94 | running = true; 95 | rising = true; 96 | singleStepping = true; 97 | remainingTicks = 2; 98 | timeSource->reset(); 99 | clockThread = std::thread(&Clock::mainLoop, this); 100 | clockThread.join(); 101 | } 102 | 103 | bool Core::Clock::isRunning() const { 104 | return running; 105 | } 106 | 107 | void Core::Clock::reset() { 108 | halted = false; 109 | } 110 | 111 | void Core::Clock::setFrequency(const double newHz) { 112 | if (Utils::debugL1()) { 113 | std::cout << "Clock: changing frequency to " << newHz << std::endl; 114 | } 115 | 116 | if (Utils::isLessThan(newHz, 0.1)) { 117 | throw std::runtime_error("Clock: frequency too low " + std::to_string(newHz)); 118 | } 119 | 120 | hz = newHz; 121 | frequency = (1.0 / (hz * 2.0) * 1000.0 * 1000.0 * 1000.0); 122 | 123 | notifyFrequencyChanged(); 124 | } 125 | 126 | void Core::Clock::increaseFrequency() { 127 | if (Utils::isLessThan(hz, 1)) { 128 | setFrequency(hz + 0.1); 129 | } else if (hz < 20) { 130 | setFrequency(hz + 1); 131 | } else if (hz < 200) { 132 | setFrequency(hz + 10); 133 | } else if (hz < 2000) { 134 | setFrequency(hz + 100); 135 | } else { 136 | setFrequency(hz + 1000); 137 | } 138 | } 139 | 140 | void Core::Clock::decreaseFrequency() { 141 | if (Utils::isLessThan(hz, 0.1) || Utils::equals(hz, 0.1)) { 142 | std::cerr << "Clock: can not decrease frequency below 0.1" << std::endl; 143 | } else if (hz <= 1) { 144 | setFrequency(hz - 0.1); 145 | } else if (hz <= 20) { 146 | setFrequency(hz - 1); 147 | } else if (hz <= 200) { 148 | setFrequency(hz - 10); 149 | } else if (hz <= 2000) { 150 | setFrequency(hz - 100); 151 | } else { 152 | setFrequency(hz - 1000); 153 | } 154 | } 155 | 156 | void Core::Clock::mainLoop() { 157 | if (Utils::debugL1()) { 158 | std::cout << "Clock: starting main loop" << std::endl; 159 | } 160 | 161 | while (running) { 162 | if (tick()) { 163 | if (rising) { 164 | notifyTick(); 165 | rising = false; 166 | } 167 | 168 | else { 169 | notifyInvertedTick(); 170 | rising = true; 171 | } 172 | 173 | if (singleStepping && --remainingTicks <= 0) { 174 | running = false; 175 | } 176 | } 177 | 178 | else { 179 | // Sleep the remaining time before the next tick 180 | timeSource->sleep(std::floor(frequency - counter)); 181 | } 182 | } 183 | 184 | if (Utils::debugL1()) { 185 | std::cout << "Clock: exiting main loop" << std::endl; 186 | } 187 | } 188 | 189 | bool Core::Clock::tick() { 190 | bool incremented = false; 191 | 192 | counter += timeSource->delta(); 193 | 194 | if (counter >= frequency) { 195 | incremented = true; 196 | counter = std::fmod(counter, frequency); 197 | } 198 | 199 | return incremented; 200 | } 201 | 202 | void Core::Clock::addListener(const std::shared_ptr &listener) { 203 | listeners.push_back(listener); 204 | } 205 | 206 | void Core::Clock::clearListeners() { 207 | listeners.clear(); 208 | } 209 | 210 | void Core::Clock::notifyTick() const { 211 | if (observer != nullptr) { 212 | observer->clockTicked(true); 213 | } 214 | 215 | for (auto &listener : listeners) { 216 | listener->clockTicked(); 217 | } 218 | } 219 | 220 | void Core::Clock::notifyInvertedTick() const { 221 | if (observer != nullptr) { 222 | observer->clockTicked(false); 223 | } 224 | 225 | for (auto &listener : listeners) { 226 | listener->invertedClockTicked(); 227 | } 228 | } 229 | 230 | void Core::Clock::notifyFrequencyChanged() const { 231 | if (observer != nullptr) { 232 | observer->frequencyChanged(hz); 233 | } 234 | } 235 | 236 | void Core::Clock::setObserver(const std::shared_ptr &newObserver) { 237 | observer = newObserver; 238 | } 239 | -------------------------------------------------------------------------------- /src/core/Clock.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_CLOCK_H 2 | #define INC_8_BIT_COMPUTER_CLOCK_H 3 | 4 | #include 5 | #include 6 | 7 | #include "ClockListener.h" 8 | #include "ClockObserver.h" 9 | #include "TimeSource.h" 10 | 11 | namespace Core { 12 | 13 | /** 14 | * The clock of the computer. Used for triggering and synchronizing operations. 15 | * 16 | * The clock is a square wave at 50% duty cycle, which means it's on 50% of the time and 17 | * off 50% of the time. One clock cycle consists of the square wave turning on (rising edge), 18 | * staying there for a bit, then turning off (falling edge), and staying there for a bit. 19 | * 20 | * Listeners are notified of both those edges, as a clock tick, and an inverted clock tick. 21 | * 22 | * Remember to set the frequency before starting the clock. 23 | */ 24 | class Clock { 25 | 26 | public: 27 | explicit Clock(const std::shared_ptr &timeSource); 28 | ~Clock(); 29 | 30 | /** 31 | * Start the clock asynchronously. Will continue until stop() or halt(). 32 | * Note: you need to either call join() or detach() right afterwards otherwise you may experience 33 | * crashes or deadlocks. 34 | */ 35 | void start(); 36 | 37 | /** Stops a running asynchronous clock. */ 38 | void stop(); 39 | 40 | /** Halt the clock when a program is finished running, until reset(). */ 41 | virtual void halt(); 42 | 43 | /** Wait for an asynchronous clock while it's running. */ 44 | void join(); 45 | 46 | /** Ignore an asynchronous clock while it's running. */ 47 | void detach(); 48 | 49 | /** Run one clock cycle synchronously and then stop. */ 50 | void singleStep(); 51 | 52 | /** Whether the clock is currently running. */ 53 | [[nodiscard]] bool isRunning() const; 54 | 55 | /** Reset the halted status of the clock to allow it to restart. */ 56 | void reset(); 57 | 58 | /** Set the speed to run the clock, in hertz. Must be at least 0.1. */ 59 | void setFrequency(double newHz); 60 | 61 | /** Increase frequency by a step factor. */ 62 | void increaseFrequency(); 63 | 64 | /** Decrease frequency by a step factor, but not below 0.1. */ 65 | void decreaseFrequency(); 66 | 67 | /** Add a listener for clock events. */ 68 | void addListener(const std::shared_ptr &listener); 69 | 70 | /** Clear all the listeners. */ 71 | void clearListeners(); 72 | 73 | /** Set an optional external observer of this clock. */ 74 | void setObserver(const std::shared_ptr &newObserver); 75 | 76 | private: 77 | std::shared_ptr timeSource; 78 | double frequency; 79 | double hz; 80 | double counter; 81 | bool running; 82 | bool halted; 83 | bool rising; 84 | bool singleStepping; 85 | int remainingTicks; 86 | std::thread clockThread; 87 | std::vector> listeners; 88 | std::shared_ptr observer; 89 | 90 | void mainLoop(); 91 | bool tick(); 92 | void notifyTick() const; 93 | void notifyInvertedTick() const; 94 | void notifyFrequencyChanged() const; 95 | }; 96 | } 97 | 98 | #endif //INC_8_BIT_COMPUTER_CLOCK_H 99 | -------------------------------------------------------------------------------- /src/core/ClockListener.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_CLOCKLISTENER_H 2 | #define INC_8_BIT_COMPUTER_CLOCKLISTENER_H 3 | 4 | namespace Core { 5 | 6 | /** 7 | * Interface to be implemented by those who want to be notified when the clock ticks. 8 | */ 9 | class ClockListener { 10 | 11 | public: 12 | /** The rising edge of the clock is triggered. */ 13 | virtual void clockTicked() = 0; 14 | 15 | /** The falling edge of the clock is triggered. */ 16 | virtual void invertedClockTicked() = 0; 17 | }; 18 | } 19 | 20 | #endif //INC_8_BIT_COMPUTER_CLOCKLISTENER_H 21 | -------------------------------------------------------------------------------- /src/core/ClockObserver.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_CLOCKOBSERVER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_CLOCKOBSERVER_H 3 | 4 | namespace Core { 5 | 6 | /** 7 | * Interface for external observation of the clock of the computer. 8 | */ 9 | class ClockObserver { 10 | 11 | public: 12 | /** The clock has ticked either on or off. */ 13 | virtual void clockTicked(bool newOn) = 0; 14 | 15 | /** The clock frequency has changed. */ 16 | virtual void frequencyChanged(double newHz) = 0; 17 | }; 18 | } 19 | 20 | #endif //INC_8_BIT_COMPUTER_EMULATOR_CLOCKOBSERVER_H 21 | -------------------------------------------------------------------------------- /src/core/Disassembler.cpp: -------------------------------------------------------------------------------- 1 | #include "Instructions.h" 2 | 3 | #include "Disassembler.h" 4 | 5 | std::string Core::Disassembler::disassemble(const uint8_t instruction) { 6 | uint8_t opcode = instruction >> 4; // First 4 bits 7 | uint8_t operand = instruction & 0x0F; // Last 4 bits 8 | 9 | switch (opcode) { 10 | case Instructions::NOP.opcode: return std::string(Instructions::NOP.mnemonic); 11 | case Instructions::LDA.opcode: return std::string(Instructions::LDA.mnemonic) + " " + std::to_string(operand); 12 | case Instructions::ADD.opcode: return std::string(Instructions::ADD.mnemonic) + " " + std::to_string(operand); 13 | case Instructions::SUB.opcode: return std::string(Instructions::SUB.mnemonic) + " " + std::to_string(operand); 14 | case Instructions::STA.opcode: return std::string(Instructions::STA.mnemonic) + " " + std::to_string(operand); 15 | case Instructions::LDI.opcode: return std::string(Instructions::LDI.mnemonic) + " " + std::to_string(operand); 16 | case Instructions::JMP.opcode: return std::string(Instructions::JMP.mnemonic) + " " + std::to_string(operand); 17 | case Instructions::JC.opcode: return std::string(Instructions::JC.mnemonic) + " " + std::to_string(operand); 18 | case Instructions::JZ.opcode: return std::string(Instructions::JZ.mnemonic) + " " + std::to_string(operand); 19 | case Instructions::OUT.opcode: return std::string(Instructions::OUT.mnemonic); 20 | case Instructions::HLT.opcode: return std::string(Instructions::HLT.mnemonic); 21 | default: return "UNKNOWN"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/core/Disassembler.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_DISASSEMBLER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_DISASSEMBLER_H 3 | 4 | #include 5 | #include 6 | 7 | namespace Core { 8 | 9 | /** 10 | * Static class for disassembling instructions, as in converting binary code into assembly again. 11 | * 12 | * Example: 0011 1001 -> SUB 9 13 | */ 14 | class Disassembler { 15 | 16 | public: 17 | Disassembler() = delete; 18 | ~Disassembler() = delete; 19 | 20 | /** Converts a binary instruction into assembly. */ 21 | static std::string disassemble(uint8_t instruction); 22 | }; 23 | } 24 | 25 | #endif //INC_8_BIT_COMPUTER_EMULATOR_DISASSEMBLER_H 26 | -------------------------------------------------------------------------------- /src/core/Emulator.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_H 3 | 4 | #include 5 | 6 | #include "ArithmeticLogicUnit.h" 7 | #include "Bus.h" 8 | #include "Clock.h" 9 | #include "FlagsRegister.h" 10 | #include "GenericRegister.h" 11 | #include "InstructionDecoder.h" 12 | #include "InstructionRegister.h" 13 | #include "MemoryAddressRegister.h" 14 | #include "OutputRegister.h" 15 | #include "ProgramCounter.h" 16 | #include "RandomAccessMemory.h" 17 | #include "StepCounter.h" 18 | 19 | namespace Core { 20 | 21 | /** 22 | * This class brings all the different components together to make the computer work. 23 | */ 24 | class Emulator { 25 | 26 | public: 27 | Emulator(); 28 | ~Emulator(); 29 | 30 | /** Initialize the emulator with the program from the specified file. */ 31 | void load(const std::string &newFileName); 32 | 33 | /** Resets state of the computer to the state where it was after load() and before run(). */ 34 | void reload(); 35 | 36 | /** Start running the loaded program. Asynchronous. */ 37 | void startAsynchronous(); 38 | 39 | /** Start running the loaded program. Synchronous. */ 40 | void startSynchronous(); 41 | 42 | /** Whether the emulator is currently running a program. */ 43 | bool isRunning(); 44 | 45 | /** Run one microinstruction synchronously and then stop. */ 46 | void singleStep(); 47 | 48 | /** Ask the emulator to stop running the current program. Asynchronous. */ 49 | void stop(); 50 | 51 | /** Set the speed to run the clock, in hertz. Must be set before running a program, and at least 0.1. */ 52 | void setFrequency(double hz); 53 | 54 | /** Increase the frequency of the clock by a step factor. */ 55 | void increaseFrequency(); 56 | 57 | /** Decrease the frequency of the clock by a step factor, but not below 0.1. */ 58 | void decreaseFrequency(); 59 | 60 | /** Set an optional external observer of the clock. */ 61 | void setClockObserver(const std::shared_ptr &observer); 62 | 63 | /** Set an optional external observer of the bus. */ 64 | void setBusObserver(const std::shared_ptr &observer); 65 | 66 | /** Set an optional external observer of the A register. */ 67 | void setARegisterObserver(const std::shared_ptr &observer); 68 | 69 | /** Set an optional external observer of the B register. */ 70 | void setBRegisterObserver(const std::shared_ptr &observer); 71 | 72 | /** Set an optional external observer of the arithmetic logic unit. */ 73 | void setArithmeticLogicUnitObserver(const std::shared_ptr &observer); 74 | 75 | /** Set an optional external observer of the memory address register. */ 76 | void setMemoryAddressRegisterObserver(const std::shared_ptr &observer); 77 | 78 | /** Set an optional external observer of the program counter. */ 79 | void setProgramCounterObserver(const std::shared_ptr &observer); 80 | 81 | /** Set an optional external observer of the random access memory. */ 82 | void setRandomAccessMemoryObserver(const std::shared_ptr &observer); 83 | 84 | /** Set an optional external observer of the instruction register. */ 85 | void setInstructionRegisterObserver(const std::shared_ptr &observer); 86 | 87 | /** Set an optional external observer of the output register. */ 88 | void setOutputRegisterObserver(const std::shared_ptr &observer); 89 | 90 | /** Set an optional external observer of the step counter. */ 91 | void setStepCounterObserver(const std::shared_ptr &observer); 92 | 93 | /** Set an optional external observer of the instruction decoder. */ 94 | void setInstructionDecoderObserver(const std::shared_ptr &observer); 95 | 96 | /** Set an optional external observer of the flags register. */ 97 | void setFlagsRegisterObserver(const std::shared_ptr &observer); 98 | 99 | private: 100 | std::shared_ptr timeSource; 101 | std::shared_ptr clock; 102 | std::shared_ptr bus; 103 | std::shared_ptr aRegister; 104 | std::shared_ptr bRegister; 105 | std::shared_ptr arithmeticLogicUnit; 106 | std::shared_ptr memoryAddressRegister; 107 | std::shared_ptr programCounter; 108 | std::shared_ptr randomAccessMemory; 109 | std::shared_ptr instructionRegister; 110 | std::shared_ptr outputRegister; 111 | std::shared_ptr stepCounter; 112 | std::shared_ptr instructionDecoder; 113 | std::shared_ptr flagsRegister; 114 | std::string fileName; 115 | 116 | void printValues(); 117 | void reset(); 118 | void initializeProgram(); 119 | [[nodiscard]] bool programMemory(); 120 | }; 121 | } 122 | 123 | #endif //INC_8_BIT_COMPUTER_EMULATOR_H 124 | -------------------------------------------------------------------------------- /src/core/FlagsRegister.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Utils.h" 4 | 5 | #include "FlagsRegister.h" 6 | 7 | Core::FlagsRegister::FlagsRegister(const std::shared_ptr &arithmeticLogicUnit) { 8 | if (Utils::debugL2()) { 9 | std::cout << "FlagsRegister construct" << std::endl; 10 | } 11 | 12 | this->arithmeticLogicUnit = arithmeticLogicUnit; 13 | this->readOnClock = false; 14 | this->carryFlag = false; 15 | this->zeroFlag = false; 16 | } 17 | 18 | Core::FlagsRegister::~FlagsRegister() { 19 | if (Utils::debugL2()) { 20 | std::cout << "FlagsRegister destruct" << std::endl; 21 | } 22 | } 23 | 24 | void Core::FlagsRegister::print() const { 25 | std::cout << "FlagsRegister: CF=" << carryFlag << ", ZF=" << zeroFlag << std::endl; 26 | } 27 | 28 | void Core::FlagsRegister::reset() { 29 | carryFlag = false; 30 | zeroFlag = false; 31 | 32 | notifyObserver(); 33 | } 34 | 35 | void Core::FlagsRegister::in() { 36 | if (Utils::debugL2()) { 37 | std::cout << "FlagsRegister: in - will read from ALU on clock tick" << std::endl; 38 | } 39 | 40 | readOnClock = true; 41 | } 42 | 43 | void Core::FlagsRegister::readFromAlu() { 44 | bool aluCarry = arithmeticLogicUnit->isCarry(); 45 | bool aluZero = arithmeticLogicUnit->isZero(); 46 | 47 | if (Utils::debugL2()) { 48 | std::cout << "FlagsRegister: read from ALU. Changing values from CF=" << carryFlag << ", ZF=" << zeroFlag 49 | << " to CF=" << aluCarry << ", ZF=" << aluZero << std::endl; 50 | } 51 | 52 | carryFlag = aluCarry; 53 | zeroFlag = aluZero; 54 | 55 | notifyObserver(); 56 | } 57 | 58 | void Core::FlagsRegister::clockTicked() { 59 | if (Utils::debugL2()) { 60 | std::cout << "FlagsRegister: clock ticked" << std::endl; 61 | } 62 | 63 | if (readOnClock) { 64 | readFromAlu(); 65 | readOnClock = false; 66 | } 67 | } 68 | 69 | bool Core::FlagsRegister::isCarryFlag() const { 70 | return carryFlag; 71 | } 72 | 73 | bool Core::FlagsRegister::isZeroFlag() const { 74 | return zeroFlag; 75 | } 76 | 77 | void Core::FlagsRegister::notifyObserver() const { 78 | if (observer != nullptr) { 79 | observer->flagsUpdated(carryFlag, zeroFlag); 80 | } 81 | } 82 | 83 | void Core::FlagsRegister::setObserver(const std::shared_ptr &newObserver) { 84 | observer = newObserver; 85 | } 86 | -------------------------------------------------------------------------------- /src/core/FlagsRegister.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_FLAGSREGISTER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_FLAGSREGISTER_H 3 | 4 | #include "ArithmeticLogicUnit.h" 5 | #include "ClockListener.h" 6 | #include "FlagsRegisterObserver.h" 7 | 8 | namespace Core { 9 | 10 | /** 11 | * Register to store flags. 12 | * 13 | * Flags are 1 bit values (0 or 1 / false or true) that can be stored and used for making decisions in the computer. 14 | * 15 | * The flags are: 16 | * - Carry: whether the ALU calculation results in a number larger than 8 bit (255) and has wrapped around. 17 | * - Zero: whether the ALU calculation results in 0. 18 | */ 19 | class FlagsRegister: public ClockListener { 20 | 21 | public: 22 | explicit FlagsRegister(const std::shared_ptr &arithmeticLogicUnit); 23 | ~FlagsRegister(); 24 | 25 | /** Print current flag values to standard out. */ 26 | void print() const; 27 | 28 | /** Reset flags to false. */ 29 | void reset(); 30 | 31 | /** Use carry and zero bits from ALU as new flag values on next clock tick. */ 32 | virtual void in(); 33 | 34 | /** Is the carry flag set. */ 35 | [[nodiscard]] virtual bool isCarryFlag() const; 36 | 37 | /** Is the zero flag set. */ 38 | [[nodiscard]] virtual bool isZeroFlag() const; 39 | 40 | /** Set an optional external observer of this register. */ 41 | void setObserver(const std::shared_ptr &newObserver); 42 | 43 | private: 44 | std::shared_ptr arithmeticLogicUnit; 45 | std::shared_ptr observer; 46 | bool readOnClock; 47 | bool carryFlag; 48 | bool zeroFlag; 49 | 50 | void readFromAlu(); 51 | void notifyObserver() const; 52 | 53 | void clockTicked() override; 54 | void invertedClockTicked() override {}; // Not implemented 55 | }; 56 | } 57 | 58 | #endif //INC_8_BIT_COMPUTER_EMULATOR_FLAGSREGISTER_H 59 | -------------------------------------------------------------------------------- /src/core/FlagsRegisterObserver.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_FLAGSREGISTEROBSERVER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_FLAGSREGISTEROBSERVER_H 3 | 4 | namespace Core { 5 | 6 | /** 7 | * Interface for external observation of the flags of the computer. 8 | */ 9 | class FlagsRegisterObserver { 10 | 11 | public: 12 | /** The flags have been updated. */ 13 | virtual void flagsUpdated(bool newCarryFlag, bool newZeroFlag) = 0; 14 | }; 15 | } 16 | 17 | #endif //INC_8_BIT_COMPUTER_EMULATOR_FLAGSREGISTEROBSERVER_H 18 | -------------------------------------------------------------------------------- /src/core/GenericRegister.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Utils.h" 4 | 5 | #include "GenericRegister.h" 6 | 7 | Core::GenericRegister::GenericRegister(const std::string& name, const std::shared_ptr &bus) { 8 | this->name = name; 9 | this->bus = bus; 10 | this->value = 0; 11 | this->readOnClock = false; 12 | 13 | if (Utils::debugL2()) { 14 | std::cout << this->name << " register construct" << std::endl; 15 | } 16 | } 17 | 18 | Core::GenericRegister::~GenericRegister() { 19 | if (Utils::debugL2()) { 20 | std::cout << this->name << " register destruct" << std::endl; 21 | } 22 | } 23 | 24 | void Core::GenericRegister::readFromBus() { 25 | uint8_t busValue = bus->read(); 26 | 27 | if (Utils::debugL2()) { 28 | std::cout << name << " register: changing value from " << (int) value << " to " << (int) busValue << std::endl; 29 | } 30 | 31 | value = busValue; 32 | 33 | notifyObserver(); 34 | notifyListener(); 35 | } 36 | 37 | void Core::GenericRegister::writeToBus() { 38 | bus->write(value); 39 | } 40 | 41 | uint8_t Core::GenericRegister::readValue() const { 42 | return value; 43 | } 44 | 45 | void Core::GenericRegister::print() { 46 | printf("%s register: %d / 0x%02X / " BYTE_PATTERN " \n", name.c_str(), value, value, BYTE_TO_BINARY(value)); 47 | } 48 | 49 | void Core::GenericRegister::reset() { 50 | value = 0; 51 | 52 | notifyObserver(); 53 | notifyListener(); 54 | } 55 | 56 | void Core::GenericRegister::in() { 57 | if (Utils::debugL2()) { 58 | std::cout << name << " register: in - will read from bus on clock tick" << std::endl; 59 | } 60 | 61 | readOnClock = true; 62 | } 63 | 64 | void Core::GenericRegister::out() { 65 | if (Utils::debugL2()) { 66 | std::cout << name << " register: out" << std::endl; 67 | } 68 | 69 | writeToBus(); 70 | } 71 | 72 | void Core::GenericRegister::clockTicked() { 73 | if (Utils::debugL2()) { 74 | std::cout << name << " register: clock ticked" << std::endl; 75 | } 76 | 77 | if (readOnClock) { 78 | readFromBus(); 79 | readOnClock = false; 80 | } 81 | } 82 | 83 | void Core::GenericRegister::notifyObserver() const { 84 | if (observer != nullptr) { 85 | observer->valueUpdated(value); 86 | } 87 | } 88 | 89 | void Core::GenericRegister::notifyListener() const { 90 | if (registerListener != nullptr) { 91 | registerListener->registerValueChanged(value); 92 | } 93 | } 94 | 95 | void Core::GenericRegister::setRegisterListener(const std::shared_ptr &newRegisterListener) { 96 | registerListener = newRegisterListener; 97 | } 98 | 99 | void Core::GenericRegister::setObserver(const std::shared_ptr &newObserver) { 100 | observer = newObserver; 101 | } 102 | -------------------------------------------------------------------------------- /src/core/GenericRegister.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_GENERICREGISTER_H 2 | #define INC_8_BIT_COMPUTER_GENERICREGISTER_H 3 | 4 | #include 5 | #include 6 | 7 | #include "Bus.h" 8 | #include "ClockListener.h" 9 | #include "RegisterListener.h" 10 | #include "ValueObserver.h" 11 | 12 | namespace Core { 13 | 14 | /** 15 | * Generic 8-bit register with standard operations for reading from the bus and writing to the bus. 16 | * 17 | * Supports an optional listener that can be notified immediately whenever the value of the register changes. 18 | */ 19 | class GenericRegister: public ClockListener { 20 | 21 | public: 22 | GenericRegister(const std::string& name, const std::shared_ptr &bus); 23 | ~GenericRegister(); 24 | 25 | /** Get the current value in the register. */ 26 | [[nodiscard]] virtual uint8_t readValue() const; 27 | 28 | /** Print current value to standard out. */ 29 | void print(); 30 | 31 | /** Reset the register value to 0. */ 32 | void reset(); 33 | 34 | /** Take value from the bus on next clock tick. */ 35 | virtual void in(); 36 | 37 | /** Output current value to the bus. */ 38 | virtual void out(); 39 | 40 | /** Set a listener that will be notified when the value changes. */ 41 | void setRegisterListener(const std::shared_ptr &newRegisterListener); 42 | 43 | /** Set an optional external observer of this register. */ 44 | void setObserver(const std::shared_ptr &newObserver); 45 | 46 | private: 47 | std::string name; 48 | std::shared_ptr registerListener; 49 | std::shared_ptr observer; 50 | std::shared_ptr bus; 51 | uint8_t value; 52 | bool readOnClock; 53 | 54 | void readFromBus(); 55 | void writeToBus(); 56 | void notifyObserver() const; 57 | void notifyListener() const; 58 | 59 | void clockTicked() override; 60 | void invertedClockTicked() override {}; // Not implemented 61 | }; 62 | } 63 | 64 | #endif //INC_8_BIT_COMPUTER_GENERICREGISTER_H 65 | -------------------------------------------------------------------------------- /src/core/InstructionDecoder.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONDECODER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONDECODER_H 3 | 4 | #include "ArithmeticLogicUnit.h" 5 | #include "Clock.h" 6 | #include "FlagsRegister.h" 7 | #include "GenericRegister.h" 8 | #include "InstructionDecoderObserver.h" 9 | #include "InstructionRegister.h" 10 | #include "MemoryAddressRegister.h" 11 | #include "OutputRegister.h" 12 | #include "ProgramCounter.h" 13 | #include "RandomAccessMemory.h" 14 | #include "StepListener.h" 15 | 16 | namespace Core { 17 | 18 | /** 19 | * The instruction decoder is responsible for orchestrating the control lines of the computer 20 | * to make it do something useful. 21 | * 22 | * The control lines are the switches to the supported operations of the different parts, 23 | * like RAM In and Instruction Register Out. Typically an operation either reads (in) from the bus 24 | * or writes (out) to the bus. 25 | * 26 | * Every instruction takes 5 steps (microinstructions) to complete, called the instruction cycle, 27 | * split into 2 phases. 28 | * 29 | * The first phase is the fetch phase. It consists of 2 steps that are the same every time: 30 | * step 0: Put the current value of the program counter into the memory address register. 31 | * step 1: Put the value of the RAM at the current address into the instruction register and 32 | * increment the program counter. 33 | * 34 | * At this point the execution phase begins, and the last 3 steps will differ for each instruction. 35 | * An example could be to load some data from a specified location in RAM into the B-register, and 36 | * store the sum of the A-register and B-register into the A-register. This is the ADD instruction. 37 | * 38 | * To make this work, the instruction decoder is notified by the step counter of which step to execute 39 | * on the falling edge of the clock cycle. It will then prepare the control lines for a particular 40 | * step of a particular instruction, and that combination is called a control word. 41 | * Output operations are executed right away, while input operations are executed on the 42 | * next rising edge of the clock cycle. Although that is the responsibility of the part itself and 43 | * not the instruction decoder. 44 | * 45 | * Some instructions also use flags to make decisions. 46 | */ 47 | class InstructionDecoder: public StepListener { 48 | 49 | public: 50 | InstructionDecoder(const std::shared_ptr &bus, 51 | const std::shared_ptr &memoryAddressRegister, 52 | const std::shared_ptr &programCounter, 53 | const std::shared_ptr &randomAccessMemory, 54 | const std::shared_ptr &instructionRegister, 55 | const std::shared_ptr &aRegister, 56 | const std::shared_ptr &bRegister, 57 | const std::shared_ptr &arithmeticLogicUnit, 58 | const std::shared_ptr &outputRegister, 59 | const std::shared_ptr &flagsRegister, 60 | const std::shared_ptr &clock); 61 | ~InstructionDecoder(); 62 | 63 | /** Set an optional external observer of this instruction decoder. */ 64 | void setObserver(const std::shared_ptr &newObserver); 65 | 66 | private: 67 | std::shared_ptr bus; 68 | std::shared_ptr memoryAddressRegister; 69 | std::shared_ptr programCounter; 70 | std::shared_ptr randomAccessMemory; 71 | std::shared_ptr instructionRegister; 72 | std::shared_ptr aRegister; 73 | std::shared_ptr bRegister; 74 | std::shared_ptr arithmeticLogicUnit; 75 | std::shared_ptr outputRegister; 76 | std::shared_ptr flagsRegister; 77 | std::shared_ptr clock; 78 | std::shared_ptr observer; 79 | 80 | void handleStep0() const; 81 | void handleStep1() const; 82 | void handleStep2() const; 83 | void handleStep3() const; 84 | void handleStep4() const; 85 | 86 | void notifyObserver(const std::vector &lines = {}) const; 87 | 88 | void stepReady(uint8_t step) override; 89 | }; 90 | } 91 | 92 | #endif //INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONDECODER_H 93 | -------------------------------------------------------------------------------- /src/core/InstructionDecoderObserver.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONDECODEROBSERVER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONDECODEROBSERVER_H 3 | 4 | #include 5 | 6 | namespace Core { 7 | 8 | /** The short name of all the different control lines. */ 9 | enum class ControlLine { 10 | // S- O- 11 | HLT, MI, RI, RO, II, IO, AI, AO, BI, BO, SM, SO, OI, OM, CE, CO, CJ, FI 12 | }; 13 | 14 | /** 15 | * Interface for external observation of the instruction decoder of the computer. 16 | */ 17 | class InstructionDecoderObserver { 18 | 19 | public: 20 | /** The control word is updated to have the following lines enabled. */ 21 | virtual void controlWordUpdated(const std::vector &newLines) = 0; 22 | }; 23 | } 24 | 25 | #endif //INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONDECODEROBSERVER_H 26 | -------------------------------------------------------------------------------- /src/core/InstructionRegister.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Utils.h" 4 | 5 | #include "InstructionRegister.h" 6 | 7 | Core::InstructionRegister::InstructionRegister(const std::shared_ptr &bus) { 8 | if (Utils::debugL2()) { 9 | std::cout << "InstructionRegister construct" << std::endl; 10 | } 11 | 12 | this->bus = bus; 13 | this->value = 0; 14 | this->readOnClock = false; 15 | } 16 | 17 | Core::InstructionRegister::~InstructionRegister() { 18 | if (Utils::debugL2()) { 19 | std::cout << "InstructionRegister destruct" << std::endl; 20 | } 21 | } 22 | 23 | void Core::InstructionRegister::readFromBus() { 24 | uint8_t busValue = bus->read(); 25 | 26 | if (Utils::debugL2()) { 27 | std::cout << "InstructionRegister: read from bus. Changing value from " << (int) value << " to " 28 | << (int) busValue << std::endl; 29 | } 30 | 31 | value = busValue; 32 | 33 | notifyObserver(); 34 | } 35 | 36 | void Core::InstructionRegister::writeToBus() { 37 | uint8_t operand = value & 0x0F; // Extract the last 4 bits 38 | bus->write(operand); 39 | } 40 | 41 | void Core::InstructionRegister::print() const { 42 | printf("InstructionRegister: %d / 0x%02X / " BYTE_PATTERN " \n", value, value, BYTE_TO_BINARY(value)); 43 | } 44 | 45 | void Core::InstructionRegister::reset() { 46 | value = 0; 47 | 48 | notifyObserver(); 49 | } 50 | 51 | void Core::InstructionRegister::in() { 52 | if (Utils::debugL2()) { 53 | std::cout << "InstructionRegister: in - will read from bus on clock tick" << std::endl; 54 | } 55 | 56 | readOnClock = true; 57 | } 58 | 59 | void Core::InstructionRegister::out() { 60 | if (Utils::debugL2()) { 61 | std::cout << "InstructionRegister: out" << std::endl; 62 | } 63 | 64 | writeToBus(); 65 | } 66 | 67 | void Core::InstructionRegister::clockTicked() { 68 | if (Utils::debugL2()) { 69 | std::cout << "InstructionRegister: clock ticked" << std::endl; 70 | } 71 | 72 | if (readOnClock) { 73 | readFromBus(); 74 | readOnClock = false; 75 | } 76 | } 77 | 78 | uint8_t Core::InstructionRegister::getOpcode() const { 79 | return value >> 4; // Extract the first 4 bits; 80 | } 81 | 82 | void Core::InstructionRegister::notifyObserver() const { 83 | if (observer != nullptr) { 84 | observer->valueUpdated(value); 85 | } 86 | } 87 | 88 | void Core::InstructionRegister::setObserver(const std::shared_ptr &newObserver) { 89 | observer = newObserver; 90 | } 91 | -------------------------------------------------------------------------------- /src/core/InstructionRegister.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONREGISTER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONREGISTER_H 3 | 4 | #include 5 | 6 | #include "Bus.h" 7 | #include "ClockListener.h" 8 | #include "ValueObserver.h" 9 | 10 | namespace Core { 11 | 12 | /** 13 | * 8-bit register that contains the currently executing instruction. 14 | * 15 | * Instructions are made up of the opcode and the operand, both 4 bits each. 16 | * The opcode is retrieved by the instruction decoder directly so it knows which instruction 17 | * to execute, while the operand can be put out on the bus for transfer to other parts of the 18 | * computer if the instruction requires it. 19 | */ 20 | class InstructionRegister: public ClockListener { 21 | 22 | public: 23 | explicit InstructionRegister(const std::shared_ptr &bus); 24 | ~InstructionRegister(); 25 | 26 | /** Print current value to standard out. */ 27 | void print() const; 28 | 29 | /** Reset the register value to 0. */ 30 | void reset(); 31 | 32 | /** Take the value from the bus on next clock tick. */ 33 | virtual void in(); 34 | 35 | /** Output the 4-bit operand from the instruction to the bus. */ 36 | virtual void out(); 37 | 38 | /** Get the 4-bit opcode from the instruction. */ 39 | [[nodiscard]] virtual uint8_t getOpcode() const; 40 | 41 | /** Set an optional external observer of this register. */ 42 | void setObserver(const std::shared_ptr &newObserver); 43 | 44 | private: 45 | std::shared_ptr bus; 46 | std::shared_ptr observer; 47 | uint8_t value; 48 | bool readOnClock; 49 | 50 | void readFromBus(); 51 | void writeToBus(); 52 | void notifyObserver() const; 53 | 54 | void clockTicked() override; 55 | void invertedClockTicked() override {}; // Not implemented 56 | }; 57 | } 58 | 59 | #endif //INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONREGISTER_H 60 | -------------------------------------------------------------------------------- /src/core/Instructions.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Instructions.h" 4 | 5 | Core::Instructions::Instruction Core::Instructions::find(const std::string& mnemonic) { 6 | for (Instruction candidate : Instructions::ALL) { 7 | if (mnemonic == candidate.mnemonic) { 8 | return candidate; 9 | } 10 | } 11 | 12 | return UNKNOWN; 13 | } 14 | 15 | std::bitset<4> Core::Instructions::noOperand() { 16 | return std::bitset<4>("0000"); 17 | } 18 | -------------------------------------------------------------------------------- /src/core/Instructions.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONS_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace Core { 10 | 11 | /** 12 | * Enum-like structure with details about the supported instructions. 13 | */ 14 | class Instructions { 15 | 16 | public: 17 | struct Instruction { 18 | std::string_view mnemonic; 19 | uint8_t opcode; 20 | bool hasOperand; 21 | 22 | [[nodiscard]] std::bitset<4> opcodeAsBitset() const { 23 | return std::bitset<4>(opcode); 24 | } 25 | 26 | constexpr bool operator==(Instruction id) const { return opcode == id.opcode && mnemonic == id.mnemonic; } 27 | constexpr bool operator!=(Instruction id) const { return opcode == id.opcode && mnemonic == id.mnemonic; } 28 | }; 29 | 30 | /** No operation */ 31 | static constexpr Instruction NOP = {"NOP", 0b0000, false}; 32 | /** Load the accumulator */ 33 | static constexpr Instruction LDA = {"LDA", 0b0001, true}; 34 | /** Add */ 35 | static constexpr Instruction ADD = {"ADD", 0b0010, true}; 36 | /** Subtract */ 37 | static constexpr Instruction SUB = {"SUB", 0b0011, true}; 38 | /** Store the accumulator */ 39 | static constexpr Instruction STA = {"STA", 0b0100, true}; 40 | /** Load immediate */ 41 | static constexpr Instruction LDI = {"LDI", 0b0101, true}; 42 | /** Jump */ 43 | static constexpr Instruction JMP = {"JMP", 0b0110, true}; 44 | /** Jump if carry */ 45 | static constexpr Instruction JC = {"JC", 0b0111, true}; 46 | /** Jump if zero */ 47 | static constexpr Instruction JZ = {"JZ", 0b1000, true}; 48 | /** Output value */ 49 | static constexpr Instruction OUT = {"OUT", 0b1110, false}; 50 | /** Halt the computer */ 51 | static constexpr Instruction HLT = {"HLT", 0b1111, false}; 52 | /** Unknown instruction */ 53 | static constexpr Instruction UNKNOWN = {"UNKNOWN", 0, false}; 54 | 55 | static Instruction find(const std::string& mnemonic); 56 | static std::bitset<4> noOperand(); 57 | 58 | private: 59 | static constexpr std::array ALL = {NOP, LDA, ADD, SUB, STA, LDI, JMP, JC, JZ, OUT, HLT}; 60 | }; 61 | } 62 | 63 | #endif //INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONS_H 64 | -------------------------------------------------------------------------------- /src/core/MemoryAddressRegister.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "Utils.h" 5 | 6 | #include "MemoryAddressRegister.h" 7 | 8 | Core::MemoryAddressRegister::MemoryAddressRegister(const std::shared_ptr ®isterListener, 9 | const std::shared_ptr &bus) { 10 | if (Utils::debugL2()) { 11 | std::cout << "MemoryAddressRegister construct" << std::endl; 12 | } 13 | 14 | this->registerListener = registerListener; 15 | this->bus = bus; 16 | this->value = 0; 17 | this->readOnClock = false; 18 | } 19 | 20 | Core::MemoryAddressRegister::~MemoryAddressRegister() { 21 | if (Utils::debugL2()) { 22 | std::cout << "MemoryAddressRegister destruct" << std::endl; 23 | } 24 | } 25 | 26 | void Core::MemoryAddressRegister::readFromBus() { 27 | uint8_t busValue = bus->read(); 28 | 29 | if (Utils::debugL2()) { 30 | std::cout << "MemoryAddressRegister: read from bus. Changing value from " << (int) value << " to " 31 | << (int) busValue << std::endl; 32 | } 33 | 34 | if (busValue > Utils::FOUR_BITS_MAX) { 35 | throw std::runtime_error("MemoryAddressRegister: address out of bounds " + std::to_string(busValue)); 36 | } 37 | 38 | value = busValue; 39 | 40 | notifyObserver(); 41 | notifyListener(); 42 | } 43 | 44 | void Core::MemoryAddressRegister::print() const { 45 | printf("MemoryAddressRegister: %d / 0x%02X / " BIT_4_PATTERN " \n", value, value, BIT_4_TO_BINARY(value)); 46 | } 47 | 48 | void Core::MemoryAddressRegister::reset() { 49 | value = 0; 50 | } 51 | 52 | void Core::MemoryAddressRegister::program(const std::bitset<4> &address) { 53 | if (Utils::debugL2()) { 54 | std::cout << "MemoryAddressRegister: programming at address " << address << std::endl; 55 | } 56 | 57 | value = address.to_ulong(); 58 | 59 | notifyObserver(); 60 | notifyListener(); 61 | } 62 | 63 | void Core::MemoryAddressRegister::in() { 64 | if (Utils::debugL2()) { 65 | std::cout << "MemoryAddressRegister: in - will read from bus on clock tick" << std::endl; 66 | } 67 | 68 | readOnClock = true; 69 | } 70 | 71 | void Core::MemoryAddressRegister::clockTicked() { 72 | if (Utils::debugL2()) { 73 | std::cout << "MemoryAddressRegister: clock ticked" << std::endl; 74 | } 75 | 76 | if (readOnClock) { 77 | readFromBus(); 78 | readOnClock = false; 79 | } 80 | } 81 | 82 | void Core::MemoryAddressRegister::notifyObserver() const { 83 | if (observer != nullptr) { 84 | observer->valueUpdated(value); 85 | } 86 | } 87 | 88 | void Core::MemoryAddressRegister::notifyListener() const { 89 | registerListener->registerValueChanged(value); 90 | } 91 | 92 | void Core::MemoryAddressRegister::setObserver(const std::shared_ptr &newObserver) { 93 | observer = newObserver; 94 | } 95 | -------------------------------------------------------------------------------- /src/core/MemoryAddressRegister.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_MEMORYADDRESSREGISTER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_MEMORYADDRESSREGISTER_H 3 | 4 | #include 5 | #include 6 | 7 | #include "Bus.h" 8 | #include "ClockListener.h" 9 | #include "RegisterListener.h" 10 | 11 | namespace Core { 12 | 13 | /** 14 | * A 4-bit (0->15) register that keeps track of the active memory address. 15 | * 16 | * The RAM is notified right away when an address changes, without waiting for the clock. 17 | * 18 | * Supports manual control like the DIP-switches using the program function. 19 | */ 20 | class MemoryAddressRegister: public ClockListener { 21 | 22 | public: 23 | MemoryAddressRegister(const std::shared_ptr ®isterListener, 24 | const std::shared_ptr &bus); 25 | ~MemoryAddressRegister(); 26 | 27 | /** Print current value to standard out. */ 28 | void print() const; 29 | 30 | /** Reset the register value to 0. */ 31 | void reset(); 32 | 33 | /** Sets the address to use in manual mode. */ 34 | void program(const std::bitset<4> &address); 35 | 36 | /** Take a 4-bit value from the bus on next clock tick. */ 37 | virtual void in(); 38 | 39 | /** Set an optional external observer of this register. */ 40 | void setObserver(const std::shared_ptr &newObserver); 41 | 42 | private: 43 | std::shared_ptr registerListener; 44 | std::shared_ptr bus; 45 | std::shared_ptr observer; 46 | uint8_t value; 47 | bool readOnClock; 48 | 49 | void readFromBus(); 50 | void notifyObserver() const; 51 | void notifyListener() const; 52 | 53 | void clockTicked() override; 54 | void invertedClockTicked() override {}; // Not implemented 55 | }; 56 | } 57 | 58 | #endif //INC_8_BIT_COMPUTER_EMULATOR_MEMORYADDRESSREGISTER_H 59 | -------------------------------------------------------------------------------- /src/core/OutputRegister.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Utils.h" 4 | 5 | #include "OutputRegister.h" 6 | 7 | Core::OutputRegister::OutputRegister(const std::shared_ptr &bus) { 8 | if (Utils::debugL2()) { 9 | std::cout << "OutputRegister construct" << std::endl; 10 | } 11 | 12 | this->bus = bus; 13 | this->value = 0; 14 | this->readOnClock = false; 15 | } 16 | 17 | Core::OutputRegister::~OutputRegister() { 18 | if (Utils::debugL2()) { 19 | std::cout << "OutputRegister destruct" << std::endl; 20 | } 21 | } 22 | 23 | void Core::OutputRegister::readFromBus() { 24 | uint8_t busValue = bus->read(); 25 | 26 | if (Utils::debugL2()) { 27 | std::cout << "OutputRegister: read from bus. Changing value from " << (int) value << " to " << (int) busValue 28 | << std::endl; 29 | } 30 | 31 | value = busValue; 32 | 33 | std::cout << "*** Display: " << (int) value << std::endl; 34 | 35 | notifyObserver(); 36 | } 37 | 38 | void Core::OutputRegister::print() const { 39 | printf("OutputRegister: %d / 0x%02X / " BYTE_PATTERN " \n", value, value, BYTE_TO_BINARY(value)); 40 | } 41 | 42 | void Core::OutputRegister::reset() { 43 | value = 0; 44 | 45 | notifyObserver(); 46 | } 47 | 48 | void Core::OutputRegister::in() { 49 | if (Utils::debugL2()) { 50 | std::cout << "OutputRegister: in - will read from bus on clock tick" << std::endl; 51 | } 52 | 53 | readOnClock = true; 54 | } 55 | 56 | void Core::OutputRegister::clockTicked() { 57 | if (Utils::debugL2()) { 58 | std::cout << "OutputRegister: clock ticked" << std::endl; 59 | } 60 | 61 | if (readOnClock) { 62 | readFromBus(); 63 | readOnClock = false; 64 | } 65 | } 66 | 67 | void Core::OutputRegister::notifyObserver() const { 68 | if (observer != nullptr) { 69 | observer->valueUpdated(value); 70 | } 71 | } 72 | 73 | void Core::OutputRegister::setObserver(const std::shared_ptr &newObserver) { 74 | observer = newObserver; 75 | } 76 | -------------------------------------------------------------------------------- /src/core/OutputRegister.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_OUTPUTREGISTER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_OUTPUTREGISTER_H 3 | 4 | #include 5 | 6 | #include "Bus.h" 7 | #include "ClockListener.h" 8 | #include "ValueObserver.h" 9 | 10 | namespace Core { 11 | 12 | /** 13 | * The register connected to the four 7-segment LEDs. 14 | * 15 | * Can read an 8-bit value from the bus and display it. And the display is currently the terminal. 16 | * 17 | * The output in the real hardware has a signed mode, allowing it to display values from -128 to 127. 18 | * This is the same two's compliment representation of numbers as used in the ALU for subtraction. 19 | * The first bit is the sign, so counting from 0 to 255 in this mode would go like this: 20 | * 21 | * 0 (0000 0000) 22 | * 1 (0000 0001) (skip...) 23 | * 126 (0111 1110) 24 | * 127 (0111 1111) 25 | * -128 (1000 0000) 26 | * -127 (1000 0001) (skip...) 27 | * -1 (1111 1111) 28 | * 29 | * Signed mode is not used in any of the instructions, and therefore not implemented here. Perhaps in the future. 30 | */ 31 | class OutputRegister: public ClockListener { 32 | 33 | public: 34 | explicit OutputRegister(const std::shared_ptr &bus); 35 | ~OutputRegister(); 36 | 37 | /** Print current value to standard out. */ 38 | void print() const; 39 | 40 | /** Reset the register value to 0. */ 41 | void reset(); 42 | 43 | /** Take value from the bus on next clock tick. */ 44 | virtual void in(); 45 | 46 | /** Set an optional external observer of this register. */ 47 | void setObserver(const std::shared_ptr &newObserver); 48 | 49 | private: 50 | std::shared_ptr bus; 51 | std::shared_ptr observer; 52 | uint8_t value; 53 | bool readOnClock; 54 | 55 | void readFromBus(); 56 | void notifyObserver() const; 57 | 58 | void clockTicked() override; 59 | void invertedClockTicked() override {}; // Not implemented 60 | }; 61 | } 62 | 63 | #endif //INC_8_BIT_COMPUTER_EMULATOR_OUTPUTREGISTER_H 64 | -------------------------------------------------------------------------------- /src/core/ProgramCounter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "Utils.h" 5 | 6 | #include "ProgramCounter.h" 7 | 8 | Core::ProgramCounter::ProgramCounter(const std::shared_ptr &bus) { 9 | if (Utils::debugL2()) { 10 | std::cout << "ProgramCounter construct" << std::endl; 11 | } 12 | 13 | this->bus = bus; 14 | this->value = 0; 15 | this->incrementOnClock = false; 16 | this->readOnClock = false; 17 | } 18 | 19 | Core::ProgramCounter::~ProgramCounter() { 20 | if (Utils::debugL2()) { 21 | std::cout << "ProgramCounter destruct" << std::endl; 22 | } 23 | } 24 | 25 | void Core::ProgramCounter::increment() { 26 | value = ++value % 16; 27 | 28 | if (Utils::debugL2()) { 29 | std::cout << "ProgramCounter: incremented to " << (int) value << std::endl; 30 | } 31 | 32 | notifyObserver(); 33 | } 34 | 35 | void Core::ProgramCounter::readFromBus() { 36 | uint8_t busValue = bus->read(); 37 | 38 | if (Utils::debugL2()) { 39 | std::cout << "ProgramCounter: changing value from " << (int) value << " to " << (int) busValue << std::endl; 40 | } 41 | 42 | if (busValue > Utils::FOUR_BITS_MAX) { 43 | throw std::runtime_error("ProgramCounter: address out of bounds " + std::to_string(busValue)); 44 | } 45 | 46 | value = busValue; 47 | 48 | notifyObserver(); 49 | } 50 | 51 | void Core::ProgramCounter::writeToBus() { 52 | if (Utils::debugL2()) { 53 | std::cout << "ProgramCounter: writing to bus " << (int) value << std::endl; 54 | } 55 | 56 | bus->write(value); 57 | } 58 | 59 | void Core::ProgramCounter::print() const { 60 | printf("ProgramCounter: %d / 0x%02X / " BIT_4_PATTERN " \n", value, value, BIT_4_TO_BINARY(value)); 61 | } 62 | 63 | void Core::ProgramCounter::reset() { 64 | value = 0; 65 | 66 | notifyObserver(); 67 | } 68 | 69 | void Core::ProgramCounter::out() { 70 | if (Utils::debugL2()) { 71 | std::cout << "ProgramCounter: out" << std::endl; 72 | } 73 | 74 | writeToBus(); 75 | } 76 | 77 | void Core::ProgramCounter::enable() { 78 | if (Utils::debugL2()) { 79 | std::cout << "ProgramCounter: enable - will increment on clock tick" << std::endl; 80 | } 81 | 82 | incrementOnClock = true; 83 | } 84 | 85 | void Core::ProgramCounter::jump() { 86 | if (Utils::debugL2()) { 87 | std::cout << "ProgramCounter: jump - will read from bus on clock tick" << std::endl; 88 | } 89 | 90 | readOnClock = true; 91 | } 92 | 93 | void Core::ProgramCounter::clockTicked() { 94 | if (Utils::debugL2()) { 95 | std::cout << "ProgramCounter: clock ticked" << std::endl; 96 | } 97 | 98 | if (incrementOnClock) { 99 | increment(); 100 | incrementOnClock = false; 101 | } 102 | 103 | if (readOnClock) { 104 | readFromBus(); 105 | readOnClock = false; 106 | } 107 | } 108 | 109 | void Core::ProgramCounter::notifyObserver() const { 110 | if (observer != nullptr) { 111 | observer->valueUpdated(value); 112 | } 113 | } 114 | 115 | void Core::ProgramCounter::setObserver(const std::shared_ptr &newObserver) { 116 | observer = newObserver; 117 | } 118 | -------------------------------------------------------------------------------- /src/core/ProgramCounter.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_PROGRAMCOUNTER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_PROGRAMCOUNTER_H 3 | 4 | #include 5 | 6 | #include "Bus.h" 7 | #include "ClockListener.h" 8 | 9 | namespace Core { 10 | 11 | /** 12 | * 4-bit counter (0->15) that keeps track of the memory location of the next instruction to execute. 13 | * 14 | * Normal operation is to output the current value to the bus during the fetch cycle, and increment 15 | * by 1 afterwards. It also supports jumping to a memory location read from the bus. 16 | */ 17 | class ProgramCounter: public ClockListener { 18 | 19 | public: 20 | explicit ProgramCounter(const std::shared_ptr &bus); 21 | ~ProgramCounter(); 22 | 23 | /** Print current value to standard out. */ 24 | void print() const; 25 | 26 | /** Reset counter to 0. */ 27 | void reset(); 28 | 29 | /** Output counter value to the bus. */ 30 | virtual void out(); 31 | 32 | /** Increment counter value by 1 on next clock tick. */ 33 | virtual void enable(); 34 | 35 | /** Use value from bus as new counter value on next clock tick. */ 36 | virtual void jump(); 37 | 38 | /** Set an optional external observer of this program counter. */ 39 | void setObserver(const std::shared_ptr &newObserver); 40 | 41 | private: 42 | std::shared_ptr bus; 43 | std::shared_ptr observer; 44 | uint8_t value; 45 | bool incrementOnClock; 46 | bool readOnClock; 47 | 48 | void increment(); 49 | void readFromBus(); 50 | void writeToBus(); 51 | void notifyObserver() const; 52 | 53 | void clockTicked() override; 54 | void invertedClockTicked() override {}; // Not implemented 55 | }; 56 | } 57 | 58 | #endif //INC_8_BIT_COMPUTER_EMULATOR_PROGRAMCOUNTER_H 59 | -------------------------------------------------------------------------------- /src/core/RandomAccessMemory.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "Utils.h" 5 | 6 | #include "RandomAccessMemory.h" 7 | 8 | Core::RandomAccessMemory::RandomAccessMemory(const std::shared_ptr &bus) { 9 | if (Utils::debugL2()) { 10 | std::cout << "RandomAccessMemory construct" << std::endl; 11 | } 12 | 13 | this->bus = bus; 14 | this->address = 0; 15 | this->readOnClock = false; 16 | } 17 | 18 | Core::RandomAccessMemory::~RandomAccessMemory() { 19 | if (Utils::debugL2()) { 20 | std::cout << "RandomAccessMemory destruct" << std::endl; 21 | } 22 | } 23 | 24 | void Core::RandomAccessMemory::readFromBus() { 25 | uint8_t busValue = bus->read(); 26 | uint8_t currentValue = memory[address]; 27 | 28 | if (Utils::debugL2()) { 29 | std::cout << "RandomAccessMemory: changing value from " << (int) currentValue << " to " << (int) busValue 30 | << " at address " << (int) address << std::endl; 31 | } 32 | 33 | memory[address] = busValue; 34 | 35 | notifyObserver(); 36 | } 37 | 38 | void Core::RandomAccessMemory::writeToBus() { 39 | if (Utils::debugL2()) { 40 | std::cout << "RandomAccessMemory: writing to bus " << (int) memory[address] << std::endl; 41 | } 42 | 43 | bus->write(memory[address]); 44 | } 45 | 46 | void Core::RandomAccessMemory::print() { 47 | printf("RandomAccessMemory: current address - %d / 0x%02X / " BIT_4_PATTERN " \n", address, address, BIT_4_TO_BINARY(address)); 48 | printf("RandomAccessMemory: current value - %d / 0x%02X / " BYTE_PATTERN " \n", memory[address], memory[address], BYTE_TO_BINARY(memory[address])); 49 | 50 | for (int i = 0; i < MEMORY_SIZE; i++) { 51 | printf("RandomAccessMemory: value at %d - %d / 0x%02X / " BYTE_PATTERN " \n", i, memory[i], memory[i], BYTE_TO_BINARY(memory[i])); 52 | } 53 | } 54 | 55 | void Core::RandomAccessMemory::reset() { 56 | address = 0; 57 | } 58 | 59 | void Core::RandomAccessMemory::program(const std::bitset<4> &opcode, const std::bitset<4> &operand) { 60 | if (Utils::debugL2()) { 61 | std::cout << "RandomAccessMemory: programming at address " << (int) address << " with opcode " << opcode 62 | << " and operand " << operand << std::endl; 63 | } 64 | 65 | std::bitset<8> newValue(opcode.to_string() + operand.to_string()); 66 | memory[address] = newValue.to_ulong(); 67 | 68 | notifyObserver(); 69 | } 70 | 71 | void Core::RandomAccessMemory::in() { 72 | if (Utils::debugL2()) { 73 | std::cout << "RandomAccessMemory: in - will read from bus on clock tick" << std::endl; 74 | } 75 | 76 | readOnClock = true; 77 | } 78 | 79 | void Core::RandomAccessMemory::out() { 80 | if (Utils::debugL2()) { 81 | std::cout << "RandomAccessMemory: out" << std::endl; 82 | } 83 | 84 | writeToBus(); 85 | } 86 | 87 | void Core::RandomAccessMemory::clockTicked() { 88 | if (Utils::debugL2()) { 89 | std::cout << "RandomAccessMemory: clock ticked" << std::endl; 90 | } 91 | 92 | if (readOnClock) { 93 | readFromBus(); 94 | readOnClock = false; 95 | } 96 | } 97 | 98 | void Core::RandomAccessMemory::registerValueChanged(const uint8_t newValue) { 99 | if (Utils::debugL2()) { 100 | std::cout << "RandomAccessMemory: registerValueChanged. " 101 | << "changing address from " << (int) address << " to " << (int) newValue << std::endl; 102 | } 103 | 104 | if (newValue >= MEMORY_SIZE) { 105 | throw std::runtime_error("RandomAccessMemory: address out of bounds " + std::to_string(newValue)); 106 | } 107 | 108 | address = newValue; 109 | 110 | notifyObserver(); 111 | } 112 | 113 | void Core::RandomAccessMemory::notifyObserver() const { 114 | if (observer != nullptr) { 115 | observer->valueUpdated(memory[address]); 116 | } 117 | } 118 | 119 | void Core::RandomAccessMemory::setObserver(const std::shared_ptr &newObserver) { 120 | observer = newObserver; 121 | } 122 | -------------------------------------------------------------------------------- /src/core/RandomAccessMemory.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_RANDOMACCESSMEMORY_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_RANDOMACCESSMEMORY_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "Bus.h" 9 | #include "ClockListener.h" 10 | #include "RegisterListener.h" 11 | 12 | namespace Core { 13 | 14 | /** 15 | * 16 bytes of static RAM. 16 | * 17 | * Any of the 16 bytes can be read and written to, but it behaves like a regular 8-bit register in 18 | * that it only works with 1 byte at a time. 19 | * 20 | * To select which byte to read or write, the 4-bit memory address register must be used. 21 | * A 4-bit value can represent all the locations in memory from byte at address 0 to byte at address 15. 22 | * 23 | * Supports manual control like the DIP-switches using the program function. 24 | */ 25 | class RandomAccessMemory: public ClockListener, public RegisterListener { 26 | 27 | public: 28 | static const int MEMORY_SIZE = 16; // 16 bytes / 16 x 8 bits 29 | 30 | explicit RandomAccessMemory(const std::shared_ptr &bus); 31 | ~RandomAccessMemory(); 32 | 33 | /** Print current address and all 16 values of memory to standard out. */ 34 | void print(); 35 | 36 | /** Reset the current address to 0. */ 37 | void reset(); 38 | 39 | /** Puts the specified opcode and operand into memory at the current address in manual mode. */ 40 | void program(const std::bitset<4> &opcode, const std::bitset<4> &operand); 41 | 42 | /** Take the value from the bus on next clock tick and insert into the current address in memory. */ 43 | virtual void in(); 44 | 45 | /** Output the value at the current address in memory to the bus. */ 46 | virtual void out(); 47 | 48 | /** Set an optional external observer of this random access memory. */ 49 | void setObserver(const std::shared_ptr &newObserver); 50 | 51 | private: 52 | std::shared_ptr bus; 53 | std::shared_ptr observer; 54 | std::array memory{}; 55 | uint8_t address; 56 | bool readOnClock; 57 | 58 | void readFromBus(); 59 | void writeToBus(); 60 | void notifyObserver() const; 61 | 62 | void clockTicked() override; 63 | void invertedClockTicked() override {}; // Not implemented 64 | void registerValueChanged(uint8_t newValue) override; 65 | }; 66 | } 67 | 68 | #endif //INC_8_BIT_COMPUTER_EMULATOR_RANDOMACCESSMEMORY_H 69 | -------------------------------------------------------------------------------- /src/core/RegisterListener.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_REGISTERLISTENER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_REGISTERLISTENER_H 3 | 4 | #include 5 | 6 | namespace Core { 7 | 8 | /** 9 | * Interface for use by a register to notify a listener (implementing this interface) 10 | * of a change in the register value. 11 | */ 12 | class RegisterListener { 13 | 14 | public: 15 | /** The register has change its value to newValue. */ 16 | virtual void registerValueChanged(uint8_t newValue) = 0; 17 | }; 18 | } 19 | 20 | #endif //INC_8_BIT_COMPUTER_EMULATOR_REGISTERLISTENER_H 21 | -------------------------------------------------------------------------------- /src/core/StepCounter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Utils.h" 4 | 5 | #include "StepCounter.h" 6 | 7 | Core::StepCounter::StepCounter(const std::shared_ptr &stepListener) { 8 | if (Utils::debugL2()) { 9 | std::cout << "StepCounter construct" << std::endl; 10 | } 11 | 12 | this->stepListener = stepListener; 13 | this->counter = 0; 14 | } 15 | 16 | Core::StepCounter::~StepCounter() { 17 | if (Utils::debugL2()) { 18 | std::cout << "StepCounter destruct" << std::endl; 19 | } 20 | } 21 | 22 | void Core::StepCounter::print() const { 23 | printf("StepCounter: %d / 0x%02X / " BIT_3_PATTERN " \n", counter, counter, BIT_3_TO_BINARY(counter)); 24 | } 25 | 26 | void Core::StepCounter::reset() { 27 | counter = 0; 28 | 29 | notifyObserver(); 30 | notifyListener(); 31 | } 32 | 33 | void Core::StepCounter::increment() { 34 | counter = ++counter % 5; 35 | 36 | if (Utils::debugL2()) { 37 | std::cout << "StepCounter: incremented to " << (int) counter << std::endl; 38 | } 39 | 40 | notifyObserver(); 41 | notifyListener(); 42 | } 43 | 44 | void Core::StepCounter::invertedClockTicked() { 45 | if (Utils::debugL2()) { 46 | std::cout << "StepCounter: inverted clock ticked" << std::endl; 47 | } 48 | 49 | increment(); 50 | } 51 | 52 | void Core::StepCounter::notifyObserver() const { 53 | if (observer != nullptr) { 54 | observer->valueUpdated(counter); 55 | } 56 | } 57 | 58 | void Core::StepCounter::notifyListener() const { 59 | stepListener->stepReady(counter); 60 | } 61 | 62 | void Core::StepCounter::setObserver(const std::shared_ptr &newObserver) { 63 | observer = newObserver; 64 | } 65 | -------------------------------------------------------------------------------- /src/core/StepCounter.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_STEPCOUNTER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_STEPCOUNTER_H 3 | 4 | #include 5 | 6 | #include "ClockListener.h" 7 | #include "StepListener.h" 8 | #include "ValueObserver.h" 9 | 10 | namespace Core { 11 | 12 | /** 13 | * 3-bit counter (0->7) that keeps track of which of the 5 steps of an instruction to execute. 14 | * These steps are also called T-states, or timing states. 15 | * 16 | * The counter increments on the falling edge of the clock and then notifies listeners of the current step. 17 | */ 18 | class StepCounter: public ClockListener { 19 | 20 | public: 21 | explicit StepCounter(const std::shared_ptr &stepListener); 22 | ~StepCounter(); 23 | 24 | /** Reset the counter to 0. */ 25 | void reset(); 26 | 27 | /** Print the current counter value to standard out. */ 28 | void print() const; 29 | 30 | /** Set an optional external observer of this step counter. */ 31 | void setObserver(const std::shared_ptr &newObserver); 32 | 33 | private: 34 | uint8_t counter; 35 | std::shared_ptr stepListener; 36 | std::shared_ptr observer; 37 | 38 | void increment(); 39 | void notifyObserver() const; 40 | void notifyListener() const; 41 | 42 | void clockTicked() override {}; // Not implemented 43 | void invertedClockTicked() override; 44 | }; 45 | } 46 | 47 | #endif //INC_8_BIT_COMPUTER_EMULATOR_STEPCOUNTER_H 48 | -------------------------------------------------------------------------------- /src/core/StepListener.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_STEPLISTENER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_STEPLISTENER_H 3 | 4 | #include 5 | 6 | namespace Core { 7 | 8 | /** 9 | * Interface to be implemented by those who want to be notified when the step counter is ready with a new step. 10 | */ 11 | class StepListener { 12 | 13 | public: 14 | /** The specified step is now ready to be handled. */ 15 | virtual void stepReady(uint8_t step) = 0; 16 | }; 17 | } 18 | 19 | #endif //INC_8_BIT_COMPUTER_EMULATOR_STEPLISTENER_H 20 | -------------------------------------------------------------------------------- /src/core/TimeSource.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "Utils.h" 5 | 6 | #include "TimeSource.h" 7 | 8 | Core::TimeSource::TimeSource() { 9 | if (Utils::debugL2()) { 10 | std::cout << "TimeSource construct" << std::endl; 11 | } 12 | 13 | lastTime = std::chrono::steady_clock::now(); 14 | } 15 | 16 | Core::TimeSource::~TimeSource() { 17 | if (Utils::debugL2()) { 18 | std::cout << "TimeSource destruct" << std::endl; 19 | } 20 | } 21 | 22 | void Core::TimeSource::reset() { 23 | lastTime = std::chrono::steady_clock::now(); 24 | } 25 | 26 | double Core::TimeSource::delta() { 27 | auto currentTime = std::chrono::steady_clock::now(); 28 | auto delta = currentTime - lastTime; 29 | 30 | lastTime = currentTime; 31 | 32 | return delta.count(); 33 | } 34 | 35 | void Core::TimeSource::sleep(const long nanoseconds) const { 36 | std::this_thread::sleep_for(std::chrono::nanoseconds(nanoseconds)); 37 | } 38 | -------------------------------------------------------------------------------- /src/core/TimeSource.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_TIMESOURCE_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_TIMESOURCE_H 3 | 4 | #include 5 | 6 | namespace Core { 7 | 8 | /** 9 | * Helper class for sleeping and keeping track of time. 10 | * 11 | * 1 second = 1000 milliseconds 12 | * 1 millisecond = 1000 microseconds 13 | * 1 microsecond = 1000 nanoseconds 14 | */ 15 | class TimeSource { 16 | 17 | public: 18 | TimeSource(); 19 | ~TimeSource(); 20 | 21 | /** Reset time delta to 0. */ 22 | virtual void reset(); 23 | 24 | /** Return amount of time in nanoseconds that has passed since last time this method or reset was called. */ 25 | virtual double delta(); 26 | 27 | /** Sleeps the current thread the specified number of nanoseconds. */ 28 | virtual void sleep(long nanoseconds) const; 29 | 30 | private: 31 | std::chrono::steady_clock::time_point lastTime; 32 | }; 33 | } 34 | 35 | #endif //INC_8_BIT_COMPUTER_EMULATOR_TIMESOURCE_H 36 | -------------------------------------------------------------------------------- /src/core/Utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Utils.h" 4 | 5 | /* 6 | * These debug methods are available to avoid warnings about value always being true/false in ifs when using the 7 | * constant, and unreachable code inside. 8 | */ 9 | 10 | bool Core::Utils::debugL1() { 11 | return DEBUG >= 1; 12 | } 13 | 14 | bool Core::Utils::debugL2() { 15 | return DEBUG >= 2; 16 | } 17 | 18 | std::bitset<4> Core::Utils::to4bits(const uint8_t value) { 19 | return std::bitset<4>(value); 20 | } 21 | 22 | bool Core::Utils::startsWith(const std::string &stringToCheck, const std::string &valueToLookFor) { 23 | if (valueToLookFor.empty()) { 24 | return false; 25 | } 26 | 27 | return stringToCheck.compare(0, valueToLookFor.length(), valueToLookFor) == 0; 28 | } 29 | 30 | bool Core::Utils::isLessThan(double x, double y) { 31 | if (x >= y) { 32 | return false; 33 | } 34 | 35 | // Feels hacky. There must be a better way... 36 | return std::fabs(x - y) > 0.000001; //std::numeric_limits::epsilon();; 37 | } 38 | 39 | bool Core::Utils::equals(double x, double y) { 40 | if (x == y) { 41 | return true; 42 | } 43 | 44 | // Feels hacky. There must be a better way... 45 | return std::fabs(x - y) <= 0.000001; //std::numeric_limits::epsilon(); 46 | } 47 | -------------------------------------------------------------------------------- /src/core/Utils.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_UTILS_H 2 | #define INC_8_BIT_COMPUTER_UTILS_H 3 | 4 | #include 5 | 6 | // Pattern for printf to display 3 bits in binary 7 | #define BIT_3_PATTERN "%c%c%c" 8 | #define BIT_3_TO_BINARY(byte) \ 9 | (byte & 0x04 ? '1' : '0'), \ 10 | (byte & 0x02 ? '1' : '0'), \ 11 | (byte & 0x01 ? '1' : '0') 12 | 13 | // Pattern for printf to display 4 bits in binary 14 | #define BIT_4_PATTERN "%c%c%c%c" 15 | #define BIT_4_TO_BINARY(byte) \ 16 | (byte & 0x08 ? '1' : '0'), \ 17 | (byte & 0x04 ? '1' : '0'), \ 18 | (byte & 0x02 ? '1' : '0'), \ 19 | (byte & 0x01 ? '1' : '0') 20 | 21 | // Pattern for printf to display a byte in binary 22 | #define BYTE_PATTERN "%c%c%c%c%c%c%c%c" 23 | #define BYTE_TO_BINARY(byte) \ 24 | (byte & 0x80 ? '1' : '0'), \ 25 | (byte & 0x40 ? '1' : '0'), \ 26 | (byte & 0x20 ? '1' : '0'), \ 27 | (byte & 0x10 ? '1' : '0'), \ 28 | (byte & 0x08 ? '1' : '0'), \ 29 | (byte & 0x04 ? '1' : '0'), \ 30 | (byte & 0x02 ? '1' : '0'), \ 31 | (byte & 0x01 ? '1' : '0') 32 | 33 | namespace Core { 34 | 35 | /** 36 | * Just misc utilities and constants. 37 | */ 38 | class Utils { 39 | 40 | public: 41 | /** Enable debug logs? 0 = none, 1 = important, 2 = all */ 42 | static const int DEBUG = 0; 43 | 44 | /** 15, or 1111 in binary, is the max value that 4 bits can represent. */ 45 | static const int FOUR_BITS_MAX = 15; 46 | 47 | /** Display the most important debug logs. */ 48 | static bool debugL1(); 49 | 50 | /** Display all debug logs. */ 51 | static bool debugL2(); 52 | 53 | /** Returns the value as a 4-bit bitset. */ 54 | static std::bitset<4> to4bits(uint8_t value); 55 | 56 | /** Checks if the first string starts with the value of the second string. Comes in C++20 */ 57 | static bool startsWith(const std::string &stringToCheck, const std::string &valueToLookFor); 58 | 59 | /** Checks if x is less than y, handling floating point rounding errors. */ 60 | static bool isLessThan(double x, double y); 61 | 62 | /** Checks if x is more or less equal to y, handling floating point rounding errors. */ 63 | static bool equals(double x, double y); 64 | }; 65 | } 66 | 67 | #endif //INC_8_BIT_COMPUTER_UTILS_H 68 | -------------------------------------------------------------------------------- /src/core/ValueObserver.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_VALUEOBSERVER_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_VALUEOBSERVER_H 3 | 4 | #include 5 | 6 | namespace Core { 7 | 8 | /** 9 | * Interface for external observation of core components of the computer. 10 | * This is a generic observer for components with only a single observable value. 11 | */ 12 | class ValueObserver { 13 | 14 | public: 15 | /** The observed value has changed to the following new value. */ 16 | virtual void valueUpdated(uint8_t newValue) = 0; 17 | }; 18 | } 19 | 20 | #endif //INC_8_BIT_COMPUTER_EMULATOR_VALUEOBSERVER_H 21 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "ui/UserInterface.h" 5 | 6 | int main(int argc, char **argv) { 7 | std::cout << "Starting the 8-bit-computer emulator" << std::endl; 8 | 9 | if (argc != 2) { 10 | std::cerr << "Usage: 8bit " << std::endl; 11 | return EXIT_FAILURE; 12 | } 13 | 14 | std::string fileName = argv[1]; 15 | 16 | try { 17 | const auto ui = std::make_unique(fileName); 18 | ui->start(); 19 | } catch (const std::runtime_error &e) { 20 | std::cerr << e.what() << std::endl; 21 | return EXIT_FAILURE; 22 | } 23 | 24 | std::cout << "Finished" << std::endl; 25 | 26 | return EXIT_SUCCESS; 27 | } 28 | -------------------------------------------------------------------------------- /src/ui/ArithmeticLogicUnitModel.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../core/Utils.h" 5 | 6 | #include "ArithmeticLogicUnitModel.h" 7 | 8 | UI::ArithmeticLogicUnitModel::ArithmeticLogicUnitModel() { 9 | if (Core::Utils::debugL2()) { 10 | std::cout << "ArithmeticLogicUnitModel construct" << std::endl; 11 | } 12 | 13 | this->value = 0; 14 | this->carry = false; 15 | this->zero = true; 16 | } 17 | 18 | UI::ArithmeticLogicUnitModel::~ArithmeticLogicUnitModel() { 19 | if (Core::Utils::debugL2()) { 20 | std::cout << "ArithmeticLogicUnitModel destruct" << std::endl; 21 | } 22 | } 23 | 24 | void UI::ArithmeticLogicUnitModel::resultUpdated(const uint8_t newValue, const bool newCarryBit, const bool newZeroBit) { 25 | value = newValue; 26 | carry = newCarryBit; 27 | zero = newZeroBit; 28 | } 29 | 30 | std::string UI::ArithmeticLogicUnitModel::getRenderText() const { 31 | return "Arithmetic Logic Unit: " + 32 | std::bitset<8>(value).to_string() + " / " + std::to_string(value) + 33 | " C=" + std::to_string(carry) + " Z=" + std::to_string(zero); 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/ArithmeticLogicUnitModel.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_ARITHMETICLOGICUNITMODEL_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_ARITHMETICLOGICUNITMODEL_H 3 | 4 | #include 5 | 6 | #include "../core/ArithmeticLogicUnitObserver.h" 7 | 8 | namespace UI { 9 | 10 | /** 11 | * Observes the core arithmetic logic unit and prepares the state for presentation in the user interface. 12 | */ 13 | class ArithmeticLogicUnitModel : public Core::ArithmeticLogicUnitObserver { 14 | 15 | public: 16 | ArithmeticLogicUnitModel(); 17 | ~ArithmeticLogicUnitModel(); 18 | 19 | [[nodiscard]] std::string getRenderText() const; 20 | 21 | private: 22 | uint8_t value; 23 | bool carry; 24 | bool zero; 25 | 26 | void resultUpdated(uint8_t newValue, bool newCarryBit, bool newZeroBit) override; 27 | }; 28 | } 29 | 30 | #endif //INC_8_BIT_COMPUTER_EMULATOR_ARITHMETICLOGICUNITMODEL_H 31 | -------------------------------------------------------------------------------- /src/ui/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/sdl2) 2 | 3 | find_package(Threads REQUIRED) 4 | find_package(SDL2 REQUIRED) 5 | find_package(SDL2_ttf REQUIRED) 6 | 7 | add_library(8bit-ui Window.cpp Window.h UserInterface.cpp UserInterface.h ValueModel.cpp ValueModel.h ClockModel.cpp ClockModel.h ArithmeticLogicUnitModel.cpp ArithmeticLogicUnitModel.h FlagsRegisterModel.cpp FlagsRegisterModel.h InstructionModel.cpp InstructionModel.h RandomAccessMemoryModel.cpp RandomAccessMemoryModel.h InstructionDecoderModel.cpp InstructionDecoderModel.h Keyboard.cpp Keyboard.h) 8 | 9 | target_link_libraries(8bit-ui 8bit-core) 10 | target_link_libraries(8bit-ui ${CMAKE_THREAD_LIBS_INIT}) 11 | target_link_libraries(8bit-ui SDL2::Main SDL2::TTF) 12 | -------------------------------------------------------------------------------- /src/ui/ClockModel.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include // Needed to build on macos and windows 4 | 5 | #include "../core/Utils.h" 6 | 7 | #include "ClockModel.h" 8 | 9 | UI::ClockModel::ClockModel() { 10 | if (Core::Utils::debugL2()) { 11 | std::cout << "ClockModel construct" << std::endl; 12 | } 13 | 14 | this->on = false; 15 | this->frequency = 0; 16 | } 17 | 18 | UI::ClockModel::~ClockModel() { 19 | if (Core::Utils::debugL2()) { 20 | std::cout << "ClockModel destruct" << std::endl; 21 | } 22 | } 23 | 24 | void UI::ClockModel::clockTicked(const bool newOn) { 25 | on = newOn; 26 | } 27 | 28 | void UI::ClockModel::frequencyChanged(const double newHz) { 29 | frequency = newHz; 30 | } 31 | 32 | std::string UI::ClockModel::getRenderText() const { 33 | std::stringstream hzStream; 34 | hzStream << std::fixed << std::setprecision(1) << frequency; 35 | 36 | return "Clock: " + std::to_string(on) + " / " + hzStream.str() + " Hz"; 37 | } 38 | -------------------------------------------------------------------------------- /src/ui/ClockModel.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_CLOCKMODEL_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_CLOCKMODEL_H 3 | 4 | #include 5 | 6 | #include "../core/ClockObserver.h" 7 | 8 | namespace UI { 9 | 10 | /** 11 | * Observes the core clock and prepares the state for presentation in the user interface. 12 | */ 13 | class ClockModel : public Core::ClockObserver { 14 | 15 | public: 16 | ClockModel(); 17 | ~ClockModel(); 18 | 19 | [[nodiscard]] std::string getRenderText() const; 20 | 21 | private: 22 | bool on; 23 | double frequency; 24 | 25 | void clockTicked(bool newOn) override; 26 | void frequencyChanged(double newHz) override; 27 | }; 28 | } 29 | 30 | #endif //INC_8_BIT_COMPUTER_EMULATOR_CLOCKMODEL_H 31 | -------------------------------------------------------------------------------- /src/ui/FlagsRegisterModel.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../core/Utils.h" 4 | 5 | #include "FlagsRegisterModel.h" 6 | 7 | UI::FlagsRegisterModel::FlagsRegisterModel() { 8 | if (Core::Utils::debugL2()) { 9 | std::cout << "FlagsRegisterModel construct" << std::endl; 10 | } 11 | 12 | this->carryFlag = false; 13 | this->zeroFlag = false; 14 | } 15 | 16 | UI::FlagsRegisterModel::~FlagsRegisterModel() { 17 | if (Core::Utils::debugL2()) { 18 | std::cout << "FlagsRegisterModel destruct" << std::endl; 19 | } 20 | } 21 | 22 | void UI::FlagsRegisterModel::flagsUpdated(const bool newCarryFlag, const bool newZeroFlag) { 23 | carryFlag = newCarryFlag; 24 | zeroFlag = newZeroFlag; 25 | } 26 | 27 | std::string UI::FlagsRegisterModel::getRenderText() const { 28 | return "Flags: C=" + std::to_string(carryFlag) + " Z=" + std::to_string(zeroFlag); 29 | } 30 | -------------------------------------------------------------------------------- /src/ui/FlagsRegisterModel.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_FLAGSREGISTERMODEL_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_FLAGSREGISTERMODEL_H 3 | 4 | #include 5 | 6 | #include "../core/FlagsRegisterObserver.h" 7 | 8 | namespace UI { 9 | 10 | /** 11 | * Observes the core flags register and prepares the state for presentation in the user interface. 12 | */ 13 | class FlagsRegisterModel : public Core::FlagsRegisterObserver { 14 | 15 | public: 16 | FlagsRegisterModel(); 17 | ~FlagsRegisterModel(); 18 | 19 | [[nodiscard]] std::string getRenderText() const; 20 | 21 | private: 22 | bool carryFlag; 23 | bool zeroFlag; 24 | 25 | void flagsUpdated(bool newCarryFlag, bool newZeroFlag) override; 26 | }; 27 | } 28 | 29 | #endif //INC_8_BIT_COMPUTER_EMULATOR_FLAGSREGISTERMODEL_H 30 | -------------------------------------------------------------------------------- /src/ui/InstructionDecoderModel.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../core/Utils.h" 4 | 5 | #include "InstructionDecoderModel.h" 6 | 7 | UI::InstructionDecoderModel::InstructionDecoderModel() { 8 | if (Core::Utils::debugL2()) { 9 | std::cout << "InstructionDecoderModel construct" << std::endl; 10 | } 11 | } 12 | 13 | UI::InstructionDecoderModel::~InstructionDecoderModel() { 14 | if (Core::Utils::debugL2()) { 15 | std::cout << "InstructionDecoderModel destruct" << std::endl; 16 | } 17 | } 18 | 19 | void UI::InstructionDecoderModel::controlWordUpdated(const std::vector &newLines) { 20 | for (auto [key, value] : lines) { 21 | lines[key] = false; 22 | } 23 | 24 | for (Core::ControlLine line : newLines) { 25 | lines[line] = true; 26 | } 27 | } 28 | 29 | std::string UI::InstructionDecoderModel::getRenderTitleText() const { 30 | return "HLT MI RI RO II IO AI AO BI BO S- SO OI O- CE CO CJ FI"; 31 | } 32 | 33 | std::string UI::InstructionDecoderModel::getRenderValueText() const { 34 | return " " + std::to_string(lines.at(Core::ControlLine::HLT)) + 35 | " " + std::to_string(lines.at(Core::ControlLine::MI)) + 36 | " " + std::to_string(lines.at(Core::ControlLine::RI)) + 37 | " " + std::to_string(lines.at(Core::ControlLine::RO)) + 38 | " " + std::to_string(lines.at(Core::ControlLine::II)) + 39 | " " + std::to_string(lines.at(Core::ControlLine::IO)) + 40 | " " + std::to_string(lines.at(Core::ControlLine::AI)) + 41 | " " + std::to_string(lines.at(Core::ControlLine::AO)) + 42 | " " + std::to_string(lines.at(Core::ControlLine::BI)) + 43 | " " + std::to_string(lines.at(Core::ControlLine::BO)) + 44 | " " + std::to_string(lines.at(Core::ControlLine::SM)) + 45 | " " + std::to_string(lines.at(Core::ControlLine::SO)) + 46 | " " + std::to_string(lines.at(Core::ControlLine::OI)) + 47 | " " + std::to_string(lines.at(Core::ControlLine::OM)) + 48 | " " + std::to_string(lines.at(Core::ControlLine::CE)) + 49 | " " + std::to_string(lines.at(Core::ControlLine::CO)) + 50 | " " + std::to_string(lines.at(Core::ControlLine::CJ)) + 51 | " " + std::to_string(lines.at(Core::ControlLine::FI)); 52 | } 53 | -------------------------------------------------------------------------------- /src/ui/InstructionDecoderModel.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONDECODERMODEL_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONDECODERMODEL_H 3 | 4 | #include 5 | #include 6 | 7 | #include "../core/InstructionDecoderObserver.h" 8 | 9 | namespace UI { 10 | 11 | /** 12 | * Observes the core instruction decoder and prepares the state for presentation in the user interface. 13 | */ 14 | class InstructionDecoderModel : public Core::InstructionDecoderObserver { 15 | 16 | public: 17 | InstructionDecoderModel(); 18 | ~InstructionDecoderModel(); 19 | 20 | [[nodiscard]] std::string getRenderTitleText() const; 21 | [[nodiscard]] std::string getRenderValueText() const; 22 | 23 | private: 24 | std::map lines { 25 | {Core::ControlLine::HLT, false}, 26 | {Core::ControlLine::MI, false}, 27 | {Core::ControlLine::RI, false}, 28 | {Core::ControlLine::RO, false}, 29 | {Core::ControlLine::II, false}, 30 | {Core::ControlLine::IO, false}, 31 | {Core::ControlLine::AI, false}, 32 | {Core::ControlLine::AO, false}, 33 | {Core::ControlLine::BI, false}, 34 | {Core::ControlLine::BO, false}, 35 | {Core::ControlLine::SM, false}, 36 | {Core::ControlLine::SO, false}, 37 | {Core::ControlLine::OI, false}, 38 | {Core::ControlLine::OM, false}, 39 | {Core::ControlLine::CE, false}, 40 | {Core::ControlLine::CO, false}, 41 | {Core::ControlLine::CJ, false}, 42 | {Core::ControlLine::FI, false}, 43 | }; 44 | 45 | void controlWordUpdated(const std::vector &newLines) override; 46 | }; 47 | } 48 | 49 | #endif //INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONDECODERMODEL_H 50 | -------------------------------------------------------------------------------- /src/ui/InstructionModel.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../core/Disassembler.h" 4 | #include "../core/Utils.h" 5 | 6 | #include "InstructionModel.h" 7 | 8 | UI::InstructionModel::InstructionModel(const std::shared_ptr &stepCounter, 9 | const std::shared_ptr &instructionRegister) { 10 | if (Core::Utils::debugL2()) { 11 | std::cout << "InstructionModel construct" << std::endl; 12 | } 13 | 14 | this->stepCounter = stepCounter; 15 | this->instructionRegister = instructionRegister; 16 | } 17 | 18 | UI::InstructionModel::~InstructionModel() { 19 | if (Core::Utils::debugL2()) { 20 | std::cout << "InstructionModel destruct" << std::endl; 21 | } 22 | } 23 | 24 | std::string UI::InstructionModel::getRenderText() const { 25 | if (stepCounter->getValue() < 2) { 26 | return "Instruction: FETCH"; 27 | } 28 | 29 | return "Instruction: " + Core::Disassembler::disassemble(instructionRegister->getValue()); 30 | } 31 | -------------------------------------------------------------------------------- /src/ui/InstructionModel.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONMODEL_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONMODEL_H 3 | 4 | #include 5 | #include 6 | 7 | #include "ValueModel.h" 8 | 9 | namespace UI { 10 | 11 | /** 12 | * Infers which instruction is being executed at the moment based on the state of the 13 | * core step counter and instruction register for presentation in the user interface. 14 | */ 15 | class InstructionModel { 16 | 17 | public: 18 | InstructionModel(const std::shared_ptr &stepCounter, 19 | const std::shared_ptr &instructionRegister); 20 | ~InstructionModel(); 21 | 22 | [[nodiscard]] std::string getRenderText() const; 23 | 24 | private: 25 | std::shared_ptr stepCounter; 26 | std::shared_ptr instructionRegister; 27 | }; 28 | } 29 | 30 | #endif //INC_8_BIT_COMPUTER_EMULATOR_INSTRUCTIONMODEL_H 31 | -------------------------------------------------------------------------------- /src/ui/Keyboard.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../core/Utils.h" 4 | 5 | #include "Keyboard.h" 6 | 7 | UI::Keyboard::Keyboard(const std::shared_ptr &emulator) { 8 | if (Core::Utils::debugL2()) { 9 | std::cout << "Keyboard construct" << std::endl; 10 | } 11 | 12 | this->emulator = emulator; 13 | } 14 | 15 | UI::Keyboard::~Keyboard() { 16 | if (Core::Utils::debugL2()) { 17 | std::cout << "Keyboard destruct" << std::endl; 18 | } 19 | } 20 | 21 | void UI::Keyboard::keyUp(const SDL_Keycode keycode) { 22 | // s: start / stop 23 | if (keycode == SDLK_s) { 24 | if (emulator->isRunning()) { 25 | emulator->stop(); 26 | } else { 27 | emulator->startAsynchronous(); 28 | } 29 | } 30 | 31 | // r: reload ram and reset everything else 32 | else if (keycode == SDLK_r) { 33 | if (!emulator->isRunning()) { 34 | emulator->reload(); 35 | } 36 | } 37 | 38 | // space: single step 39 | else if (keycode == SDLK_SPACE) { 40 | emulator->singleStep(); 41 | } 42 | 43 | // +: increase frequency 44 | else if (keycode == SDLK_PLUS || keycode == SDLK_KP_PLUS) { 45 | emulator->increaseFrequency(); 46 | } 47 | 48 | // -: decrease frequency 49 | else if (keycode == SDLK_MINUS || keycode == SDLK_KP_MINUS) { 50 | emulator->decreaseFrequency(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ui/Keyboard.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_KEYBOARD_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_KEYBOARD_H 3 | 4 | #include 5 | #include 6 | 7 | #include "../core/Emulator.h" 8 | 9 | namespace UI { 10 | 11 | /** 12 | * Handles keyboard events for controlling the emulator. 13 | */ 14 | class Keyboard { 15 | 16 | public: 17 | explicit Keyboard(const std::shared_ptr &emulator); 18 | ~Keyboard(); 19 | 20 | void keyUp(SDL_Keycode keycode); 21 | 22 | private: 23 | std::shared_ptr emulator; 24 | }; 25 | } 26 | 27 | #endif //INC_8_BIT_COMPUTER_EMULATOR_KEYBOARD_H 28 | -------------------------------------------------------------------------------- /src/ui/RandomAccessMemoryModel.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../core/Utils.h" 5 | 6 | #include "RandomAccessMemoryModel.h" 7 | 8 | UI::RandomAccessMemoryModel::RandomAccessMemoryModel(const std::shared_ptr &memoryAddressRegister) { 9 | if (Core::Utils::debugL2()) { 10 | std::cout << "RandomAccessMemoryModel construct" << std::endl; 11 | } 12 | 13 | this->memoryAddressRegister = memoryAddressRegister; 14 | this->value = 0; 15 | } 16 | 17 | UI::RandomAccessMemoryModel::~RandomAccessMemoryModel() { 18 | if (Core::Utils::debugL2()) { 19 | std::cout << "RandomAccessMemoryModel destruct" << std::endl; 20 | } 21 | } 22 | 23 | void UI::RandomAccessMemoryModel::valueUpdated(const uint8_t newValue) { 24 | value = newValue; 25 | memory[memoryAddressRegister->getValue()] = newValue; 26 | } 27 | 28 | std::string UI::RandomAccessMemoryModel::getRenderText() const { 29 | return "Random Access Memory: " + std::bitset<8>(value).to_string() + " / " + std::to_string(value); 30 | } 31 | 32 | std::array UI::RandomAccessMemoryModel::getRenderTextFull() const { 33 | std::array text{}; 34 | 35 | for (int i = 0; i < memory.size(); i++) { 36 | const uint8_t currentValue = memory[i]; 37 | text[i] = std::bitset<8>(currentValue).to_string() + " / " + std::to_string(currentValue); 38 | } 39 | 40 | return text; 41 | } 42 | -------------------------------------------------------------------------------- /src/ui/RandomAccessMemoryModel.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_RANDOMACCESSMEMORYMODEL_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_RANDOMACCESSMEMORYMODEL_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "ValueModel.h" 9 | 10 | namespace UI { 11 | 12 | /** 13 | * Observes the core random access memory and prepares the state for presentation in the user interface. 14 | * Supports presentation of the value in the current memory address as well as mapping out the full 15 | * memory content. 16 | */ 17 | class RandomAccessMemoryModel: public Core::ValueObserver { 18 | 19 | public: 20 | static const int MEMORY_SIZE = 16; 21 | 22 | explicit RandomAccessMemoryModel(const std::shared_ptr &memoryAddressRegister); 23 | ~RandomAccessMemoryModel(); 24 | 25 | [[nodiscard]] std::string getRenderText() const; 26 | [[nodiscard]] std::array getRenderTextFull() const; 27 | 28 | private: 29 | std::array memory{}; 30 | std::shared_ptr memoryAddressRegister; 31 | uint8_t value; 32 | 33 | void valueUpdated(uint8_t newValue) override; 34 | }; 35 | } 36 | 37 | #endif //INC_8_BIT_COMPUTER_EMULATOR_RANDOMACCESSMEMORYMODEL_H 38 | -------------------------------------------------------------------------------- /src/ui/UserInterface.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "Window.h" 5 | 6 | #include "../core/Utils.h" 7 | 8 | #include "UserInterface.h" 9 | 10 | UI::UserInterface::UserInterface(const std::string &fileName) { 11 | if (Core::Utils::debugL2()) { 12 | std::cout << "UserInterface construct" << std::endl; 13 | } 14 | 15 | this->fileName = fileName; 16 | this->running = false; 17 | 18 | this->emulator = std::make_shared(); 19 | this->keyboard = std::make_shared(this->emulator); 20 | this->window = std::make_unique("8bit " + fileName, this->keyboard); 21 | 22 | this->clock = std::make_shared(); 23 | this->bus = std::make_shared("Bus", 8); 24 | this->aRegister = std::make_shared("A Register", 8); 25 | this->bRegister = std::make_shared("B Register", 8); 26 | this->arithmeticLogicUnit = std::make_shared(); 27 | this->memoryAddressRegister = std::make_shared("Memory Address Register", 4); 28 | this->programCounter = std::make_shared("Program Counter", 4); 29 | this->randomAccessMemory = std::make_shared(this->memoryAddressRegister); 30 | this->instructionRegister = std::make_shared("Instruction Register", 8); 31 | this->outputRegister = std::make_shared("Output Register", 8); 32 | this->stepCounter = std::make_shared("Step Counter", 3); 33 | this->flagsRegister = std::make_shared(); 34 | this->instruction = std::make_unique(this->stepCounter, this->instructionRegister); 35 | this->instructionDecoder = std::make_shared(); 36 | 37 | this->emulator->setClockObserver(this->clock); 38 | this->emulator->setBusObserver(this->bus); 39 | this->emulator->setARegisterObserver(this->aRegister); 40 | this->emulator->setBRegisterObserver(this->bRegister); 41 | this->emulator->setArithmeticLogicUnitObserver(this->arithmeticLogicUnit); 42 | this->emulator->setMemoryAddressRegisterObserver(this->memoryAddressRegister); 43 | this->emulator->setProgramCounterObserver(this->programCounter); 44 | this->emulator->setRandomAccessMemoryObserver(this->randomAccessMemory); 45 | this->emulator->setInstructionRegisterObserver(this->instructionRegister); 46 | this->emulator->setOutputRegisterObserver(this->outputRegister); 47 | this->emulator->setStepCounterObserver(this->stepCounter); 48 | this->emulator->setFlagsRegisterObserver(this->flagsRegister); 49 | this->emulator->setInstructionDecoderObserver(this->instructionDecoder); 50 | } 51 | 52 | UI::UserInterface::~UserInterface() { 53 | if (Core::Utils::debugL2()) { 54 | std::cout << "UserInterface destruct" << std::endl; 55 | } 56 | } 57 | 58 | void UI::UserInterface::start() { 59 | if (!window->show()) { 60 | throw std::runtime_error("UserInterface: failed to open window"); 61 | } 62 | 63 | running = true; 64 | std::thread mainThread(&UserInterface::mainLoop, this); 65 | 66 | try { 67 | while (!window->isClosed()) { 68 | window->pollEvents(); 69 | } 70 | 71 | running = false; 72 | mainThread.join(); 73 | } 74 | 75 | // Stop the thread controlled, otherwise it will crash hard with no message 76 | catch (const std::runtime_error &e) { 77 | running = false; 78 | mainThread.join(); 79 | 80 | throw e; 81 | } 82 | } 83 | 84 | void UI::UserInterface::mainLoop() { 85 | std::cout << std::endl << "UserInterface: starting main loop" << std::endl << std::endl; 86 | 87 | emulator->load(fileName); 88 | emulator->setFrequency(2); 89 | 90 | while (running) { 91 | window->clearScreen(); 92 | 93 | drawLeftColumn(); 94 | drawRightColumn(); 95 | 96 | window->redraw(); 97 | 98 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 99 | } 100 | 101 | emulator->stop(); 102 | } 103 | 104 | void UI::UserInterface::drawLeftColumn() { 105 | int currentLine = 0; 106 | 107 | drawLeftText(clock->getRenderText(), currentLine++); 108 | drawLeftText(programCounter->getRenderText(), currentLine++); 109 | drawLeftText(bus->getRenderText(), currentLine++); 110 | drawLeftText(aRegister->getRenderText(), currentLine++); 111 | drawLeftText(bRegister->getRenderText(), currentLine++); 112 | drawLeftText(arithmeticLogicUnit->getRenderText(), currentLine++); 113 | drawLeftText(flagsRegister->getRenderText(), currentLine++); 114 | drawLeftText(memoryAddressRegister->getRenderText(), currentLine++); 115 | drawLeftText(randomAccessMemory->getRenderText(), currentLine++); 116 | drawLeftText(stepCounter->getRenderText(), currentLine++); 117 | drawLeftText(instructionRegister->getRenderText(), currentLine++); 118 | drawLeftText(instruction->getRenderText(), currentLine++); 119 | drawLeftText(outputRegister->getRenderText(), currentLine++); 120 | 121 | currentLine++; 122 | 123 | drawLeftText(instructionDecoder->getRenderValueText(), currentLine++); 124 | drawLeftText(instructionDecoder->getRenderTitleText(), currentLine); 125 | } 126 | 127 | void UI::UserInterface::drawLeftText(const std::string &text, const int currentLine) { 128 | window->drawText(text, LEFT_POSITION, currentLine * LINE_HEIGHT); 129 | } 130 | 131 | void UI::UserInterface::drawRightColumn() { 132 | const std::array &ramValues = randomAccessMemory->getRenderTextFull(); 133 | 134 | for (int i = 0; i < ramValues.size(); i++) { 135 | const std::string &ramValue = ramValues[i]; 136 | 137 | if (memoryAddressRegister->getValue() == i) { 138 | window->drawText("* " + ramValue, RIGHT_MARKER_POSITION, i * LINE_HEIGHT); 139 | } else { 140 | window->drawText(ramValue, RIGHT_POSITION, i * LINE_HEIGHT); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/ui/UserInterface.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_USERINTERFACE_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_USERINTERFACE_H 3 | 4 | #include 5 | 6 | #include "ArithmeticLogicUnitModel.h" 7 | #include "ClockModel.h" 8 | #include "FlagsRegisterModel.h" 9 | #include "InstructionDecoderModel.h" 10 | #include "InstructionModel.h" 11 | #include "RandomAccessMemoryModel.h" 12 | #include "ValueModel.h" 13 | #include "Window.h" 14 | 15 | #include "../core/Emulator.h" 16 | 17 | namespace UI { 18 | 19 | /** 20 | * Manages the user interface, as well as the emulator behind. 21 | */ 22 | class UserInterface { 23 | 24 | public: 25 | explicit UserInterface(const std::string &fileName); 26 | ~UserInterface(); 27 | 28 | void start(); 29 | 30 | private: 31 | static const int LINE_HEIGHT = 24; 32 | static const int LEFT_POSITION = 5; 33 | static const int RIGHT_POSITION = 640; 34 | static const int RIGHT_MARKER_POSITION = 618; 35 | 36 | std::unique_ptr window; 37 | std::shared_ptr keyboard; 38 | std::shared_ptr emulator; 39 | 40 | std::shared_ptr clock; 41 | std::shared_ptr bus; 42 | std::shared_ptr aRegister; 43 | std::shared_ptr bRegister; 44 | std::shared_ptr arithmeticLogicUnit; 45 | std::shared_ptr memoryAddressRegister; 46 | std::shared_ptr programCounter; 47 | std::shared_ptr randomAccessMemory; 48 | std::shared_ptr instructionRegister; 49 | std::shared_ptr outputRegister; 50 | std::shared_ptr stepCounter; 51 | std::shared_ptr flagsRegister; 52 | std::unique_ptr instruction; 53 | std::shared_ptr instructionDecoder; 54 | 55 | std::string fileName; 56 | bool running; 57 | 58 | void mainLoop(); 59 | void drawLeftColumn(); 60 | void drawLeftText(const std::string &text, int currentLine); 61 | void drawRightColumn(); 62 | }; 63 | } 64 | 65 | #endif //INC_8_BIT_COMPUTER_EMULATOR_USERINTERFACE_H 66 | -------------------------------------------------------------------------------- /src/ui/ValueModel.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../core/Utils.h" 5 | 6 | #include "ValueModel.h" 7 | 8 | UI::ValueModel::ValueModel(const std::string &name, const size_t bits) { 9 | this->name = name; 10 | this->bits = bits; 11 | this->value = 0; 12 | 13 | if (Core::Utils::debugL2()) { 14 | std::cout << this->name << " Model construct" << std::endl; 15 | } 16 | } 17 | 18 | UI::ValueModel::~ValueModel() { 19 | if (Core::Utils::debugL2()) { 20 | std::cout << this->name << " Model destruct" << std::endl; 21 | } 22 | } 23 | 24 | void UI::ValueModel::valueUpdated(const uint8_t newValue) { 25 | value = newValue; 26 | } 27 | 28 | std::string UI::ValueModel::getRenderText() const { 29 | return name + ": " + valueAsBinary() + " / " + std::to_string(value); 30 | } 31 | 32 | uint8_t UI::ValueModel::getValue() const { 33 | return value; 34 | } 35 | 36 | std::string UI::ValueModel::valueAsBinary() const { 37 | switch (bits) { 38 | case 3: return std::bitset<3>(value).to_string(); 39 | case 4: return std::bitset<4>(value).to_string(); 40 | case 8: return std::bitset<8>(value).to_string(); 41 | default: return "Unhandled"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ui/ValueModel.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_VALUEMODEL_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_VALUEMODEL_H 3 | 4 | #include 5 | 6 | #include "../core/ValueObserver.h" 7 | 8 | namespace UI { 9 | 10 | /** 11 | * Observes a core component with a single 3, 4 or 8-bit value and prepares the value for 12 | * presentation in the user interface. 13 | */ 14 | class ValueModel: public Core::ValueObserver { 15 | 16 | public: 17 | ValueModel(const std::string &name, size_t bits); 18 | ~ValueModel(); 19 | 20 | [[nodiscard]] std::string getRenderText() const; 21 | [[nodiscard]] uint8_t getValue() const; 22 | 23 | private: 24 | std::string name; 25 | size_t bits; 26 | uint8_t value; 27 | 28 | [[nodiscard]] std::string valueAsBinary() const; 29 | 30 | void valueUpdated(uint8_t newValue) override; 31 | }; 32 | } 33 | 34 | #endif //INC_8_BIT_COMPUTER_EMULATOR_VALUEMODEL_H 35 | -------------------------------------------------------------------------------- /src/ui/Window.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../core/Utils.h" 4 | 5 | #include "Window.h" 6 | 7 | UI::Window::Window(const std::string &windowTitle, const std::shared_ptr &keyboard) { 8 | if (Core::Utils::debugL2()) { 9 | std::cout << "Window construct" << std::endl; 10 | } 11 | 12 | this->windowTitle = windowTitle; 13 | this->keyboard = keyboard; 14 | 15 | this->closed = true; 16 | this->window = nullptr; 17 | this->renderer = nullptr; 18 | this->font = nullptr; 19 | } 20 | 21 | UI::Window::~Window() { 22 | if (Core::Utils::debugL2()) { 23 | std::cout << "Window destruct" << std::endl; 24 | } 25 | 26 | TTF_CloseFont(font); 27 | TTF_Quit(); 28 | 29 | SDL_DestroyRenderer(renderer); 30 | SDL_DestroyWindow(window); 31 | SDL_Quit(); 32 | } 33 | 34 | bool UI::Window::show() { 35 | if (init()) { 36 | closed = false; 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | 43 | bool UI::Window::init() { 44 | if (SDL_Init(SDL_INIT_VIDEO) != 0) { 45 | std::cerr << "Window: SDL Init video failed: " << SDL_GetError() << std::endl; 46 | return false; 47 | } 48 | 49 | if (TTF_Init() != 0) { 50 | std::cerr << "Window: SDL TTF failed: " << TTF_GetError() << std::endl; 51 | return false; 52 | } 53 | 54 | window = SDL_CreateWindow( 55 | windowTitle.c_str(), 56 | SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED, 57 | WIDTH, HEIGHT, 58 | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE 59 | ); 60 | 61 | if (window == nullptr) { 62 | std::cerr << "Window: SDL Window failed: " << SDL_GetError() << std::endl; 63 | return false; 64 | } 65 | 66 | renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); 67 | 68 | if (renderer == nullptr) { 69 | std::cerr << "Window: SDL Renderer failed: " << SDL_GetError() << std::endl; 70 | return false; 71 | } 72 | 73 | font = TTF_OpenFont("fonts/Hack-Regular.ttf", FONT_SIZE); 74 | 75 | if (!font) { 76 | std::cerr << "Window: SDL TTF load font failed: " << TTF_GetError() << std::endl; 77 | return false; 78 | } 79 | 80 | SDL_RenderSetLogicalSize(renderer, WIDTH, HEIGHT); // Fix scaling on retina displays 81 | 82 | return true; 83 | } 84 | 85 | void UI::Window::redraw() { 86 | SDL_RenderPresent(renderer); 87 | } 88 | 89 | void UI::Window::clearScreen() { 90 | SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); 91 | SDL_RenderClear(renderer); 92 | } 93 | 94 | void UI::Window::pollEvents() { 95 | SDL_Event windowEvent; 96 | 97 | if (SDL_WaitEvent(&windowEvent)) { 98 | if (windowEvent.type == SDL_QUIT) { 99 | closed = true; 100 | } else if (windowEvent.type == SDL_KEYUP) { 101 | keyboard->keyUp(windowEvent.key.keysym.sym); 102 | } 103 | } 104 | } 105 | 106 | bool UI::Window::isClosed() const { 107 | return closed; 108 | } 109 | 110 | void UI::Window::drawText(const std::string &text, const int xPosition, const int yPosition) { 111 | SDL_Surface *surface = TTF_RenderText_Shaded(font, text.c_str(), ORANGE, BLACK); 112 | SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface); 113 | 114 | SDL_Rect destination{xPosition, yPosition, surface->w, surface->h}; 115 | 116 | SDL_FreeSurface(surface); 117 | SDL_RenderCopy(renderer, texture, nullptr, &destination); 118 | SDL_DestroyTexture(texture); 119 | } 120 | -------------------------------------------------------------------------------- /src/ui/Window.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_8_BIT_COMPUTER_EMULATOR_WINDOW_H 2 | #define INC_8_BIT_COMPUTER_EMULATOR_WINDOW_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include "Keyboard.h" 11 | 12 | namespace UI { 13 | 14 | /** 15 | * Manages a window with support for drawing text. 16 | */ 17 | class Window { 18 | 19 | public: 20 | Window(const std::string &windowTitle, const std::shared_ptr &keyboard); 21 | ~Window(); 22 | 23 | bool show(); 24 | void redraw(); 25 | void clearScreen(); 26 | void pollEvents(); 27 | [[nodiscard]] bool isClosed() const; 28 | void drawText(const std::string &text, int xPosition, int yPosition); 29 | 30 | private: 31 | static const int WIDTH = 800; 32 | static const int HEIGHT = 400; 33 | static const int FONT_SIZE = 18; 34 | 35 | static constexpr SDL_Color ORANGE = {255, 165, 0}; 36 | static constexpr SDL_Color BLACK = {0, 0, 0}; 37 | 38 | std::shared_ptr keyboard; 39 | std::string windowTitle; 40 | bool closed; 41 | 42 | SDL_Window *window; 43 | SDL_Renderer *renderer; 44 | TTF_Font *font; 45 | 46 | bool init(); 47 | }; 48 | } 49 | 50 | #endif //INC_8_BIT_COMPUTER_EMULATOR_WINDOW_H 51 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(${PROJECT_SOURCE_DIR}/src) 2 | include_directories(include) 3 | 4 | add_executable(8bit-tests test_main.cpp core/BusTest.cpp core/FlagsRegisterTest.cpp core/StepCounterTest.cpp core/ProgramCounterTest.cpp core/ArithmeticLogicUnitTest.cpp core/EmulatorIntegrationTest.cpp core/AssemblerTest.cpp core/UtilsTest.cpp core/InstructionDecoderTest.cpp core/RandomAccessMemoryTest.cpp core/MemoryAddressRegisterTest.cpp core/TimeSourceTest.cpp core/ClockTest.cpp core/OutputRegisterTest.cpp core/InstructionRegisterTest.cpp core/GenericRegisterTest.cpp core/DisassemblerTest.cpp core/EmulatorIntegrationStepTest.cpp) 5 | target_link_libraries(8bit-tests 8bit-core) 6 | 7 | enable_testing() 8 | 9 | add_test(ArithmeticLogicUnitTest 8bit-tests --source-file=*ArithmeticLogicUnitTest.cpp) 10 | add_test(AssemblerTest 8bit-tests --source-file=*AssemblerTest.cpp) 11 | add_test(BusTest 8bit-tests --source-file=*BusTest.cpp) 12 | add_test(ClockTest 8bit-tests --source-file=*ClockTest.cpp) 13 | add_test(DisassemblerTest 8bit-tests --source-file=*DisassemblerTest.cpp) 14 | add_test(EmulatorIntegrationStepTest 8bit-tests --source-file=*EmulatorIntegrationStepTest.cpp) 15 | add_test(EmulatorIntegrationTest 8bit-tests --source-file=*EmulatorIntegrationTest.cpp) 16 | add_test(FlagsRegisterTest 8bit-tests --source-file=*FlagsRegisterTest.cpp) 17 | add_test(GenericRegisterTest 8bit-tests --source-file=*GenericRegisterTest.cpp) 18 | add_test(InstructionDecoderTest 8bit-tests --source-file=*InstructionDecoderTest.cpp) 19 | add_test(InstructionRegisterTest 8bit-tests --source-file=*InstructionRegisterTest.cpp) 20 | add_test(MemoryAddressRegisterTest 8bit-tests --source-file=*MemoryAddressRegisterTest.cpp) 21 | add_test(OutputRegisterTest 8bit-tests --source-file=*OutputRegisterTest.cpp) 22 | add_test(ProgramCounterTest 8bit-tests --source-file=*ProgramCounterTest.cpp) 23 | add_test(RandomAccessMemoryTest 8bit-tests --source-file=*RandomAccessMemoryTest.cpp) 24 | add_test(StepCounterTest 8bit-tests --source-file=*StepCounterTest.cpp) 25 | add_test(TimeSourceTest 8bit-tests --source-file=*TimeSourceTest.cpp) 26 | add_test(UtilsTest 8bit-tests --source-file=*UtilsTest.cpp) 27 | -------------------------------------------------------------------------------- /test/core/ArithmeticLogicUnitTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "core/ArithmeticLogicUnit.h" 5 | 6 | using namespace Core; 7 | 8 | TEST_SUITE("ArithmeticLogicUnitTest") { 9 | TEST_CASE("only zero bit should be true on new instance") { 10 | const std::shared_ptr &bus = std::make_shared(); 11 | fakeit::Mock aRegisterMock; 12 | auto aRegisterMockPtr = std::shared_ptr(&aRegisterMock(), [](...) {}); 13 | fakeit::Mock bRegisterMock; 14 | auto bRegisterMockPtr = std::shared_ptr(&bRegisterMock(), [](...) {}); 15 | 16 | fakeit::When(Method(aRegisterMock, readValue)).Return(0); 17 | fakeit::When(Method(bRegisterMock, readValue)).Return(0); 18 | 19 | ArithmeticLogicUnit alu(aRegisterMockPtr, bRegisterMockPtr, bus); 20 | auto &theRegister = dynamic_cast(alu); 21 | 22 | CHECK_FALSE(alu.isCarry()); 23 | CHECK(alu.isZero()); 24 | CHECK_EQ(bus->read(), 0); 25 | 26 | SUBCASE("registerValueChanged() should trigger addition from a and b registers") { 27 | fakeit::When(Method(aRegisterMock, readValue)).Return(8); 28 | fakeit::When(Method(bRegisterMock, readValue)).Return(6); 29 | 30 | theRegister.registerValueChanged(0); 31 | 32 | alu.out(); 33 | CHECK_EQ(bus->read(), 14); 34 | } 35 | 36 | SUBCASE("addition should set only zero true if result is 0") { 37 | fakeit::When(Method(aRegisterMock, readValue)).Return(0); 38 | fakeit::When(Method(bRegisterMock, readValue)).Return(0); 39 | 40 | theRegister.registerValueChanged(0); 41 | 42 | CHECK_FALSE(alu.isCarry()); 43 | CHECK(alu.isZero()); 44 | 45 | alu.out(); 46 | CHECK_EQ(bus->read(), 0); 47 | } 48 | 49 | SUBCASE("addition should set both carry and zero false when positive and not wrapped result") { 50 | fakeit::When(Method(aRegisterMock, readValue)).Return(50); 51 | fakeit::When(Method(bRegisterMock, readValue)).Return(13); 52 | 53 | theRegister.registerValueChanged(0); 54 | 55 | CHECK_FALSE(alu.isCarry()); 56 | CHECK_FALSE(alu.isZero()); 57 | 58 | alu.out(); 59 | CHECK_EQ(bus->read(), 63); 60 | } 61 | 62 | SUBCASE("addition should set both carry and zero true when wrap around to exactly 0") { 63 | fakeit::When(Method(aRegisterMock, readValue)).Return(255); 64 | fakeit::When(Method(bRegisterMock, readValue)).Return(1); 65 | 66 | theRegister.registerValueChanged(0); 67 | 68 | CHECK(alu.isCarry()); 69 | CHECK(alu.isZero()); 70 | 71 | alu.out(); 72 | CHECK_EQ(bus->read(), 0); 73 | } 74 | 75 | SUBCASE("addition should set only carry true when wrap around is more than 0") { 76 | fakeit::When(Method(aRegisterMock, readValue)).Return(250); 77 | fakeit::When(Method(bRegisterMock, readValue)).Return(10); 78 | 79 | theRegister.registerValueChanged(0); 80 | 81 | CHECK(alu.isCarry()); 82 | CHECK_FALSE(alu.isZero()); 83 | 84 | alu.out(); 85 | CHECK_EQ(bus->read(), 4); 86 | 87 | SUBCASE("reset() should set value to 0 and only zero bit to true") { 88 | alu.reset(); 89 | 90 | CHECK_FALSE(alu.isCarry()); 91 | CHECK(alu.isZero()); 92 | 93 | alu.out(); 94 | CHECK_EQ(bus->read(), 0); 95 | } 96 | } 97 | 98 | SUBCASE("addition should notify observer") { 99 | fakeit::Mock observerMock; 100 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 101 | alu.setObserver(observerPtr); 102 | fakeit::When(Method(observerMock, resultUpdated)).Return(); 103 | 104 | fakeit::When(Method(aRegisterMock, readValue)).Return(250); 105 | fakeit::When(Method(bRegisterMock, readValue)).Return(10); 106 | 107 | theRegister.registerValueChanged(0); 108 | 109 | CHECK(alu.isCarry()); 110 | CHECK_FALSE(alu.isZero()); 111 | 112 | fakeit::Verify(Method(observerMock, resultUpdated).Using(4, true, false)).Once(); 113 | } 114 | 115 | SUBCASE("subtract() should trigger subtraction from a and b registers") { 116 | fakeit::When(Method(aRegisterMock, readValue)).Return(8); 117 | fakeit::When(Method(bRegisterMock, readValue)).Return(6); 118 | 119 | alu.subtract(); 120 | 121 | alu.out(); 122 | CHECK_EQ(bus->read(), 2); 123 | } 124 | 125 | SUBCASE("subtract() should set carry for positive result") { 126 | fakeit::When(Method(aRegisterMock, readValue)).Return(30); 127 | fakeit::When(Method(bRegisterMock, readValue)).Return(12); 128 | 129 | alu.subtract(); 130 | 131 | CHECK(alu.isCarry()); 132 | CHECK_FALSE(alu.isZero()); 133 | 134 | alu.out(); 135 | CHECK_EQ(bus->read(), 18); 136 | } 137 | 138 | SUBCASE("subtract() should not set carry when wrapping around") { 139 | fakeit::When(Method(aRegisterMock, readValue)).Return(0); 140 | fakeit::When(Method(bRegisterMock, readValue)).Return(1); 141 | 142 | alu.subtract(); 143 | 144 | CHECK_FALSE(alu.isCarry()); 145 | CHECK_FALSE(alu.isZero()); 146 | 147 | alu.out(); 148 | CHECK_EQ(bus->read(), 255); 149 | } 150 | 151 | SUBCASE("subtract() should set zero when result is 0") { 152 | fakeit::When(Method(aRegisterMock, readValue)).Return(1); 153 | fakeit::When(Method(bRegisterMock, readValue)).Return(1); 154 | 155 | alu.subtract(); 156 | 157 | CHECK(alu.isCarry()); 158 | CHECK(alu.isZero()); 159 | 160 | alu.out(); 161 | CHECK_EQ(bus->read(), 0); 162 | } 163 | 164 | SUBCASE("subtract() should notify observer") { 165 | fakeit::Mock observerMock; 166 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 167 | alu.setObserver(observerPtr); 168 | fakeit::When(Method(observerMock, resultUpdated)).Return(); 169 | 170 | fakeit::When(Method(aRegisterMock, readValue)).Return(1); 171 | fakeit::When(Method(bRegisterMock, readValue)).Return(1); 172 | 173 | alu.subtract(); 174 | 175 | CHECK(alu.isCarry()); 176 | CHECK(alu.isZero()); 177 | 178 | fakeit::Verify(Method(observerMock, resultUpdated).Using(0, true, true)).Once(); 179 | } 180 | 181 | SUBCASE("print() should not fail") { 182 | alu.print(); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /test/core/BusTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "core/Bus.h" 5 | 6 | using namespace Core; 7 | 8 | TEST_SUITE("BusTest") { 9 | TEST_CASE("read() should return 0 on new instance") { 10 | Bus bus = Bus(); 11 | 12 | CHECK(bus.read() == 0); 13 | } 14 | 15 | TEST_CASE("read() should return value set with write()") { 16 | Bus bus = Bus(); 17 | bus.write(10); 18 | 19 | CHECK(bus.read() == 10); 20 | } 21 | 22 | TEST_CASE("observer should be notified when writing to the bus") { 23 | Bus bus = Bus(); 24 | 25 | fakeit::Mock observerMock; 26 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 27 | bus.setObserver(observerPtr); 28 | fakeit::When(Method(observerMock, valueUpdated)).Return(); 29 | 30 | bus.write(8); 31 | 32 | fakeit::Verify(Method(observerMock, valueUpdated).Using(8)).Once(); 33 | fakeit::VerifyNoOtherInvocations(observerMock); 34 | } 35 | 36 | TEST_CASE("reset() should set value to 0") { 37 | Bus bus = Bus(); 38 | 39 | bus.write(5); 40 | CHECK(bus.read() == 5); 41 | 42 | bus.reset(); 43 | CHECK(bus.read() == 0); 44 | } 45 | 46 | TEST_CASE("reset() should notify observer") { 47 | Bus bus = Bus(); 48 | 49 | fakeit::Mock observerMock; 50 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 51 | bus.setObserver(observerPtr); 52 | fakeit::When(Method(observerMock, valueUpdated)).AlwaysReturn(); 53 | 54 | bus.write(250); 55 | fakeit::Verify(Method(observerMock, valueUpdated).Using(250)).Once(); 56 | fakeit::VerifyNoOtherInvocations(observerMock); 57 | 58 | bus.reset(); 59 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); 60 | fakeit::VerifyNoOtherInvocations(observerMock); 61 | } 62 | 63 | TEST_CASE("print() should not fail") { 64 | Bus bus = Bus(); 65 | 66 | bus.print(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/core/DisassemblerTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include // Due to bug with doctest on macOS in release mode: https://github.com/onqtam/doctest/issues/126 4 | #include 5 | 6 | #include "core/Disassembler.h" 7 | 8 | using namespace Core; 9 | 10 | TEST_SUITE("DisassemblerTest") { 11 | TEST_CASE("should disassemble NOP") { 12 | CHECK_EQ("NOP", Disassembler::disassemble(std::bitset<8>("00000000").to_ulong())); 13 | } 14 | 15 | TEST_CASE("should disassemble LDA") { 16 | CHECK_EQ("LDA 10", Disassembler::disassemble(std::bitset<8>("00011010").to_ulong())); 17 | } 18 | 19 | TEST_CASE("should disassemble ADD") { 20 | CHECK_EQ("ADD 11", Disassembler::disassemble(std::bitset<8>("00101011").to_ulong())); 21 | } 22 | 23 | TEST_CASE("should disassemble SUB") { 24 | CHECK_EQ("SUB 12", Disassembler::disassemble(std::bitset<8>("00111100").to_ulong())); 25 | } 26 | 27 | TEST_CASE("should disassemble STA") { 28 | CHECK_EQ("STA 13", Disassembler::disassemble(std::bitset<8>("01001101").to_ulong())); 29 | } 30 | 31 | TEST_CASE("should disassemble LDI") { 32 | CHECK_EQ("LDI 14", Disassembler::disassemble(std::bitset<8>("01011110").to_ulong())); 33 | } 34 | 35 | TEST_CASE("should disassemble JMP") { 36 | CHECK_EQ("JMP 4", Disassembler::disassemble(std::bitset<8>("01100100").to_ulong())); 37 | } 38 | 39 | TEST_CASE("should disassemble JC") { 40 | CHECK_EQ("JC 5", Disassembler::disassemble(std::bitset<8>("01110101").to_ulong())); 41 | } 42 | 43 | TEST_CASE("should disassemble JZ") { 44 | CHECK_EQ("JZ 6", Disassembler::disassemble(std::bitset<8>("10000110").to_ulong())); 45 | } 46 | 47 | TEST_CASE("should disassemble OUT") { 48 | CHECK_EQ("OUT", Disassembler::disassemble(std::bitset<8>("11100000").to_ulong())); 49 | } 50 | 51 | TEST_CASE("should disassemble HLT") { 52 | CHECK_EQ("HLT", Disassembler::disassemble(std::bitset<8>("11110000").to_ulong())); 53 | } 54 | 55 | TEST_CASE("should handle unknown instruction") { 56 | CHECK_EQ("UNKNOWN", Disassembler::disassemble(std::bitset<8>("10010000").to_ulong())); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/core/EmulatorIntegrationTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "core/Emulator.h" 5 | 6 | using namespace Core; 7 | 8 | TEST_SUITE("EmulatorIntegrationTest") { 9 | TEST_CASE("emulator should work correctly") { 10 | Emulator emulator; 11 | 12 | fakeit::Mock observerMock; 13 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 14 | 15 | fakeit::When(Method(observerMock, valueUpdated)).AlwaysReturn(); 16 | 17 | emulator.setOutputRegisterObserver(observerPtr); 18 | emulator.setFrequency(5000); 19 | 20 | SUBCASE("load() should throw exception if file does not exist") { 21 | CHECK_THROWS_WITH(emulator.load("../../programs/test/does_not_exist.asm"), 22 | "Assembler: failed to open file: ../../programs/test/does_not_exist.asm"); 23 | } 24 | 25 | SUBCASE("load() should throw exception if file is empty") { 26 | CHECK_THROWS_WITH(emulator.load("../../programs/test/empty_test.asm"), 27 | "Emulator: no instructions loaded. Aborting"); 28 | } 29 | 30 | SUBCASE("startSynchronous() should complete nop_test.asm") { 31 | emulator.load("../../programs/nop_test.asm"); 32 | 33 | emulator.startSynchronous(); 34 | 35 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); // Reset before start 36 | fakeit::Verify(Method(observerMock, valueUpdated).Using(10)).Once(); 37 | fakeit::VerifyNoOtherInvocations(observerMock); 38 | } 39 | 40 | SUBCASE("startSynchronous() should complete add_two_numbers.asm") { 41 | emulator.load("../../programs/add_two_numbers.asm"); 42 | 43 | emulator.startSynchronous(); 44 | 45 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); // Reset before start 46 | fakeit::Verify(Method(observerMock, valueUpdated).Using(42)).Once(); // 28+14 47 | fakeit::VerifyNoOtherInvocations(observerMock); 48 | } 49 | 50 | SUBCASE("startSynchronous() should complete subtract_two_numbers.asm") { 51 | emulator.load("../../programs/subtract_two_numbers.asm"); 52 | 53 | emulator.startSynchronous(); 54 | 55 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); // Reset before start 56 | fakeit::Verify(Method(observerMock, valueUpdated).Using(18)).Once(); // 30-12 57 | fakeit::VerifyNoOtherInvocations(observerMock); 58 | } 59 | 60 | SUBCASE("startSynchronous() should complete multiply_two_numbers.asm") { 61 | emulator.load("../../programs/multiply_two_numbers.asm"); 62 | 63 | emulator.startSynchronous(); 64 | 65 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); // Reset before start 66 | fakeit::Verify(Method(observerMock, valueUpdated).Using(56)).Once(); // 7*8 67 | fakeit::VerifyNoOtherInvocations(observerMock); 68 | } 69 | 70 | SUBCASE("startSynchronous() should complete count_0_255_stop.asm") { 71 | emulator.load("../../programs/count_0_255_stop.asm"); 72 | 73 | emulator.startSynchronous(); 74 | 75 | // Once for reset, and once when counting 76 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Twice(); 77 | 78 | for (int i = 1; i <= 255; i++) { 79 | fakeit::Verify(Method(observerMock, valueUpdated).Using(i)).Once(); 80 | } 81 | 82 | fakeit::VerifyNoOtherInvocations(observerMock); 83 | } 84 | 85 | SUBCASE("startSynchronous() should complete count_255_0_stop.asm") { 86 | emulator.load("../../programs/count_255_0_stop.asm"); 87 | 88 | emulator.startSynchronous(); 89 | 90 | // Once for reset, and once when counting 91 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Twice(); 92 | 93 | for (int i = 1; i <= 255; i++) { 94 | fakeit::Verify(Method(observerMock, valueUpdated).Using(i)).Once(); 95 | } 96 | 97 | fakeit::VerifyNoOtherInvocations(observerMock); 98 | } 99 | 100 | SUBCASE("reload() should reset all state including memory") { 101 | emulator.load("../../programs/memory_test.asm"); 102 | 103 | emulator.startSynchronous(); 104 | 105 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); 106 | fakeit::Verify(Method(observerMock, valueUpdated).Using(7)).Once(); 107 | fakeit::VerifyNoOtherInvocations(observerMock); 108 | 109 | observerMock.ClearInvocationHistory(); 110 | 111 | emulator.reload(); 112 | emulator.startSynchronous(); 113 | 114 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); 115 | // This would be 11 if memory wasn't reset 116 | fakeit::Verify(Method(observerMock, valueUpdated).Using(7)).Once(); 117 | fakeit::VerifyNoOtherInvocations(observerMock); 118 | } 119 | 120 | SUBCASE("increaseFrequency() and decreaseFrequency() should work") { 121 | fakeit::Mock clockObserver; 122 | auto clockPtr = std::shared_ptr(&clockObserver(), [](...) {}); 123 | emulator.setClockObserver(clockPtr); 124 | 125 | fakeit::When(Method(clockObserver, frequencyChanged)).AlwaysReturn(); 126 | 127 | emulator.setFrequency(2000); 128 | clockObserver.ClearInvocationHistory(); 129 | 130 | emulator.increaseFrequency(); 131 | fakeit::Verify(Method(clockObserver, frequencyChanged).Using(3000)).Once(); 132 | 133 | emulator.decreaseFrequency(); 134 | fakeit::Verify(Method(clockObserver, frequencyChanged).Using(2000)).Once(); 135 | } 136 | 137 | SUBCASE("startAsynchronous() and stop() should work") { 138 | CHECK_FALSE(emulator.isRunning()); 139 | 140 | emulator.load("../../programs/count_255_0_stop.asm"); 141 | emulator.setFrequency(10); 142 | 143 | emulator.startAsynchronous(); 144 | 145 | std::this_thread::sleep_for(std::chrono::milliseconds(500)); 146 | CHECK(emulator.isRunning()); 147 | 148 | emulator.stop(); 149 | 150 | std::this_thread::sleep_for(std::chrono::milliseconds(200)); 151 | CHECK_FALSE(emulator.isRunning()); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /test/core/FlagsRegisterTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "core/FlagsRegister.h" 5 | 6 | using namespace Core; 7 | 8 | TEST_SUITE("FlagsRegisterTest") { 9 | TEST_CASE("flags register should work correctly") { 10 | fakeit::Mock aluMock; 11 | auto aluMockSharedPtr = std::shared_ptr(&aluMock(), [](...) {}); 12 | FlagsRegister flagsRegister(aluMockSharedPtr); 13 | 14 | auto &clock = dynamic_cast(flagsRegister); 15 | 16 | CHECK_FALSE(flagsRegister.isCarryFlag()); 17 | CHECK_FALSE(flagsRegister.isZeroFlag()); 18 | 19 | SUBCASE("clockTicked() should not update flags without in()") { 20 | fakeit::When(Method(aluMock, isCarry)).Return(true); 21 | fakeit::When(Method(aluMock, isZero)).Return(true); 22 | 23 | CHECK_FALSE(flagsRegister.isCarryFlag()); 24 | CHECK_FALSE(flagsRegister.isZeroFlag()); 25 | 26 | clock.clockTicked(); 27 | 28 | CHECK_FALSE(flagsRegister.isCarryFlag()); 29 | CHECK_FALSE(flagsRegister.isZeroFlag()); 30 | } 31 | 32 | SUBCASE("in() should update flags on clock tick") { 33 | fakeit::When(Method(aluMock, isCarry)).Return(true); 34 | fakeit::When(Method(aluMock, isZero)).Return(true); 35 | 36 | flagsRegister.in(); 37 | 38 | CHECK_FALSE(flagsRegister.isCarryFlag()); 39 | CHECK_FALSE(flagsRegister.isZeroFlag()); 40 | 41 | clock.clockTicked(); 42 | 43 | CHECK(flagsRegister.isCarryFlag()); 44 | CHECK(flagsRegister.isZeroFlag()); 45 | 46 | SUBCASE("reset() should set flags to false") { 47 | flagsRegister.reset(); 48 | 49 | CHECK_FALSE(flagsRegister.isCarryFlag()); 50 | CHECK_FALSE(flagsRegister.isZeroFlag()); 51 | } 52 | } 53 | 54 | SUBCASE("in() should only update flags on first clock tick") { 55 | fakeit::When(Method(aluMock, isCarry)).Return(true); 56 | fakeit::When(Method(aluMock, isZero)).Return(true); 57 | 58 | flagsRegister.in(); 59 | 60 | CHECK_FALSE(flagsRegister.isCarryFlag()); 61 | CHECK_FALSE(flagsRegister.isZeroFlag()); 62 | 63 | clock.clockTicked(); 64 | 65 | CHECK(flagsRegister.isCarryFlag()); 66 | CHECK(flagsRegister.isZeroFlag()); 67 | 68 | fakeit::When(Method(aluMock, isCarry)).Return(false); 69 | fakeit::When(Method(aluMock, isZero)).Return(false); 70 | 71 | clock.clockTicked(); 72 | 73 | // Nothing happened 74 | CHECK(flagsRegister.isCarryFlag()); 75 | CHECK(flagsRegister.isZeroFlag()); 76 | } 77 | 78 | SUBCASE("observer should be notified when reading from the alu") { 79 | fakeit::Mock observerMock; 80 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 81 | flagsRegister.setObserver(observerPtr); 82 | fakeit::When(Method(observerMock, flagsUpdated)).Return(); 83 | 84 | fakeit::When(Method(aluMock, isCarry)).Return(true); 85 | fakeit::When(Method(aluMock, isZero)).Return(false); 86 | 87 | flagsRegister.in(); 88 | 89 | fakeit::VerifyNoOtherInvocations(observerMock); // Nothing read from the alu yet 90 | 91 | clock.clockTicked(); 92 | 93 | fakeit::Verify(Method(observerMock, flagsUpdated).Using(true, false)).Once(); 94 | } 95 | 96 | SUBCASE("observer should be notified when resetting") { 97 | fakeit::Mock observerMock; 98 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 99 | flagsRegister.setObserver(observerPtr); 100 | fakeit::When(Method(observerMock, flagsUpdated)).AlwaysReturn(); 101 | 102 | fakeit::When(Method(aluMock, isCarry)).Return(true); 103 | fakeit::When(Method(aluMock, isZero)).Return(false); 104 | 105 | flagsRegister.in(); 106 | clock.clockTicked(); 107 | 108 | fakeit::Verify(Method(observerMock, flagsUpdated).Using(true, false)).Once(); 109 | fakeit::VerifyNoOtherInvocations(observerMock); 110 | 111 | flagsRegister.reset(); 112 | 113 | fakeit::Verify(Method(observerMock, flagsUpdated).Using(false, false)).Once(); 114 | fakeit::VerifyNoOtherInvocations(observerMock); 115 | } 116 | 117 | SUBCASE("print() should not fail") { 118 | flagsRegister.print(); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/core/GenericRegisterTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "core/GenericRegister.h" 5 | 6 | using namespace Core; 7 | 8 | TEST_SUITE("GenericRegisterTest") { 9 | TEST_CASE("generic register should work correctly") { 10 | const std::shared_ptr &bus = std::make_shared(); 11 | GenericRegister genericRegister("some", bus); 12 | auto &clock = dynamic_cast(genericRegister); 13 | 14 | CHECK_EQ(bus->read(), 0); 15 | 16 | SUBCASE("out() should put 0 on the bus on new instance") { 17 | genericRegister.out(); 18 | 19 | CHECK_EQ(bus->read(), 0); 20 | } 21 | 22 | SUBCASE("clock tick should do nothing by itself") { 23 | bus->write(8); 24 | 25 | clock.clockTicked(); 26 | 27 | genericRegister.out(); 28 | CHECK_EQ(bus->read(), 0); 29 | } 30 | 31 | SUBCASE("in() should store value from bus on clock tick") { 32 | bus->write(8); 33 | 34 | genericRegister.in(); 35 | 36 | genericRegister.out(); 37 | CHECK_EQ(bus->read(), 0); // Nothing read from the bus yet 38 | 39 | bus->write(6); // This should be stored 40 | clock.clockTicked(); 41 | 42 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 43 | 44 | genericRegister.out(); 45 | CHECK_EQ(bus->read(), 6); 46 | } 47 | 48 | SUBCASE("in() should only store on first clock tick") { 49 | bus->write(15); 50 | 51 | genericRegister.in(); 52 | clock.clockTicked(); 53 | 54 | genericRegister.out(); 55 | CHECK_EQ(bus->read(), 15); 56 | 57 | bus->write(6); 58 | clock.clockTicked(); 59 | 60 | genericRegister.out(); 61 | CHECK_EQ(bus->read(), 15); // No change 62 | } 63 | 64 | SUBCASE("listener should be notified when reading from the bus") { 65 | fakeit::Mock listenerMock; 66 | auto listenerPtr = std::shared_ptr(&listenerMock(), [](...) {}); 67 | genericRegister.setRegisterListener(listenerPtr); 68 | fakeit::When(Method(listenerMock, registerValueChanged)).Return(); 69 | 70 | bus->write(8); 71 | 72 | genericRegister.in(); // Prepare to read from bus 73 | 74 | fakeit::VerifyNoOtherInvocations(listenerMock); // Nothing read from the bus yet 75 | 76 | bus->write(6); // This should be stored 77 | clock.clockTicked(); 78 | 79 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 80 | 81 | fakeit::Verify(Method(listenerMock, registerValueChanged).Using(6)).Once(); 82 | } 83 | 84 | SUBCASE("observer should be notified when reading from the bus") { 85 | fakeit::Mock observerMock; 86 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 87 | genericRegister.setObserver(observerPtr); 88 | fakeit::When(Method(observerMock, valueUpdated)).Return(); 89 | 90 | bus->write(8); 91 | 92 | genericRegister.in(); // Prepare to read from bus 93 | 94 | fakeit::VerifyNoOtherInvocations(observerMock); // Nothing read from the bus yet 95 | 96 | bus->write(6); // This should be stored 97 | clock.clockTicked(); 98 | 99 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 100 | 101 | fakeit::Verify(Method(observerMock, valueUpdated).Using(6)).Once(); 102 | } 103 | 104 | SUBCASE("reset() should set value to 0 again") { 105 | bus->write(8); 106 | 107 | genericRegister.in(); 108 | clock.clockTicked(); 109 | 110 | genericRegister.out(); 111 | CHECK_EQ(bus->read(), 8); 112 | 113 | genericRegister.reset(); 114 | 115 | genericRegister.out(); 116 | CHECK_EQ(bus->read(), 0); 117 | } 118 | 119 | SUBCASE("reset() should notify the listener") { 120 | fakeit::Mock listenerMock; 121 | auto listenerPtr = std::shared_ptr(&listenerMock(), [](...) {}); 122 | genericRegister.setRegisterListener(listenerPtr); 123 | fakeit::When(Method(listenerMock, registerValueChanged)).AlwaysReturn(); 124 | 125 | bus->write(80); 126 | 127 | genericRegister.in(); 128 | clock.clockTicked(); 129 | 130 | fakeit::Verify(Method(listenerMock, registerValueChanged).Using(80)).Once(); 131 | fakeit::VerifyNoOtherInvocations(listenerMock); 132 | 133 | genericRegister.reset(); 134 | 135 | fakeit::Verify(Method(listenerMock, registerValueChanged).Using(0)).Once(); 136 | fakeit::VerifyNoOtherInvocations(listenerMock); 137 | } 138 | 139 | SUBCASE("reset() should notify the observer") { 140 | fakeit::Mock observerMock; 141 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 142 | genericRegister.setObserver(observerPtr); 143 | fakeit::When(Method(observerMock, valueUpdated)).AlwaysReturn(); 144 | 145 | bus->write(88); 146 | 147 | genericRegister.in(); 148 | clock.clockTicked(); 149 | 150 | fakeit::Verify(Method(observerMock, valueUpdated).Using(88)).Once(); 151 | fakeit::VerifyNoOtherInvocations(observerMock); 152 | 153 | genericRegister.reset(); 154 | 155 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); 156 | fakeit::VerifyNoOtherInvocations(observerMock); 157 | } 158 | 159 | SUBCASE("print() should not fail") { 160 | genericRegister.print(); 161 | } 162 | 163 | SUBCASE("readValue() should return after in() and clock tick") { 164 | bus->write(230); 165 | 166 | genericRegister.in(); 167 | 168 | CHECK_EQ(genericRegister.readValue(), 0); // Nothing yet 169 | 170 | clock.clockTicked(); 171 | 172 | CHECK_EQ(genericRegister.readValue(), 230); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /test/core/InstructionRegisterTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include "core/InstructionRegister.h" 7 | 8 | using namespace Core; 9 | 10 | TEST_SUITE("InstructionRegisterTest") { 11 | TEST_CASE("instruction register should work correctly") { 12 | const std::shared_ptr &bus = std::make_shared(); 13 | InstructionRegister instructionRegister(bus); 14 | auto &clock = dynamic_cast(instructionRegister); 15 | 16 | CHECK_EQ(bus->read(), 0); 17 | 18 | SUBCASE("out() should put 0 on the bus on new instance") { 19 | instructionRegister.out(); 20 | 21 | CHECK_EQ(bus->read(), 0); 22 | } 23 | 24 | SUBCASE("clock tick should do nothing by itself") { 25 | bus->write(8); 26 | 27 | clock.clockTicked(); 28 | 29 | instructionRegister.out(); 30 | CHECK_EQ(bus->read(), 0); 31 | } 32 | 33 | SUBCASE("in() should store value from bus on clock tick") { 34 | bus->write(8); 35 | 36 | instructionRegister.in(); 37 | 38 | instructionRegister.out(); 39 | CHECK_EQ(bus->read(), 0); // Nothing read from the bus yet 40 | 41 | bus->write(6); // This should be stored 42 | clock.clockTicked(); 43 | 44 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 45 | 46 | instructionRegister.out(); 47 | CHECK_EQ(bus->read(), 6); 48 | } 49 | 50 | SUBCASE("in() should only store on first clock tick") { 51 | bus->write(15); 52 | 53 | instructionRegister.in(); 54 | clock.clockTicked(); 55 | 56 | instructionRegister.out(); 57 | CHECK_EQ(bus->read(), 15); 58 | 59 | bus->write(6); 60 | clock.clockTicked(); 61 | 62 | instructionRegister.out(); 63 | CHECK_EQ(bus->read(), 15); // No change 64 | } 65 | 66 | SUBCASE("observer should be notified when reading from the bus") { 67 | fakeit::Mock observerMock; 68 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 69 | instructionRegister.setObserver(observerPtr); 70 | fakeit::When(Method(observerMock, valueUpdated)).Return(); 71 | 72 | bus->write(8); 73 | 74 | instructionRegister.in(); // Prepare to read from bus 75 | 76 | fakeit::VerifyNoOtherInvocations(observerMock); // Nothing read from the bus yet 77 | 78 | bus->write(6); // This should be stored 79 | clock.clockTicked(); 80 | 81 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 82 | 83 | fakeit::Verify(Method(observerMock, valueUpdated).Using(6)).Once(); 84 | } 85 | 86 | SUBCASE("reset() should set value to 0 again") { 87 | bus->write(8); 88 | 89 | instructionRegister.in(); 90 | clock.clockTicked(); 91 | 92 | instructionRegister.out(); 93 | CHECK_EQ(bus->read(), 8); 94 | 95 | instructionRegister.reset(); 96 | 97 | instructionRegister.out(); 98 | CHECK_EQ(bus->read(), 0); 99 | } 100 | 101 | SUBCASE("reset() should notify observer") { 102 | fakeit::Mock observerMock; 103 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 104 | instructionRegister.setObserver(observerPtr); 105 | fakeit::When(Method(observerMock, valueUpdated)).AlwaysReturn(); 106 | 107 | bus->write(83); 108 | 109 | instructionRegister.in(); 110 | clock.clockTicked(); 111 | 112 | fakeit::Verify(Method(observerMock, valueUpdated).Using(83)).Once(); 113 | fakeit::VerifyNoOtherInvocations(observerMock); 114 | 115 | instructionRegister.reset(); 116 | 117 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); 118 | fakeit::VerifyNoOtherInvocations(observerMock); 119 | } 120 | 121 | SUBCASE("print() should not fail") { 122 | instructionRegister.print(); 123 | } 124 | 125 | SUBCASE("getOpcode() should return 4 first bits after in() and clock tick") { 126 | bus->write(std::bitset<8>("11001010").to_ulong()); // 202 127 | 128 | instructionRegister.in(); 129 | 130 | CHECK_EQ(instructionRegister.getOpcode(), 0); // Nothing yet 131 | 132 | clock.clockTicked(); 133 | 134 | CHECK_EQ(instructionRegister.getOpcode(), std::bitset<4>("1100").to_ulong()); // 12 135 | } 136 | 137 | SUBCASE("out() should put 4 last bits on the bus") { 138 | bus->write(std::bitset<8>("11001010").to_ulong()); // 202 139 | 140 | instructionRegister.in(); 141 | clock.clockTicked(); 142 | 143 | instructionRegister.out(); 144 | CHECK_EQ(bus->read(), std::bitset<4>("1010").to_ulong()); // 10 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /test/core/MemoryAddressRegisterTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "core/Utils.h" 5 | #include "core/MemoryAddressRegister.h" 6 | 7 | using namespace Core; 8 | 9 | TEST_SUITE("MemoryAddressRegisterTest") { 10 | TEST_CASE("mar should work correctly") { 11 | const std::shared_ptr &bus = std::make_shared(); 12 | fakeit::Mock listenerMock; 13 | auto listenerPtr = std::shared_ptr(&listenerMock(), [](...) {}); 14 | 15 | MemoryAddressRegister mar(listenerPtr, bus); 16 | 17 | auto &clock = dynamic_cast(mar); 18 | 19 | CHECK_EQ(bus->read(), 0); 20 | 21 | fakeit::When(Method(listenerMock, registerValueChanged)).Return(); 22 | 23 | SUBCASE("program should notify listener of programmed value") { 24 | mar.program(Utils::to4bits(13)); 25 | 26 | fakeit::Verify(Method(listenerMock, registerValueChanged).Using(13)).Once(); 27 | } 28 | 29 | SUBCASE("program should notify observer of programmed value") { 30 | fakeit::Mock observerMock; 31 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 32 | mar.setObserver(observerPtr); 33 | fakeit::When(Method(observerMock, valueUpdated)).Return(); 34 | 35 | mar.program(Utils::to4bits(14)); 36 | 37 | fakeit::Verify(Method(observerMock, valueUpdated).Using(14)).Once(); 38 | } 39 | 40 | SUBCASE("clockTicked() should store value from bus on clock tick") { 41 | bus->write(8); 42 | 43 | mar.in(); // Prepare to read from bus 44 | 45 | fakeit::VerifyNoOtherInvocations(listenerMock); // Nothing read from the bus yet 46 | 47 | bus->write(6); // This should be stored 48 | clock.clockTicked(); 49 | 50 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 51 | 52 | fakeit::Verify(Method(listenerMock, registerValueChanged).Using(6)).Once(); 53 | } 54 | 55 | SUBCASE("clockTicked() should only store on first clock tick") { 56 | bus->write(15); 57 | 58 | mar.in(); 59 | clock.clockTicked(); 60 | 61 | fakeit::Verify(Method(listenerMock, registerValueChanged).Using(15)).Once(); 62 | 63 | bus->write(6); 64 | clock.clockTicked(); 65 | 66 | fakeit::VerifyNoOtherInvocations(listenerMock); // No change 67 | } 68 | 69 | SUBCASE("clockTicked() should throw exception on clock tick if value more than 4 bits") { 70 | bus->write(16); 71 | mar.in(); 72 | 73 | CHECK_THROWS_WITH(clock.clockTicked(), "MemoryAddressRegister: address out of bounds 16"); 74 | } 75 | 76 | SUBCASE("observer should be notified when reading from the bus") { 77 | fakeit::Mock observerMock; 78 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 79 | mar.setObserver(observerPtr); 80 | fakeit::When(Method(observerMock, valueUpdated)).Return(); 81 | 82 | bus->write(8); 83 | 84 | mar.in(); // Prepare to read from bus 85 | 86 | fakeit::VerifyNoOtherInvocations(observerMock); // Nothing read from the bus yet 87 | 88 | bus->write(6); // This should be stored 89 | clock.clockTicked(); 90 | 91 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 92 | 93 | fakeit::Verify(Method(observerMock, valueUpdated).Using(6)).Once(); 94 | fakeit::VerifyNoOtherInvocations(observerMock); 95 | } 96 | 97 | SUBCASE("reset() should reset address") { 98 | mar.reset(); 99 | 100 | // Don't currently have a way to verify. Maybe notify listener? Not sure if there is any point. 101 | fakeit::VerifyNoOtherInvocations(listenerMock); 102 | } 103 | 104 | SUBCASE("print() should not fail") { 105 | mar.print(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/core/OutputRegisterTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "core/OutputRegister.h" 5 | 6 | using namespace Core; 7 | 8 | TEST_SUITE("OutputRegisterTest") { 9 | TEST_CASE("output register should work correctly") { 10 | const std::shared_ptr &bus = std::make_shared(); 11 | OutputRegister outputRegister(bus); 12 | 13 | fakeit::Mock observerMock; 14 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 15 | outputRegister.setObserver(observerPtr); 16 | 17 | auto &clock = dynamic_cast(outputRegister); 18 | 19 | CHECK_EQ(bus->read(), 0); 20 | fakeit::When(Method(observerMock, valueUpdated)).AlwaysReturn(); 21 | 22 | SUBCASE("clockTicked() should store value from bus on clock tick") { 23 | bus->write(8); 24 | 25 | outputRegister.in(); // Prepare to read from bus 26 | 27 | fakeit::VerifyNoOtherInvocations(observerMock); // Nothing read from the bus yet 28 | 29 | bus->write(6); // This should be stored 30 | clock.clockTicked(); 31 | 32 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 33 | 34 | fakeit::Verify(Method(observerMock, valueUpdated).Using(6)).Once(); 35 | } 36 | 37 | SUBCASE("clockTicked() should only store on first clock tick") { 38 | bus->write(15); 39 | 40 | outputRegister.in(); 41 | clock.clockTicked(); 42 | 43 | fakeit::Verify(Method(observerMock, valueUpdated).Using(15)).Once(); 44 | 45 | bus->write(6); 46 | clock.clockTicked(); 47 | 48 | fakeit::VerifyNoOtherInvocations(observerMock); // No change 49 | } 50 | 51 | SUBCASE("clockTicked() should handle missing observer") { 52 | outputRegister.setObserver(nullptr); 53 | 54 | bus->write(150); 55 | 56 | outputRegister.in(); 57 | clock.clockTicked(); 58 | 59 | fakeit::VerifyNoOtherInvocations(observerMock); 60 | } 61 | 62 | SUBCASE("reset() should reset value to 0") { 63 | outputRegister.reset(); 64 | 65 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); 66 | } 67 | 68 | SUBCASE("reset() should handle missing observer") { 69 | outputRegister.setObserver(nullptr); 70 | 71 | outputRegister.reset(); 72 | 73 | fakeit::VerifyNoOtherInvocations(observerMock); 74 | } 75 | 76 | SUBCASE("print() should not fail") { 77 | outputRegister.print(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/core/ProgramCounterTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "core/ProgramCounter.h" 5 | 6 | using namespace Core; 7 | 8 | TEST_SUITE("ProgramCounterTest") { 9 | TEST_CASE("program counter should work") { 10 | const std::shared_ptr &bus = std::make_shared(); 11 | ProgramCounter programCounter(bus); 12 | 13 | auto &clock = dynamic_cast(programCounter); 14 | 15 | programCounter.out(); 16 | CHECK_EQ(bus->read(), 0); 17 | 18 | SUBCASE("clock tick should do nothing by itself") { 19 | bus->write(8); 20 | 21 | clock.clockTicked(); 22 | 23 | programCounter.out(); 24 | CHECK_EQ(bus->read(), 0); // No jump, and no increment 25 | } 26 | 27 | SUBCASE("enable() should increment on clock tick") { 28 | programCounter.enable(); 29 | 30 | programCounter.out(); 31 | CHECK_EQ(bus->read(), 0); // Nothing happened yet 32 | 33 | clock.clockTicked(); 34 | 35 | CHECK_EQ(bus->read(), 0); 36 | programCounter.out(); 37 | CHECK_EQ(bus->read(), 1); 38 | } 39 | 40 | SUBCASE("enable() should only increment on first clock tick") { 41 | programCounter.enable(); 42 | clock.clockTicked(); 43 | 44 | programCounter.out(); 45 | CHECK_EQ(bus->read(), 1); 46 | 47 | clock.clockTicked(); 48 | 49 | programCounter.out(); 50 | CHECK_EQ(bus->read(), 1); // No change 51 | } 52 | 53 | SUBCASE("enable() should wrap around") { 54 | for (int i = 0; i < 15; i++) { 55 | programCounter.enable(); 56 | clock.clockTicked(); 57 | } 58 | 59 | programCounter.out(); 60 | CHECK_EQ(bus->read(), 15); 61 | 62 | programCounter.enable(); 63 | clock.clockTicked(); 64 | 65 | programCounter.out(); 66 | CHECK_EQ(bus->read(), 0); 67 | } 68 | 69 | SUBCASE("observer should be notified when incrementing") { 70 | fakeit::Mock observerMock; 71 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 72 | programCounter.setObserver(observerPtr); 73 | fakeit::When(Method(observerMock, valueUpdated)).Return(); 74 | 75 | programCounter.enable(); 76 | 77 | fakeit::VerifyNoOtherInvocations(observerMock); // Nothing happened yet 78 | 79 | clock.clockTicked(); 80 | 81 | fakeit::Verify(Method(observerMock, valueUpdated).Using(1)).Once(); 82 | } 83 | 84 | SUBCASE("jump() should change counter to value from bus on clock tick") { 85 | bus->write(8); 86 | 87 | programCounter.jump(); 88 | 89 | programCounter.out(); 90 | CHECK_EQ(bus->read(), 0); // Nothing read from the bus yet 91 | 92 | bus->write(6); // This should be stored 93 | clock.clockTicked(); 94 | 95 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 96 | 97 | programCounter.out(); 98 | CHECK_EQ(bus->read(), 6); 99 | } 100 | 101 | SUBCASE("jump() should only change on first clock tick") { 102 | bus->write(15); 103 | 104 | programCounter.jump(); 105 | clock.clockTicked(); 106 | 107 | programCounter.out(); 108 | CHECK_EQ(bus->read(), 15); 109 | 110 | bus->write(6); 111 | clock.clockTicked(); 112 | 113 | programCounter.out(); 114 | CHECK_EQ(bus->read(), 15); // No change 115 | } 116 | 117 | SUBCASE("jump() should throw exception on clock tick if value more than 4 bits") { 118 | bus->write(16); 119 | programCounter.jump(); 120 | 121 | CHECK_THROWS_WITH(clock.clockTicked(), "ProgramCounter: address out of bounds 16"); 122 | } 123 | 124 | SUBCASE("observer should be notified when jumping") { 125 | fakeit::Mock observerMock; 126 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 127 | programCounter.setObserver(observerPtr); 128 | fakeit::When(Method(observerMock, valueUpdated)).Return(); 129 | 130 | bus->write(8); 131 | 132 | programCounter.jump(); 133 | 134 | fakeit::VerifyNoOtherInvocations(observerMock); // Nothing read from the bus yet 135 | 136 | bus->write(6); // This should be stored 137 | clock.clockTicked(); 138 | 139 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 140 | 141 | fakeit::Verify(Method(observerMock, valueUpdated).Using(6)).Once(); 142 | } 143 | 144 | SUBCASE("reset() should start counting at 0 again") { 145 | bus->write(8); 146 | 147 | programCounter.jump(); 148 | clock.clockTicked(); 149 | 150 | programCounter.out(); 151 | CHECK_EQ(bus->read(), 8); 152 | 153 | programCounter.reset(); 154 | 155 | programCounter.out(); 156 | CHECK_EQ(bus->read(), 0); 157 | } 158 | 159 | SUBCASE("observer should be notified when resetting") { 160 | fakeit::Mock observerMock; 161 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 162 | programCounter.setObserver(observerPtr); 163 | fakeit::When(Method(observerMock, valueUpdated)).AlwaysReturn(); 164 | 165 | bus->write(8); 166 | 167 | programCounter.jump(); 168 | clock.clockTicked(); 169 | 170 | fakeit::Verify(Method(observerMock, valueUpdated).Using(8)).Once(); 171 | fakeit::VerifyNoOtherInvocations(observerMock); 172 | 173 | programCounter.reset(); 174 | 175 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); 176 | fakeit::VerifyNoOtherInvocations(observerMock); 177 | } 178 | 179 | SUBCASE("print() should not fail") { 180 | programCounter.print(); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /test/core/RandomAccessMemoryTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "core/Utils.h" 5 | #include "core/RandomAccessMemory.h" 6 | 7 | using namespace Core; 8 | 9 | TEST_SUITE("RandomAccessMemoryTest") { 10 | TEST_CASE("ram should work correctly") { 11 | const std::shared_ptr &bus = std::make_shared(); 12 | 13 | RandomAccessMemory ram(bus); 14 | 15 | auto &mar = dynamic_cast(ram); 16 | auto &clock = dynamic_cast(ram); 17 | 18 | CHECK_EQ(bus->read(), 0); 19 | 20 | SUBCASE("memory should be initialized with 0") { 21 | for (int i = 0; i < RandomAccessMemory::MEMORY_SIZE; i++) { 22 | mar.registerValueChanged(i); 23 | ram.out(); 24 | 25 | CHECK_EQ(bus->read(), 0); 26 | } 27 | } 28 | 29 | SUBCASE("memory should be programmable manually") { 30 | // Splitting programming and verification to make sure every location is changed, 31 | // instead of potentially writing and reading from the same location by mistake 32 | for (int i = 0; i < RandomAccessMemory::MEMORY_SIZE; i++) { 33 | mar.registerValueChanged(i); 34 | ram.program(Utils::to4bits(i), Utils::to4bits(i)); 35 | } 36 | 37 | for (int i = 0; i < RandomAccessMemory::MEMORY_SIZE; i++) { 38 | mar.registerValueChanged(i); 39 | ram.out(); 40 | 41 | CHECK_EQ(bus->read(), i * 17); 42 | } 43 | } 44 | 45 | SUBCASE("memory should be programmable manually 2") { 46 | // Add a test where opcode and operand are different 47 | mar.registerValueChanged(1); 48 | ram.program(std::bitset<4>("1010"), std::bitset<4>("0011")); 49 | ram.out(); 50 | 51 | CHECK_EQ(bus->read(), std::bitset<8>("10100011").to_ulong()); 52 | } 53 | 54 | SUBCASE("program should notify observer of programmed value") { 55 | fakeit::Mock observerMock; 56 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 57 | ram.setObserver(observerPtr); 58 | fakeit::When(Method(observerMock, valueUpdated)).AlwaysReturn(); 59 | 60 | ram.program(std::bitset<4>("1111"), std::bitset<4>("1110")); 61 | 62 | fakeit::Verify(Method(observerMock, valueUpdated).Using(254)).Once(); 63 | fakeit::VerifyNoOtherInvocations(observerMock); 64 | } 65 | 66 | SUBCASE("changing address should notify observer of value at new address") { 67 | fakeit::Mock observerMock; 68 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 69 | ram.setObserver(observerPtr); 70 | fakeit::When(Method(observerMock, valueUpdated)).AlwaysReturn(); 71 | 72 | mar.registerValueChanged(1); 73 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); 74 | fakeit::VerifyNoOtherInvocations(observerMock); 75 | } 76 | 77 | SUBCASE("reset() should reset address but not memory content") { 78 | // Set a value of 10 at memory location 0 79 | ram.program(Utils::to4bits(0), Utils::to4bits(10)); 80 | ram.out(); 81 | CHECK_EQ(bus->read(), 10); 82 | 83 | // Set a value of 15 at memory location 5 84 | mar.registerValueChanged(5); 85 | ram.program(Utils::to4bits(0), Utils::to4bits(15)); 86 | ram.out(); 87 | CHECK_EQ(bus->read(), 15); 88 | 89 | // Reset. Should be back at value 10 now, from memory location 0 90 | ram.reset(); 91 | ram.out(); 92 | CHECK_EQ(bus->read(), 10); 93 | 94 | // And should still have 15 at memory location 5 95 | mar.registerValueChanged(5); 96 | ram.out(); 97 | CHECK_EQ(bus->read(), 15); 98 | } 99 | 100 | SUBCASE("clockTicked() should store value from bus on clock tick") { 101 | bus->write(8); 102 | 103 | ram.in(); // Prepare to read from bus 104 | 105 | ram.out(); 106 | CHECK_EQ(bus->read(), 0); // Nothing read from the bus yet 107 | 108 | bus->write(6); // This should be stored 109 | clock.clockTicked(); 110 | 111 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 112 | 113 | ram.out(); 114 | CHECK_EQ(bus->read(), 6); 115 | } 116 | 117 | SUBCASE("clockTicked() should only store on first clock tick") { 118 | bus->write(8); 119 | 120 | ram.in(); 121 | clock.clockTicked(); 122 | 123 | ram.out(); 124 | CHECK_EQ(bus->read(), 8); 125 | 126 | bus->write(6); 127 | clock.clockTicked(); 128 | 129 | ram.out(); 130 | CHECK_EQ(bus->read(), 8); // No change 131 | } 132 | 133 | SUBCASE("memory should be programmable from the bus") { 134 | for (int i = 0; i < RandomAccessMemory::MEMORY_SIZE; i++) { 135 | mar.registerValueChanged(i); 136 | bus->write(i + 1); 137 | ram.in(); 138 | clock.clockTicked(); 139 | } 140 | 141 | for (int i = 0; i < RandomAccessMemory::MEMORY_SIZE; i++) { 142 | mar.registerValueChanged(i); 143 | ram.out(); 144 | 145 | CHECK_EQ(bus->read(), i + 1); 146 | } 147 | } 148 | 149 | SUBCASE("ram should throw exception if memory location is out of bounds") { 150 | CHECK_THROWS_WITH(mar.registerValueChanged(16), 151 | "RandomAccessMemory: address out of bounds 16"); 152 | 153 | } 154 | 155 | SUBCASE("observer should be notified when reading from the bus") { 156 | fakeit::Mock observerMock; 157 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 158 | ram.setObserver(observerPtr); 159 | fakeit::When(Method(observerMock, valueUpdated)).Return(); 160 | 161 | bus->write(8); 162 | 163 | ram.in(); // Prepare to read from bus 164 | 165 | fakeit::VerifyNoOtherInvocations(observerMock); // Nothing happened yet 166 | 167 | bus->write(6); // This should be stored 168 | clock.clockTicked(); 169 | 170 | bus->write(4); // Change the bus after storing the last value, otherwise it's difficult to know 171 | 172 | fakeit::Verify(Method(observerMock, valueUpdated).Using(6)).Once(); 173 | fakeit::VerifyNoOtherInvocations(observerMock); 174 | } 175 | 176 | SUBCASE("print() should not fail") { 177 | ram.print(); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /test/core/StepCounterTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "core/StepCounter.h" 5 | 6 | using namespace Core; 7 | 8 | TEST_SUITE("StepCounterTest") { 9 | TEST_CASE("step counter should work") { 10 | fakeit::Mock stepListenerMock; 11 | auto stepListenerMockSharedPtr = std::shared_ptr(&stepListenerMock(), [](...) {}); 12 | StepCounter stepCounter(stepListenerMockSharedPtr); 13 | 14 | fakeit::When(Method(stepListenerMock, stepReady)).AlwaysReturn(); 15 | 16 | auto &clock = dynamic_cast(stepCounter); 17 | 18 | SUBCASE("inverted clock ticks should notify listener of incrementing steps and wrap at 4") { 19 | // First step is from 0 -> 1 20 | clock.invertedClockTicked(); 21 | fakeit::Verify(Method(stepListenerMock, stepReady).Using(1)).Once(); 22 | 23 | clock.invertedClockTicked(); 24 | fakeit::Verify(Method(stepListenerMock, stepReady).Using(2)).Once(); 25 | 26 | clock.invertedClockTicked(); 27 | fakeit::Verify(Method(stepListenerMock, stepReady).Using(3)).Once(); 28 | 29 | clock.invertedClockTicked(); 30 | fakeit::Verify(Method(stepListenerMock, stepReady).Using(4)).Once(); 31 | 32 | clock.invertedClockTicked(); 33 | fakeit::Verify(Method(stepListenerMock, stepReady).Using(0)).Once(); 34 | 35 | clock.invertedClockTicked(); 36 | fakeit::Verify(Method(stepListenerMock, stepReady).Using(1)).Twice(); 37 | 38 | fakeit::VerifyNoOtherInvocations(stepListenerMock); 39 | } 40 | 41 | SUBCASE("inverted clock ticks should notify observer of incrementing steps") { 42 | fakeit::Mock observerMock; 43 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 44 | stepCounter.setObserver(observerPtr); 45 | fakeit::When(Method(observerMock, valueUpdated)).AlwaysReturn(); 46 | 47 | clock.invertedClockTicked(); 48 | fakeit::Verify(Method(observerMock, valueUpdated).Using(1)).Once(); 49 | 50 | clock.invertedClockTicked(); 51 | fakeit::Verify(Method(observerMock, valueUpdated).Using(2)).Once(); 52 | 53 | clock.invertedClockTicked(); 54 | fakeit::Verify(Method(observerMock, valueUpdated).Using(3)).Once(); 55 | 56 | fakeit::VerifyNoOtherInvocations(observerMock); 57 | } 58 | 59 | SUBCASE("reset() should start at 0 and notify observer") { 60 | fakeit::Mock observerMock; 61 | auto observerPtr = std::shared_ptr(&observerMock(), [](...) {}); 62 | stepCounter.setObserver(observerPtr); 63 | fakeit::When(Method(observerMock, valueUpdated)).AlwaysReturn(); 64 | 65 | clock.invertedClockTicked(); 66 | fakeit::Verify(Method(observerMock, valueUpdated).Using(1)).Once(); 67 | fakeit::VerifyNoOtherInvocations(observerMock); 68 | 69 | stepCounter.reset(); 70 | 71 | fakeit::Verify(Method(observerMock, valueUpdated).Using(0)).Once(); 72 | fakeit::VerifyNoOtherInvocations(observerMock); 73 | } 74 | 75 | SUBCASE("reset() should start at 0 and notify listener") { 76 | clock.invertedClockTicked(); 77 | fakeit::Verify(Method(stepListenerMock, stepReady).Using(1)).Once(); 78 | fakeit::VerifyNoOtherInvocations(stepListenerMock); 79 | 80 | stepCounter.reset(); 81 | 82 | fakeit::Verify(Method(stepListenerMock, stepReady).Using(0)).Once(); 83 | fakeit::VerifyNoOtherInvocations(stepListenerMock); 84 | } 85 | 86 | SUBCASE("print() should not fail") { 87 | stepCounter.print(); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/core/TimeSourceTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "core/TimeSource.h" 4 | 5 | using namespace Core; 6 | 7 | // 1 microsecond = 1000 nanoseconds 8 | double microToNano(double microseconds) { 9 | return microseconds * 1000.0; 10 | } 11 | 12 | TEST_SUITE("TimeSourceTest") { 13 | TEST_CASE("delta() should return time difference between calls") { 14 | TimeSource timeSource; 15 | 16 | // Not sure if this is reliable, but works atm 17 | double startCount = timeSource.delta(); 18 | WARN(startCount < 1000); 19 | 20 | timeSource.sleep(microToNano(100)); 21 | 22 | // Not easy to verify sleep, but should have slept at least 100 microseconds, and at most 200 hopefully 23 | double stopCount = timeSource.delta(); 24 | WARN(stopCount > (startCount + microToNano(100))); 25 | WARN(stopCount < (startCount + microToNano(200))); 26 | } 27 | 28 | TEST_CASE("delta() should not increase but return difference since last time") { 29 | TimeSource timeSource; 30 | 31 | // Not sure if this is reliable, but works atm 32 | for (int i = 0; i < 10; i++) { 33 | WARN(timeSource.delta() < 1000); 34 | } 35 | } 36 | 37 | TEST_CASE("reset() should reset delta") { 38 | TimeSource timeSource; 39 | 40 | timeSource.sleep(microToNano(100)); 41 | timeSource.reset(); 42 | 43 | double count = timeSource.delta(); 44 | WARN(count < 1000); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/test_main.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include "include/doctest.h" 3 | --------------------------------------------------------------------------------