grafalex 5 сентября 2017 в 18:01
Недавно , или по русски - как флешку. Вроде бы как все относительно несложно: в графическом конфигураторе STM32CubeMX в пару кликов сгенерировал код, добавил драйвер SD карты, и вуаля - все работает. Только очень медленно - 200кбайт/с при том, что пропускная способность шины USB в режиме Full Speed гораздо выше – 12 мБит/с (грубо 1.2 Мбайт/с). Более того, время старта моей флешки в операционной системе составляет около 50 секунд, что попросту некомфортно в работе. Раз уж я нырнул в эту область, то почему бы и не зачинить скорость передачи.
Вообще-то я уже писал свой драйвер для SD карты (точнее драйвер SPI), который работал через DMA и обеспечивал скорость до 500кб/с. К сожалению в контексте USB этот драйвер не заработал. Причиной всему сама модель общения USB - там все делается на прерываниях, тогда как мой драйвер был заточен под работу в обычном потоке. Да еще и припудрен примитивами синхронизации FreeRTOS.
В этой статье я сделал парочку финтов, которые позволили выжать максимум из связки USB и SD карточки подключенной к микроконтроллеру STM32F103 по SPI. Также тут будет про FreeRTOS, объекты синхронизации и общие подходы к передаче данных через DMA. Так что, думаю, статья будет полезна и тем кто только разбирается в контроллерах STM32, и инструментах вроде DMA, и подходах при работе с FreeRTOS. Код построен на основе библиотек HAL и USB Middleware из пакета STM32Cube , а также SdFat для работы с SD картой.
С одной стороны находится библиотека USB Core. Она занимается общением с хостом, обеспечивается регистрацию устройства и реализует всякие низкоуровневые штуки USB.
Драйвер Mass Storage (с помощью ядра USB) может принимать и отправлять хосту данные. Примерно как COM порт, только данные передаются блоками. Тут важно смысловое наполнение этих данных: передаются SCSI команды и данные к ним. Причем команд бегает всего несколько видов: прочитать данные, записать данные, узнать размер запоминающего устройства, узнать готовность устройства.
Задача драйвера MSC интерпретировать SCSI команды и перенаправлять вызовы в драйвер запоминающего устройства. Это может быть любое запоминающее устройство с блочным доступом (RAM диск, флешка, сетевое хранилище, компакт диск и др.). В моем случае запоминающее устройство это карточка MicroSD, подключенная через SPI. Набор функций, которые требуются от драйвера примерно такой же: читать, писать, отдавать размер и состояние готовности.
И вот тут появляется один важный нюанс, из-за которого собственно весь сыр-бор. Дело в том, что протокол USB - хост ориентированный. Только хост может стартовать транзакции, отправлять или забирать данные. С точки зрения микроконтроллера это означает что вся активность связанная с USB будет проходить в контексте прерывания. При этом у драйвера MSC будет вызван соответствующий обработчик.
Что касается отправки данных от микроконтроллера в сторону хоста. Микроконтроллер не может самостоятельно инициировать передачу данных. Максимум что может микроконтроллер это сигнализировать ядру USB, что есть данные, которые хост может забрать.
С самой SD картой тоже не все так просто. Дело в том, что карта является сложным устройством (по всей видимости там свой микроконтроллер стоит), а протокол общения весьма нетривиальный. Т.е. это не просто отправил/принял данные по определенному адресу (как в случае с каким нибудь I2C EEPROM модулем). Протокол общения с картой предусматривает целый набор различных команд и подтверждений, проверок контрольных сумм и соблюдений всяких таймаутов.
Я использую библиотеку SdFat . Она реализует работу с SD картой на уровне файловой системы FAT, что я активно использую в своем устройстве. В случае подключения по USB все что связано с файловой системой отключается (эта роль переходит хосту). Но что важно, библиотека отдельно выделяет драйвер карты с интерфейсом, практически таким как хочет того драйвер MSC - прочитать, записать, узнать размер.
Драйвер карты реализует протокол общения с картой через SPI. Он знает какие именно команды слать карте, в какой последовательности и какие ждать ответы. Но сам драйвер не занимается общением с железом. Для этого предусмотрен еще один уровень абстракции - драйвер SPI, который транслирует запросы чтения/записи отдельных блоков в собственно передачу данных по шине SPI. Вот именно в этом месте мне удалось организовать пересылку данных через DMA, что увеличило скорость передачи данных в обычном режиме, но поломало всю малину в случае USB (DMA в итоге пришлось отключить)
Но обо всем по порядку.
Со всей этой кухней есть 2 проблемы:
Самый простой способ сократить количество операций чтения при подключении устройства – уменьшить таблицу FAT. Достаточно просто переформатировать флешку и увеличить размер кластера (тем самым уменьшить их количество и размер таблицы). Я отформатировал карту установив размер кластера в 16кб – размер таблицы FAT стал чуть менее 2 Мб, а время инициализации сократилось до 20 секунд.
Больше не всегда лучше
Я осознал, что для моего устройства флешка на 8 гиг это слишком много, мне столько не нужно. Вполне хватит 1 гига, или даже 512 мегабайт. Просто под рукой такой флешки пока нет. Более того, их даже сейчас и в продаже нет. Придется скрести по сусекам. Как найду – попробую.
В своем проекте я использую FreeRTOS. Это просто офигенный инструмент, который позволил мне каждую функцию моего устройства обрабатывать в отдельном потоке (задаче). Мне удалось выкинуть огромные машины состояний на все случаи жизни, а код стал существенно проще и понятнее. Все задачи работают одновременно, уступая друг дружке и синхронизируясь если нужно. Ну а если все потоки уснули в ожидании некоторого события, то можно использовать режимы энергосбережения микроконтроллера.
Код, который работает с SD картой так же работает в отдельном потоке. Это позволило написать функции чтения/записи весьма элегантно.
драйвер SPI для чтения/записи данных на SD карту с помощью DMA
uint8_t SdFatSPIDriver::receive(uint8_t* buf, size_t n)
{
// Start data transfer
memset(buf, 0xff, n);
HAL_SPI_TransmitReceive_DMA(&spiHandle, buf, buf, n);
// Wait until transfer is completed
xSemaphoreTake(xSema, 100);
return 0; // Ok status
}
void SdFatSPIDriver::send(const uint8_t* buf, size_t n)
{
// Start data transfer
HAL_SPI_Transmit_DMA(&spiHandle, (uint8_t*)buf, n);
// Wait until transfer is completed
xSemaphoreTake(xSema, 100);
}
void SdFatSPIDriver::dmaTransferCompletedCB()
{
// Resume SD thread
xSemaphoreGiveFromISR(xSema, NULL);
}
Проблема в том, что такой подход сложно натянуть на модель USB где вся логика работы происходит в прерываниях, а не в обычном потоке выполнения. Т.е. получается, что запрос на чтение/запись мы получим в прерывании, и завершение передачи данных также придется ждать в этом же прерывании.
Пересылку через DMA в контексте прерывания мы, конечно, организовать сможем, но толку от этого будет мало. DMA хорошо работает там где можно запустить передачу и переключить процессор на какую нибудь другую полезную работу, пока передача данных не закончится. Но запустив передачу из прерывания мы не сможем прервать прерывание (извините за тавтологию) и пойти по своим делам. Придется там и висеть в ожидании окончания передачи. Т.е. операция получится синхронной и суммарное время окажется таким же как и в случае без DMA.
Тут гораздо интереснее было бы по запросу от хоста начать передачу данных по DMA и выйти из прерывания. А потом как нибудь на следующем прерывании отчитаться о проделанной работе.
Но это еще не вся картина. Если бы чтение с карты заключалось только в пересылке блока данных, то такой подход было бы не сложно реализовать. Но ведь передача по SPI это, безусловно, самая важная часть, но не единственная. Если посмотреть на чтение/запись блока данных на уровне драйвера карты, то процесс выглядит примерно так.
Но не все так плохо. Если посмотреть еще выше - на уровень драйвера MSC - то ему вообще по барабану как именно будет происходить передача данных - одним блоком или несколькими, с DMA или без. Главное передать данные и отрапортовать о статусе.
Идеальным местом для экспериментов будет прослойка между драйвером MSC и драйвером карты. Перед всеми издевательствами этот компонент выглядел весьма тривиально - по сути это адаптер между интерфейсом, который хочет видеть драйвер MSC и тем что выдает драйвер карты.
Оригинальная реализация адаптера
int8_t SD_MSC_Read (uint8_t lun,
uint8_t *buf,
uint32_t blk_addr,
uint16_t blk_len)
{
(void)lun; // Not used
if(!card.readBlocks(blk_addr, buf, blk_len))
return USBD_FAIL;
return (USBD_OK);
}
int8_t SD_MSC_Write (uint8_t lun,
uint8_t *buf,
uint32_t blk_addr,
uint16_t blk_len)
{
(void)lun; // Not used
if(!card.writeBlocks(blk_addr, buf, blk_len))
return USBD_FAIL;
return (USBD_OK);
}
Этот поток будет получать запросы на чтение и запись через очередь. Каждый запрос включает информацию о типе операции (чтение/запись), номер блока, который нужно прочитать или записать, количество блоков и указатель на буфер данных. Еще я завел указатель на контекст операции - он нам понадобится чуть позже.
Очередь запросов на чтение/запись
enum IOOperation
{
IO_Read,
IO_Write
};
struct IOMsg
{
IOOperation op;
uint32_t lba;
uint8_t * buf;
uint16_t len;
void * context;
};
// A queue of IO commands to execute in a separate thread
QueueHandle_t sdCmdQueue = NULL;
// Initialize thread responsible for communication with SD card
bool initSDIOThread()
{
// Initialize synchronisation
sdCmdQueue = xQueueCreate(1, sizeof(IOMsg));
bool res = card.begin(&spiDriver, PA4, SPI_FULL_SPEED);
return res;
}
Поток обслуживающий чтение/запись на карту
extern "C" void cardReadCompletedCB(uint8_t res, void * context);
extern "C" void cardWriteCompletedCB(uint8_t res, void * context);
void xSDIOThread(void *pvParameters)
{
while(true)
{
IOMsg msg;
if(xQueueReceive(sdCmdQueue, &msg, portMAX_DELAY))
{
switch(msg.op)
{
case IO_Read:
{
bool res = card.readBlocks(msg.lba, msg.buf, msg.len);
cardReadCompletedCB(res ? 0: 0xff, msg.context);
break;
}
case IO_Write:
{
bool res = card.writeBlocks(msg.lba, msg.buf, msg.len);
cardWriteCompletedCB(res? 0: 0xff, msg.context);
break;
}
default:
break;
}
}
}
}
Функции MSC стали чуть сложнее, но ненамного. Теперь вместо непосредственного чтения или записи этот код отправляет запрос в соответствующий поток.
Отправка запросов на чтение/запись
int8_t SD_MSC_Read (uint8_t lun,
uint8_t *buf,
uint32_t blk_addr,
uint16_t blk_len,
void * context)
{
// Send read command to IO executor thread
IOMsg msg;
msg.op = IO_Read;
msg.lba = blk_addr;
msg.len = blk_len;
msg.buf = buf;
msg.context = context;
if(xQueueSendFromISR(sdCmdQueue, &msg, NULL) != pdPASS)
return USBD_FAIL;
return (USBD_OK);
}
int8_t SD_MSC_Write (uint8_t lun,
uint8_t *buf,
uint32_t blk_addr,
uint16_t blk_len,
void * context)
{
// Send read command to IO executor thread
IOMsg msg;
msg.op = IO_Write;
msg.lba = blk_addr;
msg.len = blk_len;
msg.buf = buf;
msg.context = context;
if(xQueueSendFromISR(sdCmdQueue, &msg, NULL) != pdPASS)
return USBD_FAIL;
return (USBD_OK);
}
А пока, чтобы проверить эти функции сделаем еще один тестовый поток. Он будет эмулировать USB ядро и посылать запросы на чтение.
Тестовый поток
uint8_t io_buf;
static TaskHandle_t xTestTask = NULL;
void cardReadCompletedCB(bool res, void * context)
{
xTaskNotifyGive(xTestTask);
}
void cardWriteCompletedCB(bool res, void * context)
{
xTaskNotifyGive(xTestTask);
}
void xSDTestThread(void *pvParameters)
{
xTestTask = xTaskGetCurrentTaskHandle();
uint32_t prev = HAL_GetTick();
uint32_t opsPer1s = 0;
uint32_t cardSize = card.cardSize();
for(uint32_t i=0; i
Но самое главное, скорость чтения в таком варианте составляет около 450кб/с, а процессор загружен всего на 3-4%. По моему неплохо.
Вообще-то протокол USB предоставляет такой механизм прямо из коробки . Приемная сторона подтверждает пересылку данных неким статусом. Если данные приняты и обработаны успешно, то приемник подтверждает транзакцию статусом ACK. Если устройство не может обработать транзакцию (не инициализировано, находится в состоянии ошибки или не работает по какой либо другой причине), то ответом будет статус STALL.
А вот если устройство распознало транзакцию, находится в работоспособном состоянии, но данные еще не готовы, то устройство может ответить NAK. В этом случае хост обязан обратиться к устройству с точно таким же запросом чуть позже. Этот статус мы могли бы использовать для отложенного чтения/записи – на первый вызов хоста начинаем передачу данных через DMA, но отвечаем на транзакцию NAK. Когда хост приходит с повторной транзакцией и пересылка через DMA уже закончилась – отвечаем ACK.
К сожалению я не нашел в библиотеке USB от ST хорошего способа отправлять сигнал NAK. Коды возврата функций либо не проверяются, либо могут обрабатывать только 2 состояния – все хорошо, либо ошибка. Во втором случае все конечные точки закрываются, везде выставляется статус STALL.
Я подозреваю, что на уровне самом низком уровне USB драйвера подтверждение NAK используется довольно активно, но как правильно воткнуться с NAK на уровне драйвера класса я не разобрался.
По всей видимости создатели библиотек от ST вместо различных подтверждений предоставили более человечный интерфейс. Если устройству есть что отправить хосту оно вызывает функцию USBD_LL_Transmit() - хост сам заберет предоставленные данные. А если функция не была вызвана, то устройство будет автоматически отвечать NAK ответами. Примерно такая же ситуация с приемом данных. Если устройство готово к приему, то оно вызывает функцию USBD_LL_PrepareReceive(). В противном случае устройство будет отвечать NAK если хост попытается передать данные. Воспользуемся этим знанием для реализации нашего MSC драйвера.
Давайте посмотрим какие транзакции бегают по шине USB (анализ производился до изменений в драйвере карты).
Тут интересно даже не сами транзакции, а их временнЫе отметки. Транзакции на этой картинке я выбрал «легкие» - такие, которые не требуют обработки. Микроконтроллер на такие запросы отвечает захардкоженными ответами, особо не размышляя. Важно тут то, что хост не пуляет транзакциями сплошным потоком. Транзакции идут не чаще чем раз в 1 мс. Даже если ответ готов сразу, хост заберет его только на следующей транзакции через 1мс.
А вот так выглядит чтение одного блока данных в терминах транзакций на шине USB.
Сначала хост отправляет SCSI команду на чтение, а потом отдельными транзакциями читает данные (вторая строка) и статус (третья). Первая транзакция – самая длинная. Во время обработки этой транзакции микроконтроллер как раз и занимается вычиткой с карты. И, опять же, между транзакциями хост выдерживает паузу в 1мс.
К слову.
В терминологии USB направление от хоста к устройству называется OUT, хотя для контроллера это прием. И наоборот, направление от устройства к хосту называется IN, хотя для нас это означает отправку данных.
У меня не получилось существенно (скажем до 1Мб/с) ускорить чтение данных – видимо такова пропускная способность карты подключенной по SPI. Но мы можем попробовать поставить к себе на службу 1мс паузы между транзакциями.
Я это вижу так (слегка упрощенно)
Функция SCSI_ProcessRead(), адаптированная под асинхронное чтение
/**
* @brief SCSI_ProcessRead
* Handle Read Process
* @param lun: Logical unit number
* @retval status
*/
static int8_t SCSI_ProcessRead (USBD_HandleTypeDef *pdev, uint8_t lun)
{
USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC;
uint32_t len;
len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET);
if(pdev->pClassSpecificInterfaceMSC->Read(lun ,
hmsc->bot_data,
hmsc->scsi_blk_addr / hmsc->scsi_blk_size,
len / hmsc->scsi_blk_size,
pdev) < 0)
{
SCSI_SenseCode(pdev,
lun,
HARDWARE_ERROR,
UNRECOVERED_READ_ERROR);
return -1;
}
hmsc->bot_state = USBD_BOT_DATA_IN;
return 0;
}
void cardReadCompletedCB(uint8_t res, void * context)
{
USBD_HandleTypeDef * pdev = (USBD_HandleTypeDef *)context;
USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC;
uint8_t lun = hmsc->cbw.bLUN;
uint32_t len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET);
if(res != 0)
{
SCSI_SenseCode(pdev,
lun,
HARDWARE_ERROR,
UNRECOVERED_READ_ERROR);
return;
}
USBD_LL_Transmit (pdev,
MSC_IN_EP,
hmsc->bot_data,
len);
hmsc->scsi_blk_addr += len;
hmsc->scsi_blk_len -= len;
/* case 6: Hi = Di */
hmsc->csw.dDataResidue -= len;
if (hmsc->scsi_blk_len == 0)
{
hmsc->bot_state = USBD_BOT_LAST_DATA_IN;
}
}
По советам в комментариях к одной из моих статей я таки обзавелся осциллографом (пускай и дешевеньким). Он оказался очень кстати для понимания что вообще там происходит. Я взял неиспользуемый пин и выставлял на нем единицу перед началом чтения и ноль после того как чтение закончилось. На осциллографе процесс чтения выглядел так.
Т.е. само чтение 512 байт занимает чуточку больше 1мс. Когда чтение с карты заканчивается данные передаются в выходной буфер, откуда в течении следующих 1мс хост их забирает. Т.е. тут либо происходит чтение с карты, либо передача по шине USB, но не одновременно.
Обычно такая ситуация решается с помощью двойной буферизации. Более того, USB периферия микроконтроллеров STM32F103 уже предлагает механизмы для двойной буферизации. Только они нам не подойдут по двум причинам:
Двойной буфер
typedef struct _USBD_MSC_BOT_HandleTypeDef { ... USBD_MSC_BOT_CBWTypeDef cbw; USBD_MSC_BOT_CSWTypeDef csw; uint16_t bot_data_length; uint8_t bot_data; uint8_t bot_data_idx; ... } USBD_MSC_BOT_HandleTypeDef;
Оригинальная реализация работала на прерывании DataIn - сигнале о том, что данные отправились. Т.е. по команде от хоста запускалось чтение, после чего данные прекладывались в выходной буфер. Чтение очередной порции данных “перезаряжалось” по прерыванию DataIn. Нам такой вариант не подходит. Мы будем начинать чтение сразу после того как предыдущее чтение закончилось.
Перезаряжаем чтение сразу после того, как предыдущее закончилось
void cardReadCompletedCB(uint8_t res, void * context)
{
USBD_HandleTypeDef * pdev = (USBD_HandleTypeDef *)context;
USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC;
uint8_t lun = hmsc->cbw.bLUN;
uint32_t len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET);
if(res != 0)
{
SCSI_SenseCode(pdev,
lun,
HARDWARE_ERROR,
UNRECOVERED_READ_ERROR);
return;
}
// Synchronization to avoid several transmits at a time
// This must be located here as it waits finishing previous USB transfer
// while the code below prepares next one
pdev->pClassSpecificInterfaceMSC->OnFinishOp();
// Save these values for transmitting data
uint8_t * txBuf = hmsc->bot_data + hmsc->bot_data_idx * MSC_MEDIA_PACKET;
uint16_t txSize = len;
// But before transmitting set the correct state
// Note: we are in context of SD thread, not the USB interrupt
// So values have to be correct when DataIn interrupt occurrs
hmsc->scsi_blk_addr += len;
hmsc->scsi_blk_len -= len;
/* case 6: Hi = Di */
hmsc->csw.dDataResidue -= len;
if (hmsc->scsi_blk_len == 0)
{
hmsc->bot_state = USBD_BOT_LAST_DATA_IN;
}
else
{
hmsc->bot_data_idx ^= 1;
hmsc->bot_data_length = MSC_MEDIA_PACKET;
SCSI_ProcessRead(pdev, lun); // Not checking error code - SCSI_ProcessRead() already enters error state in case of read failure
}
// Now we can transmit data read from SD
USBD_LL_Transmit (pdev,
MSC_IN_EP,
txBuf,
txSize);
}
Но это еще не все. Во-вторых изменилась последовательность действий. Теперь сначала заряжается чтение очередного блока данных и только потом вызывается USBD_LL_Transmit(). Так сделано потому, что функция cardReadCompletedCB() вызывается в контексте обычного потока. Если вызвать USBD_LL_Transmit() вначале, а потом менять значения полей hmsc, то потенциально в этот момент может вызваться прерывание от USB, которое также захочет менять эти поля.
В-третьих пришлось прикрутить дополнительную синхронизацию. Дело в том, что обычно чтение с карты занимает чуточку больше времени чем передача по USB. Но иногда бывает наоборот и тогда вызов USBD_LL_Transmit() для очередного блока случается раньше чем предыдущий блок был полностью отправлен. USB ядро от такой наглости дуреет и данные отправляются неверно.
Отправка данных (Transmit) подтверждается событием Data In, но иногда несколько Transmit"ов происходят подряд. Для таких случаем нужна синхронизация.
Решается это очень просто добавлением небольшой синхронизации. Я добавил в интерфейс USBD_StorageTypeDef парочку функций с довольно простой реализацией (хотя, возможно, названия не очень удачные). В реализации используется обычный . OnFinishOp(), которая вызывается, в коллбеке cardReadCompletedCB() будет спать и ждать пока предыдущий пакет данных отправится.
Факт отправки подтверждается событием DataIn, которое обрабатывается функцией SCSI_Read10(), которая вызовет OnStartOp(), которая разблокирует OnFinishOp(), которая отправит очередной пакет данных, в доме который построил Джек. Даже если функции будут вызываться в обратном порядке (а именно так и будет происходить во время первого чтения - сначала SCSI_Read10(), потом cardReadCompletedCB()) то все также будет прекрасно работать (свойство семафора в режиме signal-wait).
Реализация функций синхронизации
void SD_MSC_OnStartOp()
{
xSemaphoreGiveFromISR(usbTransmitSema, NULL);
}
void SD_MSC_OnFinishOp()
{
xSemaphoreTake(usbTransmitSema, portMAX_DELAY);
}
Красными стрелками показана синхронизация. Последний Transmit ждет предыдущий Data In
Последний кусочек паззла - функция SCSI_Read10().
функция SCSI_Read10(), которая вызывается по событию Data In
/**
* @brief SCSI_Read10
* Process Read10 command
* @param lun: Logical unit number
* @param params: Command parameters
* @retval status
*/
static int8_t SCSI_Read10(USBD_HandleTypeDef *pdev, uint8_t lun , uint8_t *params)
{
USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC;
// Synchronization to avoid several transmits at a time
pdev->pClassSpecificInterfaceMSC->OnStartOp();
if(hmsc->bot_state == USBD_BOT_IDLE) /* Idle */
{
// Params checking
…
hmsc->scsi_blk_addr = ...
hmsc->scsi_blk_len = ...
hmsc->bot_state = USBD_BOT_DATA_IN;
...
hmsc->bot_data_idx = 0;
hmsc->bot_data_length = MSC_MEDIA_PACKET;
return SCSI_ProcessRead(pdev, lun);
}
return 0;
}
В новой реализации вызов SCSI_ProcessRead() переехал внутрь if’а и вызывается только для чтения первого блока (bot_state == USBD_BOT_IDLE), тогда как чтение последующих блоков запускается из cardReadCompletedCB().
Давайте посмотрим что из этого получилось. Я специально добавил небольшие задержки между чтениями блоков, чтобы на осциллографе увидеть вот такие зазубрины. На самом деле между операциями чтения проходит так мало времени, что мой осциллограф этого не видит.
Как видно из этой картинке затея удалась. Новая операция чтения стартует сразу как только предыдущая закончилась. Паузы между чтениями довольно маленькие и диктуются, в основном, хостом (та самая задержка в 1мс между транзакциями). Средняя скорость чтения больших файлов достигает 400-440кб/с, что весьма неплохо. И, наконец, загрузка процессора составляет около 2%.
Оригинальная реализация работает примерно так.
Реализация функции записи
/**
* @brief SCSI_ProcessWrite
* Handle Write Process
* @param lun: Logical unit number
* @retval status
*/
static int8_t SCSI_ProcessWrite (USBD_HandleTypeDef *pdev, uint8_t lun)
{
uint32_t len;
USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC;
len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET);
if(pdev->pClassSpecificInterfaceMSC->Write(lun ,
hmsc->bot_data,
hmsc->scsi_blk_addr / hmsc->scsi_blk_size,
len / hmsc->scsi_blk_size,
pdev) < 0)
{
SCSI_SenseCode(pdev,
lun,
HARDWARE_ERROR,
WRITE_FAULT);
return -1;
}
return 0;
}
return 0;
}
void cardWriteCompletedCB(uint8_t res, void * context)
{
USBD_HandleTypeDef * pdev = (USBD_HandleTypeDef *)context;
USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC;
uint8_t lun = hmsc->cbw.bLUN;
uint32_t len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET);
// Check error code first
if(res != 0)
{
SCSI_SenseCode(pdev,
lun,
HARDWARE_ERROR,
WRITE_FAULT);
return;
}
hmsc->scsi_blk_addr += len;
hmsc->scsi_blk_len -= len;
/* case 12: Ho = Do */
hmsc->csw.dDataResidue -= len;
if (hmsc->scsi_blk_len == 0)
{
MSC_BOT_SendCSW (pdev, USBD_CSW_CMD_PASSED);
}
else
{
/* Prepare EP to Receive next packet */
USBD_LL_PrepareReceive (pdev,
MSC_OUT_EP,
hmsc->bot_data,
MIN (hmsc->scsi_blk_len, MSC_MEDIA_PACKET));
}
}
Скорость записи в таком режиме составляет порядка 90кб/с и в основном ограничена скоростью записи на карту. Это подтверждается осциллограммой - каждый пик это запись одного блока. Судя по картинке, запись 512 байт занимает от 3 до 6мс (каждый раз по разному).
Более того, запись иногда может залипать от 100мс до 0.5с - видимо где то в карте возникает необходимость в различных внутренних активностях - ремаппинг блоков, стирание страниц, или что нибудь в таком духе.
Исходя из этого допиливание двойного буфера вряд ли кардинально улучшит ситуацию. Впрочем все равно попробуем это сделать чисто из спортивного интереса.
Итак, суть упражнения в том, чтобы принимать следующий блок от хоста в то время как предыдущий пишется на карту. На ум сразу приходит вариант запустить запись и прием следующего блока одновременно где нибудь в функции SCSI_Write10(), т.е. по событию DataOut (завершен прием очередного блока). Только работать ничего не будет. т.к. прием идет гораздо быстрее, чем запись и может быть принято больше данных, чем карта успевает писать. Т.е. следующие данные перезатирают ранее принятые, но еще не обработанные.
В такой схеме несколько пакетов могут быть приняты подряд, но не все из них успеют быть записаны на SD карту. Скорее всего часть данных пререзатрется следующим блоком.
Нужно делать синхронизацию. Только где? В случае операции чтения двойную буферизацию и синхронизацию мы организовывали в месте где заканчивается чтение с карты и данные перебрасываются в USB. Этим местом была функция cardReadCompletedCB(). В случае операции записи таким центральным местом будет функция SCSI_Write10() - именно в ней мы окажемся, когда будет принят очередной блок данных, и именно отсюда мы будем стартовать запись на карту.
Но между функциями cardReadCompletedCB() и SCSI_Write10() есть одна принципиальная разница - первая работает в потоке SD карты, а вторая в прерывании USB. Обычный поток может быть приостановлен в ожидании некоторого события или объекта синхронизации. С прерыванием такой фокус не пройдет - все функции FreeRTOS с суффиксом FromISR неблокирующие. Они либо работают как надо (захватывают ресурс, если он свободен, отправляют/получают сообщения через очередь если там есть место или необходимое сообщение), либо эти функции возвращают ошибку. Но они никогда не ждут.
Но если нельзя организовать ожидание в прерывании, то можно попробовать сделать так, чтобы прерывание вообще не вызывалось лишний раз. Точнее даже так: чтобы прерывание возникало ровно столько раз и в такие моменты когда нам нужно.
Давайте рассмотрим несколько случаев, которые могут возникнуть в процессе приема/записи.
Случай №1: прием первого блока. Как только принят первый блок, то можно начинать запись этого блока. Одновременно с этим можно начать прием второго блока. Это избавит от паузы, когда мы не принимаем следующий блок, пока предыдущий пишется на карту.
Случай №2: прием блока в середине транзакции. Скорее всего оба буфера уже будут заполнены. Где нибудь в потоке SD карты идет запись блока данных из первого блока, тогда как второй блок мы только получили от хоста. В принципе ничего не мешает зарядить запись второго блока - там на входе стоит очередь (см функцию SD_MSC_Read() выше), которая регулирует входные запросы и будет писать блоки по очереди. Нужно только убедится, что в этой очереди есть место на 2 запроса.
Но как регулировать прием? У нас всего 2 приемных буфера. Если сразу после приема второго блока начать прием следующего, то это перезатрет данные в первом буфере, откуда в данный момент идет запись на карту. В таком случае правильнее будет начинать прием очередного блока данных когда буфер освободится - когда закончится запись (т.е. в коллбеке функции записи).
Наконец, случай №3: нужно уметь правильно завершить процедуру приема/записи. С последним блоком все понятно - нужно вместо приема очередного блока отправить хосту CSW, что данные приняты и транзакцию можно закрывать. Но нужно помнить, что вначале транзакции мы уже организовали лишний прием, поэтому предпоследний блок не должен заказывать прием лишнего блока.
Вот картинка которая описывает эти случаи.
Случай 1: на первый DataOut сразу же начинаем прием второго блока. Случай 2: начинаем прием очередного блока только после того как запись закончена и буфер свободен. Случай 3: на предпоследней записи прием не начинаем, на последней - отправляем CSW
Интересное наблюдение: если запись на карту идет из первого буфера, то по окончании записи следующий блок будет принят в тот же первый буфер. Точно так же со вторым буфером. Я бы хотел воспользоваться этим фактом в своей реализации.
Попробуем реализовать задуманное. Для реализации первого случая (прием дополнительного блока) нам понадобится специальное состояние
Новое состояние для приема первого блока
#define USBD_BOT_DATA_OUT_1ST 6 /* Data Out state for the first receiving block */
И его обработка
/**
* @brief MSC_BOT_DataOut
* Process MSC OUT data
* @param pdev: device instance
* @param epnum: endpoint index
* @retval None
*/
void MSC_BOT_DataOut (USBD_HandleTypeDef *pdev,
uint8_t epnum)
{
USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC;
switch (hmsc->bot_state)
{
case USBD_BOT_IDLE:
MSC_BOT_CBW_Decode(pdev);
break;
case USBD_BOT_DATA_OUT:
case USBD_BOT_DATA_OUT_1ST:
if(SCSI_ProcessCmd(pdev,
hmsc->cbw.bLUN,
&hmsc->cbw.CB) < 0)
{
MSC_BOT_SendCSW (pdev, USBD_CSW_CMD_FAILED);
}
break;
default:
break;
}
}
Контекст записи
typedef struct
{
uint32_t next_write_len;
uint8_t * buf;
USBD_HandleTypeDef * pdev;
} USBD_WriteBlockContext;
typedef struct _USBD_MSC_BOT_HandleTypeDef
{
…
USBD_WriteBlockContext write_ctxt;
...
}
USBD_MSC_BOT_HandleTypeDef;
Инициализация очереди
// Initialize thread responsible for communication with SD card
bool initSDIOThread()
{
// Initialize synchronisation
sdCmdQueue = xQueueCreate(2, sizeof(IOMsg));
…
}
Функция SCSI_Write10()
/**
* @brief SCSI_Write10
* Process Write10 command
* @param lun: Logical unit number
* @param params: Command parameters
* @retval status
*/
static int8_t SCSI_Write10 (USBD_HandleTypeDef *pdev, uint8_t lun , uint8_t *params)
{
USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC;
if (hmsc->bot_state == USBD_BOT_IDLE) /* Idle */
{
// Checking params
…
hmsc->scsi_blk_addr = ...
hmsc->scsi_blk_len = ...
/* Prepare EP to receive first data packet */
hmsc->bot_state = USBD_BOT_DATA_OUT_1ST;
hmsc->bot_data_idx = 0;
USBD_LL_PrepareReceive (pdev,
MSC_OUT_EP,
hmsc->bot_data,
MIN (hmsc->scsi_blk_len, MSC_MEDIA_PACKET));
}
else /* Write Process ongoing */
{
return SCSI_ProcessWrite(pdev, lun);
}
return 0;
}
Функция SCSI_ProcessWrite()
/**
* @brief SCSI_ProcessWrite
* Handle Write Process
* @param lun: Logical unit number
* @retval status
*/
static int8_t SCSI_ProcessWrite (USBD_HandleTypeDef *pdev, uint8_t lun)
{
USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC;
uint32_t len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET);
USBD_WriteBlockContext * ctxt = hmsc->write_ctxt + hmsc->bot_data_idx;
// Figure out what to do after writing the block
if(hmsc->scsi_blk_len == len)
{
ctxt->next_write_len = 0xffffffff;
}
else if(hmsc->scsi_blk_len == len + MSC_MEDIA_PACKET)
{
ctxt->next_write_len = 0;
}
else
{
ctxt->next_write_len = MIN(hmsc->scsi_blk_len - 2 * MSC_MEDIA_PACKET, MSC_MEDIA_PACKET);
}
// Prepare other fields of the context
ctxt->buf = hmsc->bot_data + hmsc->bot_data_idx * MSC_MEDIA_PACKET;
ctxt->pdev = pdev;
// Do not allow several receives at a time
if(hmsc->bot_state != USBD_BOT_DATA_OUT_1ST)
pdev->pClassSpecificInterfaceMSC->OnStartOp();
// Write received data
if(pdev->pClassSpecificInterfaceMSC->Write(lun ,
ctxt->buf,
hmsc->scsi_blk_addr / hmsc->scsi_blk_size,
len / hmsc->scsi_blk_size,
ctxt) < 0)
{
SCSI_SenseCode(pdev,
lun,
HARDWARE_ERROR,
WRITE_FAULT);
return -1;
}
// Switching blocks
hmsc->bot_data_idx ^= 1;
hmsc->scsi_blk_addr += len;
hmsc->scsi_blk_len -= len;
/* case 12: Ho = Do */
hmsc->csw.dDataResidue -= len;
// Performing one extra receive for the first time in order to run receive and write operations in parallel
if(hmsc->bot_state == USBD_BOT_DATA_OUT_1ST && hmsc->scsi_blk_len != 0)
{
hmsc->bot_state = USBD_BOT_DATA_OUT;
USBD_LL_PrepareReceive (pdev,
MSC_OUT_EP,
hmsc->bot_data + hmsc->bot_data_idx * MSC_MEDIA_PACKET, // Second buffer
MIN (hmsc->scsi_blk_len, MSC_MEDIA_PACKET));
}
return 0;
}
Наконец, специальный случай (случай №1) - организуем дополнительный прием данных в случае первого блока (состояние USBD_BOT_DATA_OUT_1ST)
Ответная часть этого кода - коллбек о завершении записи на карту. В зависимости от того какой блок был записан либо организовывается прием следующего блока, либо отправляется CSW, либо ничего не происходит.
Коллбек функции записи
void cardWriteCompletedCB(uint8_t res, void * context)
{
USBD_WriteBlockContext * ctxt = (USBD_WriteBlockContext*)context;
USBD_HandleTypeDef * pdev = ctxt->pdev;
USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC;
uint8_t lun = hmsc->cbw.bLUN;
// Check error code first
if(res != 0)
{
SCSI_SenseCode(pdev,
lun,
HARDWARE_ERROR,
WRITE_FAULT);
return;
}
if (ctxt->next_write_len == 0xffffffff)
{
MSC_BOT_SendCSW (pdev, USBD_CSW_CMD_PASSED);
}
else
{
pdev->pClassSpecificInterfaceMSC->OnFinishOp();
if(ctxt->next_write_len != 0)
{
/* Prepare EP to Receive next packet */
USBD_LL_PrepareReceive (pdev,
MSC_OUT_EP,
ctxt->buf,
ctxt->next_write_len);
}
}
}
Очень редко, но все же иногда возникает ситуация, когда запись на карту заканчивается раньше, чем принят следующий пакет. В итоге код (если бы не было синхронизации) мог бы запросить прием еще одного пакета, хотя текущий ещё не до конца принят. Чтобы такого не происходило пришлось добавить синхронизацию. Теперь прежде чем запросить прием следующего блока код будет ждать пока закончится прием предыдущего. Средства синхронизации, которые использовались при чтении (OnStartOp()/OnFinishOp()) вполне подойдут.
Условия при которых нужно синхронизироваться достаточно хитрые. За счет приема дополнительного блока в начале транзакции синхронизация идет со сдвигом в один блок. Поэтому коллбек записи N-того блока ждет приема N+1 блока. Это в свою очередь означает, что прием первого блока (происходит в контексте прерывания от USB) и запись последнего (происходит в контексте потока SD карты) в синхронизации не нуждаются.
Может показаться что красная стрелка дублирует черную, которая стартует запись следующего блока. Но если посмотреть на код, то видно, что это не так. Красная (синхронизация) синхронизирует код в драйвере MSC (синий квадратик), тогда как очередь обрабатывается в драйвере карты (там где основной цикл потока SD карты). Мне не очень хотелось мешать код разных компонентов.
Я расставил немного дебажного логирования, запись 4кб данных выглядит примерно так
Дебажный лог записи 4 кб блока
Starting write operation for LBA=0041C600, len=4096
Receiving first block into buf=1
Writing block of data for LBA=0041C600, len=512, buf=0
This will be regular block
Receiving an extra block into buf=1
Writing block of data for LBA=0041C800, len=512, buf=1
This will be regular block
Writing block of data for LBA=0041CA00, len=512, buf=0
This will be regular block
Writing block of data for LBA=0041CC00, len=512, buf=1
This will be regular block
Write completed callback with status 0 (buf=0)
Preparing next receive into buf=0
Writing block of data for LBA=0041CE00, len=512, buf=0
This will be regular block
Write completed callback with status 0 (buf=1)
Preparing next receive into buf=1
Writing block of data for LBA=0041D000, len=512, buf=1
This will be regular block
Write completed callback with status 0 (buf=0)
Preparing next receive into buf=0
Writing block of data for LBA=0041D200, len=512, buf=0
This will be one before the last block
Write completed callback with status 0 (buf=1)
Preparing next receive into buf=1
Writing block of data for LBA=0041D400, len=512, buf=1
This will be the last block
Write completed callback with status 0 (buf=0)
Write completed callback with status 0 (buf=1)
Write finished. Sending CSW
Поскольку драйвер MSC по умолчанию заточен на работу с одним блоком в единицу времени, то был смысл заменить readBlocks() на readBlock(). К моему удивлению скорость чтения даже выросла и стала на уровне 480-500кб/с! Аналогичный трюк с функциями записи, к сожалению, прироста скорости не дал.
Но меня с самого начала мучал один вопрос. Давайте еще разок взглянем на картину чтения. Между зазубринами (чтение одного блока) - около 2мс.
Тактирование SPI у меня настроено на 18МГц (используется делитель частоты ядра 72МГц на 4). Теоретически передача 512 байт должна занимать 512 байт * 8 бит /18 МГц = 228мкс. Да, тут будет определенный оверхед на синхронизацию нескольких потоков, обслуживание очереди и прочие штуки, но это никак не объясняет разницу почти в 10 раз!
С помощью осциллографа я измерил сколько реально времени занимают различные части операции чтения
К моему удивлению оказалось, что самой долгой операцией является вовсе не чтение данных, а интервал между командой на чтение и подтверждением от карты, что карта готова и можно читать данные. Причем этот интервал весьма сильно плавает в зависимости от различных параметров - частоты запросов, размера читаемых данных, а также адреса читаемого блока. Последний момент очень интересный - чем дальше от начала карты находится блок, который нужно прочитать - тем быстрее он читается (во всяком случае это было так для моей подопытной карты)
Аналогичная (но более грустная) картина наблюдается и при записи на карту. Мне не удалось достаточно хорошо измерить все тайминги, т.к. они плавали в достаточно широких пределах, но выглядит это примерно так.
Все это усугубляется достаточно большой загрузкой ЦП - около 75%. Сама запись теоретически должна занимать те же самые 228мкс, как и чтение - они же тактируются теми же самыми 18МГц. Только в данном случае еще фигурирует синхронизация потоков FreeRTOS. Видимо из-за большой загрузки ЦП и необходимости переключаться на другие (более приоритетные) потоки суммарное время получается значительно больше.
Но самая большая печаль - ожидание готовности карты. Оно во много раз больше чем в случае чтения. Более того, именно тут карта может залипнуть на 100 и даже 500 мс. К тому же в драйвере карты эта часть реализована активным ожиданием, что и приводит к той самой высокой загрузке процессора
Активное ожидание готовности карты
// wait for card to go not busy
bool SdSpiCard::waitNotBusy(uint16_t timeoutMS) {
uint16_t t0 = curTimeMS();
while (spiReceive() != 0XFF) {
if (isTimedOut(t0, timeoutMS)) {
return false;
}
}
return true;
}
Еще один забавный момент. В FreeRTOS контексты переключаются по прерыванию SysTick, который по умолчанию настроен на 1мс. Из-за этого многие операции на осциллографе дружненько выравниваются по сетке с шагом кратным 1мс. Если карта не тупит и чтение одного блока вместе с ожиданием занимает меньше 1мс, то тогда включая все потоки, синхронизации и очереди можно обернуться за один тик. Отсюда теоретическая максимальная скорость чтения в такой модели составляет ровно 500 кб/с (0.5кб за 1мс). Что радует - она достигается!
Но эту штуку можно обойти. Выравнивание по 1мс происходит по следующей причине. Прерывание от USB или от DMA ни к чему не привязано и может произойти где нибудь в середине тика. Если прерывание изменило состояние объекта синхронизации (например разблокировало семафор, или добавило сообщение в очередь), то FreeRTOS об этом мгновенно не узнает. Когда прерывание сделает свои дела, то управление передается в тот поток, который работал до прерывания. Когда тик закончится вызовется планировщик, и в зависимости от состояния объекта синхронизации может переключить на соответствующий поток.
Но как раз для таких случаев у FreeRTOS предусмотрен механизм принудительного вызова планировщика. Как я уже говорил, нельзя прервать прерывание. Зато можно маякнуть о необходимости вызова планировщика (акцентирую: не вызвать планировщик, а маякнуть о необходимости вызова). Именно это и делает функция portYIELD_FROM_ISR()
Просим планировщик переключить потоки сразу после прерывания
void SdFatSPIDriver::dmaTransferCompletedCB()
{
// Resume SD thread
BaseType_t xHigherPriorityTaskWoken;
xSemaphoreGiveFromISR(xSema, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
Но это если нет длительного ожидания готовности карты. К сожалению если карта долго думает, то чтение растягивается на 2 тика (а запись на 4-6) и скорость оказывается существенно ниже. Более того, если код активного ожидания постоянно долбится в карту, а карта долго не отвечает, то так может пройти и целый тик. В этом случае планировщик ОС может решить, что этот поток слишком долго работает и вообще переключить управление на другие потоки. Из-за этого может возникнуть дополнительная задержка.
Кстати, тестировал я все это на карте 8Гб класса 6. Я попробовал также несколько других карт, которые у меня были под рукой. Еще одна карта также на 8Гб но 10 класса почему то выдала только 300-350 кб/с на чтение, зато 120 кб/с на запись. Я даже рискнул поставить самую большую и быструю карту, которая у меня была - 32Гб. С ней удалось достичь максимальных скоростей - 650кб/с на чтение и 120кб/с на запись. Кстати, скорости, которые я привожу - средние. Мне нечем было измерить мгновенную скорость.
Какие выводы можно сделать из этого анализа?
Результат превзошел мои ожидания. Изначально я целился на скорость порядка 400кб/с, а удалось выжать аж 650кб/с. Но для меня важно даже не абсолютные показатели скорости, а то, что эта скорость достигается с минимальным вмешательством процессора. Так данные передаются с помощью DMA и периферии USB, а процессор подключается только чтобы зарядить следующую операцию.
С записью, правда, супер скоростей получить не удалось - всего 100-120кб/с. Виной всему огромные таймауты самой SD карты. Ну а поскольку карта подключена по SPI другого способа узнать о готовности карты (кроме как постоянно ее опрашивать) вроде как и нету. Из-за этого наблюдается довольно высокая загрузка процессора на операциях записи. У меня есть тайная надежда, что подключив карту по SDIO можно достичь гораздо бОльших скоростей.
Я постарался не просто привести код, но и рассказать как он устроен и почему он устроен именно так. Возможно это поможет сделать что нибудь аналогичное для других контроллеров или библиотек. Я не выделял это в отдельную библиотеку, т.к. этот код зависит от других частей моего проекта и библиотеки FreeRTOS. Более того, свой код я строил на базе . Так что если вы хотите использовать мой вариант его придется бекпортить на оригинальную библиотеку.
В предыдущей статье мы попытались описать тот хаос, который творился в 1998-2003 гг. в области протоколов сопряжения плееров с ПК. Период 2003-2004 гг. стал для отрасли временем упорядочения. Множество ручейков проприетарных протоколов сменилось тремя стройными потоками. Точнее, двумя с половиной, т.к. два из этих потоков представляли собой вариации одного и того же протокола.
Это были:
Чистый Mass Storage использовался наибольшим количеством производителей, особенно компаниями небольшого калибра из Кореи или Китая. В России он стал наиболее популярным протоколом. О нем наш сегодняшний рассказ.
Для обозначения Mass Storage «в быту» используются две аббревиатуры – MSC и UMS. MSC (Mass Storage Class) является официальной, а UMS (возможны варианты расшифровки: USB/Universal Mass Storage) – «народной». Друг другу они не противоречат, а скорее дополняют.
MSC сообщает о том, что протокол входит в число утвержденных стандартных «классов устройств» в рамках спецификации USB и тем самым является индустриальным стандартом де-юре. UMS говорит об универсальности протокола, который на сегодня поддерживается большинством операционных систем и бесчисленным множеством конечных устройств, что делает его стандартом и де-факто. Вариант расшифровки UMS как USB Mass Storage дополняет эту информацию, уточняя, что в качестве физической линии используется интерфейс USB. Буквы MS (Mass Storage), общие для всех аббревиатур, показывают, что перед нами протокол, предназначенный для работы с устройствами хранения больших объемов данных. Именно для них и был разработан данный стандарт – для «флэшек», карт-ридеров, мобильных HDD-накопителей. Как он попал в портативные плееры?
Протокол Mass Storage задумывался в первую очередь для подобных устройств. Его появление в MP3-плеерах было вынужденным шагом
В прошлых материалах мы неоднократно говорили о том, как стихийно, неожиданно появились и начали развиваться портативные аудиоплееры. Индустрия просто не хотела их замечать, сначала в силу их маргинальности, позже – из-за надуманной связи этих устройств с цифровым пиратством. Это имело много последствий, и одним из них было то, что плееры «обошли» при раздаче классов устройств USB.
Взглянем на список этих классов: тут есть и внешние звуковые карты, и коммуникационные устройства, и отдельный класс для периферии типа мышей и клавиатур, есть свои классы для принтеров, USB-хабов, вэб-камер, адаптеров беспроводной связи. Свой класс есть и для цифровых камер. И только аудио- и мультимедиа-плееры остались в категории «прочие».
Среди стандартных классов USB нашлось место для самых разных устройств. Но только не для мультимедиа плееров
Добрых лет пять никто не задумывался о разработке отдельного класса для них. Производителям оставалось выбирать из того, что есть.
На этом фоне MSC/UMS был единственным универсальным решением. Если ограничивать задачи исключительно «тупой» загрузкой контента в плеер, то ничего больше и не нужно. К тому же, протокол позволял превратить плеер в мобильный накопитель. Простые продавцы и покупатели и сейчас описывают плееры с данным протоколом как «работающие как флешка», «подключающиеся как флеш-накопитель» «программы ставить никакие не надо», «можно файлы хранить» и т.д. и т.п.
Простенький плейдрайв – « MP3- Stick» и Mass Storage протокол – созданы друг для друга
Эта дополнительная возможность хорошо вписывалась в подход «много-в-одном», выбранный азиатскими производителями MP3-плееров. Именно они стали пионерами в адаптации MSC/UMS в аудиоплееры. Они и компания Sigmatel, чья платформа STMP3400 в начале 2003 года начала поддерживать этот протокол.
Январь 2003 года – Sigmatel объявляет о поддержке Mass Storage в своих платформах D- Major
Достоинства протокола. Главное – простота: все операции осуществляются через стандартные файловые оболочки, в т.ч. Windows Explorer (Проводник), никакие дополнительные знания или обучение для работы с ним не требуются.
Распространенность – уже Windows Me и 2000 имели базовую поддержку протокола, Windows XP поддерживал его полностью. Множество других ОС – MacOS, Linux и т.п. – совместимы с Mass Storage.
ОС, в том или ином виде поддерживающие Mass Storage протокол
Сегодня сложнее найти ПК, не поддерживающий этот протокол. Поддержка в данном случае означает наличие драйверов протокола в составе операционной системы.
Mass Storage плеер на 1.8” жестком диске Toshiba подключен к ПК. Как MSC-устройство он использует стандартный драйвер USBSTOR. SYS, входящий в состав ОС. Как накопитель он также использует стандартные драйверы Windows. Установка дополнительных драйверов не требуется .
Так как вся работа с контентом также ведется стандартными средствами, через Windows Explorer (Проводник), у пользователя вообще не возникает необходимости в установке чего бы то ни было: вся поддержка протокола уже встроена в ОС.
Плеер виден в Проводнике Windows как еще один жесткий диск. Вся работа с контентом ведется в Проводнике или любом файловом менеджере на выбор пользователя. Установка дополнительного программного обеспечения не требуется
Получается настоящий Plug-and-Play: вынул из коробки, подключил и пользуйся. По таким параметрам, как прозрачность, невидимость для пользователя этот протокол просто не имеет равных.
С точки зрения совместимости с портативными устройствами у нас тоже все хорошо: протокол не зависит от файловых систем и может работать с любой из них, если она поддерживается ОС.
Немаловажным является существование спецификации USB host (on the go), позволяющей подключать Mass Storage устройства к другим портативным (и не портативным) аппаратам. Сегодня MSC-совместимый плеер можно подключить к обширному перечню устройств, будь то игровая приставка, стереосистема, автомагнитола, FM-трансмиттер, другой плеер.
Набирают популярность автомобильные FM-трансмиттеры, позволяющие подключать к себе любой Mass Storage плеер
Недостатки протокола являются продолжением его достоинств. Его функциональность является базовой, примитивной. Фактически он не способен ни на что, кроме копирования данных взад-вперед.
Но данные, с которыми имеет дело плеер, являются больше чем просто набором двоичных символов, это контент. У каждой единицы контента, будь то песня или файл, есть целый ряд свойств, таких, как название, формат, автор, продолжительность и т.п. Отдельные единицы могут быть частью более сложных совокупностей, таких, как альбом, плей-лист.
Ни о чем подобном Mass Storage знать не знает, что возлагает все заботы о менеджменте контента либо на пользователя, либо на встроенное ПО плеера. Последнее же чаще всего не способно эффективно справляться с задачей управления большим количеством контента. Как следствие, большинство MSC/UMS-плееров имеют крайне бедный механизм навигации – по папкам, аналогично навигации в Windows Explorer. При этом не используется значительный объем информации, содержащийся в метаданных, тэгах, который удобен для классификации контента.
Информация, которая может содержаться в тэгах (на примере программы MP3 tag). Мало что из этого используется в Mass Storage плеерах
Пользователь вынужден организовывать свой контент самостоятельно, с помощью системы вложенных папок. При этом, избрав, к примеру, систему классификации «песня-альбом-автор», он не сможет быстро и безболезненно перейти к системе «песня-жанр» или «песня-год записи», ему придется перетряхивать всю библиотеку.
Очень слаба по своим возможностям организация таких плеерах плей-листов. Плей-лист обычно возможен только один. Работа с плей-листами возложена исключительно на само устройство, и если через ПК в режиме MSC/UMS был удален один из файлов, входящих в плей-лист, это может нарушить работу всего листа в целом. Такая премиум-возможность, как отображение обложки альбома (Album Art или Jacket), «чистым» MSC/UMS-плеерам недоступна в принципе. Теоретически ее можно реализовать загрузкой графического файла из папки, но на практике никто этого пока не сделал. А если сделает, пользователю придется вручную рассовывать во все папки соответствующие картинки. Некоторые плееры имеют возможность отображения слов песни (Lyrics), но берутся эти слова не из метаданных: пользователю приходится самостоятельно подготавливать их с помощью специальной программы.
Во всем этом главная проблема Mass Storage. Плеер – это больше, чем просто мобильный накопитель, для эффективной работы он должен иметь глубокое понимание того, что, собственно, хранится в его памяти. Будучи современным мультимедийным устройством, рассчитанным на самый широкий круг пользователей, он не может просто пробубнить подряд все, что на него записано. Он должен уметь рассказать о том, что мы смотрим или слушаем, причем кратко, исчерпывающе и ненавязчиво, как высококлассный конферансье. Он, словно опытный библиотекарь, должен помочь нам быстро найти среди тысяч песен именно то, что нам нужно, даже если мы подзабыли название. Во всем этом предельно ограниченный в своих возможностях протокол MSC/UMS ему не помощник. И свежий зажигательный шлягер, и кандидатская диссертация, и своп-файл Windows для него являются лишь безликими массивами данных. Это превращает протокол в своего рода обезличивающее «бутылочное горлышко» между двумя мощными мультимедиа-системами – плеером и ПК. На плечи последних падает вся тяжесть преобразования безликого потока информации в удобную для пользователя форму.
На ПК все зависит от самого пользователя: проявит он усидчивость и изобретательность – организует музыкальную библиотеку на зависть всем. А может и просто сваливать все в одну папку, пока там не станет совершенно невозможно что-то найти.
На плеере же все зависит от разработчика, а они вовсе не горят желанием прилагать большие усилия для разработки мощной программной начинки. В результате конферансье из таких плееров так себе – монотонным голосом он прочитает название песни, автора, в лучшем случае – альбома. А может и вовсе только имя файла.
Интерфейс iPod по сравнению с интерфейсом Mass Storage плеера iriver h300 выглядит более спартанским, но при этом отображает значительно больше информации о проигрываемом треке. При этом плеер от iriver – это еще относительно удачный пример Mass Storage плеера
И библиотекарь он никудышный – так, рукой направление укажет, где искать, но не более.
Пользователь Mass Storage плеера (Cowon X5 в данном случае, слева) при поиске интересующей композиции может руководствоваться только логикой папок и файлов, созданной им самим. В случае применения иных решений (как в Creative Zen Touch, справа) у них есть возможность свободного поиска по параметрам
Есть отдельные исключения (например, плееры от Archos), но их не много.
Эта ситуация имеет очень простое следствие. Пользователи, которые с компьютером «на ты», привыкшие к концепции файлов и папок, не сильно требовательные к внешним эффектам и быстро приспосабливающиеся к новому, стоят горой за чистый Mass Storage. Прозрачность, открытость и распространенность протокола для них преимущества, рядом с которыми меркнут все минусы.
Компании iriver и Cowon своей популярностью среди определенных слоев покупателей обязаны не в последнюю очередь поддержке «свободного» Mass Storage
А вот пресловутые «обычные» пользователи не очень довольны. Для них плеер – это все-таки не флэшка, не хранилище для файлов, а плеер. Аккуратно сооружать пирамиду файлов и папок музыкальной библиотеки у них нет никакого желания, бродить в недрах этой пирамиды на экране плеера, ориентируясь лишь на названия папок, – тоже. Навигация по метаданным, проигрывание с красивым Album Art, автоматическая загрузка на плеер новых песен – все это им гораздо ближе. Значительное количество возвращенных в магазин и обменянных на iPod-ы MSC/UMS-плееров в тех же Соединенных Штатах – тому подтверждение.
И все же тон в отрасли задают производители, не использующие чистый Mass Storage
Есть и еще одна категория недовольных протоколом. Это студии звукозаписи и киностудии. Безразличный ко всему, Mass Storage уж точно никак не сможет отличить «пиратский» трек от честно приобретенного. Производитель, плееры которого поддерживают загрузку контента через «чистый» Mass Storage, вряд ли могут рассчитывать на плодотворное сотрудничество с крупными лейблами. Конечно, небольшим компаниям оно не очень-то и нужно. Но крупные корпорации, желающие дать пользователям вертикальное решение, включая приобретение контента, вынуждены принимать этот фактор во внимание.
В результате производитель, желающий создать плеер, который:
Привлекателен для «простого пользователя» в силу легкой, автоматизированной работы с ним, удобной, быстрой и красивой навигации, эффективного использования метаданных
Не вызывал бы отторжения у киностудий и студий звукозаписи, что позволило бы организовать удобное для этих самых «простых пользователей» (и выгодное для компании) приобретение контента
Не потребует больших вложений в разработку, как по времени, так и по квалификации программистов
– вынужден искать решения за рубежами возможностей «чистого» Mass Storage.
По поводу режима USB Mass Storage, неизвестно куда "исчезнувшего" из Android Ice Cream Sandwich, в интернете нынче бушует много страстей. Высказались все, и знающие, но редко высказывающися, и незнающие, зато всегда имеющие мнение. Страсти бушуют настолько нешуточные, что мне даже пришлось "вернуться" с блоггерской пенсии, зайти в ЖЖ и написать несколько строк.
Если по-быстрому и по сути:
- USB Mass Storage mode - это режим подключения SD карточки в телефоне, когда телефон превращается в card reader для SD карточки.
- USB Mass Storage mode требует наличия в телефоне отдельной SD карточки, заменяемой или встроенной. То есть зависит он от конретной кофигурации "железа" в телефоне.
- Из Android 4.x ICS поддержка USB Mass Storage никуда не делась - достаточно поставить ICS на Nexus S, и вы увидите, что он там есть и прекрасно работает.
- USB Mass Storage не поддерживается на Galaxy Nexus, опять-таки из-за кофигурации "железа" последнего. Кто-то просто перепутал железо с софтом, а народ это подхватил.
- Преживающим за будущие телефоны: не переживайте. Производители телефонов прекрасно знают, что на многих рынках наличие SD card в телефоне важно для покупателей, и во многие модели будет включена поддержка заменяемой внешней SD карточки, зачастую вместе со встренной "внутренней" SD карточкой.
Начнем издалека. С кофигурацией хард драйвов при сборке домашнего компьютера сталкивались все гики. Кто-то покупает один большой и разбивает его на несколько разделов, кто-то покупает небольшой SSD для установки на него системы и большой внутренний SATA для хранения файлов, кто-то докупает еще и внешний USB для хранения/переноса фильмов, фотографий и музыки. Надо прикидывать, какого размера покупать диски, какой скорости, с каким интерфейсом подключения, какой фирмой произведенные, и т.п. Знакомая ситуация? Так вот с аналогичной ситуацией сталкиваются и производители телефонов при проектировании и производстве очередной модели.
Что нужно хранить на телефоне? Да практически то же самое:
1. Системные файлы (ОС)
2. Установленные программы и их файлы
3. Файлы пользователя
Допустим, вам надо сделать так, чтобы разные типы данных хранились на разных дисках: C, D и Е, соответственно. Про подобную разбивку и ее достоинства я . Напомню, что подобное разделение дает некую "гибкость", в том числе и на десктопе. Например, надо переустановить систему - форматируем С, а наши данные на D и Е остаются. Тогда речь шла в основном про C и D. А теперь поговорим про Е.
Е - это где вы и ваши программы хранят свои данные, особенно если эти данные занимают много места. Вы обычно кладете туда свой медиа контент (фильмы, музыку, фото), камера туда пишет новые фотографии и видео, GPS навигаторы загружают туда свои гигабайтные карты, и т.п. В Андроиде - это ваша SD карточка. Исторически так сложилось, что "внутренняя" флеш-память для C и D была дорогая, и если нужно было хранить что-то обьемное, это нужно было делать на внешней дешевой microSD, то есть диске Е. Дополнительной радостью от этого было то, что пользователь получал возможность регулировать размер "диска Е", а также использовать microSD для обмена данными с другими устройствами, с целью чего она форматировалась под файловую систему VFAT, понятную всем. Примерно так, как если бы диск Е у вас был внешним USB хардом, и вы бы его подключили к другому компьютеру чтобы "скинуть фильмы".
Продолжая аналогию microSD с USB внешним хардом, скажем, что процесс подключения его к другому устройсву (компьютеру, медиа плееру, и т.п.) требовал его полного отключения от вашего смартфона. Нельзя же USB хард воткнуть сразу в два устройства. Вот и при включении USB Mass Storage Mode контроллер чтения/записи SD карты полностью переключался на USB. В результате телефон доступа к SD карте больше не имел, а контроллер ее выступал в роли USB ридера. Как и в случае с любым ридером, компьютер работал с SD картой напрямую. То есть если бы она была отформатирована под файловую систему, неизвестную ОС, стоящей на компьютере, ОС вам просто сказала бы, что целостность SD карты нарушена, и ее надо бы отформатировать. Так работает любой ридер, он не понимает файловой структуры на накопителе, он просто дает доступ к "блокам" (кластерам) на нем, остальное должна "понять" ОС.
Производители экспериментируют много. В каких-то телефонах ставят быстрый дорогой маленький С, менее быстрый, более дешевый, и больший по размеру D, и дают пользователю самому выбрать Е (Galaxy S), в каких-то C и D "живут" на одном "физическом харде", а Е по-прежему живет на microSD. А в каких-то (таблетки, Galaxy Nexus) - все живет как логические разделы на одном большом чипе. А бывают, что производители ставят и внутренний логический Е, а пользователю дают возможность воткнуть еще и диск F - внешнюю microSD (Samsung Vibrant). Выбор конфигурации обусловлен многими факторами. Факторы разные, в зависимости от компании. Например, в устройствах от Эппла все будет жить на одном внутреннем дорогом чипе. У других конфигурация будет обуславливаться в основном ценой комплектующих, пожеланиями операторов и рынком сбыта. Андроид (благодаря использованию ядра Линукс), спокойно поддерживает любую кофигурацию.
А теперь давайте перейдем на терминологию Линукса, где все устройства (логические диски, дисководы, CD-ROMы и т.п.) являются директориями, и назовем вещи своими именами. С станет /system, D - /data, E - /sdcard. На таблетках (кстати, почему весь сыр-бор не подняли тогда? Куда смотрели критики Андроида?) и на Galaxy Nexus /sdcard - это всего лишь директория /data/media, подмаунченная через FUSE. (О, FUSE - это прекрасная вещь, о ней в другой раз, для совсем безнадежных гиков). Выражаясь нормальным языком, можно сказать, что /sdcard стал таким хитрым линком на /data/media. То есть диск Е стал линком на директорию на диске D. Таким образом, пользовательские программы и пользовательский же медиа контент делят общее пространство на диске. И не получается, что места для музыки осталось еще много, но вам нужно место для очередной программки, но вот его уже не осталось. Прямо как на айФоне.
Почему нельзя включить USB Mass Storage Mode для подобной конфигурации? А именно потому, что надо подключать весь "логический диск" сразу. А он у вас ненастоящий, так как он специально сделан так, чтобы он был варьируемого размера. А чтобы иметь возможность подключать, нужно знать какой кусок чипа отведен под нужный логический диск, то есть фиксировать его размер, чего не хочется делать. "А почему тогда не весь чип" простите вы, "подключите весь чип, а я уж там сам разберусь". Хорошо, подключим весь чип. А у вас там не только и /data и /system, а еще вполне вероятно, что boot, recovery, bootloader - довольно сложная система из нескольких разделов (логических дисков), из которых несколько - в мало кому понятной файловой системе ext4. И если все это дело подключить к компьютеру как внешний USB накопитель, любой компьютер предложит вам это безобразие отформатировать. Думаю, что обьяснять не надо, что бывает, когда на вашем компьютере вдруг оказывается отформатированным диск С.
В качестве замены нам предлагается протокол MTP (Media Transfer Protocol), при использовании которого ваш компьютер видит телефон как MP3 плеер. В Windows такой протокол понимает Windows Media Player (да и сам Windows понимает, начиная с Vista), позволяя переливать на телефон медиа контент из вашей медиатеки (из того же Media Player). Решение менее демократичное, чем USB Mass Storage, но более демократичное, чем тот же iTunes. Решение спорное и не всем понравится, но давайте учтем следующее:
Galaxy Nexus, хоть и является флагманским устройством для Android Ice Cream Sandwich, создан для демонстрации не всех возможностей ОС, а лишь их части. И ни в коем случае возможности ОС не ограничены возможностями данного телефона.
Nexus устройства являются некой "игровой площадкой" Гугла, платформой для тестирования новых возможностей и идей. Вполне вероятно, что какие-то из них куда-то пойдут, а какие-то - нет. Но опробовать их по любому надо.
Nexus устройства другим производителям не указ. Гугл не накладывает ограничений на конфиграцию телефонов. Даже если Андроид не поддерживает какое-то определенное "железо", производителю ничего не стоит добавит поддержку самому - исходники-то открыты, и это делалось не раз. Так что даже если поддержка USB Mass Storage вдруг пропала бы из ICS по воле Гугла, ее быстро добавили бы обратно (это несложно, поверьте мне).
Телефоны на ICS со сьемными SD card и USB Mass Storage будут. Для рынков, где 3G связь медленна или имеет плохое покрытие (например, Индия), наличие в телефоне SD card - один из основных факторов при покупке. И, поверьте мне, производители телефонов прекрасно это знают. И знают, что это востребовано не только в Индии.
Если вы настоящий гик, вы всегда найдете способ перебросить любые (не только медиа) файлы на телефон. Подсказка: adb push file /sdcard/
P.S. Любителям конспирологических теорий: а как вам идея, что Гугл избрала такую конфигурацию, чтобы избежать использования VFAT (обязательный при использовании SD card), опасаясь судебного иска от Майкрософта на основе патента 17-летней давности на использование длинных имен файлов в файловой системе FAT?
23 ноября 2011 в 02:17Из ранних обзоров девайсов (а именно Galaxy Nexus) на новой версии Андроида 4.0 (он же ICS, он же «мороженный бутерброд») выяснилось, что они не поддерживают такую замечательную фишку, как USB Mass Storage, т.е. использование телефона как флешки, без дополнительных ухищрений. Пользователи андроид-аппаратов, вплоть до версии 3.0 «Honeycomb» (а, как оказалось, изменения произошли именно в этой версии) знают, что чтобы перекинуть файлы на телефон или с него, достаточно было просто воткнуть его в компьютер без связи с тем какая операционная система или софт на нем установлены. Логично, что новости об исчезновении этой опции в новых версиях не вызвали энтузиазма среди пользователей андроида, и даже заставили многих задуматься о наличии некой проблемы или недоработки. К счастью, один их инженеров Google Дан Морилл (Dan Morrill) в комментариях к гневному посту в reddit , прояснил ситуацию, подробно объяснив о том, что, собственно произошло, и почему. По моему это очень любопытно, так что ниже перевожу перевод его комментариев.
Сам ICS поддерживает USB Mass Storage (UMS). А телефон Galaxy Nexus - нет. Это та-же история, что и с Honeycomb: HC поддерживает UMS, а планшет Xoom - нет. Если у некого аппарата есть внешняя SD-карточка, то к ней поддерживается доступ через UMS. Если в наличии только встроенная память (как у Xoom и Galaxy Nexus) то доступ к памяти устройства поддерживается только по MTP *(Media Transfer Protocol) и PTP *(Picture Transfer Protocol) . Физически, невозможно поддерживать UMS на устройствах, у которых нету выделенного раздела для хранения информации (как, например, внешней SD-карточки или отдельного раздела, как в Nexus S). Причина в том, что USM и это блочный низкоуровневый протокол, который дает хост-машине прямой доступ к физическим блокам носителя, что не позволяет ему быть одновременно примонтированным в Андроиде. С новой объединенной моделью хранения информации, которую мы ввели в Honeycomb, все 32гб (ну или все 16гб, или все N...) полностью находятся в совместном владении и приложениями и медиа-файлами. Вам больше не приходится грустно взирать на свободные 5гб на вашем Nexus S, в то время как внутренний раздел для приложений забит под завязку - теперь это один большой, счастливый раздел. К сожалению, цена, которую за это пришлось заплатить - это то, что Андроид больше не может позволить себе предоставить ПК прямой доступ и дать ему безнаказанно домогаться до носителя информации по USB. Вместо этого, мы используем MTP. На виндоуз (который у большинства пользователей), есть встроенная поддержка MTP и в эксплорере, и устройство выглядит точно так-же как обычный диск. На Линуксе и Маке, к сожалению, все не так просто, но я уверен, что в скором времени, ситуация улучшится. В целом, это должно сделать использование телефона гораздо более удобным.
На вопрос, если у Nexus S только внутренняя память, как-же программы вроде файловых менеджеров работаю без рута, Дан объяснил:
Волшебство. ;)Сначала, мы выделяем директорию на внутренней памяти которая будет «SD-карточкой». Затем мы берем файловую систему FUSE, которая не делает ничего, кроме перемонтирования этой директории как /sdcard c выключенной проверкой доступов. Кроме доступов, FUSE это просто сквозная оболочка передающая запись и чтение прямо в/из директорию. Другими словами, мы используем липовую файловую систему FUSE, для перемонтирования определенной директории которая маскируется под SD-карточку. Это полностью прозрачно для приложений, которые не знают, что они не обращаются напрямую к диску.
Да. Собственно по определению работающая под FAT32 папка /sdcard (или, как она называется в API - «папка внешнего носителя информации») не поддерживает разграниченного доступа, что нормально, так как это общая, открытая для всех файлопомойка, где одно приложение может топтаться по файлам другого. Она изначально задумывалась для таких вещей как музыка и фотографии, а не приватных данных, которые «живут» в личном хранилище у приложений, расположенном на внутренней памяти с разделенным доступом.
На устройствах без SD-карточки, единственная физически файловая система - это хранилища личный данных приложений. Так что мы выбираем директорию, объявляем ее файловой помойкой, монтируем ее как отдельную FUSE файловую систему, которая игнорирует права доступа, так же как они игнорируются в FAT32.
О причинах всех этих изменений Дан объяснил:
(...) Мы не сделали это потому, что мы хотели перейти на ext3, хотя мы и выиграли это как побочный эффект. Мы сделали это так как хотели объединить общедоступное пространство для хранения информации (например для музыки и фотографий) с внутренним хранилищем для приложений. Нам надоело наблюдать, как производители телефонов включают гигабайты памяти для музыки, а у пользователей все равно заканчивается место для приложений и информации. Такой подход позволяет объединить все в один раздел, что намного лучше.
Еще один человек поинтересовался почему нельзя использовать оба подхода, ведь слот памяти занимает достаточно мало места. На что, было рассказано немного об идеологии Андроида:
Технически в железе нет никакой проблемы поставить и то и другое. Проблема в том, что этому не сделаешь удобный интерфейс.
Один из базовых принципов Андроида - это то, чтоб пользователю не нужен был файловый менеджер. Никогда. Мы хотели избежать синдрома, когда на каждый чих выскакивает диалог выбора файла, как это часто бывает в других OS. Внутренняя информация, с которой приложения умеют работать, должна быть просто доступна «по волшебству» или же храниться в облаке. Нельзя заставлять пользователя заниматься спелеологией, разыскивая файлы на SD-карточке.Проблема в том, что поддерживая и внутреннюю память и внешнюю SD-карточку, следовать этому принципу внезапно становится намного сложнее. Для конкретного снимка, камера должна сохранять его на внутренние 16гб или на SD карточку? Приложения из Маркета - их ставить на внутреннюю память или на SD? И так далее. Да, мы можем решить это заставив пользователя самого выбирать или выставлять настройках. Но это в конечном счете и есть тот диалог выбора файла, или что-то на него похожее настолько, что нам это так-же не нравится.
Кроме этого, будут последствия и для API - если вы втыкаете SD карточку с фотографиями, должен ли их индексировать системный media content provider?.. Если да, то пострадают приложения, потому как они не были спроектированы учитывать, что фотографии могут внезапно появляться и исчезать.
В какой-то момент, мы наверное добавим концепцию импорта/экспорта с подключаемого носителя. Тогда камера всегда будет сохранять фотографии на внутренние 16гб, а когда вы воткнете SD-карточку (или подключите USB флешку), вы сможете начать миграцию или получите окно импорта/экспорта. Но до того момента, у большинства аппаратов будет или SD-карточка или большая внутренняя память, но не оба варианта. Я прекрасно понимаю, что многим нравится SD-карточки, и мне самому не хватает USB Mass Storage, но именно по этому так классно, что у нас есть так много аппаратов, из которых можно выбирать:)
А вообще, конечно, это клубок проблем. Мы тут уже обдумываем компромиссы для будущих версий.
Надеюсь, вам это тоже помогла немного прояснить ситуацию, и было интересно заглянуть на внутреннюю кухню разработчиков Андроида:)