
В предыдущей статье я писал, что у нас появилась новая плата расширения Ethernet 1Gbit и к ней был сделан относительно простой тестовый проект для платы Марсоход3GW2. В этой работе FPGA плата могла принимать UDP пакеты и отправлять UDP пакеты. Этим проектом мы проверили работоспособность нашего шилда Ethernet. Однако, существенный недостаток был в том, что передать пакет в плату можно было только отправляя broadcast широковещательные пакеты. Это пакеты "всем", что создает излишний трафик во всей локальной сети. Дело в том, что компьютер с которого отправляются пакеты не знает физического адреса платы, MAC адреса. Даже если я знаю IP адрес своего устройства этого недостаточно, чтобы осуществлять прямую передачу от клиента к серверу.
Чтобы сделать передачу UDP пакета с компьютера на нашу FPGA плату по всем правилам нужно реализовать ARP протокол, Address Resolution Protocol. Любое устройство в локальной сети имеет в своих драйверах таблицу ARP. В этой таблице хранится соответствие IP адреса его MAC адресу. Когда какая ни будь программа посылает пакет в сеть, стек драйверов ОС прежде всего проверяет в своей таблице известен ли MAC адресата. Если известен, то пакет тут же отправляется на известный MAC адрес. А если MAC неизвестен, то сперва отправляется широковещательный запрос ARP всем устройствам в сети "У кого есть вот такой IP?" и устройство с этим IP адресом должно откликнуться и прислать ARP ответ "Это мой IP, а вот мой MAC адрес". После этого, компьютер, получив ответ уже знает MAC и может действительно послать пакет адрасату. Новый известный MAC адрес помещается в таблицу MAC адресов, и при повторной передаче следующих пакетов ARP протокол уже не используется, ведь физический адрес клиента уже известен.
Сейчас я хочу усовершенствовать наш Ethernet проект для FPGA платы Марсоход3GW2 и добавить поддержку ARP протокола.
Наша плата FPGA теперь будет делать следующее: на неё (конкретно на её статический IP платы) теперь можно будет послать UDP пакет и этот пакет
- зажигает светодиоды платы согласно принятому байту из UDP пакета;
- управляет вращением шагового двигателя с заданной скоростью согласно принятому значению из UDP пакета.
Стенд для испытаний этого проекта показан на фото в начале статьи. На плате расширения Ethernet есть 4 выхода io16, io17, io18 и io19 они идут на драйвер двигателя L298N к которому уже подключен шаговый моторчик.
Далее расскажу чуть подробнее о проекте.
Весь проект реализован на Verilog HDL. Его исходники можно взять на github.
Модуль самого верхнего уровня это gig_e. Вот его код:
// 1G Ethernet rec&send example module gig_e( input clk,key0,key1, input Rx_dv, input [3:0]Rx_D, input Rx_clk, output Tx_clk, output [3:0]Tx_D, output Tx_ena, output PHYRST, output MDC, inout MDIO, output XTAL, output [7:0] led, output wire io16, output wire io17, output wire io18, output wire io19, input [7:0]ADC_D, output ADC_CLK ); assign MDC = 1'b0; assign PHYRST = 1'b1; reg [25:0] cnt; always @(posedge clk) cnt <= cnt + 1'b1 ; /* assign ADC_CLK = cnt[2]; reg [7:0]adc_data; always @(posedge ADC_CLK) adc_data <= ADC_D; */ assign ADC_CLK = 1'b0; assign XTAL = cnt[1]; reg [25:0] R_cnt; always @(posedge Rx_clk) R_cnt <= R_cnt + 1'b1 ; wire clkout; wire clkoutp; Gowin_rPLL inst1( .clkin(Rx_clk), //input clkin .clkout(clkout), //output clkout .clkoutp(clkoutp) //output clkout ); wire r_clk = clkoutp; wire Rx_dv_p,Rx_dv_pp; wire [7:0] rx_ddr_in; Gowin_DDR inst2( .din({Rx_dv,Rx_D}), .clk(r_clk), .q({Rx_dv_p,rx_ddr_in[7:4],Rx_dv_pp,rx_ddr_in[3:0]}) ); wire [10:0]byte_cnt; wire [2:0]pkt_cnt; wire [7:0]rx_crc32_err_cnt; wire rx_crc32_ok; wire [12:0]memwr_addr_; wire [15:0]memwr_data_; wire memwr_; rx_crc32 rx_crc32_inst( .clk(r_clk), .data_valid(Rx_dv_p), .data(rx_ddr_in), .byte_count(byte_cnt), .pkt_head(pkt_cnt), .memwr_addr(memwr_addr_), .memwr_data(memwr_data_), .memwr(memwr_), .crc32_ok(rx_crc32_ok), .err_cnt(rx_crc32_err_cnt) ); wire [8:0]rd_addr; wire [255:0]rd_data; Gowin_DPB dpram_inst( .reseta( 1'b0 ), //input reseta .clka(r_clk), //input clka .douta(), //output [7:0] douta .ocea(1'b1), //input ocea .cea(1'b1), //input cea .wrea(memwr_), //input wrea .ada(memwr_addr_), //input [12:0] ada .dina(memwr_data_), //input [15:0] dina .resetb( 1'b0 ), //input resetb .clkb(r_clk), //input clkb .oceb(1'b1), //input oceb .ceb(1'b1), //input ceb .wreb( 1'b0 ), //input wreb .dinb(256'd0), //input [255:0] dinb .adb(rd_addr), //input [8:0] adb .doutb(rd_data) //output [255:0] doutb ); wire [6:0]ad_i; wire [11:0]dout_o; Gowin_pROM prom_inst( .reset(1'b0), //input reset .clk(r_clk), //input clk .oce(1'b1), //input oce .ce(1'b1), //input ce .ad(ad_i), //input [11:0] ad .dout(dout_o) //output [11:0] dout ); wire [7:0]wr_fifo_data; wire wr_fifo; wire [63:0]udp_payload; pkt_reader pkt_reader_inst( .clk(r_clk), .raddr(rd_addr), .data(rd_data), .head(pkt_cnt), .rom_addr(ad_i), .rom_byte(dout_o), .send_byte(wr_fifo_data), .send(wr_fifo), .broadcast(), .led(), .crc32(), .udp_payload(udp_payload) ); //udp_payload is data received from UDP command assign led[7:0] = udp_payload[7:0]; wire [23:0]motor_step_time; assign motor_step_time = udp_payload[55:32]; assign Tx_clk = clkout; wire Empty_o; wire [7:0]t_data; reg fifo_rd_req = 1'b0; reg t_ena_ = 1'b0; reg t_ena = 1'b0; always @(posedge Tx_clk) begin fifo_rd_req <= !Empty_o; t_ena_ <= fifo_rd_req; t_ena <= t_ena_ & fifo_rd_req; end FIFO_HS_Top fifo_inst( .WrClk(r_clk), //input WrClk .Data(wr_fifo_data), //input [7:0] Data .WrEn(wr_fifo), //input WrEn .RdClk(Tx_clk), //input RdClk .RdEn(fifo_rd_req), //input RdEn .Rnum(), //output [11:0] Rnum .Almost_Empty(), //output Almost_Empty .Almost_Full(), //output Almost_Full .Q(t_data), //output [7:0] Q .Empty(Empty_o), //output Empty .Full() //output Full ); ////////////////////////////////////////////////////////////// ddr_out inst5( .din({t_ena,t_data[7:4],t_ena,t_data[3:0]}), //input [9:0] din .clk(Tx_clk), //input clk .q({Tx_ena,Tx_D}) //output [4:0] q ); ////////////////////////////////////////////////////////////// reg [23:0]mcnt = 24'd0; reg [2:0]mcnt8 = 3'b000; always @(posedge clk) begin if( mcnt==motor_step_time ) mcnt<=0; else mcnt<=mcnt+1; if( motor_step_time==24'd0 ) mcnt8<=24'd0; else if( mcnt==motor_step_time ) mcnt8<=mcnt8+1; end motor m( .clk( clk ), .enable( 1'b1 ), .dir( 1'b0 ), .cnt8( mcnt8 ), .f0( io16 ), .f1( io17 ), .f2( io18 ), .f3( io19 ) ); endmodule
Чтобы лучше объяснить его я создал вот такую блок схему:

