Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions platform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,18 @@ list builds stayed byte-identical. Not yet run on a board.

## Changes since the copies were taken

- 2026-10-08: CHSd clocks one idle byte before each command (`cmd()` in
`SdSpi.cpp`), the SD spec's N_RC: at least 8 clocks between a response
and the next command. Identification sent its commands back to back
(`read()` and `stream()` already waited first). A SanDisk 32 GB card
("SK32G") took them misaligned and stopped answering, so CHStlView and
the word games found no card where the menu, whose `sd.c` always sent the
byte, read it. Contributed with a board run on that card (PR #21).
`tests/test_spi.cpp`'s card model now counts a command sent with no gap
as a failure (11 or 12 per init without the fix). +8 B in each of the
four sketches on CHSd (CHWords, CHWordWheel, CHCrossword, CHStlView),
RAM unchanged; the simulator does not run `SdSpi.cpp`.

- 2026-10-08: the three libraries link from archives (`dot_a_linkage=true`
in CHGfx's, CHGame's and CHSd's `library.properties`). A developer using
only `audio::` and the buttons, with their own display code, found every
Expand Down
9 changes: 9 additions & 0 deletions platform/board/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ file as the GitHub release notes.

### Fixed

- **CHSd works with more microSD cards.** It sent each command right after
the card's previous response, where the SD Physical Layer Simplified
Specification asks for at least 8 clocks in between (N_RC, SPI mode
timing). Many cards do without them; a strict one (a SanDisk 32 GB,
"SK32G") took the next command misaligned and stopped answering, so every
sketch on CHSd (CHStlView, the word games' card files) said there was no
card while the SD menu bootloader read it. `cmd()` now clocks one idle
byte first, as the bootloader's `sd.c` does: one byte per command, not per
block.
- **EEPROM no longer writes the CH32X035's option bytes.** The library is
the CH32V003's: its `commit()` erased the option-byte block and rewrote it
a half-word at a time, which the CH32X035 is not documented to accept. A
Expand Down
9 changes: 9 additions & 0 deletions platform/board/arduino/CHGame/libraries/CHSd/src/SdSpi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,16 @@ static uint8_t wait(bool tok) {
// bit. Only CMD0 and CMD8 are checked by a card in SPI mode (CRC is left
// off): their CRCs are the two constants. R1 arrives within 8 bytes; bit 7
// set means nothing answered.
//
// One idle byte goes first: the SD spec's N_RC, at least 8 clocks between a
// response and the next command (Physical Layer Simplified Specification,
// SPI mode timing, 7.5). Many cards do without it; a strict one (a SanDisk
// 32 GB, "SK32G") is still finishing its last response, takes the command
// misaligned (CMD0 answered 01, then C1 7F, then every answer 6 bits off)
// and stops answering until it loses power. The bootloader's sd.c does the
// same. It costs one byte per command, not per block.
static uint8_t cmd(uint8_t c, uint32_t arg) {
xfer(0xFF);
xfer((uint8_t)(0x40 | c));
for (int sh = 24; sh >= 0; sh -= 8) xfer((uint8_t)(arg >> sh));
xfer(c == 8 ? 0x87 : 0x95);
Expand Down
13 changes: 11 additions & 2 deletions platform/board/arduino/CHGame/libraries/CHSd/tests/test_spi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
// card keeps across an MCU reset (it stays powered): its state and whether
// CRC checking is on. CHSDtoUSB turns checking on, and the model's CMD0 does
// not turn it off, the worst case (the bootloader's test/native/sd_model.c
// assumes the same). tests/run_tests.py builds and runs it.
// assumes the same). It is also a strict card about N_RC: a command must not
// start on the byte right after the last byte of a response (some cards,
// such as a SanDisk 32 GB "SK32G", take it misaligned and stop answering).
// tests/run_tests.py builds and runs it.
#include <stdio.h>
#include <string.h>
#include "Arduino.h"
Expand Down Expand Up @@ -36,7 +39,8 @@ struct Card {
enum State { POWERUP, IDLE, READY } state = POWERUP;
bool crcOn = false, acmd = false;
int acmd41Left = 0;
uint32_t clocksCsHigh = 0, badCrc = 0, fastIdent = 0, conflicts = 0, blocks = 1024;
uint32_t clocksCsHigh = 0, badCrc = 0, fastIdent = 0, conflicts = 0, noGap = 0, blocks = 1024;
bool justAnswered = false; // the last byte out ended a response
uint8_t cmd[6];
int cmdLen = 0;
uint8_t out[600];
Expand Down Expand Up @@ -96,10 +100,14 @@ struct Card {
if (type == NONE) return 0xFF;
if (!csLow) {
if (clocksCsHigh < 1000) clocksCsHigh += 8;
justAnswered = false;
return 0xFF; // DO released: the pull-up
}
bool noRc = justAnswered;
justAnswered = outPos + 1 == outLen;
uint8_t o = outPos < outLen ? out[outPos++] : 0xFF;
if (cmdLen == 0) {
if ((in & 0xC0) == 0x40 && noRc) noGap++; // N_RC: 8 clocks at least
if ((in & 0xC0) == 0x40) cmd[cmdLen++] = in;
} else {
cmd[cmdLen++] = in;
Expand Down Expand Up @@ -140,6 +148,7 @@ static void initAndRead(const char *name, bool expectOk) {
CHECK(t_spi.CTLR1 == lcd, "SPI1 not handed back as CHGfx left it");
CHECK(card.conflicts == 0, "the panel and the card selected together");
CHECK(card.fastIdent == 0, "identification above 400 kHz");
CHECK(card.noGap == 0, "%u commands without N_RC", (unsigned)card.noGap);
CHECK(card.badCrc == 0, "%u commands refused for their CRC", (unsigned)card.badCrc);
if (!expectOk) {
CHECK(now_us - t0 < 20000, "an empty slot took %u us", (unsigned)(now_us - t0));
Expand Down