Embedded HandbookApps
All sections › XXV. Mini-projects
STM32U585 · level advanced · 42 min read
Open in the interactive handbook

198ч. Mini-project: a USB host on the B-U585I-IOT02A — keyboard and flash drive

Section 20а showed the USB host from the controller side: OTG roles, enumeration, channels. Section 20в took apart the keyboard report from the device side. In 198ф the microcontroller itself pretended to be a flash drive. Here it is the other way round: the device reads other devices. It is a warehouse terminal on the B-U585I-IOT02A board: you connect to the USB-C connector either a barcode scanner (to the host it is an ordinary keyboard) or a flash drive with the settings file CONFIG.TXT. The scanner gives the product code, the flash drive gives the device name and the product list.

The base is two examples from the board pack B-U585I-IOT02A_BSP 1.1.0 (Keil): Projects/Middleware/USB/Host/Keyboard and .../MassStorage. Both are built on the USB Host stack from MDK-Middleware and print the pressed keys or write a file to the flash drive. From them we take the host setup and the bus power, and we write our own parts ourselves: turning reports into characters, auto-repeat, the scanner line, parsing the MBR and FAT16, and loading the settings "all or nothing".

Tested without a board. Our own code is built with gcc on a PC and run against prepared data: streams of keyboard reports and four flash drive images, which are built by FatFs and by our own builder and read back by three independent paths. The firmware part is built with armclang for the Cortex-M33; it was not run on the board.

Sources: RM0456 (STM32U5, chapter 72 "OTG_FS": 72.7.2 "USB host states", 72.7.3 "Host channels"), datasheet DS13086 (STM32U585xx, table 84 — HSI48), the board pack B-U585I-IOT02A_BSP 1.1.0: USB Host examples (license Apache-2.0), the power driver b_u585i_iot02a_usbpd_pwr.c and the component tcpp0203 (license BSD-3-Clause); USB Host sources from MDK-Middleware 8.1.0 (usbh_lib_hid.c, usbh_lib_msc.c, usbh_lib_core.c; license Arm EULA — only paraphrase and short quotes); FatFs R0.11 (ChaN, from STM32Cube_FW_F1 V1.8.7; a copy is in the check together with the license); the ST USB Host library (usbh_hid_keybd.c, usbh_msc_bot.h, license SLA0044) — for cross-checking the tables; the EAN-13 check digit test — after zbar.c from github.com/openmv/openmv, commit 6236993 (LGPL, cross-check only). The texts of the HID, Bulk-Only and FAT specifications are not quoted here: the formats are taken from these sources. License: the section's own code — free to use, FatFs — ChaN's terms in the copy of the files.

Terms used in the documentation

Term Meaning
CBW (Command Block Wrapper) command wrapper, 31 bytes
CSW (Command Status Wrapper) status wrapper, 13 bytes
dCSWDataResidue residue: how many bytes the device did not deliver
READ(10), READ CAPACITY(10), TEST UNIT READY SCSI commands: read blocks, capacity, readiness
ErrorRollOver "overflow": more keys are pressed than fit in the report
typematic, auto-repeat repeating a held key
keypad the numeric block of the keyboard
EAN-13 check digit the check digit of an EAN-13 barcode
MBR (master boot record) the main boot record: the partition table in sector 0
BPB (BIOS parameter block) the parameter table in the volume boot sector
superfloppy a medium without a partition table: the volume starts at sector 0
GDP, GDC (TCPP03) TCPP03 switches: supply 5 V outward, accept 5 V from outside

198ч.1 What the device is and what it is made of

The terminal works like this:

  1. The board puts 5 V on the USB-C connector and waits for a device.
  2. A scanner is connected — each of its reports becomes characters; the digits up to Enter are the code. The code is checked for length and check digit and looked up in the product list.
  3. A flash drive is connected — the device reads CONFIG.TXT from the root and, if the file is intact, replaces the settings completely. Otherwise the old ones stay.

USB host on the B-U585I-IOT02A: TCPP03 on I2C2 at address 0x68 opens GDC, waits 2 ms and closes GDP, putting 5 V on connector CN1; it reports an overload with FLGn on PE8, the VBUS voltage is measured by ADC4 on PF14 through a 330 and 47 divider; OTG_FS on PA11 and PA12 with 12 channels is clocked from HSI48 without CRS; the MDK USB Host stack hands keyboard reports through USBH_HID_DataReceived to a queue of 32 reports, and flash drive blocks through USBH_MSC_Read; then kbd.c and term.c with Shift, code 01 and EAN-13, fat.c and cfg.c with MBR, FAT16 and the END line; the cfg_t settings change all or nothing; result of the run

