RomeoGolf

Ср 16 Май 2018

USB-polygon-18: Подключение SD-карты

Введение

К отладочной плате по SPI подключен экран. Хочется попробовать подключить также внешнюю энергонезависимую память.

Пожалуй, самый простой вариант — это SD-карточка в режиме SPI. Буду цеплять ее параллельно с экраном.

Полезная вещь, например, для хранения знаков для экрана, чтобы не занимать память контроллера. Опять же, на будущее — для хранения настроек прибора, скажем, последней использованной температуры паяльника в самодельной паяльной станции. Можно, конечно, воспользоваться энергонезависимой памятью самого контроллера. Но, во-первых, ресурс памяти по чтению-стиранию-записи не бесконечен, а карточку заменить проще. Во-вторых, теоретически может понадобиться большой объем данных, например, под картинки для вывода на экран или звуки какие-нибудь. Причем, карточку с набором «файлов» можно заменять для использования другого набора.

В конце концов, хочется просто опробовать работу с картой памяти: вдруг пригодится когда-нибудь.

Подключение карты к устройству

Нашел в закромах старенькую карточку Kingmax на 512 МБ. Вещь из тех, что «девать некуда, и выкинуть жалко». Найти ей применение сегодня непросто: почти везде в современных гаджетах используются microSD, да и такой объем памяти почти бесполезен даже в приборах постарше — ни в фотоаппарат, ни, тем более, видеорегистратор… Вот и очень хорошо, что ей выпал шанс еще поработать.

Как обычно, сперва порылся в интернетах в поисках распиновки и системы команд. Нарыл следующее:

  1. Спецификация Part_1_Physical_Layer_Simplified_Specification_Ver6.00 с сайта SD card, — первоисточник, библия, камасутра и устав. Тут есть все, что надо, но на английском и местами не очень понятно даже при знании английского. На момент написания данного опуса спецификация версии 6.0 самая свежая, в ней даже есть цоколевка карт (в отличие от, скажем, версии 3.01).
  2. Вольный перевод упомянутого документа и вынесенная отдельно глава 7 про SPI, что удобно для чтения по диагонали и поиска места, нужного именно сейчас, чтобы вдумчиво почитать в оригинале.
  3. Статья «Как работать с SD-картой» на английском.
  4. Ее переводы 1 и 2 на русский.
  5. Пожалуй, самое полезное, полное и с хорошим примером из того, что я нашел на русском — статья в двух частях (1 и 2), в которой есть все для того, чтобы взять паяльник и попробовать.

Беру паяльник и пробую. Гнезда для карты у меня нет, покупать не хочу, делать сам — тоже. Просто припаял провода к контактам карты. Потом пожалел об этом. Надо было сперва дамп снять с помощью кардридера, хотя бы первых секторов, чтобы было потом с чем сравнивать чтение по SPI, ну да ладно.

Припаял опять же провода МГТФ. С имеющимся в наличии плоским шлейфом не сложилось: очень уж легкоплавкая изоляция у него, а паяльник у меня так себе. Получилось так:

Распайка карты

На устройстве провода припаял к контактным площадкам порта B, некоторые к уже занятым площадкам, вместе с проводами экрана от Nokia:

  • 1 контакт подсоединил к площадке разряда 6: это CS, но «штатный» CS SPI-порта занят на экран, буду использовать пока свободный 6 разряд;
  • 2 контакт — к площадке 2: Data In к MOSI;
  • 3 и 6 контакты — на корпус, на полигон лицевой стороны устройства;
  • 4 контакт — питание — к тому же стабилизатору, от которого питается экран;
  • 5 контакт — к площадке 1, это CLK;
  • 7 контакт — к площадке 3: Data Out к MISO;

Контакты DI и DO должны быть «подтянуты» к питанию через 50-килоомные резисторы. Я сделал это прямо на карточке, но резисторы нашел на 30 кОм.

8 и 9 контакты карточки в режиме SPI не используются. На устройстве площадка 5 занята под (D/C) экрана, площадка 4 — под сброс экрана. Осталась свободной еще площадка 7 разряда.

В файл common.h теперь надо добавить определение для нового подключенного разряда:

    #define BIT_CS_SD (1 << 6)

Устройство в сборе стало выглядеть еще страшнее:

Подключение - вид сверху Подключение - вид снизу

Инициализация карты

Процедура инициализации просто и понятно расписана в первой части упомянутой выше статьи.

Для работы с картой, инициализации в том числе, понадобятся функции байтового обмена по SPI. Сделаю их по аналогии с out8bit(), работающей с экраном, только буду использовать линию CS карты и добавлю помимо функции записи еще и функцию чтения:

void SdOutByte(uint8_t data8) {
    PORTB &= ~BIT_CS_SD;            // cs -> 0
    SPDR = data8;
    while (!(SPSR & (1 << SPIF))) ; // wait for transmit
    PORTB |= BIT_CS_SD;             // cs -> 1
}

uint8_t SdInByte(void) {
    PORTB &= ~BIT_CS_SD;            // cs -> 0
    SPDR = 0xFF;
    while (!(SPSR & (1 << SPIF))) ; // wait for transmit
    PORTB |= BIT_CS_SD;             // cs -> 1
    return SPDR;
}

Как можно заметить, я включаю CS карты для каждого байта отдельно, а не на весь период обмена. Возможно, это не слишком оптимально с точки зрения быстродействия. Но при обмене по SPI все равно работа не слишком быстрая, зато так надежнее при одновременно подключенных нескольких ведомых: точно не пересекутся.

На основе этих функций можно сделать функцию отправки команды карте. Для этого добавлю тип-перечисление для выбора типа отклика на команду, а еще объявлю переменную-массив для получения этого самого отклика. В максимальном варианте он будет пятибайтовым, а в случае ответа типа R1 буду читать только первый байт этого массива.

enum ResponceType {R1, R2, R3, R7};
uint8_t sdResponce[5];

Теперь можно добавить и функцию для командования флэшкой:

void SdSendCommand(uint8_t Index, uint32_t Argument, uint8_t Crc,
                   enum ResponceType responceType, uint8_t *Responce) {
    SdOutByte(0xFF);
    SdOutByte(Index);
    SdOutByte((uint8_t)(Argument >> 24));
    SdOutByte((uint8_t)(Argument >> 16));
    SdOutByte((uint8_t)(Argument >> 8));
    SdOutByte((uint8_t)(Argument >> 0));
    SdOutByte(Crc);

    uint8_t cntErr = 0xFF;
    do {
        Responce[0] = SdInByte();
    } while ((cntErr-- != 0) && ((Responce[0] & 0x80) != 0));

    switch(responceType) {
        case R1:
            return;
        case R2:
            Responce[1] = SdInByte();
            return;
        case R3:
        case R7:
            Responce[4] = SdInByte();
            Responce[3] = SdInByte();
            Responce[2] = SdInByte();
            Responce[1] = SdInByte();
            return;
    }
}

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

Тип ответа надо указывать, соответствующий индексу команды в первом параметре. Контрольную сумму можно использовать, а можно и нет: по умолчанию ее вычисление в режиме SPI отключено, но младший бит должен быть «1».

А индексы команд надо бы определить отдельно макросообразно для удобства:

/* =============================================================== */
/* MMC card commands                                               */
/* =============================================================== */
/* Standart card commands CMDx */
#define MMC_COMMANDS_BASE       (unsigned char) 0x40
#define MMC_GO_IDLE_STATE       MMC_COMMANDS_BASE + 0         // CMD0
#define MMC_SEND_OP_COND        MMC_COMMANDS_BASE + 1         // CMD1
#define MMC_SEND_IF_COND        MMC_COMMANDS_BASE + 8         // CMD8
#define MMC_SEND_CSD            MMC_COMMANDS_BASE + 9         // CMD9
#define MMC_SEND_CID            MMC_COMMANDS_BASE + 10        // CMD10
#define MMC_SEND_STATUS         MMC_COMMANDS_BASE + 13        // CMD13
#define MMC_READ_SINGLE_BLOCK   MMC_COMMANDS_BASE + 17        // CMD17
#define MMC_WRITE_SINGLE_BLOCK  MMC_COMMANDS_BASE + 24        // CMD24
#define MMC_APP_CMD             MMC_COMMANDS_BASE + 55        // CMD55
#define MMC_READ_OCR            MMC_COMMANDS_BASE + 58        // CMD58
// Application specific command ACMDx
#define MMC_CMD_SD_STATUS       MMC_COMMANDS_BASE + 13        // ACMD13
#define MMC_CMD_SD_SEND_OP_COND MMC_COMMANDS_BASE + 41        // ACMD41
#define MMC_CMD_SD_SEND_SCR     MMC_COMMANDS_BASE + 51        // ACMD51

