Kommunikation via GPIB mit dem AVR

This Article in english: 

Im folgenden sind meine Experimente zum Thema Nutzung des GPIB-Busses mit AVR Mikrocontrollern beschrieben.

Hinweis: Beschrieben ist -wie immer auf meinen Seiten- ein Versuchsaufbau im Experimentallabor. Es handelt sich keinesfalls um produktiv nutzbare Hard- oder Software. Der Versuchsaufbau kann allerhöchstens Basis für eigene Versuche dienen. Ich schreibe dies, weil GPIB-fähige Geräte typischerweise sehr teuer in der Anschaffung sind. Man kann sich beim Experimentieren durchaus den GPIB-Anschluss kaputt machen, daher diese ausdrückliche Warnung. Sagen Sie nicht ich hätte Sie nicht gewarnt 🙂

Der GPIB (General Purpose Interface Bus) ist ein von HP in den sechziger Jahren entwickeltes Bus-System. Es wurde vorrangig für die Vernetzung komplexer Messgeräte entwickelt, um z.B. automatisiertes Testen zu ermöglichen. Das Bus-System wurde durch HP und andere Hersteller weiter entwickelt und ausgebaut und unter dem Namen IEEE 488 standardisiert. Der Bus ist auch unter den Namen HPIB und IEC 625 bekannt.

Im Bereich hoch entwickelter Messgeräte ist der GPIB-Bus auch über 40 Jahre nach seiner Entwicklung noch weit verbreitet. Insbesondere Gebrauchtgeräte aus den 80ger und 90er Jahren haben oft ein GPIB-Interface.

Mein bei eBay erstandenes digitales Speicheroszilloskop Tektronix 2432A von 1989 besitzt ein GPIB-Interface. Das Oszilloskop lässt sich mittels GPIB komplett fernsteuern, es lassen sich alle Einstellungen abfragen und die momentan gespeicherten/dargestellten Wellenformen auslesen. Die Idee war daher, vom PC aus auf diese Daten zuzugreifen.

Tektronix 2432a Cursor

Es gibt für PCI, USB, ISA und RS232 Karten und Geräte, welche mit dem Rechner verbunden werden können und die eine GPIB-Schnittstellen besitzen. Diese Karten und Geräte sind sehr teuer, ich habe keine Lösung -auch nicht gebraucht- für unter 100 Euro gesehen. Ich habe daher versucht, mit einem AVR Mikrocontroller die Funktion eines GPIB-Geräts nachzubilden.

Der GPIB-Bus ist ein paralleler Bus. Geräte können mit speziellen Kabeln, die an den Enden sowohl eine Buchse als auch einen Stecker besitzen, in einer „Daisy Chain“ verbunden werden. Jedes Gerät am Bus hat typischerweise zwei Adressen (eine Listener und eine Talker-Adresse). An einem Bus können maximal 15 Geräte adressiert werden. Eines der Geräte fungiert als Controller in Charge. Der Controller steuert den Datenfluss zwischen allen anderen Geräten mittels Bus-Kommandos. Es kann auf einem Bus immer nur einen Talker gleichzeitig geben, es kann aber immer mehrere Listener geben. Typischerweise kann man bei einem GPIB-Gerät dessen Adresse und dessen Verhalten einstellen. Man kann z.B. ein Gerät als Listener, als Talker oder als Listener+Talker einstellen.

Trivialansatz: Talker/Listener Szenario

Falls man genau einen Talker und einen Listener hat, benötigt man keinen Controller. Der Talker gibt seine Daten auf den Bus aus, ohne einen speziellen Listener zu adressieren und der Listener liest die Daten vom Bus herunter. Dieser Fall tritt auf, wenn man an ein Oszilloskop einen Drucker oder Plotter anschließt. Diesen Fall habe ich zuerst aufgebaut.

Hardware

Ich habe einen ATMega32 genommen und diesen auf übliche Weise mittels RS232 mit meinem PC verbunden. Der AVR wird dann direkt mit dem GPIB-Bus verbunden. Hierbei sind einige Dinge zu beachten, die im weiteren erwähnt werden.

Die Leitungen des GPIB-Busses

Der GPIB-Bus wird auf eine 24-polige Centronics-Buchse geführt. 8 Leitungen (DIO1..8) transportieren das Datenbyte. Drei Leitungen (DAV, NRFD und NDAC) sind für den Handshake auf dem Bus verantwortlich. 5 Leitungen (ATN, SRQ, EOI, REN, IFC) sind für das Management des Busses verantwortlich. Somit benötigt man für eine Vollbelegung 16 Leitungen. Die restlichen Pins der Buchse sind Masse-Leitungen.

Tektronix 2432a GPIB Buchse

Die Leitungen des Busses sind „Open Collector“ beschaltet, und „Active Low“. D.h. eine logische „0“ bedeutet, dass die Leitung HIGH, also „1“ ist. Eine logische „1“ bedeutet, dass die Leitung LOW, also „0“ ist. Die Open Collector-Beschaltung sorgt dafür dass mehrere Listener gleichzeitig an einer Leitung hängen können und unabhängig voneinander die Leitung auf LOW schalten können, ohne das dabei eine Ausgangsstufe eines der Geräte kaputt geht.
Solange nur eine Konfiguration Talker->Listener vorhanden ist und kein weiterer Listener im Spiel, braucht man auf das Thema Open Collector nicht zu achten.

Ich habe die 8 Datenleitungen auf ein Port (Port A) des AVR geführt. Die Management-Leitungen habe ich erstmal ignoriert und die drei Handshake-Leitungen auf weitere freie Pins des AVR geführt. Somit habe ich 11 Leitungen verwendet.

Software

Das spezielle Drei-Draht-Handshake des GPIB-Busses ist an verschiedenen Stellen im Internet beschrieben (z.B. hier). Der Handshake läuft wie folgt ab:

  1. Listener signalisieren mit NRFD (Not Ready For Data), wenn sie Daten empfangen können (sie setzen NRFD auf HIGH). Wenn der letzte Listener HIGH setzt, geht NRFD auf HIGH.
  2. Talker bemerkt dies und setzt Daten auf die Datenleitungen und zieht DAV (Data Valid) auf LOW.
  3. Listener bemerken dies und setzen für die Zeit des Lesens der Daten NRFD auf LOW. Wenn ein Listener mit dem Lesen fertig ist, setzt er NDAC auf HIGH. Wenn der letzte Listener fertig ist, geht dadurch NDAC auf HIGH. Dies bemerkt der Talker und setzt DAV auf HIGH. Daten sind nicht mehr gültig.
  4. Nach einer gewissen Zeit setzen die Listener NDAC wieder auf LOW, und NRFD auf HIGH so dass der Zyklus für das nächste Byte beginnen kann.

