; Monitor per lo Z80 computer
; v. 1.3

; Modifiche:
; 1.3 - Modifiche minori
; 1.2 - Jump Table, modifiche a routine input* e controllo stato Terminale prima di trasmettere
;     - inputCharM e printCharM non sono più macro (rinominate senza M)
; 1.1 - comandi I e O
; 1.0 - Prima Versione


startAddr:	equ	0x0000
ramAddr:	equ 0x2000
bufferAddr:	equ ramAddr+0x100
stackAddr:	equ ramAddr+0xFF

TERM_DATA_PORT:	equ	0x20		; porta dati del terminale
TERM_STAT_PORT:	equ 0x21		; porta stato del terminale
TERM_READY:		equ 0xAA		; valore che il terminale usa per indicare che è pronto a ricevere dati

CARRET:			equ 13			; codice per il carriage return
BACKSP:			equ 8			; codice per il backspace
; -------

		org startAddr

; Jump Table: a posizioni multiple di 4 ci sono dei jump a funzioni utili da usare
; nei programmi utente. Le istruzioni a indirizi multipli di 8 indicate con una X
; possono essere richiamate con l'istruzione RST x
		jp init				; 0x00 X - boot / riavvio
		nop					; padding
		jp editor			; 0x04   - chiama l'editor per permettere all'utente di inserire la line di comando.
		nop					;        - Chiamare con l'indirizzo nel buffer in HL, la dimensione in DE, torna quando incontra un carriage return.
		jp printChar		; 0x08 X - Stampa il carattere ASCII in A sul terminale
		nop					;
		jp printHexHL		; 0x0C   - 
		nop					;
		jp printHex			; 0x10 X -
		nop					;
		jp init				; 0x14   - 
		nop					;
		jp printString		; 0x18 X -
		nop					;
		jp init				; 0x1C   - 
		nop					;
		jp inputChar		; 0x20 X - Legge un carattere ASCII in A dal terminale
		nop					;
		jp inputHexDE		; 0x24   - Legge quattro cifre esadecimali in HL
		nop					;
		jp inputHex			; 0x28 X - Legge una coppia di cifre esadecimali in A
		nop					;
		jp init				; 0x2C   - 
		nop					;
		jp init				; 0x30 X -
		nop					;
		jp init				; 0x34   - 
		nop					;
		jp init				; 0x38 X -
		nop					;
		jp init				; 0x3C   - 
		nop					;
							; 0x40
; ----- Codice inizializzazione -----

init:	ld SP, stackAddr
		ld HL, msgwelcome
		call printString

main:	ld HL, msgprompt
		call printString

		ld HL, bufferAddr		; HL è il puntatore al primo posto libero nel buffer di editing
		ld DE, 0xFF
		call editor				; avvia l'editor della linea di comando

		; Ora si interpreta ed esegue il comando inserito nel buffer
exec:	ld HL, bufferAddr		; reimposto il puntatore al buffer

		call skipSpaces

		ld A, (HL)
		inc HL
		call skipSpaces

		cp 's'
		jp Z, setCmd
		cp 'S'
		jp Z, setCmd

		cp 'd'
		jp Z, dumpCmd
		cp 'D'
		jp Z, dumpCmd

		cp 'e'
		jp Z, exeCmd
		cp 'E'
		jp Z, exeCmd

		cp 'i'
		jp Z, inCmd
		cp 'I'
		jp Z, inCmd

		cp 'o'
		jp Z, outCmd
		cp 'O'
		jp Z, outCmd

		ld HL, msgerror			; Se non è stato riconosciuto il comando, mostra il messaggio di errore
		call printString

		jr main

