diff --git a/README.md b/README.md index 3e8878a..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) | @@ -85,7 +86,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..58ae184 --- /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 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. + +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. 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. 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..fd42d7f --- /dev/null +++ b/lib/SD/src/sd_diskio.cpp @@ -0,0 +1,949 @@ +// 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"); +// 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); +} + +// 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, + 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); + // 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"); + 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) { + using roro::SdFault; + for (int f = 0; f < 3; f++) { + if (!sdSelectCard(pdrv)) { + 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); + sdDeselectCard(pdrv); + + if (token == 0x0A) { + continue; + } else if (token == 0x0C) { + return sdFault(SdFault::DataToken, token); + } + + unsigned int resp; + 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 { + break; + } + } + sdDeselectCard(pdrv); + 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) { + char refused = sdTransaction(pdrv, SET_WR_BLK_ERASE_COUNT, currentCount, NULL); + if (refused) { + return sdFault(SdFault::EraseCount, refused); + } + } + + if (!sdSelectCard(pdrv)) { + return sdFault(SdFault::Select); + } + + 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)) { + 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; + 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; + } + + 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 { + why = SdFault::DataToken; + whyToken = token; + break; + } + } + } else { + break; + } + } + sdDeselectCard(pdrv); + return sdFault(why, whyToken); +} + +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; +} + +// 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) { + 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; +} diff --git a/lib/SD/src/sd_fault.h b/lib/SD/src/sd_fault.h new file mode 100644 index 0000000..bf02ab4 --- /dev/null +++ b/lib/SD/src/sd_fault.h @@ -0,0 +1,33 @@ +#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(); + +// 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 c71a078..36beec4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,8 @@ #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" #include "services/battery_service.h" @@ -375,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" @@ -400,8 +402,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); } @@ -537,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/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(); 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(); +}