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

198ф. Mini-project: a USB flash drive on the B-U585I-IOT02A — the device serves its log as a disk

The data logger from 198б sent its stream over a virtual COM port, and the PC needed a program of its own. Here the device is simpler for the person who uses it: plug in the cable, a disk appears in the file manager, and on it is the file LOG.CSV, which any spreadsheet can open. No drivers and no programs: every operating system understands the Mass Storage class on its own (USB host and reading flash drives are covered in 20а.4).

The trick is that there is no disk in the device at all. The log lives in RAM as a ring of binary records, 8 bytes each, and the boot sector, the FAT table, the directory and the CSV lines are built at the moment the host asks for a sector with a READ(10) command. The disk is read-only: the device rejects writes from the PC and tells the host in advance that the medium is write-protected.

Tested without a board. The disk generator fatgen.c, the Bulk-Only and SCSI handler scsi.c and the log ring logring.c are built with gcc on a PC together with a host model: it does the same things an operating system does when a flash drive is plugged in, and reads the whole disk in 64-byte packets. The three images it read are parsed in two independent ways — with the FatFs library and with our own Python parser — and the file LOG.CSV is compared byte for byte. The firmware part is built with armclang for Cortex-M33; it has not been run on the board.

Sources: the "USB Device Mass Storage" example from the B-U585I-IOT02A board pack 1.1.0 (Keil; files USBD_User_MSC_0.c, MemoryDiskImage.c, USBD_Config_MSC_0.h; library description — MDK-Middleware 7.13.0, USB component 6.15.0); the header stm32u585xx.h from the STM32U5xx_DFP 2.2.1 pack; DS13086 (STM32U585xx, tables 88 and 89 — flash memory); the MX25LM51245G driver from the board pack; the ST USB Device library (MSC class, copies for F4, F7 and G4 in github.com/betaflight/betaflight, commit 744f95f) and, in the same repository, emfat — an "on the fly" FAT32 disk for flight logs; FatFs R0.15 (ChaN) — as in 198т. Licenses: board pack — Apache-2.0 and BSD-3-Clause, MDK-Middleware examples — the Arm license for MDK, ST library — MCD-ST Liberty V2 and SLA0044, emfat — MIT, FatFs — its own free license (notice in the sources). The section contains only short quotes of third-party code; FatFs is kept in the check together with its notice. Nothing was checked against the USB Mass Storage and SCSI standards: command and error codes are taken from the MDK headers (usb_msc.h) and ST headers (usbd_msc_scsi.h).

Terms used in the documentation

Term Meaning
Mass Storage Class (MSC) the "storage device" class: flash drives, card readers
Bulk-Only Transport (BOT) the "bulk transfers only" protocol: command, data, status
Command Block Wrapper (CBW), Command Status Wrapper (CSW) the command wrapper (31 bytes) and the completion report (13 bytes)
data residue the remainder: how many of the promised bytes were not transferred
logical block address (LBA) sector (block) number from the start of the disk
sense data: sense key, ASC, ASCQ error information: the error class and its qualifying codes
UNIT ATTENTION "attention": the medium may have changed
write protect (WP) write protection
STALL, endpoint halt an endpoint stopped until the host clears it
BIOS parameter block (BPB) volume parameters in the boot sector
volume label the volume name
eject (START STOP UNIT, LoEj) "Eject": the host releases the medium

198ф.1 What the device does and where to start

The device measures temperature and humidity once every 10 seconds and puts a record into the ring. Connect it to a PC and you see a disk LOGGER with a single file:

seq,time_s,temp_C,rh_pct
00000000,00000000,-015.00,030.00
00000001,00000010,-013.69,030.71
00000002,00000020,-012.38,031.42

— this is the start of the file from the test run: the header and the first three records. Where the values come from does not matter for this section: in the model they are computed by a formula, on the board a sensor will supply them (198х).

Path of a request from the PC to the log: the file manager sends READ(10); over full-speed USB with 64-byte Bulk endpoints go the CBW of 31 bytes, the data and the CSW of 13 bytes; scsi.c runs the Bulk-Only protocol and the SCSI commands, sets write protection in MODE SENSE and answers a write with error 07/27; fatgen.c, given a sector number, builds the boot sector, FAT, directory or CSV lines from a snapshot of the log ring of 32768 records of 8 bytes in SRAM3; a new snapshot is taken only after the Eject command and is reported to the host through UNIT ATTENTION 06/28; at the bottom is the disk layout of 2055 sectors of 512 bytes: sector 0 boot, 1…6 and 7…12 the two FAT copies, 13 the root directory, 14…2054 the file data in clusters 2…2042 in a row

The board pack example. The B-U585I-IOT02A pack contains a ready-made "Mass Storage" flash drive built on the USB library from MDK-Middleware. The library itself runs the protocol and the SCSI commands; the program only has to supply seven handlers in USBD_User_MSC_0.c: initialization and de-initialization, buffer, capacity, read, write and media check. The "disk" there is an 8 KB array in RAM (16 blocks of 512 bytes) plus a ready-made 4096-byte FAT12 image in flash memory, which is copied into that array at startup. According to the example description: OTG_FS on connector CN1 (USB Type-C), CPU at 160 MHz, USB at 48 MHz. The Bulk endpoints are number 1 in both directions, 64-byte packets, a 512-byte class buffer; the INQUIRY strings are "Keil", "Disk 0", "1.0".

The fields of this image (the check parses them itself):

образ примера: 4096 байт, FAT12 «USB_Memory»; BPB: сектор 512 байт, секторов в кластере 1, FAT 1 × 2 сектора, корень 16 мест, всего секторов 64
  README.TXT: кластер 2, 107 байт: «This is a USB Memory Device Demonstration.»
  ёмкость по USBD_MSC0_GetMediaCapacity: sizeof(memory)/512 = 8192/512 = 16 блоков, а в BPB — 64

The volume in the boot sector is declared as 64 sectors, while GetMediaCapacity returns 16. As long as the used clusters lie within the first 16 sectors, this goes unnoticed; we come back to this mismatch in the pitfalls.

Why a ready-made image is a poor fit for a log: the file in it never changes. For the host to see new records, the image would have to be rewritten — FAT, directory and data — and kept entirely in memory. Here it is done the other way round: only the log is stored, and the disk is computed. emfat in betaflight uses the same idea: the flight log is served as FAT32 built on request, and STORAGE_IsWriteProtected there always returns 1.


198ф.2 Mass Storage from the inside: CBW, data, CSW

An MSC class device declares an interface with class 0x08, subclass 0x06 (SCSI commands) and protocol 0x50 (Bulk-Only), plus two Bulk endpoints — the same thing is seen from the host side in 20а.4. Everything else goes over these two endpoints in three steps:

  1. the host sends a CBW to Bulk-OUT — 31 bytes: the "USBC" signature (0x43425355), a tag, how many data bytes it expects, the direction (bit 7: 1 — to the host), the logical unit number, the command length and the SCSI command itself, up to 16 bytes;
  2. data — in one direction, in 64-byte packets at full speed;
  3. the host collects a CSW from Bulk-IN — 13 bytes: "USBS" (0x53425355), the same tag, the residue and the status: 0 — passed, 1 — failed, 2 — phase error.

On an error, status 1 does not say what happened. The host asks with a separate REQUEST SENSE command and gets three codes: the error class (sense key) and the qualifiers (ASC, ASCQ).

Connection, byte by byte. This is how the host model starts — INQUIRY, then capacity, then MODE SENSE:

  CBW: 55 53 42 43 00 00 8F 19 24 00 00 00 80 00 06 12 00 00 00 24 00 00 00 00 00 00 00 00 00 00 00
  CSW: 55 53 42 53 00 00 8F 19 00 00 00 00 00
INQUIRY: 36 байт, статус 0: «HANDBOOK» «U585 LOGGER     » «1.0 », съёмный 1
  CBW: 55 53 42 43 02 00 8F 19 08 00 00 00 80 00 0A 25 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  CSW: 55 53 42 53 02 00 8F 19 00 00 00 00 00
  данные: 00 00 08 06 00 00 02 00