Это определение команд я бессовестно упер из упомянутой статьи, точнее, из приложенного к ней примера кода.

Теперь после функций инициализации экрана и перед вызовом SetupHardware() начинаю инициализировать карту. Для начала надо отправить на нее минимум 74 такта CLK по SPI:

    // ********************************
    /* SD card */

    /* > 74 clk to init */
    for (uint8_t i = 0; i < 10; i++) {
        SdOutByte(0xFF);
    }

Затем подаю команду 0, без аргументов (точнее, с нулем в аргументе), с заранее вычисленной контрольной суммой (это важно, пока не перешли в режим SPI), с ожиданием отклика R1, который надо записать в sdResponce:

    SdSendCommand(MMC_GO_IDLE_STATE, 0, 0x95, R1, sdResponce);

Далее жду отклик:

    uint16_t cntErr = 0x7FF;

    if(sdResponce[0] == 0x01) {
        do {
            SdSendCommand(MMC_SEND_OP_COND, 0, 1, R1, sdResponce);
        } while ((sdResponce[0] != 0) && (cntErr-- != 0));

        PORTD = 0x1C;

        if (sdResponce[0] != 0) {
            PORTD = 0x55;
        }
    } else {
        /* error! */
        PORTD = 0xAA;
    }

    for(;;){}

В качестве параметра Crc для SdSendCommand() передается единица. В режиме SPI, напомню, проверка CRC7 отключена по умолчанию, но в младшем разряде обязательно должна быть единица, так что можно передавать любое нечетное число.

В переменной cntErr будет счетчик таймаута ожидания. Отправляю команду 1 до тех пор, пока не получу нулевой отклик, либо до таймаута. И здесь выставляю на светодиодах 0x1C, то есть, три огонька в серединке.

Затем проверяю отклик на не ноль. Если не ноль, то зажигаю светодиоды через один, начиная с младшего. А если отклик не пришел и на команду 0 (до команды 1 дело не дошло) — зажигаю светодиоды через один, начиная со второго.

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

В результате вижу три огонька подряд посередке, а это значит, что команда 0 прошла, команда 1 тоже прошла, и отклик на нее получен. Карта инициализирована.

Надо сказать, что проведена инициализация по самому простому варианту, и не все карты таким образом можно инициализировать. Но я использую карту SD — не SDHC и не SDXC, при этом карта припаяна, то есть, вставлять другую не предполагается. Поэтому начал с простейшего способа, который должен был заработать. И заработал.

Чтение регистров

Подготовка

Судя по косвенным признакам (по состоянию светодиодов), с картой уже можно работать. Но хотелось бы получить более достоверное подтверждение инициализированности карты. Пожалуй, считаю-ка я регистры CID и CSD. Заодно получу более полную информацию о том, что же у меня за карта и что она может.

Добавлю соответствующую функцию: SdReadReg(), но для нее заранее добавлю функцию ожидания токена данных и тип-перечисление для выбора требуемого регистра. Ожидание токена данных пригодится и дальше при чтении данных.

Кстати, надо добавить и определение токена чуть ниже определения кодов команд:

//==================================================================//
// MMC data tokens                                                  //
//==================================================================//
#define MMC_START_TOKEN_SINGLE (unsigned char) 0xFE
//==================================================================//

Вставляю где-нибудь чуть выше определения функции SdOutByte():

enum RegType {CID, CSD};

А после SdSendCommand() записываю:

bool SdWaitForDataToken(void)
{
  uint8_t answer;
  uint8_t maxErrors = 0xFF;

  do {
      answer = SdInByte();
  } while ((maxErrors--)&&(answer != MMC_START_TOKEN_SINGLE));

  if (answer != MMC_START_TOKEN_SINGLE)
  {
      return false;
  } else {
      return true;
  }
}

bool SdReadReg(enum RegType regType, uint8_t * buffer)
{
    switch (regType) {
        case CID:
            SdSendCommand(MMC_SEND_CID, 0, 1, R1, sdResponce);
            break;
        case CSD:
            SdSendCommand(MMC_SEND_CSD, 0, 1, R1, sdResponce);
            break;
        default:
            return false;
    }
    if (sdResponce[0] == 0x00) {
        if (SdWaitForDataToken()) {
            for (uint8_t i = 0; i < 16; i++) {
                buffer[i] = SdInByte();
            }
            SdOutByte(0xFF);
            SdOutByte(0xFF);
            return true;
        } else {
            return false;
        }
  } else {
      return false;
  }
}

Тут все достаточно несложно. Сперва в функции проверяется, какой из регистров нужно читать, при этом формируется соответствующая команда для карточки. Аргумент нулевой, КС нулевая с единицей в младшем разряде, ожидается отклик типа R1, который будет записан в буфер sdResponce.

Далее проверяется полученный отклик, и если он равен нулю, то есть, все в порядке, то ждем токен данных. Дождавшись пишем 16 байтов в буфер (передаваемый по указателю).

Потом идет запись двух байтов — зачем? Затем, что карта добавляет к блоку данных два байта контрольной суммы. Их тоже нужно прочитать, чтобы завершить операцию. Но сами байты КС нас не интересуют. Поэтому можно просто выдать дважды восемь тактов частоты по SCLK. Для этого можно использовать как функцию чтения, так и функцию записи, не принципиально. Семантически вернее вставить чтение, а результаты игнорировать. Но, во-первых, в прототипе (в коде, на котором я основывался) была запись, а во-вторых, захотелось попробовать и посмотреть на результат. Может, позже исправлю.

Функция возвращает булев тип, true если все прошло успешно, и false, если выбран неправильный регистр (что почти нереально), если получен ненулевой отклик на команду или долго нет токена данных.

