APS6404L PSRAM on RP2350 – consistent nibble-shift errors
I’m building a board where the RP2350 uses the secondary XIP interface (CS1) to talk to an APS6404L-3SQR 8 MB QSPI PSRAM.
I’ve have this init code that configures timing, dummy cycles, and XIP mapping, which I've mostly taken from micropython backend example:
So far I’ve tried both SPI (opcode on 1 bit) and QPI (0x35 Enter QPI) modes, experimented with divisors and RX delays, varied dummy cycles from 6 to 8, and confirmed cache flush after writes. The PSRAM responds and transfers data, but verification shows consistent 4-bit (nibble) shifts at burst boundaries:
Interestingly, writing and then immediately reading sequentially works perfectly, but if I write an entire buffer first and then read back, the verification fails roughly every 24 bytes, showing the same nibble-shift pattern.
This works:
While this fails:
make_pat is just a deterministic 0xA5A5A5A5u ^ (i * 0x01000193u)
Timing changes only move or worsen the pattern, and entering QPI sometimes makes it fail completely—suggesting a mode mismatch.
Has anyone run APS6404L reliably on the RP2350’s CS1/XIP port?
Which exact combination of mode (SPI vs QPI), opcode widths, dummy/latency, and timing settings worked for you? Any working initialization snippet would be appreciated.
This is how I use it on PCB
PSRAM_CS goes to pin 47.
Flash on it's own works absolutely fine.
The behavior is both on stock speed as well as on 260Mhz overclock. I've tried setting PICO_FLASH_SPI_CLKDIV to as low as 4, didn't help.
Any pointers?
I’ve have this init code that configures timing, dummy cycles, and XIP mapping, which I've mostly taken from micropython backend example:
Code: Select all
size_t __no_inline_not_in_flash_func(psram_detect)()
{
int psram_size = 0;
// Try and read the PSRAM ID via direct_csr.
qmi_hw->direct_csr = 30 << QMI_DIRECT_CSR_CLKDIV_LSB | QMI_DIRECT_CSR_EN_BITS;
// Need to poll for the cooldown on the last XIP transfer to expire
// (via direct-mode BUSY flag) before it is safe to perform the first
// direct-mode operation
while ((qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) != 0) {
}
// Exit out of QMI in case we've inited already
qmi_hw->direct_csr |= QMI_DIRECT_CSR_ASSERT_CS1N_BITS;
// Transmit as quad.
qmi_hw->direct_tx = QMI_DIRECT_TX_OE_BITS | QMI_DIRECT_TX_IWIDTH_VALUE_Q << QMI_DIRECT_TX_IWIDTH_LSB | 0xf5;
while ((qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) != 0) {
}
(void)qmi_hw->direct_rx;
qmi_hw->direct_csr &= ~(QMI_DIRECT_CSR_ASSERT_CS1N_BITS);
// Read the id
qmi_hw->direct_csr |= QMI_DIRECT_CSR_ASSERT_CS1N_BITS;
uint8_t kgd = 0;
uint8_t eid = 0;
for (size_t i = 0; i < 7; i++)
{
if (i == 0) {
qmi_hw->direct_tx = 0x9f;
} else {
qmi_hw->direct_tx = 0xff;
}
while ((qmi_hw->direct_csr & QMI_DIRECT_CSR_TXEMPTY_BITS) == 0) {
}
while ((qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) != 0) {
}
if (i == 5) {
kgd = qmi_hw->direct_rx;
} else if (i == 6) {
eid = qmi_hw->direct_rx;
} else {
(void)qmi_hw->direct_rx;
}
}
// Disable direct csr.
qmi_hw->direct_csr &= ~(QMI_DIRECT_CSR_ASSERT_CS1N_BITS | QMI_DIRECT_CSR_EN_BITS);
if (kgd == 0x5D)
{
psram_size = 1024 * 1024; // 1 MiB
uint8_t size_id = eid >> 5;
if (eid == 0x26 || size_id == 2) {
psram_size *= 8; // 8 MiB
} else if (size_id == 0) {
psram_size *= 2; // 2 MiB
} else if (size_id == 1) {
psram_size *= 4; // 4 MiB
}
}
return psram_size;
}
size_t __no_inline_not_in_flash_func(do_psram_init)()
{
gpio_set_function(IO_PIN_SECONDARY_FLASH, GPIO_FUNC_XIP_CS1);
uint32_t intr_stash = save_and_disable_interrupts();
size_t psram_size = psram_detect();
if (!psram_size) {
return 0;
}
// Enable direct mode, PSRAM CS, clkdiv of 10.
qmi_hw->direct_csr = 10 << QMI_DIRECT_CSR_CLKDIV_LSB | \
QMI_DIRECT_CSR_EN_BITS | \
QMI_DIRECT_CSR_AUTO_CS1N_BITS;
while (qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) {
;
}
// Enable QPI mode on the PSRAM
const uint CMD_QPI_EN = 0x35;
qmi_hw->direct_tx = QMI_DIRECT_TX_NOPUSH_BITS | CMD_QPI_EN;
while (qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) {
;
}
// Set PSRAM timing for APS6404
//
// Using an rxdelay equal to the divisor isn't enough when running the APS6404 close to 133MHz.
// So: don't allow running at divisor 1 above 100MHz (because delay of 2 would be too late),
// and add an extra 1 to the rxdelay if the divided clock is > 100MHz (i.e. sys clock > 200MHz).
const int max_psram_freq = 133000000;
const int clock_hz = clock_get_hz(clk_sys);
int divisor = (clock_hz + max_psram_freq - 1) / max_psram_freq;
if (divisor == 1 && clock_hz > 100000000) {
divisor = 2;
}
int rxdelay = divisor;
if (clock_hz / divisor > 100000000) {
rxdelay += 1;
}
// - Max select must be <= 8us. The value is given in multiples of 64 system clocks.
// - Min deselect must be >= 18ns. The value is given in system clock cycles - ceil(divisor / 2).
const int clock_period_fs = 1000000000000000ll / clock_hz;
const int max_select = (125 * 1000000) / clock_period_fs; // 125 = 8000ns / 64
const int min_deselect = (18 * 1000000 + (clock_period_fs - 1)) / clock_period_fs - (divisor + 1) / 2;
qmi_hw->m[1].timing = 1 << QMI_M1_TIMING_COOLDOWN_LSB |
QMI_M1_TIMING_PAGEBREAK_VALUE_1024 << QMI_M1_TIMING_PAGEBREAK_LSB |
max_select << QMI_M1_TIMING_MAX_SELECT_LSB |
min_deselect << QMI_M1_TIMING_MIN_DESELECT_LSB |
rxdelay << QMI_M1_TIMING_RXDELAY_LSB |
divisor << QMI_M1_TIMING_CLKDIV_LSB;
// Set PSRAM commands and formats
qmi_hw->m[1].rfmt =
QMI_M0_RFMT_PREFIX_WIDTH_VALUE_Q << QMI_M0_RFMT_PREFIX_WIDTH_LSB | \
QMI_M0_RFMT_ADDR_WIDTH_VALUE_Q << QMI_M0_RFMT_ADDR_WIDTH_LSB | \
QMI_M0_RFMT_SUFFIX_WIDTH_VALUE_Q << QMI_M0_RFMT_SUFFIX_WIDTH_LSB | \
QMI_M0_RFMT_DUMMY_WIDTH_VALUE_Q << QMI_M0_RFMT_DUMMY_WIDTH_LSB | \
QMI_M0_RFMT_DATA_WIDTH_VALUE_Q << QMI_M0_RFMT_DATA_WIDTH_LSB | \
QMI_M0_RFMT_PREFIX_LEN_VALUE_8 << QMI_M0_RFMT_PREFIX_LEN_LSB | \
6 << QMI_M0_RFMT_DUMMY_LEN_LSB;
qmi_hw->m[1].rcmd = 0xEB;
qmi_hw->m[1].wfmt =
QMI_M0_WFMT_PREFIX_WIDTH_VALUE_Q << QMI_M0_WFMT_PREFIX_WIDTH_LSB | \
QMI_M0_WFMT_ADDR_WIDTH_VALUE_Q << QMI_M0_WFMT_ADDR_WIDTH_LSB | \
QMI_M0_WFMT_SUFFIX_WIDTH_VALUE_Q << QMI_M0_WFMT_SUFFIX_WIDTH_LSB | \
QMI_M0_WFMT_DUMMY_WIDTH_VALUE_Q << QMI_M0_WFMT_DUMMY_WIDTH_LSB | \
QMI_M0_WFMT_DATA_WIDTH_VALUE_Q << QMI_M0_WFMT_DATA_WIDTH_LSB | \
QMI_M0_WFMT_PREFIX_LEN_VALUE_8 << QMI_M0_WFMT_PREFIX_LEN_LSB;
qmi_hw->m[1].wcmd = 0x38;
// Disable direct mode
qmi_hw->direct_csr = 0;
// Enable writes to PSRAM
hw_set_bits(&xip_ctrl_hw->ctrl, XIP_CTRL_WRITABLE_M1_BITS);
restore_interrupts(intr_stash);
return psram_size;
}
Code: Select all
mismatch @+0x00000018 got=0x5a3a7add exp=0xa3a5acd7
mismatch @+0x000000dc got=0x300ff330 exp=0x92a5f330
mismatch @+0x000000ec got=0x5feaf944 exp=0x9ea5f944
mismatch @+0x000000f0 got=0x5c9a19dd exp=0x99a5fbd1
mismatch @+0x00000120 got=0x5ddadeff exp=0xeda5d4fd
mismatch @+0x00000160 got=0x52da2f2d exp=0xfda52f2d
mismatch @+0x00000164 got=0x52caefbb exp=0xfca529be
mismatch @+0x00000168 got=0x52fabf0b exp=0xffa5280b
mismatch @+0x000001ac got=0x50ea0dd4 exp=0xcea50dd4
mismatch @+0x000001b0 got=0x939c0fa1 exp=0xc9a50fa1
mismatch @+0x000001bc got=0x51aa0b18 exp=0xcaa50b18This works:
Code: Select all
uint32_t* ext_sram32 = (uint32_t*)0x1d000000; // also tried 0x11, 0x15
for (uint32_t i = 0; i < words; i += stride_words)
{
uint32_t pat = make_pat(i);
ext_sram32[i] = pat;
uint32_t v = ext_sram32[i];
if (v != pat)
{
debug_printf("mismatch @+0x%08x got=0x%08x exp=0x%08x\n", i * 4u, v, pat);
....
Code: Select all
for (uint32_t i = 0; i < words; i += stride_words)
{
ext_sram32[i] = make_pat(i);
}
debug_printf("done, reading back....\n");
int errcnt = 0;
for (uint32_t i = 0; i < words; i += stride_words)
{
uint32_t pat = make_pat(i);
// ext_sram32[i] = pat;
uint32_t v = ext_sram32[i];
if (v != pat)
{
Timing changes only move or worsen the pattern, and entering QPI sometimes makes it fail completely—suggesting a mode mismatch.
Has anyone run APS6404L reliably on the RP2350’s CS1/XIP port?
Which exact combination of mode (SPI vs QPI), opcode widths, dummy/latency, and timing settings worked for you? Any working initialization snippet would be appreciated.
This is how I use it on PCB
- photo_2025年10月19日 20.21.13.jpeg
- photo_2025年10月19日 20.21.13.jpeg (37.34 KiB) Viewed 991 times
Flash on it's own works absolutely fine.
The behavior is both on stock speed as well as on 260Mhz overclock. I've tried setting PICO_FLASH_SPI_CLKDIV to as low as 4, didn't help.
Any pointers?
Re: APS6404L PSRAM on RP2350 – consistent nibble-shift errors
I think it's the same chip: viewtopic.php?t=375975&start=25#p2280143
Try to dump QMI registers, and compare.
Try to dump QMI registers, and compare.
You should never do this, it's strictly 6 for read and 0 for write.desertkun wrote:varied dummy cycles from 6 to 8
Re: APS6404L PSRAM on RP2350 – consistent nibble-shift errors
Thank you for that thread, it was helpful.
I've stacked PSRAM after FLASH, having around 14mm wires from MCU to FLASH and then thru vias an additional 17mm to PSRAM.
I've replicated them 1-to-1 and having the same behavior. Could the problem be physical? The chip in the middle is FLASH and the one to the right at the top is PSRAM. Problem still persists even if I set ridiculous divider like 32 so that means this is not likely the cause?gmx wrote: I think it's the same chip: viewtopic.php?t=375975&start=25#p2280143
Try to dump QMI registers, and compare.
- Screenshot 2025年10月20日 222608.png
- Screenshot 2025年10月20日 222608.png (159.01 KiB) Viewed 877 times
Re: APS6404L PSRAM on RP2350 – consistent nibble-shift errors
Strange indeed, try in quad mode a regular pattern like 0xF0F0F0F0 ... FF00FF00.
I don't see a clear pattern.
As you described, it looks like a problem with longer bursts, could be a problem in the PSRAM chip itself.
I don't see a clear pattern.
As you described, it looks like a problem with longer bursts, could be a problem in the PSRAM chip itself.
How are you doing it?desertkun wrote:and confirmed cache flush after writes
Re: APS6404L PSRAM on RP2350 – consistent nibble-shift errors
I've adjusted the test to push&pull 8 32-bit words of a repeting pattern
0xF0F0F0F0
0x77007700
0xAA005500
0x5A5A5A5A
and it is very interesting indeed
This is writing 8 32-bit words (uint32_t data[8]) and then reading it 3 times
It's like it is swapping bytes/nibbles at will they all almost add up. This is even with the clock divider of 64.
What is even more weirder, if that if I replace the first number pattern to 12345678, it works, it passes. So 0xF0F0F0F0 resonates with my poor wiring even on low frequencies?
0xF0F0F0F0
0x77007700
0xAA005500
0x5A5A5A5A
and it is very interesting indeed
Code: Select all
mismatch1 @+0x00000000 expected:
f0f0f0f0
77007700
aa005500
5a5a5a5a
f0f0f0f0
77007700
aa005500
5a5a5a5a
got:
00f00fff
00770077
aa005500
5a5a5a5a
00f00fff
00770077
5aaa0055
f05a5a5a
mismatch2 @+0x00000000 expected:
f0f0f0f0
77007700
aa005500
5a5a5a5a
f0f0f0f0
77007700
aa005500
5a5a5a5a
got:
00f00fff
00770077
5aaa0055
f05a5a5a
00f00fff
00770077
5aaa0055
f05a5a5a
mismatch3 @+0x00000000 expected:
f0f0f0f0
77007700
aa005500
5a5a5a5a
f0f0f0f0
77007700
aa005500
5a5a5a5a
got:
00f00fff
00770077
5aaa0055
f05a5a5a
00f00fff
00770077
5aaa0055
f05a5a5a
Code: Select all
struct sram_test_o_t test;
make_pat(&test, i);
// ext_sram32[i] = pat;
struct sram_test_o_t v;
memcpy(&v.data, &ext_sram32[i].data, sizeof(v.data));
if (memcmp(&v.data, &test.data, sizeof(test.data)) != 0)
{
debug_printf("mismatch1 @+0x%08x expected:\n", i * sizeof(test));
....
What is even more weirder, if that if I replace the first number pattern to 12345678, it works, it passes. So 0xF0F0F0F0 resonates with my poor wiring even on low frequencies?
Re: APS6404L PSRAM on RP2350 – consistent nibble-shift errors
@gmx
Happy to report that moving PSRAM a bit closer
Fixed it, so the problem was entirely physical. What an expensive lesson.
Happy to report that moving PSRAM a bit closer
- Screenshot 2025年10月29日 163725.png
- Screenshot 2025年10月29日 163725.png (128.29 KiB) Viewed 476 times
Re: APS6404L PSRAM on RP2350 – consistent nibble-shift errors
Actually this did not solve the problem entirely, PSRAM still was unreliable, until I applied more techniques, like impedance matching and separate ground plane for another logic with a simple connecting tie. It goes without saying that those PSRAMs are very sensitive devices.
Re: APS6404L PSRAM on RP2350 – consistent nibble-shift errors
Never seen such behavior even in most fierce overclocking.
Other kinds of faults, yes, like simple nibble shifts but not swaps at bytes distance, and not only in burst.
Impedance matching should have affected Flash as well, and only at high frequency (you don't have significant long traces), BTW that can be tuned with pad drive strength.
I would bet more on ground loops, voltage/decoupling glitches affecting only the Flash.
Other kinds of faults, yes, like simple nibble shifts but not swaps at bytes distance, and not only in burst.
Impedance matching should have affected Flash as well, and only at high frequency (you don't have significant long traces), BTW that can be tuned with pad drive strength.
I would bet more on ground loops, voltage/decoupling glitches affecting only the Flash.
Re: APS6404L PSRAM on RP2350 – consistent nibble-shift errors
Your routing looks nicer than mine but I don't see those sort of issues. The big thing I found for performance was drive strength. Set it to 8mA. Decoupling could be an issue, that is different on out layouts.
- Image1.jpg
- Image1.jpg (407.38 KiB) Viewed 6 times
Jump to
- Community
- General discussion
- Announcements
- Other languages
- Deutsch
- Español
- Français
- Italiano
- Nederlands
- 日本語
- Polski
- Português
- Русский
- Türkçe
- User groups and events
- Raspberry Pi Official Magazine
- Using the Raspberry Pi
- Beginners
- Troubleshooting
- Advanced users
- Assistive technology and accessibility
- Education
- Picademy
- Teaching and learning resources
- Staffroom, classroom and projects
- Astro Pi
- Mathematica
- High Altitude Balloon
- Weather station
- Programming
- C/C++
- Java
- Python
- Scratch
- Other programming languages
- Windows 10 for IoT
- Wolfram Language
- Bare metal, Assembly language
- Graphics programming
- OpenGLES
- OpenVG
- OpenMAX
- General programming discussion
- Projects
- Networking and servers
- Automation, sensing and robotics
- Graphics, sound and multimedia
- Other projects
- Media centres
- Gaming
- AIY Projects
- Hardware and peripherals
- Camera board
- Compute Module
- Official Display
- HATs and other add-ons
- Device Tree
- Interfacing (DSI, CSI, I2C, etc.)
- Keyboard computers (400, 500, 500+)
- Raspberry Pi Pico
- General
- SDK
- MicroPython
- Other RP2040 boards
- Zephyr
- Rust
- AI Accelerator
- AI Camera - IMX500
- Hailo
- Software
- Raspberry Pi OS
- Raspberry Pi Connect
- Raspberry Pi Desktop for PC and Mac
- Beta testing
- Other
- Android
- Debian
- FreeBSD
- Gentoo
- Linux Kernel
- NetBSD
- openSUSE
- Plan 9
- Puppy
- Arch
- Pidora / Fedora
- RISCOS
- Ubuntu
- Ye Olde Pi Shoppe
- For sale
- Wanted
- Off topic
- Off topic discussion