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.)
:
Bearbeitet durch Moderator
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.
:
Bearbeitet durch Moderator
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'.
:
Bearbeitet durch User
>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.
das hier:
1 | case ctr mod 8 is |
2 | when 4 => |
3 | ram_addr <= std_logic_vector(to_unsigned(ctr / 8, 4)); |
4 | wave <= shiftreg(7); |
5 | shiftreg(7 downto 1) <= shiftreg(6 downto 0); |
6 | when 5 => |
7 | ram_en <= '1'; |
8 | wave <= shiftreg(7); |
9 | shiftreg(7 downto 1) <= shiftreg(6 downto 0); |
10 | when 6 => |
11 | ram_en <= '0'; |
12 | wave <= shiftreg(7); |
13 | shiftreg(7 downto 1) <= shiftreg(6 downto 0); |
14 | when 7 => |
15 | shiftreg <= ram_data; |
16 | wave <= ram_data(7); |
17 | when others => |
18 | wave <= shiftreg(7); |
19 | shiftreg(7 downto 1) <= shiftreg(6 downto 0); |
20 | end case; |
kann man (funktionsgleich) auch so schreiben:
1 | wave <= shiftreg(7); |
2 | shiftreg(7 downto 1) <= shiftreg(6 downto 0); |
3 | |
4 | case ctr mod 8 is |
5 | when 4 => |
6 | ram_addr <= std_logic_vector(to_unsigned(ctr / 8, 4)); |
7 | when 5 => |
8 | ram_en <= '1'; |
9 | when 6 => |
10 | ram_en <= '0'; |
11 | when 7 => |
12 | shiftreg <= ram_data; |
13 | wave <= ram_data(7); |
14 | when others => |
15 | null; |
16 | end case; |
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 | signal a,b,c,d : std_logic; |
2 | :
|
3 | erster: process (clk) is |
4 | begin
|
5 | if rising_edge(clk) then |
6 | b <= a; |
7 | c <= b; |
8 | d <= c xor b; |
9 | end if; |
10 | end process; |
11 | :
|
12 | zweiter: process (clk) is |
13 | begin
|
14 | if rising_edge(clk) then |
15 | d <= c xor b; |
16 | c <= b; |
17 | b <= a; |
18 | end if; |
19 | end process; |
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:
1 | process is |
2 | begin
|
3 | wait on clk, reset; |
4 | if reset = '0' then |
5 | :
|
6 | elsif rising_edge(clk) then |
7 | :
|
8 | end if; |
9 | end process; |
:
Bearbeitet durch Moderator
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:
1 | library IEEE; |
2 | use IEEE.STD_LOGIC_1164.ALL; |
3 | use IEEE.NUMERIC_STD.ALL; |
4 | |
5 | entity RAM_SR is Port( |
6 | CLK: in std_logic; |
7 | TXD: out std_logic); |
8 | end RAM_SR; |
9 | |
10 | architecture Behavioral of RAM_SR is |
11 | |
12 | type Array16x8 is array(0 to 15) of std_logic_vector(7 downto 0); |
13 | signal RAM: Array16x8:=(x"00",x"01",x"02",x"03",x"04",x"05",x"06",x"07",x"08",x"09",x"0A",x"0B",x"0C",x"0D",x"0E",x"0F"); |
14 | signal RAM_Address: unsigned(3 downto 0):=(others => '0'); |
15 | signal RAM_Data: std_logic_vector(7 downto 0):=(others => '0'); |
16 | |
17 | signal TX_Register: std_logic_vector(9 downto 0):=(others => '0'); |
18 | signal TX_Bit_Address: integer range 0 to 9:=9; |
19 | signal TX_Busy: std_logic:='0'; |
20 | signal TX_Start: std_logic:='0'; |
21 | signal TX_Data: std_logic_vector(7 downto 0):=(others => '0'); |
22 | signal TX_Bd_counter: integer range 0 to 24 := 24; -- 4 MBd@100MHz |
23 | |
24 | begin
|
25 | |
26 | process begin |
27 | wait until rising_edge(CLK); |
28 | RAM_Data <= RAM(to_integer(RAM_Address)); |
29 | TX_Start <= '0'; |
30 | if TX_Busy = '0' then |
31 | RAM_Address <= RAM_Address +1; |
32 | TX_Start <= '1'; |
33 | TX_Data <= RAM_Data; |
34 | end if; |
35 | end process; |
36 | |
37 | process begin |
38 | wait until rising_edge(CLK); |
39 | if TX_Bd_counter < 24 then |
40 | TX_Bd_counter <= TX_Bd_counter +1; |
41 | else
|
42 | if TX_Bit_Address < 9 then |
43 | TX_Bit_Address <= TX_Bit_Address +1; |
44 | TX_Bd_counter <= 0; |
45 | else
|
46 | if TX_Start = '1' then |
47 | TX_Bit_Address <= 0; |
48 | TX_Bd_counter <= 0; |
49 | TX_Register <= "1" & TX_Data & "0"; |
50 | end if; |
51 | end if; |
52 | end if; |
53 | end process; |
54 | |
55 | TX_Busy <= '0' when TX_Start = '0' and TX_Bit_Address = 9 and TX_Bd_counter = 24 else '1'; |
56 | TXD <= TX_Register(TX_Bit_Address); |
57 | |
58 | end Behavioral; |
Und hier die Variante mit Lothars UART von http://www.lothar-miller.de/s9y/categories/42-RS232 :
1 | library IEEE; |
2 | use IEEE.STD_LOGIC_1164.ALL; |
3 | use IEEE.NUMERIC_STD.ALL; |
4 | |
5 | entity RAM_SR is Port( |
6 | CLK: in std_logic; |
7 | TXD: out std_logic); |
8 | end RAM_SR; |
9 | |
10 | architecture Behavioral of RAM_SR is |
11 | |
12 | component RS232 is |
13 | Generic( |
14 | Quarz_Taktfrequenz : integer := 50000000; -- Hertz |
15 | Baudrate : integer := 9600); -- Bits/Sec) |
16 | Port( |
17 | RXD : in STD_LOGIC; |
18 | RX_Data : out STD_LOGIC_VECTOR (7 downto 0); |
19 | RX_Busy : out STD_LOGIC; |
20 | TXD : out STD_LOGIC; |
21 | TX_Data : in STD_LOGIC_VECTOR (7 downto 0); |
22 | TX_Start : in STD_LOGIC; |
23 | TX_Busy : out STD_LOGIC; |
24 | CLK : in STD_LOGIC); |
25 | end component; |
26 | |
27 | type ARR4x8 is array(0 to 15) of std_logic_vector(7 downto 0); |
28 | signal RAM: ARR4x8:=(x"00",x"01",x"02",x"03",x"04",x"05",x"06",x"07",x"08",x"09",x"0A",x"0B",x"0C",x"0D",x"0E",x"0F"); |
29 | signal RAM_Address: unsigned(3 downto 0):=(others => '0'); |
30 | signal RAM_Data: std_logic_vector(7 downto 0):=(others => '0'); |
31 | |
32 | signal TX_Busy: std_logic:='0'; |
33 | signal TX_Start: std_logic:='0'; |
34 | signal TX_Data: std_logic_vector(7 downto 0):=(others => '0'); |
35 | |
36 | begin
|
37 | |
38 | process begin |
39 | wait until rising_edge(CLK); |
40 | RAM_Data <= RAM(to_integer(RAM_Address)); |
41 | TX_Start <= '0'; |
42 | if TX_Busy = '0' then |
43 | RAM_Address <= RAM_Address +1; |
44 | TX_Start <= '1'; |
45 | TX_Data <= RAM_Data; |
46 | end if; |
47 | end process; |
48 | |
49 | uart: RS232 |
50 | Generic map( |
51 | Quarz_Taktfrequenz => 100000000, |
52 | Baudrate => 4000000) |
53 | Port map( |
54 | RXD => '1', |
55 | RX_Data => open, |
56 | RX_Busy => open, |
57 | TXD => TXD, |
58 | TX_Data => TX_Data, |
59 | TX_Start => TX_Start, |
60 | TX_Busy => TX_Busy, |
61 | CLK => CLK); |
62 | |
63 | end Behavioral; |
:
Bearbeitet durch User
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 | if reset = '0' then |
4 | shiftreg <= "00000000"; |
5 | :
|
6 | elsif rising_edge(clk) then |
7 | :
|
8 | when 7 => shiftreg <= ram_data; |
9 | :
|
10 | end if; |
11 | end process; |
12 | |
13 | process(clk, reset, shiftreg) is |
14 | begin
|
15 | :
|
16 | elsif rising_edge(clk) then |
17 | :
|
18 | shiftreg(7 downto 1) <= shiftreg(6 downto 0); |
19 | end if; |
20 | end process; |
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 <= c and b and a; |
4 | end process; |
5 | |
6 | -- das würde man natürlich eher ohne Prozess als nebenläufige Anweisung schreiben:
|
7 | |
8 | d <= c and b and a; |
(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 | if rising_edge(clk) then |
4 | d <= c and b and a; |
5 | end if; |
6 | end process; |
7 | |
8 | -- auch das könnte man natürlich als nebenläufige Anweisung schreiben:
|
9 | |
10 | d <= c and b and a when rising_edge(clk); |
:
Bearbeitet durch Moderator
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.
:
Bearbeitet durch Moderator
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.