Прошло более полутора лет с последней статьи, посвященной применению современного C++ при разработке программ для микроконтроллеров, а именно USB. За это время удалось покрыть USB OTG FS, а также еще один класс устройств - Mass Storage.

Среди изобилия способов организовать обмен данными между устройством и хостом остановился на связке SCSI поверх транспорта Bulk-Only (он же Bulk/Bulk/Bulk), так как, насколько удалось понять, эту пару можно назвать наиболее популярной, а также честно признаюсь, что шел по стопам уважаемого @COKPOWEHEU, а именно его материала USB на регистрах: bulk endpoint на примере Mass Storage.

Интерфейсы

В нашем случае устройство класса MSC должно поддерживать один интерфейс с двумя конечными точками типа Bulk. Интерфейс также должен содержать один или несколько Logical Unit, которые для операционной системы являются логическими устройствами в составе одного физического.

Таким образом в очередной раз мы получаем иерархическую структуру, схематично представленную на рисунке 1.

Рисунок 1. Структура USB-устройства с интерфейсом SCSI.
Рисунок 1. Структура USB-устройства с интерфейсом SCSI.

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

Запросы интерфейса SCSI

Общий порядок обмена приведен на рисунке 2.

Рисунок 2. Обмен данными SCSI.
Рисунок 2. Обмен данными SCSI.

Очередной этап обмена данными начинается с передачи от хоста к устройству команды, которая задается 31-байтовой структурой CBW (Command Wrapper Block), её описание представлено в таблице.

Размер (байтов)

Название поля

Описание поля

4

dCBWSignature

Магическое число 0x43425355 (оно же 'USBC').

4

dCBWTag

Идентификатор запроса (должен совпадать с одноименным полем в структуре ответа).

4

dCBWDataTransferLength

Максимальный объем передаваемых данных (OUT или IN).

1

bmCBWFlags

Направление передачи данных. Используется только старший (седьмой) бит. Если bmCBWFlags[7:7] = 0, то передача OUT, иначе IN.

1

bCBWLUN

Номер LUN, которому адресована команда. Используются только младшие 4 бита (bCBWLUN[0:3]).

1

bCBWCBLength

Длина команды в блоке CBWCB. Используются только младшие 5 бит (bCBWCBLength[0:4]).

16

CBWCB[16]

Блок команды (формат зависит от конкретной команды).

Если команда подразумевает передачу данных (OUT или IN), то устройство (точнее соответствующие Logical Unit) должно быть готов к приему или передаче. После нее (или сразу после получения CBW, если команда не подразумевает дополнительный обмен), устройство возвращает хосту ответ, который задается 5-байтовой структурой CSW (Command Status Wrapper), описание которой приведено в таблице.

Размер (байтов)

Название поля

Описание поля

4

dCSWSignature

Магическое число 0x53425355 (оно же 'USBS').

4

dCSWTag

Идентификатор соответствующего запроса (из принятой структуры CBW).

4

dCSWDataResidue

Для передачи OUT поле содержит разницу между dCBWDataTransferLength и реально обработанных устройством данных (по документации я не понял, что считается "обработанными" - просто принятые или которые были "полезны").

Для передачи IN поле содержит разницу между dCBWDataTransferLength (ожидаемым объемом) и реально переданным количеством байтов.

1

bCSWStatus

Статус команды. 0 - успешное выполнение, 1 - ошибка исполнения, 2 - ошибка фазы (как я понял, означает нарушение последовательности действий со стороны хоста).

Таким образом, как и сказано в статье @COKPOWEHEU, первая посылка идет от хоста к устройству (блок CBW), вторая (при наличии) - либо от хоста, либо от устройства (в зависимости от команды), и третья - от устройства (блок CSW). На рисунке 3 представлен фрагмент окна Wireshark с командой чтения, демонстрирующий порядок пересылки пакетов.

Рисунок 3. Перехват команды на чтение READ (10).
Рисунок 3. Перехват команды на чтение READ (10).

Интерфейс SCSI

