da ich schon des öfteren von unseren Studenten gefragt worden bin, wie sie in ihr Design eine Soft-CPU integrieren können, dachte ich mir ich ergreife mal die Initiative und gebe mal ein kleines abgespecktes Beispiel für die Einbindung einer Altera NIOS2-CPU in ein bestehendes VHDL-Design. Eine ausführlichere Anleitung findet ihr direkt bei Altera unter http://www.altera.com/literature/tt/tt_nios2_hardware_tutorial.pdf.
Zu Begin öffnet ihr euer bestehendes VHDL-Design und startet von dort aus den Quartus SOPC-Builder
(Handbuch des SOPC-Builders http://www.altera.com/literature/lit-sop.jsp).
SOPC-Builder Einstellungen:
Hier müsst ihr nun zu allererst einen Namen und das gewünschte Ausgabeformat (VHDL/Verilog) für die zu erstellende Komponente festlegen (z.B. CPU).
Im linken Fenster "System Contents" werden jetzt alle notwendigen Komponenten ausgewählt. Für die korrekte Funktion einer NIOS2 CPU wird nicht nur die Komponenten NIOS II Processor benötigt, sondern zusätzlich noch eine System ID Peripheral, eine JTAG UART, ein Interval Timer, ein RAM (intern oder extern als Programmspeicher) im Beispiel ein SDRAM, sowie die Anschlüsse für die externe Peripherie PIO (Parallel I/O) welche in unserem kleinen Beispiel durch 10 rote und 8 grüne LEDs sowie 4 Drucktaster repräsentiert werden soll.
Durch Doppelklicken auf die jeweilige Komponente in Fenster System Contents wird diese dem System hinzugefügt. Es öffnet sich für jede Komponente ein Setup-Fenster in dem der ANweder nun diverse Einstellungen bzgl. der späteren Verwendung treffen kann.
Clock Settings:
Damit der Prozessor arbeiten kann benötigt er noch einen Takt. In meinem Beispiel verwende ich für den SDRAM 50MHz (clk_50) und für alle anderen Komponenten 100MHz (clk_100). Dies wird im oberen Fenster Clock Settings eingestellt und später jeder Komponente einzeln in der Spalte Clock zugewiesen.
NIOS II Processor:
Prinzipiell gibt es für den NIOS2 drei Ausbaustufen:
- NIOS II/e -economy
NIOS II/s -standard
NIOS II/f -fast
Die Auswahl bleibt dem Anwender überlassen, wichtig ist jedoch die Einstellung der Größen für den Instruction und Data Cache. Dieser ist abhängig von der maximal zur Verfügung stehenden Größe des FPGA internen RAMs oder des extern verwendeten SDRAMs. Unter Reset_Vector und Exception_Vector wird der zu verwendende Speicher (hier SDRAM) eingestellt. Zusätzlich kann noch unter Custom Instructions ein Interrupt_vector (für z.B. die Taster) eingebunden werden.
System ID Peripheral:
Die System ID wird automatisch generiert und dient der eindeutigen Identifikation des Prozessorkerns. Anhand dieser ID erkennt später der Altera Compiler ob ein geschriebenes C/C++ Programm auf der ausgewählten CPU lauffähig ist oder nicht. Wichtig: Darauf achten die Komponente von sysid_0 in sysid umbenennen!
JTAG UART:
Hier empfielt sich für den Anfang keine Veränderungen in den Grundeinstellungen vorzunehmen. Diese Komponente kann direkt integriert werden.
SDRAM:
Bei der Verwendung eines externen SDRAMs ist die Größe des RAMs sowie dessen Timing-Parameter einzustellen. In der Regel reicht die Einstellung Custom und eine Veränderung der Datenbreite um den SDRAM anzupassen. Genaue ANgaben sind dem jeweiligen Datenblatt des verwendeten SDRAMs zu entnehmen.
Interval Timer:
Ein Interval Timer wird benötigt um genaue zeitabhängige Berechnungen mit dem Controller durchzuführen. Hier können für den Anfang die Grundeinstellungen so übernommen werden.
PIO (Parallel I/O):
Hier werden die externen Schnittstellen für die Anbindung der LEDs und der Drucktaster vorgenommen.
Dafür erstellen wir zuerst ein 4 Bit breites PIO als Eingang mit synchronem Capturing zur steigenden Flanke und einem IRQ flankengesteuert. Anschließend einen 10 Bit breiten Ausgangsport für die roten und einen 8 Bit breiten Ausgangsport für die grünen LEDs. Nach dem Einbinden können die Ports noch beliebig umbenannt werden (z.B. button_pio, ledr_pio und ledg_pio). Anhand dieser Bezeichungen lassen sich die Ports und Komponenten später sehr gut im VHDL-Code wiederfinden sowie ein gezieltes ansprechen aus dem C-Code erst möglich. Achtung: Bei der Verwendung eines Eingansports als Interrupt, muss diesem eine eindeutige Interruptadresse zugeweisen werden. Der SOPC-Builder weist standardmäßig die 0 zu. Das kann zu Konfllikten führen wenn diese Adresse mehrmals auftaucht.
Nun sollte der Aufbau folgendermaßen aussehen:
Werden keine Warnungen im unteren Programmbildschirm aufgezeigt kann über "Generate" die CPU erzeugt werden. Nach erfolgreicher Erstellung findet man nun im Projektordner mehrere Dateien mit dem oben gewählten Namen (hier cpu.xxx)
Einbinden in die Top-Level-Entity:
Um nun die erstellte CPU (die erstellten Ein- bzw. Ausgänge der CPU-Entity könnt ihr einfach aus der vom SOPC-Builder erzeugten Datei cpu.vhd entnehmen) in das Top-Level-Design einzubinden wird eine neue Komponente nach z.B. folgendem Schema erstellt:
- Code: Alles auswählen
component cpu
port(
clk : IN STD_LOGIC;
clk_50 : IN STD_LOGIC;
reset_n : IN STD_LOGIC;
-- the SDRAM pio
zs_addr_from_the_sdram : OUT STD_LOGIC_VECTOR (11 DOWNTO 0);
zs_ba_from_the_sdram : OUT STD_LOGIC_VECTOR ( 1 DOWNTO 0);
zs_cas_n_from_the_sdram : OUT STD_LOGIC;
zs_cke_from_the_sdram : OUT STD_LOGIC;
zs_cs_n_from_the_sdram : OUT STD_LOGIC;
zs_dq_to_and_from_the_sdram : INOUT STD_LOGIC_VECTOR (15 DOWNTO 0);
zs_dqm_from_the_sdram : OUT STD_LOGIC_VECTOR ( 1 DOWNTO 0);
zs_ras_n_from_the_sdram : OUT STD_LOGIC;
zs_we_n_from_the_sdram : OUT STD_LOGIC;
-- the_button_pio
in_port_to_the_button_pio : IN STD_LOGIC_VECTOR (3 DOWNTO 0);
-- the_led_pio
out_port_from_the_ledg_pio : OUT STD_LOGIC_VECTOR (7 DOWNTO 0);
out_port_from_the_ledr_pio : OUT STD_LOGIC_VECTOR (9 DOWNTO 0)
);
end component;
Anschließend müssen Signale definiert werden mit denen die Ports verbunden werden sollen:
- Code: Alles auswählen
signal sig_reset : std_logic:='1';
signal sig_clk_50 : std_logic;
signal sig_clk_100 : std_logic;
signal sig_ledg : std_logic_vector(7 downto 0);
signal sig_ledr : std_logic_vector(0 to 9);
(Die Signale sig_ledg und sig_legdr sollen zur Ansteuerung von LEDs dienen. Diese können jedoch auch genau wie die Ports zum externen SDRAM direkt auf die FPGA-Pins geführt werden)
Anschließend wird die CPU über eine Port-Map mit dem restlichen Design verknüpft:
- Code: Alles auswählen
C0:cpu
port map(clk => sig_clk_100,
clk_50 => sig_clk_50,
reset_n => sig_reset,
-------------------------------------------------------------------
zs_addr_from_the_sdram => dram_addr,
zs_ba_from_the_sdram(1) => dram_ba1,
zs_ba_from_the_sdram(0) => dram_ba0,
zs_cas_n_from_the_sdram => dram_cas,
zs_cke_from_the_sdram => dram_cke,
zs_cs_n_from_the_sdram => dram_cs,
zs_dq_to_and_from_the_sdram => dram_dq,
zs_dqm_from_the_sdram(1) => dram_udqm,
zs_dqm_from_the_sdram(0) => dram_ldqm,
zs_ras_n_from_the_sdram => dram_ras,
zs_we_n_from_the_sdram => dram_we,
-------------------------------------------------------------------
in_port_to_the_button_pio(0) => key0,
in_port_to_the_button_pio(1) => key1,
in_port_to_the_button_pio(2) => key2,
in_port_to_the_button_pio(3) => key3,
-------------------------------------------------------------------
out_port_from_the_ledg_pio => sig_ledg,
out_port_from_the_ledr_pio => sig_ledr
);
Nun noch das gesamte Design synthetisieren und in das FPGA laden. Anschließend kann die CPU via NIOS II IDE Umgebung bequem in C/C++ programmiert werden. Eine einfache Beschreibung für die Ansteuerung der LEDs über die NIOS2-CPU aus einem C-Programm findet ihr unter http://www.fpga-talk.de/forum/viewtopic.php?f=9&t=17.
Gruß Tobias

