FPGA as USB Self-Powered device

в 9:30, , рубрики: fpga, usb, vivado

Мне досталась плата: 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 {(#defines+RAMDISKSECTORSs+)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.

Автор: johnylemonade

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js