PunchKwara doctors lament poor pay, illegal salary deductionsEl Comercio¿Meta favorece a Flávio Bolsonaro y perjudica a Lula? Las claves de la polémica electoral en BrasilRTP DesportoMotoGP: terminou a temporada para RazgatliogluESPNTransfer rumors, news: Premier League clubs eye Alexander-Arnold amid Real Madrid uncertaintyThe Jerusalem PostMandelblit says he approved questioning Netanyahu as suspect, defense challenges wordingDaily MaverickHegseth meets Netanyahu in unannounced visit to Israel, source saysInquirerMarcos vows reforms to boost investor confidence, stock marketCollider‘Sense and Sensibility’ Officially Posts One of the Weekend’s Best Per-Theater AveragesObservador DesportoMoçambique. Supostos terroristas abatidos por fuzileirosSky TG24I titoli di Sky TG24 del 20 ottobre, edizione delle 8
The Daily Newsstand · Free, Always
Tuesday, October 20, 2026

FPGA as USB Self-Powered device

Translate

Мне досталась плата: 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 хранит описание ядра.

FPGA, ULPI, USB3300: VBUS не подключён

FPGA, ULPI, USB3300: VBUS не подключён

Всё проверялось в 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 — пригодится при отладке.

Блок-дизайн в Vivado IP Integrator

Блок-дизайн в Vivado IP Integrator

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 эта цепь есть — на рисунке она зачёркнута:

Схема включения USB3300, источник: USB3300 Data Sheet, Figure 7-1, © Microchip Technology Inc.

Схема включения USB3300, источник: USB3300 Data Sheet, Figure 7-1, © Microchip Technology Inc.

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:

  1. создаёт платформу из XSA: процессор microblaze_0, standalone;

  2. создаёт пустое C-приложение и копирует в него файлы примера USB Mass Storage из драйвера usb_v* (версия драйвера берётся последняя из установки);

  3. уменьшает RAM-диск;

  4. собирает конфигурацию Release;

  5. вписывает 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.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.