pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
host_bus.c
1/*
2 * pico9918-core - drive the VDP the way a guest machine does
3 *
4 * Copyright (c) 2026 Troy Schrapel
5 *
6 * This code is licensed under the MIT license
7 *
8 * https://github.com/visrealm/pico9918-core
9 *
10 * \example host_bus.c
11 *
12 * What a system emulator needs. render_frame.c uses the pico9918_util.h helpers to set
13 * a screen up; a guest machine has no helpers - it has two ports and four operations,
14 * and this is all of them:
15 *
16 * MODE=0 write pico9918_write_data() a byte into VRAM at the current address
17 * MODE=0 read pico9918_read_data() a byte out of it
18 * MODE=1 write pico9918_write_addr() half of a two-byte command
19 * MODE=1 read pico9918_read_status() SR0, and reading it CLEARS F and 5S
20 *
21 * Three details decide whether an emulator built on this behaves like the chip:
22 *
23 * The second byte of a MODE=1 pair says what the pair was: 0x80 a register write, 0x40
24 * a VRAM write address, 0x00 a VRAM read address. A read address also prefetches, so
25 * the first pico9918_read_data() after one returns the byte you seeked to.
26 *
27 * Reading the status port is destructive - it clears F and 5S and drops /INT - and
28 * that read IS the interrupt acknowledgement. pico9918_peek_status() does none of it.
29 *
30 * /INT is R1's enable bit AND SR0's F, so masking interrupts in R1 drops the line with
31 * no status read at all.
32 *
33 * Build it against an installed package:
34 *
35 * cmake -S examples -B build-examples
36 * cmake --build build-examples
37 * ./build-examples/host_bus
38 */
39
40#include "pico9918.h"
41
42#include <stdio.h>
43
44#define ACTIVE_LINES 192
45#define FRAME_LINES 262 /* NTSC: the rest is blanking, and the guest still gets it */
46
47/* The three two-byte commands. Value or low address byte first, then a byte whose top
48 bits say which of the three it was. */
49#define CMD_REGISTER 0x80
50#define CMD_VRAM_READ 0x00
51#define CMD_VRAM_WRITE 0x40
52
53static void busWriteRegister(PICO9918_INST_ARG uint8_t reg, uint8_t value)
54{
56 pico9918_write_addr(PICO9918_INST CMD_REGISTER | reg);
57}
58
59static void busSetAddress(PICO9918_INST_ARG uint8_t cmd, uint16_t addr)
60{
61 pico9918_write_addr(PICO9918_INST(uint8_t)(addr & 0xff));
62 pico9918_write_addr(PICO9918_INST cmd | (uint8_t)(addr >> 8));
63}
64
65int main(void)
66{
67#if PICO9918_SINGLE_INSTANCE
68 pico9918_init();
69#else
70 pico9918_t* tms9918 = pico9918_new();
71 if (!tms9918) return 1;
72#endif
74
75 /* The eight register writes a Graphics I title screen starts with. A guest runs these
76 out of its own ROM; these are the same bytes it would put on the bus. R2, R3 and R4
77 are table addresses, each in its own scaled unit. */
78 busWriteRegister(PICO9918_INST 0, 0x00);
79 busWriteRegister(PICO9918_INST 1, 0xe0); /* 16K, display on, interrupt enable */
80 busWriteRegister(PICO9918_INST 2, 0x0e); /* name table 0x3800 */
81 busWriteRegister(PICO9918_INST 3, 0x80); /* colour table 0x2000 */
82 busWriteRegister(PICO9918_INST 4, 0x01); /* pattern table 0x0800 */
83 busWriteRegister(PICO9918_INST 5, 0x76); /* sprite attrs 0x3B00 */
84 busWriteRegister(PICO9918_INST 6, 0x03); /* sprite patts 0x1800 */
85 busWriteRegister(PICO9918_INST 7, 0x04); /* backdrop: dark blue */
86
87 /* A glyph, its colour, and a screen of it - a byte at a time through the data port,
88 which advances the address itself. */
89 static const uint8_t glyph[8] = {0x3c, 0x42, 0x81, 0xa5, 0x81, 0x99, 0x42, 0x3c};
90 busSetAddress(PICO9918_INST CMD_VRAM_WRITE, 0x0800 + 8); /* pattern 1 */
91 for (int i = 0; i < 8; ++i) pico9918_write_data(PICO9918_INST glyph[i]);
92
93 busSetAddress(PICO9918_INST CMD_VRAM_WRITE, 0x2000); /* patterns 0-7 share one byte */
94 pico9918_write_data(PICO9918_INST 0xf4); /* white on dark blue */
95
96 busSetAddress(PICO9918_INST CMD_VRAM_WRITE, 0x3800);
97 for (int i = 0; i < 32 * 24; ++i) pico9918_write_data(PICO9918_INST(uint8_t)(i & 1));
98
99 busSetAddress(PICO9918_INST CMD_VRAM_READ, 0x0800 + 8);
100 const uint8_t first = pico9918_read_data(PICO9918_INST_ONLY);
101 const uint8_t second = pico9918_read_data(PICO9918_INST_ONLY);
102 printf("read back 0x%02X 0x%02X (wrote 0x%02X 0x%02X)\n", first, second, glyph[0], glyph[1]);
103
104 /* The frame loop. A guest's video interrupt fires once per frame and its handler does
105 the reading; everything here is what the emulator around the guest does. */
106 unsigned lit = 0, acks = 0;
107 for (uint16_t y = 0; y < FRAME_LINES; ++y)
108 {
109 if (y < ACTIVE_LINES)
110 {
112 const uint8_t* line = pico9918_line_source(PICO9918_INST_ONLY);
113 for (uint32_t x = 0; x < pico9918_line_bytes(PICO9918_INST_ONLY); ++x)
114 if ((line[x] & 0x0f) != 0x04) ++lit;
115 }
116 else if (y == ACTIVE_LINES)
117 {
119 }
120
121 /* The guest, polling its interrupt line. Reading the status port is what drops it,
122 so this runs exactly once per frame however many lines are left. */
124 {
125 const uint8_t sr0 = pico9918_read_status(PICO9918_INST_ONLY);
126 printf("line %u: /INT, SR0 0x%02X, now %s\n", y, sr0,
127 pico9918_interrupt_status(PICO9918_INST_ONLY) ? "still asserted" : "clear");
128 ++acks;
129 }
130 }
131 printf("%u lit pixels, %u interrupt%s in one frame\n", lit, acks, acks == 1 ? "" : "s");
132
133 /* F stays set through the mask, so unmasking R1 would bring the line straight back. */
135 busWriteRegister(PICO9918_INST 1, 0xc0); /* same as above, interrupt enable cleared */
136 printf("masked in R1: /INT %s, SR0 still 0x%02X\n",
137 pico9918_interrupt_status(PICO9918_INST_ONLY) ? "asserted" : "clear",
139
140#if !PICO9918_SINGLE_INSTANCE
142#endif
143 return 0;
144}
uint8_t pico9918_peek_status(pico9918_t *tms9918)
read from the status register without resetting it
Definition pico9918.c:385
uint8_t pico9918_read_data(pico9918_t *tms9918)
read data (mode = 0) from the tms9918
Definition pico9918.c:402
void pico9918_destroy(pico9918_t *tms9918)
destroy a TMS9918
Definition pico9918.c:360
void pico9918_write_addr(pico9918_t *tms9918, uint8_t data)
write an address (mode = 1) to the tms9918
Definition pico9918.c:373
const uint8_t * pico9918_line_source(pico9918_t *tms9918)
where the scanline just generated actually is.
Definition pico9918.c:3529
void pico9918_interrupt_set(pico9918_t *tms9918)
raise the INT status flag
Definition pico9918.c:420
uint32_t pico9918_line_bytes(pico9918_t *tms9918)
how many bytes of pixels[] this mode fills.
Definition pico9918.c:3518
uint8_t pico9918_scan_line(pico9918_t *tms9918, uint16_t y)
generate a scanline
Definition pico9918.c:3256
void pico9918_reset(pico9918_t *tms9918)
reset the new TMS9918
Definition pico9918.c:318
uint8_t pico9918_read_status(pico9918_t *tms9918)
read from the status register
Definition pico9918.c:379
bool pico9918_interrupt_status(pico9918_t *tms9918)
return true if both INT status and INT control set
Definition pico9918.c:414
void pico9918_write_data(pico9918_t *tms9918, uint8_t data)
write data (mode = 0) to the tms9918
Definition pico9918.c:395
pico9918-core - core interface
pico9918_t * pico9918_new(void)
create a new TMS9918
#define PICO9918_INST_ARG
declare the instance ahead of other parameters
Definition pico9918.h:71
#define PICO9918_INST_ONLY
pass the instance as the only argument
Definition pico9918.h:74
#define PICO9918_INST
pass the instance ahead of other arguments
Definition pico9918.h:73