Теперь надо закрыть мертвый цикл, который был нужен, чтобы посмотреть состояние светодиодов после инициализации карты. Кроме того, открываю закрытый ранее фрагмент кода, инициализировавший 128-байтовый буфер данных, но заполняю его теперь, скажем, 0x0F, после чего вызываю команду чтения регистра:

    for(int i = 0; i < 128; i++) {
        data[i] = (i + 5);
        /*data[i] = 0x0F;*/
    }

    SdReadReg(CID /*CSD/*, data);

Теперь после подачи питания на устройство инициализируется карточка (после экрана, конечно), а затем, перед выполнением основного цикла программы, содержимое выбранного регистра читается в буферный массив. В модуле fake_fs уже реализовано чтение на ПК этого массива в виде файла DATA.BIN.

Теперь можно считать оба регистра по очереди. Правда, для выбора второго регистра надо переместить символы комментария в вызове функции SdReadReg() и перекомпилировать программу. Можно, конечно, сделать выбор регистра при помощи кнопок на устройстве, либо при помощи чтения какого-то специального служебного «файла», но операция одноразовая, так что не стоит усложнять код ради такого.

Разбор CID

После компиляции и перепрошивки открываю на устройстве файл DATA.BIN:

00000000: 13 4b 47 53 44 35 31 32 10 f7 02 80 11 00 68 e9  .KGSD512......h.
00000010: 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................
...

И далее до 128 байтов. Интересуют первые 16. Регистр передается старшим байтом вперед, поэтому в буфере он как бы развернут, старший байт имеет смещение 0. Разберу CID в соответствии со спецификацией:

Name Field Width CID-slice Значение
Manufacturer ID MID 8 [127:120] 0x13
OEM/Application ID OID 16 [119:104] KG
Product name PNM 40 [103:64] SD512
Product revision PRV 8 [63:56] 1.0
Product serial number PSN 32 [55:24] 0x118002F7
reserved 4 [23:20] 0x0
Manufacturing date MDT 12 [19:8] Июнь 2008
CRC7 checksum CRC 7 [7:1] 0x64
not used, always 1 1 [0:0] 1

Значение контрольной суммы в таблице записал с учетом сдвига вправо, так как младший разряд этого байта к ней не относится, она семиразрядная. Остальное все достаточно очевидно. Правда, числовые данные мне ни о чем не говорят, но «KG» для KingsMax выглядит довольно логично, а «SD512» для карточки SD объемом 512 МБ вообще подходит идеально. Из чего делаю вывод (даже не пересчитывая КС), что прочитал нужный регистр и правильно.

А еще я посчитал вручную CRC7 этого пакета данных и убедился в его совпадении. Но рассказ о пересчете КС заслуживает отдельного разговора.

Разбор CSD

Заменяю в команде чтения регистра «CID» на «CSD», перекомпилирую, перезапускаю питание, и в открывшемся на ПК окне устройства опять читаю файл DATA.BIN:

00000000: 00 5e 00 32 57 59 83 ba ed b7 7f 8f 96 40 00 45  .^.2WY.......@.E
00000010: 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................
...

И далее до 128 байтов. Буду разбирать, подобно CID, но это посложнее, потому что больше полей, и многие поля не кратны байту или полубайту. Проще бы сделать это программно, но вручную несколько нагляднее. Сделаю это (для примера), хотя в данном применении реально нужно всего несколько полей.

Name Field Width Value Cell
Type
CSD-slice Значение
CSD structure CSD_STRUCTURE 2 00b R [127:126] 0 ⇒ v 1.0
reserved 6 00 0000b R [125:120] 0
data read
access-time-1
TAAC 8 xxh R [119:112] 0x5E ⇒ 5.0 · 1ms
data read
access-time-2 in
CLK cycles (NSAC*100)
NSAC 8 xxh R [111:104] 0
max. data
transfer rate
TRAN_SPEED 8 32h or 5Ah R [103:96] 0x32 ⇒ 25 МГц
card command classes CCC 12 01x110110101b R [95:84] 010101110101
max. read data
block length
READ_BL_LEN 4 xh R [83:80] 9 ⇒ 512
partial blocks
for read allowed
READ_BL_PARTIAL 1 1b R [79:79] 1
write block
misalignment
WRITE_BLK_MISALIGN 1 xb R [78:78] 0
read block
misalignment
READ_BLK_MISALIGN 1 xb R [77:77] 0
DSR implemented DSR_IMP 1 xb R [76:76] 0
reserved 2 00b R [75:74] 0
device size C_SIZE 12 xxxh R [73:62] EEB ⇒ 477,5 MiB
max. read
current @VDD min
VDD_R_CURR_MIN 3 xxxb R [61:59] 3 ⇒ 10 мА
max. read
current @VDD max
VDD_R_CURR_MAX 3 xxxb R [58:56] 3 ⇒ 10 мА
max. write
current @VDD min
VDD_W_CURR_MIN 3 xxxb R [55:53] 3 ⇒ 25 мА
max. write
current @VDD max
VDD_W_CURR_MAX 3 xxxb R [52:50] 3 ⇒ 25 мА
device size
multiplier
C_SIZE_MULT 3 xxxb R [49:47] 2 ⇒ 256
erase single
block enable
ERASE_BLK_EN 1 xb R [46:46] 1
erase sector
size
SECTOR_SIZE 7 xxxxxxxb R [45:39] 0x7F ⇒ 256
write protect
group size
WP_GRP_SIZE 7 xxxxxxxb R [38:32] 0x0F ⇒ 16
write protect
group enable
WP_GRP_ENABLE 1 xb R [31:31] 1
reserved (Do not use) 2 00b R [30:29] 0
write speed factor R2W_FACTOR 3 xxxb R [28:26] 3 ⇒ x8
max. write data
block length
WRITE_BL_LEN 4 xxxxb R [25:22] 9 ⇒ 512
partial blocks
for write allowed
WRITE_BL_PARTIAL 1 xb R [21:21] 0
reserved 5 00000b R [20:16] 0
File format group FILE_FORMAT_GRP 1 xb R/W [15:15] 0
copy flag COPY 1 xb R/W [14:14] 0
permanent write
protection
PERM_WRITE_PROTECT 1 xb R/W [13:13] 0
temporary write
protection
TMP_WRITE_PROTECT 1 xb R/W [12:12] 0
File format FILE_FORMAT 2 xxb R/W [11:10] 0
reserved 2 00b R/W [9:8] 0
CRC CRC 7 xxxxxxxb R/W [7:1] 0x22
not used, always‘1’ 1 1b [0:0] 1

Разберу чуть подробнее:

Поле CSD_STRUCTURE содержит 0, следовательно, структура имеет версию 1.0 (что и ожидается от SD).

Поле TAAC содержит 0x5E, что в соответствии со спецификацией делится на две части: 6, соответствующую 1 мс, и 0xB, соответствующую 5,0. Итого получаем асинхронную часть времени доступа к данным. В режиме SPI обращать внимание на такие мелочи, право, не стоит. Это же относится к параметрам NSAC (равному 0) и TRAN_SPEED (равному 32, то есть, 2,5 · 10 Мбит/с или 25 МГц — норма для SD).

Поле CCC содержит 010101110101b, что несколько не соответствует заявленной в спецификации маске 01x110110101b, но это поле тоже можно игнорировать в данном случае. Здесь указано, какое подмножество команд флэшка способна воспринять. А в режиме SPI множество команд в принципе ограничено, да и нужно-то не очень много.

Поле READ_BL_LEN содержит 9 и означает размер читаемого блока данных 29 = 512. В спецификации есть примечание, что размер записываемого блока данных для карт SD всегда равен размеру читаемого блока.

Поле READ_BL_PARTIAL равно 1, что ожидаемо для карты SD и радует: можно читать блоки любого размера до 512, вплоть до 1 байта. Жаль, что WRITE_BLK_MISALIGN и READ_BLK_MISALIG равны 0, то есть, читать и писать можно только в пределах блока, через границу не перейти. Впрочем, это тоже было ожидаемо.

Поле DSR_IMP содержит 0, то есть, регистр DSR отсутствует, что, впрочем, для режима SPI тоже неважно. Бывает какой-то настраиваемый задающий каскад для выходных линий, позволяющий оптимизировать обмен. Но подробности о нем в упрощенной спецификации даже не стали печатать.

Поле C_SIZE содержит пользовательскую емкость карты, за вычетом защищенной области. Там записано 0xEEB, надо прибавить 1 и умножить на множитель из поля C_SIZE_MULT, получим размер в блоках, значит, надо еще домножить на размер блока, итого получим 500 695 040 байтов, или 488 960 килобайтов, или 477,5 метра.

Минимальные значения токов VDD_R_CURR_MIN и VDD_R_CURR_MAX равны 3, то есть, 10 мА. Максимальные значения токов VDD_W_CURR_MIN и VDD_W_CURR_MAX — тоже 3, но это уже 25 мА. Но важно ли это в данном случае?

Поле C_SIZE_MULT содержит 2, значит множитель равен 2C_SIZE_MULT + 2 = 256, это использовалось выше при вычислении пользовательской емкости флэшки.

Поле ERASE_BLK_EN содержит 1, что разрешает стирать данные блоками по 512 байтов. То есть, меньше 512 байтов стереть нельзя. Но если бы в этом поле был 0, то минимальный стираемый участок был бы (SECTOR_SIZE · 512) байтов.

Поле SECTOR_SIZE содержит число записываемых блоков (WRITE_BL_LEN = 512) в стираемом секторе. К записанному здесь 0x7F нужно еще прибавить 1, получим 128 блоков, или 65536 байтов, или 64 кбайта. Почему в этом поле такое значение, если ERASE_BLK_EN = 1? Не знаю.

Поле WP_GRP_SIZE содержит размер группы, защищаемой от записи, измеряемый в стираемых секторах. 0x0F + 1 = 16 блоков, по 64 кбайта получается метр.

Поле WP_GRP_ENABLE содержит 1 и разрешает групповую защиту от записи.

Поле R2W_FACTOR определяет типичное время программирования блока в виде множителя к времени чтения. 3 означает восьмикратное время.

Поле WRITE_BL_LEN аналогично полю READ_BL_LEN и содержит максимальную длину блока данных для записи, 512 байтов. Для SD-карт эти два поля должны быть равны. Они и равны, собственно.

Поле WRITE_BL_PARTIAL указывает, можно ли писать блоками меньшего размера, чем указано в предыдущем поле. Жаль, но значение равно 0. Не очень удобно.

Дальше пошли поля, предназначенные как для чтения, так и для записи, заполненные нулями. Причем, чтобы записать в эти поля, нужно сформировать данные для регистра CSD таким образом, чтобы в разрядах, соответствующих полям только для чтения находились разряды в том же состоянии, что и в регистре флэшки, иначе запись выполнена не будет. То есть, надо считать CSD, изменить нужные разряды и записать обратно.

  • FILE_FORMAT_GRP показывает выбранную группу форматов файлов. 0 означает один из четырех возможных файловых форматов, 1 зарезервирована.
  • COPY показывает, является ли содержимое карты оригиналом или копией.
  • PERM_WRITE_PROTECT — постоянная защита от записи всей карты от перезаписи или стирания (все команды записи и стирания навсегда отменяются). Программная защита от перезаписи.
  • TMP_WRITE_PROTECT — временная защита от записи всей карты от перезаписи или стирания (все команды записи и стирания временно отменяются). Программная защита от перезаписи.
  • FILE_FORMAT — файловая система как на жестком диске с таблицей разделов.

Поле CRC содержит контрольную сумму CRC7, которую при желании можно пересчитать и сверить. Записано 0x22. Поверю на слово. Вместе с последним разрядом регистра, который должен всегда быть установленным в 1, байт с контрольной суммой в итоге содержит 0x45.

Перед продолжением экспериментов с чтением-стиранием-записью стоит упомянуть, что на используемой и описываемой здесь SD-карточке используется адресация байтовая, в то время как на SDHC и SDXC — посекторная, блоками по 512 байтов. В моем случае надо указывать адрес в байтах, даже если операция не поддерживает побайтовой адресации. Например, стирание выполняется секторами, а указывать надо любой байт внутри стираемого сектора.

Чтение данных

Маленько

Прочитать регистры получилось. Это хорошо само по себе, плюс получена дополнительная информация о карте насчет поблочного чтения-стирания-записи. Теперь надо попробовать читать данные. Где-то там, в самом начале, должна быть MBR с таблицей разделов, а где-то подальше от начала — PBR. Закрываю вызов SdReadDataBlock(), а вместо него вставляю команду чтения данных с адреса 0, 128 байтов в буферный массив data (который можно будет прочитать в виде файла DATA.BIN):

    SdReadDataBlock(0, 128, data);

В результате читаю следующее:

00000000: fa 33 c0 8e d0 bc 00 7c 8b f4 50 07 50 1f fb fc  .3.....|..P.P...
00000010: bf 00 06 b9 00 01 f2 a5 ea 1d 06 00 00 be be 07  ................
00000020: b3 04 80 3c 80 74 0e 80 3c 00 75 1c 83 c6 10 fe  ...<.t..<.u.....
00000030: cb 75 ef cd 18 8b 14 8b 4c 02 8b ee 83 c6 10 fe  .u......L.......
00000040: cb 74 1a 80 3c 00 74 f4 be 8b 06 ac 3c 00 74 0b  .t..<.t.....<.t.
00000050: 56 bb 07 00 b4 0e cd 10 5e eb f0 eb fe bf 05 00  V.......^.......
00000060: bb 00 7c b8 01 02 57 cd 13 5f 73 0c 33 c0 cd 13  ..|...W.._s.3...
00000070: 4f 75 ed be a3 06 eb d3 be c2 06 bf fe 7d 81 3d  Ou...........}.=

Ну, допустим. Меняю начальный адрес с 0 на 2. При чтении вижу в массиве data результат инициализации, то есть, чтения не было. Чтобы выяснить, где произошел сбой, добавляю в функцию чтения данных вывод на светодиоды некоторого контрольного значения:

bool SdReadDataBlock(uint32_t address, uint32_t size, uint8_t * buffer)
{
    SdSendCommand(MMC_READ_SINGLE_BLOCK, address, 1, R1, sdResponce);
    if (sdResponce[0] == 0x00) {
        if (SdWaitForDataToken()) {
            for (uint8_t i = 0; i < size; i++) {
                buffer[i] = SdInByte();
            }
            data_device = 1;
            return true;
        } else {
            data_device = 2;
            return false;
        }
  } else {
            data_device = 3;
      return false;
  }
}

Дополнительно переключаю mode_out на 1, чтобы по умолчанию на светодиоды выводилась переменная data_device, закрываю строчку data_device = cnt_bt а при анализе mode_out в case 1 вместо PORTD = canDo; пишу PORTD = cnt_bt;.

Оказывается, что sdResponce не равен нулю, то есть, в команде ошибка. Ошибка, на самом деле, очевидная: размер блока по умолчанию равен 512 байтов, следовательно, при попытке чтения такого блока с адреса 2 будет переход через границу блока, а граница на замке, нельзя так делать.

Ввожу определение кода команды 16, установки размера блока:

    SdSendCommand(MMC_SET_BLOCK_LEN, size, 1, R1, sdResponce);

И перед командой чтения добавляю вызов команды, задающей размер блока в соответствии с размером буферного массива:

    SdSendCommand(MMC_SET_BLOCK_LEN, 128, 1, R1, sdResponce);

Теперь все читается. Вот первые 16 байтов массива:

00000000: c0 8e d0 bc 00 7c 8b f4 50 07 50 1f fb fc bf 00  .....|..P.P.....

Видно, что он начинается с третьего по счету байта, то есть, с адреса 2. Меняю адрес на 128, читаю еще блок:

00000000: 55 aa 75 c7 8b f5 ea 00 7c 00 00 49 6e 76 61 6c  U.u.....|..Inval
00000010: 69 64 20 70 61 72 74 69 74 69 6f 6e 20 74 61 62  id partition tab
00000020: 6c 65 00 45 72 72 6f 72 20 6c 6f 61 64 69 6e 67  le.Error loading
00000030: 20 6f 70 65 72 61 74 69 6e 67 20 73 79 73 74 65   operating syste
00000040: 6d 00 4d 69 73 73 69 6e 67 20 6f 70 65 72 61 74  m.Missing operat
00000050: 69 6e 67 20 73 79 73 74 65 6d 00 00 00 00 00 00  ing system......
00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

Здесь уже очевидно (по текстовым сообщениям), что имеем дело с MBR.

Как можно больше

Однако читать данные блоками по 128 байтов неинтересно и утомительно. Надо попробовать читать в виде файла такого размера, какой позволяет объем имитируемой «флэшки».

Для этого надо внести некоторые изменения. Прежде всего команду установки размера блока внесу первой строчкой в функцию SdReadDataBlock(). Пусть одним параметром задается и размер читаемого блока, и количество читаемых данных. Это не совсем оптимально, потому что при последовательном чтении множества блоков размер блока можно бы выставить один раз в начале. Но я не буду читать подряд огромное количество, а при таком подходе не надо следить за соответствием размера блока и размера данных. Правда, надо следить за соответствием адреса начала чтения размеру блока — чтобы не возникла ситуация перехода границы.

Затем следует добавить объявление функции чтения блока в файле common.h, чтобы ее можно было вызвать в Lib/fake_fs.c:

        extern bool SdReadDataBlock(uint32_t address, uint32_t size, uint8_t * buffer);

Дальше будут дополнения в модуле fake_fs. Определяю мегабайтный размер файла, читаемого с флэшки и объявляю функцию чтения этого файла:

#define SIZE_OF_SD      0x100000
uint8_t * readSD(uint8_t *data_buf, uint32_t size, uint32_t offset);

В конец файловой таблицы добавляю новый файл:

    {"SD      BIN", SIZE_OF_SD, readSD},

И где-нибудь в конце определяю функцию чтения:

uint8_t * readSD(uint8_t *data_buf, uint32_t size, uint32_t offset) {
    SdReadDataBlock(offset, 16, data_buf);
    return data_buf;
}

Делаю в простом варианте, не заморачиваясь с проверкой некратного размера читаемого куска или выхода за границы. С 16-байтовыми кусками, читаемыми по USB, такие казусы не должны возникнуть.

Очень несложные добавки приводят к возможности считывания файла SD.BIN, в котором мегабайт данных от начала SD-карты. Если в таблице файлов закрыть строчку, соответствующую файлу TESTFILE.TXT (он весит метр), то размер SD.BIN можно увеличить до трех метров, при желании даже с гаком. Имитируемая «флэшка» имеет размер 4 метра, поэтому больше не получится, операционная система занервничает, выдаст ошибки.

Компилирую, прошиваю, перезапускаю питание. На ПК открывается окно «флэшки», можно открыть вновь появившийся файл SD.BIN в HEX-редакторе. Весь приводить не буду, но:

00000000: fa 33 c0 8e d0 bc 00 7c 8b f4 50 07 50 1f fb fc  .3.....|..P.P...
00000010: bf 00 06 b9 00 01 f2 a5 ea 1d 06 00 00 be be 07  ................
00000020: b3 04 80 3c 80 74 0e 80 3c 00 75 1c 83 c6 10 fe  ...<.t..<.u.....
00000030: cb 75 ef cd 18 8b 14 8b 4c 02 8b ee 83 c6 10 fe  .u......L.......
00000040: cb 74 1a 80 3c 00 74 f4 be 8b 06 ac 3c 00 74 0b  .t..<.t.....<.t.
00000050: 56 bb 07 00 b4 0e cd 10 5e eb f0 eb fe bf 05 00  V.......^.......
00000060: bb 00 7c b8 01 02 57 cd 13 5f 73 0c 33 c0 cd 13  ..|...W.._s.3...
00000070: 4f 75 ed be a3 06 eb d3 be c2 06 bf fe 7d 81 3d  Ou...........}.=
00000080: 55 aa 75 c7 8b f5 ea 00 7c 00 00 49 6e 76 61 6c  U.u.....|..Inval
00000090: 69 64 20 70 61 72 74 69 74 69 6f 6e 20 74 61 62  id partition tab
000000a0: 6c 65 00 45 72 72 6f 72 20 6c 6f 61 64 69 6e 67  le.Error loading
000000b0: 20 6f 70 65 72 61 74 69 6e 67 20 73 79 73 74 65   operating syste
000000c0: 6d 00 4d 69 73 73 69 6e 67 20 6f 70 65 72 61 74  m.Missing operat
000000d0: 69 6e 67 20 73 79 73 74 65 6d 00 00 00 00 00 00  ing system......
000000e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000120: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000130: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000140: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000150: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000160: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000170: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000190: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000001a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000001b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03  ................
000001c0: 33 00 06 02 e2 ca ef 00 00 00 11 eb 0e 00 00 00  3...............
000001d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000001e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000001f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa  ..............U.

Первый сектор (512 байтов) заканчивается на 55 aa, как и должна заканчиваться MBR. Первые два куска по 128 байтов совпадают с тем, что было прочитано через буферный массив data. По смещению 0x1BE начинается запись первого раздела. По смещению 0x08 в записи раздела, то есть, по смещению 0x1C6 в MBR находится расстояние до первого сектора раздела в секторах. Видим там 0xEF, что означает адрес 0x1DE00. Посмотрим, что лежит по этому адресу, хотя бы первые 128 байтов:

0001de00: eb 3e 90 50 77 72 53 68 6f 74 20 00 02 20 01 00  .>.PwrShot .. ..
0001de10: 02 00 02 00 00 f8 78 00 3f 00 10 00 ef 00 00 00  ......x.?.......
0001de20: 11 eb 0e 00 80 00 29 35 62 24 00 43 41 4e 4f 4e  ......)5b$.CANON
0001de30: 5f 44 43 20 20 20 46 41 54 31 36 20 20 20 33 ff  _DC   FAT16   3.
0001de40: 8e df be 00 7c 8d 9c e4 01 8e 47 02 fc b9 00 02  ....|.....G.....
0001de50: f3 a4 c7 07 58 00 ff 2f 8c c8 fa 8e d0 bc 00 06  ....X../........
0001de60: fb 8b ec 83 ec 16 c5 36 78 00 89 76 f6 8c 5e f8  .......6x..v..^.
0001de70: 8d 7e ea b9 0b 00 57 f3 a4 5f 8e d9 be 78 00 89  .~....W.._...x..

Первые три байта: инструкция перехода 0xEB, смещение перехода 0x3E и инструкция NOP 0x90. Правда, смещение приходится в зарезервированную область, если рассматривать версию PBR, которую я использовал для имитации флэшки, но это не очень важно.

Далее 8 байтов OEM-названия: «PwrShot», что просто замечательно, так как флэшка использовалась в фотоаппарате Canon PowerShot, в котором и форматировалась.

Дальше 2 байта содержат количество байтов в секторе, их 512. Ну и так далее. В конце сектора последние два байта 55 aa, а дальше начинается FAT:

0001dff0: 00 49 42 4d 42 49 4f 20 20 43 4f 4d 00 80 55 aa  .IBMBIO  COM..U.
0001e000: f8 ff ff ff ff ff ff ff 05 00 06 00 07 00 08 00  ................
0001e010: 09 00 0a 00 0b 00 0c 00 0d 00 0e 00 0f 00 10 00  ................
0001e020: 11 00 12 00 13 00 14 00 15 00 16 00 17 00 18 00  ................
0001e030: 19 00 1a 00 1b 00 1c 00 1d 00 1e 00 1f 00 20 00  .............. .
0001e040: 21 00 22 00 23 00 24 00 25 00 26 00 27 00 28 00  !.".#.$.%.&.'.(.
0001e050: 29 00 2a 00 2b 00 2c 00 2d 00 2e 00 2f 00 30 00  ).*.+.,.-.../.0.
0001e060: 31 00 32 00 33 00 34 00 35 00 36 00 37 00 38 00  1.2.3.4.5.6.7.8.
0001e070: 39 00 3a 00 3b 00 3c 00 3d 00 3e 00 3f 00 40 00  9.:.;.<.=.>.?.@.
0001e080: 41 00 42 00 43 00 44 00 45 00 46 00 47 00 48 00  A.B.C.D.E.F.G.H.
0001e090: 49 00 4a 00 4b 00 4c 00 4d 00 4e 00 4f 00 50 00  I.J.K.L.M.N.O.P.
0001e0a0: 51 00 52 00 53 00 54 00 55 00 56 00 57 00 58 00  Q.R.S.T.U.V.W.X.
0001e0b0: 59 00 5a 00 5b 00 5c 00 5d 00 5e 00 5f 00 60 00  Y.Z.[.\.].^._.`.
0001e0c0: 61 00 62 00 63 00 64 00 65 00 66 00 67 00 68 00  a.b.c.d.e.f.g.h.
0001e0d0: 69 00 6a 00 6b 00 6c 00 6d 00 6e 00 6f 00 70 00  i.j.k.l.m.n.o.p.
0001e0e0: 71 00 72 00 73 00 74 00 75 00 76 00 77 00 78 00  q.r.s.t.u.v.w.x.
0001e0f0: 79 00 7a 00 7b 00 7c 00 7d 00 7e 00 7f 00 80 00  y.z.{.|.}.~.....
0001e100: 81 00 82 00 83 00 84 00 85 00 86 00 87 00 88 00  ................
0001e110: 89 00 8a 00 8b 00 8c 00 8d 00 8e 00 8f 00 90 00  ................
0001e120: 91 00 92 00 93 00 ff ff 95 00 96 00 97 00 98 00  ................

Видно, что сразу начинается описание длинного нефрагментированного файла, кластеры которого следуют подряд. В кластере 147 (0x93) находится конец этого файла, обозначенный в таблице с помощью ff ff, дальше начинается следующий файл.

По смещению 0x2d000 лежит резервная FAT:

0002d000: f8 ff ff ff ff ff ff ff 05 00 06 00 07 00 08 00  ................
0002d010: 09 00 0a 00 0b 00 0c 00 0d 00 0e 00 0f 00 10 00  ................
...

Ниже что-то очень похожее на корневой каталог:

00040000: 2e 20 20 20 20 20 20 20 20 20 20 10 f4 00 d5 93  .          .....
00040010: f5 48 f5 48 00 00 d5 93 f5 48 02 00 00 00 00 00  .H.H.....H......
00040020: 2e 2e 20 20 20 20 20 20 20 20 20 10 f4 00 d5 93  ..         .....
00040030: f5 48 f5 48 00 00 d5 93 f5 48 00 00 00 00 00 00  .H.H.....H......
00040040: 31 30 31 43 41 4e 4f 4e 20 20 20 10 00 00 d5 93  101CANON   .....
00040050: f5 48 02 49 00 00 d5 93 f5 48 03 00 00 00 00 00  .H.I.....H......
00040060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00040070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

Еще ниже что-то вроде каталога со списком файлов:

00044000: 2e 20 20 20 20 20 20 20 20 20 20 10 0c 00 d5 93  .          .....
00044010: f5 48 f5 48 00 00 d5 93 f5 48 03 00 00 00 00 00  .H.H.....H......
00044020: 2e 2e 20 20 20 20 20 20 20 20 20 10 0c 00 d5 93  ..         .....
00044030: f5 48 f5 48 00 00 d5 93 f5 48 02 00 00 00 00 00  .H.H.....H......
00044040: 49 4d 47 5f 32 30 37 38 4a 50 47 20 00 00 d5 93  IMG_2078JPG ....
00044050: f5 48 02 49 00 00 d5 93 f5 48 04 00 e0 e2 23 00  .H.I.....H....#.
00044060: 49 4d 47 5f 32 30 37 39 4a 50 47 20 00 00 dd 93  IMG_2079JPG ....
00044070: f5 48 02 49 00 00 dd 93 f5 48 94 00 18 56 28 00  .H.I.....H...V(.

И еще ниже что-то, сильно смахивающее на фотографию:

00048000: ff d8 ff e1 25 fe 45 78 69 66 00 00 49 49 2a 00  ....%.Exif..II*.
00048010: 08 00 00 00 09 00 0f 01 02 00 06 00 00 00 7a 00  ..............z.
00048020: 00 00 10 01 02 00 15 00 00 00 80 00 00 00 12 01  ................
00048030: 03 00 01 00 00 00 01 00 00 00 1a 01 05 00 01 00  ................
00048040: 00 00 a0 00 00 00 1b 01 05 00 01 00 00 00 a8 00  ................
00048050: 00 00 28 01 03 00 01 00 00 00 02 00 00 00 32 01  ..(...........2.
00048060: 02 00 14 00 00 00 b0 00 00 00 13 02 03 00 01 00  ................
00048070: 00 00 01 00 00 00 69 87 04 00 01 00 00 00 c4 00  ......i.........
00048080: 00 00 56 0b 00 00 43 61 6e 6f 6e 00 43 61 6e 6f  ..V...Canon.Cano
00048090: 6e 20 50 6f 77 65 72 53 68 6f 74 20 41 35 33 30  n PowerShot A530
000480a0: 00 00 00 00 00 00 00 00 00 00 00 00 b4 00 00 00  ................
000480b0: 01 00 00 00 b4 00 00 00 01 00 00 00 32 30 31 36  ............2016
000480c0: 3a 30 37 3a 32 31 20 31 38 3a 33 30 3a 34 33 00  :07:21 18:30:43.
000480d0: 1f 00 9a 82 05 00 01 00 00 00 3e 02 00 00 9d 82  ..........>.....
000480e0: 05 00 01 00 00 00 46 02 00 00 00 90 07 00 04 00  ......F.........
000480f0: 00 00 30 32 32 30 03 90 02 00 14 00 00 00 4e 02  ..0220........N.
...
00048470: 49 4d 47 3a 50 6f 77 65 72 53 68 6f 74 20 41 35  IMG:PowerShot A5
00048480: 33 30 20 4a 50 45 47 00 00 00 00 00 00 00 00 00  30 JPEG.........
00048490: 46 69 72 6d 77 61 72 65 20 56 65 72 73 69 6f 6e  Firmware Version
000484a0: 20 31 2e 30 30 00 00 00 00 00 00 00 00 00 00 00   1.00...........
000484b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000484c0: 00 00 00 00 00 00 00 00 01 00 00 00 02 00 00 00  ................
000484d0: 00 00 00 00 78 00 00 00 e7 ff ff ff e7 ff ff ff  ....x...........
000484e0: e9 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00  ................
...

В общем, чтение флэшки вышло успешным и убедительным.

Стирание

С флэшкой можно провернуть такую операцию, как стирание. Честно говоря, операция не очень-то полезная. Во-первых, при каждой операции записи контроллер флэшки все равно предварительно сотрет записываемый сектор. Во-вторых, в подобных флэшках после стирания ячейки должны содержать 0xFF, а не нули, то есть, происходит не только стирание, но и запись нулей. Но я попробую. Потому что хочется.

Для стирания потребуются команды 32, 33 и 38: указания адреса начала области стирания, адреса окончания области и собственно стирания. Добавляю их определения туда же, где остальные определения команд:

#define MMC_ERASE_WR_BLK_START  MMC_COMMANDS_BASE + 32        // CMD32
#define MMC_ERASE_WR_BLK_END    MMC_COMMANDS_BASE + 33        // CMD33
#define MMC_ERASE               MMC_COMMANDS_BASE + 38        // CMD38

Затем в файле MassStorage.c закрываю вызов команды чтения данных в буференый массив SdReadDataBlock(0, 128, data);, она уже не нужна, а вместо нее ставлю три команды подряд:

    SdSendCommand(MMC_ERASE_WR_BLK_START, 0x0, 1, R1, sdResponce);
    SdSendCommand(MMC_ERASE_WR_BLK_END, 0x800, 1, R1, sdResponce);
    SdSendCommand(MMC_ERASE, 0, 1, R1, sdResponce);

В результате выполнения этой последовательности окажутся заполнены нулями сектора начиная с того, в котортом есть адрес 0, заканчивая тем, в котором есть адрес 0x800. С тем же успехом можно было задать адреса, скажем, 9 и 0x803. Причем, эти команды должны идти строго втроем, именно в этой последовательности. Нельзя однажды задать адрес начала стирания, а потом его подразумевать: если после него не будет остальных команд, он сбросится.

В общем, ничего примечательного. Данные действительно стерлись, действительно в указанной области. Эксперименты с разными адресами тоже не показали ничего такого, о чем стоило бы упомянуть.

Проба записи

Закрываю команды стирания. Попробовал — и хватит. Далее в имеющуюся заготовку функции записи добавлю data_device = 1; перед return true и data_device = 2 перед return false, опять же для контроля успешности выполнения.

Записывать планирую для начала из буферного массива data, поэтому изменяю его инициализатор на счетчик, начинающийся с пятерки, для наглядности:

    for(int i = 0; i < 128; i++) {
        data[i] = (i + 5);
    }

И после закрытых команд стирания вызываю запись:

    SdWriteDataBlock(0x0, 128, data);

В результате можно с имитируемой «флэшки» прочитать файл DATA.BIN следующего содержания:

00000000: 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14  ................
00000010: 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24  ........... !"#$
00000020: 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34  %&'()*+,-./01234
00000030: 35 36 37 38 39 3a 3b 3c 3d 3e 3f 40 41 42 43 44  56789:;<=>?@ABCD
00000040: 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54  EFGHIJKLMNOPQRST
00000050: 55 56 57 58 59 5a 5b 5c 5d 5e 5f 60 61 62 63 64  UVWXYZ[\]^_`abcd
00000060: 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74  efghijklmnopqrst
00000070: 75 76 77 78 79 7a 7b 7c 7d 7e 7f 80 81 82 83 84  uvwxyz{|}~......

Дальнейшие эксперименты с заданием различных адресов показали забавный результат. Если адрес начала записи указать не 0, а, к примеру, 3, то операция записи выполнена не будет. Похоже, по той же причине, которая вызвала сбой операции чтения — переход границы блока. Если же предварительно задать длину блока, как при операции чтения, то запись выполняется, но не с указанного адреса, а все равно с начала блока, внутри которого находится указанный адрес.

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

Запись данных с ПК

Маленько и просто

Попробую организовать запись на SD-флэшку через имитируемую «флэшку», для начала хотя бы маленький кусочек. Сперва перенесу вызов команды установки размера блока в функцию записи, так же, как это было сделано с функцией чтения. Ну, и закрою функцию записи в MassStorage.

Определю функцию записи в файле common.h:

        extern bool SdWriteDataBlock(uint32_t address, uint32_t size, uint8_t * buffer);

А в файле fake_fs.c надо написать совсем немного. Там уже организована возможность записи «в файл». Необходимо предварительно открыть для чтения файл TO_FILE.TXT (то есть, прочитать его любым способом), после чего любые данные, отправляемые устройству на запись, будут направляться в буферный массив, пока помещаются.

Я всего лишь добавлю запись накопленных в массиве data данных на флэшку в тот момент, когда буфер наполнен. Для этого в функции process_data() при анализе writeType в case ToFile внутри фигурных скобок перед break надо добавить

        if (ind == 0) {
            ind++;
            SdWriteDataBlock(0x0, 128, data);
        }

Теперь после перекомпиляции и перезапуска питания открываю «блокнотом» файл TO_FILE.TXT, после чего забрасываю мышью в окно «флэшки» предварительно подготовленный для этой цели текстовый файл с классической «рыбой». Для чистоты эксперимента на время выключаю питание устройства, потом подключаю снова, читаю SD.BIN и в первых 128 байтах вижу следующее:

00000000: 4c 6f 72 65 6d 20 69 70 73 75 6d 20 64 6f 6c 6f  Lorem ipsum dolo
00000010: 72 20 73 69 74 20 61 6d 65 74 2c 20 63 6f 6e 73  r sit amet, cons
00000020: 65 63 74 65 74 75 72 20 61 64 69 70 69 73 63 69  ectetur adipisci
00000030: 6e 67 20 65 6c 69 74 2c 20 73 65 64 20 64 6f 20  ng elit, sed do 
00000040: 65 69 75 73 6d 6f 64 20 74 65 6d 70 6f 72 20 69  eiusmod tempor i
00000050: 6e 63 69 64 69 64 75 6e 74 20 75 74 20 6c 61 62  ncididunt ut lab
00000060: 6f 72 65 20 65 74 20 64 6f 6c 6f 72 65 20 6d 61  ore et dolore ma
00000070: 67 6e 61 20 61 6c 69 71 75 61 2e 20 55 74 20 65  gna aliqua. Ut e
00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

Запись получилась.

Побольше и посложнее

К сожалению, для такого способа записи необходим буферный массив с размером не меньше, чем у сектора, то есть, 512 байтов. А у AT90USB162 всего оперативки 512 байтов. Надо придумывать нечто иное, без буфера.

Сперва надо, как в функции записи, выдать команду отправки блока данных, получив ответ, выдать токен данных, а потом начать пересылать данные, поступающие по USB. Примерно так, как это было сделано для отправки данных из файла ПК на экран. После того, как будет выдано максимально возможное число байтов — 512 — надо выдать некоторый заменитель контрольной суммы, прочитать отклик на данные от карты (должен быть ненулевым), и дождаться окончания состояния «busy», пока контроллер SD-карты записывает полученный блок в свою флэш-память. При желании записать более одного сектора следует все вышеописанное повторить.

В общем, многое из используемого в функции записи нужно сделать видимым для модуля fake_fs. Поэтому переношу в файл common.h определения команд и токена из MassStorage.c, а также объявление типа для вариантов отклика, кроме того объявляю там же переменную и три функции:

enum ResponceType {R1, R2, R3, R7};

...

        extern uint8_t sdResponce[5];
        extern void SdOutByte(uint8_t data8);
        extern uint8_t SdInByte(void);
        extern void SdSendCommand(uint8_t Index, uint32_t Argument, uint8_t Crc, enum ResponceType responceType, uint8_t *Responce);

В файле fake_fs.c вношу изменения в функцию prepare_data(): добавляю переменных и полностью переписываю case ToFile:

void process_data(uint8_t * data_buf, uint32_t BlockAddress, uint8_t BytesInBlockDiv16){
    static uint8_t ind = 0;
    static uint8_t canToFile = 1;
    static uint8_t cnt = 0;

    ...

    case ToFile:
        if ((BlockAddress >= FILES_AREA) &&(canToFile == 1)) {
            if (ind == 0) {
                SdSendCommand(MMC_SET_BLOCK_LEN, 512, 0x1, R1, sdResponce);
                SdSendCommand(MMC_WRITE_SINGLE_BLOCK, (512 * cnt) + 0, 0x1, R1, sdResponce);
                if (sdResponce[0] == 0x00) {
                    canToFile = 1;
                    SdOutByte(0xFF);
                    SdOutByte(0xFF);
                    SdOutByte(MMC_START_TOKEN_SINGLE);
                } else {
                    canToFile = 1;
                }
            }
            if ((canToFile == 1) && (ind >= 0) && (ind < 32)) {
                for (uint8_t i = 0; i < 16; i++) {
                    SdOutByte(data_buf[i]);
                }
                ind++;
            }
            if ((canToFile == 1) && (ind == 32 )) {
                ind = 0;
                cnt++;
                if (cnt > 11) {
                    canToFile = 0;
                }

                SdOutByte(0xFF);    /* intstead CRC */
                SdOutByte(0xFF);
                uint8_t tmp = SdInByte();           /* Data Responce */
                while(SdInByte() == 0x00);
            }
        }
        break;

