개발 환경

대상버전
OSWindows 10 24H2(Desktop), MacOS Sequoia 15.5(24F74)(MacBook M2 Air)
STM32CubeIDE1.18.1
STM32CubeMX6.14.1
STM32CubeProg2.19.0
STM32CubeCLT1.18.0
보드Nucleo-F103RB
펌웨어V24J46M32

참고한 자료

어댑터에 대한 데이터시트를 찾지 못했고, 비슷한 자료에서는 통신 절차에 대한 설명이 없어서 관련 자료를 위주로 참고했습니다.

‘4.2.1 Writing Raw Data to an SD Card via SPI’

‘How to Use MMC/SDC’

Lecture 12: SPI and SD cards - EE-379 Embedded Systems and Applications | Electrical Engineering Department, University at Buffalo

‘STM32_SPI_SDCARD/Old_STM32F1/Src at master · eziya/STM32_SPI_SDCARD’

Read, Write

이전 글에서 전체 흐름을 살펴보았었다. FatFs 라이브러리의 f_readf_write를 호출한다면 user_diskio.cUSER_Read, USER_Write함수 내부에서 내가 정의한 함수를 호출하게 됨을 알 수 있었다.

Read

단일 블록을 읽는 과정은 위와 같다. CMD17을 전송하고 명령에 대한 응답(Command Response)을 확인한다면 블록 크기만큼의 데이터를 가진 데이터 패킷이 전달된다. 각 응답들의 포맷은 다음과 같다.

먼저 Command Response는 8비트 길이로, 상위 3개는 Don’t care, 하위 4개 중에 3개가 성공, 오류 여부를 응답한다. 이를 뒤따르는 데이터 패킷은 토큰(8비트) + 데이터 블럭(1~2048비트?) + CRC(16비트)로 구성되어 있다. 토큰은 어떤 명령어에 대한 응답인가를 구분하는 용도로 쓰인다. 데이터 블럭은 CubeMX에서 정한 데이터 블럭을 따른다. CRC는 2바이트나 전달되지만 건너뛰어도 무방하다.

DSTATUS SD_Read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) {
    SD_DataToken token;
    SD_Response  res;
    uint8_t      dummy = 0xFF, crc = 0x01;
    DWORD        addr = (sd_version == SD_TYPE_V2_BLOCK_ADDRESS)
                            ? sector        /* SDHC/SDXC: block address */
                            : sector * 512; /* SDSC: byte address */

    if (count == 1) {
        SD_Select();

        res = SD_Send_Command(SD_CMD17, addr);

        if (res == 0) {
            /**
             * Read DataToken
             */
            Timer1 = 200; // 타임아웃 200ms
            do {
                SD_SPI_SendReceive(dummy, &token);
            } while (Timer1 && token == 0xFF);
            /**
             * 에러 토큰 검사
             */
            if (token != 0xFE) {
                SD_Deselect();
                SD_SPI_Send(0xFF);
                return RES_ERROR;
            }

            /**
             * Read DataBlock
             */
            for (UINT i = 0; i < 512; i++) {
                SD_SPI_SendReceive(dummy, &buff[i]);
            }

            /**
             * Read CRC, but skip
             */
            SD_SPI_Send(crc);
            SD_SPI_Send(crc);

            SD_Deselect();
        } else {
            return RES_ERROR;
        }

    } else {
    }
    return RES_OK;
}

단일 블록에 대해서 읽기만 가능해도 SD카드에서 데이터를 읽어올 수 있었다. 멀티 블록은 나중에 구현해보기로…

Write

쓰기 작업의 절차는 다음과 같다.

CMD24를 전송하고 응답이 도착했다면 잠시 1바이트 정도 기다렸다가 쓸 내용을 담은 데이터 패킷을 전송한다. 전송이 끝났다면 Data Response를 확인하고 Busy Flag가 들어오게 되는데 이 신호가 끝날 때까지 기다려야 SD카드에게 다음 명령을 보낼 수 있다.

