Мне досталась плата: FPGA Artix-7, рядом микросхема USB3300 — USB PHY (микросхема физического уровня USB) с интерфейсом ULPI (UTMI+ Low Pin Interface) — и разъём USB. Плата питается от собственного источника. Вопрос простой: умеет ли FPGA на этой плате работать USB-устройством?
Готовое ядро AXI USB2 Device от Xilinx (сейчас AMD), пример USB-флешки из драйвера, сборка, прошивка — и хост устройство не видит. Ошибок нет, ядро просто молчит. Причина оказалась в одной линии питания USB, а исправление потребовало залезть внутрь Xilinx IP и разобраться, как Vivado хранит описание ядра.

Всё проверялось в Vivado и Vitis 2024.1 под Linux. Идея проверки такая: если пример USB Mass Storage на хосте превращается в обычную флешку, на которую можно записать данные и прочитать их обратно, значит, интерфейс с PHY, тактирование и разводка платы работают.
Дизайн FPGA
Блок-дизайн минимальный:
MicroBlaze со 128 КБ локальной памяти: блочная память FPGA (BRAM), подключённая по шине LMB (Local Memory Bus);
AXI Interconnect с двумя ведомыми: контроллер прерываний AXI INTC и AXI USB2 Device;
Clocking Wizard на MMCM (Mixed-Mode Clock Manager): из 50 МГц генератора платы получаем 100 МГц для процессорной системы и 24 МГц опорной частоты для USB3300;
Processor System Reset и MicroBlaze Debug Module (MDM) для отладки по JTAG;
System ILA (Integrated Logic Analyzer, встроенный логический анализатор) на шине AXI и прерывании USB — пригодится при отладке.