Здесь переменная ind содержит индекс 16-байтового куска из тех, которыми ведется обмен по USB через конечную точку. Нужна для того, чтобы закончить работу с записью текущего сектора и при необходимости начать новый.

Переменная cnt — счетчик 512-байтовых блоков. Желаемый записываемый объем в упомянутых блоках задается в последней конструкции if (cnt > x), где x — предел счета блоков.

Переменная canToFile запрещает запись на флэшку, когда заданный объем заполнен, а данные все еще идут.

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

Вот, собственно, и все. Перекомпилированная программа зашита в устройство. После перезапуска питания прочитан файл TO_FILE.TXT, после чего в окно «флэшки» заброшен на запись файл с текстовой «рыбой». Теперь на настоящей, не имитируемой флэшке с нулевого сектора хранится текст, около 6 килобайтов всякой ерунды. А также проверена возможность записи объема данных, ограниченного только объемом памяти во флэшке.

Запись с ПК на флэш хотя и работает, но не очень-то удобна, и не допускает нормального разделения на файлы. Но такая задача и не ставилась. Для нормальной записи на флэшку можно использовать функции, встроенные в библиотеку LUFA и перекладывающие часть работы на ОС и контроллер SD-карты.

Пример использования

Допустим, есть желание выводить на ЖК-экран некоторую картинку, изменяющуюся в результате внешнего воздействия, скажем, по нажатию кнопки. Места для кода картинок нет. Точнее, оно сильно ограничено, поэтому можно хранить в памяти программ очень мало очень маленьких картинок. Зато их можно хранить на флэшке, на подключенной SD-карте.

