Hello, world!

Welcome to my Project.log thread!

I want to do Advent of Code 2019 in Z80 assembler but I yet don’t know how much time I will need to devote to complete this task and what will be the difficulty.

Probably you already know it, but let’s do a recap: every day a puzzle in two parts gets unlocked on Advent of Code. Puzzles are introduced by a short story, one problem and some examples. In order to be able to answer you have to write the program that can answer for you. Solve the first problem and the  second part of the daily puzzle, which uses the same input text, gets unlocked. Solve both parts and you get two stars.

I already set up my openmsx emulating a Philips VG8020 (an MXS1 model) and a floppy drive. My starting plan is to use carts to run the code and an emulated floppy, or maybe two emulated floppies, to load the input text and save the temporary data.

I am not limiting myself to that single model or to floppies since during its life the MSX platform had four revisions (MSX, MSX2, MSX2+ and Turbo R) and hard disks, but for now it’s a start.

In order to assemble code I will use sjasm.

Now to get started on this project I need two things at the very least:

  • To be able to print some stuff on the screen.
  • To be able to read a single file from a floppy and use its content (like printing it on the screen).

For step number 1 I took inspiration and rewrite for sjasm the hello world example shown on ChibiAkumas, a quite interesting Z80 tutorial which talks about different Z80-based microcomputers. For step number 2 I found an interesting example on Msx Banzai via MSX Assembly Page

output helloworld.rom
defpage 0,$4000,$0010
defpage 1,$4010,$3FF0


page 0
code @ $4000
header ; standard MSX Cart Header
byte "AB" ; Fixed Header
word main ; Program Counter Start
word 0,0,0,0,0,0 ; Unused


page 1
code @ $4010
main ; Program code starts here
call $006F; ; Initialize the screen to graphic1 mode


ld a,$20 ; Setting screen width to 32 chars
ld ($F3B0),a


loop
ld hl,Message
call PrintString
call NewLine
call SecondWait
ld hl,Message2
call PrintString
call NewLine
call SecondWait
jp loop
di
halt ; stop cartridge execution


PrintChar equ $00A2 ; CHPUT(A) prints the character on the screen
NewLine:
push af
ld a,$d ; \n
call PrintChar
ld a,$a ; \r
call PrintChar
pop af
ret


PrintString:
ld a,(hl) ; Print a '0' terminated string
or a
ret z ; Exits if the character in the accumulator is '0'
inc hl ; Moves hl to the next character
call PrintChar
jr PrintString


SecondWait:
ld    b,$F8
ld    c,$CA
SecondWait_loop:
; padding code to waste some cycles
bit   $0,a ; Tests if the specified bit is set. Uses 8 cycles
bit $0,a
bit $0,a
and a,$FF ; 7 cycles
; now we decrement bc and test if it's zero
dec bc ; 6 cycles
ld a,c ; 4 cycles
or a,b ; 4 cycles
; when both b and c are zero then the test or a,b will set the z flag
; signifying that we can stop cycling
jp nz,SecondWait_loop ; 10 cycles, total = 55 states/iteration
ret


Message:  db 'Hello, World!',$0

Message2: db 'The MSX is pretty awesome.',$0



There’s a lot to unpack here

  • The header section is needed to make an msx recognize runnable code on a cartridge.
  • The main section just initializes the screen, then it loops printing the two messages, waiting for a second.
  • Note that for printing a single character on the screen I am calling an MSX Bios function CHPUT which reads the character stored in the accumulator (the ‘A’ register) and prints on the screen.
  • The PrintString function will cycle and continue to print characters pointed by HL until it finds the number 0. I modified this behaviour from the ChibiAkumas tutorial (which tests for 255), because strings in C are ‘0’ terminated.
  • The SecondWait function, which might wait, wrong calculations aside, for a second on MSX, was adapted from  this question on StackOverflow. The MSX1 platform did not contain a real time clock, MSX2 instead did. The modifications I have made to this code from the original were just to load the BC registers with the value $F8CA and not $0000. Note that $0000 works as a starting point because the register gets decremented, therefore becomes $FFFF, before the jump test.