What is in the pack examples. Keyboard.c starts the host with USBH_Initialize(0U) and once every 10 ms (osDelay(10U)) asks the library for a pressed key with USBH_HID_GetKeyboardKey. MassStorage.c once every 100 ms checks USBH_MSC_DriveGetMediaStatus("U0:"), mounts the disk with the MDK file system and writes README.txt. The example description: CPU at 160 MHz, USB at 48 MHz, OTG_FS on connector CN1 USB Type-C.

The host setup in USBH_Config_0.c is the same in both examples: power budget USBH0_HC_POWER = 500 mA, no more than 3 channels at once (USBH0_HC_PIPE_NUM). OTG_FS itself has 12 channels (RM0456, 72.7.3; also Host_channels = 12 in the example's CubeMX code). Enumeration and channels are as in 20а.3 and 20а.4; we will not repeat them.

Our firmware replaces Keyboard.c with the file u585_host.c, and takes main.c, the bus power and the host setup from the example. The MDK stack gives us flash drive blocks directly (USBH_MSC_Read), without the MDK file system: FAT is read by our own fat.c.


198ч.2 Bus power: who supplies 5 V and how the current is limited

The STM32U585 itself does not make 5 V: "On-chip 5 V VBUS generation is not supported" (RM0456, 72.7.2). An external source or switch is needed. On this board the connector VBUS is switched by the TCPP03 — a USB-C port protection chip. The driver b_u585i_iot02a_usbpd_pwr.c talks to it over I2C2, address TCPP0203_I2C_ADDRESS_X68 (0x68 — the eight-bit form, the pack comment says 0x34). It is enabled by pin PB5, and the fault flag FLGn comes to PE8 (pull-up, EXTI8 interrupt on the falling edge).

The order from the example's main.c and the driver:

  1. BSP_USBPD_PWR_Init — pins, the FLGn interrupt, PB5 set to one, I2C2; the driver reads the chip type and refuses to work if it is not a TCPP03.
  2. BSP_USBPD_PWR_SetRole(..., POWER_ROLE_SOURCE) — we are the source. Without this, BSP_USBPD_PWR_VBUSOn returns an error.
  3. BSP_USBPD_PWR_SetPowerMode(..., USBPD_PWR_MODE_NORMAL) and BSP_USBPD_PWR_VBUSInit — normal mode and VBUS measurement through ADC4.
  4. When the stack needs power, it calls USBH_VbusOnOff(true) from main.c, and that calls BSP_USBPD_PWR_VBUSOn(0): open the GDC switch (input 5 V from outside), wait 2 ms (HAL_Delay(2)), close the GDP switch (output 5 V outward).
  5. Switching off, BSP_USBPD_PWR_VBUSOff: open GDP, enable VBUS discharge and wait until the voltage falls below 650 mV (threshold 750 − 100), no more than 20 times by 20 ms, then another 10 ms, and disable the discharge.

The current is limited at two levels.

The TCPP03 current threshold and where its 5 V input comes from are set by the MB1551 board schematic; this section was not checked against it, and the pack has only a reference to it and to the user manual UM2839. The driver does not measure current: BSP_USBPD_PWR_VBUSGetCurrent contains adc_value = 0U; /* Not yet implemented */.

It does measure voltage: VBUS through a 330 and 47 divider on PF14 (ADC4_IN5). At 5.000 V the ADC input gets 623 mV — 773.5 code units with a 3300 mV reference. The driver formula computes in integers: from code 773 it returns 4989 mV, from 774 — 4997 mV. The "VBUS valid" threshold of OTG_FS is 4.4 V (RM0456, 72.7.2), but it works only if HNP or SRP is enabled; the example has vbus_sensing_enable = DISABLE.


198ч.3 Keyboard: report to characters

To the host, a barcode scanner is a boot keyboard: a HID interface with protocol 1 and an 8-byte report — modifiers, a reserved byte and six codes of pressed keys (20в.4). A code is a usage from the keyboard page: A is 0x04, 1 is 0x1E, Enter is 0x28. The character itself depends on Shift.

What the MDK library does. On connection it sends SET_IDLE with zero — "send a report only on change" (USBH_HID_SetIdle (instance, 0U, 0U, 0U)). The SET_PROTOCOL request in usbh_lib_hid.c 8.1.0 is disabled with #if 0: the library does not set the boot protocol, and relies on the keyboard sending 8 bytes anyway (or 9 with a report ID — it then drops the first byte). The ST library does the opposite and does set the protocol (20в.4).

The receive thread copies a new report into its buffer only if the previous one has already been taken: the condition (len <= 9U) && (ptr_hid->hid_in_data_len == 0U). USBH_HID_GetKeyboardKey returns one key per call: a press report is consumed in two calls, a release report in one. In the example the call happens once every 10 ms — so no faster than 30 ms per character, and everything the scanner sends between calls is lost. That is why we take each report immediately in the library thread — it calls USBH_HID_DataReceived after each reception — and put it into a queue of 32 reports:

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);
}

