вівторок, 28 серпня 2018 р.

STM32: підключення SD CARD по SPI з FATFS

Передмова

Знадобилось мені в одному проекті вести запис всіляких подій у файл. Просто короткі текстові рядки з датою/часом і що за подія відбулась. Тому максимально просто і не заморочувався з DMA. Бібліотека FATFS є в комплекті з CubeMX, а ось вже як саме буде працювати мікроконтролер з флешкою по шині SPI на рівні залізяччя, треба вже потурбуватись самому. І дописати декілька функцій в заздалегідь призначених для цього місцях. Ідею поцупив звідси. Дуже стисло поділюсь з вами робочим прикладом. Хто захоче розібратись детально, то вже користуйтесь пошуком.

Залізяччя для макету

Схема макету

Зробіть з'єднання відповідно до схеми:
Макет для тестування
Клацайте на зображення щоб збільшити.

CubeMX for STM32

Запускаємо CubeMX, обираємо свій чип і робимо налаштування:
Налаштування вкладки pinout
  1. Додаємо до проекту бібліотеку FATFS
  2. Вмикаємо тактування від зовнішнього кварцового резонатору
  3. Вмикаємо Debug - Serial Wire
  4. Вмикаємо SPI2
  5. Та не забуваємо налаштувати PB12 на вихід GPIO_Output і надати ім'я "CS_SD_CARD", а також PB1 на вихід GPIO_Output і надати ім'я "LED_RED"
Переходимо до вкладки "clock configuration" і робимо так як на малюнку:
Налаштування вкладки clock configuration
Всі інші налаштування за замовчуванням, нічого не міняв.

Надаємо ім'я своєму проекту "STM32F103C8_SD_CARD_DEMO", вказуємо шлях, засіб розробки та генеруємо код.

Код для роботи FATFS по SPI

Відкриваємо в своєму засобі розробки згенерований CubeMX проект "STM32F103C8_SD_CARD_DEMO". Та перш за все навчимо бібліотеку FATFS працювати з нашою SD Card по шині SPI. Відкриваємо файл "user_diskio.h" і в секцію /* USER CODE BEGIN 0 */ такі рядки: 
  • #define SPI hspi2
  • extern SPI_HandleTypeDef SPI;
  • void sdcard_systick_timerproc(void);
Надалі, для зручності буду додавати весь код в користувацьких секціях і функціях де необхідно зробити зміни.

Ось так буде виглядати секція "USER CODE 0" у файлі "user_diskio.h" з нашим кодом:
/* USER CODE BEGIN 0 */
#define SPI hspi2
/* Includes ------------------------------------------------------------------*/
/* Exported types ------------------------------------------------------------*/
extern SPI_HandleTypeDef SPI;
/* Exported constants --------------------------------------------------------*/
/* Exported functions ------------------------------------------------------- */
extern Diskio_drvTypeDef  USER_Driver;

/*-----------------------------------------------------------------------*/
/* Device Timer Interrupt Procedure  (Platform dependent)                */
/*-----------------------------------------------------------------------*/
/* This function must be called in period of 1ms                        */

void sdcard_systick_timerproc(void);
/* USER CODE END 0 */

Далі відкриваємо файл "user_diskio.c" і до секції "/* USER CODE DECL */" вносимо необхідні визначення, змінні, функції.

Секція "/* USER CODE DECL */" в файлі "user_diskio.c" тепер буде виглядати так:
/* USER CODE BEGIN DECL */

/* Includes ------------------------------------------------------------------*/
#include <string.h>
#include "ff_gen_drv.h"

