Build Toolchain¶
Build commands, cc900 compiler rules (C89 compliance, NGP_FAR, volatile, ISR, inline ASM, confirmed ABI), the memory model, known compiler/linker pitfalls, the open-source NgpCraft toolchain, and a reference description of the official CC900 toolchain internals.
1. cc900 C89 Rules¶
cc900 is Toshiba's C compiler for the TLCS-900/H CPU. It is strict C89 with proprietary extensions. The following restrictions apply.
1.1 No // Comments¶
1.2 Declarations at Block Start Only¶
/* CORRECT */
void foo(void) {
u8 i;
u8 j;
i = 0;
j = 1;
}
/* FORBIDDEN — declaration after a statement */
void foo(void) {
i = 0;
u8 j = 1; /* compile error */
}
1.3 No Declaration in for¶
/* CORRECT */
u8 i;
for (i = 0; i < 10; i++) { /* ... */ }
/* FORBIDDEN — C99 only */
for (u8 i = 0; i < 10; i++) { /* ... */ }
1.4 No 64-bit or Floating Point¶
1.5 Available Types¶
Defined in ngpc_types.h:
u8 /* unsigned char — 1 byte */
s8 /* signed char — 1 byte */
u16 /* unsigned int — 2 bytes */
s16 /* signed int — 2 bytes */
u32 /* unsigned long — 4 bytes */
s32 /* signed long — 4 bytes */
Important: on cc900,
unsigned long= 32 bits andunsigned int= 16 bits. Do not rely onintbeing a specific size without typedef.
2. Far Pointers — NGP_FAR¶
2.1 Why Far Pointers Are Required¶
cc900 uses 16-bit (near) pointers by default. Main RAM is at 0x004000-0x005FFF
(within near range). Cartridge ROM starts at 0x200000 — completely out of near reach.
Without NGP_FAR, the compiler generates a truncated 16-bit address -> reads from the
wrong location -> corrupted data, wrong image, or crash.
2.2 The NGP_FAR Macro¶
Defined in ngpc_types.h:
2.3 When to Use NGP_FAR¶
Always for pointers to ROM data (tiles, palettes, tilemaps, audio, text strings):
/* CORRECT — ROM tile data */
void ngpc_gfx_load_tiles_at(const u16 NGP_FAR *tiles, u16 count, u16 offset);
/* WRONG — compiles but reads from wrong address */
void ngpc_gfx_load_tiles_at(const u16 *tiles, u16 count, u16 offset);
Near pointers are fine for RAM data:
static u8 s_buffer[256]; /* static RAM — near OK */
NgpcRect *rect; /* struct in RAM — near OK */
const u16 NGP_FAR *tiles; /* ROM data — NGP_FAR required */
2.4 Near Pointer Extension¶
When cc900 passes a near pointer to a function expecting NGP_FAR, it zero-extends
the 16-bit address to 24 bits. For RAM (base 0x004000), this gives 0x004000 as
24-bit — correct. For ROM (0x200000), a near pointer cannot encode the address at all.
2.5 Fallback — Direct VRAM Macros¶
If display remains corrupted despite correct NGP_FAR usage (or for diagnostics),
use the direct tilemap-blit macros:
/* Direct VRAM write without passing a pointer as parameter */
NGP_TILEMAP_BLIT_SCR1(prefix, tile_base)
NGP_TILEMAP_BLIT_SCR2(prefix, tile_base)
These macros index the generated symbol directly within the compilation unit — no pointer parameter, so no near/far problem is possible.
Recommended decision tree:
1. Normal project -> use helpers (ngpc_gfx_load_tiles_at(), etc.)
2. Unexplained corrupted rendering -> switch to NGP_TILEMAP_BLIT_* to unblock
3. Any new helper reading const ROM data -> annotate parameters with NGP_FAR
3. volatile Keyword¶
Required for all hardware register accesses and all variables modified by ISRs:
/* Hardware registers: always volatile (defined in ngpc_hw.h) */
#define HW_RAS_V (*(volatile u8 *)0x8009)
/* Variables modified by ISR: volatile */
volatile u8 g_vb_counter; /* incremented in isr_vblank */
/* DMA counter: volatile, modified by hardware */
static volatile u8 s_done_flag[4];
Without volatile, the compiler may cache the value in a CPU register and never re-read
memory -> a wait loop never terminates.
4. Interrupt Functions¶
Use the __interrupt keyword for interrupt handlers. cc900 automatically generates
the RETI prologue/epilogue:
static void __interrupt isr_vblank(void)
{
HW_WATCHDOG = WATCHDOG_CLEAR;
g_vb_counter++;
}
/* Install the vector */
HW_INT_VBL = isr_vblank;
ISR rules:
- Keep ISRs as short as possible (budget is tight, especially HBlank ~ 30 cycles)
- Never call ngpc_vsync() from an ISR (deadlock)
- Shared variables must be declared volatile
5. Inline Assembly¶
5.1 Basic Syntax¶
One instruction per __asm() call:
__asm("ei"); /* enable interrupts */
__asm("di"); /* disable interrupts */
__asm("halt"); /* wait for interrupt */
__asm("swi 1"); /* BIOS call */
5.2 Stringifying C Constants¶
C constants are not directly visible in inline ASM. Use NGPC_STR(x) (defined in ngpc_hw.h):
#define BIOS_SHUTDOWN 0
#define NGPC_STR(x) #x
__asm("ldb rw3, " NGPC_STR(BIOS_SHUTDOWN));
/* generates: ldb rw3, 0 */
ld xhl, (symbol_c)does NOT compile with asm900 (Error-221: Undefined symbol). C symbols are not visible from inline ASM. Access arguments via the stack instead.
5.3 Stack Convention for Arguments¶
In a function with (void)arg to suppress unused warnings, arguments are at predictable
stack offsets (far call convention):
void my_func(const u8 NGP_FAR *ptr, u16 count)
{
(void)ptr; /* suppress "unused" warning */
(void)count;
__asm("ld xwa, (xsp+4)"); /* xwa = ptr (24-bit far) */
__asm("ld wa, (xsp+8)"); /* wa = count (16-bit) */
/* ... */
}
Keep such functions minimal — no local variables, no other C statements —
to ensure the stack prologue remains predictable for the xsp+N offset.
6. Memory Model and Sections¶
| Region | Address | Size | Usage |
|---|---|---|---|
| Main RAM | 0x004000-0x005FFF |
8 KB | C variables, stack, BSS |
| Battery RAM | 0x006000-0x006BFF |
3 KB | Lightweight saves (volatile) |
| Z80 RAM | 0x007000-0x007FFF |
4 KB | Audio driver (shared) |
| Cartridge ROM | 0x200000-0x3FFFFF |
2 MB | Code + assets (near pointers forbidden) |
The linker script (ngpc.lcf) defines all sections. The runtime bootstrap (in ngpc_sys.c)
copies initialized data ROM -> RAM and zeroes BSS at startup.
Useful linker symbols:
extern const u8 _DataROM_START; /* start of initialized data in ROM */
extern const u8 _DataROM_END;
extern u8 _DataRAM_START; /* destination in RAM */
extern u8 _Bss_START; /* start of BSS */
extern u8 _Bss_END;
RAM budget reality:
- Main RAM: 8 KB total
- 2 tilemap stream buffers (SCR1 + SCR2, 32x32 x u16): 4096 bytes (50% of RAM, untouchable)
- Remaining for stack + entities + other buffers: ~4 KB
7. Confirmed ABI¶
ABI confirmed by disassembly analysis of cc900-compiled code.
7.1 Return Registers¶
| C type | Return register |
|---|---|
u8 / s8 |
L (NOT A) |
u16 / s16 |
HL (NOT WA) |
u32 / s32 |
XHL (NOT XWA) |
7.2 Stack Layout (far call)¶
(XSP+0) = far return address (4 bytes)
(XSP+4) = arg0 (u8 uses 2 bytes on stack; u16=2B; far ptr=4B)
(XSP+6) = arg1 (if arg0 is u8 or u16)
7.3 Frame Allocation¶
cc900 uses lda XSP, XSP-N for stack frame allocation (NOT LINK/UNLK).
7.4 MUL/DIV — Hardware vs Library¶
| Operation | Codegen |
|---|---|
u16 * u16 (same width) |
HW opcode MUL (direct, fast) |
u16 * u8 (mixed width) |
Software call C9H_mullu (shift-and-add) |
s16 * s16 |
Software call C9H_mulls (via C9H_mullu) |
u16 / u16 |
Software call C9H_divlu |
C9H_* routines are provided by ngpc_runtime.c — no c900ml.lib needed.
To force HW MUL: cast both operands to the same width before multiplying:
7.5 Array Stride Codegen¶
| Array type | Stride instruction |
|---|---|
u8[] |
Direct index, no scaling |
u16[] |
add XWA, XWA (left shift x1) |
u32[] |
sll 0x2, WA (left shift x2) |
7.6 Miscellaneous Codegen Facts¶
switchwith >=8 dense cases -> cc900 generates a jump table automatically- cc900 never emits
DJNZ— usesdec reg; jr NZinstead - Callee-saved:
XIY,XIX(if used) - Caller-saved:
XWA,XBC,XDE,XHL
8. Known Bugs and Pitfalls¶
8.1 u8 x Constant: Silent 8-bit Overflow¶
Symptom: entities appear at completely wrong positions; coordinate calculations are corrupted without any compiler warning.
Cause: cc900 does not always perform standard C89 integer promotion for u8 * literal.
The expression u8_var * 8 may stay in u8 and silently overflow for values > 31
(e.g., 32 * 8 = 256 -> truncated to 0 in u8).
/* WRONG — overflow: 74 * 8 = 592 -> u8 -> 80 (completely wrong position) */
world_x = (s16)((src->x * 8) - render_off_x);
/* CORRECT — explicit cast before multiplication */
world_x = (s16)(((s16)src->x * 8) - render_off_x);
Rule: always cast to s16 or u16 before multiplying a u8 by a constant
that can produce a result > 255. Common cases: tile coordinates x 8, indices x stride.
8.2 Linker Order: Sprites After Maps -> Wrong Bank¶
Symptom: sprites are invisible (only 1 visible out of N), corrupted display for specific entities only.
Cause: the linker places f_const section data in ROM in the order .rel files appear.
Alphabetical sorting typically puts tilemaps before sprites. If tilemaps are large, they fill
bank 0x20 and sprites end up in bank 0x21 or 0x22. cc900's 16-bit near pointers can
only address bank 0x20 (0x200000-0x20FFFF). Result: a near NgpcMetasprite *def pointer
reads the wrong address -> invisible sprites.
Fix: in the Makefile (or generated asset Makefile), place *_mspr.rel files
before *_map.rel files. Sprites stay in bank 0x20.
# CORRECT order: sprites first, maps last
OBJS += $(OBJ_DIR)/player_mspr.rel
OBJS += $(OBJ_DIR)/enemy_mspr.rel
OBJS += $(OBJ_DIR)/level_map.rel # maps come last
Alternatively, in the asset export script:
def link_order_key(path):
name = path.name.lower()
if name.endswith("_mspr.c"): return (0, str(path)) # sprites first
if "_map.c" in name: return (2, str(path)) # maps last
return (1, str(path))
General rule: keep metasprites in bank 0x20. If the project grows and sprites risk
overflowing, migrate to far pointers in MsprAnimFrame.frame and ngpc_mspr_draw().
8.3 RAM: Oversized Entity Pools¶
Context: template runtimes allocate fixed-size arrays on main()'s stack for entities
(enemies, FX, props) and trigger regions. Generic defaults waste hundreds of bytes from
a total of only 8 KB of RAM.
Typical default values and RAM costs:
| Pool | Default | Struct size | RAM cost |
|---|---|---|---|
| Max enemies | 16 | ~44 bytes | 704 bytes |
| Max FX | 8 | ~22 bytes | 176 bytes |
| Max props | 16 | ~38 bytes | 608 bytes |
| Max regions | 64 | 1 byte | 64 bytes |
| Total | ~1552 bytes |
A minimal scene with 3 enemies and 6 props needs only ~572 bytes for pools. Savings: ~980 bytes = 12% of total RAM.
Define pool sizes via CDEFS in the Makefile:
CDEFS += -DNGPNG_AUTORUN_MAX_ENEMIES=4
CDEFS += -DNGPNG_AUTORUN_MAX_FX=4
CDEFS += -DNGPNG_AUTORUN_MAX_PROPS=8
CDEFS += -DNGPNG_AUTORUN_MAX_REG=8
Place pool overrides after any auto-generated CDEFS block to take precedence.
8.4 Windows Encoding¶
- Keep
.asmsource files in CRLF line endings, ASCII/ANSI encoding. - asm900 rejects files with BOM or non-ASCII characters.
- Keep documentation files in ASCII to avoid "ghost character" issues in PowerShell.
- File paths with spaces require quotes in make/batch commands.
8.5 Nested Initialized Declarations¶
Declaring and initializing a local inside a nested block can produce wrong code on
both cc900 and the open-source t900cc compiler. The stack slot can alias another
local, so subsequent reads return stale or unrelated values. Observed symptoms include
corrupted camera coordinates, false SOLID tile results in dungeon collision, and
sporadic gameplay bugs that disappear when the declaration is moved.
/* FORBIDDEN — nested initialized decl */
if (runtime_flag) {
s16 _dgn_dzx = (s16)sc->cam_follow_deadzone_x;
...
}
/* CORRECT — hoist decl to enclosing block, assign in the nested block */
s16 _dgn_dzx;
...
if (runtime_flag) {
_dgn_dzx = (s16)sc->cam_follow_deadzone_x;
...
}
Non-initialized nested decls are safer but not 100% confirmed. Prefer hoisting.
Same-family bug: t900cc also miscompiles s8 != s8 || s8 != s8. Store intermediate
comparison results in explicit u8 variables if the expression mixes multiple s8
comparisons.
8.6 Full Pitfall Table¶
| Symptom | Probable cause | Fix |
|---|---|---|
| Corrupted / shifted image | ROM pointer without NGP_FAR |
Add NGP_FAR to parameter |
| Infinite wait loop | ISR variable not volatile |
Add volatile |
| Compile error | // comments or late declarations |
Fix per section 1 |
| Crash or random behavior | Loop variable declared in for |
Declare u8 i; before for |
Wrong values from unsigned long |
Assumed 16-bit, cc900 uses 32-bit | Verify: cc900 u32 = unsigned long |
| False struct size assertion | Unexpected padding | Use u8 _pad[N] for explicit alignment |
| Entities at completely wrong positions | u8 * 8 silent overflow |
Cast to (s16) before multiply: (s16)src->x * 8 |
| Sprites invisible (1 of N visible) | Sprites in bank 0x21/0x22 (link order) | Put *_mspr.rel before *_map.rel |
| Low FPS with few entities | Full tilecol run for off-screen entities | Guard: skip physics if screen_x < -96 || > 256 |
| Enemies vanish as camera advances | Kill condition using camera-relative bounds | Use map bounds: world_x < -24 || world_x > map_w*8+24 |
| Out-of-RAM / random crash | Pool defaults 16/8/16/64 too large | Size pools to scene entity counts |
| Entity on path stuck and not moving | step_toward stops at +/-step without reaching exact target |
Snap-to-target: return diff when |diff| <= step |
| Enemies at wrong position before camera reaches them | Off-screen guard applies velocity to all | Freeze if screen_x > 256 (ahead), drift only if screen_x < -96 (behind) |
Quick Reference¶
| Item | Value / Rule |
|---|---|
| Comments | /* */ only — no // |
| Declarations | Block start only — no declaration after a statement |
| Loop variable | Declare before for — no for (u8 i = ...) |
| No 64-bit | No long long, no double, no float |
u32 type |
unsigned long (4 bytes on cc900) |
| NGP_FAR | __far — required for all ROM pointers |
| ROM base | 0x200000 — near pointer cannot reach it |
| volatile | Required for all HW regs + ISR-shared vars |
__interrupt |
Generates RETI prologue/epilogue automatically |
| Return u8 | Register L |
| Return u16 | Register HL |
| Return u32 | Register XHL |
| arg0 far call | (XSP+4) |
| arg1 far call | (XSP+6) |
| Same-width MUL | (u16)a * (u16)b -> HW opcode (fast) |
| Mixed-width MUL | u16 * u8 -> software C9H_mullu |
| DIV | Always software C9H_divlu |
u8 * const bug |
Silent 8-bit overflow — cast to s16 first |
| Link order | *_mspr.rel before *_map.rel — sprites in bank 0x20 |
| Main RAM | 8 KB — 4 KB consumed by SCR1+SCR2 stream buffers |
| ASM encoding | CRLF + ASCII/ANSI — no UTF-8 BOM |
| Nested init decls | Hoist to block start — cc900/NgpCraft codegen bug |
9. NgpCraft Open-Source Toolchain¶
The NgpCraft toolchain is a clean-room, Python-only replacement for Toshiba's
proprietary tools (cc900, asm900, tulink, tuconv, s242ngp). Its components are
t900cc (C compiler), t900as (assembler), t900ld (linker), and ngpc_romtool (ROM
packager). It can compile a full non-trivial game.
9.1 Pipeline¶
source.c
-> cpp -E (external preprocessor)
-> t900cc source.c -> source.asm
-> t900as source.asm -> source.t9obj
-> t900as crt0.asm -> crt0.t9obj
-> t900ld -m ngpc.lcf crt0.t9obj source.t9obj -> program.bin
-> ngpc_romtool program.bin --title "MYGAME" -> program.ngc
9.2 ABI __cdecl (args on stack)¶
The NgpCraft __cdecl ABI passes all arguments on the stack. It is not the same as
CC900's ABI:
| Aspect | NgpCraft ABI (__cdecl) |
CC900 (reference) |
|---|---|---|
| Arg passing | All args on stack, right-to-left | arg1=XWA, arg2=XBC, arg3=XDE, rest stack (__adecl) |
| 1-byte args | Extended to 2 bytes on stack | Same |
| Return <= 4 B | XHL |
XHL |
| Return > 4 B | Hidden pointer in XWA | Hidden pointer in XWA |
| Frame | link XIY, 0 / unlk XIY |
lda XSP, XSP-N (no link on leaf) |
| Locals access | XIY + disp |
XSP + disp or XIZ subregs |
| Caller-saved | XWA, XBC, XDE, XIX, XIY, XIZ | XWA, XBC, XDE, XHL |
| Callee-saved | XHL | XIY, XIX |
Consequence: mixing cc900-compiled object files with NgpCraft-compiled objects in the same ROM is not supported. Pick one toolchain per ROM.
9.3 Type Sizes and Pointer Model¶
char 1 B (signed default)
short / int 2 B
long 4 B
float / double -- excluded (no FPU, no soft-float)
pointers 4 B (all pointers are far — `large` addressing mode)
Near pointers and the tiny / small / medium addressing qualifiers are excluded.
NGP_FAR in project source is redundant on NgpCraft (every pointer is far) but
still required when the same code must build with both cc900 and NgpCraft.
9.4 Hardware-Validated Codegen — Silicon Quirks¶
The NgpCraft t900cc codegen emits only instructions confirmed safe by hardware bisect on
real NGPC silicon. Known broken opcodes explicitly avoided by codegen:
| Opcode pattern | Status | Workaround |
|---|---|---|
D0 prefix (all sub-ops: cpl WA, neg WA, sll N WA, sra ...) |
Broken silicon | Use D8 prefix (extz XWA then 32-bit op) or HL-based alternative |
adc W, B with W > 0 (CA 90) |
Broken | Loop count capped at 255 (W stays 0) |
add A, C (CB 81) + full CB family |
Broken | Use add A, L (CF 81) via HL |
link XIY, N with N >= 5 |
Broken | Frame capped at N <= 4, extra locals accessed via separate stack arithmetic |
inc WA (D0 61) |
Broken | ld BC, 1; add A, C; adc W, B (uses C safe, not B) |
srl/sll A, XDE with A=0 |
Zeroes XDE (not no-op) | or A, L guard (CF E1) before shift |
D1..D7 standalone |
Safe | Confirmed in CC900 production code |
LD R32, imm32 (0x40+R) |
Safe | Used for all 32-bit literal loads (function pointers etc.) |
ldirw / ldiw on NGPC |
Source = XIY, dest = XIX (not XDE/XHL) | Required save/restore of XIY through XHL |
All of this is enforced inside the codegen — project C source does not need to know about individual opcode bans.
9.5 Runtime Environment¶
crt0.asmcopies initialized data ROM -> RAM and zeroes BSS beforemain().ngpc_sys.candngpc_vramq.care both compiled C modules (no hand ASM).- ISR install pattern (mandatory for input to work): install
VBL_ISRat0x6FCCand enable withei 0. Without this, the BIOS never updates the joypad byte at0x6F82and buttons stay silent (see section 4, Interrupt Functions). - Power button is BIOS-trapped — a
ngpc_shutdown = retstub does NOT disable the hardware power button; it only suppressesTIME_SHUTDOWN_REQ(10-min idle) andBAT_SHUTDOWN_REQ(low battery). The power button is honored regardless.
9.6 ABI Tradeoffs vs CC900¶
The stack-based __cdecl ABI is simpler than CC900's register-based __adecl, but it
produces larger code and more frequent stack traffic. The structural differences that
account for most of the size delta are the per-call frame setup (link XIY / unlk XIY),
XIY + disp local access instead of XSP + disp, and fewer short relative calls (calr).
A register-passing __adecl ABI, no-frame leaf functions, relaxed jp/calr selection,
a u8 zero-extend peephole, and register auto-promotion are the main avenues for closing
the gap.
10. CC900 Toolchain Internals (Reference)¶
A full disassembly and dry-run analysis of the six Toshiba toolchain binaries decoded how
the official cc900 toolchain works. This is reference-grade information for anyone
reverse-engineering CC900 output, matching its codegen, or driving the proprietary tools
directly. It is observed behaviour and hardware facts, not Toshiba source.
10.1 Official Pipeline and Tool Roles¶
cc900.exe is just a driver. Its -# dry-run reveals it spawns two non-trivial stages,
then standard back-end tools:
source.c
+-[thc1.exe]-> TAC (frontend: lex/parse/typecheck/lower -> three-address-code text)
+-[thc2.exe]-> .asm (code generator: register allocation + all optimisation)
+-[asm900.exe]-> .rel (relocatable object)
+-[tulink.exe]-> .abs (absolute object; GNU-ld-style script)
+-[tuconv.exe -Fs24]-> .s24 (Motorola S2 record, 24-bit address)
+-[s242ngp]-> .ngp/.ngc (raw binary, BASE 0x200000, 0xFF fill)
| Tool | Role | Replaced by (NgpCraft) |
|---|---|---|
thc1.exe |
C -> TAC (parser/frontend) | NgpCraft frontend |
thc2.exe |
TAC -> asm (code generator — where optimisation lives) | NgpCraft backend |
asm900.exe |
asm -> .rel (assembler / ISA encoder) |
t900as |
tulink.exe |
.rel -> .abs (linker) |
t900ld |
tuconv.exe |
.abs -> Intel HEX / Motorola SREC |
ngpc_romtool |
s242ngp |
S24 SREC -> .ngp binary (small open homebrew utility, not Toshiba) |
ngpc_romtool |
Exact stage invocations (the driver passes -XO -XM defaults to both stages; -XM ~
maximum addressing = $MAXIMUM):
-O<n>(optimisation) is passed tothc2only. The code generator owns all optimisation.- The TAC is optimisation-independent: compiling with or without
-Oproduces the same TAC. Useful corollary for reproduction work — a stable TAC can be captured once and a code generator developed against it. -gadds debug info to both stages.
10.2 TAC — the thc1->thc2 Intermediate Representation¶
thc1.exe emits a readable three-address-code text on stdout. Each line is a quad
(OP, A1, A2, Re) where Re is the result/destination and A1/A2 are operands
(the operand order matches thc2's internal A1/A2/Re naming).
Operand forms:
| Form | Meaning |
|---|---|
Tn |
virtual temp n (typed) |
In |
named local n (frame slot) |
Pn |
parameter n |
A |
return register (WA/HL convention) |
#U<n> / #LU<n> / #<n> |
immediate: unsigned 16-bit / unsigned long 32-bit / raw (e.g. byte count) |
*Tn |
dereference (load/store via pointer) |
@_name |
global / function label |
Ln |
branch label |
Opcodes:
| OP | Semantics |
|---|---|
= |
assign / copy (implicit type conversion via the Re-temp) |
+ - * / % |
binary arithmetic |
& \| ^ |
bitwise |
+= -= ... |
compound assign (kept as a single op -> codegen may emit INC/RMW) |
== != < <= > >= |
comparisons -> bool temp (type 16) |
FALSE,cond,,L / TRUE,cond,,L |
conditional branch (if cond == 0 / != 0) |
JMP,L / LAB,L |
unconditional branch / label definition |
CARG,t |
push call argument |
CCALL,@_fn,#nbytes,Re |
call (nbytes = arg size, Re = result) |
BEGIN,fn / END,fn |
function begin / end (return) |
{,n / },n |
scope open / close |
LINE,n[,col] / FILE,path |
source markers (debug) |
Type table (numeric IDs used throughout the TAC):
| # | C type | # | C type | |
|---|---|---|---|---|
| 1 | void | 8 | signed long (s32) | |
| 2 | signed char (s8) | 9 | unsigned long (u32) | |
| 3 | unsigned char (u8) | 16 | bool (comparison result) | |
| 4 | signed short (s16) | 19 | pointer to type 3 (u8*) |
|
| 5 | unsigned short (u16) | 21 | pointer to type 5 (u16*) |
|
| 6 | int (signed 16-bit on NGPC) | 64+ | user-defined struct types | |
| 7 | unsigned int (u16) | (pointer types are derived via a symbol-table entry) |
The frontend does the heavy semantic work — &&/|| and switch are lowered to
branch chains, for to init; FALSE cond exit; body; step; loop, a->x and arr[i]
to address-compute + deref. The code generator receives an already-lowered IR.
10.3 Runtime Helpers (C9H_*)¶
The code generator emits library calls for operations the CPU cannot do in hardware.
Naming convention C9H_<op>l<u|s> (the l = long / 32-bit):
| Helper | Operation |
|---|---|
C9H_mullu / C9H_mulls |
u32 x u32 / s32 x s32 |
C9H_divlu / C9H_divls |
u32 / u32 / s32 / s32 |
C9H_remlu / C9H_remls |
u32 % u32 / s32 % s32 (remainder) |
_fadd_, _fcmp_, _fdiv_, _fld_, ... |
float (no FPU on TLCS-900) |
16-bit mul/div use native instructions; only 32-bit and float route through the
runtime. (Compare with section 7.4, which describes the same split from the project side.)
The NgpCraft runtime provides its own C9H_* implementations in ngpc_runtime.c — no
c900ml.lib needed.
10.4 asm900 Syntax and Directives¶
The asm emitted by thc2 (and accepted by asm900 / t900as) is tab-indented,
lowercase-mnemonic, with hex as 0x... and memory as (...):
Directives:
module <name>— module name;end— end of module.<name> section <type> <attrs>— e.g.f_code section code large align=1,1. Section types:code/data/const/area. Addressing class:large/medium/small. Attributes:align=N,M,abs=0xADDR,NOLOAD,ROMDATA.public _sym/extern large _sym— symbol visibility.$MAXIMUM— CPU addressing mode (set by the-XMdefault).- Data:
db/dw/dsw/ds.
The mnemonic->byte encoding is a hardware fact (the ngdis disassembler is a faithful
port of the Toshiba ISA spec; see TLCS-900/H Reference), so the
assembler does not need to be reverse-engineered from the binary.
10.5 Linker Script and ROM Packaging¶
tulink uses a GNU-ld-style script. Directives relevant to NGPC:
MEMORY { name : ORIGIN=..., LENGTH=... }— memory regions.SECTIONS { ... }— section placement;OVERLAYfor ROM banking;ALIGNfor alignment.NOLOAD— sections not loaded (BSS).ROMDATA— initialised data resident in ROM and copied to RAM at boot. This is the near/far data model: the crt0/startup performs the ROM->RAM copy (the same job the runtime bootstrap does in the NgpCraft runtime, section 6).- Targets:
-T900/-T9K16/-T9K32(TLCS-900 variants),-pic900(PIC).
ROM packaging (s242ngp): maps the ROM at BASE = 0x200000, fills unused space
with 0xFF, writes each S2 record's bytes at addr - BASE, and dumps the binary up to
the last used address. No cartridge header is added by the packager — the 64-byte
NGPC header (LICENSED BY SNK... at 0x200000) lives in the compiled startup code
itself. NgpCraft's ngpc_romtool follows the same convention.
See Also¶
- Hardware Registers — full memory map, hardware register addresses
- TLCS-900/H Reference — instruction set, opcode encodings, ABI details
- Assembly — asm900 syntax, gotchas, inline ASM, CRLF requirement
- Game Loop — VBlank ISR structure, watchdog, main loop skeleton
- Sprites and OAM — metasprite bank limit, NGP_FAR in sprite API
- Tilemaps and Scrolling — NGP_FAR in tilemap load functions
- BIOS — BIOS SWI calling convention,
swi 1usage