Now I need to read a file from the floppy disk. That’s because I do not want to put the question text inside the rom as code. Eric Wastl, the father of Advent of Code, asked people to share the solutions but not to share the input files.

Reading the content of a file from MSX, coming from Java, Go or Python where it’s quite easy, is a lot more convoluted; in fact in order to read one file we need to

  • Prepare the file handle to access the file. (FCB)
  • Call the bios to complete the data of the FCB (open)
  • Tell the bios where do you want to save the data in ram
  • Tell the bios to transfer the data to ram

But it's not enough! In order to have access to the bios functions via the BDOS call we need first to allow our MSX machine to complete the scan of system roms. During the MSX bootstrap phase the operating system cycles between memory slots to see if they are occupied by ram, carts, peripherals such as disk drives and does the necessary initialization steps to prepare them for use. A game or a program that does not need all these features gets started by the boot process and then it does not relinquish the control to the operating system. A cartridge program which needs to see needs to see, as in our case, disk drives instead needs to do some preparatory steps and give back control to the BIOS: it needs to save the number of its cartridge slot and the pointer to its first post-boot program instruction in a ram area that’s seen by the bios (the H.STKE hook), then use a ret instruction to relinquish the control.

We could also make our rom be seen as a rom with a program written in BASIC and then run the program with a USR() call, but the H.STKE hook method at least is “consistent”.

Grauw has some good info on MSX-DOS 2 function calls. Do not worry if the documentation is about MSX-DOS2 because the operating system is still fully backward compatible. It also shows what function calls you might use if you want to write programs that are compatible with CP/M!

http://map.grauw.nl/resources/dos2_functioncalls.php
http://map.grauw.nl/resources/dos2_environment.php

The MSX Banzai example shows how to access files both under MSX1 and MSX2 and uses the H.STKE hooks, it also loads and saves files.

http://msxbanzai.tni.nl/dev/faq.html

How to develop a program in cartridge ROM shows how to set up the H.STKE hook and is commented a bit better than the MSX Banzai example.

https://www.msx.org/wiki/Develop_a_program_in_cartridge_ROM

Let’s write the program!

output  roms/helloFile2019.rom
defpage 0,$8000,$0010
defpage 1,$8010,$3FF0

page 0
code @ $8000
Header:
byte "AB"
word Init
word 0,0,0,0,0,0


page 1
code @ $8010
Init:                       ; Program code starts here


; Setup the hook H.STKE to run the ROM with disk support
; We copy the code in Callback to H_STKE
ld a,c            ; Backup the ROM slot number in a
ld hl,Callback    ; Source address
ld de,H_STKE      ; Destination address
ld bc,4           ; Number of bytes
ldir
; Put the ROM slot number into the hook
ld (H_STKE+1),a
; Returns control to Bios, so that it can go back to slots scanning
ret


Callback:               ; Callback data
rst 030h       ; Inter-slot call
db 1       ; Placeholder
dw Main       ; Address that will be executed after returning from BIOS


Main:
; Remove the Callback hook
ld a,0C9h
ld (H_STKE),a     


; Catch the error routine and use a routine we want to use
ld hl,(HOOK_InsertDiskPrompt)
ld (ERRADR),hl


; Set the 32 columns Screen 1 MSX Mode (for Text)
ld a,32
ld (LINL32),a
call   InitScreen


; if there is no drive in the system we will end the program
ld a,(H_PHYD)
cp 0C9h      
jr nz,LoadFile
ld hl,TXT_NoDrive
call PrintString
jp NeverEndLoop


LoadFile:
; set the DMA data using $1A

ld      de,FileBuffer    ; pointer to save buffer
ld      c,MsxDos1_SetDma ; function to call, sets the DMA, or better where we save or read
call    SafeBDOSCall


; Initializes the FCB data using $0F
; Basically we are now going to tell the system to load the "HELLO.TXT" file from disk


; Zeroing the FCB
    ld   hl,FCB
    ld   b,FCBSize+1
    call ZeroArea
