Builtin Sd-Card Teensy 4.1 - SD.begin() errors

tompilot

Member
I have developed a device that uses Teensy 4.1 and enables the user to read and write data onto the internal SD card.
There are some cases where SD.begin() returns false.

I just finished the first 30 units and use the same type of FAT32-formatted SD-cards in all of them (Intenso microSDHC Class 4, 4GB: LINK). In about 5/30 units i have problems with the SD-card.

When initializing the SD-card, sometimes SD.begin(BUILTIN_SDCARD) returns false, like 3/10 of access attempts. This seems to be dependent on the SD-card inserted, some cards do not produce any errors while others do. For this first release i made some sort of brute-force workaround by simply trying SD.begin a few times after each other. In most cases, if the first attempt fails the second attempt is successful.

I dug deeper into the library and found out, that in the function
SdioCard::begin(SdioConfig sdioConfig) --> cardCMD6()
there are two kinds of errors produced:

SD_CARD_ERROR_CMD6 "Switch card function"<-- more often
SD_CARD_ERROR_DMA "DMA transfer failed" <-- rarely

If no errors occur, the SD clock frequency kHzSdClk is set to 50000.
I also have a bunch of low quality 64MB SD-cards from alibaba here with FAT16 formatting which used to work well on a very old version of the SD library (I don't remember which one...) but now produce the error code on most attempts of SD.begin(). They also work find on a computer.
ACMD41, "Activate card initialization"

My goal is to get my code as robust as possible for any kind of FAT-formatted SD-card that any user might insert. All of the above cards work well on a computer. Write and read speed are not so important as my files have only a few kByte.

I am also using:
- I2S audio codec
- OpenAudio library (32bit version of the regular Audio lib, but relies on the same principles)
- ADC with interrupt timer, priority 208 and 224
- WS2812Serial library
- Adafruit SSD1306 library (I2C display)
- Encoder library
- external PSRAM chip APS6404

I am actually writing my code in VScode and PlatformIO. But as their version of Teensy core and libraries usually are far behind, i also installed Teensyduino 1.56 and use the Teensyduino /cores/ and /libraries/ folder instead of the PlatformIO bundled versions.

Here is my caller code:

Code:
#define NR_OF_SD_TRIES_MAX 4U

SDClass SD;

// Workaround to give the SD-card begin a few more tries. Each try causes a delay of 1 second, probably from BUSY_TIMEOUT_MICROS = 1000000 in SdioTeensy.cpp
uint8_t ct=0;
bool success = false;
while(ct < NR_OF_SD_TRIES_MAX && !success) {
        if(SD.begin((uint8_t)(BUILTIN_SDCARD))) {
            success = true;
            break;
        }
        ct++;
}
if(!success) {
        Serial.println("MenuHandler::readGlobalDataFromSdCard(): initialization failed!");
        return 2;
}
if(ct > 0) {
        Serial.printf("MenuHandler::readGlobalDataFromSdCard(): Needed more tries to open SD-card: %i\n", ct);
}

In SdioTeensy.cpp i enabled USE_DEBUG_MODE and added some debug output in SdFat.h
Debug output for an attempt where the first try of SD.begin fails and the second one succeeds:

Code:
LINE: 420
BLKATTR 10040
XFERTYP 63A0000 CMD6 TYP0 DPSEL
PRSSTAT FF888088 SDOFF SDSTB
PROCTL 8800022 EMODE2 DWT1
IRQSTAT 0
m_irqstat B

SdioCard::begin(): kHzSdClk = 25000
SdFat.h SdBase::cardBegin(SdioConfig): m_card->errorCode() = 4
SdFat.h SdBase::begin(): cardBegin success: 0, Vol::begin success: 1

SdioCard::begin(): kHzSdClk = 50000
SdFat.h SdBase::cardBegin(SdioConfig): m_card->errorCode() = 0
SdFat.h SdBase::begin(): cardBegin success: 1, Vol::begin success: 1

MenuHandler::readGlobalDataFromSdCard(): Needed more tries to open SD-card: 1

Thank you for any hint! :)
 
