定义:15个
有调用:14个
未调用:CMD10

常见SDHC/SDXC卡:
初始化涉及6个
读取涉及3个
写入涉及3个
共12种命令

额外兼容:
CMD1、CMD16
/**
 * @file sd_spi.c
 * @brief SD 卡 SPI 模式驱动 (SPI4), 适配 JW-MCU-TM30-2
 *
 * 管脚 (按用户确认):
 *   39脚 PB2  = SD_PWR   电源使能(低有效: 拉低上电/供电, 拉高关断)
 *   3脚  PE2  = SPI4_SCK  CLK
 *   48脚 PB12 = SPI4_NSS  CS (CD/DATA3)
 *   66脚 PE13 = SPI4_MISO DATA0
 *   67脚 PE14 = SPI4_MOSI CMD
 *   93脚 PB4  = SD_DET   插入检测
 *
 * SPI4 挂在 APB2, 系统 APB2 = 48MHz (SYSCLK 288M / APB2 div 6)。
 * 初始化用低速分频 256 -> 187.5kHz (< 400kHz), 协议完成提速到 12MHz。
 */

#include "mh245x_gpio.h"
#include "mh245x_spi.h"
#include "peripheral.h"
#include "system_mh2457.h"
#include "sd_spi.h"
#include <stdio.h>

/* Keep the proven reference driver intact while routing its diagnostics to
 * this project's console service. */
#define BSP_LOGI(...) do { printf("[SD] "); printf(__VA_ARGS__); printf("\n"); } while (0)
#define BSP_LOGW(...) do { printf("[SD][WARN] "); printf(__VA_ARGS__); printf("\n"); } while (0)
#define BSP_LOGE(...) do { printf("[SD][ERR] "); printf(__VA_ARGS__); printf("\n"); } while (0)

/* ===================== SD 命令集 ===================== */
#define CMD0    0   /* GO_IDLE_STATE */
#define CMD1    1   /* SEND_OP_COND (MMC) */
#define CMD8    8   /* SEND_IF_COND */
#define CMD9    9   /* SEND_CSD */
#define CMD10   10  /* SEND_CID */
#define CMD12   12  /* STOP_TRANSMISSION */
#define CMD16   16  /* SET_BLOCKLEN */
#define CMD17   17  /* READ_SINGLE_BLOCK */
#define CMD18   18  /* READ_MULTIPLE_BLOCK */
#define CMD23   23  /* SET_BLOCK_COUNT (ACMD) */
#define CMD24   24  /* WRITE_BLOCK */
#define CMD25   25  /* WRITE_MULTIPLE_BLOCK */
#define CMD55   55  /* APP_CMD */
#define CMD58   58  /* READ_OCR */
#define ACMD41  41  /* SEND_OP_COND (SD) */

/* ===================== 私有宏 ===================== */
#define SD_CS_HIGH()   GPIO_SetBits(SD_SPI_CS_PORT,  SD_SPI_CS_PIN)
#define SD_CS_LOW()    GPIO_ResetBits(SD_SPI_CS_PORT, SD_SPI_CS_PIN)

#define SD_SPI_SLOW_PRESCALER  SPI_BaudRatePrescaler_256  /* 187.5kHz */
#define SD_SPI_FAST_PRESCALER  SPI_BaudRatePrescaler_4    /* 12MHz */

/* 下面三个计数只在 DWT 周期计数器不可用时作为兜底(见 sd_timeout_cycles),
 * 正常路径的超时按真实时间算。
 *
 * 原来它们是唯一的超时机制,而同一个数字代表的真实时间随 SPI 时钟变化:
 *   初始化 187.5 kHz → 20000 次 ≈ 854 ms
 *   运行中 12 MHz    → 20000 次 ≈ 14~20 ms
 * 偏偏"等卡写完一个块的 busy"只发生在 12 MHz 那个阶段,也就是预算最小的
 * 阶段。而 SD 规范允许卡写完块后把 DO 拉低到 250 ms,所以这个预算偏小,
 * 表现为"数据能写、紧接其后的元数据单扇区写失败"。 */
#define SD_INIT_TIMEOUT   1000
#define SD_READ_TIMEOUT   20000
#define SD_WRITE_TIMEOUT  20000

