Using CMDR-DOS in 65c02 assembly

Tutorials and help articles.

(Posts require approval. See pinned post.)
Forum rules
Post guides, tutorials, and other instructional content here.

This topic area requires approval, so please be patient while we review content to make sure it fits the expectations for this topic area.

Tech support questions should be asked in Hardware or Software support.
Post Reply
Davideesk
Posts: 4
Joined: Wed Nov 04, 2020 7:35 am

Using CMDR-DOS in 65c02 assembly

Post by Davideesk »


I couldn't find any resources on how to create and change directories on the SD card using assembly, so I wrote some helper functions based off the code in "x16-rom/basic/x16additions.s". These functions show how to use the KERNAL API to send commands and get file/directory names from CMDR-DOS.

I wrote the functions to be simple, so error & SD card checking will be left up to the reader. The code itself was written using the ACME assembler, but it should be easy to convert it to other assemblers.

Code: https://gist.github.com/DavidSM64/a036a622f4b7a2d8364c355e72766eb7

The 3 functions are:


  1. execute_dos_command


    • Takes in a string pointer that contains a channel 15 command and executes it.


    • A table of commands can be found here: https://github.com/commanderx16/x16-rom/tree/master/dos (It is the large table that starts with "BLOCK-ALLOCATE")


    • Examples:


      • "MD:FOO" = Creates a new directory called FOO


      • "CD:FOO" = Enters the FOO directory


      • "CD:.." = Enters the parent directory


      • "RD:FOO" = Deletes the FOO directory






  2. get_dos_directory


    • Takes in a string pointer that contains valid directory syntax (see examples), the length of that input string, and an output pointer where the result will be written to.


    • This function isn't particularly useful, since it also outputs the formatting used in BASIC.


    • Examples:


      • "$" = List out everything in the current directory


      • "$:*=P" = Only list out files


      • "$:*=D" = Only list out directories


      • "$:FOO*=D" = Only list out directories that start with FOO (FOO, FOO2, FOOBAR, etc.)




    • The output will look like this:


      • spacer.png


      • The first 4 bytes can be ignored


      • For each line:


        • 2 bytes at the start for the block count


        • Then follows a certain number of bytes (might be a variable amount, the line ends with $00)








  3. get_dos_dir_entries


    • Same inputs as get_dos_directory


    • This function only outputs the file / directory names, separated by a $00.


    • Returns the number of entries in the current directory into the accumulator.


    • The output will look like this:


      • spacer.png


      • This will not show the file type, but files typically have an extension while directories do not.






 

Johan Kårlin
Posts: 292
Joined: Wed Jun 03, 2020 11:33 am
Location: Kalmar, Sweden

Using CMDR-DOS in 65c02 assembly

Post by Johan Kårlin »

Very useful! I have been wondering if and how you for example can load all files with a certain extension. With help of your third function this seems absolutely doable. [emoji4]
lamb-duh
Posts: 63
Joined: Fri Jul 10, 2020 7:46 pm

Using CMDR-DOS in 65c02 assembly

Post by lamb-duh »


Could you post the symbol definitions you're using? (or point to where i could find the right ones). Some of the functions, like BASIN I can't find any information on.

Davideesk
Posts: 4
Joined: Wed Nov 04, 2020 7:35 am

Using CMDR-DOS in 65c02 assembly

Post by Davideesk »



2 hours ago, lamb-duh said:




Could you post the symbol definitions you're using? (or point to where i could find the right ones). Some of the functions, like BASIN I can't find any information on.



You can find BASIN in the programmer's reference manual under the Commodore 64 API compatibility section.

Here is the include file I am currently using: https://pastebin.com/raw/vuBfkYpZ

lamb-duh
Posts: 63
Joined: Fri Jul 10, 2020 7:46 pm

Using CMDR-DOS in 65c02 assembly

Post by lamb-duh »



15 minutes ago, Davideesk said:




You can find BASIN in the programmer's reference manual under the Commodore 64 API compatibility section.



Here is the include file I am currently using: https://pastebin.com/raw/vuBfkYpZ



oh, I was sure I had looked there! I've mostly been using this commodore reference which calls it CHRIN. Thank you!

Davideesk
Posts: 4
Joined: Wed Nov 04, 2020 7:35 am

Using CMDR-DOS in 65c02 assembly

Post by Davideesk »


Hello again everyone. I've been wanting to make a game for the X16 lately, but for what I want to do I needed to figure out a good way to stream data off a file from any random position. You can't do that yet within the r38 emulator (as far as I know). You would have to split up the file into smaller ones. However, you can do this with the POSITION command in CMDR-DOS. This does mean you must have an SD card for this to work.

The syntax for the POSITION command is:


Quote




P <channel id> <low byte> <middle-low byte> <middle-high byte> <high byte>



You have to specify both the channel the file is on and the 32-bit position you want to use. Those 5 arguments must be actual byte values, and not ascii representations of the values!