Для начала следует сформировать блок данных, который будет записан на SD-карту. В нем нужно закодировать выводимые изображения. Картинки должны составлять последовательность одинаковых пакетов данных, желательно кратных степени двойки по размеру для удобства адресации.

Делаю двадцать похожих картинок 19х19 точек в виде кружочка, внутри которого две цифры (от «00» до «19») и маленькая «стрелочка» (две точки вне круга и одна внутри). Никаких рисовально-графических программ под рукой нет, зато есть блокнот в клеточку, на котором рисую черновик:

dial_sketch

Глядя на эскиз, собираю двоичный файл по аналогии с файлом для выдачи на экран из опуса 17:

00000000: 01 42 01 a6 02 00 00 00 00 80 00 60 00 10 00 88  .B.........`....
00000010: 00 48 00 44 00 84 00 0f 00 84 00 44 00 48 00 88  .H.D.......D.H..
00000020: 00 10 00 60 00 80 00 00 00 00 01 43 01 a6 02 00  ...`.......C....
00000030: 00 00 00 0f 00 30 00 40 00 8f 00 92 00 11 00 0f  .....0.@........
00000040: 00 00 00 0f 00 12 00 91 00 8f 00 40 00 30 00 0f  ...........@.0..
00000050: 00 00 00 00 01 44 01 a6 02 00 00 00 00 00 00 00  .....D..........
00000060: 00 00 00 00 00 00 00 01 00 01 00 01 00 01 00 01  ................
00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00  ................