Dieses Protokoll habe ich zunächst in einer Schleife mittels Polling implementiert. Es wurden zwar Daten gelesen, aber dasselbe Datum gleich mehrfach, ich habe den Übergang DAV nach HIGH im Schritt 3 einfach nicht mitbekommen, egal was ich probiert habe. Es ging soweit, dass ich glaubte, dass in der TALKER/Listener-Konfiguration DAV immer LOW bleibt. Nachmessen mit dem Oszilloskop hat aber gezeigt, dass DAV sehr wohl zwischen HIGH und LOW wechselt, und zwar mit etwa 3300Hz. Das Oszilloskop sendete im Versuch den Bildschirminhalt, etwa 3000 Bytes. Möglicherweise ist Polling hier zu langsam (siehe Bemerkung weiter unten, es war ein Programmierfehler, Polling ist nicht zu langsam).

Ich habe die Software daher verändert, und zwar so, dass auf der fallenden und steigenden Flanke des DAV-Signals (das an INT0 anliegt) ein Interrupt ausgelöst wurde. Diese Interrupts habe ich zunächst einfach gezählt. Es waren etwa 6000 Interrupts, was gut zu den etwa 3000 Zeichen passte. Ich habe dann die Implementierung ganz auf Interrupts umgestellt, so dass die Zeichen in der Interrupt-Service-Routine gelesen werden. Danach wurden alle Zeichen sauber übertragen.

Im folgenden ist die zentrale Interrupt-Routine dargestellt. Da die Daten in negativer Logik ankommen, werden sie mit 0xff per XOR verknüpft, um das echte Zeichen zu erhalten. Die „dav“-Variable verwende ich außerhalb der ISR, um festzustellen ob ein neues Zeichen eingetroffen ist. Dort wird sie auch zurückgesetzt:

ISR(INT0_vect) {
   if (irq_falling_edge==1) {
         
      // handshake: clear NRFD, means i am busy now to read data
      PORTD &= ~_BV(G_NRFD);         
      // read data
      byte = PINA ^ 0xff;
      // handshake: set ndac, means i have completed/accepted the read
      PORTD |= _BV(G_NDAC); 
      // set data valid flag
      dav=1;
   
      irq_falling_edge=0; // wait for next raise
      MCUCR = (1<<ISC00)|(1<<ISC01); // raise int0 on rising edge
   } else {
      // handshake: clear ndac (this is a prerequisite for receive next byte)
      PORTD &= ~_BV(G_NDAC);
   
      irq_falling_edge=1; // wait for next fall
      MCUCR = (1<<ISC00); // raise int0 on falling edge
   }
}

Ein weiteres Problem ist, dass ich das Ende der Übertragung zunächst nicht erkannte. Der GPIB-Bus nutzt hierfür die EOI-Leitung, die der Talker beim Übertragen des letzten Bytes auf LOW zieht. Diese Leitung hatte ich aber nicht verwendet. Das Oszilloskop kann jedoch zusätzlich am Ende einer Übertragung ein CR schicken. Dies habe ich am Oszilloskop eingestellt, danach konnte ich auch das Ende der Übertragung feststellen. Für Binärübertragung muss man allerdings den Weg über die EOI-Leitung gehen.

Im folgenden ist die so gewonnene Ausgabe eine WAVEFORM dargestellt. Es handelt sich um ein Rechtecksignal. Die momentane Einstellung der Y-Achse von Kanal 1 und der Zeitachse sowie weitere Einstellungen werden als Präambel (WFRMPRE) vorangeschickt. Danach folgt ab „CURVE“ die Waveform-Beschreibung. Werte um 50 sind das HIGH, Werte um 0 sind das LOW des Signals. Die Ausgabe wird als eine lange Zeile geschickt, ganz am Ende kommt ein CR-Zeichen. Der Übersichtlichkeit halber habe ich die Ausgabe in mehrere Zeilen aufgeteilt.

