ALEX80: scheda sperimentale basata su processore Z80

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:

@alexa.academy

Un semplice modo per verificare il funzionamento di un processore Z80. Mettiamo tutti gli 8 pin del data bus a GND (per sicurezza mediante resistenze da 10K). In questo modo il processore leggerà costantemente l’istruzione NOP che gli dice di non fare nulla. Il processore eseguirà però la fase di fetch dell’istruzione composta da quattro cicli macchina. In questo modo si possono vedere mediante una barra LED gli indirizzi avanzare a partire da 0 e poi l’attivazione della fase M1 e quando viene effettuato il read dell’istruzione. #learning #z80 #maker #informatica #elettronica #retrocomputer

♬ Run Away – Ian Storm & Ron van den Beuken & Menno

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);
}