Три розовых овала вверху схемы это сигналы которые связывают микросхему Ethernet PHY Realtek 8211E с микросхемой FPGA. Например, сигнал Rx_clk из микросхемы Ethernet PHY задаёт тактовую частоту, которую нужно использовать для приёма данных. Я использую Gowin_rPLL чтобы задать фазовый сдвиг этой частоты для надёжной фиксации приходящих данных на линиях Rx_D[3:0] (это принимаемые данные, четыре бита) и Rx_dv (этот сигнал активен во время приёма пакета и неактивен, когда данных нет). Поскольку используется передача данных и по фронту и по спаду частоты Rx_clk, то нужен модуль DDR (Double Data Rate) приёмника. У меня это модуль типа Gowin_DDR и на его выходе данные имеют уже в два раза большую разрядность 8 бит. Эти принятые байты идут на модуль rx_crc32. Он выделен серым цветом. Все блоки серого цвета это модули написанные нами. А блоки выделенные голубым цветом это модули которые созданы в Gowin IDE IP Core Generator. Это всё модули, для которых Gowin предлагает в FPGA готовые аппаратные блоки, а еще к ним есть Gowin IP Core.
Главная задача модуля rx_crc32 принять все байты пакета и записать их в блочную память Gowin_DPB. Блочная память является двухпортовым ОЗУ 16 килобайт и логически эта память разделена на 8 блоков по 2 килобайта. Один блок памяти 2К хранит один принятый Ethernet пакет. Модуль rx_crc32 хранит трёхбитный указатель head (голова) на текущий принимаемый пакет. Принятые байты записываются внуть блока, и одновременно подсчитывается контрольная сумма CRC32 для принимаемого Ethernet пакета. Контрольная сумма передается в его конце и её можно принять и проверить верная ли она, соответствует ли она посчитанной. Если CRC32 корректная, то модуль rx_crc32 увеличивает указатель head на единицу. Следующий пакет примется в следующий блок памяти. Таким образом, получается циклический буффер на 8 принимаемых пакетов.
Модуль pkt_reader пожалуй самое сложное устройство. Вот его код:
module pkt_reader( input wire clk, input wire [255:0]data, input wire [2:0]head, input wire [11:0]rom_byte, output wire [8:0]raddr, output reg broadcast, output wire [3:0]led, output wire [7:0]send_byte, output wire send, output reg [6:0]rom_addr, output reg [31:0]crc32, output reg [63:0]udp_payload ); `include "crc32.v" localparam MY_MAC = 48'hA6A5A4A3A1A0; localparam MY_IP = 32'h0900080A; // my fixed IP is 10.8.0.9 localparam ETH_TYPE_ARP = 16'h0608; localparam ETH_TYPE_IP = 16'h0008; localparam IP_PROTO_TCP = 8'h06; localparam IP_PROTO_UDP = 8'd17; localparam TCP_SRV_PORT = 16'h5000; //http = 80 localparam UDP_SRV_PORT = 16'h6969; localparam STATE_WAIT_PKT = 0; localparam STATE_CHK_MAC = 1; localparam STATE_CHK_UDP0 = 2; localparam STATE_CHK_UDP = 3; localparam STATE_CHK_ARP0 = 4; localparam STATE_CHK_ARP = 5; localparam STATE_CHK_ARP_IP0 = 6; localparam STATE_CHK_ARP_IP = 7; localparam STATE_ARP_REPLY = 8; localparam STATE_END_PKT_PROC = 9; localparam STATE_END_PKT_PROC1 = 10; //decode packet data bus lines wire [47:0]L0_dst_mac_addr; assign L0_dst_mac_addr = data[111:64]; wire [47:0]L0_src_mac_addr; assign L0_src_mac_addr = data[159:112]; wire [15:0]L0_eth_type; assign L0_eth_type = data[175:160]; wire [ 7:0]L0_arp_opcode; assign L0_arp_opcode = data[239:232]; wire [ 7:0]L0_ip_proto; assign L0_ip_proto = data[255:248]; wire [31:0]L1_arp_req_ip; assign L1_arp_req_ip = data[143:112]; wire [31:0]L1_arp_rem_ip; assign L1_arp_rem_ip = data[63:32]; wire [31:0]L1_udp_src_ip; assign L1_udp_src_ip = data[47:16]; wire [31:0]L1_udp_dst_ip; assign L1_udp_dst_ip = data[79:48]; wire [15:0]L1_udp_src_port; assign L1_udp_src_port = data[95:80]; wire [15:0]L1_udp_dst_port; assign L1_udp_dst_port = data[111:96]; wire [15:0]L1_udp_data_len; assign L1_udp_data_len = data[127:112]; wire [15:0]L1_udp_ch_sum; assign L1_udp_ch_sum = data[143:128]; wire [31:0]L1_udp_payload0; assign L1_udp_payload0 = data[175:144]; wire [31:0]L1_udp_payload1; assign L1_udp_payload1 = data[207:176]; reg [2:0]tail = 0; reg [3:0]state = STATE_WAIT_PKT; always @(posedge clk) begin case(state) STATE_WAIT_PKT: begin if( tail != head ) //just wait receiver head goes forward state <= STATE_CHK_MAC; end STATE_CHK_MAC: begin if( L0_dst_mac_addr==48'hFFFFFFFFFFFF && L0_eth_type==ETH_TYPE_ARP && L0_arp_opcode==8'h01 ) //accept broadcast only for ARP req state <= STATE_CHK_ARP0; else if( L0_dst_mac_addr==MY_MAC && L0_eth_type==ETH_TYPE_IP && L0_ip_proto==IP_PROTO_UDP) //accept my MAC for TCP/IP state <= STATE_CHK_UDP0; else state <= STATE_END_PKT_PROC; //ignore packet because of wrong MAC end STATE_CHK_UDP0: begin state <= STATE_CHK_UDP; end STATE_CHK_UDP: begin //if( L1_udp_dst_ip==MY_IP && L1_udp_dst_port==UDP_SRV_PORT ) //accept UDP packet if true //begin //end state <= STATE_END_PKT_PROC; end STATE_CHK_ARP0: begin state <= STATE_CHK_ARP_IP; end STATE_CHK_ARP_IP: begin if( L1_arp_req_ip==MY_IP ) //accept ARP request for me only state <= STATE_ARP_REPLY; else state <= STATE_END_PKT_PROC; end STATE_ARP_REPLY: begin if( rom_byte_==12'hF02 ) state <= STATE_END_PKT_PROC; end STATE_END_PKT_PROC1: begin state <= STATE_END_PKT_PROC; end STATE_END_PKT_PROC: begin state <= STATE_WAIT_PKT; end endcase; end always @(posedge clk) if( state==STATE_CHK_UDP && L1_udp_dst_ip==MY_IP && L1_udp_dst_port==UDP_SRV_PORT ) begin udp_payload <= { L1_udp_payload1, L1_udp_payload0 }; end always @(posedge clk) if( state==STATE_END_PKT_PROC ) //assume packet processed and need to go forward begin tail <= tail+1; end wire [1:0]pkt_line; assign pkt_line = (state==STATE_WAIT_PKT) ? 0 : (state==STATE_CHK_MAC) ? 1 : 2; assign raddr = { tail, 4'h0, pkt_line }; assign led = { broadcast, head }; always @* broadcast = (data[112:64]==48'hFFFFFFFFFFFF); reg [47:0]remote_mac; reg [31:0]remote_ip; always @(posedge clk) begin if(state==STATE_CHK_MAC) remote_mac <= L0_src_mac_addr; if(state==STATE_CHK_ARP_IP) remote_ip <= L1_arp_rem_ip; end wire sending; assign sending = state==STATE_ARP_REPLY; always @(posedge clk) if(state==STATE_CHK_ARP_IP) rom_addr <= 0; //addr of ARP reply packet in rom else if( sending ) rom_addr <= rom_addr + 1; reg [7:0]sbyte0; reg [7:0]sbyte1; reg [7:0]sbyte2; reg [7:0]sbyte3; reg [7:0]sbyte4; reg [7:0]sbyteF; reg [7:0]sbyte_; reg [11:0]rom_byte_; reg [11:0]rom_byte__; wire [3:0]field_sel; assign field_sel = rom_byte_[11:8]; always @(posedge clk) begin if(state==STATE_WAIT_PKT) begin rom_byte__ <= 12'h000; rom_byte_ <= 12'h000; end else begin rom_byte__ <= rom_byte_; rom_byte_ <= rom_byte; end sbyte0 <= rom_byte[7:0]; sbyte1 <= remote_mac>>(rom_byte[2:0]*8); sbyte2 <= remote_ip >>(rom_byte[1:0]*8); sbyte3 <= MY_MAC>>(rom_byte[2:0]*8); sbyte4 <= MY_IP >>(rom_byte[1:0]*8); sbyteF <= crc32>>(rom_byte[1:0]*8); sbyte_ <= (field_sel==4'h0) ? sbyte0 : (field_sel==4'h1) ? sbyte1 : (field_sel==4'h2) ? sbyte2 : (field_sel==4'h3) ? sbyte3 : (field_sel==4'h4) ? sbyte4 : (field_sel==4'hF) ? sbyteF : 8'h00; end assign send_byte[7:0] = sbyte_; reg [2:0]send_delay; reg send0 = 1'b0; reg send1 = 1'b0; always @(posedge clk) begin send_delay <= { send_delay[1:0], sending }; send0 <= send_delay[2] & (rom_byte_[11:8]!=4'hE); send1 <= send_delay[2] & send_delay[0] & (rom_byte_[11:7]!=5'h1F) & (rom_byte_[11:8]!=4'hE); end assign send = send1; //count number of bytes sent (to skip crc calc for preamble) reg [3:0]sbyte_cnt; always @(posedge clk) if(state==STATE_WAIT_PKT) sbyte_cnt <= 0; else if(send0 && sbyte_cnt<8) sbyte_cnt <= sbyte_cnt + 1; reg [31:0]crc32_; always @(posedge clk) if(send0 && sbyte_cnt==8 ) begin if( rom_byte__[11:8]!=4'hF ) crc32_ <= nextCRC32_D8( { send_byte[0], send_byte[1], send_byte[2], send_byte[3], send_byte[4], send_byte[5], send_byte[6], send_byte[7] } , crc32_ ); end else crc32_ <= 32'hFFFFFFFF; //reverse bits and inverse of CRC32 integer i; always @* begin for ( i=0; i < 32; i=i+1 ) crc32[i] = crc32_[31-i]^1'b1; end endmodule
У этого модуля внутри есть свой указатель tail, хвост. Если голова-head ушла вперед, то модуль легко это обнаружит сравнивая её с хвостом-tail. Внутри есть машина состояний которая в этот момент переходит из состояния ожидания прихода нового пакета STATE_WAIT_PKT в состояние проверки адреса MAC - STATE_CHK_MAC. У нас появился принятый пакет и мы хотим убедиться, что это сообщение вообще предназначено нам. Для этого проверяем MAC адрес.
В состоянии STATE_CHK_MAC мы проверяем сейчас два случая: нас интересует либо широковещательный пакет типа ARP запроса либо пакет точно на наш MAC адрес и точно Ethernet типа IP и протокол UDP. В зависимости от результата этой проверки мы перейдем либо к обработчику ARP запроса STATE_CHK_ARP0, либо к обработчику UDP пакета STATE_CHK_UDP0 либо вообще проигнорируем пакет STATE_END_PKT_PROC.
Обработчик ARP запроса конечно должен отвечать не всегда, ведь запросы широковещательные, может мы приняли запрос, который нас не касается вообще. Поэтому в состоянии STATE_CHK_ARP_IP мы и проверяем, запрос на наш IP адрес или нет. Ответ начнётся только если это запрос к нам.
В начале модуля pkt_reader задаются константы, которые определяют MAC адрес и статический IP адрес нашего устройства в ПЛИС:
localparam MY_MAC = 48'hA6A5A4A3A1A0;
localparam MY_IP = 32'h0900080A; // my fixed IP is 10.8.0.9
Вот проверка полей MAC и IP принятых пакетов идет на эти константы. Вы можете изменить эти константы и задать своему устройству свой IP адрес или MAC.
Если ARP запрос предназначен для нашего устройства, то машина состояний модуля pkt_reader переходит в STATE_ARP_REPLY. Теперь мне нужно отдать передатчику специальный пакет ARP ответ. Проблема в том, что я не могу задать его в коде программы просто массивом констант. Мне нужно сформировать этот пакет на лету используя различные значения MAC и IP источника и приемника. Именно поэтому я приготовил шаблон пакета ARP ответа и поместил его в блочную память Gowin_pROM. В шаблоне пакета слова 12-ти битные. Нужно передать пакет из байт находящихся внутри этой памяти, но некоторые байты нужно на лету подменить. Четыре старших бита в каждом слове из памяти Gowin_pROM как раз определяют, на что заменять этот конкретный байт. Например, если старшие 4 бита равны трём, то сюда нужно вставить байт из моего MAC адреса. Если старшие 4 бита равны 1, то сюда нужно вставить MAC адрес компьютера, который делал запрос и которому мы сейчас отвечаем. Блочная память Gowin_pROM инициализируется значениями из файла rom.mif.
Так же стоит заметить, что формируя пакет на отправку я одновременно считаю его контрольную сумму CRC32 и она так же будет отправлена в конце пакета, вставлена в шаблон, когда старшие 4 бита равны 4'hF.
Байты отправляемого пакета записываются в выходное FIFO FIFO_HS_Top.
Дальше следует логика, которая забирает байты из FIFO и выдаёт их в модуль ddr_out и далее в микросхему Ethernet PHY через сигналы Tx_D[3:0] и Tx_dv (data valid).
Что касается принятых UDP пакетов, то идет проверка, чтобы пакет был на мой IP адрес и на мой UDP порт. Номер порта сейчас задан 26985, это в шестнадцатеричном виде 16'h6969. Из принятого пакета я сейчас беру только два 32х битных слова и формирую из них сигнал udp_payload[63:0].
Модуль верхнего уровня gig_e берет из сигнала udp_payload[63:0] младший байт и выводит его на светодиоды платы. А старшее слово используется для формирования длительности одного полушага для шагового двигателя подключенного к плате на выходы io16, io17, io18 и io19.
Чтобы проверить работоспособность FPGA проекта я подготовил 3 тестовые питоновские программы.
1) Посылает на адрес 10.8.0.9 один или несколько (количестко задаётся вторым аргументом программы) одинаковых UDP пакетов, которые установят светодиоды в 0x55
>python send_pkt.py 10.8.0.9 1 0x55
2) Посылает периодические UDP пакеты, которые управляют светодиодами вот так:
>send_pkt_leds.py 10.8.0.9
*-------
**------
***-----
****----
*****---
******--
*******-
********
*******-
******--
*****---
....
3) Tkinter тестовая программа со слайдером, позволяет менять скорость вращения шагового двигателя, подключенного к плате
>python udp_send_ui.py 10.8.0.9

Программа на питоне использует библиотеки Tkitner и выводит графическое окно со слайдором, который можно двигать и таким образом задавать скорость вращения шагового двигателя подключенного к плате.
Ниже представлена видеодемонстрация проекта:
Здесь в начале видео показано, как я очищаю ARP таблицу на компьютере командой "arp -d *" и ARP таблица становится почти пустая. А потом, после запуска тестовой программы send_pkt.py в ARP таблице появляется новая запись, соответствующая моему устройству: "10.8.0.9 a0-a1-a2-a3-a4-a5".
Так же видно, что я запускаю на компьютере программу анализа сетевого трафика Wireshark и в момент запуска тестовой программы send_pkt.py там регистрируются 3 пакета: желтым выделены ARP запрос-ответ и черным UDP на моё устройство.
Обратите внимание на светодиоды платы - они загораются согласно байту переданному питоновсим скриптом send_pkt.py.
Потом запускается скрипт send_pkt_leds.py и светодиоды уже потом сами загораются в цикле.
И в завершении видео показано, как я управляю скоростью вращения шагового двигателя из питоновской программы udp_send_ui.py.
Должен сказать, что этот проект сделан по мотивам другого моего проекта web-server в плате Марсоход2. Там правда использовалась плата расширения с Ethernet 100Мбит. Но к сожалению, той работой я не вполне удовлетворён. Там была попытка реализовать аппаратный протокол TCP, а это скажем прямо довольно сложно. Логика TCP довольно сложна. Чтобы правильно всё написать лучше всего заменить в этом проекте модуль pkt_reader каким ни будь процессором, хоть RISC-V, хоть AVR или Z80, но процессором. И тогда уже всю логику выписать на ассемблере или C/C++.
Можно ли как-то усовершенствовать конкретно этот проект? Несомненно можно и нужно. Чего здесь явно не хватает так это какой-то минимальной поддержки ICMP протокола, а именно хотя бы ответа на ping. Это очень полезная функция для сетевого устройства. Может когда нибудь и сделаю это.
Если вы захотите повторить или просто испытать этот проект, то проще всего приобрести плату FPGA Марсоход3GW2 в нашем интернет магазине!


Подробнее...