Skip to content

Commit 0cf1ca9

Browse files
harukasanclaude
andcommitted
Use PSRAM for mruby heap
- Add PSRAM driver (src/psram.c) for APS6404L-3SQR-SN (8 MB QSPI) - Use bootrom flash_devinfo runtime API to configure QMI CS1 - Configure QMI M1 for QPI read/write (0xEB/0x38) - Replace 256 KB SRAM heap with 8 MB PSRAM heap Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0432384 commit 0cf1ca9

5 files changed

Lines changed: 353 additions & 9 deletions

File tree

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ set_target_properties(mruby PROPERTIES
100100
add_executable(harucom_os
101101
src/main.c
102102
src/hal.c
103+
src/psram.c
103104
${PICORUBY_ROOT}/mrbgems/picoruby-gpio/ports/rp2040/gpio.c
104105
)
105106

@@ -152,6 +153,8 @@ target_link_libraries(harucom_os
152153
pico_stdlib
153154
hardware_timer
154155
hardware_gpio
156+
hardware_flash
157+
hardware_sync
155158
mruby
156159
)
157160

example/hakodate-project

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 4b5ff3fa2722953dedba8b7f676f3eae62d0e07d

src/main.c

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99

1010
#include "pico/stdlib.h"
1111
#include "picoruby.h"
12-
13-
#define HEAP_SIZE (256 * 1024)
14-
static uint8_t heap_pool[HEAP_SIZE] __attribute__((aligned(8)));
12+
#include "psram.h"
1513

1614
static const char ruby_code[] = "led = GPIO.new(23, GPIO::OUT)\n"
1715
"loop do\n"
@@ -28,20 +26,28 @@ int main(void) {
2826
sleep_ms(2000); /* Wait for UART to stabilize */
2927

3028
printf("Harucom OS %s (built %s)\n", HARUCOM_VERSION, HARUCOM_BUILD_DATE);
31-
printf("Heap size: %d bytes\n", HEAP_SIZE);
3229

33-
/* 1. Initialize VM */
34-
mrb_state *mrb = mrb_open_with_custom_alloc(heap_pool, HEAP_SIZE);
30+
/* 1. Initialize PSRAM */
31+
size_t heap_size;
32+
void *heap_pool = psram_init(&heap_size);
33+
if (!heap_pool) {
34+
printf("PSRAM init failed\n");
35+
return 1;
36+
}
37+
printf("PSRAM heap: %u bytes at %p\n", (unsigned)heap_size, heap_pool);
38+
39+
/* 2. Initialize VM */
40+
mrb_state *mrb = mrb_open_with_custom_alloc(heap_pool, heap_size);
3541
if (!mrb) {
3642
printf("mrb_open failed\n");
3743
return 1;
3844
}
3945
global_mrb = mrb;
4046

41-
/* 2. Create compiler context */
47+
/* 3. Create compiler context */
4248
mrc_ccontext *cc = mrc_ccontext_new(mrb);
4349

44-
/* 3. Compile Ruby code on-the-fly */
50+
/* 4. Compile Ruby code on-the-fly */
4551
const uint8_t *src = (const uint8_t *)ruby_code;
4652
mrc_irep *irep = mrc_load_string_cxt(cc, &src, strlen(ruby_code));
4753
if (!irep) {
@@ -51,7 +57,7 @@ int main(void) {
5157

5258
printf("Compile OK\n");
5359

54-
/* 4. Create task and run */
60+
/* 5. Create task and run */
5561
mrb_value name = mrb_str_new_cstr(mrb, "blink");
5662
mrb_value task = mrc_create_task(cc, irep, name, mrb_nil_value(),
5763
mrb_obj_value(mrb->top_self));

src/psram.c

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
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

Comments
 (0)