Most i.MX RT boards run their code straight out of an external QSPI NOR flash. The FlexSPI setup usually comes from an SDK example, the clock setting says 133 MHz, and everything works. That is exactly the situation where you assume the flash runs at full speed, and nobody has checked.
Checking is not hard, but read and write need different checks. Read speed is limited by the bus, so you compare it against a number you can calculate. Write speed is limited by the flash chip itself, so you compare it against the datasheet. A write number that looks too good is not a win: it is a bug in your measurement.
In this article I show how FlexSPI is configured for a QSPI NOR on the i.MX RT1062, how to calculate the read ceiling and check you reach it, and how to verify write speed without fooling yourself.
1. Know which of the two paths you are on
The mental model: one controller, two doors into the same flash.
FlexSPI sits between the CPU’s bus and the external flash. It offers two independent paths into the same command engine, and most FlexSPI bugs come from mixing them up:
- AHB path (memory-mapped). The CPU simply reads the flash window at
0x6000_0000, and FlexSPI runs the read sequence in the background, through a read buffer. Code execution (XIP) andmemcpy-style reads use this path. No status polling, fully transparent. - IP path (software-triggered). Software writes the address and command registers, triggers the command, and moves data through small TX/RX FIFOs. Erase and page program need this path, because they are explicit commands, not memory accesses.
The most common FlexSPI write bug
2. Write the LUT, because there is nothing else
The mental model: you are programming a tiny bytecode interpreter, not calling a library.
FlexSPI has no read() or erase() register. Every operation is a sequence of up to 8
instructions stored in a lookup table (LUT). Each instruction says what to send
(command, address, dummy cycles, data), on how many lines (1, 2, 4 or 8), and with which
operand. Here is a Quad I/O Fast Read:
/* Quad I/O Fast Read (0xEB): command on 1 line, address on 4, dummy cycles, data on 4 */
[4 * NOR_CMD_LUT_SEQ_IDX_READ + 0] =
FLEXSPI_LUT_SEQ(CMD_SDR, FLEXSPI_1PAD, 0xEB, RADDR_SDR, FLEXSPI_4PAD, 0x18),
[4 * NOR_CMD_LUT_SEQ_IDX_READ + 1] =
FLEXSPI_LUT_SEQ(DUMMY_SDR, FLEXSPI_4PAD, 0x06, READ_SDR, FLEXSPI_4PAD, 0x04),SDR or DDR, and single, dual, quad or octal, are chosen per instruction. There is no global mode register, so one sequence can send a 1-line command followed by 4-line data.
Opcodes come from the flash datasheet, not the FlexSPI manual
CMD_SDR, RADDR_SDR, the line
count. The actual opcode byte (0xEB, 0x6B, …) comes from the flash vendor. FlexSPI just
shifts out whatever byte you put in the operand field.Pick the read command with the least overhead
A typical QSPI NOR offers two quad read commands. Both move data on 4 lines; the difference is the address:
- Quad Output Read (
0x6B, “1-1-4”): command and address on 1 line, data on 4. - Quad I/O Read (
0xEB, “1-4-4”): command on 1 line, address and data on 4.
Sending the 24-bit address on 4 lines takes 6 clocks instead of 24. Prefer 0xEB when the
flash supports it at the same clock rating.
Dummy cycles: exactly what the datasheet says
3. The sampling clock caps your frequency
The mental model: at high speed, when you sample the data matters as much as how fast you clock it.
This is the most-skipped part of the FlexSPI chapter, and it decides how fast you are
allowed to go. To read data back reliably, FlexSPI must sample each bit at the right
moment. The MCR0[RXCLKSRC] field selects one of four sampling sources:
| RXCLKSRC | Sampling source | Speed | Notes |
|---|---|---|---|
| 0 | Internal dummy strobe, looped back internally | lowest | Boot-safe, no extra pad needed |
| 1 | Internal dummy strobe, looped back from the DQS pad | higher | Compensates the pad delay; the usual choice for QSPI NOR |
| 2 | Serial clock looped back from the SCK pad | similar to 1 | Rarely used |
| 3 | Read strobe driven by the flash (real DQS) | highest | Needs a flash that drives DQS and a board that routes it |
A standard QSPI NOR does not drive a read strobe, so mode 3 is not available with it. For those parts mode 1 is the ceiling, not a compromise. In mode 1 the DQS pad is only used for internal delay compensation; it is not connected to the flash, so leave it unconnected on the board.
Two places set the clock
serialClkFreq only tells the boot ROM which frequency to use while it
boots. The frequency your application runs at is set by your own clock configuration. Make
sure both point at the same target, and read the real value back at run time
(CLOCK_GetClockRootFreq) instead of trusting the configuration file.4. Verify read speed against the bus
The mental model: the read ceiling is arithmetic, not a mystery number.
For quad SDR, every clock moves 4 bits, so the read ceiling is:
$$ \text{ceiling} = f_{clk} \times 4\ \text{bits per clock} $$As an illustration: at 133 MHz that is about 532 Mbit/s. To measure your real number:
- Read a large block sequentially through the AHB window.
- Time it with the Cortex-M7 cycle counter (DWT), not a millisecond tick.
- Compare against the ceiling for the clock you read back at run time.
The gap between measured and ceiling is the command, address and dummy prefix, plus buffer refills. With a long sequential read it should be small: if you are within a few percent of the ceiling, the read path is saturated and there is nothing left to tune.
Prove that your sample was large enough
5. Verify write speed against the datasheet
The mental model: write speed belongs to the flash chip, not to the bus.
Sending one 256-byte page over a quad bus takes only a few microseconds. As an
illustration: 256 bytes at 2 clocks per byte is 512 clocks, under 4 µs at 133 MHz. The
flash then needs its internal page-program time (tPP in the datasheet), which is
typically hundreds of microseconds. So the program time dominates, and “maximum write
speed” is a fixed property of the chip. You verify that you are at tPP, not that you
have tuned past it.
A write faster than the datasheet is a bug report
If your measured time per page is clearly below the datasheet’s typical tPP, the flash
did not get faster. Your busy-poll loop is exiting before the flash has finished, so the
timed window is too short.
A passing read-back check does not disprove this: the read-back happens later, which gives
the flash time to finish in the background. To confirm, count the status reads per page
in the poll loop and compare the poll time against tPP.
Verification checklist
- Read throughput within a few percent of
f_clk × 4: the read path is saturated. - Read throughput flat across block sizes: the sample was large enough.
- Measured page-program time inside the datasheet’s
tPPtypical–maximum band. - Status polls per page plausible, not suspiciously few.
- Read-back verify passes (data integrity, a separate question from timing).
- Boot-header clock setting and run-time clock agree.
Summary
- Two paths, one engine. Reads and code fetches go through the AHB path; erase and program go through the IP path, with their code running from RAM.
- The LUT and the sampling clock set the limits. Use the read command with the least overhead, the exact dummy count from the datasheet, and the fastest sampling mode your flash and board support.
- Verify each direction against the right reference. Read against
f_clk × 4, write against the datasheet’stPP. A benchmark that beats the datasheet is not a result; it is a bug in the measurement.
Sources
- NXP, i.MX RT1060 Processor Reference Manual, FlexSPI chapter: AHB and IP paths, LUT
instruction encoding,
MCR0[RXCLKSRC]sampling modes. - The datasheet of your QSPI NOR: read command timing (mode and dummy clocks), maximum clock frequency, and the page-program and erase times.
💬 Questions, corrections, or your own war story? Leave a comment below.
💬 Discussion
Questions, corrections, or war stories from your own board? Leave a comment below — I read and answer all of them. (Sign-in with GitHub.)