LPCXpresso Урок 11. USB+SPI. Делаем картридер.
Курс для новичков продолжается ознакомлением с SPI на примере работы с SD/MMC карточками. А поскольку USB-MSC мы уже проходили, то соорудим пародию на картридер. Пользоваться им я категорически запрещаю, т.к. размер карты зашит в коде и при установки другой карты вы можете потерять ваши данные.Нам понадобится MMC либо SD карточка объемом до 2Гб. SDHC карточка нам не подойдет! С ней пример работать не будет.
Наиболее правильно было бы взять код FatFS от ChaN в котором присутствует и определение размера, и работа с SDHC картами. Но код тяжел для начинающих. Желающие могут попробовать самостоятельно подключить правильную реализацию. Я же ограничусь более простой версией.
Так же отмечу, что это довольно сложный урок, и по-хорошему вы уже должны быть не начинающим.
Схема
Используем схему из прошлого урока с подключением USB разъема и добавляем подключение карточки. Для этого нам надо собрать следующую схему:Можете подпаять проводки либо непосредственно к карточке, которую не жалко, либо как я воспользоваться слотом для карты. Приведённая схема одинакова как для SD так и для MMC карты. Все лишние (не показанные) выводы MMC карты остаются неподключенными.
Дорабатываем код
Задачи универсальности перед собой ставить не станем. Будем делать код под конкретную карту памяти (объем карты). В качестве основы для данного проекта возьмем пример из прошлого урока и доработаем его. Доработка пройдет следующим образом:
- Настройка SPI
- Код для работы с картой памяти
- Доработка функций чтения/записи USB-диска
Для первых двух пунктов обратимся за помощью к библиотеке LPC1343 Code Base.
Кроме этого надо удалить то, что нам не нужно. В директории src проекта удаляем файл diskimg.c и в директории inc файл diskimg.h, т.к. они нам больше не нужны. Для этого у файла вызываем контекстное меню и выбираем пункт Delete. На вопрос удалить ли файл из файловой системы отвечаем положительно.
Настройка SPI
В проекте (не библиотеке, т.к. здесь мы её не подключали) создаем файл ssp.h со следующим содержимым:
#ifndef SSP_H_
#define SSP_H_
typedef enum { SSP_SLOW, SSP_FAST } ssp_speed_t;
uint8_t ssp_transfer(uint8_t data);
inline void ssp_enable_cs(void);
inline void ssp_disable_cs(void);
void ssp_init(ssp_speed_t clk_speed);
#endif /* SSP_H_ */
Поскольку карта памяти должна инициализироваться на скорости 400кГц, а работать может и на 20МГц, то функцию инициализации напишем с одним параметром, задающим медленный/высокоскоростной режим работы SPI. Создаём файл ssp.c для реализации функций. После подключения заголовочных файлов добавляем функции для выбора устройства.
#define SSEL_HIGH (LPC_GPIO0->MASKED_ACCESS[(1 << 2)] = (1 << 2))
#define SSEL_LOW (LPC_GPIO0->MASKED_ACCESS[(1 << 2)] = 0)
inline void ssp_enable_cs(void) {
SSEL_LOW;
}
inline void ssp_disable_cs(void) {
SSEL_HIGH;
}
Эти функции нам нужны, т.к. мы не будем использовать имеющийся аппаратный контроль, а будем выбирать карточку самостоятельно (ввиду некоторых ограничений в протоколе работы с картой памяти).
Далее реализуем функцию передачи. В ней мы просто заносим данные для передачи в регистр данных DR контроллера SPI. После этого контроллер SPI самостоятельно запускает цикл обмена. Нам же остается только ждать его завершения, о чём сообщит нам второй бит в регистре SR контроллера SPI. По окончании обмена мы извлекаем из регистра данных DR контроллера SPI принятые данные и возвращаем их как результат работы функции.
uint8_t ssp_transfer(uint8_t data) {
LPC_SSP->DR = data;
while((LPC_SSP->SR & (1<<2)) == 0); // Ждем пока установится Rx Not Empty - завершение приема
return LPC_SSP->DR;
}
Теперь приступим к функции инициализации:
void ssp_init(ssp_speed_t clk_speed) {
uint8_t i, Dummy;
LPC_SYSCON->PRESETCTRL |= (0x1 << 0);
LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 11);
LPC_IOCON->PIO0_8 &= ~0x07; // SPI MISO
LPC_IOCON->PIO0_8 |= 0x01; //
LPC_IOCON->PIO0_9 &= ~0x07; // SPI MOSI
LPC_IOCON->PIO0_9 |= 0x01; //
LPC_IOCON->SCKLOC = 0x01; // указываем какой из возможных выводов будем задействовать
LPC_IOCON->PIO2_11 = 0x01; // сам вывод P2.11 настраиваем на функцию 1 - SSP clock
LPC_IOCON->PIO0_2 &= ~0x07; // SPI SSEL используем простой GPIO вывод
LPC_GPIO0->DIR |= (1 << 2); // P0.2 настраиваем на вывод
SSEL_HIGH; // устанавливаем SSEL в единицу - отключаем карточку
if (clk_speed == SSP_SLOW) {
/* (PCLK / (CPSDVSR - [SCR+1])) = (7,200,000 / (2 x [8 + 1])) = 400 KHz */
LPC_SYSCON->SSPCLKDIV = 10; // Делитель 10 для SPI
// Формат SPI пакета 8 бит данных, CPOL = 0, CPHA = 0, SCR (доподнительный делитель) = 8
LPC_SSP->CR0 = 0x0807;
} else {
/* (PCLK / (CPSDVSR - [SCR+1])) = (72,000,000 / (2 * [1 + 1])) = 18.0 MHz */
LPC_SYSCON->SSPCLKDIV = 1; // Делитель 1 для SPI
// Формат SPI пакета 8 бит данных, CPOL = 0, CPHA = 0, SCR = 1
LPC_SSP->CR0 = 0x0107;
}
// Минимальный предделитель частоты 0x02
LPC_SSP->CPSR = 0x2;
for (i = 0; i < 8; i++) {
Dummy = LPC_SSP->DR; // очищаем буффер приёма RxFIFO
}
// Устанавливаем мастера и разрешаем работу SPI
LPC_SSP->CR1 = 0x02;
}
Кроме уже известных нам разрешения работы, подаче тактов и выбора для выводов функции для работы с периферией у нас имеется так же:
- установка в регистр SSPCLKDIV делителя тактовой частоты для контроллера SPI (задание частоты работы контроллера SPI) в 10 или 1, в зависимости от требуемого режима;
- установка в регистр CR0 формата пакета (подробно в документации к контроллеру) и дополнительного делителя (задание частоты тактовой линии SPI) в 8 либо 1, в зависимости от требуемого режима;;
- установка в регистр CPSR делителя частоты для опорной частоты тактовой линии SPI в минимальное значение 2;
- удаление «мусора» из приемного буфера SPI;
- установка в регистр CR1 работы контроллера в режиме «мастер» и разрешение тем самым работы контроллера SPI;
На первый взгляд может показаться сложной система задания частоты шины SPI. По этому попробую описать последовательно. Системная частота делиться на делитель в регистре SSPCLKDIV и эта частота задаётся основной для работы контроллера SPI. Затем она делиться на делитель в регистре CPSR для формирования опорной частоты шины SPI (это ещё не есть частота шины). И, наконец, опорная частота делиться на коэффициент, заданные в поле регистра CR0 (+1) для получения частоты тактового сигнала шины.
Прерывания нам здесь не нужны, мы владелец шины и сами определяем, когда осуществлять передачу. Поэтому здесь всё.
Работа с картой
Подробно рассматриваться не будет (Очень кстати, пока готовился курс, появилась статья от lleeloo). Функции расположены в файле sdcard.c и описаны в файле sdcard.h, которые требуется добавить к проекту.
Отмечу, что в файле sdcard.h содержится определение размера нашей карты памяти:
#define MSC_BlockCount (2*1024*1024*2)
#define MSC_BlockSize 512
#define MSC_MemorySize ((uint32_t)MSC_BlockCount*MSC_BlockSize)
Всё имена те же, что были в примере. Укажите здесь количество блоков вашей карты памяти в MSC_BlockCount. Ещё раз напомню, что SDHC не поддерживаются и более 2Гб не может быть.
Так же добавился вызов инициализация карты памяти в функции main в файле usbmemrom_main.c:
if(SD_Init()) {
SetLed(1);
while(1) __WFI();
}
Функция SetLed предназначена для целей диагностики. Так если при инициализации карты возникла ошибка, то будет зажжен светодиод на плате, индицируя ошибку.
Функции чтения/записи
В файле msccallback.c правим функции чтения и записи блока диска. Но так как размер блока на диске у нас 512 байт, а размер блока USB всего 64 байта, то для чтения одного блока диска функция чтения будет вызвана 8 раз подряд с разным смещением для одного и того же блока. И точно так же для записи. Что бы ни делать несколько чтений одного блока с диска (а тем более записей), добавим кэширование:
uint8_t CardBuffer[512];
uint32_t Sector = 0;
uint8_t Cached = 0;
uint8_t Changed = 0;
void SwitchSector(uint32_t number) {
if(Cached) { // если в кэш что-то есть
if(Sector == number) { // если в кэш запрошенной блок, то просто возвращаемся
return;
}
// иначе кэш надо заменить
if(Changed) { // если данные в кэш изменялись, то сохраняем изменения
SD_WriteSector(Sector, CardBuffer);
Changed = 0;
}
}
// запоминаем номер нового блока
Sector = number;
SD_ReadSector(Sector, CardBuffer); // читаем новый блок с карты в кэш
Cached = 1; // устанавливаем флаг что кэш содержит данные
}
После этого функция чтения выглядит крайне просто:
void MSC_MemoryRead (uint32_t offset, uint8_t dst[], uint32_t length) {
uint32_t n;
SwitchSector(offset/512);
offset %= 512;
for (n = 0; n < length; n++) {
dst[n] = CardBuffer[offset + n];
}
}
И функция записи так же проста, только добавлен код записи буфера по достижение конца блока:
void MSC_MemoryWrite (uint32_t offset, uint8_t src[], uint32_t length) {
uint32_t n;
SwitchSector(offset/512);
offset %= 512;
for (n = 0; n < length; n++) {
CardBuffer[offset+n] = src[n];
}
Changed = 1;
if(offset + length == 512) {
SD_WriteSector(Sector, CardBuffer);
Changed = 0;
}
}
В коде присутствует вызов той же SetLed, на этот раз с целью индикации обращения к карте.
Запуск
Как обычно компилируем и исправляем ошибки.
После подключения добавленного разъема к компьютеру светодиод начнет мигать, а в системе появиться новый съемный диск.
Если светодиод постоянно ярко светится, что бывает, когда в начале был другой код, который «сбил» инициализацию карте, то обесточьте плату и снова подайте питание.
Если система предлагает вам диск отформатировать, то вероятно у вас в коде не верно указан размер. Хотя возможно просто карточка не была отформатирована.
Я повторно рекомендую не использовать карточки с важными для вас данными. Сам я при экспериментах указывал и меньшие и большие размеры, но кто знает как поведёт себя нестабильная система.
Статистика
Кода в Debug версии у меня получилось 4216 байт кода. Скорость чтения составила 194 кБайт/с, скорость записи 84 кБайт/с.
После переключения в Release версию код уменьшился до 2772 байта. Скорость чтения при этом выросла до 239 кБайт/с, а записи до 95кБайт/с.
Вместо заключения
С сожалением должен признать, что урок получился объемным и непонятным. При этом при всём непосредственно по теме (SPI) пара жалких абзацев. Даже исключение принципов работы с картой памяти хоть и сократило статью, но недостаточно.
Однако, буду надеяться, что изучение прошлых примеров научило вас разбираться в коде.
P.S.: Этот урок был написан до SPI. Подключаем дисплей от Nokia 3310., так что информация дублируются.
Файлы: usbmemrom_sd_mmc.zip