Translating to characters. The table is the US layout: usages 0x04…0x38 without Shift and with Shift, plus the keypad. Scanners set to "digits from the keypad" send 0x59…0x61 and 0x62 — and that is how the MDK example translates them:

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;
}

Parsing rules:

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;
}

A run on the PC. "Hello, STM32!" — one press report and one release report per character, 26 reports, "S" typed with the right Shift. Overlap is how people type fast: "a" is not released yet and "b" is already pressed; then "c" and "d" came in one report with 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

Overflow: six keys a…f are held, a seventh is pressed — the keyboard sends six codes 01; the seventh is released — a…f again:

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

A naive host took 01 for an ordinary report, "forgot" six keys and, when they came back, printed them a second time.

Independent cross-check: the check parses the same reports in Python with the tables from ST's usbh_hid_keybd.c (QWERTY layout, with Shift) and MDK's HID_KEYBOARD_ID_TO_ASCII (without Shift) — the texts match; our table matches both on all 53 usages 0x04…0x38 except six: 0x28…0x2C (Enter, Esc, backspace, Tab, space — ST has other codes there, or nothing with Shift) and 0x32 (ST has nothing, MDK has "Non-US # and ~").


198ч.4 Repeat, stuck keys and the scanner

Repeat. After SET_IDLE with zero the keyboard repeats nothing by itself: while a key is held, there are no reports. Auto-repeat is done by the host. Our choice (not a standard): first repeat after 500 ms, then every 50 ms, the last pressed key is repeated, and releasing it stops the repeat:

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;
}

Stuck keys. The dangerous case is a keyboard unplugged while a key is pressed: the release report will never come. The library calls USBH_HID_Uninitialize; we set a flag, and the main loop resets the state (kbd_reset), the scanner line and the queue. Model: "x" is held, after 0.7 s the cable is pulled out:

повтор 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

Without the reset, "x" keeps pouring out as long as the device runs: 51 characters in 3 s and more. With the reset — 5: the first one and four repeats before 0.7 s. For a scanner repeat is harmful anyway: a lost release gives "1111…". That is why the terminal's CONFIG.TXT has REPEAT=OFF.

Polling. The host polls the interrupt endpoint once per bInterval of its descriptor (MDK passes it to USBH_PipeCreate), one report per poll. An EAN-13 code plus Enter is 14 characters, 28 reports: 28 ms at bInterval = 1 and 280 ms at 10 (the calculation is as in 20в.4).

The scanner line. Characters accumulate until Enter. Then come the length from the settings, digits only, the check digit for 13 characters and the lookup in the product list:

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;
}

The EAN-13 check digit: digits with weights 1, 3, 1, 3…, and the check digit tops the sum up to a multiple of ten:

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';
}

The same weight 3 at odd positions is in ean_verify_checksum from zbar.c; the check ports this function to Python and verifies all codes in the section. Run (the product list comes from CONFIG.TXT on the flash drive image, 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 -> плохая длина

The fourth line is what skipped reports threaten. Between two "1"s a release was lost; the host saw one long "1", and the code was left with 12 digits. The device did not pick the wrong product — it did not find one. Double digits in codes are common, so every report matters, and the length and check digit checks are the last safety net.


198ч.5 The flash drive from the host side: Bulk-Only

A flash drive is an MSC class device with the Bulk-Only protocol (20а.4). The exchange goes in triples over bulk endpoints: the host sends a CBW (31 bytes) with a SCSI command, then the data follow, then the device replies with a CSW (13 bytes). The CBW signature is "USBC" (0x43425355), the CSW one is "USBS" (0x53425355); the same numbers and lengths 31 and 13 are in ST's usbh_msc_bot.h.

What MDK does after enumerating a flash drive (USBH_MSC_Initialize_Lib): GET MAX LUN, TEST UNIT READY five times (on failure — a 1250 ms pause and REQUEST SENSE), INQUIRY, one more TEST UNIT READY, then READ CAPACITY twice, TEST UNIT READY and a trial read of block 0. The trial read, according to the comment in the code, is there because some flash drives pass enumeration but fail on the first READ(10). Only after that is our USBH_MSC_Initialize called.

Our CBW construction for READ(10) and the CSW check follow the same rules as MDK: signature, the tag matches the command tag, the residue is zero:

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);
}

