Atmel ATmega328P-DIP28 als Breadboard Computer Coprozessor programmieren

Im letzten Arduino-Artikel habe ich ja schon beschrieben, wie man einen nackten Atmel ATmega328P ohne Uno oder Nano-Motherboard betreibt. Das war als Vorbereitung für dieses Projekt gedacht, in dem ich einen ATmega328P so programmieren möchte, dass er mir als Coprozessor zu meinem 8Bit-Breadboard-Computer mit 6502 CPU dient. Dort hatte ich als letztes ein kleines Spiel in Assembler programmiert, in dem eine durch LEDs angezeigte Binärfolge im Kopf in Hex umgerechnet und dann über eine Tastatur eingegeben werden muss.

Unschön dabei ist, dass hier immer die selben Zahlen kommen und das Spiel nach mehrmaligem Probieren dadurch natürlich langweilig wird. Was dem Breadboard Computer fehlt ist ein Pseudozufallszahlengenerator für immer unterschiedliche Zahlen, um das Spiel abwechslungsreicher zu machen. Und den Generator soll der ATmega328P zur Verfügung stellen. Und vielleicht ein Paar Funktionen mehr.

Ich habe mir natürlich vorher Gedanken gemacht, wie ich einen Coprozessor realisieren will. Zuerst kam mir natürlich der bereits auf dem Breadboard befindliche Sniffer mit einem STM32 in den Sinn, aber der war schon bei 2 KHz Takt komplett mit dem Anzeigen des Adress- und Datenbus und dem Deassemblieren beschäftigt.

Dann kam mir in den Sinn, einen zweitem STM32 zu nehmen und die bereits vorhandene Infrastruktur mit 74HC165 Chips zu nutzen, um den Adressbus auszuwerten. Dann fiel mir aber ein, dass es nicht zwei Taktmeister für das Shiften der Bits der Schieberegister geben kann. Das würde ein Konzert des Chaos ergeben und weder der eine noch der andere würde den Adressbus richtig dekodieren können.

Also entschied ich mich für die platzsparende Variante mit einem ATmega328P als CoProzessor. Da dieser nicht allzuviel zu tun hätte (erst einmal nur Zufallszahlen liefern) habe ich für folgende Architektur entschieden:

Der CoProzessor kommt an den PORT B des 6522, denn der ist noch frei und kann für mich das Latching übernehmen. Jetzt muss ich ihm aber auf der einen Seite irgendwie sagen, was er tun soll und auf der anderen Seite brauche ich Daten von ihm. Ich habe mich darum entschieden, die 8 Bits des Datenbus in zwei Segmente zu teilen: -Die oberen 4 Bits (D4...D7) für die Funktion (vom 6522 gesehen dann Input) und die unteren 4 Bits (D0...D3) für ein 4 Bit breites Datenwort (vom 6522 gesehen dann Output). Ein Byte muss dann halt mit 2 Funktionen angesprochen werden. Einmal, um die obere Hälfte zu holen und einmal für die untere Hälfte.

Vier Bit zur Auswahl der Funktion macht 16 Funktionen. Eine fällt schon mal weg für die Funktion "Idle" - in der der Coprozessor nichts tun muss. Hier wähle ich natürlich 0b1111, weil im Normalzustand alle Leitungen des 6522 auf High stehen.

Die erste Funktion - die für die Zufallszahlen - bekommt die Funktionsnr. 0 (0b000). Ist diese Funktion durch die entsprechenden Funktionsbits gesetzt, dann liefert der CoProzessor fortlaufend 4 Zufallsbits am Datenport zurück, bis die Funktion wieder auf 15 gesetzt wird. So funktionieren auch die anderen Funktionen.

Da noch 14 Funktionen frei sind, schaue ich einmal durch meinen Sensor-Park und überlege, welche Sensoren noch Sinn machen würden und einfach zu implementieren sind... Und natürlich finde ich schnell ein paar vielversprechende Kandidaten.



Oben links abgebildet das Arduino Uno-Board zum Programmieren des ATmega328P. Rechts daneben eine Batteriebox (3V) zur Speisung der Echtzeituhr.

Auf dem Breadboard befinden sich von links nach rechts: Es sind genügend GPIO-Ports vorhanden für die acht Anschlüsse für den Datenbus an den 6522 als auch für die Sensoren. Wo die Sensoren jeweils angeschlossen sind, kann man dem Source-Code entnehmen.

Schnittstellen-Definition

Als nächstes benötigen wir eine Schnittstellendefinition, die festlegt, wie CoProzessor und Breadboard Computer miteinander zu kommunizieren haben. Es muss festgelegt werden, welche Funktion was in welchem Format liefert.