WFMPRE WFID:"CH1 DC  20mV  10ms NORMAL",NR.PT:1024,PT.OFF:512,PT.FMT:Y,XUNIT:SEC,XINCR:2.000E-4,YMULT:
8.000E-4,YOFF:0,YUNIT:V,BN.FMT:RI,ENCDG:ASCII;
CURVE 0,0,-1,0,0,1,-1,0,0,0,1,1,49,50,50,50,49,49,50,51,49,50,50,50,50,51,50,50,50,50,50,50,
50,51,50,50,50,52,50,52,50,49,51,51,50,50,50,49,50,50,51,52,50,52,49,50,50,51,50,50,51,50,0,
0,1,1,-1,0,0,0,-1,0,-1,-1,-1,0,0,-2,0,0,0,0,0,-1,-1,1,0,0,-1,0,1,0,1,0,1,-1,1,1,-1,0,1,0,0,1,1,0,1,
0,-1,1,-2,0,50,51,50,50,49,51,52,50,51,51,50,50,51,50,52,49,51,52,51,50,51,50,52,50,51,52,50,
50,49,51,50,51,50,50,51,50,52,49,51,51,51,51,49,50,52,51,50,50,51,51,2,0,1,-2,0,-1,1,-1,0,1,0,
0,0,-1,-1,1,0,-1,1,0,0,-1,0,-2,1,0,0,-2,0,0,-1,-1,0,1,-1,-1,-1,0,0,-1,-2,-1,-1,1,-1,1,0,1,-2,-1,49,49,
48,51,50,50,50,50,49,52,51,51,52,50,50,51,49,49,51,50,51,50,50,49,50,51,51,51,51,51,51,50,50,
50,52,51,51,50,50,50,51,50,51,52,52,51,51,52,50,50,1,-1,0,0,1,0,-1,-1,1,-1,1,-1,-1,0,0,1,0,0,-1,
-1,-2,-1,1,0,0,-1,1,-1,1,-2,-1,-1,-1,-1,0,1,1,1,-1,1,-1,1,-1,-2,0,-1,-1,-2,1,0,50,50,50,50,50,50,50,
51,51,50,51,50,50,50,49,51,51,50,50,51,51,50,51,50,52,52,50,51,50,50,51,50,50,51,50,50,51,50,
51,51,51,50,50,51,49,49,51,49,49,50,0,0,0,-1,0,-1,1,0,0,-1,0,0,-1,0,0,1,-1,0,-2,0,0,0,-1,-1,-1,-1,
0,0,0,-1,-1,0,-2,0,-1,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-2,-1,0,49,50,49,50,49,51,50,50,51,50,51,50,50,49,
50,52,52,49,48,51,51,50,50,51,51,51,50,50,51,52,50,50,51,51,50,51,51,50,49,51,50,50,52,50,52,
50,49,50,50,50,0,0,0,0,0,0,-1,0,0,0,0,0,0,-1,-1,-1,0,1,1,-1,-1,0,0,0,-1,-2,0,0,1,0,-1,1,1,0,0,1,-1,-1,
0,-1,0,-1,2,0,0,0,-3,0,1,0,50,50,51,49,51,52,50,49,50,51,50,50,50,51,51,50,51,51,51,52,49,50,49,
50,50,49,51,53,49,51,50,48,51,51,51,50,49,51,51,49,51,50,52,51,50,50,50,51,52,50,-1,-1,0,0,-2,-1,
-1,-1,0,-2,-1,-1,-1,0,0,-2,0,-1,0,-1,-1,-1,0,-1,-1,0,0,-2,0,0,-1,1,0,-3,0,0,-2,0,-1,-1,0,0,-1,0,0,0,-2,-1,
0,-1,50,50,51,49,51,49,50,51,50,50,50,50,49,51,51,50,51,51,50,51,50,49,50,51,50,50,49,50,52,50,51,
50,49,50,51,51,50,51,50,52,50,51,52,50,50,50,50,51,51,51,-1,0,0,-1,0,0,0,0,-1,0,0,1,-2,0,0,-1,0,0,0,0,
-1,0,-1,-1,-1,0,0,0,1,-1,0,0,-1,0,-1,0,-1,-1,1,1,0,0,-1,0,-1,-1,-1,-1,1,0,50,51,50,51,51,50,50,51,50,51,49,
52,50,50,50,51,50,51,49,51,52,50,51,50,51,50,51,52,48,49,49,50,48,49,50,51,50,50,50,50,50,50,51,49,
50,51,48,50,51,52,0,0,0,0,0,0,-2,0,0,0,0,-1,-1,0,-1,0,0,-1,0,1,0,-1,-1,-2,0,0,0,0,0,-1,1,0,0,-1,0,-1,-1,1,
0,0,0,-1,0,-1,0,0,0,-1,-1,-2,49,50,50,52,50,49,50,50,49,50,52,50,49,50,51,52,49,49,49,51,51,50,50,50,50,
49,51,50,50,50,51,52,50,50,50,50,50,50,49,52,51,50,50,50,51,52,50,50,52,49,0,-1,0,-1,0,1,2,-1,-1,-1,0,1,
0,-1,0,0,1,0,0,-1,0,1,0,0,-2,0,-1,-1,0,-1,-1,-1,0,-1,0,-1,0,-1,0,0,1,-1,-2,-1,-2,1,-1,-1,1,1,50,48,49,49,49,
50,50,51,50,51,50,50,52,50,51,52,51,51,52,50,52,49,50,49,51,49,52,50,50,50,48,51,50,50,49,51,51,50,51,
50,50,48,50,52,51,49,50,50,49,51,0,-2,-1,-1,1,0,0,0,0,1,-1,-2,0,1,0,-2,-1,0,0,0,-1,-1,0,0,0,0,-2,1,0,-1,-1,-1,
-1,0,-2,-1,-1,0,-1,-1,-1,-1,0,-1,0,-2,0,0,1,-2,49,50,51,51,49,50,52,51,50,51,52,51

Im Ergebnis kann man mit dieser Konstellation Daten, die das Oszilloskop schickt, sauber zum PC übertragen. Man kann aber leider keine Kommandos zum Oszilloskop schicken. Dies würde die Erweiterung des Beispielcodes um Controller-Funktionen und die Verwendung zusätzlicher Leitungen erfordern. Außerdem muss man dann das Thema „Open Collector“ lösen, sonst geht entweder der ATMega32 oder aber das GPIB-Interface des Oszilloskops kaputt.

Ein Versuch mit HPGL-Ausgabe, die das Oszilloskop auch beherrscht, zeigt ein neues Problem: Statt in einer langen Zeile kommen die HPGL-Daten zeilenweise. Meine Erkennung des Übertragungsendes funktioniert damit nicht mehr.

Hier beispielhaft die HPGL-Ausgabe (nicht komplett):

in;sc-690,689,-512,511;
sp1;
pa-500,-400;pd;pr1000,0,0,800,-1000,0,0,-800;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl0,1;yt;pr0,100;
tl0,1;yt;pr0,100;
tl0,1;yt;pr0,100;
tl0,1;yt;pr0,100;
tl0,1;yt;pr0,100;
tl0,1;yt;pr0,100;

Ich habe eine HPGL-Ausgabe mit dem Tool „hp2xx“ in ein JPEG überführt. Die Ausgabe kann man in kermit mittels „log session <datei>“ in eine Datei umleiten. Das hpxx-Kommando konvertiert mit folgender Abfolge:

rm -f tmp.eps out.pdf
./hp2xx -p 22222222 -C -o 20 -O 150 -h 100 -a 2  -m eps -d 600 $1 -f tmp.eps
ps2pdf tmp.eps out.pdf
acroread out.pdf

Die Kurve und die Skala ist ok, die Beschriftung kommt viel zu klein heraus. Mit der Option „-d xxx“ kann man die DPI-Auflösung des Ergebnisbildes beeinflussen, ab 300 dpi kann man den Text lesen.

Versuche mit dem Tool hpgl2ps waren schlechter als die mit hp2xx.

hp2xx Output

Elaborierterer Ansatz: Talker/Listener Szenario

Als nächstes habe ich zusätzlich die Leitungen EOI, ATN, SRQ, IFC und REN an den AVR geführt. Die Beschaltung sieht dann tabellarisch wie folgt aus:

GPIB Pin Name  GPIB-Buchse Pin Nummer  ATMega32 Pin Name  AVR Board Buchse Pin Nummer
DIO1..8 (Datenpins) 1..4 und 13..16 PortA0..PortA7  1..8
EOI 5 PD4 30
DAV 6 PD2 28
NRFD 7 PD3 29
NDAC 8 PD5 31
SRQ 10 PD6 32
ATN 11 PD7 33
IFC 9 PB0 9
REN 17 PB1 10
GND 23 GND 39

Nutzung des EOI-Signals

Eine Untersuchung des EOI-Signals mit einem zweiten Oszilloskop ergab, dass das Signal ohne Übertragung LOW ist, während der kompletten Übertragung auf HIGH gehalten wird und nach dem letzten Zeichen wieder auf LOW geht. Dies unterscheidet sich von der GPIB-Dokumentation, die ich im Internet gefunden habe. Dort wird beschrieben, dass das EOI-Signal nur genau für das letzte Zeichen einer Übertragung  LOW gezogen wird. Entweder funktioniert mein Oszilloskop anders als der Standard oder ich lese die Beschreibungen nicht richtig.

Beschaltung der Pins des AVRs für Open Collector

Die AVR Controller sind zwar extrem flexibel, man kann aber die Port-Pins nicht direkt in eine Open Collector Modus schalten. Man könnte nun Open-Collector-fähige Bustreiber zwischenschalten, aber dafür bin ich zu faul. Eine Suche im Internet ergibt, dass man scheinbar die Open-Collector-Schaltung simulieren kann, indem man auf den Pin ein LOW schreibt und dann mittels des DDRx-Registers den Pin umschaltet. Die Idee bedeutet im einzelnen(am Beispiel PORTD und Pin PD4):

Funktion „normale“ Beschaltung Open Collector Simulation
Initialisierung des Pins DDRD |= _BV(PD4) // als Ausgang
DDRD &= ~BV(PD4) // als Eingang
PORTD &= ~_BV(PD4)
DDRD &= ~_BV(PD4)
Pin auf HIGH setzen PORTD |= _BV(PD4) DDRD &= ~_BV(PD4)
Pin auf LOW setzen PORTD &= ~_BV(PD4) PORTD &= ~_BV(PD4)
DDRD |= _BV(PD4)
Pin lesen unsigned char p = PIND & _BV(PD4) DDRD &= ~_BV(PD4)
unsigned char p = PIND & _BV(PD4)

Nach Umstellung funktioniert mein Code noch, die Simulation scheint also zu funktionieren. Als nächstes steht die Implementierung der Talker und Controller-Funktionalität an.

Nach einigem Hin- und Her bin ich völlig von der Interrupt-gesteuerten Leseroutine abgekommen. Die Probleme, die ich beim Pollen hatte, waren auf einen Fehler meinerseits (Lesen von PORTx statt von PINx, was nicht funktioniert) zurückzuführen. Nach der Implementierung der Lesefunktion gpib_read() und der Schreibfunktion gpib_write() stand die Implementierung der Controllerfunktionalität an.


Das Experimentierboard von Pollin. An die 40-polige Buchse (IDE-Buchse) ist ein altes IDE-Kabel angeschlossen, an den der GPIB-Stecker angelötet wurde.

GPIB Controller Implementierung

Der Controller übernimmt am Anfang den Bus  und steuert alle Geräte am Bus, d.h. er definiert sie als Listener, Talker oder „Taub“, also nicht am Bus partizipierend. Dazu nutzt er Kommandoleitungen (wie IFC und REN) und Bus-Kommandos (wie DCL, UNL, UNT). Ein Kommandobyte auf dem Bus unterscheidet sich von einem Datenbyte dadurch, dass der Controller die ATN-Leitung während des Sendens von Kommandobytes auf logisch 0 zieht.

Geräte werden durch das Ansprechen über ihre Listener-Adresse (=Geräteadresse + 0x20) zu Listenern und durch das Ansprechen über ihre Talker-Adresse (=Geräteadresse +0x40) zu Talkern. Es darf immer nur einen Talker geben, aber mehrere Listener. Der Controller hat üblicherweise, aber nicht zwingend, die Geräteadresse 0x00. Kommandos setzt der Controller mit der Funktion gpib_cmd() ab.

Meine simple Implementierung des Controllers sieht wie folgt aus:

  1. Übernahme des Busses durch REN und IFC (Funktion gpib_init() und gpib_controller_assign()), dann Senden von DCL (Funktion gpib_cmd()). Alle Geräte gehen dadurch in einen definierten Anfangszustand.
  2. Ausführen einer Endlosschleife:
    1. Kommandos werden von STDIN gelesen. Ein CR zeigt das Kommandoende an. Der Controller schaltet sich als Talker und und ein weiteres Gerät als Listener (z.B. mein Oszilloskop mit Geräte-Adresse 0x01). Das Kommando wird auf den Bus gelegt (gpib_cmd() + gpib_write()). Das Kommandoende wird über die Leitung EOI angezeigt (ein reale Controller würde sicher komplexer vorgehen…).
    2. Die Listener lesen das Kommando und führen es aus. Bei Kommandos ohne Antwort geht der Controller wieder in die Schleife, um neue Kommandos zu lesen. Bei Kommandos mit Antwort schaltet der Controller sich als Listener und ein weiteres Gerät (mein Oszilloskop) als Talker. Die Antwort wird vom Gerät gelesen (gpib_read()). Das Gerät zeigt das Ende der Antwort über die Leitung EOI an. Der Controller gibt jedes gelesene Zeichen auf STDOUT aus. Nach Ausgabe der Antwort geht der Controller wieder in die Schleife, um neue Kommandos zu lesen.
      Der Controller unterscheidet Kommandos mit und ohne Antwort anhand des Zeichens „?“ am Ende des Kommandos (stimmt nicht immer…).
  3. Asynchron Verarbeitung neben der Endlosschleife in Punkt 2.: Falls ein Gerät von sich aus, also ohne Einladung durch den Controller, am Bus aktiv werden will, erzeugt es einen Service Request SRQ (Ziehen der SRQ-Leitung auf logisch 0). Der Controller bekommt dies mit, ermittelt mittels eines Serial Polling genannten Vorgehens den Erzeuger des SRQs und liest dessen Status ein. Der Status wird auf STDOUT ausgegeben (Funktion gpib_serial_poll()). Serial Polling bedeutet, dass der Controller mit dem Kommando SPE (Serial Polling Enabled) das Polling einleitet. Alle Geräte werden dann hintereinander als Talker definiert, der Controller definiert sich als Listener und liest Daten vom Talker ein. Beim seriellen Polling gibt jeder Talker nur genau 1 Byte, sein Status Byte aus. Das SRQ-erzeugende Gerät hat das Bit 6 im Statusbyte gesetzt. Damit weiß der Controller, welches Gerät den Request erzeugt hat. Der Controller beendet mit SPD (Serial Polling Disable) das Polling, alle Geräte verhalten sich von da ab wieder „normal“. Das Behandeln des SRQs ist gerätespezifisch. Mein Oszilloskop teilt mit dreistelligen Errorcodes, wie z.B. „157“ Fehler bei der Kommandoerkennung („vertippt“) und ähnliches mit.
  4. Theoretisch gibt der Controller am Ende den Bus wieder frei (Funktion gpib_controller_release()).

