Repository navigation
GeoRAM and REU saves hold the computer while they write #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
SensoriumEmbedded
merged 5 commits into
SensoriumEmbedded:main
from
kfox:ram-save-consistent
Oct 30, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
6d3e8e7
firmware: a GeoRAM or REU save holds the computer while it writes, so…
kfox 4848e2a
fix: review findings for 6d3e8e7
kfox b1a3413
tests: the RAM-bank harness's LED macros are statements, so GCC's -We…
kfox 604331d
fix: review findings for b1a3413
kfox 518a2cf
tests: the RAM-bank harness builds and its output parses on Windows
kfox File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| // Saving GeoRAM or REU banks to SD while the computer keeps writing them. | ||
| import test from 'node:test'; | ||
| import assert from 'node:assert/strict'; | ||
| import fs from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { spawnSync } from 'node:child_process'; | ||
|
|
||
| import { commandExists } from '../../../tools/lib/toolchain.mjs'; | ||
|
|
||
| const HARNESS_SOURCE = path.join(import.meta.dirname, 'ram_banks_harness.cpp'); | ||
|
|
||
| const NOT_CI = ['', '0', 'false']; | ||
| const inCi = !NOT_CI.includes(process.env.CI ?? ''); | ||
| const skip = !commandExists('c++') && !inCi && 'c++ is not on PATH'; | ||
|
|
||
| const BANKS_BYTES = 4 * 0x2000; | ||
| const PRELOAD = 'georam.ram 3'; | ||
|
|
||
| let harnessPath; | ||
| function harness() { | ||
| if (harnessPath) return harnessPath; | ||
| const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ram-banks-')); | ||
| harnessPath = path.join(dir, 'harness'); | ||
| const built = spawnSync('c++', ['-std=gnu++17', '-Wall', '-Wextra', '-Werror', '-Wno-deprecated-declarations', '-o', harnessPath, HARNESS_SOURCE], { encoding: 'utf8' }); | ||
| assert.equal(built.status, 0, built.stderr); | ||
| return harnessPath; | ||
| } | ||
|
|
||
| function save(...args) { | ||
| const ran = spawnSync(harness(), args, { encoding: 'utf8' }); | ||
| assert.equal(ran.status, 0, ran.stderr); | ||
| // A MinGW build of the harness writes its lines as \r\n: stdout is in text mode there. | ||
| return ran.stdout.split(/\r?\n/).filter(Boolean); | ||
| } | ||
|
|
||
| test('the REU save is the banks as they were when it began, and the computer runs again', { skip }, () => { | ||
| assert.deepEqual(save('numbered'), ['consistent', PRELOAD, `reu000.reu ${BANKS_BYTES}`]); | ||
| }); | ||
|
|
||
| test('the GeoRAM save is the banks as they were when it began, and the computer runs again', { skip }, () => { | ||
| assert.deepEqual(save('replacing'), ['consistent', 'georam.bak 3', `georam.ram ${BANKS_BYTES}`]); | ||
| }); | ||
|
|
||
| test('a computer that cannot be stopped is not saved, and the last save is kept', { skip }, () => { | ||
| assert.deepEqual(save('replacing', 'holdfails'), [PRELOAD]); | ||
| assert.deepEqual(save('numbered', 'holdfails'), [PRELOAD]); | ||
| }); | ||
|
|
||
| test('a write the card refuses still lets the computer run again', { skip }, () => { | ||
| assert.deepEqual(save('replacing', 'writefails'), [PRELOAD, 'georam.tmp 8192']); | ||
| }); | ||
|
|
||
| test('a computer the host paused is saved and stays paused', { skip }, () => { | ||
| assert.deepEqual(save('replacing', 'paused'), ['consistent', 'held', 'georam.bak 3', `georam.ram ${BANKS_BYTES}`]); | ||
| assert.deepEqual(save('numbered', 'paused'), ['consistent', 'held', PRELOAD, `reu000.reu ${BANKS_BYTES}`]); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| // ram_banks_harness: one save of RAM expansion banks to an in-memory SD card, with a computer | ||
| // that writes every bank between file writes unless its bus is held, as it does through the IO | ||
| // handlers while GeoRAM or the REU is installed. Arguments: | ||
| // numbered|replacing SaveRAMBanks (the REU's save) or SaveRAMBanksReplacing (GeoRAM's) | ||
| // [holdfails] HoldC64 cannot stop the computer | ||
| // [writefails] the card refuses the second bank's write | ||
| // [paused] the host has paused the computer before the save | ||
| // Prints "consistent" when the saved file is the banks as they were when the save began, else | ||
| // "torn"; "held" if the computer is still stopped afterwards; then each file left on the card. | ||
| #include <cstdint> | ||
| #include <cstdio> | ||
| #include <cstdlib> | ||
| #include <cstring> | ||
| #include <map> | ||
| #include <string> | ||
| #include <vector> | ||
|
|
||
| #define FLASHMEM | ||
| #define PSTR(s) s | ||
| #define Fab04_FullDMACapable | ||
| #define MaxPathLength 256 | ||
| #define RAM_ImageSize 0 | ||
| #define FILE_READ 0 | ||
| #define FILE_WRITE 1 | ||
| #define SetLEDOn do {} while (0) | ||
| #define SetLEDOff do {} while (0) | ||
|
|
||
| static struct | ||
| { | ||
| template <typename... Args> void printf(const char *Format, Args... args) { std::fprintf(stderr, Format, args...); } | ||
| void println(const char *Line) { std::fprintf(stderr, "%s\n", Line); } | ||
| } Serial; | ||
|
|
||
| static uint32_t Now; | ||
| static uint32_t millis() { return Now++; } | ||
|
|
||
| enum RegMenuTypes { rmtSD, rmtUSBDrive, rmtTeensy }; | ||
| static uint8_t NumCrtChips; | ||
|
|
||
| static constexpr int NumBanks = 4; | ||
| static uint8_t *Bank[NumBanks]; | ||
| static bool Held, HoldFails, WriteFails; | ||
| static int WritesSeen; | ||
| bool isFrozen; | ||
|
|
||
| static void ComputerRuns() | ||
| { | ||
| if (Held) return; | ||
| for (int Num = 0; Num < NumBanks; Num++) Bank[Num][0]++; | ||
| } | ||
|
|
||
| struct File | ||
| { | ||
| std::vector<uint8_t> *Data = nullptr; | ||
| size_t Pos = 0; | ||
| explicit operator bool() const { return Data != nullptr; } | ||
| size_t write(const uint8_t *Buf, size_t Len) | ||
| { | ||
| ComputerRuns(); | ||
| if (WriteFails && ++WritesSeen == 2) return 0; | ||
| Data->insert(Data->end(), Buf, Buf + Len); | ||
| return Len; | ||
| } | ||
| int read(uint8_t *Buf, size_t Len) | ||
| { | ||
| size_t Got = std::min(Len, Data->size() - Pos); | ||
| std::memcpy(Buf, Data->data() + Pos, Got); | ||
| Pos += Got; | ||
| return (int)Got; | ||
| } | ||
| int available() { return (int)(Data->size() - Pos); } | ||
| uint32_t size() { return (uint32_t)Data->size(); } | ||
| void close() { Data = nullptr; } | ||
| }; | ||
|
|
||
| static struct FS | ||
| { | ||
| std::map<std::string, std::vector<uint8_t>> Files; | ||
| File open(const char *Name, int Mode) | ||
| { | ||
| File Opened; | ||
| if (Mode == FILE_WRITE) Opened.Data = &Files[Name]; | ||
| else if (Files.count(Name)) Opened.Data = &Files[Name]; | ||
| return Opened; | ||
| } | ||
| bool exists(const char *Name) { return Files.count(Name) != 0; } | ||
| bool remove(const char *Name) { return Files.erase(Name) != 0; } | ||
| bool rename(const char *From, const char *To) | ||
| { | ||
| if (!exists(From) || exists(To)) return false; | ||
| Files[To] = Files[From]; | ||
| Files.erase(From); | ||
| return true; | ||
| } | ||
| } Card; | ||
|
|
||
| static RegMenuTypes RegMenuTypeFromFileName(char **ptrptrFileName) | ||
| { | ||
| *ptrptrFileName += strlen("SD:"); | ||
| return rmtSD; | ||
| } | ||
| static bool SDFullInit() { return true; } | ||
| static bool USBFileSystemWait() { return true; } | ||
| static FS *FSfromSourceID(RegMenuTypes) { return &Card; } | ||
| static void FreeDriveDirMenu() {} | ||
| uint8_t RAM_Image[1]; | ||
| uint8_t *ptrRAM_ImageEnd = RAM_Image; | ||
|
|
||
| bool HoldC64() | ||
| { | ||
| if (HoldFails) return false; | ||
| Held = true; | ||
| return true; | ||
| } | ||
|
|
||
| bool CloseDMA() | ||
| { | ||
| Held = false; | ||
| return true; | ||
| } | ||
|
|
||
| // RAMBanks.c uses strlcpy and strlcat, which MinGW and glibc before 2.38 do not have. These | ||
| // stand in for them on every host, so the build does not depend on the C library it finds; the | ||
| // #undefs cover a C library that makes them fortify macros. | ||
| static size_t harness_strlcpy(char *Dest, const char *Src, size_t Size) | ||
| { | ||
| size_t Len = strlen(Src); | ||
| if (Size) { size_t Copy = Len < Size - 1 ? Len : Size - 1; memcpy(Dest, Src, Copy); Dest[Copy] = 0; } | ||
| return Len; | ||
| } | ||
| static size_t harness_strlcat(char *Dest, const char *Src, size_t Size) | ||
| { | ||
| size_t Len = strnlen(Dest, Size); | ||
| return Len == Size ? Size + strlen(Src) : Len + harness_strlcpy(Dest + Len, Src, Size - Len); | ||
| } | ||
| #undef strlcpy | ||
| #undef strlcat | ||
| #define strlcpy harness_strlcpy | ||
| #define strlcat harness_strlcat | ||
|
|
||
| // RAMBanks.c casts pointers to uint32_t, as wide as a pointer on the Teensy and not on the host | ||
| #define uint32_t uintptr_t | ||
| #include "../MinimalBoot/Common/IO_Handlers/RAMBanks.c" | ||
| #undef uint32_t | ||
|
|
||
| int main(int argc, char **argv) | ||
| { | ||
| if (argc < 2) return 2; | ||
| const bool Replacing = !strcmp(argv[1], "replacing"); | ||
| for (int Arg = 2; Arg < argc; Arg++) | ||
| { | ||
| HoldFails |= !strcmp(argv[Arg], "holdfails"); | ||
| WriteFails |= !strcmp(argv[Arg], "writefails"); | ||
| isFrozen |= !strcmp(argv[Arg], "paused"); | ||
| } | ||
|
|
||
| std::vector<uint8_t> AtStart; | ||
| for (int Num = 0; Num < NumBanks; Num++) | ||
| { | ||
| Bank[Num] = (uint8_t *)malloc(RAM_Bank_Size); | ||
| memset(Bank[Num], Num + 1, RAM_Bank_Size); | ||
| AtStart.insert(AtStart.end(), Bank[Num], Bank[Num] + RAM_Bank_Size); | ||
| } | ||
| Card.Files["georam.ram"] = std::vector<uint8_t>(3, 0xee); | ||
| Held = isFrozen; | ||
|
|
||
| if (Replacing) SaveRAMBanksReplacing(Bank, NumBanks, "SD:georam.ram"); | ||
| else SaveRAMBanks(Bank, NumBanks, "SD:reu.reu"); | ||
|
|
||
| const char *Saved = Replacing ? "georam.ram" : "reu000.reu"; | ||
| if (Card.exists(Saved) && Card.Files[Saved].size() == AtStart.size()) | ||
| std::puts(Card.Files[Saved] == AtStart ? "consistent" : "torn"); | ||
| if (Held) std::puts("held"); | ||
| for (auto &Entry : Card.Files) std::printf("%s %zu\n", Entry.first.c_str(), Entry.second.size()); | ||
| return 0; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.