There have been a few changes to the initialization scheme over the years. The two most significant being the SD V1 specification (update from MMC) and the V2 spec. (You might want to have a copy on hand: https://www.sdcard.org/downloads/pls/ It is up to V8 now.)

I looked over the init code and I don't see anything wrong. Well, other than MMC not being an option. CMD8 will separate the V2.0+ SD cards from the pack (set high capacity bit in ACMD41). Failure to respond to ACMD41 means you aren't an SD card. You might want to check the variable m_irqstat in CardCommand() to see which particular error is happening with ACMD41. (This is where a debugger would be nice.) One second should be more than enough time to initialize a card.

The specification does say that cards should initialize in no more than 1 second but the timeout should be greater than 1 second. (4.2.3) Also, it isn't required to send continuous ACMD41s. Just at least every 50ms. (4.4 Clock Control)

Some of those older cards could be a bit liberal with the specification. I ran into a few that didn't bother to echo the check pattern in the CMD8 argument but just sent 0xAA as though there were no other possibility.
 
Thank you so much for sharing your experiences. Will dig deeper into SD CMDs!

By now i made some improvements by following these basic rules:

- call SD.begin only once in the whole project, in setup().
- if SD.begin fails the first time, try again max three more times
- if we succeed at one of the three re-tries we are good to go, otherwise accept that SD.begin failed
- after SD.begin succeeded, check by calling SD.mediaPresent() if there is actually an SD-card in the socket
- before reading or writing to or from the card, always check if SD.mediaPresent(), it could be that the user unplugged the SD-card in the meantime.
- if SD.mediaInserted() fails, maybe give an error message to the user and let the user press a button for example to retry. Retry calls the SD.begin() routine again like in the beginning, with three-times re-try if neccessary.
- It may happen, that someone unplugs the SD-card, tries to access it (which fails) and then re-inserts it. In this case SD.mediaInserted() still returns false --> need to do SD.begin() again, then SD.mediaPresent() will return true.
 
I stumbled across this problem again and found the solution!

In the file SdioTeensy.cpp, there is a section in the function bool SdioCard::begin(SdioConfig sdioConfig)

Code:
    // Determine if High Speed mode is supported and set frequency.
    // Check status[16] for error 0XF or status[16] for new mode 0X1.
    uint8_t status[64];
    if (cardCMD6(0X00FFFFFF, status) && (2 & status[13]) &&
        cardCMD6(0X80FFFFF1, status) && (status[16] & 0XF) == 1) {
      kHzSdClk = 50000;
    } else {
      kHzSdClk = 25000;
    }

Teensy "asks" the SD-card if High speed mode is supported by calling cardCMD6(0X00FFFFFF, status) and then checks its response by checking (2 & status[13]). If the response is "yes", Teensy requests a switch to Highspeed mode by calling cardCMD6(0X80FFFFF1, status) and checks the card's response by (status[16] & 0XF) == 1.

However, if an SD-card is not highspeed, it won't respond to the switch request cardCMD6(0X80FFFFF1, status), a timeout occurs and the calling function returns an error.

The way the code is written requires the calls in a specific order, but it seems that at least my compiler (PlatformIO + GCC + build_flags -D TEENSY_OPT_FASTER) might switch the order. Calling cardCMD6(0X80FFFFF1, status) on a non-highspeed SD-card will result in a timeout.

So i rewrote the section. I tested with several non-highspeed cards, however i do not own a highspeed card and could not test that.

Maybe this is worth a change in SdioTeensy.cpp ...

Code:
    // Determine if High Speed mode is supported and set frequency.
    // Check status[16] for error 0XF or status[16] for new mode 0X1.
    uint8_t status[64];
    bool highSpeedModeAsk = cardCMD6(0X00FFFFFF, status); // Ask if highspeed mode supported. Returns true if SD card reacts to the command within timeout.
    bool highspeedModeSupported = highSpeedModeAsk & (2 & status[13]); // Check the SD-card's response. This bit must be set.
    
    // Remember
    uint8_t err_code_before = m_errorCode; 
    uint32_t m_errorLine_before = m_errorLine;

    // WARNING: This line would return false after a timeout if it is not a highspeed SD card:
    // bool switchRequestAsk = cardCMD6(0X80FFFFF1, status);
    // It produces a timeout when sending the command and waiting for the card's response in
    // cardCommand() --> if(waitTimeout(isBusyCommandComplete))

    // Solution: Do this workaround below to safely ask for a switch request and accept in this case if there is no response.
    bool switchRequestAsk = false;
    bool switchRequestDone = false;
    kHzSdClk = 25000;
    if (highspeedModeSupported) {
      
      switchRequestAsk = cardCMD6(0X80FFFFF1, status); // Switch to highspeed mode request. Returns true if SD card reacts to the command within timeout.
      switchRequestDone = switchRequestAsk & ((status[16] & 0XF) == 1);   // Check the SD-card's response. This bit must be set.
      
      if (highspeedModeSupported && switchRequestDone) {
        kHzSdClk = 50000;
      }

      // Extra safety: Maybe there are cards that say they support highspeed mode, but won't respond to a switch request. 
      // If it says that highspeed mode was supported, but the card did not react to the switch request, we accept the timeout in this case and proceed with non-highspeed mode.
      // We also need to revert to the error code from before, as cardCMD6(0X80FFFFF1, status) will change the error code & line otherwise.
      if (!switchRequestAsk || !switchRequestDone) {
        m_errorCode = err_code_before;
        m_errorLine = m_errorLine_before;
      }

    }

    // With non-highspeed SD-cards: 
    // kHzSdClk = 25000, err = 0, highspeedModeSupported = 0, switchedToHighspeedMode = 0, status[13] = 0x03, status[16] = 0x00
    if (USE_DEBUG_MODE) {
      Serial.printf("kHzSdClk = %i, err = %i, highspeedMode: Ask=%i, Supported=%i, switchedToHighspeedMode: Ask=%i, done=%i, status[13] = 0x%02x, status[16] = 0x%02x\n", 
                    kHzSdClk, m_errorCode, 
                    (int)highSpeedModeAsk, (int)highspeedModeSupported, 
                    (int)switchRequestAsk, (int)switchRequestDone, 
                    status[13], status[16]);

    }
 
I added this to Teensy's copy of SdFat. It will be in future Teensyduino starting with 1.59-beta4 (0.59.4 in Boards Manager).

https://github.com/PaulStoffregen/SdFat/commit/63e6da5ac921cf13d1f356f7f5131328fec373c0

I also spent some time (quite a few hours) looking into why some very old cards don't initialize. Watching the waveforms, it's clear something goes wrong after CMD8. Why exactly, I don't know. But reinitializing the hardware and sending CMD0 (reset to idle mode) seems to be an effective (and hopefully safe) workaround. Also added.

https://github.com/PaulStoffregen/SdFat/commit/1ecee6942a435cfc00a3a81673a5a888c4749c02
 
Cool, I am glad my code made it into the library :)
In the meantime... I got a bunch of brand new 4GB Intenso SD cards and turns out they would sometimes return false (due to timeout) for cardCMD6() after they are re-inserted into the SD card slot while the Teensy program is running. This only happens the first time a CMD6 is performed, when calling cardCMD6 a second time the response is received. Does that make any sense...?
My solution is, that when cardCMD6 returns false, just give it a second try. Actually i give it a couple more tries as i don't know what kind of wonky cards my end users might put into their devices...