USB3300 тактируется от FPGA: 24 МГц с MMCM выводятся на вход кварца PHY через ODDR. Это рекомендуемый приём для вывода тактового сигнала на пин в 7-й серии: тактовая сеть не должна напрямую идти в обычный выходной буфер. Обратно PHY отдаёт тактовый сигнал ULPI 60 МГц, на нём работает ядро USB.
Верхний модуль (wrap.sv) связывает пины платы с блок-дизайном. Назначения пинов заданы атрибутами прямо в порту:
module wrap( (* IOSTANDARD = "LVCMOS33", LOC="M21" *) input logic gen50_i, // USB3300 (* IOSTANDARD = "LVCMOS33", LOC="N2" *) output logic ulpi_xtail_in_o, (* IOSTANDARD = "LVCMOS33", LOC="D18" *) input logic ulpi_clkout_i, (* IOSTANDARD = "LVCMOS33", LOC="F15" *) input logic ulpi_nxt_i, (* IOSTANDARD = "LVCMOS33", LOC="G19" *) input logic ulpi_dir_i, (* IOSTANDARD = "LVCMOS33", SLEW = "FAST", LOC="F20" *) output logic ulpi_stp_o, (* IOSTANDARD = "LVCMOS33", SLEW = "FAST", LOC="J14" *) inout logic ulpi_data0_io, // ... ulpi_data1_io .. ulpi_data7_io ); logic clk_24; ODDR #( .DDR_CLK_EDGE("OPPOSITE_EDGE"), .INIT (1'b0), .SRTYPE ("SYNC") ) u_oddr_0 ( .Q (ulpi_xtail_in_o), .C (clk_24), .CE('1), .D1('0), .D2('1), .R('0), .S('0) ); design_1_wrapper u_design_1_wrapper_0 ( .clk_50MHz (gen50_i), .clk_24_o (clk_24), .ext_reset_in ('0), .ULPI_0_clk (ulpi_clkout_i), .ULPI_0_data_io({ulpi_data7_io, /* ... */ ulpi_data0_io}), .ULPI_0_dir (ulpi_dir_i), .ULPI_0_next (ulpi_nxt_i), .ULPI_0_rst (), .ULPI_0_stop (ulpi_stp_o) ); endmodule
Временные ограничения описывают тактовый сигнал ULPI (60 МГц, период 16,667 нс) и задержки сигналов PHY согласно рекомендациям вендора IP:
create_clock -name ulpi_clock -period 16.667 [get_nets ulpi_clkout_i] set_input_delay -max 4.5 -clock ulpi_clock {ulpi_dir_i ulpi_nxt_i} set_output_delay -max 7 -clock ulpi_clock {ulpi_stp_o} set_max_delay 24 -from [get_ports ulpi_dir_i] \ -to [get_ports {ulpi_data0_io ulpi_data1_io ulpi_data2_io ulpi_data3_io \ ulpi_data4_io ulpi_data5_io ulpi_data6_io ulpi_data7_io}] -datapath_only
set_max_delay от dir к линиям данных нужен потому, что по сигналу dir PHY забирает шину себе, и FPGA должна успеть перевести буферы данных в Z-состояние.
Почему устройство не подключается: VBUS и self-powered
VBUS — линия питания +5 В в кабеле USB. USB-устройство сообщает хосту, откуда берёт питание, двумя способами:
в дескрипторе конфигурации, поле
bmAttributes: бит 6 — Self Powered;в ответе на запрос
GET_STATUSк устройству: бит 0 — Self Powered.
Пример xusb_storage из драйвера уже настроен как self-powered: в дескрипторе конфигурации bmAttributes = 0xC0, bMaxPower = 1 (2 мА), а GET_STATUS возвращает 0x0001. С программной стороны всё готово.
Аппаратная сторона интереснее. ULPI — 12-проводной интерфейс между контроллером USB и PHY. Через него PHY передаёт контроллеру состояние линий в байте RX CMD: биты [1:0] — LineState, биты [3:2] — состояние VBUS. Ядро AXI USB2 Device сохраняет эти биты во внутреннем регистре и опирается на них во многих местах своей логики. Пока PHY не сообщит, что VBUS в норме, ядро считает, что кабель не подключён, и не подключается к шине.
На этой плате вывод VBUS микросхемы USB3300 не соединён с линией VBUS разъёма. В типовой схеме включения из документации на USB3300 эта цепь есть — на рисунке она зачёркнута:

PHY измеряет напряжение на своём выводе VBUS и по нему формирует биты VBUS в RX CMD. Раз на выводе ничего нет, PHY честно сообщает, что VBUS отсутствует.
На устройстве с питанием от шины VBUS есть всегда, иначе устройство просто не работало бы. На self-powered плате это не гарантировано, и ядро «из коробки» к хосту не подключается.
Решение — новый параметр ядра SELF_POWERED. Когда он равен 1, ядро при сбросе выставляет своё внутреннее состояние VBUS в «VBUS в норме» и больше не обновляет его из битов VBUS в RX CMD. Остальная логика ядра не меняется: состояние линий, приём и передача работают как раньше.
RTL ядра AXI USB2 Device поставляется зашифрованным (IEEE 1735), поэтому такая доработка возможна, только если у вас есть доступ к его исходному коду.
Расшифрованные исходники ядра — это два файла примерно по 10 тысяч строк: в Verilog-файле 23 модуля, в VHDL-файле 12 сущностей. Иерархия смешанная: в частности, сам usbcore написан на Verilog, а создаётся внутри VHDL-сущностей.
Параметр нужно пробросить через всю иерархию ядра: добавить его в модуль usbcore (и в вариант для HSIC — High-Speed Inter-Chip), подключить во всех экземплярах и объявить в модуле верхнего уровня axi_usb2_device. Делать это руками в таких файлах неудобно. Для этого я написал утилиту hdl-param-propagate: она добавляет параметр в модуль, находит все его экземпляры, подключает параметр в них и поднимается вверх по иерархии, переводя тип и значение по умолчанию между Verilog и VHDL.
python3 hdl-param-propagate.py hdl axi_usb2_device_v5_0_32_usbcore SELF_POWERED integer 0 --stop-at axi_usb2_device python3 hdl-param-propagate.py hdl axi_usb2_device_v5_0_32_hsic_usbcore SELF_POWERED integer 0 --stop-at axi_usb2_device
--stop-at останавливает подъём на модуле верхнего уровня ядра, а с флагом -n утилита только показывает diff.
Важная оговорка. По спецификации USB 2.0 (раздел 7.1.5) источник подтяжки D+ должен зависеть от VBUS кабеля: если VBUS нет, устройство не должно подавать ток в линию данных. С SELF_POWERED = 1 ядро считает VBUS присутствующим всегда и подключает подтяжку независимо от кабеля.
Игнорировать VBUS допустимо, когда устройство и хост гарантированно включаются и выключаются вместе или кабель не отключается в работе:
стендовая проверка платы, как в этой статье;
USB-связь внутри одного корпуса: плата с FPGA подключена к встроенному компьютеру постоянным кабелем или шлейфом;
платы, где VBUS разъёма физически не заведён на PHY и переделка невозможна, а сценарий использования известен заранее.
Определять VBUS по-настоящему нужно, если плата питается сама, а кабель к хосту могут отключить или подключить в любой момент — то есть почти в любом самостоятельном изделии. Без этого возникают две проблемы:
когда хост выключен, а плата работает, подтяжка D+ подаёт ток в порт хоста через его защитные диоды — это прямо запрещено спецификацией;
ядро не узнаёт об отключении кабеля: для него это выглядит как переход шины в режим suspend, и программа не может отличить «кабель вынули» от «хост уснул».
Как добавить параметр в Xilinx IP
Поменять HDL мало. Чтобы параметр появился в окне настройки ядра в IP Integrator и дошёл до generic, нужно править три вещи.
1. XGUI-скрипт
XGUI — Tcl-скрипт, который Vivado выполняет при открытии окна настройки ядра. Для AXI USB2 Device это xgui/axi_usb2_device_v5_0.tcl. В init_gui добавляется флажок, а отдельная процедура передаёт значение из параметра GUI в параметр модели (то есть в generic HDL):
proc init_gui { IPINST } { # ... существующие параметры ... set SELF_POWERED [ipgui::add_param $IPINST -parent $Page0 -name SELF_POWERED -widget checkBox] set_property display_name "Self Powered" $SELF_POWERED set_property tooltip "Ignore VBUS state reported by the ULPI PHY" $SELF_POWERED } proc update_MODELPARAM_VALUE.SELF_POWERED {MODELPARAM_VALUE.SELF_POWERED PARAM_VALUE.SELF_POWERED} { set_property value [get_property value ${PARAM_VALUE.SELF_POWERED}] ${MODELPARAM_VALUE.SELF_POWERED} }
Если сделать только это, Vivado при открытии настроек ядра выдаст:
WARNING: [IP_Flow 19-469] Gui parameter('SELF_POWERED') creation is incomplete. Corresponding component parameter does not exist ERROR: [Ipptcl 7-17] Invalid Parameter 'SELF_POWERED'.
GUI ссылается на параметр, которого нет в описании компонента.
2. component.xml
component.xml — описание ядра в формате IP-XACT (стандарт IEEE 1685). В нём нужны две записи. Пользовательский параметр — то, что видно в GUI и хранится в настройках экземпляра:
<spirit:parameter> <spirit:name>SELF_POWERED</spirit:name> <spirit:displayName>Self Powered</spirit:displayName> <spirit:value spirit:format="long" spirit:resolve="user" spirit:id="PARAM_VALUE.SELF_POWERED" spirit:choiceRef="choice_pairs_4873554b" spirit:order="18">0</spirit:value> <spirit:vendorExtensions> <xilinx:parameterInfo> <xilinx:enablement> <xilinx:isEnabled xilinx:resolve="generated" xilinx:id="PARAM_ENABLEMENT.SELF_POWERED">true</xilinx:isEnabled> </xilinx:enablement> </xilinx:parameterInfo> </spirit:vendorExtensions> </spirit:parameter>
choiceRef ссылается на уже существующий в этом файле список «false = 0, true = 1», которым пользуются остальные флажки ядра.
Параметр модели — то, что Vivado подставит в generic при генерации обёртки:
<spirit:modelParameter spirit:dataType="integer"> <spirit:name>SELF_POWERED</spirit:name> <spirit:value spirit:format="long" spirit:resolve="generated" spirit:id="MODELPARAM_VALUE.SELF_POWERED" spirit:minimum="0" spirit:maximum="1" spirit:rangeType="long">0</spirit:value> </spirit:modelParameter>
3. Контрольные суммы
В component.xml хранятся контрольные суммы каждого представления (viewChecksum) и разделов описания (xilinx:checksum в packagingInfo). Алгоритм не документирован, поэтому пересчитывать суммы проще всего самим Vivado:
ipx::open_core /path/to/Vivado/2024.1/data/ip/xilinx/axi_usb2_device_v5_0/component.xml ipx::update_checksums [ipx::current_core] ipx::save_core [ipx::current_core] ipx::unload_core [ipx::current_core]
ipx::save_core перезаписывает файл целиком и обновляет дату coreCreationDateTime, номер ревизии ядра при этом не меняется.
Новые суммы — это сигнал для Vivado, что определение ядра изменилось. Экземпляр в существующем блок-дизайне помечается как locked, и его нужно обновить:
upgrade_ip [get_ips design_1_axi_usb2_device_0_0]
После этого флажок появляется в настройках ячейки, а если его включить, в сгенерированной обёртке ядра будет SELF_POWERED => 1.
Проект одним Tcl-скриптом
Проект целиком воспроизводится скриптом create_project.tcl. Основу дала команда write_bd_tcl, но её вывод пришлось заметно почистить.
Во-первых, write_bd_tcl жёстко прописывает версии всех ядер (xilinx.com:ip:microblaze:11.0) и проверяет версию Vivado. Вместо этого версия ищется в каталоге:
proc ip_vlnv {name} { set defs [get_ipdefs -all xilinx.com:ip:${name}:*] if {$defs eq ""} { error "IP xilinx.com:ip:${name} not found in the IP catalog" } return [lindex [lsort -dictionary $defs] end] } set axi_usb2_device_0 [create_bd_cell -type ip -vlnv [ip_vlnv axi_usb2_device] axi_usb2_device_0] if {[lsearch -exact [list_property $axi_usb2_device_0] CONFIG.SELF_POWERED] >= 0} { set_property CONFIG.SELF_POWERED {1} $axi_usb2_device_0 } else { puts "WARNING: axi_usb2_device has no SELF_POWERED parameter, the core will wait for VBUS from the PHY" }
Параметр SELF_POWERED ставится, только если он есть у ядра. На стандартном, недоработанном ядре скрипт выводит предупреждение и собирает проект дальше — но такое ядро будет ждать VBUS от PHY.
lsort -dictionary правильно сравнивает числа внутри строк, поэтому 11.0 окажется после 9.0. Оговорка: в другой версии Vivado у ядра может поменяться набор параметров, и какой-нибудь set_property CONFIG.… придётся поправить. Версии интерфейсов (xilinx.com:interface:ulpi_rtl:1.0) я оставил — они практически не меняются.
Во-вторых, wrap.sv и constraint.xdc встроены прямо в скрипт: он сам записывает их в директорию проекта, так что весь проект — это один файл.
В-третьих, в конце скрипт запускает синтез, разводку и генерацию битстрима, проверяет результат каждого запуска и экспортирует аппаратную платформу для Vitis — файл XSA (Xilinx Support Archive) с описанием аппаратуры и битстримом:
proc run_and_check {run} { wait_on_run $run if {[get_property PROGRESS [get_runs $run]] ne "100%"} { error "$run failed: [get_property STATUS [get_runs $run]]" } puts "$run: [get_property STATUS [get_runs $run]]" } launch_runs synth_1 -jobs $jobs run_and_check synth_1 launch_runs impl_1 -to_step write_bitstream -jobs $jobs run_and_check impl_1 open_run impl_1 write_hw_platform -fixed -include_bit -force $proj_dir/wrap.xsa
Запуск:
/path/to/Vivado/2024.1/bin/vivado -mode batch -source create_project.tcl
Полный прогон занимает около 10 минут. Временные ограничения выполняются: в моих прогонах худший запас по setup (WNS, Worst Negative Slack) был около +0,9 нс, по hold (WHS) — положительный, но небольшой, +0,03…0,06 нс.
create_project.tcl целиком
set proj_name example set proj_dir [file normalize ./$proj_name] if {[llength $argv] > 0} { set proj_dir [file normalize [lindex $argv 0]] } create_project $proj_name $proj_dir -part xc7a200tfbg676-2 set_property -dict [list \ default_lib xil_defaultlib \ target_language Verilog \ simulator_language Mixed \ enable_vhdl_2008 1 \ ] [current_project] proc ip_vlnv {name} { set defs [get_ipdefs -all xilinx.com:ip:${name}:*] if {$defs eq ""} { error "IP xilinx.com:ip:${name} not found in the IP catalog" } return [lindex [lsort -dictionary $defs] end] } proc create_local_memory {name} { set old_inst [current_bd_instance .] current_bd_instance [create_bd_cell -type hier $name] create_bd_intf_pin -mode MirroredMaster -vlnv xilinx.com:interface:lmb_rtl:1.0 DLMB create_bd_intf_pin -mode MirroredMaster -vlnv xilinx.com:interface:lmb_rtl:1.0 ILMB create_bd_pin -dir I -type clk LMB_Clk create_bd_pin -dir I -type rst SYS_Rst create_bd_cell -type ip -vlnv [ip_vlnv lmb_v10] dlmb_v10 create_bd_cell -type ip -vlnv [ip_vlnv lmb_v10] ilmb_v10 set dlmb_bram_if_cntlr [create_bd_cell -type ip -vlnv [ip_vlnv lmb_bram_if_cntlr] dlmb_bram_if_cntlr] set_property CONFIG.C_ECC {0} $dlmb_bram_if_cntlr set ilmb_bram_if_cntlr [create_bd_cell -type ip -vlnv [ip_vlnv lmb_bram_if_cntlr] ilmb_bram_if_cntlr] set_property CONFIG.C_ECC {0} $ilmb_bram_if_cntlr set lmb_bram [create_bd_cell -type ip -vlnv [ip_vlnv blk_mem_gen] lmb_bram] set_property -dict [list \ CONFIG.Memory_Type {True_Dual_Port_RAM} \ CONFIG.use_bram_block {BRAM_Controller} \ ] $lmb_bram connect_bd_intf_net -intf_net microblaze_0_dlmb \ [get_bd_intf_pins dlmb_v10/LMB_M] \ [get_bd_intf_pins DLMB] connect_bd_intf_net -intf_net microblaze_0_dlmb_bus \ [get_bd_intf_pins dlmb_v10/LMB_Sl_0] \ [get_bd_intf_pins dlmb_bram_if_cntlr/SLMB] connect_bd_intf_net -intf_net microblaze_0_dlmb_cntlr \ [get_bd_intf_pins dlmb_bram_if_cntlr/BRAM_PORT] \ [get_bd_intf_pins lmb_bram/BRAM_PORTA] connect_bd_intf_net -intf_net microblaze_0_ilmb \ [get_bd_intf_pins ilmb_v10/LMB_M] \ [get_bd_intf_pins ILMB] connect_bd_intf_net -intf_net microblaze_0_ilmb_bus \ [get_bd_intf_pins ilmb_v10/LMB_Sl_0] \ [get_bd_intf_pins ilmb_bram_if_cntlr/SLMB] connect_bd_intf_net -intf_net microblaze_0_ilmb_cntlr \ [get_bd_intf_pins ilmb_bram_if_cntlr/BRAM_PORT] \ [get_bd_intf_pins lmb_bram/BRAM_PORTB] connect_bd_net -net SYS_Rst_1 \ [get_bd_pins SYS_Rst] \ [get_bd_pins dlmb_v10/SYS_Rst] \ [get_bd_pins dlmb_bram_if_cntlr/LMB_Rst] \ [get_bd_pins ilmb_v10/SYS_Rst] \ [get_bd_pins ilmb_bram_if_cntlr/LMB_Rst] connect_bd_net -net microblaze_0_Clk \ [get_bd_pins LMB_Clk] \ [get_bd_pins dlmb_v10/LMB_Clk] \ [get_bd_pins dlmb_bram_if_cntlr/LMB_Clk] \ [get_bd_pins ilmb_v10/LMB_Clk] \ [get_bd_pins ilmb_bram_if_cntlr/LMB_Clk] current_bd_instance $old_inst } create_bd_design design_1 current_bd_design design_1 create_bd_intf_port -mode Slave -vlnv xilinx.com:interface:ulpi_rtl:1.0 ULPI_0 create_bd_port -dir I -type clk -freq_hz 50000000 clk_50MHz set ext_reset_in [create_bd_port -dir I -type rst ext_reset_in] set_property CONFIG.POLARITY {ACTIVE_HIGH} $ext_reset_in create_bd_port -dir O -type clk clk_24_o set microblaze_0 [create_bd_cell -type ip -vlnv [ip_vlnv microblaze] microblaze_0] set_property -dict [list \ CONFIG.C_DEBUG_ENABLED {1} \ CONFIG.C_D_AXI {1} \ CONFIG.C_D_LMB {1} \ CONFIG.C_ENABLE_CONVERSION {0} \ CONFIG.C_I_LMB {1} \ ] $microblaze_0 set clk_wiz_0 [create_bd_cell -type ip -vlnv [ip_vlnv clk_wiz] clk_wiz_0] set_property -dict [list \ CONFIG.CLKOUT1_JITTER {139.128} \ CONFIG.CLKOUT1_PHASE_ERROR {154.678} \ CONFIG.CLKOUT2_JITTER {202.151} \ CONFIG.CLKOUT2_PHASE_ERROR {154.678} \ CONFIG.CLKOUT2_REQUESTED_OUT_FREQ {24.000} \ CONFIG.CLKOUT2_USED {true} \ CONFIG.MMCM_CLKFBOUT_MULT_F {24.000} \ CONFIG.MMCM_CLKOUT0_DIVIDE_F {12.000} \ CONFIG.MMCM_CLKOUT1_DIVIDE {50} \ CONFIG.NUM_OUT_CLKS {2} \ CONFIG.USE_RESET {false} \ ] $clk_wiz_0 create_local_memory microblaze_0_local_memory set microblaze_0_axi_periph [create_bd_cell -type ip -vlnv [ip_vlnv axi_interconnect] microblaze_0_axi_periph] set_property CONFIG.NUM_MI {2} $microblaze_0_axi_periph set microblaze_0_axi_intc [create_bd_cell -type ip -vlnv [ip_vlnv axi_intc] microblaze_0_axi_intc] set_property CONFIG.C_HAS_FAST {1} $microblaze_0_axi_intc set microblaze_0_xlconcat [create_bd_cell -type ip -vlnv [ip_vlnv xlconcat] microblaze_0_xlconcat] set_property CONFIG.NUM_PORTS {1} $microblaze_0_xlconcat create_bd_cell -type ip -vlnv [ip_vlnv mdm] mdm_1 create_bd_cell -type ip -vlnv [ip_vlnv proc_sys_reset] rst_clk_wiz_0_100M set system_ila_0 [create_bd_cell -type ip -vlnv [ip_vlnv system_ila] system_ila_0] set_property -dict [list \ CONFIG.C_ADV_TRIGGER {true} \ CONFIG.C_DATA_DEPTH {4096} \ CONFIG.C_EN_STRG_QUAL {1} \ CONFIG.C_MON_TYPE {MIX} \ CONFIG.C_TRIGIN_EN {false} \ CONFIG.C_TRIGOUT_EN {false} \ ] $system_ila_0 set axi_usb2_device_0 [create_bd_cell -type ip -vlnv [ip_vlnv axi_usb2_device] axi_usb2_device_0] if {[lsearch -exact [list_property $axi_usb2_device_0] CONFIG.SELF_POWERED] >= 0} { set_property CONFIG.SELF_POWERED {1} $axi_usb2_device_0 } else { puts "WARNING: axi_usb2_device has no SELF_POWERED parameter, the core will wait for VBUS from the PHY" } connect_bd_intf_net -intf_net ULPI_0_1 \ [get_bd_intf_ports ULPI_0] \ [get_bd_intf_pins axi_usb2_device_0/ULPI] connect_bd_intf_net -intf_net microblaze_0_axi_dp \ [get_bd_intf_pins microblaze_0_axi_periph/S00_AXI] \ [get_bd_intf_pins microblaze_0/M_AXI_DP] connect_bd_intf_net -intf_net [get_bd_intf_nets microblaze_0_axi_dp] \ [get_bd_intf_pins microblaze_0_axi_periph/S00_AXI] \ [get_bd_intf_pins system_ila_0/SLOT_0_AXI] connect_bd_intf_net -intf_net microblaze_0_axi_periph_M01_AXI \ [get_bd_intf_pins axi_usb2_device_0/S_AXI] \ [get_bd_intf_pins microblaze_0_axi_periph/M01_AXI] connect_bd_intf_net -intf_net microblaze_0_debug \ [get_bd_intf_pins mdm_1/MBDEBUG_0] \ [get_bd_intf_pins microblaze_0/DEBUG] connect_bd_intf_net -intf_net microblaze_0_dlmb_1 \ [get_bd_intf_pins microblaze_0/DLMB] \ [get_bd_intf_pins microblaze_0_local_memory/DLMB] connect_bd_intf_net -intf_net microblaze_0_ilmb_1 \ [get_bd_intf_pins microblaze_0/ILMB] \ [get_bd_intf_pins microblaze_0_local_memory/ILMB] connect_bd_intf_net -intf_net microblaze_0_intc_axi \ [get_bd_intf_pins microblaze_0_axi_periph/M00_AXI] \ [get_bd_intf_pins microblaze_0_axi_intc/s_axi] connect_bd_intf_net -intf_net microblaze_0_interrupt \ [get_bd_intf_pins microblaze_0_axi_intc/interrupt] \ [get_bd_intf_pins microblaze_0/INTERRUPT] connect_bd_net -net axi_usb2_device_0_usb_irpt \ [get_bd_pins axi_usb2_device_0/usb_irpt] \ [get_bd_pins microblaze_0_xlconcat/In0] \ [get_bd_pins system_ila_0/probe0] connect_bd_net -net clk_100MHz_1 \ [get_bd_ports clk_50MHz] \ [get_bd_pins clk_wiz_0/clk_in1] connect_bd_net -net clk_wiz_0_clk_out2 \ [get_bd_pins clk_wiz_0/clk_out2] \ [get_bd_ports clk_24_o] connect_bd_net -net clk_wiz_0_locked \ [get_bd_pins clk_wiz_0/locked] \ [get_bd_pins rst_clk_wiz_0_100M/dcm_locked] connect_bd_net -net mdm_1_debug_sys_rst \ [get_bd_pins mdm_1/Debug_SYS_Rst] \ [get_bd_pins rst_clk_wiz_0_100M/mb_debug_sys_rst] connect_bd_net -net microblaze_0_Clk \ [get_bd_pins clk_wiz_0/clk_out1] \ [get_bd_pins microblaze_0/Clk] \ [get_bd_pins microblaze_0_axi_periph/ACLK] \ [get_bd_pins microblaze_0_axi_periph/S00_ACLK] \ [get_bd_pins microblaze_0_axi_periph/M00_ACLK] \ [get_bd_pins microblaze_0_axi_periph/M01_ACLK] \ [get_bd_pins microblaze_0_axi_intc/s_axi_aclk] \ [get_bd_pins microblaze_0_axi_intc/processor_clk] \ [get_bd_pins microblaze_0_local_memory/LMB_Clk] \ [get_bd_pins rst_clk_wiz_0_100M/slowest_sync_clk] \ [get_bd_pins system_ila_0/clk] \ [get_bd_pins axi_usb2_device_0/s_axi_aclk] connect_bd_net -net microblaze_0_intr \ [get_bd_pins microblaze_0_xlconcat/dout] \ [get_bd_pins microblaze_0_axi_intc/intr] connect_bd_net -net reset_rtl_0_1 \ [get_bd_ports ext_reset_in] \ [get_bd_pins rst_clk_wiz_0_100M/ext_reset_in] connect_bd_net -net rst_clk_wiz_0_100M_bus_struct_reset \ [get_bd_pins rst_clk_wiz_0_100M/bus_struct_reset] \ [get_bd_pins microblaze_0_local_memory/SYS_Rst] connect_bd_net -net rst_clk_wiz_0_100M_mb_reset \ [get_bd_pins rst_clk_wiz_0_100M/mb_reset] \ [get_bd_pins microblaze_0/Reset] \ [get_bd_pins microblaze_0_axi_intc/processor_rst] connect_bd_net -net rst_clk_wiz_0_100M_peripheral_aresetn \ [get_bd_pins rst_clk_wiz_0_100M/peripheral_aresetn] \ [get_bd_pins microblaze_0_axi_periph/ARESETN] \ [get_bd_pins microblaze_0_axi_periph/S00_ARESETN] \ [get_bd_pins microblaze_0_axi_periph/M00_ARESETN] \ [get_bd_pins microblaze_0_axi_periph/M01_ARESETN] \ [get_bd_pins microblaze_0_axi_intc/s_axi_aresetn] \ [get_bd_pins system_ila_0/resetn] \ [get_bd_pins axi_usb2_device_0/s_axi_aresetn] assign_bd_address -force -offset 0x44800000 -range 0x00010000 \ -target_address_space [get_bd_addr_spaces microblaze_0/Data] \ [get_bd_addr_segs axi_usb2_device_0/S_AXI/Reg] assign_bd_address -force -offset 0x00000000 -range 0x00020000 \ -target_address_space [get_bd_addr_spaces microblaze_0/Data] \ [get_bd_addr_segs microblaze_0_local_memory/dlmb_bram_if_cntlr/SLMB/Mem] assign_bd_address -force -offset 0x41200000 -range 0x00010000 \ -target_address_space [get_bd_addr_spaces microblaze_0/Data] \ [get_bd_addr_segs microblaze_0_axi_intc/S_AXI/Reg] assign_bd_address -force -offset 0x00000000 -range 0x00020000 \ -target_address_space [get_bd_addr_spaces microblaze_0/Instruction] \ [get_bd_addr_segs microblaze_0_local_memory/ilmb_bram_if_cntlr/SLMB/Mem] validate_bd_design save_bd_design set wrapper_path [make_wrapper -fileset sources_1 -files [get_files -norecurse design_1.bd] -top] add_files -norecurse -fileset sources_1 $wrapper_path proc write_file {path content} { file mkdir [file dirname $path] set f [open $path w] puts -nonewline $f [string trimleft $content "\n"] close $f } set src_dir $proj_dir/src write_file $src_dir/wrap.sv { module wrap ( (* IOSTANDARD = "LVCMOS33", LOC="M21" *) input logic gen50_i, // USB3300 (* IOSTANDARD = "LVCMOS33", LOC="N2" *) output logic ulpi_xtail_in_o, (* IOSTANDARD = "LVCMOS33", LOC="D18" *) input logic ulpi_clkout_i, (* IOSTANDARD = "LVCMOS33", LOC="F15" *) input logic ulpi_nxt_i, (* IOSTANDARD = "LVCMOS33", LOC="G19" *) input logic ulpi_dir_i, (* IOSTANDARD = "LVCMOS33", SLEW = "FAST", LOC="F20" *) output logic ulpi_stp_o, (* IOSTANDARD = "LVCMOS33", SLEW = "FAST", LOC="J14" *) inout logic ulpi_data0_io, (* IOSTANDARD = "LVCMOS33", SLEW = "FAST", LOC="J15" *) inout logic ulpi_data1_io, (* IOSTANDARD = "LVCMOS33", SLEW = "FAST", LOC="J16" *) inout logic ulpi_data2_io, (* IOSTANDARD = "LVCMOS33", SLEW = "FAST", LOC="H14" *) inout logic ulpi_data3_io, (* IOSTANDARD = "LVCMOS33", SLEW = "FAST", LOC="H15" *) inout logic ulpi_data4_io, (* IOSTANDARD = "LVCMOS33", SLEW = "FAST", LOC="H16" *) inout logic ulpi_data5_io, (* IOSTANDARD = "LVCMOS33", SLEW = "FAST", LOC="G15" *) inout logic ulpi_data6_io, (* IOSTANDARD = "LVCMOS33", SLEW = "FAST", LOC="G16" *) inout logic ulpi_data7_io ); logic clk_24; ODDR #( .DDR_CLK_EDGE("OPPOSITE_EDGE"), .INIT (1'b0), .SRTYPE ("SYNC") ) u_oddr_0 ( .Q (ulpi_xtail_in_o), .C (clk_24), .CE('1), .D1('0), .D2('1), .R ('0), .S ('0) ); design_1_wrapper u_design_1_wrapper_0 ( .clk_50MHz (gen50_i), .clk_24_o (clk_24), .ext_reset_in ('0), .ULPI_0_clk (ulpi_clkout_i), .ULPI_0_data_io({ulpi_data7_io, ulpi_data6_io, ulpi_data5_io, ulpi_data4_io, ulpi_data3_io, ulpi_data2_io, ulpi_data1_io, ulpi_data0_io}), .ULPI_0_dir (ulpi_dir_i), .ULPI_0_next (ulpi_nxt_i), .ULPI_0_rst (), .ULPI_0_stop (ulpi_stp_o) ); endmodule } write_file $src_dir/constraint.xdc { set_property BITSTREAM.CONFIG.CONFIGRATE 40 [current_design] set_property BITSTREAM.CONFIG.SPI_BUSWIDTH 4 [current_design] set_property CONFIG_VOLTAGE 3.3 [current_design] set_property CFGBVS VCCO [current_design] create_clock -name ulpi_clock -period 16.667 [get_nets ulpi_clkout_i] set ulpi_input {ulpi_dir_i ulpi_nxt_i} set ulpi_output {ulpi_stp_o} set_input_delay -max 4.5 -clock ulpi_clock $ulpi_input set_output_delay -max 7 -clock ulpi_clock $ulpi_output set_max_delay 24 -from [get_ports ulpi_dir_i] -to [get_ports {ulpi_data0_io ulpi_data1_io ulpi_data2_io ulpi_data3_io ulpi_data4_io ulpi_data5_io ulpi_data6_io ulpi_data7_io}] -datapath_only } add_files -norecurse -fileset sources_1 $src_dir/wrap.sv add_files -norecurse -fileset constrs_1 $src_dir/constraint.xdc set_property top wrap [get_filesets sources_1] update_compile_order -fileset sources_1 set jobs 4 catch {set jobs [exec nproc]} proc run_and_check {run} { wait_on_run $run if {[get_property PROGRESS [get_runs $run]] ne "100%"} { error "$run failed: [get_property STATUS [get_runs $run]]" } puts "$run: [get_property STATUS [get_runs $run]]" } launch_runs synth_1 -jobs $jobs run_and_check synth_1 launch_runs impl_1 -to_step write_bitstream -jobs $jobs run_and_check impl_1 set impl [get_runs impl_1] puts "WNS=[get_property STATS.WNS $impl] TNS=[get_property STATS.TNS $impl]\ WHS=[get_property STATS.WHS $impl] THS=[get_property STATS.THS $impl]" open_run impl_1 write_hw_platform -fixed -include_bit -force $proj_dir/wrap.xsa puts "XSA: $proj_dir/wrap.xsa"
Программа для MicroBlaze
Vitis 2024.1 ещё поставляется с классическим XSCT (Xilinx Software Command-line Tool), им и воспользуемся. Скрипт create_vitis.tcl:
создаёт платформу из XSA: процессор
microblaze_0, standalone;создаёт пустое C-приложение и копирует в него файлы примера USB Mass Storage из драйвера
usb_v*(версия драйвера берётся последняя из установки);уменьшает RAM-диск;
собирает конфигурацию Release;
вписывает ELF в битстрим (
updatemem) и собирает образboot.mcsдля SPI-флеша (write_cfgmem).
set drivers $::env(XILINX_VITIS)/data/embeddedsw/XilinxProcessorIPLib/drivers set usb_drv [lindex [lsort -dictionary [glob -directory $drivers -type d usb_v*]] end] set examples $usb_drv/examples setws $ws platform create -name board -hw $xsa -proc microblaze_0 -os standalone -out $ws platform write platform generate app create -name xusb_storage -platform board -template {Empty Application(C)} foreach f {xusb_storage.c xusb_storage.h xusb_cp9.c xusb_cp9.h xusb_types.h} { importsources -name xusb_storage -path $examples/$f }
Про RAM-диск. В примере он объявлен как u32 RamDisk[RAMDISKSECTORS][RAMBLOCKS] с RAMDISKSECTORS = 0x400 и RAMBLOCKS = 128, то есть 1024 блока по 512 байт — 512 КБ. В 128 КБ локальной памяти MicroBlaze это не помещается.
Уменьшать здесь нужно именно число блоков, а не RAMBLOCKS. Программа читает и пишет каждый блок по адресу &RamDisk[LBA][0] ровно по 512 байт, а хосту объявляет ёмкость в RAMDISKSECTORS блоков. Если уменьшить RAMBLOCKS, строка массива станет короче 512 байт: соседние блоки начнут затирать друг друга, а запись в последние блоки уйдёт за конец массива. Хост при этом увидит диск прежнего размера, и ошибка проявится только при записи. Поэтому оставляем RAMBLOCKS = 128 и уменьшаем RAMDISKSECTORS до 0x80 — 128 блоков, 64 КБ. Скрипт меняет это значение в xusb_storage.h сам.
Итоговый ELF занимает около 90 КБ, большую часть — сам RAM-диск в .bss.
Запуск:
/path/to/Vitis/2024.1/bin/xsct create_vitis.tcl ./example/wrap.xsa ./vitis
create_vitis.tcl целиком
set xsa [file normalize [expr {[llength $argv] > 0 ? [lindex $argv 0] : "./example/wrap.xsa"}]] set ws [file normalize [expr {[llength $argv] > 1 ? [lindex $argv 1] : "./vitis"}]] set platform_name board set app_name xusb_storage if {![file isfile $xsa]} { error "XSA not found: $xsa" } set drivers $::env(XILINX_VITIS)/data/embeddedsw/XilinxProcessorIPLib/drivers set usb_drv [lindex [lsort -dictionary [glob -directory $drivers -type d usb_v*]] end] set examples $usb_drv/examples file mkdir $ws setws $ws platform create -name $platform_name -hw $xsa -proc microblaze_0 -os standalone -out $ws platform write platform generate app create -name $app_name -platform $platform_name -template {Empty Application(C)} foreach f {xusb_storage.c xusb_storage.h xusb_cp9.c xusb_cp9.h xusb_types.h} { importsources -name $app_name -path $examples/$f } set hdr $ws/$app_name/src/xusb_storage.h set f [open $hdr r] set text [read $f] close $f if {[regsub {(#define\s+RAMDISKSECTORS\s+)0x[0-9A-Fa-f]+} $text {\10x80} text] != 1} { error "RAMDISKSECTORS not found in $hdr" } set f [open $hdr w] puts -nonewline $f $text close $f app config -name $app_name build-config release app build -name $app_name set elf $ws/$app_name/Release/$app_name.elf puts "ELF: $elf" set hw_dir $ws/$platform_name/hw set bit [lindex [glob -directory $hw_dir *.bit] 0] set mmi [lindex [glob -directory $hw_dir *.mmi] 0] set f [open $mmi r] set mmi_text [read $f] close $f if {![regexp {<Processor[^>]*InstPath="([^"]+)"} $mmi_text -> proc_path]} { error "processor not found in $mmi" } set boot_bit $ws/boot.bit set boot_mcs $ws/boot.mcs exec $::env(XILINX_VITIS)/bin/updatemem -force -meminfo $mmi -bit $bit \ -data $elf -proc $proc_path -out $boot_bit >@ stdout 2>@ stderr puts "BIT: $boot_bit" set vivado_root [file dirname [file dirname $::env(XILINX_VITIS)]]/Vivado/[file tail $::env(XILINX_VITIS)] set cfgmem_tcl $ws/write_cfgmem.tcl set f [open $cfgmem_tcl w] puts $f "write_cfgmem -force -format mcs -interface SPIx4 -size 128 -loadbit {up 0x0 $boot_bit} -file $boot_mcs" close $f exec $vivado_root/bin/vivado -mode batch -nojournal -nolog -source $cfgmem_tcl >@ stdout 2>@ stderr file delete $cfgmem_tcl puts "MCS: $boot_mcs"
Проверка
Загружаем boot.bit через JTAG или прошиваем boot.mcs во флеш через Hardware Manager и подключаем плату к компьютеру. На хосте с Linux устройство должно появиться с VID 0x03FD (Xilinx) и PID 0x0100:
lsusb -v -d 03fd:0100
В выводе стоит проверить:
в дескрипторе конфигурации
bmAttributes 0xc0с пометкойSelf PoweredиMaxPower 2mA;Device Status: 0x0001с пометкойSelf Powered— это ответ наGET_STATUS; эта строка выводится только при запускеlsusbот root.
Bus 003 Device 012: ID 03fd:0100 Xilinx, Inc. FPGA1 Negotiated speed: High Speed (480Mbps) Device Descriptor: bcdUSB 2.00 idVendor 0x03fd Xilinx, Inc. idProduct 0x0100 FPGA1 iManufacturer 1 XILINX iProduct 2 FPGA1 Configuration Descriptor: bmAttributes 0xc0 Self Powered MaxPower 2mA Interface Descriptor: bInterfaceClass 8 Mass Storage bInterfaceSubClass 6 SCSI bInterfaceProtocol 80 Bulk-Only Endpoint Descriptor: bEndpointAddress 0x81 EP 1 IN Transfer Type Bulk wMaxPacketSize 0x0200 1x 512 bytes Endpoint Descriptor: bEndpointAddress 0x02 EP 2 OUT Transfer Type Bulk wMaxPacketSize 0x0200 1x 512 bytes
Устройство договорилось с хостом на High Speed (480 Мбит/с) — значит, ULPI и тактирование PHY работают на полной скорости.
В dmesg должно быть видно, что ядро опознало устройство как USB Mass Storage и создало блочное устройство:
usb 3-1.4: new high-speed USB device number 16 using xhci_hcd usb 3-1.4: New USB device found, idVendor=03fd, idProduct=0100, bcdDevice= 1.00 usb 3-1.4: Product: FPGA1 usb 3-1.4: Manufacturer: XILINX usb-storage 3-1.4:1.0: USB Mass Storage device detected scsi host1: usb-storage 3-1.4:1.0 scsi 1:0:0:0: Direct-Access Xilinx SMSC Flash Demo 0001 PQ: 0 ANSI: 0 sd 1:0:0:0: [sdb] 128 512-byte logical blocks: (65.5 kB/64.0 KiB) sd 1:0:0:0: [sdb] Write Protect is off sd 1:0:0:0: [sdb] Attached SCSI removable disk
Размер диска — 128 блоков по 512 байт, ровно столько, сколько задано через RAMDISKSECTORS.
Осталось проверить передачу данных в обе стороны. Файловая система для этого не нужна: запишем на диск 64 КБ случайных данных напрямую, прочитаем их обратно и сравним. Перед этим стоит убедиться, что /dev/sdb — это именно плата (в колонке MODEL — SMSC Flash Demo), иначе dd перезапишет другой диск:
lsblk -o NAME,SIZE,MODEL /dev/sdb head -c 65536 /dev/urandom > /tmp/pat.bin sudo dd if=/tmp/pat.bin of=/dev/sdb bs=512 oflag=direct sudo dd if=/dev/sdb of=/tmp/back.bin bs=512 count=128 iflag=direct cmp /tmp/pat.bin /tmp/back.bin && echo READBACK_OK
oflag=direct и iflag=direct заставляют dd обходить кэш ядра, так что данные действительно уходят на плату и возвращаются с неё. Прочитанные 65 536 байт совпали с записанными байт в байт — значит, все части тракта работают: ULPI, тактирование, PHY и разводка платы.
Обратите внимание: RAM-диск живёт в BRAM, поэтому после перезагрузки платы его содержимое пропадает.
Итог
Ядро AXI USB2 Device по умолчанию рассчитано на то, что PHY видит VBUS. На self-powered плате это не всегда так, и тогда ядро молча ждёт кабеля, который давно подключён. Параметр SELF_POWERED снимает эту зависимость, а чтобы он стал полноценным параметром IP, понадобились флажок в XGUI, две записи в component.xml и пересчёт контрольных сумм силами самого Vivado.
