STM Урок 88. SD. SPI. FATFS. Часть 3

 

 

 

 

Урок 88

 

Часть 3

 

SD. SPI. FATFS

 

 

В предыдущей части нашего урока мы написали полностью функцию инициализации карты SD, постоянно проверяя каждый наш шаг на практике.

 

Теперь наша задача — считать какие-то данные с карты. Пока попробуем считать блок, так как читать файлы без умения считать элементарный блок мы попросту не сможем.

Добавим для этого функцию в файле sd.c чуть выше функции sd_ini, во входных параметрах которой будет указатель на буфер, в который будут считываться данные блока и адрес блока

 

//-----------------------------------------------

uint8_t SD_Read_Block (uint8_t *buff, uint32_t lba)

{

  uint8_t result;

  uint16_t cnt;

  return 0;

}

//-----------------------------------------------

 

Для того, чтобы считать один блок, существует команда CMD17, аргументом которой будет 32-битный адрес блока. Причём если карта повышенной вместимости (SDHC или SDXC), то адрес блока считается именно в блоках по 512 или более байт в зависимости от величины блока. В других случаях адресом является номер байта от начала адресного пространства памяти карты. Адреса считаются с 0.

Вот описание команды

 

index58

 

Вызовем данную команду в нашей функции считывания блока

 

uint16_t cnt;

result=SD_cmd (CMD17, lba); //CMD17 даташит стр 50 и 96

if (result!=0x00) return 5; //Выйти, если результат не 0x00

 

Посмотрим, как читаются данные с карты

 

index59

 

Поэтому пропустим байт и считаем данные в буфер

 

  if (result!=0x00) return 5; //Выйти, если результат не 0x00

  SPI_Release();

  cnt=0;

  do{ //Ждем начала блока

    result=SPI_ReceiveByte();

    cnt++;

  } while ( (result!=0xFE)&&(cnt<0xFFFF) );

  if (cnt>=0xFFFF) return 5;

  for (cnt=0;cnt<512;cnt++) buff[cnt]=SPI_ReceiveByte(); //получаем байты блока из шины в буфер

  SPI_Release(); //Пропускаем контрольную сумму

  SPI_Release();

  return 0;

}

 

Добавим на данную функцию прототип в заголовочный файл и перейдём в файл main.c и добавим там пустой глобальный массив для 512 байтов, чтобы мы могли в него сохранить байты из блока

 

volatile uint16_t Timer1=0;

uint8_t sect[512];

 

Пока мы функцию вызывать не будем, а вернёмся в файл sd.c и напишем функцию записи блока. Она будет похожа на функцию чтения, только команда будет использована уже CMD24. Формат команды такой же, поэтому приводить её описание нет смысла

 

//-----------------------------------------------

uint8_t SD_Write_Block (uint8_t *buff, uint32_t lba)

{

  uint8_t result;

  uint16_t cnt;

  result=SD_cmd(CMD24,lba); //CMD24 даташит стр 51 и 97-98

  if (result!=0x00) return 6; //Выйти, если результат не 0x00

  SPI_Release();

  SPI_SendByte (0xFE); //Начало буфера

  for (cnt=0;cnt<512;cnt++) SPI_SendByte(buff[cnt]); //Данные

  SPI_Release(); //Пропустим котрольную сумму

  SPI_Release();

  result=SPI_ReceiveByte();

  if ((result&0x05)!=0x05) return 6; //Выйти, если результат не 0x05 (Даташит стр 111)

  cnt=0;

  do { //Ждем окончания состояния BUSY

    result=SPI_ReceiveByte();

    cnt++;

  } while ( (result!=0xFF)&&(cnt<0xFFFF) );

  if (cnt>=0xFFFF) return 6;

  return 0;

}

//-----------------------------------------------

 

Думаю код понятен. Правда есть вопрос насчёт байта FE, который мы передаём, это своего рода маркер начала блока, чтобы карта могла определить, с какого байта начинать запись. Чтобы понять процесс, приведу процесс записи блока из технической документации

 

index60

 

Я выделил маркер (токен). Вот его отдельное описание (стр. 112)

 

index61

 

Создадим на функцию записи также прототип и вернёмся опять в файл main.c. Добавим здесь глобальный массив с любой строкой в 512 байт. Я взял выдержку из даташита карты, полностью приводить здесь не буду. Вы можете взять любую строку

 