#include "user_diskio.h"
#include "stdbool.h"
/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Definitions for MMC/SDC command */
#define CMD0 (0x40+0) /* GO_IDLE_STATE */
#define CMD1    (0x40+1) /* SEND_OP_COND */
#define CMD8    (0x40+8) /* SEND_IF_COND */
#define CMD9    (0x40+9) /* SEND_CSD */
#define CMD10 (0x40+10) /* SEND_CID */
#define CMD12 (0x40+12) /* STOP_TRANSMISSION */
#define CMD16 (0x40+16) /* SET_BLOCKLEN */
#define CMD17 (0x40+17) /* READ_SINGLE_BLOCK */
#define CMD18 (0x40+18) /* READ_MULTIPLE_BLOCK */
#define CMD23 (0x40+23) /* SET_BLOCK_COUNT */
#define CMD24 (0x40+24) /* WRITE_BLOCK */
#define CMD25 (0x40+25) /* WRITE_MULTIPLE_BLOCK */
#define CMD41 (0x40+41) /* SEND_OP_COND (ACMD) */
#define CMD55 (0x40+55) /* APP_CMD */
#define CMD58 (0x40+58) /* READ_OCR */

#define SELECT() HAL_GPIO_WritePin(CS_SD_CARD_GPIO_Port, CS_SD_CARD_Pin, GPIO_PIN_RESET)
#define DESELECT() HAL_GPIO_WritePin(CS_SD_CARD_GPIO_Port, CS_SD_CARD_Pin, GPIO_PIN_SET)
/* Private variables ---------------------------------------------------------*/
/* Disk status */
static volatile DSTATUS Stat = STA_NOINIT;

static BYTE CardType; /* b0:MMC, b1:SDC, b2:Block addressing */
static BYTE PowerFlag = 0; /* indicates if "power" is on */
static volatile BYTE Timer1, Timer2; /* 100Hz decrement timer */

static void xmit_spi(BYTE Data)
{
 while (HAL_SPI_GetState(&SPI) != HAL_SPI_STATE_READY);
 HAL_SPI_Transmit(&SPI, &Data, 1, 5000);
}

static BYTE rcvr_spi(void)
{
 unsigned char Dummy, Data;
 Dummy = 0xFF;
 Data = 0;
 while ((HAL_SPI_GetState(&SPI) != HAL_SPI_STATE_READY));
 HAL_SPI_TransmitReceive(&SPI, &Dummy, &Data, 1, 5000);

 return Data;
}

static void rcvr_spi_m(BYTE *dst)
{
 *dst = rcvr_spi();
}

/*-----------------------------------------------------------------------*/
/* Wait for card ready                                                   */
/*-----------------------------z------------------------------------------*/

static BYTE wait_ready(void)
{
 BYTE res;

 Timer2 = 50;
 rcvr_spi();
 do
  res = rcvr_spi();
 while ((res != 0xFF) && Timer2);

 return res;
}


static bool rcvr_datablock(BYTE *buff, /* Data buffer to store received data */
UINT btr /* Byte count (must be even number) */
)
{
 BYTE token;

 Timer1 = 10;
 do { /* Wait for data packet in timeout of 100ms */
  token = rcvr_spi();
 } while ((token == 0xFF) && Timer1);
 if (token != 0xFE)
  return false; /* If not valid data token, retutn with error */

 do { /* Receive the data block into buffer */
  rcvr_spi_m(buff++);
  rcvr_spi_m(buff++);
 } while (btr -= 2);
 rcvr_spi(); /* Discard CRC */
 rcvr_spi();

 return true; /* Return with success */
}

/*-----------------------------------------------------------------------*/
/* Send a data packet to MMC                                             */
/*-----------------------------------------------------------------------*/

#if _READONLY == 0
static bool xmit_datablock(const BYTE *buff, /* 512 byte data block to be transmitted */
BYTE token /* Data/Stop token */
)
{
 BYTE resp, wc;
 uint32_t i = 0;

 if (wait_ready() != 0xFF)
  return false;

 xmit_spi(token); /* Xmit data token */
 if (token != 0xFD)
 { /* Is data token */
  wc = 0;
  do { /* Xmit the 512 byte data block to MMC */
   xmit_spi(*buff++);
   xmit_spi(*buff++);
  } while (--wc);

  rcvr_spi();
  rcvr_spi();

  while (i <= 64) {
   resp = rcvr_spi(); /* Reveive data response */
   if ((resp & 0x1F) == 0x05) /* If not accepted, return with error */
    break;
   i++;
  }
  while (rcvr_spi() == 0)
   ;
 }
 if ((resp & 0x1F) == 0x05)
  return true;
 else
  return false;
}
#endif /* _READONLY */

