
Иногда бывает, что для выполнения какого-то проекта требуется пройти несколько необходимых промежуточных этапов. Если проект исключительно программный, то эти дополнительные этапы не очень заметны. К примеру, при чистом программировании при возникновении проблемы всегда можно под неё попробовать найти готовую библиотеку и двигаться дальше.
Если проект аппаратный, то тут немного сложнее. Я расскажу про наш случай. Мы разрабатываем линейную камеру для контроля качества печати на флексографических машинах. При этом, конечно, мы не имеем возможности во время экспериментов всё время находиться рядом с печатным станком, мы тогда будем там просто мешать людям работать. То есть нам нужен простой испытательный стенд для разработчиков.
Линейная камера захватывает изображение не кадрами, как обычные камеры, а строками. Таким образом, нам для экспериментов подойдет вращающийся барабан с тестовым изображением с возможностью регулировки скорости вращения барабана. Где взять такую штуку? Её можно сделать.
Испытательный стенд будет состоять из деталей, которые нужно где-то взять, купить, выточить или напечатать, а так же из платы управления. Плату управления надо запрограммировать. Получается, что наш промежуточный этап "изготовление стенда" распадается еще на две подзадачи: физическое изготовление стенда и программирование платы управления.
Детали для стенда было решено напечатать на 3D принтере, кроме картонного барабана и алюминиевых швеллеров. Двигатель возьмём шаговый Nema. Это возможно спорный выбор, но с шаговым двигателем гораздо проще обеспечить точную равномерную скорость вращения.
Модели деталей разработали во FreeCAD. В сборке, хоть и не все здесь показаны, они выглядят примерно вот так:

Напечатанный и собранный стенд показан в начале статьи или вот немного с другого ракурса, хорошо виден шаговый двигатель:

Обратите внимание на дальнем конце барабана установлена "шестерня". Это конечно не настоящая шестеренка, а её эмулятор. Там в пластиковой детали напечатаны ушки куда просто вкручены металлические винты на 3 мм. Наша линейная камера должна уметь синхронизироваться по зубьям шестерни печатного вала реальной печатной машины и мы используем магнитный датчик для этих целей. А проверять функциональность датчика будем вот такой эмуляцией.
В качестве платы управления мы выбрали плату с FPGA Gowin, Марсоход3GW2 и плюс к ней плату расширения с гигабитным Ethernet. Я буду делать управление платой через сеть Ethernet. Почему так сложно? Зачем? Дело в том, что связь по сети нам ещё потребуется в будущем, на последующих этапах. Сейчас мы делаем камеру контроля печати с выводом изображения на HDMI монитор, а дальше мы планируем делать камеру, которая посылает поток строк-линий изображения в сеть Ethernet для дальнейшей обработки. Таким образом, можно делая сегодня "испытательный стенд" одновременно разрабатывать будущие IP Core для передачи данных по сети Ethernet.
С сетью Ethernet не всё так просто. Пока я решил ограничиться только приёмом UDP пакетов, они будут приносить в FPGA плату команду управления шаговым двигателем - а именно одну команду, длительность одного шага двигателя. Чтобы моя FPGA плата могла принимать UDP пакет предназначенный именно ей мне нужно назначить плате её статический IP адрес и MAC адрес. Это я пока прописываю прямо в коде Verilog, как константы.
Казалось бы можно просто слушать Ethernet пакеты, отбирать только те, которые для моей платы по IP адресу и готово. Но это так не работает. Чтобы это сработало нужно, чтобы моё устройство поддерживало как минимум еще один сетевой протокол ARP, Address Resolution Protocol. Отправитель пакета посылает пакет клиенту по его MAC адресу Ethernet. Если отправитель еще не знает MAC клиента, то первый запрос с компьютера отправителя будет широковещательный ARP запрос всем: "у кого есть такой IP?" Если окажется, что запрашиваемый IP адрес назначен моему устройству, то я обязан ответить ARP ответом "Это я! Вот мой MAC и мой IP". После этого, отправитель знает MAC адрес получателя и отправляет пакет уже конкретно ему.
Как видите, логика работы такого FPGA проекта уже не очень простая. Мне придется не только принимать пакеты, но и отправлять пакеты отвечая на запросы ARP. В идеале еще бы и ping поддерживать. Но это еще один уровень сложности и это конечно придется делать в реальных сетевых устройствах.
Я использую плату расширения Ethernet которая основана на микросхеме Realtek 8211E. Данные будут приходить в FPGA плату четырёхбитные, но по фронту и спаду, то есть в режиме DDR. Как и другие производители ПЛИС, у Gowin есть собственные IP Core для базовых компонентов, в том числе и DDR приёмники типа Gowin_DDR. После него данные уже восьмиразрядные и скорость передачи 125МГц. Дальше я должен данные принимаемого пакета положить в память. Для этого я написал специальный модуль rx_crc32 на Verilog. Его основная функция записывать принимаемые пакеты в циклический буфер на 8 пакетов по 2 килобайта. При этом, модуль будет перемещать указатель записи на следующий блок памяти вперёд только если у принимаемого пакета посчитанная контрольная сумма совпадёт с принятой.
Другой важный Verilog модуль pkt_reader - это машина состояний, которая наблюдает, если указатель записи пакета переместился вперёд, то значит принят новый пакет и приступает к его анализу и реагирует соответствующим образом. Пока у меня тут всё довольно примитивно. Есть три случая:
если широковещательный запрос ARP мне, то нужно отвечать ARP ответом;
если UDP пакет по моему IP и моему номеру порта, то запомнить принимаемые данные и использовать по назначению (управлять шаговым двигателем, зажигать светодиоды);
иное - проигнорировать пакет.
В целом всю структуру проекта можно описать вот такой блок схемой:

