OXYGENHUB
◆ main enhanced-x16-emulator / tests / test_vera_irq.c
17.2 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
9120e874 Andy McCall 1
// VERA's interrupt latch and mask: IEN ($9F26) and ISR ($9F27).
2
//
3
// Four sources reach ISR -- VSYNC, LINE, sprite collision and PCM AFLOW -- but
4
// only three of them are latches, and only three of ISR's eight bits can be
5
// cleared by writing to it. Which bits a write can store and which it can
6
// clear is the whole of what this file checks.
7
//
8
// Raster IRQ position is not covered. The plan rules it out as needing
9
// co-simulation, so events are used as events: nothing here asserts which
10
// scanline one arrives on.
11
//
12
// ORACLE: B -- X16Community/vera-module Verilog, tag v47.0.2
13
// (45cc1f053376dae12173ea63612820e4d289c0da). The cited lines are quoted
14
// inline; `python tests/fetch_vera_rtl.py` retrieves the sources to check them
15
// against. The IRQ logic is identical at v48.0.1, three lines further down
16
// top.v.
17
18
#include "support/video_fixture.h"
19
#include "support/harness.h"
20
21
#include "video.h"
22
23
#include <stdint.h>
24
25
#define VERA_CTRL       0x05
26
#define VERA_IEN        0x06
27
#define VERA_ISR        0x07
28
#define VERA_IRQLINE    0x08 // write: IRQLINE[7:0]. read: SCANLINE[7:0]
29
#define VERA_AUDIO_CTRL 0x1B
30
#define VERA_AUDIO_DATA 0x1D
31
32
#define IRQ_VSYNC  0x01
33
#define IRQ_LINE   0x02
34
#define IRQ_SPRCOL 0x04
35
#define IRQ_AFLOW  0x08
36
37
#define SPRITE_ATTR 0x1FC00
38
#define SPRITE_GFX  0x01000
39
40
// video_step() advances the scan position by PIXEL_FREQ * steps / mhz and
41
// subtracts one scan width per call, so any step wider than VGA_SCAN_WIDTH
42
// (800) advances exactly one line.
43
static void
44
step_line(void)
45
{
46
	video_step(25.0f, 801.0f, false);
47
}
48
49
// SCAN_HEIGHT lines: every scanline value is passed exactly once and the
50
// position ends where it began.
51
static void
52
step_frame(void)
53
{
54
	for (unsigned i = 0; i < 525; i++) {
55
		step_line();
56
	}
57
}
58
59
static uint8_t
60
read_isr(void)
61
{
62
	return video_read(VERA_ISR, false);
63
}
64
65
static uint8_t
66
read_ien(void)
67
{
68
	return video_read(VERA_IEN, false);
69
}
70
71
// top.v:529-536
72
//     irq_enable_audio_fifo_low_r   <= 0;
73
//     irq_enable_vsync_r            <= 0;
74
//     irq_enable_line_r             <= 0;
75
//     irq_enable_sprite_collision_r <= 0;
76
//     irq_status_vsync_r            <= 0;
77
//     irq_status_line_r             <= 0;
78
//     irq_status_sprite_collision_r <= 0;
79
//     irq_line_r                    <= 0;
80
static void
81
test_reset_clears_the_enables_the_latches_and_irqline(void)
82
{
83
	video_reset();
84
	video_write(VERA_IEN, 0x0F);
85
	video_write(VERA_IRQLINE, 100);
86
	step_frame();
87
	check((read_isr() & (IRQ_VSYNC | IRQ_LINE)) != 0, "the frame left something to clear");
88
89
	video_reset();
90
	check_eq(read_ien() & 0x0F, 0, "reset clears the four enables");
91
	check_eq(read_isr() & 0x07, 0, "reset clears the three status latches");
92
93
	uint16_t line = 0xFFFF;
94
	video_get_irq_state(NULL, NULL, &line);
95
	check_eq(line, 0, "reset clears IRQLINE");
96
}
97
98
// top.v:334-335
99
//     if (do_write && access_addr == 5'h05) begin
100
//         fpga_reconfigure_next = write_data[7];
101
// Reconfiguring reloads the bitstream, so CTRL bit 7 reaches the reset block
102
// above rather than being a separate clear.
103
static void
104
test_ctrl_bit_7_resets_the_irq_registers(void)
105
{
106
	video_reset();
107
	video_write(VERA_IEN, 0x0F);
108
	step_frame();
109
	check((read_isr() & IRQ_VSYNC) != 0, "VSYNC was latched before the reset");
110
111
	video_write(VERA_CTRL, 0x80);
112
	check_eq(read_ien() & 0x0F, 0, "CTRL bit 7 clears the enables");
113
	check_eq(read_isr() & 0x07, 0, "CTRL bit 7 clears the status latches");
114
}
115
116
// top.v:431-437
117
//     if (do_write && access_addr == 5'h06) begin
118
//         irq_line_next[8]                 = write_data[7];
119
//         irq_enable_audio_fifo_low_next   = write_data[3];
120
//         irq_enable_sprite_collision_next = write_data[2];
121
//         irq_enable_line_next             = write_data[1];
122
//         irq_enable_vsync_next            = write_data[0];
123
//     end
124
//
125
// top.v:175
126
//     5'h06: rddata = {irq_line_r[8], scanline[8], 2'b0, irq_enable_audio_fifo_low_r, irq_enable_sprite_collision_r, irq_enable_line_r, irq_enable_vsync_r};
127
static void
128
test_ien_stores_four_enables_and_nothing_else(void)
129
{
130
	video_reset();
131
132
	for (unsigned bit = 0; bit < 4; bit++) {
133
		video_write(VERA_IEN, (uint8_t)(1u << bit));
134
		check_eq(read_ien() & 0x0F, 1u << bit, "each enable bit round-trips on its own");
135
	}
136
137
	// Bits 5 and 4 are 2'b0 in the read and have no target in the write.
138
	video_write(VERA_IEN, 0x30);
139
	check_eq(read_ien() & 0x3F, 0, "IEN bits 5 and 4 store nothing");
140
141
	// Through the register alone that is indistinguishable from a fifth bit
142
	// stored and then masked out on the way back, so check the byte itself:
143
	// top.v:433-436 assign four flops and there is nowhere else to put one.
144
	uint8_t stored = 0xFF;
145
	video_get_irq_state(&stored, NULL, NULL);
146
	check_eq(stored, 0, "and reach no enable register behind it");
147
148
	video_write(VERA_IEN, 0xFF);
149
	check_eq(read_ien() & 0x3F, 0x0F, "and read zero even when written as ones");
150
151
	video_get_irq_state(&stored, NULL, NULL);
152
	check_eq(stored, 0x0Fu, "with only the four enables stored");
153
}
154
155
// Bit 6 of the read is scanline[8], live, with no write target. Nothing is
156
// written to it here, so a set bit can only have come from the counter.
157
static void
158
test_ien_bit_6_reports_the_scanline(void)
159
{
160
	video_reset();
161
	video_write(VERA_IEN, 0x00);
162
163
	check_eq(read_ien() & 0x40, 0, "bit 6 is clear while the scanline MSB is");
164
	check_eq(video_read(VERA_IRQLINE, false), 0, "with $9F28 reporting the low byte");
165
166
	for (unsigned i = 0; i < 256; i++) {
167
		step_line();
168
	}
169
170
	check_eq(read_ien() & 0x40, 0x40, "bit 6 follows the scanline MSB");
171
	check_eq(video_read(VERA_IRQLINE, false), 0, "as the low byte wraps to zero");
172
}
173
174
// top.v:444-446
175
//     if (do_write && access_addr == 5'h08) begin
176
//         irq_line_next[7:0] = write_data;
177
//     end
178
// with bit 8 arriving from IEN at top.v:432, so the compare value is split
179
// across two registers and one of them also carries the enables.
180
static void
181
test_irqline_spans_ien_bit_7_and_9f28(void)
182
{
183
	uint16_t line;
184
185
	video_reset();
186
	video_write(VERA_IRQLINE, 0x55);
187
	video_get_irq_state(NULL, NULL, &line);
188
	check_eq(line, 0x055, "$9F28 writes the low eight bits");
189
190
	video_write(VERA_IEN, 0x80);
191
	video_get_irq_state(NULL, NULL, &line);
192
	check_eq(line, 0x155, "IEN bit 7 writes bit 8 and leaves the low byte");
193
	check_eq(read_ien() & 0x80, 0x80, "and reads back from IRQLINE, not from an enable");
194
195
	video_write(VERA_IEN, 0x0F);
196
	video_get_irq_state(NULL, NULL, &line);
197
	check_eq(line, 0x055, "so setting the enables clears bit 8 with them");
198
}
199
200
// top.v:438-443
201
//     if (do_write && access_addr == 5'h07) begin
202
//         // Clear status bits
203
//         irq_status_sprite_collision_next = irq_status_sprite_collision_r & !write_data[2];
204
//         irq_status_line_next             = irq_status_line_r             & !write_data[1];
205
//         irq_status_vsync_next            = irq_status_vsync_r            & !write_data[0];
206
//     end
207
static void
208
test_isr_latches_are_cleared_by_writing_one(void)
209
{
210
	video_reset();
211
	video_write(VERA_IRQLINE, 100);
212
	step_frame();
213
214
	// Exact rather than masked: a frame with no sprites must latch VSYNC and
215
	// LINE and nothing else, over AFLOW live from the empty FIFO.
216
	check_eq(read_isr(), IRQ_VSYNC | IRQ_LINE | IRQ_AFLOW, "a frame latches VSYNC and LINE");
217
218
	video_write(VERA_ISR, 0x00);
219
	check_eq(read_isr(), IRQ_VSYNC | IRQ_LINE | IRQ_AFLOW, "writing zero clears nothing");
220
221
	video_write(VERA_ISR, IRQ_LINE);
222
	check_eq(read_isr(), IRQ_VSYNC | IRQ_AFLOW, "writing one bit clears only that bit");
223
224
	// The latch holds until it is written, not until the next line.
225
	step_line();
226
	step_line();
227
	check_eq(read_isr(), IRQ_VSYNC | IRQ_AFLOW, "the remaining latch survives more scanlines");
228
229
	video_write(VERA_ISR, IRQ_VSYNC);
230
	check_eq(read_isr(), IRQ_AFLOW, "and clears when its own bit is written");
231
}
232
233
// top.v:176 takes bit 3 from a wire, and the write above has no clause for it.
234
//     5'h07: rddata = {sprite_collisions,   audio_fifo_low,              irq_status_sprite_collision_r, irq_status_line_r, irq_status_vsync_r};
235
//
236
// audio_fifo.v:24,28
237
//     wire [11:0] fifo_count = wridx_r - rdidx_r;
238
//     assign almost_empty = fifo_count < 12'd1024;
239
static void
240
test_aflow_is_live_and_cannot_be_written_away(void)
241
{
242
	video_reset();
243
	check_eq(read_isr() & IRQ_AFLOW, IRQ_AFLOW, "an empty FIFO reads as AFLOW");
244
245
	video_write(VERA_ISR, 0xFF);
246
	check_eq(read_isr() & IRQ_AFLOW, IRQ_AFLOW, "which a write of $FF does not clear");
247
248
	for (unsigned i = 0; i < 1023; i++) {
249
		video_write(VERA_AUDIO_DATA, 0x40);
250
	}
251
	check_eq(read_isr() & IRQ_AFLOW, IRQ_AFLOW, "1023 bytes is still below the mark");
252
253
	video_write(VERA_AUDIO_DATA, 0x40);
254
	check_eq(read_isr() & IRQ_AFLOW, 0, "1024 clears it with no write to ISR at all");
255
256
	// $9F3B bit 7 empties the FIFO, so the bit returns without ISR being
257
	// touched in either direction.
258
	video_write(VERA_AUDIO_CTRL, 0x80);
259
	check_eq(read_isr() & IRQ_AFLOW, IRQ_AFLOW, "and emptying the FIFO brings it back");
260
}
261
262
// top.v:1197-1200
263
//     wire [3:0] irq_enable = {irq_enable_audio_fifo_low_r, irq_enable_sprite_collision_r, irq_enable_line_r, irq_enable_vsync_r};
264
//     wire [3:0] irq_status = {audio_fifo_low,              irq_status_sprite_collision_r, irq_status_line_r, irq_status_vsync_r};
265
//
266
//     assign extbus_irq_n = (irq_status & irq_enable) == 0;
267
static void
268
test_the_irq_line_is_status_and_enable(void)
269
{
270
	video_reset();
271
272
	// AFLOW is set throughout, so a released line is a statement about IEN
273
	// rather than about there being nothing to report.
274
	check_eq(read_isr() & IRQ_AFLOW, IRQ_AFLOW, "AFLOW is set");
275
	check(!video_get_irq_out(), "a status bit with no enable does not assert");
276
277
	video_write(VERA_IEN, IRQ_AFLOW);
278
	check(video_get_irq_out(), "enabling that source asserts it");
279
280
	video_write(VERA_IEN, IRQ_VSYNC);
281
	check(!video_get_irq_out(), "a different enable does not");
282
283
	step_frame();
284
	check(video_get_irq_out(), "a latch against its own enable asserts it");
285
286
	video_write(VERA_ISR, IRQ_VSYNC);
287
	check(!video_get_irq_out(), "and clearing that latch releases it");
288
}
289
290
// Nothing in the read at top.v:175-176 has a side effect; only the write at
291
// top.v:438-443 clears. A debugger polling ISR must therefore not cost the
292
// program an interrupt.
293
static void
294
test_reading_the_status_does_not_clear_it(void)
295
{
296
	const uint8_t want = IRQ_VSYNC | IRQ_LINE | IRQ_AFLOW;
297
298
	video_reset();
299
	video_write(VERA_IEN, 0x0D);
300
	step_frame();
301
	check_eq(read_isr(), want, "the frame latched VSYNC and LINE");
302
303
	check_eq(video_read(VERA_ISR, false), want, "a second real read reports the same");
304
	check_eq(video_read(VERA_ISR, true), want, "and so does a debug read");
305
	check_eq(read_isr(), want, "with the latches still set after both");
306
307
	uint8_t dbg_ien = 0;
308
	uint8_t dbg_isr = 0;
309
	video_get_irq_state(&dbg_ien, &dbg_isr, NULL);
310
	check_eq(dbg_isr, want, "the debugger's own accessor agrees");
311
	check_eq(dbg_ien, 0x0Du, "as does its view of the enables");
312
	check_eq(read_isr(), want, "and reading through it clears nothing either");
313
}
314
315
// ─── Sprite collision, ISR bits 7:4 ─────────────────────────────────────────
316
//
317
// Unlike the three status latches, the collision nibble is not part of the
318
// interrupt register at all. It belongs to the sprite renderer, and the ISR
319
// write path has no way to reach it.
320
//
321
// sprite_renderer.v:37
322
//     assign collisions = frame_collision_mask_r;
323
//
324
// sprite_renderer.v:400-403
325
//     if (frame_done) begin
326
//         sprcol_irq                = (cur_collision_mask_r != 4'b0);
327
//         frame_collision_mask_next = cur_collision_mask_r;
328
//         cur_collision_mask_next   = 4'b0;
329
//     end
330
331
// Sixteen lines of 4bpp, eight bytes each, every pixel colour index 1.
332
// Collisions are recorded for non-transparent pixels only, so index 0 would
333
// never collide however the sprites are placed.
334
static void
335
fill_sprite_graphics(void)
336
{
337
	for (unsigned i = 0; i < 16 * 8; i++) {
338
		video_space_write(SPRITE_GFX + i, 0x11);
339
	}
340
}
341
342
// Eight attribute bytes per sprite. Byte 6 holds the collision mask in bits
343
// 7:4 and the z-depth in bits 3:2; a z-depth of zero is not drawn.
344
static void
345
place_sprite(unsigned n, uint8_t mask, uint8_t zdepth)
346
{
347
	const uint32_t attr = SPRITE_ATTR + n * 8;
348
349
	video_space_write(attr + 0, SPRITE_GFX >> 5);                  // address 12:5
350
	video_space_write(attr + 1, 0x00);                             // 4bpp, address 16:13
351
	video_space_write(attr + 2, 0x00);                             // X
352
	video_space_write(attr + 3, 0x00);
353
	video_space_write(attr + 4, 0x00);                             // Y
354
	video_space_write(attr + 5, 0x00);
355
	video_space_write(attr + 6, (uint8_t)(mask | (zdepth << 2)));
356
	video_space_write(attr + 7, 0x50);                             // 16x16, palette offset 0
357
}
358
359
// Two sprites on the same pixels. The second one drawn sees the first one's
360
// mask already in the line buffer, so the reported collision is the AND of
361
// the two masks.
362
static void
363
arm_two_sprites(uint8_t mask_a, uint8_t mask_b, uint8_t zdepth_b)
364
{
365
	video_reset();
366
	fill_sprite_graphics();
367
	place_sprite(0, mask_a, 1);
368
	place_sprite(1, mask_b, zdepth_b);
369
370
	video_write(VERA_CTRL, 0x00);
371
	video_write(0x09, 0x41); // DCSEL 0, $9F29: sprites on, VGA output
372
}
373
374
static void
375
test_a_collision_latches_the_status_and_the_nibble(void)
376
{
377
	arm_two_sprites(0x70, 0xD0, 1);
378
	step_frame();
379
380
	check_eq(read_isr() & 0xF0, 0x50, "the nibble reports the masks the sprites share");
381
	check_eq(read_isr() & IRQ_SPRCOL, IRQ_SPRCOL, "and the collision status latches with it");
382
}
383
384
// frame_collision_mask_next is assigned unconditionally at frame_done, so the
385
// nibble describes the frame just ended and nothing earlier. The status bit
386
// beside it is a latch and does not follow.
387
static void
388
test_the_nibble_is_replaced_each_frame_while_the_latch_holds(void)
389
{
390
	arm_two_sprites(0x70, 0xD0, 1);
391
	step_frame();
392
	check_eq(read_isr() & 0xF0, 0x50, "a collision in the first frame");
393
394
	// Take the second sprite out of the picture, so the next frame collides
395
	// with nothing.
396
	place_sprite(1, 0xD0, 0);
397
	step_frame();
398
399
	check_eq(read_isr() & 0xF0, 0x00, "a frame without one replaces the nibble with zero");
400
	check_eq(read_isr() & IRQ_SPRCOL, IRQ_SPRCOL, "while the status latch holds until written");
401
}
402
403
// top.v:440 clears the status latch and nothing beside it:
404
//     irq_status_sprite_collision_next = irq_status_sprite_collision_r & !write_data[2];
405
static void
406
test_clearing_the_status_leaves_the_nibble(void)
407
{
408
	arm_two_sprites(0x70, 0xD0, 1);
409
	step_frame();
410
411
	video_write(VERA_ISR, IRQ_SPRCOL);
412
	check_eq(read_isr() & IRQ_SPRCOL, 0, "the collision status clears when written");
413
	check_eq(read_isr() & 0xF0, 0x50, "leaving the nibble it was reported with");
414
415
	// The nibble has no clause in the ISR write at all, so the byte a program
416
	// reaches for to acknowledge everything at once cannot touch it either.
417
	arm_two_sprites(0x70, 0xD0, 1);
418
	step_frame();
419
	video_write(VERA_ISR, 0xFF);
420
	check_divergent((read_isr() & 0xF0) == 0x50,
421
	                "a write of $FF acknowledges the latches without disturbing the nibble",
422
	                "video.c:2910 clears all eight bits, but ISR bits 7:4 are "
423
	                "sprite_renderer.v's frame_collision_mask_r, which the write at "
424
	                "top.v:438-443 cannot reach; on the machine the nibble holds until "
425
	                "the next vblank replaces it");
426
}
427
428
// sprite_renderer.v:326-328
429
//     wire [3:0] collision =
430
//         linebuf_idx_r < 'd640 &&
431
//         (!pixel_is_transparent && sprite_collision_mask_r != 4'b0) ? (linebuf_rddata[15:12] & sprite_collision_mask_r) : 4'b0;
432
//
433
// The mask is used directly. It was inverted until 4f46cb32 (2023-01-26, "Fix
434
// sprite collision and improve write timings (again)"), which is the only
435
// change to this expression in the RTL's history -- so masks that share no bit
436
// distinguish the two forms rather than merely exercising one.
437
static void
438
test_masks_collide_only_where_they_overlap(void)
439
{
440
	arm_two_sprites(0x30, 0xC0, 1);
441
	step_frame();
442
443
	check_eq(read_isr() & 0xF0, 0x00, "disjoint masks do not collide");
444
	check_eq(read_isr() & IRQ_SPRCOL, 0, "and raise no collision status");
445
}
446
447
// top.v:1198 builds irq_status from four bits, and the nibble is not among
448
// them:
449
//     wire [3:0] irq_status = {audio_fifo_low, irq_status_sprite_collision_r, irq_status_line_r, irq_status_vsync_r};
450
static void
451
test_the_nibble_cannot_raise_the_interrupt_line(void)
452
{
453
	arm_two_sprites(0x70, 0xD0, 1);
454
	step_frame();
455
456
	video_write(VERA_ISR, IRQ_VSYNC | IRQ_LINE | IRQ_SPRCOL);
457
	for (unsigned i = 0; i < 1024; i++) {
458
		video_write(VERA_AUDIO_DATA, 0x40);
459
	}
460
	check_eq(read_isr(), 0x50, "with every latch cleared only the nibble is left");
461
462
	// Every enable there is: IEN has no bit for the nibble, which is the whole
463
	// of why it cannot interrupt.
464
	video_write(VERA_IEN, 0xFF);
465
	check(!video_get_irq_out(), "which no enable can turn into an interrupt");
466
}
467
468
int
469
main(void)
470
{
471
	test_reset_clears_the_enables_the_latches_and_irqline();
472
	test_ctrl_bit_7_resets_the_irq_registers();
473
	test_ien_stores_four_enables_and_nothing_else();
474
	test_ien_bit_6_reports_the_scanline();
475
	test_irqline_spans_ien_bit_7_and_9f28();
476
	test_isr_latches_are_cleared_by_writing_one();
477
	test_aflow_is_live_and_cannot_be_written_away();
478
	test_the_irq_line_is_status_and_enable();
479
	test_reading_the_status_does_not_clear_it();
480
	test_a_collision_latches_the_status_and_the_nibble();
481
	test_the_nibble_is_replaced_each_frame_while_the_latch_holds();
482
	test_clearing_the_status_leaves_the_nibble();
483
	test_masks_collide_only_where_they_overlap();
484
	test_the_nibble_cannot_raise_the_interrupt_line();
485
	return x16_test_summary("vera_irq");
486
}