vm6502/ProgrammersReferenceManual.txt

881 lines
35 KiB
Plaintext

User Manual and Programmers Reference Manual for VM65.
by
Marek Karcz (C) 2016.
1. Introduction.
VM65 is a simulator which can be expanded to become an emulator of more
complex computer system either a real one or completely abstract virtual
machine.
VM65 acronym stands for Virtual Machine 65 and is designed for MOS-6502
simulation. In current form it allows to simulate a MOS-6502 CPU in
MS Windows environment using MS-DOS command line UI.
Software emulates all original MOS-6502 legal opcodes. The illegal opcodes
are not emulated and are opened for expansion. It is up to programmer to
implement illegal opcodes accurate emulation or replace them with extensions
or traps. In current version they do nothing except to consume CPU cycles.
The 6502 emulation is not purely abstract. The basic components of a real
hardware CPU system are emulated: microprocessor, memory and memory mapped
devices abstract layer - about which more in section 4.
Using this software user will be able to load plain text memory definition
file, Intel HEX format file, raw binary image file or binary snapshot image
created with this software and execute or debug code within it in
a continuous or in step-by-step mode.
The file will be loaded and translated to the emulated memory space and
optional emulation parameters included in the image will be interpreted and
adequately translated into values of the corresponding internal flags.
It is possible to automatically run program starting at specified address,
perform automatic reset of the CPU after memory image upload or perform
these actions from command line prompt of the debug console.
2. Command Line Interface and Debug Console.
Usage:
vm65 [-h] | [ramdeffile] [-b | -x] [-r]
Where:
ramdeffile - RAM definition file name
-b - specify input format as binary
-x - specify input format as Intel HEX
-r - after loading, perform CPU RESET
-h - print this help screen
When ran with no arguments, program will load default memory
definition files: default.rom, default.ram and will enter the debug
console menu.
When ramdeffile argument is provided with no input format specified,
program will attempt to automatically detect input format and load the
memory definition from the file, set the flags and parameters depending
on the contents of the memory definition file and enter the corresponding
mode of operation as defined in that file.
If input format is specified (-b|-x), program will load memory from the
provided image file and enter the debug console menu.
To start program, navigate to the program directory in MS-DOS prompt console
and type:
vm65
Above will run the emulator. Since no memory image is specified, program
will attempt to load dummy.rom and dummy.ram files, which supposed to be
plain text memory image definition files. The simplest you can create can
look like this:
ORG
$0000
$00
NOTE: To see the full usage help for vm65, use flag -h:
vm65 -h
Program loaded with no arguments will present user with following UI
which will be referred to as Debug Console:
STOPPED at 0
Emulation performance stats is OFF.
*-------------*-----------------------*----------*----------*
| PC: $0000 | Acc: $00 (00000000) | X: $00 | Y: $00 |
*-------------*-----------------------*----------*----------*
| NV-BDIZC | :
| 00100000 | :
*-------------*
Stack: $ff
[]
I/O status: disabled, at: $e000, local echo: OFF.
Graphics status: disabled, at: $e002
ROM: disabled. Range: $d000 - $dfff.
Op-code execute history: disabled.
------------------------------------+----------------------------------------
C - continue, S - step | A - set address for next step
G - go/cont. from new address | N - go number of steps, P - IRQ
I - toggle char I/O emulation | X - execute from new address
T - show I/O console | B - blank (clear) screen
E - toggle I/O local echo | F - toggle registers animation
J - set animation delay | M - dump memory, W - write memory
K - toggle ROM emulation | R - show registers, Y - snapshot
L - load memory image | O - display op-code exec. history
D - disassemble code in memory | Q - quit, 0 - reset, H - help
V - toggle graphics emulation | U - enable/disable exec. history
Z - enable/disable debug traces | 1 - enable/disable perf. stats
2 - display debug traces | ? - show this menu
------------------------------------+----------------------------------------
>
Upper part of the screen consists of current CPU registers status.
If performance statistics are enabled, they are right above the CPU
registers.
PC shows address of currently executed op-code, while to the right of status
flags below PC is a disassembled (now empty, showing just the ':' signs)
instructions section. It shows 2 lines, 1-st line is the previously executed
instruction while the 2-nd line shows current instruction. Instructions are
shown in binary (hex) and symbolic (disassembled mnemonics, hex arguments)
format.
Accumulator and index registers X and Y values are also shown in the CPU
registers section.
Stack pointer and its contents are displayed below the CPU registers.
The contents of the Processor registers, disassembled executed instructions
and stack pointer and contents are updated dymanically during the multi-step
program debugging.
Below the stack data we have current status of various flags and parameters
that don't belong to emulated CPU.
Next goes the full list of available commands with short descriptions.
The menu of commands is not always displayed. It is usually skipped when
there were no screen actions that could potentially obstruct that menu.
The menu of commands can be always recalled to the screen by issuing
command '?' after the command prompt '>' and pressing ENTER.
Each of the commands in Debug Console is interactive and can be used in two
ways - user can issue command in the prompt, press enter and follow the
instructions/prompts to enter remaining parameters OR if user already knows
the format of the command, all parameters can be entered in a single line.
To see full help for above commands, issue command H in the prompt and press
ENTER.
A quick overview of some of the commands available in Debug Console:
2.1. Code execution and debugging aid commands.
C - Continue
Execute code from current address after it was interrupted.
S - Step
Executes single op-code at current address. The address for the next
step can be changed with command 'A'.
A - Set address for next step
Sets current address to one provided as argument (or prompts for one).
G - Execute (continue) from address.
Asks for (if not provided as argument) address and then executes code.
N - Execute # of steps.
Useful when we want to skip ahead (e.g.: in a loop) and execute multiple
op-codes before next stop. Look also at commands 'F' and 'J' for
features and parameters associated with it.
X - Execute from address.
F - Toggle registers animation.
When in multi-step debug mode (command: N), displaying registers
can be suppressed or, when animation mode is enabled - they will
be continuously displayed after each executed step.
J - Set registers animation delay.
Sets the time added at the end of each execution step in multi
step mode (command: N). The default value is 250 ms. The change
of this parameter will basically set the speed of multi-step code
execution. Greater delay will slow down the execution but improve
readability of the registers in Debug Console as they will not change
as often as if this delay was smaller.
R - Show registers.
Displays current status of CPU registers, flags and stack.
O - Display op-codes execute history.
Show the history of last executed op-codes/instructions, full with
disassembled mnemonic and argument.
D - Disassemble code in memory.
Attempt to disassemble code in specified address range and display
the results (print) on the screen in symbolic form.
0 - Reset.
Run the processor initialization sequence, just like the real CPU
when its RTS signal is set to LOW and HIGH again. CPU will disable
interrupts, copy address from vector $FFFC to processors PC and will
start executing code. Programmer must put initialization routine
under address pointed by $FFFC vector, which will set the arithmetic
mode, initialize stack, I/O devices and enable IRQ if needed before
jumping to main loop. The reset routine disables trapping last RTS
opcode if stack is empty, so the VM will never return from opcodes
execution loop unless user interrupts with CTRL-C or CTRL-Break.
Z - Toggle enable/disable debug traces.
Emulator may produce debug messages that can be helpful when
troubleshooting issues with 6502 code. Since the immediate output
to the same console where emulated program is connected is in many
cases undesired, the debug messages are remembered in an internal
log. The log holds last 200 messages. If enabled, this log can be
recalled with command '2'. When the log is disabled and then enabled
again, the old log messages will be deleted.
2 - Display debug traces.
Displays debug traces log (200 maximum) in 20 messages long pages.
U - Toggle enable/disable op-codes execute history.
See also option 'O'.
This debuggin aid is not needed during normal emulator run, therefore
option to disable this feature was added, mainly because it affects
performance. But in debugging mode, when we often trace code step by
step, the performance compromise may be justified as a trade off for
getting more internal data from the system.
1 - Toggle enable/disable emulation performance statistics.
MKCpu class includes code to measure the emulation speed.
The speed is measured as a % of model 1 MHz MOS-6502. Number of executed
cycles is divided by number of microseconds that passed and multiplied
by 100 to get this figure.
Performance is only measured in the execute/run modes.
It doesn't make much sense in debug/step by step modes, therefore even
when the feature is enabled, the stats are not calculated in these modes
of operation.
Emulation performance feature is useful mainly during the development
cycle of the emulator and since this is a programming framework for
buiding your own virtual machines/CPU emulators, then it is included.
However as with any debugging aid, it affects performance, therefore
option was added to disable it when we just want to run the 6502 code
and enjoy it at maximum possible speed.
2.2. Memory access commands.
M - Dump memory.
Dumps contents of memory, hexadecimal and ASCII formats.
W - Write memory.
Writes provided values to memory starting at specified address.
2.3. I/O devices commands.
I - Toggle char I/O emulation.
Enables/disables basic character I/O emulation. When enabled, all writes
to the specified memory address also writes a character code to
to a virtual console. All reads from specified memory address
are interpreted as console character input.
The base address corresponds to blocking mode character I/O device,
while base address + 1 corresponds to non-blocking character I/O device.
The blocking device waits for the input character - code execution
stops until user enters the keystroke, while in non-blocking mode
the emulator takes the recently entered keystroke from the system
keyboard buffer. That means that implementing the character input
in 6502 assembly/machine code requires a loop reading from non-blocking
address until the character code is different than 0, while in blocking
mode it is enough to just read from I/O address. The emulator will only
proceed when the key was pressed by user.
Examples of 6502 code:
- a 'waiting' character input implemented using non-blocking char I/O
readchar: LDA $E001
BEQ readchar ; A = NULL, keep reading char I/O
RTS ; A = char code
- a 'waiting' character input implemented using blocking char I/O
readchar: LDA $E000
RTS ; A = char code
T - Show I/O console.
Displays/prints the contents of the virtual console screen.
Note that in run mode (commands X, G or C), virtual screen is
displayed automatically in real-time if I/O emulation is enabled.
V - Toggle graphics emulation.
Enables/disables basic raster (pixel) based RGB graphics display
emulation.
When enabled, window with graphics screen will open and several
registers are available to control the device starting at provided
base address. Detailed description of the device registers os provided
later in this document.
The difference between 'G' and 'X' is that code executed with command 'G'
will return to Debug Console when BRK or last RTS (empty stack) opcode is
encountered, while code executed with 'X' goes to full CPU emulation mode
where BRK will invoke proper interrupt routine as setup by IRQ vector in
memory. The last RTS opcode (empty stack) will return to Debug
Console in both cases unless such behavior is disabled (and this is only
true in case of Reset function, see command '0').
Entering keyboard interrupt codes (CTRL-C, CTRL-BREAK) will return to Debug
Console.
NOTE: All addresses and memory values are to be entered in hexadecimal
format.
2.4. Typical debug session.
Your typical debug session, after starting VM65 is:
* Load the memory image with command 'L' (if not already loaded from
command line). E.g.:
L A 6502_func_test.bin
* Set the program start address with command 'A' (if not already set
in the memory image definition file). E.g.:
A 0400
* Enable/disable various debug and I/O facilities as required by the
loaded 6502 code. E.g.: to enable dymanic processor registers updates
and show the executed code dynamically while executing multiple steps
with command 'N', you need to issue command 'F' to enable registers
animation and optionally 'J' to change the speed (delay) of the
step-by-step animation:
F
J 100
N 1000
OR if command 'S' is used to manually step through 6502 instructions,
the animation of the CPU registers doesn't have top be enabled.
The registers values and disassembled instructions will be refreshed
after each step on the screen.
Each step is considered a single instructions (not a clock cycle).
If user wants to skip several (thousands perhaps) instructions, then
registers animation (command 'F') should be disabled, which will
make the steps to proceed much quicker. E.g.: assuming that currently
the animation is enabled, and user wants to skip 5000 instructions
quickly without looking at the changing registers and disassembled
instructions, user would issue commands:
F
N 5000
At any moment during multi-step execution (command 'N'), user can
interrupt before all the steps are concluded with CTRL-C and then
lool-up/alter memory content (commands 'M', 'W'), change the address
of the next executed instruction (command 'A') etc. and then continue
debugging with 'S', 'N', 'X', 'C', '0' or 'P' commands.
User can enable the history of executed op-codes with command 'U' and
display the history of the last 20 op-codes and CPU registers values
in the last 20 steps. E.g.:
L A 6502_func_test.bin
A 0400
U
N 5
O
Output from 'O':
> o
PC : INSTR ACC | X | Y | PS | SP
------------------------------------+-----+-----+-----+-----
$0400: CLD $00 | $00 | $00 | $20 | $ff
$0401: LDX #$FF $00 | $ff | $00 | $a0 | $ff
$0403: TXS $00 | $ff | $00 | $a0 | $ff
$0404: LDA #$00 $00 | $ff | $00 | $22 | $ff
$0406: STA $0200 $00 | $ff | $00 | $22 | $ff
Type '?' and press [ENTER] for Menu ...
3. Implementing the Virtual Machine.
The Virtual Machine (or Emulator) is implemented by the means of multiple
layers of abstraction. The higher the layer, the less code it contains
that is virtual hardware dependent.
On the highest level we have main.cpp which implements the UI and Debug
Console. Programmer can replace this code with custom designed UI or GUI.
The definition of the emulated system layer begins in VMachine class
(VMachine.h and VMachine.cpp) which provides the implementation of the
entire emulated system. It is a template upon which programmer can expand.
Several important methods are defined here that allow to execute the 6502
code in many different modes:
Regs *Run();
Regs *Run(unsigned short addr);
Regs *Exec();
Regs *Exec(unsigned short addr);
Regs *Step();
Regs *Step(unsigned short addr);
void Reset();
Programmer should use this class to implement all the pieces of the
emulated virtual computer system on the highest abstraction level.
In current version the VMachine class initializes the basic system, which
contains following core components: CPU and memory
and devices: character I/O and raster graphics display.
Going one step down the abstraction layer are MKCPu and Memory classes.
The MKCpu class is the most important part of the emulator. It defines
the whole system's architecture. Based on the type of the CPU device we
know how to implement the rest of the core components by knowing the
maximum addressable memory space, size of the data bus (8-bit, 16-bit) etc.
The MKCpu class defines all the internal registers of the virtual CPU,
the op-codes interpreter and its interface to outside components.
The most important API methods are:
Regs *ExecOpcode(unsigned short memaddr);
Regs *GetRegs();
void SetRegs(Regs r);
void Reset(); // reset CPU
void Interrupt(); // Interrupt ReQuest (IRQ)
The Memory class (Memory.h and Memory.cpp) implements essential concept
in any microprocessor based system, which is the memory. The assumption is
that such a system requires some sort of program storage to be able to
execute code. This is not entirely true in the real world, where we can
make the CPU 'think' that it executes the real code with a tricky circuit
and get away without any memory to make the CPU run. This however has
limited usefulness (diagnostics, testing the basic operation of CPU) and
has no use in the emulation domain where we have no need to test the CPU
chip for basic operation on electrical level. The Memory class is also an
important entry level for another concept of microprocessor based system:
a memory mapped device. This brings us to the lower level of abstraction
which is the memory mapped device class: MemMapDev.
Class MemMapDev is a core or hub for implementing all the devices that are
connected to the virtual CPU via its memory address space. This is
explained in more detail in chapter 4.
All remaining classes are the implementation of the actual devices on the
lowest level of abstraction (from the emulator point of view) and other
helper classes and methods.
Examples of virtual devices emulation implementation are: Display and
GraphDisp.
Programmer can add more code and more classes and integrate them into the
emulator using this architecture. This is the starting point of the
emulator of a real microprocessor based system or completely abstract
virtual machine that has no hardware counterpart.
4. Memory Mapped Device Abstraction Layer/API.
In microprocessor based systems in majority of cases communication with
peripheral devices is done via registers which in turn are located under
specific memory addresses.
Programming API responsible for modeling this functionality is implemented
in Memory and MemMapDev classes. The Memory class implements access to
specific memory locations and maintains the memory image.
The MemMapDev class implements specific device address spaces and handling
methods that are triggered when addresses of the device are accessed by the
microprocessor.
Programmers can expand the functionality of this emulator by adding
necessary code emulating specific devices in MemMapDev and Memory classes
implementation and header files. In current version, two basic devices are
implemented:
- character I/O and
- raster (pixel based) graphics display.
Character I/O device uses 2 memory locations, one for non-blocking I/O
and one for blocking I/O. Writing to location causes character output, while
reading from location waits for character input (blocking mode) or reads the
character from keyboard buffer if available (non-blocking mode).
The graphics display can be accessed by writing to multiple memory locations.
If we assume that GRDEVBASE is the base address of the Graphics Device, there
are following registers:
Offset Register Description
----------------------------------------------------------------------------
0 GRAPHDEVREG_X_LO Least significant part of pixel's X (column)
coordinate or begin of line coord. (0-255)
1 GRAPHDEVREG_X_HI Most significant part of pixel's X (column)
coordinate or begin of line coord. (0-1)
2 GRAPHDEVREG_Y Pixel's Y (row) coordinate (0-199)
3 GRAPHDEVREG_PXCOL_R Pixel's RGB color component - Red (0-255)
4 GRAPHDEVREG_PXCOL_G Pixel's RGB color component - Green (0-255)
5 GRAPHDEVREG_PXCOL_B Pixel's RGB color component - Blue (0-255)
6 GRAPHDEVREG_BGCOL_R Backgr. RGB color component - Red (0-255)
7 GRAPHDEVREG_BGCOL_G Backgr. RGB color component - Green (0-255)
8 GRAPHDEVREG_BGCOL_B Backgr. RGB color component - Blue (0-255)
9 GRAPHDEVREG_CMD Command code
10 GRAPHDEVREG_X2_LO Least significant part of end of line's X
coordinate
11 GRAPHDEVREG_X2_HI Most significant part of end of line's X
coordinate
12 GRAPHDEVREG_Y2 End of line's Y (row) coordinate (0-199)
13 GRAPHDEVREG_CHRTBL Set the 2 kB bank where char. table resides
14 GRAPHDEVREG_TXTCURX Set text cursor position (column)
15 GRAPHDEVREG_TXTCURY Set text cursor position (row)
16 GRAPHDEVREG_PUTC Output char. to current pos. and move cursor
17 GRAPHDEVREG_CRSMODE Set cursor mode : 0 - not visible, 1 - block
18 GRAPHDEVREG_TXTMODE Set text mode : 0 - normal, 1 - reverse
NOTE: Functionality maintaining text cursor is not yet implemented.
Writing values to above memory locations when Graphics Device is enabled
allows to set the corresponding parameters of the device, while writing to
command register executes corresponding command (performs action) per codes
listed below:
Command code Command description
----------------------------------------------------------------------------
GRAPHDEVCMD_CLRSCR = 0 Clear screen
GRAPHDEVCMD_SETPXL = 1 Set the pixel location to pixel color
GRAPHDEVCMD_CLRPXL = 2 Clear the pixel location (set to bg color)
GRAPHDEVCMD_SETBGC = 3 Set the background color
GRAPHDEVCMD_SETFGC = 4 Set the foreground (pixel) color
GRAPHDEVCMD_DRAWLN = 5 Draw line
GRAPHDEVCMD_ERASLN = 6 Erase line
Reading from registers has no effect (returns 0).
Above method of interfacing GD requires no dedicated graphics memory space
in VM's RAM. It is also simple to implement.
The downside - slow performance (multiple memory writes to select/unselect
a pixel or set color).
I plan to add graphics frame buffer in the VM's RAM address space in future
release.
Simple demo program written in EhBasic that shows how to drive the graphics
screen:
1 REM GRAPHICS DISPLAY DEVICE DEMO
2 REM BASE ADDRESS $FFE2
3 REM DRAW HORIZONTAL AND VERTICAL LINES
4 REM DRAW SINUSOID
10 GB=65506:REM SET BASE ADDRESS
12 REM INITIALIZE, SET COLORS
15 POKE GB+1,0:POKE GB+11,0
16 POKE GB+3,0:POKE GB+4,255:POKE GB+5,0
17 POKE GB+6,0:POKE GB+7,0:POKE GB+8,0
18 POKE GB+9,3:POKE GB+9,4:POKE GB+9,0
19 GOSUB 1120:REM DRAW SINUSOID
20 Y=100:REM X-AXIS
30 GOSUB 1000
50 X=100:REM Y-AXIS
60 GOSUB 1060
70 REM SOME EXTRA DOTTED LINES
80 Y=50:GOSUB 1200
90 Y=150:GOSUB 1200
100 X=50:GOSUB 1260
110 X=150:GOSUB 1260
990 PRINT "DONE"
998 END
999 REM ------- SUBROUTINES SECTION -------
1000 REM DRAW HORIZONTAL LINE AT Y
1005 POKE GB+2,Y
1006 POKE GB+12,Y
1020 POKE GB,0
1025 POKE GB+10,199:POKE GB+9,5
1050 RETURN
1060 REM DRAW VERTICAL LINE AT X
1070 POKE GB,X
1075 POKE GB+10,X
1090 POKE GB+2,0
1095 POKE GB+12,199:POKE GB+9,5
1110 RETURN
1120 REM SINUSOID
1130 FOR X=0 TO 199-4 STEP 5
1140 XX=X*(6.28/200)
1145 XE=(X+5)*(6.28/200)
1150 YY=SIN(XX):YE=SIN(XE)
1160 Y=199-INT((YY+1)*100)
1165 Y2=199-INT((YE+1)*100)
1170 POKE GB,X:POKE GB+2,Y
1175 POKE GB+10,X+5:POKE GB+12,Y2:POKE GB+9,5
1180 NEXT X
1190 RETURN
1200 REM DRAW DOTTED HORIZONTAL LINE AT Y
1205 POKE GB+2,Y
1210 FOR X=0 TO 199 STEP 4
1220 POKE GB,X
1230 POKE GB+9,1
1240 NEXT X
1250 RETURN
1260 REM DRAW DOTTED VERTICAL LINE AT X
1270 POKE GB,X
1280 FOR Y=0 TO 199 STEP 4
1290 POKE GB+2,Y
1300 POKE GB+9,1
1310 NEXT Y
1320 RETURN
And another one to show how to render characters.
Before entering program below, load files c64_char.dat and ehbas_xx.dat to the
emulator. The two 2 kB C64 character banks reside at $B000.
The EhBasic version in file ehbas_xx.dat has top of RAM capped at $AFFF.
5 PRINT:PRINT "BITMAP TEXT DEMO. PRESS [SPACE] TO QUIT...":PRINT
10 C=0:M=0:N=22:B=65506:POKE B+9,0
12 PRINT "NORMAL MODE, CHAR BANK ";N*2048
15 POKE B+13,N:POKE B+17,0:POKE B+18,0
20 FOR Y=0 TO 24
30 FOR X=0 TO 39
40 POKE B+14,X:POKE B+15,Y
50 POKE B+16,C
60 C=C+1:IF C<256 THEN 120
70 IF N=22 THEN N=23:GOTO 100
80 N=22:IF M=0 THEN M=1:GOTO 100
90 M=0
100 POKE B+13,N:POKE B+18,M
110 Y=Y+1:X=-1:C=0
115 IF M=0 THEN PRINT "NORMAL"; ELSE PRINT "REVERSE";
116 PRINT " MODE, CHAR BANK ";N*2048
120 GET K$:IF K$=" " THEN END
130 NEXT X
140 NEXT Y
150 GOTO 5
4.1. Adding new device implementation.
MemMapDev.h and MemMapDev.cpp define the higher abstraction layer for memory
mapped devices. Memory address range class: AddrRange and device class:
Device are implemented to provide core facilities and class: MemMapDev for
future expansion of the emulated system.
To add a new device to the memory mapped devices, programmer must:
* Implement the device behavior in a separate class.
See GraphDisp.h, GraphDisp.cpp as example.
This is the actual device driver, code that defines how the device works.
Device driver class may define the raster graphics display functions,
a real time clock chip functions, sound chip functions, Disk Drive
functions etc.
This code does not contain definitions related to how the device is mapped
to the memory space of emulated computer system. That would be the
responsibility of MemMapDev class, providing the middle layer of
abstraction for connecting the device to the memory space of the CPU.
* Add necessary definitions, enumerators and methods to the MemMapDev.h
and MemMapDev.cpp.
The minimal set should include methods to initialize the device and
methods executed when memory mapped device registers are read from and
written to.
Other methods may be needed to perform device implementation specific
functions.
See example set of methods for simple raster graphics device:
unsigned short GetGraphDispAddrBase();
void ActivateGraphDisp();
void DeactivateGraphDisp();
int GraphDispDevice_Read(int addr);
void GraphDispDevice_Write(int addr, int val);
void GraphDisp_ReadEvents();
void GraphDisp_Update();
Important concept at the core of the memory mapped device is the method
handling the action to be performed when certain memory register is being
read from or written to. These are defined as function pointers in Device
class: read_fun_ptr and write_fun_ptr.
struct Device {
int num; // device number
string name; // device name
MemAddrRanges addr_ranges; // vector of memory address ranges for
// this device
ReadFunPtr read_fun_ptr; // pointer to memory register read
// function
WriteFunPtr write_fun_ptr; // pointer to memory register write
// function
DevParams params; // list of device parameters
[...]
Programmer implements these handler methods inside MemMapDev class.
Programmer should not forget to add the new device to the pool in the
MemMapDev::Initialize() method which adds all implemented devices to the
devices list with their corresponding default memory bases/ranges and
parameters. These devices can be later re-defined with
MemMapDev::SetupDevice() method.
typedef vector<Device> MemMappedDevices;
[...]
MemMappedDevices mDevices;
[...]
void MemMapDev::Initialize()
{
[...]
Device dev_grdisp(DEVNUM_GRDISP,
"Graphics Display",
addr_ranges_grdisp,
&MemMapDev::GraphDispDevice_Read,
&MemMapDev::GraphDispDevice_Write,
dev_params_grdisp);
mDevices.push_back(dev_grdisp);
[...]
* Add necessary ports in Memory.h and Memory.cpp.
Memory class already implements the code handling memory mapped devices
in all read/write memory methods. Some local methods and flags specific
to the devices may be needed for implementation convenience.
Important relevant methods in Memory class:
int AddDevice(int devnum);
int DeleteDevice(int devnum);
void SetupDevice(int devnum, MemAddrRanges memranges, DevParams params);
are the API to be called from higher level abstraction layer. Some proxy
methods can be implemented in Memory class for this purpose.
See Memory::SetGraphDisp() and VMachine::SetGraphDisp() methods for
an example. The SetGraphDisp() method in Memory class defines the memory
mapped device on a lower level and calls SetupDevice().
5. Debug traces.
VMachine class implements debug messages queue which can be used during
development cycle.
void EnableDebugTrace();
void DisableDebugTrace();
bool IsDebugTraceActive();
queue<string> GetDebugTraces();
void AddDebugTrace(string msg);
This is a FIFO queue and it is maintained to not exceed the length as
defined:
#define DBG_TRACE_SIZE 200 // maximum size of debug messages queue
Therefore the next message added to queue that overfills the queue causes
the earliest added message to be removed.
Shortly put - the last 200 added messages only are remembered internally
in VMachine object and can be recalled with GetDebugTraces() method like
shown in this example:
VMachine *pvm;
[...]
queue<string> dbgtrc(pvm->GetDebugTraces());
while (dbgtrc.size()) {
cout << dbgtrc.front() << endl;
dbgtrc.pop();
}
Good example of AddDebugTrace() use is EnableROM() method:
void VMachine::EnableROM(unsigned short start, unsigned short end)
{
mpRAM->EnableROM(start, end);
if (mDebugTraceActive) {
stringstream sssa, ssea;
string msg, startaddr, endaddr;
sssa << start;
sssa >> startaddr;
ssea << end;
ssea >> endaddr;
msg = "ROM ENABLED, Start: " + startaddr + ", End: " + endaddr + ".";
AddDebugTrace(msg);
}
}
NOTE: The AddDebugTrace method checks mDebugTraceActive flag internally,
however for performance's sake we also check it in the higher level
code especially if there are intermediate objects to be created
to produce the debug string. This way we don't execute code related
to creation of debug message unnecessarily if the debugging messages
are disabled.
5. Linux port.
I don't pay as much attention to Linux version of this software, however
I try my best to make the program work correctly on Linux and behave in the
same manner on both Windows and Linux platforms.
The most challenging part surprisingly was implementation of the character
I/O emulation on Linux. Easy on Windows with its conio.h header and kbhit()
and getch() functions that provide non-blocking keyboard input, Linux part
required some research and implementing some known programming tricks to get
this to work. I got it working but then lost touch with Linux version for
a while. After I added graphics device and SDL2 library, I worked on Linux
port to have these features working on Linux platform as well. To my surprise
I discovered then that my implementation of character I/O (the non-blocking
character input) stopped working. After many failed attempts and research
I decided to rewrite the Linux character I/O part using ncurses library.
This works although has its own problems. E.g.: ncurses getch() returns NL
character (0x0A) when ENTER is pressed, while Windows conio equivalent returns
CR (0x0D) and this is the code that 6502 programs, like Tiny Basic or EhBasic
expect. Backspace also wasn't working. I enabled function keys during ncurses
initialization and intercept backspace code for later conversion. This
solved it, but there may be more issues discovered later which I didn't fully
test.
unsigned char MemMapDev::ReadCharKb(bool nonblock)
{
[...]
#if defined(LINUX)
if (c == 3) { // capture CTRL-C in CONIO mode
mpConsoleIO->CloseCursesScr();
kill(getpid(),SIGINT);
} else if (c == 0x0A) {
c = 0x0D; // without this conversion EhBasic won't work
} else if (c == KEY_BACKSPACE) {
c = 8; // raw/cbreak modes do not pass the backspace
}
#endif
[...]
Another issue was that when 6502 code sends CR/LF sequence, the CR code
moves the cursor to the beginning of the line before applying NL (new line)
and clears the characters on its path! Therefore I'd have the text removed
from the screen (although my internal display device emulation in Display
class has its own buffer and all characters were there, it was only a visual
side effect). Therefore I have to convert CR/LF sequences to LF/CR.
Also because of the way I output characters to nsurses screen, control
characters don't work and must be intercepted and properly converted to
appropriate action. Example - the bell (audible) code 7:
void MemMapDev::PutCharIO(char c)
{
mCharIOBufOut[mOutBufDataEnd] = c;
mOutBufDataEnd++;
if (mOutBufDataEnd >= CHARIO_BUF_SIZE) mOutBufDataEnd = 0;
if (mCharIOActive) {
#if defined(LINUX)
// because ncurses will remove characters if sequence is
// CR,NL, I convert CR,NL to NL,CR (NL=0x0A, CR=0x0D)
static char prevc = 0;
if (c == 7) mpConsoleIO->Beep();
else
if (c == 0x0D && prevc != 0x0A) { prevc = c; c = 0x0A; }
else if (c == 0x0A && prevc == 0x0D) {
prevc = c; c = 0x0D;
mpConsoleIO->PrintChar(prevc);
mpConsoleIO->PrintChar(c);
}
else { prevc = c; mpConsoleIO->PrintChar(c); }
#else
mpConsoleIO->PrintChar(c);
#endif
CharIOFlush();
}
}