char buffer1[512] ="Selection ... The..."; //Буфер данных для записи/чтения

 

Теперь вызовем нашу функцию в main()

 

sd_ini();

SD_Write_Block((uint8_t*)buffer1,0x0400); //Запишем блок в буфер

 

Соберём код и прошьём контроллер, тем самым данные должны у нас записаться в сектор с заданным адресом. Так как карта у нас нужного типа, то сектор этот будет далеко от начала и данные, которых пока на карте не так много, пострадать не должны.

Теперь закомментируем данную строку и добавим строку с вызовом функции чтения блока, тем самым мы считаем те же байты, так как адрес мы во входных аргументах используем тот же, что и при записи

 

//SD_Write_Block((uint8_t*)buffer1,0x0400); //Запишем блок в буфер

SD_Read_Block(sect,0x0400); //Считаем блок из буфера

 

Добавим в main() локальную переменную для счёчика циклов

 

/* USER CODE BEGIN 1 */

uint16_t i;

/* USER CODE END 1 */

 

Отобразим считанные данные сектора в терминальной программе

 

SD_Read_Block(sect,0x0400); //Считаем блок из буфера

for(i=0;i<512;i++) HAL_UART_Transmit(&huart1,sect+i,1,0x1000);

HAL_UART_Transmit(&huart1,(uint8_t*)"rn",2,0x1000);

 

Соберём код, прошьём контроллер и посмотрим результат (мне, почему-то приходится перзагружать потом контроллер ещё и кнокой, видимо издержки раздельного питания)

 

index62

 

Мы видим нашу строку. Это значит, что блок у нас нормально записывается и читается. Отлично. Закомментируем весь этот код чтения сектора

 

//SD_Read_Block(sect,0x0400); //Считаем блок из буфера

//for(i=0;i<512;i++) HAL_UART_Transmit(&huart1,sect+i,1,0x1000);

//HAL_UART_Transmit(&huart1,(uint8_t*)"rn",2,0x1000);

 

Также закомментируем массив со строкой, пустой массив оставим, он ещё нам пригодится

 

//char buffer1[512] ="Sel…

uint8_t sect[512];

 

Теперь начнём потихоньку цепляться к библиотеке FatFS, чтобы мы могли работать не с блоками, а с файлами.

Для этого есть файл user_discio.c, который находится в дереве проекта в разделе Application/User.

Если открыть данный файл, то мы увидим несколько полупустых функций, которые мы и должны с вами заполнить кодом, чтобы библиотека могла работать с нашей картой.

Нам потребуются следующие функции:

USER_initialize, USER_status, USER_read, USER_write, USER_ioctl.

То есть все функции, находящиеся в данном файле.

Подключим для начала нашу библиотеку в этот файл

 

/* Includes ------------------------------------------------------------------*/

#include <string.h>

#include "ff_gen_drv.h"

#include "sd.h"

/* Private typedef -----------------------------------------------------------*/

 

Затем подключим некоторые переменные и строковый массив

 

/* Private variables ---------------------------------------------------------*/

extern UART_HandleTypeDef huart1;

extern char str1[60];

extern sd_info_ptr sdinfo;

/* Disk status */

 

Посмотрим, какие входные аргументы переданы в функциях файла

1) Функция USER_initialize:

BYTE pdrv — номер физического носителя. У нас поддерживается только один и будет ноль.

2) Функция USER_status:

То же самое.

3) Функция USER_read:

BYTE pdrv — номер физического носителя

BYTE *buff — указатель на начало буфера для считывания данных

DWORD sector — логический адрес первого блока

UINT count — количество считываемых блоков.

4) Функция USER_write:

BYTE pdrv — номер физического носителя

BYTE *buff — указатель на начало буфера для записи данных

DWORD sector — логический адрес первого блока

UINT count — количество записыываемых блоков

5) Функция USER_ioctl (функция операций ввода-вывода):

BYTE pdrv — номер физического носителя

BYTE cmd — код команды (это не те команды, которые описаны в технической документации на карту, а какие — увидим позже).

void *buff — адрес буфера для считывания или записи управляющих данных.

Теперь давайте сначала изучим, при каких условиях мы попадаем в те или иные функции этого файла. Для этого расставим ловушки в виде строк в терминале.

