Frühere Artikel in der Reihe
Vielleicht werde ich heute sogar die Tradition brechen und das Projekt nicht im Redd-Komplex, sondern in einem regulären Layout debuggen. Erstens ist mir bewusst, dass die überwiegende Mehrheit der Leser keinen Zugang zu einem solchen Komplex hat, aber sie haben Zugang zu Ali Express. Nun, und zweitens bin ich einfach zu faul, um einen Garten mit einem Paar angeschlossener USB-Geräte und Hosts zu umzäunen und auch mit den aufkommenden Störungen umzugehen.
Im Jahr 2017 suchte ich nach vorgefertigten Lösungen im Netzwerk und fand so etwas Wunderbares , oder besser gesagt, seinen Vorfahren. Jetzt haben sie alles auf einem speziellen Board, aber überall gab es Fotos eines einfachen Steckbretts von Xilinx, mit dem ein Board von WaveShare verbunden war (Sie können es hier erfahren ). Schauen wir uns das Foto dieses Boards an.
Es verfügt über zwei USB-Anschlüsse gleichzeitig. Darüber hinaus zeigt das Diagramm, dass sie parallelisiert sind. Sie können Ihre USB-Geräte an eine Typ-A-Buchse anschließen und ein Kabel an den Mini-USB-Anschluss anschließen, den wir an den Host anschließen. Und die Beschreibung des OpenVizsla-Projekts besagt, dass dieser Weg funktioniert. Schade nur, dass das Projekt selbst ziemlich schwer zu lesen ist. Sie können es auf Github nehmen, aber ich werde einen Link nicht zu dem Konto geben, das auf der Seite angegeben ist. Jeder wird es trotzdem finden, aber es wurde für MiGen überarbeitet, aber die Version, die ich 2017 gefunden habe: http: // github. com / ultraembedded / coresEs befindet sich in einem sauberen Verilog und es gibt den Zweig usb_sniffer. Dort läuft alles nicht direkt über ULPI, sondern über den ULPI-zu-UTMI-Konverter (diese beiden unanständigen Wörter sind Mikroschaltungen auf physikalischer Ebene, die dem Hochgeschwindigkeits-USB 2.0-Kanal mit Bussen entsprechen, die für Prozessoren und FPGAs verständlich sind) und funktionieren nur dann mit dieser UTMI. Wie dort alles funktioniert, habe ich nicht herausgefunden. Deshalb habe ich es vorgezogen, meine Entwicklung von Grund auf neu zu gestalten, da wir bald sehen werden, dass dort alles eher beängstigend als schwierig ist.
An welcher Hardware können Sie arbeiten?
Die Antwort auf die Frage aus dem Titel ist einfach: bei jedem mit FPGA und externem Speicher. Natürlich werden wir in dieser Serie nur Altera-FPGAs (Intel) betrachten. Beachten Sie jedoch, dass die Daten des ULPI-Mikroschaltkreises (auf diesem Taschentuch) mit 60 MHz ausgeführt werden. Lange Drähte sind hier nicht akzeptabel. Es ist auch wichtig, die CLK-Leitung mit dem FPGA-Eingang der GCK-Gruppe zu verbinden, da sonst alles funktioniert und dann fehlschlägt. Besser nicht riskieren. Ich rate Ihnen nicht, es programmgesteuert weiterzuleiten. Ich habe es versucht. Alles endete mit einem Draht zum Bein von der GCK-Gruppe.
Für die heutigen Experimente hat mir auf meine Bitte ein Bekannter ein solches System gelötet:
Mikromodul mit FPGA und SDRAM (suchen Sie es auf ALI Express mit dem Satz FPGA AC608) und dieselbe ULPI-Karte von WaveShare. So sieht das Modul auf den Fotos eines Verkäufers aus. Ich bin einfach zu faul, um es aus dem Gehäuse herauszuschrauben:
Übrigens sind die Belüftungslöcher, wie auf dem Foto meines Gehäuses, sehr interessant. Zeichnen Sie auf dem Modell eine feste Ebene, und stellen Sie im Slicer die Füllung ein, z. B. 40%, und sagen Sie, dass Sie von unten und oben keine festen Ebenen erstellen müssen. Infolgedessen zeichnet der 3D-Drucker diese Belüftung selbst. Sehr bequem.
Im Allgemeinen ist der Ansatz zum Auffinden von Hardware klar. Jetzt beginnen wir mit der Entwicklung des Analysators. Vielmehr haben wir bereits den Analysator selbst in den letzten beiden Artikeln gemacht ( hier arbeiteten wir mit Hardware , und hier - mit Zugang zu ihnen ), jetzt werden wir einfach entwerfen einen problemorientierte Kopf , dass Fangdaten aus dem ULPI Mikroschaltung kommen.
Was der Kopf kann
Beim Logikanalysator war alles einfach und unkompliziert. Es gibt Daten. Wir haben uns mit ihnen verbunden und angefangen zu packen und sie an den AVALON_ST-Bus zu senden. Hier ist alles komplizierter. Die ULPI-Spezifikation finden Sie hier . Dreiundneunzig langweilige Textblätter. Persönlich treibt mich das in die Verzweiflung. Die Beschreibung für den USB3300-Chip, der auf der WaveShare-Karte installiert ist, sieht etwas einfacher aus. Sie können es hier bekommen . Obwohl ich seit Dezember 2017 immer noch Mut gesammelt habe, habe ich manchmal das Dokument gelesen und es sofort geschlossen, als ich das Gefühl einer Depression spürte.
Aus der Beschreibung geht hervor, dass ULPI über eine Reihe von Registern verfügt, die vor Arbeitsbeginn ausgefüllt werden müssen. Dies ist hauptsächlich auf Pull-up- und Abschlusswiderstände zurückzuführen. Hier ist ein Bild, um den Punkt zu erklären:
Abhängig von der Rolle (Host oder Gerät) sowie der ausgewählten Geschwindigkeit müssen unterschiedliche Widerstände enthalten sein. Aber wir sind weder Host noch Gerät! Wir müssen alle Widerstände trennen, um die Hauptgeräte am Bus nicht zu stören! Dies erfolgt durch Schreiben in Register.
Nun und Geschwindigkeit. Es ist notwendig, eine Arbeitsgeschwindigkeit zu wählen. Dazu müssen Sie auch in die Register schreiben.
Wenn wir alles konfiguriert haben, können Sie mit dem Abrufen von Daten beginnen. Aber im Namen von ULPI bedeuten die Buchstaben "LP" "Low Pins". Und genau diese Reduzierung der Anzahl der Beine führte zu einem so wütenden Protokoll, dass man sich einfach festhält! Schauen wir uns das Protokoll genauer an.
ULPI-Protokoll
Das ULPI-Protokoll ist für den einfachen Mann etwas ungewöhnlich. Wenn Sie jedoch mit einem Dokument sitzen und meditieren, erscheinen einige mehr oder weniger verständliche Merkmale. Es wird deutlich, dass die Entwickler alle Anstrengungen unternommen haben, um die Anzahl der verwendeten Kontakte wirklich zu reduzieren.
Ich werde die vollständige Dokumentation hier nicht erneut eingeben. Beschränken wir uns auf die wichtigsten Dinge. Das wichtigste davon ist die Richtung der Signale. Es ist unmöglich, sich daran zu erinnern, es ist besser, jedes Mal das Bild zu betrachten:
ULPI LINK ist unser FPGA.
Zeitdiagramm des Datenempfangs
Im Ruhezustand müssen wir eine Konstante 0x00 an den Datenbus ausgeben, die dem IDLE-Befehl entspricht. Wenn Daten vom USB-Bus stammen, sieht das Austauschprotokoll folgendermaßen aus:
Der Zyklus beginnt mit der Tatsache, dass das DIR-Signal bis zu eins hochfliegt. Erstens wird es einen Taktzyklus geben, damit das System Zeit hat, die Richtung des Datenbusses zu ändern. Weiter - Wunder der Wirtschaft beginnen. Sehen Sie den Namen des NXT-Signals? Es bedeutet NEXT, wenn es von uns gesendet wird. Und hier ist es ein ganz anderes Signal. Wenn DIR eins ist, würde ich NXT C / D nennen. Niedriges Niveau - wir haben ein Team. High - Daten.
Das heißt, wir müssen 9 Bits (den DATA-Bus und das NXT-Signal) entweder immer auf einem hohen DIR fixieren (dann den ersten Takt per Software filtern) oder ab dem zweiten Takt nach dem Start des DIR. Wenn die DIR-Leitung auf Null fällt, schalten wir den Datenbus auf Schreiben um und beginnen erneut mit der Übertragung des IDLE-Befehls.
Mit Datenempfang ist es klar. Lassen Sie uns nun die Arbeit mit Registern analysieren.
Zeitdiagramm des Schreibens in das ULPI-Register
Um in das Register zu schreiben, wird das folgende temporäre Haus verwendet (ich habe absichtlich auf Jargon umgestellt, weil ich das Gefühl habe, dass ich zu GOST 2.105 tendiere, und das ist langweilig, also werde ich mich davon
entfernen ): Zunächst müssen wir auf den Zustand DIR = 0 warten. Beim Takt T0 müssen wir die TXD-CMD-Konstante auf dem Datenbus einstellen. Was bedeutet das? Sie können es nicht sofort herausfinden, aber wenn Sie ein wenig in den Dokumenten stöbern, stellt sich heraus, dass der gewünschte Wert hier zu finden ist:
Das heißt, der Wert "10" sollte in die oberen Datenbits (für das gesamte Byte lautet die Maske 0x80) und in die unteren - die Registernummer - eingefügt werden.
Als nächstes sollten Sie warten, bis das NXT-Signal startet. Mit diesem Signal bestätigt die Mikroschaltung, dass sie uns gehört hat. Im obigen Bild haben wir um Takt T2 darauf gewartet und die Daten auf den nächsten Takt (T3) eingestellt. Auf der Uhr T4 empfängt ULPI Daten und entfernt NXT. Und wir werden das Ende des Austauschzyklus in STP markieren. Auch bei T5 werden die Daten in das interne Register zwischengespeichert. Der Prozess ist beendet. Hier ist eine Rückzahlung für eine kleine Anzahl von Schlussfolgerungen. Wir müssen die Daten jedoch erst beim Start schreiben, daher müssen wir natürlich unter der Entwicklung leiden, aber all dies wirkt sich nicht besonders auf die Arbeit aus.
Zeitdiagramm des Lesens aus dem ULPI-Register
Ehrlich gesagt ist das Lesen von Registern für praktische Aufgaben nicht so wichtig, aber schauen wir es uns auch an. Das Lesen ist zumindest hilfreich, um sicherzustellen, dass wir den Datensatz korrekt implementiert haben.
Wir sehen, dass vor uns eine explosive Mischung aus den beiden vorherigen provisorischen Häusern liegt. Wir legen die Adresse wie beim Schreiben in das Register fest und nehmen die Daten gemäß den Regeln zum Lesen von Daten.
Und was? Beginnen wir mit der Entwicklung eines Automaten, der all dies für uns formt.
Strukturdiagramm des Kopfes
Wie Sie der obigen Beschreibung entnehmen können, muss der Kopf gleichzeitig mit zwei Bussen verbunden sein: AVALON_MM, um auf Register zuzugreifen, und AVALON_ST, um Daten zu senden, die im RAM gespeichert werden sollen. Die Hauptsache im Kopf ist das Gehirn. Es sollte also eine Zustandsmaschine sein, die die Zeitdiagramme generiert, die wir zuvor betrachtet haben.
Beginnen wir seine Entwicklung mit der Funktion, Daten zu empfangen. Hierbei ist zu beachten, dass wir den Fluss vom ULPI-Bus in keiner Weise beeinflussen können. Daten von dort, wenn es anfing zu gehen, werden es gehen. Es ist ihnen egal, ob der AVALON_ST-Bus bereit ist oder nicht. Daher werden wir die Nichtverfügbarkeit des Busses einfach ignorieren. In einem realen Analysegerät kann bei Datenausgabe ohne Bereitschaft eine Alarmanzeige hinzugefügt werden. Im Rahmen des Artikels sollte alles einfach sein. Denken wir also für die Zukunft daran. Und um die Verfügbarkeit des Busses wie bei einem Logikanalysator sicherzustellen, haben wir einen externen FIFO-Block. Insgesamt ist der Übergangsgraph des Automaten zum Empfangen des Datenstroms wie folgt:
DIR startete - begann zu empfangen. Wir haben eine Uhr auf wait1 gehängt, dann akzeptieren wir sie, während DIR gleich eins ist. Auf Null gefallen - nach einer Uhr (obwohl nicht die Tatsache, dass es benötigt wird, aber für den Moment werden wir den Zustand wait2 setzen) kehrte in den Leerlauf zurück.
Bisher ist alles einfach. Vergessen Sie nicht, dass nicht nur die D0_D7-Leitungen, sondern auch die NXT-Leitung zum AVALON_ST-Bus gehen sollten, da dieser bestimmt, was gerade übertragen wird: ein Befehl oder Daten.
Ein Registerschreibzyklus kann eine unvorhersehbare Ausführungszeit haben. Aus Sicht des AVALON_MM-Busses ist dies nicht sehr gut. Deshalb werden wir es etwas kniffliger machen. Lassen Sie uns ein Pufferregister erstellen. Die Daten werden eingespeist, wonach der AVALON_MM-Bus sofort freigegeben wird. Aus der Sicht des zu entwickelnden Automaten erscheint das Eingangssignal have_reg (Daten im Register wurden empfangen, die gesendet werden sollten) und das Ausgangssignal reg_served (was bedeutet, dass der Registerausgabeprozess abgeschlossen ist). Fügen Sie die Logik des Schreibens zum Register im Übergangsgraphen des Automaten hinzu.
Ich habe die DIR = 1-Bedingung rot hervorgehoben, um zu verdeutlichen, dass sie die höchste Priorität hat. Dann ist es möglich, die Erwartung des Nullwerts des DIR-Signals im neuen Zweig der Maschine auszuschließen. Eine Anmeldung bei einem Zweig mit einem anderen Wert ist einfach nicht möglich. Der Status SET_CMDw ist blau, da er höchstwahrscheinlich rein virtuell ist. Dies sind nur auszuführende Aktionen! Niemand stört es, die entsprechende Konstante auf dem Datenbus und nur während des Übergangs einzustellen! Im STPw-Zustand kann das reg_served-Signal unter anderem auch für einen Taktzyklus gespannt werden, um das BSY-Signal für den AVALON_MM-Bus zu löschen und einen neuen Schreibzyklus zu ermöglichen.
Nun, es bleibt noch eine Verzweigung zum Lesen des ULPI-Registers hinzuzufügen. Hier ist das Gegenteil der Fall. Der Busdienstautomat sendet uns eine Anfrage und wartet auf unsere Antwort. Wenn die Daten empfangen wurden, kann er sie verarbeiten. Und es wird mit Busaufhängung oder Polling funktionieren, das sind schon die Probleme dieser Maschine. Heute habe ich beschlossen, an einer Umfrage zu arbeiten. Daten anfordern - BSY ist erschienen. Wie BSY verschwunden ist - Sie können gelesene Daten empfangen. Insgesamt hat das Diagramm die Form:
Vielleicht werden im Laufe der Entwicklung einige Anpassungen vorgenommen, aber wir werden uns vorerst an dieses Diagramm halten. Dies ist schließlich kein Bericht, sondern eine Anleitung zur Entwicklungsmethodik. Und die Technik ist so, dass Sie zuerst ein Übergangsdiagramm zeichnen und dann - die Logik gemäß dieser Abbildung ausführen müssen, um Popup-Details anzupassen.
Funktionen der Automatenimplementierung von der Seite AVALON_MM
Wenn Sie mit dem AVALON_MM-Bus arbeiten, haben Sie zwei Möglichkeiten. Die erste besteht darin, Verzögerungen beim Buszugang zu verursachen. Wir haben diesen Mechanismus in einem der vorherigen Artikel untersucht , und ich habe gewarnt, dass er mit Problemen behaftet ist. Der zweite Weg ist klassisch. Geben Sie das Statusregister ein. Setzen Sie zu Beginn der Transaktion das BSY-Signal nach Abschluss - Zurücksetzen. Und weisen Sie der Bus-Master-Logik (Nios II-Prozessor oder JTAG-Bridge) die Verantwortung für alles zu. Jede der Optionen hat ihre eigenen Vor- und Nachteile. Da wir bereits Varianten mit Busverzögerungen durchgeführt haben, lassen Sie uns zur Abwechslung heute alles über das Statusregister erledigen.
Wir entwerfen die Hauptmaschine
Das erste, worauf ich Sie aufmerksam machen möchte, sind meine Lieblings-RS-Trigger. Wir haben zwei Maschinen. Der erste dient dem AVALON_MM-Bus, der zweite der ULPI-Schnittstelle. Wir fanden heraus, dass die Verbindung zwischen ihnen durch ein paar Flaggen geht. In jedes Flag kann nur ein Prozess schreiben. Jeder Automat wird durch einen eigenen Prozess implementiert. Wie soll ich sein? Seit einiger Zeit füge ich gerade einen RS-Trigger hinzu. Wir haben zwei Bits, also müssen sie von zwei RS-Flip-Flops erzeugt werden. Hier sind sie:
//
always_ff @(posedge ulpi_clk)
begin
//
if (reg_served)
write_busy <= 0;
else if (have_reg)
write_busy <= 1;
//
if (read_finished)
read_busy <= 0;
else if (reg_request)
read_busy <= 1;
end
Ein Prozess cocks reg_served, der zweite cocks have_reg. Und das RS-Flip-Flop erzeugt in seinem eigenen Prozess das write_busy-Signal auf ihrer Basis. In ähnlicher Weise wird read_busy aus read_finished und reg_request gebildet. Sie können es anders machen, aber in dieser Phase des kreativen Weges mag ich diese Methode.
So werden BSY-Flags gesetzt. Gelb steht für den Schreibvorgang, Blau für den Lesevorgang. Der Verilogov-Prozess hat eine sehr interessante Funktion. Darin können Sie Werte nicht nur einmal, sondern mehrmals zuweisen. Wenn ich also möchte, dass ein Signal für einen Taktzyklus abhebt, mache ich es zu Beginn des Prozesses ungültig (wir sehen, dass beide Signale dort annulliert werden) und setze es durch eine Bedingung, die während eines Taktzyklus ausgeführt wird, auf eins. Durch Eingabe der Bedingung wird die Standardeinstellung überschrieben. In allen anderen Fällen wird es funktionieren. Das Schreiben in den Datenport initiiert somit den Start des have_reg-Signals für einen Taktzyklus, und das Schreiben von Bit 0 in den Steuerport initiiert den Start des reg_request-Signals.
Der gleiche Text.
// AVALON_MM
always_ff @(posedge ulpi_clk)
begin
// ,
//
have_reg <= 0;
reg_request <= 0;
if (write == 1)
begin
case (address)
0 : addr_to_ulpi <= writedata [5:0];
//
1 : begin
data_to_ulpi <= writedata [7:0];
have_reg <= 1;
end
2 : begin
//
reg_request <= writedata[0];
force_reset = writedata [31];
end
3: begin end
endcase
end
end
Wie wir oben gesehen haben, reicht ein Taktzyklus aus, damit das entsprechende RS-Flipflop auf eins gesetzt wird. Und ab diesem Moment beginnt das eingestellte BSY-Signal aus dem Statusregister zu lesen:
Der gleiche Text.
// AVALON_MM
always_comb
begin
case (address)
// ( )
0 : readdata <= {26'b0, addr_to_ulpi};
//
1 : readdata <= {23'b0, data_from_ulpi};
// 2 - , -
//
3 : readdata <= {30'b0, (reg_request | read_busy), (have_reg | write_busy)};
default: readdata <= 0;
endcase
end
Eigentlich haben wir uns also natürlich mit den Prozessen vertraut gemacht, die für die Arbeit mit dem AVALON_MM-Bus dienen.
Ich möchte Sie auch an die Prinzipien der Arbeit mit dem ulpi_data-Bus erinnern. Dieser Bus ist bidirektional. Daher sollten Sie eine Standardtechnik verwenden, um damit zu arbeiten. So wird der entsprechende Port deklariert:
inout [7:0] ulpi_data,
Wir können von diesem Bus lesen, aber wir können nicht direkt schreiben. Stattdessen erstellen wir eine Kopie für den Datensatz.
logic [7:0] ulpi_d = 0;
Und wir verbinden diese Kopie über den folgenden Multiplexer mit dem Hauptbus:
// inout-
assign ulpi_data = (ulpi_dir == 0) ? ulpi_d : 8'hzz;
Ich habe versucht, die Logik der Hauptmaschine so weit wie möglich im Verilog-Code zu kommentieren. Wie ich während der Entwicklung des Übergangsgraphen erwartet hatte, hat sich die Logik in der realen Implementierung etwas geändert. Einige der Staaten wurden rausgeworfen. Wenn Sie jedoch die Grafik und den Quelltext vergleichen, hoffe ich, dass Sie alles verstehen, was dort getan wird. Daher werde ich nicht über diese Maschine sprechen. Es ist besser, als Referenz den vollständigen Text des Moduls anzugeben, der zum Zeitpunkt vor der Änderung auf der Grundlage der Ergebnisse praktischer Experimente relevant war.
Volltext des Moduls.
module ULPIhead
(
input reset,
output clk66,
// AVALON_MM
input [1:0] address,
input write,
input [31:0] writedata,
input read,
output logic [31:0] readdata = 0,
// AVALON_ST
input logic source_ready,
output logic source_valid = 0,
output logic [15:0] source_data = 0,
// ULPI
inout [7:0] ulpi_data,
output logic ulpi_stp = 0,
input ulpi_nxt,
input ulpi_dir,
input ulpi_clk,
output ulpi_rst
);
logic have_reg = 0;
logic reg_served = 0;
logic reg_request = 0;
logic read_finished = 0;
logic [5:0] addr_to_ulpi;
logic [7:0] data_to_ulpi;
logic [7:0] data_from_ulpi;
logic write_busy = 0;
logic read_busy = 0;
logic [7:0] ulpi_d = 0;
logic force_reset = 0;
//
always_ff @(posedge ulpi_clk)
begin
//
if (reg_served)
write_busy <= 0;
else if (have_reg)
write_busy <= 1;
//
if (read_finished)
read_busy <= 0;
else if (reg_request)
read_busy <= 1;
end
// AVALON_MM
always_comb
begin
case (address)
// ( )
0 : readdata <= {26'b0, addr_to_ulpi};
//
1 : readdata <= {23'b0, data_from_ulpi};
// 2 - , -
//
3 : readdata <= {30'b0, (reg_request | read_busy), (have_reg | write_busy)};
default: readdata <= 0;
endcase
end
// AVALON_MM
always_ff @(posedge ulpi_clk)
begin
// ,
//
have_reg <= 0;
reg_request <= 0;
if (write == 1)
begin
case (address)
0 : addr_to_ulpi <= writedata [5:0];
//
1 : begin
data_to_ulpi <= writedata [7:0];
have_reg <= 1;
end
2 : begin
//
reg_request <= writedata[0];
force_reset = writedata [31];
end
3: begin end
endcase
end
end
//
enum {idle,
wait1,wr_st,
wait_nxt_w,hold_w,
wait_nxt_r,wait_dir1,latch,wait_dir0
} state = idle;
always_ff @ (posedge ulpi_clk)
begin
if (reset)
begin
state <= idle;
end else
begin
//
source_valid <= 0;
reg_served <= 0;
ulpi_stp <= 0;
read_finished <= 0;
case (state)
idle: begin
if (ulpi_dir)
state <= wait1;
else if (have_reg)
begin
// ,
// ,
//
ulpi_d [7:6] <= 2'b10;
ulpi_d [5:0] <= addr_to_ulpi;
state <= wait_nxt_w;
end
else if (reg_request)
begin
// -
ulpi_d [7:6] <= 2'b11;
ulpi_d [5:0] <= addr_to_ulpi;
state <= wait_nxt_r;
end
end
// TURN_AROUND
wait1 : begin
state <= wr_st;
// ,
source_valid <= 1;
source_data <= {7'h0,!ulpi_nxt,ulpi_data};
end
// DIR - AVALON_ST
wr_st : begin
if (ulpi_dir)
begin
// ,
source_valid <= 1;
source_data <= {7'h0,!ulpi_nxt,ulpi_data};
end else
// wait2,
// , - .
state <= idle;
end
wait_nxt_w : begin
if (ulpi_nxt)
begin
ulpi_d <= data_to_ulpi;
state <= hold_w;
end
end
hold_w: begin
// , ULPI
// . NXT
// ...
if (ulpi_nxt) begin
// , AVALON_MM
reg_served <= 1;
ulpi_d <= 0; // idle
ulpi_stp <= 1; // STP
state <= idle; // - idle
end
end
// STPw ...
// ...
// . , NXT
// ,
wait_nxt_r : begin
if (ulpi_nxt)
begin
ulpi_d <= 0; //
state <= wait_dir1;
end
end
// ,
wait_dir1: begin
if (ulpi_dir)
state <= latch;
end
//
// -
latch: begin
data_from_ulpi <= ulpi_data;
state <= wait_dir0;
end
// ,
wait_dir0: begin
if (!ulpi_dir)
begin
state <= idle;
read_finished <= 1;
end
end
default: begin
state <= idle;
end
endcase
end
end
// inout-
assign ulpi_data = (ulpi_dir == 0) ? ulpi_d : 8'hzz;
// reset ,
assign ulpi_rst = reset | force_reset;
assign clk66 = ulpi_clk;
endmodule
Programmierhandbuch
ULPI-Registeradressport (+0)
Die Adresse des ULPI-Registers des Busses, mit dem die Arbeit ausgeführt wird, sollte mit dem Offset +0 in den Port gestellt werden
ULPI-Registerdatenport (+4)
Beim Schreiben in diesen Port: Der Vorgang zum Schreiben in das ULPI-Register, dessen Adresse im Port der Registeradresse festgelegt wurde, beginnt automatisch. Es ist verboten, an diesen Port zu schreiben, bis der Vorgang des vorherigen Schreibvorgangs abgeschlossen ist.
Beim Lesen: Dieser Port gibt den Wert zurück, der beim letzten Lesen aus dem ULPI-Register erhalten wurde.
ULPI-Steuerport (+8)
Der Lesevorgang ist immer Null. Die Bitzuweisung zum Schreiben lautet wie folgt:
Bit 0 - Startet beim Schreiben eines einzelnen Werts den Prozess des Lesens des ULPI-Registers, dessen Adresse im Adressport des ULPI-Registers festgelegt ist.
Bit 31 - Sendet beim Schreiben eines RESET-Signals ein RESET-Signal an den ULPI-Chip.
Der Rest der Bits ist reserviert.
Statusport (+ 0x0C)
Schreibgeschützt.
Bit 0 - WRITE_BUSY. Wenn gleich eins, wird gerade in das ULPI-Register geschrieben.
Bit 1 - READ_BUSY. Wenn gleich eins, wird gerade aus dem ULPI-Register gelesen.
Der Rest der Bits ist reserviert.
Fazit
Wir haben uns mit der Methode der physischen Organisation des USB-Analysatorkopfs vertraut gemacht, einen grundlegenden Automaten für die Arbeit mit der ULPI-Mikroschaltung entworfen und einen Entwurf für ein SystemVerilog-Modul für diesen Kopf implementiert. In den folgenden Artikeln werden wir uns den Modellierungsprozess ansehen, dieses Modul simulieren und dann praktische Experimente damit durchführen, anhand derer wir den Code sauber finalisieren werden. Das heißt, bis zum Ende haben wir mindestens vier weitere Artikel.