Versuche gerade, einen RAM-Block hier einzubinden, und die jeweils
gelesenen Daten dann seriell aus einem Schieberegister rauszutakten.
Mein erster Versuch hatte da zwei Prozesse, einen für die Behandlung
eines Taktzählers, der einerseits die RAM-Adressen zählt und
andererseits die Serialisierung der RAM-Ansteuersignale erledigt. Wenn
ein neues Byte gelesen werden kann, wird dieses in das
Ausgabe-Schieberegister gepackt, welches dann von einem zweiten Prozess
mit jedem Taktschritt rausgeschoben wird.
Hier hat mir der Simulator dann diverse Us und Xs im Schieberegister
gezeigt, obwohl der Reset am Anfang ja eigentlich das shiftregister
sauber ausnullen sollte.
Packe ich alles Handling in ein relativ umständlich anmutendes
case-Statement in einem Prozess, funktioniert es wie gewünscht.
Wie würde man das logisch trennen, ohne in die Undefinierbarkeiten
meines ersten Versuchs zu laufen?
(ps: Der RAM-Inhalt ist erstmal komplett durch die Initialisierungsdatei
bestimmt. Die Schreib-Seite wäre erst später dran.)
In der ersten Variante wird in zwei unterschiedlichen Prozessen in
"shiftreg" geschrieben. Das ist nicht erlaubt und erzeugt ein undefined
fuer eben diesen Vektor.
Im Prinzip ist mir das schon klar. Nur: wie würde man das besser (und
vor allem schöner) anstellen?
Diesen "case-Klumpen" in der zweiten Version finde ich irgendwie
hässlich und unübersichtlich, auch wenn er formal korrekt ist.
Also im ersten Schritt wuerde ich den Prozess in eine eigene Entity
packen und Speicher und Controller trennen. Aber das ist ja erstmal
nicht das Thema. ;-)
Erstmal finde ich die case Statements nicht wirklich unuebersichtlich.
Liesst sich wie eine Art FSM was erstmal leicht verstaendlich ist. Von
daher kannst auch gleich eine saubere FSM definieren.
Den Counter von 0 bis 127 wuerde ich dazu in einen extra Prozess machen
und dann beim Ueberlauf ein enable Flag setzen, welches den Prozess mit
der FSM anstoesst. Dort hast du dann wenn ich das richtig sehe die 3
States: Wait, Set RAM Adress und Read RAM.
Wenn du allen deinen case Statements (ausser der 5) noch ein ram_en <=
'0' spendierst, dann siehst du auch das die alle eine ziemlich grosse
Schnittmenge haben und sich zusammenfassen lassen.
Ob das am Ende Uebersichtlicher ist, ist allerdings stark
geschmacksabhaengig. Gerne mache ich da morgen mal ein Beispiel dazu,
jetzt ists mir dafuer allerdings doch etwas zu spaet. :-(
Das ist quasi ein klassisches Ding das man irgendwann mal baut. Z. B.
wenn man Daten über UART ausgeben möchte die man vorher im BRAM abgelegt
hat.
Bei mir hat die UART Komponente ein Start Signal und ein Busy. Und
sobald Busy '0' ist, dann werden neue Daten aus dem RAM gelesen, an den
Dateneingang angelegt und das Start Signal auf '1' gelegt.
Dann läuft der UART los und setzt sofort Busy auf '1' wodurch schon im
nächsten Takt auch von aussen das Start wieder auf '0' gesetzt wird, das
liegt also nur für einen Takt auf '1'.
>Das ist quasi ein klassisches Ding das man irgendwann mal baut. Z. B.>wenn man Daten über UART ausgeben möchte die man vorher im BRAM abgelegt>hat.
Ja, das kann man oft gebrauchen. Das hatte ich hier mal als erstes
Bastelprojekt versucht, wobei das Pollin-Board ein externes RAM hat:
Beitrag "Logic Analyser mit Pollin CPLD Board"
Die Kombination aus Schnittstelle, Speicher und Steuerlogik könnte man
als Standard-Template gebrauchen.
Mittlerweile bin ich aber der Meinung, dass eine SPI statt asynchroner
serielle Schnittstelle sinnvoller ist, da man höhere Datenübertragungen
schafft.
Zur Not kann man immer noch irgend eine Arduino-Variante als Seriel-SPI
Adapter nehmen, die sind teilweise auch nicht mehr viel teurer als ein
USB-Seriel-Converter.
Vielleicht sollte man mal ein Gemeinschaftsprojekt für so ein Template
starten.
indem man alles, was redundant ist, aus dem case rauszieht und nur die
Ausnahmen drin läßt. Da muss man sich als "normaler" Programmierer (wo
"falsche" Zuweisungen, die man anschliessend korrigiert, meist mit
Performanceeinbussen bestraft werden) erst mal dran gewöhnen.
Der Synthese ist das wurscht.
Ihr habt ja lustige Arbeitszeiten. ;-)
Danke mal für alle Tipps. Dass man das modularisieren kann, ist
natürlich ein anderes Thema, das hier war ja nur ein runtergebrochenes
Minimalbeispiel, damit ich den RAM überhaupt erstmal in der Simulation
gehandhabt bekomme, und all meine Us und Xs los werde.
Markus F. schrieb:> kann man (funktionsgleich) auch so schreiben:
Das war jetzt genau das, was ich gesucht habe, danke für diesen Hinweis.
Ich habe mich zwar im Großen und Ganzen ganz gut dran gewöhnen können,
dass bei so einer Hardwarebeschreibung einiges anders ist als beim
Programmieren, aber manche Dinge sind da eben nicht so offensichtlich.
Jörg W. schrieb:> aber manche Dinge sind da eben nicht so offensichtlich.
Man darf sich auch durch syntaktische Änlichkeiten nicht verleiten
lassen, irgendwelche Kenntnisse oder Erfahrungen aus der
SW-Programmierung ohne vorherige Tauglichkeitsprüfung anzuwenden. Ganz
offensichtlich ist das bei der for-Schleife, die etwas völlig anderes
macht, als ein SW-Programmierer erwarten würde.
Markus F. schrieb:> indem man alles, was redundant ist, aus dem case rauszieht und nur die> Ausnahmen drin läßt.
Denn das sehr angenehme Verhalten von Signalen in Prozessen ist:
1. sie behalten ihren Eingangswert bis zum Prozessende(*). Somit ist in
einem Prozess völlig egal, an welcher Stelle ein Signal abgefragt
wird.
2. sie übernehmen erst am Prozessende(*) den zuletzt zugewiesenen Wert.
Man kann ihnen also "vorher" oder "zwischendurch" alle möglichen Werte
zuweisen.
Diese beiden Prozesse machen/ergeben also genau das selbe:
1
signala,b,c,d:std_logic;
2
:
3
erster:process(clk)is
4
begin
5
ifrising_edge(clk)then
6
b<=a;
7
c<=b;
8
d<=cxorb;
9
endif;
10
endprocess;
11
:
12
zweiter:process(clk)is
13
begin
14
ifrising_edge(clk)then
15
d<=cxorb;
16
c<=b;
17
b<=a;
18
endif;
19
endprocess;
Denn wenn man die beiden obigen "Regeln" anwendet, dann sieht man, dass
hier die Reihenfolge der Zuweisungen völlig uninteressant ist.
(*)oder eben explizit bis zum nächsten wait, denn jede Sensitivliste
eines Prozesses kann durch ein "wait on" samt Signalliste ersetzt
werden:
So, hier mal ein Minimalbeispiel.
Das hat einen RAM, der wird getaktet gelesen und es hat ein
Schieberegister das je 10 Bits wie bei einem UART mit Start- und Stopbit
herausschiebt, hier mit 4 MBaud.
Ich habe das hier in zwei Prozesse unterteilt und zwar deshalb, weil
"man üblicherweise" den zweiten unteren Prozess als eigene Komponente
baut. Da kann man problemlos die UART Komponente von Lothar verwenden
und eben die Ports passend anbinden.
Hier wie es ohne Lothars UART aussieht:
Jörg W. schrieb:> Ihr habt ja lustige Arbeitszeiten. ;-)
Emergency call um halb drei. Dann erst mal zwei Tassen Kaffee, um
erstmal das Problem zu verstehen. Dann mal eben die Welt retten ;).
Da geht so ein "VHDL-Problemchen" fast nebenbei.
Danach braucht man nicht mehr ins Bett. Mach' ich jetzt.
Jörg W. schrieb:> Hier hat mir der Simulator dann diverse Us und Xs im Schieberegister> gezeigt, obwohl der Reset am Anfang ja eigentlich das shiftregister> sauber ausnullen sollte.
Du treibst da ein Signal aus mehreren Quellen. Denn ein Prozess
beschreibt Hardware. Ein zweiter Prozess beschreibt ebenfalls Hardware.
Und wenn beide Ptozesse schreibend auf das selbe (Schiebe-)Register
zugreifen wollen, dann rumpelts:
1
process(clk,reset)is
2
begin
3
ifreset='0'then
4
shiftreg<="00000000";
5
:
6
elsifrising_edge(clk)then
7
:
8
when7=>shiftreg<=ram_data;
9
:
10
endif;
11
endprocess;
12
13
process(clk,reset,shiftreg)is
14
begin
15
:
16
elsifrising_edge(clk)then
17
:
18
shiftreg(7downto1)<=shiftreg(6downto0);
19
endif;
20
endprocess;
Der Synthesizer nennt das "multiple drivers". Und keine Sorge: das ist
ein beliebter Fehler, da muss jeder mal durch ;-)
Noch eine Silbe zur Sensitivliste:
process(clk, reset, shiftreg) is ...
shiftreg ist hier unnötig, weil die Neuberechung des Prozesses nur bei
einer Änderung von clk oder reset nötig ist. Und in der Realität ist es
ja so, dass einem D-FF zum Glück nur die Taktflanke interessiert, aber
nicht, ob der Dateneingang herumzappelt.
Noch eine weitere Silbe zur Sensitivliste:
diese Liste interessiert nur den Simulator. Der Synthesizer schert sich
nicht darum, sondern meldet dir einfach, wenn ein Signal fehlt und meint
dazu lapidar, dass deine Simulation Käse ist: "simulation will not match
synthesis result".
Lothar M. schrieb:> Der Synthesizer nennt das "multiple drivers". Und keine Sorge: das ist> ein beliebter Fehler, da muss jeder mal durch ;-)
Ja, im Prinzip war mir das dann schon klar geworden, dass „man das nicht
macht“. Das war ja nur der Versuch, dass der hineschriebene Code etwas
schöner aussah als das, was ich danach dann fabriziert habe.
Mit Markus' Implementierungsvorschlag ist es ja dann beides, technisch
in Ordnung und übersichtlich aussehend.
> Noch eine Silbe zur Sensitivliste:> process(clk, reset, shiftreg) is ...> shiftreg ist hier unnötig, weil die Neuberechung des Prozesses nur bei> einer Änderung von clk oder reset nötig ist.
An irgendeiner Stelle hatte ich in der Simulation unerwartete Effekte,
als ich mal ein Signal da nicht hinein geschrieben habe. Weiß aber nicht
mehr ganz den Zusammenhang.
> Noch eine weitere Silbe zur Sensitivliste:> diese Liste interessiert nur den Simulator.
OK.
Jörg W. schrieb:>> shiftreg ist hier unnötig, weil die Neuberechung des Prozesses nur bei>> einer Änderung von clk oder reset nötig ist.> An irgendeiner Stelle hatte ich in der Simulation unerwartete Effekte,> als ich mal ein Signal da nicht hinein geschrieben habe. Weiß aber nicht> mehr ganz den Zusammenhang.
Vermutlich war das ein kombinatorischer Prozess. Dessen Ergebnis hängt
natürlich von allen Signalen ab, die auf der rechten Seite eines
Zuweisungsoperators stehen.
Ein UND-Gatter muss sofort neu berechnet werden, wenn sich eines der
Signale ändert:
1
erster:process(a,b,c)is
2
begin
3
d<=candbanda;
4
endprocess;
5
6
-- das würde man natürlich eher ohne Prozess als nebenläufige Anweisung schreiben:
7
8
d<=candbanda;
(das würde man natürlich normalerweise ohne Prozesspipapo als
nebenläufige Anweisung schreiben)
Wenn der Wert des UND-Gatters aber hinterher auf ein Register geht, dann
ist das Ergebnis nur vom Takt abhängig. Und demzufolge gehört in einen
getakteten Prozess nur der Takt (und bestenfalls noch der asynchrone
Reset):
1
erster:process(clk)is
2
begin
3
ifrising_edge(clk)then
4
d<=candbanda;
5
endif;
6
endprocess;
7
8
-- auch das könnte man natürlich als nebenläufige Anweisung schreiben:
Lothar M. schrieb:> Ganz> offensichtlich ist das bei der for-Schleife, die etwas völlig anderes> macht, als ein SW-Programmierer erwarten würde.
Da bin ich übrigens weiterhin anderer Meinung.
Die for-Schleife macht exakt genau das, was ein SW-Programmierer
erwarten würde. Nur eben innerhalb der Randbedingungen, die ein
HW-Design bzw. dessen Synthese vorgibt.
Wenn ich eine for-Schleife in einen Prozess packe, der alles, was drin
steht, innerhalb eines Taktes ausführen muss, dann bleibt der Synthese
gar nichts anderes übrig, als die Effekte der for-Schleife während der
Synthese "vorzuberechnen" und die Resultate zu hinterlegen.
Das ist praktisch exakt dasselbe, was z.B. in C++ bei "Template
Metaprogramming (TMP)" bzw. "constexpr" gesteuert bzw. steuerbar und bei
jedem optimierenden C-Compiler "hinter den Kulissen" während des
Compilerlaufs (z.B. als loop unrolling) passiert.
In VHDL heisst das "static" (vereinfacht: "rechne alles aus, was Du zur
Compile-Zeit ausrechnen kannst und was übrig bleibt ist eine
unveränderliche Logik, die in einem Schritt abgearbeitet werden kann
oder ein Fehler" und hat mit dem C/C++ static Begriff nicht das
Geringste zu tun) und ein Muss, um aus einem dynamischen Programm ein
statisches Schaltnetz aufzubauen. In C++ TMP/constexpr ist das praktisch
exakt identisch (mit dem Unterschied, dass der Compiler am Ende meist
doch noch in "klassisches Verhalten" zurückfallen darf, wenn das
Konstrukt "über seinen Verstand" geht).
Wer (als "normaler Programmierer") das verinnerlicht hat, muss sich
nicht mehr jede for-Schleife extra angucken.
Markus F. schrieb:> Wer (als "normaler Programmierer") das verinnerlicht hat, muss sich> nicht mehr jede for-Schleife extra angucken.
Da ich ursprünglich aus der Hardware komme und mir vorstellen kann, wie
Hardware funktioniert, wäre ich mit sowas wie einer for-Schleife in
einer HDL sowieso außerordentlich skeptisch. :)
Markus F. schrieb:> Die for-Schleife macht exakt genau das, was ein SW-Programmierer> erwarten würde.
Der normale durchschnittliche SW-Programmierer erwartet eben nicht, dass
sein Compiler versucht, alle 100 Schleifendurchläufe gleichzeitig
auszuführen.
> was übrig bleibt ist eine unveränderliche Logik, die in einem Schritt> abgearbeitet werden kann
Genau das ist eine übliche for-Schleife in einem Programm für einen
normalen Programmierer eben nicht. Normale SW-Programmierer jammern ja
wie im Beitrag "Re: Entprellen (kein AVR)" schon
darüber, wenn ihre untätige, leere for-Schleife (zu Recht) wegoptimiert
wird.
> Wer (als "normaler Programmierer") das verinnerlicht hat, muss sich> nicht mehr jede for-Schleife extra angucken.
Wer sich mit dieser Problematik ausgiebig beschäftigt hat, ist eben
schon einen Schritt weiter als der "normale" Programmierer. Ich habe da
hin- und wieder mit Studenten zu tun, die das erste mal mit einer VHDL
konfrontiert werden und zuvor SW programmiert haben. Die allermeisten
werden von der for-Schleife überrascht und zeigen sich verwundert. Denn
sie wollen erst mal wie zuvor gelernt z.B. einfach zur Implementierung
einer seriellen Schnitte 10 Bits nacheinander in einer for-Schleife
einlesen.
Erst nach der entsprechenden Aufklärung wird ihnen klar, wie sie die
Arbeit anzugehen haben.