======= 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.
[[https://www.xilinx.com/products/design-tools/microblaze.html|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 [[https://www.xilinx.com/products/intellectual-property/block_ram.html|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:
-vytvořit potřebné bloky na obvodu FPGA a namapovat piny FPGA na periferie na vývojové desce (**Vivado**)
-vygenerovat data do BRAM paměti (**Matlab**)
-implementovat lwIP stack, vč. čtení z BRAM, plnění bufferu a odesílání (**SDK Eclipse** - C/C++ kód)
-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.
{{ :2017:lwip_stack:genesys2.png?300 |}}
**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).
{{ :2017:lwip_stack:impelemntace_mpoa(1).png?700 |}}
**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 = ð_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 [[https://www.xilinx.com/products/intellectual-property/axi_bram_if_ctlr.html|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:
{{ :2017:lwip_stack:bsp_lwip.png?800 |}}
**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.
{{ :2017:lwip_stack:wireshark_output.png?1000 |}}
**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======
{{youtube>BZRjpwF6AA8?medium}}
------
====== 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í ======
{{ :2017:lwip_stack:lwip_mpoa.zip |Zdrojové kódy pro Microblaze}}
------
====== 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