LXESP32






Antonio Giuliana, 2025
2
In questa terza ed ultima parte che tratta di questa mia implementazione, esamineremo il codice
del firmware e del software che ho creato, modificato e integrato per rendere funzionante il
sistema nel suo complesso.
Per tutto il codice, viene presentata l’ultima versione stabile, tenendo presente comunque che
questo è in evoluzione, sia per eliminare i bug che si possono presentare, sia per implementare
nuove funzionalità o completare alcune parti che non sono operative al 100%. Sono apprezzati
consigli e suggerimenti da parte di chi vorrà migliorare le prestazioni o introdurre nuove
funzionalità nel codice.
Ovviamente il punto centrale sarà il firmware creato appositamente per il microcontrollore
ESP32, che verrà esaminato nel maggior dettaglio possibile. Descriverò anche il codice
implementato nelle EPROM di NE a partire dal loro contenuto originale (sia quella per il boot
del NE-DOS versione testo EP1390, sia quella del BASIC da 16 K EP548); infine commenterò
brevemente il codice creato per i nuovi comandi che ho aggiunto al NE-DOS.
3
Il codice coinvolto
Per tutto il progetto si è reso necessario scrivere codice per le varie parti utilizzando diversi
tool e linguaggi. Le varie parti che compongono tutto il sistema sono
Firmware
dell’ESP32
scritto in C/C++ con librerie
specifiche per ESP32 (nell’ambiente
IDE Arduino/ESP32)
emula il funzionamento delle schede
- LX385, interfaccia cassette
- LX389, interfaccia stampante
- LX390, interfaccia floppy disk
- LX547, gestione interrupt
-
(LX683, interfaccia hard disk)
1
Codice della
EP1390(F)
scritto in assembly Z80, compilato
con ZASM 4.4.17, modifica del
codice di NE per memoria video da
0xFE00
implementazione riconoscimento della scheda
LXESP32, ottimizzazione codice
Codice della
EP548(F)
riscrittura codice per introduzione nuovi comandi
BASIC
e gestione LXESP32,
ottimizzazione
Comando
PRNRESET
scritto in assembly Z80, compilato
con ZASM 4.4.17, utilizzando
primitive NE-DOS
nuovo comando NE-DOS per cancellare e ricreare
file associato alla stampante su SD
Comando
SDCOPY
nuovo comando NE-DOS per copiare file da NE-
DOS a SD
CARD
e viceversa
Comando
TIMESYNC
nuovo comando NE-DOS per impostare data/orario
sincronizzato da Internet
4
Il firmware della ESP32
La scheda LXESP32 ospita il modulo con il microcontrollore ESP32; la sua interfaccia
hardware con la CPLD e il Bus del NEZ80 è stata descritta nelle parti precedenti. Il firmware
che controlla tutte le funzionalità è scritto in C++ ed è suddiviso in molti file sorgenti, tra cui
Nome file sorgente Descrizione contenuto
LXESP32.ino
File sorgente principale contenente le funzioni setup(), loop() e di gestione interrupt
LXESP32.h
Include principale, definizione costanti per il file LXESP32.ino
INTRDCode.inc
Codice gestione interrupt durante accesso in lettura dello Z80
INTWRCode.inc
Codice gestione interrupt durante accesso in scrittura dello Z80
LOOPRDCode.inc
Codice loop servizio dopo interrupt in lettura
LOOPWRCode.inc
Codice loop servizio dopo interrupt in scrittura
FDCommands.cpp
Codice gestione comandi per interfaccia FD
D
FDCommands.h
Include con definizione costanti per il file FDCommands.cpp
FSHandle.cpp
Codice gestione File System ad alto livello
FSHandle.h
Include con definizione costanti per il file FSHandle.cpp
ExtCommands.cpp
Codice gestione comandi estesi
ExtCommands.h
Include con definizione costanti per il file ExtCommands.cpp
HDCommands.cpp
Codice gestione comandi Hard Disk (
non implementato
)
HDCommands.h
Include con definizione costanti per il file HDCommands.cpp (
non implementato
)
CMCommands.cpp
Codice gestione comandi Coprocessore Matematico (
non implementato
)
CMCommands.h
Include con definizione costanti per il file CMCommands.cpp (
non implementato
)
varCDP1854Tape.inc
Include con variabili per il supporto emulazione Tape (chip CDP1854)
varCMATH.inc
Include con variabili per il supporto Coprocessore Matematico (
non
implementato
)
varFD1771.inc
Include con variabili per l’emulazione del chip FD1771 (FD controller)
varFDDExt.inc
Include con variabili per il supporto comandi estesi
varHDISK.inc
Include con variabili per il supporto Hard Disk (
non
implementato
)
varLIVEVRS.inc
Include con variabili per il supporto controllo versione firmware
varPrinter.inc
Include con variabili per il supporto emulazione Printer
La versione attuale è la 1.35 ed è stabile. Il codice è vasto e possono essere descritte soltanto
alcune parti e a grandi linee. I sorgenti sono disponibili per l’approfondimento.
Un aspetto fondamentale, su cui si basa tutto il firmware, è l’uso di due linee di interrupt legate
alla richiesta della CPU Z80 di leggere o scrivere una porta tra quelle previste per le
funzionalità supportate. All’interno dell’hardware della CPLD, come descritto nelle parti
precedenti, è previsto un flip-flop che viene attivato ad ogni interrupt di questo tipo e che mette
lo Z80 in stato di WAIT. In questo modo il microcontrollore viene avvisato della richiesta da
parte dello Z80 e quest’ultimo rimane in attesa del completamento delle operazioni richieste;
alla fine, il flip-flop viene resettato con un comando specifico del firmware e lo Z80 ritorna ad
5
eseguire il suo codice (macro CPURESETWAIT). Tale macro è definita nel seguente modo e
genera un segnale negativo sul pin 27
#define
WAIT_0
\
*((uint32_t *)GPIO_OUT_REG) = *((uint32_t *)GPIO_OUT_REG) & 0b11110111111111111111111111111111; \
__asm__ __volatile__("nop")
#define WAIT_1 \
*((uint32_t *)GPIO_OUT_REG) = *((uint32_t *)GPIO_OUT_REG) | 0b00001000000000000000000000000000; \
__asm__ __volatile__("nop")
#define CPURESETWAIT \
WAIT_0; \
WAIT_1
setup()
La funzione setup() è quella che viene eseguita una sola volta al Reset del microcontrollore e
in questa è contenuto il codice che prepara tutte le risorse per il funzionamento successivo della
funzione loop(). In particolare
- sono predisposte le direzioni dei segnali dei pin CPURESW (27 in output), CPUINT (1
in output), CPUCSRD (32 in input), CPUCSWR (33 in input)
pinMode(CPURESW, OUTPUT);
pinMode(CPUINT, OUTPUT);
pinMode(CPUCSRD, INPUT_PULLUP);
pinMode(CPUCSWR, INPUT_PULLUP);
- sono predisposte tutte le 8 linee dei dati (4, 5, 12, 13, 18, 19, 21, 22 in input) e le 4 linee
degli indirizzi (34, 35, 36, 0 in input)
for (uint8_t dix = 0; dix < sizeof(CPUD); dix++)
pinMode(CPUD[dix], INPUT_PULLUP);
for (int pin = 0; pin < sizeof(CPUA); pin++)
pinMode(CPUA[pin], INPUT);
- viene disattivata la linea di wait dello Z80 (pin 27)
CPURESWAIT;
6
- sono impostati i pin di controllo dell’interfaccia verso la SDCARD (14, 15, 2) e
inizializzato il colloquio con eventuali messaggi previsti in modalità DEBUG
SD_MMC.setPins(SD_MMC_CLK, SD_MMC_CMD, SD_MMC_DAT);
if (!SD_MMC.begin("/sdcard", true, true, SDMMC_FREQ_DEFAULT, 5)) {
#ifdef _DEBUG_
Serial.println("E: Card mount failed");
#endif
} else {
#ifdef _DEBUG_
Serial.printf("\nI: Size: %.02lf MB\r\n", (double)(SD_MMC.cardSize()) / SIZE_1M);
Serial.printf("I: Total space %.02lf MB\r\n", (double)(SD_MMC.totalBytes()) / SIZE_1M);
Serial.printf("I: Used space %llu bytes\r\n", SD_MMC.usedBytes());
#endif
- sono letti dallo storage permanente i parametri della connessione WiFi (gli ultimi
impostati), che sono assegnati alle variabili wfSSID e wfPWD
pref.begin("LXESP32", false);
wfSSID = pref.getString("SSID", "");
wfPWD = pref.getString("PWD", "");
pref.end();
- le due variabili appena inizializzate sono utilizzate per il collegamento al WiFi e la
configurazione della connessione NTP per la sincronizzazione data/ora; sono previsti
eventuali messaggi in modalità DEBUG
wfSSID.toCharArray(chSSID, 32);
wfPWD.toCharArray(chPWD, 32);
wfm.APlistClean();
wfm.addAP(chSSID, chPWD);
wfm.setStrictMode(true);
if (wfm.run() == WL_CONNECTED) {
isConnected = true;
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
#ifdef _DEBUG_
Serial.println("Connectede to Internet");
#endif
} else {
isConnected = false;
#ifdef _DEBUG_
Serial.println("NOT CONNECTED to Internet");
#endif
}
- vengono impostati i valori iniziali del vettore per i registri del CDP1854 (gestione Tape)
regTape[RGT_TXHR] = 0x00;
regTape[RGT_FF] = 0x18;
7
regTape[RGT_RXHR] = 0x00;
regTape[
RGT_STATUS] = 0x18;
- vengono impostati i valori iniziali del vettore per i registri della gestione Hard Disk
(funzionalità non implementata in questa versione)
for (int ix = 0; ix < 4; ix++)
regHD[ix] = 0;
- viene inizializzato il PWM per la generazione del segnale di interrupt a 50 Hz,
risoluzione 16 bit, 99,99% duty cycle
ledcAttach(CPUINT, 50, 16);
ledcWrite(CPUINT, 65532);
- viene aperto il file delle stampe (per l’emulazione Printer)
prnFile = SD_MMC.open(PRNFILE, "a");
- viene attivato l’interrupt per il timer a 20 mS con impostazione routine
TIMER20_Active
tim20 = timerBegin(1000000);
timerAttachInterrupt(tim20, &TIMER20_Active);
timerAlarm(tim20, 20000, true, 0);
- si impostano le funzioni di interrupt per il la lettura e scrittura da parte dello Z80
(routine CSRD_Active e CSWR_Active)
attachInterrupt(CPUCSRD, CSRD_Active, FALLING);
attachInterrupt(CPUCSWR, CSWR_Active, FALLING);
Dopo la setup(), altre funzioni importanti sono quelle relative alla gestione degli interrupt
TIMER20
_Active
Gestione interrupt timer ogni 20 mS
CSRD_Active
Gestione accesso in lettura dello Z80
CSWR_Active
Gestione accesso in scrittura dello Z80
8
TIMER20_Active()
Viene attivata ogni 20 mS per controllare il segnale isIndex (rotazione simulata di 200 mS, con
presenza foro per 20 mS e assenza per 180 mS); inoltre controlla la disattivazione del segnale
isHEngaged dopo 400 mS se è attivato
portENTER_CRITICAL_ISR(&tim20Mux);
isIndex = (cntIndex == 1);
cntIndex = (cntIndex == 0) ? 9 : cntIndex - 1;
if (isHEngaged) {
if (cntHEngaged == 0)
isHEngaged = false;
else
cntHEngaged -= 1;
}
portEXIT_CRITICAL_ISR(&tim20Mux);
CSRD_Active()
Viene attivata quando lo Z80 vuole leggere una delle porte abilitate e riconosciute dalla CPLD.
La richiesta di lettura blocca lo Z80 nello stato di WAIT in modo che l’ESP32 abbia il tempo
per elaborare la richiesta e fornire il valore associato alla porta cui si è fatto accesso.
Per evitare conflitti con operazioni precedenti, si imposta il bus dati in input; questo sarà posto
in output quando il dato sarà pronto per essere restituito
for (uint8_t dix = 0; dix < sizeof(CPUD); dix++)
pinMode(CPUD[dix], INPUT_PULLUP);
Subito dopo vengono lette le linee di indirizzo (da A0 ad A3) che sono stabili e il valore binario
corrispondente viene memorizzato nella variabile cpuAddress utilizzata in seguito
cpuAddr = ((((*(uint32_t *)GPIO_IN_REG) & 0x01) << 3) | ((*(uint32_t *)GPIO_IN1_REG) >> 2) & 0x07);
In seguito, tale variabile viene usata in uno switch…case per selezionare le operazioni da
compiere (elencate in apposite funzioni) a seconda della porta selezionata (e dunque del
servizio da emulare)
switch (cpuAddr) {
case PORT_PRINTERIO:
9
CSRD_Printer();
// gestione Printer
break;
case PORT_FDDSTATUSCMD ... PORT_FDDSECTOR:
case PORT_FDDDRVSEL ... PORT_FDDDATA:
CSRD_FDD(); // gestione FD1771 controller FD
break;
case PORT_FDDEXTSTATUSCMD ... PORT_FDDEXTDATA:
CSRD_FDDEXT(); // gestione comandi estesi
break;
case PORT_LIVEVRS:
CSRD_LIVEVRS(); // gestione interrogazione versione
break;
case PORT_HDDATA ... PORT_HDSEL:
CSRD_HDISK(); // gestione Hard Disk (non implementato)
break;
case PORT_CMSTATUSCMD ... PORT_CMDATA:
CSRD_CMATH(); // gestione coprocessore matematico (non implementato)
break;
default:
CSRD_TAPE(); // gestione CDP1854 interfaccia Tape
break;
}
Al termine viene eseguita la linea
isIRQCSRD = true;
che si serve della variabile isIRQCSRD per indicare che è stato appena servito un interrupt in
lettura e questa informazione servirà alla funzione loop() (che viene eseguita continuamente)
per completare le operazioni che si dovessero rendere necessarie dopo l’interrupt per
completare il servizio.
CSWR_Active
Viene attivata quando lo Z80 vuole scrivere su una delle porte abilitate e riconosciute dalla
CPLD. Anche la richiesta di scrittura blocca lo Z80 nello stato di WAIT in modo che l’ESP32
abbia il tempo per elaborarla e completare l’operazione associata alla porta cui si è fatto accesso.
Anche in questo caso, le linee dati del bus si impostano in input, per leggere il dato fornito
dallo Z80 e che è stabile
for (uint8_t dix = 0; dix < sizeof(CPUD); dix++)
pinMode(CPUD[dix], INPUT_PULLUP);
E anche in questo caso, vengono lette le linee di indirizzo (da A0 ad A3) e il valore binario
corrispondente viene memorizzato nella variabile cpuAddress utilizzata in seguito
10
cpuAddr = ((((*(uint32_t *)GPIO_IN_REG) & 0x01) << 3) | ((*(uint32_t *)GPIO_IN1_REG) >> 2) & 0x07);
In seguito, viene letto il dato che lo Z80 sta fornendo sul bus dati; il valore binario viene
memorizzato nella variabile cpuData
uint32_t reginD = *((uint32_t *)GPIO_IN_REG);
cpuData=((reginD >> 4) & 0x03)|((reginD >> 10) & 0x0C)|((reginD >> 14) & 0x30)|((reginD >> 15) & 0xC0);
Anche in questo caso la variabile cpuAddr viene usata in uno switch…case per selezionare le
operazioni da compiere (elencate in apposite funzioni) a seconda della porta selezionata e del
dato contenuto in cpuData (e dunque del servizio da emulare)
switch (cpuAddr) {
case PORT_PRINTERIO:
CSWR_Printer(); // gestione Printer
break;
case PORT_FDDSTATUSCMD ... PORT_FDDSECTOR:
case PORT_FDDDRVSEL ... PORT_FDDDATA:
CSWR_FDD(); // gestione FD1771 controller FD
break;
case PORT_FDDEXTSTATUSCMD ... PORT_FDDEXTDATA:
CSWR_FDDEXT(); // gestione comandi estesi
break;
case PORT_LIVEVRS:
CSWR_LIVEVRS(); // gestione interrogazione versione
break;
case PORT_HDDATA ... PORT_HDSEL:
CSWR_HDISK(); // gestione Hard Disk (non implementato)
break;
case PORT_CMSTATUSCMD ... PORT_CMDATA:
CSWR_CMATH(); // gestione coprocessore matematico (non implementato)
break;
default:
CSWR_TAPE(); // gestione CDP1854 interfaccia Tape
break;
}
Al termine viene eseguita la linea
isIRQCS
WR
= true;
che si serve della variabile isIRQCSWR per indicare che è stato appena servito un interrupt in
scrittura e questa indicazione servirà alla funzione loop() (che viene eseguita continuamente)
per completare le operazioni che si dovessero rendere necessarie dopo l’interrupt per
completare il servizio.
11
loop()
La funzione loop() viene continuamente eseguita dal microcontrollore in un ciclo infinito.
Dopo l’esecuzione iniziale della funzione setup(), la funzione loop() viene continuamente
eseguita e il suo codice controlla tutte le operazioni che devono essere eseguite in conseguenza
di un interrupt legato alla richiesta di lettura o scrittura da parte dello Z80. La loop() si accorge
di tale eventualità grazie ai due flag isIRQCSRD e isIRQCSWR che indicano l’avvenuta
esecuzione della rispettiva routine di interrupt.
La maggior parte del codice della loop() è quindi costituita da due if che controllano sempre se
i due flag sono attivi, per eseguire le operazioni richieste tramite appositi switch…case e
funzioni relative al servizio da completare. Alla fine, i flag sono riportati a false in attesa del
prossimo interrupt.
Il perché questa fase non avvenga completamente all’interno delle routine di interrupt è
determinato dal fatto che tali routine devono essere le più brevi e impiegare meno tempo
possibile; inoltre nelle funzioni di interrupt non sono consentiti ritardi e, ad esempio, operazioni
“lente”, come l’accesso al WiFi, cosa che creerebbe problemi.
Il codice riguardante la gestione della conclusione del servizio legato all’interrupt in lettura è
if (isIRQCSRD) {
switch (cpuAddr) {
case PORT_PRINTERIO:
LPRD_Printer(); // Gestione Printer
break;
case PORT_FDDSTATUSCMD ... PORT_FDDSECTOR:
case PORT_FDDDRVSEL ... PORT_FDDDATA:
LPRD_FDD(); // Gestione FD1771 floppy disk controller
break;
case PORT_FDDEXTSTATUSCMD ... PORT_FDDEXTDATA:
LPRD_FDDEXT(); // Gestione comandi estesi
break;
case PORT_LIVEVRS:
LPRD_LIVEVRS(); // gestione interrogazione versione
break;
case PORT_HDDATA ... PORT_HDSEL:
LPRD_HDISK(); // gestione Hard Disk (non implementato)
break;
case PORT_CMSTATUSCMD ... PORT_CMDATA:
LPRD_CMATH(); // gestione coprocessore matematico (non implementato)
break;
case PORT_TAPEDATA ... PORT_TAPECTRL:
LPRD_TAPE(); // gestione CDP1854 interfaccia Tape
break;
}
12
Per concludere la prima if, a questo segue
- la predisposizione in output del bus dati (dal punto di vista della ESP32, mentre lo Z80
predisporrà il proprio bus in input)
- l’output dei dati veri e propri, bit per bit, sul bus dei dati che lo Z80 deve leggere come
risposta alla richiesta iniziata con l’interrupt e frutto dell’elaborazione relativa al tipo
di richiesta ed allo stato attuale del sistema
- reimpostazione del flag isIRQCSRD a false per abilitare il riconoscimento del prossimo
interrupt
SETDATAOUTPUT;
uint8_t msk = 1;
for (int pin = 0; pin < sizeof(CPUD); pin++) {
digitalWrite(CPUD[pin], cpuData & msk);
msk <<= 1;
}
CPURESETWAIT;
isIRQCSRD = false;
}
Il codice relativo alla gestione della conclusione del servizio legato all’interrupt in scrittura è
molto simile al precedente, ovviamente le funzioni richiamate faranno quello che è necessario
nel caso di scrittura e, al termine, nessun dato sarà restituito sul Bus; il codice si conclude con
il reset del segnale di WAIT e del flag isIRQCSWR
i
f (isIRQCS
WR
) {
switch (cpuAddr) {
case PORT_PRINTERIO:
LPWR_Printer(); // Gestione Printer
break;
case PORT_FDDSTATUSCMD ... PORT_FDDSECTOR:
case PORT_FDDDRVSEL ... PORT_FDDDATA:
LPWR_FDD(); // Gestione FD1771 floppy disk controller
break;
case PORT_FDDEXTSTATUSCMD ... PORT_FDDEXTDATA:
LPWR_FDDEXT(); // Gestione comandi estesi
break;
case PORT_LIVEVRS:
LPWR_LIVEVRS(); // gestione interrogazione versione
break;
case PORT_HDDATA ... PORT_HDSEL:
LPWR_HDISK(); // gestione Hard Disk (non implementato)
break;
case PORT_CMSTATUSCMD ... PORT_CMDATA:
LPWR_CMATH(); // gestione coprocessore matematico (non implementato)
break;
case PORT_TAPEDATA ... PORT_TAPECTRL:
LPWR_TAPE(); // gestione CDP1854 interfaccia Tape
break;
13
}
CPURESETWAIT;
isIRQCSWR = false;
}
Al termine della loop() è presente il codice che aggiorna continuamente il registro di STATUS
del controller FD1771 per quello che riguarda il valore dei flag
- INDEX (presenza del foro indice durante la rotazione)
- HENGAGED (stato dell’ingaggio della testina)
- TRACK0 (posizionamento attuale su traccia 0)
if (cmdTypeNum == 1) {
bitWrite(regFD1771[RGD_STATUS], T1_INDEX, isIndex);
bitWrite(regFD1771[RGD_STATUS], T1_HENGAGED, isHEngaged);
bitWrite(regFD1771[RGD_STATUS], T1_TRACK0, (regFD1771[RGD_CURRTRK] == 0));
}
14
La EP1390(F)
La EP1390 è utilizzata nella scheda LX390 di interfaccia floppy disk per il boot del NE-DOS
1.5 con BASIC 2.1. Il codice originale è stato da me modificato in poche parti, rendendolo
adatto alle nuove condizioni hardware. Le modifiche apportate, a grandi linee, sono elencate
di seguito; si rimanda ai file sorgenti EP1390F.asm/EP1390.lst e BOOTCODE_NEDOS.asm/
BOOTCODE_NEDOS.lst per i dettagli
a) tutti i riferimenti alla memoria video sono stati cambiati da 0xECxx in 0xFExx; per
praticità ho introdotto la costante
VBASE EQU 0xFE00
b) al RESET, è stato eliminato il codice che legge dalla porta 0xD6 e che era utilizzato dal
meccanismo originale implementato per avviare la ROM da F000 anche se la CPU parte
da 0000; questo codice non è più necessario percil mio sistema viene attivato tramite
il codice presente nei secondi 64K della NVRAM, tramite meccanismo di paginazione
(vedere il file EP1390F_BOOT.asm);
c) tutti i blocchi di codici del tipo
EX (SP),HL
EX (SP),HL
oppure
PUSH BC
POP BC
oppure sequenze di
NOP
sono stati eliminati non essendo necessari ritardo per l’elaborazione da parte
dell’ESP32 nella gestione dell’I/O;
15
d) alcuni codici da riutilizzare, come quello relativo alla istruzione CLS, sono stati spostati
in una routine richiamata con una CALL quando serve;
e) la sequenza
IN A,0xD7
LD (HL),A
INC HL
è stata sostituita da una INI opportunamente preparata;
f) interroga la scheda LXESP32 per ottenere l’indicazione che è attiva e presente. Lo fa
sulla porta seguente
PORT_ESP32 EQU 0xBB
inviando il seguente byte di richiesta
ESP32GVQ EQU 0xA5
e ottenendo il relativo byte di risposta
ESP32GVR EQU 0x5A
Nel file BOOTCODE_NEDOS.asm, mostrato di seguito, il semplice codice caricato
all’indirizzo 0x10000 della NVRAM ed eseguito al boot, lancia quello della EP1390 a partire
da 0xF000
10000: DI
10001: LD A,0x01
10003: OUT RBK00,A
10005: LD A,OPC_OUT
10007: LD (ATARGET-2),A
1000A: LD A,RBK00
1000C: LD (ATARGET-1),A
1000F: OUT VPAG,A
10011: JP ATARGET-2
*Descrizione della EP1390*
16
La EP548(F)
La scheda LX548 originale ospita le memorie EPROM contenenti l’interprete BASIC da 16K.
Essendo utilizzate delle EPROM da 2K, erano necessarie 8 EPROM.
Nella mia replica tutta la memoria è ospitata nella scheda CPU, all’interno del chip NVRAM
da 128K e sarà dunque sufficiente un solo file binario complessivo risultante dalla
compilazione del sorgente dell’interprete BASIC. Il sorgente è stato modificato ampiamente,
innanzitutto per permettere l’utilizzo della memoria video a partire dall’indirizzo 0xFE00 (e
non 0xEC00, da cui la versione (F)) e inoltre per correggere bug e implementare nuove
funzionalità.
Tutti i riferimenti alla memoria video sono stati cambiati da 0xECxx in 0xFExx, anche in
questo caso, tramite la costante
VBASE EQU 0xFE00
Il codice è presente dall’indirizzo 0x0000 a 0x3FFF e il boot avviene con del codice che parte
dall’indirizzo 0x10000 che chiama quello presente da 0x0000. I file sorgenti sono
EP548F.asm/EP548F.lst e BOOTCODE_BASIC.asm/ BOOTCODE_BASIC.lst.
Nel file BOOTCODE_BASIC.asm, mostrato di seguito, il breve codice caricato all’indirizzo
0x10000 della NVRAM eseguito al boot lancia quello della EP548F a partire da 0x0000
10000: DI
10001: LD A,0x01
10003: OUT RBK00,A
10005: LD A,OPC_JP
10007: LD (ATARGET+2),A
1000A: LD A,OPC_OUT
1000C: LD (ATARGET),A
1000F: LD A,RBK00
10011: LD (ATARGET+3),A
10014: LD (ATARGET+4),A
10017: LD (ATARGET+1),A
1001A: OUT VPAG,A
1001C: JP ATARGET
17
Al boot, il BASIC contatta la scheda ESP32 e ne ottiene la versione. La stessa informazione
si può ottenere con il nuovo comando ESP32
Il controllo della presenza della ESP32 viene fatto nel codice con le istruzioni seguenti
LD A,ESP32GVQ
OUT PORT_ESP32,A
IN A,PORT_ESP32
CP ESP32GVR
JR NZ,ESP32DNR
che inviano il dato ESP32GVQ (0xA5) al firmware sulla porta 0xBB e leggono dalla stessa porta
la risposta che deve essere ESP32GVR (0x5A) se la scheda LXESP32 è presente. In caso positivo
viene richiesta la versione con il codice
IN A,PORT_ESP32
CALL OUTCHA
LD A,'.'
CALL OUTCHA
IN A,PORT_ESP32
CALL OUTCHA
IN A,PORT_ESP32
CALL OUTCHA
che legge e visualizza le 3 cifre della versione nel formato x.yz
Sono stati aggiunti alcuni comandi, ovvero
BOOT Accede agli indirizzi da 0x10000 a 0x11FFF e
passa il controllo a 0x10000 (reboot)
VBNK
Cambia banco video (da 0 a 3)
MBNK
Cambia valore A16 per accesso a banco in memoria
DIR
Visualizza directory di un disco
18
Il nuovo comando BOOT è utile per riavviare il BASIC da programma. Le linee di codice
sufficienti sono le seguenti
BOOT:
LD A,0x01
OUT PORT_RBK00,A
JP HWRESET
che garantiscono il riavvio tramite il BOOTCODE; prima di passare all’indirizzo dell’inizio
del BASIC (0x0000) viene infatti attivato il banco 0 in modo che l’indirizzo effettivo a cui salta
la JP successiva sia 0x10000. Questo codice lavora correttamente perché è posizionato dopo
l’indirizzo 0x1FFF e quindi non è coinvolto nello switch,
Il nuovo comando VBNK può essere utilizzato per cambiare la pagina di testo attiva.
Ricordo che nella mia implementazione della scheda LX388 ho utilizzato una memoria video
più grande e un meccanismo per utilizzarla a pagine (4 da 512 byte). Le linee di codice che
implementano questa funzionalità sono le seguenti
VBNK:
CALL GETBYT
CP 0x04
JP NC,IFERR
OUT PORT_VPAG,A
RET
in cui viene fatto il controllo del parametro espresso dopo l’istruzione che, se compreso tra 0 e
3, viene inviato alla porta che esegue il cambio della pagina. Attenzione al fatto che dopo il
cambio, la visualizzazione può essere confusa dato che non viene automaticamente ripulita la
nuova pagina che può presentare caratteri casuali (è necessario un CLS).
Il nuovo comando MBNK serve per cambiare l’accesso delle pagine di memoria. Questo
comando accetta due parametri numerici interi; il numero di pagina (da 0 a 7, una tra 8 pagine
19
in cui sono suddivisi i 64K di memoria); il valore del bit A16 dell’indirizzo per quella pagina
(0 o 1, 0 per accedere ai primi 64K e 1 per accedere ai secondi 64K).
Per chiarire, la mia scheda LX382 prevede che la memoria disponibile sia da 128K. I primi
64K sono disponibili se l’indirizzo A16 è a 0, i secondi 64K se A16 è a 1. Il controllo della
linea A16 può essere fatto indipendentemente per 8 pagine. Se scriviamo il comando
MBNK 4, 1
intendiamo che gli indirizzi di memoria 0x8000…0x9FFF siano in realtà acceduti nella
seconda parte della memoria ovvero agli indirizzi della NVRAM 0x18000…0x19FFF.
Questa caratteristica può essere utilizzata con estrema cautela perché rischia di bloccare il
sistema che nessun controllo fa sul suo corretto uso.
Ad esempio, se eseguissimo il comando
MBNK 0, 1
allora la CPU non potrebbe più accedere all’area di memoria 0x0000…0x1FFF ma leggerebbe
e scriverebbe nell’area 0x10000…0x11FFF; non trovando più il codice del BASIC ci sarebbe
un crash o un blocco da cui si potrebbe uscire solo con il tasto Reset.
Il codice completo della MBNK è nel file sorgente ma questa è la parte finale che esegue il set
o il reset della linea A16 a seconda del banco prescelto
MBNK: …
OR E
JR EXMBNK
BRES AND D
EXMBNK OUT PORT_RBK00,A
RET
Infine, con il nuovo comando DIR (si rimanda al sorgente per il suo completo esame), è
possibile ottenere la lista dei file di ogni disco; il comando accetta obbligatoriamente un
parametro altrimenti si ottiene un errore
20
Inserendo l’argomento (numero del drive), ad esempio il 3, con il comando
DIR 3
otteniamo la lista dei file visualizzata di seguito in due parti (grazie al tasto CTRL-S)
Il BASIC da 16K tuttavia, non ha istruzioni o comandi per gestire i file su disco ma questi
saranno oggetto di una prossima implementazione.
21
I nuovi comandi del NE-DOS
PRNRESET
È un semplice comando senza argomenti, che elimina il file NEZ80_PRN presente nella
SDCARD e lo crea nuovamente. Tale file contiene tutti i dati inviati dal sistema alla stampante
da un certo momento in poi. Dopo la nuova creazione il file NEZ80_PRN è vuoto e viene
aperto per poter registrare le prossime stampe.
Il comando digitato è semplicemente
PRNRESET
Il comando sfrutta le porte EXTCMD e EXTST (rispettivamente 0xD4 e 0xD5) che non sono
utilizzate dall’interfaccia floppy disk e sono riservate a comandi estesi per il firmware.
Il codice sorgente è visibile nei file PRNRESET.CMD.asm/PRNRESET.CMD.lst ed inizia
dall’indirizzo 0x7000 della memoria quando viene caricato dal NE-DOS. All’avvio, la prima
istruzione permette di saltare al MAIN che immediatamente visualizza nome e versione del
programma
PRNRESET V.1.0. (C) AG, 2025
22
Per visualizzare le stringhe viene usata la routine VSTRING del NE-DOS. Subito dopo viene
inviato il comando FDDEXT_FORCEINT (0xD0) sulla porta 0xD4 che il firmware interpreta come
comando esteso del controller e, similmente a quello proprio inviato sulla porta 0xD0, serve a
resettare lo stato del firmware nella parte che gestisce i comandi estesi.
In seguito viene inviato il comando FDDEXT_PRNRESET (0xF1) che elimina il file NEZ80_PRN
eventualmente presente nella SDCARD e ne crea un altro con lo stesso nome ma vuoto.
Alla fine il codice passa alla routine NEDOS (punto di rientro del SO).
Le routine e gli indirizzi coinvolti del NE-DOS sono
VSTRING
0x3375
Visualizza stringa puntata dal registro HL
terminata dal carattere CR
NEDOS
0x402D
Ritorna al sistema operativo NE
-
DOS
I comandi estesi inviati al firmware tramite la porta 0xD4 sono
0
x
D0
FDDEXT_FORCEINT
Reset stato comandi estesi
0
xF1
FDDEXT_
PRNRESET
Elimina file NEZ80_PRN esistente e crea nuovo
file vuoto per le stampe successive
23
SDCOPY
È un comando un po’ più complesso, che accetta degli argomenti obbligatori. È utilizzato per
copiare file dai dischi NE-DOS nella SDCARD e viceversa. È utile per esportare e importare
file tra il sistema NE-DOS e il mondo esterno.
Anche questo codice viene caricato ed eseguito a partire dall’indirizzo di memoria 0x7000.
Il comando ha la seguente sintassi
SDCOPY filesrc/ext:drsrc filedst/ext:drdst
in cui filesrc/ext è il nome completo di estensione del file sorgente e filedst/ext è il nome
completo di estensione del file destinazione. Le regole del nome sono quelle del NE-DOS
tenendo presente che l’eventuale estensione usata sulla SDCARD sarà preceduta dal carattere .
(punto) e non dal carattere / (barra).
Il drive che segue il carattere : (due punti) per il file sorgente e per il file destinazione può
essere 0, 1, 2, 3 (per i dischi del NE-DOS) oppure S (per la SDCARD).
Ad esempio, se volessi importare in NE-DOS il file delle stampe per leggerlo, potrei scrivere
SDCOPY NEZ80_PRN:S STAMPE/TXT:0
così da copiare il file dalla SDCARD con altro nome nel primo disco NE-DOS.
Se dal BASIC NE-DOS si salva un programma con l’opzione A (formato ASCII), viene creato
un file di testo che è possibile esportare. Ad esempio, nel BASIC, posso caricare in memoria il
programma MOSTRA.BAS con la LOAD e salvarlo nel secondo disco con la
SAVE “MOSTRA/BAS:1”,A
A questo punto, tornando al NE-DOS, posso esportare il file con il comando
SDCOPY MOSTRA/BAS:1 MOSTRA/BAS:S
e in questo modo si otterrà una copia del file nella SDCARD denominata MOSTRA.BAS
24
Come ultima possibilità, che potrà tornare utile per future funzionalità da aggiungere al
firmware, è anche possibile copiare file che stanno nella SDCARD facendone una copia con
altro nome nella stessa SDCARD, ad esempio
SDCOPY FILE1/TXT:S FILE2/TXT:S
Non è possibile (si ottiene un errore di runtime) copiare da disco NE-DOS ad altro disco NE-
DOS in quanto questa operazione è svolta dal comando COPY già esistente.
Il codice sorgente è visibile nei file SDCOPY.CMD.asm/SDCOPY.CMD.lst ed inizia
dall’indirizzo 0x7000 della memoria quando viene caricato dal NE-DOS.
All’avvio, la prima istruzione permette di saltare al MAIN che immediatamente visualizza
nome e versione del programma
SDCOPY V.1.3. (C) AG, 2025
Per visualizzare le stringhe viene usata la routine VSTRING del NE-DOS. Subito dopo viene
inviato il comando FDDEXT_FORCEINT (0xD0) sulla porta 0xD4 che il firmware interpreta come
comando esteso del controller e, similmente a quello inviato sulla porta 0xD0, serve a resettare
lo stato del firmware nella parte che gestisce i comandi estesi.
In seguito viene azzerata la variabile OPDIREC il cui contenuto indica la direzione
dell’operazione di copia, secondo la seguente tabella
25
0
1
2
3
b1(DST) b0(SRC)
0 0 da NE-DOS a NE-DOS (*non consentito, usare COPY)
0 1 da SDCARD a NE-DOS
1 0 da NE-DOS a SDCARD
1 1 da SDCARD a SDCARD
Segue il controllo del primo argomento che viene validato e assegnato al DCB sorgente
(DCBLOC1) tramite la chiamata della routine CLFILESP di NE-DOS; se il filespec non viene
validato il controllo passa alla ERBFSP per visualizzare il messaggio d’errore e tornare al NE-
DOS. Lo stesso controllo viene fatto sul secondo argomento, assegnato al DCB destinazione
(DCBLOC2).
Segue la ricerca del drive :S nel primo e nel secondo DCB per impostare correttamente i bit
della variabile OPDIREC; se questa risulta uguale a zero alla fine di questi controlli, allora viene
visualizzato un messaggio di errore perché si è scelto di copiare da NE-DOS a NE-DOS e per
questa operazione basta usare il normale comando COPY.
In base al valore della OPDIREC viene eseguito il codice a partire da NOTIS1 o dal codice
precedente.
Se si intende copiare da SDCARD a NE-DOS (caso 1), viene inviato il comando di apertura
file sorgente su SDCARD al firmware (comando FDDEXT_OPSDFILE con nome file sorgente da
DCBLOC1) e viene aperto (o creato) il file destinazione su NE-DOS tramite la routine OPEN
con nome file destinazione da DCBLOC2.
La chiamata alla SD2NED provvede a copiare il file leggendolo dalla SDCARD tramite il
comando FDDEXT_READSDFILE. Alla fine del ciclo di copia, il file su SDCARD viene chiudo
con il comando FDDEXT_CL0SDFILE e quello su NE-DOS con la chiamata alla routine CLOSE.
Se la richiesta di copia è da NE-DOS a SDCARD (caso 2), le chiamate sono simili ma per file
sorgente su NE-DOS e destinazione su SDCARD.
26
L’ultima possibilità, per la richiesta di copia da SDCARD a SDCARD (caso 3) è svolto dal
codice che parte da NOTIS2 e che, dopo aver aperto i due file, invia il comando
FDDEXT_COPYSDFILE per la copia a cura del microcontrollore; seguono le chiamate per le
chiusure dei file e il ritorno al NE-DOS.
Le routine e gli indirizzi coinvolti del NE-DOS sono
I comandi estesi inviati al firmware tramite la porta 0xD4 sono
0x51
FDDEXT_SETLENSDBLK
Invia word lunghezza blocco SD tramite
porta 0xD5 (ciclo controllato da flag DRQ e
BUSY nello STATUS)
0x60
FDDEXT_OPSDFILE
Invia nome file su SD da aprire tramite
porta 0xD5 (ciclo controllato da flag DRQ e
BUSY nello STATUS)
0x61
FDDEXT_CRSDFILE
Invia nome file su SD da creare tramite
porta 0xD5 (ciclo controllato da flag DRQ e
BUSY nello STATUS)
0x70
FDDEXT_CL0SDFILE
Chiude file sorgente su SD
0x71
FDDEXT_CL1SDFILE
Chiude file destinazione su SD
0x80
FDDEXT_READSDFILE
Riceve dati da file tramite porta 0xD5
(ciclo controllato da flag DRQ e BUSY nello
STATUS)
0xA0
FDDEXT_WRITESDFILE
Invia dati da buffer per
scrittura sy
file
tramite porta 0xD5 (ciclo controllato da
flag DRQ e BUSY nello STATUS)
0xD0
FDDEXT_FORCEINT
Reset stato comandi estesi
0xE0
FDDEXT_COPYSDFILE
Invia comando di copia file da SD a SD
RDBYTE
0x
0013
Legge byte
in A
da device passando DCB
in DE
WRBYTE
0x
001B
Scrive byte
in A
su device passando DCB
in DE
OUTCHAR
0x
0033
Visualizza carattere passato nel registro A
VSTRING
0x
3375
Visualizza stringa puntata dal registro HL
terminata dal carattere CR
NEDOS
0x
402D
Ritorna al NEDOS
NEDOSERR
0x
4030
Ritorna al NEDOS con errore
IOERR
0x
4409
Gestione errori
, codice in A
CKFILESP
0x
441C
Crea/modifica e controlla filespec
puntato da
HL e lo valida in DCB puntato da DE
INIT
0x
4420
Crea file con DCB puntato da DE e buffer
puntato da HL
OPEN
0x
4424
Apre file con DCB puntato da DE e buffer
puntato da HL
CLOSE
0x
4428
Chiude il file con DCB in DE
VISESTR
0x
4467
Visualizza stringa errore puntata da HL
DCBLOC1
0x
5551
Puntatore al DCB sorgente
DCBLOC2
0x
5571
Puntatore al DCB destinazione
BUFSEC
0x
5600
Puntatore a buffer settore per file
27
TIMESYNC
Questo nuovo comando sfrutta la caratteristica dell’ESP32 di potersi collegare ad Internet
abbastanza semplicemente tramite WiFi e un Access Point. In questo modo è possibile accedere
ad un sito NTP per ottenere data e orario attuali con precisione elevata. Data ed orario del
microcontrollore saranno sincronizzati così come il NE-DOS.
Naturalmente il NE-DOS sarà aggiornato tramite il segnale di interrupt, che non è preciso come
un segnale di riferimento orario su Internet e quindi, con il tempo, potrà ritardare (o accelerare).
Basterà comunque eseguire nuovamente il comando TIMESYNC per rimettere a posto con
precisione l’orologio di sistema.
Il comando prevede una doppia sintassi
TIMESYNC
senza argomenti farà in modo che il firmware restituisca la data e l’ora interna dell’ESP32 che
è sincronizzata con quella ricevuta dal riferimento pool.ntp.org fissato nel codice. Il WiFi si
collegherà ad un SSID con una password che sono memorizzati in maniera permanente nel chip
e che, all’inizio, sono nulli. Al primo utilizzo dunque, il comando non potrà sincronizzare la
data e l’ora e restituirà il valore 00/00/00 00:00:00
Con la seconda sintassi è possibile indicare SSID e password per il collegamento
TIMESYNC SSID PWD
con due argomenti che sono i parametri per il collegamento WiFi. Questi vengono utilizzati
subito per tentare il collegamento ad Internet e la sincronizzazione della data e dell’orario.
Comunque sono salvati in memoria permanente per potere essere utilizzati alla prossima
28
attivazione del sistema. Subito dopo l’impostazione è necessario eseguire il comando senza
argomenti per la sincronizzazione.
\
Il codice sorgente è visibile nel file TIMESYNC.CMD.asm/TIMESYNC.CMD.lst ed inizia
dall’indirizzo 0x7000 della memoria quando viene caricato dal NE-DOS. All’avvio, la prima
istruzione permette di saltare al MAIN che immediatamente visualizza nome e versione del
programma
TIMESYNC V.1.25. (C) AG, 2025
Per visualizzare le stringhe viene usata la routine VSTRING del NE-DOS. Subito dopo viene
inviato il comando FDDEXT_FORCEINT (0xD0) sulla porta 0xD4 che il firmware interpreta come
comando esteso del controller e, similmente a quello inviato sulla porta 0xD0, serve a resettare
lo stato del firmware nella parte che gestisce i comandi estesi.
In seguito comincia l’analisi della linea di comando, puntata da HL, dalla quale sono scartati
tutti gli spazi. Se viene trovato il carattere CR, allora non sono stati indicati argomenti e il
controllo passa alla TSYNC da cui comincia il codice che sincronizza data/ora.
In questo caso, viene inviato il comando FDDEXT_GETDATETIME (0x10) che indica al firmware
di restituire le informazioni attuali su data/ora e queste vengono lette tramite la porta 0xD5 dei
dati in un ciclo controllato dai bit DRQ e BUSY dello STATUS.
Durante il ciclo sono scartati i caratteri / e : che sono in mezzo alla data e all’ora, mentre le
informazioni su anno, mese, giorno, ora, minuti, secondi sono memorizzate all’interno dello
29
spazio occupato in memoria dal comando e contemporaneamente visualizzati utilizzando la
routine OUTCHAR del NE-DOS.
Al termine della ricezione, i dati sono copiati nell’area dedicata del NE-DOS che parte
dall’indirizzo 0x4046. Alla fine il codice passa alla routine NEDOS (punto di rientro del SO).
Se il controllo della linea di comando identifica un argomento, il controllo continua e vengono
memorizzati nelle variabili PARG1 e PARG2 i puntatori all’inizio dei due parametri. Se viene
individuato solo un argomento, passa alla ERRARG e quindi viene visualizzato un messaggio di
errore e il codice passa alla routine NEDOSERR.
Nel caso di passaggio corretto dei due parametri, viene inviato il comando FDDEXT_SETWSSID
(0x11) al firmware seguito dalla stringa del primo parametro puntato da HL e poi viene inviato
il comando FDDEXT_SETWPWD (0x21) al firmware seguito dalla stringa del secondo parametro
puntato da HL. A questo punto il micro tenta il collegamento al WiFi con le nuove credenziali
e passa alla routine NEDOS (punto di rientro del SO).
Le routine e gli indirizzi coinvolti del NE-DOS sono
OUTCHAR
0x0033
Visualizza carattere passato nel registro A
VSTRING
0x3375
Visualizza stringa puntata dal registro HL
terminata dal carattere CR
NEDOS
0x402D
Ritorna al sistema operativo NE
-
DOS
NEDOSERR
0x4030
Ritorna al sistema operativo NE
-
DOS con errore
PDATTIM
0x4046
Puntatore al buffer della data/ora per il NE
-
DOS
VISESTR
0x4467
Visualizza stringa di errore puntata dal registro
HL terminata da CR
I comandi estesi inviati al firmware tramite la porta 0xD4 sono
0
x
10
FDDEXT_GETDATETIME
Restituisce data/ora tramite porta 0xD5 (ciclo
controllato da flag DRQ e BUSY nello STATUS)
0
x
11
FDDEXT_SETWSSID
Invia stringa SSID tramite porta 0xD5 (ciclo
controllato da flag DRQ e BUSY nello STATUS)
0
x
21
FDDEXT_SETWPWD
Invia stringa PWD tramite porta 0xD5 (ciclo
controllato da flag DRQ e BUSY nello STATUS)
0
x
D0
FDDEXT_FORCEINT
Reset stato comandi estesi
30
_________________
Bibliografia
- Roy SoltoffThe Programmer’s Guide to TRSDOS Version 6MISOSYS, Inc.
- NEWDOS/80 for the TRS-80Apparat, Inc.
- Earles L. McCaul TRS-80 Assembly Language Made SimpleHoward W. Sams & Co.,
Inc.
- LDOS 5.1.xLogical Systems, Inc.
- TRSDOS & DISK BASIC Reference ManualRadio Shake
- H.C. Pennington TRS-80 Disk & Other Misteries International Jewelry Guild, Inc.
- https://www.trs-80.com/wordpress/dos-trsdos-v2-3-disassembled/
1
Sarà implementata nella prossima versione della scheda LXESP32 e del firmware