pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
tms9900.h
Go to the documentation of this file.
1/**
2 * \file
3 * \brief pico9918-core - TMS9900 CPU interpreter (portable C)
4 *
5 * Copyright (c) 2026 Troy Schrapel
6 *
7 * This code is licensed under the MIT license
8 *
9 * https://github.com/visrealm/pico9918-core
10 *
11 * This is a full reimplementation of JasonACT's RP2040 thumb assembly core
12 * found in thumb9900_m0.S / thumb9900_m33.S. It is not cycle-accurate and
13 * intentionally mirrors the status-flag encoding used in the assembly
14 * (bits: LG=0x80, AG=0x40, EQ=0x20, C=0x10, OV=0x08, P=0x04) so existing
15 * GPU glue can remain unchanged. run9900_c takes a Tms9900Cpu the caller has
16 * initialised with tms9900_init; gpu.c adapts it to the assembly core's
17 * four-argument run9900 signature. PICO9918_GPU_C_CORE selects between them.
18 */
19
20#pragma once
21
22#include <stdbool.h>
23#include <stdint.h>
24
25#ifdef __cplusplus
26extern "C"
27{
28#endif
29
30/* Status flag bits (mirrors the assembly core's layout) */
31#define TMS_ST_LGT 0x80 /* Logic greater-than */
32#define TMS_ST_AGT 0x40 /* Arithmetic greater-than */
33#define TMS_ST_EQ 0x20 /* Equal */
34#define TMS_ST_C 0x10 /* Carry */
35#define TMS_ST_OV 0x08 /* Overflow */
36#define TMS_ST_P 0x04 /* Parity (odd) */
37
38/*
39 * Off-target, nothing watches memory for the library the way a Pico's MPU does, so
40 * a write to the GPU's DMA port is not seen until the run returns - which is far
41 * too late for a program that triggers a transfer and then keeps going. The
42 * interpreter reports writes instead. On a Pico the hardware does it and none of
43 * this is compiled.
44 */
45#if !defined(PICO_BUILD)
46#define TMS9900_WATCH_WRITES 1
47#endif
48
49/* TMS9900_STEP_HOOK is the build's to define, not this header's: it is there for
50 pico9918_debug.h's stepping entry and nothing else, and it widens this struct, so a
51 build without a debugger must not carry it. src/CMakeLists.txt sets it. */
52
53 typedef struct Tms9900Cpu
54 {
55 uint8_t* mem; /* Pointer to memory backing the CPU */
56 uint8_t* regx38; /* Pointer to the GPU control byte (TMS register 0x38) */
57 uint32_t pc; /* Program counter (uint32_t to handle WP=0xFFFE overflow) */
58 uint16_t wp; /* Workspace pointer */
59 uint16_t st; /* Status register (flag layout matches assembly core) */
60
61 /* Decode memory the way an F18A does: a few small windows above 16KB, each mirrored
62 across its 4KB and most of the space absent. False, which is what tms9900_init
63 leaves, is the PICO9918's own map - 64KB of memory, and what the assembly cores
64 see. Only a build that can answer as a plain F18A ever sets it. */
65 bool f18aMemory;
66#if defined(TMS9900_WATCH_WRITES)
67 /*
68 * Called after a write to an address the running program chose, or null.
69 *
70 * LIMITATION: workspace-relative writes are not reported - register stores and
71 * the context saves BLWP and XOP make. Those land at WP+n, and WP is >FFFE, so
72 * they can only reach a watched address if a program moves its workspace onto
73 * one with LWPI. Nothing does. Widening this means routing set_reg and the
74 * context saves through the same watch, which costs the CPU core's hot path a
75 * call per register write.
76 */
77 void (*onWrite)(uint8_t* mem, uint32_t addr);
78
79 /* Addresses the watcher wants: it is called only where
80 (addr & onWriteMask) == onWriteMatch, tested on the decoded address. Both
81 zero, which tms9900_init leaves, is every address - a watcher that cares
82 about a few narrows it here rather than being called to say no. */
83 uint32_t onWriteMask;
84 uint32_t onWriteMatch;
85#endif
86#if defined(TMS9900_STEP_HOOK)
87 /*
88 * Called before each instruction is fetched, with the PC it will come from, or
89 * null. Returning false stops the run there and is indistinguishable to the core
90 * from an exhausted budget: the PC is kept and outOfBudget is set, so the caller
91 * resumes where it left off.
92 */
93 bool (*onStep)(struct Tms9900Cpu* cpu);
94
95 /* Handed back to onStep untouched. The glue puts this run's context here. */
96 void* onStepData;
97#endif
98 } Tms9900Cpu;
99
100 /* Initialize a CPU context */
101 void tms9900_init(Tms9900Cpu* cpu, uint8_t* mem, uint8_t* regx38, uint16_t pc, uint16_t wp);
102
103 /* Execute until regx38 bit0 is cleared or an IDLE occurs. Returns final PC. */
104 uint16_t run9900_c(Tms9900Cpu* cpu);
105
106 /* The same, giving up after at most `budget` instructions - zero means no limit.
107 Returns the PC either way, which is what makes it resumable: a host with one
108 thread interleaves this with its renderer, and a program that waits on the
109 raster gets a raster that moves.
110
111 A budget that runs out and a program that parks on an IDLE or a self-jump both
112 leave the run flag set, so the flag cannot tell them apart. `outOfBudget`, if
113 given, does: only that one has work still to do.
114
115 The PC is not the whole of what a resume needs. A budget expires BETWEEN
116 instructions, which includes between a compare and the jump that reads what it
117 set, so `cpu->st` has to come back too: build the next call's Tms9900Cpu from the
118 one this returned rather than from tms9900_init, which starts the status at zero
119 and would have that jump decide on flags nothing set. */
120 uint16_t run9900_budget_c(Tms9900Cpu* cpu, uint32_t budget, bool* outOfBudget);
121
122#ifdef __cplusplus
123}
124#endif