Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32U585 · уровень продвинутый · 42 мин чтения
Открыть в интерактивном справочнике

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 Что за прибор и из чего он

Терминал работает так:

  1. Плата даёт 5 В на разъём USB-C и ждёт устройство.
  2. Подключили сканер — каждый его отчёт превращается в символы; цифры до Enter — это код. Код проверяется по длине и контрольной цифре и ищется в списке товаров.
  3. Подключили флешку — прибор читает CONFIG.TXT из корня и, если файл цел, заменяет настройки целиком. Иначе остаются прежние.

USB-хост на B-U585I-IOT02A: TCPP03 по I2C2 с адресом 0x68 открывает GDC, ждёт 2 мс и закрывает GDP, выдавая 5 В на разъём CN1; перегрузку сообщает FLGn на PE8, напряжение VBUS меряет ADC4 на PF14 через делитель 330 и 47; OTG_FS на PA11 и PA12 с 12 каналами тактируется от HSI48 без CRS; стек MDK USB Host отдаёт отчёты клавиатуры через USBH_HID_DataReceived в очередь на 32 отчёта, а блоки флешки через USBH_MSC_Read; дальше kbd.c и term.c с Shift, кодом 01 и EAN-13, fat.c и cfg.c с MBR, FAT16 и строкой END; настройки cfg_t меняются всё или ничего; итог прогона

Что лежит в примерах пакета. 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 примера и драйвера:

  1. BSP_USBPD_PWR_Init — выводы, прерывание FLGn, PB5 в единицу, I2C2; драйвер читает тип микросхемы и отказывается работать, если это не TCPP03.
  2. BSP_USBPD_PWR_SetRole(..., POWER_ROLE_SOURCE) — мы источник. Без этого BSP_USBPD_PWR_VBUSOn вернёт ошибку.
  3. BSP_USBPD_PWR_SetPowerMode(..., USBPD_PWR_MODE_NORMAL) и BSP_USBPD_PWR_VBUSInit — рабочий режим и замер VBUS через ADC4.
  4. Когда стеку нужно питание, он зовёт USBH_VbusOnOff(true) из main.c, а тот — BSP_USBPD_PWR_VBUSOn(0): открыть ключ GDC (вход 5 В снаружи), подождать 2 мс (HAL_Delay(2)), закрыть ключ GDP (выход 5 В наружу).
  5. Выключение BSP_USBPD_PWR_VBUSOff: открыть GDP, включить разряд VBUS и ждать, пока напряжение не упадёт ниже 650 мВ (порог 750 − 100), не больше 20 раз по 20 мс, затем ещё 10 мс и выключить разряд.

Ток ограничен на двух уровнях.

Порог тока 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;
}

Правила разбора:

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; здесь читаем руками, чтобы видеть каждый сектор):

  1. Сектор 0. В конце подпись 55 AA. Если сектор начинается с перехода EB или E9 и его BPB разумен (сектор 512 байт, кластер — степень двойки), это сам загрузочный сектор тома: флешка без таблицы разделов. Иначе это MBR: четыре записи по 16 байт с 446-го, у записи — тип (байт 4) и первый сектор раздела (байты 8…11).
  2. Загрузочный сектор тома. Из BPB: байт в секторе (11), секторов в кластере (13), резерв (14), копий FAT (16), записей корня (17), секторов всего (19 или 32), секторов в одной FAT (22; у FAT32 там 0).
  3. Тип FAT — по числу кластеров, а не по типу раздела и не по надписи «FAT16» в секторе: меньше 4086 — FAT12, больше 65525 — FAT32, между ними — FAT16. Пороги — из FatFs R0.11 (MIN_FAT16 = 4086, MIN_FAT32 = 65526), по ним FatFs и размечает, и монтирует том.
  4. Корень — сразу за всеми копиями FAT. Записи по 32 байта: 0x00 — конец, 0xE5 — удалённый файл, атрибут 0x0F — кусок длинного имени, 0x08 — метка тома, 0x10 — каталог. Имя ищем в виде 8.3 — "CONFIG TXT"; первый кластер — байты 26…27, размер — 28…31.
  5. Цепочка. Кластер 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;
}

Образы флешки. Четыре образа собраны на компьютере:

Каждый образ читают три независимых пути: сам 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 Итоги

Полные исходники

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-порт с консолью и клавиатура по одному кабелю.

Открыть в интерактивном справочнике