In BASIC, it is as simple as:


Quote




10 OPEN 1,8,2,"TEST,P,R"                                                                      REM Open the file "TEST" from the SD card on channel 2

20 DOS"P"+CHR$(2)+CHR$($80)+CHR$(0)+CHR$(0)+CHR$(0)  REM Set the position of the file open on channel 2 to be offset $00000080

30 FOR X=1 TO 10: GET#1,A$: PRINT A$: NEXT X                          REM Read and print out the 10 characters from that position

40 CLOSE 1                                                                                            REM Close the file



REM Note: I don't know a lot about BASIC. If there is a better way to print out the characters then let me know!



Assembly is more verbose, but it is basically the same idea. You first open the file, set the file offset, read bytes into memory, then close the file.

Because there is a lot more code to go through I have included an attachment with the source code of the example. I personally use the ACME assembler, but it shouldn't be difficult to adapt to other assemblers. You should be able to get a program file when you assemble main.asm. Once you have the .prg file built, place it and the TEST file in an SD card image and run it in the emulator.

The TEST file includes a bunch of ascii text. I'm interested in retrieving the "HELLO X16!" string without going through any of the padding.

spacer.png

The first thing we need to do is open the file. (All of the following code will be in the attachment, so you don't need to copy & paste)

; fs_open

; Opens a file to be read.

; Arguments:

;   r0 = Pointer to a null-terminated string that is the filepath to open.

;   r1L = Channel to open.

; Assumptions:

;   * SD Card is being used and is set to device #8

;   * String in r0 is less than 256 characters.

;   * Channel is not 0, 1, or 15.

fs_open:

!zone {

       +set_ram_bank RAM_BANK_KERNAL_DOS ; Make sure the ram bank is set to 0.

       lda (r0)                          ; Load the first character in the string

       bne .setup                        ; Checking for the null-terminator

       rts                               ; End early if the input is an empty string.

.setup:

       ldx r1L    ; Open passed in channel

       jsr CHKIN 

       lda #8     ; SD Card in device #8

       jsr LISTEN

       lda r1L

       ora #$F0   ; $FX = Open file in channel X (r1L)

       jsr SECOND

.loop_init:

       ldy #0 ; Start at the first character.

.send_char:

       lda (r0),Y            ; Read the current character

       beq .cleanup_and_exit ; Exit if the byte is the null terminator

       jsr IECOUT            ; Send character to the serial bus

       iny                   ; Move to the next character

       bra .send_char        ; Keep looping

.cleanup_and_exit:

       jmp UNLSN  ; Stop listening and send the filename.

}


This function was adapted from my execute_dos_command function that I shared in my original post, since opening up channel 15 is how you execute dos commands in the first place. Do note that the high-ram bank must be set to zero for this code to work.

Speaking of which, we are going to use a modified execute_dos_command function to run the POSITION dos command, but first we have to setup that command in RAM:

; The channel 15 command that sets the position of a file. 

DOS_FILE_POSITION!scr "P"

DOS_FILE_POSITION_CHANNEL!byte 0

DOS_FILE_POSITION_OFFSET!byte 0000


DOS_FILE_POSITION is used as the string pointer for the command. DOS_FILE_POSITION_CHANNEL points to the channel id, and DOS_FILE_POSITION_OFFSET points to the 32-bit offset. In the attachment I created a bunch of macros to help me simplify the code. I'm not going to go through them since it would take too long, so here is an equivalent to the line 20 of the basic code:



lda #2 ; Using channel 2

sta DOS_FILE_POSITION_CHANNEL ; Set the channel of the file to 2

lda #$80 ; "HELLO X16!" is located at offset $80 of the TEST file

sta DOS_FILE_POSITION_OFFSET ; Store $80 to the low byte

; I technically don't need these 3, since they are never modified.

; stz DOS_FILE_POSITION_OFFSET + 1 ; Store 0 to the middle-low byte

; stz DOS_FILE_POSITION_OFFSET + 2 ; Store 0 to the middle-high byte

; stz DOS_FILE_POSITION_OFFSET + 3 ; Store 0 to the high byte

lda #<DOS_FILE_POSITION ; Load the DOS_FILE_POSITION pointer into r0

sta r0L

lda #>DOS_FILE_POSITION

sta r0H

lda #6

sta r1L ; Length of the POSITION command (6 bytes)

jsr execute_dos_command_n ; Execute the command to set the file offset to $80




Now that we have the position ready, we now need to read the bytes from the file. To do this we need to set the ROM bank to 2, since that bank contains the function to read bytes. That function is called file_read_block with the arguments: .A is the number of bytes to read and .Y/.X is the destination address to write to. To help with this I wrote some macros to save the ROM bank & call the file_read_block function:

; Read bytes from a file into RAM.

!macro m_fs_read_to_address .dest, .length {

       ldx #<.dest

       ldy #>.dest

       lda #.length

       jsr file_read_block ; ROM bank must be set to 2 before this is called!

}



; Saves the current ROM bank and sets it to the CBDOS bank.

!macro m_fs_start_reading {

       +push_current_rom_bank_onto_stack

       +set_rom_bank ROM_BANK_CBDOS ; ROM_BANK_CBDOS = 2

}



; Restores the previous ROM bank.

!macro m_fs_stop_reading {

       +pull_current_rom_bank_from_stack

}


You can find the macros: set_rom_bank, push_current_rom_bank_onto_stack, and pull_current_rom_bank_from_stack in the banks.inc file in the attachments source code.

After the bytes are read, the file finally needs to be closed so that the channel can be used for other files. To do that you simple call the fs_close function.

; fs_close

; Closes a channel so that it can be used again.

; Arguments:

;   r0L = Channel to close.

; Assumptions:

;   * SD Card is being used and is set to device #8

;   * String in r0 is less than 256 characters.

;   * Channel is not 0, 1, or 15.

fs_close:

!zone {

    ldx r0L    ; Open passed in channel

    jsr CHKIN 

    lda #8     ; SD Card in device #8

    jsr LISTEN

    lda r0L

    ora #$E0   ; $EX = Close file in channel X (r0L) 

    jsr SECOND

    jmp CLRCHN ; Close the channel and return

}


When you look at the main.asm file, you'll see these macros being used:

FILENAME!SCR "TEST"0



CHANNEL_FILE = 2   ; Channel to use. Can be any value within [2, 14]

SRC_OFFSET   = $80 ; Position within the file

DEST_ADDRESS = $20 ; RAM address to write to

READ_LENGTH  = 10  ; Number of bytes to copy



main:

    +m_fs_open CHANNEL_FILE, FILENAME ; Open the file

        +m_fs_set_file_position CHANNEL_FILE, SRC_OFFSET ; Set the current position of the file.

        +m_fs_start_reading ; Saves the current ROM bank, and sets it to the CBDOS code bank.

            +m_fs_read_to_address DEST_ADDRESS, READ_LENGTH ; Read data from the file into RAM.

        +m_fs_stop_reading ; Restores the original ROM bank.

    +m_fs_close CHANNEL_FILE ; The channel must be closed before using it again.

exit:

    rts ; Return to basic.


I'm hoping that those macros make it easy to understand what is happening conceptually. You can look at fs.asm for more information on what they are actually doing. The indenting isn't required, I just think it looks nice (like python).  

When you run the code in the emulator you'll see that nothing happens on the basic screen. When you dump the RAM, you should see that the "HELLO X16!" message appeared at offset $20.

spacer.png

If you have any question or comments, then please leave a reply and I'll try to get back to you as soon I can.

 

filepos-example-v1.1.zip

Davideesk
Posts: 4
Joined: Wed Nov 04, 2020 7:35 am

Using CMDR-DOS in 65c02 assembly

Post by Davideesk »


Ah, so I made a mistake by using the execute_dos_command function with the POSITION command. The function expects an input string to end with a null terminator (0x00). Since the arguments for the POSITION command are binary, it is likely that one of the five arguments will be 0x00. I've created a new function, execute_dos_command_n, to take this into account. It takes in an additional argument for the length of the string. For the POSITION command, this argument would be 6. I've updated the attached filepos-example code to use this function instead of the original.

; execute_dos_command_n

; Processes a CMDR-DOS channel 15 command. Use when you know the exact number of characters

; that need to be sent. This is needed for the POSITION command to be processed correctly.

; Arguments:

;   r0 = Pointer to a string that is a valid DOS command.

;   r1L = Length of the input string

; Command Examples:

;   "MD:FOO" creates a new directory called FOO

;   "CD:FOO" will enter the FOO directory

;   "CD:.." will enter the parent directory

;   "RD:FOO" will delete the FOO directory

; Assumptions:

;   * SD Card is being used and is set to device #8

execute_dos_command_n:

!zone {

    lda r1L

    bne .setup ; Continue if the length > 0

    rts        ; End early if the length was 0.

.setup:

    ldx #15    ; Open Channel 15 to send DOS commands.

    jsr CHKIN 

    lda #8     ; SD Card in device #8

    jsr LISTEN

    lda #$6F   ; $6X = LISTEN to channel X, so $6F = LISTEN to channel 15

    jsr SECOND

.loop_init:

    ldy #0 ; Start at the first character.

.send_char:

    lda (r0),Y     ; Read the current character

    jsr IECOUT     ; Send character to the serial bus

    iny            ; Move to the next character

    cpy r1L        

    bne .send_char ; Keep looping if y < r1L

.cleanup_and_exit:

    jsr UNLSN  ; Stop listening and send the command.

    jmp CLRCHN ; Close the channel and return

}


Post Reply