From 3ee7ae1097f32c5ecfb51923bb52d4afee41adae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Mon, 5 Oct 2026 22:51:59 +0200 Subject: [PATCH 1/4] lib/SD: Arduino-ESP32 3.3.12's SD library, as it comes A project library named SD takes the framework's place at link time. Unchanged here (Apache-2.0), so that the next commit shows exactly what roro9stack changes in it, and a later framework update can be compared. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT --- lib/SD/library.json | 7 + lib/SD/src/SD.cpp | 134 ++++++ lib/SD/src/SD.h | 55 +++ lib/SD/src/sd_defines.h | 25 ++ lib/SD/src/sd_diskio.cpp | 891 +++++++++++++++++++++++++++++++++++++ lib/SD/src/sd_diskio.h | 34 ++ lib/SD/src/sd_diskio_crc.c | 61 +++ 7 files changed, 1207 insertions(+) create mode 100644 lib/SD/library.json create mode 100644 lib/SD/src/SD.cpp create mode 100644 lib/SD/src/SD.h create mode 100644 lib/SD/src/sd_defines.h create mode 100644 lib/SD/src/sd_diskio.cpp create mode 100644 lib/SD/src/sd_diskio.h create mode 100644 lib/SD/src/sd_diskio_crc.c diff --git a/lib/SD/library.json b/lib/SD/library.json new file mode 100644 index 0000000..ea331b2 --- /dev/null +++ b/lib/SD/library.json @@ -0,0 +1,7 @@ +{ + "name": "SD", + "version": "3.3.12", + "description": "roro9stack's copy of Arduino-ESP32's SD library (Apache-2.0), shadowing the framework's. Only sd_diskio.cpp is changed (marked \"roro:\"): it records why a write failed and resends a block the card rejects. See issue #21.", + "frameworks": "arduino", + "platforms": "espressif32" +} diff --git a/lib/SD/src/SD.cpp b/lib/SD/src/SD.cpp new file mode 100644 index 0000000..077a7c1 --- /dev/null +++ b/lib/SD/src/SD.cpp @@ -0,0 +1,134 @@ +// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "vfs_api.h" +#include "sd_diskio.h" +#include "ff.h" +#include "FS.h" +#include "SD.h" + +using namespace fs; + +SDFS::SDFS(FSImplPtr impl) : FS(impl), _pdrv(0xFF) {} + +SDFS::~SDFS() { + end(); +} + +bool SDFS::begin(uint8_t ssPin, SPIClass &spi, uint32_t frequency, const char *mountpoint, uint8_t max_files, bool format_if_empty) { + if (_pdrv != 0xFF) { + return true; + } + + if (!spi.begin()) { + return false; + } + + _pdrv = sdcard_init(ssPin, &spi, frequency); + if (_pdrv == 0xFF) { + return false; + } + + if (!sdcard_mount(_pdrv, mountpoint, max_files, format_if_empty)) { + sdcard_unmount(_pdrv); + sdcard_uninit(_pdrv); + _pdrv = 0xFF; + return false; + } + + _impl->mountpoint(mountpoint); + return true; +} + +void SDFS::end() { + if (_pdrv != 0xFF) { + _impl->mountpoint(NULL); + sdcard_unmount(_pdrv); + + sdcard_uninit(_pdrv); + _pdrv = 0xFF; + } +} + +sdcard_type_t SDFS::cardType() { + if (_pdrv == 0xFF) { + return CARD_NONE; + } + return sdcard_type(_pdrv); +} + +uint64_t SDFS::cardSize() { + if (_pdrv == 0xFF) { + return 0; + } + size_t sectors = sdcard_num_sectors(_pdrv); + size_t sectorSize = sdcard_sector_size(_pdrv); + return (uint64_t)sectors * sectorSize; +} + +size_t SDFS::numSectors() { + if (_pdrv == 0xFF) { + return 0; + } + return sdcard_num_sectors(_pdrv); +} + +size_t SDFS::sectorSize() { + if (_pdrv == 0xFF) { + return 0; + } + return sdcard_sector_size(_pdrv); +} + +uint64_t SDFS::totalBytes() { + FATFS *fsinfo; + DWORD fre_clust; + char drv[3] = {(char)(48 + _pdrv), ':', 0}; + if (f_getfree(drv, &fre_clust, &fsinfo) != 0) { + return 0; + } + uint64_t size = ((uint64_t)(fsinfo->csize)) * (fsinfo->n_fatent - 2) +#if _MAX_SS != 512 + * (fsinfo->ssize); +#else + * 512; +#endif + return size; +} + +uint64_t SDFS::usedBytes() { + FATFS *fsinfo; + DWORD fre_clust; + char drv[3] = {(char)(48 + _pdrv), ':', 0}; + if (f_getfree(drv, &fre_clust, &fsinfo) != 0) { + return 0; + } + uint64_t size = ((uint64_t)(fsinfo->csize)) * ((fsinfo->n_fatent - 2) - (fsinfo->free_clst)) +#if _MAX_SS != 512 + * (fsinfo->ssize); +#else + * 512; +#endif + return size; +} + +bool SDFS::readRAW(uint8_t *buffer, uint32_t sector) { + return sd_read_raw(_pdrv, buffer, sector); +} + +bool SDFS::writeRAW(uint8_t *buffer, uint32_t sector) { + return sd_write_raw(_pdrv, buffer, sector); +} + +SDFS SD = SDFS(FSImplPtr(new VFSImpl())); diff --git a/lib/SD/src/SD.h b/lib/SD/src/SD.h new file mode 100644 index 0000000..f4311d2 --- /dev/null +++ b/lib/SD/src/SD.h @@ -0,0 +1,55 @@ +// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#ifndef _SD_H_ +#define _SD_H_ + +#include "FS.h" +#include "SPI.h" +#include "sd_defines.h" + +namespace fs { + +class SDFS : public FS { +protected: + uint8_t _pdrv; + +public: + SDFS(FSImplPtr impl); + ~SDFS(); + bool begin( + uint8_t ssPin = SS, SPIClass &spi = SPI, uint32_t frequency = 4000000, const char *mountpoint = "/sd", uint8_t max_files = 5, bool format_if_empty = false + ); + void end(); + sdcard_type_t cardType(); + uint64_t cardSize(); + size_t numSectors(); + size_t sectorSize(); + uint64_t totalBytes(); + uint64_t usedBytes(); + bool readRAW(uint8_t *buffer, uint32_t sector); + bool writeRAW(uint8_t *buffer, uint32_t sector); +}; + +} // namespace fs + +#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_SD) +extern fs::SDFS SD; +#endif + +using namespace fs; +typedef fs::File SDFile; +typedef fs::SDFS SDFileSystemClass; +#define SDFileSystem SD + +#endif /* _SD_H_ */ diff --git a/lib/SD/src/sd_defines.h b/lib/SD/src/sd_defines.h new file mode 100644 index 0000000..9a6b839 --- /dev/null +++ b/lib/SD/src/sd_defines.h @@ -0,0 +1,25 @@ +// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#ifndef _SD_DEFINES_H_ +#define _SD_DEFINES_H_ + +typedef enum { + CARD_NONE, + CARD_MMC, + CARD_SD, + CARD_SDHC, + CARD_UNKNOWN +} sdcard_type_t; + +#endif /* _SD_DISKIO_H_ */ diff --git a/lib/SD/src/sd_diskio.cpp b/lib/SD/src/sd_diskio.cpp new file mode 100644 index 0000000..538f453 --- /dev/null +++ b/lib/SD/src/sd_diskio.cpp @@ -0,0 +1,891 @@ +// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Disable the automatic pin remapping of the API calls in this file +#define ARDUINO_CORE_BUILD + +#include "Arduino.h" +#include "sd_diskio.h" +#include "esp_system.h" +#include "esp32-hal-periman.h" + +extern "C" { +#include "ff.h" +#include "diskio.h" +#if ESP_IDF_VERSION_MAJOR > 3 +#include "diskio_impl.h" +#endif +//#include "esp_vfs.h" +#include "esp_vfs_fat.h" +char CRC7(const char *data, int length); +unsigned short CRC16(const char *data, int length); +} + +typedef enum { + GO_IDLE_STATE = 0, + SEND_OP_COND = 1, + SEND_CID = 2, + SEND_RELATIVE_ADDR = 3, + SEND_SWITCH_FUNC = 6, + SEND_IF_COND = 8, + SEND_CSD = 9, + STOP_TRANSMISSION = 12, + SEND_STATUS = 13, + SET_BLOCKLEN = 16, + READ_BLOCK_SINGLE = 17, + READ_BLOCK_MULTIPLE = 18, + SEND_NUM_WR_BLOCKS = 22, + SET_WR_BLK_ERASE_COUNT = 23, + WRITE_BLOCK_SINGLE = 24, + WRITE_BLOCK_MULTIPLE = 25, + APP_OP_COND = 41, + APP_CLR_CARD_DETECT = 42, + APP_CMD = 55, + READ_OCR = 58, + CRC_ON_OFF = 59 +} ardu_sdcard_command_t; + +// Align with ESP-IDF sdmmc SPI init (ACMD41 timeout must be >1s per SD spec) +static constexpr uint32_t sd_go_idle_delay_ms = 20; +static constexpr uint32_t sd_op_cond_timeout_ms = 3000; + +typedef struct { + uint8_t ssPin; + SPIClass *spi; + int frequency; + char *base_path; + sdcard_type_t type; + unsigned long sectors; + bool supports_crc; + int status; +} ardu_sdcard_t; + +static ardu_sdcard_t *s_cards[FF_VOLUMES] = {NULL}; + +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_ERROR +const char *fferr2str[] = { + "(0) Succeeded", + "(1) A hard error occurred in the low level disk I/O layer", + "(2) Assertion failed", + "(3) The physical drive cannot work", + "(4) Could not find the file", + "(5) Could not find the path", + "(6) The path name format is invalid", + "(7) Access denied due to prohibited access or directory full", + "(8) Access denied due to prohibited access", + "(9) The file/directory object is invalid", + "(10) The physical drive is write protected", + "(11) The logical drive number is invalid", + "(12) The volume has no work area", + "(13) There is no valid FAT volume", + "(14) The f_mkfs() aborted due to any problem", + "(15) Could not get a grant to access the volume within defined period", + "(16) The operation is rejected according to the file sharing policy", + "(17) LFN working buffer could not be allocated", + "(18) Number of open files > FF_FS_LOCK", + "(19) Given parameter is invalid" +}; +#endif + +/* + * SD SPI + * */ + +bool sdWait(uint8_t pdrv, int timeout) { + char resp; + uint32_t start = millis(); + + do { + resp = s_cards[pdrv]->spi->transfer(0xFF); + } while (resp == 0x00 && (millis() - start) < (unsigned int)timeout); + + if (!resp) { + log_w("Wait Failed"); + } + return (resp > 0x00); +} + +void sdStop(uint8_t pdrv) { + s_cards[pdrv]->spi->write(0xFD); +} + +void sdDeselectCard(uint8_t pdrv) { + ardu_sdcard_t *card = s_cards[pdrv]; + digitalWrite(card->ssPin, HIGH); +} + +bool sdSelectCard(uint8_t pdrv) { + ardu_sdcard_t *card = s_cards[pdrv]; + digitalWrite(card->ssPin, LOW); + bool s = sdWait(pdrv, 500); + if (!s) { + log_e("Select Failed"); + digitalWrite(card->ssPin, HIGH); + return false; + } + return true; +} + +char sdCommand(uint8_t pdrv, char cmd, unsigned int arg, unsigned int *resp) { + char token; + ardu_sdcard_t *card = s_cards[pdrv]; + + for (int f = 0; f < 3; f++) { + if (cmd == SEND_NUM_WR_BLOCKS || cmd == SET_WR_BLK_ERASE_COUNT || cmd == APP_OP_COND || cmd == APP_CLR_CARD_DETECT) { + token = sdCommand(pdrv, APP_CMD, 0, NULL); + sdDeselectCard(pdrv); + if (token > 1) { + break; + } + if (!sdSelectCard(pdrv)) { + token = 0xFF; + break; + } + } + + char cmdPacket[7]; + cmdPacket[0] = cmd | 0x40; + cmdPacket[1] = arg >> 24; + cmdPacket[2] = arg >> 16; + cmdPacket[3] = arg >> 8; + cmdPacket[4] = arg; + if (card->supports_crc || cmd == GO_IDLE_STATE || cmd == SEND_IF_COND) { + cmdPacket[5] = (CRC7(cmdPacket, 5) << 1) | 0x01; + } else { + cmdPacket[5] = 0x01; + } + cmdPacket[6] = 0xFF; + + card->spi->writeBytes((uint8_t *)cmdPacket, (cmd == STOP_TRANSMISSION) ? 7 : 6); + + for (int i = 0; i < 9; i++) { + token = card->spi->transfer(0xFF); + if (!(token & 0x80)) { + break; + } + } + + if (token == 0xFF) { + log_w("no token received"); + sdDeselectCard(pdrv); + delay(100); + sdSelectCard(pdrv); + continue; + } else if (token & 0x08) { + log_w("crc error"); + sdDeselectCard(pdrv); + delay(100); + sdSelectCard(pdrv); + continue; + } else if (token > 1) { + log_w("token error [%u] 0x%x", cmd, token); + break; + } + + if (cmd == SEND_STATUS && resp) { + *resp = card->spi->transfer(0xFF); + } else if ((cmd == SEND_IF_COND || cmd == READ_OCR) && resp) { + *resp = card->spi->transfer32(0xFFFFFFFF); + } + + break; + } + if (token == 0xFF) { + log_e("Card Failed! cmd: 0x%02x", cmd); + card->status = STA_NOINIT; + } + return token; +} + +bool sdReadBytes(uint8_t pdrv, char *buffer, int length) { + char token; + unsigned short crc; + ardu_sdcard_t *card = s_cards[pdrv]; + + uint32_t start = millis(); + do { + token = card->spi->transfer(0xFF); + } while (token == 0xFF && (millis() - start) < 500); + + if (token != 0xFE) { + return false; + } + + card->spi->transferBytes(NULL, (uint8_t *)buffer, length); + crc = card->spi->transfer16(0xFFFF); + return (!card->supports_crc || crc == CRC16(buffer, length)); +} + +char sdWriteBytes(uint8_t pdrv, const char *buffer, char token) { + ardu_sdcard_t *card = s_cards[pdrv]; + unsigned short crc = (card->supports_crc) ? CRC16(buffer, 512) : 0xFFFF; + if (!sdWait(pdrv, 500)) { + return 0; + } + + card->spi->write(token); + card->spi->writeBytes((uint8_t *)buffer, 512); + card->spi->write16(crc); + return (card->spi->transfer(0xFF) & 0x1F); +} + +/* + * SPI SDCARD Communication + * */ + +char sdTransaction(uint8_t pdrv, char cmd, unsigned int arg, unsigned int *resp) { + if (!sdSelectCard(pdrv)) { + return 0xFF; + } + char token = sdCommand(pdrv, cmd, arg, resp); + sdDeselectCard(pdrv); + return token; +} + +bool sdReadSector(uint8_t pdrv, char *buffer, unsigned long long sector) { + for (int f = 0; f < 3; f++) { + if (!sdSelectCard(pdrv)) { + return false; + } + if (!sdCommand(pdrv, READ_BLOCK_SINGLE, (s_cards[pdrv]->type == CARD_SDHC) ? sector : sector << 9, NULL)) { + bool success = sdReadBytes(pdrv, buffer, 512); + sdDeselectCard(pdrv); + if (success) { + return true; + } + } else { + break; + } + } + sdDeselectCard(pdrv); + return false; +} + +bool sdReadSectors(uint8_t pdrv, char *buffer, unsigned long long sector, int count) { + for (int f = 0; f < 3;) { + if (!sdSelectCard(pdrv)) { + return false; + } + + if (!sdCommand(pdrv, READ_BLOCK_MULTIPLE, (s_cards[pdrv]->type == CARD_SDHC) ? sector : sector << 9, NULL)) { + do { + if (!sdReadBytes(pdrv, buffer, 512)) { + f++; + break; + } + + sector++; + buffer += 512; + f = 0; + } while (--count); + + if (sdCommand(pdrv, STOP_TRANSMISSION, 0, NULL)) { + log_e("command failed"); + break; + } + + sdDeselectCard(pdrv); + if (count == 0) { + return true; + } + } else { + break; + } + } + sdDeselectCard(pdrv); + return false; +} + +bool sdWriteSector(uint8_t pdrv, const char *buffer, unsigned long long sector) { + for (int f = 0; f < 3; f++) { + if (!sdSelectCard(pdrv)) { + return false; + } + if (!sdCommand(pdrv, WRITE_BLOCK_SINGLE, (s_cards[pdrv]->type == CARD_SDHC) ? sector : sector << 9, NULL)) { + char token = sdWriteBytes(pdrv, buffer, 0xFE); + sdDeselectCard(pdrv); + + if (token == 0x0A) { + continue; + } else if (token == 0x0C) { + return false; + } + + unsigned int resp; + if (sdTransaction(pdrv, SEND_STATUS, 0, &resp) || resp) { + return false; + } + return true; + } else { + break; + } + } + sdDeselectCard(pdrv); + return false; +} + +bool sdWriteSectors(uint8_t pdrv, const char *buffer, unsigned long long sector, int count) { + char token; + const char *currentBuffer = buffer; + unsigned long long currentSector = sector; + int currentCount = count; + ardu_sdcard_t *card = s_cards[pdrv]; + + for (int f = 0; f < 3;) { + if (card->type != CARD_MMC) { + if (sdTransaction(pdrv, SET_WR_BLK_ERASE_COUNT, currentCount, NULL)) { + return false; + } + } + + if (!sdSelectCard(pdrv)) { + return false; + } + + if (!sdCommand(pdrv, WRITE_BLOCK_MULTIPLE, (card->type == CARD_SDHC) ? currentSector : currentSector << 9, NULL)) { + do { + token = sdWriteBytes(pdrv, currentBuffer, 0xFC); + if (token != 0x05) { + f++; + break; + } + currentBuffer += 512; + f = 0; + } while (--currentCount); + + if (!sdWait(pdrv, 500)) { + break; + } + + if (currentCount == 0) { + sdStop(pdrv); + sdDeselectCard(pdrv); + + unsigned int resp; + if (sdTransaction(pdrv, SEND_STATUS, 0, &resp) || resp) { + return false; + } + return true; + } else { + if (sdCommand(pdrv, STOP_TRANSMISSION, 0, NULL)) { + break; + } + + if (token == 0x0A) { + sdDeselectCard(pdrv); + unsigned int writtenBlocks = 0; + if (card->type != CARD_MMC && sdSelectCard(pdrv)) { + if (!sdCommand(pdrv, SEND_NUM_WR_BLOCKS, 0, NULL)) { + char acmdData[4]; + if (sdReadBytes(pdrv, acmdData, 4)) { + writtenBlocks = acmdData[0] << 24; + writtenBlocks |= acmdData[1] << 16; + writtenBlocks |= acmdData[2] << 8; + writtenBlocks |= acmdData[3]; + } + } + sdDeselectCard(pdrv); + } + currentBuffer = buffer + (writtenBlocks << 9); + currentSector = sector + writtenBlocks; + currentCount = count - writtenBlocks; + continue; + } else { + break; + } + } + } else { + break; + } + } + sdDeselectCard(pdrv); + return false; +} + +unsigned long sdGetSectorsCount(uint8_t pdrv) { + for (int f = 0; f < 3; f++) { + if (!sdSelectCard(pdrv)) { + return 0; + } + + if (!sdCommand(pdrv, SEND_CSD, 0, NULL)) { + char csd[16]; + bool success = sdReadBytes(pdrv, csd, 16); + sdDeselectCard(pdrv); + if (success) { + if ((csd[0] >> 6) == 0x01) { + unsigned long size = (((unsigned long)(csd[7] & 0x3F) << 16) | ((unsigned long)csd[8] << 8) | csd[9]) + 1; + return size << 10; + } + unsigned long size = (((unsigned long)(csd[6] & 0x03) << 10) | ((unsigned long)csd[7] << 2) | ((csd[8] & 0xC0) >> 6)) + 1; + size <<= ((((csd[9] & 0x03) << 1) | ((csd[10] & 0x80) >> 7)) + 2); + size <<= (csd[5] & 0x0F); + return size >> 9; + } + } else { + break; + } + } + + sdDeselectCard(pdrv); + return 0; +} + +namespace { + +struct AcquireSPI { + ardu_sdcard_t *card; + explicit AcquireSPI(ardu_sdcard_t *card) : card(card) { + card->spi->beginTransaction(SPISettings(card->frequency, MSBFIRST, SPI_MODE0)); + } + AcquireSPI(ardu_sdcard_t *card, int frequency) : card(card) { + card->spi->beginTransaction(SPISettings(frequency, MSBFIRST, SPI_MODE0)); + } + ~AcquireSPI() { + card->spi->endTransaction(); + } + +private: + AcquireSPI(AcquireSPI const &); + AcquireSPI &operator=(AcquireSPI const &); +}; + +} // namespace + +/* + * FATFS API + * */ + +/** + * @brief Initialize an SD card for use with FatFs + * + * This function implements the complete SD card initialization sequence according to + * the SD card specification. It performs card detection, type identification, + * and configuration for SPI mode operation. + * + * The initialization sequence follows the SD card protocol (SPI mode, aligned with IDF): + * 1. Power-up sequence with 74+ clock cycles + * 2. Two GO_IDLE_STATE attempts to enter SPI mode + * 3. CRC_ON_OFF to enable CRC checking (with retry) + * 4. SEND_IF_COND to identify SDHC/SDXC cards + * 5. APP_OP_COND / SEND_OP_COND (SPI args; timeout >1s) + * 6. Card type detection (SD/SDHC/MMC) + * 7. Final configuration and sector count retrieval + * + * @param pdrv Physical drive number (0-9) + * @return DSTATUS Status of the initialization (0 = success, STA_NOINIT = failed) + */ +DSTATUS ff_sd_initialize(uint8_t pdrv) { + char token; + unsigned int resp; + unsigned int start; + + // Get the card structure for the given drive number + ardu_sdcard_t *card = s_cards[pdrv]; + + // If the card is already initialized, return its current status + if (!(card->status & STA_NOINIT)) { + return card->status; + } + + // Lock the SPI bus and set it to a low frequency (400kHz) for initialization + // Low frequency is required during initialization for reliable communication + AcquireSPI card_locked(card, 400000); + + // Step 1: Power-up sequence - Send at least 74 clock cycles with CS high and MOSI high + // This is required by the SD card specification to ensure proper card state reset + // We send 20 bytes (160 clock cycles) to exceed the minimum requirement + digitalWrite(card->ssPin, HIGH); + for (uint8_t i = 0; i < 20; i++) { + card->spi->transfer(0XFF); + } + + // Step 2: Perform two GO_IDLE_STATE (CMD0) attempts in SPI mode. + // Per SD Simplified Spec (figure 4-1) / IDF: some cards enter SD mode on the + // first attempt, so the first response may fail; the second must succeed. + // (sdCommand may also retry internally on no-token/CRC errors.) + // Fix mount issue - sdWait fail ignored before each CMD0 attempt + digitalWrite(card->ssPin, LOW); + if (!sdWait(pdrv, 500)) { + log_w("sdWait fail ignored, card initialize continues"); + } + (void)sdCommand(pdrv, GO_IDLE_STATE, 0, NULL); + sdDeselectCard(pdrv); + delay(sd_go_idle_delay_ms); + + digitalWrite(card->ssPin, LOW); + if (!sdWait(pdrv, 500)) { + log_w("sdWait fail ignored, card initialize continues"); + } + if (sdCommand(pdrv, GO_IDLE_STATE, 0, NULL) != 1) { + sdDeselectCard(pdrv); + log_w("GO_IDLE_STATE failed"); + goto unknown_card; + } + sdDeselectCard(pdrv); + delay(sd_go_idle_delay_ms); + + // Step 3: Configure CRC checking + // Enable CRC for data transfers in SPI mode (required for reliable communication). + // Some cards reject the first CRC_ON_OFF; retry once (same as IDF). + token = sdTransaction(pdrv, CRC_ON_OFF, 1, NULL); + if (token != 1 && token != 0x5) { + delay(10); + token = sdTransaction(pdrv, CRC_ON_OFF, 1, NULL); + } + if (token == 0x5) { + // Old card that doesn't support CRC - disable CRC checking + card->supports_crc = false; + } else if (token != 1) { + log_w("CRC_ON_OFF failed: %u", token); + goto unknown_card; + } + + // Step 4: Card type detection and initialization + // Try to identify SDHC/SDXC cards using SEND_IF_COND command + if (sdTransaction(pdrv, SEND_IF_COND, 0x1AA, &resp) == 1) { + // Card responded to SEND_IF_COND - likely SDHC/SDXC + if ((resp & 0xFFF) != 0x1AA) { + log_w("SEND_IF_COND failed: %03" PRIX32, (uint32_t)(resp & 0xFFF)); + goto unknown_card; + } + + // Read Operating Conditions Register to check card capabilities + if (sdTransaction(pdrv, READ_OCR, 0, &resp) != 1 || !(resp & (1 << 20))) { + log_w("READ_OCR failed: %X", resp); + goto unknown_card; + } + + // Send APP_OP_COND to set operating conditions for SDHC/SDXC. + // In SPI mode only HCS (bit 30) is valid; voltage bits must be 0 (same as IDF). + // Timeout must be >1s per SD spec (IDF uses ~3s). + start = millis(); + do { + token = sdTransaction(pdrv, APP_OP_COND, 0x40000000, NULL); + } while (token == 1 && (millis() - start) < sd_op_cond_timeout_ms); + + if (token) { + log_w("APP_OP_COND failed: %u", token); + goto unknown_card; + } + + // Determine if it's SDHC (high capacity) or regular SD + if (!sdTransaction(pdrv, READ_OCR, 0, &resp)) { + if (resp & (1 << 30)) { + card->type = CARD_SDHC; // High capacity card (SDHC/SDXC) + } else { + card->type = CARD_SD; // Standard capacity card + } + } else { + log_w("READ_OCR failed: %X", resp); + goto unknown_card; + } + } else { + // Card didn't respond to SEND_IF_COND - try SD or MMC initialization + if (sdTransaction(pdrv, READ_OCR, 0, &resp) != 1 || !(resp & (1 << 20))) { + log_w("READ_OCR failed: %X", resp); + goto unknown_card; + } + + // Try SD card initialization first (SPI mode: ACMD41 arg must be 0) + start = millis(); + do { + token = sdTransaction(pdrv, APP_OP_COND, 0, NULL); + } while (token == 0x01 && (millis() - start) < sd_op_cond_timeout_ms); + + if (!token) { + card->type = CARD_SD; // Standard SD card + } else { + // Try MMC card initialization (SPI mode: CMD1 arg must be 0) + start = millis(); + do { + token = sdTransaction(pdrv, SEND_OP_COND, 0, NULL); + } while (token != 0x00 && (millis() - start) < sd_op_cond_timeout_ms); + + if (token == 0x00) { + card->type = CARD_MMC; // MMC card + } else { + log_w("SEND_OP_COND failed: %u", token); + goto unknown_card; + } + } + } + + // Step 5: Clear card detection for SD cards (not needed for MMC) + if (card->type != CARD_MMC) { + if (sdTransaction(pdrv, APP_CLR_CARD_DETECT, 0, NULL)) { + log_w("APP_CLR_CARD_DETECT failed"); + goto unknown_card; + } + } + + // Step 6: Set block length for non-SDHC cards + // SDHC cards have fixed 512-byte blocks, others need explicit block length setting + if (card->type != CARD_SDHC) { + if (sdTransaction(pdrv, SET_BLOCKLEN, 512, NULL) != 0x00) { + log_w("SET_BLOCKLEN failed"); + goto unknown_card; + } + } + + // Step 7: Get card capacity and finalize initialization + card->sectors = sdGetSectorsCount(pdrv); + + // Limit frequency to 25MHz for compatibility (SD spec maximum for non-UHS cards) + if (card->frequency > 25000000) { + card->frequency = 25000000; + } + + // Mark card as initialized + card->status &= ~STA_NOINIT; + return card->status; + +unknown_card: + // Mark card as unknown type if initialization failed + card->type = CARD_UNKNOWN; + return card->status; +} + +DSTATUS ff_sd_status(uint8_t pdrv) { + ardu_sdcard_t *card = s_cards[pdrv]; + AcquireSPI lock(card); + + if (sdTransaction(pdrv, SEND_STATUS, 0, NULL)) { + log_e("Check status failed"); + return STA_NOINIT; + } + return s_cards[pdrv]->status; +} + +DRESULT ff_sd_read(uint8_t pdrv, uint8_t *buffer, DWORD sector, UINT count) { + ardu_sdcard_t *card = s_cards[pdrv]; + if (card->status & STA_NOINIT) { + return RES_NOTRDY; + } + DRESULT res = RES_OK; + + AcquireSPI lock(card); + + if (count > 1) { + res = sdReadSectors(pdrv, (char *)buffer, sector, count) ? RES_OK : RES_ERROR; + } else { + res = sdReadSector(pdrv, (char *)buffer, sector) ? RES_OK : RES_ERROR; + } + return res; +} + +DRESULT ff_sd_write(uint8_t pdrv, const uint8_t *buffer, DWORD sector, UINT count) { + ardu_sdcard_t *card = s_cards[pdrv]; + if (card->status & STA_NOINIT) { + return RES_NOTRDY; + } + + if (card->status & STA_PROTECT) { + return RES_WRPRT; + } + DRESULT res = RES_OK; + + AcquireSPI lock(card); + + if (count > 1) { + res = sdWriteSectors(pdrv, (const char *)buffer, sector, count) ? RES_OK : RES_ERROR; + } else { + res = sdWriteSector(pdrv, (const char *)buffer, sector) ? RES_OK : RES_ERROR; + } + return res; +} + +DRESULT ff_sd_ioctl(uint8_t pdrv, uint8_t cmd, void *buff) { + switch (cmd) { + case CTRL_SYNC: + { + AcquireSPI lock(s_cards[pdrv]); + if (sdSelectCard(pdrv)) { + sdDeselectCard(pdrv); + return RES_OK; + } + } + return RES_ERROR; + case GET_SECTOR_COUNT: *((unsigned long *)buff) = s_cards[pdrv]->sectors; return RES_OK; + case GET_SECTOR_SIZE: *((WORD *)buff) = 512; return RES_OK; + case GET_BLOCK_SIZE: *((uint32_t *)buff) = 1; return RES_OK; + } + return RES_PARERR; +} + +bool sd_read_raw(uint8_t pdrv, uint8_t *buffer, DWORD sector) { + return ff_sd_read(pdrv, buffer, sector, 1) == ESP_OK; +} + +bool sd_write_raw(uint8_t pdrv, uint8_t *buffer, DWORD sector) { + return ff_sd_write(pdrv, buffer, sector, 1) == ESP_OK; +} + +/* + * Public methods + * */ + +uint8_t sdcard_uninit(uint8_t pdrv) { + ardu_sdcard_t *card = s_cards[pdrv]; + if (pdrv >= FF_VOLUMES || card == NULL) { + return 1; + } + { + AcquireSPI lock(card); + sdTransaction(pdrv, GO_IDLE_STATE, 0, NULL); + } // lock is destructed here + ff_diskio_register(pdrv, NULL); + s_cards[pdrv] = NULL; + esp_err_t err = ESP_OK; + if (card->base_path) { + err = esp_vfs_fat_unregister_path(card->base_path); + free(card->base_path); + } + free(card); + return err; +} + +uint8_t sdcard_init(uint8_t cs, SPIClass *spi, int hz) { + + uint8_t pdrv = 0xFF; + if (ff_diskio_get_drive(&pdrv) != ESP_OK || pdrv == 0xFF) { + return pdrv; + } + + ardu_sdcard_t *card = (ardu_sdcard_t *)malloc(sizeof(ardu_sdcard_t)); + if (!card) { + return 0xFF; + } + + card->base_path = NULL; + card->frequency = hz; + card->spi = spi; + card->ssPin = digitalPinToGPIONumber(cs); + + card->supports_crc = true; + card->type = CARD_NONE; + card->status = STA_NOINIT; + + pinMode(card->ssPin, OUTPUT); + digitalWrite(card->ssPin, HIGH); + perimanSetPinBusExtraType(card->ssPin, "SD_SS"); + + s_cards[pdrv] = card; + + static const ff_diskio_impl_t sd_impl = { + .init = &ff_sd_initialize, .status = &ff_sd_status, .read = &ff_sd_read, .write = &ff_sd_write, .ioctl = &ff_sd_ioctl + }; + ff_diskio_register(pdrv, &sd_impl); + + return pdrv; +} + +uint8_t sdcard_unmount(uint8_t pdrv) { + ardu_sdcard_t *card = s_cards[pdrv]; + if (pdrv >= FF_VOLUMES || card == NULL) { + return 1; + } + card->status |= STA_NOINIT; + card->type = CARD_NONE; + + char drv[3] = {(char)('0' + pdrv), ':', 0}; + f_mount(NULL, drv, 0); + return 0; +} + +bool sdcard_mount(uint8_t pdrv, const char *path, uint8_t max_files, bool format_if_empty) { + ardu_sdcard_t *card = s_cards[pdrv]; + if (pdrv >= FF_VOLUMES || card == NULL) { + return false; + } + + if (card->base_path) { + free(card->base_path); + } + card->base_path = strdup(path); + + FATFS *fs; + char drv[3] = {(char)('0' + pdrv), ':', 0}; + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) + esp_err_t err = esp_vfs_fat_register(path, drv, max_files, &fs); +#else + esp_vfs_fat_conf_t conf = {.base_path = path, .fat_drive = drv, .max_files = max_files}; + esp_err_t err = esp_vfs_fat_register(&conf, &fs); +#endif + + if (err == ESP_ERR_INVALID_STATE) { + log_e("esp_vfs_fat_register failed 0x(%x): SD is registered.", err); + return false; + } else if (err != ESP_OK) { + log_e("esp_vfs_fat_register failed 0x(%x)", err); + return false; + } + + FRESULT res = f_mount(fs, drv, 1); + if (res != FR_OK) { + log_e("f_mount failed: %s", fferr2str[res]); + if (res == 13 && format_if_empty) { + BYTE *work = (BYTE *)malloc(sizeof(BYTE) * FF_MAX_SS); + if (!work) { + log_e("alloc for f_mkfs failed"); + return false; + } + //FRESULT f_mkfs (const TCHAR* path, const MKFS_PARM* opt, void* work, UINT len); + const MKFS_PARM opt = {(BYTE)FM_ANY, 0, 0, 0, 0}; + res = f_mkfs(drv, &opt, work, sizeof(BYTE) * FF_MAX_SS); + free(work); + if (res != FR_OK) { + log_e("f_mkfs failed: %s", fferr2str[res]); + esp_vfs_fat_unregister_path(path); + return false; + } + res = f_mount(fs, drv, 1); + if (res != FR_OK) { + log_e("f_mount failed: %s", fferr2str[res]); + esp_vfs_fat_unregister_path(path); + return false; + } + } else { + esp_vfs_fat_unregister_path(path); + return false; + } + } + AcquireSPI lock(card); + card->sectors = sdGetSectorsCount(pdrv); + return true; +} + +uint32_t sdcard_num_sectors(uint8_t pdrv) { + ardu_sdcard_t *card = s_cards[pdrv]; + if (pdrv >= FF_VOLUMES || card == NULL) { + return 0; + } + return card->sectors; +} + +uint32_t sdcard_sector_size(uint8_t pdrv) { + if (pdrv >= FF_VOLUMES || s_cards[pdrv] == NULL) { + return 0; + } + return 512; +} + +sdcard_type_t sdcard_type(uint8_t pdrv) { + ardu_sdcard_t *card = s_cards[pdrv]; + if (pdrv >= FF_VOLUMES || card == NULL) { + return CARD_NONE; + } + return card->type; +} diff --git a/lib/SD/src/sd_diskio.h b/lib/SD/src/sd_diskio.h new file mode 100644 index 0000000..762cb6e --- /dev/null +++ b/lib/SD/src/sd_diskio.h @@ -0,0 +1,34 @@ +// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#ifndef _SD_DISKIO_H_ +#define _SD_DISKIO_H_ + +#include "Arduino.h" +#include "SPI.h" +#include "sd_defines.h" +// #include "diskio.h" + +uint8_t sdcard_init(uint8_t cs, SPIClass *spi, int hz); +uint8_t sdcard_uninit(uint8_t pdrv); + +bool sdcard_mount(uint8_t pdrv, const char *path, uint8_t max_files, bool format_if_empty); +uint8_t sdcard_unmount(uint8_t pdrv); + +sdcard_type_t sdcard_type(uint8_t pdrv); +uint32_t sdcard_num_sectors(uint8_t pdrv); +uint32_t sdcard_sector_size(uint8_t pdrv); +bool sd_read_raw(uint8_t pdrv, uint8_t *buffer, uint32_t sector); +bool sd_write_raw(uint8_t pdrv, uint8_t *buffer, uint32_t sector); + +#endif /* _SD_DISKIO_H_ */ diff --git a/lib/SD/src/sd_diskio_crc.c b/lib/SD/src/sd_diskio_crc.c new file mode 100644 index 0000000..9c839a5 --- /dev/null +++ b/lib/SD/src/sd_diskio_crc.c @@ -0,0 +1,61 @@ +/* SD/MMC File System Library + * Copyright (c) 2014 Neil Thiessen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const char m_CRC7Table[] = {0x00, 0x09, 0x12, 0x1B, 0x24, 0x2D, 0x36, 0x3F, 0x48, 0x41, 0x5A, 0x53, 0x6C, 0x65, 0x7E, 0x77, 0x19, 0x10, 0x0B, 0x02, 0x3D, 0x34, + 0x2F, 0x26, 0x51, 0x58, 0x43, 0x4A, 0x75, 0x7C, 0x67, 0x6E, 0x32, 0x3B, 0x20, 0x29, 0x16, 0x1F, 0x04, 0x0D, 0x7A, 0x73, 0x68, 0x61, + 0x5E, 0x57, 0x4C, 0x45, 0x2B, 0x22, 0x39, 0x30, 0x0F, 0x06, 0x1D, 0x14, 0x63, 0x6A, 0x71, 0x78, 0x47, 0x4E, 0x55, 0x5C, 0x64, 0x6D, + 0x76, 0x7F, 0x40, 0x49, 0x52, 0x5B, 0x2C, 0x25, 0x3E, 0x37, 0x08, 0x01, 0x1A, 0x13, 0x7D, 0x74, 0x6F, 0x66, 0x59, 0x50, 0x4B, 0x42, + 0x35, 0x3C, 0x27, 0x2E, 0x11, 0x18, 0x03, 0x0A, 0x56, 0x5F, 0x44, 0x4D, 0x72, 0x7B, 0x60, 0x69, 0x1E, 0x17, 0x0C, 0x05, 0x3A, 0x33, + 0x28, 0x21, 0x4F, 0x46, 0x5D, 0x54, 0x6B, 0x62, 0x79, 0x70, 0x07, 0x0E, 0x15, 0x1C, 0x23, 0x2A, 0x31, 0x38, 0x41, 0x48, 0x53, 0x5A, + 0x65, 0x6C, 0x77, 0x7E, 0x09, 0x00, 0x1B, 0x12, 0x2D, 0x24, 0x3F, 0x36, 0x58, 0x51, 0x4A, 0x43, 0x7C, 0x75, 0x6E, 0x67, 0x10, 0x19, + 0x02, 0x0B, 0x34, 0x3D, 0x26, 0x2F, 0x73, 0x7A, 0x61, 0x68, 0x57, 0x5E, 0x45, 0x4C, 0x3B, 0x32, 0x29, 0x20, 0x1F, 0x16, 0x0D, 0x04, + 0x6A, 0x63, 0x78, 0x71, 0x4E, 0x47, 0x5C, 0x55, 0x22, 0x2B, 0x30, 0x39, 0x06, 0x0F, 0x14, 0x1D, 0x25, 0x2C, 0x37, 0x3E, 0x01, 0x08, + 0x13, 0x1A, 0x6D, 0x64, 0x7F, 0x76, 0x49, 0x40, 0x5B, 0x52, 0x3C, 0x35, 0x2E, 0x27, 0x18, 0x11, 0x0A, 0x03, 0x74, 0x7D, 0x66, 0x6F, + 0x50, 0x59, 0x42, 0x4B, 0x17, 0x1E, 0x05, 0x0C, 0x33, 0x3A, 0x21, 0x28, 0x5F, 0x56, 0x4D, 0x44, 0x7B, 0x72, 0x69, 0x60, 0x0E, 0x07, + 0x1C, 0x15, 0x2A, 0x23, 0x38, 0x31, 0x46, 0x4F, 0x54, 0x5D, 0x62, 0x6B, 0x70, 0x79}; + +char CRC7(const char *data, int length) { + char crc = 0; + for (int i = 0; i < length; i++) { + crc = m_CRC7Table[(crc << 1) ^ data[i]]; + } + return crc; +} + +const unsigned short m_CRC16Table[256] = { + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, + 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, + 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, + 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, + 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, + 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, + 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, + 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, + 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, + 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, + 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, + 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, + 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, + 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 +}; + +unsigned short CRC16(const char *data, int length) { + unsigned short crc = 0; + for (int i = 0; i < length; i++) { + crc = (crc << 8) ^ m_CRC16Table[((crc >> 8) ^ data[i]) & 0x00FF]; + } + return crc; +} From 3f2650c56e83447ae864afe977e79a33f1d253af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Mon, 5 Oct 2026 22:55:02 +0200 Subject: [PATCH 2/4] SD driver: a dummy byte before the ready test; say why a write failed (#21) The card "refused" a write about once in 2,000 multi-block writes: three 1.7 MB uploads in ten. Measured with a driver that records where it gives up: every time, all blocks were accepted, and the status check after Stop Tran came back as 0xFF or 0x1F. The driver tests for ready with the first byte after selecting the card, which reads 0xFF before the card has signalled busy, so CMD13 went out mid-programming. A dummy byte first, as in ChaN's reference driver, and one after Stop Tran. 30 uploads in a row since, each read back by SHA-256, ten with the radio listening: no fault. 10 MHz made no difference; the card stays at 20 MHz. `info` shows the driver's write faults; `put` prints the step and the card's answer when one happens. ADR 0007. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT --- README.md | 2 +- docs/adr/0007-own-copy-of-the-sd-driver.md | 20 ++++++++ lib/SD/src/sd_diskio.cpp | 60 ++++++++++++++++++---- lib/SD/src/sd_fault.h | 29 +++++++++++ src/main.cpp | 5 +- src/services/debug_console.cpp | 10 ++++ src/services/storage_service.cpp | 4 +- src/services/storage_service.h | 1 + 8 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 docs/adr/0007-own-copy-of-the-sd-driver.md create mode 100644 lib/SD/src/sd_fault.h diff --git a/README.md b/README.md index 3e8878a..e395a03 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ The LoRa Scanner (docs/milestones/M3.md) listens with the Cap's radio and **neve | `irc say ` | Types into a Buffer, commands included (`irc say 0 /join #test`) | | `irc dump` | Prints IRC status, memory, and the last lines of each Buffer | | `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap | -| `info` | Firmware, uptime, last start reason, memory, Wi-Fi, and both app slots with their versions and OTA states | +| `info` | Firmware, uptime, last start reason, memory, Wi-Fi, the SD card and its write faults since boot, and both app slots with their versions and OTA states | | `tasks` | FreeRTOS tasks: state, priority, lowest free stack, CPU share | | `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) | | `log level <0-5>` | ESP-IDF log level | diff --git a/docs/adr/0007-own-copy-of-the-sd-driver.md b/docs/adr/0007-own-copy-of-the-sd-driver.md new file mode 100644 index 0000000..1b1e552 --- /dev/null +++ b/docs/adr/0007-own-copy-of-the-sd-driver.md @@ -0,0 +1,20 @@ +# Our own copy of the SD driver, for one missing byte + +Arduino-ESP32's `SD` library talks to the card over SPI through `sd_diskio.cpp`. That driver gives up on a write without saying why, and about once in 2,000 multi-block writes it gave up on one that had worked (issue #21). A 1.7 MB upload failed about three times in ten; before M3, the retry on top of it then filled the gap with zeros. + +The cause, measured with a driver that records where it stops: after the "Stop Tran" token that ends a multi-block write, a card takes about a byte of clock to signal busy. The driver deselects, selects again, and reads one byte to see whether the card is ready. Read too early, that byte is 0xFF, "ready"; the status check (CMD13) then goes out while the card is still programming, and its answer (0xFF, 0x1F) is taken for an error. Every failure seen was this one: all blocks accepted, then a status that isn't one. ChaN's reference driver, which FatFs ships as its example, sends a dummy byte after selecting the card for this reason. Arduino's doesn't. + +PlatformIO links the framework's library objects directly, so one file can't be replaced from `src`. A project library with the same name takes its place: **`lib/SD` is Arduino-ESP32 3.3.12's SD library (Apache-2.0), with `sd_diskio.cpp` changed** and the other files as they came. The changes are marked `roro:`: + +- A dummy byte after selecting the card, before the ready test, and one after Stop Tran. +- Each place a write gives up records the step and the card's answer (`sd_fault.h`): `info` shows the count, and the Debug Console's `put` prints the detail. + +Halving the SPI clock to 10 MHz didn't change the failure rate, so the card stays at 20 MHz. + +## Consequences + +- 30 uploads of 1.7 MB in a row, each read back and compared by SHA-256, ten of them with the LoRa radio listening on the same bus: no write fault. Before: 3 failures in 10. +- Every writer gains: Logs, Tracks, Gemini pages, Saved Pages, Captures and Update Files installed from the card all go through this driver, and none of them checked. +- **The copy has to follow the framework.** When the platform is updated, compare `lib/SD` with the new `libraries/SD` and carry the `roro:` changes over. If upstream fixes the ready test, drop the copy. +- One more defect was read in the code and left alone, because nothing here exercises it: the driver tests the card's answer to a data block against 0x0A and 0x0C, values it can't take (accepted is 0x05, CRC error 0x0B, write error 0x0D), so a block rejected for a CRC error is never resent. No such rejection was seen in any failure. If `DataToken` faults ever show in `info`, that's the next fix. +- A fault is now counted and explained instead of silent, so the next cause, if there is one, starts with evidence. diff --git a/lib/SD/src/sd_diskio.cpp b/lib/SD/src/sd_diskio.cpp index 538f453..7d003a6 100644 --- a/lib/SD/src/sd_diskio.cpp +++ b/lib/SD/src/sd_diskio.cpp @@ -1,3 +1,8 @@ +// roro9stack's copy of the SD-over-SPI driver from Arduino-ESP32 3.3.12 (libraries/SD/src/ +// sd_diskio.cpp), linked instead of the framework's: defining every function the SD class needs +// keeps the library's file out of the link. Changes are marked "roro:". Why (issue #21): the +// original gives up on a write without saying why, and never resends a block the card rejects. +// // Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,10 +33,25 @@ extern "C" { #endif //#include "esp_vfs.h" #include "esp_vfs_fat.h" + char CRC7(const char *data, int length); unsigned short CRC16(const char *data, int length); } +// roro: why the last write failed. +#include "sd_fault.h" +static roro::SdFault s_fault; +static bool sdFault(roro::SdFault::Step step, uint8_t token = 0, uint32_t resp = 0) { + s_fault.step = step; + s_fault.token = token; + s_fault.resp = resp; + s_fault.count++; + return false; +} +namespace roro { +SdFault sdLastFault() { return s_fault; } +} // namespace roro + typedef enum { GO_IDLE_STATE = 0, SEND_OP_COND = 1, @@ -128,6 +148,11 @@ void sdDeselectCard(uint8_t pdrv) { bool sdSelectCard(uint8_t pdrv) { ardu_sdcard_t *card = s_cards[pdrv]; digitalWrite(card->ssPin, LOW); + // roro: one dummy byte before asking whether the card is ready, as ChaN's reference driver does. + // A card that has just been given a write takes about a byte of clock to signal busy; without + // this, that first byte reads 0xFF, "ready", and the next command (the status check after a + // write) goes out while the card is still programming and comes back as garbage (#21). + card->spi->transfer(0xFF); bool s = sdWait(pdrv, 500); if (!s) { log_e("Select Failed"); @@ -308,9 +333,10 @@ bool sdReadSectors(uint8_t pdrv, char *buffer, unsigned long long sector, int co } bool sdWriteSector(uint8_t pdrv, const char *buffer, unsigned long long sector) { + using roro::SdFault; for (int f = 0; f < 3; f++) { if (!sdSelectCard(pdrv)) { - return false; + return sdFault(SdFault::Select); // roro: say why } if (!sdCommand(pdrv, WRITE_BLOCK_SINGLE, (s_cards[pdrv]->type == CARD_SDHC) ? sector : sector << 9, NULL)) { char token = sdWriteBytes(pdrv, buffer, 0xFE); @@ -319,12 +345,13 @@ bool sdWriteSector(uint8_t pdrv, const char *buffer, unsigned long long sector) if (token == 0x0A) { continue; } else if (token == 0x0C) { - return false; + return sdFault(SdFault::DataToken, token); } unsigned int resp; - if (sdTransaction(pdrv, SEND_STATUS, 0, &resp) || resp) { - return false; + char status = sdTransaction(pdrv, SEND_STATUS, 0, &resp); + if (status || resp) { + return token != 0x05 ? sdFault(SdFault::DataToken, token, resp) : sdFault(SdFault::Status, status, resp); } return true; } else { @@ -332,25 +359,29 @@ bool sdWriteSector(uint8_t pdrv, const char *buffer, unsigned long long sector) } } sdDeselectCard(pdrv); - return false; + return sdFault(SdFault::Command); } bool sdWriteSectors(uint8_t pdrv, const char *buffer, unsigned long long sector, int count) { + using roro::SdFault; char token; const char *currentBuffer = buffer; unsigned long long currentSector = sector; int currentCount = count; ardu_sdcard_t *card = s_cards[pdrv]; + SdFault::Step why = SdFault::Command; // roro: what stopped it, for the last return + uint8_t whyToken = 0; for (int f = 0; f < 3;) { if (card->type != CARD_MMC) { - if (sdTransaction(pdrv, SET_WR_BLK_ERASE_COUNT, currentCount, NULL)) { - return false; + char refused = sdTransaction(pdrv, SET_WR_BLK_ERASE_COUNT, currentCount, NULL); + if (refused) { + return sdFault(SdFault::EraseCount, refused); } } if (!sdSelectCard(pdrv)) { - return false; + return sdFault(SdFault::Select); } if (!sdCommand(pdrv, WRITE_BLOCK_MULTIPLE, (card->type == CARD_SDHC) ? currentSector : currentSector << 9, NULL)) { @@ -365,20 +396,25 @@ bool sdWriteSectors(uint8_t pdrv, const char *buffer, unsigned long long sector, } while (--currentCount); if (!sdWait(pdrv, 500)) { + why = SdFault::BusyAfter; break; } if (currentCount == 0) { sdStop(pdrv); + card->spi->transfer(0xFF); // roro: the byte the card takes to go busy after Stop Tran (#21) sdDeselectCard(pdrv); unsigned int resp; - if (sdTransaction(pdrv, SEND_STATUS, 0, &resp) || resp) { - return false; + char status = sdTransaction(pdrv, SEND_STATUS, 0, &resp); + if (status || resp) { + return sdFault(SdFault::Status, status, resp); } return true; } else { if (sdCommand(pdrv, STOP_TRANSMISSION, 0, NULL)) { + why = SdFault::StopCommand; + whyToken = token; break; } @@ -402,6 +438,8 @@ bool sdWriteSectors(uint8_t pdrv, const char *buffer, unsigned long long sector, currentCount = count - writtenBlocks; continue; } else { + why = SdFault::DataToken; + whyToken = token; break; } } @@ -410,7 +448,7 @@ bool sdWriteSectors(uint8_t pdrv, const char *buffer, unsigned long long sector, } } sdDeselectCard(pdrv); - return false; + return sdFault(why, whyToken); } unsigned long sdGetSectorsCount(uint8_t pdrv) { diff --git a/lib/SD/src/sd_fault.h b/lib/SD/src/sd_fault.h new file mode 100644 index 0000000..347f892 --- /dev/null +++ b/lib/SD/src/sd_fault.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace roro { + +// Why the SD driver last gave up on a write (see sd_diskio.cpp, issue #21). The framework's driver +// fails without saying why; ours records it. +struct SdFault { + enum Step : uint8_t { + None, + EraseCount, // ACMD23 before a multi-block write was refused + Select, // the card stayed busy for 500 ms + Command, // the write command itself was refused + DataToken, // the card's answer to a data block: 0x0B CRC error, 0x0D write error + BusyAfter, // still busy 500 ms after the last block + Status, // CMD13 after the write reported an error (resp) + StopCommand, // CMD12 after a rejected block was refused + }; + Step step = None; + uint8_t token = 0; // the driver's or the card's answer at that step + uint32_t resp = 0; // CMD13's status bits, for Status + uint32_t count = 0; // failed writes since boot + uint32_t retried = 0; // blocks resent after a CRC error, since boot +}; + +SdFault sdLastFault(); + +} // namespace roro diff --git a/src/main.cpp b/src/main.cpp index c71a078..c492b66 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ #include "platform/crash_report.h" #include "platform/nvs_store.h" #include "platform/system_info.h" +#include "sd_fault.h" #include "service_manager.h" #include "platform/identity.h" #include "services/battery_service.h" @@ -400,8 +401,8 @@ static void runCommand(String line) { if (line == "help") console.print(kHelp); if (line == "info") { system_info::printSystem(console); - console.printf("wifi: %s, ip %s, rssi %d | sd: %s\n", wifi->ssid().c_str(), wifi->ip().c_str(), wifi->rssi(), - storageService->state().present ? "present" : "none"); + console.printf("wifi: %s, ip %s, rssi %d | sd: %s, %u write faults\n", wifi->ssid().c_str(), wifi->ip().c_str(), + wifi->rssi(), storageService->state().present ? "present" : "none", (unsigned)sdLastFault().count); console.printf("update: %s\n", update->onProbation() ? "on probation" : "confirmed"); system_info::printSlots(console, nvs); } diff --git a/src/services/debug_console.cpp b/src/services/debug_console.cpp index dddce6b..e2ebaca 100644 --- a/src/services/debug_console.cpp +++ b/src/services/debug_console.cpp @@ -17,6 +17,7 @@ #include "file_receiver.h" #include "sha256.h" #include "platform/console.h" +#include "sd_fault.h" #include "version.h" namespace roro { @@ -232,7 +233,16 @@ void DebugConsole::put(NetworkClient& client, const std::string& args) { while (r.state() == S::Receiving || r.state() == S::Writing) { if (r.state() == S::Writing) { const auto& c = r.chunk(); + uint32_t t0 = millis(); bool ok = f.write(c.data(), c.size()) == c.size(); + uint32_t took = millis() - t0; + // Why, from our copy of the SD driver (lib/SD, #21): the step that gave up and what + // the card answered. How long it took tells a 500 ms busy timeout from a refusal. + if (!ok) { + SdFault sd = sdLastFault(); + console.printf("put: write failed at %u after %u ms: SD step %u, answer 0x%02X, status 0x%04X (%u so far)\n", + (unsigned)r.received(), (unsigned)took, sd.step, sd.token, (unsigned)sd.resp, (unsigned)sd.count); + } // A card can fail one write and take the next. FATFS keeps a failed file in error, // so: close, cut back to the last good byte, reopen, try again. Earlier chunks may // have been lost with the write buffer (M3: 3 KB came back as zeros): never extend diff --git a/src/services/storage_service.cpp b/src/services/storage_service.cpp index 3a400fd..c8c9929 100644 --- a/src/services/storage_service.cpp +++ b/src/services/storage_service.cpp @@ -166,7 +166,7 @@ void StorageService::format() { mounted_ = false; bool ok = false; - uint8_t pdrv = sdcard_init(pins::kSdCs, &sharedSpi(), 20000000); + uint8_t pdrv = sdcard_init(pins::kSdCs, &sharedSpi(), kSdHz); if (pdrv != 0xFF) { constexpr size_t kWorkSize = 4096; // FF_MAX_SS std::unique_ptr work(new uint8_t[kWorkSize]); @@ -182,7 +182,7 @@ void StorageService::format() { } bool StorageService::mount() { - return SD.begin(pins::kSdCs, sharedSpi(), 20000000, "/sd", 5, false); + return SD.begin(pins::kSdCs, sharedSpi(), kSdHz, "/sd", 5, false); } void StorageService::poll() { diff --git a/src/services/storage_service.h b/src/services/storage_service.h index 6836d28..c9d0312 100644 --- a/src/services/storage_service.h +++ b/src/services/storage_service.h @@ -56,6 +56,7 @@ class StorageService : public Service { static constexpr uint32_t kWakeMs = 1000; static constexpr uint32_t kPollEvery = 15; // wake-ups between usage checks static constexpr size_t kMaxPendingBytes = 16 * 1024; + static constexpr uint32_t kSdHz = 20000000; // the card's SPI clock; 10 MHz changed nothing (ADR 0007) static void taskEntry(void* self); void loop(); From 370f067fbf8b690cd0b22a27a1771d9171fb7d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Mon, 5 Oct 2026 23:15:29 +0200 Subject: [PATCH 3/4] `sd card`: what the card says it is, from its CID register Type, size, and the identity register read by our SD driver (CMD10): manufacturer, OEM, product name, revision, serial and date, decoded by a host-tested parser. Needed for the upstream report of #21: nothing else here could read the card's identity. This one is a Samsung 8 GB SDHC from June 2013. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT --- README.md | 1 + lib/SD/src/sd_diskio.cpp | 20 +++++++++ lib/SD/src/sd_fault.h | 4 ++ lib/storage_model/src/sd_card_id.cpp | 53 +++++++++++++++++++++++ lib/storage_model/src/sd_card_id.h | 27 ++++++++++++ src/main.cpp | 15 ++++++- test/test_sd_card_id/test_sd_card_id.cpp | 54 ++++++++++++++++++++++++ 7 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 lib/storage_model/src/sd_card_id.cpp create mode 100644 lib/storage_model/src/sd_card_id.h create mode 100644 test/test_sd_card_id/test_sd_card_id.cpp diff --git a/README.md b/README.md index e395a03..fc9ee3f 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ The LoRa Scanner (docs/milestones/M3.md) listens with the Cap's radio and **neve | `short` / `normal` | Screen timeouts 5 s / 10 s, or 30 s / 60 s | | `wifi add ` | Adds a Saved Network (so credentials stay out of the repo) | | `log ` | Appends a line to a test IRC Log (`/irc/dev/#test/.log`) | +| `sd card` | What the SD card says it is: type, size, and its identity register (maker, name, revision, serial, date) | | `sd list` | Lists the files of each Storage Clean-up category | | `cat ` | Prints the first ~1.2 KB of a file on the SD card | | `irc start` | Starts the IRC Service (normally done by opening the IRC App) | diff --git a/lib/SD/src/sd_diskio.cpp b/lib/SD/src/sd_diskio.cpp index 7d003a6..fd42d7f 100644 --- a/lib/SD/src/sd_diskio.cpp +++ b/lib/SD/src/sd_diskio.cpp @@ -920,6 +920,26 @@ uint32_t sdcard_sector_size(uint8_t pdrv) { return 512; } +// roro: the card's CID. CMD10 in SPI mode (the enum's SEND_CID, 2, is the SD-mode command). +namespace roro { +bool sdReadCid(uint8_t cid[16]) { + for (uint8_t pdrv = 0; pdrv < FF_VOLUMES; ++pdrv) { + ardu_sdcard_t *card = s_cards[pdrv]; + if (!card) { + continue; + } + AcquireSPI lock(card); + if (!sdSelectCard(pdrv)) { + return false; + } + bool ok = !sdCommand(pdrv, 10, 0, NULL) && sdReadBytes(pdrv, (char *)cid, 16); + sdDeselectCard(pdrv); + return ok; + } + return false; +} +} // namespace roro + sdcard_type_t sdcard_type(uint8_t pdrv) { ardu_sdcard_t *card = s_cards[pdrv]; if (pdrv >= FF_VOLUMES || card == NULL) { diff --git a/lib/SD/src/sd_fault.h b/lib/SD/src/sd_fault.h index 347f892..bf02ab4 100644 --- a/lib/SD/src/sd_fault.h +++ b/lib/SD/src/sd_fault.h @@ -26,4 +26,8 @@ struct SdFault { SdFault sdLastFault(); +// The mounted card's identity register (CID, CMD10): who made it, its name, serial and date. +// Call it where card access is allowed (the storage task). +bool sdReadCid(uint8_t cid[16]); + } // namespace roro diff --git a/lib/storage_model/src/sd_card_id.cpp b/lib/storage_model/src/sd_card_id.cpp new file mode 100644 index 0000000..21a2cdc --- /dev/null +++ b/lib/storage_model/src/sd_card_id.cpp @@ -0,0 +1,53 @@ +#include "sd_card_id.h" + +#include + +namespace roro { + +namespace { +char printable(uint8_t b) { return b >= 0x20 && b < 0x7F ? static_cast(b) : '?'; } +} // namespace + +SdCardId parseSdCid(const uint8_t cid[16]) { + SdCardId id; + id.manufacturer = cid[0]; + for (int i = 0; i < 2; ++i) id.oem[i] = printable(cid[1 + i]); + for (int i = 0; i < 5; ++i) id.product[i] = printable(cid[3 + i]); + id.revisionMajor = cid[8] >> 4; + id.revisionMinor = cid[8] & 0x0F; + id.serial = static_cast(cid[9]) << 24 | cid[10] << 16 | cid[11] << 8 | cid[12]; + // 4 reserved bits, 8 bits of year since 2000, 4 bits of month. + id.year = 2000 + (((cid[13] & 0x0F) << 4) | (cid[14] >> 4)); + id.month = cid[14] & 0x0F; + return id; +} + +const char* sdManufacturerName(uint8_t id) { + switch (id) { + case 0x01: return "Panasonic"; + case 0x02: return "Toshiba/Kioxia"; + case 0x03: return "SanDisk"; + case 0x1B: return "Samsung"; + case 0x1D: return "ADATA"; + case 0x27: return "Phison"; + case 0x28: return "Lexar"; + case 0x31: return "Silicon Power"; + case 0x41: return "Kingston"; + case 0x74: return "Transcend"; + case 0x76: return "Patriot"; + case 0x82: return "Sony"; + case 0x9C: return "Angelbird/Hoodman"; + case 0xAD: return "Longsys/Lexar"; + default: return "unknown"; + } +} + +std::string sdCardSummary(const SdCardId& id) { + char s[112]; + std::snprintf(s, sizeof s, "%s (0x%02X) \"%s\" \"%s\" rev %u.%u, serial %08x, made %04d-%02d", + sdManufacturerName(id.manufacturer), id.manufacturer, id.oem, id.product, id.revisionMajor, + id.revisionMinor, static_cast(id.serial), id.year, id.month); + return s; +} + +} // namespace roro diff --git a/lib/storage_model/src/sd_card_id.h b/lib/storage_model/src/sd_card_id.h new file mode 100644 index 0000000..30e3de3 --- /dev/null +++ b/lib/storage_model/src/sd_card_id.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +namespace roro { + +// What an SD card says it is: its CID register, 16 bytes (SD Physical Layer spec, 5.2). +struct SdCardId { + uint8_t manufacturer = 0; // assigned by the SD Association; see sdManufacturerName() + char oem[3] = {}; // two characters + char product[6] = {}; // five characters + uint8_t revisionMajor = 0, revisionMinor = 0; + uint32_t serial = 0; + int year = 0, month = 0; // manufactured +}; + +SdCardId parseSdCid(const uint8_t cid[16]); + +// The usual holder of a manufacturer ID. The SD Association doesn't publish its list: these are +// the commonly reported ones, and resold or counterfeit cards carry whatever their maker chose. +const char* sdManufacturerName(uint8_t id); + +// "SanDisk (0x03) "SD" "SU08G" rev 8.0, serial 1234abcd, made 2014-03". +std::string sdCardSummary(const SdCardId& id); + +} // namespace roro diff --git a/src/main.cpp b/src/main.cpp index c492b66..36beec4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ #include "platform/crash_report.h" #include "platform/nvs_store.h" #include "platform/system_info.h" +#include "sd_card_id.h" #include "sd_fault.h" #include "service_manager.h" #include "platform/identity.h" @@ -376,7 +377,7 @@ static const char* const kHelp = "gemini get fetch a Gemini page and report header, size, certificate, heap\n" "irc start | irc stop | irc dump | irc say \n" "ls [folder] | rm | install (Update from SD)\n" - "sd list | cat | log | burst | sound on|off | short | normal\n" + "sd card | sd list | cat | log | burst | sound on|off | short | normal\n" #ifdef RORO_DEBUG "crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n" "lora inject [rssi] [snr] a packet into the LoRa Scanner as if received (nothing is sent)\n" @@ -538,6 +539,18 @@ static void runCommand(String line) { } } if (line.startsWith("sd put ")) startUpload(line.substring(7)); // then raw bytes: see uploadStep() + if (line == "sd card") { // what the card says it is, from its CID register + if (!storageService->state().present) return (void)console.println("sd card: no card"); + storageService->runJob([]() { + uint8_t cid[16]; + const char* type = SD.cardType() == CARD_SDHC ? "SDHC/SDXC" : SD.cardType() == CARD_SD ? "SDSC" : "MMC or unknown"; + if (!sdReadCid(cid)) return (void)console.println("sd card: the card didn't answer"); + console.printf("sd card: %s, %.1f GB, %s\n", type, SD.cardSize() / 1e9, sdCardSummary(parseSdCid(cid)).c_str()); + console.printf("sd card: CID"); + for (uint8_t b : cid) console.printf(" %02x", b); + console.println(); + }); + } if (line == "sd list") { storageService->requestListing(); listingWanted = true; diff --git a/test/test_sd_card_id/test_sd_card_id.cpp b/test/test_sd_card_id/test_sd_card_id.cpp new file mode 100644 index 0000000..a946c5d --- /dev/null +++ b/test/test_sd_card_id/test_sd_card_id.cpp @@ -0,0 +1,54 @@ +#include + +#include "sd_card_id.h" + +using namespace roro; + +void setUp() {} +void tearDown() {} + +// A SanDisk 8 GB card's CID: manufacturer 0x03, OEM "SD", product "SU08G", revision 8.0, +// serial 0x1234ABCD, made in March 2014. +static const uint8_t kSanDisk[16] = {0x03, 'S', 'D', 'S', 'U', '0', '8', 'G', 0x80, + 0x12, 0x34, 0xAB, 0xCD, 0x00, 0xE3, 0xFF}; + +void test_fields() { + SdCardId id = parseSdCid(kSanDisk); + TEST_ASSERT_EQUAL_HEX8(0x03, id.manufacturer); + TEST_ASSERT_EQUAL_STRING("SD", id.oem); + TEST_ASSERT_EQUAL_STRING("SU08G", id.product); + TEST_ASSERT_EQUAL_UINT8(8, id.revisionMajor); + TEST_ASSERT_EQUAL_UINT8(0, id.revisionMinor); + TEST_ASSERT_EQUAL_HEX32(0x1234ABCD, id.serial); + TEST_ASSERT_EQUAL_INT(2014, id.year); + TEST_ASSERT_EQUAL_INT(3, id.month); +} + +// Clone cards put anything in the name: unprintable bytes mustn't reach the console. +void test_unprintable_names() { + uint8_t cid[16] = {0x00, 0x00, 0x01, 'A', 0x00, 0xFF, 'B', ' ', 0x10}; + SdCardId id = parseSdCid(cid); + TEST_ASSERT_EQUAL_STRING("??", id.oem); + TEST_ASSERT_EQUAL_STRING("A??B ", id.product); + TEST_ASSERT_EQUAL_UINT8(1, id.revisionMajor); +} + +void test_manufacturer_names() { + TEST_ASSERT_EQUAL_STRING("SanDisk", sdManufacturerName(0x03)); + TEST_ASSERT_EQUAL_STRING("Samsung", sdManufacturerName(0x1B)); + TEST_ASSERT_EQUAL_STRING("unknown", sdManufacturerName(0xEE)); +} + +void test_summary_line() { + TEST_ASSERT_EQUAL_STRING("SanDisk (0x03) \"SD\" \"SU08G\" rev 8.0, serial 1234abcd, made 2014-03", + sdCardSummary(parseSdCid(kSanDisk)).c_str()); +} + +int main() { + UNITY_BEGIN(); + RUN_TEST(test_fields); + RUN_TEST(test_unprintable_names); + RUN_TEST(test_manufacturer_names); + RUN_TEST(test_summary_line); + return UNITY_END(); +} From 7b2cf88a0acb4c698bf4e34ef6c36c797d289dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Mon, 5 Oct 2026 23:16:17 +0200 Subject: [PATCH 4/4] ADR 0007: link the upstream report and the issue that follows it Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT --- docs/adr/0007-own-copy-of-the-sd-driver.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/adr/0007-own-copy-of-the-sd-driver.md b/docs/adr/0007-own-copy-of-the-sd-driver.md index 1b1e552..58ae184 100644 --- a/docs/adr/0007-own-copy-of-the-sd-driver.md +++ b/docs/adr/0007-own-copy-of-the-sd-driver.md @@ -1,6 +1,6 @@ # Our own copy of the SD driver, for one missing byte -Arduino-ESP32's `SD` library talks to the card over SPI through `sd_diskio.cpp`. That driver gives up on a write without saying why, and about once in 2,000 multi-block writes it gave up on one that had worked (issue #21). A 1.7 MB upload failed about three times in ten; before M3, the retry on top of it then filled the gap with zeros. +Arduino-ESP32's `SD` library talks to the card over SPI through `sd_diskio.cpp`. That driver gives up on a write without saying why, and about once in 1,500 multi-block writes it gave up on one that had worked (issue #21). A 1.7 MB upload failed about three times in ten; before M3, the retry on top of it then filled the gap with zeros. The cause, measured with a driver that records where it stops: after the "Stop Tran" token that ends a multi-block write, a card takes about a byte of clock to signal busy. The driver deselects, selects again, and reads one byte to see whether the card is ready. Read too early, that byte is 0xFF, "ready"; the status check (CMD13) then goes out while the card is still programming, and its answer (0xFF, 0x1F) is taken for an error. Every failure seen was this one: all blocks accepted, then a status that isn't one. ChaN's reference driver, which FatFs ships as its example, sends a dummy byte after selecting the card for this reason. Arduino's doesn't. @@ -15,6 +15,6 @@ Halving the SPI clock to 10 MHz didn't change the failure rate, so the card stay - 30 uploads of 1.7 MB in a row, each read back and compared by SHA-256, ten of them with the LoRa radio listening on the same bus: no write fault. Before: 3 failures in 10. - Every writer gains: Logs, Tracks, Gemini pages, Saved Pages, Captures and Update Files installed from the card all go through this driver, and none of them checked. -- **The copy has to follow the framework.** When the platform is updated, compare `lib/SD` with the new `libraries/SD` and carry the `roro:` changes over. If upstream fixes the ready test, drop the copy. +- **The copy has to follow the framework.** When the platform is updated, compare `lib/SD` with the new `libraries/SD` and carry the `roro:` changes over. If upstream fixes the ready test, drop the copy. Reported as [arduino-esp32#12970](https://github.com/espressif/arduino-esp32/issues/12970); issue #39 follows it. - One more defect was read in the code and left alone, because nothing here exercises it: the driver tests the card's answer to a data block against 0x0A and 0x0C, values it can't take (accepted is 0x05, CRC error 0x0B, write error 0x0D), so a block rejected for a CRC error is never resent. No such rejection was seen in any failure. If `DataToken` faults ever show in `info`, that's the next fix. - A fault is now counted and explained instead of silent, so the next cause, if there is one, starts with evidence.