On the board this work is done by MDK; bot.c runs in the model, where the flash drive is played by a device model on top of the image. The second read of a.img is the volume boot sector, block 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

The check parses these bytes in Python on its own:

CBW 31 байт: подпись 43425355, тег 2, данных 512, флаги 80, LUN 0, команда 10 байт: 0x28, блок 63, блоков 1
CSW 13 байт: подпись 53425355, тег 2, остаток 0, состояние 0

The block number in the command is big-endian (00 00 00 3F), while the wrapper fields are little-endian (00 02 00 00 is 512).

Two MDK details that affect the device. MDK always computes the data length in READ(10) as block_num << 9, that is, blocks of 512 bytes. That is why load_config first asks for the capacity and rejects a flash drive with a different block size. And USBH_MSC_Read makes up to three attempts with a 1 s pause (USBH_Delay(1000U)); if the flash drive is removed, it exits after the first pause with usbDeviceError (about the third branch — in 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 and CONFIG.TXT step by step

The layout of a flash drive on disk (section 42 describes FAT through FatFs; here we read it by hand to see every sector):

  1. Sector 0. At the end is the signature 55 AA. If the sector starts with a jump EB or E9 and its BPB is sane (512-byte sector, cluster is a power of two), it is the volume boot sector itself: a flash drive without a partition table. Otherwise it is an MBR: four 16-byte entries from offset 446; each entry has a type (byte 4) and the first sector of the partition (bytes 8…11).
  2. Volume boot sector. From the BPB: bytes per sector (11), sectors per cluster (13), reserved (14), number of FATs (16), root entries (17), total sectors (19 or 32), sectors per FAT (22; FAT32 has 0 there).
  3. The FAT type is decided by the number of clusters, not by the partition type and not by the "FAT16" label in the sector: fewer than 4086 — FAT12, more than 65525 — FAT32, in between — FAT16. The thresholds are from FatFs R0.11 (MIN_FAT16 = 4086, MIN_FAT32 = 65526); FatFs uses them both to format and to mount a volume.
  4. The root follows right after all copies of the FAT. Entries are 32 bytes: 0x00 — end, 0xE5 — deleted file, attribute 0x0F — a piece of a long name, 0x08 — volume label, 0x10 — directory. We look for the name in 8.3 form — "CONFIG TXT"; the first cluster is in bytes 26…27, the size in 28…31.
  5. The chain. Cluster N starts at sector data + (N − 2) × sectors per cluster; the next cluster is word N in the FAT; 0xFFF8 and above is the end.
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;
}

Protection against a damaged flash drive: a cluster number outside the volume, a chain longer than the number of clusters, or a chain end before the file size — error FAT_ECHAIN, rather than reading someone else's sectors.

The file itself is lines KEY=VALUE, # is a comment, and the last one must be 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

…and 16 more SKU= lines, then END; 733 bytes in total. Parsing is strict: an unknown key, a wrong check digit in SKU or a cut-off last line rejects the whole file. The settings are assembled in a separate structure and replace the working ones with a single assignment:

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;
}

Flash drive images. Four images are built on the PC:

Each image is read by three independent paths: FatFs itself (f_read), our fat.c through the Bulk-Only model, and the Python parser fatpy.py, which uses neither of them. 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

Our firmware part on the same images:

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

The six sectors of a.img are the MBR, the boot sector, the root, the first cluster of the file, a FAT sector and the second cluster (cluster 5, then 8). For d.img the FAT does not need to be read: 733 bytes fit in one cluster of two sectors. The file length and crc32 matched for all three readers. Without the END rule, the cut-off b.img would give a device with three products out of 18 — and nobody would notice.