/* 等卡 busy / 等响应 token 的时间上限。规范上限 250 ms,这里留一倍余量。 */
#define SD_BUSY_TIMEOUT_MS  500U

/* SD_SendCmdEx 回传的命令阶段子状态。0xFF 作为 R1 有两种来源,混成一个值
 * 会让日志完全无法解释:一个是卡 busy 选不中,一个是命令被吞。 */
#define SD_CMD_ST_OK      0
#define SD_CMD_ST_SELECT  1   /* SD_Select 失败:卡一直 busy,选不中 */
#define SD_CMD_ST_NO_R1   2   /* 命令已上总线,但卡一个字节都没回 */

/* SD_XmitData 的失败阶段。原来只返回 0/1,看不出是卡 busy 超时还是卡明确
 * 拒绝了数据块,而这两种原因的修法完全不同。 */
#define SD_XMIT_OK        0
#define SD_XMIT_BUSY      1   /* 进入时卡一直 busy,等到超时 */
#define SD_XMIT_NO_TOKEN  2   /* 数据块已发完,但等不到数据响应 token */
#define SD_XMIT_REJECTED  3   /* 收到响应但不是"接受"(0x05),如 CRC 错/写错 */
#define SD_CMD_REJECTED   4   /* 命令阶段失败(R1 非 0,或根本没收到 R1) */

/* ===================== 私有变量 ===================== */
static uint8_t  s_sdType = 0;      /* 卡类型 SD_CT_* */
static uint8_t  s_inited = 0;      /* 协议初始化完成 */
static DSTATUS  s_status = STA_NOINIT;
static DWORD    s_sectorCount = 0; /* 卡总扇区数 (CSD 解析) */

/* 连续写失败计数:FatFs 上层会把同一个扇区反复重发,逐次打印会淹没串口。 */
static uint32_t s_write_fail_streak;

/* 本次事务的超时上限,单位 CPU 周期。返回 0 表示 DWT 周期计数器没在跑,
 * 调用方必须退回固定的迭代计数——否则 CYCCNT 恒为 0、比较永远不成立,
 * 等待循环会退化成死循环。DWT 由 BootTask 在任何 SD 事务之前使能
 * (app/tasks/task_boot.c)。 */
static uint32_t sd_timeout_cycles(void)
{
    uint32_t cycles_per_ms = SystemCoreClock / 1000U;
    if ((DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) == 0U || cycles_per_ms == 0U) {
        return 0U;
    }
    return cycles_per_ms * SD_BUSY_TIMEOUT_MS;
}

/* ===================== SD 协议层 ===================== */

/* 等待卡就绪: 返回 0=就绪 1=超时
 * 超时优先按真实时间算,DWT 不可用时才退回 fallback 次迭代。 */
static int SD_WaitReady(uint32_t fallback)
{
    uint8_t d;
    uint32_t start = DWT->CYCCNT;
    uint32_t limit = sd_timeout_cycles();

    for (;;) {
        d = SPI4_RW(0xFF);
        if (d == 0xFF) return 0;
        if (limit != 0U) {
            if ((uint32_t)(DWT->CYCCNT - start) >= limit) return 1;
        } else if (--fallback == 0U) {
            return 1;
        }
    }
}

static void SD_Deselect(void)
{
    SD_CS_HIGH();
    SPI4_RW(0xFF);   /* 额外 8 时钟释放 DO */
}

/* 选卡并等待就绪, 返回 1=成功 0=失败
 * 关键: 先拉高 CS 再拉低, 保证每个命令事务都有 CS 下降沿。
 * SD SPI 规范要求每个命令以 CS 下降沿开始; 若 CS 持续为低
 * (背靠背命令), 部分 v2 卡的命令状态机不重置, CMD8/ACMD41 无响应。 */
static int SD_Select(void)
{
    SD_CS_HIGH();
    SPI4_RW(0xFF);      /* CS 高期间 8 时钟, 让卡释放 MISO */
    SD_CS_LOW();
    SPI4_RW(0xFF);      /* CS 低后 8 时钟, 卡开始驱动 MISO */
    if (SD_WaitReady(SD_READ_TIMEOUT)) {
        SD_Deselect();
        return 0;
    }
    return 1;
}