Beim Handshake kann man den Controller mittels busy waiting auf das Ändern einer Handshake-Leitung warten lassen. Falls die Änderung nie kommt, weil z.B. das Gerät abgeschaltet wurde oder das Gerät den Handshake nicht richtig mitmacht, wartet der Controller dann ewig. Ich habe daher diese busy-waiting-Schleifen daher durch Schleifen ersetzt, die nach einem Timeout (5s) abbrechen. Damit bleibt der Controller auch in diesen Fällen benutzbar.

Im folgenden ist eine Session dargestellt. Wenn das Oszilloskop eingeschaltet wird und bereit ist, generiert es einen SRQ. Der Controller fragt bei der SRQ-Behandlung mittels „EVENT?“ den SRQ-Grund ab, es kommt die Antwort „401“. 401 bedeutet laut Oszi-Handbuch „2432A was just powered on.“. Danach folgt die User Eingabe „id?“ auf die das Gerät mit der Ausgabe seiner Typenbezeichnung antwortet. Danach werden ein paar Kommandos abgesetzt, die keine Antwort liefern:

INIT
MEASUREMENT WINDOW:ON
CURSOR FUNCTION: TIME,TARGET:CH1,UNITS:BASE
CURSOR TPOS:ONE:200,TPOS:TWO:500

Zum Schluss wird mittels „CH1?“ die aktuelle Konfiguration des Kanals 1 abgefragt.

dennis@soc:~> kermit -c
Connecting to /dev/ttyS2, speed 19200
 Escape character: Ctrl- (ASCII 28, FS): enabled
Type the escape character followed by C to get back,
or followed by ? to see other options.
----------------------------------------------------
 
SRQ detected.
status byte from device 0x41 = 0x41 (char=A)
SRQ emitter is device = 0x41
 
command: EVENT?
Query. Will check for answer.
EVENT 401
> id?
command: id?
Query. Will check for answer.
ID TEK/2432A,V81.1,"24-DEC-89  V2.30 /2.5"
> INIT
command: INIT
Command only.
> MEASUREMENT WINDOW:ON
command: MEASUREMENT WINDOW:ON
Command only.
> CURSOR FUNCTION: TIME,TARGET:CH1,UNITS:BASE
command: CURSOR FUNCTION: TIME,TARGET:CH1,UNITS:BASE
Command only.
> CURSOR TPOS:ONE:200,TPOS:TWO:500
command: CURSOR TPOS:ONE:200,TPOS:TWO:500
Command only.
> CH1?
command: CH1?
Query. Will check for answer.
CH1 VOLTS:1E-1,VARIABLE:0,POSITION:0,COUPLING:DC,FIFTY:OFF,INVERT:OFF
>

Fazit: Insgesamt funktioniert somit die Implementierung eines Controllers mit dem AVR komplett in Software recht gut. Für einen echten Praxis-Einsatz müsste man die Software noch stark erweitern und vor allen auch mit anderen Geräten testen.

Hier die Software unter GNU General Public Licence als ZIP-Datei. Die ZIP-Datei enthält auch die Dateien uart.h/uart.c von Peter Fleury für die RS232 Kommunikation, die ebenfalls unter der GNU GPL steht.

ZIP-File mit allen Dateien:  gpib-0.66.zip

Versionshistorie:
0.1: Kommunikation mit einem Gerät (Tek 2432 getestet) ok.
0.63: Kommunikation mit 2 Geräten (und vermutlich auch mit mehr als 2) möglich. Fehler in Lesefunktion entfernt.
0.66: doxygen Dokumentation hinzugefügt

Eine Aufzeichnung einer kompletten Session mit 2 Geräten ist hier zu finden: Session mit 2 Geräten mit Code Version 0.63. Die verwendeten Geräte waren ein Oszilloskop Tektronix 2432A und ein Logik Analysator Tektronix 1241. Bei den Geräten wird die ID? abgefragt, beim Oszilloskop die momentane Konfigurationen der beiden Kanäle CH1? und CH2?, die Konfiguration für die Darstellung der aktuellen angezeigten Kurve WFMPRE? und dann die Kurve selbst (CURVE?). Das Datenformat wird dabei von Binary nach ASCII verändert. Danach wird der Logic Analysator angesprochen und mit REFMEM? der momentane Inhalt des Referenzspeichers ausgelesen.

Aufbau des Geräts

Im folgenden ein paar Bilder zum aufgebauten Gerät.

 

Weiterführende Links

Communication via GPIB with AVR

This document in german:

I have experimented using GPIB together with the AVR microcontroller.

Warning: I describe here -as I do in all my web documents- a lab experiment. I do not describe a method to be used in production environments, neither hardware nor software. The experiments I describe may serve as a base for your own experiments. I am writing this because GPIB-devices are usually expensive. During experiment, it is possible to destroy the GPIB interface of the device. So you have been warned…
The GPIB (General Purpose Interface Bus) was developed by HP in the sixties. It was introduced mainly for connecting complex lab equipment for example to do automated testing. The bus system was improved by HP and other companies and starndardized as IEEE 488. The GPIB bus is also known as HPIB and IEC 625.

In the area of complex lab equipment, GPIB is after 40 years still widely in use. Especially high end surplus devices from the eighties and nineties often have a GPIB interface.

