zemu 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e21eb92ebce3eb8f03d9a17ddaec4f857989216a673791267602c0eb848319c7
4
- data.tar.gz: 3ff049b8e132ae33a3376f21768378f06d6bd59b8f5014b81ede14f50e37eda6
3
+ metadata.gz: caa59294431c1faadda8328bc7fbefa504526ae43a5531e2cf1d6c2bc7dd5c87
4
+ data.tar.gz: 72ced5e284f94e586862e80e97f5d95f55a9a3dbc931dc889fdc73352cef3a82
5
5
  SHA512:
6
- metadata.gz: 34706fc39b8212b6b3a07e077f5cd63479550e1832509bb01f742f86928c8c79844ce9689d4a11d79a2664d4ac13c6433f8a502b8806bf5e3390110009a27237
7
- data.tar.gz: 74082dc5aad00935a79f3af11db3b288179015ddce3ce4cbd96439771cd1bce4377f08cc069c23574474942dc4ba5dff736c44cc7cc263df0a6a007e715a6fe0
6
+ metadata.gz: dbb449fa4a076320a3073391f98687098e4e0f8b484099a7ca2c67325f857e8923397267dc01c5bee521cf66e9457149a8845e15f4a8b74ae7962e1f84623ae1
7
+ data.tar.gz: 846f7d09837e09fdb5711ba7d3ee09ca734cf0454c10efaff77a2d92de742e88332cc2c3e008db7026c528460368e410c9c9ebaacf29694e513880255085d579
data/lib/zemu/config.rb CHANGED
@@ -439,6 +439,215 @@ module Zemu
439
439
  end
440
440
  end
441
441
 
442
+ # Block drive object
443
+ #
444
+ # Represents a device with a sequence of sectors of a fixed size,
445
+ # which can be accessed via IO instructions as an IDE drive.
446
+ class BlockDrive < IOPort
447
+ # Constructor.
448
+ #
449
+ # Takes a block in which the parameters of the block drive
450
+ # can be initialized.
451
+ #
452
+ # All parameters can be set within this block.
453
+ # They become readonly as soon as the block completes.
454
+ #
455
+ # Constructor raises RangeError if a file is provided for initialization
456
+ # and it is of the wrong size.
457
+ #
458
+ # @example
459
+ #
460
+ # Zemu::Config::BlockDrive.new do
461
+ # name "drive"
462
+ # base_port 0x0c
463
+ # sector_size 512
464
+ # num_sectors 64
465
+ # end
466
+ #
467
+ #
468
+ def initialize
469
+ @blocks = []
470
+ @initialize_from = nil
471
+
472
+ super
473
+
474
+ num_sectors.times do
475
+ sector = []
476
+ sector_size.times do
477
+ sector << 0
478
+ end
479
+ @blocks << sector
480
+ end
481
+
482
+ # Initialize from provided file if applicable.
483
+ unless @initialize_from.nil?
484
+ # Check file size.
485
+ file_size = File.size(@initialize_from)
486
+ if (file_size != num_sectors * sector_size)
487
+ raise RangeError, "Initialization file for Zemu::Config::BlockDrive '#{name}' is of wrong size."
488
+ end
489
+
490
+ File.open(@initialize_from, "rb") do |f|
491
+ num_sectors.times do |s|
492
+ sector_size.times do |b|
493
+ @blocks[s][b] = f.getbyte()
494
+ end
495
+ end
496
+ end
497
+ end
498
+
499
+ when_setup do
500
+ <<-eos
501
+ #include <stdio.h>
502
+
503
+ zuint8 sector_data_#{name}[#{sector_size}];
504
+ zuint32 sector_data_#{name}_offset;
505
+ zuint32 loaded_sector_#{name} = Z_UINT32_MAXIMUM;
506
+ zuint8 drive_mode_#{name};
507
+ zuint8 drive_status_#{name} = 0b01000000;
508
+
509
+ zuint8 lba_#{name}_0;
510
+ zuint8 lba_#{name}_1;
511
+ zuint8 lba_#{name}_2;
512
+ zuint8 lba_#{name}_3;
513
+
514
+ void internal_#{name}_load_sector(zuint32 sector)
515
+ {
516
+ if (loaded_sector_#{name} == sector) return;
517
+
518
+ FILE * fptr = fopen("#{@initialize_from}", "rb");
519
+ fseek(fptr, sector * #{sector_size}, SEEK_SET);
520
+ fread(sector_data_#{name}, #{sector_size}, 1, fptr);
521
+ fclose(fptr);
522
+
523
+ loaded_sector_#{name} = sector;
524
+ }
525
+
526
+ void internal_#{name}_write_current_sector()
527
+ {
528
+ FILE * fptr = fopen("#{@initialize_from}", "r+b");
529
+ fseek(fptr, loaded_sector_#{name} * #{sector_size}, SEEK_SET);
530
+ fwrite(sector_data_#{name}, 1, #{sector_size}, fptr);
531
+ fclose(fptr);
532
+ }
533
+
534
+ zuint8 zemu_io_#{name}_readbyte(zuint32 sector, zuint32 offset)
535
+ {
536
+ internal_#{name}_load_sector(sector);
537
+ return sector_data_#{name}[offset];
538
+ }
539
+ eos
540
+ end
541
+
542
+ when_read do
543
+ <<-eos
544
+ if (port == #{base_port})
545
+ {
546
+ zuint8 b = sector_data_#{name}[sector_data_#{name}_offset];
547
+ sector_data_#{name}_offset++;
548
+ if (sector_data_#{name}_offset >= #{sector_size})
549
+ {
550
+ drive_status_#{name} = 0b01000000;
551
+ }
552
+ return b;
553
+ }
554
+ else if (port == #{base_port+7})
555
+ {
556
+ return drive_status_#{name};
557
+ }
558
+ eos
559
+ end
560
+
561
+ when_write do
562
+ <<-eos
563
+ if (port == #{base_port})
564
+ {
565
+ if (drive_mode_#{name} == 0x01)
566
+ {
567
+ sector_data_#{name}[sector_data_#{name}_offset] = value;
568
+ sector_data_#{name}_offset++;
569
+ if (sector_data_#{name}_offset >= #{sector_size})
570
+ {
571
+ internal_#{name}_write_current_sector();
572
+ drive_status_#{name} = 0b01000000;
573
+ }
574
+ }
575
+ }
576
+ else if (port == #{base_port+3})
577
+ {
578
+ lba_#{name}_0 = value;
579
+ }
580
+ else if (port == #{base_port+4})
581
+ {
582
+ lba_#{name}_1 = value;
583
+ }
584
+ else if (port == #{base_port+5})
585
+ {
586
+ lba_#{name}_2 = value;
587
+ }
588
+ else if (port == #{base_port+6})
589
+ {
590
+ lba_#{name}_3 = value & 0b00011111;
591
+ }
592
+ else if (port == #{base_port+7})
593
+ {
594
+ if (value == 0x20)
595
+ {
596
+ zuint32 sector = 0;
597
+ sector |= (zuint32)lba_#{name}_3 << 24;
598
+ sector |= (zuint32)lba_#{name}_2 << 16;
599
+ sector |= (zuint32)lba_#{name}_1 << 8;
600
+ sector |= (zuint32)lba_#{name}_0;
601
+
602
+ internal_#{name}_load_sector(sector);
603
+ sector_data_#{name}_offset = 0;
604
+
605
+ drive_mode_#{name} = 0x00;
606
+ drive_status_#{name} = 0b00001000;
607
+ }
608
+ else if (value == 0x30)
609
+ {
610
+ zuint32 sector = 0;
611
+ sector |= (zuint32)lba_#{name}_3 << 24;
612
+ sector |= (zuint32)lba_#{name}_2 << 16;
613
+ sector |= (zuint32)lba_#{name}_1 << 8;
614
+ sector |= (zuint32)lba_#{name}_0;
615
+
616
+ internal_#{name}_load_sector(sector);
617
+ sector_data_#{name}_offset = 0;
618
+
619
+ drive_mode_#{name} = 0x01;
620
+ drive_status_#{name} = 0b00001000;
621
+ }
622
+ }
623
+ eos
624
+ end
625
+ end
626
+
627
+ # Array of sectors of this drive.
628
+ def blocks
629
+ @blocks
630
+ end
631
+
632
+ # Set file to initialize from.
633
+ def initialize_from(file)
634
+ @initialize_from = file
635
+ end
636
+
637
+ # Defines FFI API which will be available to the instance wrapper if this IO device is used.
638
+ def functions
639
+ [
640
+ {"name" => "zemu_io_#{name}_readbyte", "args" => [:uint32, :uint32], "return" => :uint8},
641
+ ]
642
+ end
643
+
644
+ # Valid parameters for a BlockDrive, along with those
645
+ # defined in [Zemu::Config::IOPort].
646
+ def params
647
+ super + %w(base_port sector_size num_sectors)
648
+ end
649
+ end
650
+
442
651
  # Non-Maskable Interrupt Timer
443
652
  #
444
653
  # Represents a timer device, the period of which can be controlled
@@ -490,7 +699,7 @@ module Zemu
490
699
 
491
700
  # Parameters accessible by this configuration object.
492
701
  def params
493
- return %w(name compiler output_directory clock_speed)
702
+ return %w(name compiler output_directory clock_speed serial_delay)
494
703
  end
495
704
 
496
705
  # Initial value for parameters of this configuration object.
@@ -498,7 +707,8 @@ module Zemu
498
707
  return {
499
708
  "compiler" => "clang",
500
709
  "output_directory" => "bin",
501
- "clock_speed" => 0
710
+ "clock_speed" => 0,
711
+ "serial_delay" => 0
502
712
  }
503
713
  end
504
714
 
data/lib/zemu/debug.rb ADDED
@@ -0,0 +1,68 @@
1
+ module Zemu
2
+ # Handles debugging functionality, like mapping of symbols to addresses,
3
+ # disassembling of instructions, etc.
4
+ module Debug
5
+ # Loads a map file at the given path, and returns a hash of address => Symbol
6
+ # for the symbols defined within.
7
+ def self.load_map(path)
8
+ symbols = {}
9
+
10
+ File.open(path, "r") do |f|
11
+ f.each_line do |l|
12
+ s = Symbol.parse(l)
13
+
14
+ if symbols[s.address].nil?
15
+ symbols[s.address] = []
16
+ end
17
+
18
+ symbols[s.address] << s
19
+ symbols[s.address].sort_by!(&:label)
20
+ end
21
+ end
22
+
23
+ return symbols
24
+ end
25
+
26
+ # Represents a symbol definition, of the form `label = address`.
27
+ class Symbol
28
+ # Parse a symbol definition, returning a Symbol instance.
29
+ def self.parse(s)
30
+ # Split on whitespace.
31
+ tokens = s.to_s.split(' ')
32
+
33
+ if tokens.size < 3
34
+ raise ArgumentError, "Invalid symbol definition: '#{s}'"
35
+ end
36
+
37
+ label = tokens[0]
38
+
39
+ address = nil
40
+
41
+ if /0x[0-9a-fA-F]+/ =~ tokens[2]
42
+ address = tokens[2][2..-1].to_i(16)
43
+ elsif /\$[0-9a-fA-F]+/ =~ tokens[2]
44
+ address = tokens[2][1..-1].to_i(16)
45
+ elsif /\d+/ =~ tokens[2]
46
+ address = tokens[2].to_i
47
+ end
48
+
49
+ if address.nil?
50
+ raise ArgumentError, "Invalid symbol address: '#{tokens[2]}'"
51
+ end
52
+
53
+ return self.new(label, address)
54
+ end
55
+
56
+ # Textual label for this symbol.
57
+ attr_reader :label
58
+
59
+ # Address of this symbol in the binary.
60
+ attr_reader :address
61
+
62
+ def initialize(label, address)
63
+ @label = label
64
+ @address = address
65
+ end
66
+ end
67
+ end
68
+ end
data/lib/zemu/instance.rb CHANGED
@@ -54,6 +54,7 @@ module Zemu
54
54
 
55
55
  def initialize(configuration)
56
56
  @clock = configuration.clock_speed
57
+ @serial_delay = configuration.serial_delay
57
58
 
58
59
  @wrapper = make_wrapper(configuration)
59
60
 
@@ -65,7 +66,7 @@ module Zemu
65
66
 
66
67
  @state = RunState::UNDEFINED
67
68
 
68
- @breakpoints = []
69
+ @breakpoints = {}
69
70
  end
70
71
 
71
72
  # Returns the clock speed of this instance in Hz.
@@ -73,6 +74,11 @@ module Zemu
73
74
  return @clock
74
75
  end
75
76
 
77
+ # Returns the delay between characters on the serial port for this instance in seconds.
78
+ def serial_delay
79
+ return @serial_delay
80
+ end
81
+
76
82
  # Returns a hash containing current values of the emulated
77
83
  # machine's registers. All names are as those given in the Z80
78
84
  # reference manual.
@@ -136,6 +142,16 @@ module Zemu
136
142
  return return_string
137
143
  end
138
144
 
145
+ # Get a byte from the attached disk drive.
146
+ #
147
+ # @param sector The sector to read
148
+ # @param offset The offset in sector to read
149
+ #
150
+ # Gets a byte at the given offset in the given sector.
151
+ def drive_readbyte(sector, offset)
152
+ return @wrapper.zemu_io_block_drive_readbyte(sector, offset)
153
+ end
154
+
139
155
  # Continue running this instance until either:
140
156
  # * A HALT instruction is executed
141
157
  # * A breakpoint is hit
@@ -161,7 +177,7 @@ module Zemu
161
177
 
162
178
  # If the PC is now pointing to one of our breakpoints,
163
179
  # we're in the BREAK state.
164
- if (@breakpoints.select { |b| b == pc }.size) > 0
180
+ if @breakpoints[pc]
165
181
  @state = RunState::BREAK
166
182
  elsif @wrapper.zemu_debug_halted()
167
183
  @state = RunState::HALTED
@@ -177,7 +193,7 @@ module Zemu
177
193
  # @param type The type of breakpoint:
178
194
  # * :program => Break when the program counter hits the address given.
179
195
  def break(address, type)
180
- @breakpoints << address
196
+ @breakpoints[address] = true
181
197
  end
182
198
 
183
199
  # Remove a breakpoint of the given type at the given address.
@@ -186,7 +202,7 @@ module Zemu
186
202
  # @param address The address of the breakpoint to be removed.
187
203
  # @param type The type of breakpoint. See Instance#break.
188
204
  def remove_break(address, type)
189
- @breakpoints.reject! { |b| b == address }
205
+ @breakpoints[address] = false
190
206
  end
191
207
 
192
208
  # Returns true if the CPU has halted, false otherwise.
@@ -232,7 +248,7 @@ module Zemu
232
248
 
233
249
  configuration.io.each do |device|
234
250
  device.functions.each do |f|
235
- wrapper.attach_function(f["name"], f["args"], f["return"])
251
+ wrapper.attach_function(f["name"].to_sym, f["args"], f["return"])
236
252
  end
237
253
  end
238
254
 
@@ -8,6 +8,8 @@ module Zemu
8
8
  def initialize(instance)
9
9
  @instance = instance
10
10
 
11
+ @symbol_table = {}
12
+
11
13
  @master, @slave = PTY.open
12
14
  log "Opened PTY at #{@slave.path}"
13
15
  end
@@ -59,6 +61,9 @@ module Zemu
59
61
  memory(cmd[1], cmd[2])
60
62
  end
61
63
 
64
+ elsif cmd[0] == "map"
65
+ load_map(cmd[1])
66
+
62
67
  elsif cmd[0] == "help"
63
68
  log "Available commands:"
64
69
  log " continue [<n>] - Continue execution for <n> cycles"
@@ -66,6 +71,7 @@ module Zemu
66
71
  log " registers - View register contents"
67
72
  log " memory <a> [<n>] - View <n> bytes of memory, starting at address <a>."
68
73
  log " <n> defaults to 1 if omitted."
74
+ log " map <path> - Load symbols from map file at <path>"
69
75
  log " break <a> - Set a breakpoint at the given address <a>."
70
76
  log " quit - End this emulator instance."
71
77
 
@@ -79,16 +85,53 @@ module Zemu
79
85
  end
80
86
 
81
87
  # Outputs a table giving the current values of the instance's registers.
88
+ # For the 16-bit registers (BC, DE, HL, IX, IY, SP, PC), attempts to identify the symbol
89
+ # to which they point.
82
90
  def registers
83
91
  log "A: #{r("A")} F: #{r("F")}"
84
- log "B: #{r("B")} C: #{r("C")}"
85
- log "D: #{r("D")} E: #{r("E")}"
86
- log "H: #{r("H")} L: #{r("L")}"
92
+
93
+ registers_gp('B', 'C')
94
+ registers_gp('D', 'E')
95
+ registers_gp('H', 'L')
96
+
87
97
  log ""
88
- log "IX: #{r16("IX")}"
89
- log "IY: #{r16("IY")}"
90
- log "SP: #{r16("SP")}"
91
- log "PC: #{r16("PC")}"
98
+
99
+ register_16("IX")
100
+ register_16("IY")
101
+ register_16("SP")
102
+ register_16("PC")
103
+ end
104
+
105
+ # Displays the value of a 16-bit register.
106
+ def register_16(r)
107
+ value = @instance.registers[r]
108
+
109
+ log "#{r}: #{r16(r)} (#{get_symbol(value)})"
110
+ end
111
+
112
+ # Displays the value of a general-purpose 16-bit register pair.
113
+ def registers_gp(hi, lo)
114
+ value = hilo(@instance.registers[hi], @instance.registers[lo])
115
+
116
+ log "#{hi}: #{r(hi)} #{lo}: #{r(lo)} (#{get_symbol(value)})"
117
+ end
118
+
119
+ # Gets the symbol associated with the given value.
120
+ # If no matching symbol, gives offset from previous symbol.
121
+ def get_symbol(value)
122
+ syms = nil
123
+ addr = value
124
+ while addr > 0 do
125
+ syms = @symbol_table[addr]
126
+ break unless syms.nil?
127
+ addr -= 1
128
+ end
129
+
130
+ sym = if syms.nil? then nil else syms[0] end
131
+
132
+ sym_str = "<#{if sym.nil? then 'undefined' else sym.label end}#{if addr == value then '' else "+#{value-addr}" end}>"
133
+
134
+ return sym_str
92
135
  end
93
136
 
94
137
  # Returns a particular 8-bit register value.
@@ -101,6 +144,11 @@ module Zemu
101
144
  return "0x%04x" % @instance.registers[reg]
102
145
  end
103
146
 
147
+ # Concatenates two 8-bit values, in big-endian format.
148
+ def hilo(hi, lo)
149
+ return (hi << 8) | lo
150
+ end
151
+
104
152
  # Continue for *up to* the given number of cycles.
105
153
  # Fewer cycles may be executed, depending on the behaviour of the processor.
106
154
  def continue(cycles=-1)
@@ -114,13 +162,19 @@ module Zemu
114
162
  cycles_left = cycles
115
163
  actual_cycles = 0
116
164
 
165
+ serial_count = @instance.serial_delay.to_f
166
+
117
167
  while ((cycles == -1) || (cycles_left > 0))
118
168
  # Get time before execution.
119
169
  start = Time.now
120
170
 
121
171
  old_pc = r16("PC")
122
172
 
123
- process_serial
173
+ if (serial_count >= @instance.serial_delay)
174
+ process_serial
175
+ serial_count = 0.0
176
+ end
177
+
124
178
  cycles_done = @instance.continue(1)
125
179
  cycles_left -= cycles_done
126
180
  actual_cycles += cycles_done
@@ -132,7 +186,9 @@ module Zemu
132
186
  if @instance.clock_speed > 0
133
187
  elapsed = ending - start
134
188
 
135
- execution_time = cycles_done * (1/@instance.clock_speed)
189
+ execution_time = cycles_done * (1.0/@instance.clock_speed)
190
+ serial_count += execution_time
191
+
136
192
  padding = execution_time - elapsed
137
193
  sleep(padding) unless padding < 0
138
194
  end
@@ -169,7 +225,7 @@ module Zemu
169
225
 
170
226
  (address.to_i(16)...address.to_i(16) + size.to_i(16)).each do |a|
171
227
  m = @instance.memory(a)
172
- if (m < 32)
228
+ if (m < 32 || m > 126)
173
229
  log "%04x: %02x ." % [a, m]
174
230
  else
175
231
  log ("%04x: %02x " % [a, m]) + m.chr("UTF-8")
@@ -177,6 +233,34 @@ module Zemu
177
233
  end
178
234
  end
179
235
 
236
+ # Loads a MAP file from the given path.
237
+ def load_map(path)
238
+ if path.nil?
239
+ log "No path specified."
240
+ return
241
+ end
242
+
243
+ unless File.exist?(path.to_s)
244
+ log "Map file '#{path}' does not exist."
245
+ return
246
+ end
247
+
248
+ if File.directory?(path.to_s)
249
+ log "Cannot open '#{path}': it is a directory."
250
+ return
251
+ end
252
+
253
+ syms = {}
254
+ begin
255
+ syms.merge! Debug.load_map(path.to_s)
256
+ rescue ArgumentError => e
257
+ log "Error loading map file: #{e.message}"
258
+ syms.clear
259
+ end
260
+
261
+ @symbol_table.merge! syms
262
+ end
263
+
180
264
  # Process serial input/output via the TTY.
181
265
  def process_serial
182
266
  # Read/write serial.
data/lib/zemu.rb CHANGED
@@ -5,6 +5,7 @@ require 'pty'
5
5
  require_relative 'zemu/config'
6
6
  require_relative 'zemu/instance'
7
7
  require_relative 'zemu/interactive'
8
+ require_relative 'zemu/debug'
8
9
 
9
10
  # Zemu is a module providing an interface to build and interact with
10
11
  # configurable Z80 emulators.
@@ -128,7 +129,7 @@ module Zemu
128
129
 
129
130
  includes_str += " -I" + autogen
130
131
 
131
- command = "#{compiler} -Werror -Wno-unknown-warning-option -fPIC -shared -Wl,-undefined -Wl,dynamic_lookup #{includes_str} #{defines_str} -o #{output} #{inputs_str}"
132
+ command = "#{compiler} -O2 -Werror -Wno-unknown-warning-option -fPIC -shared -Wl,-undefined -Wl,dynamic_lookup #{includes_str} #{defines_str} -o #{output} #{inputs_str}"
132
133
 
133
134
  # Run the compiler and generate a library.
134
135
  return system(command)
data/src/io.c.erb CHANGED
@@ -4,6 +4,21 @@
4
4
  <%= device.setup %>
5
5
  <% end %>
6
6
 
7
+ void zemu_io_nmi(Z80 * instance)
8
+ {
9
+ z80_nmi(instance);
10
+ }
11
+
12
+ void zemu_io_int_on(Z80 * instance)
13
+ {
14
+ z80_int(instance, TRUE);
15
+ }
16
+
17
+ void zemu_io_int_off(Z80 * instance)
18
+ {
19
+ z80_int(instance, FALSE);
20
+ }
21
+
7
22
  zuint8 zemu_io_in(void * context, zuint16 port)
8
23
  {
9
24
  /* Z80 IO ports occupy the lower half of the address bus.
@@ -29,11 +44,6 @@ void zemu_io_out(void * context, zuint16 port, zuint8 value)
29
44
  <% end %>
30
45
  }
31
46
 
32
- void zemu_io_nmi(Z80 * instance)
33
- {
34
- z80_nmi(instance);
35
- }
36
-
37
47
  void zemu_io_clock(Z80 * instance)
38
48
  {
39
49
  <% io.each do |device| %>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zemu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jay Valentine
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-23 00:00:00.000000000 Z
11
+ date: 2021-10-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Zemu is a gem which allows the user to configure a Z80-based system
@@ -29,6 +29,7 @@ extra_rdoc_files: []
29
29
  files:
30
30
  - lib/zemu.rb
31
31
  - lib/zemu/config.rb
32
+ - lib/zemu/debug.rb
32
33
  - lib/zemu/instance.rb
33
34
  - lib/zemu/interactive.rb
34
35
  - src/debug.c
@@ -333,8 +334,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
333
334
  - !ruby/object:Gem::Version
334
335
  version: '0'
335
336
  requirements: []
336
- rubyforge_project:
337
- rubygems_version: 2.7.6
337
+ rubygems_version: 3.1.2
338
338
  signing_key:
339
339
  specification_version: 4
340
340
  summary: A configurable Z80 emulator.