Individální projekty MPOA

Mikroprocesory s architekturou ARM

Uživatelské nástroje

Nástroje pro tento web


2017:microblaze-lwip

Stack lwIP pro Microblaze

Tomáš Svoboda

Zadání

Na 32bitový softprocesor Microblaze v FPGA implementuje lwIP stack. Vyřešte připojení k obvodu fyzické vrstvy Ethernetu Realtek RTL8211E-VLs a jeho konfiguraci. Ověřte úspěšnou implementaci pomocí přenosu dat přes UDP stream.


Úvod

Cílem tohoto projektu je implementovat lwIP stack na 32bitový soft procesor Microblaze. lwIP stack je volně dostupný TCP/IP stack, který v sobě zahrnuje podporu mnoha protokolů - IP (jak IPv4, tak IPv6), TCP, UDP, apod., tak i klientů jako DHCP či DNS, volitelně pak HTTP či TFTP server. V rámci tohoto projektu je pozornost zaměřena především na protokol UDP. Samotný lwIP stack vyžaduje minimální nároky, mj. je dostupná i light verze, otázka potřebné paměti je dána velikostí odesílacího, resp. přijímacího bufferu.

Microblaze je 32bitový soft procesors RISC instrukční sadou, dostupný jako IP jádro pro FPGA Xilinx (vč. rodiny Zynq, která má i hardwarový ARM procesor). Je vytvořen za použití prostředků samotné struktury FPGA. Z hlediska konfigurace jsou možnosti poměrně bohaté - velikosti instrukční a datové cache, lokální paměť, násobičky, děličky, breakpointy, apod. Pro potřeby tohoto projektu není většina bloků využita - především pro úsporu samotných FPGA prostředků.