Можно это сделать конечно и при помощи брейкпоинтов, но ходят определённые слухи, что работу с картой лучше не тормозить, ибо инициализация очень быстро просрочивается и нужно её проводить повторно, правда я работал и в отладке и данные слухи не подтвердились. Но тем не менее всё-таки терминальная программа.

Заголовки функций не привожу, так как из коментариев видно какая именно функция, чтобы не было слишком много текста

 

/* USER CODE BEGIN INIT */

Stat = STA_NOINIT;

HAL_UART_Transmit(&huart1,(uint8_t*)"USER_initializern",17,0x1000);

return Stat;

/* USER CODE END INIT */

 

/* USER CODE BEGIN STATUS */

Stat = STA_NOINIT;

HAL_UART_Transmit(&huart1,(uint8_t*)"USER_statusrn",13,0x1000);

return Stat;

/* USER CODE END STATUS */

 

/* USER CODE BEGIN READ */

HAL_UART_Transmit(&huart1,(uint8_t*)"USER_readrn",11,0x1000);

sprintf(str1,"sector: %lu; count: %drn",sector, count);

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

return RES_OK;

/* USER CODE END READ */

 

/* USER CODE BEGIN WRITE */

/* USER CODE HERE */

HAL_UART_Transmit(&huart1,(uint8_t*)"USER_writern",12,0x1000);

sprintf(str1,"sector: %lurn",sector);

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

return RES_OK;

/* USER CODE END WRITE */

 

/* USER CODE BEGIN IOCTL */

DRESULT res = RES_ERROR;

HAL_UART_Transmit(&huart1,(uint8_t*)"USER_ioctlrn",12,0x1000);

sprintf(str1,"cmd: %drn",cmd);

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

return res;

/* USER CODE END IOCTL */

 

На флеш-карте у меня следующая стркутура файлов и каталогов

 

index63

 

Перейдём в файл main.c и добавим некоторе количество глобальных переменных и структур

 

uint8_t sect[512];

extern char str1[60];

uint32_t byteswritten,bytesread;

uint8_t result;

extern char USER_Path[4]; /* logical drive path */

FATFS SDFatFs;

FATFS *fs;

FIL MyFile;

/* USER CODE END PV */

 

В функции main() сначала закомментируем код включения и инициализации нашей карты SD

 

//SD_PowerOn();

//sd_ini();

 

 

Вызовем теперь функцию инициализацию диска из библиотеки FatFS, хотя этого делать необязательно, при необходиости она вызовется потом сама, но всё же давайте вызовем, чтобы этот процесс у нас прошел заранее отдельно от всех остальных функций работы с библиотекой

 

//HAL_UART_Transmit(&huart1,(uint8_t*)"rn",2,0x1000);

disk_initialize(SDFatFs.drv);

/* USER CODE END 2 */

 

Соберём код, прошьём контроллер и посмотрим результат в терминальной программе

 

index64

 

Мы видим, что была вызвана функция инициализации.

Поэтому давайте вернёмся в файл user_discio.c и напишем код в эту функцию, убрав там кое-что ненужное

 

Stat = STA_NOINIT;

HAL_UART_Transmit(&huart1,(uint8_t*)"USER_initializern",17,0x1000);

SD_PowerOn();

if(sd_ini()==0) {Stat &= ~STA_NOINIT;} //Сбросим статус STA_NOINIT

return Stat;

 

Соберём код, прошьём контроллер и увидим, что наша карта нормально инициализировалась

 

index65

 

Продолжим наш код в main() и начнём писать процедуру чтения содержимого файла из карты SD. Мы раньше этим занимались уже неоднократно при использовании интерфейса SDIO, поэтому, думаю, что затруднений с этим у нас никаких не будет

 

disk_initialize(SDFatFs.drv);

//read

if(f_mount(&SDFatFs,(TCHAR const*)USER_Path,0)!=FR_OK)

{

  Error_Handler();

}

else

{

  if(f_open(&MyFile,"123.txt",FA_READ)!=FR_OK)

  {

    Error_Handler();

  }

  else

  {

    f_close(&MyFile);

  }

}

/* USER CODE END 2 */

 

Пока у нас нет собственно чтения файла, есть только открытие в режиме чтения.

