diff --git a/c/f28335-rs-boot/README.md b/c/f28335-rs-boot/README.md new file mode 100644 index 0000000..9adcf23 --- /dev/null +++ b/c/f28335-rs-boot/README.md @@ -0,0 +1,45 @@ +# Загрузчик F28335 по RS для periph_28335 + +Постоянный загрузчик принимает `INITLOAD` (58), `LOAD` (51) и `TFLASH` (55), +совместимые с Flash RS в терминале `periph_28335` SETGUI. Ядро не зависит от +UART, GPIO, ОС и Qt. Оно использует существующий валидатор и writer +`c/protocan-boot/ports/tms320f28335`, сохраняя единый формат SCI8/FAB1. + +Слои: клиент periph_28335 → автомат RS → проверка SCI8/FAB1 → callbacks платы +→ TI Flash API. Передаются октеты; адреса CPU считаются 16-битными словами. + +| Файл | Назначение и зависимости | +|---|---| +| `include/f28335_rs_boot.h`, `src/f28335_rs_boot.c` | Переносимый автомат, зависит от protocan-boot | +| `ports/tms320f28335/flash_api.c` | Официальный TI API v2.10, заголовки устройства, частота F28335_RS_CPU_HZ | +| `ports/tms320f28335/boot.cmd` | Постоянный сектор A, отдельные RAMAPI, RAMFUNCS и стек | +| `tools/build.py` | Сборка boot и выпуск проверенных Flash OUT/SCI8 с manifest/SHA-256 | +| `tests/test_boot.py` | Настоящий автомат и writer в модели RAM/Flash без оборудования | + +Приложение занимает `[0x300000,0x338000)`, первые 16 слов содержат FAB1. +Сектор A и переход `0x33FFF6` принадлежат постоянному загрузчику. +`INITLOAD` разрешает только staging `0x200000`, чётный размер 24..130560 +октетов. Ошибочные CRC, таймаут и ошибка UART отзывают подтверждение LOAD. +Перед erase повторно проверяются RAM, CRC32, все адреса, entry и FAB1. +Общий writer пишет FAB1 последним и проверяет физическую Flash. +Ошибка не удаляет recovery; перезапуск разрешён после успешного ACK. + +Контракт callbacks: `stage_write(user, offset, octet)` и `stage.read(user, +offset, buffer, size)` обслуживают RAM относительно начала файла; +`transmit(user, data, size)` возвращает true после физического окончания TX; +`program(user, image)` вызывает общий Flash writer; `reboot(user)` сбрасывает +контроллер. Все чтения/записи должны завершиться полностью либо вернуть false. +Прерывания в постоянном загрузчике запрещены, watchdog выключен на время +стирания. Межбайтовый таймаут аппаратный порт передаёт в `f28335_rs_abort`. + +Быстрый старт: выделить `f28335_rs_boot_t` в RAM, заполнить пять callbacks, +вызвать `f28335_rs_init(&boot, &port, 16)` и передавать каждый октет SCI +через `f28335_rs_feed(&boot, octet)`. До запуска приложения вызвать +`f28335_flash_app_validate`, после успеха перейти на возвращённый entry. +Аппаратные таймауты, выключение силовых выходов и выбор скорости задаёт плата. + +Подключено в `prolitarski_ups`: SCI-A/RS-422, 115200 8N1, адрес 16; +Monitor №93=1 в остановленном состоянии запрашивает boot через watchdog. +Первичная установка требует записи двух OUT через JTAG с сохранением +сектора A при загрузке приложения. Все последующие RS-обновления меняют B–H. +Повреждённое приложение оставляет устройство в RS recovery после сброса. diff --git a/c/f28335-rs-boot/include/f28335_rs_boot.h b/c/f28335-rs-boot/include/f28335_rs_boot.h new file mode 100644 index 0000000..5243bcf --- /dev/null +++ b/c/f28335-rs-boot/include/f28335_rs_boot.h @@ -0,0 +1,23 @@ +#ifndef F28335_RS_BOOT_H /* Один контракт обслуживает постоянный загрузчик и его модель на ПК. */ +#define F28335_RS_BOOT_H /* Исключает повторные объявления при подключении аппаратного порта. */ +#include "../../protocan-boot/ports/tms320f28335/f28335_flash_update.h" /* Повторно использует проверку SCI8/FAB1 и запись с защитой сектора A. */ +#define F28335_RS_STAGE 0x200000UL /* Адрес слова внешней RAM совпадает с INITLOAD в periph_28335. */ +#define F28335_RS_LIMIT F28335_AB_SLOT_BYTES /* Проверяемый SCI8 ограничен 130560 октетами общего валидатора F28335. */ +typedef struct { /* Аппаратная плата владеет памятью и UART; ядро владеет только состоянием протокола. */ + bool (*stage_write)(void *, uint32_t, unsigned int); /* Записывает один октет по относительному смещению staging RAM. */ + f28335_boot_io_t stage; /* Читает октеты staging для повторной CRC32 и проверки всех адресов перед erase. */ + bool (*transmit)(void *, const unsigned int *, unsigned int); /* Передаёт полный ответ и дожидается последнего стопового бита. */ + unsigned int (*program)(void *, f28335_flash_update_t *); /* Возвращает 0 после записи и проверки, код TI либо 1004/1006 при защите или ошибке проверки; управление остаётся в секторе A. */ + void (*reboot)(void *); /* Выполняет reset только после успешного ответа о завершении записи. */ + void *user; /* Независимый контекст UART, Flash и модели испытаний. */ +} f28335_rs_port_t; /* Аппаратный порт не собирает кадры и не дублирует алгоритм проверки образа. */ +typedef struct { /* Экземпляр допускает работу без heap и без прерываний. */ + f28335_rs_port_t port; /* Сохраняет callbacks на весь сеанс обновления. */ + f28335_flash_update_t image; /* Ограниченный индекс адресных секций и страничный кеш общего валидатора. */ + uint32_t length, position, expected, received, crc32; /* Длины и позиции считаются октетами, независимо от sizeof(char) C28x. */ + unsigned int address, command, crc16, wire_crc, loaded, fault, header[16]; /* Заголовок вмещает максимальный фиксированный запрос TFLASH на 16 октетов. */ +} f28335_rs_boot_t; /* Загруженный блок никогда не исполняется как произвольный код RAM. */ +void f28335_rs_init(f28335_rs_boot_t *s, const f28335_rs_port_t *port, unsigned int address); /* Настраивает адрес 1..255 и сбрасывает разрешение программирования. */ +void f28335_rs_abort(f28335_rs_boot_t *s); /* Сбрасывает неполный кадр и подтверждение LOAD при таймауте или аппаратной ошибке SCI. */ +void f28335_rs_feed(f28335_rs_boot_t *s, unsigned int value); /* Принимает один восьмибитный октет совместимых INITLOAD/LOAD/TFLASH. */ +#endif /* Завершает публичный интерфейс общего RS-загрузчика. */ diff --git a/c/f28335-rs-boot/ports/tms320f28335/boot.cmd b/c/f28335-rs-boot/ports/tms320f28335/boot.cmd new file mode 100644 index 0000000..ceb04ff --- /dev/null +++ b/c/f28335-rs-boot/ports/tms320f28335/boot.cmd @@ -0,0 +1,28 @@ +MEMORY /* Все адреса и размеры относятся к 16-битным словам CPU C28x. */ +{ /* Постоянный загрузчик использует только сектор A и внутреннюю RAM. */ +PAGE 0: /* Исполняемый код и адреса загрузки из Flash. */ + FLASHA : origin = 0x338000, length = 0x007F80 /* Область постоянного boot заканчивается до служебной области CSM. */ + BEGIN : origin = 0x33FFF6, length = 0x000002 /* Два слова аппаратного перехода Boot ROM в режиме Flash. */ + RAMAPI : origin = 0x008000, length = 0x001800 /* 6144 слова предназначены для копии официального TI Flash API. */ + RAMFUNCS : origin = 0x00F800, length = 0x000800 /* InitFlash выполняется из RAM, не затрагивая код самого API. */ + RESET : origin = 0x3FFFC0, length = 0x000002 /* Вектор reset находится в ROM и не программируется. */ +PAGE 1: /* Данные загрузчика не зависят от размещения будущего приложения. */ + RAMM0 : origin = 0x000058, length = 0x0003A8 /* Слова 50..57 сохранены для запроса boot через watchdog reset. */ + RAMM1 : origin = 0x000400, length = 0x000400 /* Стек на 1024 слова вмещает буфер writer и контекст runtime. */ + RAMDATA : origin = 0x00E000, length = 0x001800 /* Индекс SCI8, состояния протокола и аппаратные данные находятся в RAM. */ +} /* Внешняя RAM используется через callbacks и не содержит исполняемого кода. */ +SECTIONS /* C runtime загрузчика, в отличие от приложения, целиком сохраняется в A. */ +{ /* Размещение API указано до общей .text, чтобы извлечь все его код и константы. */ + codestart : > BEGIN, PAGE = 0 /* Штатный TI startup запрещает watchdog и вызывает _c_int00 загрузчика. */ + flash_api : { -lFlash28335_API_V210.lib(.text) -lFlash28335_API_V210.lib(.econst) } LOAD = FLASHA, RUN = RAMAPI, LOAD_START(_FlashApiLoadStart), LOAD_END(_FlashApiLoadEnd), RUN_START(_FlashApiRunStart), PAGE = 0 /* Ни одна функция TI API не исполняется из занятого банка Flash. */ + .text : > FLASHA, PAGE = 0 /* Протокол, валидатор и собственный runtime загрузчика остаются после обновления приложения. */ + .cinit : > FLASHA, PAGE = 0 /* Инициализация данных не зависит от целостности B–H. */ + .pinit : > FLASHA, PAGE = 0 /* Поддерживает стандартную секцию инициализации COFF. */ + .econst : > FLASHA, PAGE = 0 /* Таблицы и константы загрузчика также защищены от erase B–H. */ + .switch : > FLASHA, PAGE = 0 /* Таблицы переходов автомата не находятся в заменяемом приложении. */ + ramfuncs : LOAD = FLASHA, RUN = RAMFUNCS, LOAD_START(_RamfuncsLoadStart), LOAD_END(_RamfuncsLoadEnd), RUN_START(_RamfuncsRunStart), PAGE = 0 /* InitFlash переносится до изменения Flash wait states. */ + .stack : > RAMM1, PAGE = 1 /* Стек отделён от handoff и буфера внешней RAM. */ + .ebss : > RAMDATA, PAGE = 1 /* Нулевая инициализация касается только состояния boot. */ + .esysmem : > RAMDATA, PAGE = 1 /* Оставляет корректную секцию runtime без использования heap в алгоритме. */ + .reset : > RESET, PAGE = 0, TYPE = DSECT /* ROM-вектор не попадает в программируемый OUT. */ +} /* Пароли CSM, OTP и сектора приложения не создаются в образе загрузчика. */ diff --git a/c/f28335-rs-boot/ports/tms320f28335/flash_api.c b/c/f28335-rs-boot/ports/tms320f28335/flash_api.c new file mode 100644 index 0000000..d5a6237 --- /dev/null +++ b/c/f28335-rs-boot/ports/tms320f28335/flash_api.c @@ -0,0 +1,33 @@ +#include "DSP2833x_Device.h" /* Использует официальные описания регистров выбранной платы F28335. */ +#include "../../include/f28335_rs_boot.h" /* Получает общий writer с повторной проверкой staging перед erase. */ +#include "Flash2833x_API_Library.h" /* Официальный TI API v2.10 выполняет физические операции внутренней Flash. */ +#include /* Копирует код и таблицы TI API в RAM до изменения общего банка Flash. */ +extern Uint16 FlashApiLoadStart, FlashApiLoadEnd, FlashApiRunStart; /* LOAD/RUN-символы создаёт linker постоянного загрузчика. */ +static FLASH_ST api_status; /* Диагностика TI хранится в RAM и доступна во время занятости Flash. */ +static unsigned int api_error; /* Код последней операции TI передаётся клиенту без подмены успехом. */ +static bool erase(void *user) /* Стирает исключительно семь секторов приложения B–H. */ +{ /* Сам API исполняется из RAM; вызывающий код продолжит работу из сохранённого сектора A. */ + (void)user; api_error = Flash_Erase(0x00FEU, &api_status); return api_error == STATUS_SUCCESS; /* В TI v2.10 бит 0 выбирает A, маска FE исключает его и сохраняет recovery; результат сохраняет диагностику API. */ +} /* Ненулевой результат запрещает программирование данных. */ +static bool program(void *user, uint32_t address, uint16_t *data, unsigned int words) /* Записывает и немедленно проверяет одну ограниченную порцию. */ +{ /* Дополнительная аппаратная проверка диапазона защищает boot независимо от wire-парсера. */ + Uint16 result; (void)user; /* Результат официального API не подменяется успешным кодом протокола. */ + if (!words || address < F28335_FLASH_APP_START || address >= F28335_FLASH_APP_STOP || words > F28335_FLASH_APP_STOP - address) return false; /* Исключает сектор A, CSM, OTP и переполнение верхней границы. */ + result = Flash_Program((Uint16 *)address, data, words, &api_status); /* Данные и рабочий буфер находятся в RAM. */ + if (!result) result = Flash_Verify((Uint16 *)address, data, words, &api_status); /* Подтверждает физическую запись до перехода к следующей порции. */ + api_error = result; return result == STATUS_SUCCESS; /* Ошибка оставляет FAB1 неопубликованным, сохраняет код TI и RS recovery. */ +} /* Все прерывания остаются запрещёнными на протяжении жизни постоянного загрузчика. */ +uint16_t F28335Rs_ReadFlash(void *user, uint32_t address) /* Читает физическое слово для общей проверки приложения перед запуском. */ +{ /* Общий валидатор ограничивает адреса диапазоном B–H. */ + (void)user; return *(volatile Uint16 *)address; /* Чтение применяется только вне активной операции Flash API. */ +} /* Стёртые промежутки проверяются вместе с загруженными секциями. */ +unsigned int F28335Rs_Program(void *user, f28335_flash_update_t *image) /* Callback работает из сектора A и возвращает полный статус, совместимый с Flash RS periph_28335. */ +{ /* Проект задаёт реальный SYSCLK в герцах через F28335_RS_CPU_HZ при компиляции. */ + f28335_flash_ops_t ops; (void)user; /* Таблица операций не содержит обращений к приложению из B–H. */ + if (CsmRegs.CSMSCR.bit.SECURE) return 0x1004U; /* Не начинает стирание защищённого CSM устройства и передаёт клиенту стандартный статус защиты. */ + memcpy(&FlashApiRunStart, &FlashApiLoadStart, &FlashApiLoadEnd - &FlashApiLoadStart); /* Все .text и .econst TI API копируются во внутреннюю RAM. */ + Flash_CPUScaleFactor = (Uint32)(1048576.0L * 200.0L * F28335_RS_CPU_HZ / 1000000000.0L); /* Формула IQ20 из TI v2.10 учитывает фактическую частоту CPU, единицы — Гц. */ + Flash_CallbackPtr = 0; ops.erase = erase; ops.program = program; ops.read = F28335Rs_ReadFlash; ops.user = 0; /* Callback во Flash во время erase/program запрещён. */ + api_error = 0U; if (f28335_flash_update_program(image, &ops)) return 0U; /* Повторная CRC staging, запись FAB1 последним и CRC физической Flash находятся в общем writer; ноль означает подтверждённый успех. */ + return api_error ? api_error : 0x1006U; /* Ошибка TI сохраняет свой код, отказ структуры/readback без ошибки API получает отдельный ненулевой статус. */ +} /* После отказа управление остаётся в постоянном загрузчике и допускает новый LOAD. */ diff --git a/c/f28335-rs-boot/src/f28335_rs_boot.c b/c/f28335-rs-boot/src/f28335_rs_boot.c new file mode 100644 index 0000000..e95d41f --- /dev/null +++ b/c/f28335-rs-boot/src/f28335_rs_boot.c @@ -0,0 +1,79 @@ +#include "../include/f28335_rs_boot.h" /* Единственная реализация автомата используется платой и host-тестами. */ +#include /* Сбрасывает состояние без динамического выделения памяти. */ +static unsigned int crc16(unsigned int crc, unsigned int value) /* CRC16 Modbus защищает запросы и ответы существующего periph_28335. */ +{ /* Все операции явно ограничены 16 битами и подходят для 16-битного int C28x. */ + unsigned int bit; /* Каждый принятый октет содержит ровно восемь значащих битов. */ + crc ^= value & 255U; /* Старшие биты аппаратного регистра SCI не являются данными протокола. */ + for (bit = 0U; bit < 8U; ++bit) crc = (crc & 1U) ? ((crc >> 1) ^ 0xA001U) : (crc >> 1); /* Отражённый полином A001 соответствует CRC16 Modbus. */ + return crc & 65535U; /* На ПК unsigned int шире C28x, поэтому маска сохраняет одинаковый результат. */ +} /* Завершает переносимый шаг CRC. */ +static uint32_t get32(const unsigned int *p) /* Поля BIOS передаются младшим октетом вперёд, в отличие от адреса внутри SCI8. */ +{ /* Приведение перед сдвигом исключает потерю старших битов на 16-битном int. */ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); /* Возвращает адрес слова либо длину октетов по назначению поля. */ +} /* Формат запроса совпадает с periph28335_firmware.request. */ +static bool reply(f28335_rs_boot_t *s, unsigned int command, unsigned int status) /* TFLASH сообщает конкретный статус, остальные успешные команды — короткий ACK. */ +{ /* Ответ не содержит хвостового паддинга, который клиент periph_28335 считает необязательным. */ + unsigned int frame[6], crc = 0xFFFFU, i, size = command == 55U ? 4U : 2U; /* CMD_TFLASH=55 включает два октета результата; ACK INITLOAD/LOAD содержит адрес и команду. */ + frame[0] = s->address; frame[1] = command; frame[2] = status & 255U; frame[3] = status >> 8; /* Сохраняет принятый адрес и little-endian статус. */ + for (i = 0U; i < size; ++i) crc = crc16(crc, frame[i]); /* Контрольная сумма защищает все значащие поля ответа. */ + frame[size] = crc & 255U; frame[size + 1U] = crc >> 8; /* CRC16 передаётся младшим октетом вперёд. */ + return s->port.transmit(s->port.user, frame, size + 2U); /* Reset разрешается только после фактического завершения передачи. */ +} /* Ошибка UART никогда не превращается в разрешение перезапуска. */ +void f28335_rs_abort(f28335_rs_boot_t *s) /* Инвалидирует частичный кадр и прежнее подтверждение образа. */ +{ /* Порт вызывает эту функцию при межбайтовом таймауте, переполнении либо RXERROR. */ + s->position = 0UL; s->loaded = 0U; s->length = 0UL; s->image.valid = false; /* Новый TFLASH после сбоя требует полного нового INITLOAD/LOAD. */ +} /* Прерывание сеанса не изменяет действующую Flash. */ +void f28335_rs_init(f28335_rs_boot_t *s, const f28335_rs_port_t *port, unsigned int address) /* Создаёт независимый экземпляр без скрытых глобальных переменных. */ +{ /* Конфигурация платы должна передать все обязательные callbacks. */ + memset(s, 0, sizeof(*s)); s->port = *port; s->address = address; /* Обнуляет кеш, счётчики и разрешение LOAD до первого принятого кадра. */ +} /* Аппаратная инициализация SCI и GPIO остаётся в проекте платы. */ +static void execute(f28335_rs_boot_t *s) /* Выполняет только полностью принятый запрос с подтверждённой CRC16. */ +{ /* Транспортные поля никогда не разыменовываются как произвольные CPU-адреса. */ + uint32_t source, destination, length; unsigned int status = 0U; pcan_boot_manifest_t manifest; /* Manifest общего валидатора используется только для размера и транспортной CRC32. */ + if (s->command == 58U) { /* CMD_INITLOAD=58 резервирует фиксированный безопасный staging вместо произвольной записи памяти. */ + source = get32(s->header + 2); length = get32(s->header + 6); s->loaded = 0U; s->length = 0UL; /* Новая подготовка всегда отзывает подтверждение прежнего блока. */ + if (source == F28335_RS_STAGE && length >= 24UL && length <= F28335_RS_LIMIT && !(length & 1UL)) { s->length = length; reply(s, 58U, 0U); } /* Чётный SCI8 должен поместиться в предел общего проверяемого формата. */ + } else if (s->command == 51U) { /* CMD_LOAD=51 уже принял поток в staging, но до этой точки не разрешал запись Flash. */ + if (!s->fault && s->length) { s->loaded = 1U; reply(s, 51U, 0U); } /* Подтверждает блок только после полного приёма, успешной записи RAM и CRC16. */ + } else if (s->command == 55U) { /* CMD_TFLASH=55 совместим с шестибайтовым статусом periph_28335. */ + destination = get32(s->header + 2); source = get32(s->header + 6); length = get32(s->header + 10); /* Назначение, источник и размер сохраняют порядок полей существующего GUI. */ + if (destination || source || length) { /* Нулевая тройка служит безопасной пробой поддержки без стирания и запуска. */ + if (!s->loaded || destination != F28335_FLASH_APP_START || source != F28335_RS_STAGE || length != s->length) status = 0x1003U; /* Требует ровно тот блок, который подтвердил завершённый LOAD. */ + else { /* Общий валидатор повторно читает RAM и сравнивает CRC32, записанную при приёме. */ + memset(&manifest, 0, sizeof(manifest)); manifest.image_size = length; manifest.image_crc32 = s->crc32; /* Не доверяет CRC из непроверенного пользовательского manifest. */ + if (!f28335_flash_update_validate(&s->image, &s->port.stage, 0UL, &manifest)) status = 0x1002U; /* Отвергает сектор A, CSM, OTP, перекрытия, неверный FAB1 и повреждённые данные до erase. */ + else status = s->port.program(s->port.user, &s->image); /* Сохраняет конкретный код TI, защиту CSM 1004 и ошибку readback 1006; отказ оставляет RS recovery доступным. */ + } /* Завершает проверку и аппаратную запись текущего образа. */ + s->loaded = 0U; /* Повторная команда записи требует нового подтверждённого LOAD независимо от результата. */ + } /* Безопасная проба не потребляет подтверждённый блок RAM. */ + if (reply(s, 55U, status) && !status && length) s->port.reboot(s->port.user); /* Успешный reset следует только после записи, проверки физической Flash и отправки ACK. */ + } /* Остальные команды не поддерживаются и не меняют память. */ +} /* EEPROM параметров не входит ни в один callback загрузчика. */ +void f28335_rs_feed(f28335_rs_boot_t *s, unsigned int value) /* Разбирает запросы последовательно без большого приёмного массива. */ +{ /* Один вызов обрабатывает ровно один октет; время межбайтового ожидания контролирует порт. */ + uint32_t position = s->position; pcan_boot_byte_t octet = value & 255U; /* Маска исключает флаги SCI и старшие биты широкого char. */ + if (!position) { /* Новый кадр начинается только с адреса этого устройства. */ + if (octet != s->address || !s->address || s->address > 255U) return; /* Чужой адрес не получает ACK и не запускает запись RAM. */ + s->crc16 = 0xFFFFU; s->wire_crc = 0U; s->fault = 0U; /* CRC16 каждого запроса начинается со значения FFFF протокола Modbus. */ + } /* После первого октета автомат ожидает номер команды. */ + if (position == 1UL) { /* Второй октет задаёт фиксированную либо потоковую длину кадра. */ + s->command = octet; /* Сохраняет неизменный номер BIOS-команды. */ + if (octet == 58U) s->expected = 12UL; /* INITLOAD содержит адрес/команду, два u32 и CRC16: 12 октетов. */ + else if (octet == 55U) s->expected = 16UL; /* TFLASH содержит три u32 и занимает 16 октетов с CRC16. */ + else if (octet == 51U && s->length) { s->expected = s->length + 4UL; s->loaded = 0U; s->crc32 = 0UL; } /* LOAD состоит из адреса/команды, точного SCI8 и CRC16. */ + else { f28335_rs_abort(s); return; } /* Неизвестная команда не позволяет использовать оставшиеся байты как адрес CPU. */ + } /* Только распознанный заголовок разрешает приём полезных данных. */ + if (position < 2UL || s->command != 51U) { if (position < 16UL) s->header[position] = octet; } /* Фиксированные запросы сохраняются в ограниченном заголовке. */ + else if (position < s->expected - 2UL) { /* Поток LOAD записывается в RAM, пока завершающая CRC16 ещё не подтверждена. */ + if (!s->port.stage_write(s->port.user, position - 2UL, octet)) s->fault = 1U; /* Ошибка RAM запрещает подтверждение и последующий erase. */ + s->crc32 = pcan_boot_crc32_update(s->crc32, &octet, 1U); /* Независимая CRC32 позволяет обнаружить изменение staging после LOAD. */ + } /* Два завершающих октета CRC не попадают в staging образа. */ + if (position < 2UL || position < s->expected - 2UL) s->crc16 = crc16(s->crc16, octet); /* CRC16 включает адрес, команду и всю полезную нагрузку. */ + else if (position == s->expected - 2UL) s->wire_crc = octet; /* Первый октет контрольной суммы является младшим. */ + else s->wire_crc |= (unsigned int)octet << 8; /* Второй октет завершает 16-битную CRC принятого кадра. */ + s->position = position + 1UL; /* Продвигает курсор только после обработки текущего октета. */ + if (position >= 1UL && s->position == s->expected) { /* Запрос исполняется ровно один раз после получения последнего октета. */ + s->position = 0UL; /* Следующий адрес начнёт независимый кадр даже при ошибочной CRC текущего. */ + if (s->wire_crc == s->crc16) execute(s); else f28335_rs_abort(s); /* Повреждённый кадр отзывает прежний LOAD и никогда не вызывает program. */ + } /* Частичный кадр остаётся в ожидании следующего октета либо внешнего timeout. */ +} /* Автомат не использует системные часы, регистры MCU или динамическую память. */ diff --git a/c/f28335-rs-boot/tests/model.c b/c/f28335-rs-boot/tests/model.c new file mode 100644 index 0000000..8c5bd9a --- /dev/null +++ b/c/f28335-rs-boot/tests/model.c @@ -0,0 +1,51 @@ +#include "../src/f28335_rs_boot.c" /* Модель включает настоящий автомат, чтобы эталонный генератор кадров использовал тот же публичный wire CRC. */ +static f28335_rs_boot_t state; /* Каждый тест начинает независимый экземпляр MCU-протокола. */ +static pcan_boot_byte_t staging[F28335_RS_LIMIT]; /* Внешняя RAM модели хранит ровно допустимый максимум октетов SCI8. */ +static uint16_t flash[F28335_FLASH_APP_STOP - F28335_FLASH_APP_START + 16UL]; /* Дополнительные шестнадцать слов изображают защищённое начало сектора A. */ +static unsigned int response[6], response_size, erase_count, program_count, reset_count, fail_operation, tx_failure, read_failure; /* Счётчики и инъекции отказов доступны только испытаниям. */ +static bool stage_write(void *user, uint32_t offset, unsigned int value) /* Моделирует запись внешней RAM без изменения Flash. */ +{ /* Граница callback совпадает с физическим контрактом production-порта. */ + (void)user; if (offset >= F28335_RS_LIMIT) return false; staging[offset] = value; return true; /* Неверное смещение не получает неявного расширения буфера. */ +} /* Каждая принятая октета проходит настоящий автомат CRC16/CRC32. */ +static bool stage_read(void *user, uint32_t offset, pcan_boot_byte_t *data, unsigned int n) /* Имитирует readback RAM перед первым erase и внутри writer. */ +{ /* Тест может отказать в чтении, не подменяя валидатор. */ + (void)user; if (read_failure || offset > F28335_RS_LIMIT || n > F28335_RS_LIMIT - offset) return false; memcpy(data, staging + offset, n * sizeof(*data)); return true; /* Ошибка чтения должна остановить обновление до стирания. */ +} /* Копирование модели сохраняет исходные октеты файла. */ +static bool transmit(void *user, const unsigned int *data, unsigned int n) /* Сохраняет фактический ответ автомата без обработки на стороне модели. */ +{ /* Отдельная инъекция моделирует отказ отправки последнего ACK. */ + (void)user; response_size = n; memcpy(response, data, n * sizeof(*data)); return !tx_failure; /* Неуспешный TX не должен разрешать reset даже после правильной записи Flash. */ +} /* Статус и CRC ответа проверяет тест Python. */ +static bool erase(void *user) /* Моделирует выбранную аппаратным портом маску B–H. */ +{ /* Сектор A оставлен нетронутым для проверки каждого сценария обрыва. */ + (void)user; ++erase_count; if (fail_operation == 100U) return false; memset(flash, 255, (F28335_FLASH_APP_STOP - F28335_FLASH_APP_START) * sizeof(*flash)); return true; /* Отказ erase возвращается до любых program; успешное стирание затрагивает только приложение. */ +} /* Marker защищённого сектора не очищается даже при ошибке. */ +static bool program(void *user, uint32_t address, uint16_t *data, unsigned int n) /* Имитирует атомарность одной порции официального Flash API. */ +{ /* Порядок порций полностью задаёт production f28335_flash_update_program. */ + (void)user; ++program_count; if (program_count == fail_operation) return false; /* Любая порция, включая последнюю публикацию FAB1, может прерваться. */ + if (address < F28335_FLASH_APP_START || address >= F28335_FLASH_APP_STOP || n > F28335_FLASH_APP_STOP - address) return false; /* Выход за B–H запрещается аппаратной моделью независимо от парсера. */ + memcpy(flash + address - F28335_FLASH_APP_START, data, n * sizeof(*data)); return true; /* Модель предоставляет отдельную физическую память для заключительной проверки CRC. */ +} /* Частично записанный образ не получает искусственного разрешения запуска. */ +static uint16_t read_flash(void *user, uint32_t address) /* Читает фактическую модель Flash вместо staging. */ +{ /* Общий валидатор отвечает за допустимые адреса FAB1 и payload. */ + (void)user; return flash[address - F28335_FLASH_APP_START]; /* Одинаковый callback используется перед запуском и в writer. */ +} /* Содержимое staging после erase не влияет на физическую проверку CRC. */ +static unsigned int write_image(void *user, f28335_flash_update_t *image) /* Callback протокола вызывает настоящий общий writer. */ +{ /* Ни разбор SCI8, ни порядок записи не повторяются в модели. */ + f28335_flash_ops_t ops = {erase, program, read_flash, 0}; (void)user; return f28335_flash_update_program(image, &ops) ? 0U : 22U; /* Ошибка TI 22 моделирует отказ записи, возвращая управление в постоянный boot. */ +} /* Только production writer может опубликовать FAB1. */ +static void reboot(void *user) /* Запоминает факт запроса reset без завершения тестового процесса. */ +{ /* Результат можно сопоставить состоянию Flash и завершённому TX. */ + (void)user; ++reset_count; /* Ненулевой счётчик допустим только после проверки и ACK. */ +} /* Модель сохраняет данные после виртуального reset для независимой проверки. */ +void model_reset(void) /* Обнуляет состояние между независимыми испытаниями. */ +{ /* Восстановление модели никогда не обращается к настоящему устройству. */ + f28335_rs_port_t port = {stage_write, {stage_read, 0, 0}, transmit, write_image, reboot, 0}; /* Порт подключает только управляемые RAM/Flash callbacks модели. */ + memset(staging, 0, sizeof(staging)); memset(flash, 255, sizeof(flash)); flash[F28335_FLASH_APP_STOP - F28335_FLASH_APP_START] = 0xA55AU; /* Известный marker сектора A выявляет его ошибочное стирание в каждом сценарии. */ + response_size = erase_count = program_count = reset_count = fail_operation = tx_failure = read_failure = 0U; f28335_rs_init(&state, &port, 16U); /* Все тесты используют адрес 16 production-профиля ИБП. */ +} /* Старое разрешение LOAD не переносится в следующий тест. */ +void model_feed(unsigned int octet) { response_size = 0U; f28335_rs_feed(&state, octet); } /* Передаёт ровно один октет, как аппаратный SCI-порт. */ +void model_abort(void) { f28335_rs_abort(&state); } /* Моделирует таймаут 500 мс либо аппаратную ошибку UART. */ +void model_fault(unsigned int operation, unsigned int tx, unsigned int read) { fail_operation = operation; tx_failure = tx; read_failure = read; } /* Настраивает отказ без изменения самого протокола. */ +void model_corrupt(unsigned int offset) { staging[offset] ^= 1U; } /* Имитирует изменение staging после уже подтверждённого LOAD. */ +unsigned int model_crc(const unsigned char *data, unsigned int n) { unsigned int crc = 0xFFFFU, i; for (i = 0U; i < n; ++i) crc = crc16(crc, data[i]); return crc; } /* Генерирует запросы через production CRC; отдельный фиксированный вектор проверяет wire-совместимость. */ +unsigned int model_value(unsigned int index) { uint32_t entry; switch (index) { case 0: return response_size; case 1: return erase_count; case 2: return program_count; case 3: return reset_count; case 4: return f28335_flash_app_validate(read_flash, 0, &entry); case 5: return flash[F28335_FLASH_APP_STOP - F28335_FLASH_APP_START]; default: return index >= 10U && index < 16U ? response[index - 10U] : 0U; } } /* Предоставляет только наблюдаемые результаты, оставляя production-состояния закрытыми. */ diff --git a/c/f28335-rs-boot/tests/test_boot.py b/c/f28335-rs-boot/tests/test_boot.py new file mode 100644 index 0000000..1606b69 --- /dev/null +++ b/c/f28335-rs-boot/tests/test_boot.py @@ -0,0 +1,88 @@ +"""Проверяет production-автомат RS и общий writer на модели сбоев без платы.""" # Не вызывает JTAG, COM и TI Flash API. +import ctypes # Передаёт октеты настоящему скомпилированному C-автомату. +import importlib.util # Использует общий подписыватель FAB1 для корректных тестовых образов. +import os # Добавляет runtime GCC только в окружение тестового процесса. +from pathlib import Path # Разрешает пути относительно установленной библиотеки templates. +import struct # Формирует стандартные поля запросов и адресные секции SCI8. +import subprocess # Собирает C-модель с production-исходниками GCC. +import tempfile # Удаляет только собственные временные DLL проверки. +import unittest # Каждый отказ выполняется независимо и получает отдельный результат. +LIBRARY = Path(__file__).resolve().parents[1]; TEMPLATES = LIBRARY.parents[1]; PORT = TEMPLATES / "c/protocan-boot/ports/tms320f28335" # Тест использует те же валидатор и writer, которые компилируются для платы. +spec = importlib.util.spec_from_file_location("boot_test_contract", TEMPLATES / "python/protocan/f28335_flash.py"); contract = importlib.util.module_from_spec(spec); spec.loader.exec_module(contract) # Эталон FAB1 берётся из действующей общей библиотеки. + +def section(address, words): # Создаёт адресный блок TI без дополнительных правил обновления. + return struct.pack("> 16, address & 65535) + struct.pack("<" + "H" * len(words), *words) # Длина считается словами, каждое слово little endian, адрес — старшим словом вперёд. + +def image(): # Минимальный правильный образ содержит разрыв для проверки CRC стёртых промежутков. + entry = contract.APP_DATA; loaded = {entry: 0x1234, entry + 1: 0x5678, entry + 12: 0xAABB, entry + 13: 0xCCDD} # Промежуток между двумя секциями должен остаться FFFF после erase. + return b"\xaa\x08" + bytes(16) + struct.pack("> 16, entry & 65535) + section(contract.APP_START, struct.unpack("<16H", contract.descriptor(entry, loaded))) + section(entry, [0x1234, 0x5678]) + section(entry + 12, [0xAABB, 0xCCDD]) + bytes(2) # Заголовок FAB1 передаётся первым, но writer обязан опубликовать его последним. + +class BootTests(unittest.TestCase): # Проверяет наблюдаемое поведение полного обмена, а не отдельные строки реализации. + @classmethod # DLL компилируется один раз для всех сценариев отказа. + def setUpClass(cls): # Изолирует бинарный результат и зависимости GCC. + gcc = Path(os.environ.get("CC", "C:/msys64/mingw64/bin/gcc.exe")); os.environ["PATH"] = str(gcc.parent) + os.pathsep + os.environ["PATH"] # CC позволяет выбрать другой совместимый GCC без изменения теста. + cls.temporary = tempfile.TemporaryDirectory(prefix="f28335-rs-tests-"); dll = Path(cls.temporary.name) / "model.dll" # Все артефакты испытания находятся вне прошивки пользователя. + subprocess.run([str(gcc), "-shared", "-std=c99", "-Wall", "-Wextra", "-Werror", str(LIBRARY / "tests/model.c"), str(PORT / "f28335_flash_app.c"), str(PORT / "f28335_flash_update.c"), str(TEMPLATES / "c/protocan-boot/src/pcan_boot.c"), "-o", str(dll)], check=True) # Компилирует production parser, CRC, validator и writer без извлечения копий функций. + cls.native = ctypes.CDLL(str(dll)); cls.native.model_crc.argtypes = [ctypes.c_char_p, ctypes.c_uint]; cls.native.model_crc.restype = ctypes.c_uint; cls.native.model_value.restype = ctypes.c_uint # ABI содержит только явно заданные целые числа и октеты. + + @classmethod # Windows требует освободить библиотеку до удаления DLL. + def tearDownClass(cls): # Закрывает только дескриптор, созданный этим тестом. + if os.name == "nt": # На Windows открытая DLL удерживает файл от удаления. + release = ctypes.WinDLL("kernel32").FreeLibrary; release.argtypes = [ctypes.c_void_p]; release.restype = ctypes.c_int; release(cls.native._handle) # Освобождает единственную тестовую DLL с полным размером указателя. + cls.temporary.cleanup() # Временные файлы не остаются в библиотеке или проекте ИБП. + + def setUp(self): self.native.model_reset() # Стирает модель между тестами, сохраняя независимость сценариев. + + def frame(self, command, payload=b"", address=16): # Формирует запросы совместимого BIOS для управления production-автоматом. + body = bytes((address, command)) + payload; return body + struct.pack("|'): parser.error("artifact должен быть именем OUT без расширения") # Имя файла не может выйти за выбранный каталог выпуска. + if args.command == "build": # Ручной полный build полезен для проверки проекта вне запущенной IDE. + sources, includes = project_paths(args); compile_image(args, sources, includes, args.app_linker.resolve(), args.build_dir / (args.artifact + ".out")) # Использует действующие linked resources без ручного списка копий исходников. + publish(args) # Ненулевые коды компиляции, конверсии и проверки распространяются в CCS. + +if __name__ == "__main__": main() # Импорт тестами не запускает компилятор и не изменяет рабочую версию. diff --git a/c/firmware-info/tools/identity.py b/c/firmware-info/tools/identity.py index 542c56a..ae5f719 100644 --- a/c/firmware-info/tools/identity.py +++ b/c/firmware-info/tools/identity.py @@ -9,6 +9,7 @@ from __future__ import annotations import argparse from datetime import datetime, timedelta, timezone import hashlib +import importlib.util # Загружает общий контракт JSON без импорта протокольного runtime и без зависимости от установленного пакета. import json import os import re @@ -18,6 +19,10 @@ from pathlib import Path import subprocess import tempfile # Резервирует номер сборки через временный файл на том же диске, как общий итератор ветки 166. +_flashing_spec = importlib.util.spec_from_file_location("firmware_flashing_contract", Path(__file__).resolve().parents[3] / "python/set_devices/firmware_flashing.py") # Общий контракт переносится вместе с полным checkout templates. +_flashing = importlib.util.module_from_spec(_flashing_spec) # Изолирует проверку настроек от GUI и системных транспортов. +_flashing_spec.loader.exec_module(_flashing) # Использует ту же проверку полей, что SETGUI при чтении manifest. + def git_state(repository: Path) -> dict: """Record full commits, and flag untracked sources as well as tracked edits. @@ -66,6 +71,7 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di for field in ("product", "target"): if not isinstance(settings.get(field), str) or not settings[field].strip(): raise ValueError(f"missing {field}") + flashing = _flashing.validate_flashing(settings.get("flashing", {})) # Проверяет скорости и адреса до резервирования следующего номера сборки. if settings.get("increment_version", False): # Прямые вызовы templates из CCS, Keil и CLI используют один и тот же сохраняемый итератор. version = settings["version"] = next_version(version) # Вычисляет номер для JSON, заголовка и метаданных только один раз. with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=config.parent, prefix=".firmware-version-", delete=False) as stream: # Временный файл на том же диске позволяет атомарно заменить настройки без частичной записи. @@ -94,6 +100,7 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di "version": version, "build_id": build_id, "built_at_utc": stamp.isoformat(), "source": source, "templates": dependency, "identity_contract": 1, "identity_register_count": 12} + if "flashing" in settings: metadata["flashing"] = flashing # Переносит все CAN/RS-профили в prebuild, manifest, архив и комплект Публикатора без изменения бинарного протокола паспорта. constants = dict(zip(("FIRMWARE_VERSION_MAJOR", "FIRMWARE_VERSION_MINOR", "FIRMWARE_VERSION_PATCH"), version)) constants.update(dict(zip(("FI_YEAR", "FI_MONTH", "FI_DAY", "FI_HOUR", "FI_MINUTE", "FI_SECOND"), (stamp.year, stamp.month, stamp.day, stamp.hour, stamp.minute, stamp.second)))) diff --git a/python/set_devices/firmware_flashing.py b/python/set_devices/firmware_flashing.py new file mode 100644 index 0000000..911bde0 --- /dev/null +++ b/python/set_devices/firmware_flashing.py @@ -0,0 +1,50 @@ +"""Настройки формы прошивки из JSON проекта и manifest без зависимости от Qt.""" # Один контракт обслуживает сборщики Keil/CCS и потребителей комплекта. +from copy import deepcopy # Возвращает отдельный профиль, чтобы GUI не изменял сохранённый паспорт. + +LOADER_TRANSPORTS = {"manual": "manual", "set-rs485": "rs485", "set-can": "can", "protocan-can": "can", "stm32-uart": "stm32", "stm32-usb": "stm32", "f28335-jtag": "tms", "f28335-eeprom": "tms", "f28335-flash-rs": "tms"} # Коды совпадают с действующими загрузчиками SETGUI; транспорт сам по себе не определяет протокол. +COMMON_MODES = {"set-rs485": 0, "set-can": 1, "stm32-uart": 2, "protocan-can": 6, "stm32-usb": 7} # Индексы существующей общей формы нужны только порту GUI, без кодирования пакетов. +CAN_FIELDS = {"bitrate", "node_id", "product_type", "hardware_min", "hardware_max", "base_address"} # Скорость CAN выражается в бит/с, адрес приложения — в байтах. +RS_FIELDS = {"baudrate", "controller", "timeout_ms"} # Профиль ПМ35 задаёт скорость UART в бодах, адрес контроллера и добавочный тайм-аут ответа в миллисекундах. +PROFILE_FIELDS = {"manual": set(), "set-rs485": {"baudrate", "base_address"}, "set-can": CAN_FIELDS, "protocan-can": (CAN_FIELDS - {"base_address"}) | {"device_type", "session_id", "memory"}, "stm32-uart": {"baudrate", "base_address"}, "stm32-usb": {"base_address"}, "f28335-jtag": set(), "f28335-eeprom": RS_FIELDS, "f28335-flash-rs": RS_FIELDS} # ProtoCAN получает адреса из самого образа; остальные поля соответствуют действующим контролам загрузчиков. +INTEGER_LIMITS = {"bitrate": (1, 1000000), "baudrate": (1, 2147483647), "node_id": (0, 255), "product_type": (0, 65535), "hardware_min": (0, 255), "hardware_max": (0, 255), "base_address": (0, 4294967295), "controller": (0, 255), "timeout_ms": (50, 10000), "device_type": (0, 7), "session_id": (1, 255)} # Пределы взяты из полей SETGUI: Classic CAN до 1 Мбит/с, uint32 адреса и диапазоны существующих виджетов. + + +LOADER_TRANSPORTS["f28335-sci"] = "tms" # Штатный первичный SCI-загрузчик UlitCo использует собственный маршрут, отдельный от BIOS ПМ35. +COMMON_MODES["f28335-sci"] = 9 # Новый режим добавлен после существующих USB и STM32/J-Link без изменения их индексов. +PROFILE_FIELDS["f28335-sci"] = {"baudrate", "oscillator_hz", "enter_monitor", "timeout_ms"} # Частота кварца нужна Flash API, а переход Monitor передаётся только по явной настройке. + + +def validate_flashing(value): # Проверяет весь блок до изменения версии, формы или сохранённого manifest. + if not isinstance(value, dict) or set(value) - {"default_loader", "profiles"}: # Опечатки и null не должны молча превращаться в штатную скорость. + raise ValueError("flashing: ожидается объект с default_loader и profiles") # Сообщает имя исправляемого раздела проектного JSON. + profiles = value.get("profiles", {}) # Отсутствующий раздел сохраняет совместимость старых проектов без настроек прошивки. + if not isinstance(profiles, dict): # Список профилей должен сопоставлять стабильный код загрузчика с его параметрами. + raise ValueError("flashing.profiles: ожидается объект") # Не допускает строку или массив вместо именованных профилей. + for loader, profile in profiles.items(): # Проверяет также альтернативные CAN/RS-профили до резервирования номера сборки. + if loader not in PROFILE_FIELDS or not isinstance(profile, dict): # Неизвестный протокол требует поддержки в GUI, а не угадывания по названию. + raise ValueError("flashing.profiles: неизвестный загрузчик или неверный профиль " + str(loader)) # Указывает конкретный неподдержанный ключ. + unknown = set(profile) - PROFILE_FIELDS[loader] # Исключает параметры другого протокола и неработающие настройки интерфейса. + if unknown: # Лишнее поле часто означает опечатку в скорости или неверный выбор загрузчика. + raise ValueError("flashing." + loader + ": неподдерживаемые поля " + ", ".join(sorted(unknown))) # Не создаёт manifest с настройками, которые GUI не способен применить. + for field, number in profile.items(): # Проверяет типы без неявного преобразования строк и логических значений в числа. + if field in INTEGER_LIMITS: # Все числовые параметры имеют явно определённые единицы и пределы. + low, high = INTEGER_LIMITS[field] # Диапазон соответствует конкретному виджету или целочисленному адресу загрузчика. + if loader == "protocan-can" and field == "node_id": high = 15 # Поле Device действующей формы ProtoCAN ограничено четырьмя битами. + if type(number) is not int or not low <= number <= high: # JSON true и дробные скорости запрещены даже при числовой совместимости Python. + raise ValueError(f"flashing.{loader}.{field}: нужно целое число {low}..{high}") # Останавливает prebuild и импорт до применения неверного значения. + elif field == "enter_monitor" and type(number) is not bool: # Переход UlitCo в первичный загрузчик включается явным логическим значением JSON. + raise ValueError("flashing.f28335-sci.enter_monitor: нужно true или false") # Не трактует строку false как разрешение управляющей команды. + elif field == "oscillator_hz" and (type(number) is not int or number not in (20000000, 25000000, 30000000)): # Целочисленные частоты кварца дают 100, 125 и 150 МГц при штатном PLL ядра TI. + raise ValueError("flashing.f28335-sci.oscillator_hz: нужно 20000000, 25000000 или 30000000") # Ошибочная частота не должна приводить к неверным временам Flash API. + elif field == "memory" and number not in ("eeprom", "flash", "stm32"): # Коды соответствуют существующему выбору памяти ProtoCAN. + raise ValueError("flashing.protocan-can.memory: нужно eeprom, flash или stm32") # Не подменяет назначение неподдержанного значения EEPROM по умолчанию. + if profile.get("hardware_min", 0) > profile.get("hardware_max", 255): # Проверяет согласованность диапазона аппаратных ревизий до изменения формы. + raise ValueError("flashing." + loader + ": hardware_min больше hardware_max") # Неверный диапазон совместимости не должен попасть в опубликованный паспорт. + default = value.get("default_loader") # Отсутствующий маршрут оставляет прежнее определение назначения по артефакту. + if "default_loader" in value and (not isinstance(default, str) or default not in profiles): # Явный маршрут обязан иметь собственный проверенный профиль, включая пустой manual/JTAG. + raise ValueError("flashing.default_loader: укажите загрузчик из profiles") # Ссылка на отсутствующий профиль не должна оставлять параметры предыдущего изделия. + return deepcopy(value) # Сохраняет исходные единицы и типы JSON, отделяя данные потребителя от настроек проекта. + + +def flashing_profile(metadata, loader): # Возвращает параметры конкретного загрузчика одного проверенного выпуска. + return validate_flashing(metadata.get("flashing", {})).get("profiles", {}).get(loader, {}) # Старый manifest без flashing продолжает работать с прежними ручными настройками.