V rámci řešení se předpokládá odesílání dat z paměti typu BRAM do počítače skrze protokol UDP. V počítači pak otestovat příjem pomocí programu Wireshark nebo zápis do *.txt/*.csv souboru za využití skriptovacího jazyka Python. Data uložená v paměti BRAM budou představovat fiktivní data, která budou v pozdějším využití (mimo tento projekt) nahrazena skutečnými vzorky získanými z A/D převodníku.

V rámci řešení je tedy nutno:

  1. vytvořit potřebné bloky na obvodu FPGA a namapovat piny FPGA na periferie na vývojové desce (Vivado)
  2. vygenerovat data do BRAM paměti (Matlab)
  3. implementovat lwIP stack, vč. čtení z BRAM, plnění bufferu a odesílání (SDK Eclipse - C/C++ kód)
  4. vytvořit skript pro čtení dat (Python)

Použitý HW

Pro vývoj a testování je použita vývojová deska Genesys 2 - obrázek 1 (výrobce Digilent). Kromě samotného obvodu FPGA, kde bude vytvořen Microblaze procesor, obsahuje ještě další periferie. Pro účely tohoto projektu především

  • USB/JTAG programátor
  • USB/UART převodník
  • 1Gbit obvod fyzické vrstvy Ethernetu (RGMII rozhraní) + konektor s magnetikou
  • 2 DDR3 paměťové obvody (celková šířka 32 bitů, 1800Mbps)
  • Flash paměť

Dále se jedná například o HDMI, VGA, HID USB, OLED displej, tlačítka, přepínače, LED nebo GPIO PMOD header.

Obr.1 Použitá vývojová deska Genesys 2 s FPGA rodiny Kintex 7


Realizace

Na obrázku 2 je znázorněna realizace. Základ tvoří samotná vývojová deska osazená obvodem FPGA. Na obvodu ja realizován samotný procesor, Bloková RAM (BRAM), patřičné bloky pro přístup k paměti a MAC jádro (Medium Access Controller). V rámci procesoru jsou pak mj. využity především knihovny pro samotný lwIP stack, SPI komunikaci a UART, případně obsluha GPIO. Z externí obvodů dostupných na desce je využita DDR paměť (vyžaduje lwIP stack), převodník USB/UART - využito pro ladící výstupy, obvod fyzické vrstvy Ethernetu (rozhraní RGMII) a Flash paměť. Ve Flash paměti je uložena jedinečná MAC adresy v OTP registru, paměť používá SPI rozhraní. Posledním článkem je počítač - zde je spuštěn Python skript, který přijímá jednotlivé UDP datagramny, separuje jednotlivé vzorky a ukládá je do souboru typu CSV (data oddělená čárkou).

Obr.2 Blokové znázornění realizace

Programování samotného procesoru probíhá v jazyce C (lze i C++) v prostředí SDK, není tak nutno zasahovat do popisu ve VHDL. V rámci VHDL je vytvořen pouze tzv, HDL wrapper -„black box“, který má patřičné vstupy a výstupy.

Generovaná data pro paměť

Následujícím kódem je pomocí Matlabu vygenerováno celkem 200 000 vzorků data, data představují v tomto případě sinusovku (v absolutní hodnotě). Šířka dat je 32 bitů, což odpovídá nastavení šířce paměti BRAM (taktéž 32 bitů). Parametr radix na prvním řádku udává v jakém vyjádření data jsou, v tomto případě se jedná o desítkovou soustavu. Formát souboru je typu *.coe, jeho struktura je dána. Primárně slouží pro uložení koeficientů při realizaci digitálních filtrů, nicméně ho lze použít i pro tyto účely. Takto vytvořený soubor lze již přímo použít v rámci vývojového prostředí Vivado.

header1 = 'memory_initialization_radix=10;';
header2 = 'memory_initialization_vector='
fid=fopen('sinsamples_v3.coe','w');
fprintf(fid, [ header1 '\r\n' header2 '\r\n']);
for i = 1:199999
    y=round(abs((2^32-1)*sin(2*pi*1/200000*i)));
    fprintf(fid, [num2str(y) ',\r\n']);
end
fprintf(fid, [num2str(2^32-1) ';']);
fclose(fid);

Čtení MAC adresy a inicializace síťového rozhraní

Získání unikátní MAC adresy

Pro MAC adresu je vytvořena struktura, ve které jsou jednotlivé oktety uloženy jako proměnná typu uint8 (=u8). Jedná se o globální proměnou. V rámci funkce read_MAC() je pak skrze funkci SpiReadOTP() MAC adresa přečtena - parametrem je patřičná komponenta (sFlashSpi), adresa OTP registru a proměnná. Funkce SpiReadOTP() vrací MAC adresu jako strukturu char, proto přetypování.

typedef struct
	{
	    u8 MAC_oct[6];
	}   MAC_adress;
 
MAC_adress mac = {};
.
.
.
void read_MAC(void)
{
	//Initialize Flash Quad SPI Controller
	Status = init_qspi(&sFlashSpi);
	if(Status != XST_SUCCESS)
	{
	   xil_printf("ERR init QSPI\n\r");
	}
	else
	{
	   xil_printf("QSPI init OK\n\r");
	}
	//Read MAC from OTP
	Status = SpiReadOTP(&sFlashSpi, MAC_OTP_ADDR, (u8*)&mac, sizeof(mac));
	if (XST_SUCCESS != Status)
	{
	   xil_printf("Err reading MAC\r\n");
	}
	else
	{
	   xil_printf("MAC from Flash OTP: %02x-%02x-%02x-%02x-%02x-%02x\r\n", mac.MAC_oct[0],mac.MAC_oct[1],
			       mac.MAC_oct[2],mac.MAC_oct[3],mac.MAC_oct[4],mac.MAC_oct[5]);
	}
}

Inicializace rozhraní

V rámci funkce eth_init() je vyčtena MAC adresa, nastaveny patřičné IP adresy (pokud se nevyužije DHCP), inicializován lwIP stack, přiřazení rozhraní pro obsluhu MAC jádrem (obstarává FPGA), zprovozněno patřičné rozhraní, a konečně, (pokud je využit DHCP) získána IP adresa desky, maska a adresa síťového rozhraní (gateway). V případě DHCP se pokouší získat IP adresa, pokud je po vypršení času adresa nulová (=adresu se nepodařilo získat) je nastavena adresa defaultní. Funkce pro DHCP jsou překládány pouze v případě, že je DHCP využit (podmíněný překlad).

void eth_init(void)
{
	read_MAC();
	udp_netif = &eth_netif;
	#if LWIP_DHCP==1					//DHCP on
		ipaddr.addr = 0;
		gw.addr = 0;
		netmask.addr = 0;
	#else							//DHCP OFF - default setting will be used
		/* initliaze IP addresses to be used */
		IP4_ADDR(&ipaddr,  192, 168,   0, 108);
		...
	#endif
	IP4_ADDR(&pc_ipaddr,      192, 168,   0,  102);
	lwip_init();
 
	/* Add network interface */
	if (!xemac_add(udp_netif, &ipaddr, &netmask,&gw, (unsigned char *)&mac,PLATFORM_EMAC_BASEADDR))
	{
		xil_printf("Err adding interface\n\r");
	}
	netif_set_default(udp_netif);
	/* set up udp_netif network */
	netif_set_up(udp_netif);
 
#if (LWIP_DHCP==1)
	// Create a new DHCP client
 
	dhcp_start(udp_netif);
	dhcp_timoutcntr = 24;
 
	while(((udp_netif->ip_addr.addr) == 0) && (dhcp_timoutcntr > 0))
		xemacif_input(udp_netif);
 
	if (dhcp_timoutcntr <= 0)
	{
		if ((udp_netif->ip_addr.addr) == 0)
		{
			xil_printf("DHCP Timeout\r\n");
			xil_printf("Default IP of 192.168.0.108\r\n");
			IP4_ADDR(&(udp_netif->ip_addr),  192, 168,  0, 108);
			...
		}
	 }
	 ipaddr.addr = udp_netif->ip_addr.addr;
	 ...
