C64: File I/O support

This commit is contained in:
Karol Stasiak 2019-01-05 01:19:14 +01:00
parent 7d596f3ed6
commit 492300d298
15 changed files with 485 additions and 2 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

81
docs/stdlib/cbm_file.md Normal file
View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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()
}

View File

@ -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

View File

@ -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)
}

219
include/cbm_file.mfk Normal file
View File

@ -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)
}

View File

@ -5,6 +5,9 @@ enum error_number {
err_outofmemory
err_domain
err_range
err_nofile
err_nodevice
err_eof
}

View File

@ -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