static void power_on(void)
{
 unsigned char i, cmd_arg[6];
 unsigned int Count = 0x1FFF;

 DESELECT();

 for (i = 0; i < 10; i++)
  xmit_spi(0xFF);

 SELECT();

 cmd_arg[0] = (CMD0 | 0x40);
 cmd_arg[1] = 0;
 cmd_arg[2] = 0;
 cmd_arg[3] = 0;
 cmd_arg[4] = 0;
 cmd_arg[5] = 0x95;

 for (i = 0; i < 6; i++)
  xmit_spi(cmd_arg[i]);

 while ((rcvr_spi() != 0x01) && Count)
  Count--;

 DESELECT();
 xmit_spi(0XFF);

 PowerFlag = 1;
}

static void power_off(void) {
 PowerFlag = 0;
}
/*-----------------------------------------------------------------------*/
/* Send a command packet to MMC                                          */
/*-----------------------------------------------------------------------*/

static BYTE send_cmd(BYTE cmd, /* Command byte */
DWORD arg /* Argument */
)
{
 BYTE n, res;

 if (wait_ready() != 0xFF)
  return 0xFF;

 /* Send command packet */
 xmit_spi(cmd); /* Command */
 xmit_spi((BYTE) (arg >> 24)); /* Argument[31..24] */
 xmit_spi((BYTE) (arg >> 16)); /* Argument[23..16] */
 xmit_spi((BYTE) (arg >> 8)); /* Argument[15..8] */
 xmit_spi((BYTE) arg); /* Argument[7..0] */
 n = 0;
 if (cmd == CMD0)
  n = 0x95; /* CRC for CMD0(0) */
 if (cmd == CMD8)
  n = 0x87; /* CRC for CMD8(0x1AA) */
 xmit_spi(n);

 /* Receive command response */
 if (cmd == CMD12)
  rcvr_spi(); /* Skip a stuff byte when stop reading */
 n = 10; /* Wait for a valid response in timeout of 10 attempts */
 do
  res = rcvr_spi();
 while ((res & 0x80) && --n);

 return res; /* Return with the response value */
}

static int chk_power(void) /* Socket power state: 0=off, 1=on */
{
 return PowerFlag;
}

/*-----------------------------------------------------------------------*/
/* Device Timer Interrupt Procedure  (Platform dependent)                */
/*-----------------------------------------------------------------------*/
/* This function must be called in period of 1ms                        */

void disk_timerproc(void)
{
 uint8_t n;

 n = Timer1; /* 100Hz decrement timer */
 if (n)
  Timer1 = --n;
 n = Timer2;
 if (n)
  Timer2 = --n;

}

volatile unsigned short int sdcard_timer;

void inline sdcard_systick_timerproc(void)
{
 ++sdcard_timer;
 if (sdcard_timer >= 10)
 {
  sdcard_timer = 0;
  disk_timerproc();
 }
}
/* USER CODE END DECL */

А далі вносимо зміни до функцій в файлі user_diskio.c:
  • DSTATUS USER_initialize (BYTE pdrv);
  • DSTATUS USER_status (BYTE pdrv);
  • DRESULT USER_read (BYTE pdrv, BYTE *buff, DWORD sector, UINT count);
  • DRESULT USER_write (BYTE pdrv, const BYTE *buff, DWORD sector, UINT count);
  • DRESULT USER_ioctl (BYTE pdrv, BYTE cmd, void *buff);
Функція USER_initialize:
/**
  * @brief  Initializes a Drive
  * @param  pdrv: Physical drive number (0..)
  * @retval DSTATUS: Operation status
  */
