diff --git a/include/drv/sd_spi.h b/include/drv/sd_spi.h new file mode 100644 index 0000000..a18bd38 --- /dev/null +++ b/include/drv/sd_spi.h @@ -0,0 +1,720 @@ +/*! + * \file drv/sd_spi.h + * \brief + * SD card driver using SPI interface + * + * \copyright Copyright (C) 2021 Christos Choutouridis + * + *
License
+ * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + *
+ */ + +#ifndef TBX_DRV_SD_SPI_H_ +#define TBX_DRV_SD_SPI_H_ + +#include +#include + +//#include + +#include +#include + +namespace tbx { + +/*! + * + * http://elm-chan.org/docs/mmc/mmc_e.html + * + * CRTP requirements + * bool WP_impl (); // write protect, true => write protect + * bool CD_impl (); // check disk present, true => present + * void CS_impl (bool select); // Chip select, true => select + * void PWR_impl(bool state); // SD power, true => power the card + * data_type SPI_rw_impl (data_type); // SPI read-write functionality + * bool SPI_set_clk_impl(uint32_t clk); // SPI set clock functionality + * clock_t clock_impl(); // get system's CPU time + */ +template +class sd_card { + _CRTP_IMPL(Impl_t); + + using data_type = uint8_t; + + // Driver settings + constexpr static clock_t SD_WaitTimeout = 500; // 500 [CPU time] + constexpr static clock_t SD_PowerTimeout= 250; // 250 [CPU time] + constexpr static clock_t SD_RxTimeout = 100; // 100 [CPU time] + constexpr static clock_t SD_InitTimeout = 2000; // 2000 [CPU time] + constexpr static uint32_t MaxInitClock = 400000; // 400000 [Hz] + + // MMC/SDC definitions + constexpr static data_type CMD_MSB = 0x40; + constexpr static data_type CMD_CRC_LSB = 0x01; + + constexpr static data_type CMD0 = (CMD_MSB | 0); //!< GO_IDLE_STATE + constexpr static data_type CMD1 = (CMD_MSB | 1); //!< SEND_OP_COND (MMC) + constexpr static data_type CMD8 = (CMD_MSB | 8); //!< SEND_IF_COND + constexpr static data_type CMD9 = (CMD_MSB | 9); //!< SEND_CSD + constexpr static data_type CMD10 = (CMD_MSB | 10); //!< SEND_CID + constexpr static data_type CMD12 = (CMD_MSB | 12); //!< STOP_TRANSMISSION + constexpr static data_type CMD16 = (CMD_MSB | 16); //!< SET_BLOCKLEN + constexpr static data_type CMD17 = (CMD_MSB | 17); //!< READ_SINGLE_BLOCK + constexpr static data_type CMD18 = (CMD_MSB | 18); //!< READ_MULTIPLE_BLOCK + constexpr static data_type CMD23 = (CMD_MSB | 23); //!< SET_BLOCK_COUNT (MMC) + constexpr static data_type CMD24 = (CMD_MSB | 24); //!< WRITE_BLOCK + constexpr static data_type CMD25 = (CMD_MSB | 25); //!< WRITE_MULTIPLE_BLOCK + constexpr static data_type CMD55 = (CMD_MSB | 55); //!< APP_CMD + constexpr static data_type CMD58 = (CMD_MSB | 58); //!< READ_OCR + + constexpr static data_type ACMD13 = (0xC0 + 13); //!< SD_STATUS (SDC) + constexpr static data_type ACMD23 = (0xC0 + 23); //!< SET_WR_BLK_ERASE_COUNT (SDC) + constexpr static data_type ACMD41 = (0xC0 + 41); //!< SEND_OP_COND (SDC) + + constexpr static data_type R1_READY_STATE = 0x00; //!< status for card in the ready state + constexpr static data_type R1_IDLE_STATE = 0x01; //!< status for card in the idle state + constexpr static data_type R1_ILLEGAL_COMMAND = 0x04; //!< status bit for illegal command + constexpr static data_type DATA_START_BLOCK = 0xFE; //!< start data token for read or write single block + constexpr static data_type STOP_TRAN_TOKEN = 0xFD; //!< stop token for write multiple blocks + constexpr static data_type WRITE_MULTIPLE_TOKEN= 0xFC; //!< start data token for write multiple blocks + constexpr static data_type DATA_RES_MASK = 0x1F; //!< mask for data response tokens after a write block operation + constexpr static data_type DATA_RES_ACCEPTED = 0x05; //!< write data accepted token + + // MMC card type flags (MMC_GET_TYPE) + //! \note + //! These types are compatible with FatFS types + constexpr static data_type CT_NONE = 0x00; + constexpr static data_type CT_MMC = 0x01; //!< MMC ver 3 + constexpr static data_type CT_SD1 = 0x02; //!< SD ver 1 + constexpr static data_type CT_SD2 = 0x04; //!< SD ver 2 + constexpr static data_type CT_SDC = (CT_SD1|CT_SD2); //!< SD + constexpr static data_type CT_BLOCK = 0x08; //!< Block addressing + +public: + + enum status_t : uint8_t { + ST_OK =0, + ST_NOINIT = 1, + ST_NODISK = 2, + ST_WRPROTECT = 3, + ST_ERROR = 4 + }; + + enum ioctl_cmd { + // Fatfs compatibility + IOCTL_SYNC =0, //!< Flush disk cache (for write functions) + IOCTL_GET_SECTOR_COUNT =1, //!< Get media size (for only f_mkfs()) + IOCTL_GET_SECTOR_SIZE =2, //!< Get sector size (for multiple sector size (_MAX_SS >= 1024)) + IOCTL_GET_BLOCK_SIZE =3, //!< Get erase block size (for only f_mkfs()) + IOCTL_ERASE_SECTOR =4, //!< Force erased a block of sectors (for only _USE_ERASE) + + // Generics + IOCTL_POWER =5, //!< Get/Set power status + IOCTL_LOCK =6, //!< Lock/Unlock media removal + IOCTL_EJECT =7, //!< Eject media + IOCTL_FORMAT =8, //!< Create physical format on the media + + // SD/MMC specific + IOCTL_MMC_GET_TYPE =10, //!< Get card type + IOCTL_MMC_GET_CSD =11, //!< Get CSD + IOCTL_MMC_GET_CID =12, //!< Get CID + IOCTL_MMC_GET_OCR =13, //!< Get OCR + IOCTL_MMC_GET_SDSTAT =14, //!< Get SD status + }; + +public: + sd_card() : + status{ST_NOINIT} { } + + sd_card(const sd_card&) = delete; + sd_card& operator=(const sd_card&) = delete; + +private: + + /*! + * \brief + * Calculate the maximum data transfer rate per one data line + * from the CSD. + * TRAN_SPEED is the CSD[103..96] + * + * TRAN_SPEED bit code + * --------------------------------------------------- + * 2:0 | transfer rate unit + * | 0=100kbit/s, 1=1Mbit/s, 2=10Mbit/s, + * | 3=100Mbit/s, 4... 7=reserved + * --------------------------------------------------- + * 6:3 | time value + * -------------------------------------------------- + * 7 | Reserved + * + * \param csd Pointer to CSD array 128bit. + * \return The maximum spi baud rate. + */ + uint32_t csd2bautrate (data_type *csd) { + data_type brmul = 0; + uint32_t br = 100000; // 100Kbit + + // Mask [2..0] bits of TRAN_SPEED + brmul = csd[3] & 0x07; + while (brmul--) + br *= 10; + return br; + } + + void delay (clock_t t) { + clock_t mark = impl().clock_impl(); + while (impl().clock_impl() - mark < t) + ; + } + + /*! + * \brief Check if SD Card is present. + * \return The sd card present status + * \arg false Is NOT present + * \arg true Is present. + */ + bool is_present () { + return impl().CD_impl(); + } + + /*! + * \brief Check if SD Card is write protected. + * \return The write protect status + * \arg false Is NOT write protected + * \arg true Is write protected. + */ + bool is_write_protected () { + return impl().WP_impl(); + } + + /*! + * \brief Powers up or down the SD Card. + * \param on On/Off flag. + * \return The new power state state + */ + bool power (bool on) { + impl().PWR_impl(on); + return pwr_flag = on; + } + + /*! + * \brief Check if SD Card is powered. + * \return The power status + * \arg false The drive is not powered + * \arg true The drive is powered + */ + bool power () { return pwr_flag; } + + /*! + * \brief Chip-select control + * \param state True to Select, false to de-select. + */ + void select() { + spi_tx(0xFF); + impl().CS_impl(false); + spi_tx(0xFF); + } + + /*! + * \brief De-select SD Card and release SPI bus + * \return None. + */ + void release () { + spi_tx(0xFF); + impl().CS_impl(true); + spi_tx(0xFF); + } + + /*! + * \brief Transmit a byte to SD/MMC via SPI + * \param data The data to send to the SPI bus. + */ + void spi_tx (data_type data) { + impl().SPI_rw_impl (data); + } + + /*! + * \brief Receive a byte to SD/MMC via SPI. + * \return The data received from SPI bus. + */ + data_type spi_rx () { + return impl().SPI_rw_impl (0xFF); + } + + /*! + * \brief Keep calling spi_rx until response \c resp. + * + * \param resp the response we wait for + * \param timeout timeout for the operation + * \return + * \arg true Ready + * \arg false NOT ready. + */ + bool spi_wait_for (data_type resp, clock_t timeout) { + data_type res; + clock_t mark = impl().clock_impl(); + + do + res = spi_rx (); + while ((res != resp) && ((impl().clock_impl() - mark) < timeout)); + + return (res == resp); + } + + bool activate (bool state) { + if (state) { + power(true); // power on with delay + delay (SD_PowerTimeout); + impl().CS_impl(1); // make sure CS is high + for (size_t i=0 ; i<10 ; ++i) // 80 dummy clocks with DI high + spi_tx(0xFF); + status = ST_NOINIT; // mark the status + } + else { + power(false); // power off + impl().CS_impl(0); // keep CS pin voltage low + status = ST_NOINIT; // mark the status + } + return state; + } + + /*! + * \brief + * Receive a data packet from MMC/SD + * + * \param buffer Pointer to data buffer to store received data + * \param n Byte count (must be multiple of 4) + * \return The operation status + * \arg false Fail + * \arg true Success. + */ + bool rx_datablock (data_type* buffer, size_t n) { + + if (! spi_wait_for(DATA_START_BLOCK, SD_RxTimeout)) + return false; + + /*! + * Receive the data block into buffer and make sure + * we receive multiples of 4 + */ + n += (n%4) ? 4-(n%4):0; + for ( ; n>0 ; --n) + *buffer++ = spi_rx (); + + spi_rx (); // Discard CRC + spi_rx (); + return true; + } + + /*! + * \brief + * Transmit a data block (512bytes) to MMC/SD + * + * \param buffer Pointer to 512 byte data block to be transmitted + * \param token Data/Stop token + * \return The operation status + * \arg false Fail + * \arg true Success. + */ + bool tx_datablock (const data_type* buffer, data_type token) { + if (!spi_wait_for(0xFF, SD_WaitTimeout)) + return false; + + spi_tx(token); // transmit data token + if (token != STOP_TRAN_TOKEN) { + // if its data token, transmit the 512 byte block + size_t cnt = 512; + do + spi_tx(*buffer++); + while (--cnt); + spi_tx(0xFF); // CRC (Dummy) + spi_tx(0xFF); + data_type r = spi_rx(); // Receive data response + if ((r & DATA_RES_MASK) != DATA_RES_ACCEPTED) // If not accepted, return with error + return false; + } + return true; + } + + /*! + * \brief + * Send a command packet to SD/MMC and return the response + * + * \param cmd Command byte + * \param arg Argument + * \return The response as operation status + */ + data_type command (data_type cmd, uint32_t arg) { + data_type n, r; + + if (cmd & 0x80) { + /*! + * SD_ACMD is the command sequence of CMD55-SD_CMD + */ + cmd &= 0x7F; + r = command (CMD55, 0); + if (r > 1) + return r; + } + + // Send command packet + spi_tx (cmd); // Start + Command index + spi_tx ((data_type)(arg>>24)); // Argument [31..24] + spi_tx ((data_type)(arg>>16)); // Argument [23..16] + spi_tx ((data_type)(arg>>8)); // Argument [15..8] + spi_tx ((data_type)arg); // Argument [7..0] + + if (cmd == CMD0) n = 0x94; // Valid CRC for CMD0(0) + else if (cmd == CMD8) n = 0x86; // Valid CRC for CMD8(0x1AA) + else n = 0x00; + spi_tx (n | CMD_CRC_LSB); + + // Receive command response + if (cmd == CMD12) + spi_rx (); // Skip a stuff byte when stop reading + + // Wait for a valid response in timeout of 0xFF attempts + size_t nn = 0xFF; + do + r = spi_rx (); + while ((r & 0x80) && --nn); + + return r; // Return with the response value + } + + bool do_command_until (data_type done, data_type cmd, uint32_t arg, clock_t timeout) { + clock_t mark = impl().clock_impl(); + data_type ret; + do + ret = command (cmd, arg); + while (ret != done && impl().clock_impl() - mark < timeout); + return ret == done; + } + +public: + bool get_CSD (data_type* csd) { + bool ret = false; + select(); // select card's CS + if (command (CMD9, 0) == R1_READY_STATE && rx_datablock (csd, 16)) // READ_CSD + ret = true; + release(); // release card's CS + return ret; + } + + bool get_CID (data_type* cid) { + bool ret = false; + select(); // select card's CS + if (command (CMD10, 0) == R1_READY_STATE && rx_datablock (cid, 16)) // READ_CID + ret = true; + release(); // release card's CS + return ret; + } + + bool get_OCR (data_type* ocr) { + bool ret = false; + select(); // select card's CS + // Receive OCR as an R3 response (4 bytes) + if (command (CMD58, 0) == 0) { // READ_OCR + for (size_t n = 0; n < 4; ++n) + *ocr++ = spi_rx (); + ret = true; + } + release(); // release card's CS + return ret; + } + +// bool get_SDSTAT (data_type* sdstat) { +// bool ret = false; +// select(); // select card's CS +// if (command (ACMD13, 0) == 0) { // SD_STATUS +// spi_rx (); +// if (rx_datablock (sdstat, 64)) +// ret = true; +// } +// release(); // release card's CS +// return ret; +// } + + bool sync () { + select(); // select card's CS + bool st = spi_wait_for(0xFF, SD_WaitTimeout); // flush + release(); // release card's CS + return st; + } + + size_t sector_count() { + size_t ret =0; + data_type csd[16]; + + select(); // select card's CS + if (get_CSD(csd)) { + if ((csd[0] >> 6) == 1) { + // SDC version 2.00 + size_t csize = csd[9] + ((uint16_t)csd[8] << 8) + 1; + ret = csize << 10; + } + else { + // SDC version 1.XX or MMC + uint8_t n = (csd[5] & 15) + ((csd[10] & 128) >> 7) + ((csd[9] & 3) << 1) + 2; + size_t csize = (csd[8] >> 6) + ((uint16_t)csd[7] << 2) + ((uint16_t)(csd[6] & 3) << 10) + 1; + ret = csize << (n - 9); + } + } + release(); // release card's CS + return ret; + } + + size_t sector_size() const { return 512; } + + size_t block_size() { + size_t ret =0; + data_type csd[16]; + + select(); // select card's CS + if (card_type & CT_SD2) { + // SDC version 2.00 + if (command (ACMD13, 0) == R1_READY_STATE) { + spi_rx (); // Read SD status + if (rx_datablock (csd, 16)) { // Read partial block + for (size_t n = 64 - 16; n; n--) // Purge trailing data + spi_rx (); + ret = 16UL << (csd[10] >> 4); + } + } + } + else { + // SDC version 1.XX or MMC + if (get_CSD(csd)) { // Read CSD + if (card_type & CT_SD1) // SDC version 1.XX + ret = (((csd[10] & 63) << 1) + ((uint16_t)(csd[11] & 128) >> 7) + 1) << ((csd[13] >> 6) - 1); + else // MMC + ret = ((uint16_t)((csd[10] & 124) >> 2) + 1) * (((csd[11] & 3) << 3) + ((csd[11] & 224) >> 5) + 1); + } + } + release(); // release card's CS + return ret; + } + + /*! + * \brief + * De-Initialize SD Drive. + * \return None + */ + void deinit () { + card_type = data_type{}; + status = status_t{}; + activate (0); // finally power off the card + } + + /*! + * \brief + * Initialize SD Drive. + * + * \return The status of the operation + * \arg false On error. + * \arg true On success. + */ + bool init () { + uint32_t clk; + data_type ocr[4], csd[16]; + + clk = 400000; // Start at lower clk + impl().SPI_set_clk_impl(clk); + + activate (0); // Initially power off the card + if (!is_present()) { // check for presence + status = ST_NODISK; + return false; + } + activate (1); // activate and wait for PowerTimeout delay + + select(); // select card + data_type type = CT_NONE; + + if (command (CMD0, 0) == R1_IDLE_STATE) { // Command to enter Idle state + if (command (CMD8, 0x1AA) == 1) { // check SD version + // SDHC + for (size_t n=0 ; n<4 ; ++n) // Get trailing return value of R7 response + ocr[n] = spi_rx (); + if (ocr[2] == 0x01 && ocr[3] == 0xAA) { + // Wait for leaving idle state (ACMD41 with HCS bit) + bool st = do_command_until(R1_READY_STATE, ACMD41, 1UL << 30, SD_InitTimeout); + + if (st && get_OCR(ocr)) + type = (ocr[0] & 0x40) ? CT_SD2 | CT_BLOCK : CT_SD2; + } + } else { + data_type cmd; + // SDSC or MMC + if (command (ACMD41, 0) <= 1) { // SDSC + type = CT_SD1; cmd = ACMD41; + } else { // MMC + type = CT_MMC; cmd = CMD1; + } + // Wait for leaving idle state (ACMD41 || CMD1) + bool st = do_command_until(R1_READY_STATE, cmd, 0, SD_InitTimeout); + // On failure, set R/W block length to 512 (For FAT compatibility) + if (!st || command (CMD16, 512) != R1_READY_STATE) + type = CT_NONE; + } + } + card_type = type; + release (); // Initialization ended + + if (type != CT_NONE) { + // Success + get_CSD(csd); + clk = csd2bautrate(csd); + impl().SPI_set_clk_impl(clk); + status = ST_OK; + return true; + } + else { + activate(0); + return false; + } + } + + status_t get_status () const { return status; } + + /*! + * \brief + * Read Sector(s) + * + * \param sector Start sector number (LBA) + * \param buf Pointer to the data buffer to store read data + * \param count Sector (512 bytes) count (1..255) + * \return The status of the operation + * \arg false On error. + * \arg true On success. + */ + bool read (size_t sector, data_type *buf, size_t count) { + + if (status != ST_OK) return false; + + if (!(card_type & CT_BLOCK)) // Convert to byte address if needed + sector *= 512; + select(); + if (count == 1) { //Single block read + if (command (CMD17, sector) == 0) + if (rx_datablock (buf, 512)) + count = 0; + } + else { // Multiple block read + if (command (CMD18, sector) == 0) { + do { + if (!rx_datablock (buf, 512)) + break; + buf += 512; + } while (--count); + command (CMD12, 0); // STOP_TRANSMISSION + } + } + release (); + return (count == 0); + } + + /*! + * \brief + * Write Sector(s) + * + * \param sector Start sector number (LBA) + * \param buf Pointer to the data to be written + * \param count Sector(512 bytes) count (1..255) + * \return The status of the operation + * \arg false On error. + * \arg true On success. + */ + bool write (size_t sector, const data_type *buf, size_t count) { + + if (!count) return false; + if (status != ST_OK) return false; + + if (!(card_type & CT_BLOCK)) // Convert to byte address if needed + sector *= 512; + + select(); + if (count == 1) { // Single block write + if ((command (CMD24, sector) == 0) && tx_datablock (buf, 0xFE)) + count = 0; + } else { // Multiple block write + if (card_type & CT_SDC) + command (ACMD23, count); + if (command (CMD25, sector) == 0) { + do { + if (!tx_datablock (buf, WRITE_MULTIPLE_TOKEN)) + break; + buf += 512; + } while (--count); + if (!tx_datablock (0, STOP_TRAN_TOKEN)) // STOP token + count = 1; + } + } + release (); + return (count == 0); + } + + bool ioctl (ioctl_cmd cmd, void* buffer) { + switch (cmd) { + // SD/MMC specific + case IOCTL_MMC_GET_TYPE: *(data_type*)buffer = card_type; return true; + case IOCTL_MMC_GET_CSD: return get_CSD ((data_type*)buffer); + case IOCTL_MMC_GET_CID: return get_CID ((data_type*)buffer); + case IOCTL_MMC_GET_OCR: return get_OCR ((data_type*)buffer); + case IOCTL_MMC_GET_SDSTAT: return false; + + // Generic + case IOCTL_POWER: + switch (*(data_type*)buffer) { + case 0: *((data_type*)buffer+1) = (data_type)activate(0); return true; + case 1: *((data_type*)buffer+1) = (data_type)activate(0); return true; + case 2: *((data_type*)buffer+1) = (data_type)power(); return true; + default: return false; + } + break; + case IOCTL_LOCK: + case IOCTL_EJECT: + case IOCTL_FORMAT: + return false; + + // FatFS compatibility + case IOCTL_SYNC: return sync(); + case IOCTL_GET_SECTOR_COUNT:return (*(size_t*) buffer = sector_count() != 0); + case IOCTL_GET_SECTOR_SIZE: return (*(size_t*) buffer = sector_size() != 0); + case IOCTL_GET_BLOCK_SIZE: return (*(size_t*) buffer = block_size() != 0); + case IOCTL_ERASE_SECTOR: return false; + + default: + return false; + } + } + +private: + data_type card_type{}; + status_t status{}; + bool pwr_flag{}; + +}; + +} // namespace tbx + +#endif /* TBX_DRV_SD_SPI_H_ */ diff --git a/include/tbx.h b/include/tbx.h index ffdc4dc..55c6df6 100644 --- a/include/tbx.h +++ b/include/tbx.h @@ -44,5 +44,6 @@ #include #include #include +#include #endif /* TBX_H_ */