READ CAPACITY(10): последний сектор 2054, размер 512 → 2055 секторов
  CBW: 55 53 42 43 04 00 8F 19 C0 00 00 00 80 00 06 1A 00 3F 00 C0 00 00 00 00 00 00 00 00 00 00 00
  CSW: 55 53 42 53 04 00 8F 19 BC 00 00 00 00
MODE SENSE(6): 4 байт, остаток 188, байт 2 = 80 → защита от записи 1

How to read the first CBW: 55 53 42 43 — "USBC"; tag 00 00 8F 19; expects 0x24 = 36 bytes; 80 — data to the host; unit 0; a 6-byte command: 12 00 00 00 24 00 — INQUIRY for 36 bytes. The CSW has the same tag, residue 0, status 0. For MODE SENSE the host asked for 192 bytes (0xC0), the device returned 4, and the CSW residue is BC 00 00 00 = 188.

Commands the device understands. The codes are from MDK usb_msc.h and ST usbd_msc_scsi.h:

Command Code Device response
TEST UNIT READY 0x00 ready; after "Eject" — error 02/3A (medium not present)
REQUEST SENSE 0x03 18 bytes: 0x70, error class in byte 2, ASC in byte 12
INQUIRY 0x12 36 bytes: disk, removable, "HANDBOOK", "U585 LOGGER", "1.0"
MODE SENSE(6) / (10) 0x1A / 0x5A header 4 / 8 bytes, WP bit = 0x80 — protected
START STOP UNIT 0x1B LoEj = 1, Start = 0 — "Eject"
PREVENT ALLOW MEDIUM REMOVAL, VERIFY(10), SYNCHRONIZE CACHE(10) 0x1E, 0x2F, 0x35 nothing to do — passed
READ FORMAT CAPACITIES 0x23 12 bytes: 2055 blocks of 512, "formatted"
READ CAPACITY(10) 0x25 last block number 2054 and size 512
READ(10) 0x28 sectors from fatgen
WRITE(10) 0x2A error 07/27 — write-protected
others — error 05/20 — no such command

The whole command parser:

static void scsi_command(const uint8_t *cb, bool in) {
    uint8_t op = cb[0];
    if (!in && m.dlen) {                    /* данные от хоста: WRITE(10), MODE SELECT … */
        if (op == 0x2A) fail(SK_DATA_PROTECT, 0x27, false);   /* WRITE PROTECTED */
        else fail(SK_ILLEGAL, 0x20, false);
        return;
    }
    if (m.ua && op != 0x12 && op != 0x03) { /* INQUIRY и REQUEST SENSE проходят всегда */
        m.ua = 0;
        fail(SK_UNIT_ATTENTION, 0x28, in);  /* MEDIUM MAY HAVE CHANGED */
        return;
    }
    switch (op) {
    case 0x00:                              /* TEST UNIT READY */
        if (m.ejected) fail(SK_NOT_READY, 0x3A, in);          /* MEDIUM NOT PRESENT */
        else m.state = ST_CSW;
        break;
    case 0x03: {                            /* REQUEST SENSE: фиксированный формат, 18 байт */
        uint8_t s[18] = {0x70, 0, m.key, 0, 0, 0, 0, 10, 0, 0, 0, 0, m.asc, 0, 0, 0, 0, 0};
        m.key = m.asc = 0;
        reply(s, sizeof s, cb[4]);
        break;
    }
    case 0x12:                              /* INQUIRY */
        reply(INQUIRY, sizeof INQUIRY, be16(cb + 3));
        break;
    case 0x1A: {                            /* MODE SENSE(6): заголовок 4 байта, бит WP = 0x80 */
        static const uint8_t s[4] = {3, 0, 0x80, 0};
        reply(s, sizeof s, cb[4]);
        break;
    }
    case 0x5A: {                            /* MODE SENSE(10): заголовок 8 байт */
        static const uint8_t s[8] = {0, 6, 0, 0x80, 0, 0, 0, 0};
        reply(s, sizeof s, be16(cb + 7));
        break;
    }
    case 0x1B:                              /* START STOP UNIT: LoEj = 1, Start = 0 — «Извлечь» */
        if ((cb[4] & 0x03u) == 0x02u) m.ejected = 1;
        m.state = ST_CSW;
        break;
    case 0x1E:                              /* PREVENT ALLOW MEDIUM REMOVAL */
    case 0x2F:                              /* VERIFY(10) */
    case 0x35:                              /* SYNCHRONIZE CACHE(10) */
        m.state = ST_CSW;
        break;
    case 0x23: {                            /* READ FORMAT CAPACITIES */
        uint8_t s[12] = {0, 0, 0, 8, 0, 0, 0, 0, 0x02, 0, SECTOR >> 8, SECTOR & 0xFF};
        put_be32(s + 4, TOTAL_SECTORS);     /* 0x02 — носитель размечен */
        reply(s, sizeof s, be16(cb + 7));
        break;
    }
    case 0x25: {                            /* READ CAPACITY(10): НОМЕР последнего сектора и размер */
        uint8_t s[8];
        put_be32(s, TOTAL_SECTORS - 1u);
        put_be32(s + 4, SECTOR);
        reply(s, sizeof s, sizeof s);
        break;
    }
    case 0x28: {                            /* READ(10) */
        uint32_t lba = be32(cb + 2), cnt = be16(cb + 7);
        if (m.ejected) {
            fail(SK_NOT_READY, 0x3A, in);
        } else if (lba >= TOTAL_SECTORS || cnt > TOTAL_SECTORS - lba) {
            fail(SK_ILLEGAL, 0x21, in);     /* LOGICAL BLOCK ADDRESS OUT OF RANGE */
        } else if (m.dlen != cnt * SECTOR) {
            m.status = 2;                   /* хост и команда не сошлись в длине: ошибка фазы */
            if (m.dlen) m.in_halt = 1;
            m.state = ST_CSW;
        } else {
            m.lba = lba;
            m.left = cnt;
            m.pos = m.len = 0;
            m.state = cnt ? ST_DATA : ST_CSW;
        }
        break;
    }
    default:
        fail(SK_ILLEGAL, 0x20, in);         /* INVALID COMMAND OPERATION CODE */
    }
}

Two helper functions decide what goes to the host. reply returns no more than the command asked for and than the CBW promised; fail stores the error for REQUEST SENSE and stops (STALLs) the endpoint the data would have gone through — this way the host understands that no data will come, clears the halt and collects the CSW:

/* Короткий ответ: не больше, чем просил хост в команде и чем он ждёт по CBW */
static void reply(const uint8_t *d, uint32_t n, uint32_t alloc) {
    if (n > alloc) n = alloc;
    if (n > m.dlen) n = m.dlen;
    memcpy(m.buf, d, n);
    m.pos = 0;
    m.len = n;
    m.left = 0;
    m.state = n ? ST_DATA : ST_CSW;
}

/* Ошибка: запомнить причину, остановить точку, по которой шли бы данные, ответить CSW = 1 */
static void fail(uint8_t key, uint8_t asc, bool in) {
    m.key = key;
    m.asc = asc;
    m.status = 1;
    if (m.dlen) {
        if (in) m.in_halt = 1;
        else m.out_halt = 1;
    }
    m.state = ST_CSW;
}

Receiving the CBW and handing out packets to Bulk-IN. The sector for READ(10) is built into a 512-byte buffer once the previous one has been sent, and it goes out as eight packets:

int msc_out(const uint8_t *p, uint32_t n) {
    if (m.out_halt) return -1;
    if (m.state != ST_CBW) return 0;
    if (n != 31u || get32(p) != CBW_SIG) {  /* не CBW: обе точки стоят до сброса BOT */
        m.in_halt = m.out_halt = 1;
        m.state = ST_DEAD;
        return 0;
    }
    m.tag = get32(p + 4);
    m.dlen = get32(p + 8);
    m.sent = 0;
    m.status = 0;
    scsi_command(p + 15, (p[12] & 0x80u) != 0);
    return 0;
}