DSTATUS USER_initialize (
 BYTE pdrv           /* Physical drive nmuber to identify the drive */
)
{
  /* USER CODE BEGIN INIT */
 BYTE n, ty, ocr[4];

 if (pdrv)
  return STA_NOINIT; /* Supports only single drive */
 if (Stat & STA_NODISK)
  return Stat; /* No card in the socket */

 power_on(); /* Force socket power on */
 //send_initial_clock_train();

 SELECT(); /* CS = L */
 ty = 0;
 if (send_cmd(CMD0, 0) == 1)
 { /* Enter Idle state */
  Timer1 = 100; /* Initialization timeout of 1000 msec */
  if (send_cmd(CMD8, 0x1AA) == 1)
  { /* SDC Ver2+ */
   for (n = 0; n < 4; n++)
    ocr[n] = rcvr_spi();
   if (ocr[2] == 0x01 && ocr[3] == 0xAA)
   { /* The card can work at vdd range of 2.7-3.6V */
    do {
     if (send_cmd(CMD55, 0) <= 1
       && send_cmd(CMD41, 1UL << 30) == 0)
      break; /* ACMD41 with HCS bit */
    } while (Timer1);
    if (Timer1 && send_cmd(CMD58, 0) == 0)
    { /* Check CCS bit */
     for (n = 0; n < 4; n++)
      ocr[n] = rcvr_spi();
     ty = (ocr[0] & 0x40) ? 6 : 2;
    }
   }
  }
  else
  { /* SDC Ver1 or MMC */
   ty = (send_cmd(CMD55, 0) <= 1 && send_cmd(CMD41, 0) <= 1) ? 2 : 1; /* SDC : MMC */
   do {
    if (ty == 2)
    {
     if (send_cmd(CMD55, 0) <= 1 && send_cmd(CMD41, 0) == 0)
      break; /* ACMD41 */
    }
    else
    {
     if (send_cmd(CMD1, 0) == 0)
      break; /* CMD1 */
    }
   } while (Timer1);
   if (!Timer1 || send_cmd(CMD16, 512) != 0) /* Select R/W block length */
    ty = 0;
  }
 }
 CardType = ty;
 DESELECT(); /* CS = H */
 rcvr_spi(); /* Idle (Release DO) */

 if (ty) /* Initialization succeded */
  Stat &= ~STA_NOINIT; /* Clear STA_NOINIT */
 else
  /* Initialization failed */
  power_off();

 return Stat;
  /* USER CODE END INIT */
}

Функція USER_status:
/**
  * @brief  Gets Disk Status 
  * @param  pdrv: Physical drive number (0..)
  * @retval DSTATUS: Operation status
  */
DSTATUS USER_status (
 BYTE pdrv       /* Physical drive number to identify the drive */
)
{
  /* USER CODE BEGIN STATUS */
 if (pdrv)
  return STA_NOINIT; /* Supports only single drive */
 return Stat;
  /* USER CODE END STATUS */
}

Функція USER_read:
/**
  * @brief  Reads Sector(s) 
  * @param  pdrv: Physical drive number (0..)
  * @param  *buff: Data buffer to store read data
  * @param  sector: Sector address (LBA)
  * @param  count: Number of sectors to read (1..128)
  * @retval DRESULT: Operation result
  */
DRESULT USER_read (
 BYTE pdrv,      /* Physical drive nmuber to identify the drive */
 BYTE *buff,     /* Data buffer to store read data */
 DWORD sector,   /* Sector address in LBA */
 UINT count      /* Number of sectors to read */
)
{
  /* USER CODE BEGIN READ */
 if (pdrv || !count)
  return RES_PARERR;
 if (Stat & STA_NOINIT)
  return RES_NOTRDY;

 if (!(CardType & 4))
  sector *= 512; /* Convert to byte address if needed */

 SELECT(); /* CS = L */

 if (count == 1)
 { /* Single block read */
  if ((send_cmd(CMD17, sector) == 0) /* READ_SINGLE_BLOCK */
  && rcvr_datablock(buff, 512))
   count = 0;
 }
 else
 { /* Multiple block read */
  if (send_cmd(CMD18, sector) == 0)
  { /* READ_MULTIPLE_BLOCK */
   do {
    if (!rcvr_datablock(buff, 512))
     break;
    buff += 512;
   } while (--count);
   send_cmd(CMD12, 0); /* STOP_TRANSMISSION */
  }
 }

 DESELECT(); /* CS = H */
 rcvr_spi(); /* Idle (Release DO) */

 return count ? RES_ERROR : RES_OK;
  /* USER CODE END READ */
}

