mirror of
https://github.com/KarolS/millfork.git
synced 2024-12-31 14:30:50 +00:00
C64: File I/O support
This commit is contained in:
parent
7d596f3ed6
commit
492300d298
@ -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
|
||||
|
70
docs/api/commodore-programming-guide.md
Normal file
70
docs/api/commodore-programming-guide.md
Normal 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
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
81
docs/stdlib/cbm_file.md
Normal 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.
|
@ -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.
|
||||
|
@ -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
|
||||
|
42
examples/c64/multifile.ini
Normal file
42
examples/c64/multifile.ini
Normal 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
|
||||
|
||||
|
23
examples/c64/multifile.mfk
Normal file
23
examples/c64/multifile.mfk
Normal 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()
|
||||
}
|
@ -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
|
||||
|
@ -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
219
include/cbm_file.mfk
Normal 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)
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,9 @@ enum error_number {
|
||||
err_outofmemory
|
||||
err_domain
|
||||
err_range
|
||||
err_nofile
|
||||
err_nodevice
|
||||
err_eof
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user