Базовый интерфейс должен выполнять две функции:

  1. Обработка общих команд, адресованных конкретно интерфейсу. Таких для интерфейса SCSI предусмотрено две: Bulk-Only Mass Storage Reset (значение bRequest в пакете Setup равно 255) и Get Max LUN (значение bRequest в пакете Setup равно 254). На первый из них, насколько я понял, можно особо не реагировать, на второй же нужно вернуть номер последнего LUN, Logical Unit-ы должны начинаться с нулевого и идти по порядку.

  2. Обрабатывать OUT-пакеты от хоста и диспетчеризовать запросы по соответствующим Logical Unit-ам.

Если с первым пунктом всё понятно, то реализация второго не совсем очевидна, примерная блок-схема соответствующего метода представлена на рисунке 4 (прошу прощения за мои блок-схемы).

Рисунок 4. Блок-схема обработчика OUT-пакета интерфейса SCSI.
Рисунок 4. Блок-схема обработчика OUT-пакета интерфейса SCSI.

Рассмотрим фрагменты исходного кода соответствующего интерфейса. Класс является шаблонным и производным от базового класса Interface:

template <uint8_t _Number, uint8_t _AlternateSetting, typename _Ep0, typename _OutEp,
         typename _InEp, typename... _Luns>
class ScsiBulkInterface : public Interface<_Number, _AlternateSetting,
         InterfaceClass::Storage, static_cast<uint8_t>(MscSubclass::Scsi),
         static_cast<uint8_t>(MscProtocol::Bbb), _Ep0, _OutEp, _InEp>
{     
  using Base = Interface<_Number, _AlternateSetting, InterfaceClass::Storage, static_cast<uint8_t>(MscSubclass::Scsi), static_cast<uint8_t>(MscProtocol::Bbb), _Ep0, _OutEp, _InEp>;

  static constexpr std::add_pointer_t<bool(void* buffer, uint16_t size)> _lunRxHandlers[]
    = {_Luns::RxHandler...};
  static constexpr std::add_pointer_t<bool(const BulkOnlyCBW& cbw, BulkOnlyCSW& csw, InTransferCallback callback)> _lunCommandHandlers[]
    = {(ScsiLun<_Luns>::template CommandHandler<_InEp>)...};
  ...

Ключевыми элементами объявления класса является variadic-параметр Luns, а также два constexpr-массива с указателями на обработчики OUT-пакетов с данными и обработчики команд соответственно, причем "базовый" класс ScsiLun расширяется за счет пользовательских типов, что является некоторым аналогом виртуальности в нешаблонном программировании на C++.

Реализация метода обработчика OUT-пакетов не так интересна, поэтому спрятана под спойлер.

Обработчик OUT-пакетов интерфейса SCSI
static void HandleRx(void* data, uint16_t size)
{
  static BulkOnlyCBW request;
  static BulkOnlyCSW response;
  static uint8_t cbwBytesReceived = 0;
  static bool needReceive = false;

  if(cbwBytesReceived < sizeof(BulkOnlyCBW)) {
    CopyFromUsbPma(reinterpret_cast<uint8_t*>(&request) + cbwBytesReceived, data, size);

    cbwBytesReceived += size;

    if(cbwBytesReceived == sizeof(BulkOnlyCBW)) {
      needReceive = _lunCommandHandlers[request.Lun](request, response, [](){
        cbwBytesReceived = 0;
        _InEp::SendData(&response, sizeof(response));
      });
    } else {
      return;
    }
  } else if (needReceive) {
    needReceive = _lunRxHandlers[request.Lun](data, size);
    if(!needReceive) {
      cbwBytesReceived = 0;
      _InEp::SendData(&response, sizeof(response));
    }
  }
}

Класс Lun

Поскольку реальным хранилищем данных может быть все, что угодно (RAM, Flash, EEPROM, sd-карта), то код доступа непосредственно к хранилищу унифицировать невозможно. Итогом размышлений об архитектуре соответствующих классов стало такое решение: главный шаблонный класс, реализующий обработку команд и формирующий блок CSW, который в качестве шаблонного аргумента принимает тип, реализующий четыре метода (в дальнейшем список можно расширить):

  • GetLbaSize - возвращает размер блока LBA;

  • GetLbaCount- возвращает количество блоков LBA;

  • Read10Handler - обработчик команды READ (10);

  • Write10Handler - обработчик команды WRITE (10).

Класс ScsiLun, который расширяется переданными пользователем типами Luns содержит, по сути, главный обработчик команд. Его код представлен под спойлером. Магические массивы взял из статьи @COKPOWEHEU.

Базовый (хотя технически наоборот, это эмуляция виртуальности) класс ScsiLun
template<typename _LunSpecialization>
class ScsiLun : public ScsiLunBase
{
public:
  template<typename _InEp>
  static bool CommandHandler(const BulkOnlyCBW& cbw, BulkOnlyCSW& csw, InTransferCallback callback)
  {
    csw.Tag = cbw.Tag;
    csw.Status = BulkOnlyCSW::CswStatus::Passed;
    csw.DataResidue = 0;

    switch (static_cast<ScsiCommand>(cbw.CommandBlock[0]))
    {
    case ScsiCommand::Inquiry:
      if(cbw.CommandBlock[1] & 0x01) {
        _InEp::SendData(inquiry_page00_data, cbw.DataLength < sizeof(inquiry_page00_data) ? cbw.DataLength : sizeof(inquiry_page00_data), callback);
      } else {
        _InEp::SendData(inquiry_response, cbw.DataLength < sizeof(inquiry_response) ? cbw.DataLength : sizeof(inquiry_response), callback);
      }
      break;
    case ScsiCommand::MmcReadFormatCapacity: {
      constexpr uint8_t buffer[] = { 0, 0, 0, 8,
        (_LunSpecialization::GetLbaCount() >> 24) & 0xff,
        (_LunSpecialization::GetLbaCount() >> 16) & 0xff,
        (_LunSpecialization::GetLbaCount() >> 8) & 0xff,
        (_LunSpecialization::GetLbaCount() >> 0) & 0xff,

        0b10, // formatted media
        (_LunSpecialization::GetLbaSize() >> 16) & 0xff,
        (_LunSpecialization::GetLbaSize() >> 8) & 0xff,
        (_LunSpecialization::GetLbaSize() >> 0) & 0xff,
      };
      _InEp::SendData(buffer, sizeof(buffer), callback);
      break;
    }
    case ScsiCommand::ReadCapacity: {
      uint32_t buffer[] = { ConvertLeBe(_LunSpecialization::GetLbaCount() - 1), ConvertLeBe(_LunSpecialization::GetLbaSize()) };

      _InEp::SendData(buffer, sizeof(buffer), callback);
      break;
    }
    case ScsiCommand::ModeSense6: {
      uint8_t buffer[] = {3, 0, 0, 0};
      csw.DataResidue = cbw.DataLength - sizeof(buffer);
      _InEp::SendData(buffer, sizeof(buffer), callback);
      break;
    }
    case ScsiCommand::TestUnitReady : {
      callback();
      break;
    }
    case ScsiCommand::Read10: {
      uint32_t startLba = ConvertLeBe(reinterpret_cast<const ScsiReadWrite10Request*>(&cbw.CommandBlock[0])->BlockAddress);
      uint32_t lbaCount = ConvertLeBe(reinterpret_cast<const ScsiReadWrite10Request*>(&cbw.CommandBlock[0])->Length);

      _LunSpecialization::template Read10Handler<_InEp>(startLba, lbaCount, callback);
      break;
    }
    case ScsiCommand::Write10: {
      uint32_t startLba = ConvertLeBe(reinterpret_cast<const ScsiReadWrite10Request*>(&cbw.CommandBlock[0])->BlockAddress);
      uint32_t lbaCount = ConvertLeBe(reinterpret_cast<const ScsiReadWrite10Request*>(&cbw.CommandBlock[0])->Length);

      return _LunSpecialization::Write10Handler(startLba, lbaCount);
    }
    case ScsiCommand::MmcStartStopUnit:
    case ScsiCommand::MmcPreventAllowRemoval:
      callback();
    default:
      break;
    }

    return false;
  }
};

В итоге пользователю остается предоставить только размер хранилища (размер блока + их количество), а также обеспечить поддержку непосредственно чтения и записи.

Простейшее устройство класса Mass Storage

Для проверки работоспособности кода внедрен наиболее примитивный вариант Logical Unit с хранилищем в оперативной памяти. Ниже представлены определения методов - обработчиков команд READ (10) и WRITE (10), а также обработчик принятия пакета данных OUT, они примитивны.

Обработчики команд READ/WRITE и RxHandler
template<typename _InEp>
static void Read10Handler(uint32_t startLba, uint32_t lbaCount, InTransferCallback callback)
{
  _InEp::SendData(&_buffer[startLba * _LbaSize], lbaCount * _LbaSize, callback);
}
static bool Write10Handler(uint32_t startLba, uint32_t lbaCount)
{
  _rxAddress = startLba * _LbaSize;
  _rxBytesRemain = lbaCount * _LbaSize;
  
  return lbaCount > 0;
}
static bool RxHandler(void* data, uint16_t size)
{
  CopyFromUsbPma(&_buffer[_rxAddress], data, size);

  _rxAddress += size;
  _rxBytesRemain -= size;

  return _rxBytesRemain > 0;
}

Определение и форматирование устройства

Экспериментировал на контроллере Stm32f401ccu6, который предлагает 64Кб RAM, чего, в принципе, хватает для носителя с файловой системой FAT 12.

При подключении устройства к компьютеру Windows оповещает, что носитель не отформатирован и предлагает это сделать (см. рисунок 5), однако для FAT 16 места недостаточно и можно воспользоваться утилитой mkdosfs, как показано на рисунке 6.

Рисунок 5. Подключение носителя.
Рисунок 5. Подключение носителя.
Рисунок 6. Форматирование с помощью mkdosfs.
Рисунок 6. Форматирование с помощью mkdosfs.

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

Рисунок 7. Свойства носителя.
Рисунок 7. Свойства носителя.

Замеры скорости

Для измерения скорости можно объявить LUN объема гораздо большего, чем есть на самом деле памяти, а все старшие LBA переадресовать на последний реально доступный. Таким образом для операционной системы действительно доступно начало носителя (как раз для служебных данных файловой системы), а операции чтения и записи больших файлов будут приводить соответственно к чтению/перезаписи одного и того же участка памяти микроконтроллера. Для измерения скорости такой подход годится, так как данные действительно пересылаются по шине USB (более того, происходит реальный доступ на чтение/запись к памяти микроконтроллера).

Запись большого файла показала достаточно низкую скорость (см. рисунок 8), но этот факт я связываю с некачественной реализацией USB OTG в целом (например, для F0/F1 удалось реализовать двойную буферизацию, для OTG пока нет).

Рисунок 8. Запись крупного файла на носитель.
Рисунок 8. Запись крупного файла на носитель.

Проверить чтение обычным копированием у меня не получилось (оно выполнялось мгновенно, видимо, Windows закешировала скопированный ранее файл и реального чтения не происходило), поэтому взял первую попавшуюся программу проверки носителей, её скриншот приведен на рисунке 9. Результат чуть лучше, но все равно примерно вдвое меньше предела в 12,5 Мбит/с.

Рисунок 9. Тест на чтение.
Рисунок 9. Тест на чтение.

Заключение

Наличие шаблонного ядра USB в очередной раз позволило с минимальными усилиями добавить в библиотеку новый класс.

Стоит заметить, что SCSI оказался весьма объемным и сложным для понимания, однако все работает даже если на многие сложность закрыть глаза и возвращать "магию" (знаю, что это не есть хорошо, но радует, что такая возможность в принципе есть). Скорость записи, как и чтения, оставляют желать лучшего, но это проблема конкретной реализации USB стека для контроллеров с OTG.

В репозитории проекта можно посмотреть полный код примера и реализации классов, относящихся к MSC.

P.S. Композитное устройство

Читая тематические статьи на хабре обнаружил, что значительная часть из них посвящена разработке составных (композитных) устройств, поэтому и я решил кратко рассмотреть этот вопрос. На отдельную статью это не тянет, поэтому будет "бонусом".

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

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

Объявления класса Mass Storage
constexpr Zhele::TemplateUtils::fixed_string_16 Manufacturer(u"ZheleProduction");
constexpr Zhele::TemplateUtils::fixed_string_16 Product(u"MSDExample");
constexpr Zhele::TemplateUtils::fixed_string_16 Serial(u"88005553535");

using MscOutEpBase = OutEndpointBase<1, EndpointType::Bulk, 64, 0>;
using MscInEpBase = InEndpointWithoutZlpBase<2, EndpointType::Bulk, 64, 0>;

using EpInitializer = EndpointsInitializer<DefaultEp0, MscOutEpBase, MscInEpBase>;
using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;

using MscOutEp = EpInitializer::ExtendEndpoint<MscOutEpBase>;
using MscInEp = EpInitializer::ExtendEndpoint<MscInEpBase>;

using Lun0 = DefaultScsiLun<512, 120>;

using Scsi = ScsiBulkInterface<0, 0, Ep0, MscOutEp, MscInEp, Lun0>;

using Config = Configuration<0, 250, false, false, Scsi>;
using MyDevice = Device<0x0200, DeviceAndInterfaceClass::Storage, 0, 0, 0x0483, 0x5711, 0, Ep0, Config>;

Из примера HID-устройства возьмем объявления report-а, дескриптора, и единственной конечной точки:

Конечная точка, HID report и HID descriptor
using Report = HidReport<
        0x06, 0x00, 0xff,              // USAGE_PAGE (Generic Desktop)
        0x09, 0x01,                    // USAGE (Vendor Usage 1)
        0xa1, 0x01,                    // COLLECTION (Application)
        0x85, 0x01,                    //   REPORT_ID (1)
        0x09, 0x01,                    //   USAGE (Vendor Usage 1)
        0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
        0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
        0x75, 0x08,                    //   REPORT_SIZE (8)
        0x95, 0x01,                    //   REPORT_COUNT (1)
        0xb1, 0x82,                    //   FEATURE (Data,Var,Abs,Vol)
        0x85, 0x01,                    //   REPORT_ID (1)
        0x09, 0x01,                    //   USAGE (Vendor Usage 1)
        0x91, 0x82,                    //   OUTPUT (Data,Var,Abs,Vol)
        0xc0                           // END_COLLECTION
    >;

using HidDesc = HidImpl<0x1001, Report>;

using LedsControlEpBase = OutEndpointBase<1, EndpointType::Interrupt, 4, 255>;
using EpInitializer = EndpointsInitializer<DefaultEp0, LedsControlEpBase>;

using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;
using LedsControlEp = EpInitializer::ExtendEndpoint<LedsControlEpBase>;

using Hid = HidInterface<0, 0, 0, 0, HidDesc, Ep0, LedsControlEp>;

Учитывая, что составное устройство - это всего лишь устройство с несколькими интерфейсами, произведем композицию двух примеров:

Объявления элементов составного устройства
using Report = HidReport<
        0x06, 0x00, 0xff,              // USAGE_PAGE (Generic Desktop)
        0x09, 0x01,                    // USAGE (Vendor Usage 1)
        0xa1, 0x01,                    // COLLECTION (Application)
        0x85, 0x01,                    //   REPORT_ID (1)
        0x09, 0x01,                    //   USAGE (Vendor Usage 1)
        0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
        0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
        0x75, 0x08,                    //   REPORT_SIZE (8)
        0x95, 0x01,                    //   REPORT_COUNT (1)
        0xb1, 0x82,                    //   FEATURE (Data,Var,Abs,Vol)
        0x85, 0x01,                    //   REPORT_ID (1)
        0x09, 0x01,                    //   USAGE (Vendor Usage 1)
        0x91, 0x82,                    //   OUTPUT (Data,Var,Abs,Vol)
        0xc0                           // END_COLLECTION
    >;

using HidDesc = HidImpl<0x1001, Report>;

// Конечные точки
using MscOutEpBase = OutEndpointBase<1, EndpointType::Bulk, 64, 0>;
using MscInEpBase = InEndpointWithoutZlpBase<2, EndpointType::Bulk, 64, 0>;
using LedsControlEpBase = OutEndpointBase<3, EndpointType::Interrupt, 4, 255>;

// Расширение конечных точек
using EpInitializer = EndpointsInitializer<DefaultEp0, MscOutEpBase, MscInEpBase, LedsControlEpBase>;
using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;
using MscOutEp = EpInitializer::ExtendEndpoint<MscOutEpBase>;
using MscInEp = EpInitializer::ExtendEndpoint<MscInEpBase>;
using LedsControlEp = EpInitializer::ExtendEndpoint<LedsControlEpBase>;

// Интерфейс MSC (номер 0)
using Lun0 = DefaultScsiLun<512, 120>;
using Scsi = ScsiBulkInterface<0, 0, Ep0, MscOutEp, MscInEp, Lun0>;
// Интерфейс HID (номер 1)
using Hid = HidInterface<1, 0, 0, 0, HidDesc, Ep0, LedsControlEp>;

using Config = Configuration<0, 250, false, false, Scsi, Hid>; // Просто вписываем два интерфейса
using MyDevice = Device<0x0200, DeviceAndInterfaceClass::InterfaceSpecified, 0x02, 0, 0x0483, 0x5711, 0, Ep0, Config>;

После прошивки и подключения устройства в системе появились запоминающее устройство и HID-устройство, как показано на рисунке 10.

Рисунок 10. Композитное устройство.
Рисунок 10. Композитное устройство.

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

Комментарии (10)


  1. Bobovor
    12.11.2022 22:09
    +2

    Заместо нечитаемых функций на 10 000 строк, пришло не читаемое это. А можно же просто писать одноразовый код под текущий проект и быть счастливым.


    Ответ для DSarovsky, ну я про нее и имел ввиду. Типо базовое тупейшее ООП без попыток в супер универсальность (ну или суперуниверсальность по самому минимуму вплоть до хардкода).


    1. DSarovsky Автор
      12.11.2022 22:13
      +1

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

      Ответ для Bobovor: тогда я вас не понял, честно говоря. Пока я на опыте вижу такую зависимость: стремление к суперуниверсальности приводит к усложнению кода, по-другому не получалось. И тогда, как вы и сказали, либо писать понятный одноразовый код под текущий проект (считаю это нормальной практикой, кстати, иметь набор сниппетов и их использовать просто), либо однажды нагородить и потом использовать. Как ни крути, большая часть программистов если не сама пишет НЕодноразовый код, то как минимум его использует (в виде различных библиотек).


  1. Kelbon
    12.11.2022 22:56

    Интересно что же такое Interface... Кажется это переусложнено, без потери эффективности думаю можно сделать и с меньшим количеством шаблонных аргументов, хотя бы


    1. DSarovsky Автор
      12.11.2022 23:01
      +1

      Для всех элементов из иерархии я предложил такой набор параметров: всё, что должно быть в дескрипторе + дочерние элементы (то есть для конфигурации интерфейсы, для интерфейсов — конечные точки + что-то классозависимое). Хотя буквально недавно на радиокоте случилась дискуссия с VladislavS, у него видение ровно наоборот: пользователь задает дескриптор всего устройства, а уже суперконфигуратор из него рождает конфигурации, интерфейсы, точки и прочее. Так, наверно, конкретные типы получатся более простыми (по крайней мере банальные параметры типа номера/класса/протокола отпадут). Но уже слишком много сил потрачено, поздно мне переобуваться.


  1. VelocidadAbsurda
    12.11.2022 23:12

    Зацепился параноидальным глазом: в HandleRx - возможность переполнения буфера (в качестве CBW шлём два пакета: 30 байт, затем 64, в результате в структуру размером 31 байт прилетят все 94).

    Кроме того, для mass storage принимать из bulk EP произвольные куски пока не наберётся целый CBW - противоречит стандарту. Одиночный bulk transfer заканчивается либо пакетом длины меньше MaxPacketSize данной EP, либо пакетом нулевой длины (размеры CBW и CSW нарочно выбраны не кратными никакому из возможных MaxPacketSize). Если вам, к примеру, шлют два пакета 20+11 байт - это две отдельные (в данном случае неверные) транзакции, сшивать такое в целый CBW - некорректно (корректно - игнорировать обе). То же самое касается и фазы данных - если в CBW пообещали прислать гигабайт, но завершили передачу досрочно (теми же short packet/ZLP) - обрабатываете что дали и заявляете о недостаче в CSW (то самое поле dCSWDataResidue).

    Mass Storage Reset через EP0 - важен, им, к примеру, хост может оборвать команду в фазе данных (заставить вас забросить текущий приём/передачу данных и вернуться к ожиданию нового CBW).

    Если интересно, usb.org публикует всякие command verifier tools для тестирования на соответствие стандарту, можно много чего отловить.


    1. DSarovsky Автор
      12.11.2022 23:17

      Спасибо за замечания, с первым полностью согласен, что такая проверка необходима.
      По поводу второго замечания сходу не уверен, что правильно понял: то есть нужна проверка того, что если размер меньше макс. размера конечной точки, то он обязательно должен быть последним (дополнить структуру CBW)?


      1. VelocidadAbsurda
        13.11.2022 00:29

        В состоянии ожидания CBW прямо проверяете на соответствие параметра size==sizeof(CBW), если нет - никуда ничего не копируете и сигналите STALL (см. п. 6.6.1 https://www.usb.org/sites/default/files/usbmassbulk_10.pdf), хост снимает его через Mass Storage Reset. В состоянии приёма данных смотрите, если size<MaxPacketSize (у вас он 64) - это конец фазы данных (даже если пришло меньше, чем заявлено в CBW). Если вы в фазе данных, а вам в EP0 прилетел Mass Storage Reset - "забываете" текущую команду и переходите в ожидание CBW.


        1. DSarovsky Автор
          13.11.2022 12:15

          Спасибо, я понял. Правда, проверять size==sizeof(CBW) опасно, так как точка может быть с макс. размером пакета в 8/16 байтов, это не запрещено, но, как понял, в этом случае должны прилететь N * MaxPacketSize + M (последний).
          Со сбросом тоже вроде понял, надо обнулить все аккумуляторы и состояния.


  1. COKPOWEHEU
    13.11.2022 12:58

    её скриншот приведен на рисунке 9 (0.756 МБ/с). Результат чуть лучше, но все равно примерно вдвое меньше предела в 12,5 Мбит/с.

    Да вроде все нормально, что-то близкое к теоретическому пределу вы и выжали. Максимальная частота на линиях D+, D- составляет 12 МГц, то есть сырых 12 Мб/с (откуда взялось 12.5?) или 1.5 МБ/с. Добавляем накладные расходы на служебные пакеты, добавление нулей после каждых шести единиц, задержку между пакетами, сигналы SOF и прочее. Добавляем 10%, зарезервированные под Control endpoints. Как раз 800 — 1000 кБ/с и получается. Ну, может еще сколько-то времени теряете на копирование буферов, я в вашем коде не разбирался.


    Экспериментировал на контроллере Stm32f401ccu6, который предлагает 64Кб RAM, чего, в принципе, хватает для носителя с файловой системой FAT 12.

    У вас 256кБ флешки, можно было там готовый образ ФС разместить, чтобы при подключении к компу показывалось не что-то бракованное "отформатируй меня", а нормальная флешка.


    Для той же цели можно было и виртуальную FAT собрать (получился бы у вас новый шаблон в коллекцию)


    Не удержался и проверил свою VirFat на L151 — создал на ней пять "файлов" по 24 МБ:


    # dd if=/dev/sdc of=/dev/null status=progress
    127517184 байт (128 MB, 122 MiB) скопирован, 207 s, 615 kB/s
    250112+0 записей получено
    250112+0 записей отправлено
    128057344 байт (128 MB, 122 MiB) скопирован, 207,865 s, 616 kB/s

    Еще проверил "честный" MSD с таким же отображением конца на начало, что у вас. Правда, готовый образ ФС все же использовал — не форматировать же каждый раз заново:


    # dd if=/dev/sdc of=/dev/null status=progress
    30917120 байт (31 MB, 29 MiB) скопирован, 39 s, 791 kB/s 
    61440+0 записей получено
    61440+0 записей отправлено
    31457280 байт (31 MB, 30 MiB) скопирован, 39,5894 s, 795 kB/s

    В общем-то, порядок скорости такой же, как у вас.


    1. DSarovsky Автор
      13.11.2022 13:12

      откуда взялось 12.5
      Не знаю, я почему-то всегда считал, что 12.5, сейчас заглянул в вики, действительно 12. Не совсем доволен, потому что в прошлом году, когда плотно занимался USB на F103, при поддержке EasyLy удалось из CDC выжать 8.8, но там мы, правда, просто через libusb в конкретную конечную точку чистые данные гнали, без служебных данных, так что, наверно, примерно так и выходит.

      У вас 256кБ флешки, можно было там готовый образ ФС разместить, чтобы при подключении к компу показывалось не что-то бракованное «отформатируй меня», а нормальная флешка.
      Я так и хотел сначала (собственно, по мотивам Вашей статьи), но в итоге решил сделать на RAM, было интересно именно штатными средствами попробовать поработать (включая форматирование).