542 lines
16 KiB
Markdown
542 lines
16 KiB
Markdown
|
|
libgtemu -- David Kolf's Library for Gigatron Emulation
|
|
=======================================================
|
|
|
|
Version 0.4.0
|
|
|
|
About
|
|
-----
|
|
|
|
libgtemu provides functions for emulating the [Gigatron TTL microcomputer][gt]
|
|
and rendering its graphics through the [SDL library][sdl].
|
|
|
|
It can be used as a lightweight stand-alone emulator as well as being
|
|
controlled by a scripting language such as [Lua][lua].
|
|
|
|
[gt]: https://gigatron.io/
|
|
[sdl]: http://www.libsdl.org/
|
|
[lua]: https://www.lua.org/
|
|
|
|
License
|
|
-------
|
|
|
|
Copyright (C) 2019 [David Heiko Kolf](http://dkolf.de/).
|
|
|
|
Published under the [BSD 2-Clause License][lic].
|
|
|
|
[lic]: https://opensource.org/licenses/BSD-2-Clause
|
|
|
|
Compiling
|
|
---------
|
|
|
|
You will probably have to modify the provided Makefile to fit to your
|
|
system, but there are only a few files and few dependencies. As an example
|
|
for compiling Lua bindings you might look at [LPEG][lpeg].
|
|
|
|
[lpeg]: http://www.inf.puc-rio.br/~roberto/lpeg/#download
|
|
|
|
### Files
|
|
|
|
* Main library: gtemu.c, gtloader.c and gtsdl.c
|
|
* Example application: gtmain.c
|
|
* Lua bindings: luagt.c
|
|
|
|
The main library could also be compiled without gtsdl.c if you want to
|
|
use a different library for graphics and sound.
|
|
|
|
### Dependencies
|
|
|
|
* [Core SDL][sdl] at least version 2.0.4.
|
|
* [Lua][lua] version 5.3 (for the Lua bindings).
|
|
|
|
Future
|
|
------
|
|
|
|
The following features might be nice to have:
|
|
|
|
* Python bindings: Python is used for most other scripts related to the
|
|
Gigatron, so it would be nice to be able to use this emulator from
|
|
Python as well.
|
|
* Proper Makefile and precompiled binaries for Windows.
|
|
* More RAM aliases in the script bindings.
|
|
* IIR audio filters to replicate the sound output of the Gigatron more
|
|
precisely. According to the [Gigatron schematics][scm] (page 8/8)
|
|
there are two single order filters in series, a low pass at 700 Hz
|
|
and a high pass at 160 Hz that overlap to reduce the peak volume by a
|
|
factor of 5.
|
|
|
|
[scm]: https://cdn.hackaday.io/files/20781889094304/Schematics.pdf
|
|
|
|
Stand-alone emulator
|
|
--------------------
|
|
|
|
The stand-alone emulator offers the following basic features on the
|
|
command line:
|
|
|
|
usage: ./gtrun [-h] [options]
|
|
|
|
Arguments:
|
|
-h Display this help.
|
|
-l filename.gt1 GT1 program to be loaded at the start.
|
|
-t filename.gtb Text file to be sent with Ctrl-F3.
|
|
-r filename.rom ROM file (default name: gigatron.rom).
|
|
-64 Expand RAM to 64k.
|
|
|
|
Special keys:
|
|
Ctrl-F2 Send designated GT1 file.
|
|
Ctrl-F3 Send designated text file.
|
|
Alt-L Perform hard reset and select loader.
|
|
Alt-X Perform hard reset and send GT1 file.
|
|
ESC Close the emulation.
|
|
|
|
|
|
Structure and C-API
|
|
-------------------
|
|
|
|
This library is split into two parts: pure emulation functions without
|
|
dependencies and SDL rendering functions. You can use the emulation
|
|
functions directly and use other means to output graphics and sound.
|
|
|
|
### Emulation and peripherals
|
|
|
|
The core emulation is written without external dependencies and just
|
|
performs the raw computations. Its functions are defined in the header
|
|
file gtemu.h.
|
|
|
|
gtemu.h defines two major structures: `GTState` and `GTPeriph`. `GTState`
|
|
containes the state of the CPU and its fields are open for inspection and
|
|
manipulation:
|
|
|
|
struct GTRomEntry {
|
|
unsigned char i;
|
|
unsigned char d;
|
|
};
|
|
|
|
struct GTState {
|
|
int pc;
|
|
unsigned char ir, d, ac, x, y, out, in;
|
|
struct GTRomEntry *rom;
|
|
size_t romcount;
|
|
unsigned char *ram;
|
|
unsigned int rammask;
|
|
};
|
|
|
|
`GTPeriph` on the other hand contains the state of the peripherals; the
|
|
board, the video output, the audio output, the GT1 loader and the serial
|
|
output. The variables are implementation details and should not be
|
|
accessed directly.
|
|
|
|
struct GTPeriph;
|
|
|
|
#### gtemu_init
|
|
|
|
extern void gtemu_init (struct GTState *gt,
|
|
struct GTRomEntry *rom, size_t romsize,
|
|
unsigned char *ram, size_t ramsize);
|
|
|
|
Initializes the contents of the `GTState` structure and sets
|
|
pointers to the given RAM and ROM arrays. You can (and should) initialize
|
|
the content of those arrays. `romsize` is the size in bytes, not the number
|
|
of entries.
|
|
|
|
The RAM size has to be at least 256 bytes.
|
|
|
|
#### gtemu_initperiph
|
|
|
|
extern void gtemu_initperiph (struct GTPeriph *ph, int audiofreq,
|
|
unsigned long randseed);
|
|
|
|
Initializes the state of the simulated peripherals.
|
|
`audiofreq` should be a valid value even if you do not want to output the
|
|
sound (just set it to 48000). Otherwise you can get it from the SDL
|
|
functions. `randseed` is a seed value for the random number generator.
|
|
|
|
#### gtemu_randomizemem
|
|
|
|
unsigned long gtemu_randomizemem (unsigned long seed,
|
|
void *mem, size_t size);
|
|
|
|
Randomizes a designated area of memory (like the RAM or the ROM). The
|
|
return value is the new state of the xorshift32-random number generator.
|
|
|
|
#### gtemu_getclock
|
|
|
|
extern unsigned long long gtemu_getclock (struct GTPeriph *ph);
|
|
|
|
Returns the total number of instructions executed so far.
|
|
|
|
#### gtemu_getxout
|
|
|
|
extern unsigned char gtemu_getxout (struct GTPeriph *ph);
|
|
|
|
Returns the current state of the XOUT register (the "blinkenlights"
|
|
and the sound output).
|
|
|
|
#### gtemu_processtick
|
|
|
|
extern int gtemu_processtick (struct GTState *gt, struct GTPeriph *ph);
|
|
|
|
Advances the emulation one tick further. This does not output any graphics
|
|
or sound but still keeps track of the position of the video beam.
|
|
|
|
It returns the value of rising edges on the synchronization signals in OUT.
|
|
|
|
#### gtemu_processscreen
|
|
|
|
extern int gtemu_processscreen (struct GTState *gt, struct GTPeriph *ph,
|
|
void *pixels, int pitch,
|
|
unsigned short *samples, size_t maxsamples, size_t *nsamples);
|
|
|
|
Advances the simulation until either the next rising edge of the VSync
|
|
signal was detected or 110000 steps were processed.
|
|
|
|
This function is called by internally by `gtsdl_render` (and
|
|
`gtsdl_runuiframe`), you only need to call it when not using one of those
|
|
functions.
|
|
|
|
It will output the pixels in a 16-bit RGB444 format, the pitch variable
|
|
determines the length of one row in bytes. The pixel and pitch arguments
|
|
are chosen to be compatible with the `SDL_LockTexture` function from
|
|
[SDL][sdl].
|
|
|
|
The sound samples are expected to be 16-bit in native byte order. This
|
|
function will output only positive 15-bit numbers to be compatible with
|
|
both unsigned and signed formats.
|
|
|
|
The pointers `pixels` and `samples` may be NULL if you are not interested
|
|
in the output.
|
|
|
|
`nsamples` points to a variable with the current position inside the samples
|
|
array and must not be NULL, even if no sound is being played. Just let
|
|
it point to a dummy variable on the stack.
|
|
|
|
The return value is the number of instructions that were executed.
|
|
|
|
#### gtemu_placelights
|
|
|
|
void gtemu_placelights (struct GTPeriph *ph, void *pixels, int pitch,
|
|
int power);
|
|
|
|
Places LEDs as pixels on the bottom of the screen. The positions are
|
|
chosen to not obscure any pixel of the emulation completely and the colors
|
|
are chosen from outside the palette available to Gigatron applications.
|
|
|
|
This function is called internally by `gtsdl_render` (and
|
|
`gtsdl_runuiframe`), you only need to use it when not using one of those
|
|
functions.
|
|
|
|
`power` indicates the state of the power LED. The default rendering function
|
|
uses it to indicate that the frame rate is at the expected 60 FPS.
|
|
|
|
#### gtserialout_setbuffer
|
|
|
|
void gtserialout_setbuffer (struct GTPeriph *ph, char *buffer,
|
|
size_t buffersize, size_t *bufferpos);
|
|
|
|
Sets the buffer into which emulated programs can write data by manipulating
|
|
the synchronization signals. `bufferpos` points to a variable containing
|
|
the current index into the buffer. You can change that variable at any
|
|
time to make place for new data.
|
|
|
|
TinyBasic on the Gigatron sends an empty newline (`\n`) when it wants to
|
|
clear the buffer and start a new output. In this emulation the empty line
|
|
is just placed into the buffer as well without having any built-in special
|
|
effects.
|
|
|
|
#### gtloader_sendgt1
|
|
|
|
extern int gtloader_sendgt1 (struct GTPeriph *ph,
|
|
const char *data, size_t datasize);
|
|
|
|
Instructs the emulation to send the contents of a GT1 file to the emulated
|
|
Gigatron that can be evaluated by the Loader program. This function does
|
|
not check whether the GT1 is valid and expects the Loader application to be
|
|
running already.
|
|
|
|
The memory pointed to by `data` must stay valid until everything is sent.
|
|
|
|
It returns 1 on success and 0 on failure when there is still previous data
|
|
that was not completely sent.
|
|
|
|
#### gtloader_isactive
|
|
|
|
extern int gtloader_isactive (struct GTPeriph *ph);
|
|
|
|
Returns whether the loader is active at the moment.
|
|
|
|
#### gtloader_validategt1
|
|
|
|
extern int gtloader_validategt1 (const char *data, size_t datasize);
|
|
|
|
Validates a GT1 file.
|
|
|
|
The return value is 1 if the contents were valid, 0 otherwise.
|
|
|
|
#### gtloader_sendtext
|
|
|
|
extern int gtloader_sendtext (struct GTPeriph *ph,
|
|
const char *data, size_t datasize);
|
|
|
|
Sends data as keystrokes to the emulated Gigatron that can be received by
|
|
TinyBASIC and other programs accepting text input.
|
|
|
|
The memory pointed to by `data` must stay valid until everything is sent.
|
|
|
|
It returns 1 on success and 0 on failure when there is still previous data
|
|
that was not completely sent.
|
|
|
|
#### gtloader_sendkey
|
|
|
|
extern int gtloader_sendkey (struct GTState *gt, struct GTPeriph *ph,
|
|
char key);
|
|
|
|
Sends a single keypress to the emulated Gigatron. When two different keys
|
|
are sent quickly after each other, the second key will directly replace
|
|
the input value without first sending a null input.
|
|
|
|
It returns 1 on success and 0 on failure when there is still previous data
|
|
that was not completely sent.
|
|
|
|
### SDL
|
|
|
|
The necessary resources for SDL and some state are kept in the GTSDLState
|
|
structure. You should access its content only through the provided
|
|
functions.
|
|
|
|
struct GTSDLState;
|
|
|
|
#### gtsdl_openwindow
|
|
|
|
extern int gtsdl_openwindow (struct GTSDLState *s, const char *title);
|
|
|
|
Initializes the necessary SDL subsystems (you should call `SDL_Init` before)
|
|
and creates a standalone window.
|
|
|
|
It returns 1 in case of success and 0 in case of errors. The error can
|
|
be requested using `SDL_GetError`.
|
|
|
|
#### gtsdl_getaudiocallback
|
|
|
|
extern SDL_AudioCallback gtsdl_getaudiocallback();
|
|
|
|
Returns a pointer to the audio callback. Can be used if you want to set
|
|
up audio on your own (see also `gtsdl_setup`).
|
|
|
|
#### gtsdl_setup
|
|
|
|
extern int gtsdl_setup (struct GTSDLState *s, SDL_Renderer *renderer,
|
|
SDL_AudioDeviceID audiodev, SDL_AudioSpec *audiospec);
|
|
|
|
This function is an alternative to `gtsdl_openwindow` in case your
|
|
application initialized SDL and created an window on its own.
|
|
|
|
The audiospec has to be 16-bit integer and you can get the callback
|
|
function through `gtsdl_getaudiocallback`.
|
|
|
|
In case you want to mix the audio you could avoid the provided SDL
|
|
functions of this library and just call `gtemu_processscreen` directly in
|
|
your own sound and video rendering function.
|
|
|
|
It returns 1 in case of success and 0 in case of errors. The error can
|
|
be requested using `SDL_GetError`.
|
|
|
|
#### gtsdl_close
|
|
|
|
extern void gtsdl_close (struct GTSDLState *s);
|
|
|
|
Closes all SDL resources that were requested by this library. In case of
|
|
the `gtsdl_openwindow` function it will free all resources and you just need
|
|
to call `SDL_Close`. In case of the `gtsdl_setup` function it will not free
|
|
resources allocated outside of the library.
|
|
|
|
#### gtsdl_getaudiofreq
|
|
|
|
extern int gtsdl_getaudiofreq (struct GTSDLState *s);
|
|
|
|
Returns the frequency of the used audio device.
|
|
|
|
#### gtsdl_render
|
|
|
|
extern SDL_Texture *gtsdl_render (struct GTSDLState *s, struct GTState *gt,
|
|
struct GTPeriph *ph);
|
|
|
|
Emulates and renders a single frame, queues the audio and returns the
|
|
finished texture.
|
|
|
|
This function should be used when the library was initialized with
|
|
`gtsdl_setup`. You need to keep control of the frame rate yourself,
|
|
otherwise the emulation might get ahead of the audio output.
|
|
|
|
#### gtsdl_runuiframe
|
|
|
|
extern int gtsdl_runuiframe (struct GTSDLState *s, struct GTState *gt,
|
|
struct GTPeriph *ph, SDL_Event *ev);
|
|
|
|
Renders one frame in the standalone window created by
|
|
`gtsdl_openwindow` and checks for `SDL_Events` using `SDL_PollEvent`.
|
|
|
|
In case there is a event this function returns immediately without any
|
|
further actions with the return value 1. The SDL_Event structure contains
|
|
the current event.
|
|
|
|
When there is no event it will emulate and render one frame and will call
|
|
`SDL_Delay` to limit the frame rate to 60 FPS. In this case the return
|
|
value is 0;
|
|
|
|
#### gtsdl_handleevent
|
|
|
|
extern int gtsdl_handleevent (struct GTSDLState *s, struct GTState *gt,
|
|
SDL_Event *ev);
|
|
|
|
Processes keyboard events and updates the IN register accordingly. It
|
|
returns 1 if it was a handled event, 0 otherwise.
|
|
|
|
Lua bindings
|
|
------------
|
|
|
|
### About Lua
|
|
|
|
[Lua][lua] is a lightweight scripting language that can be embedded into
|
|
other applications as well as being used as a standalone interpreter with
|
|
an interactive prompt.
|
|
|
|
The Lua bindings for the emulation library can be loaded using
|
|
|
|
gtemu = require "gtemu"
|
|
|
|
assuming that the DLL is in the packages search path.
|
|
|
|
### API
|
|
|
|
#### gtemu.initsdl ()
|
|
|
|
Calls `SDL_Init` and registers `SDL_Close` for `atexit`.
|
|
|
|
#### gtemu.openwindow (title)
|
|
|
|
Initializes the SDL subsystems and creates a window. A window object is
|
|
returned.
|
|
|
|
#### window:runloop (emulation, eventhandlers)
|
|
|
|
Runs an event loop for the specified emulation (see below) until either the
|
|
event `SQL_QUIT` was received, one of the eventhandlers requested a break
|
|
or the Escape key was detected.
|
|
|
|
`eventhandlers` is an optional table where the callback functions
|
|
`onkeydown (keyname, mods, repeated, scancode, keycode)`,
|
|
`onkeyup (keyname, mods, repeated, scancode, keycode)`,
|
|
`ontextinput (text)`, `ondropfile (filename)` and `onframe ()` can be
|
|
defined.
|
|
|
|
The functions can return either `true`, `false` or the string `"break"`.
|
|
When `true` is returned the event handling is finished, for `false` the
|
|
default event handling is executed, for `break` the loop is interrupted.
|
|
|
|
#### window:close ()
|
|
|
|
Closes the window and releases the SDL subsystems. This function is called
|
|
automatically when the window object is garbage collected or at the end
|
|
of the application.
|
|
|
|
#### gtemu.newemulation (window [, ramsize])
|
|
|
|
Initializes the emulation. `ramsize` can be either 32 or 64. When
|
|
omitted it defaults to 32.
|
|
|
|
This function returns an emulation object.
|
|
|
|
Example of setting up the application and starting an emulation:
|
|
|
|
gtemu = require "gtemu"
|
|
|
|
gtemu.initsdl()
|
|
window = gtemu.openwindow("Gigatron in Lua")
|
|
emulation = gtemu.newemulation(window, 64)
|
|
|
|
f = assert(io.open("gigatron.rom", "rb"))
|
|
emulation:loadrom(f:read("*a"))
|
|
f:close()
|
|
|
|
window:runloop(emulation)
|
|
|
|
#### Properties of emulation
|
|
|
|
In the emulation the following registers can be both read and modified:
|
|
|
|
pc, ir, d, ac, x, y, out, inp
|
|
|
|
For example:
|
|
|
|
if emulation.pc == 0x0123 then
|
|
emulation.inp = 0xff
|
|
end
|
|
|
|
The contents of the RAM can be accessed using the array notation:
|
|
|
|
if emulation[0x0123] == 0x10 then
|
|
emulation[0x1234] = 0x20
|
|
end
|
|
|
|
There is a special variable that maps the the vPC memory location in
|
|
ram:
|
|
|
|
if emulation.vpc >= 0x5a0c then
|
|
-- An application (probably the Loader) is running inside
|
|
-- screen memory
|
|
end
|
|
|
|
emulation.vpc = 0x200
|
|
|
|
This is equivalent to manual access to the RAM locations 0x16 and 0x17.
|
|
Further aliases might be defined in future versions.
|
|
|
|
The following read-only values are also available:
|
|
|
|
clock, xout
|
|
|
|
#### emulation:loadrom (data)
|
|
|
|
Loads a string into ROM:
|
|
|
|
f = assert(io.open("gigatron.rom", "rb"))
|
|
emulation:loadrom(f:read("*a"))
|
|
f:close()
|
|
|
|
#### emulation:processtick ()
|
|
|
|
Advances the simulation for a single tick without any output.
|
|
|
|
#### emulation:processscreen ()
|
|
|
|
Advances the simulation for an entire screen without any output.
|
|
|
|
#### emulation:sendgt1 (data)
|
|
|
|
Sends data from a GT1 file to the Loader application (assuming it is
|
|
running):
|
|
|
|
f = assert(io.open("Overworld.gt1", "rb"))
|
|
gt1 = f:read("*a")
|
|
f:close()
|
|
|
|
if emulation.vpc >= 0x5a0c then
|
|
emulation:sendgt1(gt1)
|
|
end
|
|
|
|
The data is automatically verified before sending, an error will be raised
|
|
for invalid data.
|
|
|
|
#### emulation:createbuffer (size)
|
|
|
|
Creates a buffer the Gigatron can send output to.
|
|
|
|
#### emulation:getbuffer ()
|
|
|
|
Requests the current contents of the buffer.
|
|
|
|
#### emulation:resetbuffer ()
|
|
|
|
Clears the buffer.
|
|
|