LPCXpresso Урок 11. USB+SPI. Делаем картридер.

Курс для новичков продолжается ознакомлением с SPI на примере работы с SD/MMC карточками. А поскольку USB-MSC мы уже проходили, то соорудим пародию на картридер. Пользоваться им я категорически запрещаю, т.к. размер карты зашит в коде и при установки другой карты вы можете потерять ваши данные.

Нам понадобится MMC либо SD карточка объемом до 2Гб. SDHC карточка нам не подойдет! С ней пример работать не будет.

Наиболее правильно было бы взять код FatFS от ChaN в котором присутствует и определение размера, и работа с SDHC картами. Но код тяжел для начинающих. Желающие могут попробовать самостоятельно подключить правильную реализацию. Я же ограничусь более простой версией.

Так же отмечу, что это довольно сложный урок, и по-хорошему вы уже должны быть не начинающим.

Схема

Используем схему из прошлого урока с подключением USB разъема и добавляем подключение карточки. Для этого нам надо собрать следующую схему:
Схема подключения SD/MMC карты

Можете подпаять проводки либо непосредственно к карточке, которую не жалко, либо как я воспользоваться слотом для карты. Приведённая схема одинакова как для SD так и для MMC карты. Все лишние (не показанные) выводы MMC карты остаются неподключенными.

Дорабатываем код

Задачи универсальности перед собой ставить не станем. Будем делать код под конкретную карту памяти (объем карты). В качестве основы для данного проекта возьмем пример из прошлого урока и доработаем его. Доработка пройдет следующим образом:

  1. Настройка SPI
  2. Код для работы с картой памяти
  3. Доработка функций чтения/записи 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