- Разработка простейшей «прошивки» для ПЛИС, установленной в Redd, и отладка на примере теста памяти.
- Разработка простейшей «прошивки» для ПЛИС, установленной в Redd. Часть 2. Программный код.
- Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС.
- Разработка программ для центрального процессора Redd на примере доступа к ПЛИС.
- Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd.
- Веселая Квартусель, или как процессор докатился до такой жизни.
- Методы оптимизации кода для Redd. Часть 1: влияние кэша.
- Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин.
- Экстенсивная оптимизация кода: замена генератора тактовой частоты для повышения быстродействия системы.
- Доступ к шинам комплекса Redd, реализованным на контроллерах FTDI
- Работа с нестандартными шинами комплекса Redd
Классификация накопителей по системам команд
При работе с накопителями следует различать физический интерфейс и систему команд. В частности, накопители CD/DVD/BD и прочая оптика. Традиционно они подключаются к кабелю SATA (ранее — IDE). Но конкретно по этому проводу во время работы бегают только команды PACKET, в блоке данных которых размещаются команды, закодированные по совершенно иному принципу (скоро мы узнаем, по какому). Поэтому сейчас мы будем говорить не столько о проводах, сколько о командах, которые в них бегают. Мне известно три распространённых системы команд для работы с накопителями.
- MMC. Её понимают SD-карты. Честно говоря, для меня это самая загадочная система команд. Как их подавать, вроде, ясно, но как управлять накопителем, не вчитываясь внимательно в документ, содержащий массу графов переходов, — я вечно путаюсь. К счастью, сегодня нас это не тревожит, так как хоть мы и работаем с SD-картой, но работу с ней осуществляет контроллер STM32 в режиме «чёрного ящика».
- ATA. Исходно эти команды бегали по шине IDE, затем — по SATA. Замечательная система команд, но сегодня мы также лишь упомянем, что она существует.
- SCSI. Эта система команд используется у широкого спектра устройств. Рассмотрим её применение в накопителях. Там сегодня SCSI-команды бегают, в первую очередь, по проводам шины SAS (кстати, сейчас в моду входят даже SSD с интерфейсом SAS). Как ни странно, оптические накопители, физически подключённые к шине SATA, также работают через SCSI-команды. По шине USB при работе по стандарту Mass Storage Device, также команды идут в формате SCSI. Микроконтроллер STM32 подключён к комплексу Redd через шину USB, то есть, в нашем случае, команды проходят по следующему пути
:
От PC до контроллера, по шине USB, команды идут в формате SCSI. Контроллер перекодирует команды по правилу MMC и отправляет их по шине SDIO. Но нам предстоит писать программу для PC, поэтому от нас команды уходят именно в формате SCSI. Их готовит драйвер устройства Mass Storage Device, с которым мы общаемся через драйвер файловой системы. Можно ли к этим запросам подмешивать запросы к прочим устройствам? Давайте разбираться.
Детали системы команд SCSI
Если подходить к делу формально, то описание стандарта SCSI имеется на сайте t10.org, но будем реалистами. Никто добровольно читать его не станет. Точнее, не его, а их: там лежит целый ворох открытых документов и гора закрытых.Только крайняя необходимость заставит погрузиться в тот сложный язык, которым написан стандарт (это, кстати, касается и стандарта ATA на t13.org). Намного проще читать документацию на реальные накопители. Она написана более живым языком, а ещё из неё вырезаны гипотетические, но реально не используемые части. При подготовке статьи я наткнулся на довольно новый (2016 года) документ SCSI Commands Reference Manual фирмы Seagate (прямая ссылка www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf но, как всегда, я не знаю, сколько она проживёт). Думаю, если кто-то хочет освоить эту систему команд, ему лучше начать именно с этого документа. Помним только, что SD-ридеры реализуют ещё более мелкое подмножество команд из того описания.
Если же говорить совсем коротко, то в накопитель уходит командный блок, имеющий длину от 6 до 16 байт. К командному блоку может быть прикреплён блок данных либо от PC к накопителю, либо от накопителя к PC (стандарт SCSI допускает и двунаправленный обмен, но для Mass Storage Device через USB допускается только один блок, а значит, направление — только одно). В блоке команд первый байт — это всегда код команды. Остальные байты — её аргументы. Правила заполнения аргументов описываются исключительно деталями реализации команды.
Сначала я вставил в статью массу примеров, но потом понял, что они затрудняют чтение. Поэтому всем желающим я предлагаю сравнить поля команды READ CAPACITY (10) из таблицы 119 Сигейтовского документа и поля команды READ(10) из таблицы 97 того же документа (ссылку см. выше). Кто не нашёл никакой связи — не пугайтесь. Именно это я и хотел показать. Кроме поля «команда» в нулевом байте, назначение всех полей зависит исключительно от специфики конкретной команды. Всегда надо открывать документ и изучать назначение остальных полей в нём.
Итак:
- Для общения с накопителем следует сформировать блок команды длиной от 6 до 16 байт (зависит от формата команды, точное число указывается в документации на неё).
- Самым важным является нулевой байт блока: именно он задаёт код команды.
- Остальные байты блока не имеют чёткого назначения. Чтобы понять, как их заполнять, следует открыть документацию на конкретную команду.
- К команде может быть прикреплён блок данных, которые передаются в накопитель или из накопителя.
Собственно, всё. Мы изучили правила подачи SCSI-команд. Теперь мы можем их подавать, была бы на них документация. Но как это сделать на уровне операционной системы?
Подача SCSI команд в ОС Linux
Поиск целевого устройства
Для подачи команд следует открыть дисковое устройство. Давайте найдём его имя. Для этого мы пойдём абсолютно тем же путём, каким шли в статье про последовательные порты. Посмотрим список «файлов» в каталоге /dev (помним, что в Linux устройства также показываются в виде файлов и их список отображается той же командой ls).
Сегодня обращаем внимание на виртуальный каталог disk:
Смотрим его содержимое:
Знакомый набор вложенных каталогов! Пробуем рассмотреть каталог by-id, применив уже знакомый нам по статье про последовательные порты ключ –l команды ls:
Выделенные слова говорят сами за себя. Это накопитель, содержащий внутреннюю SDшку комплекса Redd. Отлично! Теперь мы знаем, что устройству MIR_Redd_Internal_SD соответствует устройства /dev/sdb и /dev/sdb1. То, которое без цифры, — это сам накопитель, работать мы будем именно с ним, а с цифрой — это файловая система, размещённая на вставленном в него носителе. В терминах работы с SD-картой, /dev/sdb — это ридер, а /dev/sdb1 — файловая система на вставленной в него карточке.
Функция операционной системы для подачи команд
Обычно в любой ОС все нестандартные вещи с устройствами делаются через прямые запросы к драйверу. В Linux для посылки таких запросов имеется функция ioctl(). Не исключение и наш случай. В качестве аргумента передаём запрос SG_IO, описанный в заголовочном файле sg.h. Там же описана и структура sg_io_hdr_t, содержащая параметры запроса. Полностью структуру приводить я не стану, так как далеко не все её поля подлежат заполнению. Приведу только наиболее важные из них:
typedef struct sg_io_hdr
{
int interface_id; /* [i] 'S' for SCSI generic (required) */
int dxfer_direction; /* [i] data transfer direction */
unsigned char cmd_len; /* [i] SCSI command length ( <= 16 bytes) */
unsigned char mx_sb_len; /* [i] max length to write to sbp */
unsigned short int iovec_count; /* [i] 0 implies no scatter gather */
unsigned int dxfer_len; /* [i] byte count of data transfer */
void * dxferp; /* [i], [*io] points to data transfer memory
or scatter gather list */
unsigned char * cmdp; /* [i], [*i] points to command to perform */
unsigned char * sbp; /* [i], [*o] points to sense_buffer memory */
unsigned int timeout; /* [i] MAX_UINT->no timeout (unit: millisec) */
Описывать те поля, которые хорошо документированы в комментариях (interface_id, dxfer_direction, timeout) нет смысла. Статья и так разрастается.
Поле cmd_len содержит число байтов в блоке команды, а cmdp — указатель на этот блок. Без команды обойтись нельзя, поэтому число байтов должна быть не нулевым (от 6 до 16).
Данные же опциональны. Если они есть, то длина выделенного буфера задаётся в поле dxfer_len, а указатель на него — в поле dxferp. Накопитель может физически передать меньше данных, чем указан размер буфера. Направление передачи задаётся в поле dxfer_direction. Допустимые для USB Mass Storage Device значения: SG_DXFER_NONE, SG_DXFER_TO_DEV, SG_DXFER_FROM_DEV. В заголовочном файле есть ещё одно, но стандарт Mass Storage Device не позволяет реализовать это физически.
Также можно запросить возврат расширенного кода ошибки (SENSE). Что это такое, можно прочитать в Сигейтовском документе, раздел 2.4. Длина выделенного буфера указывается в поле mx_sb_len, а указатель на сам буфер — в поле sbp.
Как видим, в этой структуре заполняется всё то, о чём я говорил выше (плюс даётся возможность получить расширенные сведения об ошибке). Подробнее про работу с запросом SG_IO можно прочитать здесь: sg.danny.cz/sg/sg_io.html
Посылаем стандартную команду в накопитель
Ну что ж, мы выяснили формат команды, мы выяснили, в какое устройство её посылать, мы выяснили, какую функцию для этого вызывать. Давайте попробуем послать какую-нибудь стандартную команду в наше устройство. Пусть это будет команда получения имени накопителя. Вот так она описана в Сигейтовском документе:
Обратите внимание, что согласно SCSI-идеологии, все поля в стандартных командах заполняются в нотации Big Endian, то есть, старшим байтом вперёд. Поэтому поле с длиной буфера мы заполняем не в формате «0x80, 0x00», а наоборот – «0x00, 0x80». Но это – в стандартных командах. В нестандартных возможно всё, всегда надо сверяться с описанием. Собственно, только код команды (12h) и длину мы и должны заполнить. Страницу мы будем запрашивать нулевую, а остальные поля или зарезервированы, или устарели, или по умолчанию нулевые. Так что все их заполняем нулями.
#include
#include
#include
#include // open
#include // close
#include
#include
#include
int main()
{
printf("hello from SdAccessTest!n");
int s_fd = open("/dev/sdb", O_NONBLOCK | O_RDWR);
if (s_fd < 0)
{
printf("Cannot open filen");
return -1;
}
sg_io_hdr_t header;
memset(&header;, 0, sizeof(header));
uint8_t cmd12h[] = { 0x12,0x00,0x00,0x00,0x80,0x00};
uint8_t data[0x80];
uint8_t sense[0x80];
header.interface_id = 'S'; // Обязательно 'S'
// Команда
header.cmd_len = sizeof(cmd12h);
header.cmdp = cmd12h;
// Данные
header.dxfer_len = sizeof(data);
header.dxferp = data;
header.dxfer_direction = SG_DXFER_TO_FROM_DEV;
// Технологическая информация о результате
header.mx_sb_len = sizeof(sense);
header.sbp = sense;
//Таймаут
header.timeout = 100; // 100 мс
int res = ioctl(s_fd, SG_IO, &header;);
close(s_fd);
return 0;
}
Как такие программы запускать на удалённом устройства Redd, мы уже рассматривали в одной из предыдущих статей. Правда, запустив её первый раз, я сразу получил ошибку вызова функции open(). Оказалось, что у пользователя по умолчанию не хватает прав для открытия дисковых устройств. Какой из меня специалист по Линуксу, я много раз писал, но в сети мне удалось найти, что для решения этой беды можно изменить права доступа к устройству, подав команду:
sudo chmod 666 /dev/sdb
Однако мой начальник (а вот он — большой специалист по этой ОС) позже отметил, что решение действует до перезагрузки операционной системы. Чтобы получить права наверняка, надо добавить пользователя в группу disk.
Каким бы из этих двух путей мы ни пошли, но после того, как всё заработало, ставим точку останова на строку close(s_fd); и осматриваем результаты к моменту её достижения в среде разработки (так как программа — даже не однодневка, а значит — некогда нам тратить силы и время на вставку отображалок, если нам всё может показать среда разработки). Значение переменной res равно нулю. Значит, команда отработала без ошибок.
Что пришло в буфер? Когда я ввёл в адрес для дампа слово data, мне сказали, что не могут вычислить значение, пришлось вводить &data;. Странно, ведь data – это указатель, при отладке под Windows всё работает, но просто отмечаю этот факт, работает так: смотрим на результат, полученный так:
Всё верно, нам вернули имя и ревизию накопителя. Подробнее с форматом полученной структуры можно ознакомиться в Сигейтовском документе (раздел 3.6.2, таблица 59). Буфер sense не заполнился, но и описание IOCTL запроса говорит, что он заполняется, только когда возникла ошибка, возвращающая что-то в этом буфере. Дословно: Sense data (only used when 'status' is CHECK CONDITION or (driver_status & DRIVER_SENSE) is true).
Формат нестандартной команды для накопителя Redd Internal SD
Теперь, когда мы не только изучили сухое описание стандарта, но и попробовали всё на практике, прочувствовав, что такое блок команды, уже можно показать формат команды, при помощи которой можно вызывать нестандартные функции, «прошитые» в контроллер STM32 на плате комплекса. Код команды я выбрал из начала диапазона Vendor Specific команд. Он равен 0xC0. Традиционно, в описаниях SCSI команд, пишут C0h. Длина команды всегда равна 10 байтам. Формат команды унифицирован и представлен в таблице ниже.
Байт | Назначение |
0 | Код команды C0h |
1 | Код подкоманды |
2 | Аргумент arg1. Задаётся в нотации Little Endian (младшим байтом вперёд) |
3 | |
4 | |
5 | |
6 | Аргумент arg2. Задаётся в нотации Little Endian (младшим байтом вперёд) |
7 | |
8 | |
9 |
Как видно, аргументы задаются в нотации Little Endian. Это позволит описать команду в виде структуры и обращаться к её полям напрямую, не прибегая к функции перестановки байт. Проблем выравнивания (двойные слова в структуре имеют смещения, не кратные четырём) на архитектурах x86 и x64 не стоит.
Коды подкоманд описаны следующим перечислением:
enum vendorSubCommands
{
subCmdSdEnable = 0, // 00 Switch SD card to PC or Outside
subCmdSdPower, // 01 Switch Power of SD card On/Off
subCmdSdReinit, // 02 Reinitialize SD card (for example, after Power Cycle)
subCmdSpiFlashEnable, // 03 Switch SPI Flash to PC or Outside
subCmdSpiFlashWritePage, // 04 Write Page to SPI Flash
subCmdSpiFlashReadPage, // 05 Read Page from SPI Flash
subCmdSpiFlashErasePage,// 06 Erase Pages on SPI Flash (4K block)
subCmdRelaysOn, // 07 Switch relays On by mask
subCmdRelaysOff, // 08 Switch relays off by mask
subCmdRelaysSet, // 09 Set state of all relays by data
subCmdFT4222_1_Reset, // 0A Activate Reset State or switch chip to normal mode
subCmdFT4222_2_Reset, // 0B Activate Reset State or switch chip to normal mode
subCmdFT4222_3_Reset, // 0C Activate Reset State or switch chip to normal mode
subCmdFT4232_Reset, // 0D Activate Reset State or switch chip to normal mode
subCmdFT2232_Reset, // 0E Activate Reset State or switch chip to normal mode
subCmdMAX3421_Reset, // 0F Activate Reset State or switch chip to normal mode
subCmdFT4222_1_Cfg, // 10 Write to CFG pins of FT4222_1
subCmdFT4222_2_Cfg, // 11 Write to CFG pins of FT4222_2
subCmdFT4222_3_Cfg, // 12 Write to CFG pins of FT4222_3
};
Их можно разбить на группы.
Переключение устройств на внутренний и внешний режимы
Команды subCmdSdEnable и subCmdSpiFlashEnable коммутируют SD-карту и SPI-флэш соответственно. В параметре arg1 передаётся одно из следующих значений:
enum enableMode
{
enableModeToPC = 0,
enableModeOutside
};
По умолчанию, оба устройства подключены к PC.
Коммутация питания
Протокол SDIO требует достаточно больших манипуляций при инициализации. Иногда бывает полезно сбросить SD-карту к начальному состоянию (например, при переключении её линий на внешний разъём). Для этого надо отключить, затем — включить её питание. Это можно сделать при помощи команды subCmdSdPower. В аргументе arg1 передаётся одно из следующих значений: 0 — выключение питания, 1 — включение. Не забудьте дать время на разрядку конденсаторов на линии питания.
После включения питания, карту, если она подключена к PC, следует переинициализировать. Для этого используйте команду subCmdSdReinit (у неё нет аргументов).
Работа с SPI флэшкой
Если SD-карта подключается к системе как полноценный накопитель, микросхеме SPI Flash доступ в текущей версии сделан достаточно ограничено. Можно обращаться только к отдельным её страницам (256 байт) и только по одной. Объём памяти в микросхеме таков, что даже при работе по странице всё равно много времени процесс не займёт, но зато такой подход существенно упрощает «прошивку» микроконтроллера.
Команда subCmdSpiFlashReadPage считывает страницу. Адрес задаётся в параметре arg1, число страниц на передачу — в параметре arg2. Но в текущей версии число страниц должно быть равно единице. Команда вернёт 256 байт данных.
Зеркальная для неё — команда subCmdSpiFlashWritePage. Аргументы для неё заполняются по тому же принципу. Направление передачи данных — к устройству.
Особенностью флэш-памяти является то, что при записи можно заменять только единичные биты на нулевые. Чтобы вернуть их в единичное значение, страницы следует стереть. Для этого имеется команда subCmdSpiFlashErasePage. Правда, из-за особенностей применённой микросхемы, стирается не одиночная страница, адрес которой задан в параметре arg1, а блок размером 4 килобайта, содержащий её.
Управление твердотельными реле
В комплексе установлено шесть твердотельных реле. Для управления ими имеется три команды.
subCmdRelaysSet — устанавливает значение всех шести реле одновременно. В параметре arg1 передаётся значение, каждый бит которого соответствует своему реле (нулевой бит — реле с индексом 0, первый — с индексом 1 и т. д.). Единичное значение бита приводит к замыканию реле, нулевое — к размыканию.
Такой метод работы хорош, когда все реле работают, как единая группа. Если же они работают независимо друг от друга, при таком подходе приходится заводить буферную переменную, которая хранит значение состояний всех реле. Если же разными реле управляют разные программы, проблема хранения сводного значения становится крайне острой. В этом случае можно воспользоваться двумя другими командами:
subCmdRelaysOn — включает выбранные реле по маске. Те реле, которым соответствуют единичные биты в аргументе arg1, будут включены. Реле же, которым соответствуют нули в маске, сохранят своё текущее состояние.
Зеркальная ей команда subCmdRelaysOff выключит выбранные реле по маске. Те реле, которым соответствуют единичные биты в аргументе arg1, будут выключены. Реле же, которым соответствуют нули в маске, сохранят своё текущее состояние.
Сброс контроллеров FTDI и Maxim
Для подачи сигналов сброса на микросхемы FTDI и Maxim, используется группа команд subCmdFT4222_1_Reset, subCmdFT4222_2_Reset, subCmdFT4222_3_Reset, subCmdFT4232_Reset, subCmdFT2232_Reset и subCmdMAX3421_Reset. Из их имён видно, сигналами сброса каких микросхем они управляют. Мостов FT4222, как мы рассматривали ранее, в схеме два (их индексы 1 и 2), ещё один мост FT4222 передаёт данные в микросхему MAX3421, которую мы будем рассматривать в следующей статье.
В параметре arg1 передаётся одно из следующих значений:
enum ResetState
{
resetStateActive =0,
resetStateNormalOperation
};
По умолчанию, все мосты находятся в нормальном рабочем состоянии. Как уже отмечалось в прошлой статье, мы пока сами не уверены, понадобится ли эта функциональность, но когда нет прямого доступа к устройству — лучше иметь возможность дистанционно сбрасывать всё и вся.
Переключение конфигурационных линий микросхем FT4222
Микросхемы FT4222 имеют четыре режима. Вряд ли кому-то понадобится режим, отличный от «00», но если вдруг понадобится — для переключения можно использовать команды subCmdFT4222_1_Cfg, subCmdFT4222_2_Cfg и subCmdFT4222_3_Cfg для первой, второй и третьей микросхем. Значение линий CFG0 и CFG1 задаётся в младших двух битах параметра arg1.
Практический опыт подачи команды в контроллер STM32
Для проверки полученного теоретического материала на практике попробуем переключить SD-карту наружу. Для этого надо подать команду subCmdSdEnable, имеющую код 0x00 с аргументом enableModeOutside, имеющим код 0x01. Прекрасно. Переписываем программу из прошлого опыта следующим образом.
#include
#include
#include
#include // open
#include // close
#include
#include
#include
int main()
{
printf("hello from SdAccessTest!n");
int s_fd = open("/dev/sdb", O_NONBLOCK | O_RDWR);
if (s_fd < 0)
{
printf("Cannot open filen");
return -1;
}
sg_io_hdr_t header;
memset(&header;, 0, sizeof(header));
uint8_t cmdSdToOutside[] = { 0xC0,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00 };
uint8_t cmdSdToPC[] = { 0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 };
uint8_t sense[32];
memset(sense, 0, sizeof(sense));
header.interface_id = 'S'; // Обязательно 'S'
// Команда
header.cmd_len = sizeof(cmdSdToOutside);
header.cmdp = cmdSdToOutside;
// Данные (их нет)
header.dxfer_len = 0;
header.dxferp = 0;
header.dxfer_direction = SG_DXFER_NONE;
// Технологическая информация о результате
header.mx_sb_len = sizeof(sense);
header.sbp = sense;
//Таймаут
header.timeout = 100; // 100 мс
int res = ioctl(s_fd, SG_IO, &header;);
// Включаем обратно
header.cmdp = cmdSdToPC;
res = ioctl(s_fd, SG_IO, &header;);
close(s_fd);
return 0;
}
Мы изменили длину команды до десяти байт и убрали блок данных. Ну, и записали код команды с аргументами, согласно требованиям. В остальном, всё осталось то же самое. Запускаем… И… Ничего не работает. Функция ioctl() возвращает ошибку. Причина описана в документе на команду SG_IO. Дело в том, что мы подаём Vendor Specific команду C0h, а про них там сказано дословно следующее:
Any other SCSI command (opcode) not mentioned for the sg driver needs O_RDWR. Any other SCSI command (opcode) not mentioned for the block layer SG_IO ioctl needs a user with CAP_SYS_RAWIO capability.
Как объяснил мне начальник (я просто пересказываю его слова), значения capabilities назначаются на исполняемый файл. По этой причине трассировать из среды разработки мне пришлось, заходя под пользователем root. Не самое лучшее решение, но хоть что-то. На самом деле, в Windows запрос IOCTL_SCSI_PASS_THROUGH_DIRECT тоже требует администраторских прав. Возможно, в комментариях кто-то даст совет, как решить вопрос с трассировкой без таких радикальных шагов, но уже написанную программу можно запускать и не от имени root, если прописать ей правильные capabilities. А пока — меняем имя пользователя в среде разработки и ставим точку останова на строку:
int res = ioctl(s_fd, SG_IO, &header;);
и перед вызовом функции ioctl() смотрим перечень запоминающих устройств:
Вызываем ioctl() и смотрим перечень ещё раз:
Устройство /dev/sdb осталось (грубо говоря, это сам считыватель SD-карт), а /dev/sdb1 – исчезло. Это устройство соответствует файловой системе на носителе. Носитель отключился от ЭВМ – его стало не видно. Продолжаем трассировку. После вызова второй функции ioctl(), снова смотрим список устройств:
SD-карта снова подключена к системе, поэтому /dev/sdb1 снова на месте. Собственно, мы научились подавать vendor specific команды и управлять устройством на базе микроконтроллера STM32 в комплексе Redd. Прочие команды оставим читателям для самостоятельного изучения. Контролировать срабатывание некоторых из них можно аналогичным образом. Если какая-то микросхема ftdi ушла в состояние сброса, из системы исчезнет соответствующее ей устройство. Срабатывание реле и управление ножками конфигурации придётся контролировать измерительными приборами. Ну, и работу с флэшкой можно проверять, записывая страницы с последующим их контрольным считыванием.
Заключение
Мы рассмотрели две больших темы, не связанные с ПЛИС в комплексе Redd. Осталась третья — работа с микросхемой MAX3421, позволяющей реализовывать устройства USB 2.0 FS. На самом деле, хосты тоже, но хостов и на материнской плате имеется предостаточно. Функциональность Устройства же позволят комплексу прикидываться USB флэшкой (чтобы передавать обновления «прошивки»), USB клавиатурой (чтобы управлять внешними блоками) и т.п. Эту тему мы рассмотрим в следующей статье.