; Copying "HELLO   TXT" into the correct FCB Area
ld de,FCB+1
ld hl,FCB_Hello
ld   bc,11
ldir
 ; Open the FCB
ld de,FCB           ; pointer to data to load
ld c,MsxDos1_OpenFcb ; function to call, opens the FCB
call SafeBDOSCall
or a
jp nz,FileError


; After opening the FCB now we can Read and Print the file to screen
    call ReadAndPrintFile
NeverEndLoop:
; Program end
jr NeverEndLoop


; Clears the RAM Area
ZeroArea:
    xor a
ZeroArea_Loop:
    ld (hl),a
    inc hl
    dec b
    jr nz,ZeroArea_Loop
    ret    


; Let’s print the file to screen
ReadAndPrintFile:
    xor a
    ld (FileBufferEnd),a
RAP_Loop:
    ld   hl,FileBuffer
    ld   b,FileBlockSize+1
    call ZeroArea
    ld   hl,FileBuffer
    ld   de,FCB         ; pointer to data to load
    ld c,MsxDos1_Read ; function to call, sets the DMA, or better where we save or read
    call SafeBDOSCall
    push af
        ld   hl,FileBuffer
        call PrintString
    pop af
    or a
    jr z,RAP_Loop
    ret


InsertDiskPrompt:
ld sp,($D000) ; Restore SP register
ld a,c     ; Get error flags
and 2
jp z,FileError ; Jumps if disk is present in drive


ld hl,TXT_InsertDiskPrompt
call PrintString 
FileError:
ld hl,TXT_LoadError
    call PrintString
    ld  hl,FCB+1
    call PrintString


TestReturnKey:
ld a,(NEWKEY+7)
bit 7,a
jr nz,TestReturnKey


jp LoadFile


; Print the text pointed by HL


PrintString:
ld   a,(hl) ; Prints a '0' terminated string
or   a
ret  z ; Exits if the character in the accumulator is '0'
inc  hl ; Moves hl to the next character
call PrintChar
jr   PrintString


SafeBDOSCall:
ld ($D000),sp ; Store SP register
jp BasicDos


; Data


TXT_NoDrive: ; Text pointer label
db "No disk installed!",LF,CR
db "Please turn off the MSX and connect a disk drive.",0 ; Zero indicates the end of text
TXT_LoadError:
    db "Load file error!!!",LF,CR,0
TXT_InsertDiskPrompt:
db "Please insert a disk in the disk drive",LF,CR
db "then press the RETURN key",LF,CR,0
HOOK_InsertDiskPrompt:
dw InsertDiskPrompt


FCB_Hello:
db "HELLO   TXT",0


; ;;; constants ;;;


LF:             equ $0A ; '\n'
CR:             equ $0D ; '\r'
FCBSize:        equ 128 ;
FileBlockSize:  equ 128 ;


; ;;; BIOS Functions ;;;


InitScreen: equ $006F   ; INIT32
PrintChar: equ $00A2   ; CHPUT(A) prints the character on the screen
BasicDos: equ $F37D   ; BDOS Calls the BasicDos using an MSX DOS Function as parameter


; ;;; MSX_DOS Functions ;;;


MsxDos1_SetDma:  equ $1A
MsxDos1_OpenFcb: equ $0F
MsxDos1_Read:    equ $14


; ;;; addresses of variables ;;;


FCB:            equ $C000
FileBuffer:     equ FCB + FCBSize
FileBufferEnd:  equ FileBuffer + FileBlockSize
; Extra space to keep a '0' character since we print our file as text


; ;;; addresses of BIOS variables ;;;


ERRADR: equ $F323
LINL32: equ $F3AF
H_STKE: equ $FEDA
H_PHYD: equ $FFA7

NEWKEY: equ $FBE5


And the effect is...


(original: https://www.asciiart.eu/holiday-and-events/christmas/trees)

Well, we can go to sleep happy that we cleared the first of many many many obstacles!

Comments

Popular posts from this blog

Santa's Challenge

Dec 1st 2015