ec82764bef
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression (272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps, and all dump-derived textures/traces) is excluded via .gitignore and stays local. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
7.5 KiB
C
121 lines
7.5 KiB
C
// retroDE_ps2 — ps2_lpddr_probe_test (Ch352 board bring-up: isolate the HPS->LPDDR write/read path)
|
|
//
|
|
// Codex debug step 1: prove the write-probe + read-probe ALONE, with a tiny known pattern, BEFORE the texture
|
|
// cache or renderer is involved. Writes N known words to a scratch LPDDR region, reads them back, and reports
|
|
// per-word: did the auto-increment pointer (0x04C) advance? did wr_busy (0x054 bit1) assert? does the readback
|
|
// match? This pinpoints whether "word 0 ok, rest 0" is a PACING race, an AUTO-INCREMENT bug, or a read-probe
|
|
// latency issue — none of which the cache/render path can disambiguate.
|
|
//
|
|
// Build on the board: gcc -O2 -o ps2_lpddr_probe_test ps2_lpddr_probe_test.c
|
|
// Run: sudo ./ps2_lpddr_probe_test [N=64] [--base 0x40000000] [--lpddr 0x100000]
|
|
//
|
|
// Pattern: word i = 0x11110000 + i (non-trivial, low bytes change per word so a stuck/striped path is obvious).
|
|
// Scratch LPDDR base defaults to 0x100000 (1 MiB) — clear of the texture region at 0x200000.
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include <sys/mman.h>
|
|
|
|
#define OFF_LPDDR_STATUS 0x02C // R: [3] rd_pending
|
|
#define OFF_LPDDR_RDADDR 0x03C // W: read byte addr + trigger ; R: result word
|
|
#define OFF_LPDDR_WRADDR 0x04C // RW: write byte addr (auto-inc +4 per WRDATA write)
|
|
#define OFF_LPDDR_WRDATA 0x050 // W: data word -> one 32-bit LPDDR write + addr+=4
|
|
#define OFF_TEX_FILL_CTRL 0x054 // R: [0]fill_done [1]wr_busy
|
|
#define OFF_LPDDR_WR_ERRS 0x06C // R: write-probe non-OKAY (BRESP) responses
|
|
#define OFF_LPDDR_STATUS2 0x02C // R: [0]wr_idle [3]rd_pending [4]scan_valid [5]scan_err
|
|
#define OFF_TEX_FILL_BEATS 0x058 // R: cache-fill beats (emif_clk READ liveness)
|
|
#define OFF_TEX_FILL_BYTES 0x05C // R: cache-fill bytes
|
|
#define OFF_TEX_RD_ERRS 0x068 // R: cache-fill non-OKAY read responses
|
|
#define WR_BUSY_BIT 0x2
|
|
#define WR_PENDING 0x4 // 0x054 bit2 — Ch352 STABLE write-done flag (poll this, not transient wr_busy)
|
|
#define RD_PENDING 0x8
|
|
#define FILL_DONE 0x1
|
|
|
|
typedef struct { volatile uint8_t *base; } br_t;
|
|
static void w(br_t *b,int o,uint32_t v){ *(volatile uint32_t*)(b->base+o)=v; }
|
|
static uint32_t r(br_t *b,int o){ return *(volatile uint32_t*)(b->base+o); }
|
|
|
|
int main(int argc,char**argv){
|
|
unsigned long base=0x40000000UL, lpddr=0x00100000UL; int N=64;
|
|
char *e=getenv("PS2_BRIDGE_BASE"); if(e) base=strtoul(e,NULL,0);
|
|
for(int i=1;i<argc;i++){
|
|
if(!strcmp(argv[i],"--base")&&i+1<argc) base=strtoul(argv[++i],NULL,0);
|
|
else if(!strcmp(argv[i],"--lpddr")&&i+1<argc) lpddr=strtoul(argv[++i],NULL,0);
|
|
else if(argv[i][0]!='-') N=atoi(argv[i]);
|
|
}
|
|
if(N<1||N>4096) N=64;
|
|
printf("[probe] N=%d bridge=0x%lx lpddr-scratch=0x%lx pattern word[i]=0x11110000+i\n", N, base, lpddr);
|
|
|
|
int fd=open("/dev/mem",O_RDWR|O_SYNC);
|
|
if(fd<0){ fprintf(stderr,"open /dev/mem (root?): %s\n",strerror(errno)); return 1; }
|
|
void*map=mmap(NULL,0x1000,PROT_READ|PROT_WRITE,MAP_SHARED,fd,(off_t)base);
|
|
if(map==MAP_FAILED){ fprintf(stderr,"mmap: %s\n",strerror(errno)); return 1; }
|
|
br_t b={(volatile uint8_t*)map};
|
|
|
|
// ---- WRITE: set WRADDR once, then stream; per word verify pointer advance + observe busy high->low ----
|
|
uint32_t err0 = r(&b,OFF_LPDDR_WR_ERRS);
|
|
w(&b,OFF_LPDDR_WRADDR,(uint32_t)lpddr);
|
|
int ptr_bad=0, pending_stuck=0;
|
|
for(int i=0;i<N;i++){
|
|
uint32_t data=0x11110000u+(uint32_t)i;
|
|
w(&b,OFF_LPDDR_WRDATA,data);
|
|
// Wait for the STABLE write_pending flag (0x054 bit2) to clear = the write-probe completed THIS write.
|
|
// Unlike the transient wr_busy level, this can't be missed by a slow Linux poll (Codex).
|
|
int g=0; while((r(&b,OFF_TEX_FILL_CTRL)&WR_PENDING) && g<2000000) g++;
|
|
if(g>=2000000) pending_stuck++;
|
|
uint32_t ptr=r(&b,OFF_LPDDR_WRADDR), exp=(uint32_t)lpddr+(uint32_t)(i+1)*4;
|
|
if(ptr!=exp){ if(ptr_bad<6) printf(" PTR after word %d: 0x%08x exp 0x%08x\n",i,ptr,exp); ptr_bad++; }
|
|
}
|
|
uint32_t err1=r(&b,OFF_LPDDR_WR_ERRS);
|
|
printf("[probe] wrote %d words. ptr-mismatches=%d write_pending_stuck=%d wr_bresp_errs: %u->%u\n",
|
|
N, ptr_bad, pending_stuck, err0, err1);
|
|
|
|
// ---- READBACK: per word set RDADDR, poll rd_pending clear, read result. Count rd_pending TIMEOUTS
|
|
// explicitly (Codex): if rd_pending never clears, the value read below is STALE — not a real LPDDR read. ----
|
|
int rd_bad=0, rd_timeouts=0;
|
|
for(int i=0;i<N;i++){
|
|
uint32_t addr=(uint32_t)lpddr+(uint32_t)i*4, exp=0x11110000u+(uint32_t)i;
|
|
w(&b,OFF_LPDDR_RDADDR,addr);
|
|
int g=0; while((r(&b,OFF_LPDDR_STATUS)&RD_PENDING)&&g<1000000) g++;
|
|
if(g>=1000000) rd_timeouts++; // rd_pending stuck -> the read is stale
|
|
uint32_t got=r(&b,OFF_LPDDR_RDADDR);
|
|
if(got!=exp){ if(rd_bad<8) printf(" RD word %d @0x%08x: got 0x%08x exp 0x%08x\n",i,addr,got,exp); rd_bad++; }
|
|
}
|
|
printf("[probe] readback mismatches=%d/%d rd_pending_timeouts=%d%s\n", rd_bad, N, rd_timeouts,
|
|
rd_timeouts? " (reads were STALE, not real LPDDR reads)":"");
|
|
|
|
// ---- EMIF READ-domain liveness: arm the texture-cache fill (issues emif_clk reads from LPDDR and
|
|
// counts beats), independent of the write path. This disambiguates the failure:
|
|
// beats advance -> emif READ domain is ALIVE => the bug is WRITE-specific (probe/pulse wiring).
|
|
// beats stay 0 -> the whole emif_clk domain is DEAD (EMIF didn't calibrate / clock / reset),
|
|
// which by itself explains busy-never-high AND the all-zero readback.
|
|
// Snapshot BEFORE arming (Codex): the fill counters reset only after the new toggle reaches the EMIF
|
|
// domain, so a stale fill_done=1 / beats from an earlier fill would be a false positive. Require, in order:
|
|
// fill_done seen LOW after arming (the NEW fill is in flight), then fill_done HIGH (it completed), with
|
|
// final beats=8192 / bytes=262144 / rd_errs=0.
|
|
uint32_t st0 = r(&b,OFF_LPDDR_STATUS2);
|
|
uint32_t fd0 = (r(&b,OFF_TEX_FILL_CTRL)&FILL_DONE)?1:0;
|
|
uint32_t beats0 = r(&b,OFF_TEX_FILL_BEATS);
|
|
w(&b,OFF_TEX_FILL_CTRL,FILL_DONE); // arm: toggle fill_start
|
|
int saw_low=0; for(int g=0;g<8000000;g++){ if(!(r(&b,OFF_TEX_FILL_CTRL)&FILL_DONE)){saw_low=1;break;} }
|
|
int fdone=0; for(int g=0;g<8000000;g++){ if( r(&b,OFF_TEX_FILL_CTRL)&FILL_DONE){fdone=1;break;} }
|
|
uint32_t beats=r(&b,OFF_TEX_FILL_BEATS), bytes=r(&b,OFF_TEX_FILL_BYTES), rderr=r(&b,OFF_TEX_RD_ERRS);
|
|
int fill_ok = saw_low && fdone && beats==8192 && bytes==262144 && rderr==0;
|
|
printf("[probe] EMIF fill liveness: pre(status=0x%08x fill_done=%u beats=%u) -> armed: saw_done_low=%d final_done=%d\n",
|
|
st0, fd0, beats0, saw_low, fdone);
|
|
printf("[probe] final beats=%u (exp 8192) bytes=%u (exp 262144) rd_errs=%u (exp 0)\n", beats, bytes, rderr);
|
|
if(fill_ok) printf("[probe] => emif READ domain ALIVE and well — failure is WRITE-specific.\n");
|
|
else if(beats||fdone) printf("[probe] => emif read PARTIALLY working (counts off) — investigate fill path / cal margin.\n");
|
|
else printf("[probe] => emif_clk domain appears DEAD (cal/clock/reset) — explains busy-never-high + zero readback.\n");
|
|
|
|
if(rd_bad==0 && ptr_bad==0) printf("[probe] write/read path clean.\n");
|
|
else printf("[probe] FAIL — write/read path broken; see liveness line above for which domain.\n");
|
|
munmap(map,0x1000); close(fd);
|
|
return (rd_bad||ptr_bad)?1:0;
|
|
}
|