198ф. Мини-проект: USB-флешка на B-U585I-IOT02A — прибор отдаёт журнал как диск
Самописец из 198б отдавал поток через виртуальный COM-порт, и на ПК
нужна была своя программа. Здесь прибор проще для того, кто им
пользуется: подключил кабель — в проводнике появился диск, на нём файл
LOG.CSV, его открывает любая таблица. Никаких драйверов и программ:
класс Mass Storage понимают все системы сами (про USB-хост и чтение
флешек — 20а.4).
Хитрость в том, что никакого диска в приборе нет. Журнал лежит в ОЗУ кольцом двоичных записей по 8 байт, а загрузочный сектор, таблица FAT, каталог и строки CSV собираются в тот момент, когда хост просит сектор командой READ(10). Диск только для чтения: запись с ПК прибор отклоняет и заранее говорит хосту, что носитель защищён.
Проверено без платы. Генератор диска fatgen.c, обработчик Bulk-Only и
SCSI scsi.c и кольцо журнала logring.c собраны gcc на ПК вместе с
моделью хоста: она делает то же, что система при подключении флешки, и
читает весь диск пакетами по 64 байта. Три прочитанных образа разобраны
двумя независимыми путями — библиотекой FatFs и своим разбором на Python —
и файл LOG.CSV сверен байт в байт. Прошивочная часть собрана armclang
под Cortex-M33; на плате она не запускалась.
Опоры: пример «USB Device Mass Storage» из пакета платы B-U585I-IOT02A 1.1.0
(Keil; файлы USBD_User_MSC_0.c,
MemoryDiskImage.c, USBD_Config_MSC_0.h; описание библиотеки — MDK-Middleware 7.13.0, компонент USB 6.15.0); заголовок stm32u585xx.h
пакета STM32U5xx_DFP 2.2.1; DS13086 (STM32U585xx, таблицы 88 и 89 —
флеш-память); драйвер MX25LM51245G из пакета платы; библиотека ST USB
Device (класс MSC, копии для F4, F7 и G4 в github.com/betaflight/betaflight,
коммит 744f95f) и там же emfat — диск FAT32 «на лету» для журналов
полёта; FatFs R0.15 (ChaN) — как в 198т. Лицензия: пакет платы —
Apache-2.0 и BSD-3-Clause, примеры MDK-Middleware — лицензия Arm для MDK,
библиотека ST — MCD-ST Liberty V2 и SLA0044, emfat — MIT, FatFs — своя
свободная лицензия (уведомление в исходниках). Из чужого кода в разделе
только короткие цитаты; FatFs лежит в проверке с уведомлением. Со
стандартами USB Mass Storage и SCSI не сверялось: коды команд и ошибок
взяты из заголовков MDK (usb_msc.h) и ST (usbd_msc_scsi.h).
Как это называется в документации
Оригинал Перевод Mass Storage Class (MSC) класс «накопитель»: флешки, карточные читалки Bulk-Only Transport (BOT) протокол «только массовые передачи»: команда, данные, состояние Command Block Wrapper (CBW), Command Status Wrapper (CSW) обёртка команды (31 байт) и ответ о выполнении (13 байт) data residue остаток: сколько байт из обещанных не передано logical block address (LBA) номер сектора (блока) от начала диска sense data: sense key, ASC, ASCQ сведения об ошибке: класс ошибки и её уточняющие коды UNIT ATTENTION «внимание»: носитель мог смениться write protect (WP) защита от записи STALL, endpoint halt остановка конечной точки до сброса хостом BIOS parameter block (BPB) параметры тома в загрузочном секторе volume label метка тома eject (START STOP UNIT, LoEj) «Извлечь»: хост отпускает носитель
198ф.1 Что делает прибор и с чего начать
Прибор меряет температуру и влажность раз в 10 секунд и кладёт запись в
кольцо. Подключили к ПК — видно диск LOGGER с одним файлом:
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
— это начало файла из прогона: заголовок и первые три записи. Откуда берутся значения, для этого раздела неважно: в модели они считаются по формуле, на плате их даст датчик (198х).
Пример пакета платы. В пакете B-U585I-IOT02A есть готовая флешка
«Mass Storage» на библиотеке USB из MDK-Middleware. Библиотека сама ведёт
протокол и команды SCSI, программе остаются семь обработчиков в
USBD_User_MSC_0.c: инициализация и её отмена, буфер, ёмкость, чтение,
запись и проверка носителя. «Диск» там — массив в ОЗУ на 8 КБ (16 блоков по 512
байт) и готовый образ FAT12 на 4096 байт во флеш-памяти, который при
старте копируется в этот массив. По описанию примера: OTG_FS на разъёме
CN1 (USB Type-C), процессор 160 МГц, USB 48 МГц. Конечные точки Bulk —
номер 1 в обе стороны, пакет 64 байта, буфер класса 512 байт; строка
INQUIRY — «Keil», «Disk 0», «1.0».
Разбор этого образа по полям (проверка делает это сама):
образ примера: 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
Том в загрузочном секторе заявлен на 64 сектора, а GetMediaCapacity
отдаёт 16. Пока занятые кластеры лежат в первых 16 секторах, это не
видно; к этой неувязке вернёмся в граблях.
Чем плох готовый образ для журнала: файл в нём неподвижен. Чтобы хост
увидел новые записи, образ пришлось бы переписывать — FAT, каталог и
данные — и держать его целиком в памяти. Здесь сделано наоборот: хранится
только журнал, а диск вычисляется. Та же мысль у emfat в betaflight:
журнал полёта отдаётся как FAT32, собранный по запросу, а
STORAGE_IsWriteProtected там всегда возвращает 1.
198ф.2 Mass Storage изнутри: CBW, данные, CSW
Устройство класса MSC объявляет интерфейс с классом 0x08, подклассом 0x06 (команды SCSI) и протоколом 0x50 (Bulk-Only) и две точки Bulk — это же видно со стороны хоста в 20а.4. Всё остальное идёт по этим двум точкам тремя шагами:
- хост шлёт в Bulk-OUT CBW — 31 байт: сигнатура «USBC» (0x43425355), метка, сколько байт данных он ждёт, направление (бит 7: 1 — к хосту), номер логического устройства, длина команды и сама команда SCSI до 16 байт;
- данные — в одну сторону, пакетами по 64 байта на полной скорости;
- хост забирает из Bulk-IN CSW — 13 байт: «USBS» (0x53425355), та же метка, остаток и состояние: 0 — выполнено, 1 — ошибка, 2 — ошибка фазы.
При ошибке состояние 1 не говорит, что случилось. Хост спрашивает отдельной командой REQUEST SENSE и получает три кода: класс ошибки (sense key) и уточнение (ASC, ASCQ).
Подключение байт за байтом. Так начинает модель хоста — INQUIRY, потом ёмкость, потом 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
Как читать первый CBW: 55 53 42 43 — «USBC»; метка 00 00 8F 19;
ждёт 0x24 = 36 байт; 80 — данные к хосту; устройство 0; команда 6 байт:
12 00 00 00 24 00 — INQUIRY на 36 байт. В CSW та же метка, остаток 0,
состояние 0. У MODE SENSE хост просил 192 байта (0xC0), прибор отдал 4, и
в CSW остаток BC 00 00 00 = 188.
Команды, которые понимает прибор. Коды — из usb_msc.h MDK и
usbd_msc_scsi.h ST:
| Команда | Код | Ответ прибора |
|---|---|---|
| TEST UNIT READY | 0x00 | готов; после «Извлечь» — ошибка 02/3A (носителя нет) |
| REQUEST SENSE | 0x03 | 18 байт: 0x70, класс ошибки в байте 2, ASC в байте 12 |
| INQUIRY | 0x12 | 36 байт: диск, съёмный, «HANDBOOK», «U585 LOGGER», «1.0» |
| MODE SENSE(6) / (10) | 0x1A / 0x5A | заголовок 4 / 8 байт, бит WP = 0x80 — защищён |
| START STOP UNIT | 0x1B | LoEj = 1, Start = 0 — «Извлечь» |
| PREVENT ALLOW MEDIUM REMOVAL, VERIFY(10), SYNCHRONIZE CACHE(10) | 0x1E, 0x2F, 0x35 | ничего делать не нужно — выполнено |
| READ FORMAT CAPACITIES | 0x23 | 12 байт: 2055 блоков по 512, «размечен» |
| READ CAPACITY(10) | 0x25 | номер последнего блока 2054 и размер 512 |
| READ(10) | 0x28 | сектора из fatgen |
| WRITE(10) | 0x2A | ошибка 07/27 — защищён от записи |
| остальные | — | ошибка 05/20 — нет такой команды |
Разбор команды целиком:
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 */
}
}
Две вспомогательные функции решают, что уйдёт хосту. reply не отдаёт
больше, чем просила команда и чем обещал CBW; fail запоминает ошибку для
REQUEST SENSE и останавливает (STALL) ту точку, по которой шли бы данные,
— так хост понимает, что данных не будет, снимает остановку и забирает
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;
}
Приём CBW и выдача пакетов в Bulk-IN. Сектор для READ(10) собирается в буфер на 512 байт, когда предыдущий отдан, и уходит восемью пакетами:
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;
}
Модуль не знает ни регистров OTG, ни библиотеки: драйвер точек отдаёт ему пакеты Bulk-OUT и берёт пакеты Bulk-IN. Поэтому он и проверяется на ПК.
198ф.3 FAT12 «на лету»: раскладка секторов
Сколько места нужно. Строка CSV — 34 байта, заголовок — 26:
последняя: 00001499,00014990,-001.31,054.29
В файле не больше 30 720 записей (почему не все 32 768 — в 198ф.4).
Остальное считается подряд, и так же это делают макросы fatgen.h:
| Что | Расчёт | Итог |
|---|---|---|
| наибольший файл | 26 + 34 × 30 720 | 1 044 506 байт |
| кластеров по 512 байт | 1 044 506 / 512 = 2040,05 → вверх | 2041 |
| FAT12: 12 бит на кластер, плюс два служебных | (2041 + 2) × 1,5 = 3064,5 → 3065 байт | 6 секторов |
| загрузочный сектор | сектор 0 | |
| две копии FAT | 2 × 6 | сектора 1…12 |
| корневой каталог: 16 мест по 32 байта | 16 × 32 = 512 | сектор 13 |
| данные: кластер 2 = сектор 14 | 14 + 2041 | 2055 секторов = 1 052 160 байт |
2041 кластер — меньше 4085, поэтому это FAT12: тип тома определяется
числом кластеров, а не надписью в загрузочном секторе. В FatFs та же
граница — MAX_FAT12 0xFF5, с пометкой, что она выбрана по поведению
DOS и Windows. Кластер — один сектор: так строка файла и сектор диска
считаются одной формулой.
#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)
Загрузочный сектор. Только BPB и подпись 0x55AA; кода загрузки нет:
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;
}
Как он выглядит в прочитанном образе:
загрузочный, байты 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 байт в секторе, 01 — сектор в кластере, 01 00 — один
служебный сектор, 02 — две FAT, 10 00 — 16 мест в корне, 07 08 =
2055 секторов, F8 — тип носителя, 06 00 — 6 секторов на FAT. Дальше
29 — за ним номер тома 29 09 26 20 и метка LOGGER.
FAT12. Файл лежит кластерами подряд, так что таблица — это просто «следующий = текущий + 1», а у последнего — конец цепочки 0xFFF. Хитрость FAT12 в упаковке: два 12-битных элемента занимают три байта. Функция считает любой байт таблицы по его смещению, не собирая таблицу в памяти:
/* Цепочка файла: кластеры 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)
Первые три байта F8 FF FF — служебные элементы 0xFF8 и 0xFFF. Дальше
03 40 00 — это кластеры 2 → 3 и 3 → 4. Элемент 341 начинается в
последнем байте первого сектора FAT и кончается в первом байте второго:
кто собирает FAT посекторно по номерам элементов, тот здесь ошибается.
Счёт по смещению байта этой ошибки не знает.
Каталог и данные. В корне два места: метка тома и файл с атрибутами «только чтение» и «архивный». Дату без часов реального времени пришлось зашить: 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 — атрибуты, 3D 5D — дата 0x5D3D, 02 00 — первый кластер,
52 C7 00 00 = 51 026 байт. Байты файла считаются по смещению: сначала
заголовок, потом строка номер (смещение − 26) / 34; за концом файла —
нули. Сборка сектора целиком:
/* Байты файла [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 Где хранить журнал: память U585
Диску места не нужно — нужно журналу. Что есть у STM32U585 по
stm32u585xx.h и DS13086:
| Память | Сколько | Для журнала |
|---|---|---|
| SRAM1 | 192 КБ | переменные программы и стек |
| SRAM2 | 64 КБ | |
| SRAM3 | 512 КБ | кольцо 32 768 × 8 байт = 256 КБ |
| SRAM4 | 16 КБ | |
| флеш-память | 2 МБ, два банка по 1 МБ, страница 8 КБ | запись по 128 бит (16 байт), 118 мкс; 10 000 циклов стирания (таблицы 88 и 89) |
| MX25LM51245G на плате | 64 МБ, страница 256 байт | по драйверу пакета платы |
Запись в ОЗУ — 8 байт: время, температура и влажность в сотых. Номер записи не хранится, он следует из положения в кольце. В файле та же запись занимает 34 байта — в 4,25 раза больше: CSV выгоднее считать, чем хранить. Кольцо на 256 КБ при записи раз в 10 секунд — это 32 768 × 10 с = 91 час.
Журнал в ОЗУ пропадёт при отключении питания. Если это важно, кольцо
переезжает во флеш-память: 16 байт на запись — ровно одна запись по
128 бит; банк 1 МБ — 128 страниц и 65 536 записей; страница стирается раз
за оборот кольца, и 10 000 циклов хватит на 655 360 000 записей. Генератор
диска при этом не меняется — меняется только log_get. Этот вариант не
собирался.
Кольцо. Пишет главный цикл, читает обработчик USB в своём потоке. Счётчик растёт только после того, как запись целиком легла в ячейку, а чтение проверяет, что ячейку не переписали, пока её копировали:
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; /* пока копировали, ячейку не тронули */
}
Самая старая ячейка считается уже потерянной: в неё сейчас пишет
log_append.
Снимок. Хост читает FAT и каталог один раз, а файл — потом, кусками. Если бы размер файла рос прямо во время чтения, каталог и данные разошлись бы. Поэтому при подключении прибор берёт снимок — какие записи видны — и не меняет его, пока хост не отпустит диск:
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;
}
Снимок — не больше 30 720 записей: оставшиеся 2048 ячеек — запас на то время, пока хост читает, а журнал пишет дальше. При записи раз в 10 секунд это 2048 × 10 с = 5,7 часа. Если запас кончился, старые строки файла не врут, а честно заменяются прочерками той же длины.
Сборка под Cortex-M33. Кольцо помещено в SRAM3 атрибутом секции и файлом раскладки:
; Раздел 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, два варианта (без драйвера USB и без библиотеки):
== вариант 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 байт
С библиотекой MDK наш код — 1556 байт флеш-памяти и 528 байт SRAM1, из них 512 — буфер сектора для библиотеки. Со своим BOT и SCSI — 2332 байта и 568 байт. Кольцо — 262 144 байта SRAM3 в обоих вариантах.
198ф.5 Запись с ПК: почему опасна и как её запретить
Диск вычисляется, а хост об этом не знает. Запись с ПК — новый файл,
правка LOG.CSV, служебная папка системы — меняет FAT и каталог в кэше
хоста и отправляет сектора прибору. Прибору их некуда положить: при
следующем чтении он снова соберёт свою FAT, и хост увидит том, который
сам не писал. Отсюда правило: защищать на всех уровнях сразу.
- MODE SENSE: бит 7 байта 2 (в MODE SENSE(10) — байта 3) — носитель защищён. Хост, который спрашивает MODE SENSE при подключении, знает это до первой записи.
- WRITE(10): отказ с остановкой точки OUT и ошибкой 07/27 (DATA PROTECT, WRITE PROTECTED) — если хост всё же попробует.
- Атрибут файла «только чтение» — для программ на ПК.
Попытка записи в модели:
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
Хост обещал 512 байт (00 02 00 00, направление 00 — к устройству),
прибор не принял ни одного: точка OUT остановлена, в CSW остаток
00 02 00 00 = 512 и состояние 1.
Как это сделано в библиотеках. В MDK защита — бит
USBD_MSC_MEDIA_PROTECTED в ответе USBD_MSC0_CheckMedia; что именно
библиотека отвечает на MODE SENSE, по исходникам не проверить — она
поставляется собранной. В библиотеке ST V2.4.2 (копия для F4 в betaflight)
SCSI_ModeSense6 копирует восемь нулевых байт MSC_Mode_Sense6_data и
бит WP не ставит, даже если IsWriteProtected вернул 1: хост узнаёт о
защите, только когда запись уже отклонена. В копиях для F7 и G4 того же
репозитория бит добавлен строкой с пометкой «set bit 7 of the device
configuration byte to indicate write protection». Отказ записи ST
отмечает ключом 02 (NOT READY) с кодом 0x27; наш обработчик — ключом 07
(DATA PROTECT).
С библиотекой MDK весь «диск» прибора — это обработчики в
USBD_User_MSC_0.c. Файл целиком:
/* Раздел 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 Прогон на ПК: модель хоста, FatFs и разбор на Python
host.c — модель хоста. Она вызывает msc_out и msc_in так, как это
делал бы драйвер USB, и снимает остановки точек так, как это делает хост.
Сценарий:
- в журнале 1500 записей; подключение: INQUIRY, TEST UNIT READY, READ CAPACITY, READ FORMAT CAPACITIES, MODE SENSE, сектор 0;
- весь диск по READ(10) кусками по 128 секторов, журнал тем временем пишет по записи на каждую команду;
- запись, чтение за концом диска, неизвестная команда;
- «Извлечь», прибор добавляет 3000 записей и берёт новый снимок, снова весь диск;
- кольцо переполнено: ещё 40 000 записей, новый снимок, и ещё 2052 записи до чтения — больше запаса;
- испорченный CBW и сброс протокола.
disk1.img: 2055 секторов прочитано командами READ(10): 17 команд, 16457 пакетов IN
снимок: первая запись 0, записей 1500, файл 51026 байт; в журнале при конце чтения 1517
каталог при подключённом диске: LOG.CSV 51026 байт (журнал уже 1517 записей)
17 команд READ(10) по 128 секторов и 16 457 пакетов IN: 2055 × 8 пакетов данных и 17 CSW. Пока хост читал, журнал вырос до 1517 записей, а файл в каталоге остался 51 026 байт = 26 + 34 × 1500 — снимок держится.
Извлечь и вставить снова. Хост отпускает диск командой START STOP UNIT, прибор видит это, берёт новый снимок и «вставляет» носитель:
«Извлечь»: 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 — носителя нет; 06/28 — внимание, носитель мог смениться; дальше диск снова готов, и в нём 4517 записей.
Разбор образов. Первый путь — FatFs R0.15: монтирует образ, читает
метку, свободное место и LOG.CSV через f_read кусками по 4096 байт.
Писать на диск не пыталась ни разу:
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
Второй путь — fatread.py, свой разбор без кода fatgen.c: поля BPB,
элементы FAT по 12 бит, каталог, цепочка кластеров. Ожидаемый файл
скрипт строит сам по той же формуле записей, что и модель:
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
CRC-32 файла у FatFs и у Python совпали во всех трёх образах. В третьем образе снимок — последние 30 720 записей из 44 534, первая — 13 814. Пока хост не читал, журнал добавил 2052 записи, на 4 больше запаса, и 5 самых старых строк стали прочерками (четыре сверх запаса и самая старая ячейка, в которую пишут). Остальные 30 715 строк — точно те, что в кольце.
Испорченный CBW. Не та сигнатура — обе точки остановлены, и простое снятие остановки не помогает; помогает только сброс протокола (запрос класса 0xFF по нулевой точке):
после мусора: IN -1, OUT -1
CLEAR_FEATURE без сброса: IN -1
TEST UNIT READY (после сброса BOT): статус CSW 0
198ф.7 Грабли
Новые записи не появляются, пока диск подключён. Система читает FAT и каталог и держит их у себя. Менять их под подключённым хостом нельзя — получится файл, у которого каталог и данные из разного времени. Новый снимок — только после «Извлечь», и сразу UNIT ATTENTION 06/28. Как именно конкретная система перечитает диск после 06/28, на модели не проверить.
«Извлечь» не отключает кабель. После START STOP UNIT с LoEj хост ещё на шине и спрашивает TEST UNIT READY. Ответ «готов» после извлечения выглядит как новый диск без предупреждения — нужно 02/3A, пока прибор не «вставит» носитель снова. Выдернуть кабель без «Извлечь» диску только для чтения не страшно: несохранённого нет.
Размер из READ CAPACITY и из BPB разошлись. В образе примера пакета том заявлен на 64 сектора, а ёмкость — 16. Хост, который начнёт писать в свободные кластеры по FAT, уйдёт за конец носителя. Здесь одно число 2055 стоит и в BPB, и в READ CAPACITY, и в READ FORMAT CAPACITIES.
READ CAPACITY отдаёт номер последнего блока, а не их число. 2054, не 2055. Ошибка на единицу — и хост считает последний сектор лишним или недостающим.
Размер блока — 512. Сектор диска не обязан совпадать со страницей флеш-памяти (8 КБ у U585, 256 байт у MX25LM51245G). Хосту отдаются сектора по 512 байт, а как они лежат в памяти — забота прибора; у вычисляемого диска этого вопроса нет совсем.
Элемент FAT12 на стыке секторов. 12 бит — полтора байта, и элемент 341 разрезан между первым и вторым сектором FAT. Генератор, который строит сектор FAT по номерам элементов, теряет половину. Считать по смещению байта.
Больше 4084 кластеров — уже не FAT12. Тип тома определяется числом
кластеров. Раздули журнал — либо кластер больше, либо FAT16; надпись
FAT12 в загрузочном секторе ничего не решает.
Хост ждёт данных, а их нет. Если команда не удалась, а CBW обещал данные, одного CSW с ошибкой мало: точку данных нужно остановить, иначе хост будет ждать обещанные байты. В модели так отработаны WRITE(10) (точка OUT), чтение за концом и неизвестная команда (точка IN).
Защита от записи только в WRITE(10). Если MODE SENSE молчит о защите, хост узнаёт о ней по отказу посреди работы. Бит WP — в MODE SENSE с самого подключения; в библиотеке ST V2.4.2 его нет.
198ф.8 Итоги
- Прибор виден как диск без драйверов: класс 0x08, SCSI 0x06, Bulk-Only 0x50; CBW — 31 байт, CSW — 13, данные — пакетами по 64 байта.
- Диск FAT12 на 2055 секторов по 512 байт (1 052 160 байт) не хранится нигде: загрузочный сектор, две FAT по 6 секторов, каталог в секторе 13 и данные с сектора 14 считаются по номеру сектора.
LOG.CSV— заголовок 26 байт и строки по 34 байта, до 30 720 записей (1 044 506 байт, 2041 кластер); кольцо — 32 768 записей по 8 байт, 256 КБ SRAM3, запас 2048 записей на время чтения.- Запись запрещена трижды: бит WP в MODE SENSE, отказ WRITE(10) с 07/27 и атрибут файла «только чтение».
- Новые записи хост видит после «Извлечь»: 02/3A, новый снимок, 06/28.
- Модель хоста прочитала три образа по 16 457 пакетов IN; FatFs R0.15 и
свой разбор на Python получили тот же
LOG.CSVбайт в байт. - Под Cortex-M33 (armclang 6.24,
-Os): 1556 байт флеш-памяти с библиотекой MDK и 2332 байта со своим BOT и SCSI, не считая драйвера USB.
Полные исходники
fatgen.h — раскладка диска FAT12 «на лету».
/* Раздел 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 — генератор секторов FAT12.
/* Раздел 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 и команды SCSI.
/* Раздел 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 — кольцо журнала.
/* Раздел 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; /* пока копировали, ячейку не тронули */
}
Дальше: раздел 198х «Мини-проект: датчики платы B-U585I-IOT02A — климат, движение и свет» — откуда в журнале берутся температура и влажность: датчики на той же плате.