Skip to content

Commit 963a4b8

Browse files
JoeMattclaude
andcommitted
Add SRAM interface test with custom EEPROM test ROM
- gen_eeprom_test_rom.c: generates a 128KB Jaguar ROM that writes known values (0xCAFE, 0xBEEF, 0xDEAD, 0x1234) to EEPROM via JERRY serial interface bit-banging - sram_test.c: headless test harness that loads the core via dlopen, runs the ROM, and verifies SRAM buffer contents, round-trip, and persistence across retro_reset() - Fix exit(0) on odd PC in M68KInstructionHook — a libretro core must never call exit() - Add SRAM test step to regression-test CI workflow Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 820aaba commit 963a4b8

5 files changed

Lines changed: 725 additions & 1 deletion

File tree

.github/workflows/regression-test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ jobs:
7777
chmod +x "${MINIRETRO_BIN}"
7878
./test/regression_test.sh ./${{ matrix.config.core }}
7979
80+
- name: Run SRAM interface tests
81+
run: ./test/sram_test.sh ./${{ matrix.config.core }}
82+
8083
- name: Upload diff artifacts
8184
if: failure()
8285
uses: actions/upload-artifact@v4

src/jaguar.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ void M68KInstructionHook(void)
172172
pcQPtr &= 0x3FF;
173173

174174
if (m68kPC & 0x01) // Oops! We're fetching an odd address!
175-
exit(0);
175+
return;
176176
}
177177

178178
/* Custom UAE 68000 read/write/IRQ functions */

test/sram_test.sh

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env bash
2+
#
3+
# SRAM interface test for virtualjaguar-libretro
4+
#
5+
# Generates a test ROM that writes known EEPROM values, then verifies
6+
# the libretro SRAM interface works correctly (pack/unpack/round-trip).
7+
#
8+
# Usage: ./test/sram_test.sh <core_path>
9+
# Example: ./test/sram_test.sh ./virtualjaguar_libretro.so
10+
#
11+
set -euo pipefail
12+
13+
CORE="${1:?Usage: $0 <core_path>}"
14+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15+
WORK_DIR="$(mktemp -d)"
16+
17+
trap 'rm -rf "${WORK_DIR}"' EXIT
18+
19+
# --- Detect platform ---
20+
LDFLAGS=""
21+
BUILD_CC="${CC:-cc}"
22+
case "$(uname -s)" in
23+
Linux)
24+
LDFLAGS="-ldl"
25+
;;
26+
esac
27+
28+
# --- Resolve core to absolute path ---
29+
CORE="$(cd "$(dirname "${CORE}")" && pwd)/$(basename "${CORE}")"
30+
31+
# --- Build ROM generator ---
32+
echo "==> Building EEPROM test ROM generator..."
33+
${BUILD_CC} -O2 -Wall -o "${WORK_DIR}/gen_eeprom_test_rom" \
34+
"${SCRIPT_DIR}/tools/gen_eeprom_test_rom.c"
35+
36+
# --- Generate test ROM ---
37+
echo "==> Generating EEPROM test ROM..."
38+
"${WORK_DIR}/gen_eeprom_test_rom" "${WORK_DIR}/eeprom_test.j64"
39+
40+
# --- Build SRAM test harness ---
41+
echo "==> Building SRAM test harness..."
42+
${BUILD_CC} -O2 -Wall -o "${WORK_DIR}/sram_test" \
43+
"${SCRIPT_DIR}/tools/sram_test.c" ${LDFLAGS}
44+
45+
# --- Run tests ---
46+
echo "==> Running SRAM tests..."
47+
"${WORK_DIR}/sram_test" "${CORE}" "${WORK_DIR}/eeprom_test.j64"