My oscilloscope Tektronix 2432A of 1989, bought by ebay, has a GPIB interface. The scope can be controlled completely by GPIB, for example all settings can be queried and the stored waveforms can be retrieved. So I tried to connect the scope to my PC.

Tektronix 2432a Cursor

For PCI, USB, ISA and RS232, commercial solutions are available, which server as a bridge between todays PC interfaces and GPIB. These devices are still today very expensive, I haven’t seen any solution below 100 Euro, even used on ebay. So I tried to build my own device with an AVR microcontroller.

GPIB is a parallel bus. Devices can be connected in a daisy chain with special cables, which have a female and a male connector at each end. Each devide has two addresses on the bus (a talker and a listener address). At one GPIB bus, a maximum of 15 devices can be interconnected. One of these devices serves as the „Controller in Charge“. The controller controls data flow between devices using bus commands. On the bus, there can be one one talker at the same time, but there can me many listeners. The device address and the basic behaviour (listener only, talker only, talker+listener) can be configured usually at the device.

Trivial approach: Talker/Listener Scenario

If there is only one talker and one listener on the bus, no controller is needed. The talker puts his data on the bus without addressing explicit a listener. The listener read all data from bus. This scenario takes place if an oscilloscope is connected to a printer or plotter. I started implemented this scenario first.

Hardware

I took a ATMega32 and connected it in the usual way via RS232 to my PC. The AVR port lines then are connected directly to the GPIB bus. Doing this, some things have to be taken into account. This is described below.

The signal lines of GPIB bus

The GPIB bus uses a 24-pin centronics interface. 8 lines (DIO1..8) are for the data bytes. Three lines (DAV, NRFD and NDAC) are for the handshake on the bus. 5 lines (ATN, SRQ, EOI, REN, IFC) are for bus management. This means that in total 16 lines are required. The remaining lines are ground lines.

Tektronix 2432a GPIB Buchse

The lines are „open collector“ and „active low“. This means a logical „0“ means that the signal is set. A logical „1“ means that the signal is not set. „Open Collector“ means that several listener can share the lines and can set a line independent from the the other listeners without destroying their output drivers. As long as there is only one talker and exaclty one listener and in total only two devices, the „open collector“ topic can be ignored.

I put the 8 data lines to one port (Port A) of the AVR. I ignored the management lines in the first step and used only the handshake lines, which i put to further three ports of the AVR. So I used 11 lines in this scenario.

Software

The three wire handshake on the GPIB bus is described at many places in internet (e.g. here). I will not describe it again.

After implementing the handshake I realized that I do not recognize the end of a data transmission in all cases. The GPIB bus uses the EOI line for that. The talker sets this line during sending the last byte of a transmission. I have not used this line so far. My scope can be configured to send also a CR at the end of all transmissions, so I was saved in that case. For sending binary data, this is not a solution.

The following paragraph shows the output created by the implementation described. The waveform received from the oscilloscope is a square wave. The settings for y-axis of channel 1 and further settings are sent before the waveform itself as the block following WFMPRE. The waveform is sent as block after CURVE. Values about 50 are the Ones from the signal, values about 0 are the zeroes. For better viewing, i split the single curve line into several lines.

WFMPRE WFID:"CH1 DC  20mV  10ms
NORMAL",NR.PT:1024,PT.OFF:512,PT.FMT:Y,XUNIT:SEC,XINCR:2.000E-4,YMULT: 8.000E-4,YOFF:0,YUNIT:V,BN.FMT:RI,ENCDG:ASCII; 
CURVE 0,0,-1,0,0,1,-1,0,0,0,1,1,49,50,50,50,49,49,50,51,49,
50,50,50,50,51,50,50,50,50,50,50, 50,51,50,50,50,52,50,52,
50,49,51,51,50,50,50,49,50,50,51,52,50,52,49,50,50,51,50,
50,51,50,0, 0,1,1,-1,0,0,0,-1,0,-1,-1,-1,0,0,-2,0,0,0,0,0,
-1,-1,1,0,0,-1,0,1,0,1,0,1,-1,1,1,-1,0,1,0,0,1,1,0,1, 0,
-1,1,-2,0,50,51,50,50,49,51,52,50,51,51,50,50,51,50,52,49,
51,52,51,50,51,50,52,50,51,52,50, 50,49,51,50,51,50,50,51,
50,52,49,51,51,51,51,49,50,52,51,50,50,51,51,2,0,1,-2,0,-1,
1,-1,0,1,0, 0,0,-1,-1,1,0,-1,1,0,0,-1,0,-2,1,0,0,-2,0,0,-1,
-1,0,1,-1,-1,-1,0,0,-1,-2,-1,-1,1,-1,1,0,1,-2,-1,49,49, 48,
51,50,50,50,50,49,52,51,51,52,50,50,51,49,49,51,50,51,50,
50,49,50,51,51,51,51,51,51,50,50, 50,52,51,51,50,50,50,51,
50,51,52,52,51,51,52,50,50,1,-1,0,0,1,0,-1,-1,1,-1,1,-1,-1,
0,0,1,0,0,-1, -1,-2,-1,1,0,0,-1,1,-1,1,-2,-1,-1,-1,-1,0,1,
1,1,-1,1,-1,1,-1,-2,0,-1,-1,-2,1,0,50,50,50,50,50,50,50, 
51,51,50,51,50,50,50,49,51,51,50,50,51,51,50,51,50,52,52,
50,51,50,50,51,50,50,51,50,50,51,50, 51,51,51,50,50,51,49,
49,51,49,49,50,0,0,0,-1,0,-1,1,0,0,-1,0,0,-1,0,0,1,-1,0,-2,
0,0,0,-1,-1,-1,-1, 0,0,0,-1,-1,0,-2,0,-1,0,-1,0,0,-1,0,0,
-1,0,-1,0,0,-2,-1,0,49,50,49,50,49,51,50,50,51,50,51,50,
50,49, 50,52,52,49,48,51,51,50,50,51,51,51,50,50,51,52,50,
50,51,51,50,51,51,50,49,51,50,50,52,50,52, 50,49,50,50,50,
0,0,0,0,0,0,-1,0,0,0,0,0,0,-1,-1,-1,0,1,1,-1,-1,0,0,0,-1,
-2,0,0,1,0,-1,1,1,0,0,1,-1,-1, 0,-1,0,-1,2,0,0,0,-3,0,1,
0,50,50,51,49,51,52,50,49,50,51,50,50,50,51,51,50,51,51,
51,52,49,50,49, 50,50,49,51,53,49,51,50,48,51,51,51,50,49,
51,51,49,51,50,52,51,50,50,50,51,52,50,-1,-1,0,0,-2,-1,
-1,-1,0,-2,-1,-1,-1,0,0,-2,0,-1,0,-1,-1,-1,0,-1,-1,0,0,
-2,0,0,-1,1,0,-3,0,0,-2,0,-1,-1,0,0,-1,0,0,0,-2,-1,
0,-1,50,50,51,49,51,49,50,51,50,50,50,50,49,51,51,50,51,
51,50,51,50,49,50,51,50,50,49,50,52,50,51, 50,49,50,51,51,
50,51,50,52,50,51,52,50,50,50,50,51,51,51,-1,0,0,-1,0,0,0,
0,-1,0,0,1,-2,0,0,-1,0,0,0,0, -1,0,-1,-1,-1,0,0,0,1,-1,0,0,
-1,0,-1,0,-1,-1,1,1,0,0,-1,0,-1,-1,-1,-1,1,0,50,51,50,51,
51,50,50,51,50,51,49, 52,50,50,50,51,50,51,49,51,52,50,51,
50,51,50,51,52,48,49,49,50,48,49,50,51,50,50,50,50,50,50,
51,49, 50,51,48,50,51,52,0,0,0,0,0,0,-2,0,0,0,0,-1,-1,0,
-1,0,0,-1,0,1,0,-1,-1,-2,0,0,0,0,0,-1,1,0,0,-1,0,-1,-1,1,
0,0,0,-1,0,-1,0,0,0,-1,-1,-2,49,50,50,52,50,49,50,50,49,
50,52,50,49,50,51,52,49,49,49,51,51,50,50,50,50,49,51,
50,50,50,51,52,50,50,50,50,50,50,49,52,51,50,50,50,51,52,
50,50,52,49,0,-1,0,-1,0,1,2,-1,-1,-1,0,1, 0,-1,0,0,1,0,0,
-1,0,1,0,0,-2,0,-1,-1,0,-1,-1,-1,0,-1,0,-1,0,-1,0,0,1,-1,
-2,-1,-2,1,-1,-1,1,1,50,48,49,49,49, 50,50,51,50,51,50,
50,52,50,51,52,51,51,52,50,52,49,50,49,51,49,52,50,50,50,
48,51,50,50,49,51,51,50,51, 50,50,48,50,52,51,49,50,50,
49,51,0,-2,-1,-1,1,0,0,0,0,1,-1,-2,0,1,0,-2,-1,0,0,0,
-1,-1,0,0,0,0,-2,1,0,-1,-1,-1, -1,0,-2,-1,-1,0,-1,-1,
-1,-1,0,-1,0,-2,0,0,1,-2,49,50,51,51,49,50,52,51,50,51,
52,51

