198ч. Мини-проект: USB-хост на B-U585I-IOT02A — клавиатура и флешка
Раздел 20а показал хост USB со стороны контроллера: роли OTG, опознание,
каналы. Раздел 20в разобрал отчёт клавиатуры со стороны устройства. В 198ф
флешкой притворялся сам микроконтроллер. Здесь всё наоборот: прибор сам
читает чужие устройства. Это складской терминал на плате B-U585I-IOT02A: к
разъёму USB-C подключают сканер штрихкодов (для хоста это обычная
клавиатура) или флешку с файлом настроек CONFIG.TXT. Сканер даёт код
товара, флешка — название прибора и список товаров.
Основа — два примера из пакета платы B-U585I-IOT02A_BSP 1.1.0 (Keil):
Projects/Middleware/USB/Host/Keyboard и .../MassStorage. Оба построены
на стеке USB Host из MDK-Middleware и печатают нажатые клавиши или пишут
файл на флешку. Мы берём из них настройку хоста и питание шины, а свои
части пишем сами: перевод отчётов в символы, автоповтор, строку сканера,
разбор MBR и FAT16 и загрузку настроек «всё или ничего».
Проверено без платы. Свой код собран gcc на компьютере и прогнан против подготовленных данных: потоков отчётов клавиатуры и четырёх образов флешки, которые собраны FatFs и своим сборщиком и прочитаны обратно тремя независимыми путями. Прошивочная часть собрана armclang под Cortex-M33; на плате она не запускалась.
Опоры: RM0456 (STM32U5, глава 72 «OTG_FS»: 72.7.2 «USB host states»,
72.7.3 «Host channels»), даташит DS13086 (STM32U585xx, таблица 84 — HSI48),
пакет платы B-U585I-IOT02A_BSP 1.1.0: примеры USB Host (лицензия
Apache-2.0), драйвер питания b_u585i_iot02a_usbpd_pwr.c и компонент
tcpp0203 (лицензия BSD-3-Clause); исходники USB Host из MDK-Middleware
8.1.0 (usbh_lib_hid.c, usbh_lib_msc.c, usbh_lib_core.c; лицензия Arm
EULA — только пересказ и короткие цитаты); FatFs R0.11 (ChaN, из
STM32Cube_FW_F1 V1.8.7; копия в проверке вместе с лицензией); библиотека USB
Host ST (usbh_hid_keybd.c, usbh_msc_bot.h, лицензия SLA0044) — для
сверки таблиц; проверка контрольной цифры EAN-13 — по zbar.c из
github.com/openmv/openmv, коммит 6236993 (LGPL, только сверка). Тексты
спецификаций HID, Bulk-Only и FAT здесь не цитируются: форматы взяты из этих
исходников. Лицензия: свой код раздела — свободно, FatFs — условия ChaN в
копии файлов.
Как это называется в документации
Оригинал Перевод CBW (Command Block Wrapper) обёртка команды, 31 байт CSW (Command Status Wrapper) обёртка ответа, 13 байт dCSWDataResidue остаток: сколько байт устройство недодало READ(10), READ CAPACITY(10), TEST UNIT READY команды SCSI: чтение блоков, объём, готовность ErrorRollOver «переполнение»: нажато больше клавиш, чем влезает в отчёт typematic, auto-repeat автоповтор зажатой клавиши keypad цифровой блок клавиатуры EAN-13 check digit контрольная цифра штрихкода EAN-13 MBR (master boot record) главная загрузочная запись: таблица разделов в секторе 0 BPB (BIOS parameter block) таблица параметров в загрузочном секторе тома superfloppy носитель без таблицы разделов: том начинается с сектора 0 GDP, GDC (TCPP03) ключи TCPP03: отдать 5 В наружу, принять 5 В снаружи
198ч.1 Что за прибор и из чего он
Терминал работает так:
- Плата даёт 5 В на разъём USB-C и ждёт устройство.
- Подключили сканер — каждый его отчёт превращается в символы; цифры до Enter — это код. Код проверяется по длине и контрольной цифре и ищется в списке товаров.
- Подключили флешку — прибор читает
CONFIG.TXTиз корня и, если файл цел, заменяет настройки целиком. Иначе остаются прежние.
Что лежит в примерах пакета. Keyboard.c запускает хост
USBH_Initialize(0U) и раз в 10 мс (osDelay(10U)) спрашивает у
библиотеки нажатую клавишу USBH_HID_GetKeyboardKey. MassStorage.c раз
в 100 мс проверяет USBH_MSC_DriveGetMediaStatus("U0:"), монтирует диск
файловой системой MDK и пишет README.txt. Описание примеров: процессор
160 МГц, USB 48 МГц, OTG_FS на разъёме CN1 USB Type-C.
Настройка хоста в USBH_Config_0.c у обоих примеров одна: бюджет питания
USBH0_HC_POWER = 500 мА, не больше 3 каналов сразу
(USBH0_HC_PIPE_NUM). У самого OTG_FS каналов 12 (RM0456, 72.7.3; так же
Host_channels = 12 в коде CubeMX примера). Опознание и каналы — как в
20а.3 и 20а.4, повторять их не будем.
Наша прошивка заменяет Keyboard.c файлом u585_host.c, а main.c,
питание шины и настройку хоста берёт из примера. Стек MDK отдаёт нам
блоки флешки напрямую (USBH_MSC_Read), без файловой системы MDK: FAT
читает свой fat.c.
198ч.2 Питание шины: кто даёт 5 В и как ограничен ток
Сам STM32U585 5 В не делает: «On-chip 5 V VBUS generation is not
supported» (RM0456, 72.7.2). Нужен внешний источник или ключ. На этой плате
VBUS разъёма коммутирует TCPP03 — микросхема защиты порта USB-C. Драйвер
b_u585i_iot02a_usbpd_pwr.c говорит с ней по I2C2, адрес
TCPP0203_I2C_ADDRESS_X68 (0x68 — восьмибитная запись, в комментарии
пакета — 0x34). Включает её вывод PB5, флаг неисправности FLGn приходит на
PE8 (подтяжка, прерывание EXTI8 по спаду).
Порядок из main.c примера и драйвера:
BSP_USBPD_PWR_Init— выводы, прерывание FLGn, PB5 в единицу, I2C2; драйвер читает тип микросхемы и отказывается работать, если это не TCPP03.BSP_USBPD_PWR_SetRole(..., POWER_ROLE_SOURCE)— мы источник. Без этогоBSP_USBPD_PWR_VBUSOnвернёт ошибку.BSP_USBPD_PWR_SetPowerMode(..., USBPD_PWR_MODE_NORMAL)иBSP_USBPD_PWR_VBUSInit— рабочий режим и замер VBUS через ADC4.- Когда стеку нужно питание, он зовёт
USBH_VbusOnOff(true)изmain.c, а тот —BSP_USBPD_PWR_VBUSOn(0): открыть ключ GDC (вход 5 В снаружи), подождать 2 мс (HAL_Delay(2)), закрыть ключ GDP (выход 5 В наружу). - Выключение
BSP_USBPD_PWR_VBUSOff: открыть GDP, включить разряд VBUS и ждать, пока напряжение не упадёт ниже 650 мВ (порог 750 − 100), не больше 20 раз по 20 мс, затем ещё 10 мс и выключить разряд.
Ток ограничен на двух уровнях.
- Бюджет в стеке. При опознании MDK сравнивает
bMaxPowerиз дескриптора конфигурации сUSBH0_HC_POWER / 2(поле считается в единицах по 2 мА). Устройство, которое просит больше 500 мА, не настраивается:USBH_NOTIFY_INSUFFICIENT_POWER,usbNotConfigured. - Перегрузка в железе. TCPP03 сам видит перегрузку по VBUS и роняет
FLGn. Обработчик читает регистр флагов; при перегрузке (
OCP VBUS) драйвер возвращает питание, только если с прошлой перегрузки прошло больше 1 с (USBPD_PWR_FAULT_MIN_TIME_RECOVERY= 1000). Вторая перегрузка быстрее — питание не возвращают.
Порог тока TCPP03 и то, откуда на её вход приходят 5 В, задаёт схема
платы MB1551; с ней раздел не сверялся, в пакете — только ссылка на неё и на
руководство UM2839. Ток драйвер не меряет: в BSP_USBPD_PWR_VBUSGetCurrent
стоит adc_value = 0U; /* Not yet implemented */.
Напряжение меряет: VBUS через делитель 330 и 47 на PF14 (ADC4_IN5). При
5,000 В на входе АЦП приходит 623 мВ — 773,5 единицы кода при опоре
3300 мВ. Формула драйвера считает в целых числах: из кода 773 она вернёт
4989 мВ, из 774 — 4997 мВ. Порог «VBUS в норме» у
OTG_FS — 4,4 В (RM0456, 72.7.2), но он работает, только если включены HNP
или SRP; в примере vbus_sensing_enable = DISABLE.
198ч.3 Клавиатура: отчёт в символы
Сканер штрихкодов для хоста — загрузочная клавиатура: интерфейс HID с протоколом 1 и отчёт 8 байт — модификаторы, резерв и шесть кодов нажатых клавиш (20в.4). Код — это назначение страницы клавиатуры: A — 0x04, 1 — 0x1E, Enter — 0x28. Сам символ зависит от Shift.
Что делает библиотека MDK. При подключении она шлёт SET_IDLE с нулём —
«присылать отчёт только при изменении» (USBH_HID_SetIdle (instance, 0U,
0U, 0U)). Запрос SET_PROTOCOL в usbh_lib_hid.c 8.1.0 закрыт #if 0:
загрузочный протокол библиотека не ставит, а полагается на то, что
клавиатура и так шлёт 8 байт (или 9 с номером отчёта — первый байт она
тогда отбрасывает). Библиотека ST поступает наоборот и протокол ставит
(20в.4).
Поток приёма копирует новый отчёт в свой буфер, только если прошлый уже
забрали: условие (len <= 9U) && (ptr_hid->hid_in_data_len == 0U).
USBH_HID_GetKeyboardKey отдаёт по одной клавише за вызов: отчёт
нажатия разбирается за два вызова, отчёт отпускания — за один. В примере
вызов раз в 10 мс — значит, не быстрее 30 мс на символ, а всё, что сканер
пришлёт между вызовами, теряется. Поэтому мы забираем каждый отчёт сразу в
потоке библиотеки — она зовёт USBH_HID_DataReceived после каждого
приёма — и кладём в очередь на 32 отчёта:
void USBH_HID_DataReceived(uint8_t instance, uint32_t len)
{
uint8_t r[8];
if (len == 8u && USBH_HID_Read(instance, r, 8) == 8)
(void)osMessageQueuePut(kq, r, 0U, 0U);
}
Перевод в символы. Таблица — раскладка США: назначения 0x04…0x38 без Shift и с Shift, плюс цифровой блок. Сканеры с настройкой «цифры с цифрового блока» шлют 0x59…0x61 и 0x62 — так их и переводит пример MDK:
char kbd_char(uint8_t usage, uint8_t mods)
{
if (usage >= 0x04u && usage <= 0x38u)
return (mods & KBD_MOD_SHIFT) ? shifted[usage - 0x04u] : plain[usage - 0x04u];
if (usage >= 0x59u && usage <= 0x61u) /* цифровой блок 1…9 */
return (char)('1' + (usage - 0x59u));
if (usage == 0x62u) return '0'; /* цифровой блок 0 */
if (usage == 0x58u) return '\r'; /* Enter цифрового блока */
return 0;
}
Правила разбора:
- символ дают только новые коды — те, которых не было в прошлом отчёте; удержание клавиши — это тот же код в следующих отчётах;
- если в отчёте есть код 01…03, отчёт пропускается целиком, прошлое
состояние не трогается. 01 — ErrorRollOver: клавиш нажато больше, чем
помещается в шесть мест, и клавиатура пишет 01 во все шесть (так их и
ловит MDK: сравнение с
"\1\1\1\1\1\1"); - Shift — любой из двух: биты 0x02 и 0x20 байта 0.
int kbd_report(kbd_t *k, const uint8_t r[8], uint32_t now, char *out, int max)
{
int n = 0;
const uint8_t *keys = &r[2];
for (int i = 0; i < 6; i++)
if (keys[i] >= KBD_ROLLOVER && keys[i] <= 0x03u) { /* 01…03 — ошибки, а не клавиши: */
k->rollovers++; /* отчёт целиком пропускаем, */
return 0; /* прошлое состояние не трогаем */
}
for (int i = 0; i < 6; i++) {
uint8_t u = keys[i];
if (u < 0x04u || was_down(k, u)) continue; /* пусто, служебный код или уже был нажат */
char c = kbd_char(u, r[0]);
if (c && n < max) out[n++] = c;
if (c && k->rep_delay) { /* повторяем последнюю нажатую */
k->rep_key = u;
k->rep_next = now + k->rep_delay;
}
}
if (k->rep_key) { /* отпущена — повтор кончился */
int held = 0;
for (int i = 0; i < 6; i++)
if (keys[i] == k->rep_key) held = 1;
if (!held) k->rep_key = 0;
}
memcpy(k->prev, keys, 6);
k->mods = r[0];
return n;
}
Прогон на компьютере. «Hello, STM32!» — по отчёту нажатия и отпускания на символ, 26 отчётов, «S» набрано правым Shift. Нахлёст — так набирают быстро: «a» ещё не отпущена, а «b» уже нажата; потом «c» и «d» пришли в одном отчёте с Shift:
= Hello, STM32!
R 00 00 04 00 00 00 00 00
R 00 00 04 05 00 00 00 00
R 00 00 05 00 00 00 00 00
R 00 00 00 00 00 00 00 00
R 02 00 06 07 00 00 00 00
R 00 00 00 00 00 00 00 00
= abCD
Переполнение: держат шесть клавиш a…f, жмут седьмую — клавиатура шлёт шесть кодов 01; седьмую отпускают — снова a…f:
R 00 00 04 05 06 07 08 09
R 00 00 01 01 01 01 01 01
R 00 00 04 05 06 07 08 09
R 00 00 00 00 00 00 00 00
= abcdef
наивный хост: abcdefabcdef
отчётов 01 пропущено: 1
Наивный хост принял 01 за обычный отчёт, «забыл» шесть клавиш и, когда они вернулись, напечатал их второй раз.
Независимая сверка: проверка разбирает те же отчёты на Python таблицами
из usbh_hid_keybd.c ST (раскладка QWERTY, со Shift) и
HID_KEYBOARD_ID_TO_ASCII MDK (без Shift) — тексты совпадают; наша
таблица совпадает с обеими на всех 53 назначениях 0x04…0x38, кроме шести:
0x28…0x2C (Enter, Esc, забой, Tab, пробел — у ST там другие коды или пусто
с Shift) и 0x32 (у ST пусто, у MDK «Non-US # and ~»).
198ч.4 Повтор, залипание и сканер
Повтор. Клавиатура после SET_IDLE с нулём сама ничего не повторяет: пока клавишу держат, отчётов нет. Автоповтор делает хост. Наш выбор (не стандарт): первый повтор через 500 мс, дальше каждые 50 мс, повторяется последняя нажатая клавиша, отпускание её останавливает:
int kbd_tick(kbd_t *k, uint32_t now, char *out, int max)
{
int n = 0;
while (k->rep_key && (int32_t)(now - k->rep_next) >= 0 && n < max) {
out[n++] = kbd_char(k->rep_key, k->mods);
k->rep_next += k->rep_period;
}
return n;
}
Залипание. Опасный случай — клавиатуру выдернули, пока клавиша нажата:
отчёта отпускания не будет никогда. Библиотека зовёт
USBH_HID_Uninitialize; мы ставим флаг, и главный цикл сбрасывает
состояние (kbd_reset), строку сканера и очередь. Модель: «x» держат,
через 0,7 с кабель выдёргивают:
повтор 500/50 мс, «x» держат 1 с: символов «x» за 3 с — 11
повтор выключен (REPEAT=OFF), «x» держат 1 с: символов «x» за 3 с — 1
клавиатуру выдернули через 0,7 с с зажатой «x», без сброса: символов «x» за 3 с — 51
клавиатуру выдернули через 0,7 с с зажатой «x», kbd_reset: символов «x» за 3 с — 5
Без сброса «x» сыплется, пока прибор работает: 51 символ за 3 с и дальше.
Со сбросом — 5: первый и четыре повтора до 0,7 с. Для сканера повтор
вообще вреден: потерянное отпускание даст «1111…». Поэтому в CONFIG.TXT
терминала — REPEAT=OFF.
Опрос. Хост спрашивает прерывающую точку раз в bInterval её
дескриптора (MDK передаёт его в USBH_PipeCreate), за опрос — один отчёт.
Код EAN-13 и Enter — 14 символов, 28 отчётов: 28 мс при bInterval = 1 и
280 мс при 10 (расчёт — как в 20в.4).
Строка сканера. Символы копятся до Enter. Дальше — длина из настроек, только цифры, контрольная цифра для 13 знаков и поиск в списке товаров:
int term_feed(term_t *t, char c, const cfg_t *cfg, const char **name)
{
*name = 0;
if (c == '\b') { /* клавиатура: забой стирает символ */
if (t->n) t->n--;
return TERM_NONE;
}
if (c != '\r') {
if (t->n < sizeof t->line - 1) t->line[t->n++] = c;
else t->overflow = 1;
return TERM_NONE;
}
t->line[t->n] = 0;
uint8_t n = t->n, of = t->overflow;
term_reset(t);
if (of || n < cfg->bc_min || n > cfg->bc_max) return TERM_BADLEN;
for (uint8_t i = 0; i < n; i++)
if (t->line[i] < '0' || t->line[i] > '9') return TERM_BADCHAR;
if (n == 13 && !ean13_ok(t->line)) return TERM_BADSUM;
for (uint8_t i = 0; i < cfg->n_sku; i++)
if (strcmp(cfg->sku[i].code, t->line) == 0) {
*name = cfg->sku[i].name;
return TERM_FOUND;
}
return TERM_UNKNOWN;
}
Контрольная цифра EAN-13: цифры с весами 1, 3, 1, 3…, контрольная дополняет сумму до десятка:
int ean13_ok(const char *d)
{
int sum = 0;
for (int i = 0; i < 13; i++)
if (d[i] < '0' || d[i] > '9') return 0;
for (int i = 0; i < 12; i++)
sum += (d[i] - '0') * ((i & 1) ? 3 : 1);
return (10 - sum % 10) % 10 == d[12] - '0';
}
Тот же вес 3 у нечётных позиций — в ean_verify_checksum из zbar.c;
проверка переносит эту функцию на Python и сверяет все коды раздела.
Прогон (список товаров — из CONFIG.TXT на образе флешки, 198ч.6):
ряд цифр: «4607001234579», отчётов 28, символов 14 -> найден NUT-M6
цифровой блок: «4650123456701», отчётов 28, символов 14 -> найден ANCHOR-M10
ряд цифр: «4610000111222», отчётов 28, символов 14 -> найден NUT-M8
потерян отчёт отпускания между «1» и «1»: «4610000111222», отчётов 28 (один потерян), символов 13 -> нет в списке
чужой товар: «4600000000008», отчётов 28, символов 14 -> нет в списке
ошибка в последней цифре: «4607001234578», отчётов 28, символов 14 -> плохая контрольная
короткий: «12345», отчётов 12, символов 6 -> плохая длина
Четвёртая строка — то, чем грозит пропуск отчётов. Между двумя «1» потерялось отпускание; хост увидел одну долгую «1», и в коде осталось 12 цифр. Прибор не ошибся товаром — он его не нашёл. Двойные цифры в кодах обычны, поэтому каждый отчёт важен, а проверки длины и контрольной цифры — последняя страховка.
198ч.5 Флешка со стороны хоста: Bulk-Only
Флешка — класс MSC с протоколом Bulk-Only (20а.4). Обмен — тройками по
массовым точкам: хост шлёт CBW (31 байт) с командой SCSI, затем идут
данные, затем устройство отвечает CSW (13 байт). Подпись CBW — «USBC»
(0x43425355), CSW — «USBS» (0x53425355); те же числа и длины 31 и 13 — в
usbh_msc_bot.h ST.
Что MDK делает после опознания флешки (USBH_MSC_Initialize_Lib):
GET MAX LUN, пять раз TEST UNIT READY (при отказе — пауза 1250 мс и
REQUEST SENSE), INQUIRY, ещё TEST UNIT READY, затем дважды READ
CAPACITY, TEST UNIT READY и пробное чтение блока 0. Пробное чтение, по
комментарию в коде, — потому что некоторые флешки проходят опознание, но
падают на первом READ(10). Только после этого зовётся наш
USBH_MSC_Initialize.
Наша сборка CBW для READ(10) и проверка CSW — те же правила, что у MDK: подпись, тег совпадает с тегом команды, остаток равен нулю:
void bot_cbw_read10(uint8_t cbw[31], uint32_t tag, uint32_t lba, uint16_t blocks)
{
memset(cbw, 0, 31);
put32le(cbw + 0, BOT_CBW_SIG);
put32le(cbw + 4, tag); /* устройство вернёт его в CSW */
put32le(cbw + 8, (uint32_t)blocks * 512u);/* сколько байт данных ждём */
cbw[12] = 0x80; /* данные: устройство -> хост */
cbw[13] = 0; /* LUN 0 */
cbw[14] = 10; /* длина команды SCSI */
cbw[15] = 0x28; /* READ(10) */
cbw[17] = (uint8_t)(lba >> 24); /* номер блока — старшим байтом вперёд */
cbw[18] = (uint8_t)(lba >> 16);
cbw[19] = (uint8_t)(lba >> 8);
cbw[20] = (uint8_t)lba;
cbw[22] = (uint8_t)(blocks >> 8);
cbw[23] = (uint8_t)blocks;
}
int bot_csw_check(const uint8_t csw[13], uint32_t tag)
{
uint32_t sig = csw[0] | csw[1] << 8 | csw[2] << 16 | (uint32_t)csw[3] << 24;
uint32_t t = csw[4] | csw[5] << 8 | csw[6] << 16 | (uint32_t)csw[7] << 24;
uint32_t res = csw[8] | csw[9] << 8 | csw[10] << 16 | (uint32_t)csw[11] << 24;
if (sig != BOT_CSW_SIG) return BOT_ESIG;
if (t != tag) return BOT_ETAG; /* ответ не на нашу команду */
if (res != 0) return BOT_ERESIDUE; /* устройство отдало не всё */
return csw[12] == 0 ? BOT_OK : (csw[12] == 1 ? BOT_FAILED : BOT_PHASE);
}
На плате эту работу делает MDK; bot.c работает в модели, где флешку
изображает модель устройства над образом. Второе чтение a.img — загрузочный
сектор тома, блок 63:
CBW: 55 53 42 43 02 00 00 00 00 02 00 00 80 00 0A 28 00 00 00 00 3F 00 00 01 00 00 00 00 00 00 00
CSW: 55 53 42 53 02 00 00 00 00 00 00 00 00
Проверка разбирает эти байты на Python сама:
CBW 31 байт: подпись 43425355, тег 2, данных 512, флаги 80, LUN 0, команда 10 байт: 0x28, блок 63, блоков 1
CSW 13 байт: подпись 53425355, тег 2, остаток 0, состояние 0
Номер блока в команде — старшим байтом вперёд (00 00 00 3F), а поля
обёртки — младшим (00 02 00 00 — это 512).
Две детали MDK, которые влияют на прибор. Длина данных в READ(10) MDK
всегда считает как block_num << 9, то есть блоки по 512 байт. Поэтому
load_config сначала спрашивает объём и отказывается от флешки с другим
размером блока. И USBH_MSC_Read делает до трёх попыток с паузой 1 с
(USBH_Delay(1000U)); если флешку вынули, выходит после первой паузы с
usbDeviceError (о третьей ветке — в 198ч.7).
static int msc_rd(void *ctx, uint32_t lba, uint8_t *buf)
{
(void)ctx;
return USBH_MSC_Read(0U, lba, 1U, buf) == usbOK ? 0 : -1;
}
static void load_config(void)
{
uint32_t blocks, bsize, bytes;
if (USBH_MSC_ReadCapacity(0U, &blocks, &bsize) != usbOK || bsize != 512u) {
last_load = FAT_ESECT;
return;
}
last_load = config_load(msc_rd, NULL, &cfg, &bytes);
if (last_load == CFG_OK)
kbd_init(&kbd, cfg.repeat ? REP_DELAY_MS : 0u, REP_PERIOD_MS);
}
198ч.6 MBR, FAT16 и CONFIG.TXT по шагам
Устройство флешки на диске (раздел 42 описывает FAT через FatFs; здесь читаем руками, чтобы видеть каждый сектор):
- Сектор 0. В конце подпись 55 AA. Если сектор начинается с перехода EB или E9 и его BPB разумен (сектор 512 байт, кластер — степень двойки), это сам загрузочный сектор тома: флешка без таблицы разделов. Иначе это MBR: четыре записи по 16 байт с 446-го, у записи — тип (байт 4) и первый сектор раздела (байты 8…11).
- Загрузочный сектор тома. Из BPB: байт в секторе (11), секторов в кластере (13), резерв (14), копий FAT (16), записей корня (17), секторов всего (19 или 32), секторов в одной FAT (22; у FAT32 там 0).
- Тип FAT — по числу кластеров, а не по типу раздела и не по надписи
«FAT16» в секторе: меньше 4086 — FAT12, больше 65525 — FAT32, между
ними — FAT16. Пороги — из FatFs R0.11 (
MIN_FAT16= 4086,MIN_FAT32= 65526), по ним FatFs и размечает, и монтирует том. - Корень — сразу за всеми копиями FAT. Записи по 32 байта: 0x00 —
конец, 0xE5 — удалённый файл, атрибут 0x0F — кусок длинного имени,
0x08 — метка тома, 0x10 — каталог. Имя ищем в виде 8.3 —
"CONFIG TXT"; первый кластер — байты 26…27, размер — 28…31. - Цепочка. Кластер N лежит с сектора
данные + (N − 2) × секторов в кластере; следующий кластер — слово N в FAT; 0xFFF8 и выше — конец.
int fat16_mount(fat16_t *f, blk_read_fn rd, void *ctx)
{
uint8_t *s = f->buf;
f->rd = rd;
f->ctx = ctx;
f->fat_cached = 0xFFFFFFFFu;
if (rd(ctx, 0, s)) return FAT_EIO;
if (s[510] != 0x55 || s[511] != 0xAA) return FAT_ESIG;
f->part_lba = 0;
if (!is_vbr(s)) { /* сектор 0 — MBR: ищем раздел FAT */
int found = 0;
for (int i = 0; i < 4 && !found; i++) {
const uint8_t *e = s + 446 + 16 * i;
uint8_t t = e[4];
if (t == 0x01 || t == 0x04 || t == 0x06 || t == 0x0E || t == 0x0B || t == 0x0C) {
f->part_lba = le32(e + 8);
found = 1;
}
}
if (!found) return FAT_ENOVOL;
if (rd(ctx, f->part_lba, s)) return FAT_EIO;
if (s[510] != 0x55 || s[511] != 0xAA) return FAT_ESIG;
if (le16(s + 11) != 512) return FAT_ESECT;
if (!is_vbr(s)) return FAT_ENOVOL;
}
uint16_t rsvd = le16(s + 14);
uint16_t fatsz = le16(s + 22); /* у FAT32 здесь 0 */
uint32_t tot = le16(s + 19) ? le16(s + 19) : le32(s + 32);
f->spc = s[13];
f->n_fats = s[16];
f->root_ents = le16(s + 17);
if (fatsz == 0 || f->root_ents == 0) return FAT_ETYPE;
uint32_t root_secs = (f->root_ents * 32u + 511u) / 512u;
f->fat_lba = f->part_lba + rsvd;
f->root_lba = f->fat_lba + (uint32_t)f->n_fats * fatsz;
f->data_lba = f->root_lba + root_secs;
uint32_t meta = rsvd + (uint32_t)f->n_fats * fatsz + root_secs;
if (tot <= meta) return FAT_ENOVOL;
f->n_clst = (tot - meta) / f->spc;
if (f->n_clst < 4086u || f->n_clst > 65525u) return FAT_ETYPE; /* тип — по числу кластеров */
return FAT_OK;
}
int fat16_find(fat16_t *f, const char name83[11], uint16_t *clus, uint32_t *size)
{
uint32_t root_secs = (f->root_ents * 32u + 511u) / 512u;
for (uint32_t sec = 0; sec < root_secs; sec++) {
if (f->rd(f->ctx, f->root_lba + sec, f->buf)) return FAT_EIO;
for (int i = 0; i < 16; i++) {
const uint8_t *e = f->buf + 32 * i;
uint8_t attr = e[11];
if (e[0] == 0x00) return FAT_ENOFILE; /* дальше записей нет */
if (e[0] == 0xE5) continue; /* удалённый файл */
if (attr == 0x0F) continue; /* кусок длинного имени */
if (attr & 0x18) continue; /* метка тома или каталог */
if (memcmp(e, name83, 11) == 0) {
*clus = le16(e + 26);
*size = le32(e + 28);
return FAT_OK;
}
}
}
return FAT_ENOFILE;
}
static int fat_next(fat16_t *f, uint16_t c, uint16_t *next)
{
uint32_t lba = f->fat_lba + (c * 2u) / 512u;
if (lba != f->fat_cached) {
if (f->rd(f->ctx, lba, f->fat_buf)) return FAT_EIO;
f->fat_cached = lba;
}
*next = le16(f->fat_buf + (c * 2u) % 512u);
return FAT_OK;
}
int fat16_read(fat16_t *f, uint16_t clus, uint32_t size, uint8_t *dst, uint32_t max)
{
uint32_t done = 0, steps = 0;
if (size > max) return FAT_EBIG;
while (done < size) {
if (clus < 2 || clus >= f->n_clst + 2u || ++steps > f->n_clst) return FAT_ECHAIN;
uint32_t lba = f->data_lba + (uint32_t)(clus - 2u) * f->spc;
for (uint8_t s = 0; s < f->spc && done < size; s++) {
if (f->rd(f->ctx, lba + s, f->buf)) return FAT_EIO;
uint32_t n = size - done < 512u ? size - done : 512u;
memcpy(dst + done, f->buf, n);
done += n;
}
if (done < size) {
int r = fat_next(f, clus, &clus);
if (r) return r;
if (clus >= 0xFFF8u) return FAT_ECHAIN; /* цепочка кончилась раньше файла */
}
}
return FAT_OK;
}
Защита от битой флешки: номер кластера вне тома, цепочка длиннее числа
кластеров или конец цепочки раньше размера файла — ошибка FAT_ECHAIN, а
не чтение чужих секторов.
Сам файл — строки КЛЮЧ=ЗНАЧЕНИЕ, # — комментарий, последней
обязательно END:
# CONFIG.TXT - scanner terminal settings
# KEY=VALUE per line, '#' starts a comment, END must be last
DEVICE=SKLAD-07
BARCODE_MIN=8
BARCODE_MAX=13
REPEAT=OFF
# item list: SKU=<EAN-13> <name>
SKU=4601234567893 BOLT-M6X20
SKU=4607001234562 BOLT-M8X30
…и ещё 16 строк SKU=, затем END; всего 733 байта. Разбор строгий:
незнакомый ключ, неверная контрольная цифра в SKU или оборванная
последняя строка — отказ всего файла. Настройки собираются в отдельной
структуре и заменяют рабочие одним присваиванием:
int cfg_parse(const char *t, uint32_t len, cfg_t *out)
{
char line[64];
uint32_t i = 0;
int no = 0;
cfg_default(out);
while (i < len) {
uint32_t n = 0;
no++;
while (i < len && t[i] != '\n') {
if (t[i] != '\r') {
if (n >= sizeof line - 1) return no; /* слишком длинная строка */
line[n++] = t[i];
}
i++;
}
int ended = i < len; /* строка дошла до '\n' */
i++;
line[n] = 0;
if (!ended && strcmp(line, "END") != 0)
return CFG_ENOEND; /* последняя строка оборвана */
int r = cfg_line(line, out);
if (r < 0) return no;
if (r == 1) return out->bc_min <= out->bc_max ? CFG_OK : no;
}
return CFG_ENOEND; /* файл оборван: END не дошёл */
}
int config_load(blk_read_fn rd, void *ctx, cfg_t *active, uint32_t *bytes)
{
static fat16_t fs;
static uint8_t text[CFG_FILE_MAX];
static cfg_t fresh;
uint16_t clus;
uint32_t size;
int r = fat16_mount(&fs, rd, ctx);
if (r == FAT_OK) r = fat16_find(&fs, "CONFIG TXT", &clus, &size);
if (r == FAT_OK) r = fat16_read(&fs, clus, size, text, sizeof text);
if (r != FAT_OK) return r; /* старые настройки не тронуты */
*bytes = size;
r = cfg_parse((const char *)text, size, &fresh);
if (r != CFG_OK) return r;
*active = fresh; /* подмена одним присваиванием */
return CFG_OK;
}
Образы флешки. Четыре образа собраны на компьютере:
a.img— FatFs R0.11, 4 МБ, MBR, FAT16, кластер 512 байт. На томе метка, файл с длинным именем, удалённый файл, аCONFIG.TXTзаписан в два приёма с файломFILL.BINмежду ними — его цепочка разорвана;b.img— FatFs, 4 МБ без MBR;CONFIG.TXTоборван на 300 байтах, как будто флешку выдернули из компьютера во время записи;c.img— FatFs, 40 МБ: FatFs сам выбирает FAT32;d.img— свой сборщикmkfat.py«как у флешки из магазина»: раздел с сектора 2048, две копии FAT, кластер 1 КБ, каталог, длинное имя и удалённый старыйCONFIG.TXTперед настоящим. Свой — потому чтоf_mkfsв FatFs R0.11 всегда ставит одну копию FAT (N_FATS 1).
Каждый образ читают три независимых пути: сам FatFs (f_read), наш
fat.c через модель Bulk-Only и разбор на Python fatpy.py, который не
использует ни того, ни другого. Python:
a.img: MBR тип раздела 0x04, том с сектора 63, FAT16 (кластеров 8064), копий FAT 1 по 32 секторов, секторов в кластере 1
CONFIG.TXT: 733 байт, кластеры 5 -> 8, crc32 b4b17d26
b.img: без MBR, том с сектора 0, FAT16 (кластеров 8126), копий FAT 1 по 33 секторов, секторов в кластере 1
c.img: MBR тип раздела 0x0C, том с сектора 63, FAT32 (кластеров 81185), копий FAT 1 по 640 секторов, секторов в кластере 1
d.img: MBR тип раздела 0x06, том с сектора 2048, FAT16 (кластеров 7122), копий FAT 2 по 28 секторов, секторов в кластере 2
корень: метка, каталог, длинное имя, длинное имя, файл SETTIN~1.TXT, удалён, файл CONFIG.TXT
CONFIG.TXT: 733 байт, кластеры 300, crc32 b4b17d26
Наша прошивочная часть на тех же образах:
a.img: настройки приняты (0); секторов прочитано 6, передач 18; DEVICE=SKLAD-07, товаров 18, REPEAT=0
b.img: нет строки END (-20); секторов прочитано 3, передач 9; DEVICE=PREV-01, товаров 0, REPEAT=1
без правила END приняли бы: DEVICE=SKLAD-07, товаров 3 из 18, REPEAT=0, файл 300 байт
c.img: не FAT16 (-5); секторов прочитано 2, передач 6; DEVICE=PREV-01, товаров 0, REPEAT=1
d.img: настройки приняты (0); секторов прочитано 5, передач 15; DEVICE=SKLAD-07, товаров 18, REPEAT=0
Шесть секторов a.img — это MBR, загрузочный сектор, корень, первый
кластер файла, сектор FAT и второй кластер (кластер 5, потом 8). У d.img
FAT читать не нужно: 733 байта помещаются в один кластер из двух секторов.
Длина и crc32 файла сошлись у всех трёх читателей. Оборванный b.img без
правила END дал бы прибор с тремя товарами из 18 — и никто бы этого не
заметил.
Флешку вынули посреди чтения. Модель устройства «пропадает» перед k-й передачей, k от 0 до 18 (у a.img всего 18 передач):
обрыв после k передач, k = 0…18: старые настройки целы 18 раз, новые приняты 1 раз, иначе 0
При любом обрыве — 18 случаев — остались прежние настройки PREV-01;
новые приняты только при полном чтении. Половинчатых настроек не бывает:
файл сначала читается в буфер целиком, проверяется и только потом
подменяет рабочую копию. Флешку вставили снова — USBH_MSC_Initialize
придёт заново, и файл прочитается с начала.
Размеры прошивочной части — armclang под Cortex-M33, -Os, объектные
файлы (стек USB MDK и RTX сюда не входят):
файл код RO RW ZI
kbd.c 464 40 0 0
term.c 184 16 0 0
fat.c 764 32 0 0
cfg.c 920 55 0 3873
bot.c 162 16 0 0
u585_host.c 530 84 4 2882
всего 3024 243 4 6755
флеш-память (код + RO + RW): 3271 байт, ОЗУ (RW + ZI): 6759 байт
Из ОЗУ 3873 байта — буферы config_load (том, текст до 2 КБ, новая копия
настроек), 2882 — поток терминала со стеком 2 КБ и рабочая копия настроек.
198ч.7 Грабли
Сканер теряет символы, а клавиатура — нет. Пример читает клавиши раз
в 10 мс, а библиотека держит один непрочитанный отчёт и новые не копирует,
пока его не забрали. Пока отчёты приходят реже, чем раз в 30 мс на символ,
этого не видно; сканер, который шлёт код подряд, теряет символы. Забирать
отчёт в USBH_HID_DataReceived, в потоке библиотеки.
Заглавные буквы пропадают. USBH_HID_GetKeyboardKey кладёт
модификаторы в биты 8…15 ответа, а пример печатает только значения от 1 до
127. Всё, набранное с Shift (и с Ctrl, Alt), пример молча выбрасывает.
Shift надо применять самому, своей таблицей.
После переполнения клавиши печатаются дважды. Отчёт из шести 01 — не «всё отпущено», а «состояние неизвестно». Его пропускают целиком и не обновляют прошлое состояние.
Клавиша «залипла» после отключения. Отпускания не будет, а автоповтор
идёт по таймеру хоста. Сбрасывать состояние в USBH_HID_Uninitialize; для
сканера повтор выключить.
USBH_MSC_Read ответил «успех», а данные — мусор. В 8.1.0 после
неудачной попытки и паузы переменная состояния перезаписывается результатом
CheckInstance. Если флешка на месте, это usbOK: ветка восстановления
usbTransferError недостижима, а после третьей неудачи функция вернёт
usbOK. Свою проверку содержимого — строку END, контрольные цифры,
строгий разбор — не убирать.
Флешка на 32 ГБ «не видна». Большие флешки размечены в FAT32, и
fat.c честно отвечает «не FAT16» (c.img). Либо форматировать флешку под
FAT16 (до 65525 кластеров), либо брать FatFs (раздел 42).
Тип FAT определили по типу раздела. У a.img тип 0x04, у d.img 0x06, у FAT32 — 0x0C, но FatFs выбирает тип 0x04 или 0x06 по размеру тома, а не по FAT. Верное правило одно — число кластеров.
Корень искали сразу за первой FAT. У флешек из магазина копий FAT две
(d.img). Корень — за n_fats × fatsz секторами, а FatFs R0.11 делает
образы с одной копией, и такую ошибку на них не увидеть.
Хост тактируется от HSI48 без подстройки. В примере USB берёт 48 МГц
от HSI48 (RCC_CLK48CLKSOURCE_HSI48), а блока CRS нет. По таблице 84
DS13086 HSI48 после заводской калибровки — 47,5…48,5 МГц при 30 °C, то
есть ±1 %, а по температуре — от −3 до +2 %; USB требует ±0,25 % (раздел
20). Подстройку по SOF хост не сделает — SOF шлёт он сам; нужен кварц:
LSE для CRS или HSE для PLL. Есть ли они на плате, скажет схема MB1551;
с ней раздел не сверялся.
Изделие на продажу на MDK-Middleware. Файл supplementary_terms.txt
пакета 8.1.0: без действующей лицензии MDK Professional или Essential
MDK-Middleware — только для некоммерческого использования. Для серийного
прибора — лицензия или другой стек (USB Host ST, своя реализация).
198ч.8 Итоги
- 5 В на разъём даёт ключ TCPP03 по I2C2 (адрес 0x68): GDC открыть, 2 мс,
GDP закрыть. Перегрузку он сообщает выводом FLGn на PE8; вторую быстрее
1 с драйвер не прощает. Бюджет стека — 500 мА по
bMaxPower. - Клавиатура: 8 байт, символ — только у новых кодов, Shift — биты 0x02 и 0x20, отчёт с 01 пропускать целиком: «abcdef» вместо «abcdefabcdef».
- Каждый отчёт забирать сразу: потерянное отпускание между двумя «1» сделало из 13 цифр 12. Сканер — 28 отчётов на код EAN-13 с Enter.
- Автоповтор делает хост: 11 символов за секунду удержания при 500/50 мс; без сброса при отключении — 51 символ за 3 с и дальше, со сбросом — 5.
- Bulk-Only: CBW 31 байт, CSW 13 байт; номер блока — старшим байтом вперёд, длины — младшим. MDK считает блоки по 512 байт.
- FAT16 по шагам: сектор 0, BPB, тип по числу кластеров (пороги 4086 и 65526), корень за всеми копиями FAT, цепочка по FAT. a.img — 6 секторов, 18 передач.
CONFIG.TXT— «всё или ничего»: обрыв перед любой из 18 передач оставил старые настройки, оборванный файл безENDотвергнут.- Прошивочная часть — 3271 байт флеш-памяти и 6759 байт ОЗУ без стека USB.
Полные исходники
kbd.c — перевод загрузочных отчётов клавиатуры в символы.
/* kbd.c — перевод загрузочных отчётов клавиатуры в символы (раскладка США). Раздел 198ч. */
#include "kbd.h"
#include <string.h>
/* Назначения 0x04…0x38 подряд: без Shift и с Shift. 0x28 Enter -> '\r',
0x29 Esc, 0x2A Backspace, 0x2B Tab. */
static const char plain[0x39 - 0x04 + 1] =
"abcdefghijklmnopqrstuvwxyz1234567890\r\x1b\b\t -=[]\\#;'`,./";
static const char shifted[0x39 - 0x04 + 1] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()\r\x1b\b\t _+{}|~:\"~<>?";
char kbd_char(uint8_t usage, uint8_t mods)
{
if (usage >= 0x04u && usage <= 0x38u)
return (mods & KBD_MOD_SHIFT) ? shifted[usage - 0x04u] : plain[usage - 0x04u];
if (usage >= 0x59u && usage <= 0x61u) /* цифровой блок 1…9 */
return (char)('1' + (usage - 0x59u));
if (usage == 0x62u) return '0'; /* цифровой блок 0 */
if (usage == 0x58u) return '\r'; /* Enter цифрового блока */
return 0;
}
void kbd_init(kbd_t *k, uint16_t rep_delay, uint16_t rep_period)
{
memset(k, 0, sizeof *k);
k->rep_delay = rep_delay;
k->rep_period = rep_period;
}
void kbd_reset(kbd_t *k)
{
memset(k->prev, 0, sizeof k->prev);
k->mods = 0;
k->rep_key = 0;
}
static int was_down(const kbd_t *k, uint8_t u)
{
for (int i = 0; i < 6; i++)
if (k->prev[i] == u) return 1;
return 0;
}
int kbd_report(kbd_t *k, const uint8_t r[8], uint32_t now, char *out, int max)
{
int n = 0;
const uint8_t *keys = &r[2];
for (int i = 0; i < 6; i++)
if (keys[i] >= KBD_ROLLOVER && keys[i] <= 0x03u) { /* 01…03 — ошибки, а не клавиши: */
k->rollovers++; /* отчёт целиком пропускаем, */
return 0; /* прошлое состояние не трогаем */
}
for (int i = 0; i < 6; i++) {
uint8_t u = keys[i];
if (u < 0x04u || was_down(k, u)) continue; /* пусто, служебный код или уже был нажат */
char c = kbd_char(u, r[0]);
if (c && n < max) out[n++] = c;
if (c && k->rep_delay) { /* повторяем последнюю нажатую */
k->rep_key = u;
k->rep_next = now + k->rep_delay;
}
}
if (k->rep_key) { /* отпущена — повтор кончился */
int held = 0;
for (int i = 0; i < 6; i++)
if (keys[i] == k->rep_key) held = 1;
if (!held) k->rep_key = 0;
}
memcpy(k->prev, keys, 6);
k->mods = r[0];
return n;
}
int kbd_tick(kbd_t *k, uint32_t now, char *out, int max)
{
int n = 0;
while (k->rep_key && (int32_t)(now - k->rep_next) >= 0 && n < max) {
out[n++] = kbd_char(k->rep_key, k->mods);
k->rep_next += k->rep_period;
}
return n;
}
term.c — строка сканера: длина, цифры, контрольная EAN-13, поиск в списке.
/* term.c — строка сканера: длина, цифры, контрольная EAN-13, поиск в списке. Раздел 198ч. */
#include "term.h"
#include <string.h>
void term_reset(term_t *t)
{
t->n = 0;
t->overflow = 0;
}
int term_feed(term_t *t, char c, const cfg_t *cfg, const char **name)
{
*name = 0;
if (c == '\b') { /* клавиатура: забой стирает символ */
if (t->n) t->n--;
return TERM_NONE;
}
if (c != '\r') {
if (t->n < sizeof t->line - 1) t->line[t->n++] = c;
else t->overflow = 1;
return TERM_NONE;
}
t->line[t->n] = 0;
uint8_t n = t->n, of = t->overflow;
term_reset(t);
if (of || n < cfg->bc_min || n > cfg->bc_max) return TERM_BADLEN;
for (uint8_t i = 0; i < n; i++)
if (t->line[i] < '0' || t->line[i] > '9') return TERM_BADCHAR;
if (n == 13 && !ean13_ok(t->line)) return TERM_BADSUM;
for (uint8_t i = 0; i < cfg->n_sku; i++)
if (strcmp(cfg->sku[i].code, t->line) == 0) {
*name = cfg->sku[i].name;
return TERM_FOUND;
}
return TERM_UNKNOWN;
}
bot.c — CBW и CSW протокола Bulk-Only.
/* bot.c — CBW и CSW протокола Bulk-Only (31 и 13 байт). Раздел 198ч. */
#include "bot.h"
#include <string.h>
static void put32le(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24);
}
void bot_cbw_read10(uint8_t cbw[31], uint32_t tag, uint32_t lba, uint16_t blocks)
{
memset(cbw, 0, 31);
put32le(cbw + 0, BOT_CBW_SIG);
put32le(cbw + 4, tag); /* устройство вернёт его в CSW */
put32le(cbw + 8, (uint32_t)blocks * 512u);/* сколько байт данных ждём */
cbw[12] = 0x80; /* данные: устройство -> хост */
cbw[13] = 0; /* LUN 0 */
cbw[14] = 10; /* длина команды SCSI */
cbw[15] = 0x28; /* READ(10) */
cbw[17] = (uint8_t)(lba >> 24); /* номер блока — старшим байтом вперёд */
cbw[18] = (uint8_t)(lba >> 16);
cbw[19] = (uint8_t)(lba >> 8);
cbw[20] = (uint8_t)lba;
cbw[22] = (uint8_t)(blocks >> 8);
cbw[23] = (uint8_t)blocks;
}
int bot_csw_check(const uint8_t csw[13], uint32_t tag)
{
uint32_t sig = csw[0] | csw[1] << 8 | csw[2] << 16 | (uint32_t)csw[3] << 24;
uint32_t t = csw[4] | csw[5] << 8 | csw[6] << 16 | (uint32_t)csw[7] << 24;
uint32_t res = csw[8] | csw[9] << 8 | csw[10] << 16 | (uint32_t)csw[11] << 24;
if (sig != BOT_CSW_SIG) return BOT_ESIG;
if (t != tag) return BOT_ETAG; /* ответ не на нашу команду */
if (res != 0) return BOT_ERESIDUE; /* устройство отдало не всё */
return csw[12] == 0 ? BOT_OK : (csw[12] == 1 ? BOT_FAILED : BOT_PHASE);
}
fat.c — MBR, загрузочный сектор FAT16, корневой каталог, цепочка кластеров.
/* fat.c — MBR, загрузочный сектор FAT16, корневой каталог, цепочка кластеров. Раздел 198ч. */
#include "fat.h"
#include <string.h>
static uint16_t le16(const uint8_t *p) { return (uint16_t)(p[0] | p[1] << 8); }
static uint32_t le32(const uint8_t *p) { return p[0] | p[1] << 8 | p[2] << 16 | (uint32_t)p[3] << 24; }
/* Похож ли сектор на загрузочный сектор FAT: переход в начале и разумный BPB. */
static int is_vbr(const uint8_t *s)
{
uint8_t spc = s[13];
return (s[0] == 0xEB || s[0] == 0xE9) && le16(s + 11) == 512 && spc && !(spc & (spc - 1))
&& le16(s + 14) != 0 && s[16] != 0;
}
int fat16_mount(fat16_t *f, blk_read_fn rd, void *ctx)
{
uint8_t *s = f->buf;
f->rd = rd;
f->ctx = ctx;
f->fat_cached = 0xFFFFFFFFu;
if (rd(ctx, 0, s)) return FAT_EIO;
if (s[510] != 0x55 || s[511] != 0xAA) return FAT_ESIG;
f->part_lba = 0;
if (!is_vbr(s)) { /* сектор 0 — MBR: ищем раздел FAT */
int found = 0;
for (int i = 0; i < 4 && !found; i++) {
const uint8_t *e = s + 446 + 16 * i;
uint8_t t = e[4];
if (t == 0x01 || t == 0x04 || t == 0x06 || t == 0x0E || t == 0x0B || t == 0x0C) {
f->part_lba = le32(e + 8);
found = 1;
}
}
if (!found) return FAT_ENOVOL;
if (rd(ctx, f->part_lba, s)) return FAT_EIO;
if (s[510] != 0x55 || s[511] != 0xAA) return FAT_ESIG;
if (le16(s + 11) != 512) return FAT_ESECT;
if (!is_vbr(s)) return FAT_ENOVOL;
}
uint16_t rsvd = le16(s + 14);
uint16_t fatsz = le16(s + 22); /* у FAT32 здесь 0 */
uint32_t tot = le16(s + 19) ? le16(s + 19) : le32(s + 32);
f->spc = s[13];
f->n_fats = s[16];
f->root_ents = le16(s + 17);
if (fatsz == 0 || f->root_ents == 0) return FAT_ETYPE;
uint32_t root_secs = (f->root_ents * 32u + 511u) / 512u;
f->fat_lba = f->part_lba + rsvd;
f->root_lba = f->fat_lba + (uint32_t)f->n_fats * fatsz;
f->data_lba = f->root_lba + root_secs;
uint32_t meta = rsvd + (uint32_t)f->n_fats * fatsz + root_secs;
if (tot <= meta) return FAT_ENOVOL;
f->n_clst = (tot - meta) / f->spc;
if (f->n_clst < 4086u || f->n_clst > 65525u) return FAT_ETYPE; /* тип — по числу кластеров */
return FAT_OK;
}
int fat16_find(fat16_t *f, const char name83[11], uint16_t *clus, uint32_t *size)
{
uint32_t root_secs = (f->root_ents * 32u + 511u) / 512u;
for (uint32_t sec = 0; sec < root_secs; sec++) {
if (f->rd(f->ctx, f->root_lba + sec, f->buf)) return FAT_EIO;
for (int i = 0; i < 16; i++) {
const uint8_t *e = f->buf + 32 * i;
uint8_t attr = e[11];
if (e[0] == 0x00) return FAT_ENOFILE; /* дальше записей нет */
if (e[0] == 0xE5) continue; /* удалённый файл */
if (attr == 0x0F) continue; /* кусок длинного имени */
if (attr & 0x18) continue; /* метка тома или каталог */
if (memcmp(e, name83, 11) == 0) {
*clus = le16(e + 26);
*size = le32(e + 28);
return FAT_OK;
}
}
}
return FAT_ENOFILE;
}
static int fat_next(fat16_t *f, uint16_t c, uint16_t *next)
{
uint32_t lba = f->fat_lba + (c * 2u) / 512u;
if (lba != f->fat_cached) {
if (f->rd(f->ctx, lba, f->fat_buf)) return FAT_EIO;
f->fat_cached = lba;
}
*next = le16(f->fat_buf + (c * 2u) % 512u);
return FAT_OK;
}
int fat16_read(fat16_t *f, uint16_t clus, uint32_t size, uint8_t *dst, uint32_t max)
{
uint32_t done = 0, steps = 0;
if (size > max) return FAT_EBIG;
while (done < size) {
if (clus < 2 || clus >= f->n_clst + 2u || ++steps > f->n_clst) return FAT_ECHAIN;
uint32_t lba = f->data_lba + (uint32_t)(clus - 2u) * f->spc;
for (uint8_t s = 0; s < f->spc && done < size; s++) {
if (f->rd(f->ctx, lba + s, f->buf)) return FAT_EIO;
uint32_t n = size - done < 512u ? size - done : 512u;
memcpy(dst + done, f->buf, n);
done += n;
}
if (done < size) {
int r = fat_next(f, clus, &clus);
if (r) return r;
if (clus >= 0xFFF8u) return FAT_ECHAIN; /* цепочка кончилась раньше файла */
}
}
return FAT_OK;
}
cfg.c — разбор CONFIG.TXT и загрузка «всё или ничего».
/* cfg.c — разбор CONFIG.TXT и загрузка «всё или ничего». Раздел 198ч. */
#include "cfg.h"
#include <string.h>
void cfg_default(cfg_t *c)
{
memset(c, 0, sizeof *c);
strcpy(c->device, "NONAME");
c->bc_min = 8;
c->bc_max = 13;
c->repeat = 1;
}
int ean13_ok(const char *d)
{
int sum = 0;
for (int i = 0; i < 13; i++)
if (d[i] < '0' || d[i] > '9') return 0;
for (int i = 0; i < 12; i++)
sum += (d[i] - '0') * ((i & 1) ? 3 : 1);
return (10 - sum % 10) % 10 == d[12] - '0';
}
static int num(const char *v, int lo, int hi)
{
int x = 0;
if (!*v) return -1;
for (; *v; v++) {
if (*v < '0' || *v > '9' || x > 1000) return -1;
x = x * 10 + (*v - '0');
}
return (x < lo || x > hi) ? -1 : x;
}
/* Одна строка без конца строки. 0 — принята, 1 — это END, -1 — ошибка. */
static int cfg_line(char *s, cfg_t *c)
{
if (!*s || *s == '#') return 0;
if (strcmp(s, "END") == 0) return 1;
char *v = strchr(s, '=');
if (!v) return -1;
*v++ = 0;
if (strcmp(s, "DEVICE") == 0) {
size_t n = strlen(v);
if (n == 0 || n > 16) return -1;
memcpy(c->device, v, n + 1);
} else if (strcmp(s, "BARCODE_MIN") == 0) {
int x = num(v, 1, 32);
if (x < 0) return -1;
c->bc_min = (uint8_t)x;
} else if (strcmp(s, "BARCODE_MAX") == 0) {
int x = num(v, 1, 32);
if (x < 0) return -1;
c->bc_max = (uint8_t)x;
} else if (strcmp(s, "REPEAT") == 0) {
if (strcmp(v, "ON") == 0) c->repeat = 1;
else if (strcmp(v, "OFF") == 0) c->repeat = 0;
else return -1;
} else if (strcmp(s, "SKU") == 0) { /* SKU=<13 цифр> <название> */
char *name = strchr(v, ' ');
if (!name || name - v != 13 || c->n_sku >= CFG_MAX_SKU) return -1;
*name++ = 0;
if (!ean13_ok(v) || strlen(name) == 0 || strlen(name) > 16) return -1;
strcpy(c->sku[c->n_sku].code, v);
strcpy(c->sku[c->n_sku].name, name);
c->n_sku++;
} else {
return -1; /* незнакомый ключ — не гадаем */
}
return 0;
}
int cfg_parse(const char *t, uint32_t len, cfg_t *out)
{
char line[64];
uint32_t i = 0;
int no = 0;
cfg_default(out);
while (i < len) {
uint32_t n = 0;
no++;
while (i < len && t[i] != '\n') {
if (t[i] != '\r') {
if (n >= sizeof line - 1) return no; /* слишком длинная строка */
line[n++] = t[i];
}
i++;
}
int ended = i < len; /* строка дошла до '\n' */
i++;
line[n] = 0;
if (!ended && strcmp(line, "END") != 0)
return CFG_ENOEND; /* последняя строка оборвана */
int r = cfg_line(line, out);
if (r < 0) return no;
if (r == 1) return out->bc_min <= out->bc_max ? CFG_OK : no;
}
return CFG_ENOEND; /* файл оборван: END не дошёл */
}
int config_load(blk_read_fn rd, void *ctx, cfg_t *active, uint32_t *bytes)
{
static fat16_t fs;
static uint8_t text[CFG_FILE_MAX];
static cfg_t fresh;
uint16_t clus;
uint32_t size;
int r = fat16_mount(&fs, rd, ctx);
if (r == FAT_OK) r = fat16_find(&fs, "CONFIG TXT", &clus, &size);
if (r == FAT_OK) r = fat16_read(&fs, clus, size, text, sizeof text);
if (r != FAT_OK) return r; /* старые настройки не тронуты */
*bytes = size;
r = cfg_parse((const char *)text, size, &fresh);
if (r != CFG_OK) return r;
*active = fresh; /* подмена одним присваиванием */
return CFG_OK;
}
u585_host.c — прошивка терминала на B-U585I-IOT02A поверх хоста USB из MDK-Middleware.
/* u585_host.c — прошивка терминала на B-U585I-IOT02A: хост USB из MDK-Middleware 8.x,
клавиатура или сканер штрихкодов и флешка с CONFIG.TXT. Раздел 198ч.
Заменяет Keyboard.c примера пакета платы; main.c, питание VBUS (USBH_VbusOnOff)
и настройка хоста — из примера. На плате не запускалось. */
#include <string.h>
#include "cmsis_os2.h"
#include "rl_usb.h"
#include "kbd.h"
#include "term.h"
#include "cfg.h"
#define REP_DELAY_MS 500u /* наш выбор, не стандарт */
#define REP_PERIOD_MS 50u
static osMessageQueueId_t kq; /* отчёты клавиатуры по 8 байт */
static volatile uint8_t kbd_gone, msc_new;
static cfg_t cfg;
static kbd_t kbd;
static term_t term;
volatile int last_scan, last_load = 1; /* итоги — для отладчика и индикации */
const char *volatile last_name;
/* Поток HID библиотеки зовёт это сразу после каждого принятого отчёта:
забираем отчёт здесь, а не раз в 10 мс из главного цикла. */
void USBH_HID_DataReceived(uint8_t instance, uint32_t len)
{
uint8_t r[8];
if (len == 8u && USBH_HID_Read(instance, r, 8) == 8)
(void)osMessageQueuePut(kq, r, 0U, 0U);
}
void USBH_HID_Uninitialize(uint8_t instance) /* клавиатуру выдернули */
{
(void)instance;
kbd_gone = 1u;
}
void USBH_MSC_Initialize(uint8_t instance) /* флешка опознана */
{
(void)instance;
msc_new = 1u;
}
static int msc_rd(void *ctx, uint32_t lba, uint8_t *buf)
{
(void)ctx;
return USBH_MSC_Read(0U, lba, 1U, buf) == usbOK ? 0 : -1;
}
static void load_config(void)
{
uint32_t blocks, bsize, bytes;
if (USBH_MSC_ReadCapacity(0U, &blocks, &bsize) != usbOK || bsize != 512u) {
last_load = FAT_ESECT;
return;
}
last_load = config_load(msc_rd, NULL, &cfg, &bytes);
if (last_load == CFG_OK)
kbd_init(&kbd, cfg.repeat ? REP_DELAY_MS : 0u, REP_PERIOD_MS);
}
static __NO_RETURN void app_main(void *argument)
{
(void)argument;
kq = osMessageQueueNew(32U, 8U, NULL);
cfg_default(&cfg);
kbd_init(&kbd, REP_DELAY_MS, REP_PERIOD_MS);
term_reset(&term);
if (USBH_Initialize(0U) != usbOK) {
for (;;) {}
}
for (;;) {
uint8_t r[8];
char out[16];
int n = 0;
if (osMessageQueueGet(kq, r, NULL, 5U) == osOK)
n = kbd_report(&kbd, r, osKernelGetTickCount(), out, (int)sizeof out);
if (kbd_gone) { /* ничего не держим нажатым */
kbd_gone = 0u;
kbd_reset(&kbd);
term_reset(&term);
osMessageQueueReset(kq);
}
n += kbd_tick(&kbd, osKernelGetTickCount(), out + n, (int)sizeof out - n);
for (int i = 0; i < n; i++) {
const char *name;
int res = term_feed(&term, out[i], &cfg, &name);
if (res != TERM_NONE) {
last_scan = res;
last_name = name;
}
}
if (msc_new) {
msc_new = 0u;
load_config();
}
}
}
static uint64_t app_stk[2048 / 8];
static const osThreadAttr_t app_attr = { .stack_mem = app_stk, .stack_size = sizeof app_stk };
void app_initialize(void)
{
osThreadNew(app_main, NULL, &app_attr);
}
Дальше: раздел 198ш «Мини-проект: USB-COM и клавиатура в одном приборе на B-U585I-IOT02A — составное устройство CDC + HID» — плата была хостом, теперь она снова устройство, и сразу два: COM-порт с консолью и клавиатура по одному кабелю.