Здесь я не стал делать предварительное стирание картинки, так как его достаточно сделать один раз, а потом все картинки будут рисоваться на одном и том же месте, затирая предыдущие.

19 точек в высоту и 19 в ширину — это три байтовые линии по 19 байтов, в третьей линии используются только 3 младших разряда. Две пары байтов используются для установки начальной позиции каждой линии, итого — 12 байтов.

Позиция по горизонтали вычисляется так: (96 / 2) - (19 / 2) = 38,5 = 0x26 (округляя до меньшего). Добавив разряд команды установки адреса по X (0x80), получим 0xA6.

По вертикали начну с третьей линейки, то есть, адрес 2. Плюс разряд команды установки адреса по Y (0x40), получаем 0x42.

Для второй и третьей линеек картинки адрес по X будет тот же, а адреса по Y — 0x43 и 0x44 соответственно.

19 пар байтов для данных для каждой линии, итого (с установкой адреса) — 114 байтов. Всего на картинку из трех линий получается 126 байтов. Пара байтов осталось. Добавляю еще страховочную команду «стоп» в виде тройки в первом байте пары при произвольном втором байте. При известной длине «файла» картинки команда остановки вообще-то не нужна, достаточно ограничить цикл чтения заданным числом. Но место есть, поэтому вставлю.

