FPGA as USB Self-Powered device

Мне досталась плата: 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.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.