Давно хотел сделать такой проект — WebServer в плате Марсоход2. Но хотел сделать не так, как это делают все, с помощью процессора Nios или любого другого. Нет, хотелось сделать именно аппаратный сервер в FPGA в виде стэйт-машины, которая принимает и отсылает пакеты Ethernet без участия традиционного процессора. Хотелось бы сделать очень быстрый сервер.
Эта задача честно говоря очень сложная. Этот проект я сделал, но его пока можно рассматривать только как черновик, там осталось несколько нерешенных проблем о которых я знаю. Может быть когда нибудь в будущем исправлю, если потребуется для реального, не игрушечного проекта.
А пока, вы можете попробовать работоспособность моего веб сервера в живую. Попробуйте обратиться по адресу http://84.51.195.178 (не знаю как долго проработает сервер и плата на этом IP адресе). Это адрес ADSL модема, который переадресует запросы на подключение к плате Марсоход2 с шилдом Ethernet. По идее, если все сработает нормально, вы должны увидеть в своем браузере елочку, как на картинке выше.
Сама плата Марсоход2 подключена к модему кабелем Ethernet. Модем отконфигурирован, чтобы отсылать запросы на TCP подключение к порту 80 на плату Марсоход2 на ее IP адрес. Адрес платы в локальной сети фиксированный - 10.8.3.9, ну а адрес модема в интернет, как вы поняли 84.51.195.178
Как это все работает.
Вот топ модуль проекта в среде Altera Quartus II. Выглядит все довольно мелко, так как проект большой. Можно кликнуть на картинке, чтоб увидеть чуть крупнее.
Итак:
- модуль eth_receive принимает Ethernet пакет и считает его контрольную сумму. Выдает принятые байты и их порядковый номер в пакете. Так же генерирует сигнал crc32_ok по окончании приема пакета.
- модуль pkt_write пишет принятые пакеты в двухпортовое ОЗУ прямо по мере приема пакета. Модуль содержит указатель на принимаемый пакет head[2:0]. Если по окончании приема контрольная сума пакета была верной (сигнал crc32_ok), то указатель head будет увеличен. Следующий принимаемый пакет будет принят по другим адресам в двухпортовом ОЗУ. Указатель head имеет 3 разряда, всего может адресовать 8 принятых пакетов.
- модуль pkt_ram – это двухпортовое ОЗУ для хранения принятых пакетов. Получается как бы ФИФО на 8 пакетов по 2048 байт. Обратите внимание, что запись пакета в память ведется байтами, а чтение пакета ведется словами по 256 бит, то есть 32-х байтными словами. Ну я же хочу сделать быстрый сервер, поэтому для анализа заголовков пакетов ethernet мне достаточно сделать всего 2 чтения из статической памяти.
- самый главный модуль pkt_reader. Он читает из двухпортового ОЗУ принятые пакеты и анализирует их и формирует ответы. В этом модуле есть трехбитный указатель на последний обработанный пакет tail[2:0]. Как только новый пакет принят и указатель head из модуля pkt_write изменяется, то pkt_reader тут же видит, что голова ушла вперед, отличается от tail и модуль переходит к обработке пакета. Один такт на решение о начале обработки пакета:
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-machine. Как я уже сказал, чтение из памяти ведется 32-х байтными словами. Одно чтение сразу позволяет определить тип пакета ARP или IP, еще определить предназначен ли пакет для моего MAC адреса.
Следующее чтение позволяет проанализировать другие важные поля пакета. Об этом расскажу чуть позже. Тут важно, что когда модуль принял решение об отправке ответного пакета данных, то он начинает их считывать из памяти шаблонов пакетов и, модифицируя на лету шаблон пакета, передает байты в фифо на передачу в ethernet.
- модуль rom хранит шаблоны пакетов для отправки клиентам. Всего заготовлено 3 пакета (вообще-то маловато, нужно бы больше), это минимальное число заготовок пакетов, необходимое для более или менее правильной работы системы. Вообще-то по честному нужно реализовать весь стек протоколов TCP/IP. Я же делаю какую-то самую минимальную функциональность. В памяти rom хранится заготовка для пакета ARP Replay, TCP SYN-ACK и TCP ACK-PUSH-FIN. Вся моя веб страничка помещается в один ethernet пакет.
- модуль send_fifo – это просто фифо передаваемых данных. Оно нужно, чтобы отвязать частоту приемника от частоты передатчика, ведь чип Realtek Ethernet сам диктует нам приемную и передающую частоты и не гарантируется, что они одинаковые (в смысле не гарантируется, что они синфазные).
- модуль sender читает из фифо и отправляет пакет в RTL чип, который находится на ethernet шилде платы Марсоход2.
Это я только очень коротко описал, какой модуль за что отвечает. На самом деле в проекте очень много ньюансов. Я например, вообще не рассказал о сетевых протоколах, а надо было бы. В короткой статье трудно изложить все четко и подробно, особенно в новогодние каникулы, которые я собственно и потратил на этот проект.
Про сетевые протоколы.
Чтобы устройство работало в сети оно прежде всего должно отвечать на запросы ARP. Каждое сетевое устройство имеет шесть байт аппаратного адреса MAC и 4 байта адреса TCP/IP.
В моем проекте MAC адрес и IP адрес платы задается прямо в модуле pkt_reader на писанном на языке Verilog HDL:
localparam MY_MAC = 48'hd850e6ea5678;
localparam MY_IP = 32'h0903080A; // my fixed IP is 10.8.3.9
В своей локальной сети я использую адреса типа 10.8.xx.xx и маска подсети 255.255.0.0
Так вот, когда кто-то хочет обратиться к заданному IP адресу прежде всего в сеть выдается широковещательный (broadcast) ARP запрос: "У кого IP=x.x.x.x?"
Тот, кто имеет такой IP должен ответить пакетом ARP Reply: "У меня IP и мой MAC адрес xx.xx.xx.xx.xx.xx".
После этого уже пакеты для нужного IP адреса отправляются непосредственно на нужный MAC адрес и сетевой маршрутизатор прямо из заголовков пакетов знает на какой порт коммутировать пакет.
Вот такой обмен происходит от браузера к моему серверу:
Сам протокол TCP/IP так же очень сложный, нужно проанализировать довольно много полей входящего запроса, чтобы сформировать правильный ответ. Нужно проверить на мой ли IP адрес пришел пакет, нужно проверить пришел ли пакет на мой порт HTTP=80, проверить TCP флаги: SYN/ACK/PUSH и другие. Еще нужно учесть SEQ и ACK номера в пакете, они отвечают за очередность передаваемых данных. Очень много тут всего. Я упростил себе жизнь тем что условился, что вся моя веб страница должна умещаться в один ethernet пакет. То есть она должна быть где-то около 1 килобайта, не больше. Тогда мне не нужно хранить данные о TCP сессиях для каждого входящего соединения. Конечно, когда страницы будут большими так сделать не получится, придется делать полностью стек TCP/IP (кстати думаю я мог бы сделать).
В принципе, как я сказал, сейчас я делаю только три ответа:
- ARP Reply на ARP Request
- TCP SYN-ACK на TCP SYN
- TCP ACK-PUSH-FIN на TCP ACK-PUSH-FIN
это все. Все остальные пакеты просто игнорируются.
Конечно, такой проект нельзя сделать без анализатора сетевых протоколов Wireshark. С его помощью можно смотреть приходящие и отправляемые пакеты. Вот как выглядит обмен пакетами с точки зрения программы WIreshark:
Еще думаю нужно рассказать про шаблоны пакетов, которые я храню в модуле rom.
Дело в том, что нельзя просто так взять и хранить целиком готовый пакет и его просто отсылать. Например я не знаю MAC адреса клиентов, которые будут мне присылать запросы. Или я заранее не знаю TCP порт и IP клиента, который будет присылать пакеты..
Поэтому в памяти я придумал хранить шаблоны пакетов, а не готовые пакеты. Разрядность памяти 12 бит. Младшие 8 бит — это передаваемый байт, а старшие 4 бита — это возможная команда подстановки. В шаблоне оставлены поля, которые будут заполняться на лету.
Содержимое памяти rom я зараннее подготавливаю специальной утилиткой-программой mkrom, написанной на C в Visual Studio. Эта утилита генерирует для Quartus II Memory Initialization File файл rom.mif
Вот так выглядит в программе шаблон ARP Reply:
#define CMD_REMOTE_MAC 0x100
#define CMD_REMOTE_IP 0x200
#define CMD_MY_MAC 0x300
#define CMD_MY_IP 0x400
#define CMD_TCP_PORT 0x500
#define CMD_TCP_ACK 0x600
#define CMD_TCP_CHS0 0x700
#define CMD_IP_CHS0 0x800
#define CMD_PRESUM 0xE00
#define CMD_CRC 0xF00
unsigned short arp_reply_pkt[] = {
CMD_REMOTE_MAC+0, //dest addr
CMD_REMOTE_MAC+1,
CMD_REMOTE_MAC+2,
CMD_REMOTE_MAC+3,
CMD_REMOTE_MAC+4,
CMD_REMOTE_MAC+5,
CMD_MY_MAC+0, //0x55, 0x44, 0x33, 0x55, 0x44, 0x33, //src addr (my MAC addr)
CMD_MY_MAC+1,
CMD_MY_MAC+2,
CMD_MY_MAC+3,
CMD_MY_MAC+4,
CMD_MY_MAC+5,
0x08, 0x06, //type = arp
0x00, 0x01, //hw type = ethernet
0x08, 0x00, //protocol type = IP
0x06,
0x04,
0x00, 0x02, //arp opcode - reply
CMD_MY_MAC+0, //0x55, 0x44, 0x33, 0x55, 0x44, 0x33, //sender MAC (my MAC addr)
CMD_MY_MAC+1,
CMD_MY_MAC+2,
CMD_MY_MAC+3,
CMD_MY_MAC+4,
CMD_MY_MAC+5,
CMD_MY_IP+0, //0x0a, 0x08, 0x03, 0x09, //sender IP (my fixed IP)
CMD_MY_IP+1,
CMD_MY_IP+2,
CMD_MY_IP+3,
CMD_REMOTE_MAC+0, //target MAC
CMD_REMOTE_MAC+1,
CMD_REMOTE_MAC+2,
CMD_REMOTE_MAC+3,
CMD_REMOTE_MAC+4,
CMD_REMOTE_MAC+5,
CMD_REMOTE_IP+0, //target IP
CMD_REMOTE_IP+1,
CMD_REMOTE_IP+2,
CMD_REMOTE_IP+3,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
CMD_CRC+0,
CMD_CRC+1,
CMD_CRC+2,
CMD_CRC+3 //end packet
};
Там, где находятся слова CMD_xxx будет на лету вставлено соответствующее значение вычисленное или взятое из входного запроса.
Довольно сложно получилось, но кажется работает.
ARP – это еще не самое сложное. Вот TCP пакет отправить — вот это эпопея..
Нужно дополнительно вычислить IP checksum и TCP checksum и на лету вставлять их в отправляемый пакет...
Кое-что заранее подсчитывается в утилите mkrom, которая генерирует MIF файл, чтобы облегчить операции вычисления контрольных сумм IP и TCP...
Если вам нужно сгенерировать свою страницу, то придется исправлять текст в программе mkrom, перекомпилировать эту утилиту с Visual Studio и заново генерировать MIF файл для Quartus II проекта.
Ну и из плохих новостей: не все клиенты видят мою web страницу. Видимо не все правильно в моей реализации TCP/IP..
Я проверял на:
- Windows 8 / Windows 7 – работает
- Android Tablet: Samsung Galaxy Tab 10” и Asus Google Nexus 7” New – работает
- Android Phone HTC One – работает
- Apple iPad mini – не работает :-(
Вот такие пироги..
Надеюсь, Вы сможете увидеть отклик о моего web server а по адресу 81.54.195.178...
PS: ну и конечно, я не делал стресс тестов, когда много клиентов подключаются одновременно. Посмотрим, что получится..
PSS: Весь проект со всеми исходниками можно взять здесь:
Подробнее...