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:
- LED zur Anzeige von Aktivität (an A3)
- 433 MHz-Fernbedienungsempfänger. Dazu gehört eine Funkfernbedienung mit 4 Knöpfen A bis D, die das entsprechende Bit auf dem 4-Bit-Datenbus setzen sollen. Anwendungsbeispiel wäre: Es werden 4 Fernbedienungen an bis zu 4 Spieler ausgegeben als "Buzzer". Auf dem LCD erscheint eine Frage. Wer meint, die Frage richtig beantworten zu können, drückt seinen Buchstabe auf der Fernbedienung. Der Breadboard Computer erkennt, wer zuerst gedrückt hat und der Kandidat muss die Frage richtig beantworten per Multiply-Choice (3 Zeilen = 3 Antworten), indem er A, B oder C drückt. War die Frage richtig, bekommt der Spieler einen Punkt. War sie falsch, die Mitspieler jeweils einen.
- DHT11 Sensor für Temperatur und Luftfeuchtigkeit
- Echtzeituhr DS1307 für Datum und Zeit
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:
- 433 MHz-Fernbedienungsempfänger: 433 MHz-Codes empfangen und den Raspi über Funk fernbedienen
- DHT11 Sensor: Auslesen des DHT11-Sensors und Anzeige von Temperatur und Luftfeuchtigkeit auf dem Multi Function Shield
- Echtzeituhr DS1307: Echtzeituhr mit RTC auf 8fach-7Segment-Display anzeigen
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.
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.