test/tools/gen_eeprom_test_rom.c

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
* gen_eeprom_test_rom.c — Generate a minimal Atari Jaguar ROM that writes
3+
* known values to the EEPROM via the JERRY serial interface (NM93C14).
4+
*
5+
* The ROM writes 0xCAFE to address 0 and 0xBEEF to address 1, then loops.
6+
* After running for a few hundred frames, the SRAM buffer should contain
7+
* these values packed big-endian at the corresponding offsets.
8+
*
9+
* Build: cc -o gen_eeprom_test_rom gen_eeprom_test_rom.c
10+
* Usage: ./gen_eeprom_test_rom eeprom_test.j64
11+
*
12+
* Jaguar ROM layout:
13+
* 0x000-0x003: unused (gets overwritten by SSP in RAM)
14+
* 0x400-0x403: ROM flags (0x04040404)
15+
* 0x404-0x407: Entry point address (0x00802000)
16+
* 0x2000+: 68K code
17+
*/
18+
19+
#include <stdio.h>
20+
#include <stdlib.h>
21+
#include <string.h>
22+
#include <stdint.h>
23+
24+
/* Jaguar EEPROM register addresses */
25+
#define JERRY_EE_DI 0x00F14801 /* Data In (write bit 0) */
26+
#define JERRY_EE_CS 0x00F15001 /* Chip Select (write to assert) */
27+
28+
/* TOM interrupt control register — writing 0 masks all interrupts */
29+
#define TOM_INT1 0x00F000E0 /* Interrupt control register 1 */
30+
31+
/* 68K instruction encoders.
32+
* All instructions are word-aligned (even byte count). */
33+
34+
/* MOVE.B #imm8, (abs32).L — 8 bytes */
35+
static void emit_move_b_imm_abs(uint8_t *buf, int *pos, uint8_t val, uint32_t addr)
36+
{
37+
buf[(*pos)++] = 0x13; buf[(*pos)++] = 0xFC;
38+
buf[(*pos)++] = 0x00; buf[(*pos)++] = val;
39+
buf[(*pos)++] = (addr >> 24) & 0xFF;
40+
buf[(*pos)++] = (addr >> 16) & 0xFF;
41+
buf[(*pos)++] = (addr >> 8) & 0xFF;
42+
buf[(*pos)++] = (addr >> 0) & 0xFF;
43+
}
44+
45+
/* MOVE.W #imm16, (abs32).L — 8 bytes */
46+
static void emit_move_w_imm_abs(uint8_t *buf, int *pos, uint16_t val, uint32_t addr)
47+
{
48+
buf[(*pos)++] = 0x33; buf[(*pos)++] = 0xFC;
49+
buf[(*pos)++] = (val >> 8) & 0xFF;
50+
buf[(*pos)++] = val & 0xFF;
51+
buf[(*pos)++] = (addr >> 24) & 0xFF;
52+
buf[(*pos)++] = (addr >> 16) & 0xFF;
53+
buf[(*pos)++] = (addr >> 8) & 0xFF;
54+
buf[(*pos)++] = (addr >> 0) & 0xFF;
55+
}
56+
57+
/* MOVE.L #imm32, (abs32).L — 12 bytes */
58+
static void emit_move_l_imm_abs(uint8_t *buf, int *pos, uint32_t val, uint32_t addr)
59+
{
60+
buf[(*pos)++] = 0x23; buf[(*pos)++] = 0xFC;
61+
buf[(*pos)++] = (val >> 24) & 0xFF;
62+
buf[(*pos)++] = (val >> 16) & 0xFF;
63+
buf[(*pos)++] = (val >> 8) & 0xFF;
64+
buf[(*pos)++] = (val >> 0) & 0xFF;
65+
buf[(*pos)++] = (addr >> 24) & 0xFF;
66+
buf[(*pos)++] = (addr >> 16) & 0xFF;
67+
buf[(*pos)++] = (addr >> 8) & 0xFF;
68+
buf[(*pos)++] = (addr >> 0) & 0xFF;
69+
}
70+
71+
/* MOVE #imm16, SR — 4 bytes (set status register, must be in supervisor mode) */
72+
static void emit_move_to_sr(uint8_t *buf, int *pos, uint16_t val)
73+
{
74+
buf[(*pos)++] = 0x46; buf[(*pos)++] = 0xFC;
75+
buf[(*pos)++] = (val >> 8) & 0xFF;
76+
buf[(*pos)++] = val & 0xFF;
77+
}
78+
79+
/* BRA.S offset (branch to self = 0x60FE for infinite loop) — 2 bytes */
80+
static void emit_bra_self(uint8_t *buf, int *pos)
81+
{
82+
buf[(*pos)++] = 0x60;
83+
buf[(*pos)++] = 0xFE;
84+
}
85+
86+
/* Write a single bit to the EEPROM data-in register */
87+
static void emit_ee_di(uint8_t *buf, int *pos, int bit)
88+
{
89+
emit_move_b_imm_abs(buf, pos, bit & 1, JERRY_EE_DI);
90+
}
91+
92+
/* Assert chip select */
93+
static void emit_ee_cs(uint8_t *buf, int *pos)
94+
{
95+
emit_move_b_imm_abs(buf, pos, 0x01, JERRY_EE_CS);
96+
}
97+
98+
/*
99+
* Emit code to write a 16-bit value to a 6-bit EEPROM address.
100+
*
101+
* NM93C14 serial protocol:
102+
* 1. Assert CS (also enables writes in VJ's implementation)
103+
* 2. START bit: 1
104+
* 3. Opcode WRITE: 01
105+
* 4. 6-bit address (MSB first)
106+
* 5. 16-bit data (MSB first)
107+
*/
108+
static void emit_eeprom_write(uint8_t *buf, int *pos, uint8_t addr, uint16_t data)
109+
{
110+
int i;
111+
112+
/* Assert CS — resets state machine & enables writes */
113+
emit_ee_cs(buf, pos);
114+
115+
/* START bit */
116+
emit_ee_di(buf, pos, 1);
117+
118+
/* Opcode: WRITE = 01 */
119+
emit_ee_di(buf, pos, 0);
120+
emit_ee_di(buf, pos, 1);
121+
122+
/* 6-bit address, MSB first */
123+
for (i = 5; i >= 0; i--)
124+
emit_ee_di(buf, pos, (addr >> i) & 1);
125+
126+
/* 16-bit data, MSB first */
127+
for (i = 15; i >= 0; i--)
128+
emit_ee_di(buf, pos, (data >> i) & 1);
129+
}
130+
131+
int main(int argc, char **argv)
132+
{
133+
FILE *fp;
134+
uint8_t rom[0x20000]; /* 128KB ROM — minimum for JST_ROM detection */
135+
int code_pos, rte_addr;
136+
137+
if (argc != 2)
138+
{
139+
fprintf(stderr, "Usage: %s <output.j64>\n", argv[0]);
140+
return 1;
141+
}
142+
143+
memset(rom, 0xFF, sizeof(rom));
144+
145+
/* ROM header at offset 0x400 */
146+
rom[0x400] = 0x04; rom[0x401] = 0x04;
147+
rom[0x402] = 0x04; rom[0x403] = 0x04;
148+
149+
/* Entry point at 0x802000 (ROM offset 0x2000) */
150+
rom[0x404] = 0x00; rom[0x405] = 0x80;
151+
rom[0x406] = 0x20; rom[0x407] = 0x00;
152+
153+
/* 68K code starts at ROM offset 0x2000.
154+
*
155+
* The first thing we do is set up exception vectors in RAM so that
156+
* any interrupt (TOM halfline, etc.) doesn't jump to random garbage.
157+
* We place an RTE instruction in RAM and point all vectors at it.
158+
*
159+
* We also mask TOM interrupts to prevent the halfline callback
160+
* from interfering before we're done writing to EEPROM. But the
161+
* emulator's event system runs independently of 68K interrupts,
162+
* so frame boundaries still work. */
163+
code_pos = 0x2000;
164+
165+
/* Disable 68K interrupts by setting SR interrupt mask to 7 (supervisor mode).
166+
* SR = 0x2700: supervisor mode, interrupt mask = 7 */
167+
emit_move_to_sr(rom, &code_pos, 0x2700);
168+
169+
/* Place an RTE instruction at a known RAM address (0x1000).
170+
* Then point all exception vectors (0x08-0x3FC) at it. */
171+
rte_addr = 0x001000;
172+
173+
/* Write RTE (0x4E73) at RAM 0x1000 */
174+
emit_move_w_imm_abs(rom, &code_pos, 0x4E73, rte_addr);
175+
176+
/* Also write a BRA.S self (0x60FE) right after as a safety net */
177+
emit_move_w_imm_abs(rom, &code_pos, 0x60FE, rte_addr + 2);
178+
179+
/* Set critical exception vectors to point to our RTE handler.
180+
* Vectors 0,1 are SSP and PC (already set by boot).
181+
* Only set the ones that might actually fire:
182+
* 2: Bus error, 3: Address error, 4: Illegal instruction,
183+
* 24: Spurious interrupt, 25-31: Autovector interrupts */
184+
{
185+
int vecs[] = { 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
186+
24, 25, 26, 27, 28, 29, 30, 31, -1 };
187+
int v;
188+
for (v = 0; vecs[v] >= 0; v++)
189+
emit_move_l_imm_abs(rom, &code_pos, rte_addr, vecs[v] * 4);
190+
}
191+
192+
/* Now do the actual EEPROM writes */
193+
194+
/* Write 0xCAFE to EEPROM address 0 */
195+
emit_eeprom_write(rom, &code_pos, 0x00, 0xCAFE);
196+
197+
/* Write 0xBEEF to EEPROM address 1 */
198+
emit_eeprom_write(rom, &code_pos, 0x01, 0xBEEF);
199+
200+
/* Write 0xDEAD to EEPROM address 2 */
201+
emit_eeprom_write(rom, &code_pos, 0x02, 0xDEAD);
202+
203+
/* Write 0x1234 to EEPROM address 63 (last address) */
204+
emit_eeprom_write(rom, &code_pos, 0x3F, 0x1234);
205+
206+
/* Infinite loop */
207+
emit_bra_self(rom, &code_pos);
208+
209+
if (code_pos > (int)sizeof(rom))
210+
{
211+
fprintf(stderr, "ERROR: code too large (%d bytes, max %d)\n",
212+
code_pos, (int)sizeof(rom));
213+
return 1;
214+
}
215+
216+
/* Write ROM file */
217+
fp = fopen(argv[1], "wb");
218+
if (!fp)
219+
{
220+
perror("fopen");
221+
return 1;
222+
}
223+
fwrite(rom, 1, sizeof(rom), fp);
224+
fclose(fp);
225+
226+
printf("Generated %s (%d bytes, code ends at offset 0x%04X)\n",
227+
argv[1], (int)sizeof(rom), code_pos);
228+
return 0;
229+
}

0 commit comments

Comments
 (0)