diff --git a/kernel/zxspectrum/jumps.asm b/kernel/zxspectrum/jumps.asm new file mode 100644 index 0000000..de70c46 --- /dev/null +++ b/kernel/zxspectrum/jumps.asm @@ -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 diff --git a/kernel/zxspectrum/kbd.asm b/kernel/zxspectrum/kbd.asm new file mode 100644 index 0000000..3453b6d --- /dev/null +++ b/kernel/zxspectrum/kbd.asm @@ -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 diff --git a/kernel/zxspectrum/tapeblk.asm b/kernel/zxspectrum/tapeblk.asm new file mode 100644 index 0000000..fd39559 --- /dev/null +++ b/kernel/zxspectrum/tapeblk.asm @@ -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 \ No newline at end of file diff --git a/kernel/zxspectrum/vid.asm b/kernel/zxspectrum/vid.asm new file mode 100644 index 0000000..f3ef9c7 --- /dev/null +++ b/kernel/zxspectrum/vid.asm @@ -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 diff --git a/recipes/zxspectrum/Apps.tap b/recipes/zxspectrum/Apps.tap new file mode 100644 index 0000000..4855e1e Binary files /dev/null and b/recipes/zxspectrum/Apps.tap differ diff --git a/recipes/zxspectrum/Boot.tap b/recipes/zxspectrum/Boot.tap new file mode 100644 index 0000000..579a10c Binary files /dev/null and b/recipes/zxspectrum/Boot.tap differ diff --git a/recipes/zxspectrum/Hardware.tap b/recipes/zxspectrum/Hardware.tap new file mode 100644 index 0000000..37177eb Binary files /dev/null and b/recipes/zxspectrum/Hardware.tap differ diff --git a/recipes/zxspectrum/Kernel.tap b/recipes/zxspectrum/Kernel.tap new file mode 100644 index 0000000..b8ab228 Binary files /dev/null and b/recipes/zxspectrum/Kernel.tap differ diff --git a/recipes/zxspectrum/README.md b/recipes/zxspectrum/README.md new file mode 100644 index 0000000..89095e3 --- /dev/null +++ b/recipes/zxspectrum/README.md @@ -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 diff --git a/recipes/zxspectrum/glue.asm b/recipes/zxspectrum/glue.asm new file mode 100644 index 0000000..457d0dc --- /dev/null +++ b/recipes/zxspectrum/glue.asm @@ -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) + + \ No newline at end of file diff --git a/recipes/zxspectrum/mono_ed_glue.asm b/recipes/zxspectrum/mono_ed_glue.asm new file mode 100644 index 0000000..e9305c5 --- /dev/null +++ b/recipes/zxspectrum/mono_ed_glue.asm @@ -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: diff --git a/recipes/zxspectrum/mono_zasm_glue.asm b/recipes/zxspectrum/mono_zasm_glue.asm new file mode 100644 index 0000000..30afd23 --- /dev/null +++ b/recipes/zxspectrum/mono_zasm_glue.asm @@ -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: diff --git a/recipes/zxspectrum/tapeutil.asm b/recipes/zxspectrum/tapeutil.asm new file mode 100644 index 0000000..973823f --- /dev/null +++ b/recipes/zxspectrum/tapeutil.asm @@ -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 \ No newline at end of file diff --git a/recipes/zxspectrum/user.h b/recipes/zxspectrum/user.h new file mode 100644 index 0000000..1716a56 --- /dev/null +++ b/recipes/zxspectrum/user.h @@ -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