/* 发送命令, 返回 R1。
 *
 * 返回值 0xFF 有两种完全不同的来源,混在一起就无法解释日志:
 *   - SD_Select() 失败:卡一直把 DO 拉低(busy),根本选不中
 *   - R1 轮询超时:命令已经发上总线,卡一个字节都没回
 * 两者都指向"卡不应答",但一个是忙死、一个是命令被吞。cmd_status 回传
 * 具体是哪种;cmd_status 非 0 时 R1 无意义。 */
static uint8_t SD_SendCmdEx(uint8_t cmd, uint32_t arg, int *cmd_status)
{
    uint8_t buf[6];
    uint8_t r1;
    uint32_t n;

    if (cmd_status != NULL) *cmd_status = SD_CMD_ST_OK;

    if (SD_Select() == 0) {
        if (cmd_status != NULL) *cmd_status = SD_CMD_ST_SELECT;
        return 0xFF;
    }

    buf[0] = (uint8_t)(0x40 | cmd);
    buf[1] = (uint8_t)(arg >> 24);
    buf[2] = (uint8_t)(arg >> 16);
    buf[3] = (uint8_t)(arg >> 8);
    buf[4] = (uint8_t)(arg);
    buf[5] = 0x01;
    if (cmd == CMD0)      buf[5] = 0x95;   /* CMD0 CRC */
    else if (cmd == CMD8) buf[5] = 0x87;   /* CMD8 CRC */

    SPI4_TxMulti(buf, 6);

    if (cmd == CMD12) SPI4_RW(0xFF);       /* CMD12 后补 1 字节 */

    n = SD_INIT_TIMEOUT;
    do {
        r1 = SPI4_RW(0xFF);
    } while ((r1 & 0x80) && --n);

    if ((r1 & 0x80) != 0U && cmd_status != NULL) {
        *cmd_status = SD_CMD_ST_NO_R1;
    }
    return r1;
}

static uint8_t SD_SendCmd(uint8_t cmd, uint32_t arg)
{
    return SD_SendCmdEx(cmd, arg, NULL);
}

/* 发送应用命令。CMD55 在卡仍处于 idle 时合法返回 0x01;旧代码把它当成
 * 失败并被 || 短路,结果 ACMD41 从未真正上总线,卡永远停在 idle。
 * 两个命令各自结束 CS 事务,兼容要求命令间存在 CS 高电平的 SD 卡。 */
static uint8_t SD_SendAppCmd(uint8_t acmd, uint32_t arg)
{
    uint8_t r1 = SD_SendCmd(CMD55, 0);
    SD_Deselect();
    if (r1 > SD_R1_IDLE) {
        return r1;
    }

    r1 = SD_SendCmd(acmd, arg);
    SD_Deselect();
    return r1;
}

/* 接收数据块 (含 0xFE 起始令牌 + 2 CRC), 返回 0=OK 1=ERR
 * 等 token 同样按真实时间计时:读也可能紧跟在一次写的 busy 之后。 */
static int SD_RecvData(uint8_t *buff, uint16_t btr)
{
    uint8_t token;
    uint32_t start = DWT->CYCCNT;
    uint32_t limit = sd_timeout_cycles();
    uint32_t n = SD_READ_TIMEOUT;

    for (;;) {
        token = SPI4_RW(0xFF);
        if (token == 0xFE) break;
        if (token != 0xFF) return 1;   /* 非起始、非空闲 → 错误 token */
        if (limit != 0U) {
            if ((uint32_t)(DWT->CYCCNT - start) >= limit) return 1;
        } else if (--n == 0U) {
            return 1;
        }
    }

    SPI4_RxMulti(buff, btr);
    SPI4_RW(0xFF);     /* CRC16 高 */
    SPI4_RW(0xFF);     /* CRC16 低 */
    return 0;
}

/* 发送数据块, token=0xFE(单块)/0xFC(多块)。
 * 返回 SD_XMIT_* 阶段码,并把卡的数据响应写进 resp_out(0xFF = 没收到)。 */
