Формат данных EEPROM

Контроллеры JetHome хранят идентификационные данные плат в микросхеме EEPROM на шине I2C: название и версию платы, серийный номер, MAC-адрес, идентификаторы процессора, а на части устройств — цифровую подпись производителя. Адрес микросхемы указан в описании каждого контроллера.

Данные записаны в формате JEEFS: заголовок платы с контрольной суммой, за ним — простая файловая система. Формат открыт, его эталонная реализация — библиотека jeefs для C, C++, Python и Rust.

See also

Проверка подписи производителя описана в разделе Проверка цифровой подписи.

Организация памяти

Смещение

Размер

Содержимое

0x0000

256 байт

Заголовок платы (у устаревшей версии 1 — 512 байт)

0x0100 (у версии 1 — 0x0200)

переменный

Файловая система: цепочка файлов, каждый файл — заголовок 28 байт и данные

после последнего файла

до конца памяти

Свободное место, байты 0x00 или 0xFF

Общие правила для всех структур:

  • Многобайтовые поля записаны в порядке little-endian.

  • Структуры упакованы без выравнивания.

  • Контрольная сумма — CRC32 по стандарту IEEE 802.3 (полином 0xEDB88320), совпадает с функцией crc32() библиотеки zlib. Хранится в порядке little-endian. В заголовке платы и в записи device.id она покрывает все байты структуры до поля контрольной суммы. У файла контрольных сумм две: headerCrc32 покрывает байты заголовка файла до себя, а crc32 — данные файла, а не заголовок.

  • Резервные байты внутри записанных структур равны 0x00. Незаписанная область стертой микросхемы читается как 0xFF. Оба значения означают отсутствие данных, а целостность записанных данных определяют сигнатура, версия и контрольная сумма.

  • Строковые поля бывают двух видов:

    • строки с завершающим нулем (boardname, boardversion) — до 31 символа, после строки обязателен нулевой байт;

    • строки фиксированной длины (board_serial, usid, cpuid) — печатаемые символы ASCII (0x20–0x7E), дополненные нулевыми байтами. Если значение занимает поле целиком, нулевого байта нет.

Заголовок платы

Заголовок описывает одну плату. Процессорный модуль и материнская плата несут каждый свой заголовок в своей микросхеме EEPROM.

Определение версии заголовка

Первые 12 байт одинаковы для всех версий:

Смещение

Размер

Поле

Описание

0–7

8

magic

Сигнатура JETHOME\0: байты 4A 45 54 48 4F 4D 45 00

8

1

version

Версия заголовка

9–11

3

—

Назначение зависит от версии

Порядок определения:

  1. Прочитать 12 байт и побайтно сравнить сигнатуру. Если все байты равны 0x00 или 0xFF, заголовок не записан.

  2. По байту version выбрать структуру: версия 1 — 512 байт, версии 2, 3 и 4 — 256 байт.

  3. Неизвестная версия — ошибка разбора.

Заголовок версии 4

Текущая версия заголовка — 4.

Карта байтов 256-байтового заголовка платы версии 4

Раскладка заголовка версии 4. Строки — по 16 байт, адрес строки указан слева. Зеленым отмечены сведения о плате, желтым — значения, из которых строится строка, подписываемая подписью устройства (см. Проверка цифровой подписи).

Смещение

Размер

Поле

Описание

0–7

8

magic

Сигнатура JETHOME\0

8

1

version

Версия заголовка: 4

9

1

signature_version

10

1

fs_version

Номер версии файловой системы, текущая версия — 1

11

1

—

Резерв

12–43

32

boardname

Название платы

44–75

32

boardversion

Версия платы

76–107

32

board_serial

Серийный номер платы

108–139

32

usid

Внутренний идентификатор устройства (USID)

140–171

32

cpuid

Идентификатор процессора

172–177

6

mac

MAC-адрес, 6 байт в двоичном виде

178–179

2

—

Резерв

180–243

64

signature

244–251

8

timestamp

Время записи заголовка: знаковое 64-битное число, секунды Unix-времени

252–255

4

crc32

CRC32 байтов 0–251

Назначение полей:

  • board_serial — серийный номер этой платы. Серийный номер устройства в целом хранится в записи device.id.

  • usid и cpuid заполняются, если у платы есть такие идентификаторы, иначе поля нулевые. У контроллеров серии JXD usid — строка из 30 символов, cpuid — заводской MAC-адрес микроконтроллера ESP32 в виде XX:XX:XX:XX:XX:XX.

  • timestamp — момент записи заголовка. Значение 0 означает, что время не задано.

Заголовок, у которого все байты от смещения 12 до поля crc32 нулевые, — пустой: он резервирует место под заголовок, но данных платы не содержит.

Чтение заголовка

  1. Определить версию по первым 12 байтам.

  2. Сравнить CRC32 байтов 0–251 со значением в байтах 252–255.

  3. Извлечь строки: до первого нулевого байта, а если его нет — все поле целиком.

  4. Прочитать fs_version — номер версии файловой системы, которая описана ниже. Незнакомая версия — ошибка разбора.

  5. Ненулевые байты в резервных полях игнорировать.

Файловая система

Версия файловой системы записана в байте fs_version заголовка платы; текущая версия — 1. Файлы образуют однонаправленную цепочку, которая начинается сразу за заголовком платы: со смещения 0x0100, для заголовка версии 1 — 0x0200. Каждый файл — это заголовок файла и следующие за ним данные.

Заголовок файла

Смещение

Размер

Поле

Описание

0–15

16

name

Имя файла: до 15 печатаемых символов ASCII, завершается нулевым байтом

