pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
config_test.c
Go to the documentation of this file.
1/**
2 * \file
3 * \brief pico9918-core - the config block's validation, defaults and migration
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 * Nothing else covers these. The goldens and the scene suite both start from a block
12 * that is already valid, so the paths that decide what "valid" means - the identity
13 * check, the reset to defaults, and the per-version migration that decides which
14 * fields a firmware upgrade re-defaults - have never been exercised by a test.
15 *
16 * The migration case is the one that matters: it is driven by a version number
17 * compared against this library's own descriptor table, and the two have to come from
18 * the same build to mean anything.
19 */
20
21#include "pico9918_config.h"
22
23#include <stdio.h>
24#include <string.h>
25
26#define HW_V0_3 0x03
27#define HW_V1_X 0x10
28#define HW_V2_X 0x20
29
30static int failures;
31
32static void check(const char* what, unsigned wanted, unsigned got)
33{
34 if (wanted == got) return;
35 ++failures;
36 printf(" FAIL %s: want %02x got %02x\n", what, wanted, got);
37}
38
39/* A block as a host would have stored it, then aged back to the given version. There is
40 no way to ask the library to stamp a version other than its own, which is the point -
41 so the stamp is rewritten here, the way a unit running an older release left it. */
42static void storedAt(uint8_t* config, uint8_t major, uint8_t minor, uint8_t patch)
43{
45 pico9918_config_prepare_save(config, HW_V1_X);
46
47 config[PICO9918_CONF_SW_VERSION] = (uint8_t)((major << 4) | minor);
48 config[PICO9918_CONF_SW_PATCH_VERSION] = patch;
49}
50
51/* The descriptor for a byte, so a case can assert against the table rather than against
52 a literal that has to be kept in step with it. */
53static const pico9918_config_field_t* field(uint8_t offset)
54{
55 for (size_t i = 0; i < pico9918_config_field_count; ++i)
56 {
57 if (pico9918_config_fields[i].offset == offset) return &pico9918_config_fields[i];
58 }
59 return NULL;
60}
61
62int main(void)
63{
64 uint8_t config[PICO9918_CONFIG_BYTES];
65 const pico9918_config_field_t* base = field(PICO9918_CONF_VDP_BASE);
66
67 if (!base)
68 {
69 printf("FAIL: no descriptor for PICO9918_CONF_VDP_BASE\n");
70 return 1;
71 }
72
73 /* 1. a block this build just saved is returned untouched, with nothing to persist */
74 {
75 uint8_t before[PICO9918_CONFIG_BYTES];
76
78 pico9918_config_prepare_save(config, HW_V1_X);
79 memcpy(before, config, sizeof(config));
80
81 check("round-trip reports a change", 0, pico9918_config_validate(config, HW_V1_X));
82 check("round-trip altered the block", 0, memcmp(before, config, sizeof(config)) != 0);
83 }
84
85 /* 2. an upgrade defaults the fields introduced since, and leaves the others alone */
86 {
87 storedAt(config, 1, 2, 0);
88 config[PICO9918_CONF_VDP_BASE] = 1;
89 config[PICO9918_CONF_CRT_SCANLINES] = 1;
90
91 check("upgrade reports no change", 1, pico9918_config_validate(config, HW_V1_X));
92 check("upgrade left byte 15 unswept", base->defaultValue, config[PICO9918_CONF_VDP_BASE]);
93 check("upgrade lost a settled field", 1, config[PICO9918_CONF_CRT_SCANLINES]);
94 check("upgrade did not ask to be saved", 1, config[PICO9918_CONF_SAVE_FORCED]);
95 }
96
97 /* 3. the stamp is this build's own version, and no caller can claim otherwise. A block
98 that says it is newer is still re-stamped, so a downgrade settles rather than
99 migrating on every boot. */
100 {
101 storedAt(config, 1, 2, 0);
102 pico9918_config_validate(config, HW_V1_X);
103 check("upgrade stamped a foreign version", PICO9918_BUILD_SW_VERSION,
104 config[PICO9918_CONF_SW_VERSION]);
105 check("upgrade stamped a foreign patch", PICO9918_BUILD_SW_PATCH,
106 config[PICO9918_CONF_SW_PATCH_VERSION]);
107
108 storedAt(config, 9, 9, 9);
109 check("downgrade reports no change", 1, pico9918_config_validate(config, HW_V1_X));
110 check("downgrade left a newer stamp", PICO9918_BUILD_SW_VERSION,
111 config[PICO9918_CONF_SW_VERSION]);
112 check("downgrade migrates a second time", 0, pico9918_config_validate(config, HW_V1_X));
113 }
114
115 /* 4. the model is the board revision's, not something a caller states separately */
116 {
118 pico9918_config_prepare_save(config, HW_V2_X);
119 check("v2.x is not the PRO tier", PICO9918_MODEL_RP2350, config[PICO9918_CONF_PICO_MODEL]);
120 check("v2.x lost its revision", HW_V2_X, config[PICO9918_CONF_HW_VERSION]);
121
123 pico9918_config_prepare_save(config, HW_V1_X);
124 check("v1.x is not the RP2040", PICO9918_MODEL_RP2040, config[PICO9918_CONF_PICO_MODEL]);
125
127 pico9918_config_prepare_save(config, HW_V0_3);
128 check("v0.3 is not the RP2040", PICO9918_MODEL_RP2040, config[PICO9918_CONF_PICO_MODEL]);
129 }
130
131 /* 5. a tier toggle corrects the identity and keeps the settings. One stored block has to
132 survive it, or a host offering both loses the user's settings on every switch. */
133 {
135 pico9918_config_prepare_save(config, HW_V2_X);
136 config[PICO9918_CONF_CRT_SCANLINES] = 1;
137
138 check("tier toggle asked to be persisted", 0, pico9918_config_validate(config, HW_V1_X));
139 check("tier toggle lost a setting", 1, config[PICO9918_CONF_CRT_SCANLINES]);
140 check("tier toggle left the model", PICO9918_MODEL_RP2040, config[PICO9918_CONF_PICO_MODEL]);
141 check("tier toggle left the revision", HW_V1_X, config[PICO9918_CONF_HW_VERSION]);
142 }
143
144 /* 6. junk is still junk: the marker and the range checks are what reset a block */
145 {
147 pico9918_config_prepare_save(config, HW_V1_X);
148 config[PICO9918_CONF_CRT_SCANLINES] = 1;
149 config[PICO9918_CONF_SCANLINE_SPRITES] = 0xff;
150
151 check("out-of-range block kept", 1, pico9918_config_validate(config, HW_V1_X));
152 check("out-of-range block not reset", 0, config[PICO9918_CONF_CRT_SCANLINES]);
153
155 pico9918_config_prepare_save(config, HW_V1_X);
156 config[PICO9918_CONF_CRT_SCANLINES] = 1;
157 config[PICO9918_CONF_PALETTE_IDX_0 + 2] &= 0x0f;
158
159 check("uninitialised block kept", 1, pico9918_config_validate(config, HW_V1_X));
160 check("uninitialised block not reset", 0, config[PICO9918_CONF_CRT_SCANLINES]);
161 }
162
163 /* 7. a command byte read back from storage is not a command */
164 {
166 pico9918_config_prepare_save(config, HW_V1_X);
167 config[PICO9918_CONF_SAVE_TO_FLASH] = 1;
168 config[PICO9918_CONF_PENDING_CONFIRM] = 1;
169
170 pico9918_config_validate(config, HW_V1_X);
171 check("stored save command survived", 0, config[PICO9918_CONF_SAVE_TO_FLASH]);
172 check("stored confirm command survived", 0, config[PICO9918_CONF_PENDING_CONFIRM]);
173 }
174
175 /* 8. no field may be stamped later than the build that carries it. A firmware upgrade
176 never writes the settings block, so migration is the only thing that brings a new
177 field to its default - and one stamped ahead of this version passes that test on
178 every boot, re-defaulting itself and discarding whatever the user chose. */
179 {
180 const uint16_t running =
181 ((uint16_t)PICO9918_BUILD_SW_VERSION << 8) | PICO9918_BUILD_SW_PATCH;
182
183 for (size_t i = 0; i < pico9918_config_field_count; ++i)
184 {
185 if (pico9918_config_fields[i].introducedIn <= running) continue;
186 ++failures;
187 printf(" FAIL byte %u is stamped %04x, ahead of this build's %04x\n",
188 pico9918_config_fields[i].offset, pico9918_config_fields[i].introducedIn, running);
189 }
190 }
191
192 printf("%s: config validation, defaults and migration, %d failure(s)\n",
193 failures ? "FAIL" : "PASS", failures);
194 return failures != 0;
195}
void pico9918_config_defaults(uint8_t config[PICO9918_CONFIG_BYTES])
write a complete, valid settings block: every field at its default
bool pico9918_config_validate(uint8_t config[PICO9918_CONFIG_BYTES], uint8_t hwVersion)
validate a config block just read from host storage, and stamp its identity
void pico9918_config_prepare_save(uint8_t config[PICO9918_CONFIG_BYTES], uint8_t hwVersion)
stamp the identity and the initialised marker into a block about to be persisted
pico9918-core - config byte layout
#define PICO9918_CONFIG_BYTES
size of the config block, in bytes
one config field's descriptor
uint8_t defaultValue
what a reset or a migration writes