static int SD_XmitData(const uint8_t *buff, uint8_t token, uint8_t *resp_out)
{
    uint8_t resp;
    uint32_t n = SD_INIT_TIMEOUT;

    if (resp_out != NULL) *resp_out = 0xFFU;

    if (SD_WaitReady(SD_WRITE_TIMEOUT) != 0) return SD_XMIT_BUSY;

    SPI4_RW(token);
    SPI4_TxMulti(buff, 512);
    SPI4_RW(0xFF);     /* 伪 CRC */
    SPI4_RW(0xFF);

    /* 数据响应 token 在 CRC 之后才出现,卡可以再拖几个字节时钟。原来只读
     * 一次就下结论:读到 0xFF(空闲线)直接算失败,卡稍慢一点就被误判。 */
    do {
        resp = SPI4_RW(0xFF);
    } while (resp == 0xFFU && --n != 0U);

    if (resp_out != NULL) *resp_out = resp;
    if (resp == 0xFFU) return SD_XMIT_NO_TOKEN;

    /* 0x05 = 接受;0x0B = CRC 错;0x0D = 写错 */
    return ((resp & 0x1FU) == 0x05U) ? SD_XMIT_OK : SD_XMIT_REJECTED;
}

/* 读取 CSD 并解析总扇区数 (512B 单位)。返回 0=OK */
static int SD_ReadCapacity(DWORD *pSectors)
{
    uint8_t csd[16];
    uint32_t c_size, c_size_mult, read_bl_len;
    uint64_t blocks;

    if (SD_SendCmd(CMD9, 0) != 0 || SD_RecvData(csd, 16)) {
        SD_Deselect();
        return 1;
    }
    SD_Deselect();

    if ((csd[0] & 0xC0) == 0x40) {            /* CSD v2.0 (SDHC/SDXC) */
        c_size = ((uint32_t)csd[7] << 16) | ((uint32_t)csd[8] << 8) | csd[9];
        blocks = ((uint64_t)c_size + 1) << 10;   /* (c_size+1)*1024 KB / 512B */
    } else {                                /* CSD v1.0 (SDSC) */
        read_bl_len  = csd[5] & 0x0F;
        c_size       = ((uint32_t)(csd[6] & 0x03) << 10) |
                       ((uint32_t)csd[7] << 2) | ((csd[8] & 0xC0) >> 6);
        c_size_mult  = ((uint32_t)(csd[9] & 0x03) << 1) | ((csd[10] & 0x80) >> 7);
        blocks = ((uint64_t)(c_size + 1) << (c_size_mult + 2 + read_bl_len))
                 >> 9;   /* 转 512B 扇区 */
    }

    *pSectors = (blocks > 0xFFFFFFFFUL) ? 0xFFFFFFFFUL : (DWORD)blocks;
    return 0;
}

/* ===================== 公有接口 ===================== */

