Этот пост — продолжение моего проекта по созданию завершённой компьютерной системы на компонентах дискретной логики. У меня уже есть компьютер, способный выполнять сетевые приложения, например, HTTP-сервер или игру по LAN.
В прошлом году я изготовил адаптер физического уровня, преобразующий сигнал Ethernet 10BASE-T в SPI и обратно. Тогда для тестирования его работы я использовал микроконтроллер STM32, а теперь реализую модуль слоя MAC, чтобы подключить его к своему самодельному компьютеру.
Оба адаптера полнодуплексные и имеют отдельные передатчик и приёмник.
Компьютер целиком. Новый модуль находится справа внизу
Новый модуль со снятым шилдом физического уровня
Краткое описание работы приёмника:
FCS не проверяется на оборудовании.
Сначала последовательные данные SPI необходимо преобразовать в поток байтов.
Последовательные данные сдвигаются в регистр сдвига (
Полученные байты записываются в буфер на 2 кБ статической ОЗУ 6116 (
Для обеспечения доступа к полученным данным и их длине ОЗУ и счётчик байтов подключены к системной шине данных буферами с тремя состояниями:
При анализе трафика Ethernet я заметил, что фреймы обычно поступают небольшими группами (3-4 фрейма, разделённых короткой паузой). Фреймы в одной группе обычно имеют разные MAC-адреса получателей. Из-за этого я подумал, что мой компьютер не способен отфильтровывать полученные фреймы по MAC и повторно включать приёмник достаточно быстро, чтобы улавливать кадры, предназначающиеся для него. Мне нужна аппаратная фильтрация MAC-адресов.
Мне не подходило решение с хранением MAC-адреса и сравнением с ним первых шести полученных байтов — слишком сложно. Ещё я мог сделать это повторением одного байта (например, FE:FE:FE:FE:FE:FE), но это скучно. Чтобы добавить моему MAC вариативности, я сделал его функцией байтового индекса:
При использовании этого правила MAC-адрес принимает вид
На этой схеме шина
Этот блок проверяет только валидность одного байта. Чтобы проверить все шесть, результат аккумулируется в
Аналогично приёмнику, передатчик не реализует генерацию FCS, она выполняется программно. Чтобы ещё больше упростить передатчик, я решил поддерживать фреймы только фиксированной длины. Благодаря этому не требуется сложный цифровой компаратор, а логика передачи фреймов зависит только одного бита в счётчике байтов. Я выбрал в качестве длины фрейма 1024 байта, что близко к обычному MTU 1500 байтов. Преамбула фрейма (последовательность множества 0x55, заканчивающаяся 0xD5, что требуется 10BASE-T) тоже включена в эти 1024 байта и должна загружаться туда программно.
Фиксация длины фрейма никак не повлияла на протоколы более высокого уровня, потому что они кодируют размер пакета в своих заголовках и поэтому не полагаются на истинную длину фрейма Ethernet.
Краткое описание работы передатчика:
Как и в приёмнике, два счётчика используются для подсчёта битов (
Как и в приёмнике, три мультиплексора 74HC157 (на изображении не показаны) используются для выбора входного адреса для ОЗУ (
После изготовления устройства я заметил, что перепутал порядок битов, поступающих из ОЗУ в регистр сдвига. Мне пришлось программно менять порядок битов, чтобы устранить этот аппаратный баг. Я не мог заранее протестировать это в Verilog.
Для формирования красивого сигнала 10BASE-T (см. мой предыдущий пост)
С точки зрения программиста мой Ethernet-адаптер имеет следующий интерфейс:
Прерываний нет, потому что мой CPU их не поддерживает.
Все соответствующие адреса начинаются с
Бит 11 должен быть равен нулю для адреса буфера. Это проверяют
Равенство второй шестнадцатеричной цифры для регистров значению
Два светодиода обозначают доступ к буферам или регистрам.
Я хотел обеспечить своему компьютеру поддержку сети, но мне лениво было реализовывать стек TCP/IP самостоятельно. Кроме того, мне хотелось иметь приличный компилятор C, потому что мой первый компилятор был отстойным, а программирование на ассемблере утомляет. Поэтому я создал компилятор C. Он достаточно совершенен, чтобы скомпилировать uIP 1.0 (крошечную библиотеку TCP/IP). Несмотря на то, что мой CPU обладает ужасной низкой плотностью кода, uIP достаточно мала, чтобы поместиться в ОЗУ и оставить в ней место для приложения.
Сетевая скорость очень мала, но я всё равно очень доволен ею, ведь всё создавалось без применения коммерческих CPU или специальных чипов:
Модели, файлы схем и чертежи печатных плат выложены на Github.
В прошлом году я изготовил адаптер физического уровня, преобразующий сигнал Ethernet 10BASE-T в SPI и обратно. Тогда для тестирования его работы я использовал микроконтроллер STM32, а теперь реализую модуль слоя MAC, чтобы подключить его к своему самодельному компьютеру.
Оба адаптера полнодуплексные и имеют отдельные передатчик и приёмник.

Компьютер целиком. Новый модуль находится справа внизу

Новый модуль со снятым шилдом физического уровня
Приёмник
Краткое описание работы приёмника:
- Последовательные данные SPI преобразуются в побайтовые параллельные данные, извлекается тактовый сигнал байтов;
- Первые шесть байтов проверяются на соответствие критериям MAC-адреса получателя, несовпадающие фреймы отбрасываются;
- Байты записываются в буфер статической ОЗУ;
- При завершении фрейма приёмник отключается и дальнейшие фреймы отклоняются, пока пользователь не включит приёмник повторно. Счётчик байтов останавливается, его значение становится доступным пользователю.
FCS не проверяется на оборудовании.
Сбор данных
Сначала последовательные данные SPI необходимо преобразовать в поток байтов.

Последовательные данные сдвигаются в регистр сдвига (
U32). U30 и U31 подсчитывают биты и байты. Сигнал записи статической ОЗУ recv_buf_we формируется при помощи D-триггера U29B. Этот сигнал кратковременно становится низким после каждых 8 битов входных данных:
Полученные байты записываются в буфер на 2 кБ статической ОЗУ 6116 (
U20).
U13, U16 формируют U18 мультиплексор адресов: он выбирает в качестве входного адреса для SRAM (U20) или счётчик байтов, или системную шину адреса. Буфер с тремя состояниями U21 перенаправляет полученный байт в ОЗУ.Для обеспечения доступа к полученным данным и их длине ОЗУ и счётчик байтов подключены к системной шине данных буферами с тремя состояниями:

U25 соединяет ОЗУ приёмника с системной шиной данных. После завершения фрейма счётчик байтов не сбрасывается и его значение хранится в шине recv_byte_cnt. Эта шина соединена с системной шиной данных при помощи U26 и U27. Они активируются, когда CPU выполняет запрос чтения к определённым адресам. Вторая половина U27 образует регистр с двумя состояниями только для чтения, который используется для опроса состояния приёмника и передатчика.Фильтрация MAC-адресов
При анализе трафика Ethernet я заметил, что фреймы обычно поступают небольшими группами (3-4 фрейма, разделённых короткой паузой). Фреймы в одной группе обычно имеют разные MAC-адреса получателей. Из-за этого я подумал, что мой компьютер не способен отфильтровывать полученные фреймы по MAC и повторно включать приёмник достаточно быстро, чтобы улавливать кадры, предназначающиеся для него. Мне нужна аппаратная фильтрация MAC-адресов.
Мне не подходило решение с хранением MAC-адреса и сравнением с ним первых шести полученных байтов — слишком сложно. Ещё я мог сделать это повторением одного байта (например, FE:FE:FE:FE:FE:FE), но это скучно. Чтобы добавить моему MAC вариативности, я сделал его функцией байтового индекса:
- Бит 0 имеет фиксированное значение 0;
- Бит 1 имеет фиксированное значение 1;
- Биты 2-4 обратны байтовому индексу;
- Биты 5-7 имеют фиксированное значение 1.
При использовании этого правила MAC-адрес принимает вид
FE:FA:F6:F2:EE:EA. Также для работы с ARP нам нужно принимать широковещательный MAC FF:FF:FF:FF:FF:FF.
На этой схеме шина
a[0..3] — это младшие 4 бита счётчика байтов. Шина d[0..7] — это полученный байт. U33 сравнивает биты данных 0 и 2-4 с нужными значениями; если они совпадают, то на выходе U34A будет высокий сигнал. U35A реализует проверку широковещательного MAC: на его выходе будет высокий сигнал, когда биты 0 и 2-4 равны единицам. Эти два сигнала комбинируются при помощи логического OR (реализованного при помощи диодов D7 и резистора R6). Остальные биты проверяются на равенство единице при помощи U35B.Этот блок проверяет только валидность одного байта. Чтобы проверить все шесть, результат аккумулируется в
U10A. Если фреймы не принимаются, сигнал ss (входящий сигнал выбора ведомого устройства SPI) низкий, а U10A имеет значение 1. В процессе приёма фреймов это значение обновляется для каждого полученного байта. Если MAC-адрес получателя соответствует критериям, то значение U10A остаётся высоким. Когда адрес байта достигает 5, конечное значение защёлкивается в U36B. Этот вывод используется для прекращения приёма фреймов, если адрес получателя не совпадает.Передатчик
Аналогично приёмнику, передатчик не реализует генерацию FCS, она выполняется программно. Чтобы ещё больше упростить передатчик, я решил поддерживать фреймы только фиксированной длины. Благодаря этому не требуется сложный цифровой компаратор, а логика передачи фреймов зависит только одного бита в счётчике байтов. Я выбрал в качестве длины фрейма 1024 байта, что близко к обычному MTU 1500 байтов. Преамбула фрейма (последовательность множества 0x55, заканчивающаяся 0xD5, что требуется 10BASE-T) тоже включена в эти 1024 байта и должна загружаться туда программно.
Фиксация длины фрейма никак не повлияла на протоколы более высокого уровня, потому что они кодируют размер пакета в своих заголовках и поэтому не полагаются на истинную длину фрейма Ethernet.
Краткое описание работы передатчика:
- Данные сохраняются в статической ОЗУ;
- Тактовый сигнал 20 МГц подаётся на 4-битный счётчик, его вывод переполнения используется как тактовый сигнал байтов;
- Для передачи кадра пользователь выполняет запись в определённую область памяти только для чтения, что приводит к включению счётчика;
- Параллельные байтовые данные сериализируются при помощи регистра сдвига.
Счётчики

Как и в приёмнике, два счётчика используются для подсчёта битов (
U12) и байтов (U14). На первый счётчик подаётся тактовый сигнал 20 МГц от интегрального генератора. 20 МГц используются не напрямую, а делятся минимум на 2. Благодаря этому рабочий цикл генератора не влияет на выходной сигнал.Поток данных

Как и в приёмнике, три мультиплексора 74HC157 (на изображении не показаны) используются для выбора входного адреса для ОЗУ (
U22). U23 применяется для загрузки данных в ОЗУ. U24 используется как промежуточное хранилище для текущего передаваемого байта. Принцип здесь схож с моим конвейером VGA: счётчик байтов 74HC4040 — медленно стабилизируемый счётчик числа колебаний, U24 обеспечивает стабильный вывод, в то время как вывод ОЗУ по-прежнему невалиден. Эти данные передаются на регистр сдвига U28, где побайтово сдвигаются.После изготовления устройства я заметил, что перепутал порядок битов, поступающих из ОЗУ в регистр сдвига. Мне пришлось программно менять порядок битов, чтобы устранить этот аппаратный баг. Я не мог заранее протестировать это в Verilog.
Для формирования красивого сигнала 10BASE-T (см. мой предыдущий пост)
MOSI и SCK должны быть точно синхронизированы. Эту задачу решают U11A и U8B. tx_cnt0 (бит 0 счётчика битов, делённые пополам 20 МГц) используется в качестве тактового сигнала. U11A меняет его вывод синхронно с этим сигналом. U8B выполняет задержку тактового сигнала, чтобы он соответствовал задержке, привносимой U11A. Так как D-защёлка сложнее, чем простой вентиль AND и имеет чуть большую (на 5 нс) задержку, здесь используется более быстрый 74LV74A. Его задержка распространения такая же, как у 74HC08. Это единственный на моей плате чип из «быстрого» семейства.Интерфейс CPU
С точки зрения программиста мой Ethernet-адаптер имеет следующий интерфейс:
- Оба буфера фреймов отображены на
0xF000. - Есть два регистра только для чтения:
- 8-битный регистр состояния в
0xFB00имеет два флага:-
RX_FULL— фрейм получен, -
TX_BUSY— фрейм передаётся;
-
- 16-битный регистр длины полученных данных в
0xFB02.
- 8-битный регистр состояния в
- Запись любого значения в
0xFB00повторно включает приёмник. - Запись любого значения в
0xFB01начинает передачу.
Прерываний нет, потому что мой CPU их не поддерживает.

Все соответствующие адреса начинаются с
F (все старшие 4 бита равны единице). Это условие проверяется U2A.Бит 11 должен быть равен нулю для адреса буфера. Это проверяют
U1D, D2, R2 и U1E. Затем сигнал выбора буфера сочетается с сигналами включения записи или вывода, чтобы выбрать запись в буфер TX или считывание из буфера RX.Равенство второй шестнадцатеричной цифры для регистров значению
B (1011) проверяется U1B и U2B. Затем ещё один блок диодной логики (D1, R1, U1C) комбинирует её с проверкой первой цифры. Декодеры U4A и U4B используются для выбора конкретной функции.Два светодиода обозначают доступ к буферам или регистрам.
Программирование
Я хотел обеспечить своему компьютеру поддержку сети, но мне лениво было реализовывать стек TCP/IP самостоятельно. Кроме того, мне хотелось иметь приличный компилятор C, потому что мой первый компилятор был отстойным, а программирование на ассемблере утомляет. Поэтому я создал компилятор C. Он достаточно совершенен, чтобы скомпилировать uIP 1.0 (крошечную библиотеку TCP/IP). Несмотря на то, что мой CPU обладает ужасной низкой плотностью кода, uIP достаточно мала, чтобы поместиться в ОЗУ и оставить в ней место для приложения.
Сетевая скорость очень мала, но я всё равно очень доволен ею, ведь всё создавалось без применения коммерческих CPU или специальных чипов:
- Полный путь пинга в среднем составляет 85 мс;
- Скорость скачивания HTTP-сервера составляет 2,6 кБ/с (передача статических файлов с SD-карты).
Репозиторий проекта
Модели, файлы схем и чертежи печатных плат выложены на Github.
SIISII
Маньяк этот Ivan -- прям как я (и, кстати, тоже Иван) :) Вполне может быть, стащу в будущем его решение для себя.