So, this is the code i am currently using successfully.
Any thoughts on this are highly appreciated :)

Code:
uint8_t status[64];
kHzSdClk = 25000;
bool highSpeedConfirmedAsked = false;
bool highSpeedConfirmed = false;
bool highSpeedAvailableAsked = cardCMD6(0X00FFFFFF, status);
bool highSpeedAvailable = highSpeedAvailableAsked && (2 & status[13]);

// This is important. After re-insert, sometimes the card only responds on the second attempt.
// Anyways, give it some more tries...
uint8_t cnt = 0;
while (!highSpeedAvailableAsked && cnt < 3) {
  ++cnt;
  Serial.printf("highspeed: no answer on speed available request. cnt = %i\n", cnt);
  highSpeedAvailableAsked = cardCMD6(0X00FFFFFF, status);
  highSpeedAvailable = highSpeedAvailableAsked && (2 & status[13]);
}

if (highSpeedAvailable) {
  // It may happen after re-insert that highSpeedAvailable == true, but no answer on the line below.
  highSpeedConfirmedAsked = cardCMD6(0X80FFFFF1, status);
  highSpeedConfirmed = highSpeedConfirmedAsked && ((status[16] & 0XF) == 1);

  // This is important. After re-insert, sometimes the card only responds on the second attempt. Give it two tries...
  // Anyways, give it some more tries...
  cnt = 0;
  while (!highSpeedConfirmedAsked && cnt < 3) {
    ++cnt;
    Serial.printf("highspeed: no answer on speed change request. cnt = %i\n", cnt);
    highSpeedConfirmedAsked = cardCMD6(0X80FFFFF1, status);
    highSpeedConfirmed = highSpeedConfirmedAsked && ((status[16] & 0XF) == 1);
  }
 
  // Reset SD card error in any case, necessary if first attempt failed.
  sdError(SD_CARD_ERROR_NONE);
}

if (highSpeedConfirmed) {
  kHzSdClk = 50000;
}
 
Is the only consequence that we end up using 25 MHz for a card which would have worked at 50 MHz, had it replied correctly the first time?
 
I did some more testing with many different cards now and my last provided code snippet seems to work fine. It is very rare that a card will not respond to CMD6 after re-inserting, but it happened. The re-try mechanism always successfully made the card respond. I also tested what would happen if kHzSdClk stays at 25000 for a card that could handle 50000 by simply skipping the cmd6. In my tests that was not a problem, the card worked anyways.
 
I 'm trouble-shooting an issue with writing to SD using SdFat, and I noticed the code below in SdioCard::begin(SdioConfig sdioConfig). A signed int is used, which I think can cause CARD_ERROR_ACMD41 to be incorrectly returned, depending on the value of micros(). I don't know if this could have anything to do with what you guys have been working on, but thought I should mention it.

Code:
  arg = m_version2 ? 0X40300000 : 0x00300000;
  int m = micros();
  do {
    if (!cardAcmd(0, ACMD41_XFERTYP, arg) ||
       ((micros() - m) > BUSY_TIMEOUT_MICROS)) {
      return sdError(SD_CARD_ERROR_ACMD41);
    }
  } while ((SDHC_CMDRSP0 & 0x80000000) == 0);
 
That mixing of signed/unsigned should be triggering a compiler warning - wonder if there are lots of other warnings?
 
Back
Top