DSTATUS SD_SPI_Initialize(void)
{
    uint8_t n, r1, ocr[4];
    uint8_t ty = 0;
    uint32_t ntry;

    /* 幂等: 协议已成功且卡仍在位, 直接返回就绪。
     * f_mount 会再次调用 disk_initialize, 避免重复跑 CMD0~CMD9 握手。 */
    if (s_inited && SD_SPI_IsInserted()) {
        s_status = 0;
        return s_status;
    }

    BSP_LOGI("[SD] init start: inserted=%d", (int)SD_SPI_IsInserted());
    if (!SD_SPI_IsInserted()) {
        s_status = STA_NOINIT | STA_NODISK;
        s_inited = 0;
        s_sdType = 0;
        s_sectorCount = 0;
        BSP_LOGW("[SD] init FAIL: card NOT inserted (DET pin high)");
        return s_status;
    }

    SD_SPI_PowerOn();
    SystemDelay(50);   /* 上电稳定:10ms 对部分卡(尤其热重启)不足,曾致 CMD0 失败 */

    SPI4_SetPrescaler(SD_SPI_SLOW_PRESCALER);
    SD_CS_HIGH();
    for (n = 0; n < 10; n++) SPI4_RW(0xFF);   /* >=74 时钟 */
    SD_Deselect();

    /* 进入 Idle: CMD0 */
    ntry = SD_INIT_TIMEOUT;
    while ((r1 = SD_SendCmd(CMD0, 0)) != SD_R1_IDLE) {
        SD_Deselect();
        if (!--ntry) {
            BSP_LOGE("[SD] init FAIL: CMD0 timeout, last R1=0x%02X", r1);
            s_status = STA_NOINIT; return s_status;
        }
    }
    BSP_LOGI("[SD] CMD0 OK (idle), R1=0x%02X", r1);

    /* CMD8 检查电压 2.7-3.6V */
    r1 = SD_SendCmd(CMD8, 0x1AA);
    if (r1 == SD_R1_IDLE) {
        /* v2.x */
        for (n = 0; n < 4; n++) ocr[n] = SPI4_RW(0xFF);
        SD_Deselect();
        BSP_LOGI("[SD] CMD8 OK (v2), OCR=%02X %02X %02X %02X",
                 ocr[0], ocr[1], ocr[2], ocr[3]);
        if (ocr[3] == 0xAA) {
            ntry = SD_INIT_TIMEOUT;
            while ((r1 = SD_SendAppCmd(ACMD41, 0x40000000U)) != 0) {
                if (!--ntry) {
                    BSP_LOGE("[SD] init FAIL: ACMD41 timeout, last R1=0x%02X", r1);
                    s_status = STA_NOINIT; return s_status;
                }
            }
            BSP_LOGI("[SD] ACMD41 OK, R1=0x%02X (tries left=%lu)", r1, (unsigned long)ntry);
            r1 = SD_SendCmd(CMD58, 0);
            if (r1 == 0) {
                for (n = 0; n < 4; n++) ocr[n] = SPI4_RW(0xFF);
                SD_Deselect();
                /* OCR bit30(CCS) 单独表示块寻址;bit31 只是上电完成状态。 */
                ty = (ocr[0] & 0x40U) ? (SD_CT_SD2 | SD_CT_BLOCK) : SD_CT_SD2;
                BSP_LOGI("[SD] CMD58 OCR=%02X %02X %02X %02X, type=%s",
                         ocr[0], ocr[1], ocr[2], ocr[3],
                         (ty & SD_CT_BLOCK) ? "SDHC/SDXC(block)" : "SD2(byte)");
            } else {
                SD_Deselect();
                BSP_LOGW("[SD] CMD58 failed, R1=0x%02X", r1);
            }
        } else {
            BSP_LOGE("[SD] init FAIL: CMD8 echo mismatch (got 0x%02X, expect 0xAA)", ocr[3]);
            s_status = STA_NOINIT; return s_status;
        }
    } else {
        /* v1.x 或 MMC */
        SD_Deselect();
        BSP_LOGI("[SD] CMD8 R1=0x%02X -> try v1/MMC path", r1);
        r1 = SD_SendAppCmd(ACMD41, 0);
        ty = (r1 <= SD_R1_IDLE) ? SD_CT_SD1 : SD_CT_MMC;
        ntry = SD_INIT_TIMEOUT;
        if (ty == SD_CT_SD1) {
            while ((r1 = SD_SendAppCmd(ACMD41, 0)) != 0) {
                if (!--ntry) { ty = 0; break; }
            }
        } else if (ty == SD_CT_MMC) {
            while (SD_SendCmd(CMD1, 0) != 0) {
                SD_Deselect();
                if (!--ntry) { ty = 0; break; }
            }
        }
    }

    SD_Deselect();
    if (ty == 0) {
        BSP_LOGE("[SD] init FAIL: card type detection failed (v1/MMC path)");
        s_status = STA_NOINIT; return s_status;
    }

    /* SDHC/SDXC 固定为 512 字节块且不接受 CMD16;仅字节寻址卡设置块长。 */
    if ((ty & SD_CT_BLOCK) == 0U) {
        if (SD_SendCmd(CMD16, 512) != 0) {
            BSP_LOGE("[SD] init FAIL: CMD16 (SET_BLOCKLEN) failed");
            SD_Deselect(); s_status = STA_NOINIT; return s_status;
        }
        SD_Deselect();
        BSP_LOGI("[SD] CMD16 OK (blocklen=512)");
    } else {
        BSP_LOGI("[SD] CMD16 skipped (SDHC/SDXC fixed 512-byte blocks)");
    }

    /* 提速 */
    SPI4_SetPrescaler(SD_SPI_FAST_PRESCALER);

    /* 解析 CSD 取得总扇区数 */
    if (SD_ReadCapacity(&s_sectorCount) != 0) {
        BSP_LOGE("[SD] init FAIL: CMD9 (READ_CSD) failed - no data token 0xFE");
        s_status = STA_NOINIT; return s_status;
    }
    BSP_LOGI("[SD] CMD9 CSD OK, sectors=%lu", (unsigned long)s_sectorCount);

    s_sdType = ty;
    s_inited = 1;
    s_status = 0;
    BSP_LOGI("[SD] init SUCCESS: type=0x%02X, sectors=%lu", (unsigned)ty, (unsigned long)s_sectorCount);
    return s_status;
}

