Tomáš Svoboda
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.
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:
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
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
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.
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);
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]); } }
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 }
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
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ě:
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
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
[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