Урок 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.
Вот описание команды
Вызовем данную команду в нашей функции считывания блока
uint16_t cnt;
result=SD_cmd (CMD17, lba); //CMD17 даташит стр 50 и 96
if (result!=0x00) return 5; //Выйти, если результат не 0x00
Посмотрим, как читаются данные с карты
Поэтому пропустим байт и считаем данные в буфер
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, который мы передаём, это своего рода маркер начала блока, чтобы карта могла определить, с какого байта начинать запись. Чтобы понять процесс, приведу процесс записи блока из технической документации
Я выделил маркер (токен). Вот его отдельное описание (стр. 112)
Создадим на функцию записи также прототип и вернёмся опять в файл 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);
Соберём код, прошьём контроллер и посмотрим результат (мне, почему-то приходится перзагружать потом контроллер ещё и кнокой, видимо издержки раздельного питания)
Мы видим нашу строку. Это значит, что блок у нас нормально записывается и читается. Отлично. Закомментируем весь этот код чтения сектора
//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 */
На флеш-карте у меня следующая структура файлов и каталогов
Перейдём в файл 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 */
Соберём код, прошьём контроллер и посмотрим результат в терминальной программе
Мы видим, что была вызвана функция инициализации.
Поэтому давайте вернёмся в файл 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;
Соберём код, прошьём контроллер и увидим, что наша карта нормально инициализировалась
Продолжим наш код в 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 */
Пока у нас нет собственно чтения файла, есть только открытие в режиме чтения.
Снова соберём код и прошьём контроллер. Посмотрим результат в терминале
Мы видим, что засчёт вызова функции открытия файла у нас произошел вызов функции 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;
Ещё раз соберём код и прошьём котроллер. Результат в терминальной программе будет теперь следующий
Мы видим, что у нас была вызвана функция чтения нулевого блока видимо для того, чтобы считать информацию о файловой таблице (сектор 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, тем самым определяя адрес побайтный а не поблочный.
Ещё раз попробуем собрать код и прошить контроллер, а затем посмотреть результат в терминале
Мы видим, что у нас было запрошено ещё несколько секторов, а затем был запрошен статус вызовом соответствующей функции.
Немного подправим код в функции вызова статуса
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);
}
Соберём код, прошьём контроллер и увидим содержимое нашего файла в терминальной программе. Я приведу лишь последнюю часть текста из файла, так как он слишком большой. Это выдержка из технической документации на наш контроллер и я специально взял оттуда побольше, чтобы проследить работу с несколькими блоками
Мы видим, что порой в тексте появляется информация о вызове функций и о считывании определённых секторов. Мы потом уберём вывод данной информации, а пока она нам нужна.
В следующей части нашего урока мы научимся записывать файлы на карту SD, а также получать с неё информацию в виде списка файлов и каталогов, а также общего размера и свободного размера для данных.
Предыдущая часть Программирование МК STM32 Следующая часть
Отладочную плату можно приобрести здесь STM32F103C8T6
Модуль Micro-SD SPI можно приобрести здесь Micro-SD SPI
Переходник USB to TTL можно приобрести здесь USB to TTL ftdi ft232rl
Смотреть ВИДЕОУРОК (нажмите на картинку)
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!
Thank you also for your interest in my resource!
1. I use a variety of documentation.
2. With this memory, this project is likely to be incompatible, since it has a block size of 256 bytes.
Добрый день! Прохожу по шагам ваш урок, и на этом шаге у меня пошли отличия: когда я добавляю код проверки на открытие файла, после инициализации сразу идет вызов функции чтения «USER_read», а не «USER_ioctl». И дальше процесс завершается ошибкой. Версия библиотеки FatFS R0.11
Я нашел, в чем отличия — максимальный размер кластера у меня стоял 512, поэтому библиотека и не пытается выяснять размер кластера. А возвращаемый код ошибки — FR_NO_FILESYSTEM. При этом карта памяти на ПК читается нормально. Файловая система FAT32 с размером кластера 512.
Добрый день! Автор огромное спасибо , но есть вопрос по поводу строки
if (!(sdinfo.type & 4)) sector *= 512; /* Convert тоесть если карта не v1 то Сектор умножаем на 512.
Может знак отрицания лишний здесь ? У нас же когда карта v1 адресация побайтная как раз, а когда v2 то по 512 байт
Вполне возможно, сейчас даже тяжело разбираться. Мог и перепутать.
Здравствуйте.
В этой строке, видимо действительно ошибка.
У меня карта v2, чтение файла заработало после стирания «!».
Если у вас не работает, попробуйте удалить.
Возможно, тут двойная путаница.
Я не помню, есть ли SDv2, которые не-HC, но если нужно адресовать в байтах, а не блоках для всех устройств ниже SDHC, то правильно было бы заменить в условии 4 на 8.
А еще лучше так, чтоб без магических чисел:
if (!(sdinfo.type & CT_BLOCK)) …
А также проверить корректность определения типа тут:
sdinfo.type = (ocr[0] & 0x40) ? CT_SD2 | CT_BLOCK : CT_SD2; // SDv2 (HC or SC)
Если SDSC — это SD 1.1 до 4Гб, т.е. по идеологии кода просто CT_SD2, то проверки с CT_BLOCK достаточно.
why did u declared UserPath as extern in main.c? in which header file UserPath has been defined? shouldn't we need to include that header file in main.c?
Help Please.
The code is working fine till Read Write block operation.
but the movement we add
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;
variable to main.c file and call function » if(f_mount(&SDFatFs,(TCHAR const*)USER_Path,0)!=FR_OK) »
it gives error —> undefined symbol USER_Path …. we have declared it with extern class register in mian.c so here my question is, in which file the variable USER_Path is actually declared..?
or how do i overcome this error.
USER_Path need use as USERPath
without underline
Спасибо. Наткнулся на эту же проблему. Если бы не этот комментарий, встал бы в тупик.
Доброго времени суток! У меня 5 карточек 1, 2, 8, 8 и 16ГБ 1,2,16 определяются как Type SD: 0x00 и не работают совсем. Те что на 8ГБ определяются как Type SD: 0x04 и останавливаются на sector: 0; count: 1. Что можно сделать? Спасибо за ранее.
Здравствуйте, всё работает, но если вытащить карту и вставить обратно, вылетает ошибка DISK_ERR, помогает только сброс питания. Может знаете в чем дело?
ошибка в f_mount(
пока отложил проект..
смогли победить?