The flash drive is removed in the middle of a read. The device model "disappears" before the k-th transfer, k from 0 to 18 (a.img has 18 transfers in total):

обрыв после k передач, k = 0…18: старые настройки целы 18 раз, новые приняты 1 раз, иначе 0

On any interruption — 18 cases — the old settings PREV-01 stayed; the new ones were accepted only after a complete read. Half-applied settings do not happen: the file is first read into a buffer in full, checked, and only then replaces the working copy. When the flash drive is inserted again, USBH_MSC_Initialize comes again, and the file is read from the start.

Sizes of the firmware part — armclang for the Cortex-M33, -Os, object files (the MDK USB stack and RTX are not included):

файл            код       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 байт

Of the RAM, 3873 bytes are the config_load buffers (volume, text up to 2 KB, the new copy of the settings), 2882 are the terminal thread with a 2 KB stack and the working copy of the settings.


198ч.7 Pitfalls

The scanner loses characters, but the keyboard does not. The example reads keys once every 10 ms, and the library holds one unread report and does not copy new ones until it is taken. As long as reports come less often than once per 30 ms per character, this is invisible; a scanner that sends a code in one burst loses characters. Take the report in USBH_HID_DataReceived, in the library thread.

Capital letters disappear. USBH_HID_GetKeyboardKey puts the modifiers into bits 8…15 of the result, and the example prints only values from 1 to 127. Everything typed with Shift (and with Ctrl, Alt) the example silently throws away. Apply Shift yourself, with your own table.

After an overflow, keys are printed twice. A report of six 01s does not mean "everything released" but "state unknown". Skip it entirely and do not update the previous state.

A key "stuck" after disconnection. There will be no release, and auto-repeat runs on the host timer. Reset the state in USBH_HID_Uninitialize; for a scanner, turn repeat off.

USBH_MSC_Read reported "success", but the data are garbage. In 8.1.0, after a failed attempt and the pause, the status variable is overwritten with the result of CheckInstance. If the flash drive is in place, that is usbOK: the usbTransferError recovery branch is unreachable, and after the third failure the function returns usbOK. Do not remove your own content check — the END line, check digits, strict parsing.

A 32 GB flash drive is "not seen". Large flash drives are formatted as FAT32, and fat.c honestly answers "not FAT16" (c.img). Either format the flash drive as FAT16 (up to 65525 clusters), or use FatFs (section 42).

The FAT type was decided by the partition type. a.img has type 0x04, d.img 0x06, FAT32 has 0x0C, but FatFs chooses type 0x04 or 0x06 by the volume size, not by the FAT. There is only one correct rule — the number of clusters.

The root was looked for right after the first FAT. Flash drives from a shop have two copies of the FAT (d.img). The root is after n_fats × fatsz sectors, and FatFs R0.11 makes images with one copy, so on them this mistake cannot be seen.

The host is clocked from HSI48 without trimming. In the example USB takes 48 MHz from HSI48 (RCC_CLK48CLKSOURCE_HSI48), and there is no CRS block. According to table 84 of DS13086, HSI48 after factory calibration is 47.5…48.5 MHz at 30 °C, that is ±1 %, and over temperature from −3 to +2 %; USB requires ±0.25 % (section 20). The host cannot trim by SOF — it sends SOF itself; a crystal is needed: LSE for CRS or HSE for the PLL. Whether they are on the board will be shown by the MB1551 schematic; this section was not checked against it.

A product for sale on MDK-Middleware. The file supplementary_terms.txt of pack 8.1.0: without a valid MDK Professional or Essential license, MDK-Middleware is for non-commercial use only. For a production device — a license or another stack (ST USB Host, your own implementation).


198ч.8 Summary

Full sources

kbd.c — translating boot keyboard reports into characters.

/* 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 — the scanner line: length, digits, EAN-13 check digit, lookup in the list.

/* 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 and CSW of the Bulk-Only protocol.

/* 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 boot sector, root directory, cluster chain.

/* 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 — parsing CONFIG.TXT and "all or nothing" loading.

/* 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 — the terminal firmware on the B-U585I-IOT02A on top of the USB host from 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);
}

Next: section 198ш "Mini-project: a USB COM port and a keyboard in one device on the B-U585I-IOT02A — a composite CDC + HID device" — the board was a host, now it is a device again, and two at once: a COM port with a console and a keyboard over one cable.

Open in the interactive handbook