Ich habe dafür folgende Übersicht erstellt: Funktionen: 0 0b0000 = Zufallswert (4 Bit) 1 0b0001 = 433 MHz Fernbedienung DCBA (4 Bits, gedrücktes gesetzt) DHT11 Temp (Messbereich: 0...50°C, Auflösung 1°C (+/- 2°) 2 0b0010 = DHT11 Temp High (0...50) = 6 Bits 3 0b0011 = Temp Low DHT11 Luftfeuchte (Messbereich: 20...90% RH, Auflösung 1% (+/- 5%) 4 0b0100 = DHT11 Fcht High (0...90) = 7 Bits 5 0b0101 = Fcht Low RTC DS1307 6 0b0110 = YYYY Jahr High 1900 + 0...255 7 0b0111 = YYYY Low 8 0b1000 = MMMM Monat (0...12) = 4 Bits 9 0b1001 = DDDD Tag (0...31) = 5 Bits 10 0b1010 = DHHH Stunde (0...23) = 5 Bits 11 0b1011 = HHMM Minute (0...59) = 6 Bits 12 0b1100 = MMMM 13 0b1101 = xxSS Sekunde (0...59) = 6 Bits 14 0b1110 = SSSS 15 0b1111 = idle (Normalzustand 6522 Port) Die 4-Bit-Zufallswerte kann ich ja auch mehrmals hintereinander abfragen und dann aneinanderreihen, um ein ganzes Byte Zufallswert zu bekommen. Beim 433 MHz-Empfänger werden einfach die Bits gesetzt, die gedrückt sind. Hier muss ich mir auf Breadboard Computer Seite noch überlegen, ob ich Mehrfachdrucke haben will oder abbreche, sobald die 1. Taste gedrückt wurde.

Bei den DHT11-Werten gebe ich einfach eine in Nibbles aufgeteilte Ganzzahl (0...255) zurück. Da die Auflösung des DHT11 nicht so übermäßig ist, reicht hier jeweils ein Byte für Temperatur und Luftfeuchtigkeit.

Bei der Echtzeituhr musste ich tricksen, damit ich noch alles in den übrigen 9 Funktionen unterbringen konnte. Hier müssen mehrere Funktionen hintereinander abgefragt und zwischengespeichert werden und dann danach die Bits wieder richtig zusammengefügt werden, um auf Datum und Zeit zu kommen.

Die Software

Als erstes müssen natürlich die Bauteile verdrahtet werden. Und dann die entsprechenden Libraries eingebunden und der Code geschrieben werden.

Da ich schon alle Komponenten einmal in anderen Projekten verbaut hatte, stellten sich hier keine Schwierigkeiten. Das war eigentlich nur Copy und Paste und ein klein bisschen Anpassung. Allerdings habe ich für dieses Projekt die Arduino-IDE und nicht die Platform IO als Plattform benutzt, weil Visual Code mit Platform IO nicht alle bereits verwendeten Libs zur Verfügung stellt und ich dann neu programmieren müsste.

Aus folgenden Projekten habe ich den Source-Code kopiert:

Ein erster Test

Über die serielle Schnittstelle, die mir am Uno ja noch zur Verfügung steht, habe ich dann erst einmal getestet, ob alle Komponenten richtig reagieren und eine kleine Testfunktion geschrieben.



Wie im Video zu sehen, funktionieren die Komponenten zur Zufriedenheit. Nun müssen die Sensorwerte noch auf die Funktionen und Datenbits aufgeteilt werden.

Source-Code

Das ist mit ein bisschen Bit-Schubserei erledigt. Die sollte schnell durch den ATmega erledigt und die Daten schnell nach Anforderung geliefert sein.

bbc-coprozessor.ino (klicken, um diesen Abschnitt aufzuklappen)
//////////////////////////////////////////////////////// // (C) 2020 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/arduino/ // //////////////////////////////////////////////////////// // Co-Prozessor für den 6502-Breadboard-Computer // Anschluss an den 6522 Port B auf dem BBC // 4 Input-Bits (D4-D7) -> wählt Funktion aus // 4 Output-Bits (D0-D3) -> gibt 4-Bit-Returnwert zurück // Funktionen: // 0 0b0000 = Zufallswert (4 Bit) // --- 433 MHz Fernbedienung: http://cool-web.de/raspberry/433-mhz-codes-empfangen-und-den-raspi-ueber-funk-fernbedienen.htm // 1 0b0001 = 433 MHz Fernbedienung DCBA (4 Bits, gedrücktes gesetzt) // --- DHT11: http://cool-web.de/arduino/multi-function-shield-mit-ky-015-dht11-temperatur-luftfeuchtigkeit-sensor.htm#dht11 // DHT11 Temp (Messbereich: 0...50°C, Auflösung 1°C (+/- 2°) // 2 0b0010 = DHT11 Temp High (0...50) = 6 Bits // 3 0b0011 = Temp Low // DHT11 Luftfeuchte (Messbereich: 20...90% RH, Auflösung 1% (+/- 5%) // 4 0b0100 = DHT11 Fcht High (0...90) = 7 Bits // 5 0b0101 = Fcht Low // --- RTC DS1307: http://cool-web.de/arduino/echtzeituhr-rtc-auf-8fach-7segment-display.htm // 6 0b0110 = YYYY Jahr High 1900 + 0...255 // 7 0b0111 = YYYY Low // 8 0b1000 = MMMM Monat (0...12) = 4 Bits // 9 0b1001 = DDDD Tag (0...31) = 5 Bits // 10 0b1010 = DHHH Stunde (0...23) = 5 Bits // 11 0b1011 = HHMM Minute (0...59) = 6 Bits // 12 0b1100 = MMMM // 13 0b1101 = xxSS Sekunde (0...59) = 6 Bits // 14 0b1110 = SSSS // 15 0b1111 = idle (Normalzustand 6522 Port) #include <Arduino.h> #include <Wire.h> #include "RTClib.h" // RTC-Lib by JeeLabs http://news.jeelabs.org/code/ #include <DHT.h> // Libraries für DHT-11 #include <DHT_U.h> #define DHTTYPE DHT11 #define PinData0 2 #define PinData1 3 #define PinData2 4 #define PinData3 5 #define PinData4 6 #define PinData5 7 #define PinData6 8 #define PinData7 9 #define PinFBA 10 // 433 MHz Fernbedienung #define PinFBB 11 #define PinFBC 12 #define PinFBD 13 #define PinFBVT A1 #define PinDHT A2 #define PinLED A3 RTC_DS1307 RTC; DHT_Unified dht(PinDHT, DHTTYPE); // DHT Instanz erzeugen void test() { DateTime now; char msg[30]; byte z; sensor_t sensor; // DHT11 sensors_event_t event; dht.temperature().getSensor(&sensor); Serial.print ("Sensor: "); Serial.println(sensor.name); Serial.println(); while (1) { // Test RTC ------------------------------------------- DateTime now = RTC.now(); sprintf (msg, "%02d.%02d.%04d %2d:%02d:%02d", now.day(), now.month(), now.year(), now.hour(), now.minute(), now.second()); Serial.println (msg); // Test Zufallszahl ----------------------------------- z = random (0,15); sprintf (msg, "Zufallszahl (0...15): %2d", z); Serial.println (msg); // DHT11 dht.temperature().getEvent(&event); sprintf(msg, "Temperatur: %d °C", (int) event.temperature); Serial.println (msg); dht.humidity().getEvent(&event); sprintf(msg, "Luftfeuchte: %d %%RH", (int) event.relative_humidity); Serial.println (msg); sprintf(msg, "Fernbedienung: %d %d %d %d %d (ABCD, VT)", digitalRead(PinFBA), digitalRead(PinFBB), digitalRead(PinFBC), digitalRead(PinFBD), digitalRead(PinFBVT)); Serial.println (msg); Serial.println(); delay (500); digitalWrite(PinLED, !digitalRead(PinLED)); } //end while 1 } void setup() { pinMode(PinLED, OUTPUT); // Datenbus zum 6522er pinMode(PinData0, OUTPUT); pinMode(PinData1, OUTPUT); pinMode(PinData2, OUTPUT); pinMode(PinData3, OUTPUT); pinMode(PinData4, INPUT); pinMode(PinData5, INPUT); pinMode(PinData6, INPUT); pinMode(PinData7, INPUT); // 433 MHz Fernbedienung pinMode(PinFBA, INPUT); pinMode(PinFBB, INPUT); pinMode(PinFBC, INPUT); pinMode(PinFBD, INPUT); pinMode(PinFBVT, INPUT); Wire.begin(); RTC.begin(); // RTC-Objekt initialisieren // Serial.begin(115200); // while (!Serial) ; // wait for serial delay(200); if (! RTC.isrunning()) { // RT-Clock läuft nicht, initialisieren mit Kompilierungsdatum / Zeit RTC.adjust(DateTime(__DATE__, __TIME__)); // Serial.println ("RTC auf Kompilierungsdatum / Zeit gesetzt"); } dht.begin(); // DHT11 initialisieren randomSeed(analogRead(0)); // test(); } void loop () { byte func; byte f0, f1, f2, f3; byte data; byte b, b2; DateTime now; sensor_t sensor; // DHT11 sensors_event_t event; dht.temperature().getSensor(&sensor); while (1) { // Funktion ermitteln f0=digitalRead(PinData4); f1=digitalRead(PinData5); f2=digitalRead(PinData6); f3=digitalRead(PinData7); func = f3*8 + f2 *4 + f1*2 + f0; //func = 14; // TEST data=15; if (func == 15) { // idle data = 15; } else if (func == 0) { // Zufallszahl data = random (0,15); } else if (func == 1) { // 433 MHz Fernbedienung DCBA (4 Bits, gerücktes gesetzt) data = digitalRead(PinFBC)*8 + digitalRead(PinFBD)*4 + digitalRead(PinFBB)*2 + digitalRead(PinFBA); } else if (func == 2) { // DHT11 Temp High (0...50) = 6 Bits dht.temperature().getEvent(&event); b = event.temperature; data = b>>4; } else if (func == 3) { // Temp Low dht.temperature().getEvent(&event); b = event.temperature; data = b; } else if (func == 4) { // DHT11 Fcht High (0...90) = 7 Bits dht.humidity().getEvent(&event); b = event.relative_humidity; data = b>>4; } else if (func == 5) { // Fcht Low dht.humidity().getEvent(&event); b = event.relative_humidity; data = b; } else if (func == 6) { // YYYY Jahr High 1900 + 0...255 now = RTC.now(); b = now.year() - 1900; data = b>>4; } else if (func == 7) { // YYYY Low now = RTC.now(); b = now.year() - 1900; data = b; } else if (func == 8) { // MMMM Monat (0...12) = 4 Bits now = RTC.now(); b = now.month(); data = b; } else if (func == 9) { // DDDD Tag (0...31) = 5 Bits now = RTC.now(); b = now.day(); data = b>>1; } else if (func == 10) { // DHHH Stunde (0...23) = 5 Bits now = RTC.now(); data = now.hour()>>2; b = now.day() & 0b1; data |= (b<<3); } else if (func == 11) { // HHMM Minute (0...59) = 6 Bits now = RTC.now(); b = now.hour(); b2 = b & 0b00000011; data = b2<<2; b = now.minute(); b = b >> 6; data |= b; } else if (func == 12) { // MMMM now = RTC.now(); b = now.minute(); data = b; } else if (func == 13) { // xxSS Sekunde (0...59) = 6 Bits now = RTC.now(); b = now.second(); data = b>>4; } else if (func == 14) { // SSSS now = RTC.now(); b = now.second(); data = b; } data &= 0b1111; // Datenleitungen setzen lt. data digitalWrite(PinData0, data & 1); digitalWrite(PinData1, data & 2); digitalWrite(PinData2, data & 4); digitalWrite(PinData3, data & 8); /* Serial.print ("Funktion "); Serial.print (func); Serial.print (". Output: "); Serial.print (data,BIN); Serial.print (", dez.: "); Serial.println (data,DEC); */ // LED für Aktivitätsanzeige digitalWrite(PinLED, data == 15 ? LOW : HIGH); } }

Wie immer ist der Source-Code gut mit Kommentaren dokumentiert und sollte keine Frage offen lassen. Zu den einzeln verwendeten Komponenten gibt es extra Web-Pages im Cool-Web, auf denen deren Funktion jeweils detailliert erklärt sind (Links siehe oben).

Wenn der ATmega im produktiven Modus arbeitet, ist die serielle Schnittstelle ausgeschaltet (die entsprechenden Zeilen sind auskommentiert). Für den Test müssen die entsprechenden Zeilen wieder entkommentiert werden.

Wie man sieht, reicht der ATmega328P dicke für die Aufgabe. Es ist noch reichlich Platz: Der Sketch verwendet 9178 Bytes (28%) des Programmspeicherplatzes. Das Maximum sind 32256 Bytes. Globale Variablen verwenden 517 Bytes (25%) des dynamischen Speichers, 1531 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes. Jetzt muss der ATmega "nur" noch in den Breadboard-Computer integriert werden und dort die Gegenseite der Schnittstelle in Assembler programmiert werden und "schon" kann der Breadboard Computer eine ganze Menge mehr.

Schaut dort gerne vorbei, wenn euch auch die Mikroprozessor-Programmierung eines 6502 mit Assembler interessiert.