Снова соберём код и прошьём контроллер. Посмотрим результат в терминале

 

index66

 

Мы видим, что засчёт вызова функции открытия файла у нас произошел вызов функции USER_ioctl с кодом команды 2.

Посмотрим, что это за команда. Да и вообще глянем коды всех команд

 

/* Generic command (Used by FatFs) */

#define CTRL_SYNC 0 /* Complete pending write process (needed at _FS_READONLY == 0) */

#define GET_SECTOR_COUNT 1 /* Get media size (needed at _USE_MKFS == 1) */

#define GET_SECTOR_SIZE 2 /* Get sector size (needed at _MAX_SS != _MIN_SS) */

#define GET_BLOCK_SIZE 3 /* Get erase block size (needed at _USE_MKFS == 1) */

#define CTRL_TRIM 4 /* Inform device that the data on the block of sectors is no longer used (needed at _USE_TRIM == 1) */

 

/* Generic command (Not used by FatFs) */

#define CTRL_POWER 5 /* Get/Set power status */

#define CTRL_LOCK 6 /* Lock/Unlock media removal */

#define CTRL_EJECT 7 /* Eject media */

#define CTRL_FORMAT 8 /* Create physical format on the media */

 

/* MMC/SDC specific ioctl command */

#define MMC_GET_TYPE 10 /* Get card type */

#define MMC_GET_CSD 11 /* Get CSD */

#define MMC_GET_CID 12 /* Get CID */

#define MMC_GET_OCR 13 /* Get OCR */

#define MMC_GET_SDSTAT 14 /* Get SD status */

 

/* ATA/CF specific ioctl command */

#define ATA_GET_REV 20 /* Get F/W revision */

#define ATA_GET_MODEL 21 /* Get model name */

#define ATA_GET_SN 22 /* Get serial number */

 

Вот их сколько!

Но нам нужны будут максимум 3 вида команд, не больше.

Посмотрем, что за команда эта с индексом 2. Это команда GET_SECTOR_SIZE, требование которой — вернуть в буфере количество байт в секторе.

Этим мы и займёмся. Зайдём в функцию USER_ioctl в файл user_diskio.c и напишем там следующий код, немного подправив существующий

 

DRESULT res = RES_ERROR;

HAL_UART_Transmit(&huart1,(uint8_t*)"USER_ioctlrn",12,0x1000);

sprintf(str1,"cmd: %drn",cmd);

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

if (pdrv) return RES_PARERR;

if (Stat & STA_NOINIT) return RES_NOTRDY;

res = RES_ERROR;

switch (cmd)

{

  case GET_SECTOR_SIZE : /* Get sectors on the disk (WORD) */

    *(WORD*)buff = 512;

    res = RES_OK;

    break;

  default:

    res = RES_PARERR;

}

SPI_Release();

return res;

 

Ещё раз соберём код и прошьём котроллер. Результат в терминальной программе будет теперь следующий

 

index67

 

Мы видим, что у нас была вызвана функция чтения нулевого блока видимо для того, чтобы считать информацию о файловой таблице (сектор MBR).

Поэтому перейдём в данную функцию и напишем в неё следующий код

 

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

if (pdrv || !count) return RES_PARERR;

if (Stat & STA_NOINIT) return RES_NOTRDY;

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

if (count == 1) /* Single block read */

{

  SD_Read_Block(buff,sector); //Считаем блок в буфер

  count = 0;

}

else /* Multiple block read */

{

}

SPI_Release();

return count ? RES_ERROR : RES_OK;

/* USER CODE END READ */

 

Код оказался не таким уж и сложным. Условие считывания нескольких секторов мы не обрабатываем, так как у нас запрашивается только один сектор, а забегая вперёд скажу, что скорее всего у нас несколько и не запросятся. Также заметим, что если у нас карта не SDHC, то мы умножаем номер блока на 512, тем самым определяя адрес побайтный а не поблочный.

Ещё раз попробуем собрать код и прошить контроллер, а затем посмотреть результат в терминале

 

index68

 

Мы видим, что у нас было запрошено ещё несколько секторов, а затем был запрошен статус вызовом соответствующей функции.

Немного подправим код в функции вызова статуса

 

Stat = STA_NOINIT;

