pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
platform_pico.h
Go to the documentation of this file.
1/**
2 * \file
3 * \brief pico9918-core - Platform Abstraction (RP2040 / RP2350)
4 *
5 * Copyright (c) 2021 Troy Schrapel
6 *
7 * This code is licensed under the MIT license
8 *
9 * https://github.com/visrealm/pico9918-core
10 *
11 * Included by impl/platform.h when PICO_BUILD is defined.
12 * Do not include directly.
13 */
14
15#pragma once
16
17#include <stdint.h>
18
19#include "hardware/dma.h"
20#include "hardware/gpio.h"
21#include "pico/divider.h"
22
23/*
24 * Tier-1 host op: drive the /INT pin.
25 *
26 * A single gpio_put with COMPILE-TIME polarity. No runtime branch, no function
27 * pointer - it expands on the per-active-scanline path.
28 *
29 * The host supplies the pin, and optionally the polarity, as compile definitions
30 * reaching every library TU (the PICO9918 firmware emits both from its root
31 * CMakeLists via add_definitions()):
32 *
33 * PICO9918_INT_GPIO - required. GPIO number driving /INT.
34 * PICO9918_INT_ACTIVE_HIGH - optional. Defined when the board drives INT high;
35 * absent means active-low, which is the TMS9918A part.
36 *
37 * No default pin: guessing one would silently drive the wrong line if the host's
38 * definition ever failed to arrive, and nothing downstream could detect it.
39 */
40#ifndef PICO9918_INT_GPIO
41#error "PICO9918_INT_GPIO must be defined by the host build (the GPIO driving /INT)"
42#endif
43
44#ifdef PICO9918_INT_ACTIVE_HIGH
45#define PICO9918_HOST_SET_INT(active) gpio_put(PICO9918_INT_GPIO, (active))
46#else
47#define PICO9918_HOST_SET_INT(active) gpio_put(PICO9918_INT_GPIO, !(active))
48#endif
49
50
51/*
52 * Section placement - fast SRAM banks.
53 */
54#define PICO9918_SECTION_SCRATCH_X(name) __attribute__((section(".scratch_x." #name)))
55#define PICO9918_SECTION_SCRATCH_Y(name) __attribute__((section(".scratch_y." #name)))
56
57/* Objects every byte of which is written before anything reads them, so they can skip
58 the crt0 .bss zero-fill. The SDK macro names the object, so it wraps the declarator. */
59#define PICO9918_UNINITIALIZED(decl) __uninitialized_ram(decl)
60
61/* Keep a cold function resident in flash (XIP) rather than copied to RAM, for a
62 firmware built copy_to_ram with PICO9918_COLD_IN_FLASH on. Not the SDK's
63 __in_flash_func: that host redefines it, and a library cannot depend on a host
64 header. See pico9918's src/xip.h for the reachability rule this relies on. */
65#if PICO9918_COLD_IN_FLASH
66#define PICO9918_IN_FLASH_FUNC(fn) __attribute__((section(".flashcode." __STRING(fn)), noinline)) fn
67#else
68#define PICO9918_IN_FLASH_FUNC(fn) fn
69#endif
70
71
72/*
73 * DMA abstraction
74 *
75 * THE CHANNEL NUMBERS ARE MACROS, NOT `extern unsigned int`, and that is a measured
76 * requirement rather than a style choice. The SDK's dma_channel_* helpers index
77 * dma_hw->ch[], so a COMPILE-TIME channel folds the whole thing to a store at a
78 * fixed address held once in the literal pool. An extern is opaque to the TU, so
79 * every access instead emits a load of the channel number, a shift by 6 to scale it
80 * to the 64-byte channel stride, and an add - three extra instructions AT EVERY SITE.
81 *
82 * This is the same trap pico9918HostOps.h records for the PIO state-machine indices,
83 * and it is a real one here: as externs, the seven fill sites on the scanline path
84 * each pay a load, a shift and an add for nothing, and the library's own fills in
85 * pico9918_scan_line and the mode renderers pay it too.
86 *
87 * A host that needs different channels overrides these before including the library,
88 * which is the documented mechanism for every other PICO9918_* platform macro. They
89 * must stay compile-time constants either way.
90 *
91 * COLLISION SAFETY. Every channel here is reserved at library init via
92 * PICO9918_DMA_CLAIM below, which panics ("DMA channel %d is already claimed") if
93 * anything else already holds one. dma_claim_unused_channel() was measured and
94 * rejected: it forces the numbers to runtime values, which the fill and copy sites
95 * would pay for at every expansion. Claiming a SPECIFIC channel gets the same
96 * guarantee for free, and a stronger one - allocation order cannot silently shuffle
97 * which channel the display owns.
98 */
99
100/*
101 * 32-bit fill instances
102 *
103 * Each instance is an independent DMA channel with its own read address and
104 * transfer count, so two fills can be in flight concurrently:
105 *
106 * PICO9918_FILL_BORDER - the scanline's border fill
107 *
108 * Compile-time channel numbers, for the reason stated above: an instance then costs
109 * NOTHING at runtime, each expansion resolving to the register writes alone.
110 *
111 * Usage:
112 * PICO9918_FILL32_INIT(inst, srcPtr) once, at library/frame init - never lazily
113 * PICO9918_FILL32_SET_COUNT(inst, n) set transfer count (no trigger)
114 * PICO9918_FILL32_TRIGGER(inst, dst) start fill at dst, reusing the last count
115 * PICO9918_FILL32_WAIT(inst) block until the fill completes
116 *
117 * SET_COUNT deliberately does not trigger, so a caller can "set count once,
118 * trigger twice" (the left/right border pair).
119 */
120#ifndef PICO9918_FILL_BORDER
121#define PICO9918_FILL_BORDER 2u
122#endif
123
124#define PICO9918_FILL32_INIT(inst, srcPtr) \
125 do \
126 { \
127 dma_channel_config _cfg = dma_channel_get_default_config(inst); \
128 channel_config_set_read_increment(&_cfg, false); \
129 channel_config_set_write_increment(&_cfg, true); \
130 channel_config_set_transfer_data_size(&_cfg, DMA_SIZE_32); \
131 dma_channel_set_config((inst), &_cfg, false); \
132 dma_channel_set_read_addr((inst), (srcPtr), false); \
133 } while (0)
134
135#define PICO9918_FILL32_SET_COUNT(inst, n) dma_channel_set_trans_count((inst), (n), false)
136#define PICO9918_FILL32_TRIGGER(inst, dstPtr) dma_channel_set_write_addr((inst), (dstPtr), true)
137#define PICO9918_FILL32_WAIT(inst) dma_channel_wait_for_finish_blocking(inst)
138
139/*
140 * Two more fill instances and a copy channel, for the unified tile pipeline.
141 *
142 * PICO9918_FILL_MASKS clears the row-mask block in one transfer
143 * PICO9918_FILL_LINE lays the background colour across a line
144 * PICO9918_COPY moves a layer run into the pixel line
145 *
146 * The copy channel runs byte-wide or word-wide depending on the source alignment of
147 * the run, so it carries two configs and selects one per run rather than rebuilding
148 * a config in the scanline path.
149 */
150#ifndef PICO9918_FILL_MASKS
151#define PICO9918_FILL_MASKS 5u
152#endif
153
154#ifndef PICO9918_FILL_LINE
155#define PICO9918_FILL_LINE 6u
156#endif
157
158#ifndef PICO9918_COPY
159#define PICO9918_COPY 7u
160#endif
161
162extern dma_channel_config pico9918_copy_byte;
163extern dma_channel_config pico9918_copy_word;
164
165/* The library defines the two configs above; off-target they do not exist. */
166#define PICO9918_COPY_STATE() \
167 dma_channel_config pico9918_copy_byte; \
168 dma_channel_config pico9918_copy_word;
169
170#define PICO9918_COPY_INIT(inst) \
171 do \
172 { \
173 dma_channel_config _cfg = dma_channel_get_default_config(inst); \
174 channel_config_set_write_increment(&_cfg, true); \
175 channel_config_set_high_priority(&_cfg, true); \
176 channel_config_set_transfer_data_size(&_cfg, DMA_SIZE_8); \
177 pico9918_copy_byte = _cfg; \
178 channel_config_set_transfer_data_size(&_cfg, DMA_SIZE_32); \
179 pico9918_copy_word = _cfg; \
180 dma_channel_set_config((inst), &pico9918_copy_byte, false); \
181 } while (0)
182
183#define PICO9918_COPY_SET_WIDTH(inst, wordAligned) \
184 dma_channel_set_config((inst), (wordAligned) ? &pico9918_copy_word : &pico9918_copy_byte, false)
185
186#define PICO9918_COPY_SET_SRC(inst, srcPtr) dma_channel_set_read_addr((inst), (srcPtr), false)
187#define PICO9918_COPY_SET_DST(inst, dstPtr) dma_channel_set_write_addr((inst), (dstPtr), false)
188#define PICO9918_COPY_TRIGGER(inst, n) dma_channel_set_trans_count((inst), (n), true)
189#define PICO9918_COPY_WAIT(inst) dma_channel_wait_for_finish_blocking(inst)
190
191/* Integer divide-modulo for the F18A timing counters. The RP2040's SIO has a hardware
192 divider behind this and the RP2350 does not, where the SDK's is software. Cold path. */
193#define PICO9918_DIVMOD_U32(n, d, q, r) \
194 do \
195 { \
196 divmod_result_t _dm = divmod_u32u32((n), (d)); \
197 (q) = to_quotient_u32(_dm); \
198 (r) = to_remainder_u32(_dm); \
199 } while (0)
200
201/*
202 * Reserve every channel this platform header names. Called once from library init,
203 * before any channel is configured. Panics on collision with any other user.
204 *
205 * dma_claim_mask takes the whole set in one call, so one bitmask replaces the
206 * per-channel calls and folds to a single constant argument.
207 */
208#define PICO9918_DMA_CLAIM() \
209 dma_claim_mask((1u << PICO9918_FILL_BORDER) | (1u << PICO9918_FILL_MASKS) | \
210 (1u << PICO9918_FILL_LINE) | (1u << PICO9918_COPY))
211
212
213/*
214 * Pixel output policy - BGR12 in the low 12 bits of a uint16_t (4-4-4, not 5-6-5).
215 *
216 * INPUT CONTRACT: the argument is a pram entry, which is *byte-swapped* RGB444,
217 * i.e. 0xGB0R - NOT the canonical 0x0RGB the macro name suggests. pram is filled
218 * either by bswap16 of a canonical 0xARGB literal (pico9918.c, palette reset
219 * and pico9918_config.c) or directly in that order by the F18A DPM port
220 * (pico9918_priv.h: palWriteStage0Value | (data << 8)). The name is therefore
221 * a misnomer, kept until the pico9918-core fork renames it (plan section 10).
222 *
223 * in: bits 15-12 = G, 11-8 = B, 7-4 = A or 0, 3-0 = R
224 * out: bits 15-12 = G (dead), 11-8 = B, 7-4 = G, 3-0 = R
225 *
226 * data &= 0xFF0F; clear bits 7-4. On the DPM path they are already
227 * zero, but the palette-reset path byte-swaps a
228 * 0xARGB literal and lands ALPHA there - this is
229 * what strips it. Not optional.
230 * data |= ((data >> 12) << 4); copy GREEN (not red) down into bits 7-4.
231 *
232 * So it is a bit shuffle, not a colour-space conversion. Verified against the DAC:
233 * the RGB pin group is 12 wide starting at GPIO2 (vga.h VGA_RGB_PINS_COUNT,
234 * vga.c sm_config_set_out_pins) with shift-right OSR, so word bit 0 drives red's
235 * LSB, bit 4 green's, bit 8 blue's; bits 15-12 never reach a pin.
236 *
237 * CAUTION - bits 15-12 are dead at the pin boundary but NOT everywhere: the
238 * RP2040 CRT-scanline dim shifts the whole word right by one UNMASKED
239 * (vga.c, currentBuffer[i] >>= 1), so anything in bit 12 lands in bit 11 -
240 * blue's MSB. Keep the top nibble zero in any value that can reach the
241 * framebuffer. The RP2350 branch masks 0x07770777 and is unaffected.
242 */
243typedef uint16_t PICO9918_PIXEL_T;
244
245#define PICO9918_PIXEL_FROM_RGB12(rgb) ((PICO9918_PIXEL_T)(((rgb) & 0xFF0F) | ((((rgb) & 0xFF0F) >> 12) << 4)))
246
247/* Both entries of a word at once: the transform above is a masked nibble move, so
248 one xor-and-mask over the pair does what two applications would. */
249#define PICO9918_PIXEL_FROM_RGB12_PAIR(packed) \
250 ((packed) ^ (((packed) ^ ((packed) >> 8)) & 0x00F000F0u))
251
252/* The same 16-bit pixel in both halves of a word, so a writer emits two pixels at a
253 time. The RP2040 arm avoids GCC's multi-instruction expansion of the constant
254 multiply, and its low-half extraction avoids the shift pair. */
255#if PICO_RP2040
256#define PICO9918_PIXEL_PAIR(p) \
257 __extension__({ \
258 uint32_t _v = (p), _f = 0x10001u; \
259 __asm__("mul %0, %1" : "+l"(_v) : "l"(_f) : "cc"); \
260 _v; \
261 })
262#define PICO9918_LOW16(x) \
263 __extension__({ \
264 uint32_t _l, _x = (x); \
265 __asm__("uxth %0, %1" : "=l"(_l) : "l"(_x)); \
266 _l; \
267 })
268#else
269#define PICO9918_PIXEL_PAIR(p) ((uint32_t)(p) * 0x10001u)
270#define PICO9918_LOW16(x) ((uint32_t)(uint16_t)(x))
271#endif
272
273/* Dim an existing pixel (diagnostics overlay) - two stops down, per channel. */
274#define PICO9918_PIXEL_DARKEN(p) ((PICO9918_PIXEL_T)(((p) >> 2) & 0x333))
275
276/* One stop down, both pixels of a word at once - the CRT-scanline effect. The mask is
277 what keeps each channel's LSB out of the channel below it, and out of the next
278 pixel's MSB. vga.c omits it on RP2040, where the strays are dead at the pins and the
279 pindir mask covers the rest; anything writing to a host buffer cannot. */
280#define PICO9918_PIXEL_PAIR_DIM(w) (((w) >> 1) & 0x07770777u)
281
282/*
283 * The unit the overlay's glyph blit works on. Two pixels to a word here, so a
284 * glyph cell is three stores rather than six, and the darkened background falls
285 * out of the same masked expression as the ink.
286 */
287typedef uint32_t PICO9918_INK_T;
288#define PICO9918_INK_PIXELS 2
289#define PICO9918_INK_FILL(fg) PICO9918_PIXEL_PAIR(fg)
290#define PICO9918_INK_DARKEN(w) (((w) >> 2) & 0x03330333u)
291#define PICO9918_INK_ONE(k) (0xffffu << ((k) * 16))
292
293/*
294 * Palette LUT - 256 entries of a packed pixel *pair* (two 16-bit pixels in one
295 * 32-bit word), so the expansion loop emits one store per two output pixels.
296 */
297typedef uint32_t PICO9918_PALETTE_LUT_T;
298
299/*
300 * Indexed bytes -> pixel pairs. 8-way unrolled; n is a multiple of 8
301 * (TMS9918_PIXELS_X and its V9938 multiples all are).
302 */
303#if PICO_RP2040
304#include "hardware/interp.h"
305
306/*
307 * Four packed indices in, four LUT addresses out, from the two interpolators - so the
308 * line is read a word at a time and the LUT is never indexed here. PICO9918_EXPAND_INIT
309 * points them at the LUT, and must run on the core that does the expanding: the
310 * interpolators are per-core state.
311 */
312#define PICO9918_EXPAND_INIT(lut) \
313 do \
314 { \
315 const uint8_t _shift[4] = {0, 8, 14, 22}; \
316 for (uint _i = 0; _i < 4; ++_i) \
317 { \
318 interp_hw_t* _interp = (_i & 2) ? interp1 : interp0; \
319 const uint _lane = _i & 1; \
320 interp_config _c = interp_default_config(); \
321 interp_config_set_shift(&_c, _shift[_i]); \
322 interp_config_set_mask(&_c, 2, 9); \
323 interp_config_set_cross_input(&_c, _lane == 1); \
324 interp_set_config(_interp, _lane, &_c); \
325 interp_set_base(_interp, _lane, (uintptr_t)(lut)); \
326 } \
327 } while (0)
328
329#define PICO9918_EXPAND_INDEXED(dst, src, n, lut) \
330 do \
331 { \
332 const uint32_t* _s = (const uint32_t*)(src); \
333 const uint32_t* _e = _s + (n) / 4; \
334 uint32_t* _d = (uint32_t*)(dst); \
335 while (_s < _e) \
336 { \
337 uint32_t _w = *_s++; \
338 interp1->accum[0] = _w; \
339 interp0->accum[0] = _w << 2; \
340 _d[0] = *(const uint32_t*)interp0->peek[0]; \
341 _d[1] = *(const uint32_t*)interp0->peek[1]; \
342 _d[2] = *(const uint32_t*)interp1->peek[0]; \
343 _d[3] = *(const uint32_t*)interp1->peek[1]; \
344 _w = *_s++; \
345 interp1->accum[0] = _w; \
346 interp0->accum[0] = _w << 2; \
347 _d[4] = *(const uint32_t*)interp0->peek[0]; \
348 _d[5] = *(const uint32_t*)interp0->peek[1]; \
349 _d[6] = *(const uint32_t*)interp1->peek[0]; \
350 _d[7] = *(const uint32_t*)interp1->peek[1]; \
351 _d += 8; \
352 } \
353 } while (0)
354#else
355/*
356 * The RP2350 has interpolators; what it lacks is the IOPORT that makes a peek free. Over AHB a
357 * peek costs about an SRAM read while Thumb-2 folds the indexing into the load, so routing
358 * through them would add work here - and its SHIFT rotates rather than shifts, so the lane
359 * config above would not port as written.
360 */
361#define PICO9918_EXPAND_INIT(lut) ((void)0)
362
363#define PICO9918_EXPAND_INDEXED(dst, src, n, lut) \
364 do \
365 { \
366 const uint8_t* _s = (const uint8_t*)(src); \
367 const uint8_t* _e = _s + (n); \
368 uint32_t* _d = (uint32_t*)(dst); \
369 const PICO9918_PALETTE_LUT_T* _l = (lut); \
370 while (_s < _e) \
371 { \
372 _d[0] = _l[_s[0]]; \
373 _d[1] = _l[_s[1]]; \
374 _d[2] = _l[_s[2]]; \
375 _d[3] = _l[_s[3]]; \
376 _d[4] = _l[_s[4]]; \
377 _d[5] = _l[_s[5]]; \
378 _d[6] = _l[_s[6]]; \
379 _d[7] = _l[_s[7]]; \
380 _d += 8; \
381 _s += 8; \
382 } \
383 } while (0)
384#endif
385
386/*
387 * The 80-column 8bpp line, which is already at full pixel width: a LUT entry holds the
388 * colour in both halves, so a destination word is two indices and two lookups rather
389 * than one index doubled. Only reachable on a board with that tier, which is why it
390 * needs no RP2040 arm.
391 */
392#define PICO9918_EXPAND_INDEXED_WIDE(dst, src, n, lut) \
393 do \
394 { \
395 const uint8_t* _s = (const uint8_t*)(src); \
396 const uint8_t* _e = _s + (n); \
397 uint32_t* _d = (uint32_t*)(dst); \
398 const PICO9918_PALETTE_LUT_T* _l = (lut); \
399 while (_s < _e) \
400 { \
401 _d[0] = (_l[_s[0]] & 0xffff) | (_l[_s[1]] << 16); \
402 _d[1] = (_l[_s[2]] & 0xffff) | (_l[_s[3]] << 16); \
403 _d[2] = (_l[_s[4]] & 0xffff) | (_l[_s[5]] << 16); \
404 _d[3] = (_l[_s[6]] & 0xffff) | (_l[_s[7]] << 16); \
405 _d += 4; \
406 _s += 8; \
407 } \
408 } while (0)