|
| 1 | +/* |
| 2 | + * PSRAM driver for APS6404L-3SQR-SN (8 MB QSPI PSRAM) on RP2350. |
| 3 | + * |
| 4 | + * The APS6404L is connected to QMI chip select 1 (CS1) via a GPIO pin |
| 5 | + * defined by PICO_RP2350_PSRAM_CS_PIN in the board header. It shares |
| 6 | + * the QSPI bus (SCLK, SD0–SD3) with the flash on CS0. |
| 7 | + * |
| 8 | + * Initialization sequence |
| 9 | + * ----------------------- |
| 10 | + * 1. Detect — Read the PSRAM device ID via QMI direct mode to confirm |
| 11 | + * the chip is present and determine its size. |
| 12 | + * 2. Configure CS1 — Use the bootrom's flash_devinfo runtime API |
| 13 | + * (flash_devinfo_set_cs_gpio / flash_devinfo_set_cs_size) to tell |
| 14 | + * the bootrom about the CS1 device, then call rom_connect_internal_flash() |
| 15 | + * so the bootrom sets up ATRANS address translation and GPIO pads. |
| 16 | + * Writing ATRANS registers manually is NOT sufficient; the bootrom must |
| 17 | + * configure additional internal state for memory-mapped CS1 access to |
| 18 | + * work. See: https://github.com/raspberrypi/pico-sdk/issues/2205 |
| 19 | + * 3. Enter QPI — Send Reset Enable (0x66), Reset (0x99), and Enter Quad |
| 20 | + * Mode (0x35) to the PSRAM via QMI direct mode. |
| 21 | + * 4. Configure QMI M1 — Set timing, read/write format, and command |
| 22 | + * registers for QPI access (0xEB read, 0x38 write). |
| 23 | + * 5. Enable writes — Set XIP_CTRL.WRITABLE_M1 so the XIP subsystem |
| 24 | + * permits writes to memory window 1 (default is read-only). |
| 25 | + * |
| 26 | + * Address mapping |
| 27 | + * --------------- |
| 28 | + * After initialization the PSRAM is memory-mapped at 0x11000000 (cached) |
| 29 | + * through the XIP subsystem. The 16 kB XIP write-back cache covers both |
| 30 | + * CS0 (flash) and CS1 (PSRAM). |
| 31 | + * |
| 32 | + * Alternatives to the runtime API |
| 33 | + * -------------------------------- |
| 34 | + * The same CS1 configuration can be made permanent by programming OTP: |
| 35 | + * - FLASH_DEVINFO.CS1_GPIO = <pin> |
| 36 | + * - FLASH_DEVINFO.CS1_SIZE = 0xB (8 MB) |
| 37 | + * - BOOT_FLAGS0.FLASH_DEVINFO_ENABLE = 1 |
| 38 | + * This causes the bootrom to configure CS1 automatically on every boot, |
| 39 | + * eliminating the need for the runtime API calls above. |
| 40 | + * |
| 41 | + * References |
| 42 | + * ---------- |
| 43 | + * - APS6404L-3SQR datasheet (AP Memory) |
| 44 | + * - RP2350 datasheet §4.4 "External flash and PSRAM (XIP)" |
| 45 | + * - pico-sdk issue #2205 — runtime CS1 configuration without OTP |
| 46 | + * - SparkFun sparkfun-pico library (sfe_psram.c, MIT license) |
| 47 | + */ |
| 48 | + |
| 49 | +#include "psram.h" |
| 50 | + |
| 51 | +#include <stdio.h> |
| 52 | +#include "pico/stdlib.h" |
| 53 | +#include "pico/bootrom.h" |
| 54 | +#include "hardware/clocks.h" |
| 55 | +#include "hardware/flash.h" |
| 56 | +#include "hardware/structs/qmi.h" |
| 57 | +#include "hardware/structs/xip_ctrl.h" |
| 58 | +#include "hardware/regs/addressmap.h" |
| 59 | +#include "hardware/gpio.h" |
| 60 | +#include "hardware/sync.h" |
| 61 | + |
| 62 | +#define PSRAM_SIZE (8 * 1024 * 1024) |
| 63 | + |
| 64 | +/* |
| 65 | + * APS6404L SPI/QPI command set (see datasheet §8.5, Table). |
| 66 | + * |
| 67 | + * The device powers up in SPI mode. After issuing Enter Quad Mode |
| 68 | + * (0x35), all subsequent commands use QPI (4-bit) transfers. Exit |
| 69 | + * Quad Mode (0xF5) returns to SPI and is the only command that must |
| 70 | + * be sent in quad width while the device is in QPI mode. |
| 71 | + */ |
| 72 | +#define CMD_RESET_ENABLE 0x66 /* Reset Enable (SPI & QPI) */ |
| 73 | +#define CMD_RESET 0x99 /* Reset (SPI & QPI) — must follow 0x66 immediately */ |
| 74 | +#define CMD_QUAD_ENABLE 0x35 /* Enter Quad Mode (SPI only) */ |
| 75 | +#define CMD_QUAD_END 0xF5 /* Exit Quad Mode (QPI only) */ |
| 76 | +#define CMD_READ_ID 0x9F /* Read ID (SPI only) */ |
| 77 | +#define CMD_READ_QUAD 0xEB /* Fast Quad Read: cmd + 24-bit addr + 6 wait cycles + data */ |
| 78 | +#define CMD_WRITE_QUAD 0x38 /* Quad Write: cmd + 24-bit addr + data (no wait cycles) */ |
| 79 | +#define CMD_NOOP 0xFF |
| 80 | + |
| 81 | +/* Known Good Die identifier (datasheet §10.4, Table 4) */ |
| 82 | +#define PSRAM_KGD 0x5D |
| 83 | + |
| 84 | +/* |
| 85 | + * Timing constants in femtoseconds (1 fs = 1e-15 s) for integer math. |
| 86 | + * From the APS6404L datasheet §14.6 "AC Characteristics": |
| 87 | + * tCEM (max CS# low) = 8 µs → 8e6 ns → 8e15 fs / 64 = 125e6 fs |
| 88 | + * tCPH (min CS# high) = 50 ns → 50e6 fs |
| 89 | + * fmax (VDD = 3.3 V) = 109 MHz (Wrapped Burst) |
| 90 | + */ |
| 91 | +#define SEC_TO_FS 1000000000000000ll |
| 92 | +#define PSRAM_MAX_SELECT_FS 125000000 /* tCEM / 64 (QMI MAX_SELECT unit) */ |
| 93 | +#define PSRAM_MIN_DESELECT_FS 50000000 /* tCPH */ |
| 94 | +#define PSRAM_MAX_SCK_HZ 109000000 /* max SCK at 3.3 V */ |
| 95 | + |
| 96 | +/* ------------------------------------------------------------------ */ |
| 97 | + |
| 98 | +/* |
| 99 | + * Detect the PSRAM via a direct-mode SPI Read ID (0x9F) transaction. |
| 100 | + * |
| 101 | + * Direct mode lets us talk to the PSRAM without memory-mapped access, |
| 102 | + * so this function is safe even when ATRANS / M1 are not yet configured. |
| 103 | + * It also sends Exit QPI (0xF5) first in case the device is still in |
| 104 | + * QPI mode from a previous (warm) boot. |
| 105 | + * |
| 106 | + * Returns the detected size in bytes, or 0 if no PSRAM is found. |
| 107 | + * |
| 108 | + * Must run from RAM (__no_inline_not_in_flash_func) because QMI direct |
| 109 | + * mode pauses all XIP (flash) access. |
| 110 | + */ |
| 111 | +static size_t __no_inline_not_in_flash_func(get_psram_size)(void) |
| 112 | +{ |
| 113 | + size_t psram_size = 0; |
| 114 | + uint32_t save = save_and_disable_interrupts(); |
| 115 | + |
| 116 | + /* Enter direct mode with a conservative clock (sys_clk / 30 ≈ 5 MHz) */ |
| 117 | + qmi_hw->direct_csr = 30 << QMI_DIRECT_CSR_CLKDIV_LSB | QMI_DIRECT_CSR_EN_BITS; |
| 118 | + while (qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) {} |
| 119 | + |
| 120 | + /* Exit QPI mode (0xF5 in quad width) — harmless if already in SPI */ |
| 121 | + qmi_hw->direct_csr |= QMI_DIRECT_CSR_ASSERT_CS1N_BITS; |
| 122 | + qmi_hw->direct_tx = QMI_DIRECT_TX_OE_BITS |
| 123 | + | (QMI_DIRECT_TX_IWIDTH_VALUE_Q << QMI_DIRECT_TX_IWIDTH_LSB) |
| 124 | + | CMD_QUAD_END; |
| 125 | + while (qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) {} |
| 126 | + (void)qmi_hw->direct_rx; |
| 127 | + qmi_hw->direct_csr &= ~QMI_DIRECT_CSR_ASSERT_CS1N_BITS; |
| 128 | + |
| 129 | + /* |
| 130 | + * SPI Read ID (0x9F): 1 cmd byte + 3 address bytes + 2 ID bytes = 7 |
| 131 | + * clocked bytes total. Byte 5 = KGD, byte 6 = EID. |
| 132 | + */ |
| 133 | + qmi_hw->direct_csr |= QMI_DIRECT_CSR_ASSERT_CS1N_BITS; |
| 134 | + uint8_t kgd = 0, eid = 0; |
| 135 | + for (int i = 0; i < 7; i++) { |
| 136 | + qmi_hw->direct_tx = (i == 0) ? CMD_READ_ID : CMD_NOOP; |
| 137 | + while ((qmi_hw->direct_csr & QMI_DIRECT_CSR_TXEMPTY_BITS) == 0) {} |
| 138 | + while (qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) {} |
| 139 | + if (i == 5) kgd = (uint8_t)qmi_hw->direct_rx; |
| 140 | + else if (i == 6) eid = (uint8_t)qmi_hw->direct_rx; |
| 141 | + else (void)qmi_hw->direct_rx; |
| 142 | + } |
| 143 | + qmi_hw->direct_csr &= ~QMI_DIRECT_CSR_ASSERT_CS1N_BITS; |
| 144 | + qmi_hw->direct_csr &= ~(QMI_DIRECT_CSR_ASSERT_CS1N_BITS | QMI_DIRECT_CSR_EN_BITS); |
| 145 | + |
| 146 | + restore_interrupts(save); |
| 147 | + |
| 148 | + printf("PSRAM ID: KGD=0x%02x EID=0x%02x\n", kgd, eid); |
| 149 | + |
| 150 | + if (kgd == PSRAM_KGD) { |
| 151 | + /* Decode size from EID[7:5] (datasheet §10.4) */ |
| 152 | + psram_size = 1024 * 1024; |
| 153 | + uint8_t size_id = eid >> 5; |
| 154 | + if (eid == 0x26 || size_id == 2) |
| 155 | + psram_size *= 8; |
| 156 | + else if (size_id == 0) |
| 157 | + psram_size *= 2; |
| 158 | + else if (size_id == 1) |
| 159 | + psram_size *= 4; |
| 160 | + } |
| 161 | + return psram_size; |
| 162 | +} |
| 163 | + |
| 164 | +/* |
| 165 | + * Compute and apply M1 timing parameters based on the current sys_clk. |
| 166 | + * |
| 167 | + * Call this again after overclocking to keep PSRAM within spec. |
| 168 | + */ |
| 169 | +static void __no_inline_not_in_flash_func(set_psram_timing)(void) |
| 170 | +{ |
| 171 | + uint32_t sys_hz = clock_get_hz(clk_sys); |
| 172 | + uint8_t clkdiv = (sys_hz + PSRAM_MAX_SCK_HZ - 1) / PSRAM_MAX_SCK_HZ; |
| 173 | + uint32_t fs_per_cycle = SEC_TO_FS / sys_hz; |
| 174 | + uint8_t max_select = PSRAM_MAX_SELECT_FS / fs_per_cycle; |
| 175 | + uint8_t min_deselect = (PSRAM_MIN_DESELECT_FS + fs_per_cycle - 1) / fs_per_cycle; |
| 176 | + |
| 177 | + uint32_t save = save_and_disable_interrupts(); |
| 178 | + qmi_hw->m[1].timing = |
| 179 | + (2u << QMI_M1_TIMING_PAGEBREAK_LSB) /* 1024-byte page boundary */ |
| 180 | + | (3u << QMI_M1_TIMING_SELECT_HOLD_LSB) /* 3 extra hold cycles */ |
| 181 | + | (1u << QMI_M1_TIMING_COOLDOWN_LSB) /* sequential burst reuse */ |
| 182 | + | (1u << QMI_M1_TIMING_RXDELAY_LSB) /* ½ sys_clk sample delay */ |
| 183 | + | (max_select << QMI_M1_TIMING_MAX_SELECT_LSB) |
| 184 | + | (min_deselect << QMI_M1_TIMING_MIN_DESELECT_LSB) |
| 185 | + | (clkdiv << QMI_M1_TIMING_CLKDIV_LSB); |
| 186 | + restore_interrupts(save); |
| 187 | +} |
| 188 | + |
| 189 | +/* |
| 190 | + * Full PSRAM setup: bootrom CS1 config → QPI mode → M1 registers. |
| 191 | + * |
| 192 | + * After this function returns, the PSRAM is accessible as memory-mapped |
| 193 | + * read/write storage at XIP_BASE + 16 MB (0x11000000). |
| 194 | + */ |
| 195 | +static void __no_inline_not_in_flash_func(setup_psram)(void) |
| 196 | +{ |
| 197 | + /* |
| 198 | + * Inform the bootrom about CS1 via the runtime flash_devinfo API. |
| 199 | + * |
| 200 | + * This is equivalent to programming OTP FLASH_DEVINFO but does not |
| 201 | + * require burning any fuses. After these calls, rom_connect_internal_flash() |
| 202 | + * will configure ATRANS and GPIO pads for CS1. |
| 203 | + * |
| 204 | + * IMPORTANT: Manually writing QMI ATRANS registers is NOT enough. |
| 205 | + * The bootrom must set up additional internal state (pad config, etc.) |
| 206 | + * for memory-mapped CS1 access to work. |
| 207 | + */ |
| 208 | + flash_devinfo_set_cs_gpio(1, PICO_RP2350_PSRAM_CS_PIN); |
| 209 | + flash_devinfo_set_cs_size(1, FLASH_DEVINFO_SIZE_8M); |
| 210 | + |
| 211 | + uint32_t save = save_and_disable_interrupts(); |
| 212 | + |
| 213 | + /* Apply CS1 configuration through the bootrom */ |
| 214 | + rom_connect_internal_flash(); |
| 215 | + rom_flash_exit_xip(); |
| 216 | + rom_flash_enter_cmd_xip(); |
| 217 | + |
| 218 | + /* |
| 219 | + * Enter QPI mode on the PSRAM via direct mode. |
| 220 | + * |
| 221 | + * After reset the device is in SPI mode (datasheet §8.4). |
| 222 | + * We send Reset Enable + Reset to ensure a clean state, then |
| 223 | + * Enter Quad Mode to switch to QPI for higher throughput. |
| 224 | + */ |
| 225 | + qmi_hw->direct_csr = 30 << QMI_DIRECT_CSR_CLKDIV_LSB | QMI_DIRECT_CSR_EN_BITS; |
| 226 | + while (qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) {} |
| 227 | + |
| 228 | + const uint8_t cmds[] = { CMD_RESET_ENABLE, CMD_RESET, CMD_QUAD_ENABLE }; |
| 229 | + for (int i = 0; i < 3; i++) { |
| 230 | + qmi_hw->direct_csr |= QMI_DIRECT_CSR_ASSERT_CS1N_BITS; |
| 231 | + qmi_hw->direct_tx = cmds[i]; |
| 232 | + while (qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) {} |
| 233 | + qmi_hw->direct_csr &= ~QMI_DIRECT_CSR_ASSERT_CS1N_BITS; |
| 234 | + for (volatile int j = 0; j < 20; j++) { __asm volatile("nop"); } |
| 235 | + (void)qmi_hw->direct_rx; |
| 236 | + } |
| 237 | + |
| 238 | + qmi_hw->direct_csr &= ~(QMI_DIRECT_CSR_ASSERT_CS1N_BITS | QMI_DIRECT_CSR_EN_BITS); |
| 239 | + restore_interrupts(save); |
| 240 | + |
| 241 | + set_psram_timing(); |
| 242 | + |
| 243 | + save = save_and_disable_interrupts(); |
| 244 | + |
| 245 | + /* |
| 246 | + * QMI M1 read format — QPI Fast Quad Read (0xEB): |
| 247 | + * All phases (prefix, addr, dummy, data) on 4 lines. |
| 248 | + * 24-bit address, 6 wait cycles = 24 dummy bits. |
| 249 | + * Max frequency 133 MHz (datasheet §8.5). |
| 250 | + */ |
| 251 | + qmi_hw->m[1].rfmt = |
| 252 | + (QMI_M1_RFMT_PREFIX_WIDTH_VALUE_Q << QMI_M1_RFMT_PREFIX_WIDTH_LSB) |
| 253 | + | (QMI_M1_RFMT_ADDR_WIDTH_VALUE_Q << QMI_M1_RFMT_ADDR_WIDTH_LSB) |
| 254 | + | (QMI_M1_RFMT_SUFFIX_WIDTH_VALUE_Q << QMI_M1_RFMT_SUFFIX_WIDTH_LSB) |
| 255 | + | (QMI_M1_RFMT_DUMMY_WIDTH_VALUE_Q << QMI_M1_RFMT_DUMMY_WIDTH_LSB) |
| 256 | + | (QMI_M1_RFMT_DUMMY_LEN_VALUE_24 << QMI_M1_RFMT_DUMMY_LEN_LSB) |
| 257 | + | (QMI_M1_RFMT_DATA_WIDTH_VALUE_Q << QMI_M1_RFMT_DATA_WIDTH_LSB) |
| 258 | + | (QMI_M1_RFMT_PREFIX_LEN_VALUE_8 << QMI_M1_RFMT_PREFIX_LEN_LSB) |
| 259 | + | (QMI_M1_RFMT_SUFFIX_LEN_VALUE_NONE << QMI_M1_RFMT_SUFFIX_LEN_LSB); |
| 260 | + qmi_hw->m[1].rcmd = CMD_READ_QUAD << QMI_M1_RCMD_PREFIX_LSB; |
| 261 | + |
| 262 | + /* |
| 263 | + * QMI M1 write format — QPI Quad Write (0x38): |
| 264 | + * All phases on 4 lines, no dummy/wait cycles. |
| 265 | + * Max frequency 133 MHz (datasheet §8.5). |
| 266 | + */ |
| 267 | + qmi_hw->m[1].wfmt = |
| 268 | + (QMI_M1_WFMT_PREFIX_WIDTH_VALUE_Q << QMI_M1_WFMT_PREFIX_WIDTH_LSB) |
| 269 | + | (QMI_M1_WFMT_ADDR_WIDTH_VALUE_Q << QMI_M1_WFMT_ADDR_WIDTH_LSB) |
| 270 | + | (QMI_M1_WFMT_SUFFIX_WIDTH_VALUE_Q << QMI_M1_WFMT_SUFFIX_WIDTH_LSB) |
| 271 | + | (QMI_M1_WFMT_DUMMY_WIDTH_VALUE_Q << QMI_M1_WFMT_DUMMY_WIDTH_LSB) |
| 272 | + | (QMI_M1_WFMT_DUMMY_LEN_VALUE_NONE << QMI_M1_WFMT_DUMMY_LEN_LSB) |
| 273 | + | (QMI_M1_WFMT_DATA_WIDTH_VALUE_Q << QMI_M1_WFMT_DATA_WIDTH_LSB) |
| 274 | + | (QMI_M1_WFMT_PREFIX_LEN_VALUE_8 << QMI_M1_WFMT_PREFIX_LEN_LSB) |
| 275 | + | (QMI_M1_WFMT_SUFFIX_LEN_VALUE_NONE << QMI_M1_WFMT_SUFFIX_LEN_LSB); |
| 276 | + qmi_hw->m[1].wcmd = CMD_WRITE_QUAD << QMI_M1_WCMD_PREFIX_LSB; |
| 277 | + |
| 278 | + /* |
| 279 | + * Enable writes to XIP memory window 1. |
| 280 | + * |
| 281 | + * XIP memory is read-only by default (RP2350 datasheet §4.4.5, |
| 282 | + * XIP_CTRL.WRITABLE_M1). Without this bit, writes silently fail |
| 283 | + * and the XIP cache may return stale data. |
| 284 | + */ |
| 285 | + xip_ctrl_hw->ctrl |= XIP_CTRL_WRITABLE_M1_BITS; |
| 286 | + |
| 287 | + restore_interrupts(save); |
| 288 | +} |
| 289 | + |
| 290 | +void *psram_init(size_t *size_out) |
| 291 | +{ |
| 292 | + gpio_set_function(PICO_RP2350_PSRAM_CS_PIN, GPIO_FUNC_XIP_CS1); |
| 293 | + |
| 294 | + size_t psram_size = get_psram_size(); |
| 295 | + if (psram_size == 0) { |
| 296 | + printf("PSRAM not detected (check CS pin %d)\n", PICO_RP2350_PSRAM_CS_PIN); |
| 297 | + return NULL; |
| 298 | + } |
| 299 | + printf("PSRAM detected: %u KB\n", (unsigned)(psram_size / 1024)); |
| 300 | + |
| 301 | + setup_psram(); |
| 302 | + |
| 303 | + /* Verify with a write-readback test via uncached alias */ |
| 304 | + volatile uint32_t *test = (volatile uint32_t *)(XIP_NOCACHE_NOALLOC_BASE + 16 * 1024 * 1024); |
| 305 | + test[0] = 0xDEADBEEF; |
| 306 | + test[1] = 0x12345678; |
| 307 | + if (test[0] != 0xDEADBEEF || test[1] != 0x12345678) { |
| 308 | + printf("PSRAM verify failed: %08lx %08lx\n", |
| 309 | + (unsigned long)test[0], (unsigned long)test[1]); |
| 310 | + return NULL; |
| 311 | + } |
| 312 | + |
| 313 | + if (size_out) |
| 314 | + *size_out = psram_size; |
| 315 | + |
| 316 | + return (void *)(XIP_BASE + 16 * 1024 * 1024); |
| 317 | +} |
0 commit comments