Program QSPI Flash Memory
Use Xilinx program_flash (part of Vivado / Vitis) to write the Yocto
boot images directly to on-board QSPI flash. For NAND flash, see
Program NAND Flash Memory, which uses the same workflow with a different
-flash_type and image.ub offset.
Prerequisites
Vivado / Vitis installed with
program_flashonPATH.JTAG cable connected and board powered on.
A completed Yocto build. Boot images are located at:
firmware/build/YoctoProjects/<your-target-dir>/linux/For the scripted recipe: the packaged Yocto tarball at
firmware/targets/<your-target-dir>/images/<full-name>.linux.tar.gz
Note
Set the correct boot mode before or after flashing using XSCT (see Change Boot Mode via XSCT in this how-to section).
Note
BuildYoctoProject.sh packages the FSBL built from the design’s own
.xsa as linux/zynqmp_fsbl.elf, so both the linux/ build output
and the tarball carry one. The scripted recipe picks it up automatically
with no -e; pass it to the manual recipe as
-fsbl linux/zynqmp_fsbl.elf.
Warning
Do not rely on the generic FSBL that Vitis bundles in
data/xicom/cfgmem/cfgmem_util/cfgmem_fsbl.zip (for this flash type,
zynqmp_qspi_dual_parallel_x8_fsbl.elf). It carries a reference PS/DDR
configuration rather than this board’s, so on a custom carrier it never
finishes bringing up DDR and program_flash gives up with:
Downloading FSBL...
Running FSBL...
ERROR: [Xicom 50-331] Timed out while waiting for FSBL to complete.
Problem in Initializing Hardware
Confirmed on the SLAC RFMC carrier, where the same run succeeds as soon as
the FSBL from the Yocto build is used instead. Tarballs produced before the
FSBL was packaged do not contain one, so for those pass -e explicitly,
pointing at
<YoctoProject>/build/tmp/deploy/images/zynqmp-user/fsbl-zynqmp-user.elf.
Warning
program_flash requires -flash_density for the dual_parallel
and dual_stacked configurations. Omitting it fails with
ERROR: Flash Density not specified. The value is the total size in
MB across the configuration, one of 16 32 64 128 256 512 1024 2048, as
in the tool’s own example: “Zynq MP (1GB QSPI Dual Parallel) …
-flash_density 1024”. It also selects the mini u-boot flash writer.
Every board this platform supports wires QSPI as Dual Parallel with a
x4 data mode, which is the qspi-x8-dual_parallel flash type. Only
the density differs. Read it from the parallel-memories property of the
&qspi node in the board’s device tree, which lists the size of each of
the two parallel devices:
Board |
|
per device |
|
|---|---|---|---|
ZCU208 / ZCU216 |
|
256 MB |
|
ZCU111 / ZCU670 |
|
256 MB |
|
SLAC RFMC carrier |
|
256 MB |
|
ZCU102 |
|
64 MB |
|
-d is the sum of the pair, so it is twice the per-device size. The 512
default therefore covers every supported board except the ZCU102, which needs
-d 128.
Note
The compatible comments in the Xilinx device trees are unreliable:
zynqmp-zcu208-revA.dts and zynqmp-zcu216-revA.dts both label the
MT25QU02G as 1Gb while zynqmp-zcu670-revB.dts labels the same part
2Gb, and the ZCU102 and ZCU111 trees both say 32MB. Trust
parallel-memories, not the comment.
Manual recipe
The following recipe targets a qspi-x8-dual_parallel configuration,
which is typical for Zynq UltraScale+ boards with dual-parallel QSPI
flash.
# Go to the Yocto project output directory
cd firmware/build/YoctoProjects/<your-target-dir>
# Define default parameters for QSPI
# -flash_density is the total size in MB across the pair and is required
# for the dual_parallel and dual_stacked configurations
default_parameter="\
-flash_type qspi-x8-dual_parallel \
-flash_density 512 \
-blank_check -verify"
# Program each boot image at its partition offset
program_flash -f linux/BOOT.BIN -offset 0x0000000 $default_parameter
program_flash -f linux/boot.scr -offset 0x3E80000 $default_parameter
program_flash -f linux/image.ub -offset 0x4000000 $default_parameter
Note
The image.ub offset above is the QSPI one (0x4000000, from
QSPI_FIT_IMAGE_OFFSET in shared/Yocto/zynqmp-user.conf). NAND
uses 0x4180000 instead; see Program NAND Flash Memory.
Scripted recipe (program_qspi_flash.sh)
The program_qspi_flash.sh script, provided in the
scripts/program_qspi_flash.sh of the platform repository, automates
the recipe above. Instead of a Yocto build directory it takes the packaged
.linux.tar.gz, extracts it to a scratch directory, and programs each
image at its partition offset.
firmware/submodules/axi-soc-ultra-plus-core/scripts/program_qspi_flash.sh \
-f firmware/targets/<your-target-dir>/images/<full-name>.linux.tar.gz
Replace <full-name> with the timestamped artifact name produced by
the Yocto build (schema:
<TargetName>-<PRJ_VERSION>-<YYYYMMDDHHMMSS>-<user>-<git-short-SHA>).
Options:
-f PATH– path to the.linux.tar.gz(required).-t FLASH_TYPE– value passed to-flash_type(defaultqspi-x8-dual_parallel). Must be aqspi-*type; the script rejectsnand-*types and points atprogram_nand_flash.sh, since theimage.uboffset it programs is fixed at the QSPI one.-d DENSITY– total flash density in MB passed to-flash_density(default512). Passed only for thedual_parallel/dual_stackedconfigurations that require it, or whenever-dis given explicitly.-e PATH– FSBL.elfpassed to-fsbl. Defaults tolinux/zynqmp_fsbl.elffrom the tarball, whichBuildYoctoProject.shpackages, so this is normally not needed. Only when the tarball predates that packaging does the script fall back to the generic Vitis FSBL, which does not work on a custom carrier (see the FSBL warning under Prerequisites).-n– skip the-blank_check -verifyQA/QC pass, which is on by default.-verifyreads the payload back and compares it against the image, the only positive proof the dual-parallel addressing put the data where it belongs. The cost depends strongly on the Vitis release, so see the throughput warning below before assuming it is cheap.-c– accepted for compatibility and now redundant, since the checks it used to enable are the default. Use-nto turn them off.-F– program even if anhw_serveris already running. By default the script stops, becauseprogram_flashwould attach to that session and share its JTAG cable. Note thatkillVivadodoes not reaphw_server.-J– skip the JTAG boot mode step and flash the board as-is.-H– show the help text.
Throughput
Warning
Throughput varies by roughly two orders of magnitude between Vitis releases, so treat every figure below as specific to the release it was taken on, and re-measure before planning around it.
Release |
Write |
Read back |
|---|---|---|
Vitis 2026.1 |
~150 kB/s |
~103 kB/s |
Vitis 2025.2 |
~2.3 kB/s |
~1.35 kB/s |
Both measured over a Digilent JTAG-SMT3 at its default 15 MHz with
qspi-x8-dual_parallel. On 2026.1, a 1,949,892 byte BOOT.BIN
takes 76 s end to end: erase 1 s, blank_check 19 s, write 13 s,
verify 19 s. The checks cost about 38 s, which is why they are on by
default. On 2025.2 the same write is nearer 14 min with the checks
adding roughly 48 min on top, so pass -n there.
-a is expensive on any release, since image.ub is roughly 60x
larger than BOOT.BIN. At 2026.1 rates a full -a run extrapolates
to about 52 min, against roughly 14 min with -n. That extrapolation
has not been measured.
Boot mode handling
program_flash has to halt the PS and download an FSBL over JTAG, so the
script puts the device into JTAG boot mode first, running the JTAG sequence
from Change Boot Mode via XSCT verbatim: write 0x0100 to the Boot Mode
register at 0xff5e0200, then rst -system, which leaves the CPU halted
instead of booting from flash. Pass -J to skip this if you manage boot
modes yourself.
The board is left in JTAG boot mode afterwards, via the 0xff5e0200
override. A power-cycle clears that override and the M[3:0] straps take
effect again, so if the straps already select QSPI (0b0010 = QSPI32) a
plain power-cycle is all you need. To switch boot mode without a
power-cycle, write 0x2100 (Quad-SPI 32-bit dual-parallel) to
0xff5e0200 and rst -system. The script prints this reminder when it
finishes.
Note
Do not combine the two: writing 0x2100 and then power-cycling is
self-defeating, because the POR clears the very override you just wrote.
The register write is for changing boot mode while the board stays
powered; the power-cycle is for falling back to the straps.
Warning
program_flash reports the M[3:0] strap value rather than the
0xff5e0200 override, so you will see this warning whenever the straps
are not JTAG:
BOOT_MODE REG = 0x0222
WARNING: [Xicom 50-100] The current boot mode is QSPI32.
Flash programming is not supported with the selected boot mode.
0x0222 has bit 8 clear and [3:0] = 2, which is the M[3:0]
strap (0b0010 = QSPI32), not the override. It is cosmetic: the
override did take effect, the tool proceeds, and the run goes on to
Finished running FSBL. and Flash Operation Successful. Verified on
the SLAC RFMC carrier from a true cold boot with the straps at 0b0010,
including a passing -verify. JTAG straps (0b0000) are not required,
though they do silence the warning.
Do not read this warning as the cause of a failure that follows it. An
ERROR: [Xicom 50-331] Timed out while waiting for FSBL to complete
later in the same run is a separate problem, almost always the wrong FSBL
(see the FSBL warning under Prerequisites). The two appear together
often enough to be easy to conflate.
Warning
Every dual-parallel run also emits:
WARNING: [Xicom 50-353] WARNING: Flash Mismatched. If flash programming
fails, select the correct Flash and try again.
Selected Flash: cfgmem-512-qspi-x8-dual_parallel
Detected Flash: mt25qu02g
This is cosmetic too. program_flash string-compares the selected
cfgmem name against the single-die ID the mini u-boot probed, and those
can never match for a dual-parallel pair. A passing -verify (on by
default) is what confirms the addressing was right regardless.
Note
The script runs this sequence with xsdb, not xsct. Vitis 2026.1
ships xsct but hard-disables it, printing
XSCT is disabled in Vitis 2026.1 release. xsdb accepts the same
commands. The script falls back to xsct only on older installs that
predate xsdb.
Tip
If the boot mode step fails with
no targets found with "name =~ "*PSU*"" and the available target is
whole scan chain (DR shift through all zeroes), no device is answering
on the JTAG chain: the board is powered off, held in reset, or miscabled.
Check it directly with:
xsdb -eval 'connect; after 4000; jtag targets'
A healthy chain lists the device IDCODEs. Catching this here is
deliberate: if you skip the boot mode step with -J, program_flash
segfaults on the same condition instead of reporting it.
After flashing
Switch the board to boot from the newly programmed flash using XSCT (Change Boot Mode via XSCT in this how-to section), then power-cycle the board.
Note
The first boot from freshly programmed flash prints:
Loading Environment from SPIFlash... SF: Detected mt25qu02g ...
*** Warning - bad CRC, using default environment
Expected and harmless. U-Boot keeps its environment in a separate sector at
0x1E00000 (CONFIG_ENV_OFFSET for ZynqMP with
CONFIG_ENV_IS_IN_SPI_FLASH, size 0x40000), which none of the images
programmed here touch, so it reads blank and the CRC cannot match. U-Boot
falls back to the environment compiled into BOOT.BIN, which is exactly
what the tftp-only flow depends on: bootcmd comes from
platform-top.h, not from stored state.
Warning
Do not silence that warning with saveenv. A saved environment takes
precedence over the compiled-in default permanently, and reflashing
BOOT.BIN does not update it, because the write never reaches
0x1E00000. A later build with a changed bootcmd, netboot, or
loadpl_net is then silently ignored, and that symptom (board still boots
the old way after a successful reflash) is far more confusing than the
warning it replaced. To undo one, erase the sector at the U-Boot prompt:
sf probe 0 0 0
sf erase 0x1E00000 0x80000
There is a second reason on boards flashed with a bitstream-carrying
BOOT.BIN: at roughly 34 MB it extends past 0x1E00000, so the boot
image and the environment sector share flash and a saveenv would corrupt
the boot image. Only the tftp-only BOOT.BIN (under 2 MB) clears it.