int msc_in(uint8_t *pkt) {
    if (m.in_halt) return -1;
    if (m.state == ST_DATA) {
        if (m.pos == m.len) {               /* буфер отдан: считать следующий сектор */
            disk_read(m.lba++, m.buf);
            m.left--;
            m.pos = 0;
            m.len = SECTOR;
        }
        uint32_t n = m.len - m.pos;
        if (n > MSC_PACKET) n = MSC_PACKET;
        memcpy(pkt, m.buf + m.pos, n);
        m.pos += n;
        m.sent += n;
        if (m.pos == m.len && m.left == 0) m.state = ST_CSW;
        return (int)n;
    }
    if (m.state == ST_CSW) {
        put32(pkt, CSW_SIG);
        put32(pkt + 4, m.tag);              /* dCSWTag — как в CBW */
        put32(pkt + 8, m.dlen - m.sent);    /* dCSWDataResidue: сколько не дослали */
        pkt[12] = m.status;
        m.state = ST_CBW;
        return 13;
    }
    return 0;
}

void msc_clear_halt(uint8_t ep) {
    if (m.state == ST_DEAD) return;         /* после плохого CBW помогает только сброс BOT */
    if (ep & 0x80u) m.in_halt = 0;
    else m.out_halt = 0;
}

The module knows neither the OTG registers nor the library: the endpoint driver hands it Bulk-OUT packets and takes Bulk-IN packets from it. That is why it can be tested on a PC.


198ф.3 FAT12 "on the fly": sector layout

How much space is needed. A CSV line is 34 bytes, the header is 26:

  последняя: 00001499,00014990,-001.31,054.29

The file holds no more than 30720 records (why not all 32768 — see 198ф.4). The rest is computed in sequence, and the fatgen.h macros do it the same way:

What Calculation Result
largest file 26 + 34 × 30720 1044506 bytes
clusters of 512 bytes 1044506 / 512 = 2040.05 → round up 2041
FAT12: 12 bits per cluster, plus two reserved (2041 + 2) × 1.5 = 3064.5 → 3065 bytes 6 sectors
boot sector sector 0
two FAT copies 2 × 6 sectors 1…12
root directory: 16 entries of 32 bytes 16 × 32 = 512 sector 13
data: cluster 2 = sector 14 14 + 2041 2055 sectors = 1052160 bytes

2041 clusters is fewer than 4085, so this is FAT12: the volume type is determined by the number of clusters, not by the label in the boot sector. FatFs uses the same limit — MAX_FAT12 0xFF5, with a note that it was chosen to match the behavior of DOS and Windows. A cluster is one sector: that way a file line and a disk sector are computed with one formula.

#define SECTOR        512u                              /* байт в секторе и в кластере */
#define HDR_LEN       26u                               /* "seq,time_s,temp_C,rh_pct\r\n" */
#define LINE_LEN      34u                               /* "00000001,00000010,+023.45,045.10\r\n" */
#define SNAP_GUARD    2048u                             /* запас кольца на время чтения хостом */
#define SNAP_MAX      (LOG_CAP - SNAP_GUARD)            /* записей в файле не больше */
#define FILE_MAX      (HDR_LEN + LINE_LEN * SNAP_MAX)
#define CLUSTERS      ((FILE_MAX + SECTOR - 1u) / SECTOR)
#define FAT_BYTES     ((3u * (CLUSTERS + 2u) + 1u) / 2u) /* 12 бит на кластер, два первых — служебные */
#define FAT_SECTORS   ((FAT_BYTES + SECTOR - 1u) / SECTOR)
#define ROOT_ENTRIES  16u                               /* корневой каталог — ровно один сектор */
#define FAT_LBA       1u                                /* сектор 0 — загрузочный */
#define ROOT_LBA      (FAT_LBA + 2u * FAT_SECTORS)      /* две копии FAT */
#define DATA_LBA      (ROOT_LBA + 1u)                   /* кластер 2 */
#define TOTAL_SECTORS (DATA_LBA + CLUSTERS)

Boot sector. Only the BPB and the 0x55AA signature; there is no boot code:

static void boot_sector(uint8_t *b) {
    static const uint8_t jmp_oem[11] = {0xEB, 0x3C, 0x90, 'M', 'S', 'D', 'O', 'S', '5', '.', '0'};
    memcpy(b, jmp_oem, sizeof jmp_oem);     /* без EB xx 90 том не признают за FAT */
    put16(b + 11, SECTOR);                  /* BPB_BytsPerSec */
    b[13] = 1;                              /* BPB_SecPerClus */
    put16(b + 14, FAT_LBA);                 /* BPB_RsvdSecCnt: только загрузочный сектор */
    b[16] = 2;                              /* BPB_NumFATs */
    put16(b + 17, ROOT_ENTRIES);            /* BPB_RootEntCnt */
    put16(b + 19, TOTAL_SECTORS);           /* BPB_TotSec16 — то же число, что в READ CAPACITY */
    b[21] = 0xF8;                           /* BPB_Media: 0xF8, как в образе примера пакета */
    put16(b + 22, FAT_SECTORS);             /* BPB_FATSz16 */
    put16(b + 24, 63);                      /* BPB_SecPerTrk и BPB_NumHeads — для старых систем */
    put16(b + 26, 255);
    b[36] = 0x80;                           /* BS_DrvNum */
    b[38] = 0x29;                           /* BS_BootSig: дальше идут номер и метка тома */
    put32(b + 39, VOLUME_ID);
    memcpy(b + 43, VOLUME_LABEL, 11);
    memcpy(b + 54, "FAT12   ", 8);
    b[510] = 0x55;
    b[511] = 0xAA;
}

What it looks like in the image read back:

  загрузочный, байты 0…35: EB 3C 90 4D 53 44 4F 53 35 2E 30 00 02 01 01 00 02 10 00 07 08 F8 06 00 3F 00 FF 00 00 00 00 00 00 00 00 00
  загрузочный, байты 36…61: 80 00 29 29 09 26 20 4C 4F 47 47 45 52 20 20 20 20 20 46 41 54 31 32 20 20 20; 510…511: 55 AA

00 02 — 512 bytes per sector, 01 — sectors per cluster, 01 00 — one reserved sector, 02 — two FATs, 10 00 — 16 root entries, 07 08 = 2055 sectors, F8 — media type, 06 00 — 6 sectors per FAT. Then 29 — followed by the volume serial number 29 09 26 20 and the label LOGGER.

FAT12. The file occupies consecutive clusters, so the table is simply "next = current + 1", and the last one holds the end-of-chain mark 0xFFF. The catch of FAT12 is the packing: two 12-bit entries take three bytes. The function computes any byte of the table from its offset, without building the table in memory:

/* Цепочка файла: кластеры 2, 3, … подряд, у последнего — конец цепочки 0xFFF */
static uint32_t fat_entry(uint32_t c) {
    if (c == 0) return 0xFF8;               /* 0xF00 | BPB_Media */
    if (c == 1) return 0xFFF;
    if (c >= 2u + snap_clusters) return 0;  /* свободный кластер */
    return c == 1u + snap_clusters ? 0xFFF : c + 1u;
}

/* Байт FAT12 по смещению: три байта хранят два 12-битных элемента */
static uint8_t fat_byte(uint32_t off) {
    uint32_t c = off / 3u * 2u;
    switch (off % 3u) {
    case 0:
        return (uint8_t)fat_entry(c);
    case 1:
        return (uint8_t)((fat_entry(c) >> 8) | (fat_entry(c + 1u) << 4));
    default:
        return (uint8_t)(fat_entry(c + 1u) >> 4);
    }
}
  FAT, байты 0…11: F8 FF FF 03 40 00 05 60 00 07 80 00
  элемент 341 на стыке секторов 1 и 2: байты 511 = 61 и 512 = 15 -> 156 (следующий кластер 342)

