Skip to content

Commit

Permalink
Cards can now load ROMs from config file as well as CLI.
Browse files Browse the repository at this point in the history
  • Loading branch information
magnetrwn committed Nov 6, 2024
1 parent 79fdeed commit 43c1eba
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 23 deletions.
78 changes: 75 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

+ **[Doxygen Documentation](https://magnetrwn.github.io/buddy8800)**
+ [Building](#building)
+ [Implementation Map](#implementation-map)
+ [Running from CLI](#running-from-cli)
+ [Configuration File](#configuration-file)
+ [Resources and Documentation](#resources-and-documentation)
+ [Screenshots](#screenshots)

Expand Down Expand Up @@ -41,9 +42,80 @@ Development builds are usually compiled with `./build.sh -d -T`.

**Note:** Make sure you have `config.toml` placed in the same directory as the final executable. This file contains the configuration for the emulator, such as what cards to place and where.

### Implementation Map
### Running from CLI

**Work in progress.**
You can either call the emulator from CLI passing pairs of file locations for ROMs and their load addresses, or set these loads directly in the configuration file for a more permanent solution. While you can load from the CLI, the config file will still need to contain information about what cards the system is setup with, in fact you could load these ROMs' data into address ranges belonging to any memory device in the system (not I/O since it belongs to a separate address space where an I/O request signal is detected).

```shell
bin/buddy8800 your/path/to/rom100.bin 0x100 your/path/to/rom7F00.bin 0x7F00 ...
```

Is equivalent (see **Note**) to:

```toml
[emulator]
...
start_with_pc_at = 0x0100
...

[[card]]
slot = 3
type = "rom"
at = 0x0100
load = "your/path/to/rom100.bin"

[[card]]
slot = 4
type = "rom"
at = 0x7F00
load = "your/path/to/rom7F00.bin"
```

**Note:** The first pair of ROM and address provided by the CLI will generate a few reset vector instruction in the zero page to jump to the first loaded ROM. The config file allows you to specify the starting address for the program counter, which is **not the same as the reset vector** as it directly sets PC to the specified address, skipping the reset vector. Because of this, be wary of having a value in the config file when trying to load a ROM from the CLI, as the emulator will give priority to the config file provided PC value.

For loading from CLI, the size (`range`) of the data cards must be determined in the config file, while if using the `load` field in the config file, the size will be determined by the size of the binary to load. Here is an example of specifying `range` and not `load`, while using the CLI to load the data:

```toml
[[card]]
slot = 10
type = "rom"
at = 0x0100
range = 2048
```

```shell
bin/buddy8800 your/path/to/rom100.bin 0x100
```

Note that this will not check if the binary file is too large for the specified range, and in which case will just continue writing to other cards in the system, which is expected behavior. The CLI loading utility allows you to load data in any range of memory as it serves exactly that purpose, even after having loaded a file already from a config file.

Much of this information is also immediately reported upon running the emulator during setup phase, allowing you to quickly see if the loaded configuration is correct.

### Configuration File

Please check the highly descriptive [config.toml](static/config.toml) file for a full list of options and their descriptions. The configuration file is used to specify the system's setup, such as what cards are placed in the system and where, as well as the initial state of the emulator.

For reference, here is a simple test machine setup (only the cards portion of the config file):

```toml
[[card]] # 88-SIO serial interface
slot = 10
type = "serial"
at = 0x10

[[card]] # Diagnostics II expects to be loaded in RAM
slot = 3
type = "ram"
at = 0x0100
load = "tests/res/diag2.com"

[[card]] # Cover all memory space with RAM just in case
slot = 4
type = "ram"
at = 0x0000
range = 65536
let_collide = true
```

### Resources and Documentation

Expand Down
4 changes: 4 additions & 0 deletions src/core/bus/bus.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ class bus {
*
* This method will call each card's possibly distinct way to refresh, which is important for example to
* let a serial card be able to poll or send new data.
*
* @note It is very important that cards implement ways to not make this method slow down the emulation,
* since it's highly likely that many devices would operate at rates much slower than the CPU clock step,
* thus it's beneficial to add a cycle counter to the method implementation on the card.
*/
inline void refresh() {
for (card* card : cards)
Expand Down
15 changes: 14 additions & 1 deletion src/core/bus/card.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ class card {
* card immediately after construction, and can be write-locked after construction if needed. Write locking
* can also be toggled at any time.
*
* When setting up the card by copying data to it, the capacity of it will be determined by the size of the
* provided data. This might be a bit unrealistic in address range.
*
* @note For convenience, use the aliases `ram_card` and `rom_card` instead of this class.
* @warning Out of range addresses are not checked, they should be checked by the bus instead, to avoid
* calling in_range() twice.
Expand All @@ -157,6 +160,13 @@ class data_card : public card {
const usize capacity;
std::vector<u8> data;

/*static constexpr usize next_pow2_to_v(usize v) {
usize pw = 1;
while (pw < v)
pw <<= 1;
return pw;
}*/

public:
data_card(u16 start_adr, usize capacity, u8 fill = 0x00, bool lock = construct_then_write_lock)
: start_adr(start_adr), capacity(capacity) {
Expand All @@ -170,6 +180,9 @@ class data_card : public card {
data_card(u16 start_adr, T begin, T end, bool lock = construct_then_write_lock)
: start_adr(start_adr), capacity(std::distance(begin, end)) {

static_assert(std::is_same_v<typename std::iterator_traits<T>::value_type, u8>,
"Iterator value type must be u8.");

data.reserve(capacity);
std::copy(begin, end, data.begin());
this->write_locked = lock;
Expand Down Expand Up @@ -343,7 +356,7 @@ class serial_card : public card {
}

if (!TDRE()) {
serial.putch(TX_DATA()); // This seems to lock until the slave fd is connected, or maybe not?
serial.putch(TX_DATA());
TDRE(true);
}
}
Expand Down
23 changes: 17 additions & 6 deletions src/core/cpu/cpu.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class cpu {
private:
cpu_state state;
bus_iface cardbus;
bool just_booted;
bool allow_reset_twice;
bool halted;
bool do_handle_bdos;
bool interrupts_enabled;
Expand Down Expand Up @@ -182,13 +182,13 @@ class cpu {

void handle_bdos() {
if (state.PC() == 0x0000) {
if (just_booted) {
if (allow_reset_twice) {

#ifdef ENABLE_TRACE
puts("\x1B[47;01mBDOS 0x0000: Reset vector! \x1B[0m");
#endif

just_booted = false;
allow_reset_twice = false;
return;
}

Expand Down Expand Up @@ -930,14 +930,18 @@ class cpu {
*/
cpu_state save_state() const { return state; }

/// @brief Set the PC of the CPU.
/// @param pc The new PC value.
void set_pc(u16 pc) { state.PC(pc); }

/// @brief Check if the CPU is halted.
/// @return True if the CPU is halted, false otherwise.
bool is_halted() const { return halted; }

/// @brief Reset the CPU.
void clear() {
state = cpu_state();
just_booted = true;
allow_reset_twice = true;
halted = false;
}

Expand Down Expand Up @@ -979,8 +983,15 @@ class cpu {

/// \}

cpu(bus_iface init_adr_space) : state(), cardbus(init_adr_space), just_booted(true), halted(false), do_handle_bdos(false),
interrupts_enabled(true), printer(std::cout), ext_op_idx(false) {}
cpu(bus_iface init_adr_space, bool allow_reset_twice = true)
: state(),
cardbus(init_adr_space),
allow_reset_twice(allow_reset_twice),
halted(false),
do_handle_bdos(false),
interrupts_enabled(true),
printer(std::cout),
ext_op_idx(false) {}
};

#endif
45 changes: 39 additions & 6 deletions src/util/sysconf.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,45 @@ class system_config {
private:
bus cardbus;
std::vector<card*> cards;
u16 start_pc;
bool do_pseudo_bdos;

inline card* create_card(const std::string& type, u16 at, usize range) {
inline card* create_card(const std::string& type, u16 at, usize range, const std::string& load) {
card* cardptr = nullptr;
std::ifstream load_file;
std::vector<u8> load_file_vec;

if (load.empty() and range == 0 and (type == "ram" or type == "rom"))
throw std::runtime_error("Config has data card with no range or load. You need at least one of the two.");

if (!load.empty()) {
load_file = std::ifstream(load, std::ios::binary);

if (!load_file)
throw std::runtime_error("Could not open file: " + std::string(load));

load_file_vec.assign(std::istreambuf_iterator<char>(load_file), {});

if (load_file_vec.empty())
throw std::runtime_error("File is empty or could not be read: " + std::string(load));
}

if (type == "ram")
return new ram_card(at, range);
if (load.empty())
cardptr = new ram_card(at, range);
else
cardptr = new ram_card(at, load_file_vec.begin(), load_file_vec.end());
else if (type == "rom")
return new rom_card(at, range);
if (load.empty())
cardptr = new rom_card(at, range);
else
cardptr = new rom_card(at, load_file_vec.begin(), load_file_vec.end());
else if (type == "serial")
return new serial_card(at);
cardptr = new serial_card(at);
else
throw std::runtime_error("config has unknown card type: " + type);
throw std::runtime_error("Config has unknown card type: " + type);

return cardptr;
}

inline void insert_card(card* card, usize slot, bool let_collide) {
Expand All @@ -55,13 +83,15 @@ class system_config {
create_card(
toml::find<std::string>(card, "type"),
toml::find<u16>(card, "at"),
toml::find_or<usize>(card, "range", 0 /* unused */)
toml::find_or<usize>(card, "range", 0),
toml::find_or<std::string>(card, "load", "")
),
toml::find<usize>(card, "slot"),
toml::find_or<bool>(card, "let_collide", false)
);
}

start_pc = toml::find_or<u16>(emulator, "start_with_pc_at", 0);
do_pseudo_bdos = toml::find_or<bool>(emulator, "pseudo_bdos_enabled", false);
}

Expand All @@ -76,6 +106,9 @@ class system_config {

/// @brief Get whether pseudo BDOS is enabled.
inline bool get_do_pseudo_bdos() const { return do_pseudo_bdos; }

/// @brief Get the starting value of PC.
inline u16 get_start_pc() const { return start_pc; }
};

#endif
9 changes: 7 additions & 2 deletions src/ux/ux.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class emulator {

public:
void setup(int argc, char** argv) {
if (argc < 3 or !(argc & 1))
if (argc < 1 or !(argc & 1))
throw std::invalid_argument("Invalid number of arguments. Provide pairs of ROM/data files and integer load addresses.");

processor.do_pseudo_bdos(conf.get_do_pseudo_bdos());
Expand All @@ -38,6 +38,8 @@ class emulator {
// The first ROM is the one that will have the reset vector jump to.
processor.load(load_rom_vec.begin(), load_rom_vec.end(), std::stoul(argv[i + 1], nullptr, 0), i == 1);
}

processor.set_pc(conf.get_start_pc());
}

void run() {
Expand All @@ -51,7 +53,10 @@ class emulator {

std::string get_bus_map_s() const { return cardbus.bus_map_s(); }

emulator(const char* config_filename) : conf(config_filename), cardbus(conf.get_bus()), processor(cardbus) {}
emulator(const char* config_filename)
: conf(config_filename),
cardbus(conf.get_bus()),
processor(cardbus, conf.get_start_pc() == 0x0000) {}
};

struct terminal_ux {
Expand Down
20 changes: 15 additions & 5 deletions static/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,38 @@

[emulator]
pseudo_bdos_enabled = true # Redirect and handle calls that match addresses of BDOS calls.
start_with_pc_at = 0x0100 # Start the program counter at this address. Note that this bypasses the reset vector. 0 to disable.

# List of cards here, make sure to append cards you wish to add. Available parameters are:
# - slot: Slot number of the card [0, 18], which also determines IRQ priority (lower is higher priority).
# - type: Type of the card. Available types are: "ram", "rom", "serial".
# - (in the future: "disk", "printer", "timer", "kbd", "display", "network").
# - load: Path to the file to load into the card. Only for "ram" or "rom" type.
# (you can omit this if using range, as it will automatically set the closest bigger power of 2 size)
# - at: Address of the card in the memory space.
# - range: Size or address range (like I/O register count) of the card in bytes.
# (you can omit this if using load)
# - let_collide: Allow the card to have overlapping address range with other cards.
#
# IMPORTANT: cards can be de/activated by the IORQ signal according to them being memory or I/O, so
# you might not need to enable overlapping, as overlap of I/O and memory is expected.
# Only use this in case of overlap of the same type of card (e.g. two RAM cards).
#
# Note: there is (usually) a limit of 18 cards in the system.
# Note: there is (usually) a limit of 18 cards in the system. Lower slot number means higher IRQ priority.

[[card]]
[[card]] # 88-SIO serial interface
slot = 10
type = "serial"
at = 0x0010
at = 0x10

[[card]]
[[card]] # Diagnostics II expects to be loaded in RAM
slot = 3
type = "ram"
at = 0x0100
load = "tests/res/diag2.com"

[[card]] # Cover all memory space with RAM just in case
slot = 4
type = "ram"
at = 0x0000
range = 65536
let_collide = true

0 comments on commit 43c1eba

Please sign in to comment.