Проверяю эту картинку по той же технологии, что и в опусе 17: подключаю устройство, в окне «флэшки» читаю файл TO_SPI.TXT, затем записываю на «флэшку» полученный двоичный файл. Вижу — нарисовано именно то, что задумано.

dial_00

Картинка занимает как раз кратное степени двойки (27) число байтов. Собираю файл из двадцати таких картинок. Но вручную лениво, поэтому набросал за вечер с перерывами программку на С++, которая заполняет массив одинаковыми кругами, цифрами в зависимости от номера картинки и «стрелочками», которые рисовались почти вручную, по точкам.

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

Пора вносить изменения в программу контроллера, в файл MassStorage.c.

Для наглядности возвращаю значение режима вывода на светодиоды mode_out в нулевое и добавляю переменную, хранящую его предыдущее значение:

    unsigned char mode_out = 0;   // режим вывода
    unsigned char mode_out_old = 0;   // режим вывода

Добавляю еще несколько переменных:

    int8_t cnt20 = 0;  // счетчик для циферблата
    int8_t cnt20old = -1;
    uint8_t canDoOld = 0;  // для определения момента включения

Там, где изменяется счетчик нажатия кнопок, добавляю изменение вновь введенного счетчика по модулю 20. Вместо

                    if ((bt_now & BT_1) == 0) {cnt_bt++;}
                    if ((bt_now & BT_2) == 0) {cnt_bt--;}