The first three bytes F8 FF FF are the reserved entries 0xFF8 and 0xFFF. Next, 03 40 00 means clusters 2 → 3 and 3 → 4. Entry 341 starts in the last byte of the first FAT sector and ends in the first byte of the second: a generator that builds the FAT sector by sector from entry numbers gets this wrong. Computing from the byte offset cannot make this mistake.

Directory and data. The root has two entries: the volume label and the file with the attributes "read-only" and "archive". Without a real-time clock the date had to be hard-coded: 29.09.2026, 12:00.

static void root_dir(uint8_t *b) {
    memcpy(b, VOLUME_LABEL, 11);
    b[11] = 0x08;                           /* метка тома */
    uint8_t *e = b + 32;
    memcpy(e, FILE_NAME, 11);
    e[11] = 0x21;                           /* только чтение + архивный */
    put16(e + 14, FAT_TIME);                /* создан */
    put16(e + 16, FAT_DATE);
    put16(e + 18, FAT_DATE);                /* последний доступ */
    put16(e + 22, FAT_TIME);                /* изменён */
    put16(e + 24, FAT_DATE);
    put16(e + 26, 2);                       /* первый кластер */
    put32(e + 28, snap_size);
}
  каталог, LOG.CSV: 4C 4F 47 20 20 20 20 20 43 53 56 21 00 00 00 60 3D 5D 3D 5D 00 00 00 60 3D 5D 02 00 52 C7 00 00

21 — attributes, 3D 5D — date 0x5D3D, 02 00 — first cluster, 52 C7 00 00 = 51026 bytes. The file bytes are computed from the offset: first the header, then line number (offset − 26) / 34; past the end of the file — zeros. Building a whole sector:

/* Байты файла [off, off + len): заголовок, потом строки по 34 байта; за концом файла — нули */
static void csv_fill(uint32_t off, uint8_t *dst, uint32_t len) {
    char ln[LINE_LEN];
    uint32_t cur = UINT32_MAX;
    for (uint32_t i = 0; i < len; i++, off++) {
        if (off >= snap_size) {
            dst[i] = 0;
        } else if (off < HDR_LEN) {
            dst[i] = (uint8_t)HDR[off];
        } else {
            uint32_t k = (off - HDR_LEN) / LINE_LEN;
            if (k != cur) {
                make_line(snap_first + k, ln);
                cur = k;
            }
            dst[i] = (uint8_t)ln[(off - HDR_LEN) % LINE_LEN];
        }
    }
}

void disk_read(uint32_t lba, uint8_t *b) {
    memset(b, 0, SECTOR);
    if (lba == 0) {
        boot_sector(b);
    } else if (lba < ROOT_LBA) {            /* обе копии FAT одинаковы */
        uint32_t off = ((lba - FAT_LBA) % FAT_SECTORS) * SECTOR;
        for (uint32_t i = 0; i < SECTOR; i++) b[i] = fat_byte(off + i);
    } else if (lba == ROOT_LBA) {
        root_dir(b);
    } else if (lba < TOTAL_SECTORS) {
        csv_fill((lba - DATA_LBA) * SECTOR, b, SECTOR);
    }
}

198ф.4 Where to keep the log: U585 memory

The disk needs no space — the log does. What the STM32U585 has according to stm32u585xx.h and DS13086:

Memory Size For the log
SRAM1 192 KB program variables and the stack
SRAM2 64 KB
SRAM3 512 KB ring of 32768 × 8 bytes = 256 KB
SRAM4 16 KB
flash memory 2 MB, two banks of 1 MB, 8 KB page programming in 128 bits (16 bytes), 118 µs; 10000 erase cycles (tables 88 and 89)
MX25LM51245G on the board 64 MB, 256-byte page per the board pack driver

A record in RAM is 8 bytes: time, temperature and humidity in hundredths. The record number is not stored; it follows from the position in the ring. In the file the same record takes 34 bytes — 4.25 times more: CSV is cheaper to compute than to store. A 256 KB ring with one record every 10 seconds is 32768 × 10 s = 91 hours.

A log in RAM is lost when power is removed. If that matters, the ring moves to flash memory: 16 bytes per record is exactly one 128-bit programming operation; a 1 MB bank is 128 pages and 65536 records; a page is erased once per revolution of the ring, and 10000 cycles are enough for 655360000 records. The disk generator does not change — only log_get changes. This variant was not built.

The ring. The main loop writes, the USB handler reads in its own thread. The counter is incremented only after the record has been fully stored in the slot, and the reader checks that the slot was not overwritten while it was being copied:

void log_append(uint32_t t_s, int16_t temp_c100, uint16_t rh_c100) {
    log_rec_t *r = &ring[count % LOG_CAP];
    r->t_s = t_s;
    r->temp_c100 = temp_c100;
    r->rh_c100 = rh_c100;
    BARRIER();                              /* сначала запись, потом счётчик */
    count = count + 1u;                     /* 32-битная запись на Cortex-M33 неделима */
}

uint32_t log_count(void) {
    return count;
}

bool log_get(uint32_t seq, log_rec_t *r) {
    uint32_t c = count;
    if (seq >= c || c - seq >= LOG_CAP) return false;   /* самую старую ячейку сейчас могут переписывать */
    *r = ring[seq % LOG_CAP];
    BARRIER();
    return count - seq < LOG_CAP;           /* пока копировали, ячейку не тронули */
}

The oldest slot is treated as already lost: log_append is writing into it right now.

The snapshot. The host reads the FAT and the directory once, and the file later, in pieces. If the file size grew while it was being read, the directory and the data would disagree. So on connection the device takes a snapshot — which records are visible — and does not change it until the host releases the disk:

void disk_snapshot(void) {
    uint32_t cnt = log_count();
    snap_n = cnt < SNAP_MAX ? cnt : SNAP_MAX;
    snap_first = cnt - snap_n;
    snap_size = HDR_LEN + LINE_LEN * snap_n;
    snap_clusters = (snap_size + SECTOR - 1u) / SECTOR;
}

The snapshot is at most 30720 records: the remaining 2048 slots are a reserve for the time while the host is reading and the log keeps writing. With one record every 10 seconds that is 2048 × 10 s = 5.7 hours. If the reserve runs out, the old lines of the file do not lie: they are honestly replaced with dashes of the same length.

Building for Cortex-M33. The ring is placed in SRAM3 with a section attribute and a scatter file:

; Раздел 198ф: STM32U585AI — флеш-память 2 МБ с 0x08000000, SRAM1 192 КБ с 0x20000000,
; SRAM3 512 КБ с 0x20040000 (stm32u585xx.h пакета STM32U5xx_DFP 2.2.1). Кольцо журнала — в SRAM3.
LR_FLASH 0x08000000 0x00200000 {
  ER_FLASH 0x08000000 0x00200000 {
    *(+RO)
  }
  RW_SRAM1 0x20000000 0x00030000 {
    *(+RW +ZI)
  }
  RW_SRAM3 0x20040000 0x00080000 {
    *(.bss.sram3)
  }
}

armclang 6.24, -Os, two variants (without the USB driver and without the library):

== вариант mdk: ER_FLASH с 0x08000000 — 1556 байт, RW_SRAM1 с 0x20000000 — 528 байт, RW_SRAM3 с 0x20040000 — 262144 байт
== вариант bot: ER_FLASH с 0x08000000 — 2332 байт, RW_SRAM1 с 0x20000000 — 568 байт, RW_SRAM3 с 0x20040000 — 262144 байт

With the MDK library our code is 1556 bytes of flash memory and 528 bytes of SRAM1, of which 512 is the sector buffer for the library. With our own BOT and SCSI it is 2332 bytes and 568 bytes. The ring is 262144 bytes of SRAM3 in both variants.


