Un microprocessore è un dispositivo elettronico programmabile. E’ in grado cioè di leggere una sequenza di istruzioni da una memoria e di eseguirle una dopo l’altra. Un sistema a microprocessore dispone di dispositivi di ingresso che sono in grado di ricevere dati dall’esterno (tastiere, sensori vari, pulsanti) li elaborano ed inviano dati all’esterno mediante dispositivi di uscita (luci, display, indicatori vari). Il programma, cioè la sequenza di istruzioni, può essere memorizzato in modo permanente e non modificabile in una memoria di sola lettura oppure può essere caricato da dispositivi di memorizzazione quali dischi, schede di memoria, ecc.
Se vogliamo capire come si progetta un sistema basato su microprocessore a partire da zero non possiamo utilizzare un processore attuale: sarebbe troppo complesso. Un “vecchio” processore ad 8 bit come lo Z80 è invece l’ideale per comprendere tutti gli aspetti di progettazione e programmazione acquisendo delle conoscenze che potranno poi essere utilizzate anche per capire sistemi più complessi e recenti.
Il manuale dello Z80 è sicuramente un buon punto di partenza per capire l’architettura interna del processore ed il suo funzionamento.
L’istruzione NOP
Se, come me, vi siete ritrovati nel cassetto dei componenti uno Z80 o compatibile (nel mio caso era un NEC D780C-2) e volete capire se funziona c’è un modo molto semplice che non richiede altri componenti. Basta una breadboard (una basetta sperimentale), un alimentatore da 5V (va bene anche un Arduino), un po’ di cavi e una manciata di LED e di resistenze. Ma come, direte voi, da dove prende le istruzioni e gli input di cui si parlava prima? Esiste l’istruzione No OPeration che non fa nulla ma che deve essere caricata dal processore secondo la logica del fetch delle istruzioni e che quindi incrementa l’indirizzo presente sul bus degli indirizzi. Nello Z80 l’istruzione NOP è codificata con l’esadecimale 0x00. Quindi se si mette tutti e gli 8 bit del data bus a 0 si dovrebbero vedere gli indirizzi incrementarsi di una unità ogni quattro colpi di clock.
In questo video su TikTok mostro questo funzionamento:
Scheda con Z80: evoluzioni su breadboard
Dopo questo primo test iniziale il passo successivo è quello di provare a fornire delle istruzioni al processore. Normalmente sarebbe necessaria una memoria ma per iniziale si può pensare di utilizzare un semplice sketch Arduino per fornire le istruzioni al processore.
In questi due video vediamo i componenti interni di un microprocessore e come fornire istruzioni allo Z80 con un Arduino:
Su github gli schemi elettricihttps://github.com/Alexa-Academy/arduino-z80-rom/blob/main/Schematic_Z80_breadboard_2023-03-31.pdf utilizzati nei video.
Questo il codice dello sketch caricato su Arduino:
#include "Arduino.h"
#define Z80_D0 4
#define Z80_D1 5
#define Z80_D2 6
#define Z80_D3 7
#define Z80_D4 8
#define Z80_D5 9
#define Z80_D6 10
#define Z80_D7 11
#define Z80_A0 A0
#define Z80_A1 A1
#define Z80_A2 A2
#define Z80_A3 A3
#define Z80_A4 A4
#define Z80_M1 12
//#define Z80_WR A4
#define Z80_RD 3
#define Z80_IOREQ 13
#define Z80_REFRESH A5
#define Z80_CLOCK 2
// NOP
//byte ROM[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// loop
//byte ROM[] = {0x3E, 0x00, 0x3C, 0xC3, 0x02, 0x00 };
// out_wr
//byte ROM[] = { 0x3E, 0x05, 0xD3, 0x06, 0x76 };
// loop_io
//byte ROM[] = {0x3E, 0x00, 0xD3, 0x06, 0x3C, 0xC3, 0x02, 0x00 };
// blink
//byte ROM[] = {0xAF, 0xD3, 0x06, 0x06, 0x0A, 0x10, 0xFE, 0x2F, 0xD3, 0x06, 0x06, 0x0A, 0x10, 0xFE, 0xC3, 0x00, 0x00};
// blink1
byte ROM[] = {0xAF, 0xD3, 0x06, 0x06, 0x0A, 0x10, 0xFE, 0x3E, 0x01, 0xD3, 0x06, 0x06, 0x0A, 0x10, 0xFE, 0xC3, 0x00, 0x00};
int cycle=0;
void setDatabusOut(bool isOut) {
if (isOut) {
pinMode(Z80_D0, OUTPUT);
pinMode(Z80_D1, OUTPUT);
pinMode(Z80_D2, OUTPUT);
pinMode(Z80_D3, OUTPUT);
pinMode(Z80_D4, OUTPUT);
pinMode(Z80_D5, OUTPUT);
pinMode(Z80_D6, OUTPUT);
pinMode(Z80_D7, OUTPUT);
} else {
pinMode(Z80_D0, INPUT);
pinMode(Z80_D1, INPUT);
pinMode(Z80_D2, INPUT);
pinMode(Z80_D3, INPUT);
pinMode(Z80_D4, INPUT);
pinMode(Z80_D5, INPUT);
pinMode(Z80_D6, INPUT);
pinMode(Z80_D7, INPUT);
}
}
void setup() {
Serial.begin(115200);
cycle = 0;
setDatabusOut(true);
pinMode(Z80_A0, INPUT_PULLUP);
pinMode(Z80_A1, INPUT_PULLUP);
pinMode(Z80_A2, INPUT_PULLUP);
pinMode(Z80_A3, INPUT_PULLUP);
pinMode(Z80_A4, INPUT_PULLUP);
pinMode(Z80_M1, INPUT_PULLUP);
pinMode(Z80_REFRESH, INPUT_PULLUP);
pinMode(Z80_IOREQ, INPUT_PULLUP);
//pinMode(Z80_WR, INPUT_PULLUP);
pinMode(Z80_RD, INPUT_PULLUP);
pinMode(Z80_CLOCK, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(Z80_CLOCK), ClockTrigger, RISING);
attachInterrupt(digitalPinToInterrupt(Z80_RD), ReadTrigger, FALLING);
}
void ClockTrigger() {
Serial.print("Clock #");
Serial.print(cycle++);
Serial.print(" Indirizzo: ");
byte add = decodeAddress();
Serial.print(add);
Serial.print(" (");
Serial.print(digitalRead(Z80_A4));
Serial.print(digitalRead(Z80_A3));
Serial.print(digitalRead(Z80_A2));
Serial.print(digitalRead(Z80_A1));
Serial.print(digitalRead(Z80_A0));
Serial.print(")");
Serial.print(" Dati: ");
bool d7 = digitalRead(Z80_D7);
bool d6 = digitalRead(Z80_D6);
bool d5 = digitalRead(Z80_D5);
bool d4 = digitalRead(Z80_D4);
bool d3 = digitalRead(Z80_D3);
bool d2 = digitalRead(Z80_D2);
bool d1 = digitalRead(Z80_D1);
bool d0 = digitalRead(Z80_D0);
Serial.print(d7);
Serial.print(d6);
Serial.print(d5);
Serial.print(d4);
Serial.print(d3);
Serial.print(d2);
Serial.print(d1);
Serial.print(d0);
byte data_bus = d7<<7 | d6<<6 | d5<<5 | d4<<4 | d3<<3 | d2<<2 | d1<<1 | d0;
Serial.print(" (");
Serial.print(data_bus, HEX);
Serial.print(") ");
byte mem_rd = digitalRead(Z80_RD);
Serial.print(" R:");
Serial.print(mem_rd);
/*
byte mem_wr = digitalRead(Z80_WR);
Serial.print(" W:");
Serial.print(mem_wr); */
byte io_req = digitalRead(Z80_IOREQ);
Serial.print(" IOREQ:");
Serial.print(io_req);
byte refresh = digitalRead(Z80_REFRESH);
Serial.print(" RFSH:");
Serial.print(refresh);
byte m1 = digitalRead(Z80_M1);
Serial.print(" M1:");
Serial.print(m1);
Serial.println("");
}
void ReadTrigger() {
byte add = decodeAddress();
if (add >= 0 && add < sizeof(ROM)) {
writeByte(ROM[add]);
} else {
writeByte(0);
}
}
void writeByte(byte b) {
digitalWrite(Z80_D0, bitRead(b, 0));
digitalWrite(Z80_D1, bitRead(b, 1));
digitalWrite(Z80_D2, bitRead(b, 2));
digitalWrite(Z80_D3, bitRead(b, 3));
digitalWrite(Z80_D4, bitRead(b, 4));
digitalWrite(Z80_D5, bitRead(b, 5));
digitalWrite(Z80_D6, bitRead(b, 6));
digitalWrite(Z80_D7, bitRead(b, 7));
}
byte decodeAddress() {
byte add = 0;
bitWrite(add, 0, digitalRead(Z80_A0));
bitWrite(add, 1, digitalRead(Z80_A1));
bitWrite(add, 2, digitalRead(Z80_A2));
bitWrite(add, 3, digitalRead(Z80_A3));
bitWrite(add, 4, digitalRead(Z80_A4));
return add;
}
void loop() {
delay(10);
}