This means that it is possible to read data from a GPIB device using an AVR and a PC. In the scenario, no commands can be send from PC to GPIB device. This would require the extension of the existing code by controller functionality and to use more lines from the GPIB interface. Besides this, the topic „Open Collector“ nust be solved as described above. If not, the output driver of either the AVR or the oscilloscope may be destroyed. The controller approach is described further below.

My scope is able to produce wave forms in HPGL format. Below is an example output in HPGL (not complete):

in;sc-690,689,-512,511;
sp1;
pa-500,-400;pd;pr1000,0,0,800,-1000,0,0,-800;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl1;xt;pr100,0;
tl0,1;yt;pr0,100;
tl0,1;yt;pr0,100;
tl0,1;yt;pr0,100;
tl0,1;yt;pr0,100;
tl0,1;yt;pr0,100;
tl0,1;yt;pr0,100;

I converted this output with the open source tool „hp2xx“ in a jpeg and a PDF file. The output can be captured e.g. with kermit log session feature („log session <file>“). The following command sequence creates a well looking PDF output:

rm -f tmp.eps out.pdf
./hp2xx -p 22222222 -C -o 20 -O 150 -h 100 -a 2  -m eps -d 600 $1 -f tmp.eps     ps2pdf tmp.eps out.pdf
acroread out.pdf

Curve and scales are ok, but legend strings are too small. Using option „-d <n>“, the DPI-resolution of the result can be increased. Starting with about 300 DPI, the legend strings can be read.

Tests with another tool called hpgl2ps were even worse.

Elaborated Approach: Talker/Listener Scenario

To move forward, I connected also the GPIB lines EOI, ATN, SRQ, IFC and REN to AVR ports. The port connection table is listed below:

GPIB Pin Name  GPIB-Interface Pin Nummer  ATMega32 Pin Name  AVR Board Interface Pin Nummer
DIO1..8 (Datenpins) 1..4 und 13..16 PortA0..PortA7  1..8
EOI 5 PD4 30
DAV 6 PD2 28
NRFD 7 PD3 29
NDAC 8 PD5 31
SRQ 10 PD6 32
ATN 11 PD7 33
IFC 9 PB0 9
REN 17 PB1 10
GND 23 GND 39

Usage of EOI signal

Checking the the EOI-signal with a second scope showed, that the signal is without transmission LOW, during the transmission HIGH and after the last byte LOW again. This seems to be different from all descriptions I found in the internet. There it is stated that the signal is LOW only during transmission of the last byte.

Using AVR pins with „open collector“  behaviour

The AVR microcontroller is extremly flexible, but the port pins can’t be switched directly to a open collector mode. It is possible to put open collector bus transceivers like  74245 in front of the AVR, but I am lazy. Internet search shows that the open collector behaviour can be emulated with standard AVR pins. The port is set to LOW and the output (LOW or HIGH) is controlled by the DDRx data direction register. For example PORTD and Pin PD4:

Function „common“ use Open collector emulation use
Initialisation of Pin DDRD |= _BV(PD4) // as output
DDRD &= ~BV(PD4) // as input
PORTD &= ~_BV(PD4)
DDRD &= ~_BV(PD4)
set Pin to HIGH PORTD |= _BV(PD4) DDRD &= ~_BV(PD4)
set Pin to LOW PORTD &= ~_BV(PD4) PORTD &= ~_BV(PD4)
DDRD |= _BV(PD4)
read Pin value unsigned char p = PIND & _BV(PD4) DDRD &= ~_BV(PD4)
unsigned char p = PIND & _BV(PD4)

