a recipe for the ZX Spectrum (#105)

* Add files via upload

* a monolithic build recipe for ZX Spectrum

* emulation and emulated tapes in README.md
This commit is contained in:
devisn 2020-06-11 20:34:34 +03:00 committed by GitHub
parent 78b9f89950
commit 0576d2dfa1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1500 additions and 0 deletions

View File

@ -0,0 +1,30 @@
; *** JUMP TABLE ***
; include the addresses of the actual table into user.h for userspace utilities
jp strncmp
jp upcase
jp findchar
jp parseHex
jp parseDecimal
jp blkSel
jp blkSet
jp fsFindFN
jp fsOpen
jp fsGetB
jp fsPutB
jp fsSetSize
jp fsOn
jp fsIter
jp fsAlloc
jp fsHandle
jp fsblkTell
jp printstr
jp printnstr
jp printcrlf
jp stdioPutC
jp stdioGetC
jp stdioReadLine
jp _blkGetB
jp _blkPutB
jp _blkSeek
jp _blkTell

15
kernel/zxspectrum/kbd.asm Normal file
View File

@ -0,0 +1,15 @@
; the ZX Spectrum BASIC firmware scans the keyboard for ASCII codes on clock interrupts
; this routine just waits for a key and reads its value
k_getc:
;ei
push hl
ld hl, 23611 ; ZXS_FLAGS
res 5, (hl)
.loop:
bit 5, (hl) ; pressed?
jr z, .loop
ld hl, 23560 ; ZXS_LASTK
ld a, (hl)
pop hl
ret

View File

@ -0,0 +1,222 @@
; tape blkdev read-only
; to be included as a kernel module.
; In the glue.asm devices list the PutB pointer has to be unsetZ
; defines:
; tap_buffer = 256-byte tape loading buffer in RAM
; buf_pos = read position in the buffer
; crossing the buffer boundaries require loading or rewinding+loading
; tap_pos = previous read position of the block device,
; then the difference btw current and previous positions, both in EDLH format as throughout the kernel code
tapeblk_init:
; initialized CFS and a placeholder 1-block, 1-byte file in the buffer
; the tape fs is not default, has to be mounted
ld hl, .plh
ld de, tap_buffer
ld bc, 6
ldir
ret
.plh:
.db "CFS",1,1,0,'@'
tapeGetB:
; it gets the new position in DE/HL, has to return value in A
push bc
push ix
; First of all, is the difference between positions negative or positive?
push hl ; store the position
push de
ld ix, tap_pos ; previous
push de ; working copy, higher bytes
ld e, (ix+2) ; lower bytes of previous position
ld d, (ix+3)
scf
ccf
sbc hl,de
ld (ix+6), l
ld (ix+7), h
pop hl
ld e, (ix+0) ; higher bytes
ld d, (ix+1)
sbc hl,de
ld (ix+4), l
ld (ix+5), h
jp nc, .tblk_posdif
.tblk_negdif:
; at this point we have the negative difference
pop de ; restore the current position
pop hl
; store it as 'the previous'
ld (ix+0), e
ld (ix+1), d
ld (ix+2), l
ld (ix+3), h
; let's set the buffer position while we're here
ld a, (buf_pos)
add a, (ix+6) ; l
; the difference bytes are negative, so e.g. add 255 = sub 1
ld (buf_pos), a
; no carry would mean underflow in this case
jp nc, .tblk_rewind
; we now have a chance that the higher bytes are FF (due to lower CY)
xor a
dec a
and (ix+7)
and (ix+4)
and (ix+5)
cp 255
jp z, .tblk_readbyte ; a negative difference within the buffer
; we have to rewind the tape and load back to the current position,
; so it's safe to discard the difference and treat the current position as the positive difference
.tblk_rewind:
; as we will rewind to zero, at least one additional block is to be loaded
xor a
inc h
cp h
jr nz, .tblk_store
inc de
.tblk_store:
ld (ix+4), e ; diff
ld (ix+5), d
ld (ix+6), l ; diff
ld (ix+7), h
; purple border means 'rewind the tape and press enter'
di
ld a, 3
out (254), a
.tblk_key:
ld a, 191 ; waiting for enter
in a,(254)
rra
jr c, .tblk_key
ei
jr .tblk_skip
; we don't have to set the buffer position, done it already
.tblk_posdif:
; at this point we have the difference and know it is positive
pop de ; restore the current position
pop hl
; store it as 'the previous'
ld (ix+0), e
ld (ix+1), d
ld (ix+2), l
ld (ix+3), h
.tblk_buffer:
; setting the buffer position for the positive difference
ld a, (buf_pos)
add a, (ix+6) ; l
ld (buf_pos), a
jr nc, .tblk_skip
; now we increase the higher difference bytes due to overflow
xor a
inc (ix+7) ; h
cp (ix+7)
jr nz, .tblk_skip
inc (ix+4) ; e
cp (ix+4)
jr nz, .tblk_skip
inc (ix+5) ; d
.tblk_skip:
; Now, how many tape blocks do we have to load before the target block appears in the buffer?
; it is shown by the 3 higher bytes of the difference
; We've just set them up for the positive difference case.
; For the negative difference case, the L-byte has to be equal to the buf_pos we set earlier
; (ix+7) H, (ix+4) E, (ix+5) D is now the counter for blocks to be loaded
; if it's 0, the block is already at the buffer and we don't have to load anything
xor a
or (ix+7)
or (ix+4)
or (ix+5)
jp z, .tblk_readbyte
; well, let's play the tape
ld a, (ix+5)
ld b,a ; this is the outer cycle
inc b ; as we will djnz
ld a, (ix+7)
ld l, a ; lower byte of the inner counter
ld a, (ix+4)
ld h, a ; higher byte
dec hl ; as we know at this point that at least one block is to be loaded
ld c, 0 ; it's a 16-bit cycle flag used below
xor a
or h
or l
jp z, .tblk_load
ld c, 1 ; hl=nonzero
.tblk_load:
push bc ; counters
push hl
ld ix, tap_buffer ; we don't need the ix value anymore
ld de, 256
ld a, 255
call t_load
pop hl
pop bc
; counter
xor a
dec hl
or h
or l
jr nz, .tblk_ccheck
ld c, 0 ; on the next cycle, b has to be decremented
jr .tblk_load
.tblk_ccheck:
xor a
or c
jp nz, .tblk_load
inc c ; next 16-bit cycle
djnz .tblk_load
.tblk_readbyte:
ld hl, tap_buffer
ld b, 0
ld a, (buf_pos)
ld c, a
add hl,bc
ld a, (hl) ; here it is!
pop ix
pop bc
cp a
ret
t_load:
push iy
ld iy, IYBAS
scf
; one can not call directly, as RST8 is then called upon break
inc d
ex af,af'
dec d
di
ld a, 15
out (254), a
call 1378 ; jump into LD-BYTES
t_ldret:
ld a, (23624) ; restore border
and 0x38
rrca
rrca
rrca
out (254),a
pop iy
ei
ret
;end

58
kernel/zxspectrum/vid.asm Normal file
View File

@ -0,0 +1,58 @@
v_init:
call 3435 ; ZXS_CLS
ld a, 2
call 5633 ; ZXS_STRM, current stream = 1, main screen
ret
; the ZX Spectrum BASIC firmware puts the character in A into the current output stream by RST 16
v_putc:
; save all
push hl
push bc
push de
push af
push ix
push iy
ld iy, IYBAS ; restore IY for BASIC
; main
push af ; char
push bc ; curflag
push de ; coords
ld a, 22 ; AT_CTRL, screen position, 22x32
rst 16
pop de
ld a, d
push de
rst 16
pop de
ld a, e
rst 16
pop bc
xor a
cp c
jp z, .char
ld a, 18 ; FLASH_CTRL
rst 16
xor a
inc a ; on
rst 16
pop af
rst 16
ld a, 18 ; FLASH_CTRL
rst 16
xor a ; off
rst 16
jp .rest
.char:
pop af
rst 16
; restore and return
.rest:
pop iy
pop ix
pop af
pop de
pop bc
pop hl
ret

BIN
recipes/zxspectrum/Apps.tap Normal file

Binary file not shown.

BIN
recipes/zxspectrum/Boot.tap Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,138 @@
# Sinclair ZX Spectrum
Sinclair ZX Spectrum is the British Z80-based home computer manufactured and sold in app. 5 mln branded units since the early 1980s and available worldwide in various clones until today. The most widespread legacy 48K model is a challenging Collapse OS target due to:
* absense of standard communication protocols, with the expansion egde connector requiring complex additional hardware;
* cassette tape storage as the only medium supported out-of-the-box;
* 16K ROM space with no paging, 7K memory-mapped screen, and the infamous [contended memory][contention], limiting the resources further against the competitors.
This recipe builds a RAM-based kernel self-assembling on the 48K model with tape storage. The ZX Spectrum firmware routines are used extensively for console and tape handling, which requires the `IM 2` interrupt mode of the Z80. Tape is accessed in two ways, via a read-only block device abstraction (required for `zasm`) and a set of short read/write applications directly bridging the MMAP blkdev and file system to tape. The assembly of the kernel binary is done with the tape-based CFS as the source and the bare MMAP blkdev as the destination. `Zasm`, `ed`, and tape applications are included into the RAM kernel binary. The MMAP size is 16K, the remaining userspace is app 7K.
## Gathering parts
* A Sinclair ZX Spectrum with access to tape storage. The 48K and 128K+ models have EAR/MIC [3.5 mm sockets][sockets] on board, the +3 has a single input/output socket, the +2A needs restoration of the EAR input, the +2/+2B models have a built-in tape recorder (reportedly out of order on most currently available units) and no sockets, which requires restoration of the pinout.
* A tape recorder with blank tapes, or a digital wave playback/recording device
* A cord pluggable into the playback/recording device and the ZX tape sockets
* Tape conversion routines for modern hardware, such as [BIN2TAP][bin2tap] for tape format conversion of the kernel binary and [TAPIR][tapir] for direct playback or WAV conversion of the tape image (both under Windows). See the description of [tape file format][TAP]
* A hex file editor for SD card image manipulations
## Kernel assembly
The necessary kernel modules include grid display emulation (32x21) and keyboard scan routine, both using the firmware ROM calls for simplicity and thus requiring preservation of the `IY` register extensively used by Spectrum BASIC. RAM-based kernels have to work in `IM 2` mode for this, with the handler located in the non-contended memory above `0x8000`. The `IM 2` [interrupt vector table][im2readmore] should be placed in the non-contended memory as well for the [128K model compatibility][128Kim2].
Stack memory is contended in this recipe, with the bottom at `0x8000`, which should be changed (e.g. by patching the kernel init routine before launching) if precise timings are needed. The top of the user memory `0xc000` is one possible bottom of the moved stack.
The tape-based blkdev is another platform-specific kernel module. The device is read-only due to infeasibility of tape-to-tape assembly. Instead, MMAP-tape bridge routines are designed as applications independent of the tape blkdev, but included into the binary in this recipe. They naturally require the MMAP blkdev in the kernel.
To save some RAM for MMAP, the shell and ed line buffers are slightly reduced compared to defaults (2K `BUF_POOLSIZE` left for 256 lines with shell, 3K `ED_BUF_PADMAXLEN` for 1024 `ED_BUF_MAXLINES` lines with ed). The 1K shell `BUF_LINES` buffer is placed to contended memory.
Due to features of screen handling by firmware ROM, the three bottom screen rows are left blank.
See the respective glue.asm and user.h files for details.
## Assembly of application binaries
The application binaries, `zxs/ed.bin`, `zxs/zasm.bin` and `zxs/tapeutil.bin` were assembled in advance and included into glue.asm using `.bin` directive.
The starting point is learning `BAS_RAMEND` value by examining a memory dump of the test build assembled with the necessary shell memory allocation. On init, the basic shell saves the SP value, which is the value set in glue.asm minus 2 (`0x7ffe` in this recipe). The address of this storage +6 is `BAS_RAMEND`, which is the reference point for all memory assignments for included binaries. The tape blkdev and utilities use 527 bytes in this build, which allows assigning memory from `BAS_RAMEND+527` to `USER_CODE`, incl. `ed` and `zasm` working buffers, by writing the value in `user.h`.
Finally, the binaries lengths should be used to learn the necessary .org values for each application. The `.org` and `.equ USER_RAMSTART USER_CODE` directives in `mono/ed` and `mono/zasm` glue files override the `user.h` settings.
## Tape conversion
The kernel binary can be converted to a tape file with a single CODE-type block and a header using the utilities described above or similar.
Conversion of the OS sources to the tape-based CFS is, however, a tedious two-step process and requires:
* splitting the SD card image, using a hex editor, into smaller parts of size equal to MMAP volume of your build, containing an integer number of CFS blocks;
* converting each part to a tape image with a single headerless block;
* loading the blocks into the ZX Spectrum Collapse OS MMAP filesystem using the `BINLD` application (see below);
(with a Spectrum emulator such as Fuse, the two previous steps can be shortcut by loading image parts into MMAP area directly using the `File/Load binary data` option)
* saving them to tape from the MMAP using the `CFSSV` application (see below), as a chain of 256-byte headerless blocks.
The resulting sequence of blocks is readable by the tape blkdev.
To save some assembly time, it is advised to minimize the working tapes content: one tape per application, one tape for kernel sources of the currently processed build.
The tape files with the OS sources converted from the emulator cfs images (as standing on May, 2020) are:
* `Kernel.tap` (Spectrum kernel modules, utilities, basic shell, old shell)
* `Apps.tap` (ed, zasm, memt, sdct, at28w)
* `Hardware.tap` (non-Spectrum kernel modules)
in this directory.
## Launching
To load and run the kernel from tape, execute the following in 48K BASIC mode of the Spectrum:
CLEAR 24316
LOAD "" CODE 24317
RANDOMIZE USR 24317
You may adjust your screen colours using `INK/PAPER/BRIGHT/BORDER` and `BORDCR (23624)` variable prior to that.
An example of a loader package is shown as `Boot.tap` in this directory.
## Emulation
The recipe has been tested to work on the emulated ZX Spectrum under Windows, using Fuse 1.1.1 binary build and the original Sinclair Research Ltd ROMs. The original 48K and 128K configurations both run the boot binary, starting from either 48K or 128K BASIC mode. The binary works with the Beta Disk/TR-DOS emulation turned either on or off. No other emulated clones were tested.
To run the provided package or your converted boot tape, open the `Boot.tap` using the `Media/Tape/Open (F7)` command in Fuse and run the launching commands above. Depending on Fuse configuration, the binary file will be loaded instantly or in real time.
Please take note of the following when using emulated tapes with Collapse OS under Fuse:
* The `Use tape traps` option in `Options/General (F4)` menu has to be turned on when *saving* to an emulated tape. Before loading from the tape just created, always save the modified content to a new .tap file by `Media/Tape/Write (F6)` and re-open it by `Media/Tape/Open (F7)`
* When learning the system with minor works, the `Use tape traps` option can be turned off when *loading* from the tape. The playback is started and stopped manually by `Media/Tape/Play (F8)` in this case. Do not rewind the tape unless prompted by the magenta border
* When assembling from the tape with `zasm`, the `Use tape traps` option can be turned on for speed, as the tape is always rewound after loading a new data portion
* The new content is written at the end of the .tap file and can not be erased or overwritten
* The `CFSSV` and `CUTSV` utilities add a 2-second pause between the saved CFS blocks, which is required for `zasm` to load and process the blocks seamlessly, but not preserved in .tap files. It is not relevant to tape emulation, but if you plan to convert your .tap files for real hardware, the pauses have to be somehow restored.
## Usage
The devices list is as follows:
* 0 - MMAP,
* 1 - file handle 0,
* 2 - file handle 1,
* 3 - tape blkdev.
CFS is initialized on MMAP upon launch. Major assembly works with zasm should be done from tape using the bare MMAP blkdev (0) as the destination.
The tape CFS is not default, it has to be mounted by typing `bsel 3: fson`, then `fls`. File listing should be completed by loading a null ("invalid") block. It's useful to have at least one at the end of every tape, including the boot tape. Other tape operations which get rid of the placeholder data in the buffer, including `ed` or `fopen`, will suffice.
File handles are then assigned by typing fopen and loading the first block of the file. When searching for a tape block, purple border means "rewind the tape and press ENTER". Writing does not work with the tape device (specifically, PutB points to unsetZ with all due consequences). For creation of new tape files, MMAP and the dedicated routines have to be used.
In case the CFS 'magic' label is damaged in the tape buffer due to a loading error, fs commands will error out. To restore the buffer, type `bseek 256: getb: bseek 0: getb` to reload the first CFS sector from tape, then `fls` or `ed` the first file before starting any assembly works.
The MMAP-tape routines, exposed as shell commands in this recipe, are as follows (optional arguments are in parentheses; only decimal values are accepted):
* `BINSV (len)` - saving the MMAP content as a raw headerless binary, the whole MMAP area by default
*(may be convenient for storing the existing MMAP filesystem within a single block, saving the freshly assembled kernel binary, creating a null block)*
* `BINLD len` (a/o, a by default) - raw binary loading, appending or overwriting the filesystem
*(may be useful for loading the filesystem converted from an SD card image, or a previously stored MMAP)*
* `HEAD addr len` - saving of the CODE-block header in the Spectrum BASIC format
*(required for loading and launching the self-assembled kernel binary from Spectrum BASIC)*
* `FILSV file` - saving a particular file as a raw headerless binary
*(useful for exporting a file from the tape CFS)*
* `FILLD filename len (a/o, a by default)` - loading of the raw binary as a file into the existing MMAP filesystem, appending or overwriting the filesystem
*(for importing an external source file or binary)*
* `CFSSV (file)` - saving a particular file or (by default) the whole existing filesystem as a chain of 256-byte blocks *(to be readable by the tape blkdev)*
* `CUTSV filename (len)` - saving the raw MMAP content (whole MMAP area by default) as a single CFS file, cutting it into a chain of blocks *(for saving an application binary assembled to bare MMAP. Saving an existing filesystem will error out)*
* `CFSLD (num, 1 by default (a/o, a by default))` - loading of the CFS chain, appending or overwriting the filesystem
*(useful for viewing, editing and converting source files, particularly kernel sources)*
For getting the length of a freshly assembled binary, ZTELL routine is exposed, fetching the internal writing counter from within the zasm working memory. It can be accessed by executing `addr ztell: s=a`, then `usr s: print h`.
## Modifications and further development
Some additional memory can be acquired by reducing the grid display to 32x15 (2K + 256b additionally) or 32x7 (4K + 512b additionally) size and thus freeing a part of the screen memory. The bottom row in the last third of the screen used for grid emulation has to be left blank. The two memory blocks are located *down* from `0x5800` (bitmap) and `0x5b00` (attributes) respectively in the contended memory and should be managed accordingly (e.g. for placing interrupt and jump tables upon boot; for `basic`/`ed` line buffers etc.).
All of the advanced models and clones are backwards-compatible with the 48K model, making this recipe a suitable point of departure.
There is a number of external disk systems for the Spectrum. Many of them (e.g. the +3's native +3DOS, or Beta Disk interface implemented in many Eastern European clones), while removing the need for included applications, may still require `IM 2` mode, and thus the 'non-contended memory', even with completely stand-alone console drivers, due to the interface architecture.
The 128K/+2/+3 models have a combined MIDI/RS232 port on board, with the pin diagrams widely available. This allows writing serial I/O drivers pluggable into the `IM 2` interrupt handler, expanding usability in various ways.
The extended RAM of the 128K+/+2/+3 models may be accessed by modifying the MMAP kernel module to directly support RAM paging. This may be useful for viewing and editing large source files, but is not enough for memory-to-memory kernel reassembly, as the minimal set of kernel sources is app. 160K. The ROM/RAM paging available on the +3 model allows the kernel to reside in RAM under `0x0`. Some of the paged RAM is contended on all mass produced models.
More sophisticated tape bufferization, with the additional RAM footprint, may increase the speed of tape-to-memory self-assembly, which is almost prohibitively slow at the moment.
The 16K model would require a ROM-based kernel completely replacing the firmware for Collapse OS to remain self-hosted. It can however be expanded with the additional RAM module up to 32K plugged to the edge connector.
[bin2tap]: https://sourceforge.net/p/zxspectrumutils/wiki/bin2tap/
[tapir]: http://www.worldofspectrum.org/pub/sinclair/tools/pc/tapir1.0.zip
[TAP]: https://faqwiki.zxnet.co.uk/wiki/TAP_format
[im2readmore]: http://www.breakintoprogram.co.uk/computers/zx-spectrum/interrupts
[sockets]: https://faqwiki.zxnet.co.uk/wiki/Tape_leads
[contention]: https://www.worldofspectrum.org/faq/reference/48kreference.htm#Contention
[128Kim2]: https://www.worldofspectrum.org/faq/reference/128kreference.htm

263
recipes/zxspectrum/glue.asm Normal file
View File

@ -0,0 +1,263 @@
.equ IYBAS 23610 ; user.h
; The ZX Spectrum firmware requires IY value to be equal to 23610
; (in this case, to handle keyboard interrupts and screen correctly).
; an IM2 handler has to be included to manage this.
.equ RAMEND 0xffff ; user.h
.equ USER_CODE 41639 ; = BAS_RAMEND+527 in user.h
; when changing the memory layout, it should have a dummy value for a test assembly,
; then changed to the actual value in relation to the new BAS_RAMEND
.org 0x5efd
jp init
.inc "err.h"
.inc "ascii.h"
.inc "blkdev.h"
.inc "fs.h"
.inc "kernel/zxs/jumps.asm"
.inc "core.asm"
.inc "kernel/str.asm"
.inc "kernel/zxs/vid.asm"
.inc "kernel/zxs/kbd.asm"
.equ GRID_COLS 32
.equ GRID_ROWS 21 ; to avoid CRLF outside of the #2 screen
.equ GRID_SETCELL v_putc
.equ GRID_GETC k_getc
.equ GRID_RAMSTART RAMSTART
.inc "kernel/grid.asm"
.equ BLOCKDEV_RAMSTART GRID_RAMEND
.equ BLOCKDEV_COUNT 4
.inc "kernel/blockdev.asm"
; List of devices
.dw mmapGetB, mmapPutB
.dw blk1GetB, blk1PutB
.dw blk2GetB, blk2PutB
.dw tapeGetB, unsetZ ;read-only
.equ STDIO_RAMSTART BLOCKDEV_RAMEND
.equ STDIO_GETC gridGetC
.equ STDIO_PUTC gridPutC
.equ STDIO_SETCUR gridSetCurH
.inc "kernel/stdio.asm"
.equ MMAP_START 0xc000 ; 49152
.equ MMAP_LEN RAMEND-MMAP_START+1
; 16K, 64 fs blocks for MMAP FS
.inc "kernel/mmap.asm"
.equ FS_RAMSTART STDIO_RAMEND
.equ FS_HANDLE_COUNT 2
.inc "kernel/fs.asm"
; BASIC shell
; RAM space used in different routines for short term processing.
.equ SCRATCHPAD_SIZE STDIO_BUFSIZE
.equ SCRATCHPAD FS_RAMEND
.inc "lib/util.asm"
.inc "lib/ari.asm"
.inc "lib/parse.asm"
.inc "lib/fmt.asm"
.equ EXPR_PARSE parseLiteralOrVar
.inc "lib/expr.asm"
.inc "basic/util.asm"
.inc "basic/parse.asm"
.inc "basic/tok.asm"
.equ VAR_RAMSTART SCRATCHPAD+SCRATCHPAD_SIZE
.inc "basic/var.asm"
.equ BUF_RAMSTART VAR_RAMEND
.equ BUF_POOLSIZE 0x800 ; 0x1000 by default, cut to save some RAM
.equ BUF_POOL shell_buf ; in contended memory
.equ BUF_MAXLINES 0x100
.equ BUF_LINES BUF_RAMSTART+4
.equ BUF_RAMEND @+BUF_MAXLINES*4 ; continue allocating in higher RAM
.inc "basic/buf.asm"
.equ BFS_RAMSTART BUF_RAMEND
.inc "basic/fs.asm"
.inc "basic/blk.asm"
.equ BAS_RAMSTART BFS_RAMEND
.inc "basic/main.asm"
; BASIC records the SP value, which is glue init value-2; the address of this storage +6 is BAS_RAMEND
; this is the value to be learned from a memory dump for user.h!
.equ tap_buffer BAS_RAMEND
.equ buf_pos @+256
.equ tap_pos @+1
.equ TAP_RAMEND @+8 ; user.h for assembling zxs/tapeutil.bin, BAS_RAMEND+265
.inc "kernel/zxs/tapeblk.asm"
;.equ USER_CODE BAS_RAMEND+527 ; 265 tapeblk + 262 tapeutil.bin below
.equ ZBCOUNT USER_CODE+14
tpztell:
; fetches the zasm internal counter (IO_OUT_BLK) at ZASM_RAMSTART+14
; it's called through 'addr ztell: s=a: usr s: print h'
ld hl, (ZBCOUNT)
xor a
ret
basFindCmdExtra:
ld hl, basBLKCmds
call basFindCmd
ret z
ld hl, basFSCmds
call basFindCmd
ret z
ld hl, .mycmds
call basFindCmd
ret z
jp basPgmHook
.mycmds:
.db "binsv", 0
.dw tapeutil
.db "binld", 0
.dw tapeutil+3
.db "head", 0
.dw tapeutil+6
.db "filsv", 0
.dw tapeutil+9
.db "filld", 0
.dw tapeutil+12
.db "cfssv", 0
.dw tapeutil+15
.db "cfsld", 0
.dw tapeutil+18
.db "cutsv", 0
.dw tapeutil+21
.db "ztell", 0
.dw tpztell
.db "ed", 0
.dw edrun
.db "zasm", 0
.dw zasmrun
.db 0xff
init:
di
ld sp, 0x8000
; if precise timings are needed,
; the stack should be moved to non-contended memory
call int_init
call v_init
call tapeblk_init
; init a FS in mmap
; possibly not needed in the final build
ld hl, MMAP_START
ld a, 'C'
ld (hl), a
inc hl
ld a, 'F'
ld (hl), a
inc hl
ld a, 'S'
ld (hl), a
call gridInit
call fsInit
xor a
ld de, BLOCKDEV_SEL
call blkSel
call fsOn
call basInit
ld hl, basFindCmdExtra
ld (BAS_FINDHOOK), hl
ei
jp basStart
; *** blkdev 1: file handle 0 ***
blk1GetB:
ld ix, FS_HANDLES
jp fsGetB
blk1PutB:
ld ix, FS_HANDLES
jp fsPutB
; *** blkdev 2: file handle 1 ***
blk2GetB:
ld ix, FS_HANDLES+FS_HANDLE_SIZE
jp fsGetB
blk2PutB:
ld ix, FS_HANDLES+FS_HANDLE_SIZE
jp fsPutB
int_init:
di
ld a,0x80
ld i,a
im 2
ret ; does not enable interrupts yet
tapeutil:
.bin "zxs/tapeutil.bin"
; all tape utilities in one block with library
; t_load ROM call routine is duplicated in zxs/tapeblk.asm in case those utilities are moved to userspace
; 0x7492 is free space, +2K=7c92, the rest 0x36e=878b for stack space
shell_buf:
; basic shell BUF_POOL points here
.fill 0x8000-$
; The ZX Spectrum hardware noises the CPU data bus so that any value can appear on interrupt instead of 0xFF.
; A 257-byte table is thus required to hold the INT handler address for IM2 mode.
; interrupt table = 128 words + 1 byte
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.dw 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181, 0x8181
.db 0x81
; the handler and the table are to reside in the non-contended memory, i.e. in 0x8000-0xC000
; (or rather in 0x8080-0xBFBF)
.fill 0x8181-$
; *** interrupt handler
; other possible int routines here, e.g. RS232 or debug calls
push iy
ld iy, IYBAS
rst 56
pop iy
ei
ret
;0x818c
edrun:
.bin "zxs/ed.bin" ;1108 = 0x0454
;0x85e0 = 0x818c+0x454
zasmrun:
.bin "zxs/zasm.bin" ;4881 = 0x1311
RAMSTART: ; 0x98f1 = 0x85e0+0x1311 (39153)
; bin length (14836)

View File

@ -0,0 +1,46 @@
; *** Requirements ***
; _blkGetB
; _blkPutB
; _blkSeek
; _blkTell
; fsFindFN
; fsOpen
; fsGetB
; fsPutB
; fsSetSize
; printstr
; printcrlf
; stdioReadLine
; stdioPutC
;
.inc "user.h"
.org 0x818c
.equ USER_RAMSTART USER_CODE
; *** Overridable consts ***
; Maximum number of lines allowed in the buffer.
.equ ED_BUF_MAXLINES 0x400
; Size of our scratchpad
.equ ED_BUF_PADMAXLEN 0xc00
; ******
.inc "err.h"
.inc "blkdev.h"
.inc "fs.h"
jp edMain
.inc "core.asm"
.inc "lib/util.asm"
.inc "lib/parse.asm"
.inc "ed/util.asm"
.equ IO_RAMSTART USER_RAMSTART
.inc "ed/io.asm"
.equ BUF_RAMSTART IO_RAMEND
.inc "ed/buf.asm"
.equ CMD_RAMSTART BUF_RAMEND
.inc "ed/cmd.asm"
.equ ED_RAMSTART CMD_RAMEND
.inc "ed/main.asm"
USER_RAMSTART:

View File

@ -0,0 +1,90 @@
; zasm
;
; Reads input from specified blkdev ID, assemble the binary in two passes and
; spit the result in another specified blkdev ID.
;
; We don't buffer the whole source in memory, so we need our input blkdev to
; support Seek so we can read the file a second time. So, for input, we need
; GetB and Seek.
;
; For output, we only need PutB. Output doesn't start until the second pass.
;
; The goal of the second pass is to assign values to all symbols so that we
; can have forward references (instructions referencing a label that happens
; later).
;
; Labels and constants are both treated the same way, that is, they can be
; forward-referenced in instructions. ".equ" directives, however, are evaluated
; during the first pass so forward references are not allowed.
;
; *** Requirements ***
; strncmp
; upcase
; findchar
; blkSel
; blkSet
; fsFindFN
; fsOpen
; fsGetB
; _blkGetB
; _blkPutB
; _blkSeek
; _blkTell
; printstr
; printcrlf
.inc "user.h"
.org 0x85e0
.equ USER_RAMSTART USER_CODE
; *** Overridable consts ***
; NOTE: These limits below are designed to be *just* enough for zasm to assemble
; itself. Considering that this app is Collapse OS' biggest app, it's safe to
; assume that it will be enough for many many use cases. If you need to compile
; apps with lots of big symbols, you'll need to adjust these.
; With these default settings, zasm runs with less than 0x1800 bytes of RAM!
; Maximum number of symbols we can have in the global and consts registry
.equ ZASM_REG_MAXCNT 0xff
; Maximum number of symbols we can have in the local registry
.equ ZASM_LREG_MAXCNT 0x20
; Size of the symbol name buffer size. This is a pool. There is no maximum name
; length for a single symbol, just a maximum size for the whole pool.
; Global labels and consts have the same buf size
.equ ZASM_REG_BUFSZ 0x700
; Size of the names buffer for the local context registry
.equ ZASM_LREG_BUFSZ 0x100
; ******
.inc "err.h"
.inc "ascii.h"
.inc "blkdev.h"
.inc "fs.h"
jp zasmMain
.inc "core.asm"
.inc "zasm/const.asm"
.inc "lib/util.asm"
.inc "lib/ari.asm"
.inc "lib/parse.asm"
.inc "zasm/util.asm"
.equ IO_RAMSTART USER_RAMSTART
.inc "zasm/io.asm"
.equ TOK_RAMSTART IO_RAMEND
.inc "zasm/tok.asm"
.equ INS_RAMSTART TOK_RAMEND
.inc "zasm/instr.asm"
.equ DIREC_RAMSTART INS_RAMEND
.inc "zasm/directive.asm"
.inc "zasm/parse.asm"
.equ EXPR_PARSE parseNumberOrSymbol
.inc "lib/expr.asm"
.equ SYM_RAMSTART DIREC_RAMEND
.inc "zasm/symbol.asm"
.equ ZASM_RAMSTART SYM_RAMEND
.inc "zasm/main.asm"
USER_RAMSTART:

View File

@ -0,0 +1,594 @@
; The include file with tape utilities exposed as commands, for kernel build
; *** A library file for MMAP-TAPE bridge, which can be included with the userspace versions of the routines
; (user routines are below, addressed by the jump table)
; required syscalls:
; strncmp
; fsAlloc
; fsblkTell
; the applications require:
; parseDecimal
; fsFindFN
.inc "user.h"
.org 0x711f ; 0x7492-0x373
; for CUTSV 1st block
.equ sv_buffer TAP_RAMEND
; Parsed arguments
; block, then byte length
.equ par_blocklen @+256
; length of the parsed filename
.equ par_namelen @+3
; pointer to filename in the args
.equ par_filename @+1
.equ TAPUTIL_RAMEND @+2
jp tpbinsv
jp tpbinld
jp tphead
jp tpfilsv
jp tpfilld
jp tpcfssv
jp tpcfsld
jp tpcutsv
; t_CFSHead
; t_parseName
; t_parseOver
; t_chainEnd
; t_newFile
; t_save
; t_load
; t_stkStor
; t_stkRet
; CFS header preparation (for CUTSV 1st block, for FILLD newfile)
t_CFSHead:
push hl
push bc
push de
; position in ix
push ix
ld hl, sv_buffer
push hl
pop de
inc de
ld bc, 32
xor a
ld (hl), a
ldir ; clear the header
ld ix, sv_buffer ; for generality
ld a, 'C'
ld (ix+0), a
ld a, 'F'
ld (ix+1), a
ld a, 'S'
ld (ix+2), a
ld hl, par_blocklen ; block length stored by parser
ld a, (hl)
ld (ix+3), a
inc hl ; actual file length stored by parser
ld a, (hl)
ld (ix+4), a
inc hl
ld a, (hl)
ld (ix+5), a
ld bc, 6
ld hl, sv_buffer
add hl, bc
push hl
pop de ; destination for the parsed filename transfer
ld hl, par_namelen ; filename length stored by the parser
ld c, (hl)
ld hl, (par_filename) ; filename pointer stored by the parser
ldir ; copy filename
pop ix
pop de
pop bc
pop hl
ret
; Parsing of the filename (HL)
t_parseName:
push de
push bc
push hl
push hl
pop de
ld hl, par_filename
ld (hl), e
inc hl
ld (hl), d
pop hl
ld c, 0
.loop:
ld a, (hl)
cp 32
jr z, .end
inc c
inc hl
jr .loop
.end:
push hl
ld hl, par_namelen
ld (hl), c
pop hl
inc hl ; the next argument in HL
pop bc
pop de
ret
; Parsing of the overwrite flag 'o'
t_parseOver:
push de
ld de, .flag
ld a, 1
call strncmp ; Z - equal (overwrite)
pop de
ret
.flag:
.db 'o'
; Search for CFS chain end (for binary/CFS loading)
t_chainEnd:
push hl
ld a, (par_blocklen)
ld hl,.dummy ; placeholder name
call fsAlloc
call fsblkTell ; position in HL
ld bc, MMAP_START
add hl, bc
push hl
pop ix ; chainend in ix
pop hl
ret
.dummy:
.db '@', 0
; New file at the CFS chain end (for binary loading into CFS)
t_newFile:
push de
push hl
push bc
call t_chainEnd
call t_CFSHead
; start of the new file in ix
push ix
pop de
ld hl, sv_buffer
ld bc, 32
ldir ; copy header to CFS
push de
pop ix ; loading position in ix
pop bc
pop hl
pop de
ret
; SAVE-BYTES ROM call
; header/bytes flag set outside
t_save:
;ld ix,addr
;ld de,len
;ld a,head
push iy
ld iy, IYBAS
; one can not call directly, as RST8 is then called upon break
call 0x04c6 ; SA-BYTES+4 to skip SA/LDRET
jr t_ldret
; LOAD-BYTES ROM call
t_load:
;ld ix,addr
;ld de,len
;ld a,head
push iy
ld iy, IYBAS
scf
; one can not call directly, as RST8 is then called upon break
inc d
ex af,af'
dec d
di
ld a, 15
out (254), a
call 1378 ; jump into LD-BYTES
t_ldret:
ld a, (23624) ; restore border
and 0x38
rrca
rrca
rrca
out (254),a
pop iy
ei
ret
; Stack store (CALL)
t_stkStor:
ld (.stkbc), bc
pop bc
ld (.stkret), bc
ld bc, (.stkbc)
push hl
push bc
push de
push ix
push iy
ld bc, (.stkret)
push bc
ret
.stkbc:
.dw 0
.stkret:
.dw 0
; Stack restore (JP)
t_stkRet:
pop iy
pop ix
pop de
pop bc
pop hl
ret
; 2-sec pause between savings
t_pause:
push bc
ld b,100
.loop:
push bc
halt
pop bc
djnz .loop
pop bc
ret
; *** APPLICATIONS ***
tpbinsv:
call t_stkStor
call parseDecimal
jr z, .arglen
ld de, MMAP_LEN
.arglen:
ld ix, MMAP_START
ld a, 255
call t_save
xor a
jp t_stkRet
tpbinld:
call t_stkStor
call parseDecimal
jp nz, t_stkRet
inc hl
call t_parseOver
jr z, .ovlen
push de
call t_chainEnd
pop de
jr .applen
.ovlen:
ld ix, MMAP_START
.applen:
ld a, 255
call t_load
xor a
jp t_stkRet
tphead:
xor a
inc a
call t_stkStor
call parseDecimal
jp nz, t_stkRet ; ERR no addr
ld ix, sv_buffer
ld a,3
ld (ix+0),3
ld (ix+13),e
ld (ix+14),d
inc hl
call parseDecimal
jp nz, t_stkRet ; ERR no len
ld ix, sv_buffer
ld (ix+11),e
ld (ix+12),d
ld de, 17
xor a
call t_save
xor a
jp t_stkRet
tpfilsv:
xor a
inc a
call t_stkStor
call fsFindFN
jp nz, t_stkRet ; ERR file not found
call fsblkTell ; position in HL
ld bc, MMAP_START
add hl, bc
push hl
pop ix
ld b, (ix+3) ; blocks
;xor a
;or b
;jp z, t_stkRet
ld de, 256
ld hl, 0
.loop:
add hl,de
djnz .loop
ld bc,32
sbc hl,bc
push hl
pop de
push ix
pop hl
add hl,bc
push hl
pop ix
ld a, 255
call t_save
xor a
jp t_stkRet
tpfilld:
call t_stkStor
ld (par_filename), hl
ld b,26
ld c,0
.loop:
ld a,(hl)
cp 32
jr z, .namlen
inc c
inc hl
djnz .loop
.namlen:
ld a, c
ld (par_namelen), a
inc hl
call parseDecimal
jp nz, t_stkRet ; something wrong
ld (par_blocklen+1), de
inc hl
push hl
push de
pop hl
ld de, 256
ld b,1
.div:
sbc hl,de
jr c, .blklen
inc b
jr .div
.blklen:
ld a,b
ld (par_blocklen), a
pop hl
call t_parseOver
jr z, .whole
call t_newFile ; load to ix
jr .load
.whole:
call t_CFSHead ; in sv_buffer
ld de, MMAP_START
ld hl, sv_buffer
ld bc, 32
ldir
push de
pop ix
.load:
ld de, (par_blocklen+1)
ld a,255
call t_load
xor a
jp t_stkRet
tpcfssv:
call t_stkStor
xor a
cp (hl)
jp z, .whole
call fsFindFN
jp nz, .err
call fsblkTell ; position in HL
ld bc, MMAP_START
add hl, bc
push hl
pop ix
ld b, (ix+3) ; blocks
;xor a
;or b
;jp z, t_stkRet
ld de, 256
jr .len
.whole:
ld de, 256
ld ix, MMAP_START
ld hl, MMAP_LEN
ld b,0
.div:
sbc hl,de
jr c, .len
inc b
jr .div
.len:
; blocklen in B
xor a
or b
jp z, .err
push ix
pop hl
sbc hl,de
push hl
.loop:
pop ix
ld de, 256
add ix, de
ld a, 255
push ix
push bc
call t_save
pop bc
call t_pause
djnz .loop
pop ix
xor a
jp t_stkRet
.err:
xor a
inc a
jp t_stkRet
tpcfsld:
call t_stkStor
call parseDecimal
jr z, .arglen
.deflen:
ld b, 1
jr .flag
.arglen:
xor a
or d
jr nz, .deflen
ld b, e
ld a, b
ld (par_blocklen), a
.flag:
push bc ; cycle counter
inc hl
call t_parseOver
jr z, .whole
call t_chainEnd ; position in ix
jr .load
.whole:
ld ix, MMAP_START
.load:
pop bc
ld de, 256
; blocklen in B
push ix
pop hl
sbc hl,de
push hl
.loop:
pop ix
ld de, 256
add ix, de
ld a, 255
push ix
push bc
call t_load
pop bc
djnz .loop
pop ix
xor a
jp t_stkRet
tpcutsv:
call t_stkStor
push hl
ld hl, MMAP_START
ld de, .cfslabel
ld a,3
call strncmp
jr nz, .noerr
inc a ; 'CFS'=ERR
pop hl
jp t_stkRet
.cfslabel:
.db "CFS",0
.noerr:
pop hl
cp (hl)
jp z, t_stkRet ; no name provided
ld (par_filename), hl
ld b,26
ld c,0
.loop:
ld a,(hl)
cp 32
jr z, .namlen
inc c
inc hl
djnz .loop
.namlen:
ld a, c
ld (par_namelen), a
inc hl
call parseDecimal
jr z, .arglen
ld de, MMAP_LEN
ld b, 0
jr .deflen
.arglen:
ld b,1
.deflen:
xor a
or d
or e
jp z, t_stkRet ; len=0
ld (par_blocklen+1), de
push de
pop hl
ld de, 256
.div:
sbc hl,de
jr c, .blklen
inc b
jr .div
.blklen:
ld a,b
ld (par_blocklen), a
call t_CFSHead
push bc
ld de, sv_buffer+32
ld hl, MMAP_START
ld bc, 224
ldir
push hl
ld ix, sv_buffer
ld de, 256
ld a, 255
call t_save ; 1st block+meta
pop ix
pop bc
xor a
dec b
jp z, t_stkRet
ld de, 256
push ix
pop hl
sbc hl,de
push hl
.svloop:
pop ix
ld de, 256
add ix, de
ld a, 255
push ix
push bc
call t_save
pop bc
call t_pause
djnz .svloop
pop ix
xor a
jp t_stkRet
;end

44
recipes/zxspectrum/user.h Normal file
View File

@ -0,0 +1,44 @@
.equ BAS_RAMEND 0xa098 ; (41112)
; BASIC shell records the SP on init,
; the storage addr+6 is BAS_RAMEND
.equ USER_CODE @+527 ; 41639
.org USER_CODE ; overrideable
.equ IYBAS 23610
.equ MMAP_START 0xc000
.equ RAMEND 0xffff
.equ MMAP_LEN RAMEND-MMAP_START+1
.equ TAP_RAMEND BAS_RAMEND+265
; for tapeutil.bin
.equ strncmp 0x5f00
.equ upcase @+3 ; 0x5f03
.equ findchar @+3 ; 0x5f06
.equ parseHex @+3 ; 0x5f09
.equ parseDecimal @+3 ; 0x5f0c
.equ blkSel @+3 ; 0x5f0f
.equ blkSet @+3 ; 0x5f12
.equ fsFindFN @+3 ; 0x5f15
.equ fsOpen @+3 ; 0x5f18
.equ fsGetB @+3 ; 0x5f1b
.equ fsPutB @+3 ; 0x5f1e
.equ fsSetSize @+3 ; 0x5f21
.equ fsOn @+3 ; 0x5f24
.equ fsIter @+3 ; 0x5f27
.equ fsAlloc @+3 ; 0x5f2a
.equ fsHandle @+3 ; 0x5f2d
.equ fsblkTell @+3 ; 0x5f30
.equ printstr @+3 ; 0x5f33
.equ printnstr @+3 ; 0x5f36
.equ printcrlf @+3 ; 0x5f39
.equ stdioPutC @+3 ; 0x5f3c
.equ stdioGetC @+3 ; 0x5f3f
.equ stdioReadLine @+3 ; 0x5f42
.equ _blkGetB @+3 ; 0x5f45
.equ _blkPutB @+3 ; 0x5f48
.equ _blkSeek @+3 ; 0x5f4b
.equ _blkTell @+3 ; 0x5f4e