198ф.5 Writes from the PC: why they are dangerous and how to forbid them

The disk is computed, but the host does not know that. A write from the PC — a new file, an edit of LOG.CSV, a system's service folder — changes the FAT and the directory in the host's cache and sends sectors to the device. The device has nowhere to put them: on the next read it builds its own FAT again, and the host sees a volume it never wrote. Hence the rule: protect at every level at once.

  1. MODE SENSE: bit 7 of byte 2 (byte 3 in MODE SENSE(10)) — the medium is write-protected. A host that asks MODE SENSE on connection knows this before the first write.
  2. WRITE(10): rejection with the OUT endpoint stalled and error 07/27 (DATA PROTECT, WRITE PROTECTED) — in case the host tries anyway.
  3. File attribute "read-only" — for programs on the PC.

A write attempt in the model:

  CBW: 55 53 42 43 18 00 8F 19 00 02 00 00 00 00 0A 2A 00 00 00 00 0E 00 00 01 00 00 00 00 00 00 00
  CSW: 55 53 42 53 18 00 8F 19 00 02 00 00 01
WRITE(10) сектора 14: принято 0 байт, STALL OUT 1, статус 1, остаток 512
  REQUEST SENSE (после записи): ключ 07, ASC 27, ASCQ 00, статус 0

The host promised 512 bytes (00 02 00 00, direction 00 — to the device), the device accepted none of them: the OUT endpoint is stalled, the CSW has residue 00 02 00 00 = 512 and status 1.

How the libraries do it. In MDK protection is the USBD_MSC_MEDIA_PROTECTED bit in the USBD_MSC0_CheckMedia return value; what exactly the library answers to MODE SENSE cannot be checked from the sources — it ships precompiled. In the ST library V2.4.2 (the F4 copy in betaflight) SCSI_ModeSense6 copies eight zero bytes of MSC_Mode_Sense6_data and does not set the WP bit even if IsWriteProtected returned 1: the host learns about the protection only when a write has already been rejected. In the F7 and G4 copies of the same repository the bit is added by a line with the comment "set bit 7 of the device configuration byte to indicate write protection". ST reports a rejected write with key 02 (NOT READY) and code 0x27; our handler uses key 07 (DATA PROTECT).

With the MDK library the device's whole "disk" is the handlers in USBD_User_MSC_0.c. The complete file:

/* Раздел 198ф: обработчики класса MSC библиотеки USB из MDK-Middleware (пример пакета платы
 * B-U585I-IOT02A, «Mass Storage»). BOT и SCSI библиотека ведёт сама; «диск» отдаёт fatgen. */
#include "rl_usb.h"
#include "fatgen.h"

static uint32_t block_buf[SECTOR / 4u];     /* буфер библиотеки: один сектор */

void USBD_MSC0_Initialize(void) {
    disk_snapshot();
}

void USBD_MSC0_Uninitialize(void) {
}

bool USBD_MSC0_GetCacheInfo(uint32_t *buffer, uint32_t *size) {
    *buffer = (uint32_t)block_buf;
    *size = sizeof block_buf;
    return true;
}

bool USBD_MSC0_GetMediaCapacity(uint32_t *block_count, uint32_t *block_size) {
    *block_count = TOTAL_SECTORS;           /* то же число, что в BPB_TotSec16 */
    *block_size = SECTOR;
    return true;
}

bool USBD_MSC0_Read(uint32_t lba, uint32_t cnt, uint8_t *buf) {
    if (lba >= TOTAL_SECTORS || cnt > TOTAL_SECTORS - lba) return false;
    for (uint32_t i = 0; i < cnt; i++) disk_read(lba + i, buf + i * SECTOR);
    return true;
}

bool USBD_MSC0_Write(uint32_t lba, uint32_t cnt, const uint8_t *buf) {
    (void)lba;
    (void)cnt;
    (void)buf;
    return false;                           /* диск только для чтения */
}

uint32_t USBD_MSC0_CheckMedia(void) {
    return USBD_MSC_MEDIA_READY | USBD_MSC_MEDIA_PROTECTED;
}

198ф.6 Test run on a PC: host model, FatFs and a Python parser

host.c is the host model. It calls msc_out and msc_in the way a USB driver would, and clears endpoint halts the way a host does. The scenario:

  1. 1500 records in the log; connection: INQUIRY, TEST UNIT READY, READ CAPACITY, READ FORMAT CAPACITIES, MODE SENSE, sector 0;
  2. the whole disk via READ(10) in chunks of 128 sectors, while the log writes one record per command;
  3. a write, a read past the end of the disk, an unknown command;
  4. "Eject", the device adds 3000 records and takes a new snapshot, the whole disk again;
  5. the ring overflows: 40000 more records, a new snapshot, and another 2052 records before reading — more than the reserve;
  6. a corrupted CBW and a protocol reset.
disk1.img: 2055 секторов прочитано командами READ(10): 17 команд, 16457 пакетов IN
  снимок: первая запись 0, записей 1500, файл 51026 байт; в журнале при конце чтения 1517
каталог при подключённом диске: LOG.CSV 51026 байт (журнал уже 1517 записей)

17 READ(10) commands of 128 sectors and 16457 IN packets: 2055 × 8 data packets and 17 CSWs. While the host was reading, the log grew to 1517 records, yet the file in the directory stayed at 51026 bytes = 26 + 34 × 1500 — the snapshot holds.

Eject and insert again. The host releases the disk with START STOP UNIT, the device sees it, takes a new snapshot and "inserts" the medium:

«Извлечь»: PREVENT ALLOW 0, START STOP UNIT LoEj=1 — статус 0, прибор видит извлечение: 1
TEST UNIT READY (после извлечения): статус CSW 1
  REQUEST SENSE (после извлечения): ключ 02, ASC 3A, ASCQ 00, статус 0
прибор: журнал 4517 записей, новый снимок, носитель «вставлен» снова
TEST UNIT READY (первая команда после вставки): статус CSW 1
  REQUEST SENSE (первая команда после вставки): ключ 06, ASC 28, ASCQ 00, статус 0
TEST UNIT READY (вторая): статус CSW 0

02/3A — medium not present; 06/28 — attention, the medium may have changed; after that the disk is ready again, and it holds 4517 records.

Parsing the images. The first way is FatFs R0.15: it mounts the image, reads the label, the free space and LOG.CSV through f_read in 4096-byte chunks. It never tried to write to the disk:

disk1.img: f_mount 0, тип 1 (1 = FAT12), кластер 1 секторов, элементов FAT 2043
  метка «LOGGER», свободных кластеров 1941; LOG.CSV: f_stat 0, 51026 байт, атрибуты 21, дата 5D3D
  f_read 0: 51026 байт, CRC-32 290CD8BE; записей на диск: 0
disk3.img: f_mount 0, тип 1 (1 = FAT12), кластер 1 секторов, элементов FAT 2043
  метка «LOGGER», свободных кластеров 0; LOG.CSV: f_stat 0, 1044506 байт, атрибуты 21, дата 5D3D
  f_read 0: 1044506 байт, CRC-32 9ED3FEDF; записей на диск: 0

The second way is fatread.py, our own parser that does not use fatgen.c code: BPB fields, 12-bit FAT entries, the directory, the cluster chain. The script builds the expected file itself with the same record formula as the model:

  LOG.CSV: 51026 байт, атрибуты 21, дата 5D3D, время 6000, кластеры 2…101 (100 подряд: True), свободных 1941
  совпал байт в байт с ожидаемым: True; строк 1501, затёртых 0; CRC-32 290CD8BE
  LOG.CSV: 1044506 байт, атрибуты 21, дата 5D3D, время 6000, кластеры 2…2042 (2041 подряд: True), свободных 0
  совпал байт в байт с ожидаемым: True; строк 30721, затёртых 5; CRC-32 9ED3FEDF
  первые строки: seq,time_s,temp_C,rh_pct | --------,--------,-------,------ | 00013819,00138190,-012.11,041.49