After changing the code according to the table, my code still works. So the controller implementation is the next step to do.

The experimental board from the german company Pollin. Of course, every other board can be used, as long as enough port pins are available. Connectivity to this board is done via a 40-pin IDE connector, I attached an old IDE cable and soldered the GPIB connector to it.

GPIB Controller Implementation

This part is a little bit tricky, because the information on internet is not really clear.

The controller initially takes over the bus and controls all devices connected to the bus. this means he defines them als listener, talker or not participating in the actual transfer. For this, the controller uses management lines  (like IFC and REN) and also bus commands (like DCL, UNL, UNT). The controllerdistinguishes command bytes from data bytes by assigning the ATN line during transmission of command bytes. When ATN is assigned, the talker device stops talking and all devices listen to the commands transfered on the bus.

Devices become listeners if they are addressed from the controller by their listener address (=device adress + 0x20) and become talkers if they are addressed from the controller by their talker address (=device adress + 0x40). There is always zero or one talker, but there may be many listeners at the same time. Controller address is usually (but need not to be) 0x00.

My simple controller implementation works as follows:

  1. Controller takes over bus by assigning REN and IFC (in functions gpib_init() and gpib_controller_assign()). Then he sends DCL command (using gpib_cmd()). All devices go to an initial state.
  2. Excution of a endless loop:
    1. Read in commands from STDIN. A CR is interpreted as end of command line. Then the controller switches itself to talker und a (selectable) device to listener (for example my scope with device adress 0x01 becomes listener by receiving its listener address 0x21). The command line read from STDIN is written to the bus (gpib_write()). The controller uses EOI line to communicate end of transmission.
    2. Listener reads in command and executes it. There are two types of commands: one way commands where no answer is expected and commands with an answer. The controller distinguishes these command types by checking if a ‚?‘ is part of command. If yes, it is assumed that it is a command with answer. If not, it is assumed that it is a one way command. This interpretation is at least true for device from Tektronix. I have only Tektronix and can not test for other devices.
      For commands with no answer, the loop starts again
      For commands with answer, the controller becomes a listener and sets the device to be a talker (gpib_cmd()). Then the controller reads in the answer and prints out every byte to STDOUT. The device communicated end of transmission by assigning EOI line. Then the loop starts again.
  3. Besides the loop, the controller also has a asynchronous behaviour: A device can initiate a service request SRQ. This is done by the device by assigning the SRQ line. The device may signalize by SRQ that it wants e.g. transfer data to the bus. The controller then initiates a „serial poll“ by sending the SPE command (serial poll enable). It addresses each known device as talker and reads data from device. The device goes in a special mode when receiving SPE, meaning it send a status byte if addressed as talker. By examining bit 6 of the status byte, the controller can find out the SRQ emitting device because this device sets the bit to 1 and all other devices have it set to 0. Having found the service requesting device, the controller ends serial poll with command SPD (serial poll disable).
    Handling the SRQ is device dependent. For example, my scope communicates with an errorcode like „157“ errors in commands received (misspelling etc.).
  4. In theory, the controller releases bus at the end (gpib_controller_release()).

During GPIB handshake, the controller may wait forever if there is an error in communication or the device was switched off. I added timeout code, so the controller recognizes a timeout and returns to the global loop.

Below, a session is shown. If the scope is switched on, it generates a SRQ. The controller asks the device for SRQ reason using „EVENT?“ command. The answer by the scope is „401“. 401 means according to scope manual „2432A was just powered on.“ which is correct. Then the command „id?“ is entered, the scope returns his id. After that, some commands are sent to the scope.

INIT
MEASUREMENT WINDOW:ON
CURSOR FUNCTION: TIME,TARGET:CH1,UNITS:BASE
CURSOR TPOS:ONE:200,TPOS:TWO:500

Finally settings for channel 1 are queried.

dennis@soc:~> kermit -c 
Connecting to /dev/ttyS2, speed 19200  Escape character: Ctrl- (ASCII 28, FS): enabled 
Type the escape character followed by C to get back, or followed by ? to see other options. 
---------------------------------------------------- 
SRQ detected. status byte from device 0x41 = 0x41 (char=A) 
SRQ emitter is device = 0x41 
command: EVENT? 
Query. Will check for answer. 
EVENT 401 
> id? command: id? 
Query. Will check for answer. 
ID TEK/2432A,V81.1,"24-DEC-89  V2.30 /2.5" 
> INIT 
command: INIT 
Command only. 
> MEASUREMENT WINDOW:ON 
command: MEASUREMENT WINDOW:ON 
Command only. 
> CURSOR FUNCTION: TIME,TARGET:CH1,UNITS:BASE 
command: CURSOR FUNCTION: TIME,TARGET:CH1,UNITS:BASE 
Command only. 
> CURSOR TPOS:ONE:200,TPOS:TWO:500 command: CURSOR TPOS:ONE:200,TPOS:TWO:500 Command only. 
> CH1? 
command: CH1? 
Query. Will check for answer. 
CH1 VOLTS:1E-1,VARIABLE:0,POSITION:0,COUPLING:DC,FIFTY:OFF,INVERT:OFF 
>

Result: the controller implementation based on AVR works quite good. For a real production version the software should be extended and tested with many more devices.

Below I provide the software under General Public Licence as ZIP file. The ZIP file includes the files uart.h/uart.c from peter Fleury for RS232 handling. This library is also under GNU GPL.

ZIP file with all files including docs:  gpib-rev689.zip

Version history:

  • 0.689 Improved the still spartanic command line user interface, improved stability
  • 0.66: Added doxygen documentation, see directory doc
  • 0.63: Communication with two (and maybe more) devices works. A error in the read function was remoed.
  • 0.1: Communication with a single device (Tek 2432A tested) works ok.

A complete session log with version 0.66 can be found here: Session with 2 devices The used devices were scope Tektronix 2432A and Logic Analyzer Tektronix 1241.

For some (homebrew, experimental) additional software that is able to display captured data from the Tektronix 1241, see here.

Prototype

Some photos of the prototype mounted into a case.

 

 

Related Links