Функція USER_write:
/**
  * @brief  Writes Sector(s)  
  * @param  pdrv: Physical drive number (0..)
  * @param  *buff: Data to be written
  * @param  sector: Sector address (LBA)
  * @param  count: Number of sectors to write (1..128)
  * @retval DRESULT: Operation result
  */
#if _USE_WRITE == 1
DRESULT USER_write (
 BYTE pdrv,          /* Physical drive nmuber to identify the drive */
 const BYTE *buff,   /* Data to be written */
 DWORD sector,       /* Sector address in LBA */
 UINT count          /* Number of sectors to write */
)
{ 
  /* USER CODE BEGIN WRITE */
  /* USER CODE HERE */
 if (pdrv || !count)
  return RES_PARERR;
 if (Stat & STA_NOINIT)
  return RES_NOTRDY;
 if (Stat & STA_PROTECT)
  return RES_WRPRT;

 if (!(CardType & 4))
  sector *= 512; /* Convert to byte address if needed */

 SELECT(); /* CS = L */

 if (count == 1)
 { /* Single block write */
  if ((send_cmd(CMD24, sector) == 0) /* WRITE_BLOCK */
  && xmit_datablock(buff, 0xFE))
   count = 0;
 }
 else
 { /* Multiple block write */
  if (CardType & 2)
  {
   send_cmd(CMD55, 0);
   send_cmd(CMD23, count); /* ACMD23 */
  }
  if (send_cmd(CMD25, sector) == 0)
  { /* WRITE_MULTIPLE_BLOCK */
   do {
    if (!xmit_datablock(buff, 0xFC))
     break;
    buff += 512;
   } while (--count);
   if (!xmit_datablock(0, 0xFD)) /* STOP_TRAN token */
    count = 1;
  }
 }

 DESELECT(); /* CS = H */
 rcvr_spi(); /* Idle (Release DO) */

 return count ? RES_ERROR : RES_OK;
  /* USER CODE END WRITE */
}
#endif /* _USE_WRITE == 1 */

Функція USER_ioctl:
/**
  * @brief  I/O control operation  
  * @param  pdrv: Physical drive number (0..)
  * @param  cmd: Control code
  * @param  *buff: Buffer to send/receive control data
  * @retval DRESULT: Operation result
  */