А исходники этого проекта можно взять на гитхабе.
Хочу заметить, что если требуется более продвинутая обработка пакетов из сети, то можно попробовать модуль pkt_reader заменить каким ни будь софт процессором, например типа picoRV (RISC-V) или любым другим. Процессором гораздо проще реализовать сложные машины состояний. Для реализации TCP это возможно единственный верный путь.
Отдельно хотел бы обратить внимание на подсчёт контрольной суммы Ethernet пакета. Алгоритм это общеизвестный CRC32, но важны детали реализации.

Здесь:
• PRE – первые пилотные одинаковые 7 байт 0x55
• SFD – Start-of-Frame Delimeter, начало кадра, один байт 0xD5.
• DA – Destination Address MAC, адрес назначения, 6 байт.
• SA – Source Address MAC, адрес источника пакета, 6 байт.
• TYPE - Два байта типа данных. Для IP пакетов 0x0800, для ARP – 0x0806.
• DATA, PAD - Данные пакета переменной длины.
• FCS – Frame Check Sequence, 4 байта контрольной суммы CRC32.
Подсчитывать контрольную сумму нужно только по избранным полям, без PRE и SFD и FCS.
При этом, если принимать побайтно, то придётся в проекте хранить последние четыре вычисленные контрольные суммы, ведь мы достоверно не знаем момент окончания пакета, узнаем только как свершившийся факт. Из-за этого четвертая с конца контрольная сумма это то, что нужно для проверки, а последние 3 контрольные суммы содержат подмешанные байты из самой контрольной суммы, это просто издержки вычислений.
Сама функция вычисления контрольной суммы на Verilog HDL может выглядеть вот так:
// polynomial: (0 1 2 4 5 7 8 10 11 12 16 22 23 26 32)
// data width: 8
// convention: the first serial bit is D[7]
function [31:0] nextCRC32_D8;
input [7:0] Data;
input [31:0] crc;
reg [7:0] d;
reg [31:0] c;
reg [31:0] newcrc;
begin
d = Data;
c = crc;
newcrc[0] = d[6] ^ d[0] ^ c[24] ^ c[30];
newcrc[1] = d[7] ^ d[6] ^ d[1] ^ d[0] ^ c[24] ^ c[25] ^ c[30] ^ c[31];
newcrc[2] = d[7] ^ d[6] ^ d[2] ^ d[1] ^ d[0] ^ c[24] ^ c[25] ^ c[26] ^ c[30] ^ c[31];
newcrc[3] = d[7] ^ d[3] ^ d[2] ^ d[1] ^ c[25] ^ c[26] ^ c[27] ^ c[31];
newcrc[4] = d[6] ^ d[4] ^ d[3] ^ d[2] ^ d[0] ^ c[24] ^ c[26] ^ c[27] ^ c[28] ^ c[30];
newcrc[5] = d[7] ^ d[6] ^ d[5] ^ d[4] ^ d[3] ^ d[1] ^ d[0] ^ c[24] ^ c[25] ^ c[27] ^ c[28] ^ c[29] ^ c[30] ^ c[31];
newcrc[6] = d[7] ^ d[6] ^ d[5] ^ d[4] ^ d[2] ^ d[1] ^ c[25] ^ c[26] ^ c[28] ^ c[29] ^ c[30] ^ c[31];
newcrc[7] = d[7] ^ d[5] ^ d[3] ^ d[2] ^ d[0] ^ c[24] ^ c[26] ^ c[27] ^ c[29] ^ c[31];
newcrc[8] = d[4] ^ d[3] ^ d[1] ^ d[0] ^ c[0] ^ c[24] ^ c[25] ^ c[27] ^ c[28];
newcrc[9] = d[5] ^ d[4] ^ d[2] ^ d[1] ^ c[1] ^ c[25] ^ c[26] ^ c[28] ^ c[29];
newcrc[10] = d[5] ^ d[3] ^ d[2] ^ d[0] ^ c[2] ^ c[24] ^ c[26] ^ c[27] ^ c[29];
newcrc[11] = d[4] ^ d[3] ^ d[1] ^ d[0] ^ c[3] ^ c[24] ^ c[25] ^ c[27] ^ c[28];
newcrc[12] = d[6] ^ d[5] ^ d[4] ^ d[2] ^ d[1] ^ d[0] ^ c[4] ^ c[24] ^ c[25] ^ c[26] ^ c[28] ^ c[29] ^ c[30];
newcrc[13] = d[7] ^ d[6] ^ d[5] ^ d[3] ^ d[2] ^ d[1] ^ c[5] ^ c[25] ^ c[26] ^ c[27] ^ c[29] ^ c[30] ^ c[31];
newcrc[14] = d[7] ^ d[6] ^ d[4] ^ d[3] ^ d[2] ^ c[6] ^ c[26] ^ c[27] ^ c[28] ^ c[30] ^ c[31];
newcrc[15] = d[7] ^ d[5] ^ d[4] ^ d[3] ^ c[7] ^ c[27] ^ c[28] ^ c[29] ^ c[31];
newcrc[16] = d[5] ^ d[4] ^ d[0] ^ c[8] ^ c[24] ^ c[28] ^ c[29];
newcrc[17] = d[6] ^ d[5] ^ d[1] ^ c[9] ^ c[25] ^ c[29] ^ c[30];
newcrc[18] = d[7] ^ d[6] ^ d[2] ^ c[10] ^ c[26] ^ c[30] ^ c[31];
newcrc[19] = d[7] ^ d[3] ^ c[11] ^ c[27] ^ c[31];
newcrc[20] = d[4] ^ c[12] ^ c[28];
newcrc[21] = d[5] ^ c[13] ^ c[29];
newcrc[22] = d[0] ^ c[14] ^ c[24];
newcrc[23] = d[6] ^ d[1] ^ d[0] ^ c[15] ^ c[24] ^ c[25] ^ c[30];
newcrc[24] = d[7] ^ d[2] ^ d[1] ^ c[16] ^ c[25] ^ c[26] ^ c[31];
newcrc[25] = d[3] ^ d[2] ^ c[17] ^ c[26] ^ c[27];
newcrc[26] = d[6] ^ d[4] ^ d[3] ^ d[0] ^ c[18] ^ c[24] ^ c[27] ^ c[28] ^ c[30];
newcrc[27] = d[7] ^ d[5] ^ d[4] ^ d[1] ^ c[19] ^ c[25] ^ c[28] ^ c[29] ^ c[31];
newcrc[28] = d[6] ^ d[5] ^ d[2] ^ c[20] ^ c[26] ^ c[29] ^ c[30];
newcrc[29] = d[7] ^ d[6] ^ d[3] ^ c[21] ^ c[27] ^ c[30] ^ c[31];
newcrc[30] = d[7] ^ d[4] ^ c[22] ^ c[28] ^ c[31];
newcrc[31] = d[5] ^ c[23] ^ c[29];
nextCRC32_D8 = newcrc;
end
endfunction
И при этом:
• Начальное значение для регистра crc32_, хранящего текущую контрольную сумму должно быть 0FFFFFFFFh;
• Принятый байт нужно передавать функции вычисляющей CRC32 развернутым, the first serial bit is D[7]. Старший бит – на самом деле это младший и наоборот;
• Вычисленная контрольная сумма должна быть так же развернута: младший бит – это старший и наоборот;
• Вычисленная контрольная сумма должна быть инвертирована (XOR 0xFFFFFFFF).
Вот теперь вычисленную CRC32 можно проверять с принятой и тогда решать это битый пакет или нет. На самом деле я пытался в своём проекте обнаружить битые пакеты - не обнаружил. Приём идет очень устойчиво и CRC32 всегда получается правильный.
При передаче пакета все эти нюансы создания Ethernet пакета и подсчёта CRC32 так же нужно учитывать. Без правильной контрольной суммы пакета компьютер в сети просто его не примет.
Мой проект сейчас принимает UDP пакеты на фиксированный IP адрес 10.8.0.9 и на порт 26985. А из самого принятого пакета используются только первый байт, который выводится на 8 светодиодов платы Марсоход3GW2 и байты 4, 5, 6 используются как слово управления, длительность одного полушага двигателя.
Для компьютера я написал простую Tkinter программку на питоне (она так же есть на гитхабе), с помощью которой можно управлять удалённо по сети скоростью вращения шагового двигателя нашего испытательного стенда:

Ну и в завершении статьи хочу продемонстрировать работу стенда, точнее, как вращается барабан стенда:
Если же вдруг вас заинтересовало, какую линейную камеру мы делаем, то вот другая демонстрация:
Я так же надеюсь, что фрагменты кода Verilog HDL по подсчёту контрольной суммы Ethernet пакетов, которые я привёл в этой статье, помогут другим разработчикам.
Комментарии (2)
alcotel
20.05.2025 12:59Да, UDP весьма удобен для небольших FPGAшных проектов. Тоже применяю.
Но с контрольной суммой вы перемудрили. Всё гораздо проще - при окончании пакета у вас в регистре crc32 должна получиться определённая магическая константа.
И меня каждый раз коробит, когда я встречаю вот эту простыню текста с XORами, которую копипастят уже не одно поколение. Достаточно же написать один AND с константой из стандарта, сдвиг и сумму. И применить это 8 раз, для каждго бита. Синтезированная логика от этого не поменяется, а простыня станет краткой и понятной.
HardWrMan
Поздравляю! Вы переизобрели SSTV! https://ru.wikipedia.org/wiki/Телевидение_с_медленной_развёрткой
Радиолюбители вам аплодируют стоя.