Skocz do zawartości

FPGA (własne programy #7): Mikrokontroler z rdzeniem PicoBlaze


Elvis

Pomocna odpowiedź

Użyty w tytule mikrokontroler jest pewnie trochę na wyrost, ale to co chciałem opisać to próba dodania własnych peryferiów do rdzenia PicoBlaze. Układy FPGA świetnie sprawdzają się w zastosowaniach krytycznych ze względu na czas. Natomiast implementacja maszyny stanów i rozbudowa programu pochłania potworne ilości cennych zasobów. Można więc połączyć dwa światy: w FPGA zaimplementować peryferia, a program uruchmić na procesorze.

Mój przykładowy projekt obsługuje w tej chwili:

• 6 przycisków

• 8 mikroswitchy

• 8 diod LED

• 3 wyświetlacze 7-segmentowe

• monitor VGA

• procesor PicoBlaze z programem 1024 instrukcji

Całość wykorzystuje 28% zasobów układu Spartan3, więc jeszcze sporo możliwości rozszerzeń.

Program miał działać inaczej, ale nie dogadałem się z kompilatorem C, a na pisanie w asemblerze PZ w tej chwili nie mam siły. Więc zamiast odbijającej się piłeczki jest takie coś (ale wystarczy napisać program i powinno zadziałać):

Jak już nieraz wspominałem dopiero się uczę FPGA, więc proszę o wyrozumiałość oraz ew. rady i porady jak udoskonalić projekt.

Procesor PicoBlaze

O implementacji soft-procesora pisaliśmy ostatnio całkiem sporo: https://www.forbot.pl/forum/topics51/picoblaze-dla-spartan-3-xilinx-8-bit-microcontroller-linki-vt15016.htm

W internecie znajdziemy dużo informacji o tym procesorze, dostępny jest asembler, symulator, a nawet bardzo kiepski kompilator C (tego akurat nie polecam, chociaż używam).

Program procesora składa się z maksymalnie 1024 słów przechowywanych w pamięci blockRAM. Moduł procesora ma nazwę kcpsm3.vhd, jego kod źródłowy znajdziemy w załączniku.

Programowanie procesora

Kod programu ma postać wartości początkowych dla zawartości pamięci blockRAM. Przykładowy program wygląda następująco (fragment pliku, całość w załączniku prog_1.vhd):

attribute INIT_00 of ram_1024_x_18  : label is "0300F320030082021200F210020081011100F100010000004003000440020F3F";
attribute INIT_01 of ram_1024_x_18  : label is "4602402B8901E9FF1970581F4578581B45029680957008010702060405040400";
attribute INIT_02 of ram_1024_x_18  : label is "78000400585D440A8401C8060801C605C504402B8701E7FF1780582746785823";
attribute INIT_03 of ram_1024_x_18  : label is "F810880178104041583D48097810F8000800404DF80088017800403758334809";
attribute INIT_04 of ram_1024_x_18  : label is "138008061830F8200800404DF82088017820404B584748097820F8100800404D";
attribute INIT_05 of ram_1024_x_18  : label is "C8010A750830C8037820C8027810C8017800C800183003014055505448001830";
attribute INIT_06 of ram_1024_x_18  : label is "00000000000000000000000000000000A0004015405F50664B00DBA01B80EA00";
attribute INIT_07 of ram_1024_x_18  : label is "0000000000000000000000000000000000000000000000000000000000000000";
attribute INIT_08 of ram_1024_x_18  : label is "0000000000000000000000000000000000000000000000000000000000000000";

Oznacza to niestety, że po każdej zmianie programu trzeba od nowa zsyntetyzować układ. Można napisać bootloader, ale na razie go nie ma. Oczywiście ręczne tworzenie takiego pliku byłoby bardzo niewygodne. Na szczęscie dostępny jest asembler, który tworzy powyższy plik na podstawie nieco łatwiejszej do zrozumienia składni. Przykładowo:

;--------------------------------------------------------
; File Created by SDCC : free open source ANSI-C Compiler
; Version 3.0.1 #6227 (Jan  1 2018) (Linux)
; This file was generated Fri Jan  5 18:20:24 2018
;--------------------------------------------------------
;--------------------------------------------------------
; global & static initialisations
;--------------------------------------------------------

       LOAD    sF, 3f
       JUMP    __sdcc_program_startup
;--------------------------------------------------------
; Home
;--------------------------------------------------------
__sdcc_program_startup:
       CALL    _main
;       return from main will lock up
__sdcc_loop:
       JUMP    __sdcc_loop
;--------------------------------------------------------
; code
;--------------------------------------------------------
       ;       prog_1.c:15: void main()
_main:
       ;       prog_1.c:18: unsigned char value[3] = {0};
       LOAD    s0, 00
       LOAD    s1, 00
       STORE   s1, (s0)
       LOAD    s1, s0
       ADD     s1, 01
       LOAD    s2, 00
       STORE   s2, (s1)
       LOAD    s2, s0

Jak widać po komentarzu, ten program nie został napisany ręcznie, ale jest wynikiem działania kompilatora języka C. Nie będę się jednak o tym rozpisywać ponieważ ten "kompilator" działa tak marnie, że konieczne będzie napisanie programu od nowa w asemblerze.

Programowanie przebiega więc następująco. Piszemy kod w asemblerze, następnie kompilujemy go do kodu VHDL i dodajemy do projektu. Całość syntetyzujemy i program działa na soft-procesorze w układzie FPGA.

Komunikacja między procesorem a peryferiami

PicoBlaze ma interfejs (port) zdefiniowany następująco:

entity kcpsm3 is
   Port (      address : out std_logic_vector(9 downto 0);
           instruction : in std_logic_vector(17 downto 0);
               port_id : out std_logic_vector(7 downto 0);
          write_strobe : out std_logic;
              out_port : out std_logic_vector(7 downto 0);
           read_strobe : out std_logic;
               in_port : in std_logic_vector(7 downto 0);
             interrupt : in std_logic;
         interrupt_ack : out std_logic;
                 reset : in std_logic;
                   clk : in std_logic);
   end kcpsm3;

Do komunikacji między procesorem a pozostałymi modułami FPGA służą sygnały (pomijając przerwania):

• write_strobe - informuje o wykonaniu instrukcji OUTPUT

• read_strobe - instrukcja INPUT

• in_port - sygnały przekazujące dane do instrukcji INPUT

• out_port - sygnały przekazujące dane z instrukcji OUTPUT

• port_id - numer użytego portu

Znacznie łatwiej jest zrozumieć działanie analizując przykład. Załóżmy że w kod programu wygląda następująco:

        LOAD    s8, ff
       OUTPUT  s8, 00

Ponieważ użyta jest instrukcja OUTPUT, pojawi się impuls na linii write_strobe. Numer użytego portu to 0, więc linie port_id będą wyzerowane. Przekazywana wartość to 0xff i znajdziemy ją na liniach out_port.

Instrukcja INPUT działa bardzo podobnie, poza tym, że dane są dostępne w następnym cyklu zegarowym i użyte są inne linie.

Dzięki temu, że porty mają swoje numery (od 0 do 255), każdemu możemy przypisać odpowiednią funkcję. Zdefiniowałem moduł cpu_defs.vhd, gdzie zapisane są funckcje przypisane odpowiednim portom:

library IEEE;
use IEEE.STD_LOGIC_1164.all;

package cpu_defs is
type input_port is (IN_DIP_SWITCH, IN_SWITCH, IN_UNKNOWN);
type output_port is (OUT_LEDS, OUT_SEG0, OUT_SEG1, OUT_SEG2, OUT_PIX_X, OUT_PIX_Y, OUT_PIX, OUT_UNKNOWN);

function get_input(id : std_logic_vector(7 downto 0)) return input_port;
function get_output(id : std_logic_vector(7 downto 0)) return output_port;
end cpu_defs;

package body cpu_defs is

function get_input(id : std_logic_vector(7 downto 0)) return input_port is
begin
case id is
	when "00000001"		=> return IN_DIP_SWITCH;
	when "00000010"		=> return IN_SWITCH;
	when others				=> return IN_UNKNOWN;
end case;
end function;

function get_output(id : std_logic_vector(7 downto 0)) return output_port is
begin
case id is
	when "00000000"		=> return OUT_LEDS;
	when "00000001"		=> return OUT_SEG0;
	when "00000010"		=> return OUT_SEG1;
	when "00000011"		=> return OUT_SEG2;
	when "00000100"		=> return OUT_PIX_X;
	when "00000101"		=> return OUT_PIX_Y;
	when "00000110"		=> return OUT_PIX;
	when others				=> return OUT_UNKNOWN;
end case;
end function;

end cpu_defs;

Dla portów INPUT, mamy więc:

• IN_DIP_SWITCH - odczyt stanu dip-switchy

• IN_SWITCH - odczyt stanu przycisków

Dla portów OUTPUT:

• OUT_LEDS - ustawienie stanu diod LED

• OUT_SEG0 - pierwsza cyfra wyświetlacza 7-segmentowego

• OUT_SEG1 - druga cyfra wyświetlacza 7-segmentowego

• OUT_SEG2 - trzecia cyfra wyświetlacza 7-segmentowego

• OUT_PIX_X - współrzędna X piksela

• OUT_PIX_Y - współrzędna Y piksela

• OUT_PIX - zapis 1 zapala, 0 gasi wybrany piksel

(aktualnie piksele są używane nieco inaczej - mamy tylko jeden piksel zapalony, OUT_PIX zmienia jego położenie).

Kod odpowiedzialny za realizację funkcji przypisanych portom jest bardzo krótki:

  process (clk_pll)
   variable pix_x : integer range 0 to 127 := 0;
   variable pix_y : integer range 0 to 127 := 0;
 begin
   if rising_edge(clk_pll) then
	if read_strobe = '1' then
		case get_input(port_id) is
			when IN_DIP_SWITCH => 
				in_port <= dpswitch;
			when IN_SWITCH => 
				in_port <= "00" & switch;
			when others => null;
			end case;
	end if;
	if write_strobe = '1' then
		case get_output(port_id) is
			when OUT_LEDS => 
				led <= out_port;
			when OUT_SEG0 =>
				disp_value(3 downto 0) <= out_port(3 downto 0);
			when OUT_SEG1 =>
				disp_value(7 downto 4) <= out_port(3 downto 0);
			when OUT_SEG2 =>
				disp_value(11 downto 8) <= out_port(3 downto 0);
			when OUT_PIX_X =>
				pix_x := to_integer(unsigned(out_port));
			when OUT_PIX_Y =>
				pix_y := to_integer(unsigned(out_port));
			when OUT_PIX =>
				ball_x <= pix_x;
				ball_y <= pix_y;
			when others => null;
		end case;		
	end if;
   end if;
 end process;

Obsługa switchy i diod LED jest trywialna, po prostu sterowane są odpowiednie porty układu:

entity pr_2 is port( 
clk : in std_logic;
led : out std_logic_vector(7 downto 0);
dpswitch : in std_logic_vector(7 downto 0);
switch : in std_logic_vector(5 downto 0);
sevensegment : out std_logic_vector(7 downto 0);
enable : out std_logic_vector(2 downto 0);
  hsync : out std_logic;
  vsync : out std_logic;
  red : out std_logic_vector(2 downto 0);
  green : out std_logic_vector(2 downto 0);
  blue : out std_logic_vector(2 downto 1));
end pr_2;

Czyli wykonując instrukcję OUTPUT do portu 0 bezpośrednio zapiszemy stan linii "led". Odczyty stanu przełączników są równie proste.

Cały plik główny projektu wygląda następująco:

library IEEE;
use IEEE.STD_LOGIC_1164.all;
use IEEE.STD_LOGIC_ARITH.all;
use IEEE.STD_LOGIC_UNSIGNED.all;
use WORK.cpu_defs.all;

entity pr_2 is port( 
clk : in std_logic;
led : out std_logic_vector(7 downto 0);
dpswitch : in std_logic_vector(7 downto 0);
switch : in std_logic_vector(5 downto 0);
sevensegment : out std_logic_vector(7 downto 0);
enable : out std_logic_vector(2 downto 0);
  hsync : out std_logic;
  vsync : out std_logic;
  red : out std_logic_vector(2 downto 0);
  green : out std_logic_vector(2 downto 0);
  blue : out std_logic_vector(2 downto 1));
end pr_2;

architecture Behavioral of pr_2 is

signal address : std_logic_vector(9 downto 0);
signal instruction : std_logic_vector(17 downto 0);
signal port_id : std_logic_vector(7 downto 0);
signal out_port : std_logic_vector(7 downto 0);
signal in_port : std_logic_vector(7 downto 0);
signal write_strobe : std_logic;
signal read_strobe : std_logic;
signal interrupt : std_logic;
signal interrupt_ack : std_logic;
signal disp_value : std_logic_vector(11 downto 0);
signal clk_pll : std_logic;
signal x : integer range 0 to 1023;
signal y : integer range 0 to 1023;
signal ball_x : integer range 0 to 127;
signal ball_y : integer range 0 to 127;
signal pixel_color : std_logic_vector(7 downto 0);

component pll is
  port ( CLKIN_IN        : in    std_logic;
         CLKFX_OUT       : out   std_logic);
end component;

begin
 cpu: entity WORK.kcpsm3 port map(      
			address, instruction, port_id,
           write_strobe, out_port,
           read_strobe, in_port,
           interrupt, interrupt_ack,
           '0', clk_pll);

 pll_clock : pll port map (CLKIN_IN => clk, CLKFX_OUT => clk_pll);
 program_rom: entity WORK.prog_1 port map(      
			address, instruction, clk_pll);
 led_disp: entity segment_driver port map(clk_pll, disp_value, sevensegment, enable);

 sync : entity lcd_sync port map (clk_pll, hsync, vsync, x, y);
 screen: entity lcd_fb port map (clk_pll, x, y, pixel_color, ball_x, ball_y);

 interrupt <= interrupt_ack;

 process (clk_pll)
   variable pix_x : integer range 0 to 127 := 0;
   variable pix_y : integer range 0 to 127 := 0;
 begin
   if rising_edge(clk_pll) then
	if read_strobe = '1' then
		case get_input(port_id) is
			when IN_DIP_SWITCH => 
				in_port <= dpswitch;
			when IN_SWITCH => 
				in_port <= "00" & switch;
			when others => null;
			end case;
	end if;
	if write_strobe = '1' then
		case get_output(port_id) is
			when OUT_LEDS => 
				led <= out_port;
			when OUT_SEG0 =>
				disp_value(3 downto 0) <= out_port(3 downto 0);
			when OUT_SEG1 =>
				disp_value(7 downto 4) <= out_port(3 downto 0);
			when OUT_SEG2 =>
				disp_value(11 downto 8) <= out_port(3 downto 0);
			when OUT_PIX_X =>
				pix_x := to_integer(unsigned(out_port));
			when OUT_PIX_Y =>
				pix_y := to_integer(unsigned(out_port));
			when OUT_PIX =>
				ball_x <= pix_x;
				ball_y <= pix_y;
			when others => null;
		end case;		
	end if;
   end if;
 end process;

red <= pixel_color(7 downto 5);
green <= pixel_color(4 downto 2);
blue <= pixel_color(1 downto 0);

end Behavioral;

Zalety FPGA

Dotychczas nie było widać żadnego sensu zastosowania FPGA. Najmniejszy attiny mógłby z powodzeniem wysterować diody i odczytać stany przełączników.

Pierwszy "moduł" to multipleksowane sterowanie wyświetlaczem 7-segmentowym. Jak wiemy w przypadku mikrokontrolerów jest to proste, ale wymaga przerwania zegarowego i nieco kodowania.

Dzięki użyciu FPGA całe multipleksowanie odbywa się sprzętowo. Procesor po prostu zapisuje do rejestrów OUT_SEG0, OUT_SEG1 i OUT_SEG2 wartości które chce wyświetlić.

Za multipleksowanie odpowiada moduł segment_driver.vhd:

library IEEE;
use IEEE.STD_LOGIC_1164.all;
use IEEE.NUMERIC_STD.all;

entity segment_driver is port(
       clk : in std_logic;
       value : in std_logic_vector(11 downto 0);
       led_out : out std_logic_vector(7 downto 0);
       led_sel : out std_logic_vector(2 downto 0));
end segment_driver;

architecture Behavioral of segment_driver is

constant DIVIDER : natural := 25000;

signal digit : std_logic_vector(3 downto 0);
signal enable : std_logic_vector(2 downto 0);

begin

refresh: process (clk)
variable cnt : natural range 0 to DIVIDER := 0;
begin
if rising_edge(clk) then
	if cnt = DIVIDER-1 then
		cnt := 0;
		case enable is
			when "001" =>
				enable <= "010";
				digit  <= value(7 downto 4);
			when "010" =>
				enable <= "100";
				digit  <= value(11 downto 8);
			when others =>
				enable <= "001";
				digit  <= value(3 downto 0);
		end case;
	else
		cnt := cnt + 1;
	end if;
end if;			 
end process;

with digit 
select led_out <=
	not "11111100" when "0000",
	not "01100000" when "0001",
     not "11011010" when "0010",
     not "11110010" when "0011",
     not "01100110" when "0100",
     not "10110110" when "0101",
     not "10111110" when "0110",
     not "11100000" when "0111",
     not "11111110" when "1000",
     not "11110110" when "1001",
     not "11101110" when "1010",
     not "00111110" when "1011",
     not "10011100" when "1100",
     not "01111010" when "1101",
     not "10011110" when "1110",     
     not "10001110" when others;     

led_sel <= not enable;

end Behavioral;

O ile multipleksowanie wyświetlacza 7-segmentowego jak najbardziej można zrealizować programowo, FPGA nie tylko ułatwia programowanie ale pozwala na użycie modułów które wymagają bardzo precyzyjnych czasów dostępu. Wcześniej opisywałem sterownik VGA, teraz "podłączę" go do tworzonego mikrokontrolera.

Docelowo sterownik VGA będzie współpracował z kolejnym blokiem pamięci, ma mieć rozdzielczość 128x128 w dwóch kolorach (bez szaleństw, ale to w końcu 8-bitowiec). Na razie przygotowałem uproszczoną implementację, gdzie przechowujemy współrzędne "piłeczki" i ją wyświetlamy.

Moduł sterujący sygnałami synchronizacji VGA:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity lcd_sync is port(
clk : in std_logic;
hsync : out std_logic;
vsync : out std_logic;
next_x : inout integer range 0 to 1023;
next_y : inout integer range 0 to 1023);
end lcd_sync;

architecture Behavioral of lcd_sync is

signal x : integer range 0 to 1023;
signal y : integer range 0 to 1023;

begin

process (clk) is
begin
if rising_edge(clk) then
	if (next_x = 799) then
		next_x <= 0;
		if (next_y = 525) then
			next_y <= 0;
		else
			next_y <= next_y + 1;
		end if;
	else
		next_x <= next_x + 1;
	end if;

	if (y >= 490) and (y < 492) then
		vsync <= '0';
	else
		vsync <= '1';
	end if;

	if (x >= 656) and (x < 752) then
		hsync <= '0';
	else
		hsync <= '1';
	end if;

	x <= next_x;
	y <= next_y;
end if;	
end process;


end Behavioral;

Samo generowanie obrazu tzn. rysowanie piłeczki:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity lcd_fb is port(
clk : in std_logic;
x : in integer range 0 to 1023;
y : in integer range 0 to 1023;
color : out std_logic_vector(7 downto 0);
ball_x : in integer range 0 to 127;
ball_y : in integer range 0 to 127);
end lcd_fb;

architecture Behavioral of lcd_fb is
begin

process (clk) is
variable pix_x : integer range 0 to 127;
variable pix_y : integer range 0 to 127;
begin
if rising_edge(clk) then
	if (x >= 64) and (x < 576) and (y < 480) then
		pix_x := (x - 64) / 4;
		pix_y := y / 4;
		if (pix_x = ball_x) and (pix_y = ball_y) then
			color <= x"ff";
		else
			color <= x"00";
		end if;
	else
		color <= x"00";
	end if;
end if;
end process;

end Behavioral;

Podsumowanie

Opisywany projekt daje możliwość poznania niskopoziomowych mechanizmów komunikacji procesora z układami peryferyjnymi. Jest to bardzo prosty układ w porównaniu z dzisiejszymi mikrokontrolerami, ale możemy go łatwo zsyntetyzować i modyfikować nawet w wydawałoby się prostym (i małym) układzie Spartan3.

Mam nadzieję, że ten opis przyda się innym osobom w rozwijaniu zainteresowania układami FPGA. A jak napisałem na wstępie za wszystkie błędy w kodzie z góry przepraszam.

source.zip

  • Lubię! 1
Link do komentarza
Share on other sites

Cześć Elvis,

bardzo fajny projekt i post 🙂

Jutro z rana będę go wypróbowywał. Gratulacje i stawiam piwo.

Pozdrawiam

BTW: program dla PicoBlze można w locie (bez ponownej syntezy 'przeładować' korzystając z interfejsu JTAG, ale to spore koszty - trzeba kupić Programator/Debbuger JTAG dla FPGA Xilinxa.

Link do komentarza
Share on other sites

Można też napisać bootloader i ładować przez uart 🙂 taniej wyjdzie

Ja się nie podejmę (dla mnie to trochę za skomplikowane), ale jeśli Ty napiszesz to z chęcią będę z niego korzystał 😅

Pozdrawiam

Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.