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х).
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:
- 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;
- data — in one direction, in 64-byte packets at full speed;
- 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.
- 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.
- WRITE(10): rejection with the OUT endpoint stalled and error 07/27 (DATA PROTECT, WRITE PROTECTED) — in case the host tries anyway.
- 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:
- 1500 records in the log; connection: INQUIRY, TEST UNIT READY, READ CAPACITY, READ FORMAT CAPACITIES, MODE SENSE, sector 0;
- the whole disk via READ(10) in chunks of 128 sectors, while the log writes one record per command;
- a write, a read past the end of the disk, an unknown command;
- "Eject", the device adds 3000 records and takes a new snapshot, the whole disk again;
- the ring overflows: 40000 more records, a new snapshot, and another 2052 records before reading — more than the reserve;
- 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
- The device appears as a disk without drivers: class 0x08, SCSI 0x06, Bulk-Only 0x50; CBW — 31 bytes, CSW — 13, data — in 64-byte packets.
- The FAT12 disk of 2055 sectors of 512 bytes (1052160 bytes) is not stored anywhere: the boot sector, two FATs of 6 sectors, the directory in sector 13 and the data from sector 14 on are computed from the sector number.
LOG.CSV— a 26-byte header and 34-byte lines, up to 30720 records (1044506 bytes, 2041 clusters); the ring — 32768 records of 8 bytes, 256 KB of SRAM3, a reserve of 2048 records for the time of reading.- Writing is forbidden three times over: the WP bit in MODE SENSE, rejection of WRITE(10) with 07/27, and the "read-only" file attribute.
- The host sees new records after "Eject": 02/3A, a new snapshot, 06/28.
- The host model read three images of 16457 IN packets each; FatFs R0.15 and
our own Python parser got the same
LOG.CSVbyte for byte. - For Cortex-M33 (armclang 6.24,
-Os): 1556 bytes of flash memory with the MDK library and 2332 bytes with our own BOT and SCSI, not counting the USB driver.
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.