From 593fc45db008a33865d96090342244d6dd390f65 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 4 Jun 2026 07:45:52 +0100 Subject: [PATCH 1/7] refactor: S29.05 extract FS-mount seam from shared FreeRTOS pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lift the FatFs-specific mount / format-on-first-use / unmount and the SolidSyslogFile create/destroy out of BddTargetFreeRtosPipeline into a new BddTargetFatFsMount shim, reached through four function pointers on BddTargetFreeRtosPipelineConfig (MountStore / UnmountStore / CreateStoreFile / DestroyStoreFile). The pipeline no longer includes ff.h or SolidSyslogFatFsFile.h and is now FS-vendor-agnostic. Both targets still use FatFs via the shim — no behaviour change; sets up the PlusFAT swap on the Plus-TCP target. --- Bdd/Targets/Common/BddTargetFatFsMount.c | 72 +++++++++++++++ Bdd/Targets/Common/BddTargetFatFsMount.h | 33 +++++++ .../Common/BddTargetFreeRtosPipeline.c | 90 +++++-------------- .../Common/BddTargetFreeRtosPipeline.h | 34 +++++-- Bdd/Targets/FreeRtos/CMakeLists.txt | 1 + Bdd/Targets/FreeRtos/main.c | 5 ++ Bdd/Targets/FreeRtosLwip/CMakeLists.txt | 1 + Bdd/Targets/FreeRtosLwip/main.c | 5 ++ 8 files changed, 167 insertions(+), 74 deletions(-) create mode 100644 Bdd/Targets/Common/BddTargetFatFsMount.c create mode 100644 Bdd/Targets/Common/BddTargetFatFsMount.h diff --git a/Bdd/Targets/Common/BddTargetFatFsMount.c b/Bdd/Targets/Common/BddTargetFatFsMount.c new file mode 100644 index 00000000..29cb4b4a --- /dev/null +++ b/Bdd/Targets/Common/BddTargetFatFsMount.c @@ -0,0 +1,72 @@ +/* ChaN-FatFs implementation of the shared pipeline's FS-mount seam — see + * BddTargetFatFsMount.h. Extracted from BddTargetFreeRtosPipeline.c in + * SolidSyslog S29.05 when the two FreeRTOS targets first diverged on the + * filesystem (FatFs on lwIP, FreeRTOS-Plus-FAT on Plus-TCP). The logic here is + * the former in-pipeline EnsureFatFsMounted / f_unmount / SolidSyslogFatFsFile_* + * path, lifted verbatim. */ + +#include "BddTargetFatFsMount.h" + +#include "SolidSyslogFatFsFile.h" + +#include "ff.h" /* f_mount / f_mkfs — eager mount-or-format on the `set store file` rebuild trigger. */ + +#include + +/* FATFS object lives in .bss because f_mount stores its address inside the FatFs + * volume registry — the object must outlive every f_open / f_stat / f_unlink. + * One per volume (FF_VOLUMES = 1). */ +static FATFS fatfs; +static bool fatfsMounted = false; + +bool BddTargetFatFsMount_Mount(void) +{ + if (fatfsMounted) + { + return true; + } + FRESULT res = f_mount(&fatfs, "", 1); /* opt=1 → mount immediately, surface FR_NO_FILESYSTEM here */ + if (res == FR_NO_FILESYSTEM) + { + /* Fresh disk image — lay down a FAT and re-mount. FM_FAT keeps the + * formatter on FAT12/16; at the shared 8 MiB geometry auto cluster + * sizing clears the ~4085-cluster boundary, so this lands FAT16 (the + * geometry the FreeRTOS-Plus-FAT formatter needs on the sibling + * target). The work buffer is sized to FF_MAX_SS (512 B), the minimum + * f_mkfs accepts on a FAT12/16 volume. */ + static BYTE workBuffer[FF_MAX_SS]; + const MKFS_PARM opts = {.fmt = FM_FAT | FM_SFD, .n_fat = 1, .align = 1, .n_root = 0, .au_size = 0}; + res = f_mkfs("", &opts, workBuffer, sizeof(workBuffer)); + if (res == FR_OK) + { + res = f_mount(&fatfs, "", 1); + } + } + if (res != FR_OK) + { + (void) printf("[solidsyslog] fatfs mount failed: FRESULT=%d\n", (int) res); + return false; + } + fatfsMounted = true; + return true; +} + +void BddTargetFatFsMount_Unmount(void) +{ + if (fatfsMounted) + { + (void) f_unmount(""); + fatfsMounted = false; + } +} + +struct SolidSyslogFile* BddTargetFatFsMount_CreateFile(void) +{ + return SolidSyslogFatFsFile_Create(); +} + +void BddTargetFatFsMount_DestroyFile(struct SolidSyslogFile* file) +{ + /* FatFsFile_Destroy → Close → f_close flushes the underlying FIL's dir entry. */ + SolidSyslogFatFsFile_Destroy(file); +} diff --git a/Bdd/Targets/Common/BddTargetFatFsMount.h b/Bdd/Targets/Common/BddTargetFatFsMount.h new file mode 100644 index 00000000..173ea936 --- /dev/null +++ b/Bdd/Targets/Common/BddTargetFatFsMount.h @@ -0,0 +1,33 @@ +#ifndef BDDTARGETFATFSMOUNT_H +#define BDDTARGETFATFSMOUNT_H + +#include "ExternC.h" + +#include + +EXTERN_C_BEGIN + + /* ChaN-FatFs implementation of the shared pipeline's FS-mount seam + * (struct BddTargetFreeRtosPipelineConfig). Owns the single-volume FATFS + * registry object + mounted flag and wraps the ff_* lifecycle so the + * pipeline stays FS-vendor-agnostic. Wired by the lwIP target's main.c; + * the FreeRTOS-Plus-TCP target uses the FreeRTOS-Plus-FAT sibling + * (BddTargetPlusFatMount). */ + + /* Mount volume 0, formatting on first use if the disk image has no FAT + * yet. Idempotent — repeated calls short-circuit on the mounted flag. + * Returns false on an unrecoverable mount/format failure so the caller + * can leave the target on its original store. */ + bool BddTargetFatFsMount_Mount(void); + + /* Unmount volume 0 if mounted; no-op otherwise. */ + void BddTargetFatFsMount_Unmount(void); + + /* Create / destroy the ChaN-FatFs SolidSyslogFile adapter (forward-declared + * to keep ff.h / SolidSyslogFatFsFile.h out of the seam's public surface). */ + struct SolidSyslogFile* BddTargetFatFsMount_CreateFile(void); + void BddTargetFatFsMount_DestroyFile(struct SolidSyslogFile* file); + +EXTERN_C_END + +#endif /* BDDTARGETFATFSMOUNT_H */ diff --git a/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c b/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c index d02a59f8..27bbe7d9 100644 --- a/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c +++ b/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c @@ -25,7 +25,6 @@ #include "SolidSyslogCrc16Policy.h" #include "SolidSyslogEndpoint.h" #include "SolidSyslogError.h" -#include "SolidSyslogFatFsFile.h" #include "SolidSyslogFileBlockDevice.h" #include "SolidSyslogFormatter.h" #include "SolidSyslogFreeRtosMutex.h" @@ -43,8 +42,6 @@ #include "SolidSyslogTimeQualitySd.h" #include "SolidSyslogTunables.h" -#include "ff.h" /* f_mount / f_mkfs — eager mount-or-format on the `set store file` rebuild trigger. */ - #include #include @@ -104,12 +101,6 @@ static volatile bool solidSyslogTeardown = false; * … which fit 8.3 short-filename mode (LFN=0 in our shared ffconf.h). */ static const char STORE_PATH_PREFIX[] = "STORE"; -/* FATFS object lives in .bss because f_mount stores its address inside the FatFs - * volume registry — the object must outlive every f_open / f_stat / f_unlink. - * One per volume (FF_VOLUMES = 1). */ -static FATFS fatfs; -static bool fatfsMounted = false; - static struct SolidSyslogFile* storeFile = NULL; static struct SolidSyslogBlockDevice* storeBlockDevice = NULL; static struct SolidSyslogStore* currentStore = NULL; @@ -175,7 +166,6 @@ static bool OnSet(const char* name, const char* value); static struct SolidSyslogSecurityPolicy* CreateSecurityPolicy(void); static void DestroySecurityPolicy(void); static bool RebuildWithFileStore(void); -static bool EnsureFatFsMounted(void); static void DestroyCurrentStore(void); static enum SolidSyslogDiscardPolicy MapDiscardPolicy(const char* policy); static void OnStoreFull(void* context); @@ -541,18 +531,20 @@ static struct SolidSyslogSecurityPolicy* CreateSecurityPolicy(void) return policy; } -/* `set store file` trigger: swap the default NullStore for a FatFs-backed - * BlockStore. One-way for the lifetime of this QEMU instance. The lifecycle - * mutex blocks the Service task across the Destroy → re-Create transition. */ +/* `set store file` trigger: swap the default NullStore for a file-backed + * BlockStore over the platform FS-mount seam. One-way for the lifetime of this + * QEMU instance. The lifecycle mutex blocks the Service task across the + * Destroy → re-Create transition. */ static bool RebuildWithFileStore(void) { SolidSyslogMutex_Lock(lifecycleMutex); - /* FatFs does NOT auto-mount on first f_open — mount (and format-on-first-use) - * before tearing down the existing store so a mount failure leaves the - * target running on the original NullStore (zero-disruption); return false - * so OnSet reports the failure to the harness. */ - if (!EnsureFatFsMounted()) + /* The FS layer does NOT auto-mount on first open — mount (and + * format-on-first-use) via the platform FS-mount seam before tearing down + * the existing store so a mount failure leaves the target running on the + * original NullStore (zero-disruption); return false so OnSet reports the + * failure to the harness. */ + if (!g_config->MountStore()) { SolidSyslogMutex_Unlock(lifecycleMutex); return false; @@ -563,7 +555,7 @@ static bool RebuildWithFileStore(void) solidSyslog = NULL; DestroyCurrentStore(); - storeFile = SolidSyslogFatFsFile_Create(); + storeFile = g_config->CreateStoreFile(); storeBlockDevice = SolidSyslogFileBlockDevice_Create(storeFile, STORE_PATH_PREFIX); struct SolidSyslogSecurityPolicy* policy = CreateSecurityPolicy(); @@ -593,40 +585,6 @@ static bool RebuildWithFileStore(void) return true; } -/* Mount volume 0; format-on-first-use if the disk image has no FAT yet. - * Idempotent — subsequent calls short-circuit on fatfsMounted. The work buffer - * for f_mkfs is sized to FF_MAX_SS (512 B), the minimum f_mkfs accepts on a - * FAT12/16 volume. */ -static bool EnsureFatFsMounted(void) -{ - if (fatfsMounted) - { - return true; - } - FRESULT res = f_mount(&fatfs, "", 1); /* opt=1 → mount immediately, surface FR_NO_FILESYSTEM here */ - if (res == FR_NO_FILESYSTEM) - { - /* Fresh disk image — lay down a FAT and re-mount. FM_FAT keeps the - * formatter on FAT12/16; at the shared 8 MiB geometry auto cluster - * sizing clears the ~4085-cluster boundary, so this lands FAT16 (the - * geometry the later FreeRTOS-Plus-FAT formatter needs). */ - static BYTE workBuffer[FF_MAX_SS]; - const MKFS_PARM opts = {.fmt = FM_FAT | FM_SFD, .n_fat = 1, .align = 1, .n_root = 0, .au_size = 0}; - res = f_mkfs("", &opts, workBuffer, sizeof(workBuffer)); - if (res == FR_OK) - { - res = f_mount(&fatfs, "", 1); - } - } - if (res != FR_OK) - { - (void) printf("[solidsyslog] fatfs mount failed: FRESULT=%d\n", (int) res); - return false; - } - fatfsMounted = true; - return true; -} - static void DestroySecurityPolicy(void) { if (strcmp(installedSecurityPolicy, "hmac-sha256") == 0) @@ -646,7 +604,8 @@ static void DestroySecurityPolicy(void) } /* Tears down whichever store is currently installed (file-backed or null). - * FatFsFile_Destroy → Close → f_close flushes the underlying FIL's dir entry. */ + * The platform DestroyStoreFile hook's Close flushes any buffered dir entry / + * file data through to the media. */ static void DestroyCurrentStore(void) { if (currentStoreIsFile) @@ -654,7 +613,7 @@ static void DestroyCurrentStore(void) SolidSyslogBlockStore_Destroy(currentStore); SolidSyslogFileBlockDevice_Destroy(storeBlockDevice); DestroySecurityPolicy(); - SolidSyslogFatFsFile_Destroy(storeFile); + g_config->DestroyStoreFile(storeFile); } /* else: NullStore is shared and immutable — nothing to destroy. */ } @@ -702,11 +661,12 @@ static void OnThresholdCrossed(void* context) /* Full teardown of every shared resource. Two entry points — `quit` (falls * through after BddTargetInteractive_Run returns) and `set shutdown 1` — both - * route through here. f_unmount fires regardless so the next session's f_mount - * finds STORE*.log directory entries up-to-date (power_cycle_replay relies on - * this). The lifecycle mutex held across the SolidSyslog + store destroy keeps - * Service from racing the teardown. The platform's network teardown runs last, - * after the shared resources are released. */ + * route through here. The platform UnmountStore hook fires regardless so the + * next session's mount finds STORE*.log directory entries up-to-date + * (power_cycle_replay relies on this). The lifecycle mutex held across the + * SolidSyslog + store destroy keeps Service from racing the teardown. The + * platform's network teardown runs last, after the shared resources are + * released. */ static void TeardownAll(void) { SolidSyslogMutex_Lock(lifecycleMutex); @@ -720,11 +680,7 @@ static void TeardownAll(void) SolidSyslogMetaSd_Destroy(metaSd); SolidSyslogStdAtomicCounter_Destroy(atomicCounter); DestroyCurrentStore(); - if (fatfsMounted) - { - (void) f_unmount(""); - fatfsMounted = false; - } + g_config->UnmountStore(); SolidSyslogMutex_Unlock(lifecycleMutex); /* Wait for Service to observe the teardown flag and vTaskDelete itself @@ -791,8 +747,8 @@ void BddTargetFreeRtosPipeline_InteractiveTask(void* argument) * very first iteration without a NULL check. */ lifecycleMutex = SolidSyslogFreeRtosMutex_Create(); - /* Default store is NullStore — flipped to FatFs/BlockStore by `set store - * file` via RebuildWithFileStore(). */ + /* Default store is NullStore — flipped to the file-backed BlockStore by + * `set store file` via RebuildWithFileStore(). */ currentStore = SolidSyslogNullStore_Get(); currentStoreIsFile = false; diff --git a/Bdd/Targets/Common/BddTargetFreeRtosPipeline.h b/Bdd/Targets/Common/BddTargetFreeRtosPipeline.h index f4c251a5..ef3f08f3 100644 --- a/Bdd/Targets/Common/BddTargetFreeRtosPipeline.h +++ b/Bdd/Targets/Common/BddTargetFreeRtosPipeline.h @@ -5,19 +5,25 @@ #include "SolidSyslogSender.h" #include "SolidSyslogStringFunction.h" +#include #include /* Shared FreeRTOS BDD-target pipeline. * * Everything platform-independent lives in this component: the SolidSyslog - * lifecycle, the FatFs-backed store + security-policy machinery (crc16 / + * lifecycle, the file-backed store + security-policy machinery (crc16 / * hmac-sha256 / aes-256-gcm / null), the SD set, the interactive `set` handler, - * the CircularBuffer + Service drain task, the console glue, and the - * mount/format-on-first-use semihosting FatFs path. Both FreeRTOS BDD targets - * (Bdd/Targets/FreeRtos = FreeRTOS-Plus-TCP, Bdd/Targets/FreeRtosLwip = lwIP) - * drive this; their main.c files keep only the network backend behind the seam - * below — the adapter wiring (PlusTcp vs LwipRaw) and the IP-stack bring-up that - * genuinely differ. See SolidSyslog S29.03. */ + * the CircularBuffer + Service drain task, and the console glue. Both FreeRTOS + * BDD targets (Bdd/Targets/FreeRtos = FreeRTOS-Plus-TCP, Bdd/Targets/FreeRtosLwip + * = lwIP) drive this; their main.c files keep only the network backend and the + * FS-mount seam behind the config below — the network adapter wiring (PlusTcp vs + * LwipRaw), the IP-stack bring-up, and the filesystem vendor (FreeRTOS-Plus-FAT + * vs ChaN-FatFs) that genuinely differ. See SolidSyslog S29.03 (network seam) + * and S29.05 (FS-mount seam). */ + +/* Forward declaration — the FS-mount seam traffics in SolidSyslogFile handles + * without the pipeline header pulling SolidSyslogFile.h. */ +struct SolidSyslogFile; /* The platform seam each target injects via BddTargetFreeRtosPipeline_SetConfig. */ struct BddTargetFreeRtosPipelineConfig @@ -35,6 +41,20 @@ struct BddTargetFreeRtosPipelineConfig /* Tear down the sender + platform adapters. Runs on the interactive task * after the shared pipeline teardown (SolidSyslog / SD / store / buffer). */ void (*TeardownNetwork)(void); + + /* FS-mount seam — the FS-vendor-specific half of the file-backed store. + * The Plus-TCP target wires the FreeRTOS-Plus-FAT shim (BddTargetPlusFatMount); + * the lwIP target wires the ChaN-FatFs shim (BddTargetFatFsMount). The + * pipeline drives the store machinery (BlockStore / FileBlockDevice / + * security policy) FS-vendor-agnostically through these four hooks. */ + /* Mount + format-on-first-use; idempotent; false on unrecoverable failure + * (the rebuild path then leaves the target on its current store). */ + bool (*MountStore)(void); + /* Unmount on teardown; safe to call when not mounted. */ + void (*UnmountStore)(void); + /* Create / destroy the platform SolidSyslogFile adapter for the store. */ + struct SolidSyslogFile* (*CreateStoreFile)(void); + void (*DestroyStoreFile)(struct SolidSyslogFile* file); }; /* Install the platform seam. Call once from main() before the tasks run. */ diff --git a/Bdd/Targets/FreeRtos/CMakeLists.txt b/Bdd/Targets/FreeRtos/CMakeLists.txt index e758cf17..f2a572ea 100644 --- a/Bdd/Targets/FreeRtos/CMakeLists.txt +++ b/Bdd/Targets/FreeRtos/CMakeLists.txt @@ -279,6 +279,7 @@ add_executable(SolidSyslogBddTarget ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetErrorText.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFatFsMount.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetInteractive.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c diff --git a/Bdd/Targets/FreeRtos/main.c b/Bdd/Targets/FreeRtos/main.c index 487b7a65..04e472c2 100644 --- a/Bdd/Targets/FreeRtos/main.c +++ b/Bdd/Targets/FreeRtos/main.c @@ -12,6 +12,7 @@ * Static IPv4 (10.0.2.15) on the QEMU slirp network with the host reachable at * the slirp gateway 10.0.2.2. */ +#include "BddTargetFatFsMount.h" #include "BddTargetFreeRtosPipeline.h" #include "BddTargetMtlsConfig.h" #include "BddTargetSwitchConfig.h" @@ -100,6 +101,10 @@ static const struct BddTargetFreeRtosPipelineConfig PIPELINE_CONFIG = { .BuildSender = BuildSender, .GetHostname = GetHostname, .TeardownNetwork = TeardownNetwork, + .MountStore = BddTargetFatFsMount_Mount, + .UnmountStore = BddTargetFatFsMount_Unmount, + .CreateStoreFile = BddTargetFatFsMount_CreateFile, + .DestroyStoreFile = BddTargetFatFsMount_DestroyFile, }; int main(void) diff --git a/Bdd/Targets/FreeRtosLwip/CMakeLists.txt b/Bdd/Targets/FreeRtosLwip/CMakeLists.txt index eb053c69..9186475c 100644 --- a/Bdd/Targets/FreeRtosLwip/CMakeLists.txt +++ b/Bdd/Targets/FreeRtosLwip/CMakeLists.txt @@ -250,6 +250,7 @@ add_executable(SolidSyslogBddTargetLwip ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/diskio.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetErrorText.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFatFsMount.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetInteractive.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c diff --git a/Bdd/Targets/FreeRtosLwip/main.c b/Bdd/Targets/FreeRtosLwip/main.c index 6e26f035..1ecca1a2 100644 --- a/Bdd/Targets/FreeRtosLwip/main.c +++ b/Bdd/Targets/FreeRtosLwip/main.c @@ -17,6 +17,7 @@ * that name statically to 10.0.2.2 (slirp can't return a reachable address for * the docker alias over real DNS). */ +#include "BddTargetFatFsMount.h" #include "BddTargetFreeRtosPipeline.h" #include "EthernetIf.h" @@ -78,6 +79,10 @@ static const struct BddTargetFreeRtosPipelineConfig PIPELINE_CONFIG = { .BuildSender = BuildSender, .GetHostname = GetHostname, .TeardownNetwork = TeardownNetwork, + .MountStore = BddTargetFatFsMount_Mount, + .UnmountStore = BddTargetFatFsMount_Unmount, + .CreateStoreFile = BddTargetFatFsMount_CreateFile, + .DestroyStoreFile = BddTargetFatFsMount_DestroyFile, }; /* lwIP randomness source (declared by arch/cc.h's LWIP_RAND). sys_now() comes From 723457088e8a2ef40759844f1dd34db901d2cbf6 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 4 Jun 2026 08:25:00 +0100 Subject: [PATCH 2/7] refactor: S29.05 extract shared semihosting block I/O from diskio Lift the BKPT 0xAB trap, the open/read/write/seek/length/close primitives, the 8 MiB FAT16 geometry, and the open-or-create-sparse image logic out of diskio.c into a new Bdd/Targets/Common/SemihostingDisk TU. diskio.c now maps the ChaN-FatFs disk_* contract onto SemihostingDisk_{EnsureReady,IsReady, Read,Write}, preserving its RES_NOTRDY / RES_PARERR / RES_ERROR distinctions. This lets the upcoming FreeRTOS-Plus-FAT FF_Disk_t driver share byte-for-byte the same host image and trap. No behaviour change. Verified both FreeRTOS cross targets still build. --- Bdd/Targets/Common/SemihostingDisk.c | 222 ++++++++++++++++++++ Bdd/Targets/Common/SemihostingDisk.h | 58 +++++ Bdd/Targets/Common/diskio.c | 268 +++++------------------- Bdd/Targets/FreeRtos/CMakeLists.txt | 1 + Bdd/Targets/FreeRtosLwip/CMakeLists.txt | 1 + 5 files changed, 330 insertions(+), 220 deletions(-) create mode 100644 Bdd/Targets/Common/SemihostingDisk.c create mode 100644 Bdd/Targets/Common/SemihostingDisk.h diff --git a/Bdd/Targets/Common/SemihostingDisk.c b/Bdd/Targets/Common/SemihostingDisk.c new file mode 100644 index 00000000..3f7f31e5 --- /dev/null +++ b/Bdd/Targets/Common/SemihostingDisk.c @@ -0,0 +1,222 @@ +/* Host-backed flat-disk media access over ARM semihosting — see + * SemihostingDisk.h. The BKPT 0xAB trap and the open / read / write / seek / + * length / close primitives were extracted from diskio.c in SolidSyslog S29.05 + * so the FreeRTOS-Plus-FAT FF_Disk_t driver (FFSemihostingDisk.c) shares the + * exact same host image, geometry, and trap as the ChaN-FatFs disk_* glue. */ + +#include "SemihostingDisk.h" + +#include +#include + +enum +{ + SEMIHOSTING_SYS_OPEN = 0x01, + SEMIHOSTING_SYS_CLOSE = 0x02, + SEMIHOSTING_SYS_WRITE = 0x05, + SEMIHOSTING_SYS_READ = 0x06, + SEMIHOSTING_SYS_SEEK = 0x0A, + SEMIHOSTING_SYS_FLEN = 0x0C, +}; + +/* Semihosting file open modes per the ARM Semihosting spec table 5-3. + * "r+b" opens an existing file for read/write without truncation; + * "w+b" creates or truncates and opens for read/write. */ +enum +{ + SEMIHOSTING_MODE_READ_PLUS_BINARY = 3, + SEMIHOSTING_MODE_WRITE_PLUS_BINARY = 7, +}; + +enum +{ + DISK_TOTAL_BYTES = SEMIHOSTING_DISK_SECTOR_COUNT * SEMIHOSTING_DISK_SECTOR_SIZE, +}; + +static const char DISK_IMAGE_PATH[] = "solidsyslog-disk.img"; + +static int g_diskHandle = -1; + +static int Semihosting(int op, const void* args); +static int SemihostingOpen(const char* path, int mode); +static int SemihostingRead(int handle, void* buffer, int count); +static int SemihostingWrite(int handle, const void* buffer, int count); +static int SemihostingSeek(int handle, int position); +static int SemihostingFlen(int handle); +static int SemihostingClose(int handle); + +bool SemihostingDisk_EnsureReady(void) +{ + if (g_diskHandle >= 0) + { + return true; + } + g_diskHandle = SemihostingOpen(DISK_IMAGE_PATH, SEMIHOSTING_MODE_READ_PLUS_BINARY); + if (g_diskHandle >= 0) + { + int length = SemihostingFlen(g_diskHandle); + if (length >= (int) DISK_TOTAL_BYTES) + { + return true; + } + (void) SemihostingClose(g_diskHandle); + g_diskHandle = -1; + } + /* Create or truncate a fresh image. Sparse-extend by seeking to the + * last byte and writing one zero — POSIX hosts under semihosting + * back-fill the hole with zeros on read, which is what the filesystem + * needs to see no FAT and fall through to format-on-first-use. */ + g_diskHandle = SemihostingOpen(DISK_IMAGE_PATH, SEMIHOSTING_MODE_WRITE_PLUS_BINARY); + if (g_diskHandle < 0) + { + return false; + } + static const uint8_t ZERO_BYTE = 0U; + if (SemihostingSeek(g_diskHandle, (int) DISK_TOTAL_BYTES - 1) != 0) + { + (void) SemihostingClose(g_diskHandle); + g_diskHandle = -1; + return false; + } + if (SemihostingWrite(g_diskHandle, &ZERO_BYTE, 1) != 0) + { + (void) SemihostingClose(g_diskHandle); + g_diskHandle = -1; + return false; + } + return true; +} + +bool SemihostingDisk_IsReady(void) +{ + return g_diskHandle >= 0; +} + +enum SemihostingDiskResult SemihostingDisk_Read(void* buffer, uint32_t sector, uint32_t count) +{ + enum SemihostingDiskResult result = SEMIHOSTING_DISK_OK; + if ((sector + count) > (uint32_t) SEMIHOSTING_DISK_SECTOR_COUNT) + { + result = SEMIHOSTING_DISK_OUT_OF_RANGE; + } + else + { + int position = (int) sector * (int) SEMIHOSTING_DISK_SECTOR_SIZE; + int bytes = (int) count * (int) SEMIHOSTING_DISK_SECTOR_SIZE; + if (SemihostingSeek(g_diskHandle, position) != 0) + { + result = SEMIHOSTING_DISK_IO_ERROR; + } + else if (SemihostingRead(g_diskHandle, buffer, bytes) != 0) + { + result = SEMIHOSTING_DISK_IO_ERROR; + } + } + return result; +} + +enum SemihostingDiskResult SemihostingDisk_Write(const void* buffer, uint32_t sector, uint32_t count) +{ + enum SemihostingDiskResult result = SEMIHOSTING_DISK_OK; + if ((sector + count) > (uint32_t) SEMIHOSTING_DISK_SECTOR_COUNT) + { + result = SEMIHOSTING_DISK_OUT_OF_RANGE; + } + else + { + int position = (int) sector * (int) SEMIHOSTING_DISK_SECTOR_SIZE; + int bytes = (int) count * (int) SEMIHOSTING_DISK_SECTOR_SIZE; + if (SemihostingSeek(g_diskHandle, position) != 0) + { + result = SEMIHOSTING_DISK_IO_ERROR; + } + else if (SemihostingWrite(g_diskHandle, buffer, bytes) != 0) + { + result = SEMIHOSTING_DISK_IO_ERROR; + } + } + return result; +} + +static int Semihosting(int op, const void* args) +{ + /* BKPT 0xAB is the Cortex-M Thumb semihosting trap. r0 is the + * operation number on entry and the return value on exit; r1 + * is a pointer to a per-op parameter block. memory clobber so + * the compiler doesn't reorder around buffers passed by pointer. */ + register int result __asm("r0") = op; + register const void* request __asm("r1") = args; + __asm volatile("bkpt 0xAB" : "+r"(result) : "r"(request) : "memory"); + return result; +} + +static int SemihostingOpen(const char* path, int mode) +{ + const struct + { + const char* name; + int mode; + int length; + } args = {path, mode, (int) strlen(path)}; + + return Semihosting(SEMIHOSTING_SYS_OPEN, &args); +} + +static int SemihostingRead(int handle, void* buffer, int count) +{ + /* SYS_READ returns the number of bytes NOT read (0 == full read). */ + const struct + { + int handle; + void* buffer; + int count; + } args = {handle, buffer, count}; + + return Semihosting(SEMIHOSTING_SYS_READ, &args); +} + +static int SemihostingWrite(int handle, const void* buffer, int count) +{ + /* SYS_WRITE returns the number of bytes NOT written (0 == full write). */ + const struct + { + int handle; + const void* buffer; + int count; + } args = {handle, buffer, count}; + + return Semihosting(SEMIHOSTING_SYS_WRITE, &args); +} + +static int SemihostingSeek(int handle, int position) +{ + /* SYS_SEEK returns 0 on success, a negative value on error. */ + const struct + { + int handle; + int position; + } args = {handle, position}; + + return Semihosting(SEMIHOSTING_SYS_SEEK, &args); +} + +static int SemihostingFlen(int handle) +{ + /* SYS_FLEN returns the file length in bytes, -1 on error. */ + const struct + { + int handle; + } args = {handle}; + + return Semihosting(SEMIHOSTING_SYS_FLEN, &args); +} + +static int SemihostingClose(int handle) +{ + const struct + { + int handle; + } args = {handle}; + + return Semihosting(SEMIHOSTING_SYS_CLOSE, &args); +} diff --git a/Bdd/Targets/Common/SemihostingDisk.h b/Bdd/Targets/Common/SemihostingDisk.h new file mode 100644 index 00000000..bc1d8bfc --- /dev/null +++ b/Bdd/Targets/Common/SemihostingDisk.h @@ -0,0 +1,58 @@ +#ifndef SEMIHOSTINGDISK_H +#define SEMIHOSTINGDISK_H + +#include "ExternC.h" + +#include +#include + +EXTERN_C_BEGIN + + /* Host-backed flat-disk media access over ARM semihosting (BKPT 0xAB), + * shared by both filesystem media drivers on the QEMU mps2-an385 BDD + * targets: the ChaN-FatFs disk_* glue (diskio.c) and the FreeRTOS-Plus-FAT + * FF_Disk_t driver (FFSemihostingDisk.c). One image, one geometry, one + * semihosting trap — so a FatFs run and a Plus-FAT run exercise byte-for- + * byte the same host file (solidsyslog-disk.img in QEMU's working dir). + * + * Behave's after_scenario removes the image so each scenario starts with + * no filesystem; EnsureReady creates a fresh 8 MiB sparse file when it sees + * the open fail or the file is too small, the filesystem's mount then sees + * a zero-filled image and the integrator falls through to format-on-first- + * use. + * + * Single logical drive; 16384 sectors x 512 B = 8 MiB — large enough for + * the store-and-forward / capacity / power-cycle scenarios that exercise + * multi-block files, and clear of the ~4085-cluster FAT12/16 boundary so + * both formatters land FAT16. */ + + enum + { + SEMIHOSTING_DISK_SECTOR_SIZE = 512, + SEMIHOSTING_DISK_SECTOR_COUNT = 16384, + }; + + enum SemihostingDiskResult + { + SEMIHOSTING_DISK_OK = 0, + SEMIHOSTING_DISK_OUT_OF_RANGE, + SEMIHOSTING_DISK_IO_ERROR, + }; + + /* Open the host image, creating + sparse-extending a fresh zero-filled 8 MiB + * file on first use. Idempotent — repeated calls short-circuit on the cached + * handle. Returns true once a usable handle is held. */ + bool SemihostingDisk_EnsureReady(void); + + /* True once EnsureReady has acquired a handle. */ + bool SemihostingDisk_IsReady(void); + + /* Read / write `count` consecutive 512 B sectors starting at LBA `sector`. + * Both validate the range against the disk geometry before touching the + * image. The caller is responsible for checking readiness first. */ + enum SemihostingDiskResult SemihostingDisk_Read(void* buffer, uint32_t sector, uint32_t count); + enum SemihostingDiskResult SemihostingDisk_Write(const void* buffer, uint32_t sector, uint32_t count); + +EXTERN_C_END + +#endif /* SEMIHOSTINGDISK_H */ diff --git a/Bdd/Targets/Common/diskio.c b/Bdd/Targets/Common/diskio.c index 634e5e65..a560268e 100644 --- a/Bdd/Targets/Common/diskio.c +++ b/Bdd/Targets/Common/diskio.c @@ -1,72 +1,21 @@ -/* Semihosting disk_* glue between FatFs and a host-backed flat disk - * image. Runs on QEMU mps2-an385 with -semihosting-config enable=on so - * BKPT 0xAB invocations are trapped by QEMU and forwarded to host - * file I/O against solidsyslog-disk.img in QEMU's working directory. +/* ChaN-FatFs disk_* glue over the shared semihosting media (SemihostingDisk). * - * Behave's after_scenario removes the image so each scenario starts - * with no filesystem; disk_initialize creates a fresh 8 MiB sparse - * file when it sees the open fail or the file is too small, - * f_mount returns FR_NO_FILESYSTEM on the zero-filled image, and the - * integrator falls through to f_mkfs to lay down a FAT. + * Maps FatFs's caller-facing disk_* contract onto the host-backed flat disk + * image that both FreeRTOS BDD targets share. The semihosting trap, the 8 MiB + * FAT16 geometry, and the open-or-create-sparse logic live in + * Bdd/Targets/Common/SemihostingDisk so the FreeRTOS-Plus-FAT FF_Disk_t driver + * (FFSemihostingDisk.c) exercises byte-for-byte the same image (S29.05). * - * Shared by the FreeRTOS-Plus-TCP and lwIP BDD targets (Bdd/Targets/Common) - * so both exercise the same semihosting media geometry. - * - * Single logical drive (pdrv 0); no exFAT, no LFN. 16384 sectors x - * 512 B = 8 MiB — large enough for the store-and-forward / capacity / - * power-cycle scenarios that exercise multi-block files, and clear of - * the ~4085-cluster FAT12/16 boundary so f_mkfs lays down FAT16 (the - * geometry the later FreeRTOS-Plus-FAT formatter needs). */ + * Used by the lwIP BDD target (FatFs-backed store); the FreeRTOS-Plus-TCP + * target uses Plus-FAT instead. Single logical drive (pdrv 0); no exFAT, no + * LFN. */ /* ff.h before diskio.h: diskio.h declares disk_* in terms of BYTE / UINT / * LBA_t / DWORD / WORD which are typedef'd in ff.h's integer headers. */ #include "ff.h" #include "diskio.h" -#include -#include -#include -#include - -enum -{ - SEMIHOSTING_SYS_OPEN = 0x01, - SEMIHOSTING_SYS_CLOSE = 0x02, - SEMIHOSTING_SYS_WRITE = 0x05, - SEMIHOSTING_SYS_READ = 0x06, - SEMIHOSTING_SYS_SEEK = 0x0A, - SEMIHOSTING_SYS_FLEN = 0x0C, -}; - -/* Semihosting file open modes per the ARM Semihosting spec table 5-3. - * "r+b" opens an existing file for read/write without truncation; - * "w+b" creates or truncates and opens for read/write. */ -enum -{ - SEMIHOSTING_MODE_READ_PLUS_BINARY = 3, - SEMIHOSTING_MODE_WRITE_PLUS_BINARY = 7, -}; - -enum -{ - DISK_SECTOR_SIZE = 512, - DISK_SECTOR_COUNT = 16384, - DISK_TOTAL_BYTES = DISK_SECTOR_COUNT * DISK_SECTOR_SIZE, -}; - -static const char DISK_IMAGE_PATH[] = "solidsyslog-disk.img"; - -static int g_diskHandle = -1; - -static int Semihosting(int op, const void* args); -static int SemihostingOpen(const char* path, int mode); -static int SemihostingRead(int handle, void* buffer, int count); -static int SemihostingWrite(int handle, const void* buffer, int count); -static int SemihostingSeek(int handle, int position); -static int SemihostingFlen(int handle); -static int SemihostingClose(int handle); - -static bool DiskImageIsReady(void); +#include "SemihostingDisk.h" DSTATUS disk_initialize(BYTE pdrv) { @@ -74,119 +23,7 @@ DSTATUS disk_initialize(BYTE pdrv) { return STA_NOINIT; } - return DiskImageIsReady() ? 0 : STA_NOINIT; -} - -static bool DiskImageIsReady(void) -{ - if (g_diskHandle >= 0) - { - return true; - } - g_diskHandle = SemihostingOpen(DISK_IMAGE_PATH, SEMIHOSTING_MODE_READ_PLUS_BINARY); - if (g_diskHandle >= 0) - { - int length = SemihostingFlen(g_diskHandle); - if (length >= (int) DISK_TOTAL_BYTES) - { - return true; - } - (void) SemihostingClose(g_diskHandle); - g_diskHandle = -1; - } - /* Create or truncate a fresh image. Sparse-extend by seeking to the - * last byte and writing one zero — POSIX hosts under semihosting - * back-fill the hole with zeros on read, which is what FatFs needs - * to see FR_NO_FILESYSTEM and fall through to f_mkfs. */ - g_diskHandle = SemihostingOpen(DISK_IMAGE_PATH, SEMIHOSTING_MODE_WRITE_PLUS_BINARY); - if (g_diskHandle < 0) - { - return false; - } - static const uint8_t ZERO_BYTE = 0U; - if (SemihostingSeek(g_diskHandle, (int) DISK_TOTAL_BYTES - 1) != 0) - { - (void) SemihostingClose(g_diskHandle); - g_diskHandle = -1; - return false; - } - if (SemihostingWrite(g_diskHandle, &ZERO_BYTE, 1) != 0) - { - (void) SemihostingClose(g_diskHandle); - g_diskHandle = -1; - return false; - } - return true; -} - -static int SemihostingOpen(const char* path, int mode) -{ - const struct - { - const char* name; - int mode; - int length; - } args = {path, mode, (int) strlen(path)}; - - return Semihosting(SEMIHOSTING_SYS_OPEN, &args); -} - -static int Semihosting(int op, const void* args) -{ - /* BKPT 0xAB is the Cortex-M Thumb semihosting trap. r0 is the - * operation number on entry and the return value on exit; r1 - * is a pointer to a per-op parameter block. memory clobber so - * the compiler doesn't reorder around buffers passed by pointer. */ - register int result __asm("r0") = op; - register const void* request __asm("r1") = args; - __asm volatile("bkpt 0xAB" : "+r"(result) : "r"(request) : "memory"); - return result; -} - -static int SemihostingFlen(int handle) -{ - /* SYS_FLEN returns the file length in bytes, -1 on error. */ - const struct - { - int handle; - } args = {handle}; - - return Semihosting(SEMIHOSTING_SYS_FLEN, &args); -} - -static int SemihostingClose(int handle) -{ - const struct - { - int handle; - } args = {handle}; - - return Semihosting(SEMIHOSTING_SYS_CLOSE, &args); -} - -static int SemihostingSeek(int handle, int position) -{ - /* SYS_SEEK returns 0 on success, a negative value on error. */ - const struct - { - int handle; - int position; - } args = {handle, position}; - - return Semihosting(SEMIHOSTING_SYS_SEEK, &args); -} - -static int SemihostingWrite(int handle, const void* buffer, int count) -{ - /* SYS_WRITE returns the number of bytes NOT written (0 == full write). */ - const struct - { - int handle; - const void* buffer; - int count; - } args = {handle, buffer, count}; - - return Semihosting(SEMIHOSTING_SYS_WRITE, &args); + return SemihostingDisk_EnsureReady() ? 0 : STA_NOINIT; } DSTATUS disk_status(BYTE pdrv) @@ -195,66 +32,57 @@ DSTATUS disk_status(BYTE pdrv) { return STA_NOINIT; } - return (g_diskHandle >= 0) ? 0 : STA_NOINIT; + return SemihostingDisk_IsReady() ? 0 : STA_NOINIT; } DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) { - if ((pdrv != 0) || (g_diskHandle < 0)) - { - return RES_NOTRDY; - } - if ((sector + count) > (LBA_t) DISK_SECTOR_COUNT) + DRESULT result; + if ((pdrv != 0) || !SemihostingDisk_IsReady()) { - return RES_PARERR; - } - int position = (int) sector * (int) DISK_SECTOR_SIZE; - if (SemihostingSeek(g_diskHandle, position) != 0) - { - return RES_ERROR; + result = RES_NOTRDY; } - int bytes = (int) count * (int) DISK_SECTOR_SIZE; - if (SemihostingRead(g_diskHandle, buff, bytes) != 0) + else { - return RES_ERROR; + switch (SemihostingDisk_Read(buff, (uint32_t) sector, (uint32_t) count)) + { + case SEMIHOSTING_DISK_OK: + result = RES_OK; + break; + case SEMIHOSTING_DISK_OUT_OF_RANGE: + result = RES_PARERR; + break; + default: + result = RES_ERROR; + break; + } } - return RES_OK; -} - -static int SemihostingRead(int handle, void* buffer, int count) -{ - /* SYS_READ returns the number of bytes NOT read (0 == full read). */ - const struct - { - int handle; - void* buffer; - int count; - } args = {handle, buffer, count}; - - return Semihosting(SEMIHOSTING_SYS_READ, &args); + return result; } DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count) { - if ((pdrv != 0) || (g_diskHandle < 0)) + DRESULT result; + if ((pdrv != 0) || !SemihostingDisk_IsReady()) { - return RES_NOTRDY; + result = RES_NOTRDY; } - if ((sector + count) > (LBA_t) DISK_SECTOR_COUNT) + else { - return RES_PARERR; - } - int position = (int) sector * (int) DISK_SECTOR_SIZE; - if (SemihostingSeek(g_diskHandle, position) != 0) - { - return RES_ERROR; - } - int bytes = (int) count * (int) DISK_SECTOR_SIZE; - if (SemihostingWrite(g_diskHandle, buff, bytes) != 0) - { - return RES_ERROR; + switch (SemihostingDisk_Write(buff, (uint32_t) sector, (uint32_t) count)) + { + case SEMIHOSTING_DISK_OK: + result = RES_OK; + break; + case SEMIHOSTING_DISK_OUT_OF_RANGE: + result = RES_PARERR; + break; + default: + result = RES_ERROR; + break; + } } - return RES_OK; + return result; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff) @@ -270,10 +98,10 @@ DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff) * file — no kernel-level dirty pages we need to flush. */ return RES_OK; case GET_SECTOR_COUNT: - *(LBA_t*) buff = (LBA_t) DISK_SECTOR_COUNT; + *(LBA_t*) buff = (LBA_t) SEMIHOSTING_DISK_SECTOR_COUNT; return RES_OK; case GET_SECTOR_SIZE: - *(WORD*) buff = (WORD) DISK_SECTOR_SIZE; + *(WORD*) buff = (WORD) SEMIHOSTING_DISK_SECTOR_SIZE; return RES_OK; case GET_BLOCK_SIZE: /* Erase-block size in sectors. The semihosting flat file diff --git a/Bdd/Targets/FreeRtos/CMakeLists.txt b/Bdd/Targets/FreeRtos/CMakeLists.txt index f2a572ea..a87979ef 100644 --- a/Bdd/Targets/FreeRtos/CMakeLists.txt +++ b/Bdd/Targets/FreeRtos/CMakeLists.txt @@ -289,6 +289,7 @@ add_executable(SolidSyslogBddTarget ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetTlsConfig.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/diskio.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/SemihostingDisk.c ${BAKED_PEM_HEADERS} $ ) diff --git a/Bdd/Targets/FreeRtosLwip/CMakeLists.txt b/Bdd/Targets/FreeRtosLwip/CMakeLists.txt index 9186475c..302b8cd5 100644 --- a/Bdd/Targets/FreeRtosLwip/CMakeLists.txt +++ b/Bdd/Targets/FreeRtosLwip/CMakeLists.txt @@ -249,6 +249,7 @@ add_executable(SolidSyslogBddTargetLwip ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/diskio.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/SemihostingDisk.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetErrorText.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFatFsMount.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c From 1f158d7d748cc72d8854d52a1de1fbce4a18760b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 4 Jun 2026 09:12:18 +0100 Subject: [PATCH 3/7] wip: S29.05 wire PlusFAT-backed store on the Plus-TCP target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BLOCKED at link on a pre-existing S29.04 adapter defect — see below. - FFSemihostingDisk: FF_Disk_t media driver over the shared SemihostingDisk (mount-or-format-on-first-use, FAT16, IO-manager cache from heap_4). - BddTargetPlusFatMount: the Plus-FAT FS-mount seam impl. - Plus-TCP target main.c: install the PlusFat seam (was FatFs). - Plus-TCP CMakeLists: drop FatFs (ff.c/ffsystem.c/diskio.c/staging), add the 14 Plus-FAT core sources to the relaxed OBJECT lib (+ -Wno-cpp for ff_fat.c's one-FAT-table #warning), add the adapter/driver/shim to the strict executable (-Wno-sign-conversion only on FFSemihostingDisk.c for Plus-FAT's unsigned FF_ERR_* macros). - FreeRTOSConfig.h: configNUM_THREAD_LOCAL_STORAGE_POINTERS = 3 (ff_stdio TLS). - New integrator FreeRTOSFATConfig.h. - Shared pipeline: STORE_PATH_PREFIX "STORE" -> "/STORE" (Plus-FAT ff_stdio needs absolute paths with ffconfigHAS_CWD=0; valid for ChaN too). lwIP FatFs target still builds green. The Plus-TCP target COMPILES fully but does NOT LINK: SolidSyslogPlusFatFile.c calls ff_fflush(), which FreeRTOS-Plus-FAT (SHA 8d38036) only DECLARES in ff_stdio.h and never defines. PlusFatFake defines it, so S29.04 host tests pass and masked the gap. Real durability primitive is FF_FlushCache(Fp->pxIOManager). Fixing it touches the merged S29.04 Tier-2 adapter (+ the fake + host tests), which the S29.05 AC says to leave untouched — needs David's decision. --- .../Common/BddTargetFreeRtosPipeline.c | 9 +- Bdd/Targets/Common/BddTargetPlusFatMount.c | 30 +++ Bdd/Targets/Common/BddTargetPlusFatMount.h | 34 +++ Bdd/Targets/Common/FFSemihostingDisk.c | 193 ++++++++++++++++++ Bdd/Targets/Common/FFSemihostingDisk.h | 37 ++++ Bdd/Targets/FreeRtos/CMakeLists.txt | 104 ++++++---- Bdd/Targets/FreeRtos/FreeRTOSConfig.h | 6 + Bdd/Targets/FreeRtos/FreeRTOSFATConfig.h | 26 +++ Bdd/Targets/FreeRtos/main.c | 24 ++- 9 files changed, 407 insertions(+), 56 deletions(-) create mode 100644 Bdd/Targets/Common/BddTargetPlusFatMount.c create mode 100644 Bdd/Targets/Common/BddTargetPlusFatMount.h create mode 100644 Bdd/Targets/Common/FFSemihostingDisk.c create mode 100644 Bdd/Targets/Common/FFSemihostingDisk.h create mode 100644 Bdd/Targets/FreeRtos/FreeRTOSFATConfig.h diff --git a/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c b/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c index 27bbe7d9..91999f6b 100644 --- a/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c +++ b/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c @@ -97,9 +97,12 @@ static volatile bool solidSyslogTeardown = false; /* File-backed store storage. Lives in .bss so it persists across the `set store * file` rebuild; only populated when that command fires. STORE_PATH_PREFIX is - * "STORE" — sequence-numbered FAT filenames land as STORE00.log, STORE01.log, - * … which fit 8.3 short-filename mode (LFN=0 in our shared ffconf.h). */ -static const char STORE_PATH_PREFIX[] = "STORE"; + * "/STORE" — sequence-numbered filenames land at the volume root as + * /STORE00.log, /STORE01.log, … which fit 8.3 short-filename mode. The leading + * slash makes the path absolute: ChaN-FatFs treats it as the default-drive root + * (unchanged behaviour), and FreeRTOS-Plus-FAT's ff_stdio requires an absolute + * path when ffconfigHAS_CWD is 0 (its prvABSPath is a pass-through). */ +static const char STORE_PATH_PREFIX[] = "/STORE"; static struct SolidSyslogFile* storeFile = NULL; static struct SolidSyslogBlockDevice* storeBlockDevice = NULL; diff --git a/Bdd/Targets/Common/BddTargetPlusFatMount.c b/Bdd/Targets/Common/BddTargetPlusFatMount.c new file mode 100644 index 00000000..222ac51a --- /dev/null +++ b/Bdd/Targets/Common/BddTargetPlusFatMount.c @@ -0,0 +1,30 @@ +/* FreeRTOS-Plus-FAT implementation of the shared pipeline's FS-mount seam — see + * BddTargetPlusFatMount.h. The Plus-FAT sibling of BddTargetFatFsMount: the + * volume mount/unmount lives in the FF_Disk_t media driver (FFSemihostingDisk); + * the SolidSyslogFile adapter is SolidSyslogPlusFatFile. SolidSyslog S29.05. */ + +#include "BddTargetPlusFatMount.h" + +#include "FFSemihostingDisk.h" +#include "SolidSyslogPlusFatFile.h" + +bool BddTargetPlusFatMount_Mount(void) +{ + return FFSemihostingDisk_Mount(); +} + +void BddTargetPlusFatMount_Unmount(void) +{ + FFSemihostingDisk_Unmount(); +} + +struct SolidSyslogFile* BddTargetPlusFatMount_CreateFile(void) +{ + return SolidSyslogPlusFatFile_Create(); +} + +void BddTargetPlusFatMount_DestroyFile(struct SolidSyslogFile* file) +{ + /* PlusFatFile_Destroy → Close → ff_fclose flushes the file's dir entry. */ + SolidSyslogPlusFatFile_Destroy(file); +} diff --git a/Bdd/Targets/Common/BddTargetPlusFatMount.h b/Bdd/Targets/Common/BddTargetPlusFatMount.h new file mode 100644 index 00000000..0d83839e --- /dev/null +++ b/Bdd/Targets/Common/BddTargetPlusFatMount.h @@ -0,0 +1,34 @@ +#ifndef BDDTARGETPLUSFATMOUNT_H +#define BDDTARGETPLUSFATMOUNT_H + +#include "ExternC.h" + +#include + +EXTERN_C_BEGIN + + /* FreeRTOS-Plus-FAT implementation of the shared pipeline's FS-mount seam + * (struct BddTargetFreeRtosPipelineConfig). The Plus-FAT sibling of + * BddTargetFatFsMount: mount/unmount delegate to the FF_Disk_t semihosting + * media driver (FFSemihostingDisk); the SolidSyslogFile adapter is + * SolidSyslogPlusFatFile. Wired by the FreeRTOS-Plus-TCP target's main.c; + * the lwIP target uses the ChaN-FatFs sibling. Introduced in SolidSyslog + * S29.05. */ + + /* Mount the Plus-FAT volume, formatting on first use. Idempotent; false on + * an unrecoverable mount/format failure so the caller can leave the target + * on its original store. */ + bool BddTargetPlusFatMount_Mount(void); + + /* Unmount the Plus-FAT volume if mounted; no-op otherwise. */ + void BddTargetPlusFatMount_Unmount(void); + + /* Create / destroy the Plus-FAT SolidSyslogFile adapter (forward-declared to + * keep ff_stdio.h / SolidSyslogPlusFatFile.h out of the seam's public + * surface). */ + struct SolidSyslogFile* BddTargetPlusFatMount_CreateFile(void); + void BddTargetPlusFatMount_DestroyFile(struct SolidSyslogFile* file); + +EXTERN_C_END + +#endif /* BDDTARGETPLUSFATMOUNT_H */ diff --git a/Bdd/Targets/Common/FFSemihostingDisk.c b/Bdd/Targets/Common/FFSemihostingDisk.c new file mode 100644 index 00000000..a263cb34 --- /dev/null +++ b/Bdd/Targets/Common/FFSemihostingDisk.c @@ -0,0 +1,193 @@ +/* FreeRTOS-Plus-FAT FF_Disk_t media driver over the shared semihosting flat + * disk — see FFSemihostingDisk.h. Block read/write delegate to SemihostingDisk + * (the same host image + BKPT 0xAB trap the ChaN-FatFs diskio.c uses). The IO + * manager / partition / format / mount sequence mirrors Plus-FAT's reference + * portable/common/ff_ramdisk.c, with format made first-use-only so a persistent + * image survives a power cycle (power_cycle_replay). SolidSyslog S29.05. */ + +#include "FFSemihostingDisk.h" + +#include "SemihostingDisk.h" + +#include "FreeRTOS.h" +#include "semphr.h" + +#include "ff_headers.h" +#include "ff_sys.h" + +#include +#include + +enum +{ + /* Only a single primary partition, partition 0, is used. */ + DISK_PARTITION_NUMBER = 0, + DISK_PRIMARY_PARTITIONS = 1, + /* Keep the conventional first track free, matching ff_ramdisk.c. */ + DISK_HIDDEN_SECTORS = 8, + /* IO manager sector cache, in 512 B sectors. Multiple of the sector size and + * at least 2 sectors (FF_CreateIOManager asserts both). Eight sectors (4 KB) + * gives the FAT / directory traversal comfortable headroom while staying a + * small fraction of the 96 KB FreeRTOS heap this target also shares with the + * Plus-TCP buffers and the mbedTLS handshake. */ + DISK_CACHE_SECTORS = 8, +}; + +/* Magic stamped into the FF_Disk_t so the block callbacks can validate they were + * handed our disk before touching it. "SYSL". */ +#define DISK_SIGNATURE 0x5359534CUL + +/* The single FF_Disk_t volume + its mounted flag live in .bss; only the Plus-FAT + * IO manager cache (allocated inside FF_CreateIOManager) hits the heap. */ +static FF_Disk_t g_disk; +static bool g_mounted = false; +static StaticSemaphore_t g_mutexStorage; + +static int32_t ReadBlocks(uint8_t* buffer, uint32_t sector, uint32_t count, FF_Disk_t* disk); +static int32_t WriteBlocks(uint8_t* buffer, uint32_t sector, uint32_t count, FF_Disk_t* disk); +static FF_Error_t PartitionAndFormat(void); + +bool FFSemihostingDisk_Mount(void) +{ + if (g_mounted) + { + return true; + } + if (!SemihostingDisk_EnsureReady()) + { + return false; + } + + memset(&g_disk, 0, sizeof(g_disk)); + g_disk.ulSignature = DISK_SIGNATURE; + g_disk.ulNumberOfSectors = (uint32_t) SEMIHOSTING_DISK_SECTOR_COUNT; + + FF_CreationParameters_t parameters; + memset(¶meters, 0, sizeof(parameters)); + parameters.pucCacheMemory = NULL; /* Plus-FAT mallocs the cache from heap_4. */ + parameters.ulMemorySize = (uint32_t) (DISK_CACHE_SECTORS * SEMIHOSTING_DISK_SECTOR_SIZE); + parameters.ulSectorSize = SEMIHOSTING_DISK_SECTOR_SIZE; + parameters.fnWriteBlocks = WriteBlocks; + parameters.fnReadBlocks = ReadBlocks; + parameters.pxDisk = &g_disk; + parameters.pvSemaphore = (void*) xSemaphoreCreateRecursiveMutexStatic(&g_mutexStorage); + parameters.xBlockDeviceIsReentrant = pdFALSE; + + FF_Error_t error = FF_ERR_NONE; + g_disk.pxIOManager = FF_CreateIOManager(¶meters, &error); + if (g_disk.pxIOManager == NULL) + { + return false; + } + g_disk.xStatus.bIsInitialised = pdTRUE; + g_disk.xStatus.bPartitionNumber = DISK_PARTITION_NUMBER; + + /* Mount-or-format: try the existing FAT first so a power cycle keeps its + * data; only a fresh / zero-filled image (no mountable partition) falls + * through to partition + format + remount. */ + error = FF_Mount(&g_disk, DISK_PARTITION_NUMBER); + if (FF_isERR(error) != pdFALSE) + { + error = PartitionAndFormat(); + if (FF_isERR(error) == pdFALSE) + { + error = FF_Mount(&g_disk, DISK_PARTITION_NUMBER); + } + } + if (FF_isERR(error) != pdFALSE) + { + (void) printf("[solidsyslog] plusfat mount failed: 0x%08X\n", (unsigned int) error); + FF_DeleteIOManager(g_disk.pxIOManager); + g_disk.pxIOManager = NULL; + return false; + } + + g_disk.xStatus.bIsMounted = pdTRUE; + FF_FS_Add("/", &g_disk); + g_mounted = true; + return true; +} + +void FFSemihostingDisk_Unmount(void) +{ + if (g_mounted) + { + FF_FS_Remove("/"); + (void) FF_Unmount(&g_disk); + FF_DeleteIOManager(g_disk.pxIOManager); + g_disk.pxIOManager = NULL; + g_disk.xStatus.bIsMounted = pdFALSE; + g_mounted = false; + } +} + +/* Lay down a single primary partition spanning the disk and format it FAT16 + * (xPreferFAT16 = pdTRUE). The 8 MiB geometry sits clear of the ~4085-cluster + * FAT12/16 boundary, so the formatter lands FAT16 as intended. Runs only on a + * fresh image. */ +static FF_Error_t PartitionAndFormat(void) +{ + FF_PartitionParameters_t partition; + memset(&partition, 0, sizeof(partition)); + partition.ulSectorCount = g_disk.ulNumberOfSectors; + partition.ulHiddenSectors = DISK_HIDDEN_SECTORS; + partition.xPrimaryCount = DISK_PRIMARY_PARTITIONS; + partition.eSizeType = eSizeIsQuota; + + FF_Error_t error = FF_Partition(&g_disk, &partition); + if (FF_isERR(error) == pdFALSE) + { + error = FF_Format(&g_disk, DISK_PARTITION_NUMBER, pdTRUE, pdFALSE); + } + return error; +} + +static int32_t ReadBlocks(uint8_t* buffer, uint32_t sector, uint32_t count, FF_Disk_t* disk) +{ + int32_t result; + if ((disk == NULL) || (disk->ulSignature != DISK_SIGNATURE) || (disk->xStatus.bIsInitialised == pdFALSE)) + { + result = FF_ERR_IOMAN_DRIVER_FATAL_ERROR | FF_ERRFLAG; + } + else + { + switch (SemihostingDisk_Read(buffer, sector, count)) + { + case SEMIHOSTING_DISK_OK: + result = FF_ERR_NONE; + break; + case SEMIHOSTING_DISK_OUT_OF_RANGE: + result = (int32_t) (FF_ERR_IOMAN_OUT_OF_BOUNDS_READ | FF_ERRFLAG); + break; + default: + result = FF_ERR_IOMAN_DRIVER_FATAL_ERROR | FF_ERRFLAG; + break; + } + } + return result; +} + +static int32_t WriteBlocks(uint8_t* buffer, uint32_t sector, uint32_t count, FF_Disk_t* disk) +{ + int32_t result; + if ((disk == NULL) || (disk->ulSignature != DISK_SIGNATURE) || (disk->xStatus.bIsInitialised == pdFALSE)) + { + result = FF_ERR_IOMAN_DRIVER_FATAL_ERROR | FF_ERRFLAG; + } + else + { + switch (SemihostingDisk_Write(buffer, sector, count)) + { + case SEMIHOSTING_DISK_OK: + result = FF_ERR_NONE; + break; + case SEMIHOSTING_DISK_OUT_OF_RANGE: + result = (int32_t) (FF_ERR_IOMAN_OUT_OF_BOUNDS_WRITE | FF_ERRFLAG); + break; + default: + result = FF_ERR_IOMAN_DRIVER_FATAL_ERROR | FF_ERRFLAG; + break; + } + } + return result; +} diff --git a/Bdd/Targets/Common/FFSemihostingDisk.h b/Bdd/Targets/Common/FFSemihostingDisk.h new file mode 100644 index 00000000..73892dab --- /dev/null +++ b/Bdd/Targets/Common/FFSemihostingDisk.h @@ -0,0 +1,37 @@ +#ifndef FFSEMIHOSTINGDISK_H +#define FFSEMIHOSTINGDISK_H + +#include "ExternC.h" + +#include + +EXTERN_C_BEGIN + + /* FreeRTOS-Plus-FAT FF_Disk_t media driver over the shared semihosting flat + * disk (SemihostingDisk) for the QEMU mps2-an385 BDD target. The Plus-FAT + * analogue of the ChaN-FatFs diskio.c glue: same host image, same 8 MiB + * FAT16 geometry, same BKPT 0xAB trap — only the vtable shape differs + * (FF_Disk_t block callbacks vs ChaN's global disk_*). Modelled on Plus-FAT's + * own portable/common/ff_ramdisk.c reference driver, but persistent (the host + * image survives the run) so it mounts an existing FAT and only partitions + + * formats on a fresh / zero-filled image — format-on-first-use, not + * format-always. + * + * The single FF_Disk_t volume lives in this TU; the FF_IOManager cache is + * allocated internally by Plus-FAT from the FreeRTOS heap (heap_4) — the one + * place this target allocates for the filesystem, exactly the documented + * vendor-allocates stance. Registered into ff_stdio's virtual FS at "/". + * Introduced in SolidSyslog S29.05. */ + + /* Ensure the host image, build the IO manager, mount (formatting on first + * use), and register the volume at "/". Idempotent — repeated calls + * short-circuit once mounted. Returns true once the volume is mounted. */ + bool FFSemihostingDisk_Mount(void); + + /* Deregister, unmount, and release the IO manager (frees the Plus-FAT cache). + * Safe to call when not mounted. */ + void FFSemihostingDisk_Unmount(void); + +EXTERN_C_END + +#endif /* FFSEMIHOSTINGDISK_H */ diff --git a/Bdd/Targets/FreeRtos/CMakeLists.txt b/Bdd/Targets/FreeRtos/CMakeLists.txt index a87979ef..8b0b4b59 100644 --- a/Bdd/Targets/FreeRtos/CMakeLists.txt +++ b/Bdd/Targets/FreeRtos/CMakeLists.txt @@ -30,11 +30,11 @@ if(NOT FREERTOS_PLUS_TCP_PATH) "container, which sets this to /opt/freertos/plus-tcp.") endif() -set(FATFS_PATH "$ENV{FATFS_PATH}") -if(NOT FATFS_PATH) +set(FREERTOS_PLUS_FAT_PATH "$ENV{FREERTOS_PLUS_FAT_PATH}") +if(NOT FREERTOS_PLUS_FAT_PATH) message(FATAL_ERROR - "FATFS_PATH not set. Use the cpputest-freertos-cross container, " - "which sets this to /opt/fatfs.") + "FREERTOS_PLUS_FAT_PATH not set. Use the cpputest-freertos-cross " + "container, which sets this to /opt/freertos/plus-fat.") endif() set(MBEDTLS_SOURCE_DIR "$ENV{MBEDTLS_DIR}") @@ -44,17 +44,12 @@ if(NOT EXISTS "${MBEDTLS_SOURCE_DIR}/CMakeLists.txt") "container, which sets this to /opt/mbedtls.") endif() -# Stage the upstream FatFs source pack alongside our integrator ffconf.h in -# the build dir. FatFs's ff.h does `#include "ffconf.h"` and GCC's `""` -# include lookup checks the source file's own directory first, so the only -# way our integrator config wins over upstream's is to colocate them. -# configure_file(COPYONLY) sets up the CMake dependency so reconfigure -# triggers on changes to either side. -set(FATFS_STAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/fatfs") -configure_file(${FATFS_PATH}/source/ff.c ${FATFS_STAGE_DIR}/ff.c COPYONLY) -configure_file(${FATFS_PATH}/source/ff.h ${FATFS_STAGE_DIR}/ff.h COPYONLY) -configure_file(${FATFS_PATH}/source/diskio.h ${FATFS_STAGE_DIR}/diskio.h COPYONLY) -configure_file(${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/ffconf.h ${FATFS_STAGE_DIR}/ffconf.h COPYONLY) +# No source staging for FreeRTOS-Plus-FAT (unlike ChaN-FatFs). ff_headers.h's +# `#include "FreeRTOSFATConfig.h"` finds no copy beside itself in +# ${FREERTOS_PLUS_FAT_PATH}/include, so GCC's `""` lookup falls through to the +# -I path, where our integrator FreeRTOSFATConfig.h (this directory) wins. + +set(PLUS_FAT_SRC_DIR "${FREERTOS_PLUS_FAT_PATH}") set(FREERTOS_PORT_DIR "${FREERTOS_KERNEL_PATH}/portable/GCC/ARM_CM3") @@ -124,18 +119,26 @@ add_library(solid_syslog_freertos_upstream OBJECT # MPS2_AN385 LAN9118 NetworkInterface + driver. ${PLUS_TCP_NIF_DIR}/NetworkInterface.c ${PLUS_TCP_NIF_DIR}/ether_lan9118/smsc9220_eth_drv.c - # FatFs upstream (R0.16, FFCONF_DEF 80386). ff.c trips the same - # sign-conversion / shadow / type-narrowing warnings the FreeRTOS - # kernel sources do, so it lives in the upstream OBJECT lib under the - # relaxed warning set. ffsystem.c is our integrator copy - # (Bdd/Targets/Common/, shared with the lwIP target) with OS_TYPE 3 - # (FreeRTOS) selected — it sits under the same relaxations because it - # includes ff.h and inherits FatFs's typedef-via-BYTE/UINT patterns. - # ff.c is staged into the build dir so its `#include "ff.h"` (and - # ff.h's "ffconf.h") resolve to our integrator config — see the - # configure_file calls above. - ${FATFS_STAGE_DIR}/ff.c - ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/ffsystem.c + # FreeRTOS-Plus-FAT upstream core (SHA 8d38036). Like the FreeRTOS kernel + # and Plus-TCP sources, these trip the relaxed warnings (sign-conversion, + # shadow, type narrowing), so they live in this OBJECT lib. The integrator + # FreeRTOSFATConfig.h is found off the -I path (CMAKE_CURRENT_SOURCE_DIR + # below) — no source staging, see note above. The FF_Disk_t media driver + # (FFSemihostingDisk.c) is OUR code and stays in the strict executable + # target alongside the SolidSyslogPlusFatFile adapter. + ${PLUS_FAT_SRC_DIR}/ff_crc.c + ${PLUS_FAT_SRC_DIR}/ff_dir.c + ${PLUS_FAT_SRC_DIR}/ff_error.c + ${PLUS_FAT_SRC_DIR}/ff_fat.c + ${PLUS_FAT_SRC_DIR}/ff_file.c + ${PLUS_FAT_SRC_DIR}/ff_format.c + ${PLUS_FAT_SRC_DIR}/ff_ioman.c + ${PLUS_FAT_SRC_DIR}/ff_locking.c + ${PLUS_FAT_SRC_DIR}/ff_memory.c + ${PLUS_FAT_SRC_DIR}/ff_stdio.c + ${PLUS_FAT_SRC_DIR}/ff_string.c + ${PLUS_FAT_SRC_DIR}/ff_sys.c + ${PLUS_FAT_SRC_DIR}/ff_time.c ) target_compile_options(solid_syslog_freertos_upstream PRIVATE @@ -144,9 +147,9 @@ target_compile_options(solid_syslog_freertos_upstream PRIVATE -ffunction-sections -fdata-sections -fno-common - # FreeRTOS-Kernel + Plus-TCP V11.1.0/V4.2.2 use non-prototype function - # declarations, type-narrowing constructs, sign-conversion patterns, and - # shadowed locals that trip our strict host warnings. Scoped to this + # FreeRTOS-Kernel + Plus-TCP V11.1.0/V4.2.2 + Plus-FAT use non-prototype + # function declarations, type-narrowing constructs, sign-conversion patterns, + # and shadowed locals that trip our strict host warnings. Scoped to this # OBJECT library so the relaxations never reach project code. -Wno-unused-parameter -Wno-unused-but-set-variable @@ -156,6 +159,10 @@ target_compile_options(solid_syslog_freertos_upstream PRIVATE -Wno-sign-conversion -Wno-shadow -Wno-pedantic + # Plus-FAT's ff_fat.c emits `#warning Only maintaining one FAT table` when + # built with a single FAT (our config: n_fat = 1, the intended layout). The + # informational #warning would otherwise be promoted by -Werror. + -Wno-cpp ) target_include_directories(solid_syslog_freertos_upstream PRIVATE @@ -166,7 +173,7 @@ target_include_directories(solid_syslog_freertos_upstream PRIVATE ${PLUS_TCP_PORT_GCC_DIR} # pack_struct_*.h ${PLUS_TCP_NIF_DIR} # NetworkInterface.h consumed locally ${PLUS_TCP_NIF_DIR}/ether_lan9118 # SMM_MPS2.h, smsc9220_*.h - ${FATFS_STAGE_DIR} # ff.h, diskio.h, our ffconf.h colocated + ${FREERTOS_PLUS_FAT_PATH}/include # ff_headers.h, ff_stdio.h, ff_ioman.h, … ) # --- mbedTLS upstream as a subproject ---------------------------------------- @@ -259,8 +266,8 @@ add_executable(SolidSyslogBddTarget Startup.c ${CMAKE_CURRENT_SOURCE_DIR}/Common/CmsdkUart.c ${CMAKE_CURRENT_SOURCE_DIR}/Common/Syscalls.c - ${CMAKE_SOURCE_DIR}/Platform/FatFs/Source/SolidSyslogFatFsFile.c - ${CMAKE_SOURCE_DIR}/Platform/FatFs/Source/SolidSyslogFatFsFileStatic.c + ${CMAKE_SOURCE_DIR}/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c + ${CMAKE_SOURCE_DIR}/Platform/PlusFat/Source/SolidSyslogPlusFatFileStatic.c ${CMAKE_SOURCE_DIR}/Platform/PlusTcp/Source/SolidSyslogPlusTcpAddress.c ${CMAKE_SOURCE_DIR}/Platform/PlusTcp/Source/SolidSyslogPlusTcpAddressStatic.c ${CMAKE_SOURCE_DIR}/Platform/PlusTcp/Source/SolidSyslogPlusTcpDatagram.c @@ -279,16 +286,16 @@ add_executable(SolidSyslogBddTarget ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetErrorText.c - ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFatFsMount.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetInteractive.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetLanguage.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetMtlsConfig.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetPlusFatMount.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetSwitchConfig.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetTlsConfig.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c - ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/diskio.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/FFSemihostingDisk.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/SemihostingDisk.c ${BAKED_PEM_HEADERS} $ @@ -309,7 +316,7 @@ target_include_directories(SolidSyslogBddTarget PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Common # CmsdkUart.h ${CMAKE_SOURCE_DIR}/Core/Interface # SolidSyslog*.h ${CMAKE_SOURCE_DIR}/Core/Source # internal headers consumed by adapters - ${CMAKE_SOURCE_DIR}/Platform/FatFs/Interface # SolidSyslogFatFsFile.h + ${CMAKE_SOURCE_DIR}/Platform/PlusFat/Interface # SolidSyslogPlusFatFile.h ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Interface # SolidSyslogFreeRtos{Mutex,SysUpTime}.h ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source # SolidSyslogFreeRtosMutexPrivate.h ${CMAKE_SOURCE_DIR}/Platform/PlusTcp/Interface # SolidSyslogFreeRtos{Address,Datagram,Resolver,TcpStream}.h @@ -322,13 +329,24 @@ target_include_directories(SolidSyslogBddTarget PRIVATE ${FREERTOS_PORT_DIR} # portmacro.h ${PLUS_TCP_SRC_DIR}/include ${PLUS_TCP_PORT_GCC_DIR} # pack_struct_*.h - ${FATFS_STAGE_DIR} # ff.h, diskio.h + integrator ffconf.h colocated — must match - # the OBJECT lib (above) so ff.c, main.c, diskio.c, and - # SolidSyslogFatFsFile.c all see the SAME ffconf.h. Pointing - # the executable at FATFS_PATH/source would resolve ff.h's - # `#include "ffconf.h"` to upstream's sample (same directory - # rule) and diverge FATFS / config-conditional layout between - # translation units. + ${FREERTOS_PLUS_FAT_PATH}/include # ff_stdio.h (adapter) + ff_headers.h / ff_ioman.h / + # ff_format.h / ff_sys.h (FFSemihostingDisk.c). The + # integrator FreeRTOSFATConfig.h resolves to + # CMAKE_CURRENT_SOURCE_DIR above — same as the OBJECT lib, + # so the Plus-FAT core and our adapter/driver share one + # config across all translation units. +) + +# FFSemihostingDisk.c drives the mount/format lifecycle via Plus-FAT's FF_isERR / +# FF_ERR_* macros, whose error codes carry the 0x80000000 FF_ERRFLAG bit and so +# are unsigned-with-high-bit — they cannot be made -Wsign-conversion clean at the +# call site. Relax only that one diagnostic for only this file; every other +# strict warning (and the rest of our code) is unaffected. The SolidSyslogPlusFatFile +# adapter needs no such relaxation — it uses the ff_stdio handle API, not the +# error-code macros. +set_source_files_properties( + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/FFSemihostingDisk.c + PROPERTIES COMPILE_OPTIONS "-Wno-sign-conversion" ) target_link_libraries(SolidSyslogBddTarget PRIVATE diff --git a/Bdd/Targets/FreeRtos/FreeRTOSConfig.h b/Bdd/Targets/FreeRtos/FreeRTOSConfig.h index 24437ee6..a29514cd 100644 --- a/Bdd/Targets/FreeRtos/FreeRTOSConfig.h +++ b/Bdd/Targets/FreeRtos/FreeRTOSConfig.h @@ -41,6 +41,12 @@ #define configSUPPORT_DYNAMIC_ALLOCATION 1 #define configKERNEL_PROVIDED_STATIC_MEMORY 1 +/* FreeRTOS-Plus-FAT's ff_stdio stores its errno, CWD, and FF_Error in per-task + * thread-local storage at offsets ffconfigCWD_THREAD_LOCAL_INDEX + {0,1,2}. With + * the index pinned to 0 (see FreeRTOSFATConfig.h), three slots are the minimum + * ff_stdio.h enforces at compile time. */ +#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 3 + #define configUSE_CO_ROUTINES 0 #define configMAX_CO_ROUTINE_PRIORITIES 1 diff --git a/Bdd/Targets/FreeRtos/FreeRTOSFATConfig.h b/Bdd/Targets/FreeRtos/FreeRTOSFATConfig.h new file mode 100644 index 00000000..299c89b7 --- /dev/null +++ b/Bdd/Targets/FreeRtos/FreeRTOSFATConfig.h @@ -0,0 +1,26 @@ +/* Integrator FreeRTOS-Plus-FAT configuration for the FreeRTOS-Plus-TCP BDD + * target (QEMU mps2-an385). Plus-FAT is header-configured: ff_headers.h includes + * this file (after FreeRTOS.h) and FreeRTOSFATConfigDefaults.h fills in every + * value not set here. Resolved off the -I path (ff_headers.h's `#include + * "FreeRTOSFATConfig.h"` finds no copy beside itself), so no source staging is + * needed — unlike ChaN-FatFs's ffconf.h. Introduced in SolidSyslog S29.05. + * + * Mirrors the host-test config in Tests/Support/PlusFatFakes/Interface so the + * adapter sees the same Plus-FAT layout on-target as under host TDD. The + * matching configNUM_THREAD_LOCAL_STORAGE_POINTERS (>= 3) lives in + * FreeRTOSConfig.h, which FreeRTOS.h pulls in before this file. */ +#ifndef FREERTOSFATCONFIG_H +#define FREERTOSFATCONFIG_H + +/* QEMU mps2-an385 (Cortex-M3) is little-endian. */ +#define ffconfigBYTE_ORDER (pdFREERTOS_LITTLE_ENDIAN) + +/* Base index into the per-task thread-local storage array for ff_stdio's errno + * / CWD / FF_Error pointers (offsets +0/+1/+2). Pinned to 0; FreeRTOSConfig.h + * sizes configNUM_THREAD_LOCAL_STORAGE_POINTERS to cover the three slots. */ +#define ffconfigCWD_THREAD_LOCAL_INDEX (0) + +/* The store uses absolute 8.3 paths at the volume root (/STORE00.log, …), so + * relative-path / long-filename support is left at the defaults (both off). */ + +#endif /* FREERTOSFATCONFIG_H */ diff --git a/Bdd/Targets/FreeRtos/main.c b/Bdd/Targets/FreeRtos/main.c index 04e472c2..fbdd8d61 100644 --- a/Bdd/Targets/FreeRtos/main.c +++ b/Bdd/Targets/FreeRtos/main.c @@ -1,20 +1,24 @@ /* FreeRTOS-Plus-TCP SolidSyslog BDD target for QEMU mps2-an385. * - * The platform-independent pipeline — SolidSyslog lifecycle, FatFs-backed store + * The platform-independent pipeline — SolidSyslog lifecycle, file-backed store * + security policies, SD set, the interactive `set` handler, the Service drain * task, and the console glue — lives in Bdd/Targets/Common/BddTargetFreeRtosPipeline * (shared with the lwIP target, S29.03). This file keeps only the FreeRTOS-Plus-TCP - * network backend behind the pipeline seam: static-IP bring-up, the LAN9118 IRQ - * priority fix, the per-endpoint RNG / sequence hooks, the PlusTcp sender wiring - * (UDP datagram + octet-framed TCP + TLS/mTLS via mbedTLS over PlusTcp TCP), and - * the RFC 5424 HOSTNAME read from the Plus-TCP endpoint. + * network backend and the FreeRTOS-Plus-FAT store behind the pipeline seam: + * static-IP bring-up, the LAN9118 IRQ priority fix, the per-endpoint RNG / + * sequence hooks, the PlusTcp sender wiring (UDP datagram + octet-framed TCP + + * TLS/mTLS via mbedTLS over PlusTcp TCP), the RFC 5424 HOSTNAME read from the + * Plus-TCP endpoint, and the Plus-FAT FS-mount seam (BddTargetPlusFatMount over + * the FF_Disk_t semihosting media driver). The lwIP target pairs lwIP with + * ChaN-FatFs instead — together the two targets prove the SolidSyslogFile seam + * is FS-vendor-portable (S29.05). * * Static IPv4 (10.0.2.15) on the QEMU slirp network with the host reachable at * the slirp gateway 10.0.2.2. */ -#include "BddTargetFatFsMount.h" #include "BddTargetFreeRtosPipeline.h" #include "BddTargetMtlsConfig.h" +#include "BddTargetPlusFatMount.h" #include "BddTargetSwitchConfig.h" #include "BddTargetTlsConfig.h" #include "BddTargetTlsSender.h" @@ -101,10 +105,10 @@ static const struct BddTargetFreeRtosPipelineConfig PIPELINE_CONFIG = { .BuildSender = BuildSender, .GetHostname = GetHostname, .TeardownNetwork = TeardownNetwork, - .MountStore = BddTargetFatFsMount_Mount, - .UnmountStore = BddTargetFatFsMount_Unmount, - .CreateStoreFile = BddTargetFatFsMount_CreateFile, - .DestroyStoreFile = BddTargetFatFsMount_DestroyFile, + .MountStore = BddTargetPlusFatMount_Mount, + .UnmountStore = BddTargetPlusFatMount_Unmount, + .CreateStoreFile = BddTargetPlusFatMount_CreateFile, + .DestroyStoreFile = BddTargetPlusFatMount_DestroyFile, }; int main(void) From 7ebb795d8d765c3cc9183385bd8cec7b0bffca6b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 4 Jun 2026 11:07:25 +0100 Subject: [PATCH 4/7] fix: S29.05 use FF_FlushCache for PlusFat write durability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ff_fflush is declared in FreeRTOS-Plus-FAT's ff_stdio.h but never defined in the library (SHA 8d38036) — PlusFatFake defined it, so the S29.04 host tests passed while the real Plus-TCP target failed to link. Switch the adapter's per-write durability to FF_FlushCache(Fp->pxIOManager), the actual Plus-FAT cache-flush primitive (FF_FILE carries pxIOManager; ff_stdio.h pulls ff_ioman.h + ff_file.h, so no new includes). The directory-entry size is still committed on Close. Update PlusFatFake (FF_FlushCache spy replaces ff_fflush) and the three durability tests. Approved expansion of the S29.05 scope (the adapter was the one place the host fake masked a real-library gap). Host: SolidSyslogPlusFatFileTest 30/30 green. Plus-TCP cross target now links; nm confirms FF_FlushCache present, no ff_fflush, no FatFs symbols. --- .../PlusFat/Source/SolidSyslogPlusFatFile.c | 9 ++++-- Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp | 10 +++---- .../PlusFatFakes/Interface/PlusFatFake.h | 6 ++-- .../Support/PlusFatFakes/Source/PlusFatFake.c | 28 ++++++++++--------- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c index 04b550f1..c408f780 100644 --- a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c @@ -92,10 +92,13 @@ static bool PlusFatFile_Read(struct SolidSyslogFile* base, void* buf, size_t cou static bool PlusFatFile_Write(struct SolidSyslogFile* base, const void* buf, size_t count) { struct SolidSyslogPlusFatFile* self = PlusFatFile_SelfFromBase(base); - /* Flush after every complete write so a power loss never loses a record - * the BlockStore was told had been stored. */ + /* Flush the IO-manager cache after every complete write so a power loss + * never loses a record the BlockStore was told had been stored. Plus-FAT + * has no per-file flush — ff_stdio.h declares ff_fflush but the library + * never defines it; FF_FlushCache against the file's IO manager is the real + * durability primitive. The directory entry's size is committed on Close. */ bool wroteAll = ff_fwrite(buf, 1, count, self->Fp) == count; - return wroteAll && (ff_fflush(self->Fp) == 0); + return wroteAll && (FF_FlushCache(self->Fp->pxIOManager) == FF_ERR_NONE); } static void PlusFatFile_SeekTo(struct SolidSyslogFile* base, size_t offset) diff --git a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp index a3ac68cc..978ac179 100644 --- a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp +++ b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp @@ -135,13 +135,13 @@ TEST(SolidSyslogPlusFatFile, WriteCallsFfwriteWithData) UNSIGNED_LONGS_EQUAL(sizeof(buffer), PlusFatFake_LastWriteItems()); } -TEST(SolidSyslogPlusFatFile, WriteCommitsToDiskWithFfflush) +TEST(SolidSyslogPlusFatFile, WriteCommitsToDiskWithFlushCache) { SolidSyslogFile_Open(file, TEST_PATH); SolidSyslogFile_Write(file, buffer, 1); - CALLED_FAKE(PlusFatFake_Fflush, ONCE); + CALLED_FAKE(PlusFatFake_FlushCache, ONCE); } TEST(SolidSyslogPlusFatFile, WriteFailsAndSkipsFlushWhenFfwriteIncomplete) @@ -151,13 +151,13 @@ TEST(SolidSyslogPlusFatFile, WriteFailsAndSkipsFlushWhenFfwriteIncomplete) CHECK_FALSE(SolidSyslogFile_Write(file, buffer, sizeof(buffer))); - CALLED_FAKE(PlusFatFake_Fflush, NEVER); + CALLED_FAKE(PlusFatFake_FlushCache, NEVER); } -TEST(SolidSyslogPlusFatFile, WriteFailsWhenFfflushFails) +TEST(SolidSyslogPlusFatFile, WriteFailsWhenFlushCacheFails) { SolidSyslogFile_Open(file, TEST_PATH); - PlusFatFake_SetFflushFails(); + PlusFatFake_SetFlushCacheFails(); CHECK_FALSE(SolidSyslogFile_Write(file, buffer, sizeof(buffer))); } diff --git a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h index 89a49a98..079010ef 100644 --- a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h +++ b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h @@ -29,9 +29,9 @@ EXTERN_C_BEGIN const void* PlusFatFake_LastWriteBytes(void); unsigned long PlusFatFake_LastWriteItems(void); - /* ff_fflush */ - void PlusFatFake_SetFflushFails(void); - int PlusFatFake_FflushCallCount(void); + /* FF_FlushCache */ + void PlusFatFake_SetFlushCacheFails(void); + int PlusFatFake_FlushCacheCallCount(void); /* ff_fseek */ int PlusFatFake_SeekCallCount(void); diff --git a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c index 64e6854f..095720ca 100644 --- a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c +++ b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c @@ -38,9 +38,9 @@ static unsigned char lastWriteBytes[WRITE_CAPTURE_CAPACITY]; static size_t lastWriteItems; static bool writeIncomplete; -/* ff_fflush state */ -static int fflushCallCount; -static int fflushResult; +/* FF_FlushCache state */ +static int flushCacheCallCount; +static int flushCacheResult; /* ff_fseek state */ static int seekCallCount; @@ -83,8 +83,8 @@ void PlusFatFake_Reset(void) memset(lastWriteBytes, 0, sizeof(lastWriteBytes)); lastWriteItems = 0; writeIncomplete = false; - fflushCallCount = 0; - fflushResult = 0; + flushCacheCallCount = 0; + flushCacheResult = 0; seekCallCount = 0; lastSeekOffset = 0; lastSeekWhence = 0; @@ -224,21 +224,23 @@ size_t ff_fwrite(const void* pvBuffer, size_t xSize, size_t xItems, FF_FILE* pxS return itemsWritten; } -void PlusFatFake_SetFflushFails(void) +void PlusFatFake_SetFlushCacheFails(void) { - fflushResult = -1; + /* Any error code with the FF_ERRFLAG bit set is non-FF_ERR_NONE — the + * adapter treats it as a flush failure. */ + flushCacheResult = (int) (FF_ERR_IOMAN_DRIVER_FATAL_ERROR | FF_ERRFLAG); } -int PlusFatFake_FflushCallCount(void) +int PlusFatFake_FlushCacheCallCount(void) { - return fflushCallCount; + return flushCacheCallCount; } -int ff_fflush(FF_FILE* pxStream) +FF_Error_t FF_FlushCache(FF_IOManager_t* pxIOManager) { - (void) pxStream; - fflushCallCount++; - return fflushResult; + (void) pxIOManager; + flushCacheCallCount++; + return flushCacheResult; } int PlusFatFake_SeekCallCount(void) From d92144229ffa99482d479035386bedfe50e3363a Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 4 Jun 2026 11:25:28 +0100 Subject: [PATCH 5/7] docs: S29.05 integrating-plusfat guide + DEVLOG; clang-format reflow - docs/integrating-plusfat.md: integrator guide (FF_Disk_t media-driver contract, FreeRTOSFATConfig.h + kernel-config requirements, absolute-path convention, FF_FlushCache durability + dirent-on-close nuance, heap-is-vendor-scoped note, the BDD target as reference integration). - DEVLOG: S29.05 session entry (prepended). - clang-format reflow of the two new seam headers' pointer alignment. misra_suppressions.txt left untouched: misra_renumber proposed no changes; the AMBIGUOUS pairs it reported are pre-existing cross-run cppcheck variance in Core headers this story never touched (per the cppcheck-misra-invariant note). --- Bdd/Targets/Common/BddTargetFatFsMount.h | 2 +- Bdd/Targets/Common/BddTargetPlusFatMount.h | 2 +- DEVLOG.md | 59 ++++++++++ docs/integrating-plusfat.md | 128 +++++++++++++++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 docs/integrating-plusfat.md diff --git a/Bdd/Targets/Common/BddTargetFatFsMount.h b/Bdd/Targets/Common/BddTargetFatFsMount.h index 173ea936..cba4e292 100644 --- a/Bdd/Targets/Common/BddTargetFatFsMount.h +++ b/Bdd/Targets/Common/BddTargetFatFsMount.h @@ -26,7 +26,7 @@ EXTERN_C_BEGIN /* Create / destroy the ChaN-FatFs SolidSyslogFile adapter (forward-declared * to keep ff.h / SolidSyslogFatFsFile.h out of the seam's public surface). */ struct SolidSyslogFile* BddTargetFatFsMount_CreateFile(void); - void BddTargetFatFsMount_DestroyFile(struct SolidSyslogFile* file); + void BddTargetFatFsMount_DestroyFile(struct SolidSyslogFile * file); EXTERN_C_END diff --git a/Bdd/Targets/Common/BddTargetPlusFatMount.h b/Bdd/Targets/Common/BddTargetPlusFatMount.h index 0d83839e..5db00fc5 100644 --- a/Bdd/Targets/Common/BddTargetPlusFatMount.h +++ b/Bdd/Targets/Common/BddTargetPlusFatMount.h @@ -27,7 +27,7 @@ EXTERN_C_BEGIN * keep ff_stdio.h / SolidSyslogPlusFatFile.h out of the seam's public * surface). */ struct SolidSyslogFile* BddTargetPlusFatMount_CreateFile(void); - void BddTargetPlusFatMount_DestroyFile(struct SolidSyslogFile* file); + void BddTargetPlusFatMount_DestroyFile(struct SolidSyslogFile * file); EXTERN_C_END diff --git a/DEVLOG.md b/DEVLOG.md index d68c10bd..c380ac29 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,64 @@ # Dev Log +## 2026-06-04 — S29.05 PlusFAT media driver + Plus-TCP target swap + +The last functional story of E29: a FreeRTOS-Plus-FAT `FF_Disk_t` semihosting +media driver, the Plus-TCP BDD target swapped from FatFs to PlusFAT, and the full +`@store` suite green on QEMU over PlusFAT — proving the `SolidSyslogFile` seam is +FS-vendor-portable (FatFs on the lwIP target, PlusFAT on the Plus-TCP target) the +way E28 proved the transport seam. + +### Decisions + +- **FS-mount seam, introduced now.** S29.03 extracted only the *network* backend + behind the pipeline config — both targets shared FatFs, so no FS seam was + needed (YAGNI). S29.05 is the first divergence, so the pipeline gained four + function pointers (`MountStore` / `UnmountStore` / `CreateStoreFile` / + `DestroyStoreFile`); the FatFs logic moved to `BddTargetFatFsMount`, the new + PlusFAT logic to `BddTargetPlusFatMount`. The pipeline no longer includes + `ff.h` / `SolidSyslogFatFsFile.h` and is FS-vendor-agnostic. +- **DRY'd the semihosting trap.** The BKPT-0xAB host-file I/O and the 8 MiB FAT16 + geometry moved from `diskio.c` into a shared `SemihostingDisk` TU, consumed by + both the ChaN `diskio.c` glue and the new `FFSemihostingDisk` FF_Disk_t driver — + so a FatFs run and a PlusFAT run hit byte-for-byte the same host image. +- **`FFSemihostingDisk` is mount-or-format-on-first-use**, not format-always + (unlike Plus-FAT's volatile `ff_ramdisk.c` template): it tries `FF_Mount` first + so a power cycle keeps its data, and only partitions + formats a fresh image. + FAT16 forced via `FF_Format(..., xPreferFAT16 = pdTRUE)` on the 8 MiB geometry. +- **`ff_fflush` is a phantom — fixed the adapter (approved scope expansion).** + The S29.04 adapter called `ff_fflush` for write durability; FreeRTOS-Plus-FAT + *declares* it in `ff_stdio.h` but never *defines* it (SHA `8d38036`). + `PlusFatFake` defined it, so S29.04's host tests passed while the real Plus-TCP + target failed to link — the host-fake masking a real-library gap. With David's + go-ahead the adapter now flushes via `FF_FlushCache(Fp->pxIOManager)` (the real + cache-flush primitive; the dirent size is still committed on Close); the fake + and the three durability tests were updated to match. This expanded S29.05 past + its "adapter untouched" AC by design. +- **Absolute store paths.** Plus-FAT `ff_stdio` only accepts absolute paths at + `ffconfigHAS_CWD = 0`, so the shared store prefix became `/STORE` (valid for + ChaN too — the lwIP FatFs target stayed green). +- **Warning scoping.** Plus-FAT core sources sit in the relaxed upstream OBJECT + lib (+ `-Wno-cpp` for `ff_fat.c`'s one-FAT-table `#warning`); `FFSemihostingDisk.c` + stays in the strict executable with only `-Wno-sign-conversion` (Plus-FAT's + `FF_ERR_*` carry the 0x80000000 flag bit and can't be sign-clean at the call + site). The adapter needed no relaxation. + +### Validation + +- Host `SolidSyslogPlusFatFileTest`: 30/30 green with the `FF_FlushCache` swap. +- Both FreeRTOS cross targets build; Plus-TCP ELF links — `nm` confirms + `FF_FlushCache` present, no `ff_fflush`, no FatFs symbols. +- Plus-TCP `@store` BDD on QEMU: 5 features / 12 scenarios passed, 0 failed + (`store_and_forward`, `store_capacity`, `power_cycle_replay`, `capacity_threshold`, + `block_lifecycle`). `power_cycle_replay` passing confirms the + flush-data + commit-size-on-close durability is sound across a graceful restart. + +### Open questions + +- Hard-power-cut durability (vs the graceful BDD restart) is best-effort: the + dirent size lands on Close, so a crash mid-record could leave a stale size. + Documented in `docs/integrating-plusfat.md`; revisit only if a target needs it. + ## 2026-06-03 — S29.04 Platform/PlusFat adapter pack (host TDD) The FreeRTOS-Plus-FAT `SolidSyslogFile` adapter, mirroring the FatFs sibling but diff --git a/docs/integrating-plusfat.md b/docs/integrating-plusfat.md new file mode 100644 index 00000000..c2c33c5b --- /dev/null +++ b/docs/integrating-plusfat.md @@ -0,0 +1,128 @@ +# Integrating SolidSyslog with FreeRTOS-Plus-FAT + +`SolidSyslogPlusFatFile` is the `SolidSyslogFile` adapter backed by +[FreeRTOS-Plus-FAT](https://www.freertos.org/Documentation/03-Libraries/05-FreeRTOS-labs/04-FreeRTOS-plus-FAT/00-FreeRTOS-Plus-FAT) +(the `ff_stdio` API). It is the FreeRTOS-Plus ecosystem counterpart to the +OS-agnostic [ChaN FatFs adapter](../Platform/FatFs/) — pair it with +FreeRTOS-Plus-TCP for a coherent all-FreeRTOS-Plus storage + transport stack. It +gives the store-and-forward layer (`SolidSyslogBlockStore` over +`SolidSyslogFileBlockDevice`) a real on-flash file backend. + +This guide covers what you must supply around the adapter. For the file seam +itself see [`SolidSyslogFile.h`](../Core/Interface/SolidSyslogFile.h); for the +store see [`SolidSyslogBlockStore.h`](../Core/Interface/SolidSyslogBlockStore.h). + +## The shape + +``` +SolidSyslogBlockStore + │ (SolidSyslogStore vtable) +SolidSyslogFileBlockDevice <-- sequence-numbered .log files + │ (SolidSyslogFile vtable) +SolidSyslogPlusFatFile <-- this adapter: ff_fopen / ff_fread / ff_fwrite / … + │ (ff_stdio API) +FreeRTOS-Plus-FAT core (ff_*.c) <-- vendor library, you compile it + │ (FF_Disk_t block callbacks) +your FF_Disk_t media driver <-- YOU write this: SD / eMMC / QSPI-flash / RAM +``` + +The adapter owns only the middle box. The vendor core and the media driver are +yours to provide; the library never reaches the block device directly. + +## What you must provide + +1. **The FreeRTOS-Plus-FAT sources**, compiled into your image (`ff_crc.c`, + `ff_dir.c`, `ff_error.c`, `ff_fat.c`, `ff_file.c`, `ff_format.c`, `ff_ioman.c`, + `ff_locking.c`, `ff_memory.c`, `ff_stdio.c`, `ff_string.c`, `ff_sys.c`, + `ff_time.c`). These are not -Wsign-conversion / -Wconversion clean; compile + them under a relaxed warning set, as you would the FreeRTOS kernel. + +2. **An `FF_Disk_t` media driver** for your storage hardware — `FF_CreateIOManager` + with read/write block callbacks, `FF_Mount` (format-on-first-use via + `FF_Partition` + `FF_Format` when no FAT is present), and `FF_FS_Add("/", disk)` + to register the volume in `ff_stdio`'s virtual file system. Plus-FAT ships + reference drivers under `portable/` (`ff_ramdisk.c` is the clearest template). + The library's BDD target ships a semihosting example — + [`Bdd/Targets/Common/FFSemihostingDisk.c`](../Bdd/Targets/Common/FFSemihostingDisk.c). + +3. **A `FreeRTOSFATConfig.h`** on your include path. `ff_headers.h` pulls it via + `#include "FreeRTOSFATConfig.h"`; unlike ChaN FatFs's `ffconf.h`, it resolves + off the `-I` path (no source-tree colocation needed). At minimum set + `ffconfigBYTE_ORDER` and `ffconfigCWD_THREAD_LOCAL_INDEX`; + `FreeRTOSFATConfigDefaults.h` fills the rest. See + [`Bdd/Targets/FreeRtos/FreeRTOSFATConfig.h`](../Bdd/Targets/FreeRtos/FreeRTOSFATConfig.h). + +4. **Kernel configuration** in `FreeRTOSConfig.h`: + - `configUSE_RECURSIVE_MUTEXES = 1` (Plus-FAT's `ff_locking.c` enforces this). + - Event groups compiled in (`event_groups.c`) — the IO manager uses them. + - `configNUM_THREAD_LOCAL_STORAGE_POINTERS >= ffconfigCWD_THREAD_LOCAL_INDEX + 3`. + `ff_stdio` stores its `errno`, CWD, and `FF_Error` in per-task thread-local + storage at offsets `ffconfigCWD_THREAD_LOCAL_INDEX + {0,1,2}`. With the index + at 0 that means **at least 3** slots — `ff_stdio.h` enforces this at compile + time. + - `configSUPPORT_STATIC_ALLOCATION = 1` if your media driver creates its IO + manager mutex statically (the example does); dynamic allocation otherwise. + +## Path convention — absolute paths + +With `ffconfigHAS_CWD = 0` (the default), Plus-FAT's `ff_stdio` accepts **only +absolute paths** — its relative-path resolver (`prvABSPath`) is a pass-through. +Configure `SolidSyslogFileBlockDevice` with an **absolute** path prefix, e.g. +`/STORE`, so the store files land at the volume root as `/STORE00.log`, +`/STORE01.log`, … (A leading `/` is equally valid for the ChaN FatFs adapter, so +the same prefix works for either backend.) The default 8.3 short-filename mode +(`ffconfigLFN_SUPPORT = 0`) is sufficient for that naming. + +## Durability contract + +`SolidSyslogPlusFatFile_Write` flushes after **every complete write** so a power +loss never loses a record the `BlockStore` was told had been stored. Two notes +specific to Plus-FAT: + +- **There is no per-file flush.** `ff_stdio.h` *declares* `ff_fflush`, but the + library (as of SHA `8d38036`) never *defines* it. The adapter instead calls + `FF_FlushCache(file->pxIOManager)`, the IO-manager cache flush, which is the + real durability primitive. +- **The directory entry (file size) is committed on `Close`**, not on each + flush. `FF_FlushCache` persists the file's *data* sectors; the dirent's size + field is written by `ff_fclose`. A graceful shutdown (the library's + `SolidSyslog` teardown closes the store file) therefore leaves both data and + metadata consistent. If your platform must survive a hard power cut + mid-record, size the records and the discard policy with that in mind. + +## Heap usage + +The adapter struct itself is pool-allocated (`SOLIDSYSLOG_FILE_POOL_SIZE`) and +never calls `malloc`. FreeRTOS-Plus-FAT, however, **does** allocate — the IO +manager sector cache and internal buffers come from the FreeRTOS heap (`heap_4` +or your `pvPortMalloc`). This is integrator-scoped and expected, exactly the +mbedTLS precedent: the vendor library allocates; SolidSyslog's own structures do +not. Size your heap for the IO-manager cache you request in `FF_CreateIOManager`. + +## Reference integration + +[`Bdd/Targets/FreeRtos/`](../Bdd/Targets/FreeRtos/) is the worked example — the +FreeRTOS-Plus-TCP + FreeRTOS-Plus-FAT QEMU BDD target. It wires: + +- [`FFSemihostingDisk.c`](../Bdd/Targets/Common/FFSemihostingDisk.c) — an + `FF_Disk_t` over an ARM-semihosting host-backed flat disk (8 MiB, FAT16), + modelled on Plus-FAT's `ff_ramdisk.c` but persistent (mount-or-format-on-first- + use, so a power cycle keeps its data). +- [`BddTargetPlusFatMount.c`](../Bdd/Targets/Common/BddTargetPlusFatMount.c) — + the mount/unmount + `SolidSyslogPlusFatFile` create/destroy wired into the + shared FreeRTOS pipeline's FS-mount seam. +- [`FreeRTOSFATConfig.h`](../Bdd/Targets/FreeRtos/FreeRTOSFATConfig.h) and the + `configNUM_THREAD_LOCAL_STORAGE_POINTERS` knob in + [`FreeRTOSConfig.h`](../Bdd/Targets/FreeRtos/FreeRTOSConfig.h). + +The full store / capacity / power-cycle-replay BDD suite runs against this target +on QEMU (`bdd-freertos-qemu-plustcp`). + +## What this adapter does not own + +- **The media driver.** SD/eMMC/flash/RAM block I/O is yours (or a Plus-FAT + `portable/` driver). The adapter speaks only `ff_stdio`. +- **Mounting and formatting.** The adapter opens/reads/writes files; bringing the + volume up (`FF_Mount` / `FF_Format` / `FF_FS_Add`) is the media driver's job. +- **The FreeRTOS-Plus-FAT sources and their licence.** You vendor and compile + them. From 4e3d8f8efe2c2f48ca3d3c5b40a43cbb248a640e Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 4 Jun 2026 12:19:11 +0100 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20S29.05=20address=20PR=20review?= =?UTF-8?q?=20=E2=80=94=20intent-named=20predicates=20+=20ordering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit + review feedback on the S29.05 sources (no behaviour change): - SemihostingDisk.c: overflow-safe SemihostingDisk_IsRangeValid (subtraction form, no sector+count wrap), shared by Read/Write; reorder the Semihosting* primitives beneath their first callers (top-down reading). - FFSemihostingDisk.c: extract the duplicated disk-validation check into FFSemihostingDisk_IsValidDisk. - diskio.c: extract the readiness check into Diskio_IsDriveReady. - SolidSyslogPlusFatFile.c: reorder so Close sits beneath its first caller (Cleanup); add PlusFatFile_IsFileOpen used at every open-state site; extract the write durability flush into PlusFatFile_Flush (honest action verb; parenthesise the wroteAll equality). - docs/integrating-plusfat.md: label the architecture fence 'text' (MD040). Verified: clang-format clean tree-wide; host SolidSyslogPlusFatFileTest 30/30; both FreeRTOS cross targets build. --- Bdd/Targets/Common/FFSemihostingDisk.c | 12 +- Bdd/Targets/Common/SemihostingDisk.c | 160 ++++++++++-------- Bdd/Targets/Common/diskio.c | 14 +- .../PlusFat/Source/SolidSyslogPlusFatFile.c | 43 +++-- docs/integrating-plusfat.md | 2 +- 5 files changed, 137 insertions(+), 94 deletions(-) diff --git a/Bdd/Targets/Common/FFSemihostingDisk.c b/Bdd/Targets/Common/FFSemihostingDisk.c index a263cb34..fc6724e2 100644 --- a/Bdd/Targets/Common/FFSemihostingDisk.c +++ b/Bdd/Targets/Common/FFSemihostingDisk.c @@ -46,6 +46,7 @@ static StaticSemaphore_t g_mutexStorage; static int32_t ReadBlocks(uint8_t* buffer, uint32_t sector, uint32_t count, FF_Disk_t* disk); static int32_t WriteBlocks(uint8_t* buffer, uint32_t sector, uint32_t count, FF_Disk_t* disk); static FF_Error_t PartitionAndFormat(void); +static bool FFSemihostingDisk_IsValidDisk(const FF_Disk_t* disk); bool FFSemihostingDisk_Mount(void) { @@ -145,7 +146,7 @@ static FF_Error_t PartitionAndFormat(void) static int32_t ReadBlocks(uint8_t* buffer, uint32_t sector, uint32_t count, FF_Disk_t* disk) { int32_t result; - if ((disk == NULL) || (disk->ulSignature != DISK_SIGNATURE) || (disk->xStatus.bIsInitialised == pdFALSE)) + if (!FFSemihostingDisk_IsValidDisk(disk)) { result = FF_ERR_IOMAN_DRIVER_FATAL_ERROR | FF_ERRFLAG; } @@ -167,10 +168,17 @@ static int32_t ReadBlocks(uint8_t* buffer, uint32_t sector, uint32_t count, FF_D return result; } +/* Guards the block callbacks: a disk handed to us must be non-NULL, carry our + * signature, and have been through FF_CreateIOManager. */ +static bool FFSemihostingDisk_IsValidDisk(const FF_Disk_t* disk) +{ + return (disk != NULL) && (disk->ulSignature == DISK_SIGNATURE) && (disk->xStatus.bIsInitialised != pdFALSE); +} + static int32_t WriteBlocks(uint8_t* buffer, uint32_t sector, uint32_t count, FF_Disk_t* disk) { int32_t result; - if ((disk == NULL) || (disk->ulSignature != DISK_SIGNATURE) || (disk->xStatus.bIsInitialised == pdFALSE)) + if (!FFSemihostingDisk_IsValidDisk(disk)) { result = FF_ERR_IOMAN_DRIVER_FATAL_ERROR | FF_ERRFLAG; } diff --git a/Bdd/Targets/Common/SemihostingDisk.c b/Bdd/Targets/Common/SemihostingDisk.c index 3f7f31e5..7ecc6e72 100644 --- a/Bdd/Targets/Common/SemihostingDisk.c +++ b/Bdd/Targets/Common/SemihostingDisk.c @@ -44,6 +44,7 @@ static int SemihostingWrite(int handle, const void* buffer, int count); static int SemihostingSeek(int handle, int position); static int SemihostingFlen(int handle); static int SemihostingClose(int handle); +static bool SemihostingDisk_IsRangeValid(uint32_t sector, uint32_t count); bool SemihostingDisk_EnsureReady(void) { @@ -87,55 +88,16 @@ bool SemihostingDisk_EnsureReady(void) return true; } -bool SemihostingDisk_IsReady(void) -{ - return g_diskHandle >= 0; -} - -enum SemihostingDiskResult SemihostingDisk_Read(void* buffer, uint32_t sector, uint32_t count) +static int SemihostingOpen(const char* path, int mode) { - enum SemihostingDiskResult result = SEMIHOSTING_DISK_OK; - if ((sector + count) > (uint32_t) SEMIHOSTING_DISK_SECTOR_COUNT) - { - result = SEMIHOSTING_DISK_OUT_OF_RANGE; - } - else + const struct { - int position = (int) sector * (int) SEMIHOSTING_DISK_SECTOR_SIZE; - int bytes = (int) count * (int) SEMIHOSTING_DISK_SECTOR_SIZE; - if (SemihostingSeek(g_diskHandle, position) != 0) - { - result = SEMIHOSTING_DISK_IO_ERROR; - } - else if (SemihostingRead(g_diskHandle, buffer, bytes) != 0) - { - result = SEMIHOSTING_DISK_IO_ERROR; - } - } - return result; -} + const char* name; + int mode; + int length; + } args = {path, mode, (int) strlen(path)}; -enum SemihostingDiskResult SemihostingDisk_Write(const void* buffer, uint32_t sector, uint32_t count) -{ - enum SemihostingDiskResult result = SEMIHOSTING_DISK_OK; - if ((sector + count) > (uint32_t) SEMIHOSTING_DISK_SECTOR_COUNT) - { - result = SEMIHOSTING_DISK_OUT_OF_RANGE; - } - else - { - int position = (int) sector * (int) SEMIHOSTING_DISK_SECTOR_SIZE; - int bytes = (int) count * (int) SEMIHOSTING_DISK_SECTOR_SIZE; - if (SemihostingSeek(g_diskHandle, position) != 0) - { - result = SEMIHOSTING_DISK_IO_ERROR; - } - else if (SemihostingWrite(g_diskHandle, buffer, bytes) != 0) - { - result = SEMIHOSTING_DISK_IO_ERROR; - } - } - return result; + return Semihosting(SEMIHOSTING_SYS_OPEN, &args); } static int Semihosting(int op, const void* args) @@ -150,29 +112,37 @@ static int Semihosting(int op, const void* args) return result; } -static int SemihostingOpen(const char* path, int mode) +static int SemihostingFlen(int handle) { + /* SYS_FLEN returns the file length in bytes, -1 on error. */ const struct { - const char* name; - int mode; - int length; - } args = {path, mode, (int) strlen(path)}; + int handle; + } args = {handle}; - return Semihosting(SEMIHOSTING_SYS_OPEN, &args); + return Semihosting(SEMIHOSTING_SYS_FLEN, &args); } -static int SemihostingRead(int handle, void* buffer, int count) +static int SemihostingClose(int handle) { - /* SYS_READ returns the number of bytes NOT read (0 == full read). */ const struct { int handle; - void* buffer; - int count; - } args = {handle, buffer, count}; + } args = {handle}; - return Semihosting(SEMIHOSTING_SYS_READ, &args); + return Semihosting(SEMIHOSTING_SYS_CLOSE, &args); +} + +static int SemihostingSeek(int handle, int position) +{ + /* SYS_SEEK returns 0 on success, a negative value on error. */ + const struct + { + int handle; + int position; + } args = {handle, position}; + + return Semihosting(SEMIHOSTING_SYS_SEEK, &args); } static int SemihostingWrite(int handle, const void* buffer, int count) @@ -188,35 +158,75 @@ static int SemihostingWrite(int handle, const void* buffer, int count) return Semihosting(SEMIHOSTING_SYS_WRITE, &args); } -static int SemihostingSeek(int handle, int position) +bool SemihostingDisk_IsReady(void) { - /* SYS_SEEK returns 0 on success, a negative value on error. */ - const struct + return g_diskHandle >= 0; +} + +enum SemihostingDiskResult SemihostingDisk_Read(void* buffer, uint32_t sector, uint32_t count) +{ + enum SemihostingDiskResult result = SEMIHOSTING_DISK_OK; + if (!SemihostingDisk_IsRangeValid(sector, count)) { - int handle; - int position; - } args = {handle, position}; + result = SEMIHOSTING_DISK_OUT_OF_RANGE; + } + else + { + int position = (int) sector * (int) SEMIHOSTING_DISK_SECTOR_SIZE; + int bytes = (int) count * (int) SEMIHOSTING_DISK_SECTOR_SIZE; + if (SemihostingSeek(g_diskHandle, position) != 0) + { + result = SEMIHOSTING_DISK_IO_ERROR; + } + else if (SemihostingRead(g_diskHandle, buffer, bytes) != 0) + { + result = SEMIHOSTING_DISK_IO_ERROR; + } + } + return result; +} - return Semihosting(SEMIHOSTING_SYS_SEEK, &args); +/* Range-checks an LBA span without overflow: the subtraction form keeps the sum + * `sector + count` from wrapping in uint32 (an arithmetic add could wrap a huge + * sector past the comparison and admit an out-of-bounds seek). */ +static bool SemihostingDisk_IsRangeValid(uint32_t sector, uint32_t count) +{ + return (count <= (uint32_t) SEMIHOSTING_DISK_SECTOR_COUNT) && + (sector <= ((uint32_t) SEMIHOSTING_DISK_SECTOR_COUNT - count)); } -static int SemihostingFlen(int handle) +static int SemihostingRead(int handle, void* buffer, int count) { - /* SYS_FLEN returns the file length in bytes, -1 on error. */ + /* SYS_READ returns the number of bytes NOT read (0 == full read). */ const struct { int handle; - } args = {handle}; + void* buffer; + int count; + } args = {handle, buffer, count}; - return Semihosting(SEMIHOSTING_SYS_FLEN, &args); + return Semihosting(SEMIHOSTING_SYS_READ, &args); } -static int SemihostingClose(int handle) +enum SemihostingDiskResult SemihostingDisk_Write(const void* buffer, uint32_t sector, uint32_t count) { - const struct + enum SemihostingDiskResult result = SEMIHOSTING_DISK_OK; + if (!SemihostingDisk_IsRangeValid(sector, count)) { - int handle; - } args = {handle}; - - return Semihosting(SEMIHOSTING_SYS_CLOSE, &args); + result = SEMIHOSTING_DISK_OUT_OF_RANGE; + } + else + { + int position = (int) sector * (int) SEMIHOSTING_DISK_SECTOR_SIZE; + int bytes = (int) count * (int) SEMIHOSTING_DISK_SECTOR_SIZE; + if (SemihostingSeek(g_diskHandle, position) != 0) + { + result = SEMIHOSTING_DISK_IO_ERROR; + } + else if (SemihostingWrite(g_diskHandle, buffer, bytes) != 0) + { + result = SEMIHOSTING_DISK_IO_ERROR; + } + } + return result; } diff --git a/Bdd/Targets/Common/diskio.c b/Bdd/Targets/Common/diskio.c index a560268e..7f6b6678 100644 --- a/Bdd/Targets/Common/diskio.c +++ b/Bdd/Targets/Common/diskio.c @@ -17,6 +17,10 @@ #include "SemihostingDisk.h" +#include + +static bool Diskio_IsDriveReady(BYTE pdrv); + DSTATUS disk_initialize(BYTE pdrv) { if (pdrv != 0) @@ -38,7 +42,7 @@ DSTATUS disk_status(BYTE pdrv) DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) { DRESULT result; - if ((pdrv != 0) || !SemihostingDisk_IsReady()) + if (!Diskio_IsDriveReady(pdrv)) { result = RES_NOTRDY; } @@ -60,10 +64,16 @@ DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) return result; } +/* Single logical drive 0 must be present and the backing image opened. */ +static bool Diskio_IsDriveReady(BYTE pdrv) +{ + return (pdrv == 0) && SemihostingDisk_IsReady(); +} + DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count) { DRESULT result; - if ((pdrv != 0) || !SemihostingDisk_IsReady()) + if (!Diskio_IsDriveReady(pdrv)) { result = RES_NOTRDY; } diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c index c408f780..e7f43cc7 100644 --- a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c @@ -23,6 +23,8 @@ static bool PlusFatFile_Exists(struct SolidSyslogFile* base, const char* path); static bool PlusFatFile_Delete(struct SolidSyslogFile* base, const char* path); static inline struct SolidSyslogPlusFatFile* PlusFatFile_SelfFromBase(struct SolidSyslogFile* base); +static inline bool PlusFatFile_IsFileOpen(const struct SolidSyslogPlusFatFile* self); +static inline bool PlusFatFile_Flush(struct SolidSyslogPlusFatFile* self); void PlusFatFile_Initialise(struct SolidSyslogFile* base) { @@ -53,33 +55,40 @@ void PlusFatFile_Cleanup(struct SolidSyslogFile* base) *base = *SolidSyslogNullFile_Get(); } -static bool PlusFatFile_Open(struct SolidSyslogFile* base, const char* path) +static void PlusFatFile_Close(struct SolidSyslogFile* base) { struct SolidSyslogPlusFatFile* self = PlusFatFile_SelfFromBase(base); - /* "r+" opens an existing file without truncating; if it does not exist, - * "w+" creates it. Together this is open-or-create with no data loss — - * Plus-FAT has no single open-always mode. */ - self->Fp = ff_fopen(path, "r+"); - if (self->Fp == NULL) + if (PlusFatFile_IsFileOpen(self)) { - self->Fp = ff_fopen(path, "w+"); + ff_fclose(self->Fp); + self->Fp = NULL; } +} + +/* Open-state is carried by the FF_FILE* sentinel: non-NULL once a file is open, + * NULL when closed. The single source of truth for every open-state check. */ +static inline bool PlusFatFile_IsFileOpen(const struct SolidSyslogPlusFatFile* self) +{ return self->Fp != NULL; } -static void PlusFatFile_Close(struct SolidSyslogFile* base) +static bool PlusFatFile_Open(struct SolidSyslogFile* base, const char* path) { struct SolidSyslogPlusFatFile* self = PlusFatFile_SelfFromBase(base); - if (self->Fp != NULL) + /* "r+" opens an existing file without truncating; if it does not exist, + * "w+" creates it. Together this is open-or-create with no data loss — + * Plus-FAT has no single open-always mode. */ + self->Fp = ff_fopen(path, "r+"); + if (!PlusFatFile_IsFileOpen(self)) { - ff_fclose(self->Fp); - self->Fp = NULL; + self->Fp = ff_fopen(path, "w+"); } + return PlusFatFile_IsFileOpen(self); } static bool PlusFatFile_IsOpen(struct SolidSyslogFile* base) { - return PlusFatFile_SelfFromBase(base)->Fp != NULL; + return PlusFatFile_IsFileOpen(PlusFatFile_SelfFromBase(base)); } static bool PlusFatFile_Read(struct SolidSyslogFile* base, void* buf, size_t count) @@ -97,8 +106,14 @@ static bool PlusFatFile_Write(struct SolidSyslogFile* base, const void* buf, siz * has no per-file flush — ff_stdio.h declares ff_fflush but the library * never defines it; FF_FlushCache against the file's IO manager is the real * durability primitive. The directory entry's size is committed on Close. */ - bool wroteAll = ff_fwrite(buf, 1, count, self->Fp) == count; - return wroteAll && (FF_FlushCache(self->Fp->pxIOManager) == FF_ERR_NONE); + bool wroteAll = (ff_fwrite(buf, 1, count, self->Fp) == count); + return wroteAll && PlusFatFile_Flush(self); +} + +/* Flush the IO-manager cache to the media; returns whether it succeeded. */ +static inline bool PlusFatFile_Flush(struct SolidSyslogPlusFatFile* self) +{ + return FF_FlushCache(self->Fp->pxIOManager) == FF_ERR_NONE; } static void PlusFatFile_SeekTo(struct SolidSyslogFile* base, size_t offset) diff --git a/docs/integrating-plusfat.md b/docs/integrating-plusfat.md index c2c33c5b..1a7c31d9 100644 --- a/docs/integrating-plusfat.md +++ b/docs/integrating-plusfat.md @@ -14,7 +14,7 @@ store see [`SolidSyslogBlockStore.h`](../Core/Interface/SolidSyslogBlockStore.h) ## The shape -``` +```text SolidSyslogBlockStore │ (SolidSyslogStore vtable) SolidSyslogFileBlockDevice <-- sequence-numbered .log files From 31dcbb851ebfb5d1cadd0e4538884ece8ba02e25 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 4 Jun 2026 12:50:31 +0100 Subject: [PATCH 7/7] fix: S29.05 renumber PlusFatFile 11.3 suppression after reorder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The intent-named-predicate reorder shifted SolidSyslogPlusFatFile.c's SelfFromBase cast from line 45 to 47, leaving the misra-c2012-11.3 suppression stale and the (deviated) cast unsuppressed — failing analyze-cppcheck. Point the suppression at the new line. cppcheck-misra clean on Platform/PlusFat again. --- misra_suppressions.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misra_suppressions.txt b/misra_suppressions.txt index 8a00fcd3..b55acc09 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -37,7 +37,7 @@ misra-c2012-11.3:Core/Source/SolidSyslogTimeQualitySd.c:63 misra-c2012-11.3:Core/Source/SolidSyslogUdpSender.c:100 misra-c2012-11.3:Platform/Atomics/Source/SolidSyslogStdAtomicCounter.c:29 misra-c2012-11.3:Platform/FatFs/Source/SolidSyslogFatFsFile.c:49 -misra-c2012-11.3:Platform/PlusFat/Source/SolidSyslogPlusFatFile.c:45 +misra-c2012-11.3:Platform/PlusFat/Source/SolidSyslogPlusFatFile.c:47 misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawAddressPrivate.h:29 misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawAddressPrivate.h:36 misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawAddressStatic.c:35