DSTATUS SD_Write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count) {
    SD_DataResponse data_res;
    SD_Response     res;
    uint8_t         dummy = 0xFF;

    SD_Select();
    if (count == 1) {
        data_res = (SD_DataResponse)SD_Send_Command(SD_CMD24, sector);

        if (data_res == 0) {
            /**
             * 0. 더미 1바이트 전송
             * 1. 데이터 토큰 전송
             * 2. 데이터 블록 전송
             * 3. CRC 전송
             * 4. busy flag 처리
             */
            uint8_t token = SD_DATA_TOKEN_CMD17_18_24;
            uint8_t crc   = 0xFF;

            SD_SPI_Send(dummy);
            SD_SPI_Send(token);

            for (int i = 0; i < 512; i++) {
                SD_SPI_Send(*(buff++));
            }
            SD_SPI_Send(crc);
            SD_SPI_Send(crc);

            SD_SPI_SendReceive(dummy, &data_res);
            if (SD_IS_DATA_ACCEPTED(data_res)) {
                do {
                    SD_SPI_SendReceive(dummy, &res);
                } while (res != 0xFF);
            }
        }
    } else {
    }
    SD_Deselect();
    return RES_OK;
}

ioctl

FatFs 라이브러리를 사용할 때 가장 먼저 호출하게 되는 함수는 f_mount다. 디스크를 마운트하는 작업을 하는데, 이 함수 내부에서는 볼륨(디스크)을 찾기 위해 find_volume이라는 함수를 호출한다. find_volume함수는 다음과 같다.


static
FRESULT find_volume (    /* FR_OK(0): successful, !=0: any error occurred */
    FATFS** rfs,        /* Pointer to pointer to the found file system object */
    const TCHAR** path,    /* Pointer to pointer to the path name (drive number) */
    BYTE wmode            /* !=0: Check write protection for write access */
)
{
    BYTE fmt, *pt;
    int vol;
    DSTATUS stat;
    DWORD bsect, fasize, tsect, sysect, nclst, szbfat, br[4];
    WORD nrsv;
    FATFS *fs;
    UINT i;

    // 생략 ...

    if (fs->fs_type) {                    /* If the volume has been mounted */
        stat = disk_status(fs->drv);
        if (!(stat & STA_NOINIT)) {        /* and the physical drive is kept initialized */
            if (!_FS_READONLY && wmode && (stat & STA_PROTECT))    /* Check write protection if needed */
                return FR_WRITE_PROTECTED;
            return FR_OK;                /* The file system object is valid */
        }
    }

    /* The file system object is not valid. */
    /* Following code attempts to mount the volume. (analyze BPB and initialize the fs object) */

    fs->fs_type = 0;                    /* Clear the file system object */
    fs->drv = LD2PD(vol);                /* Bind the logical drive and a physical drive */
    stat = disk_initialize(fs->drv);    /* Initialize the physical drive */
    if (stat & STA_NOINIT)                /* Check if the initialization succeeded */
        return FR_NOT_READY;            /* Failed to initialize due to no medium or hard error */
    if (!_FS_READONLY && wmode && (stat & STA_PROTECT))    /* Check disk write protection if needed */
        return FR_WRITE_PROTECTED;
#if _MAX_SS != _MIN_SS                        /* Get sector size (multiple sector size cfg only) */
    if (disk_ioctl(fs->drv, GET_SECTOR_SIZE, &SS(fs)) != RES_OK
        || SS(fs) < _MIN_SS || SS(fs) > _MAX_SS) return FR_DISK_ERR;
#endif

    // 생략 ... 
}

disk_가 접두어인 함수들은 모두 diskio.h 에 정의되어 있는 함수들인데, 이전 글에서 작성했었던 초기화 함수 역시 disk_initialize에 의해 호출된다. 위 함수에서 등장하는 disk_status, disk_ioctl 역시 diskio.h에 정의되어 있고, 구현하지 않으면 정상적으로 동작하지 않는다.

disk_status

DSTATUS SD_Status(BYTE pdrv) { return status; }

초기화 함수나, 읽기, 쓰기 함수의 성공 여부를 별도 변수에 저장해둔다. 이 함수에서는 그것을 반환하기만 한다.

disk_ioctl

읽기 쓰기만 구현하기로 마음먹어서, 처리해야할 명령어는 두 가지 밖에 되지 않는다.


DSTATUS SD_ioctl(BYTE pdrv, BYTE cmd, void *buff) {
    SD_Response r;
    uint8_t     csd[32];
    uint8_t     dummy = 0xFF;
    DSTATUS     res   = RES_OK;

    if (cmd == CTRL_SYNC) {
        // Timer2 = 1000;

        if (!SD_BusyWait()) {
            res = RES_ERROR;
        }
    } else if (cmd == GET_SECTOR_SIZE) {
        *(WORD *)buff = 512; // 왜 512?
    } else if (cmd == GET_BLOCK_SIZE) {
        *(DWORD *)buff = 8;
    } else {
        res = RES_PARERR;
    }
    return res;
}

전체 코드

https://github.com/10cheon00/STM32-Study/tree/main/MicroSD_FATFS_ex