The CRC-32 of the file from FatFs and from Python matched in all three images. In the third image the snapshot is the last 30720 records out of 44534, the first being 13814. While the host was not reading, the log added 2052 records, 4 more than the reserve, and the 5 oldest lines became dashes (four beyond the reserve and the oldest slot, which is being written). The remaining 30715 lines are exactly those in the ring.

Corrupted CBW. A wrong signature stalls both endpoints, and simply clearing the halt does not help; only a protocol reset helps (a class request 0xFF on endpoint zero):

после мусора: IN -1, OUT -1
CLEAR_FEATURE без сброса: IN -1
TEST UNIT READY (после сброса BOT): статус CSW 0

198ф.7 Pitfalls

New records do not appear while the disk is connected. The operating system reads the FAT and the directory and keeps them cached. Changing them under a connected host is not allowed — you get a file whose directory and data come from different moments. A new snapshot only after "Eject", followed right away by UNIT ATTENTION 06/28. How exactly a particular operating system rereads the disk after 06/28 cannot be checked on the model.

"Eject" does not unplug the cable. After START STOP UNIT with LoEj the host is still on the bus and asks TEST UNIT READY. A "ready" answer after ejection looks like a new disk with no warning — you need 02/3A until the device "inserts" the medium again. Pulling the cable without "Eject" does no harm to a read-only disk: there is nothing unsaved.

The size from READ CAPACITY and from the BPB disagree. In the board pack example image the volume is declared as 64 sectors, while the capacity is 16. A host that starts writing to free clusters according to the FAT will go past the end of the medium. Here the single number 2055 is used in the BPB, in READ CAPACITY and in READ FORMAT CAPACITIES.

READ CAPACITY returns the number of the last block, not the block count. 2054, not 2055. An off-by-one error — and the host considers the last sector extra or missing.

The block size is 512. A disk sector does not have to match a flash memory page (8 KB on the U585, 256 bytes on the MX25LM51245G). The host gets 512-byte sectors, and how they are laid out in memory is the device's business; a computed disk does not have this problem at all.

A FAT12 entry across a sector boundary. 12 bits is one and a half bytes, and entry 341 is split between the first and second FAT sectors. A generator that builds a FAT sector from entry numbers loses half of it. Compute from the byte offset.

More than 4084 clusters is no longer FAT12. The volume type is determined by the number of clusters. If the log grows, use either a larger cluster or FAT16; the label FAT12 in the boot sector decides nothing.

The host waits for data that never comes. If a command failed but the CBW promised data, a single CSW with an error is not enough: the data endpoint must be stalled, otherwise the host will wait for the promised bytes. In the model this is done for WRITE(10) (the OUT endpoint), a read past the end and an unknown command (the IN endpoint).

Write protection only in WRITE(10). If MODE SENSE says nothing about protection, the host learns about it from a rejection in the middle of work. The WP bit belongs in MODE SENSE from the moment of connection; the ST library V2.4.2 does not set it.


198ф.8 Summary

Full source code

fatgen.h — the "on the fly" FAT12 disk layout.

/* Раздел 198ф: диск FAT12 только для чтения, который собирается «на лету» из журнала.
 * В памяти нет ни одного сектора: каждый считается в момент запроса READ(10). */
#ifndef FATGEN_H
#define FATGEN_H
#include <stdint.h>
#include "logring.h"

#define SECTOR        512u                              /* байт в секторе и в кластере */
#define HDR_LEN       26u                               /* "seq,time_s,temp_C,rh_pct\r\n" */
#define LINE_LEN      34u                               /* "00000001,00000010,+023.45,045.10\r\n" */
#define SNAP_GUARD    2048u                             /* запас кольца на время чтения хостом */
#define SNAP_MAX      (LOG_CAP - SNAP_GUARD)            /* записей в файле не больше */
#define FILE_MAX      (HDR_LEN + LINE_LEN * SNAP_MAX)
#define CLUSTERS      ((FILE_MAX + SECTOR - 1u) / SECTOR)
#define FAT_BYTES     ((3u * (CLUSTERS + 2u) + 1u) / 2u) /* 12 бит на кластер, два первых — служебные */
#define FAT_SECTORS   ((FAT_BYTES + SECTOR - 1u) / SECTOR)
#define ROOT_ENTRIES  16u                               /* корневой каталог — ровно один сектор */
#define FAT_LBA       1u                                /* сектор 0 — загрузочный */
#define ROOT_LBA      (FAT_LBA + 2u * FAT_SECTORS)      /* две копии FAT */
#define DATA_LBA      (ROOT_LBA + 1u)                   /* кластер 2 */
#define TOTAL_SECTORS (DATA_LBA + CLUSTERS)

void     disk_snapshot(void);                           /* зафиксировать, какие записи видит хост */
uint32_t disk_file_size(void);
uint32_t disk_first_seq(void);
void     disk_read(uint32_t lba, uint8_t *buf);         /* один сектор, 512 байт */

#endif

fatgen.c — the FAT12 sector generator.

/* Раздел 198ф: генератор FAT12. Сектор 0 — загрузочный, 1…12 — две копии FAT, 13 — корневой
 * каталог, с 14 — данные файла LOG.CSV кластерами по одному сектору подряд. */
#include <string.h>
#include "fatgen.h"

#define VOLUME_ID    0x20260929u
#define VOLUME_LABEL "LOGGER     "
#define FILE_NAME    "LOG     CSV"
#define FAT_DATE     ((uint16_t)(((2026u - 1980u) << 9) | (9u << 5) | 29u))   /* 29.09.2026 */
#define FAT_TIME     ((uint16_t)(12u << 11))                                  /* 12:00:00 */

static const char HDR[HDR_LEN + 1u] = "seq,time_s,temp_C,rh_pct\r\n";
static const char LOST[LINE_LEN + 1u] = "--------,--------,-------,------\r\n";

static uint32_t snap_first, snap_n, snap_size, snap_clusters;

static void put16(uint8_t *p, uint32_t v) {
    p[0] = (uint8_t)v;
    p[1] = (uint8_t)(v >> 8);
}
static void put32(uint8_t *p, uint32_t v) {
    put16(p, v);
    put16(p + 2, v >> 16);
}

void disk_snapshot(void) {
    uint32_t cnt = log_count();
    snap_n = cnt < SNAP_MAX ? cnt : SNAP_MAX;
    snap_first = cnt - snap_n;
    snap_size = HDR_LEN + LINE_LEN * snap_n;
    snap_clusters = (snap_size + SECTOR - 1u) / SECTOR;
}
uint32_t disk_file_size(void) {
    return snap_size;
}
uint32_t disk_first_seq(void) {
    return snap_first;
}

static void boot_sector(uint8_t *b) {
    static const uint8_t jmp_oem[11] = {0xEB, 0x3C, 0x90, 'M', 'S', 'D', 'O', 'S', '5', '.', '0'};
    memcpy(b, jmp_oem, sizeof jmp_oem);     /* без EB xx 90 том не признают за FAT */
    put16(b + 11, SECTOR);                  /* BPB_BytsPerSec */
    b[13] = 1;                              /* BPB_SecPerClus */
    put16(b + 14, FAT_LBA);                 /* BPB_RsvdSecCnt: только загрузочный сектор */
    b[16] = 2;                              /* BPB_NumFATs */
    put16(b + 17, ROOT_ENTRIES);            /* BPB_RootEntCnt */
    put16(b + 19, TOTAL_SECTORS);           /* BPB_TotSec16 — то же число, что в READ CAPACITY */
    b[21] = 0xF8;                           /* BPB_Media: 0xF8, как в образе примера пакета */
    put16(b + 22, FAT_SECTORS);             /* BPB_FATSz16 */
    put16(b + 24, 63);                      /* BPB_SecPerTrk и BPB_NumHeads — для старых систем */
    put16(b + 26, 255);
    b[36] = 0x80;                           /* BS_DrvNum */
    b[38] = 0x29;                           /* BS_BootSig: дальше идут номер и метка тома */
    put32(b + 39, VOLUME_ID);
    memcpy(b + 43, VOLUME_LABEL, 11);
    memcpy(b + 54, "FAT12   ", 8);
    b[510] = 0x55;
    b[511] = 0xAA;
}

