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:
- The board puts 5 V on the USB-C connector and waits for a device.
- 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.
- A flash drive is connected — the device reads
CONFIG.TXTfrom the root and, if the file is intact, replaces the settings completely. Otherwise the old ones stay.
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:
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.BSP_USBPD_PWR_SetRole(..., POWER_ROLE_SOURCE)— we are the source. Without this,BSP_USBPD_PWR_VBUSOnreturns an error.BSP_USBPD_PWR_SetPowerMode(..., USBPD_PWR_MODE_NORMAL)andBSP_USBPD_PWR_VBUSInit— normal mode and VBUS measurement through ADC4.- When the stack needs power, it calls
USBH_VbusOnOff(true)frommain.c, and that callsBSP_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). - 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.
- Budget in the stack. During enumeration MDK compares
bMaxPowerfrom the configuration descriptor withUSBH0_HC_POWER / 2(the field counts in units of 2 mA). A device that asks for more than 500 mA is not configured:USBH_NOTIFY_INSUFFICIENT_POWER,usbNotConfigured. - Overload in hardware. The TCPP03 itself detects a VBUS overload and pulls
FLGn low. The handler reads the flag register; on an overload (
OCP VBUS) the driver restores power only if more than 1 s has passed since the previous overload (USBPD_PWR_FAULT_MIN_TIME_RECOVERY= 1000). If the second overload comes sooner, power is not restored.
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:
- only new codes produce a character — those that were not in the previous report; holding a key means the same code in the following reports;
- if the report contains a code 01…03, the report is skipped entirely, and the previous
state is not touched. 01 is ErrorRollOver: more keys are pressed than
fit in six slots, and the keyboard writes 01 into all six (that is how MDK
catches them: a comparison with
"\1\1\1\1\1\1"); - Shift is either of the two: bits 0x02 and 0x20 of byte 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;
}
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):
- 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).
- 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).
- 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. - 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. - 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:
a.img— FatFs R0.11, 4 MB, MBR, FAT16, 512-byte cluster. The volume has a label, a file with a long name, a deleted file, andCONFIG.TXTis written in two passes with the fileFILL.BINbetween them — its chain is fragmented;b.img— FatFs, 4 MB without MBR;CONFIG.TXTis cut off at 300 bytes, as if the flash drive was pulled out of the PC during writing;c.img— FatFs, 40 MB: FatFs chooses FAT32 by itself;d.img— our own buildermkfat.py, "like a flash drive from a shop": partition from sector 2048, two copies of the FAT, 1 KB cluster, a directory, a long name and a deleted oldCONFIG.TXTbefore the real one. Our own becausef_mkfsin FatFs R0.11 always creates one copy of the FAT (N_FATS 1).
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
- 5 V on the connector comes from the TCPP03 switch over I2C2 (address 0x68): open GDC, 2 ms,
close GDP. It reports an overload with the FLGn pin on PE8; a second one sooner than
1 s the driver does not forgive. The stack budget is 500 mA by
bMaxPower. - Keyboard: 8 bytes, a character only for new codes, Shift is bits 0x02 and 0x20, skip a report with 01 entirely: "abcdef" instead of "abcdefabcdef".
- Take each report immediately: a lost release between two "1"s turned 13 digits into 12. A scanner needs 28 reports per EAN-13 code with Enter.
- Auto-repeat is done by the host: 11 characters per second of holding at 500/50 ms; without a reset on disconnection — 51 characters in 3 s and more, with the reset — 5.
- Bulk-Only: CBW 31 bytes, CSW 13 bytes; the block number is big-endian, the lengths little-endian. MDK counts blocks as 512 bytes.
- FAT16 step by step: sector 0, BPB, type by the number of clusters (thresholds 4086 and 65526), the root after all copies of the FAT, the chain through the FAT. a.img — 6 sectors, 18 transfers.
CONFIG.TXTis "all or nothing": an interruption before any of the 18 transfers kept the old settings, a cut-off file withoutENDwas rejected.- The firmware part is 3271 bytes of flash and 6759 bytes of RAM without the USB stack.
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.