; ------------ Editor ------------
; entra con l'indirizzo nel buffer in HL e la dimensione in DE
editor:
		push AF					; salviamo AF, BC, HL
		push BC
		push DE
		push HL

		ld B, H					; salvo l'indirizzo iniziale del buffer (come ld BC, HL)
		ld C, L
		add HL, DE
		ex DE, HL				; (come ld DE, HL)
		dec DE					; in DE metto l'indirizzo finale del buffer
		dec DE					; faccio spazio per il carriage return finale
		dec DE					; e per il terminatore 0
		ld H, B					; ripristino HL
		ld L, C

;termNoReady:
;		call getTermStat		; Controlliamo che il terminale sia pronto
;		cp TERM_READY
;		jp NZ, termNoReady		; e ripetiamo se non lo è

editorLp:						; editor Loop
		call inputChar
		cp CARRET				; è un a capo?
		jp Z, editorEnd			; si, il nostro lavoro è finito
		cp BACKSP				; è un backspace?
		jr NZ, noBack			; no, passa oltre
		push HL					; si:
		scf						; testa se il puntatore è già all'inizio del buffer
		ccf						; mi assicuro che il carry sia disattivo
		sbc HL, BC				; faccio un compare
		pop HL					; ripristino HL (non modifica i flag)
		jp z, editorLp			; se sono uguali, ritorno al ciclo
		dec HL					; altrimenti, toglie una posizione nel buffer
		call printChar			; stampa il \b
		ld a, ' '				; uno spazio per cancellare il carattere precedente
		call printChar
		ld a, BACKSP			; e un'altro backspace per spostare il cursore
		call printChar
		jr editorLp				; conclude

noBack: cp ' '					; se è un carattere di controllo (che non è già stato gestito)
		jp M, editorLp			; non stampare e ricomincia
		cp 127					; se è un carattere fuori dall'ASCII standard
		jp P, editorLp			; non stampare e ricomincia

		push HL					; carattere buono, verifica disponibilità nel buffer
		scf
		ccf						; carry disattivo
		sbc HL, DE				; cmp HL, DE
		pop HL					; se sono uguali ritorna al ciclo
		jr Z, editorLp			; (non viene stampato il carattere)

		ld (HL), A				; altrimenti inserisco nel buffer
		inc HL

print:	call printChar			; echo del carattere
		jr editorLp

editorEnd:
		ld (HL), A				; inserisco nel buffer e
		call printChar			; stampa il carriage return
		inc HL					; inserisco un carattere null
		xor A
		ld (HL), A

		pop HL					; ripristiniamo i registri
		pop DE
		pop BC
		pop AF
		ret


; --------- Comandi Monitor ---------
dumpCmd:call inputHexDE		; input primo indirizzo
		jr NC, dcInvChar2	; car. non valido
		push DE
		call inputHexDE		; input secondo indirizzo
		jr NC, dcInvChar2	; car. non valido
		push DE
		pop BC
		pop HL
		call memDump
		jp main

dcInvChar1:
		pop DE
dcInvChar2:
		ld HL, msgInvChar
		call printString
		jp main

;--
setCmd:	call inputHexDE		; input indirizzo
		jr NC, scInvChar
		push DE
		pop BC				; usa BC come puntatore in memoria
		call skipSpaces

scLoop:	ld A, (HL)			; prendi il carattere attuale del buffer (non incrementare)
		cp CARRET			; se è il carriage return
		jp z, main			; ritorna al main
		call inputHex		; altrimenti input dato
		jr NC, scInvChar	; car. non valido
		ld (BC), A			; inserisco in memoria
		inc BC				; incremento puntatore
		jr scLoop			; ripeto

scInvChar:
		ld HL, msgInvChar
		call printString
		jp main

;--
exeCmd:	call inputHexDE		; prende l'indirizzo
		jr NC, ecInvChar		; car. non valido
		push DE
		pop HL				; lo carica in HL
		ld DE, retHere		; simuliamo una call
		push DE				; inserendo l'indirizzo di ritorno nello stack
		jp (HL)				; e saltando a quell'indirizzo
retHere:jp main				; con un return torniamo qui

ecInvChar:
		ld HL, msgInvChar
		call printString
		jp main

;--
inCmd:	call inputHexDE		; prende il numero di porta
		jr NC, icInvChar		; car. non valido
		push DE
		pop BC				; il numero di porta è in BC
		in A, (C)			; lo Z80 in realtà mette anche B nei bit più significativi dell'address bus
		call printHex		; stampo il valore ottenuto
		jp main

icInvChar:
		ld HL, msgInvChar
		call printString
		jp main

;--
outCmd:	call inputHexDE		; prende il numero di porta in DE
		jr NC, ocInvChar		; car. non valido
		push DE
		pop BC				; il numero di porta è in BC
		call inputHex		; prende il valore da scrivere in A
		jr NC, ocInvChar		; car. non valido
		out (C), A			; output del dato
		jp main

ocInvChar:
		ld HL, msgInvChar
		call printString
		jp main

; ########### Funzioni ###########


; Avanza HL finchè non trova un carattere che non è uno spazio o un carattere di blank
skipSpaces:
		push AF
ssLoop:	ld A, (HL)
		cp ' '
		inc HL
		jr Z, ssLoop
		dec HL
		pop AF
		ret


; Esegue un dump della memoria, l'indirizzo di partenza è in HL,
; l'indirizzo di fine in BC
memDump:
		push AF
		push BC
		push HL

mdPrintAddr:
		ld A, CARRET
		call printChar
		call printHexHL		; stampa l'indirizzo all'inizio della riga
		ld A, ':'
		call printChar

mdLoop:	ld A, ' '
		call printChar
		ld A, (HL)
		call printHex

		; Nota: c'è un bug nel codice sottostante, quando si cercano di comparare un numero minore di 0x8000 e
		; un numero maggiore o uguale a 0x8000. Questo perchè lo Z80 li interpreta il primo come un numero
		; positivo, e il secondo come un numero negativo, verificando così la condizione di uscita già alla
		; prima iterazione. Se prima c'è un numero maggiore e per secondo un minore, la condizione si verifica al
		; contrario e va avanti a stampare finchè non va in overflow e raggiunge il secondo indirizzo.
		; Si è scelto di non correggerlo per non complicare inutilmente il codice.
		push HL				; vede se siamo arrivati alla fine
		sbc HL, BC
		pop HL
		jp P, mdEnd			; termina quando HL-BC > 0 (cioè HL diventa maggiore di BC)
							; termina a > e non >= perchè incrementiamo dopo il contatore
		inc HL				; incrementa il puntatore

		ld A, L				; se l'indirizzo è multiplo di 16
		and 0x0F
		jr Z, mdPrintAddr	; stampa l'indirizzo

		jr mdLoop

mdEnd:	pop HL
		pop BC
		pop AF
		ret

; Stampa una stringa (terminata da 0) puntata da HL
printString:
		push AF
		push HL
psLoop: ld A, (HL)
		cp 0
		jr Z, psEnd
		call printChar
		inc HL
		jr psLoop
psEnd:	pop HL
		pop AF
		ret

;Stampa il contenuto di HL come 4 cifre esadecimali
printHexHL:
		push AF
		ld A, H
		call printHex
		ld A, L
		call printHex
		pop AF
		ret


; Stampa il contenuto di A come 2 cifre esadecimali
printHex:
		push AF
		rrca				; Ottengo il nibble più significativo
		rrca
		rrca
		rrca
		call printNibble	; stampo il più significativo
		pop AF
;		call printNibble	; stampo il meno significativo
;		ret
; Le istruzioni sopra non sono necessarie, perchè ora il flusso del programma
; cadrà nella funzione printNibble automaticamente, salvando qualche ciclo e 4 byte

; Stampa il nibble meno significativo di A come una cifra esadecimale
printNibble:
		push AF
		and 0x0F
		cp 0x0A
		jp M, pnNotAlpha
		add A, 'A'-'0'-10	; Se è una lettera, devo sommare l'offset per le lettere (togliendo l'offset dai numeri)