#endif
 
}

Čtení z paměti, plnění bufferu a odesílání dat

Pro odeslání dat je vytvořena funkce udp_transfer(), ta je aktuálně volána z hlavní smyčky ve funkci main po stisku tlačítka. Funkce je blokující, tzn. že se vrací zpět to funkce main po odeslání všech dat z paměti. V cílové aplikaci bude ve funkci main pouze pouze konfigurace A/D převodníku (bez jeho další obsluhy) a zasílání dat, v tomto případě není třeba vykonávat po dobu odesílání jiné instrukce. Přístup do BRAM zajišťuje AXI BRAM Controller.

void udp_transfer(void)
{
	XGpio_DiscreteWrite(&sGpio, LED_CHANNEL, 0b11110000);
	u32 buffer_send[256]={0};
 
	struct udp_pcb *pcb;
	err_t err;
	unsigned port = TCP_PORT;
 
	struct pbuf *p;
	pcb = udp_new();
	pcb->ttl = UDP_TTL;
	if (!pcb)
	{
	    xil_printf("Err creating PCB\n\r");
	}
	err = udp_bind(pcb, IP_ADDR_ANY, port);
	if (err != ERR_OK)
	{
		xil_printf("Err bind to port %d: err = %d\n\r", port, err);
	}
 
	err = udp_connect(pcb, IP_ADDR_BROADCAST, port);
	p = pbuf_alloc(PBUF_TRANSPORT,sizeof(buffer_send),PBUF_RAM);
 
	u32 data;
	u16 no_sample=0;
	u32 ptr=0;
	while(ptr<=SAMPLES)
	{
		data = XIo_In32(XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR+4U+4U*ptr);// 
		 //xil_printf("\tdata from BRAM : %x\r\n",data);
		buffer_send[no_sample]=data;
	        no_sample++;
		ptr=ptr+1;
		if((no_sample==SAMPLES_IN_PACKET)||(ptr==SAMPLES))
		{
		       //xil_printf("Packet\n\r");
			memcpy (p->payload, buffer_send, sizeof(buffer_send));
			udp_sendto(pcb, p,&pc_ipaddr,port);
			pbuf_free(p);
			p = pbuf_alloc(PBUF_TRANSPORT,sizeof(buffer_send),PBUF_RAM);
			no_sample=0;
			xemacif_input(udp_netif);
	    }
	}
	udp_disconnect(pcb);
	XGpio_DiscreteWrite(&sGpio, LED_CHANNEL, 0b00001111);
}

Data z BRAM jsou při odesílání postupně rozdělena do bloků velikosti 1024 Bytů, pokud má slovo v paměti šířku 32 bitů, je tedy naráz odesláno 256 vzorků. Teoreticky lze však může být blok velký až 65 507 Bytů nebo i větší při použití JumboFramu (lwIP jej podporuje). Jelikož je UDP nespolehlivý protokol, je třeba volit kompromis mezi přijatelnou ztrátou a zbytečnou režií při malém užitečném obsahu v rámci.

Parametr XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR udává první dostupnou přístupnou adresu. Při ladění však bylo zjištěno, že první vzorek je až na druhé pozici v paměti, proto za zaveden „offset“. Funkce postupně ukládá do bufferu buffer_send jednotlivé „vzorky“ z paměti, v okamžiku dosažení nastavených počtu vzorků v jednom paketu (SAMPLES_IN_PACKET) je alokován buffer lwipu stacku a data jsou předána k odeslání. V případě, že poslední paket není zcela naplněn, zůstávají na posledních pozicích předešlá data, to je řešeno už snadno v aplikaci na PC.

Oproti klasické aplikaci, kde jsou data odesílána kontinuálně (například konvertor) nebo je známa pozice posledního uloženého vzorku ve formě ukazatele, je v této aplikaci odeslána pevný počet vzorků. V cílové aplikaci pak bude ukazatel na poslední uložený vzorek znám.

Počet vzorků k odeslání, resp. počet vzorků na 1 paket lze měnit pomocí direktiv:

#define SAMPLES				200000			// number of samples in BRAM
 
#define SAMPLES_IN_PACKET		256			//number samples per one packet

Příklad možných nastavení v rámci BSP:

Obr. 3 Možná nastavení lwIP stacku v rámci BSP

Příjem a zápis dat

Funkčnost zasílání dat přes protokol UDP byla ověřena jak aplikací Wireshark, tak i pomocí skriptu napsaném v jazyce Python.