DWORD SD_SPI_GetSectorCount(void)
{
    return s_sectorCount;
}

DSTATUS SD_SPI_Status(void)
{
    if (!SD_SPI_IsInserted()) {
        /* 拔卡会丢失整个协议会话;再次插入必须重新 CMD0..CMD9,不能只
         * 清 STA_NODISK 后把尚未初始化的新卡误报为 ready。 */
        s_status = STA_NOINIT | STA_NODISK;
        s_inited = 0;
        s_sdType = 0;
        s_sectorCount = 0;
        return s_status;
    }
    if (!s_inited) {
        s_status = STA_NOINIT;
    } else {
        s_status = 0;
    }
    return s_status;
}

DRESULT SD_SPI_ReadSectors(BYTE *buff, DWORD sector, UINT count)
{
    DRESULT res = RES_OK;
    DWORD   sec = (s_sdType & SD_CT_BLOCK) ? sector : (sector << 9);

    if (s_status & STA_NOINIT) return RES_NOTRDY;

    if (count == 1) {
        if (SD_SendCmd(CMD17, sec) != 0 || SD_RecvData(buff, 512)) res = RES_ERROR;
        SD_Deselect();
    } else {
        if (SD_SendCmd(CMD18, sec) == 0) {
            do {
                if (SD_RecvData(buff, 512)) { res = RES_ERROR; break; }
                buff += 512;
            } while (--count);
        } else {
            res = RES_ERROR;
        }
        SD_SendCmd(CMD12, 0);
        SD_Deselect();
    }
    return res;
}

/* 单次写入尝试。sec 已经按卡类型换算过(块寻址 / 字节寻址)。
 * 返回失败阶段(SD_XMIT_OK = 成功),并回传全部诊断信息。 */
static int sd_write_once(const BYTE *buff, DWORD sec, UINT count,
                         uint8_t *resp, int *cmd_status, uint8_t *r1,
                         UINT *done)
{
    int stage;

    *resp = 0xFFU;
    *r1 = 0xFFU;
    *done = 0U;
    *cmd_status = SD_CMD_ST_OK;

    if (count == 1) {
        *r1 = SD_SendCmdEx(CMD24, sec, cmd_status);
        stage = (*r1 != 0U) ? SD_CMD_REJECTED : SD_XmitData(buff, 0xFE, resp);
        if (stage == SD_XMIT_OK) *done = 1U;
        SD_Deselect();
    } else {
        /* ACMD23 只是可选的预擦除提示;不支持时仍应继续尝试 CMD25。 */
        (void)SD_SendAppCmd(CMD23, count);
        *r1 = SD_SendCmdEx(CMD25, sec, cmd_status);
        if (*r1 == 0U) {
            for (;;) {
                stage = SD_XmitData(buff, 0xFC, resp);
                if (stage != SD_XMIT_OK) break;
                buff += 512;
                ++*done;
                if (--count == 0U) break;
            }
            if (stage == SD_XMIT_OK) {
                /* 停止令牌必须等卡结束编程(DO 拉高)之后再发。
                 *
                 * SD_XmitData 是一读到数据响应 token 就返回的,而卡在发出响应
                 * 之后立刻拉低 DO 开始编程;卡在整个 busy 期间会忽略 MOSI 上的
                 * 一切 —— 紧跟着发出去的 0xFD 会被直接丢掉,多块写收不了尾。
                 * 中间那些块之所以没出事,是因为下一次 SD_XmitData 开头的
                 * SD_WaitReady 把 busy 兜住了;只有最后这个令牌后面没有防护,
                 * 所以必然被吞,卡停在多块写状态。
                 *
                 * 规范时序:数据块 → 数据响应 → busy(DO 低) → DO 高 → 停止令牌。 */
                if (SD_WaitReady(SD_WRITE_TIMEOUT) != 0) {
                    stage = SD_XMIT_BUSY;
                } else {
                    SPI4_RW(0xFD);
                }
            }
            /* 中途失败时不做额外动作:后面紧跟的 SD_Deselect()(CS 拉高)
             * 就是 SPI 模式下的中止手段,不需要再加 CMD12。 */
        } else {
            stage = SD_CMD_REJECTED;
        }
        SD_Deselect();
    }
    return stage;
}