pnNotAlpha:
		add A, '0'			; in qualsiasi caso, sommo l'offset per i numeri
		call printChar
		pop AF
		ret


; Input un numero esadecimale da 1 byte in A dal buffer.
; Se incontra caratteri non validi, ritorna con carry attivo.
; Termina l'input quando incontra uno spazio un ritorno a capo
; o ottiene 2 cifre esadecimali (può leggere anche una sola cifra)
inputHex:
		push BC
		xor A, A
		call skipSpaces

		ld B, 2					; prende al massimo 2 cifre
ihLoop:	ld C, A
		call inputHexNibble
		jr NC, ihEnd			; carattere non valido, termina
		jr Z, ihEnd				; se è un terminatore, termina
		sla C					; shifto C
		sla C
		sla C
		sla C
		or C					; aggiungo il nuovo nibble
		djnz ihLoop				; ripeti
		ccf						; azzero il carry settato da djnz
		ld C, A

ihEnd:	ld A, C
		pop BC
		ret

;Input di un numero da 2 byte in DE dal buffer. Valgono le considerazioni per inpuHex
inputHexDE:
		push BC
		xor A, A
		ld D, A
		call skipSpaces

		ld C, 4					; prende al massimo 4 cifre

ihHLlp:	ld E, A
		call inputHexNibble
		jr NC, ihHLEnd			; carattere non valido, termina
		jr Z, ihHLEnd			; se è un terminatore, termina

		ld B, 4
shl16:	sla E					; shifto DE a sinistra di 4 posti
		rl D
		djnz shl16

		or E					; aggiungo il nuovo nibble
		dec C
		jr NZ, ihHLlp			; ripeti
		ld E, A
		ccf						; serve perchè il dec attiva il flag, e potrebbe essere interpretato come un errore
ihHLEnd:
		pop BC
		ret

; Input di una singola cifra esadecimale
; se il carattere non è valido, ritorna con il carry DISattivo
; ritorna con il flag zero attivo se il carattere è un terminatore
inputHexNibble:
		ld A, (HL)			; prendo il carattere dal buffer

		cp ' '
		jr Z, inTerm		; ritorna se il carattere è uno spazio
		cp CARRET
		jr Z, inTerm		; ritorna se il carattere è un a capo

		inc HL				; incremento dopo così, se il carattere è un terminatore, HL punta al terminatore

		cp '0'				; controlla se è minore di '0'
		jp M, inInvalidChar
		sub '0'
		cp 10				; controlla se è un numero
		ret M

		cp 'A'-'0'			; controlla se è minore di 'A'
		jp M, inInvalidChar
		sub 'A'-'0'-10
		cp 16				; controlla se è una lettera maiuscola
		ret M

		cp 'a'-'A'+10		; controlla se è minore di 'a'
		jp M, inInvalidChar
		sub 'a'-'A'
		cp 16				; controlla se è una lettere minuscola
		ret M

inInvalidChar:				; se non si è fermato prima, è un carattere non valido
		scf					; setta e complementa il carry per disattivarlo
inTerm:
		ccf					; Se è un terminatore, il carry non sarà attivo perciò lo attiviamo invertendolo
		ret					; ritorna

;------------------------------------------------------------------
;              Routine principali di I/O a terminale
;------------------------------------------------------------------

; Scrive carattere nel terminale
printChar:
		out (TERM_DATA_PORT), A
		ret

; Legge carattere dalterminale
inputChar:
		in A, (TERM_DATA_PORT)
		ret

; Legge lo stato del terminale
getTermStat:
		in A, (TERM_STAT_PORT)
		ret

; ########### Stringhe ###########
msgwelcome:		db "Z80 Monitor 1.3 OK\n", 0
msgprompt:		db "\n->", 0
msgerror:		db "Errore", 0
msgInvChar:		db "Carattere non valido", 0