#if _USE_IOCTL == 1
DRESULT USER_ioctl (
 BYTE pdrv,      /* Physical drive nmuber (0..) */
 BYTE cmd,       /* Control code */
 void *buff      /* Buffer to send/receive control data */
)
{
  /* USER CODE BEGIN IOCTL */
 DRESULT res;
 BYTE n, csd[16], *ptr = buff;
 WORD csize;

 if (pdrv)
  return RES_PARERR;

 res = RES_ERROR;

 if (cmd == CTRL_POWER)
 {
  switch (*ptr) {
  case 0: /* Sub control code == 0 (POWER_OFF) */
   if (chk_power())
    power_off(); /* Power off */
   res = RES_OK;
   break;
  case 1: /* Sub control code == 1 (POWER_ON) */
   power_on(); /* Power on */
   res = RES_OK;
   break;
  case 2: /* Sub control code == 2 (POWER_GET) */
   *(ptr + 1) = (BYTE) chk_power();
   res = RES_OK;
   break;
  default:
   res = RES_PARERR;
  }
 }
 else
 {
  if (Stat & STA_NOINIT)
   return RES_NOTRDY;

  SELECT(); /* CS = L */

  switch (cmd) {
  case GET_SECTOR_COUNT: /* Get number of sectors on the disk (DWORD) */
   if ((send_cmd(CMD9, 0) == 0) && rcvr_datablock(csd, 16))
   {
    if ((csd[0] >> 6) == 1)
    { /* SDC ver 2.00 */
     csize = csd[9] + ((WORD) csd[8] << 8) + 1;
     *(DWORD*) buff = (DWORD) csize << 10;
    }
    else
    { /* MMC or SDC ver 1.XX */
     n = (csd[5] & 15) + ((csd[10] & 128) >> 7)
       + ((csd[9] & 3) << 1) + 2;
     csize = (csd[8] >> 6) + ((WORD) csd[7] << 2)
       + ((WORD) (csd[6] & 3) << 10) + 1;
     *(DWORD*) buff = (DWORD) csize << (n - 9);
    }
    res = RES_OK;
   }
   break;

  case GET_SECTOR_SIZE: /* Get sectors on the disk (WORD) */
   *(WORD*) buff = 512;
   res = RES_OK;
   break;

  case CTRL_SYNC: /* Make sure that data has been written */
   if (wait_ready() == 0xFF)
    res = RES_OK;
   break;

  case MMC_GET_CSD: /* Receive CSD as a data block (16 bytes) */
   if (send_cmd(CMD9, 0) == 0 /* READ_CSD */
   && rcvr_datablock(ptr, 16))
    res = RES_OK;
   break;

  case MMC_GET_CID: /* Receive CID as a data block (16 bytes) */
   if (send_cmd(CMD10, 0) == 0 /* READ_CID */
   && rcvr_datablock(ptr, 16))
    res = RES_OK;
   break;

  case MMC_GET_OCR: /* Receive OCR as an R3 resp (4 bytes) */
   if (send_cmd(CMD58, 0) == 0)
   { /* READ_OCR */
    for (n = 0; n < 4; n++)
     *ptr++ = rcvr_spi();
    res = RES_OK;
   }

  default:
   res = RES_PARERR;
  }

  DESELECT(); /* CS = H */
  rcvr_spi(); /* Idle (Release DO) */
 }

 return res;
  /* USER CODE END IOCTL */
}
#endif /* _USE_IOCTL == 1 */

Демо код

Відкриваємо файл stm32f1xx_it.c і до секції /* USER CODE 0 */ додаємо рядок:
/* USER CODE BEGIN 0 */
extern void sdcard_systick_timerproc(void);
/* USER CODE END 0 */

А до функції "SysTick_Handler" додаємо виклик функції "sdcard_systick_timerproc":
void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  HAL_SYSTICK_IRQHandler();
  /* USER CODE BEGIN SysTick_IRQn 1 */
  sdcard_systick_timerproc();
  /* USER CODE END SysTick_IRQn 1 */
}

І нарешті відкриваємо файл main.c нашого проекту і в секцію /* USER CODE 2 */ додаємо такий демо-код:
/* USER CODE BEGIN 2 */
  //-------------SD DEMO START-----------------
  char buffer[512];
  static FATFS g_sFatFs;
  FRESULT fresult;
  FIL file;
  int len;
  UINT bytes_written;
  HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_SET);
  fresult = f_mount(&g_sFatFs, "0:", 0);        //mount SD card
  fresult = f_open(&file, "testfile.txt", FA_OPEN_ALWAYS | FA_WRITE); //open file on SD card
  fresult = f_lseek(&file, file.fsize);         //go to the end of the file
  len = sprintf(buffer, "Hello, World!\r\nThis is demonstration write on SD Card\r\n");    //generate some string
  fresult = f_write(&file, buffer, len, &bytes_written);     //write data to the file
  fresult = f_close(&file);
  HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_RESET);
  //-------------SD DEMO END-----------------
  /* USER CODE END 2 */

Компілюємо проект, заливаємо до мікроконтролеру. Стартуємо мікроконтролер. Можна декілька разів перезавантажити. При цьому світлодіод має блимнути. Тепер можна витягти флешку з SD Card Adapter і вставити її до ПК та пересвідчитись що файл "testfile.txt" з декількома текстовими рядками "Hello, World!" та "This is demonstration write on SD Card" існує.

Файли