DRESULT SD_SPI_WriteSectors(const BYTE *buff, DWORD sector, UINT count)
{
    DWORD   sec = (s_sdType & SD_CT_BLOCK) ? sector : (sector << 9);
    UINT    done = 0U;
    uint8_t resp = 0xFFU;
    uint8_t r1 = 0xFFU;
    int     cmd_status = SD_CMD_ST_OK;
    int     stage;

    if (s_status & STA_NOINIT) return RES_NOTRDY;

    stage = sd_write_once(buff, sec, count, &resp, &cmd_status, &r1, &done);
    if (stage == SD_XMIT_OK) {
        s_write_fail_streak = 0U;
        return RES_OK;
    }

    ++s_write_fail_streak;
    if (s_write_fail_streak <= 1U || (s_write_fail_streak % 16U) == 0U) {
        /* 每个字段都用于区分原因:
         *   stage = 1 卡 busy 超时 / 2 数据块发完没等到响应 token /
         *           3 卡回了响应但不是"接受"(resp 0x0B=CRC错 0x0D=写错)/
         *           4 命令阶段失败,看 cmd:1=选卡时卡 busy、2=没收到 R1
         *   r1 = 卡对 CMD24/CMD25 的真实应答(0x20 写保护、0x40 参数错、
         *        0x04 非法命令)
         * 没有这几个数,上层只能看到 FatFs 的 result=1,无从定位。 */
        BSP_LOGE("write failed: stage=%d cmd=%d r1=0x%02X resp=0x%02X "
                 "sector=%lu count=%u done=%u streak=%lu",
                 stage, cmd_status, (unsigned)r1, (unsigned)resp,
                 (unsigned long)sec, (unsigned)count, (unsigned)done,
                 (unsigned long)s_write_fail_streak);
    }
    return RES_ERROR;
}

DRESULT SD_SPI_ReadSectors(BYTE *buff, DWORD sector, UINT count)
{
    DRESULT res = RES_OK;
    DWORD   sec = (s_sdType & SD_CT_BLOCK) ? sector : (sector << 9);

    if (s_status & STA_NOINIT) return RES_NOTRDY;

    if (count == 1) {
        if (SD_SendCmd(CMD17, sec) != 0 || SD_RecvData(buff, 512)) res = RES_ERROR;
        SD_Deselect();
    } else {
        if (SD_SendCmd(CMD18, sec) == 0) {
            do {
                if (SD_RecvData(buff, 512)) { res = RES_ERROR; break; }
                buff += 512;
            } while (--count);
        } else {
            res = RES_ERROR;
        }
        SD_SendCmd(CMD12, 0);
        SD_Deselect();
    }
    return res;
}

DRESULT SD_SPI_WriteSectors(const BYTE *buff, DWORD sector, UINT count)
{
    DRESULT res = RES_OK;
    DWORD   sec = (s_sdType & SD_CT_BLOCK) ? sector : (sector << 9);

    if (s_status & STA_NOINIT) return RES_NOTRDY;

    if (count == 1) {
        if (SD_SendCmd(CMD24, sec) != 0 || SD_XmitData(buff, 0xFE)) res = RES_ERROR;
        SD_Deselect();
    } else {
        /* ACMD23 只是可选的预擦除提示;不支持时仍应继续尝试 CMD25。 */
        (void)SD_SendAppCmd(CMD23, count);
        if (SD_SendCmd(CMD25, sec) == 0) {
            do {
                if (SD_XmitData(buff, 0xFC)) { res = RES_ERROR; break; }
                buff += 512;
            } while (--count);
            SPI4_RW(0xFD);   /* 停止令牌 */
        } else {
            res = RES_ERROR;
        }
        SD_Deselect();
    }
    return res;
}