PicUsbOszilloskop - ProjektEin Mini-Oszilloskop mit einem PIC18F2550, der analoge Spannungen digitalisiert und die Daten via Usb-Schnittstelle an den PC leitet.
Das Projekt ist eventuell für Leute interessant, die sich gerade mit der PIC-Usb-Schnittstelle herumschlagen.
Die Grundlage des Projekts ist die PIC-Firmware von Microchip (Interrupt-Transfer) (Version von 2004).
Die Samplerate des PIC ist 114,5 KByte/s und die Usb-Übertragungsrate ist 128 KByte/s.
Die Entwicklungsumgebung ist auf der PIC-Seite: MPLab mit MCC18-C-Compiler und auf der PC-Seite: VisualC++.
Der PIC ist ein 18F2550 mit 20MHz Quarz, braucht eigene Stromversorgung (nicht USB-powered), VIDPID = 04D8_000C
Ich beschreibe hier mein Projekt - aber eigentlich sind es lauter Fragen. Ich bin mir bei einigen Dingen nicht sicher, ob sie so stimmen, wie ich sie beschreibe und hoffe auf Kommentare, die mögliche Fehler korrigieren und Verbesserungsvorschläge machen. Themen:PIC:ADC, maximale Samplerate(

), Usb-Interrupt-Transfer, UsbFirmware anpassen, Synchronisation des PicProgramms mit dem UsbBusTakt, simples Ping-Pong-buffering
PC:Synchronisation des PC-Programms mit dem UsbBusTakt, MultimediaTimer, Untersuchung der Funktion "MPUSBReadInt"
Ziel: Ziel des Projekts war, eine Art Spielzeugoszilloskop zu bauen, das aus einem PIC und aus einer UsbLeitung zum PC besteht und sonst aus nichts. Die ADC-Möglichkeiten des PIC sollten möglichst gut ausgenutzt und die Daten via USB möglichst in Echtzeit an den PC gesendet werden, wo sie dann in beliebiger Form ausgewertet und angezeigt werden können.
Schwierigkeiten:Der PIC muss einerseits den ADC in konstanten Zeitabständen (und möglichst oft) auslesen und andererseits die gesammelten Daten über die UsbSchnittstelle paketweise aussenden. Das Absenden der Datenpakete darf nicht so lange dauern, dass der PIC mit dem lesen des ADC in Verzug kommt.
Lösungsansatz:Sampling (ADC, Samplerate):Das Wichtigste für ein Oszilloskop scheint mir eine konstante und einigermaßen hohe Samplerate zu sein. Daher wird der ADC über einen Interrupt getriggert, der vom TIMER1 ausgelöst wird (CCP-Modul als „Special-Event-Trigger“). Die gesamte ADC-Prozedur wird von der PicHardware übernommen - das Programm muss nur noch das ADC-Resultat auslesen. Das Auslesen erfolgt in einer InterruptRoutine, die die ADC-Daten in einen Puffer kopiert. Die damit erreichbare Samplerate (ADC leicht übertaktet, kaum AcquisitionTime) ist bei mir 8,666ys pro Sample -> ca. 115 kHz. Für ein Oszilloskop immer noch recht mager. Die Auflösung betragt nur 8 Bit (ca. 20mV). (Auf die beiden LSB des 10-Bit-ADC habe ich verzichtet).
USB:InterruptTransfer Die UsbVerbindung muss pro ms (pro USB-Frame) 115 Bytes ADC-Daten und ein paar Informationsdaten übertragen.
Mein UsbProgrammteil baut auf der Firmware von Microchip auf (MCHPFSUSB/fw/Demo).
Ich verwende Interrupt-Datentransfer (siehe:usb in a nutshell/ usb4/ InterruptTransfer). Wahrscheinlich wäre Isochronous Transfer besser geeignet, aber das InterruptDing war schon kompliziert genug. InterruptDatenPekete sind auf 64 Bytes beschränkt. Das heißt ich brauche 2 Pakete pro UsbFrame (pro 1 ms). Es ist mir aber nicht gelungen, mehr als ein solches Paket innerhalb eines Frames über eine einzelne Usb-Pipe zu versenden . Daher habe ich zunächst 2 Pipes für 2 separate Endpoints installiert, die mein PC-Programm jede Millisekunde gleichzeitig abruft (um daraufhin 2 * 64 Bytes zu empfangen). Falls jemand weiß, wie man das besser lösen kann, wäre ich für eine Mitteilung sehr dankbar.
PingPongPufferDie nächste Schwierigkeit war, die ADC-Daten, die sich in einer Millisekunde ansammeln (114 Bytes) schnell genug aus einem temporären Puffer in den USB-Puffer zu schreiben, ohne dass der ADC-Interrupt diesen Vorgang unterbricht und Bytes in den Puffer schreibt, die eigentlich erst ins nächste Datenpaket gehören. Es ist normaler Weise nicht möglich, die Bytes direkt in den USB-Puffer zu schreiben, weil es da zu Konflikten mit der USB-Hardware kommt, sobald diese den Puffer ausliest um den Inhalt an den PC zu senden (shared buffer) - außer man hat 2 solche Puffer, die abwechselnd zum Einsatz kommen (Ping-Pong-Puffer). ((Beim PIC gibt es die Möglichkeit, jeden einzelnen Endpoint mit 2 UsbPuffern auszustatten, also mit PingPongPuffern. Das war mir aber zu mühsam.)) Daher habe ich einen Endpoint für Ping (PufferA) und einen anderen Endpoint für Pong (PufferB) eingesetzt. In der ersten Millisekunde wird Puffer-A kontinuierlich von der ADC-Interrupt-Routine beschrieben (alle 8,66ys ein Byte) während Puffer-B zunächst auf den UsbTransfer wartet und dann von der Hardware gesendet wird (wie lange er wartet, hängt vom Datenverkehr im UsbBus ab, aber es muss innerhalb dieser Millisekunde [dieses Frames] passieren). In der nächsten Millisekunde ist es umgekehrt: Puffer-B wird vom ADC-Interrupt beschrieben und Puffer-A wird gesendet.
Pointer zum UsbPufferWoher weiß die ADC-Interrupt-Routine, in welchen Puffer sie schreiben darf? Die Routine benutzt einen Pointer (FSR0, mit automatischem increment [POSTINC0]), der von der AdcSendRoutine jedes mal, wenn Puffer-A gerade gesendet wurde auf die Startadresse des Puffers-A gesetzt wird. Wenn Puffer-B gesendet wurde, wird der Pointer auf Puffer-B gesetzt.
Bevor die AdcSendRoutine den Pointer umschreibt, kopiert sie ihn noch in den Puffer hinein. Diese Zahl gibt an, wie viele Bytes, die die Interruptroutine in den Puffer geschrieben hat (im Regelfall 114 bis 115) – die Zahl wird vom PC-Programm benötigt.
Vier EndpointsGenau genommen sind es nicht nur 2, sondern insgesamt 4 Puffer: Puffer-A und Puffer-B bestehen jeweils aus zwei 64-Byte-Puffern. Jeder ist einem eigenen Endpoint zugeordnet. Der Grund dafür ist, dass Interrupt-Endpoints nur 64Bytes/ Paket übertragen können.
In der ersten Millisekunde werden Puffer1&2(Puffer-A) beschrieben (2 * 64 Bytes) und Puffer3&4(Puffer-B) gesendet, in der nächsten Millisekunde ist es umgekehrt. Ich habe also 4 Endpoints, die über 4 Pipes mit dem PC kommunizieren.
AdcSendRoutineDie AdcSendRoutine ist ein Loop, der folgende Stationen durchläuft:
1.) Warte auf ein „Start-Of-Frame-Signal“ (SOFIF). Hier wird die
AdcSendRoutine mit dem UsbBusTakt synchronisiert.
Wenn SOFIF == 1, gehe zur nächsten Station:
2.) Warte bis Puffer1&2 gesendet wurde (wait until(USIE[1,2] == 0)).
(USIE ist ein Semaphor: eine Flag, die anzeigt, ob der Puffer von der
UsbHardware genutzt wird (USIE==1)oder ob er für das UserProgramm zur
Verfügung steht(USIE==0)).
3.) Setze den ADC-Interrupt-Schreibe-Pointer auf die Adresse von Puffer1.
Ab jetzt darf hier geschrieben werden, weil der Puffer gerade gesendet
wurde und erst in ca.1,5 bis 2 ms wieder gesendet wird. Die Interruptroutine
wird in der kommenden Millisekunde ca. 114 Bytes in Puffer1 und Puffer2 schreiben.
Da der Pointer jetzt auf die Puffer1&2 gerichtet ist, sind die Puffer3&4 frei
um gesendet zu werden: USIE[3,4] = 1 (set Buffer ready). Die Puffer sollten
noch in dieser Millisekunde (innerhalb dieses Frames) abgeschickt werden.
Wann genau, ist ungewiss und hängt davon ab, wann der USB-Bus das
„IN-Token“(= Signal: “schick etwas an den PC“) an die Endpoints3&4 ausgibt.
((Bevor die Puffer3&4 zum Senden frei gegeben werden, werden noch Steuerdaten
für das PC-Programm an die letzten zwei Pufferstelle von Puffer4 geschrieben)).
4.) Warte auf das nächste „Start-Of-Frame-Signal“ (SOFIF). (jetzt kommt die
selbe Prozedur für die anderen 2 Puffer ):
5.) Warte bis die Puffer3&4 gesendet wurden (wait until(USIE[3,4] == 0)).
6.) Setze den ADC-Interrupt-Schreibe-Pointer auf die Adresse von Puffer3.
Jetzt sind die Puffer1&2 frei um gesendet zu werden-> USIE[1,2] = 1 (set Buffer
ready). Die Interruptroutine wird in der kommenden Millisekunde ca. 114 Bytes
in Puffer3 und Puffer4 schreiben.
7.) Zurück zu Station 1.).
Die AdcSendRoutine ist ein Loop, der nur von einem PC-Steuerbefehl abgebrochen werden kann. Das heißt: innerhalb der Routine müssten auch alle StandardRequests des Usb-Protokolls abgearbeitet werden (RESET, SUSPEND, STALL... (siehe: USB in a NutShell)). Darauf verzichtet mein Programm innerhalb der AdcSendRoutine (ohne dass es zu Problemen kommt). Nur die "UsbTransactionComplete-Flag" (TRNIF)muss regelmäßig gelöscht werden (siehe: MCHPFSUSB_FW_UG_51679a.pdf / Kapitel:1.3.2.2.2). Das passiert in den 4 Warteschleifen der Routine.
Die Wartezeit zwischen SOFIF ==1 und USIE ==1 ist nicht immer gleich lang. Daher werden in den Puffern 1&2 und 3&4 mal mehr, mal weniger Bytes sein. War die Wartezeit diesmal länger, wird sie beim nächsten mal entsprechend kürzer sein, so dass im Mittel immer 114,5 AdcDatenBytes pro ms an den PC gesendet werden. Aus diesem Grund ist es wichtig dem PC-Programm die genaue Anzahl der gültigen AdcDatenBytes für jedes DatenPaket mitzuteilen(Steuerbyte am Ende des Pakets).
Außerdem wird an jedes Paket eine fortlaufende Paketnummer angehängt. Das ist wichtig, weil das System in der Anfangsphase (in den ersten 10 ms) oft "stolpert", was zur Folge hat, dass die Pakete in falscher Reihenfolge ankommen (Genaueres später).
PC-Programm:Steuerbefehl an den PICNachdem der PIC über USB an den PC angeschlossen und vom Betriebssystem erkannt worden ist (SetupPhase), tut er zunächst einmal nichts, als auf einen "SteuerBefehl" des PC-UserProgramms zu warten. Die Steuerbefehle werden nicht über die Endpointpipes-1bis4 abgewickelt, sondern über einen eigenen Endpoint, der auch Daten vom PC empfangen kann (Endpoint_IN_OUT). Beim Befehl: "SendeAdcDaten_Start" springt das PIC-Programm in den oben beschriebenen AdcSendRoutine-Loop, in dem es solange bleibt, bis der Befehl: "Send_Stop" vom PC kommt.
IN-TransferGleich nachdem das PC-Programm den Startbefehl gegeben hat, öffnet es die 4 Kommunikationspipes zum PIC. Ab diesem Moment schickt der Usb-Controller des PC jede Millisekunde 4 IN-Tokens (“schick etwas an den PC“) über die 4 Pipes an die 4 Endpoints des PIC.
a.)IN-Token für Endpoint1: "sende 64 Bytes über die Pipe1 an den PC"
b.)IN-Token für Endpoint2: "sende 64 Bytes über die Pipe2 an den PC"
c.)IN-Token für Endpoint3: "sende 64 Bytes über die Pipe3 an den PC"
d.)IN-Token für Endpoint4: "sende 64 Bytes über die Pipe4 an den PC"
Diese 4 Befehle kommen kurz nacheinander beim PIC an. Von der AdcSendRoutine des PIC können aber nur zwei dieser Befehle innerhalb einer Millisekunde ausgeführt werden (es sind immer nur zwei der vier Endpointbuffers sendebereit [USIE = 1]). Die anderen 2 Befehle werden vom PIC „geNAKt“(= der PIC sendet das NAK-Signal an den PC, das bedeutet: EndpointBuffer-n nicht bereit). Das hat zur Folge, dass die Befehle vom UsbController beim nächsten Frame (1 ms später) erneut an den PIC geschickt werden - genau zu dem Zeitpunkt, zu dem sie vom PIC erwartet werden (zu diesem Zeitpunkt sind nämlich die zuvor geNAKten Endpoints bereits sendebereit, dafür werden jetzt die 2 anderen Endpoints geNAKt).
Die Reihenfolge der erfolgreich beantworteten in IN-Tokens ist: 1.Millisekunde: a&b, 2.Millisekunde: c&d, 3.Millisekunde: a&b, 4.Millisekunde: c&d, u.s.w...
Die gesendeten Datenpakete werden in einem Puffer des Usbtreibers geparkt und müssen vom Userprogramm von dort ausgelesen werden: für jede Pipe ein eigener Lesebefehl (Genaueres später).
EinpendelphaseIn den ersten paar Millisekunden nach dem Startsignal passiert es manchmal, dass sich die Reihenfolge der beantworteten und der geNAKten IN-Tokens vertauscht. Wie das genau vor sich geht ist mir unklar. Jedenfalls kommen dann im UserProgramm Datenpakete mit vertauschten Paketnummern an. Da hilft es nur, die Lesebefehle für Pipe1&2 oder für Pipe3&4 für zwei Millisekunden auszusetzen. Nach 5 bis 30 ms ist der Fehler ausgeglichen und das System ist eingependelt. Ab dann lauft es stabil. Das heißt: sowohl PIC- als auch PC-Programm haben sich mit dem UsbBusTakt synchronisiert.
MultimediaTimerDer Teil des PC-Programms, der die Kommunikation mit dem PIC abwickelt, ist eine sogenannte TimerCallbackRoutine: das ist im Prinzip vergleichbar mit einer TimerInterrupt-Routine beim PIC. Sie wird vom "MultimediaTimer" des PC, alle 2 ms getriggert.
Mit "timeSetEvent" wird der MultimediaTimer (high-resolution-timer) etabliert. Er ruft in einstellbaren Intervallen eine TimerCallbackRoutine des Anwenderprogramms auf (bei mir alle 2ms).
TimerCallbackRoutineDiese Routine durchlauft im Wesentlichen folgende Stationen:
1.)
if(SyncFlag != 1) //wenn mit den DatenPaketnummern alles in Ordnung ist...
Read(Pipe1 ) //(Befehl: a) 64Bytes in den UserProgPuffer+Offset+0
Read(Pipe2 ) //(Befehl: b) 64Bytes in den UserProgPuffer+Offset+64
if(SyncFlag != 2) //wenn mit den DatenPaketnummern alles in Ordnung ist...
Read(Pipe3 ) //(Befehl: c) 64Bytes in den UserProgPuffer+Offset+128
Read(Pipe4 ) //(Befehl: d) 64Bytes in den UserProgPuffer+Offset+192
//als Read-Befehl verwendet das Programm "MPUSBReadInt",
//ein spezielles Api von Microchip, das mit dem Microchip-USB-
//Driver "mchpusb.sys" kommuniziert. (siehe:
//[MCHPFSUSB/Pc/mpusbapi/Dll/Borland_C /Source/_mpusbapi.cpp])
SyncFlag = 0
2.)
if(PaketNummer_Pipe3&4 - PaketNummer_Pipe1&2 > 2) //wenn Datenpakete der
//Pipe1&2 verschluckt wurden....
SyncFlag = 2 //überspringe beim nächsten Durchlauf die Befehle: c und d
if(PaketNummer_Pipe3&4 <= PaketNummer_Pipe1&2 ) //wenn Datenpakete der Pipe3&4
//verschluckt wurden....
SyncFlag = 1 //überspringe beim nächsten Durchlauf die Befehle: a und b
if(AnzahlAdcDaten_Pipe1&2 < 90 ODER AnzahlAdcDaten_Pipe3&4<90)
//wenn die Anzahl der gültigen AdcDatenBytes zu
//gering ist...
Sende_Stop; SendeAdcDaten_Start; //Neustart der
//PIC-Adc-SendRoutine (das hilft!!!)
3.)
UserProgPuffer_Offset += 256
if(UserProgPuffer_Offset >= UserProgPuffer_Size)
UserProgPuffer_Offset = 0
ZeichneOszilloskopGraph() //der UserProgPuffer wird gezeichnet
return
MPUSBReadIntWenn ich das richtig verstanden habe, lauft das Lesen der Daten mit dem Befehl MPUSBReadInt folgender Maßen ab:
Sobald die Pipe (der Datentransfer) zu einem bestimmten PIC-Endpoint geöffnet ist (in meinem Fall mit der Funktion: MPUSBOpen (EndpointX_ASYNC)), sendet der UsbTreiber in regelmäßigen Intervallen ein IN-Token an den Endpoint (via UsbController ). Die Dauer des Intervalls ist durch den EndpointDescriptor festgesetzt (bei mir 1ms). Die hereinkommenden PIC-Daten werden in einem Treiberpuffer gespeichert (da passen 100 Datenpakete a 64 Bytes hinein). Wenn der Puffer voll ist, werden alle neu hereinkommenden Daten ignoriert.
Wenn jetzt das Userprogramm MPUSBReadInt aufruft, wird das älteste Datenpaket aus dem Treiberpuffer gelesen (= in den UserProgPuffer verschoben) und die Funktion kehrt sofort zum Programm zurück. Das nächste MPUSBReadInt liest dann das zweitältesten Paket u.s.w..
Wenn MPUSBReadInt zu selten aufgerufen wird, wenn also weniger Pakete aus dem Treiberpuffer ausgelesen werden, als hereinkommen, wird der Puffer immer voller, bis er die 100er-Grenze errreicht. Ab da ignoriert der Treiber jedes Datenpaket, wenn nicht zuvor ein MPUSBReadInt durch das Lesen eines Pakets, Platz im Puffer freigeschaufelt hat. (Wenn die Lesefrequenz halb so groß ist, wie die Frequenz der hereinkommenden Pakete, geht jedes zweite Paket verloren).
Wenn die MPUSBReadInt-Befehle häufiger kommen, als die Datenpakete, wird der Puffer immer leerer, bis kein Datenpaket mehr auf Vorrat liegt. Ab da tritt die Wartefunktion von MPUSBReadInt in Kraft: Die Wartezeit von MPUSBReadInt ("timeout-interval") ist frei wählbar (z.B. 2ms) und wird mit dem Befehl mitgeschickt. MPUSBReadInt wartet also eine Weile auf hereinkommende Daten und blockiert dadurch den Programmfluss. Wenn innerhalb des Timeout Daten hereinkommen, werden sie gelesen, MPUSBReadInt kehrt zurück und das Programm geht weiter.
(Wenn keine Daten innerhalb des Timeout hereinkommen, kehrt MPUSBReadInt mit einer Fehlermeldung (im Returnwert) zurück).
SynchronisationDiese Wartefähigkeit des Read-Befehls ist sehr wichtig. Denn nur dadurch synchronisiert sich das Programm mit dem UsbBusTakt und es gehen keine Daten verloren. Voraussetzung dafür, dass die Read-Befehle warten ist, dass sie oft genug aufgerufen werden, damit sich der Treiberpuffer leert. (Für dieses Oszilloskopprojekt ist es in Wirklichkeit nicht unbedingt nötig, dass das Userprogramm sofort auf die Daten zugreift, wenn sie hereingekommen.)
Vermischtes:Pic-UsbFirmware-FileUm einen besseren Überblick über die Microchip-UsbFirmware zu bekommen, habe ich alle benötigten Firmware-Funktionen zusammen mit meinen Userfunktionen in ein einzelnes File gepackt (UsbOszi.c). Daneben gibt es in meinem PIC-Projekt nur noch zwei Header-Dateien und "18F2550.lkr". Die AdcSendRoutine und die ADC-Interrupt-Routine sind in Assembler Code geschrieben, um eine bessere Kontrolle über das timing und den Pointer (FSR0) zu gewährleisten.
PC-Projekt-FilesDas PC-Programm verwendet den UsbTreiber von Microchip "mchpusb.sys". In das C++Projekt sind neben den Standard-Headerdateien auch usbDsc.lib & .h (für die Microchip-Usb-APIs) , und WINMM.lib (für den Multimediatimer) eingebunden.
Schwächen des ProjektsDas PC-Programm ist nicht gerade ressourcenschonend. Durch die Wartezeiten in der TimerRoutine beansprucht das Programm viel Rechnerzeit.
Das Oszilloskop wird auf langsameren Rechnern durch andere Usb-Geräte (z.B. eine Usb-Maus) leicht irritiert. Das Äußert sich darin, dass die Datenpakete in unregelmäßigen Intervallen übertragen werden (1ms +/- 0,12ms). Daher kann es vorkommen, dass in einem Paketen z.B. nur 96 ADC-DatenBytes (statt 114) sind. Im darauf Folgenden müssten es dann 132 ADC-DatenBytes sein. Die Paketgröße ist aber nur 128 -> 4 Bytes gehen verloren. ((genau genommen gehen 6 Bytes verloren, weil die letzten 2 Bytes im Datenpaket für Paketnummer und Anzahl_der_gültigen_Bytes reserviert sind))
Der PIC-Quellcode ist ziemlich chaotisch. Im PC-Programm fehlen noch ein paar Features.
Tests und HardwareIch habe das Programm auf unterschiedlich schnellen Rechnern, aber nur unter WindowsXP getestet.
Die Hardware des Oszilloskops besteht nur aus einem PIC, einer LED und einem Quarz(+Kondensatoren). Um Eingangsschutzdioden und Spannungsteiler am Analogeingang habe ich mich nicht gekümmert. Ich habe die Schaltung ausschließlich mit Wechselspannungen im Bereich: 0 bis 5V getestet.
DateianhangusbOszilloskop_PIC.zipHexFile, Anschluss-Schema, UsbTreiber (mchpusb.sys + inf) und 3 Quellcodedateien (UsbOszi.c, UsbOszi.h, typedefs.h)
usbOszilloskop_Pc.zipUsbOszilloskop.exe und Quellcodedateien (MainFrm.cpp, Oszilloskop.cpp, MainFrm.h)
BilderUsbOszi1: Ein Screenshot des PC-Programms. Angezeigt wird der Output eines 6kHz-Multivibrators
UsbOszi2: Ein Screenshot des PC-Programms. Angezeigt wird der Output einer s/w-Cmos-Kamera (eindeutig zu schnell für das Oszilloskop)