/* Цепочка файла: кластеры 2, 3, … подряд, у последнего — конец цепочки 0xFFF */
static uint32_t fat_entry(uint32_t c) {
    if (c == 0) return 0xFF8;               /* 0xF00 | BPB_Media */
    if (c == 1) return 0xFFF;
    if (c >= 2u + snap_clusters) return 0;  /* свободный кластер */
    return c == 1u + snap_clusters ? 0xFFF : c + 1u;
}

/* Байт FAT12 по смещению: три байта хранят два 12-битных элемента */
static uint8_t fat_byte(uint32_t off) {
    uint32_t c = off / 3u * 2u;
    switch (off % 3u) {
    case 0:
        return (uint8_t)fat_entry(c);
    case 1:
        return (uint8_t)((fat_entry(c) >> 8) | (fat_entry(c + 1u) << 4));
    default:
        return (uint8_t)(fat_entry(c + 1u) >> 4);
    }
}

static void root_dir(uint8_t *b) {
    memcpy(b, VOLUME_LABEL, 11);
    b[11] = 0x08;                           /* метка тома */
    uint8_t *e = b + 32;
    memcpy(e, FILE_NAME, 11);
    e[11] = 0x21;                           /* только чтение + архивный */
    put16(e + 14, FAT_TIME);                /* создан */
    put16(e + 16, FAT_DATE);
    put16(e + 18, FAT_DATE);                /* последний доступ */
    put16(e + 22, FAT_TIME);                /* изменён */
    put16(e + 24, FAT_DATE);
    put16(e + 26, 2);                       /* первый кластер */
    put32(e + 28, snap_size);
}

static void dec(char *p, uint32_t v, int n) {   /* n цифр с ведущими нулями */
    while (n--) {
        p[n] = (char)('0' + v % 10u);
        v /= 10u;
    }
}

static void make_line(uint32_t seq, char *p) {
    log_rec_t r;
    if (!log_get(seq, &r)) {                /* запись затёрли, пока хост читал */
        memcpy(p, LOST, LINE_LEN);
        return;
    }
    int32_t t = r.temp_c100;
    dec(p, seq, 8);
    p[8] = ',';
    dec(p + 9, r.t_s, 8);
    p[17] = ',';
    p[18] = t < 0 ? '-' : '+';
    if (t < 0) t = -t;
    dec(p + 19, (uint32_t)t / 100u, 3);
    p[22] = '.';
    dec(p + 23, (uint32_t)t % 100u, 2);
    p[25] = ',';
    dec(p + 26, r.rh_c100 / 100u, 3);
    p[29] = '.';
    dec(p + 30, r.rh_c100 % 100u, 2);
    p[32] = '\r';
    p[33] = '\n';
}

/* Байты файла [off, off + len): заголовок, потом строки по 34 байта; за концом файла — нули */
static void csv_fill(uint32_t off, uint8_t *dst, uint32_t len) {
    char ln[LINE_LEN];
    uint32_t cur = UINT32_MAX;
    for (uint32_t i = 0; i < len; i++, off++) {
        if (off >= snap_size) {
            dst[i] = 0;
        } else if (off < HDR_LEN) {
            dst[i] = (uint8_t)HDR[off];
        } else {
            uint32_t k = (off - HDR_LEN) / LINE_LEN;
            if (k != cur) {
                make_line(snap_first + k, ln);
                cur = k;
            }
            dst[i] = (uint8_t)ln[(off - HDR_LEN) % LINE_LEN];
        }
    }
}

void disk_read(uint32_t lba, uint8_t *b) {
    memset(b, 0, SECTOR);
    if (lba == 0) {
        boot_sector(b);
    } else if (lba < ROOT_LBA) {            /* обе копии FAT одинаковы */
        uint32_t off = ((lba - FAT_LBA) % FAT_SECTORS) * SECTOR;
        for (uint32_t i = 0; i < SECTOR; i++) b[i] = fat_byte(off + i);
    } else if (lba == ROOT_LBA) {
        root_dir(b);
    } else if (lba < TOTAL_SECTORS) {
        csv_fill((lba - DATA_LBA) * SECTOR, b, SECTOR);
    }
}

scsi.c — Bulk-Only Transport and SCSI commands.

/* Раздел 198ф: Bulk-Only Transport. Хост шлёт CBW (31 байт), потом данные в одну сторону,
 * потом забирает CSW (13 байт). Все команды с данными от хоста отклоняются: диск только для чтения. */
#include <string.h>
#include "fatgen.h"
#include "scsi.h"

#define CBW_SIG 0x43425355u                 /* "USBC" */
#define CSW_SIG 0x53425355u                 /* "USBS" */

enum { ST_CBW, ST_DATA, ST_CSW, ST_DEAD };
enum { SK_NOT_READY = 2, SK_ILLEGAL = 5, SK_UNIT_ATTENTION = 6, SK_DATA_PROTECT = 7 };

static const uint8_t INQUIRY[36] = {
    0x00,                                   /* прямой доступ: диск */
    0x80,                                   /* RMB: съёмный носитель */
    0x02, 0x02, 31, 0, 0, 0,                /* версия, формат ответа, ещё 31 байт */
    'H', 'A', 'N', 'D', 'B', 'O', 'O', 'K', /* производитель, 8 байт */
    'U', '5', '8', '5', ' ', 'L', 'O', 'G', 'G', 'E', 'R', ' ', ' ', ' ', ' ', ' ',   /* изделие, 16 */
    '1', '.', '0', ' ',                     /* версия, 4 байта */
};

static struct {
    uint8_t  state, in_halt, out_halt;
    uint32_t tag, dlen, sent;               /* dCBWDataTransferLength и сколько уже ушло */
    uint8_t  status;                        /* bCSWStatus: 0 — успех, 1 — ошибка, 2 — ошибка фазы */
    uint32_t lba, left;                     /* READ(10): следующий сектор и сколько осталось */
    uint8_t  buf[SECTOR];
    uint32_t pos, len;
    uint8_t  key, asc;                      /* ошибка для REQUEST SENSE */
    uint8_t  ua, ejected;
} m;

static uint32_t get32(const uint8_t *p) {
    return p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
}
static void put32(uint8_t *p, uint32_t v) {
    for (int i = 0; i < 4; i++) p[i] = (uint8_t)(v >> (8 * i));
}
static uint32_t be32(const uint8_t *p) {
    return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
}
static uint32_t be16(const uint8_t *p) {
    return (uint32_t)p[0] << 8 | p[1];
}
static void put_be32(uint8_t *p, uint32_t v) {
    for (int i = 0; i < 4; i++) p[i] = (uint8_t)(v >> (24 - 8 * i));
}

void msc_reset(void) {
    uint8_t ej = m.ejected, ua = m.ua;
    memset(&m, 0, sizeof m);
    m.ejected = ej;                         /* сброс протокола не вставляет носитель обратно */
    m.ua = ua;
}
void msc_media_changed(void) {
    m.ejected = 0;
    m.ua = 1;                               /* следующая команда получит UNIT ATTENTION 06/28 */
}
bool msc_ejected(void) {
    return m.ejected;
}

/* Короткий ответ: не больше, чем просил хост в команде и чем он ждёт по CBW */
static void reply(const uint8_t *d, uint32_t n, uint32_t alloc) {
    if (n > alloc) n = alloc;
    if (n > m.dlen) n = m.dlen;
    memcpy(m.buf, d, n);
    m.pos = 0;
    m.len = n;
    m.left = 0;
    m.state = n ? ST_DATA : ST_CSW;
}

