Прошло более полутора лет с последней статьи, посвященной применению современного C++ при разработке программ для микроконтроллеров, а именно USB. За это время удалось покрыть USB OTG FS, а также еще один класс устройств - Mass Storage.
Среди изобилия способов организовать обмен данными между устройством и хостом остановился на связке SCSI поверх транспорта Bulk-Only (он же Bulk/Bulk/Bulk), так как, насколько удалось понять, эту пару можно назвать наиболее популярной, а также честно признаюсь, что шел по стопам уважаемого @COKPOWEHEU, а именно его материала USB на регистрах: bulk endpoint на примере Mass Storage.
Интерфейсы
В нашем случае устройство класса MSC должно поддерживать один интерфейс с двумя конечными точками типа Bulk. Интерфейс также должен содержать один или несколько Logical Unit, которые для операционной системы являются логическими устройствами в составе одного физического.
Таким образом в очередной раз мы получаем иерархическую структуру, схематично представленную на рисунке 1.
Как можно заметить, интерфейс и все логические устройства разделяют между собой две конечные точки, что требует обеспечить диспетчеризацию запросов от хоста на уровне самого интерфейса.
Запросы интерфейса SCSI
Общий порядок обмена приведен на рисунке 2.
Очередной этап обмена данными начинается с передачи от хоста к устройству команды, которая задается 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 с командой чтения, демонстрирующий порядок пересылки пакетов.
Интерфейс SCSI
Базовый интерфейс должен выполнять две функции:
Обработка общих команд, адресованных конкретно интерфейсу. Таких для интерфейса SCSI предусмотрено две: Bulk-Only Mass Storage Reset (значение bRequest в пакете Setup равно 255) и Get Max LUN (значение bRequest в пакете Setup равно 254). На первый из них, насколько я понял, можно особо не реагировать, на второй же нужно вернуть номер последнего LUN, Logical Unit-ы должны начинаться с нулевого и идти по порядку.
Обрабатывать OUT-пакеты от хоста и диспетчеризовать запросы по соответствующим Logical Unit-ам.
Если с первым пунктом всё понятно, то реализация второго не совсем очевидна, примерная блок-схема соответствующего метода представлена на рисунке 4 (прошу прощения за мои блок-схемы).
Рассмотрим фрагменты исходного кода соответствующего интерфейса. Класс является шаблонным и производным от базового класса 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.
После форматирования окно свойств выглядит так, как на рисунке 7, а с носителем штатными способами можно осуществлять операции записи и чтения.
Замеры скорости
Для измерения скорости можно объявить LUN объема гораздо большего, чем есть на самом деле памяти, а все старшие LBA переадресовать на последний реально доступный. Таким образом для операционной системы действительно доступно начало носителя (как раз для служебных данных файловой системы), а операции чтения и записи больших файлов будут приводить соответственно к чтению/перезаписи одного и того же участка памяти микроконтроллера. Для измерения скорости такой подход годится, так как данные действительно пересылаются по шине USB (более того, происходит реальный доступ на чтение/запись к памяти микроконтроллера).
Запись большого файла показала достаточно низкую скорость (см. рисунок 8), но этот факт я связываю с некачественной реализацией USB OTG в целом (например, для F0/F1 удалось реализовать двойную буферизацию, для OTG пока нет).
Проверить чтение обычным копированием у меня не получилось (оно выполнялось мгновенно, видимо, Windows закешировала скопированный ранее файл и реального чтения не происходило), поэтому взял первую попавшуюся программу проверки носителей, её скриншот приведен на рисунке 9. Результат чуть лучше, но все равно примерно вдвое меньше предела в 12,5 Мбит/с.
Заключение
Наличие шаблонного ядра 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)
Kelbon
12.11.2022 22:56Интересно что же такое Interface... Кажется это переусложнено, без потери эффективности думаю можно сделать и с меньшим количеством шаблонных аргументов, хотя бы
DSarovsky Автор
12.11.2022 23:01+1Для всех элементов из иерархии я предложил такой набор параметров: всё, что должно быть в дескрипторе + дочерние элементы (то есть для конфигурации интерфейсы, для интерфейсов — конечные точки + что-то классозависимое). Хотя буквально недавно на радиокоте случилась дискуссия с VladislavS, у него видение ровно наоборот: пользователь задает дескриптор всего устройства, а уже суперконфигуратор из него рождает конфигурации, интерфейсы, точки и прочее. Так, наверно, конкретные типы получатся более простыми (по крайней мере банальные параметры типа номера/класса/протокола отпадут). Но уже слишком много сил потрачено, поздно мне переобуваться.
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 для тестирования на соответствие стандарту, можно много чего отловить.
DSarovsky Автор
12.11.2022 23:17Спасибо за замечания, с первым полностью согласен, что такая проверка необходима.
По поводу второго замечания сходу не уверен, что правильно понял: то есть нужна проверка того, что если размер меньше макс. размера конечной точки, то он обязательно должен быть последним (дополнить структуру CBW)?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.
DSarovsky Автор
13.11.2022 12:15Спасибо, я понял. Правда, проверять size==sizeof(CBW) опасно, так как точка может быть с макс. размером пакета в 8/16 байтов, это не запрещено, но, как понял, в этом случае должны прилететь N * MaxPacketSize + M (последний).
Со сбросом тоже вроде понял, надо обнулить все аккумуляторы и состояния.
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
В общем-то, порядок скорости такой же, как у вас.
DSarovsky Автор
13.11.2022 13:12откуда взялось 12.5
Не знаю, я почему-то всегда считал, что 12.5, сейчас заглянул в вики, действительно 12. Не совсем доволен, потому что в прошлом году, когда плотно занимался USB на F103, при поддержке EasyLy удалось из CDC выжать 8.8, но там мы, правда, просто через libusb в конкретную конечную точку чистые данные гнали, без служебных данных, так что, наверно, примерно так и выходит.У вас 256кБ флешки, можно было там готовый образ ФС разместить, чтобы при подключении к компу показывалось не что-то бракованное «отформатируй меня», а нормальная флешка.
Я так и хотел сначала (собственно, по мотивам Вашей статьи), но в итоге решил сделать на RAM, было интересно именно штатными средствами попробовать поработать (включая форматирование).
Bobovor
Заместо нечитаемых функций на 10 000 строк, пришло не читаемое это. А можно же просто писать одноразовый код под текущий проект и быть счастливым.
Ответ для DSarovsky, ну я про нее и имел ввиду. Типо базовое тупейшее ООП без попыток в супер универсальность (ну или суперуниверсальность по самому минимуму вплоть до хардкода).
DSarovsky Автор
Я соглашусь разве что с нечитаемостью шаблонной магии, все остальное, как мне кажется, читается даже лучше за счет отсутствия лишних параметров (чего получатся добиться за счет статических элементов). А для конечного потребителя магия скрыта: объяви конечные точки, положи их в интерфейсы, интерфейсы положи в конфигурации, конфигурации в устройство и все заработает.
Ответ для Bobovor: тогда я вас не понял, честно говоря. Пока я на опыте вижу такую зависимость: стремление к суперуниверсальности приводит к усложнению кода, по-другому не получалось. И тогда, как вы и сказали, либо писать понятный одноразовый код под текущий проект (считаю это нормальной практикой, кстати, иметь набор сниппетов и их использовать просто), либо однажды нагородить и потом использовать. Как ни крути, большая часть программистов если не сама пишет НЕодноразовый код, то как минимум его использует (в виде различных библиотек).