пишу

                    if ((bt_now & BT_1) == 0) {
                        cnt_bt++;
                        cnt20++;
                        if (cnt20 >= 20) {cnt20 = 0;}
                    }
                    if ((bt_now & BT_2) == 0) {
                        cnt_bt--;
                        cnt20--;
                        if (cnt20 < 0) {cnt20 = 19;}
                    }

В блоке switch (mode_out) меняю в case 2: cnt_bt на cnt20. Далее, после выполнения всех операций внутри условия if (bt_now != bt_old) вставляю такой блок кода:

            if (canDo == 1) {
                if (canDoOld == 0) {
                    canDoOld = 1;
                    mode_out_old = mode_out;
                    mode_out = 2;
                    scrClear();
                }
                if (cnt20 != cnt20old) {
                    cnt20old = cnt20;
                    uint32_t addr = cnt20 << 7;
                    uint32_t size = 2;
                    uint8_t forScreen[2];
                    bool canOut = false;
                    for (uint8_t i = 0; i < (128 / 2); i++) {
                        SdReadDataBlock(addr++, size, forScreen);
                        addr++;
                        switch (forScreen[0]) {
                            case 1:
                                PORTB &= ~BIT_DC;       // d/c -> 0
                                canOut = true;
                                break;
                            case 2:
                                PORTB |= BIT_DC;        // d/c -> 1
                                canOut = true;
                                break;
                            case 3:
                                canOut = false;
                                break;
                            case 4:
                                scrClear();
                                canOut = false;
                                break;
                            default:
                                ;
                        }
                        if (forScreen[0] == 3) {break;}
                        if (canOut) {out8bit(forScreen[1]);}
                    }
                }
            } else {
                canDoOld = 0;
                mode_out = mode_out_old;
            }

Здесь используется введенный в опусе 15 флаг canDo. Он переключается нижними кнопками, четвертой и пятой. Сперва проверяется начало включения режима работы с экраном при помощи вспомогательной переменной canDoOld, при его включении режим отображения на светодиодную линейку (таймер-счетчик) переключается на счетчик по модулю 20 — для наглядности. При выключении режима работы с экраном режим отображения на диодах возвращается в запомненное состояние.

В режиме canDo == 1 проверяется изменение счетчика cnt20. Если счетчик изменился, то вычисляется адрес начала картинки, соответствующей значению счетчика. Вычисляется очень просто — сдвигом на 7 влево, раз уж картинка занимает 27 байтов.

Далее читаются подряд пары байтов из кода картинки, расшифровывается значение первого — командного — байта пары, выполняются соответствующие команде действия и при необходимости передается второй байт пары.

Так как картинки относительно небольшие, можно было бы считывать их целиком в буферный массив data[128], но если считывать именно так, парами байтов, можно использовать картинки, размером в весь экран.

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

dial_300

Итог

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

Показано применение одновременно сосуществующих на шине SPI разных устройств. Причем, одно из них отображает данные из другого, хоть и не напрямую.

Надо сказать, что на подобных ограниченных по памяти устройствах оправдано использование именно SD-карт, а не SDHC или SDXC. Старые карты проще в использовании и способны читаться побайтово, а не посекторно.

И да, многое в проекте сделано неоптимально, но цель проекта — демонстрация и отладка. В реальном проекте полезного прибора оптимизацию нужно выполнять по ситуации, выбирая, что важнее: удобство, простота и скорость написания кода, быстродействие устройства или объем памяти, занимаемый программой и данными. Чем-то придется пожертвовать, а чаще всего — двумя из трех.


Теги: