Compare commits
4
Commits
v1.1.0
...
1.1.branch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
634ea933f1 | ||
|
|
6b5976be8f | ||
|
|
3530e9e99a | ||
|
|
7daeb9e978 |
No files matched your search
@@ -1,9 +1,9 @@
|
|||||||
SYMON - A 6502 System Simulator
|
SYMON - A 6502 System Simulator
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
**Version:** 1.1.0
|
**Version:** 1.1.1
|
||||||
|
|
||||||
**Last Updated:** 31 December, 2015
|
**Last Updated:** 2 January, 2016
|
||||||
|
|
||||||
See the file COPYING for license.
|
See the file COPYING for license.
|
||||||
|
|
||||||
@@ -103,19 +103,32 @@ Memory contents can be viewed (and edited) one page at a time through the Memory
|
|||||||
|
|
||||||
![Trace Log] (https://github.com/sethm/symon/raw/master/screenshots/trace_log.png)
|
![Trace Log] (https://github.com/sethm/symon/raw/master/screenshots/trace_log.png)
|
||||||
|
|
||||||
The last 20,000 execution steps are disassembled and logged to the Trace Log Window.
|
The last 20,000 execution steps are disassembled and logged to the Trace Log
|
||||||
|
Window.
|
||||||
|
|
||||||
### 3.6 NEW - Experimental 6545 CRTC Video
|
### 3.6 Simulator Speeds
|
||||||
|
|
||||||
|
![Speeds] (https://github.com/sethm/symon/raw/master/screenshots/simulator_menu.png)
|
||||||
|
|
||||||
|
Simulated speeds may be set from 1MHz to 8MHz.
|
||||||
|
|
||||||
|
### 3.7 Breakpoints
|
||||||
|
|
||||||
|
![Breakpoints] (https://github.com/sethm/symon/raw/master/screenshots/breakpoints.png)
|
||||||
|
|
||||||
|
Breakpoints can be set and removed through the Breakpoints window.
|
||||||
|
|
||||||
|
### 3.8 Experimental 6545 CRTC Video
|
||||||
|
|
||||||
![Composite Video] (https://github.com/sethm/symon/raw/master/screenshots/video_window.png)
|
![Composite Video] (https://github.com/sethm/symon/raw/master/screenshots/video_window.png)
|
||||||
|
|
||||||
This feature is highly experimental. It's possible to open a video window from the "View" menu.
|
This feature is highly experimental. It's possible to open a video window
|
||||||
This window simulates the output of a MOS 6545 CRT Controller located at address `$9000` and
|
from the "View" menu. This window simulates the output of a MOS 6545 CRT
|
||||||
`$9001`.
|
Controller located at address `$9000` and `$9001`.
|
||||||
|
|
||||||
By default, the 40 x 25 character display uses video memory located at base address `$7000`.
|
By default, the 40 x 25 character display uses video memory located at base
|
||||||
This means that the memory from address `$7000` (28672 decimal) to `$73E8` (29672 decimal)
|
address `$7000`. This means that the memory from address `$7000` (28672
|
||||||
is directly mapped to video.
|
decimal) to `$73E8` (29672 decimal) is directly mapped to video.
|
||||||
|
|
||||||
- Address Register (at address `$9000`)
|
- Address Register (at address `$9000`)
|
||||||
- R1: Horizontal Displayed Columns
|
- R1: Horizontal Displayed Columns
|
||||||
@@ -128,18 +141,20 @@ is directly mapped to video.
|
|||||||
- R14: Cursor Position (High Byte)
|
- R14: Cursor Position (High Byte)
|
||||||
- R15: Cursor Position (Low Byte)
|
- R15: Cursor Position (Low Byte)
|
||||||
|
|
||||||
Although the simulation is pretty good, there are a few key differences between
|
Although the simulation is pretty good, there are a few key differences
|
||||||
the simulated 6545 and a real 6545:
|
between the simulated 6545 and a real 6545:
|
||||||
|
|
||||||
- The simulated 6545 supports only the straight binary addressing mode of the real 6545,
|
- The simulated 6545 supports only the straight binary addressing
|
||||||
and not the Row/Column addressing mode.
|
mode of the real 6545, and not the Row/Column addressing mode.
|
||||||
- The simulated 6545 has full 16 bit addressing, where the real 6545 has only
|
|
||||||
a 14-bit address bus.
|
- The simulated 6545 has full 16 bit addressing, where the real 6545
|
||||||
- The simulation is done at a whole-frame level, meaning that lots of
|
has only a 14-bit address bus.
|
||||||
6545 programming tricks that were achieved by updating the frame address
|
|
||||||
during vertical and horizontal sync times are not achievable. There is no way
|
- The simulation is done at a whole-frame level, meaning that lots
|
||||||
(for example) to change the Display Start Address (R12 and R13) while a
|
of 6545 programming tricks that were achieved by updating the
|
||||||
frame is being drawn.
|
frame address during vertical and horizontal sync times are not
|
||||||
|
achievable. There is no way (for example) to change the Display Start
|
||||||
|
Address (R12 and R13) while a frame is being drawn.
|
||||||
|
|
||||||
For more information on the 6545 CRTC and its programming model, please see the following resources
|
For more information on the 6545 CRTC and its programming model, please see the following resources
|
||||||
|
|
||||||
@@ -148,7 +163,7 @@ For more information on the 6545 CRTC and its programming model, please see the
|
|||||||
- [MOS 6545 Datasheet (PDF)] (http://www.6502.org/users/andre/hwinfo/crtc/crtc.html)
|
- [MOS 6545 Datasheet (PDF)] (http://www.6502.org/users/andre/hwinfo/crtc/crtc.html)
|
||||||
|
|
||||||
|
|
||||||
#### 3.6.1 Example BASIC Program to test Video
|
#### 3.8.1 Example BASIC Program to test Video
|
||||||
|
|
||||||
This program will fill the video screen with all printable characters.
|
This program will fill the video screen with all printable characters.
|
||||||
|
|
||||||
@@ -173,7 +188,7 @@ Maven will build Symon, run unit tests, and produce a jar file in the
|
|||||||
Symon is meant to be invoked directly from the jar file. To run with
|
Symon is meant to be invoked directly from the jar file. To run with
|
||||||
Java 1.8 or greater, just type:
|
Java 1.8 or greater, just type:
|
||||||
|
|
||||||
$ java -jar symon-1.1.0.0-SNAPSHOT.jar
|
$ java -jar symon-1.1.1.jar
|
||||||
|
|
||||||
When Symon is running, you should be presented with a simple graphical
|
When Symon is running, you should be presented with a simple graphical
|
||||||
interface.
|
interface.
|
||||||
@@ -220,6 +235,9 @@ running.
|
|||||||
|
|
||||||
## 5.0 Revision History
|
## 5.0 Revision History
|
||||||
|
|
||||||
|
- **1.1.1:** 2 January, 2016 - Minor enhancement: Allows breakpoints
|
||||||
|
to be added with the Enter key.
|
||||||
|
|
||||||
- **1.1.0:** 31 December, 2015 - Fixed delay loop to better
|
- **1.1.0:** 31 December, 2015 - Fixed delay loop to better
|
||||||
simulate various clock speeds. Added ability to select clock
|
simulate various clock speeds. Added ability to select clock
|
||||||
speed at runtime. Status display now shows the next instruction
|
speed at runtime. Status display now shows the next instruction
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<groupId>com.loomcom.symon</groupId>
|
<groupId>com.loomcom.symon</groupId>
|
||||||
<artifactId>symon</artifactId>
|
<artifactId>symon</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<version>1.1.0</version>
|
<version>1.1.1</version>
|
||||||
<name>symon</name>
|
<name>symon</name>
|
||||||
<url>http://www.loomcom.com/symon</url>
|
<url>http://www.loomcom.com/symon</url>
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 259 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,85 @@
|
|||||||
|
package com.loomcom.symon;
|
||||||
|
|
||||||
|
import com.loomcom.symon.exceptions.MemoryAccessException;
|
||||||
|
import com.loomcom.symon.util.Utils;
|
||||||
|
|
||||||
|
import javax.swing.table.AbstractTableModel;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
public class Breakpoints extends AbstractTableModel {
|
||||||
|
|
||||||
|
private TreeSet<Integer> breakpoints;
|
||||||
|
private Simulator simulator;
|
||||||
|
|
||||||
|
public Breakpoints(Simulator simulator) {
|
||||||
|
this.breakpoints = new TreeSet<>();
|
||||||
|
this.simulator = simulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(int address) {
|
||||||
|
return this.breakpoints.contains(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addBreakpoint(int address) {
|
||||||
|
this.breakpoints .add(address);
|
||||||
|
fireTableDataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeBreakpoint(int address) {
|
||||||
|
this.breakpoints.remove(address);
|
||||||
|
fireTableDataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeBreakpointAtIndex(int index) {
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<Integer> values = new ArrayList<>(breakpoints);
|
||||||
|
int value = values.get(index);
|
||||||
|
this.breakpoints.remove(value);
|
||||||
|
fireTableDataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refresh() {
|
||||||
|
fireTableDataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName(int index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return "Address";
|
||||||
|
} else {
|
||||||
|
return "Inst";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getRowCount() {
|
||||||
|
return breakpoints.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getColumnCount() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||||
|
ArrayList<Integer> values = new ArrayList<>(breakpoints);
|
||||||
|
|
||||||
|
if (columnIndex == 0) {
|
||||||
|
return "$" + Utils.wordToHex(values.get(rowIndex));
|
||||||
|
} else if (columnIndex == 1) {
|
||||||
|
int address = values.get(rowIndex);
|
||||||
|
try {
|
||||||
|
return simulator.disassembleOpAtAddress(address);
|
||||||
|
} catch (MemoryAccessException ex) {
|
||||||
|
return "???";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
package com.loomcom.symon;
|
package com.loomcom.symon;
|
||||||
|
|
||||||
import com.loomcom.symon.exceptions.MemoryAccessException;
|
import com.loomcom.symon.exceptions.MemoryAccessException;
|
||||||
import com.loomcom.symon.util.HexUtil;
|
import com.loomcom.symon.util.Utils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ public class Cpu implements InstructionTable {
|
|||||||
private Bus bus;
|
private Bus bus;
|
||||||
|
|
||||||
/* The CPU state */
|
/* The CPU state */
|
||||||
private static final CpuState state = new CpuState();
|
private final CpuState state = new CpuState();
|
||||||
|
|
||||||
/* start time of op execution, needed for speed simulation */
|
/* start time of op execution, needed for speed simulation */
|
||||||
private long opBeginTime;
|
private long opBeginTime;
|
||||||
@@ -114,7 +114,7 @@ public class Cpu implements InstructionTable {
|
|||||||
state.sp = 0xff;
|
state.sp = 0xff;
|
||||||
|
|
||||||
// Set the PC to the address stored in the reset vector
|
// Set the PC to the address stored in the reset vector
|
||||||
state.pc = address(bus.read(RST_VECTOR_L), bus.read(RST_VECTOR_H));
|
state.pc = Utils.address(bus.read(RST_VECTOR_L), bus.read(RST_VECTOR_H));
|
||||||
|
|
||||||
// Clear instruction register.
|
// Clear instruction register.
|
||||||
state.ir = 0;
|
state.ir = 0;
|
||||||
@@ -202,7 +202,7 @@ public class Cpu implements InstructionTable {
|
|||||||
case 2: // Accumulator - ignored
|
case 2: // Accumulator - ignored
|
||||||
break;
|
break;
|
||||||
case 3: // Absolute
|
case 3: // Absolute
|
||||||
effectiveAddress = address(state.args[0], state.args[1]);
|
effectiveAddress = Utils.address(state.args[0], state.args[1]);
|
||||||
break;
|
break;
|
||||||
case 5: // Zero Page,X / Zero Page,Y
|
case 5: // Zero Page,X / Zero Page,Y
|
||||||
if (state.ir == 0x96 || state.ir == 0xb6) {
|
if (state.ir == 0x96 || state.ir == 0xb6) {
|
||||||
@@ -224,7 +224,7 @@ public class Cpu implements InstructionTable {
|
|||||||
switch (irAddressMode) {
|
switch (irAddressMode) {
|
||||||
case 0: // (Zero Page,X)
|
case 0: // (Zero Page,X)
|
||||||
tmp = (state.args[0] + state.x) & 0xff;
|
tmp = (state.args[0] + state.x) & 0xff;
|
||||||
effectiveAddress = address(bus.read(tmp), bus.read(tmp + 1));
|
effectiveAddress = Utils.address(bus.read(tmp), bus.read(tmp + 1));
|
||||||
break;
|
break;
|
||||||
case 1: // Zero Page
|
case 1: // Zero Page
|
||||||
effectiveAddress = state.args[0];
|
effectiveAddress = state.args[0];
|
||||||
@@ -233,10 +233,10 @@ public class Cpu implements InstructionTable {
|
|||||||
effectiveAddress = -1;
|
effectiveAddress = -1;
|
||||||
break;
|
break;
|
||||||
case 3: // Absolute
|
case 3: // Absolute
|
||||||
effectiveAddress = address(state.args[0], state.args[1]);
|
effectiveAddress = Utils.address(state.args[0], state.args[1]);
|
||||||
break;
|
break;
|
||||||
case 4: // (Zero Page),Y
|
case 4: // (Zero Page),Y
|
||||||
tmp = address(bus.read(state.args[0]),
|
tmp = Utils.address(bus.read(state.args[0]),
|
||||||
bus.read((state.args[0] + 1) & 0xff));
|
bus.read((state.args[0] + 1) & 0xff));
|
||||||
effectiveAddress = (tmp + state.y) & 0xffff;
|
effectiveAddress = (tmp + state.y) & 0xffff;
|
||||||
break;
|
break;
|
||||||
@@ -277,7 +277,7 @@ public class Cpu implements InstructionTable {
|
|||||||
case 0x20: // JSR - Jump to Subroutine - Implied
|
case 0x20: // JSR - Jump to Subroutine - Implied
|
||||||
stackPush((state.pc - 1 >> 8) & 0xff); // PC high byte
|
stackPush((state.pc - 1 >> 8) & 0xff); // PC high byte
|
||||||
stackPush(state.pc - 1 & 0xff); // PC low byte
|
stackPush(state.pc - 1 & 0xff); // PC low byte
|
||||||
state.pc = address(state.args[0], state.args[1]);
|
state.pc = Utils.address(state.args[0], state.args[1]);
|
||||||
break;
|
break;
|
||||||
case 0x28: // PLP - Pull Processor Status - Implied
|
case 0x28: // PLP - Pull Processor Status - Implied
|
||||||
setProcessorStatus(stackPop());
|
setProcessorStatus(stackPop());
|
||||||
@@ -294,7 +294,7 @@ public class Cpu implements InstructionTable {
|
|||||||
setProcessorStatus(stackPop());
|
setProcessorStatus(stackPop());
|
||||||
int lo = stackPop();
|
int lo = stackPop();
|
||||||
int hi = stackPop();
|
int hi = stackPop();
|
||||||
setProgramCounter(address(lo, hi));
|
setProgramCounter(Utils.address(lo, hi));
|
||||||
break;
|
break;
|
||||||
case 0x48: // PHA - Push Accumulator - Implied
|
case 0x48: // PHA - Push Accumulator - Implied
|
||||||
stackPush(state.a);
|
stackPush(state.a);
|
||||||
@@ -310,7 +310,7 @@ public class Cpu implements InstructionTable {
|
|||||||
case 0x60: // RTS - Return from Subroutine - Implied
|
case 0x60: // RTS - Return from Subroutine - Implied
|
||||||
lo = stackPop();
|
lo = stackPop();
|
||||||
hi = stackPop();
|
hi = stackPop();
|
||||||
setProgramCounter((address(lo, hi) + 1) & 0xffff);
|
setProgramCounter((Utils.address(lo, hi) + 1) & 0xffff);
|
||||||
break;
|
break;
|
||||||
case 0x68: // PLA - Pull Accumulator - Implied
|
case 0x68: // PLA - Pull Accumulator - Implied
|
||||||
state.a = stackPop();
|
state.a = stackPop();
|
||||||
@@ -398,20 +398,20 @@ public class Cpu implements InstructionTable {
|
|||||||
|
|
||||||
/** JMP *****************************************************************/
|
/** JMP *****************************************************************/
|
||||||
case 0x4c: // JMP - Absolute
|
case 0x4c: // JMP - Absolute
|
||||||
state.pc = address(state.args[0], state.args[1]);
|
state.pc = Utils.address(state.args[0], state.args[1]);
|
||||||
break;
|
break;
|
||||||
case 0x6c: // JMP - Indirect
|
case 0x6c: // JMP - Indirect
|
||||||
lo = address(state.args[0], state.args[1]); // Address of low byte
|
lo = Utils.address(state.args[0], state.args[1]); // Address of low byte
|
||||||
|
|
||||||
if (state.args[0] == 0xff &&
|
if (state.args[0] == 0xff &&
|
||||||
(behavior == CpuBehavior.NMOS_WITH_INDIRECT_JMP_BUG ||
|
(behavior == CpuBehavior.NMOS_WITH_INDIRECT_JMP_BUG ||
|
||||||
behavior == CpuBehavior.NMOS_WITH_ROR_BUG)) {
|
behavior == CpuBehavior.NMOS_WITH_ROR_BUG)) {
|
||||||
hi = address(0x00, state.args[1]);
|
hi = Utils.address(0x00, state.args[1]);
|
||||||
} else {
|
} else {
|
||||||
hi = lo + 1;
|
hi = lo + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.pc = address(bus.read(lo), bus.read(hi));
|
state.pc = Utils.address(bus.read(lo), bus.read(hi));
|
||||||
/* TODO: For accuracy, allow a flag to enable broken behavior of early 6502s:
|
/* TODO: For accuracy, allow a flag to enable broken behavior of early 6502s:
|
||||||
*
|
*
|
||||||
* "An original 6502 has does not correctly fetch the target
|
* "An original 6502 has does not correctly fetch the target
|
||||||
@@ -770,7 +770,7 @@ public class Cpu implements InstructionTable {
|
|||||||
setIrqDisableFlag();
|
setIrqDisableFlag();
|
||||||
|
|
||||||
// Load interrupt vector address into PC
|
// Load interrupt vector address into PC
|
||||||
state.pc = address(bus.read(vectorLow), bus.read(vectorHigh));
|
state.pc = Utils.address(bus.read(vectorLow), bus.read(vectorHigh));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1198,23 +1198,23 @@ public class Cpu implements InstructionTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getAccumulatorStatus() {
|
public String getAccumulatorStatus() {
|
||||||
return "$" + HexUtil.byteToHex(state.a);
|
return "$" + Utils.byteToHex(state.a);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getXRegisterStatus() {
|
public String getXRegisterStatus() {
|
||||||
return "$" + HexUtil.byteToHex(state.x);
|
return "$" + Utils.byteToHex(state.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getYRegisterStatus() {
|
public String getYRegisterStatus() {
|
||||||
return "$" + HexUtil.byteToHex(state.y);
|
return "$" + Utils.byteToHex(state.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getProgramCounterStatus() {
|
public String getProgramCounterStatus() {
|
||||||
return "$" + HexUtil.wordToHex(state.pc);
|
return "$" + Utils.wordToHex(state.pc);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getStackPointerStatus() {
|
public String getStackPointerStatus() {
|
||||||
return "$" + HexUtil.byteToHex(state.sp);
|
return "$" + Utils.byteToHex(state.sp);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getProcessorStatus() {
|
public int getProcessorStatus() {
|
||||||
@@ -1298,19 +1298,12 @@ public class Cpu implements InstructionTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Given two bytes, return an address.
|
|
||||||
*/
|
|
||||||
int address(int lowByte, int hiByte) {
|
|
||||||
return ((hiByte << 8) | lowByte) & 0xffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a hi byte and a low byte, return the Absolute,X
|
* Given a hi byte and a low byte, return the Absolute,X
|
||||||
* offset address.
|
* offset address.
|
||||||
*/
|
*/
|
||||||
int xAddress(int lowByte, int hiByte) {
|
int xAddress(int lowByte, int hiByte) {
|
||||||
return (address(lowByte, hiByte) + state.x) & 0xffff;
|
return (Utils.address(lowByte, hiByte) + state.x) & 0xffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1318,7 +1311,7 @@ public class Cpu implements InstructionTable {
|
|||||||
* offset address.
|
* offset address.
|
||||||
*/
|
*/
|
||||||
int yAddress(int lowByte, int hiByte) {
|
int yAddress(int lowByte, int hiByte) {
|
||||||
return (address(lowByte, hiByte) + state.y) & 0xffff;
|
return (Utils.address(lowByte, hiByte) + state.y) & 0xffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1362,249 +1355,81 @@ public class Cpu implements InstructionTable {
|
|||||||
} while (opBeginTime + interval >= end);
|
} while (opBeginTime + interval >= end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a formatted string representing the last instruction and
|
||||||
|
* operands that were executed.
|
||||||
|
*
|
||||||
|
* @return A string representing the mnemonic and operands of the instruction
|
||||||
|
*/
|
||||||
|
public static String disassembleOp(int opCode, int[] args) {
|
||||||
|
String mnemonic = opcodeNames[opCode];
|
||||||
|
|
||||||
|
if (mnemonic == null) {
|
||||||
|
return "???";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder(mnemonic);
|
||||||
|
|
||||||
|
switch (instructionModes[opCode]) {
|
||||||
|
case ABS:
|
||||||
|
sb.append(" $").append(Utils.wordToHex(Utils.address(args[0], args[1])));
|
||||||
|
break;
|
||||||
|
case ABX:
|
||||||
|
sb.append(" $").append(Utils.wordToHex(Utils.address(args[0], args[1]))).append(",X");
|
||||||
|
break;
|
||||||
|
case ABY:
|
||||||
|
sb.append(" $").append(Utils.wordToHex(Utils.address(args[0], args[1]))).append(",Y");
|
||||||
|
break;
|
||||||
|
case IMM:
|
||||||
|
sb.append(" #$").append(Utils.byteToHex(args[0]));
|
||||||
|
break;
|
||||||
|
case IND:
|
||||||
|
sb.append(" ($").append(Utils.wordToHex(Utils.address(args[0], args[1]))).append(")");
|
||||||
|
break;
|
||||||
|
case XIN:
|
||||||
|
sb.append(" ($").append(Utils.byteToHex(args[0])).append(",X)");
|
||||||
|
break;
|
||||||
|
case INY:
|
||||||
|
sb.append(" ($").append(Utils.byteToHex(args[0])).append("),Y");
|
||||||
|
break;
|
||||||
|
case REL:
|
||||||
|
case ZPG:
|
||||||
|
sb.append(" $").append(Utils.byteToHex(args[0]));
|
||||||
|
break;
|
||||||
|
case ZPX:
|
||||||
|
sb.append(" $").append(Utils.byteToHex(args[0])).append(",X");
|
||||||
|
break;
|
||||||
|
case ZPY:
|
||||||
|
sb.append(" $").append(Utils.byteToHex(args[0])).append(",Y");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A compact, struct-like representation of CPU state.
|
* Return a formatted string representing the next instruction and
|
||||||
|
* operands to be executed.
|
||||||
|
*
|
||||||
|
* @return A string representing the mnemonic and operands of the instruction
|
||||||
*/
|
*/
|
||||||
public static class CpuState {
|
public String disassembleNextOp() {
|
||||||
/**
|
return Cpu.disassembleOp(state.nextIr, state.nextArgs);
|
||||||
* Accumulator
|
}
|
||||||
*/
|
|
||||||
public int a;
|
|
||||||
/**
|
|
||||||
* X index regsiter
|
|
||||||
*/
|
|
||||||
public int x;
|
|
||||||
/**
|
|
||||||
* Y index register
|
|
||||||
*/
|
|
||||||
public int y;
|
|
||||||
/**
|
|
||||||
* Stack Pointer
|
|
||||||
*/
|
|
||||||
public int sp;
|
|
||||||
/**
|
|
||||||
* Program Counter
|
|
||||||
*/
|
|
||||||
public int pc;
|
|
||||||
/**
|
|
||||||
* Last Loaded Instruction Register
|
|
||||||
*/
|
|
||||||
public int ir;
|
|
||||||
/**
|
|
||||||
* Peek-Ahead to next IR
|
|
||||||
*/
|
|
||||||
public int nextIr;
|
|
||||||
public int[] args = new int[2];
|
|
||||||
public int[] nextArgs = new int[2];
|
|
||||||
public int instSize;
|
|
||||||
public boolean opTrap;
|
|
||||||
public boolean irqAsserted;
|
|
||||||
public boolean nmiAsserted;
|
|
||||||
public int lastPc;
|
|
||||||
|
|
||||||
/* Status Flag Register bits */
|
/**
|
||||||
public boolean carryFlag;
|
* @param address Address to disassemble
|
||||||
public boolean negativeFlag;
|
* @return String containing the disassembled instruction and operands.
|
||||||
public boolean zeroFlag;
|
*/
|
||||||
public boolean irqDisableFlag;
|
public String disassembleOpAtAddress(int address) throws MemoryAccessException {
|
||||||
public boolean decimalModeFlag;
|
int opCode = bus.read(address);
|
||||||
public boolean breakFlag;
|
int args[] = new int[2];
|
||||||
public boolean overflowFlag;
|
int size = Cpu.instructionSizes[opCode];
|
||||||
public long stepCounter = 0L;
|
for (int i = 1; i < size; i++) {
|
||||||
|
int nextRead = (address + i) % bus.endAddress();
|
||||||
/**
|
args[i-1] = bus.read(nextRead);
|
||||||
* Create an empty CPU State.
|
|
||||||
*/
|
|
||||||
public CpuState() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Snapshot a copy of the CpuState.
|
|
||||||
*
|
|
||||||
* (This is a copy constructor rather than an implementation of <code>Cloneable</code>
|
|
||||||
* based on Josh Bloch's recommendation)
|
|
||||||
*
|
|
||||||
* @param s The CpuState to copy.
|
|
||||||
*/
|
|
||||||
public CpuState(CpuState s) {
|
|
||||||
this.a = s.a;
|
|
||||||
this.x = s.x;
|
|
||||||
this.y = s.y;
|
|
||||||
this.sp = s.sp;
|
|
||||||
this.pc = s.pc;
|
|
||||||
this.ir = s.ir;
|
|
||||||
this.nextIr = s.nextIr;
|
|
||||||
this.lastPc = s.lastPc;
|
|
||||||
this.args[0] = s.args[0];
|
|
||||||
this.args[1] = s.args[1];
|
|
||||||
this.nextArgs[0] = s.nextArgs[0];
|
|
||||||
this.nextArgs[1] = s.nextArgs[1];
|
|
||||||
this.instSize = s.instSize;
|
|
||||||
this.opTrap = s.opTrap;
|
|
||||||
this.irqAsserted = s.irqAsserted;
|
|
||||||
this.carryFlag = s.carryFlag;
|
|
||||||
this.negativeFlag = s.negativeFlag;
|
|
||||||
this.zeroFlag = s.zeroFlag;
|
|
||||||
this.irqDisableFlag = s.irqDisableFlag;
|
|
||||||
this.decimalModeFlag = s.decimalModeFlag;
|
|
||||||
this.breakFlag = s.breakFlag;
|
|
||||||
this.overflowFlag = s.overflowFlag;
|
|
||||||
this.stepCounter = s.stepCounter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return disassembleOp(opCode, args);
|
||||||
* Returns a string formatted for the trace log.
|
|
||||||
*
|
|
||||||
* @return a string formatted for the trace log.
|
|
||||||
*/
|
|
||||||
public String toTraceEvent() {
|
|
||||||
String opcode = disassembleLastOp();
|
|
||||||
return getInstructionByteStatus() + " " +
|
|
||||||
String.format("%-14s", opcode) +
|
|
||||||
"A:" + HexUtil.byteToHex(a) + " " +
|
|
||||||
"X:" + HexUtil.byteToHex(x) + " " +
|
|
||||||
"Y:" + HexUtil.byteToHex(y) + " " +
|
|
||||||
"F:" + HexUtil.byteToHex(getStatusFlag()) + " " +
|
|
||||||
"S:1" + HexUtil.byteToHex(sp) + " " +
|
|
||||||
getProcessorStatusString() + "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The value of the Process Status Register, as a byte.
|
|
||||||
*/
|
|
||||||
public int getStatusFlag() {
|
|
||||||
int status = 0x20;
|
|
||||||
if (carryFlag) {
|
|
||||||
status |= P_CARRY;
|
|
||||||
}
|
|
||||||
if (zeroFlag) {
|
|
||||||
status |= P_ZERO;
|
|
||||||
}
|
|
||||||
if (irqDisableFlag) {
|
|
||||||
status |= P_IRQ_DISABLE;
|
|
||||||
}
|
|
||||||
if (decimalModeFlag) {
|
|
||||||
status |= P_DECIMAL;
|
|
||||||
}
|
|
||||||
if (breakFlag) {
|
|
||||||
status |= P_BREAK;
|
|
||||||
}
|
|
||||||
if (overflowFlag) {
|
|
||||||
status |= P_OVERFLOW;
|
|
||||||
}
|
|
||||||
if (negativeFlag) {
|
|
||||||
status |= P_NEGATIVE;
|
|
||||||
}
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getInstructionByteStatus() {
|
|
||||||
switch (Cpu.instructionSizes[ir]) {
|
|
||||||
case 0:
|
|
||||||
case 1:
|
|
||||||
return HexUtil.wordToHex(lastPc) + " " +
|
|
||||||
HexUtil.byteToHex(ir) + " ";
|
|
||||||
case 2:
|
|
||||||
return HexUtil.wordToHex(lastPc) + " " +
|
|
||||||
HexUtil.byteToHex(ir) + " " +
|
|
||||||
HexUtil.byteToHex(args[0]) + " ";
|
|
||||||
case 3:
|
|
||||||
return HexUtil.wordToHex(lastPc) + " " +
|
|
||||||
HexUtil.byteToHex(ir) + " " +
|
|
||||||
HexUtil.byteToHex(args[0]) + " " +
|
|
||||||
HexUtil.byteToHex(args[1]);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a formatted string representing the last instruction and
|
|
||||||
* operands that were executed.
|
|
||||||
*
|
|
||||||
* @return A string representing the mnemonic and operands of the instruction
|
|
||||||
*/
|
|
||||||
private String disassembleOp(int ir, int[] args) {
|
|
||||||
String mnemonic = opcodeNames[ir];
|
|
||||||
|
|
||||||
if (mnemonic == null) {
|
|
||||||
return "???";
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder(mnemonic);
|
|
||||||
|
|
||||||
switch (instructionModes[ir]) {
|
|
||||||
case ABS:
|
|
||||||
sb.append(" $").append(HexUtil.wordToHex(address(args[0], args[1])));
|
|
||||||
break;
|
|
||||||
case ABX:
|
|
||||||
sb.append(" $").append(HexUtil.wordToHex(address(args[0], args[1]))).append(",X");
|
|
||||||
break;
|
|
||||||
case ABY:
|
|
||||||
sb.append(" $").append(HexUtil.wordToHex(address(args[0], args[1]))).append(",Y");
|
|
||||||
break;
|
|
||||||
case IMM:
|
|
||||||
sb.append(" #$").append(HexUtil.byteToHex(args[0]));
|
|
||||||
break;
|
|
||||||
case IND:
|
|
||||||
sb.append(" ($").append(HexUtil.wordToHex(address(args[0], args[1]))).append(")");
|
|
||||||
break;
|
|
||||||
case XIN:
|
|
||||||
sb.append(" ($").append(HexUtil.byteToHex(args[0])).append(",X)");
|
|
||||||
break;
|
|
||||||
case INY:
|
|
||||||
sb.append(" ($").append(HexUtil.byteToHex(args[0])).append("),Y");
|
|
||||||
break;
|
|
||||||
case REL:
|
|
||||||
case ZPG:
|
|
||||||
sb.append(" $").append(HexUtil.byteToHex(args[0]));
|
|
||||||
break;
|
|
||||||
case ZPX:
|
|
||||||
sb.append(" $").append(HexUtil.byteToHex(args[0])).append(",X");
|
|
||||||
break;
|
|
||||||
case ZPY:
|
|
||||||
sb.append(" $").append(HexUtil.byteToHex(args[0])).append(",Y");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String disassembleLastOp() {
|
|
||||||
return disassembleOp(ir, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a formatted string representing the next instruction and
|
|
||||||
* operands to be executed.
|
|
||||||
*
|
|
||||||
* @return A string representing the mnemonic and operands of the instruction
|
|
||||||
*/
|
|
||||||
public String disassembleNextOp() {
|
|
||||||
return disassembleOp(nextIr, nextArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given two bytes, return an address.
|
|
||||||
*/
|
|
||||||
private int address(int lowByte, int hiByte) {
|
|
||||||
return ((hiByte << 8) | lowByte) & 0xffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return A string representing the current status register state.
|
|
||||||
*/
|
|
||||||
public String getProcessorStatusString() {
|
|
||||||
return "[" + (negativeFlag ? 'N' : '.') +
|
|
||||||
(overflowFlag ? 'V' : '.') +
|
|
||||||
"-" +
|
|
||||||
(breakFlag ? 'B' : '.') +
|
|
||||||
(decimalModeFlag ? 'D' : '.') +
|
|
||||||
(irqDisableFlag ? 'I' : '.') +
|
|
||||||
(zeroFlag ? 'Z' : '.') +
|
|
||||||
(carryFlag ? 'C' : '.') +
|
|
||||||
"]";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package com.loomcom.symon;
|
||||||
|
|
||||||
|
import com.loomcom.symon.util.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A compact, struct-like representation of CPU state.
|
||||||
|
*/
|
||||||
|
public class CpuState {
|
||||||
|
/**
|
||||||
|
* Accumulator
|
||||||
|
*/
|
||||||
|
public int a;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X index regsiter
|
||||||
|
*/
|
||||||
|
public int x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Y index register
|
||||||
|
*/
|
||||||
|
public int y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stack Pointer
|
||||||
|
*/
|
||||||
|
public int sp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Program Counter
|
||||||
|
*/
|
||||||
|
public int pc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last Loaded Instruction Register
|
||||||
|
*/
|
||||||
|
public int ir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Peek-Ahead to next IR
|
||||||
|
*/
|
||||||
|
public int nextIr;
|
||||||
|
public int[] args = new int[2];
|
||||||
|
public int[] nextArgs = new int[2];
|
||||||
|
public int instSize;
|
||||||
|
public boolean opTrap;
|
||||||
|
public boolean irqAsserted;
|
||||||
|
public boolean nmiAsserted;
|
||||||
|
public int lastPc;
|
||||||
|
|
||||||
|
/* Status Flag Register bits */
|
||||||
|
public boolean carryFlag;
|
||||||
|
public boolean negativeFlag;
|
||||||
|
public boolean zeroFlag;
|
||||||
|
public boolean irqDisableFlag;
|
||||||
|
public boolean decimalModeFlag;
|
||||||
|
public boolean breakFlag;
|
||||||
|
public boolean overflowFlag;
|
||||||
|
public long stepCounter = 0L;
|
||||||
|
|
||||||
|
public CpuState() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snapshot a copy of the CpuState.
|
||||||
|
*
|
||||||
|
* (This is a copy constructor rather than an implementation of <code>Cloneable</code>
|
||||||
|
* based on Josh Bloch's recommendation)
|
||||||
|
*
|
||||||
|
* @param s The CpuState to copy.
|
||||||
|
*/
|
||||||
|
public CpuState(CpuState s) {
|
||||||
|
this.a = s.a;
|
||||||
|
this.x = s.x;
|
||||||
|
this.y = s.y;
|
||||||
|
this.sp = s.sp;
|
||||||
|
this.pc = s.pc;
|
||||||
|
this.ir = s.ir;
|
||||||
|
this.nextIr = s.nextIr;
|
||||||
|
this.lastPc = s.lastPc;
|
||||||
|
this.args[0] = s.args[0];
|
||||||
|
this.args[1] = s.args[1];
|
||||||
|
this.nextArgs[0] = s.nextArgs[0];
|
||||||
|
this.nextArgs[1] = s.nextArgs[1];
|
||||||
|
this.instSize = s.instSize;
|
||||||
|
this.opTrap = s.opTrap;
|
||||||
|
this.irqAsserted = s.irqAsserted;
|
||||||
|
this.carryFlag = s.carryFlag;
|
||||||
|
this.negativeFlag = s.negativeFlag;
|
||||||
|
this.zeroFlag = s.zeroFlag;
|
||||||
|
this.irqDisableFlag = s.irqDisableFlag;
|
||||||
|
this.decimalModeFlag = s.decimalModeFlag;
|
||||||
|
this.breakFlag = s.breakFlag;
|
||||||
|
this.overflowFlag = s.overflowFlag;
|
||||||
|
this.stepCounter = s.stepCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string formatted for the trace log.
|
||||||
|
*
|
||||||
|
* @return a string formatted for the trace log.
|
||||||
|
*/
|
||||||
|
public String toTraceEvent() {
|
||||||
|
String opcode = Cpu.disassembleOp(ir, args);
|
||||||
|
return getInstructionByteStatus() + " " +
|
||||||
|
String.format("%-14s", opcode) +
|
||||||
|
"A:" + Utils.byteToHex(a) + " " +
|
||||||
|
"X:" + Utils.byteToHex(x) + " " +
|
||||||
|
"Y:" + Utils.byteToHex(y) + " " +
|
||||||
|
"F:" + Utils.byteToHex(getStatusFlag()) + " " +
|
||||||
|
"S:1" + Utils.byteToHex(sp) + " " +
|
||||||
|
getProcessorStatusString() + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The value of the Process Status Register, as a byte.
|
||||||
|
*/
|
||||||
|
public int getStatusFlag() {
|
||||||
|
int status = 0x20;
|
||||||
|
if (carryFlag) {
|
||||||
|
status |= Cpu.P_CARRY;
|
||||||
|
}
|
||||||
|
if (zeroFlag) {
|
||||||
|
status |= Cpu.P_ZERO;
|
||||||
|
}
|
||||||
|
if (irqDisableFlag) {
|
||||||
|
status |= Cpu.P_IRQ_DISABLE;
|
||||||
|
}
|
||||||
|
if (decimalModeFlag) {
|
||||||
|
status |= Cpu.P_DECIMAL;
|
||||||
|
}
|
||||||
|
if (breakFlag) {
|
||||||
|
status |= Cpu.P_BREAK;
|
||||||
|
}
|
||||||
|
if (overflowFlag) {
|
||||||
|
status |= Cpu.P_OVERFLOW;
|
||||||
|
}
|
||||||
|
if (negativeFlag) {
|
||||||
|
status |= Cpu.P_NEGATIVE;
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInstructionByteStatus() {
|
||||||
|
switch (Cpu.instructionSizes[ir]) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
return Utils.wordToHex(lastPc) + " " +
|
||||||
|
Utils.byteToHex(ir) + " ";
|
||||||
|
case 2:
|
||||||
|
return Utils.wordToHex(lastPc) + " " +
|
||||||
|
Utils.byteToHex(ir) + " " +
|
||||||
|
Utils.byteToHex(args[0]) + " ";
|
||||||
|
case 3:
|
||||||
|
return Utils.wordToHex(lastPc) + " " +
|
||||||
|
Utils.byteToHex(ir) + " " +
|
||||||
|
Utils.byteToHex(args[0]) + " " +
|
||||||
|
Utils.byteToHex(args[1]);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A string representing the current status register state.
|
||||||
|
*/
|
||||||
|
public String getProcessorStatusString() {
|
||||||
|
return "[" + (negativeFlag ? 'N' : '.') +
|
||||||
|
(overflowFlag ? 'V' : '.') +
|
||||||
|
"-" +
|
||||||
|
(breakFlag ? 'B' : '.') +
|
||||||
|
(decimalModeFlag ? 'D' : '.') +
|
||||||
|
(irqDisableFlag ? 'I' : '.') +
|
||||||
|
(zeroFlag ? 'Z' : '.') +
|
||||||
|
(carryFlag ? 'C' : '.') +
|
||||||
|
"]";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,7 +119,7 @@ public class Simulator {
|
|||||||
private JFileChooser fileChooser;
|
private JFileChooser fileChooser;
|
||||||
private PreferencesDialog preferences;
|
private PreferencesDialog preferences;
|
||||||
|
|
||||||
private SortedSet<Integer> breakpoints;
|
private Breakpoints breakpoints;
|
||||||
|
|
||||||
private final Object commandMonitorObject = new Object();
|
private final Object commandMonitorObject = new Object();
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ public class Simulator {
|
|||||||
private static final String[] STEPS = {"1", "5", "10", "20", "50", "100"};
|
private static final String[] STEPS = {"1", "5", "10", "20", "50", "100"};
|
||||||
|
|
||||||
public Simulator(Class machineClass) throws Exception {
|
public Simulator(Class machineClass) throws Exception {
|
||||||
this.breakpoints = new TreeSet<>();
|
this.breakpoints = new Breakpoints(this);
|
||||||
|
|
||||||
this.machine = (Machine) machineClass.getConstructors()[0].newInstance();
|
this.machine = (Machine) machineClass.getConstructors()[0].newInstance();
|
||||||
|
|
||||||
@@ -443,6 +443,10 @@ public class Simulator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String disassembleOpAtAddress(int address) throws MemoryAccessException {
|
||||||
|
return machine.getCpu().disassembleOpAtAddress(address);
|
||||||
|
}
|
||||||
|
|
||||||
class LoadProgramAction extends AbstractAction {
|
class LoadProgramAction extends AbstractAction {
|
||||||
public LoadProgramAction() {
|
public LoadProgramAction() {
|
||||||
super("Load Program...", null);
|
super("Load Program...", null);
|
||||||
@@ -473,10 +477,14 @@ public class Simulator {
|
|||||||
program[i++] = dis.readByte();
|
program[i++] = dis.readByte();
|
||||||
}
|
}
|
||||||
|
|
||||||
SwingUtilities.invokeLater(() -> console.reset());
|
|
||||||
|
|
||||||
// Now load the program at the starting address.
|
// Now load the program at the starting address.
|
||||||
loadProgram(program, preferences.getProgramStartAddress());
|
loadProgram(program, preferences.getProgramStartAddress());
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
console.reset();
|
||||||
|
breakpoints.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: "Don't Show Again" checkbox
|
// TODO: "Don't Show Again" checkbox
|
||||||
JOptionPane.showMessageDialog(mainWindow,
|
JOptionPane.showMessageDialog(mainWindow,
|
||||||
"Loaded Successfully At " +
|
"Loaded Successfully At " +
|
||||||
@@ -524,6 +532,9 @@ public class Simulator {
|
|||||||
|
|
||||||
updateVisibleState();
|
updateVisibleState();
|
||||||
|
|
||||||
|
// Refresh breakpoints to show new memory contents.
|
||||||
|
breakpoints.refresh();
|
||||||
|
|
||||||
logger.info("ROM File `{}' loaded at {}", romFile.getName(),
|
logger.info("ROM File `{}' loaded at {}", romFile.getName(),
|
||||||
String.format("0x%04X", machine.getRomBase()));
|
String.format("0x%04X", machine.getRomBase()));
|
||||||
// TODO: "Don't Show Again" checkbox
|
// TODO: "Don't Show Again" checkbox
|
||||||
|
|||||||
@@ -23,15 +23,15 @@
|
|||||||
|
|
||||||
package com.loomcom.symon.ui;
|
package com.loomcom.symon.ui;
|
||||||
|
|
||||||
import com.loomcom.symon.util.HexUtil;
|
import com.loomcom.symon.Breakpoints;
|
||||||
|
import com.loomcom.symon.util.Utils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.border.EmptyBorder;
|
import javax.swing.border.EmptyBorder;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.util.ArrayList;
|
import java.awt.event.ActionListener;
|
||||||
import java.util.SortedSet;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple window to enter breakpoints.
|
* Simple window to enter breakpoints.
|
||||||
@@ -44,45 +44,9 @@ public class BreakpointsWindow extends JFrame {
|
|||||||
private static final String EMPTY_STRING = "";
|
private static final String EMPTY_STRING = "";
|
||||||
|
|
||||||
private JFrame mainWindow;
|
private JFrame mainWindow;
|
||||||
private SortedSet<Integer> breakpoints;
|
private Breakpoints breakpoints;
|
||||||
|
|
||||||
/**
|
public BreakpointsWindow(Breakpoints breakpoints,
|
||||||
* Simple ListModel to back the list of breakpoints.
|
|
||||||
*/
|
|
||||||
private class BreakpointsListModel extends AbstractListModel<String> {
|
|
||||||
private SortedSet<Integer> breakpoints;
|
|
||||||
|
|
||||||
public BreakpointsListModel(SortedSet<Integer> breakpoints) {
|
|
||||||
this.breakpoints = breakpoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSize() {
|
|
||||||
return breakpoints.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getElementAt(int index) {
|
|
||||||
ArrayList<Integer> values = new ArrayList<>(breakpoints);
|
|
||||||
return "$" + HexUtil.wordToHex(values.get(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addElement(Integer breakpoint) {
|
|
||||||
breakpoints.add(breakpoint);
|
|
||||||
ArrayList<Integer> values = new ArrayList<>(breakpoints);
|
|
||||||
int index = values.indexOf(breakpoint);
|
|
||||||
fireIntervalAdded(this, index, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeElement(int index) {
|
|
||||||
ArrayList<Integer> values = new ArrayList<>(breakpoints);
|
|
||||||
Integer breakpoint = values.get(index);
|
|
||||||
breakpoints.remove(breakpoint);
|
|
||||||
fireIntervalRemoved(this, index, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public BreakpointsWindow(SortedSet<Integer> breakpoints,
|
|
||||||
JFrame mainWindow) {
|
JFrame mainWindow) {
|
||||||
this.breakpoints = breakpoints;
|
this.breakpoints = breakpoints;
|
||||||
this.mainWindow = mainWindow;
|
this.mainWindow = mainWindow;
|
||||||
@@ -102,31 +66,27 @@ public class BreakpointsWindow extends JFrame {
|
|||||||
JButton removeButton = new JButton("Del");
|
JButton removeButton = new JButton("Del");
|
||||||
removeButton.setEnabled(false);
|
removeButton.setEnabled(false);
|
||||||
|
|
||||||
JTextField addTextField = new JTextField(5);
|
JTextField addTextField = new JTextField(4);
|
||||||
|
|
||||||
BreakpointsListModel listModel = new BreakpointsListModel(breakpoints);
|
JTable breakpointsTable = new JTable(breakpoints);
|
||||||
JList<String> breakpointsList = new JList<>(listModel);
|
breakpointsTable.setShowGrid(true);
|
||||||
breakpointsList.setFont(new Font("Monospace", Font.PLAIN, 14));
|
breakpointsTable.setGridColor(Color.LIGHT_GRAY);
|
||||||
breakpointsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
breakpointsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
|
breakpointsTable.getSelectionModel().addListSelectionListener(e -> {
|
||||||
|
if (e.getFirstIndex() > -1) {
|
||||||
|
removeButton.setEnabled(true);
|
||||||
|
} else {
|
||||||
|
removeButton.setEnabled(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
JScrollPane scrollPane = new JScrollPane(breakpointsList);
|
JScrollPane scrollPane = new JScrollPane(breakpointsTable);
|
||||||
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||||
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
|
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
|
||||||
|
|
||||||
breakpointsPanel.add(scrollPane, BorderLayout.CENTER);
|
breakpointsPanel.add(scrollPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
ActionListener addBreakpointListener = e -> {
|
||||||
breakpointsList.addListSelectionListener(le -> {
|
|
||||||
int idx = breakpointsList.getSelectedIndex();
|
|
||||||
|
|
||||||
if (idx == -1) {
|
|
||||||
removeButton.setEnabled(false);
|
|
||||||
} else {
|
|
||||||
removeButton.setEnabled(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addButton.addActionListener(e -> {
|
|
||||||
int value = -1;
|
int value = -1;
|
||||||
|
|
||||||
String newBreakpoint = addTextField.getText();
|
String newBreakpoint = addTextField.getText();
|
||||||
@@ -136,7 +96,7 @@ public class BreakpointsWindow extends JFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
value = Integer.parseInt(addTextField.getText(), 16);
|
value = (Integer.parseInt(addTextField.getText(), 16) & 0xffff);
|
||||||
} catch (NumberFormatException ex) {
|
} catch (NumberFormatException ex) {
|
||||||
logger.warn("Can't parse page number {}", newBreakpoint);
|
logger.warn("Can't parse page number {}", newBreakpoint);
|
||||||
return;
|
return;
|
||||||
@@ -146,14 +106,17 @@ public class BreakpointsWindow extends JFrame {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
listModel.addElement(value);
|
breakpoints.addBreakpoint(value);
|
||||||
|
|
||||||
logger.debug("Added breakpoint ${}", HexUtil.wordToHex(value));
|
logger.debug("Added breakpoint ${}", Utils.wordToHex(value));
|
||||||
|
|
||||||
addTextField.setText(EMPTY_STRING);
|
addTextField.setText(EMPTY_STRING);
|
||||||
});
|
};
|
||||||
|
|
||||||
removeButton.addActionListener(e -> listModel.removeElement(breakpointsList.getSelectedIndex()));
|
addButton.addActionListener(addBreakpointListener);
|
||||||
|
addTextField.addActionListener(addBreakpointListener);
|
||||||
|
|
||||||
|
removeButton.addActionListener(e -> breakpoints.removeBreakpointAtIndex(breakpointsTable.getSelectedRow()));
|
||||||
|
|
||||||
controlPanel.add(addTextField);
|
controlPanel.add(addTextField);
|
||||||
controlPanel.add(addButton);
|
controlPanel.add(addButton);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ package com.loomcom.symon.ui;
|
|||||||
|
|
||||||
import com.loomcom.symon.Bus;
|
import com.loomcom.symon.Bus;
|
||||||
import com.loomcom.symon.exceptions.MemoryAccessException;
|
import com.loomcom.symon.exceptions.MemoryAccessException;
|
||||||
import com.loomcom.symon.util.HexUtil;
|
import com.loomcom.symon.util.Utils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ public class MemoryWindow extends JFrame implements ActionListener {
|
|||||||
|
|
||||||
previousPageButton.setEnabled(pageNumber > 0x00);
|
previousPageButton.setEnabled(pageNumber > 0x00);
|
||||||
nextPageButton.setEnabled(pageNumber < 0xff);
|
nextPageButton.setEnabled(pageNumber < 0xff);
|
||||||
pageNumberTextField.setText(HexUtil.byteToHex(pageNumber));
|
pageNumberTextField.setText(Utils.byteToHex(pageNumber));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -328,13 +328,13 @@ public class MemoryWindow extends JFrame implements ActionListener {
|
|||||||
public Object getValueAt(int row, int column) {
|
public Object getValueAt(int row, int column) {
|
||||||
try {
|
try {
|
||||||
if (column == 0) {
|
if (column == 0) {
|
||||||
return HexUtil.wordToHex(fullAddress(row, 1));
|
return Utils.wordToHex(fullAddress(row, 1));
|
||||||
} else if (column < 9) {
|
} else if (column < 9) {
|
||||||
// Display hex value of the data
|
// Display hex value of the data
|
||||||
return HexUtil.byteToHex(bus.read(fullAddress(row, column)));
|
return Utils.byteToHex(bus.read(fullAddress(row, column)));
|
||||||
} else {
|
} else {
|
||||||
// Display the ASCII equivalent (if printable)
|
// Display the ASCII equivalent (if printable)
|
||||||
return HexUtil.byteToAscii(bus.read(fullAddress(row, column - 8)));
|
return Utils.byteToAscii(bus.read(fullAddress(row, column - 8)));
|
||||||
}
|
}
|
||||||
} catch (MemoryAccessException ex) {
|
} catch (MemoryAccessException ex) {
|
||||||
return "??";
|
return "??";
|
||||||
|
|||||||
@@ -24,14 +24,13 @@
|
|||||||
package com.loomcom.symon.ui;
|
package com.loomcom.symon.ui;
|
||||||
|
|
||||||
import com.loomcom.symon.Cpu;
|
import com.loomcom.symon.Cpu;
|
||||||
|
import com.loomcom.symon.CpuState;
|
||||||
import com.loomcom.symon.machines.Machine;
|
import com.loomcom.symon.machines.Machine;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.border.Border;
|
import javax.swing.border.Border;
|
||||||
import javax.swing.border.EtchedBorder;
|
import javax.swing.border.EtchedBorder;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.event.ActionEvent;
|
|
||||||
import java.awt.event.ActionListener;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI component that displays the current state of the simulated CPU.
|
* UI component that displays the current state of the simulated CPU.
|
||||||
@@ -277,7 +276,7 @@ public class StatusPanel extends JPanel {
|
|||||||
*/
|
*/
|
||||||
public void updateState() {
|
public void updateState() {
|
||||||
Cpu cpu = machine.getCpu();
|
Cpu cpu = machine.getCpu();
|
||||||
Cpu.CpuState cpuState = cpu.getCpuState();
|
CpuState cpuState = cpu.getCpuState();
|
||||||
|
|
||||||
// Update the Processor Status Flag display
|
// Update the Processor Status Flag display
|
||||||
int status = cpuState.getStatusFlag();
|
int status = cpuState.getStatusFlag();
|
||||||
@@ -293,7 +292,7 @@ public class StatusPanel extends JPanel {
|
|||||||
// Update the register and address displays
|
// Update the register and address displays
|
||||||
|
|
||||||
// We always want to show the NEXT instruction that will be executed
|
// We always want to show the NEXT instruction that will be executed
|
||||||
opcodeField.setText(cpu.getCpuState().disassembleNextOp());
|
opcodeField.setText(cpu.disassembleNextOp());
|
||||||
pcField.setText(cpu.getProgramCounterStatus());
|
pcField.setText(cpu.getProgramCounterStatus());
|
||||||
spField.setText(cpu.getStackPointerStatus());
|
spField.setText(cpu.getStackPointerStatus());
|
||||||
aField.setText(cpu.getAccumulatorStatus());
|
aField.setText(cpu.getAccumulatorStatus());
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
package com.loomcom.symon.ui;
|
package com.loomcom.symon.ui;
|
||||||
|
|
||||||
import com.loomcom.symon.Cpu;
|
import com.loomcom.symon.CpuState;
|
||||||
import com.loomcom.symon.util.FifoRingBuffer;
|
import com.loomcom.symon.util.FifoRingBuffer;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
@@ -36,7 +36,7 @@ import java.awt.*;
|
|||||||
*/
|
*/
|
||||||
public class TraceLog extends JFrame {
|
public class TraceLog extends JFrame {
|
||||||
|
|
||||||
private final FifoRingBuffer<Cpu.CpuState> traceLog;
|
private final FifoRingBuffer<CpuState> traceLog;
|
||||||
private final JTextArea traceLogTextArea;
|
private final JTextArea traceLogTextArea;
|
||||||
|
|
||||||
private static final Dimension MIN_SIZE = new Dimension(320, 200);
|
private static final Dimension MIN_SIZE = new Dimension(320, 200);
|
||||||
@@ -71,7 +71,7 @@ public class TraceLog extends JFrame {
|
|||||||
StringBuilder logString = new StringBuilder();
|
StringBuilder logString = new StringBuilder();
|
||||||
|
|
||||||
synchronized(traceLog) {
|
synchronized(traceLog) {
|
||||||
for (Cpu.CpuState state : traceLog) {
|
for (CpuState state : traceLog) {
|
||||||
logString.append(state.toTraceEvent());
|
logString.append(state.toTraceEvent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,9 +99,9 @@ public class TraceLog extends JFrame {
|
|||||||
*
|
*
|
||||||
* @param state The CPU State to append.
|
* @param state The CPU State to append.
|
||||||
*/
|
*/
|
||||||
public void append(Cpu.CpuState state) {
|
public void append(CpuState state) {
|
||||||
synchronized(traceLog) {
|
synchronized(traceLog) {
|
||||||
traceLog.push(new Cpu.CpuState(state));
|
traceLog.push(new CpuState(state));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-17
@@ -24,24 +24,9 @@
|
|||||||
package com.loomcom.symon.util;
|
package com.loomcom.symon.util;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hex String Utilities.
|
* Various Utilities
|
||||||
*
|
|
||||||
* <p/>
|
|
||||||
*
|
|
||||||
* But why? Java, after all, has a number of ways to convert an integer into a hex string,
|
|
||||||
* so it may look absurd to go to the trouble of writing yet another conversion! The answer is
|
|
||||||
* performance.
|
|
||||||
*
|
|
||||||
* <p/>
|
|
||||||
*
|
|
||||||
* The most convenient way to get a formatted hex value from an integer is with the <code>String.format</code>
|
|
||||||
* method, but this turns out to be extremely inefficient. Formatting a million integers
|
|
||||||
* with <code>String.format</code> takes something like 1600ms. Formatting the same number of integers
|
|
||||||
* with <code>HexUtil</code> takes only 160ms. This is on par with <code>Integer.toHexString</code>,
|
|
||||||
* but also gives the desired padding.
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public class HexUtil {
|
public class Utils {
|
||||||
|
|
||||||
static final String NON_PRINTABLE = ".";
|
static final String NON_PRINTABLE = ".";
|
||||||
|
|
||||||
@@ -93,6 +78,7 @@ public class HexUtil {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Very fast 8-bit int to ASCII conversion.
|
* Very fast 8-bit int to ASCII conversion.
|
||||||
|
*
|
||||||
* @param val The value of an ASCII character.
|
* @param val The value of an ASCII character.
|
||||||
* @return A string representing the ASCII character.
|
* @return A string representing the ASCII character.
|
||||||
*/
|
*/
|
||||||
@@ -123,4 +109,11 @@ public class HexUtil {
|
|||||||
public static String wordToHex(int val) {
|
public static String wordToHex(int val) {
|
||||||
return HEX_CONSTANTS[(val >> 8) & 0xff] + HEX_CONSTANTS[val & 0xff];
|
return HEX_CONSTANTS[(val >> 8) & 0xff] + HEX_CONSTANTS[val & 0xff];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given two bytes, return an address.
|
||||||
|
*/
|
||||||
|
public static int address(int lowByte, int hiByte) {
|
||||||
|
return ((hiByte << 8) | lowByte) & 0xffff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.loomcom.symon;
|
package com.loomcom.symon;
|
||||||
|
|
||||||
|
import com.loomcom.symon.util.Utils;
|
||||||
import junit.framework.*;
|
import junit.framework.*;
|
||||||
|
|
||||||
import com.loomcom.symon.devices.*;
|
import com.loomcom.symon.devices.*;
|
||||||
@@ -537,11 +538,11 @@ public class CpuTest extends TestCase {
|
|||||||
|
|
||||||
|
|
||||||
public void testAddress() {
|
public void testAddress() {
|
||||||
assertEquals(0xf1ea, cpu.address(0xea, 0xf1));
|
assertEquals(0xf1ea, Utils.address(0xea, 0xf1));
|
||||||
assertEquals(0x00ea, cpu.address(0xea, 0x00));
|
assertEquals(0x00ea, Utils.address(0xea, 0x00));
|
||||||
assertEquals(0xf100, cpu.address(0x00, 0xf1));
|
assertEquals(0xf100, Utils.address(0x00, 0xf1));
|
||||||
assertEquals(0x1234, cpu.address(0x34, 0x12));
|
assertEquals(0x1234, Utils.address(0x34, 0x12));
|
||||||
assertEquals(0xffff, cpu.address(0xff, 0xff));
|
assertEquals(0xffff, Utils.address(0xff, 0xff));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testZpxAddress() {
|
public void testZpxAddress() {
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package com.loomcom.symon;
|
|
||||||
|
|
||||||
import com.loomcom.symon.util.HexUtil;
|
|
||||||
import junit.framework.TestCase;
|
|
||||||
|
|
||||||
public class HexUtilTest extends TestCase {
|
|
||||||
public void testByteToHex() {
|
|
||||||
assertEquals("FE", HexUtil.byteToHex(0xfe));
|
|
||||||
assertEquals("00", HexUtil.byteToHex(0));
|
|
||||||
assertEquals("0A", HexUtil.byteToHex(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testByteToHexIgnoresSign() {
|
|
||||||
assertEquals("FF", HexUtil.byteToHex(-1));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testByteToHexMasksLowByte() {
|
|
||||||
assertEquals("FE", HexUtil.byteToHex(0xfffe));
|
|
||||||
assertEquals("00", HexUtil.byteToHex(0xff00));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testWordToHex() {
|
|
||||||
assertEquals("0000", HexUtil.wordToHex(0));
|
|
||||||
assertEquals("FFFF", HexUtil.wordToHex(65535));
|
|
||||||
assertEquals("FFFE", HexUtil.wordToHex(65534));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testWordToHexIgnoresSign() {
|
|
||||||
assertEquals("FFFF", HexUtil.wordToHex(-1));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testWordToHexMasksTwoLowBytes() {
|
|
||||||
assertEquals("FFFE", HexUtil.wordToHex(0xfffffe));
|
|
||||||
assertEquals("FF00", HexUtil.wordToHex(0xffff00));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testAllBytesAreCorrect() {
|
|
||||||
for (int i = 0; i <= 0xff; i++) {
|
|
||||||
assertEquals(String.format("%02X", i), HexUtil.byteToHex(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.loomcom.symon;
|
||||||
|
|
||||||
|
import com.loomcom.symon.util.Utils;
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
public class UtilsTest extends TestCase {
|
||||||
|
public void testByteToHex() {
|
||||||
|
assertEquals("FE", Utils.byteToHex(0xfe));
|
||||||
|
assertEquals("00", Utils.byteToHex(0));
|
||||||
|
assertEquals("0A", Utils.byteToHex(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testByteToHexIgnoresSign() {
|
||||||
|
assertEquals("FF", Utils.byteToHex(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testByteToHexMasksLowByte() {
|
||||||
|
assertEquals("FE", Utils.byteToHex(0xfffe));
|
||||||
|
assertEquals("00", Utils.byteToHex(0xff00));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testWordToHex() {
|
||||||
|
assertEquals("0000", Utils.wordToHex(0));
|
||||||
|
assertEquals("FFFF", Utils.wordToHex(65535));
|
||||||
|
assertEquals("FFFE", Utils.wordToHex(65534));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testWordToHexIgnoresSign() {
|
||||||
|
assertEquals("FFFF", Utils.wordToHex(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testWordToHexMasksTwoLowBytes() {
|
||||||
|
assertEquals("FFFE", Utils.wordToHex(0xfffffe));
|
||||||
|
assertEquals("FF00", Utils.wordToHex(0xffff00));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testAllBytesAreCorrect() {
|
||||||
|
for (int i = 0; i <= 0xff; i++) {
|
||||||
|
assertEquals(String.format("%02X", i), Utils.byteToHex(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user