mirror of
https://github.com/makarcz/vm6502.git
synced 2025-01-14 12:33:39 +00:00
b730b0cb1a
Correct typo in Programmers Reference Manual.
881 lines
35 KiB
Plaintext
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();
|
|
}
|
|
}
|
|
|