Na obrázku 4 jsou zobrazeny detailní informace o prvním přijatém paketu (v prvním paketu lze najít k porovnání očekávaná data). Konkrétně:

  • Délka rámce 1066 B, z toho délka UDP datagramu 1032 B, a z toho užitečných dat 1024 B. Hodnota 1024 B správně představuje celkem 256 32bitových vzorků.
  • Adresa TCP portu 50000
  • MAC adresa totožná s adresou získanou z OTP registru (80:0c:1f:00:f4:ad)
  • IP adresa zdroje 192.168.0.103, cíle 192.168.0.102
  • první data tvoří skutečně první vzorky uložené v paměti (134930, …) - Little endian.

Obr. 4 Zachycené pakety programem Wireshark

Pro Python skript je použito prostředí Spyder (dnes Anaconda). Jedná se o skriptovací jazyk, není zde standardní kompilace, tak, jak je tomu například u C++ nebo C#. K vykonávání programu je třeba tzv. interpret. Existuje i možnost jak vytvořit spustitelný soubor, stejně tak je možné vytvořit patřičné GUI. Využita je knihovna Socket (podporuje UDP i TCP). Skript čte ve smyčce přijímací buffer. Funkce socket vrací data jako tzv. bytearray, je tedy nutno vždy seskupit 4 B pro 32bitovou proměnou integer. Data jsou ukládána do CSV souboru společně s „pořadovým“ číslem vzorku - lze nahradit například časem. Skript končí po vypršení timeoutu, kdy již nejsou zasílána další data.

import socket
import csv
import struct
 
 
host="0.0.0.0"
port = 50000
samples = 200000
 
csvf = 'test.csv'
 
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.bind((host,port))
 
addr = (host,port)
buf=10000000
array=[]
 
sample=1;
with open(csvf, 'w', newline='', encoding='ascii') as csv_handle:
    csv_writer = csv.writer(csv_handle, delimiter=',')
    data,addr = s.recvfrom(buf)
    try:
        while(data):
            length=(len(data))
            x=0
            for i in range(0,length//4):
              value=struct.unpack("I", bytearray(data[x:x+4]))
              array.append(value)
              data_list=[x for xs in array for x in xs]
              data_list.append(sample)
              sample=sample+1
              array=[]
              csv_writer.writerow(data_list)
              x=x+4
              s.settimeout(5)
              if (sample==samples+1):
                  break;
            data,addr = s.recvfrom(buf)
    except (socket.timeout,KeyboardInterrupt, SystemExit):
        print ("Operation completed")
        raise

Demonstrace funkčnosti


Závěr

V rámci tohoto projektu byl úspěšně implementován lwIP stack na softprocesor Microblaze, konkrétně protokol UDP v režimu RAW (bez RTOS) a DHCP klient pro získání IP adresy. Implementace byla testována zasláním několika tisíc vzorků do počítače, s 256 vzorky v jednom paketu. Přijatá data byla analyzována programem Wireshark a ukládána do CSV souboru skrze vytvořený skript v jazyce Python.

Způsob práce v procesorem Microblaze se výrazně neliší od práce s procesory s architekturou ARM, stejně tak samotný lwIP stack je nezávislý na použité platformě - liší se pouze ve způsobu práce s MAC vrstvou, přerušeními, apod. Xilinx už knihovny lwIP dodává v rámci BSP balíčků.

Byť se v rámci internetu nepodařilo nalézt vhodné inspirativní kódy s využitím TCP nebo UDP, ukázalo se použití lwIP stacku, po seznámení s jeho knihovnami, jako poměrně jednoduché. Po stránce hardwarové nakonec nebylo zapotřebí řešit speciální konfiguraci obvodu fyzické vrstvy, pokud se spokojíme s rychlostí 100Mbit (nutno nastavit v rámci konfigurace BSP).

V rámci další práce se počítá s využitím RTOS (Free RTOS) a implementací protokolu TCP namísto protokolu UDP, případně zvýšení rychlosti na 1Gbit, bude-li to třeba. Počítáno je rovněž s rozšířením aplikace v Pythonu - větší možnosti konfigurace a GUI.

Použitá vývojová prostředí: Spyder (Python), Vivado, SDK Eclipse (C), Matlab

Zdrojové kódy ke stažení

Reference

[1] lwIP Stack dokumentace: http://www.nongnu.org/lwip/2_0_x/group__udp__raw.html

[2] UDP komunikace v Pythonu: https://wiki.python.org/moin/UdpCommunication

[3] COE File Syntax: https://www.xilinx.com/support/documentation/sw_manuals/xilinx11/cgn_r_coe_file_syntax.htm

[4] Dokumentace a example kódy dostupné skrze SDK

2017/microblaze-lwip.txt · Poslední úprava: 2018/01/14 20:43 autor: Tomáš Svoboda