HAL_UART_Transmit(&huart1,(uint8_t*)"USER_statusrn",13,0x1000);

if (pdrv) return STA_NOINIT;

return Stat;

 

Если проверить код, то мы должны будем увидеть то же самое с одной лишь разницей, что вызов функции определения статуса будет выполнен дважды.

Теперь перейдём в файл main.c и займёмся собственно считыванием содержимого нашего файла. Но так как из своей немалой практики работы с файлами мы помним, что читать большими блоками нелегко, лучше всё-таки оперировать небольшими порциями, да и памяти мы на это меньше израсходуем, то напишем отдельную функцию для этого

 

/* USER CODE BEGIN 0 */

FRESULT ReadLongFile(void)

{

  uint16_t i=0, i1=0;

  uint32_t ind=0;

  uint32_t f_size = MyFile.fsize;

  sprintf(str1,"fsize: %lurn",(unsigned long)f_size);

  HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

  ind=0;

  do

  {

    if(f_size<512)

    {

      i1=f_size;

    }

    else

    {

      i1=512;

    }

    f_size-=i1;

    f_lseek(&MyFile,ind);

    f_read(&MyFile,sect,i1,(UINT *)&bytesread);

    for(i=0;i<bytesread;i++)

    {

      HAL_UART_Transmit(&huart1,sect+i,1,0x1000);

    }

    ind+=i1;

  }

  while(f_size>0);

  HAL_UART_Transmit(&huart1,(uint8_t*)"rn",2,0x1000);

  return FR_OK;

}

/* USER CODE END 0 */

 

Код функции практически был взят из уроков по выводу файлов с картинками на экран дисплея, поэтому не нуждается в объяснении. Следует лишь напомнить, что считываем мы содержимое файла порциями по 512 байт. Имя файла мы не передаём, так как оно уже в структуре.

Вызовем теперь данную функцию в соответствующем месте в коде функции main()

 

else

{

  ReadLongFile();

  f_close(&MyFile);

}

 

Соберём код, прошьём контроллер и увидим содержимое нашего файла в терминальной программе. Я приведу лишь последнюю часть текста из файла, так как он слишком большой. Это выдержка из технической документации на наш контроллер и я специально взял оттуда побольше, чтобы проследить работу с несколькими блоками

 

index69

 

Мы видим, что порой в тексте появляется информация о вызове функций и о считывании определённых секторов. Мы потом уберём вывод данной информации, а пока она нам нужна.

 

В следующей части нашего урока мы научимся записывать файлы на карту SD, а также получать с неё информацию в виде списка файлов и каталогов, а также общего размера и свободного размера для данных.

 

Предыдущая часть Программирование МК STM32 Следующая часть

 

 

Отладочную плату можно приобрести здесь STM32F103C8T6

Модуль Micro-SD SPI можно приобрести здесь Micro-SD SPI

Переходник USB to TTL можно приобрести здесь USB to TTL ftdi ft232rl

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

STM SD. SPI. FATFS

6 комментариев на “STM Урок 88. SD. SPI. FATFS. Часть 3
  1. Anh:

    thanks for your helpful tutorial. I have some question:
    1. Do you make this project with only data sheet of SD Card?
    2. Can I make fatfs with Memory flash (example : W25Q64…)
    Thanks for your help!

  2. Василий:

    Добрый день! Прохожу по шагам ваш урок, и на этом шаге у меня пошли отличия: когда я добавляю код проверки на открытие файла, после инициализации сразу идет вызов функции чтения «USER_read», а не «USER_ioctl». И дальше процесс завершается ошибкой. Версия библиотеки FatFS R0.11

    • Василий:

      Я нашел, в чем отличия — максимальный размер кластера у меня стоял 512, поэтому библиотека и не пытается выяснять размер кластера. А возвращаемый код ошибки — FR_NO_FILESYSTEM. При этом карта памяти на ПК читается нормально. Файловая система FAT32 с размером кластера 512.

  3. Дмитрий:

    Добрый день! Автор огромное спасибо , но есть вопрос по поводу строки
    if (!(sdinfo.type & 4)) sector *= 512; /* Convert тоесть если карта не v1 то Сектор умножаем на 512.
    Может знак отрицания лишний здесь ? У нас же когда карта v1 адресация побайтная как раз, а когда v2 то по 512 байт

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*