/* Ошибка: запомнить причину, остановить точку, по которой шли бы данные, ответить CSW = 1 */
static void fail(uint8_t key, uint8_t asc, bool in) {
    m.key = key;
    m.asc = asc;
    m.status = 1;
    if (m.dlen) {
        if (in) m.in_halt = 1;
        else m.out_halt = 1;
    }
    m.state = ST_CSW;
}

static void scsi_command(const uint8_t *cb, bool in) {
    uint8_t op = cb[0];
    if (!in && m.dlen) {                    /* данные от хоста: WRITE(10), MODE SELECT … */
        if (op == 0x2A) fail(SK_DATA_PROTECT, 0x27, false);   /* WRITE PROTECTED */
        else fail(SK_ILLEGAL, 0x20, false);
        return;
    }
    if (m.ua && op != 0x12 && op != 0x03) { /* INQUIRY и REQUEST SENSE проходят всегда */
        m.ua = 0;
        fail(SK_UNIT_ATTENTION, 0x28, in);  /* MEDIUM MAY HAVE CHANGED */
        return;
    }
    switch (op) {
    case 0x00:                              /* TEST UNIT READY */
        if (m.ejected) fail(SK_NOT_READY, 0x3A, in);          /* MEDIUM NOT PRESENT */
        else m.state = ST_CSW;
        break;
    case 0x03: {                            /* REQUEST SENSE: фиксированный формат, 18 байт */
        uint8_t s[18] = {0x70, 0, m.key, 0, 0, 0, 0, 10, 0, 0, 0, 0, m.asc, 0, 0, 0, 0, 0};
        m.key = m.asc = 0;
        reply(s, sizeof s, cb[4]);
        break;
    }
    case 0x12:                              /* INQUIRY */
        reply(INQUIRY, sizeof INQUIRY, be16(cb + 3));
        break;
    case 0x1A: {                            /* MODE SENSE(6): заголовок 4 байта, бит WP = 0x80 */
        static const uint8_t s[4] = {3, 0, 0x80, 0};
        reply(s, sizeof s, cb[4]);
        break;
    }
    case 0x5A: {                            /* MODE SENSE(10): заголовок 8 байт */
        static const uint8_t s[8] = {0, 6, 0, 0x80, 0, 0, 0, 0};
        reply(s, sizeof s, be16(cb + 7));
        break;
    }
    case 0x1B:                              /* START STOP UNIT: LoEj = 1, Start = 0 — «Извлечь» */
        if ((cb[4] & 0x03u) == 0x02u) m.ejected = 1;
        m.state = ST_CSW;
        break;
    case 0x1E:                              /* PREVENT ALLOW MEDIUM REMOVAL */
    case 0x2F:                              /* VERIFY(10) */
    case 0x35:                              /* SYNCHRONIZE CACHE(10) */
        m.state = ST_CSW;
        break;
    case 0x23: {                            /* READ FORMAT CAPACITIES */
        uint8_t s[12] = {0, 0, 0, 8, 0, 0, 0, 0, 0x02, 0, SECTOR >> 8, SECTOR & 0xFF};
        put_be32(s + 4, TOTAL_SECTORS);     /* 0x02 — носитель размечен */
        reply(s, sizeof s, be16(cb + 7));
        break;
    }
    case 0x25: {                            /* READ CAPACITY(10): НОМЕР последнего сектора и размер */
        uint8_t s[8];
        put_be32(s, TOTAL_SECTORS - 1u);
        put_be32(s + 4, SECTOR);
        reply(s, sizeof s, sizeof s);
        break;
    }
    case 0x28: {                            /* READ(10) */
        uint32_t lba = be32(cb + 2), cnt = be16(cb + 7);
        if (m.ejected) {
            fail(SK_NOT_READY, 0x3A, in);
        } else if (lba >= TOTAL_SECTORS || cnt > TOTAL_SECTORS - lba) {
            fail(SK_ILLEGAL, 0x21, in);     /* LOGICAL BLOCK ADDRESS OUT OF RANGE */
        } else if (m.dlen != cnt * SECTOR) {
            m.status = 2;                   /* хост и команда не сошлись в длине: ошибка фазы */
            if (m.dlen) m.in_halt = 1;
            m.state = ST_CSW;
        } else {
            m.lba = lba;
            m.left = cnt;
            m.pos = m.len = 0;
            m.state = cnt ? ST_DATA : ST_CSW;
        }
        break;
    }
    default:
        fail(SK_ILLEGAL, 0x20, in);         /* INVALID COMMAND OPERATION CODE */
    }
}

int msc_out(const uint8_t *p, uint32_t n) {
    if (m.out_halt) return -1;
    if (m.state != ST_CBW) return 0;
    if (n != 31u || get32(p) != CBW_SIG) {  /* не CBW: обе точки стоят до сброса BOT */
        m.in_halt = m.out_halt = 1;
        m.state = ST_DEAD;
        return 0;
    }
    m.tag = get32(p + 4);
    m.dlen = get32(p + 8);
    m.sent = 0;
    m.status = 0;
    scsi_command(p + 15, (p[12] & 0x80u) != 0);
    return 0;
}

int msc_in(uint8_t *pkt) {
    if (m.in_halt) return -1;
    if (m.state == ST_DATA) {
        if (m.pos == m.len) {               /* буфер отдан: считать следующий сектор */
            disk_read(m.lba++, m.buf);
            m.left--;
            m.pos = 0;
            m.len = SECTOR;
        }
        uint32_t n = m.len - m.pos;
        if (n > MSC_PACKET) n = MSC_PACKET;
        memcpy(pkt, m.buf + m.pos, n);
        m.pos += n;
        m.sent += n;
        if (m.pos == m.len && m.left == 0) m.state = ST_CSW;
        return (int)n;
    }
    if (m.state == ST_CSW) {
        put32(pkt, CSW_SIG);
        put32(pkt + 4, m.tag);              /* dCSWTag — как в CBW */
        put32(pkt + 8, m.dlen - m.sent);    /* dCSWDataResidue: сколько не дослали */
        pkt[12] = m.status;
        m.state = ST_CBW;
        return 13;
    }
    return 0;
}

void msc_clear_halt(uint8_t ep) {
    if (m.state == ST_DEAD) return;         /* после плохого CBW помогает только сброс BOT */
    if (ep & 0x80u) m.in_halt = 0;
    else m.out_halt = 0;
}

logring.c — the log ring.

/* Раздел 198ф: кольцо журнала. Пишет главный цикл, читает обработчик USB: счётчик растёт
 * только после того, как запись целиком легла в ячейку. */
#include "logring.h"

#ifndef LOG_RAM_ATTR
#define LOG_RAM_ATTR                        /* на U585: __attribute__((section(".bss.sram3"))) */
#endif
#define BARRIER() __asm volatile("" ::: "memory")   /* компилятор не переставляет доступы через неё */

LOG_RAM_ATTR static log_rec_t ring[LOG_CAP];
static volatile uint32_t count;

void log_append(uint32_t t_s, int16_t temp_c100, uint16_t rh_c100) {
    log_rec_t *r = &ring[count % LOG_CAP];
    r->t_s = t_s;
    r->temp_c100 = temp_c100;
    r->rh_c100 = rh_c100;
    BARRIER();                              /* сначала запись, потом счётчик */
    count = count + 1u;                     /* 32-битная запись на Cortex-M33 неделима */
}

uint32_t log_count(void) {
    return count;
}

bool log_get(uint32_t seq, log_rec_t *r) {
    uint32_t c = count;
    if (seq >= c || c - seq >= LOG_CAP) return false;   /* самую старую ячейку сейчас могут переписывать */
    *r = ring[seq % LOG_CAP];
    BARRIER();
    return count - seq < LOG_CAP;           /* пока копировали, ячейку не тронули */
}

Next: section 198х "Mini-project: the B-U585I-IOT02A board sensors — climate, motion and light" — where the temperature and humidity in the log come from: the sensors on the same board.

Open in the interactive handbook