16–17

2

dataSize

Размер данных: от 1 до 32767 байт

18–21

4

crc32

CRC32 данных файла

22–23

2

nextFileAddress

Смещение следующего файла от начала EEPROM; 0x0000 или 0xFFFF — конец цепочки

24–27

4

headerCrc32

CRC32 байтов 0–23 заголовка файла

Правила цепочки

  • Данные файла со смещением A и размером D занимают байты с A+28 по A+28+D-1, следующий файл начинается с A+28+D. У последнего файла в цепочке поле nextFileAddress равно 0x0000 или 0xFFFF, у остальных файлов оно обязано содержать именно это вычисленное значение; другое значение означает повреждение.

  • Файл хранится непрерывно. При удалении файла последующие файлы сдвигаются на его место, освободившийся конец заполняется 0x00.

  • Файлы расположены в порядке добавления. Исключение — файл device.id: он всегда первый.

  • Место свободно, если первый байт имени равен 0x00 или 0xFF. Записанное имя при неверном headerCrc32 означает повреждение, а не свободное место.

  • headerCrc32 проверяется при обходе цепочки для каждого заголовка, crc32 данных — при каждом чтении файла.

  • Смещения 16-битные, поэтому объем памяти не превышает 64 КБайт.

Идентификационные данные устройства

Устройство может состоять из нескольких плат, у каждой своя EEPROM. Модель, серийный номер и аппаратная ревизия устройства в целом хранятся отдельно от данных платы — в файле device.id, чтобы идентификационные данные устройства сохранялись при замене платы в ремонте.

Файл device.id всегда первый в цепочке, поэтому загрузчик получает данные устройства, прочитав 540 байт: заголовок платы (256), заголовок файла (28) и запись (256). С заголовком версии 1 — 796 байт.

Формат записи device.id, 256 байт:

Смещение

Размер

Поле

Описание

0–7

8

magic

Сигнатура JHDEVID\0

8

1

record_version

Версия записи: 1

9

1

signature_version

Алгоритм подписи устройства: 0 — подписи нет, 1 — ECDSA secp192r1, 2 — ECDSA secp256r1

10–11

2

—

Резерв

12–43

32

device_model

Модель устройства

44–75

32

device_serial

Серийный номер устройства

76–91

16

hw_revision

Аппаратная ревизия: числа через точку с необязательной буквой варианта исполнения, например 1.2 или 1.2a

92–93

2

flags

Флаги, все биты зарезервированы: записывается 0, при чтении игнорируется

94–179

86

—

Резерв

180–243

64

signature

Подпись устройства: пара чисел r и s без кодирования DER, дополненная нулями

244–251

8

timestamp

Время создания записи, секунды Unix-времени

252–255

4

crc32

CRC32 байтов 0–251

Строковые поля записи — строки фиксированной длины. Хвост записи (signature, timestamp, crc32) расположен по тем же смещениям, что и в заголовке платы. Запись защищена двумя контрольными суммами: своей и CRC32 данных файла. Поэтому ее можно проверить и внутри файловой системы, и отдельно извлеченной.

Правила чтения записи:

  • Буфер из одних 0x00 или 0xFF означает, что записи нет.

  • record_version, отличный от 1, и неизвестный signature_version — ошибка разбора.

Подпись записи — подпись устройства. Что она подписывает и как ее проверить, описано в разделе Проверка цифровой подписи.

Предыдущие версии заголовка

Библиотека jeefs разбирает все версии заголовка.

Версия

Размер

Отличия от версии 4

1

512 байт

Полей signature_version, signature и timestamp нет. Байты 180–211 — массив modules из 16 идентификаторов модулей (16-битные числа), 212–507 — резерв, CRC32 байтов 0–507 записан в байтах 508–511. Файловая система начинается со смещения 0x0200.

2

256 байт

Полей signature_version, signature и timestamp нет: байты 9, 11 и 180–251 зарезервированы.

3

256 байт

Раскладка совпадает с версией 4, поле по смещению 76 называется serial.

В версиях 1 и 2 байт 10 описан как часть резерва, но он всегда остается байтом fs_version: запись файловой системы проставляет в него номер версии и пересчитывает контрольную сумму заголовка. При перезаписи заголовка этот байт нужно сохранять.

Библиотека jeefs

Библиотека jeefs реализует формат на нескольких языках. Реализации проверяются общим набором тестов и эталонных образов.

Язык

Установка

Возможности

Python

pip install jeefs

Заголовки, запись device.id, сборка и разбор образа EEPROM целиком

Rust

cargo add jeefs-header

Заголовки, запись device.id, файловая система (без динамической памяти, no_std), сборка и разбор образа

C, C++17

CMake-пакет jeefs, pkg-config

Заголовки, запись device.id, файловая система. Библиотека не выполняет ввод-вывод и работает с буфером, прочитанным из EEPROM

Разбор образа EEPROM на Python:

from pathlib import Path

from jeefs import parse_image

image = parse_image(Path("eeprom.bin").read_bytes())
header = image.header
if header is None:  # the Python package does not parse v1 and v2 fields
    print(f"v{image.version}: header fields unavailable")
else:
    print(f"v{image.version}", header.boardname, header.boardversion, header.mac)
for file in image.files:
    print(file.name, len(file.data))

Функция parse_image выдает исключение ValueError, если заголовок платы не найден или поврежден, цепочка файлов нарушена или версия файловой системы неизвестна. Файлы с неверной контрольной суммой данных перечислены в image.unreadable. У образов с заголовками версий 1 и 2 image.header равен None: поля этих версий Python-пакет не разбирает, их читает реализация на C.