вівторок, 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" існує.

Файли

7 коментарів:

  1. I can't make it work in every SD card. Do you have any idea why? I've tested many cards and, until now, there's only one generating the file and writing on it (I've even tested with a similar one).

    ВідповістиВидалити
    Відповіді
    1. Greeting.
      Yes, I also do not work on every SD-CARD. I do not know the reason. Not taken care of If you find a reason, please let me know, I will correct it and update the library.

      Видалити
    2. I ended up using a different code from yours, but it is actually pretty similar. I'm not sure if this is going to help you, but I just had to change the Vcc from 3.3V to 5V (remember that SPI2 ports on STM32f103C8 are 5V tolerant).

      Видалити
  2. sincerely thank you for the information. I will test it in practice.

    ВідповістиВидалити
  3. напишіть, будь-ласка, приклад, як читати построково файл з gcode, такого виду:
    G1 X166.895 Y40.157 E10.07675
    G1 X167.512 Y41.109 E10.20410
    G1 X168.271 Y42.549 E10.38682
    G1 X168.899 Y44.196 E10.58469
    G1 X169.319 Y45.894 E10.78104
    G1 X169.532 Y47.597 E10.97369
    G1 X169.550 Y49.208 E11.15454

    ВідповістиВидалити
    Відповіді
    1. Вітання.
      Ваше питання ближче до документації бібліотеки FatFS, чим до теми цієї статті.
      Ось вам ланка на потрібну вам функцію бібліотеки FatFS: http://microsin.net/programming/file-systems/fatfs-gets-read-string.html

      Видалити
  4. дякую за відповідь, я на f_gets реалізував читання построково, а потім sscanf розпарсив строчки. Ніби все працює, але таке "рагульство" вийшло, я думав є кращий і більш елегантний спосіб

    ВідповістиВидалити