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 typedef struct Tms9900Cpu
50 {
51 uint8_t* mem; /* Pointer to memory backing the CPU */
52 uint8_t* regx38; /* Pointer to the GPU control byte (TMS register 0x38) */
53 uint32_t pc; /* Program counter (uint32_t to handle WP=0xFFFE overflow) */
54 uint16_t wp; /* Workspace pointer */
55 uint16_t st; /* Status register (flag layout matches assembly core) */
56
57 /* Decode memory the way an F18A does: a few small windows above 16KB, each mirrored
58 across its 4KB and most of the space absent. False, which is what tms9900_init
59 leaves, is the PICO9918's own map - 64KB of memory, and what the assembly cores
60 see. Only a build that can answer as a plain F18A ever sets it. */
61 bool f18aMemory;
62#if defined(TMS9900_WATCH_WRITES)
63 /*
64 * Called after a write to an address the running program chose, or null.
65 *
66 * LIMITATION: workspace-relative writes are not reported - register stores and
67 * the context saves BLWP and XOP make. Those land at WP+n, and WP is >FFFE, so
68 * they can only reach a watched address if a program moves its workspace onto
69 * one with LWPI. Nothing does. Widening this means routing set_reg and the
70 * context saves through the same watch, which costs the CPU core's hot path a
71 * call per register write.
72 */
73 void (*onWrite)(uint8_t* mem, uint32_t addr);
74#endif
75 } Tms9900Cpu;
76
77 /* Initialize a CPU context */
78 void tms9900_init(Tms9900Cpu* cpu, uint8_t* mem, uint8_t* regx38, uint16_t pc, uint16_t wp);
79
80 /* Execute until regx38 bit0 is cleared or an IDLE occurs. Returns final PC. */
81 uint16_t run9900_c(Tms9900Cpu* cpu);
82
83 /* The same, giving up after at most `budget` instructions - zero means no limit.
84 Returns the PC either way, which is what makes it resumable: a host with one
85 thread interleaves this with its renderer, and a program that waits on the
86 raster gets a raster that moves.
87
88 A budget that runs out and a program that parks on an IDLE or a self-jump both
89 leave the run flag set, so the flag cannot tell them apart. `outOfBudget`, if
90 given, does: only that one has work still to do.
91
92 The PC is not the whole of what a resume needs. A budget expires BETWEEN
93 instructions, which includes between a compare and the jump that reads what it
94 set, so `cpu->st` has to come back too: build the next call's Tms9900Cpu from the
95 one this returned rather than from tms9900_init, which starts the status at zero
96 and would have that jump decide on flags nothing set. */
97 uint16_t run9900_budget_c(Tms9900Cpu* cpu, uint32_t budget, bool* outOfBudget);
98
99#ifdef __cplusplus
100}
101#endif