From 492300d298aeb6561fdb3302c1dfd94363197144 Mon Sep 17 00:00:00 2001 From: Karol Stasiak Date: Sat, 5 Jan 2019 01:19:14 +0100 Subject: [PATCH] C64: File I/O support --- docs/README.md | 2 + docs/api/commodore-programming-guide.md | 70 ++++++++ docs/api/target-platforms.md | 2 + docs/index.md | 2 + docs/stdlib/c64.md | 6 +- docs/stdlib/cbm_file.md | 81 +++++++++ docs/stdlib/string.md | 5 + examples/README.md | 3 + examples/c64/multifile.ini | 42 +++++ examples/c64/multifile.mfk | 23 +++ include/c64.ini | 2 +- include/c64_kernal.mfk | 19 ++ include/cbm_file.mfk | 219 ++++++++++++++++++++++++ include/err.mfk | 3 + include/string.mfk | 8 + 15 files changed, 485 insertions(+), 2 deletions(-) create mode 100644 docs/api/commodore-programming-guide.md create mode 100644 docs/stdlib/cbm_file.md create mode 100644 examples/c64/multifile.ini create mode 100644 examples/c64/multifile.mfk create mode 100644 include/cbm_file.mfk diff --git a/docs/README.md b/docs/README.md index b43cce87..9143baa9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -49,6 +49,8 @@ * [C64-only modules](stdlib/c64.md) +* [`cbm_file` module](stdlib/cbm_file.md) + * [NES-only modules](stdlib/nes.md) ## Implementation details diff --git a/docs/api/commodore-programming-guide.md b/docs/api/commodore-programming-guide.md new file mode 100644 index 00000000..372f3cfb --- /dev/null +++ b/docs/api/commodore-programming-guide.md @@ -0,0 +1,70 @@ +[< back to index](../index.md) + +### A note about Commodore 64 + +#### Multifile programs + +A multifile program is a program stored on a disk that consists of the main program file that is executed first +and several other files that can be loaded into memory on demand. +This allows for creating programs that are larger than 64 kilobytes. + +Millfork allows building such programs, but leaves several things to the programmer: + +* tracking of which parts of the program are currently loaded and which are not + +* whether memory ranges of various parts overlap or not + +* whether loading succeeded or not + +##### Writing multifile programs + +You will need to create a platform definition file with multiple segments. +The default segment should start at $80D. +Example: + + segments=default,extra + ; the first file will contain the initial code: + default_code_segment=default + segment_default_start=$80D + segment_default_codeend=$7fff + segment_default_datastart=after_code + segment_default_end=$7fff + ; the second file will contain the extra code: + segment_extra_start=$8000 + segment_extra_codeend=$9fff + segment_extra_datastart=after_code + segment_extra_end=$cfff + +You also need to set `style=per_segment` in the `[output]` section. + +Annotate things you want to place in that file with the `segment` keyword: + + segment(extra) + void extra_function () { + // + } + +Then in your code you need to load the file: + + load_file(last_used_device(), "eee"z) + if errno == err_ok { + extra_function() + } else { + // handle error + } + +(Prefer `last_used_device()` instead of hardcoded `8`, so your program will work when loaded from any disk drive.) + +Compiling the program with `-o OUTPUT` will yield several PRG files. +The default segment will be in `OUTPUT.prg`, the segment called `extra` in `OUTPUT.extra.prg` and so on. + +##### Packaging multifile programs + +The Millfork compiler does not create Commodore disk images. + +You can use a variety of tools to perform that task, +for example the `c1531` tool shipped with [the VICE emulator](http://vice-emu.sourceforge.net/). + +To create a new disk image for the last example, use: + + c1541 -format "example,01" d64 example.d64 -write OUTPUT.prg start -write OUTPUT.extra.prg eee diff --git a/docs/api/target-platforms.md b/docs/api/target-platforms.md index 4e786657..dc9144d4 100644 --- a/docs/api/target-platforms.md +++ b/docs/api/target-platforms.md @@ -15,6 +15,8 @@ The following platforms are currently supported: * `c64` – Commodore 64. The compiler emits PRG files, not disk or tape images. +If you want to create a program consisting of multiple PRG files, +see [the Commodore programming guide](./commodore-programming-guide.md) for more info. * `c64_crt8k` – Commodore 64, 8K ROM cartridge diff --git a/docs/index.md b/docs/index.md index b43cce87..9143baa9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -49,6 +49,8 @@ * [C64-only modules](stdlib/c64.md) +* [`cbm_file` module](stdlib/cbm_file.md) + * [NES-only modules](stdlib/nes.md) ## Implementation details diff --git a/docs/stdlib/c64.md b/docs/stdlib/c64.md index 867feb1d..b69812a9 100644 --- a/docs/stdlib/c64.md +++ b/docs/stdlib/c64.md @@ -2,14 +2,18 @@ # Commodore 64-oriented modules -## `c64_kernal` module +## c64_kernal module The `c64_kernal` module is imported automatically on the C64 target. +It provides access to Kernal routines, so it requires the Kernal ROM to be enabled. TODO ## c64_basic module +The `c64_basic` module provides access to Kernal routines, so it requires the Basic ROM to be enabled. +In particular, this means that it will not work on cartridge targets without extra preparations. + TODO ## c64_hardware diff --git a/docs/stdlib/cbm_file.md b/docs/stdlib/cbm_file.md new file mode 100644 index 00000000..bd772b4e --- /dev/null +++ b/docs/stdlib/cbm_file.md @@ -0,0 +1,81 @@ +[< back to index](../index.md) + +## cbm_file + +The `cbm_file` module provides support for loading and saving files to tape and disk. +Currently, it works only for Commodore 64 targets, although support for more targets is coming. +It uses Kernal routines, so it requires the Kernal ROM to be enabled. + +#### byte last_used_device() + +Returns the last device number, or 8 if none. + +#### void load_file(byte device, pointer name) + +Loads a PRG file with the given null-terminated name to the address specified in the file. +Sets `errno`. + +#### void load_file_at(byte device, pointer name, pointer addr) + +Loads a PRG file with the given null-terminated name to the given address. +Sets `errno`. + +#### void save_file(byte device, pointer name, pointer start, word length) + +Saves `length` bytes starting from `start` to a PRG file named `name` on device `device`. +Sets `errno`. + +#### void exec_disk(byte device, pointer command) + +Executes a CBM DOS command on the given drive. + +#### void delete_file(byte device, pointer name) + +Deletes given file in the given drive. (`S0:`) + +#### void rename_file(byte device, pointer old_name, pointer new_name) + +Renames given file in the given drive. (`R0:`) + +#### void copy_file(byte device, pointer old_name, pointer new_name) + +Copies given file in the given drive. (`C0:`) + +#### void initialize_disk(byte device) + +Reinitialized disk status in the given drive. (`I0`) + +#### void validate_disk(byte device) + +Validates disk status in the given drive. (`V0`) + +#### void format_disk(byte device) + +Formats disk status in the given drive. (`N0:`) + +#### void open_file(byte device, pointer name, byte fd, byte mode) + +Opens a file. +Sets `errno`. +TODO: buggy. + +#### const byte MODE_READ = 0 +#### const byte MODE_WRITE = 1 + +#### void close_file(byte fd) + +Closes the given file. +Sets `errno`. +TODO: buggy. + +#### byte getbyte_safe() + +Reads a byte from file. +Sets `errno` to `err_eof` when reading the last byte, so don't abort reading when getting `errno != err_ok`. +TODO: buggy. + +#### void putbyte_safe(byte b) + +Wrires a byte from file. +Sets `errno`. +TODO: buggy. diff --git a/docs/stdlib/string.md b/docs/stdlib/string.md index 2d72ef7f..7e59cd03 100644 --- a/docs/stdlib/string.md +++ b/docs/stdlib/string.md @@ -23,3 +23,8 @@ If the source string is longer than 255 bytes, then the behaviour is undefined ( Converts a null-terminated string to a number. Sets `errno`. + +#### `void strzappend(pointer buffer, pointer str)` +#### `void strzappendchar(pointer buffer, byte char)` + +Modifies the given null-terminated buffer by appending a null-terminated string or s single character respectively. diff --git a/examples/README.md b/examples/README.md index 7c721bab..fe63dc0d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -28,6 +28,9 @@ ### Other examples +* Multifile ([source code](c64/multifile.mfk), [platform definition](c64/multifile.ini)) – +how to create a program made of multiple files loaded on demand + * [Panic](c64/panic_test.mfk) – how panic works on C64, showing the address of where it happened ## Famicom/NES examples diff --git a/examples/c64/multifile.ini b/examples/c64/multifile.ini new file mode 100644 index 00000000..481895e2 --- /dev/null +++ b/examples/c64/multifile.ini @@ -0,0 +1,42 @@ +; Commodore C64 +; mostly based on c64.ini +; to use with multifile.mfk + +[compilation] +arch=nmos +encoding=petscii +screen_encoding=petscr +modules=c64_hardware,loader_0801,c64_kernal,c64_panic,stdlib + +[allocation] +zp_pointers=$FB,$FD,$43,$45,$47,$4B,$F7,$F9,$9E,$9B,$3D +; we want to have two output files: +segments=default,extra +; the first file will contain the initial code: +default_code_segment=default +segment_default_start=$80D +segment_default_codeend=$7fff +segment_default_datastart=after_code +segment_default_end=$7fff +; the second file will contain the extra code: +segment_extra_start=$8000 +segment_extra_codeend=$9fff +segment_extra_datastart=after_code +segment_extra_end=$cfff + +[define] +CBM=1 +CBM_64=1 +MOS_6510=1 +WIDESCREEN=1 +KEYBOARD=1 +JOYSTICKS=2 +HAS_BITMAP_MODE=1 + +[output] +; every segment should land in its own file: +style=per_segment +format=startaddr,allocated +extension=prg + + diff --git a/examples/c64/multifile.mfk b/examples/c64/multifile.mfk new file mode 100644 index 00000000..1a053dc6 --- /dev/null +++ b/examples/c64/multifile.mfk @@ -0,0 +1,23 @@ +// compile with +// millfork -t multifile -o multifile multifile.mfk +// build a disk image with +// c1541 -format "multifile,11" d64 multifile.d64 -write multifile.prg start -write multifile.extra.prg extra + +import stdio +import cbm_file + +void main() { + load_file(last_used_device(), "extra"z) + if errno == err_ok { + extra() + } else { + putstrz("failed to load file"z) + new_line() + } +} + +segment(extra) +void extra() { + putstrz("hello from loaded file!"z) + new_line() +} \ No newline at end of file diff --git a/include/c64.ini b/include/c64.ini index be961cda..5395ed3a 100644 --- a/include/c64.ini +++ b/include/c64.ini @@ -31,7 +31,7 @@ JOYSTICKS=2 HAS_BITMAP_MODE=1 [output] -; how the banks are laid out in the output files; so far, there is no bank support in the compiler yet +; how the banks are laid out in the output files style=single ; output file format ; startaddr - little-endian address of the first used byte in the bank diff --git a/include/c64_kernal.mfk b/include/c64_kernal.mfk index 21f2098e..354cac47 100644 --- a/include/c64_kernal.mfk +++ b/include/c64_kernal.mfk @@ -4,6 +4,25 @@ // Input: A = Byte to write. asm void putchar(byte a) @$FFD2 extern +// CHRIN. Read byte from default input (for keyboard, read a line from the screen). (If not keyboard, must call OPEN and CHKIN beforehands.) +// Output: A = Byte read. +asm byte getchar() @$FFCF extern + +// CHKIN. Define file as default input. (Must call OPEN beforehands.) +// Input: X = Logical number. +asm void chkin(byte x) @$FFC6 extern + +// CHKOUT. Define file as default output. (Must call OPEN beforehands.) +// Input: X = Logical number. +asm void chkout(byte x) @$FFC9 extern + +// CLRCHN. Close default input/output files (for serial bus, send UNTALK and/or UNLISTEN); restore default input/output to keyboard/screen. +asm void clrchn() @$FFCC extern + +// READST. Fetch status of current input/output device, value of ST variable. (For RS232, status is cleared.) +// Output: A = Device status. +asm byte readst() @$FFB7 extern + inline void new_line() { putchar(13) } diff --git a/include/cbm_file.mfk b/include/cbm_file.mfk new file mode 100644 index 00000000..384c0fbe --- /dev/null +++ b/include/cbm_file.mfk @@ -0,0 +1,219 @@ +#if not(CBM) +#warn cbm_file module should be only used on Commodore targets +#endif + +import string +import err + +byte __last_used_device @$ba +inline byte last_used_device() { + byte device + device = __last_used_device + if device == 0 { device = 8 } + return device +} + +void load_file(byte device, pointer name) { + setnamz(name) + setlfs(1, device, 1) + asm { + lda #0 + jsr load + ? jmp __handle_disk_err + } +} + +void load_file_at(byte device, pointer name, pointer at) { + setnamz(name) + setlfs(1, device, 0) + asm { + lda #0 + ? ldx at + ? ldy at+1 + jsr load + ? jmp __handle_disk_err + } +} + +asm void __handle_disk_err() { + bcs __handle_disk_err_failed + lda #err_ok + ? jmp __handle_disk_err_store + __handle_disk_err_failed: + ora #$40 + jsr $FFD2 + and #$BF + lsr + eor #2 + bne __handle_disk_err_not_4_or_5 + lda #err_nofile + bcc __handle_disk_err_store + lda #err_nodevice + [ $2c ] + __handle_disk_err_not_4_or_5: + lda #err_fail + __handle_disk_err_store: + ? sta errno + ? rts +} + +void save_file(byte device, pointer name, pointer start, word length) { + setnamz(name) + setlfs(1, device, 0) + word end + end = start + length + asm { + lda #start + ? ldx end + ? ldy end+1 + jsr save + ? jmp __handle_disk_err + } +} + +inline void setnamz(pointer name) { + setnam(name, strzlen(name)) +} + +array __cbm_cmd_buffer[64] + +void exec_disk(byte device, pointer command) { + setnamz(command) + setlfs(1, device, 15) + open() + close(1) +} + +void __exec_disk(byte device) { + setnamz(__cbm_cmd_buffer) + setlfs(1, device, 15) + open() + close(1) +} + +void delete_file(byte device, pointer name) { + byte i + byte length + __cbm_cmd_buffer[0] = 's' + __cbm_cmd_buffer[1] = '0' + __cbm_cmd_buffer[2] = ':' + length = strzlen(name) + for i,0,parallelto,length { + __cbm_cmd_buffer[i + 3] = name[i] + } + __exec_disk(device) +} + +void initialize_disk(byte device) { + __cbm_cmd_buffer[0] = 'i' + __cbm_cmd_buffer[1] = '0' + __cbm_cmd_buffer[2] = 0 + __exec_disk(device) +} + +void validate_disk(byte device) { + __cbm_cmd_buffer[0] = 'v' + __cbm_cmd_buffer[1] = '0' + __cbm_cmd_buffer[2] = 0 + __exec_disk(device) +} + +void format_disk(byte device) { + setnam(__cmd_format_disk, __cmd_format_disk.length) + setlfs(1, device, 15) + open() + close(1) +} + +const byte MODE_READ = 0 +const byte MODE_WRITE = 1 +// const byte MODE_OVERWRITE = 3 // TODO: SAVE@ bug? + +void open_file(byte device, pointer name, byte fd, byte mode) { + byte length + byte i + __cbm_cmd_buffer[0] = '0' + __cbm_cmd_buffer[1] = ':' + length = strzlen(name) + for i,0,parallelto,length { + __cbm_cmd_buffer[i + 2] = name[i] + } + if length < 3 || __cbm_cmd_buffer[length] != ',' || (__cbm_cmd_buffer[length+1] != 'r' && __cbm_cmd_buffer[length+1] != 'w') { + strzappendchar(__cbm_cmd_buffer, ',') + if mode & MODE_WRITE != 0 { + strzappendchar(__cbm_cmd_buffer, 'w') + } else { + strzappendchar(__cbm_cmd_buffer, 'r') + } + length += 2 + } + setnam(__cbm_cmd_buffer, length + 2) + setlfs(fd, device, fd) + asm { + jsr open + ? jmp __handle_disk_err + } +} + +void close_file(byte fd) { + asm { + ? lda fd + ? jsr close + ? jmp __handle_disk_err + } +} + +void __translate_st_to_errno() { + byte st + st = readst() + if st == 0 { + errno = err_ok + } else if st & 0x80 != 0 { + errno = err_nodevice + } else if st & 0x40 != 0 { + errno = err_eof + } else { + errno = err_fail + } +} + +byte getbyte_safe() { + byte b + b = getchar() + garbage[garbage_index] = b + garbage_index += 1 + __translate_st_to_errno() + return b +} + +void putbyte_safe(byte b) { + putchar(b) + garbage[garbage_index] = b + garbage_index += 1 + __translate_st_to_errno() +} + +array __cmd_format_disk = "n0:disk,01"z + + +void rename_file(byte device, pointer old_name, pointer new_name) { + __cbm_cmd_buffer[0]='r' + __cmd_rename_copy_common(device, old_name, new_name) +} + +void copy_file(byte device, pointer old_name, pointer new_name) { + __cbm_cmd_buffer[0]='c' + __cmd_rename_copy_common(device, old_name, new_name) +} + +void __cmd_rename_copy_common(byte device, pointer old_name, pointer new_name) { + __cbm_cmd_buffer[1]='0' + __cbm_cmd_buffer[2]=':' + __cbm_cmd_buffer[3]=0 + strzappend(__cbm_cmd_buffer, new_name) + strzappendchar(__cbm_cmd_buffer, '=') + strzappend(__cbm_cmd_buffer, old_name) + __exec_disk(device) +} + + diff --git a/include/err.mfk b/include/err.mfk index fb478040..6c624180 100644 --- a/include/err.mfk +++ b/include/err.mfk @@ -5,6 +5,9 @@ enum error_number { err_outofmemory err_domain err_range + err_nofile + err_nodevice + err_eof } diff --git a/include/string.mfk b/include/string.mfk index b2783d36..f330376d 100644 --- a/include/string.mfk +++ b/include/string.mfk @@ -6,6 +6,14 @@ import string_fastpointers import string_fastindices #endif +void strzappend(pointer buffer, pointer str) { + strzcopy(buffer + strzlen(buffer), str) +} +void strzappendchar(pointer buffer, byte char) { + buffer += strzlen(buffer) + buffer[0] = char + buffer[1] = 0 +} word strz2word(pointer str) { byte i