Files
wadamesh/variants/crowpanel_35/CrowPanel35SD.cpp
T
Michael A. CojocariandClaude Opus 5.5 dbecef0b6f feat(crowpanel-35): add Elecrow CrowPanel 3.5" (ESP32-S3, ILI9488 + GT911) touch target
- New crowpanel_35 variant: display, SD, board and target files, plus board JSON
- Portrait mode is drawn rotated 180°; landscape stays at rotation 3
- Share the GT911 touch driver between Wio Tracker L2 and CrowPanel
  (WioTrackerL2Touch.cpp -> src/helpers/input/LgfxGt911Touch.cpp)
- platformio envs, flasher/site entries, release and merge-bin support
- Tests for the CrowPanel SD codec and merge-bin

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132MkJ7RUV32mV9F7o4PJMm
2026-10-02 20:16:06 -04:00

479 lines
15 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2025-2026 Michael A. Cojocari and contributors
// Adapted from Camillia-MT's sd_soft_spi.cpp for WadaMesh's SD filesystem API.
#include "CrowPanel35SD.h"
#include "CrowPanel35SdCodec.h"
#include <Arduino.h>
#include <vfs_api.h>
#include <freertos/semphr.h>
#include "diskio_impl.h"
#include "esp_vfs_fat.h"
#include "ff.h"
#include "soc/gpio_reg.h"
#include "rom/ets_sys.h"
static_assert(PIN_SD_SCLK >= 0 && PIN_SD_SCLK < 32, "SD SCLK must be GPIO0-31");
static_assert(PIN_SD_MISO >= 0 && PIN_SD_MISO < 32, "SD MISO must be GPIO0-31");
static_assert(PIN_SD_MOSI >= 0 && PIN_SD_MOSI < 32, "SD MOSI must be GPIO0-31");
static_assert(PIN_SD_CS >= 0 && PIN_SD_CS < 32, "SD CS must be GPIO0-31");
namespace {
constexpr const char* kMountPoint = "/sd";
constexpr uint8_t kMaxOpenFiles = 8;
constexpr uint32_t kClockMask = 1UL << PIN_SD_SCLK;
constexpr uint32_t kMosiMask = 1UL << PIN_SD_MOSI;
constexpr uint32_t kCsMask = 1UL << PIN_SD_CS;
constexpr uint8_t kReady = 0x00, kIdle = 0x01, kIllegal = 0x04;
constexpr uint8_t kSingle = 0xFE, kMulti = 0xFC, kStop = 0xFD;
sdcard_type_t s_type = CARD_NONE;
uint32_t s_sectors = 0;
BYTE s_drive = FF_DRV_NOT_USED;
bool s_mounted = false, s_vfs_registered = false, s_slow = true;
SemaphoreHandle_t s_mutex = nullptr;
SemaphoreHandle_t s_lifecycle_mutex = nullptr;
// FatFs serializes filesystem operations, but the health probe also sends SD
// commands outside FatFs. Keep each command/data sequence indivisible.
class CardLock {
SemaphoreHandle_t _mutex;
public:
explicit CardLock(SemaphoreHandle_t mutex = s_mutex) : _mutex(mutex) {
xSemaphoreTakeRecursive(_mutex, portMAX_DELAY);
}
~CardLock() { xSemaphoreGiveRecursive(_mutex); }
CardLock(const CardLock&) = delete;
CardLock& operator=(const CardLock&) = delete;
};
uint8_t transfer(uint8_t out) {
uint8_t in = 0;
for (int bit = 7; bit >= 0; --bit) {
REG_WRITE((out >> bit) & 1 ? GPIO_OUT_W1TS_REG : GPIO_OUT_W1TC_REG, kMosiMask);
if (s_slow) ets_delay_us(2);
REG_WRITE(GPIO_OUT_W1TS_REG, kClockMask);
if (s_slow) ets_delay_us(2);
in = (uint8_t)((in << 1) | ((REG_READ(GPIO_IN_REG) >> PIN_SD_MISO) & 1));
REG_WRITE(GPIO_OUT_W1TC_REG, kClockMask);
}
return in;
}
uint8_t receive() { return transfer(0xFF); }
void select() { REG_WRITE(GPIO_OUT_W1TC_REG, kCsMask); }
void release() {
REG_WRITE(GPIO_OUT_W1TS_REG, kCsMask);
receive(); // Clock MISO free after deselecting.
}
bool waitReady(uint32_t timeout) {
const uint32_t start = millis();
do {
if (receive() == 0xFF) return true;
delay(1);
} while ((uint32_t)(millis() - start) < timeout);
return false;
}
uint8_t command(uint8_t cmd, uint32_t arg) {
select();
if (cmd != 0 && cmd != 12 && !waitReady(500)) return 0xFF;
const uint8_t frame[] = {
(uint8_t)(0x40 | cmd), (uint8_t)(arg >> 24), (uint8_t)(arg >> 16),
(uint8_t)(arg >> 8), (uint8_t)arg
};
for (uint8_t byte : frame) transfer(byte);
transfer(CrowPanel35SdCodec::commandCrc(frame, sizeof frame));
if (cmd == 12) receive(); // STOP_TRANSMISSION has a leading stuff byte.
uint8_t response = 0xFF;
for (int i = 0; i < 10; ++i) {
response = receive();
if (!(response & 0x80)) break;
}
return response;
}
uint8_t appCommand(uint8_t cmd, uint32_t arg) {
const uint8_t response = command(55, 0);
return response <= kIdle ? command(cmd, arg) : response;
}
bool readBlock(uint8_t* data, size_t length) {
const uint32_t start = millis();
uint8_t token;
do {
token = receive();
if (token == 0xFF) delay(1);
} while (token == 0xFF && (uint32_t)(millis() - start) < 300);
if (token != kSingle) return false;
uint16_t crc = 0;
for (size_t i = 0; i < length; ++i) {
data[i] = receive();
crc = CrowPanel35SdCodec::dataCrc(crc, data[i]);
}
const uint16_t expected = (uint16_t)receive() << 8;
return (expected | receive()) == crc;
}
bool writeBlock(const uint8_t* data, uint8_t token) {
if (!waitReady(1000)) return false;
transfer(token);
uint16_t crc = 0;
for (size_t i = 0; i < 512; ++i) {
transfer(data[i]);
crc = CrowPanel35SdCodec::dataCrc(crc, data[i]);
}
transfer((uint8_t)(crc >> 8));
transfer((uint8_t)crc);
const bool accepted = (receive() & 0x1F) == 0x05;
const bool ready = waitReady(1000);
return accepted && ready;
}
bool cardInit() {
digitalWrite(PIN_SD_CS, HIGH);
pinMode(PIN_SD_CS, OUTPUT);
pinMode(PIN_SD_SCLK, OUTPUT);
pinMode(PIN_SD_MOSI, OUTPUT);
pinMode(PIN_SD_MISO, INPUT_PULLUP);
REG_WRITE(GPIO_OUT_W1TC_REG, kClockMask);
REG_WRITE(GPIO_OUT_W1TS_REG, kMosiMask);
s_slow = true;
s_type = CARD_NONE;
s_sectors = 0;
for (int i = 0; i < 10; ++i) receive(); // At least 74 power-up clocks, CS high.
uint8_t response = 0xFF;
for (int i = 0; i < 10 && response != kIdle; ++i) {
response = command(0, 0);
release();
if (response != kIdle) delay(10);
}
if (response != kIdle) {
Serial.printf("[sd] soft SPI: CMD0 failed (R1=0x%02X)\n", response);
return false;
}
response = command(59, 1);
release();
if (response == (kIdle | kIllegal)) {
Serial.println("[sd] soft SPI: card does not support write CRC checks");
} else if (response != kIdle) {
Serial.printf("[sd] soft SPI: CRC enable failed (R1=0x%02X)\n", response);
return false;
}
sdcard_type_t type = CARD_NONE;
uint32_t start = millis();
response = command(8, 0x1AA);
if (response == kIdle) {
uint8_t echo[4];
for (auto& byte : echo) byte = receive();
release();
if (echo[2] != 0x01 || echo[3] != 0xAA) {
Serial.println("[sd] soft SPI: CMD8 voltage/check pattern mismatch");
return false;
}
do {
response = appCommand(41, 1UL << 30);
release();
if (response == kIdle) delay(1);
} while (response == kIdle && (uint32_t)(millis() - start) < 1000);
if (response != kReady) {
Serial.printf("[sd] soft SPI: ACMD41 failed (R1=0x%02X)\n", response);
return false;
}
response = command(58, 0);
if (response != kReady) {
release();
Serial.printf("[sd] soft SPI: CMD58 failed (R1=0x%02X)\n", response);
return false;
}
uint8_t ocr[4];
for (auto& byte : ocr) byte = receive();
release();
if (!(ocr[0] & 0x80) || !(ocr[1] & 0x10)) {
Serial.println("[sd] soft SPI: card not ready for 3.3 V operation");
return false;
}
type = (ocr[0] & 0x40) ? CARD_SDHC : CARD_SD;
} else {
release();
if (response != (kIdle | kIllegal)) {
Serial.printf("[sd] soft SPI: CMD8 failed (R1=0x%02X)\n", response);
return false;
}
response = appCommand(41, 0);
release();
if (response <= kIdle) {
type = CARD_SD;
while (response == kIdle && (uint32_t)(millis() - start) < 1000) {
delay(1);
response = appCommand(41, 0);
release();
}
} else {
type = CARD_MMC;
start = millis();
do {
response = command(1, 0);
release();
if (response == kIdle) delay(1);
} while (response == kIdle && (uint32_t)(millis() - start) < 1000);
}
if (response != kReady) {
Serial.printf("[sd] soft SPI: legacy init failed (R1=0x%02X)\n", response);
return false;
}
}
if (type != CARD_SDHC) {
response = command(16, 512);
release();
if (response != kReady) {
Serial.printf("[sd] soft SPI: CMD16 failed (R1=0x%02X)\n", response);
return false;
}
}
uint8_t csd[16];
const bool csd_ok = command(9, 0) == kReady && readBlock(csd, sizeof csd);
release();
if (!csd_ok) {
Serial.println("[sd] soft SPI: CSD read/CRC failed");
return false;
}
s_sectors = CrowPanel35SdCodec::sectorsFromCsd(csd);
if (!s_sectors || (type != CARD_SDHC && s_sectors > UINT32_MAX / 512UL + 1)) {
Serial.println("[sd] soft SPI: invalid or unsupported capacity");
s_sectors = 0;
return false;
}
s_type = type;
s_slow = false;
return true;
}
bool cardReady() {
if (s_type == CARD_NONE) return false;
const uint8_t response = command(13, 0);
const uint8_t status = response == kReady ? receive() : 0xFF;
release();
return response == kReady && status == 0;
}
bool readSectors(uint8_t* data, uint32_t sector, unsigned count) {
const uint32_t address = s_type == CARD_SDHC ? sector : sector * 512;
bool ok;
if (count == 1) {
ok = command(17, address) == kReady && readBlock(data, 512);
} else {
ok = command(18, address) == kReady;
if (ok) {
for (unsigned i = 0; ok && i < count; ++i) ok = readBlock(data + i * 512, 512);
const bool stopped = command(12, 0) == kReady;
const bool ready = waitReady(500);
ok = ok && stopped && ready;
}
}
release();
return ok;
}
bool writeSectors(const uint8_t* data, uint32_t sector, unsigned count) {
const uint32_t address = s_type == CARD_SDHC ? sector : sector * 512;
bool ok;
if (count == 1) {
ok = command(24, address) == kReady && writeBlock(data, kSingle);
} else {
ok = command(25, address) == kReady;
if (ok) {
for (unsigned i = 0; ok && i < count; ++i) ok = writeBlock(data + i * 512, kMulti);
if (waitReady(1000)) {
transfer(kStop); // Only an accepted CMD25 owes a stop token.
receive();
const bool ready = waitReady(1000);
ok = ok && ready;
} else {
ok = false;
}
}
}
release();
return ok;
}
DSTATUS diskStatus(unsigned char) {
CardLock lock;
return cardReady() ? 0 : STA_NOINIT;
}
DSTATUS diskInit(unsigned char drive) { return diskStatus(drive); }
DRESULT diskRead(unsigned char, unsigned char* data, uint32_t sector, unsigned count) {
CardLock lock;
if (s_type == CARD_NONE) return RES_NOTRDY;
if (!data || !CrowPanel35SdCodec::validRange(s_sectors, sector, count)) {
Serial.printf("[sd] soft SPI: invalid read sector=%lu count=%u\n", (unsigned long)sector, count);
return RES_PARERR;
}
if (readSectors(data, sector, count) || readSectors(data, sector, count)) return RES_OK;
Serial.printf("[sd] soft SPI: read failed sector=%lu count=%u\n", (unsigned long)sector, count);
return RES_ERROR;
}
DRESULT diskWrite(unsigned char, const unsigned char* data, uint32_t sector, unsigned count) {
CardLock lock;
if (s_type == CARD_NONE) return RES_NOTRDY;
if (!data || !CrowPanel35SdCodec::validRange(s_sectors, sector, count)) {
Serial.printf("[sd] soft SPI: invalid write sector=%lu count=%u\n", (unsigned long)sector, count);
return RES_PARERR;
}
if (writeSectors(data, sector, count) || writeSectors(data, sector, count)) return RES_OK;
Serial.printf("[sd] soft SPI: write failed sector=%lu count=%u\n", (unsigned long)sector, count);
return RES_ERROR;
}
DRESULT diskIoctl(unsigned char, unsigned char cmd, void* data) {
CardLock lock;
if (s_type == CARD_NONE) return RES_NOTRDY;
if (cmd != CTRL_SYNC && !data) {
Serial.printf("[sd] soft SPI: missing ioctl output for %u\n", (unsigned)cmd);
return RES_PARERR;
}
switch (cmd) {
case CTRL_SYNC: {
select();
const bool ready = waitReady(500);
release();
if (!ready) Serial.println("[sd] soft SPI: sync timed out");
return ready ? RES_OK : RES_ERROR;
}
case GET_SECTOR_COUNT: *(DWORD*)data = s_sectors; return RES_OK;
case GET_SECTOR_SIZE: *(WORD*)data = 512; return RES_OK;
case GET_BLOCK_SIZE: *(DWORD*)data = 1; return RES_OK;
default:
Serial.printf("[sd] soft SPI: unsupported ioctl %u\n", (unsigned)cmd);
return RES_PARERR;
}
}
const ff_diskio_impl_t kDiskImpl = { diskInit, diskStatus, diskRead, diskWrite, diskIoctl };
void driveName(char (&name)[8], uint8_t drive) { snprintf(name, sizeof name, "%u:", (unsigned)drive); }
bool space(uint64_t& total, uint64_t& used) {
const uint8_t drive = SD.driveNumber();
if (drive == FF_DRV_NOT_USED) return false;
char name[8];
driveName(name, drive);
FATFS* fat = nullptr;
DWORD free_clusters;
// Do not hold CardLock while acquiring FatFs' volume lock: disk callbacks
// acquire CardLock in the opposite order.
const FRESULT result = f_getfree(name, &free_clusters, &fat);
if (result != FR_OK || !fat || fat->n_fatent < 2 || free_clusters > fat->n_fatent - 2) {
Serial.printf("[sd] soft SPI: free-space read failed (%u)\n", (unsigned)result);
return false;
}
total = (uint64_t)(fat->n_fatent - 2) * fat->csize * 512ULL;
used = total - (uint64_t)free_clusters * fat->csize * 512ULL;
return true;
}
} // namespace
CrowPanel35SD SD;
CrowPanel35SD::CrowPanel35SD() : fs::FS(fs::FSImplPtr(new VFSImpl())) {}
bool CrowPanel35SD::begin() {
if (!s_mutex) s_mutex = xSemaphoreCreateRecursiveMutex();
if (!s_lifecycle_mutex) s_lifecycle_mutex = xSemaphoreCreateRecursiveMutex();
if (!s_mutex || !s_lifecycle_mutex) {
Serial.println("[sd] soft SPI: mutex allocation failed");
return false;
}
CardLock lifecycle(s_lifecycle_mutex);
{
CardLock lock;
if (s_mounted) return true;
}
{
CardLock lock;
if (!cardInit()) return false;
}
const esp_err_t drive_error = ff_diskio_get_drive(&s_drive);
if (drive_error != ESP_OK || s_drive == FF_DRV_NOT_USED) {
Serial.printf("[sd] soft SPI: no FatFs drive (%s)\n", esp_err_to_name(drive_error));
end();
return false;
}
ff_diskio_register(s_drive, &kDiskImpl);
char name[8];
driveName(name, s_drive);
FATFS* fat = nullptr;
const esp_err_t vfs_error = esp_vfs_fat_register(kMountPoint, name, kMaxOpenFiles, &fat);
if (vfs_error != ESP_OK) {
Serial.printf("[sd] soft SPI: VFS registration failed (%s)\n", esp_err_to_name(vfs_error));
end();
return false;
}
s_vfs_registered = true;
const FRESULT mount_error = f_mount(fat, name, 1);
if (mount_error != FR_OK) {
Serial.printf("[sd] soft SPI: FAT mount failed (%u); card left unchanged\n", (unsigned)mount_error);
end();
return false;
}
_impl->mountpoint(kMountPoint);
{
CardLock lock;
s_mounted = true;
}
Serial.printf("[sd] soft SPI mounted: %llu MB\n", (unsigned long long)(cardSize() / (1024ULL * 1024)));
return true;
}
void CrowPanel35SD::end() {
if (!s_lifecycle_mutex) return; // No begin() has created a mount.
CardLock lifecycle(s_lifecycle_mutex);
_impl->mountpoint(nullptr);
if (s_vfs_registered) {
char name[8];
driveName(name, s_drive);
const FRESULT result = f_mount(nullptr, name, 0);
if (result != FR_OK) Serial.printf("[sd] soft SPI: FAT unmount failed (%u)\n", (unsigned)result);
const esp_err_t vfs_error = esp_vfs_fat_unregister_path(kMountPoint);
if (vfs_error != ESP_OK)
Serial.printf("[sd] soft SPI: VFS unregister failed (%s)\n", esp_err_to_name(vfs_error));
}
if (s_drive != FF_DRV_NOT_USED) ff_diskio_unregister(s_drive);
if (!s_mutex) return;
CardLock lock;
s_drive = FF_DRV_NOT_USED;
s_type = CARD_NONE;
s_sectors = 0;
s_mounted = false;
s_vfs_registered = false;
}
sdcard_type_t CrowPanel35SD::cardType() const {
if (!s_mutex) return CARD_NONE;
CardLock lock;
return s_mounted ? s_type : CARD_NONE;
}
uint64_t CrowPanel35SD::cardSize() const {
if (!s_mutex) return 0;
CardLock lock;
return s_mounted ? (uint64_t)s_sectors * 512ULL : 0;
}
uint8_t CrowPanel35SD::driveNumber() const {
if (!s_mutex) return FF_DRV_NOT_USED;
CardLock lock;
return s_mounted ? s_drive : FF_DRV_NOT_USED;
}
bool CrowPanel35SD::probeAlive() const {
if (!s_mutex) return false;
CardLock lock;
return s_mounted && cardReady();
}
uint64_t CrowPanel35SD::totalBytes() const {
uint64_t total = 0, used = 0;
return space(total, used) ? total : 0;
}
uint64_t CrowPanel35SD::usedBytes() const {
uint64_t total = 0, used = 0;
return space(total, used) ? used : 0;
}