Добавить общие графики и драйвер parallel NAND #3
@@ -34,16 +34,22 @@ templates/
|
|||||||
| [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 |
|
| [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 |
|
||||||
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
|
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
|
||||||
| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android и STM32F4-порты в комплекте** |
|
| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android и STM32F4-порты в комплекте** |
|
||||||
|
| [`c/set-protocol/ports/stm32-bxcan`](c/set-protocol/ports/stm32-bxcan) | порт прикладного ProtoCAN для STM32, бывший SETCAN; сохранён API `PROTOCAN_*` | STM32 HAL CAN/RTC/TIM + общее ядро `pcan_id` | classic bxCAN; настройки платы предоставляет прошивка |
|
||||||
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
|
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
|
||||||
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
|
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
|
||||||
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
|
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
|
||||||
| [`c/firmware-info`](c/firmware-info) | SemVer, build stamp и git build ID работающего образа | C99 | config-порты STM32F1/F4/G4 и К1921ВК028; упаковка для Modbus/SETGUI |
|
| [`c/firmware-info`](c/firmware-info) | SemVer, build stamp и git build ID работающего образа | C99 | config-порты STM32F1/F4/G4 и К1921ВК028; упаковка для Modbus/SETGUI |
|
||||||
|
| [`c/parallel-nand`](c/parallel-nand) | parallel NAND x8: raw main/OOB, bad-block marker и банковое окно GAS для SETGUI | C99, опционально `set-protocol/pcan_gas` | **порты TMS320F2812, STM32F407VE и STM32G474CEU6 в комплекте** |
|
||||||
|
|
||||||
### Python
|
### Python
|
||||||
|
|
||||||
| Модуль | Что делает | Зависимости |
|
| Модуль | Что делает | Зависимости |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
|
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
|
||||||
|
| [`python/protocan/trends.py`](python/protocan/trends.py) | общие настройки графиков, ограниченная история, ctypes-декодер GAS/raw CAN | stdlib, опционально SETProtocol DLL/SO |
|
||||||
|
|
||||||
|
Общие тренды для Android GUI и SETGUI: [формат JSON, C99-ядро и адаптеры](c/set-protocol/docs/GUI_TRENDS.md).
|
||||||
|
Общие масштабирование и маркеры: [C99, JNI и Python/Qt](c/set-protocol/docs/GUI_PLOT.md).
|
||||||
|
|
||||||
Кодировщики `c/set-protocol` и `python/protocan` дают побайтово
|
Кодировщики `c/set-protocol` и `python/protocan` дают побайтово
|
||||||
одинаковый результат — это зафиксировано эталонами в тестах на C.
|
одинаковый результат — это зафиксировано эталонами в тестах на C.
|
||||||
@@ -94,7 +100,6 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
|
|||||||
|
|
||||||
| Код | Почему не переносится | Что можно вытащить |
|
| Код | Почему не переносится | Что можно вытащить |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `SETCAN/Src/protocan.c` | 59 вызовов `HAL_*`, привязка к `CAN_HandleTypeDef`, `RTC_HandleTypeDef`, `NVIC_SystemReset()` | разбиение длинных посылок на кадры и проверка даты — чистая логика, нужны два интерфейса: «отправить кадр» и «часы». Раскладка идентификатора уже вынесена в `pcan_id` |
|
|
||||||
| `1921vk028/drivers/user/src/one_wire_user.c` | аппаратный 1-Wire `OWI_*` из `plib028`, мигание светодиодом прямо внутри опроса датчика | логика DS18B20 уже есть в `c/ds18b20` — переносится порт, а не алгоритм |
|
| `1921vk028/drivers/user/src/one_wire_user.c` | аппаратный 1-Wire `OWI_*` из `plib028`, мигание светодиодом прямо внутри опроса датчика | логика DS18B20 уже есть в `c/ds18b20` — переносится порт, а не алгоритм |
|
||||||
| `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
|
| `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
|
||||||
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |
|
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |
|
||||||
|
|||||||
26
c/parallel-nand/CMakeLists.txt
Normal file
26
c/parallel-nand/CMakeLists.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(parallel_nand C)
|
||||||
|
|
||||||
|
set(CMAKE_C_STANDARD 99)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
add_library(parallel_nand
|
||||||
|
src/parallel_nand.c
|
||||||
|
src/parallel_nand_gas.c
|
||||||
|
)
|
||||||
|
target_include_directories(parallel_nand PUBLIC
|
||||||
|
include
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(parallel_nand_pcan_gas src/parallel_nand_pcan_gas.c)
|
||||||
|
target_include_directories(parallel_nand_pcan_gas PUBLIC
|
||||||
|
include
|
||||||
|
../set-protocol/include
|
||||||
|
)
|
||||||
|
target_link_libraries(parallel_nand_pcan_gas PUBLIC parallel_nand)
|
||||||
|
|
||||||
|
add_executable(test_parallel_nand tests/test_parallel_nand.c)
|
||||||
|
target_link_libraries(test_parallel_nand PRIVATE parallel_nand)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
add_test(NAME parallel_nand_core COMMAND test_parallel_nand)
|
||||||
88
c/parallel-nand/README.md
Normal file
88
c/parallel-nand/README.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# parallel-nand
|
||||||
|
|
||||||
|
Переносимый C99-драйвер асинхронной parallel NAND x8 и окно управления через
|
||||||
|
общее адресное пространство GAS. Первый профиль — Micron `MT29F1G08ABADA`:
|
||||||
|
2048+64 байта, 64 страницы в блоке, 1024 блока.
|
||||||
|
|
||||||
|
Подготовлены аппаратные порты:
|
||||||
|
|
||||||
|
- `TMS320F2812` — XINTF Zone 6;
|
||||||
|
- `STM32F407VET6` — аппаратный FSMC NAND Bank 2;
|
||||||
|
- `STM32G474CEU6` — GPIO bit-bang для корпуса UFQFPN-48.
|
||||||
|
|
||||||
|
Ядро не зависит от HAL и транспорта GUI. STM32 HAL используется только в
|
||||||
|
конкретных портах. `parallel_nand_gas` реализует переносимую 16-битную карту
|
||||||
|
GAS и компилируется в том числе TI C28x, где `char` имеет 16 бит.
|
||||||
|
|
||||||
|
## Подключение
|
||||||
|
|
||||||
|
В проект добавить:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c/parallel-nand/src/parallel_nand.c
|
||||||
|
c/parallel-nand/src/parallel_nand_gas.c
|
||||||
|
```
|
||||||
|
|
||||||
|
Пути заголовков:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c/parallel-nand/include
|
||||||
|
c/parallel-nand/ports/<нужный-порт>
|
||||||
|
```
|
||||||
|
|
||||||
|
И один аппаратный порт, например:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c/parallel-nand/ports/stm32g474ce/parallel_nand_port_stm32g474ce.c
|
||||||
|
```
|
||||||
|
|
||||||
|
Минимальная инициализация:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static parallel_nand_t nand;
|
||||||
|
static parallel_nand_gas_t nand_gas;
|
||||||
|
|
||||||
|
void memory_init(void)
|
||||||
|
{
|
||||||
|
int rc = parallel_nand_init(&nand,
|
||||||
|
parallel_nand_port_stm32g474ce(),
|
||||||
|
¶llel_nand_mt29f1g08_geometry);
|
||||||
|
if (rc != PNAND_OK) {
|
||||||
|
/* Оставить диагностику доступной GUI и сообщить ошибку платы. */
|
||||||
|
}
|
||||||
|
|
||||||
|
parallel_nand_gas_init(&nand_gas, &nand, PNAND_GAS_DEFAULT_BASE);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Диспетчер общего адресного пространства вызывает
|
||||||
|
`parallel_nand_gas_read()`/`parallel_nand_gas_write()` для адресов
|
||||||
|
`0xE000..0xE51F`. Для STM32 с `c/set-protocol/pcan_gas` дополнительно включить
|
||||||
|
`src/parallel_nand_pcan_gas.c`, создать `pcan_gas_region_t` и вызвать
|
||||||
|
`parallel_nand_pcan_gas_region_init()`. Этот адаптер намеренно не нужен TMS.
|
||||||
|
Для полного дампа предпочтительны пакетные `READ_REGISTERS/WRITE_REGISTERS`,
|
||||||
|
а classic CAN GAS оставлен для совместимости и коротких диагностических чтений.
|
||||||
|
|
||||||
|
Доступ GUI и алгоритм полного дампа описаны в
|
||||||
|
[docs/MEMORY_GAS.md](docs/MEMORY_GAS.md). Электрические соединения — в README
|
||||||
|
соответствующего порта и [docs/EMPTY_BOARD_PINOUT.md](docs/EMPTY_BOARD_PINOUT.md).
|
||||||
|
|
||||||
|
## Текущие границы
|
||||||
|
|
||||||
|
- Реализовано безопасное чтение, RESET, READ ID, STATUS и bad-block marker.
|
||||||
|
- PROGRAM/ERASE намеренно отсутствуют; ядро включает аппаратную защиту `WP#`.
|
||||||
|
- ECC пока не исправляет данные. GUI получает физические main/OOB и должен
|
||||||
|
помечать дамп как raw.
|
||||||
|
- В полном дампе bad-блоки не пропускаются: сохраняется физический порядок.
|
||||||
|
- Для другой NAND нужно передать другую `parallel_nand_geometry_t` и проверить
|
||||||
|
команды/маркер по её datasheet.
|
||||||
|
|
||||||
|
## Проверка первого запуска
|
||||||
|
|
||||||
|
1. Запустить `parallel_nand_init()`.
|
||||||
|
2. Через GAS проверить `0xE000 = 0x4E44`.
|
||||||
|
3. Записать `2` в `0xE004` (`READ_ID`).
|
||||||
|
4. Проверить `0xE00B = 0x002C`, `0xE00C = 0x00F1`.
|
||||||
|
5. Записать номер страницы в `0xE005/0xE006`, затем `3` в `0xE004`.
|
||||||
|
6. Дождаться `READY | PAGE_VALID` в `0xE002`.
|
||||||
|
7. Прочитать main из `0xE100..0xE4FF`, OOB из `0xE500..0xE51F`.
|
||||||
81
c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md
Normal file
81
c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Подключение parallel NAND на пустой плате
|
||||||
|
|
||||||
|
Документ рассчитан на асинхронную NAND x8 `MT29F1G08ABADA` 3,3 В в корпусе
|
||||||
|
TSOP-48 (суффикс `WP`). На второй фотографии NAND — длинная микросхема справа
|
||||||
|
с маркировкой `29F1G08ABADA`. Микросхема Micron `D9MDK` сверху — DDR2 SDRAM,
|
||||||
|
не NAND. На первой фотографии `AM29LV800BT` — parallel NOR, а не NAND.
|
||||||
|
|
||||||
|
Уточнение по ёмкости с фотографий:
|
||||||
|
|
||||||
|
- `K6R4008V1D` — SRAM 4 Mbit (512 KiB), не Flash;
|
||||||
|
- `AM29LV800BT` — parallel NOR 8 Mbit (1 MiB);
|
||||||
|
- `MT29F1G08ABADA` — parallel NAND 1 Gbit, то есть 128 MiB main area.
|
||||||
|
|
||||||
|
Поэтому текущий NAND-профиль рассчитан на 1-Gbit чип со второй платы. Для
|
||||||
|
другой плотности меняется `parallel_nand_geometry_t`; GAS и GUI-команды
|
||||||
|
остаются теми же.
|
||||||
|
|
||||||
|
Не переносите номера выводов TSOP-48 на вариант `H4` VFBGA-63. Для BGA нужно
|
||||||
|
использовать ball map конкретного полного part number.
|
||||||
|
|
||||||
|
## TSOP-48, x8
|
||||||
|
|
||||||
|
| NAND | Вывод | Назначение |
|
||||||
|
|---|---:|---|
|
||||||
|
| `R/B#` | 7 | open-drain Ready/Busy, подтяжка 4,7–10 кОм к 3,3 В |
|
||||||
|
| `RE#` | 8 | строб чтения от МК |
|
||||||
|
| `CE#` | 9 | выбор кристалла от МК |
|
||||||
|
| `VCC` | 12, 37 | питание 3,3 В |
|
||||||
|
| `VSS` | 13, 36 | земля |
|
||||||
|
| `CLE` | 16 | фиксация команды |
|
||||||
|
| `ALE` | 17 | фиксация адреса |
|
||||||
|
| `WE#` | 18 | строб команды/адреса от МК |
|
||||||
|
| `WP#` | 19 | для read-only 10 кОм на GND; для записи — GPIO и pull-down |
|
||||||
|
| `I/O0` | 29 | двунаправленная шина, бит 0 |
|
||||||
|
| `I/O1` | 30 | двунаправленная шина, бит 1 |
|
||||||
|
| `I/O2` | 31 | двунаправленная шина, бит 2 |
|
||||||
|
| `I/O3` | 32 | двунаправленная шина, бит 3 |
|
||||||
|
| `I/O4` | 41 | двунаправленная шина, бит 4 |
|
||||||
|
| `I/O5` | 42 | двунаправленная шина, бит 5 |
|
||||||
|
| `I/O6` | 43 | двунаправленная шина, бит 6 |
|
||||||
|
| `I/O7` | 44 | двунаправленная шина, бит 7 |
|
||||||
|
| `VCC1` | 34, 39 | соединить с 3,3 В для совместимости корпуса/ONFI |
|
||||||
|
| `VSS1` | 25, 48 | соединить с GND для совместимости корпуса/ONFI |
|
||||||
|
| `DNU` | 38, 47 | оставить неподключёнными |
|
||||||
|
| `NC` | остальные | не подключать |
|
||||||
|
|
||||||
|
У каждого VCC/VCC1 поставить 100 нФ на ближайший VSS/VSS1; рядом с NAND —
|
||||||
|
общий 1–4,7 мкФ. Сигналы не должны превышать питание NAND. Для трасс длиннее
|
||||||
|
нескольких сантиметров предусмотреть последовательные резисторы 22–47 Ом у
|
||||||
|
источника на `WE#`, `RE#`, `CLE`, `ALE` и при необходимости на данных.
|
||||||
|
|
||||||
|
## Соединение с МК
|
||||||
|
|
||||||
|
| NAND | TMS320F2812 | STM32F407VET6 | STM32G474CEU6 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `I/O0..7` | `XD0..XD7` | `PD14,PD15,PD0,PD1,PE7..PE10` | `PB0..PB7` |
|
||||||
|
| `CLE` | `XA0` | `PD11/FSMC_A16` | `PA0` |
|
||||||
|
| `ALE` | `XA1` | `PD12/FSMC_A17` | `PA1` |
|
||||||
|
| `WE#` | `XWE` | `PD5/FSMC_NWE` | `PA2` |
|
||||||
|
| `RE#` | `XRD` | `PD4/FSMC_NOE` | `PA3` |
|
||||||
|
| `CE#` | `XZCS6AND7` | `PD7/FSMC_NE2` | `PA4` |
|
||||||
|
| `WP#` | GND через 10 кОм | GND через 10 кОм | `PA5` и pull-down 10 кОм |
|
||||||
|
| `R/B#` | `GPIOE0` | `PD6/FSMC_NWAIT` | `PA6` |
|
||||||
|
|
||||||
|
Подробные номера выводов корпуса МК и ограничения находятся в README каждого
|
||||||
|
каталога `ports/`. Перед изготовлением платы нужно сверить выбранный корпус и
|
||||||
|
полный part number NAND с актуальным datasheet.
|
||||||
|
|
||||||
|
## Первый запуск
|
||||||
|
|
||||||
|
1. Не устанавливать NAND и проверить 3,3 В, отсутствие КЗ и уровни `CE#/WE#/RE#`.
|
||||||
|
2. Установить NAND, оставить `WP#` в нуле и выполнить RESET `FFh`.
|
||||||
|
3. Прочитать ID `90h`: ожидаемые первые байты для профиля — `2C F1`.
|
||||||
|
4. Считать одну страницу два раза и сравнить main/OOB побайтно.
|
||||||
|
5. Только после устойчивого чтения запускать полный дамп через GAS.
|
||||||
|
|
||||||
|
## Источники для проверки footprint
|
||||||
|
|
||||||
|
- [Micron: каталог SLC NAND](https://www.micron.com/products/storage/nand-flash/slc-nand/part-catalog)
|
||||||
|
- [MT29F1G08ABADA: signal assignments и геометрия](https://helpdesk.trx.pl/pliki/SERWER/Micron-MT29F1G08ABADAWP-IT%20D-datasheet.pdf)
|
||||||
|
- [Micron FBGA part decoder](https://in.micron.com/sales-support/design-tools/fbga-parts-decoder)
|
||||||
177
c/parallel-nand/docs/MEMORY_GAS.md
Normal file
177
c/parallel-nand/docs/MEMORY_GAS.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# Работа с parallel NAND через GAS
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
Внешняя NAND значительно больше общего адресного пространства GAS. Например,
|
||||||
|
`MT29F1G08ABADA` содержит 128 МиБ main area, а GAS имеет только 65536
|
||||||
|
16-битных регистров. Поэтому NAND представлена не линейным диапазоном, а
|
||||||
|
банковым окном одной физической страницы.
|
||||||
|
|
||||||
|
```text
|
||||||
|
SETGUI
|
||||||
|
│ GUI protocol v1: READ_REGISTERS / WRITE_REGISTERS
|
||||||
|
▼
|
||||||
|
карта GAS 0x0000..0xFFFF
|
||||||
|
│ 0xE000: управление, 0xE100: main, 0xE500: OOB
|
||||||
|
▼
|
||||||
|
parallel_nand_gas
|
||||||
|
▼
|
||||||
|
общее ядро parallel_nand
|
||||||
|
├─ TMS320F2812: XINTF
|
||||||
|
├─ STM32F407VE: FSMC
|
||||||
|
└─ STM32G474CE: GPIO
|
||||||
|
```
|
||||||
|
|
||||||
|
GUI работает одинаково со всеми МК. Тип процессора влияет только на выбранный
|
||||||
|
файл из `ports/`.
|
||||||
|
|
||||||
|
Встроенный проект должен направлять весь диапазон `0xE000..0xE51F` в
|
||||||
|
`parallel_nand_gas_read()` и `parallel_nand_gas_write()`. Их аргументы имеют
|
||||||
|
тип `unsigned int`: на C28x это нативное 16-битное слово, а на ARM функции
|
||||||
|
явно оставляют только младшие 16 бит. Так карта и wire-формат совпадают без
|
||||||
|
предположения, что C `char` всегда восьмибитный.
|
||||||
|
|
||||||
|
Для STM32, использующего карту `pcan_gas`, готов адаптер:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "parallel_nand_pcan_gas.h"
|
||||||
|
|
||||||
|
static pcan_gas_region_t nand_region;
|
||||||
|
parallel_nand_pcan_gas_region_init(&nand_region, &nand_gas);
|
||||||
|
```
|
||||||
|
|
||||||
|
Для TMS320F2812 переносимый обработчик вызывается прямо из существующего GAS
|
||||||
|
диспетчера:
|
||||||
|
|
||||||
|
```c
|
||||||
|
if (address >= nand_gas.base &&
|
||||||
|
address < nand_gas.base + PNAND_GAS_REGION_REGS) {
|
||||||
|
return parallel_nand_gas_read(&nand_gas, address, value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если тип результата диспетчера отличается, статусы `PNAND_GAS_OK`,
|
||||||
|
`PNAND_GAS_NO_REG`, `PNAND_GAS_READ_ONLY` и `PNAND_GAS_REJECTED`
|
||||||
|
преобразуются в его локальные статусы так же, как это сделано в
|
||||||
|
`parallel_nand_pcan_gas.c`.
|
||||||
|
|
||||||
|
## Транспорт до GUI
|
||||||
|
|
||||||
|
Карта GAS не привязана к физическому каналу. Рекомендуемый путь для полного
|
||||||
|
дампа — используемые SETGUI сообщения `WRITE_REGISTERS (0x0A)` и
|
||||||
|
`READ_REGISTERS (0x09)`: они позволяют передавать крупные блоки поверх USB,
|
||||||
|
UART, RS-485 или Ethernet. Адаптер `pcan_gas` нужен, когда теми же регистрами
|
||||||
|
обмениваются по classic CAN; там один кадр несёт только четыре регистра, и
|
||||||
|
полный 128-MiB дамп будет значительно медленнее.
|
||||||
|
|
||||||
|
Устройство не должно собирать весь дамп в RAM. Буферизуется только одна
|
||||||
|
страница. Из-за переносимости на C28x каждый octet хранится в `unsigned short`,
|
||||||
|
поэтому статические буферы GAS занимают примерно 4,2 KiB RAM и на TMS, и на
|
||||||
|
ARM.
|
||||||
|
|
||||||
|
## Карта GAS
|
||||||
|
|
||||||
|
Базовый адрес по умолчанию — `0xE000`. Его можно изменить при вызове
|
||||||
|
`parallel_nand_gas_init()`; все смещения ниже останутся прежними.
|
||||||
|
|
||||||
|
| Адрес | Доступ | Название | Значение |
|
||||||
|
|---:|:---:|---|---|
|
||||||
|
| `E000` | R | `SIGNATURE` | `0x4E44` (`ND`) |
|
||||||
|
| `E001` | R | `VERSION` | версия окна, сейчас 1 |
|
||||||
|
| `E002` | R | `STATUS` | состояние операции |
|
||||||
|
| `E003` | R | `ERROR` | код `PNAND_*`, 0 = нет ошибки |
|
||||||
|
| `E004` | W | `COMMAND` | команда окна |
|
||||||
|
| `E005` | R/W | `PAGE_LO` | младшие 16 бит физической страницы |
|
||||||
|
| `E006` | R/W | `PAGE_HI` | старшие 16 бит физической страницы |
|
||||||
|
| `E007` | R | `PAGE_MAIN` | main bytes, для MT29 = 2048 |
|
||||||
|
| `E008` | R | `PAGE_OOB` | OOB bytes, для MT29 = 64 |
|
||||||
|
| `E009` | R | `PAGES_PER_BLOCK` | для MT29 = 64 |
|
||||||
|
| `E00A` | R | `BLOCKS` | для MT29 = 1024 |
|
||||||
|
| `E00B` | R | `MANUFACTURER_ID` | ожидается `0x2C` |
|
||||||
|
| `E00C` | R | `DEVICE_ID` | ожидается `0xF1` |
|
||||||
|
| `E00D` | R | `BAD_BLOCK` | 1 для физически плохого блока |
|
||||||
|
| `E00E` | R | `GENERATION_LO` | счётчик загруженных страниц, low |
|
||||||
|
| `E00F` | R | `GENERATION_HI` | счётчик загруженных страниц, high |
|
||||||
|
| `E100..E4FF` | R | `PAGE_DATA` | 1024 регистра main, два байта LE |
|
||||||
|
| `E500..E51F` | R | `PAGE_OOB` | 32 регистра OOB, два байта LE |
|
||||||
|
|
||||||
|
Между управляющими регистрами и окнами данных есть зарезервированные адреса.
|
||||||
|
Чтение блока через них не выполняется: каждый запрос должен начинаться внутри
|
||||||
|
существующего участка.
|
||||||
|
|
||||||
|
## STATUS
|
||||||
|
|
||||||
|
| Бит | Маска | Смысл |
|
||||||
|
|---:|---:|---|
|
||||||
|
| 0 | `0x0001` | `READY` |
|
||||||
|
| 1 | `0x0002` | `BUSY` |
|
||||||
|
| 2 | `0x0004` | `PAGE_VALID` |
|
||||||
|
| 3 | `0x0008` | `BAD_BLOCK` |
|
||||||
|
| 15 | `0x8000` | `ERROR`, подробность в `E003` |
|
||||||
|
|
||||||
|
GUI читает окно данных только при `READY=1`, `PAGE_VALID=1`, `ERROR=0`.
|
||||||
|
|
||||||
|
## COMMAND
|
||||||
|
|
||||||
|
| Значение | Команда | Результат |
|
||||||
|
|---:|---|---|
|
||||||
|
| 1 | `RESET` | сброс NAND и ожидание Ready |
|
||||||
|
| 2 | `READ_ID` | обновляет `E00B/E00C` |
|
||||||
|
| 3 | `READ_PAGE` | загружает main/OOB выбранной страницы |
|
||||||
|
|
||||||
|
Запись номера страницы и запуск — две отдельные операции. Сначала GUI пишет
|
||||||
|
`PAGE_LO/PAGE_HI`, затем записывает `READ_PAGE` в `COMMAND`. Нельзя отправлять
|
||||||
|
один блок, начинающийся с `COMMAND`: команда выполнилась бы до обновления номера
|
||||||
|
страницы.
|
||||||
|
|
||||||
|
## Последовательность полного дампа
|
||||||
|
|
||||||
|
1. Прочитать `E000..E00F`, проверить подпись, версию и геометрию.
|
||||||
|
2. Выполнить `READ_ID`; для микросхемы с фотографии проверить `2C F1`.
|
||||||
|
3. Рассчитать `page_count = PAGES_PER_BLOCK * BLOCKS`.
|
||||||
|
4. Для каждой физической страницы от 0 до `page_count - 1`:
|
||||||
|
- записать номер страницы в `E005/E006`;
|
||||||
|
- записать `3` в `E004`;
|
||||||
|
- опрашивать `E002`, пока не установлен `READY`;
|
||||||
|
- проверить `ERROR`, `BAD_BLOCK` и изменение `GENERATION`;
|
||||||
|
- прочитать `E100..E4FF`;
|
||||||
|
- прочитать `E500..E51F`;
|
||||||
|
- сразу дописать данные и метаданные в файлы.
|
||||||
|
5. Не пропускать bad-блоки: иначе файл потеряет соответствие физическим
|
||||||
|
страницам микросхемы.
|
||||||
|
|
||||||
|
GUI protocol v1 допускает payload до 512 байт. Ответ `READ_REGISTERS` содержит
|
||||||
|
четырёхбайтовый заголовок диапазона, поэтому за один запрос следует читать не
|
||||||
|
более 254 GAS-регистров. Страница main читается пятью запросами: четыре раза по
|
||||||
|
254 регистра и последний раз 8 регистров. OOB читается одним запросом на 32
|
||||||
|
регистра.
|
||||||
|
|
||||||
|
## Возобновление
|
||||||
|
|
||||||
|
GUI хранит номер последней полностью записанной страницы. После разрыва связи
|
||||||
|
он повторно проверяет подпись/ID/геометрию и продолжает со следующей страницы.
|
||||||
|
Страница считается готовой только после записи main, OOB и записи её статуса в
|
||||||
|
метаданные.
|
||||||
|
|
||||||
|
Рекомендуемые выходные файлы:
|
||||||
|
|
||||||
|
- `dump.bin` — 2048 байт main каждой физической страницы;
|
||||||
|
- `dump.oob` — 64 байта OOB каждой физической страницы;
|
||||||
|
- `dump.json` — ID, геометрия, bad-блоки, ошибки и последняя готовая страница.
|
||||||
|
|
||||||
|
Для `MT29F1G08ABADA` размеры завершённого дампа: `dump.bin` = 134217728 байт,
|
||||||
|
`dump.oob` = 4194304 байта.
|
||||||
|
|
||||||
|
## ECC
|
||||||
|
|
||||||
|
Текущая версия шаблона отдаёт raw main/OOB и не заявляет исправление ошибок.
|
||||||
|
Статус bad-блока определяется по первому байту OOB первых двух страниц блока.
|
||||||
|
Перед использованием дампа как рабочего образа нужно добавить ECC-политику для
|
||||||
|
точного чипа и формата OOB. Само окно GAS при этом менять не требуется: можно
|
||||||
|
добавить новые флаги состояния в свободные биты `STATUS`.
|
||||||
|
|
||||||
|
## PROGRAM/ERASE
|
||||||
|
|
||||||
|
Запись и стирание не включены намеренно. Они требуют отдельного разрешения,
|
||||||
|
проверки ECC, защиты загрузочных блоков и двухфазного подтверждения через GAS.
|
||||||
|
До добавления этих функций `WP#` удерживается активным аппаратным портом.
|
||||||
68
c/parallel-nand/include/parallel_nand.h
Normal file
68
c/parallel-nand/include/parallel_nand.h
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#ifndef PARALLEL_NAND_H
|
||||||
|
#define PARALLEL_NAND_H
|
||||||
|
|
||||||
|
#include "parallel_nand_port.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define PNAND_MT29F1G08_PAGE_MAIN 2048UL
|
||||||
|
#define PNAND_MT29F1G08_PAGE_OOB 64UL
|
||||||
|
#define PNAND_MT29F1G08_PAGES_PER_BLOCK 64UL
|
||||||
|
#define PNAND_MT29F1G08_BLOCKS 1024UL
|
||||||
|
#define PNAND_MT29F1G08_COLUMN_CYCLES 2U
|
||||||
|
#define PNAND_MT29F1G08_ROW_CYCLES 2U
|
||||||
|
#define PNAND_MT29F1G08_PAGE_TOTAL \
|
||||||
|
(PNAND_MT29F1G08_PAGE_MAIN + PNAND_MT29F1G08_PAGE_OOB)
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
PNAND_OK = 0,
|
||||||
|
PNAND_ERROR_ARGUMENT = -1,
|
||||||
|
PNAND_ERROR_PORT = -2,
|
||||||
|
PNAND_ERROR_TIMEOUT = -3,
|
||||||
|
PNAND_ERROR_RANGE = -4,
|
||||||
|
PNAND_ERROR_BUFFER = -5
|
||||||
|
} parallel_nand_result_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
unsigned long page_main_size;
|
||||||
|
unsigned long page_oob_size;
|
||||||
|
unsigned long pages_per_block;
|
||||||
|
unsigned long block_count;
|
||||||
|
unsigned int column_cycles;
|
||||||
|
unsigned int row_cycles;
|
||||||
|
unsigned long ready_timeout_us;
|
||||||
|
} parallel_nand_geometry_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const parallel_nand_port_t *port;
|
||||||
|
parallel_nand_geometry_t geometry;
|
||||||
|
} parallel_nand_t;
|
||||||
|
|
||||||
|
extern const parallel_nand_geometry_t parallel_nand_mt29f1g08_geometry;
|
||||||
|
|
||||||
|
int parallel_nand_init(parallel_nand_t *nand,
|
||||||
|
const parallel_nand_port_t *port,
|
||||||
|
const parallel_nand_geometry_t *geometry);
|
||||||
|
int parallel_nand_reset(parallel_nand_t *nand);
|
||||||
|
int parallel_nand_read_status(parallel_nand_t *nand, pnand_octet_t *status);
|
||||||
|
int parallel_nand_read_id(parallel_nand_t *nand, pnand_octet_t address,
|
||||||
|
pnand_octet_t *id, unsigned int count);
|
||||||
|
int parallel_nand_read_page(parallel_nand_t *nand, unsigned long page,
|
||||||
|
pnand_octet_t *main_data,
|
||||||
|
unsigned long main_capacity,
|
||||||
|
pnand_octet_t *oob,
|
||||||
|
unsigned long oob_capacity);
|
||||||
|
int parallel_nand_read_page_slice(parallel_nand_t *nand, unsigned long page,
|
||||||
|
unsigned long column, pnand_octet_t *out,
|
||||||
|
unsigned long count);
|
||||||
|
int parallel_nand_is_bad_block(parallel_nand_t *nand, unsigned long block,
|
||||||
|
int *is_bad);
|
||||||
|
unsigned long parallel_nand_page_count(const parallel_nand_t *nand);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
84
c/parallel-nand/include/parallel_nand_gas.h
Normal file
84
c/parallel-nand/include/parallel_nand_gas.h
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#ifndef PARALLEL_NAND_GAS_H
|
||||||
|
#define PARALLEL_NAND_GAS_H
|
||||||
|
|
||||||
|
#include "parallel_nand.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define PNAND_GAS_DEFAULT_BASE 0xE000U
|
||||||
|
#define PNAND_GAS_REGION_REGS 0x0520U
|
||||||
|
#define PNAND_GAS_DATA_OFFSET 0x0100U
|
||||||
|
#define PNAND_GAS_DATA_REGS 1024U
|
||||||
|
#define PNAND_GAS_OOB_OFFSET 0x0500U
|
||||||
|
#define PNAND_GAS_OOB_REGS 32U
|
||||||
|
|
||||||
|
enum {
|
||||||
|
PNAND_GAS_REG_SIGNATURE = 0x0000,
|
||||||
|
PNAND_GAS_REG_VERSION = 0x0001,
|
||||||
|
PNAND_GAS_REG_STATUS = 0x0002,
|
||||||
|
PNAND_GAS_REG_ERROR = 0x0003,
|
||||||
|
PNAND_GAS_REG_COMMAND = 0x0004,
|
||||||
|
PNAND_GAS_REG_PAGE_LO = 0x0005,
|
||||||
|
PNAND_GAS_REG_PAGE_HI = 0x0006,
|
||||||
|
PNAND_GAS_REG_PAGE_MAIN = 0x0007,
|
||||||
|
PNAND_GAS_REG_PAGE_OOB = 0x0008,
|
||||||
|
PNAND_GAS_REG_PAGES_PER_BLOCK = 0x0009,
|
||||||
|
PNAND_GAS_REG_BLOCKS = 0x000A,
|
||||||
|
PNAND_GAS_REG_MANUFACTURER_ID = 0x000B,
|
||||||
|
PNAND_GAS_REG_DEVICE_ID = 0x000C,
|
||||||
|
PNAND_GAS_REG_BAD_BLOCK = 0x000D,
|
||||||
|
PNAND_GAS_REG_GENERATION_LO = 0x000E,
|
||||||
|
PNAND_GAS_REG_GENERATION_HI = 0x000F
|
||||||
|
};
|
||||||
|
|
||||||
|
enum {
|
||||||
|
PNAND_GAS_CMD_NONE = 0,
|
||||||
|
PNAND_GAS_CMD_RESET = 1,
|
||||||
|
PNAND_GAS_CMD_READ_ID = 2,
|
||||||
|
PNAND_GAS_CMD_READ_PAGE = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
#define PNAND_GAS_STATUS_READY 0x0001U
|
||||||
|
#define PNAND_GAS_STATUS_BUSY 0x0002U
|
||||||
|
#define PNAND_GAS_STATUS_PAGE_VALID 0x0004U
|
||||||
|
#define PNAND_GAS_STATUS_BAD_BLOCK 0x0008U
|
||||||
|
#define PNAND_GAS_STATUS_ERROR 0x8000U
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
parallel_nand_t *nand;
|
||||||
|
unsigned int base;
|
||||||
|
unsigned int status;
|
||||||
|
int error;
|
||||||
|
unsigned long selected_page;
|
||||||
|
unsigned long generation;
|
||||||
|
unsigned long cached_block;
|
||||||
|
int cached_bad_block;
|
||||||
|
int block_cache_valid;
|
||||||
|
pnand_octet_t id[8];
|
||||||
|
pnand_octet_t main_data[PNAND_MT29F1G08_PAGE_MAIN];
|
||||||
|
pnand_octet_t oob[PNAND_MT29F1G08_PAGE_OOB];
|
||||||
|
} parallel_nand_gas_t;
|
||||||
|
|
||||||
|
void parallel_nand_gas_init(parallel_nand_gas_t *gas,
|
||||||
|
parallel_nand_t *nand, unsigned int base);
|
||||||
|
|
||||||
|
/* Portable GAS callbacks. Values and addresses are exactly 16 bits on wire. */
|
||||||
|
typedef enum {
|
||||||
|
PNAND_GAS_OK = 0,
|
||||||
|
PNAND_GAS_NO_REG,
|
||||||
|
PNAND_GAS_READ_ONLY,
|
||||||
|
PNAND_GAS_REJECTED
|
||||||
|
} parallel_nand_gas_result_t;
|
||||||
|
|
||||||
|
parallel_nand_gas_result_t parallel_nand_gas_read(
|
||||||
|
parallel_nand_gas_t *gas, unsigned int address, unsigned int *value);
|
||||||
|
parallel_nand_gas_result_t parallel_nand_gas_write(
|
||||||
|
parallel_nand_gas_t *gas, unsigned int address, unsigned int value);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
11
c/parallel-nand/include/parallel_nand_pcan_gas.h
Normal file
11
c/parallel-nand/include/parallel_nand_pcan_gas.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#ifndef PARALLEL_NAND_PCAN_GAS_H
|
||||||
|
#define PARALLEL_NAND_PCAN_GAS_H
|
||||||
|
|
||||||
|
#include "parallel_nand_gas.h"
|
||||||
|
#include "pcan_gas.h"
|
||||||
|
|
||||||
|
/* Adapter for targets where c/set-protocol pcan_gas is available. */
|
||||||
|
void parallel_nand_pcan_gas_region_init(pcan_gas_region_t *region,
|
||||||
|
parallel_nand_gas_t *gas);
|
||||||
|
|
||||||
|
#endif
|
||||||
28
c/parallel-nand/include/parallel_nand_port.h
Normal file
28
c/parallel-nand/include/parallel_nand_port.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#ifndef PARALLEL_NAND_PORT_H
|
||||||
|
#define PARALLEL_NAND_PORT_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Один octet хранится в целом типе: это важно для C28x с 16-битным char. */
|
||||||
|
typedef unsigned short pnand_octet_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int (*init)(void);
|
||||||
|
void (*select)(void);
|
||||||
|
void (*deselect)(void);
|
||||||
|
void (*write_command)(pnand_octet_t value);
|
||||||
|
void (*write_address)(pnand_octet_t value);
|
||||||
|
void (*write_data)(pnand_octet_t value);
|
||||||
|
pnand_octet_t (*read_data)(void);
|
||||||
|
int (*is_ready)(void);
|
||||||
|
void (*set_write_protect)(int enabled);
|
||||||
|
void (*delay_us)(unsigned long us);
|
||||||
|
} parallel_nand_port_t;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
26
c/parallel-nand/ports/stm32f407ve/README.md
Normal file
26
c/parallel-nand/ports/stm32f407ve/README.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Порт STM32F407VET6
|
||||||
|
|
||||||
|
Используется аппаратный FSMC NAND Bank 2, шина x8.
|
||||||
|
|
||||||
|
| NAND | STM32F407VE | LQFP-100 |
|
||||||
|
|---|---|---:|
|
||||||
|
| `I/O0` | `PD14/FSMC_D0` | 61 |
|
||||||
|
| `I/O1` | `PD15/FSMC_D1` | 62 |
|
||||||
|
| `I/O2` | `PD0/FSMC_D2` | 81 |
|
||||||
|
| `I/O3` | `PD1/FSMC_D3` | 82 |
|
||||||
|
| `I/O4` | `PE7/FSMC_D4` | 38 |
|
||||||
|
| `I/O5` | `PE8/FSMC_D5` | 39 |
|
||||||
|
| `I/O6` | `PE9/FSMC_D6` | 40 |
|
||||||
|
| `I/O7` | `PE10/FSMC_D7` | 41 |
|
||||||
|
| `RE#` | `PD4/FSMC_NOE` | 85 |
|
||||||
|
| `WE#` | `PD5/FSMC_NWE` | 86 |
|
||||||
|
| `R/B#` | `PD6/FSMC_NWAIT` | 87 |
|
||||||
|
| `CE#` | `PD7/FSMC_NE2` | 88 |
|
||||||
|
| `CLE` | `PD11/FSMC_A16` | 58 |
|
||||||
|
| `ALE` | `PD12/FSMC_A17` | 59 |
|
||||||
|
|
||||||
|
Порт использует STM32Cube HAL. В `stm32f4xx_hal_conf.h` включить
|
||||||
|
`HAL_NAND_MODULE_ENABLED`, добавить `stm32f4xx_hal_nand.c` и
|
||||||
|
`stm32f4xx_ll_fsmc.c`, включить FSMC и не создавать второй
|
||||||
|
`NAND_HandleTypeDef` для Bank 2. Начальные тайминги
|
||||||
|
консервативны; финальные значения подтвердить осциллографом.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#include "parallel_nand_port_stm32f407ve.h"
|
||||||
|
#include "stm32f4xx_hal.h"
|
||||||
|
#include "stm32f4xx_hal_nand.h"
|
||||||
|
|
||||||
|
#define PNAND_F407_BANK2_BASE 0x70000000UL
|
||||||
|
#define PNAND_F407_CMD_AREA (1UL << 16)
|
||||||
|
#define PNAND_F407_ADDR_AREA (1UL << 17)
|
||||||
|
|
||||||
|
static NAND_HandleTypeDef hnand;
|
||||||
|
|
||||||
|
static int f407_init(void)
|
||||||
|
{
|
||||||
|
GPIO_InitTypeDef io;
|
||||||
|
FSMC_NAND_PCC_TimingTypeDef common;
|
||||||
|
FSMC_NAND_PCC_TimingTypeDef attribute;
|
||||||
|
|
||||||
|
__HAL_RCC_GPIOD_CLK_ENABLE();
|
||||||
|
__HAL_RCC_GPIOE_CLK_ENABLE();
|
||||||
|
__HAL_RCC_FSMC_CLK_ENABLE();
|
||||||
|
|
||||||
|
io.Mode = GPIO_MODE_AF_PP;
|
||||||
|
io.Pull = GPIO_NOPULL;
|
||||||
|
io.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
|
||||||
|
io.Alternate = GPIO_AF12_FSMC;
|
||||||
|
io.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5 |
|
||||||
|
GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_11 | GPIO_PIN_12 |
|
||||||
|
GPIO_PIN_14 | GPIO_PIN_15;
|
||||||
|
HAL_GPIO_Init(GPIOD, &io);
|
||||||
|
io.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10;
|
||||||
|
HAL_GPIO_Init(GPIOE, &io);
|
||||||
|
|
||||||
|
hnand.Instance = FSMC_NAND_DEVICE;
|
||||||
|
hnand.Init.NandBank = FSMC_NAND_BANK2;
|
||||||
|
hnand.Init.Waitfeature = FSMC_NAND_PCC_WAIT_FEATURE_DISABLE;
|
||||||
|
hnand.Init.MemoryDataWidth = FSMC_NAND_PCC_MEM_BUS_WIDTH_8;
|
||||||
|
hnand.Init.EccComputation = FSMC_NAND_ECC_DISABLE;
|
||||||
|
hnand.Init.ECCPageSize = FSMC_NAND_ECC_PAGE_SIZE_2048BYTE;
|
||||||
|
hnand.Init.TCLRSetupTime = 2U;
|
||||||
|
hnand.Init.TARSetupTime = 2U;
|
||||||
|
|
||||||
|
common.SetupTime = 2U;
|
||||||
|
common.WaitSetupTime = 5U;
|
||||||
|
common.HoldSetupTime = 3U;
|
||||||
|
common.HiZSetupTime = 2U;
|
||||||
|
attribute = common;
|
||||||
|
return HAL_NAND_Init(&hnand, &common, &attribute) == HAL_OK ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void f407_select(void) { /* FSMC_NCE2 активируется доступом. */ }
|
||||||
|
static void f407_deselect(void) { }
|
||||||
|
static void f407_write_command(pnand_octet_t value)
|
||||||
|
{
|
||||||
|
*(__IO uint8_t *)(PNAND_F407_BANK2_BASE | PNAND_F407_CMD_AREA) =
|
||||||
|
(uint8_t)value;
|
||||||
|
}
|
||||||
|
static void f407_write_address(pnand_octet_t value)
|
||||||
|
{
|
||||||
|
*(__IO uint8_t *)(PNAND_F407_BANK2_BASE | PNAND_F407_ADDR_AREA) =
|
||||||
|
(uint8_t)value;
|
||||||
|
}
|
||||||
|
static void f407_write_data(pnand_octet_t value)
|
||||||
|
{
|
||||||
|
*(__IO uint8_t *)PNAND_F407_BANK2_BASE = (uint8_t)value;
|
||||||
|
}
|
||||||
|
static pnand_octet_t f407_read_data(void)
|
||||||
|
{
|
||||||
|
return *(__IO uint8_t *)PNAND_F407_BANK2_BASE;
|
||||||
|
}
|
||||||
|
static int f407_is_ready(void)
|
||||||
|
{
|
||||||
|
return HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_6) == GPIO_PIN_SET;
|
||||||
|
}
|
||||||
|
static void f407_set_wp(int enabled)
|
||||||
|
{
|
||||||
|
/* WP# аппаратный в базовой схеме. */
|
||||||
|
(void)enabled;
|
||||||
|
}
|
||||||
|
static void f407_delay_us(unsigned long us)
|
||||||
|
{
|
||||||
|
uint32_t cycles = (SystemCoreClock / 3000000UL) * (uint32_t)us;
|
||||||
|
while (cycles-- != 0U) __NOP();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const parallel_nand_port_t f407_port = {
|
||||||
|
f407_init, f407_select, f407_deselect,
|
||||||
|
f407_write_command, f407_write_address, f407_write_data,
|
||||||
|
f407_read_data, f407_is_ready, f407_set_wp, f407_delay_us
|
||||||
|
};
|
||||||
|
|
||||||
|
const parallel_nand_port_t *parallel_nand_port_stm32f407ve(void)
|
||||||
|
{
|
||||||
|
return &f407_port;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef PARALLEL_NAND_PORT_STM32F407VE_H
|
||||||
|
#define PARALLEL_NAND_PORT_STM32F407VE_H
|
||||||
|
|
||||||
|
#include "parallel_nand_port.h"
|
||||||
|
|
||||||
|
const parallel_nand_port_t *parallel_nand_port_stm32f407ve(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
21
c/parallel-nand/ports/stm32g474ce/README.md
Normal file
21
c/parallel-nand/ports/stm32g474ce/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Порт STM32G474CEU6
|
||||||
|
|
||||||
|
У UFQFPN-48 нет выведенного FSMC, поэтому используется GPIO bit-bang.
|
||||||
|
|
||||||
|
| NAND | STM32G474CEU6 | UFQFPN-48 |
|
||||||
|
|---|---|---:|
|
||||||
|
| `I/O0..I/O7` | `PB0..PB7` | 17, 18, 19, 41, 42, 43, 44, 45 |
|
||||||
|
| `CLE` | `PA0` | 8 |
|
||||||
|
| `ALE` | `PA1` | 9 |
|
||||||
|
| `WE#` | `PA2` | 10 |
|
||||||
|
| `RE#` | `PA3` | 11 |
|
||||||
|
| `CE#` | `PA4` | 12 |
|
||||||
|
| `WP#` | `PA5` | 13 |
|
||||||
|
| `R/B#` | `PA6` | 14 |
|
||||||
|
|
||||||
|
`R/B#` требует внешней подтяжки 4,7–10 кОм к 3,3 В. Порт использует STM32Cube
|
||||||
|
HAL только для инициализации, а циклы NAND выполняет прямым доступом к GPIO.
|
||||||
|
|
||||||
|
Выбранные `PB3/PB4` нельзя одновременно использовать для trace/JTAG; SWD на
|
||||||
|
`PA13/PA14` остаётся доступен. Если эти пины нужны проекту, измените таблицу и
|
||||||
|
операции шины: текущая оптимизация рассчитана на непрерывные `PB0..PB7`.
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#include "parallel_nand_port_stm32g474ce.h"
|
||||||
|
#include "stm32g4xx_hal.h"
|
||||||
|
|
||||||
|
/* Пустая плата STM32G474CEU6: PB0..PB7 образуют непрерывную x8 шину. */
|
||||||
|
#define DATA_MASK 0x00FFU
|
||||||
|
#define CLE_PIN GPIO_PIN_0
|
||||||
|
#define ALE_PIN GPIO_PIN_1
|
||||||
|
#define WE_PIN GPIO_PIN_2
|
||||||
|
#define RE_PIN GPIO_PIN_3
|
||||||
|
#define CE_PIN GPIO_PIN_4
|
||||||
|
#define WP_PIN GPIO_PIN_5
|
||||||
|
#define RB_PIN GPIO_PIN_6
|
||||||
|
|
||||||
|
static int data_is_output;
|
||||||
|
|
||||||
|
static void data_input(void)
|
||||||
|
{
|
||||||
|
if (data_is_output) {
|
||||||
|
GPIOB->MODER &= ~0x0000FFFFUL;
|
||||||
|
data_is_output = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void data_output(void)
|
||||||
|
{
|
||||||
|
if (!data_is_output) {
|
||||||
|
GPIOB->MODER = (GPIOB->MODER & ~0x0000FFFFUL) | 0x00005555UL;
|
||||||
|
data_is_output = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void short_delay(void)
|
||||||
|
{
|
||||||
|
__NOP(); __NOP(); __NOP(); __NOP();
|
||||||
|
__NOP(); __NOP(); __NOP(); __NOP();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int g474_init(void)
|
||||||
|
{
|
||||||
|
GPIO_InitTypeDef io;
|
||||||
|
__HAL_RCC_GPIOA_CLK_ENABLE();
|
||||||
|
__HAL_RCC_GPIOB_CLK_ENABLE();
|
||||||
|
|
||||||
|
io.Pin = DATA_MASK;
|
||||||
|
io.Mode = GPIO_MODE_OUTPUT_PP;
|
||||||
|
io.Pull = GPIO_NOPULL;
|
||||||
|
io.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
|
||||||
|
HAL_GPIO_Init(GPIOB, &io);
|
||||||
|
data_is_output = 1;
|
||||||
|
data_input();
|
||||||
|
|
||||||
|
/* Set inactive levels before enabling the pins as outputs. */
|
||||||
|
GPIOA->BSRR = WE_PIN | RE_PIN | CE_PIN |
|
||||||
|
((uint32_t)(CLE_PIN | ALE_PIN | WP_PIN) << 16);
|
||||||
|
io.Pin = CLE_PIN | ALE_PIN | WE_PIN | RE_PIN | CE_PIN | WP_PIN;
|
||||||
|
io.Mode = GPIO_MODE_OUTPUT_PP;
|
||||||
|
io.Pull = GPIO_NOPULL;
|
||||||
|
io.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
|
||||||
|
HAL_GPIO_Init(GPIOA, &io);
|
||||||
|
io.Pin = RB_PIN;
|
||||||
|
io.Mode = GPIO_MODE_INPUT;
|
||||||
|
io.Pull = GPIO_NOPULL;
|
||||||
|
HAL_GPIO_Init(GPIOA, &io);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void g474_select(void) { GPIOA->BSRR = (uint32_t)CE_PIN << 16; }
|
||||||
|
static void g474_deselect(void) { GPIOA->BSRR = CE_PIN; }
|
||||||
|
|
||||||
|
static void write_cycle(pnand_octet_t value)
|
||||||
|
{
|
||||||
|
data_output();
|
||||||
|
GPIOB->BSRR = (uint32_t)DATA_MASK << 16;
|
||||||
|
GPIOB->BSRR = (uint32_t)value & DATA_MASK;
|
||||||
|
short_delay();
|
||||||
|
GPIOA->BSRR = (uint32_t)WE_PIN << 16;
|
||||||
|
short_delay();
|
||||||
|
GPIOA->BSRR = WE_PIN;
|
||||||
|
short_delay();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void g474_write_command(pnand_octet_t value)
|
||||||
|
{
|
||||||
|
GPIOA->BSRR = CLE_PIN | ((uint32_t)ALE_PIN << 16);
|
||||||
|
write_cycle(value);
|
||||||
|
GPIOA->BSRR = (uint32_t)CLE_PIN << 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void g474_write_address(pnand_octet_t value)
|
||||||
|
{
|
||||||
|
GPIOA->BSRR = ALE_PIN | ((uint32_t)CLE_PIN << 16);
|
||||||
|
write_cycle(value);
|
||||||
|
GPIOA->BSRR = (uint32_t)ALE_PIN << 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void g474_write_data(pnand_octet_t value)
|
||||||
|
{
|
||||||
|
GPIOA->BSRR = ((uint32_t)(CLE_PIN | ALE_PIN) << 16);
|
||||||
|
write_cycle(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pnand_octet_t g474_read_data(void)
|
||||||
|
{
|
||||||
|
pnand_octet_t value;
|
||||||
|
data_input();
|
||||||
|
GPIOA->BSRR = ((uint32_t)(CLE_PIN | ALE_PIN | RE_PIN) << 16);
|
||||||
|
short_delay();
|
||||||
|
value = (pnand_octet_t)(GPIOB->IDR & DATA_MASK);
|
||||||
|
GPIOA->BSRR = RE_PIN;
|
||||||
|
short_delay();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int g474_is_ready(void) { return (GPIOA->IDR & RB_PIN) != 0U; }
|
||||||
|
static void g474_set_wp(int enabled)
|
||||||
|
{
|
||||||
|
GPIOA->BSRR = enabled ? ((uint32_t)WP_PIN << 16) : WP_PIN;
|
||||||
|
}
|
||||||
|
static void g474_delay_us(unsigned long us)
|
||||||
|
{
|
||||||
|
uint32_t cycles = (SystemCoreClock / 3000000UL) * (uint32_t)us;
|
||||||
|
while (cycles-- != 0U) __NOP();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const parallel_nand_port_t g474_port = {
|
||||||
|
g474_init, g474_select, g474_deselect,
|
||||||
|
g474_write_command, g474_write_address, g474_write_data,
|
||||||
|
g474_read_data, g474_is_ready, g474_set_wp, g474_delay_us
|
||||||
|
};
|
||||||
|
|
||||||
|
const parallel_nand_port_t *parallel_nand_port_stm32g474ce(void)
|
||||||
|
{
|
||||||
|
return &g474_port;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef PARALLEL_NAND_PORT_STM32G474CE_H
|
||||||
|
#define PARALLEL_NAND_PORT_STM32G474CE_H
|
||||||
|
|
||||||
|
#include "parallel_nand_port.h"
|
||||||
|
|
||||||
|
const parallel_nand_port_t *parallel_nand_port_stm32g474ce(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
21
c/parallel-nand/ports/tms320f2812/README.md
Normal file
21
c/parallel-nand/ports/tms320f2812/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Порт TMS320F2812
|
||||||
|
|
||||||
|
Порт использует XINTF Zone 6:
|
||||||
|
|
||||||
|
- `XD0..XD7` — NAND `I/O0..I/O7`;
|
||||||
|
- `XA0` — `CLE`;
|
||||||
|
- `XA1` — `ALE`;
|
||||||
|
- `XWE` — `WE#`;
|
||||||
|
- `XRD` — `RE#`;
|
||||||
|
- `XZCS6AND7` — `CE#`;
|
||||||
|
- `GPIOE0/XINT1` — `R/B#` с внешней подтяжкой 4,7–10 кОм.
|
||||||
|
|
||||||
|
Адреса C28x задаются в 16-битных словах: data `0x100000`, command
|
||||||
|
`0x100001`, address `0x100002`. Используется только младший байт XINTF.
|
||||||
|
|
||||||
|
`WP#` в базовом варианте аппаратный: для режима только чтения соединить с GND
|
||||||
|
через 10 кОм; если позже понадобится PROGRAM/ERASE, заменить на GPIO и реализовать
|
||||||
|
`tms_set_wp()`.
|
||||||
|
|
||||||
|
Задержка в порте приблизительная. После выбора частоты CPU проверить импульсы
|
||||||
|
осциллографом и при необходимости изменить `PNAND_TMS_CPU_MHZ`.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#include "parallel_nand_port_tms320f2812.h"
|
||||||
|
#include "DSP281x_Device.h"
|
||||||
|
|
||||||
|
/* C28x addresses are 16-bit word addresses. XA0/XA1 drive CLE/ALE. */
|
||||||
|
#define PNAND_TMS_DATA_REG (*(volatile unsigned int *)0x100000UL)
|
||||||
|
#define PNAND_TMS_CMD_REG (*(volatile unsigned int *)0x100001UL)
|
||||||
|
#define PNAND_TMS_ADDR_REG (*(volatile unsigned int *)0x100002UL)
|
||||||
|
|
||||||
|
#ifndef PNAND_TMS_CPU_MHZ
|
||||||
|
#define PNAND_TMS_CPU_MHZ 150UL
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static void tms_delay_us(unsigned long us)
|
||||||
|
{
|
||||||
|
volatile unsigned long loops = us * (PNAND_TMS_CPU_MHZ / 5UL);
|
||||||
|
while (loops-- != 0UL) asm(" NOP");
|
||||||
|
}
|
||||||
|
|
||||||
|
static int tms_init(void)
|
||||||
|
{
|
||||||
|
EALLOW;
|
||||||
|
GpioMuxRegs.GPEMUX.bit.XINT1_XBIO_GPIOE0 = 0;
|
||||||
|
GpioMuxRegs.GPEDIR.bit.GPIOE0 = 0;
|
||||||
|
|
||||||
|
XintfRegs.XINTCNF2.bit.XTIMCLK = 1;
|
||||||
|
XintfRegs.XINTCNF2.bit.WRBUFF = 0;
|
||||||
|
XintfRegs.XINTCNF2.bit.CLKOFF = 0;
|
||||||
|
XintfRegs.XINTCNF2.bit.CLKMODE = 1;
|
||||||
|
XintfRegs.XTIMING6.bit.XWRLEAD = 3;
|
||||||
|
XintfRegs.XTIMING6.bit.XWRACTIVE = 7;
|
||||||
|
XintfRegs.XTIMING6.bit.XWRTRAIL = 3;
|
||||||
|
XintfRegs.XTIMING6.bit.XRDLEAD = 3;
|
||||||
|
XintfRegs.XTIMING6.bit.XRDACTIVE = 7;
|
||||||
|
XintfRegs.XTIMING6.bit.XRDTRAIL = 3;
|
||||||
|
XintfRegs.XTIMING6.bit.USEREADY = 0;
|
||||||
|
XintfRegs.XTIMING6.bit.READYMODE = 0;
|
||||||
|
XintfRegs.XTIMING6.bit.XSIZE = 3;
|
||||||
|
EDIS;
|
||||||
|
asm(" RPT #7 || NOP");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tms_select(void) { /* XZCS6AND7 формируется каждым доступом. */ }
|
||||||
|
static void tms_deselect(void) { }
|
||||||
|
static void tms_write_command(pnand_octet_t v) { PNAND_TMS_CMD_REG = v & 0xFFU; }
|
||||||
|
static void tms_write_address(pnand_octet_t v) { PNAND_TMS_ADDR_REG = v & 0xFFU; }
|
||||||
|
static void tms_write_data(pnand_octet_t v) { PNAND_TMS_DATA_REG = v & 0xFFU; }
|
||||||
|
static pnand_octet_t tms_read_data(void) { return PNAND_TMS_DATA_REG & 0xFFU; }
|
||||||
|
static int tms_is_ready(void) { return GpioDataRegs.GPEDAT.bit.GPIOE0 != 0; }
|
||||||
|
static void tms_set_wp(int enabled)
|
||||||
|
{
|
||||||
|
/* Шаблон платы фиксирует WP# аппаратно. См. README порта. */
|
||||||
|
(void)enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const parallel_nand_port_t tms_port = {
|
||||||
|
tms_init, tms_select, tms_deselect,
|
||||||
|
tms_write_command, tms_write_address, tms_write_data,
|
||||||
|
tms_read_data, tms_is_ready, tms_set_wp, tms_delay_us
|
||||||
|
};
|
||||||
|
|
||||||
|
const parallel_nand_port_t *parallel_nand_port_tms320f2812(void)
|
||||||
|
{
|
||||||
|
return &tms_port;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef PARALLEL_NAND_PORT_TMS320F2812_H
|
||||||
|
#define PARALLEL_NAND_PORT_TMS320F2812_H
|
||||||
|
|
||||||
|
#include "parallel_nand_port.h"
|
||||||
|
|
||||||
|
const parallel_nand_port_t *parallel_nand_port_tms320f2812(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
191
c/parallel-nand/src/parallel_nand.c
Normal file
191
c/parallel-nand/src/parallel_nand.c
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
#include "parallel_nand.h"
|
||||||
|
|
||||||
|
#define PNAND_CMD_READ_1 0x00U
|
||||||
|
#define PNAND_CMD_READ_2 0x30U
|
||||||
|
#define PNAND_CMD_READ_ID 0x90U
|
||||||
|
#define PNAND_CMD_READ_STATUS 0x70U
|
||||||
|
#define PNAND_CMD_RESET 0xFFU
|
||||||
|
|
||||||
|
const parallel_nand_geometry_t parallel_nand_mt29f1g08_geometry = {
|
||||||
|
PNAND_MT29F1G08_PAGE_MAIN,
|
||||||
|
PNAND_MT29F1G08_PAGE_OOB,
|
||||||
|
PNAND_MT29F1G08_PAGES_PER_BLOCK,
|
||||||
|
PNAND_MT29F1G08_BLOCKS,
|
||||||
|
PNAND_MT29F1G08_COLUMN_CYCLES,
|
||||||
|
PNAND_MT29F1G08_ROW_CYCLES,
|
||||||
|
1000000UL
|
||||||
|
};
|
||||||
|
|
||||||
|
static int port_valid(const parallel_nand_port_t *port)
|
||||||
|
{
|
||||||
|
return port != 0 && port->init != 0 && port->select != 0 &&
|
||||||
|
port->deselect != 0 && port->write_command != 0 &&
|
||||||
|
port->write_address != 0 && port->read_data != 0 &&
|
||||||
|
port->is_ready != 0 && port->set_write_protect != 0 &&
|
||||||
|
port->delay_us != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int geometry_valid(const parallel_nand_geometry_t *g)
|
||||||
|
{
|
||||||
|
return g != 0 && g->page_main_size != 0UL &&
|
||||||
|
g->pages_per_block != 0UL && g->block_count != 0UL &&
|
||||||
|
g->column_cycles != 0U && g->row_cycles != 0U &&
|
||||||
|
g->ready_timeout_us != 0UL;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long parallel_nand_page_count(const parallel_nand_t *nand)
|
||||||
|
{
|
||||||
|
if (nand == 0) return 0UL;
|
||||||
|
return nand->geometry.pages_per_block * nand->geometry.block_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int wait_ready(parallel_nand_t *nand)
|
||||||
|
{
|
||||||
|
unsigned long elapsed = 0UL;
|
||||||
|
while (!nand->port->is_ready()) {
|
||||||
|
if (elapsed >= nand->geometry.ready_timeout_us)
|
||||||
|
return PNAND_ERROR_TIMEOUT;
|
||||||
|
nand->port->delay_us(1UL);
|
||||||
|
elapsed++;
|
||||||
|
}
|
||||||
|
return PNAND_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parallel_nand_reset(parallel_nand_t *nand)
|
||||||
|
{
|
||||||
|
if (nand == 0 || !port_valid(nand->port)) return PNAND_ERROR_ARGUMENT;
|
||||||
|
nand->port->select();
|
||||||
|
nand->port->write_command(PNAND_CMD_RESET);
|
||||||
|
nand->port->deselect();
|
||||||
|
return wait_ready(nand);
|
||||||
|
}
|
||||||
|
|
||||||
|
int parallel_nand_init(parallel_nand_t *nand,
|
||||||
|
const parallel_nand_port_t *port,
|
||||||
|
const parallel_nand_geometry_t *geometry)
|
||||||
|
{
|
||||||
|
if (nand == 0 || !port_valid(port) || !geometry_valid(geometry))
|
||||||
|
return PNAND_ERROR_ARGUMENT;
|
||||||
|
nand->port = port;
|
||||||
|
nand->geometry = *geometry;
|
||||||
|
if (port->init() != 0) return PNAND_ERROR_PORT;
|
||||||
|
port->deselect();
|
||||||
|
/* Ядро пока только читает: аппаратно запрещаем PROGRAM/ERASE. */
|
||||||
|
port->set_write_protect(1);
|
||||||
|
return parallel_nand_reset(nand);
|
||||||
|
}
|
||||||
|
|
||||||
|
int parallel_nand_read_status(parallel_nand_t *nand, pnand_octet_t *status)
|
||||||
|
{
|
||||||
|
if (nand == 0 || status == 0) return PNAND_ERROR_ARGUMENT;
|
||||||
|
nand->port->select();
|
||||||
|
nand->port->write_command(PNAND_CMD_READ_STATUS);
|
||||||
|
*status = nand->port->read_data() & 0xFFU;
|
||||||
|
nand->port->deselect();
|
||||||
|
return PNAND_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parallel_nand_read_id(parallel_nand_t *nand, pnand_octet_t address,
|
||||||
|
pnand_octet_t *id, unsigned int count)
|
||||||
|
{
|
||||||
|
unsigned int i;
|
||||||
|
if (nand == 0 || id == 0 || count == 0U) return PNAND_ERROR_ARGUMENT;
|
||||||
|
nand->port->select();
|
||||||
|
nand->port->write_command(PNAND_CMD_READ_ID);
|
||||||
|
nand->port->write_address(address & 0xFFU);
|
||||||
|
for (i = 0U; i < count; i++) id[i] = nand->port->read_data() & 0xFFU;
|
||||||
|
nand->port->deselect();
|
||||||
|
return PNAND_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int begin_page_read(parallel_nand_t *nand, unsigned long page,
|
||||||
|
unsigned long column)
|
||||||
|
{
|
||||||
|
unsigned int i;
|
||||||
|
unsigned long value;
|
||||||
|
unsigned long total = nand->geometry.page_main_size +
|
||||||
|
nand->geometry.page_oob_size;
|
||||||
|
if (page >= parallel_nand_page_count(nand) || column >= total)
|
||||||
|
return PNAND_ERROR_RANGE;
|
||||||
|
|
||||||
|
nand->port->select();
|
||||||
|
nand->port->write_command(PNAND_CMD_READ_1);
|
||||||
|
value = column;
|
||||||
|
for (i = 0U; i < nand->geometry.column_cycles; i++) {
|
||||||
|
nand->port->write_address((pnand_octet_t)(value & 0xFFUL));
|
||||||
|
value >>= 8;
|
||||||
|
}
|
||||||
|
value = page;
|
||||||
|
for (i = 0U; i < nand->geometry.row_cycles; i++) {
|
||||||
|
nand->port->write_address((pnand_octet_t)(value & 0xFFUL));
|
||||||
|
value >>= 8;
|
||||||
|
}
|
||||||
|
nand->port->write_command(PNAND_CMD_READ_2);
|
||||||
|
nand->port->deselect();
|
||||||
|
return wait_ready(nand);
|
||||||
|
}
|
||||||
|
|
||||||
|
int parallel_nand_read_page_slice(parallel_nand_t *nand, unsigned long page,
|
||||||
|
unsigned long column, pnand_octet_t *out,
|
||||||
|
unsigned long count)
|
||||||
|
{
|
||||||
|
unsigned long i;
|
||||||
|
unsigned long total;
|
||||||
|
int result;
|
||||||
|
if (nand == 0 || out == 0 || count == 0UL) return PNAND_ERROR_ARGUMENT;
|
||||||
|
total = nand->geometry.page_main_size + nand->geometry.page_oob_size;
|
||||||
|
if (column >= total || count > total - column) return PNAND_ERROR_RANGE;
|
||||||
|
result = begin_page_read(nand, page, column);
|
||||||
|
if (result != PNAND_OK) return result;
|
||||||
|
nand->port->select();
|
||||||
|
for (i = 0UL; i < count; i++) out[i] = nand->port->read_data() & 0xFFU;
|
||||||
|
nand->port->deselect();
|
||||||
|
return PNAND_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parallel_nand_read_page(parallel_nand_t *nand, unsigned long page,
|
||||||
|
pnand_octet_t *main_data,
|
||||||
|
unsigned long main_capacity,
|
||||||
|
pnand_octet_t *oob,
|
||||||
|
unsigned long oob_capacity)
|
||||||
|
{
|
||||||
|
unsigned long i;
|
||||||
|
int result;
|
||||||
|
if (nand == 0 || main_data == 0 || oob == 0)
|
||||||
|
return PNAND_ERROR_ARGUMENT;
|
||||||
|
if (main_capacity < nand->geometry.page_main_size ||
|
||||||
|
oob_capacity < nand->geometry.page_oob_size)
|
||||||
|
return PNAND_ERROR_BUFFER;
|
||||||
|
result = begin_page_read(nand, page, 0UL);
|
||||||
|
if (result != PNAND_OK) return result;
|
||||||
|
nand->port->select();
|
||||||
|
for (i = 0UL; i < nand->geometry.page_main_size; i++)
|
||||||
|
main_data[i] = nand->port->read_data() & 0xFFU;
|
||||||
|
for (i = 0UL; i < nand->geometry.page_oob_size; i++)
|
||||||
|
oob[i] = nand->port->read_data() & 0xFFU;
|
||||||
|
nand->port->deselect();
|
||||||
|
return PNAND_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parallel_nand_is_bad_block(parallel_nand_t *nand, unsigned long block,
|
||||||
|
int *is_bad)
|
||||||
|
{
|
||||||
|
unsigned long first_page;
|
||||||
|
pnand_octet_t marker;
|
||||||
|
int result;
|
||||||
|
if (nand == 0 || is_bad == 0) return PNAND_ERROR_ARGUMENT;
|
||||||
|
if (block >= nand->geometry.block_count) return PNAND_ERROR_RANGE;
|
||||||
|
first_page = block * nand->geometry.pages_per_block;
|
||||||
|
result = parallel_nand_read_page_slice(nand, first_page,
|
||||||
|
nand->geometry.page_main_size, &marker, 1UL);
|
||||||
|
if (result != PNAND_OK) return result;
|
||||||
|
if ((marker & 0xFFU) != 0xFFU) {
|
||||||
|
*is_bad = 1;
|
||||||
|
return PNAND_OK;
|
||||||
|
}
|
||||||
|
result = parallel_nand_read_page_slice(nand, first_page + 1UL,
|
||||||
|
nand->geometry.page_main_size, &marker, 1UL);
|
||||||
|
if (result != PNAND_OK) return result;
|
||||||
|
*is_bad = ((marker & 0xFFU) != 0xFFU);
|
||||||
|
return PNAND_OK;
|
||||||
|
}
|
||||||
151
c/parallel-nand/src/parallel_nand_gas.c
Normal file
151
c/parallel-nand/src/parallel_nand_gas.c
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
#include "parallel_nand_gas.h"
|
||||||
|
|
||||||
|
static unsigned int pack_pair(const pnand_octet_t *data, unsigned int index)
|
||||||
|
{
|
||||||
|
return (unsigned int)((data[index * 2U] & 0xFFU) |
|
||||||
|
((data[index * 2U + 1U] & 0xFFU) << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
parallel_nand_gas_result_t parallel_nand_gas_read(
|
||||||
|
parallel_nand_gas_t *gas, unsigned int address, unsigned int *value)
|
||||||
|
{
|
||||||
|
unsigned int offset;
|
||||||
|
const parallel_nand_geometry_t *g;
|
||||||
|
if (gas == 0 || value == 0 || address < gas->base)
|
||||||
|
return PNAND_GAS_REJECTED;
|
||||||
|
g = &gas->nand->geometry;
|
||||||
|
offset = address - gas->base;
|
||||||
|
if (offset >= PNAND_GAS_REGION_REGS) return PNAND_GAS_NO_REG;
|
||||||
|
if (offset >= PNAND_GAS_DATA_OFFSET &&
|
||||||
|
offset < PNAND_GAS_DATA_OFFSET + PNAND_GAS_DATA_REGS) {
|
||||||
|
if ((gas->status & PNAND_GAS_STATUS_PAGE_VALID) == 0U)
|
||||||
|
return PNAND_GAS_REJECTED;
|
||||||
|
*value = pack_pair(gas->main_data,
|
||||||
|
offset - PNAND_GAS_DATA_OFFSET);
|
||||||
|
return PNAND_GAS_OK;
|
||||||
|
}
|
||||||
|
if (offset >= PNAND_GAS_OOB_OFFSET &&
|
||||||
|
offset < PNAND_GAS_OOB_OFFSET + PNAND_GAS_OOB_REGS) {
|
||||||
|
if ((gas->status & PNAND_GAS_STATUS_PAGE_VALID) == 0U)
|
||||||
|
return PNAND_GAS_REJECTED;
|
||||||
|
*value = pack_pair(gas->oob,
|
||||||
|
offset - PNAND_GAS_OOB_OFFSET);
|
||||||
|
return PNAND_GAS_OK;
|
||||||
|
}
|
||||||
|
switch (offset) {
|
||||||
|
case PNAND_GAS_REG_SIGNATURE: *value = 0x4E44U; break; /* ND */
|
||||||
|
case PNAND_GAS_REG_VERSION: *value = 0x0001U; break;
|
||||||
|
case PNAND_GAS_REG_STATUS: *value = gas->status; break;
|
||||||
|
case PNAND_GAS_REG_ERROR: *value = (unsigned int)gas->error & 0xFFFFU; break;
|
||||||
|
case PNAND_GAS_REG_COMMAND: *value = PNAND_GAS_CMD_NONE; break;
|
||||||
|
case PNAND_GAS_REG_PAGE_LO: *value = (unsigned int)gas->selected_page & 0xFFFFU; break;
|
||||||
|
case PNAND_GAS_REG_PAGE_HI: *value = (unsigned int)(gas->selected_page >> 16) & 0xFFFFU; break;
|
||||||
|
case PNAND_GAS_REG_PAGE_MAIN: *value = (unsigned int)g->page_main_size; break;
|
||||||
|
case PNAND_GAS_REG_PAGE_OOB: *value = (unsigned int)g->page_oob_size; break;
|
||||||
|
case PNAND_GAS_REG_PAGES_PER_BLOCK: *value = (unsigned int)g->pages_per_block; break;
|
||||||
|
case PNAND_GAS_REG_BLOCKS: *value = (unsigned int)g->block_count; break;
|
||||||
|
case PNAND_GAS_REG_MANUFACTURER_ID: *value = (unsigned int)gas->id[0]; break;
|
||||||
|
case PNAND_GAS_REG_DEVICE_ID: *value = (unsigned int)gas->id[1]; break;
|
||||||
|
case PNAND_GAS_REG_BAD_BLOCK:
|
||||||
|
*value = (gas->status & PNAND_GAS_STATUS_BAD_BLOCK) != 0U; break;
|
||||||
|
case PNAND_GAS_REG_GENERATION_LO: *value = (unsigned int)gas->generation & 0xFFFFU; break;
|
||||||
|
case PNAND_GAS_REG_GENERATION_HI: *value = (unsigned int)(gas->generation >> 16) & 0xFFFFU; break;
|
||||||
|
default: return PNAND_GAS_NO_REG;
|
||||||
|
}
|
||||||
|
*value &= 0xFFFFU;
|
||||||
|
return PNAND_GAS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void finish(parallel_nand_gas_t *gas, int result)
|
||||||
|
{
|
||||||
|
gas->error = result;
|
||||||
|
gas->status &= (0xFFFFU ^ PNAND_GAS_STATUS_BUSY);
|
||||||
|
gas->status |= PNAND_GAS_STATUS_READY;
|
||||||
|
if (result != PNAND_OK) gas->status |= PNAND_GAS_STATUS_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
static parallel_nand_gas_result_t run_command(parallel_nand_gas_t *gas,
|
||||||
|
unsigned int command)
|
||||||
|
{
|
||||||
|
int result = PNAND_OK;
|
||||||
|
int bad = 0;
|
||||||
|
gas->status = PNAND_GAS_STATUS_BUSY;
|
||||||
|
if (command == PNAND_GAS_CMD_RESET) {
|
||||||
|
result = parallel_nand_reset(gas->nand);
|
||||||
|
} else if (command == PNAND_GAS_CMD_READ_ID) {
|
||||||
|
result = parallel_nand_read_id(gas->nand, 0U, gas->id, 8U);
|
||||||
|
} else if (command == PNAND_GAS_CMD_READ_PAGE) {
|
||||||
|
unsigned long block;
|
||||||
|
if (gas->selected_page >= parallel_nand_page_count(gas->nand)) {
|
||||||
|
finish(gas, PNAND_ERROR_RANGE);
|
||||||
|
return PNAND_GAS_REJECTED;
|
||||||
|
}
|
||||||
|
result = parallel_nand_read_page(gas->nand, gas->selected_page,
|
||||||
|
gas->main_data, PNAND_MT29F1G08_PAGE_MAIN,
|
||||||
|
gas->oob, PNAND_MT29F1G08_PAGE_OOB);
|
||||||
|
if (result == PNAND_OK) {
|
||||||
|
gas->status |= PNAND_GAS_STATUS_PAGE_VALID;
|
||||||
|
gas->generation++;
|
||||||
|
block = gas->selected_page /
|
||||||
|
gas->nand->geometry.pages_per_block;
|
||||||
|
if (!gas->block_cache_valid || gas->cached_block != block) {
|
||||||
|
result = parallel_nand_is_bad_block(gas->nand, block, &bad);
|
||||||
|
if (result == PNAND_OK) {
|
||||||
|
gas->cached_block = block;
|
||||||
|
gas->cached_bad_block = bad;
|
||||||
|
gas->block_cache_valid = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bad = gas->cached_bad_block;
|
||||||
|
}
|
||||||
|
if (result == PNAND_OK && bad)
|
||||||
|
gas->status |= PNAND_GAS_STATUS_BAD_BLOCK;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gas->status = PNAND_GAS_STATUS_READY;
|
||||||
|
return PNAND_GAS_REJECTED;
|
||||||
|
}
|
||||||
|
finish(gas, result);
|
||||||
|
return result == PNAND_OK ? PNAND_GAS_OK : PNAND_GAS_REJECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
parallel_nand_gas_result_t parallel_nand_gas_write(
|
||||||
|
parallel_nand_gas_t *gas, unsigned int address, unsigned int value)
|
||||||
|
{
|
||||||
|
unsigned int offset;
|
||||||
|
if (gas == 0 || address < gas->base) return PNAND_GAS_REJECTED;
|
||||||
|
offset = address - gas->base;
|
||||||
|
if (offset >= PNAND_GAS_REGION_REGS) return PNAND_GAS_NO_REG;
|
||||||
|
if ((gas->status & PNAND_GAS_STATUS_BUSY) != 0U)
|
||||||
|
return PNAND_GAS_REJECTED;
|
||||||
|
value &= 0xFFFFU;
|
||||||
|
switch (offset) {
|
||||||
|
case PNAND_GAS_REG_COMMAND: return run_command(gas, value);
|
||||||
|
case PNAND_GAS_REG_PAGE_LO:
|
||||||
|
gas->selected_page = (gas->selected_page & 0xFFFF0000UL) | value;
|
||||||
|
gas->status &= (0xFFFFU ^ PNAND_GAS_STATUS_PAGE_VALID);
|
||||||
|
return PNAND_GAS_OK;
|
||||||
|
case PNAND_GAS_REG_PAGE_HI:
|
||||||
|
gas->selected_page = (gas->selected_page & 0x0000FFFFUL) |
|
||||||
|
((unsigned long)value << 16);
|
||||||
|
gas->status &= (0xFFFFU ^ PNAND_GAS_STATUS_PAGE_VALID);
|
||||||
|
return PNAND_GAS_OK;
|
||||||
|
default: return PNAND_GAS_READ_ONLY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void parallel_nand_gas_init(parallel_nand_gas_t *gas,
|
||||||
|
parallel_nand_t *nand, unsigned int base)
|
||||||
|
{
|
||||||
|
unsigned int i;
|
||||||
|
gas->nand = nand;
|
||||||
|
gas->base = base;
|
||||||
|
gas->status = PNAND_GAS_STATUS_READY;
|
||||||
|
gas->error = PNAND_OK;
|
||||||
|
gas->selected_page = 0UL;
|
||||||
|
gas->generation = 0UL;
|
||||||
|
gas->cached_block = 0UL;
|
||||||
|
gas->cached_bad_block = 0;
|
||||||
|
gas->block_cache_valid = 0;
|
||||||
|
for (i = 0U; i < 8U; i++) gas->id[i] = 0U;
|
||||||
|
}
|
||||||
43
c/parallel-nand/src/parallel_nand_pcan_gas.c
Normal file
43
c/parallel-nand/src/parallel_nand_pcan_gas.c
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#include "parallel_nand_pcan_gas.h"
|
||||||
|
|
||||||
|
static pcan_gas_status_t convert(parallel_nand_gas_result_t result)
|
||||||
|
{
|
||||||
|
switch (result) {
|
||||||
|
case PNAND_GAS_OK: return PCAN_GAS_OK;
|
||||||
|
case PNAND_GAS_NO_REG: return PCAN_GAS_NO_REG;
|
||||||
|
case PNAND_GAS_READ_ONLY: return PCAN_GAS_READ_ONLY;
|
||||||
|
default: return PCAN_GAS_REJECTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static pcan_gas_status_t read_reg(const pcan_gas_region_t *region,
|
||||||
|
uint16_t offset, uint16_t *value)
|
||||||
|
{
|
||||||
|
parallel_nand_gas_t *gas = (parallel_nand_gas_t *)region->user;
|
||||||
|
unsigned int portable_value;
|
||||||
|
parallel_nand_gas_result_t result = parallel_nand_gas_read(
|
||||||
|
gas, (unsigned int)region->base + offset, &portable_value);
|
||||||
|
if (result == PNAND_GAS_OK) *value = (uint16_t)portable_value;
|
||||||
|
return convert(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pcan_gas_status_t write_reg(const pcan_gas_region_t *region,
|
||||||
|
uint16_t offset, uint16_t value)
|
||||||
|
{
|
||||||
|
parallel_nand_gas_t *gas = (parallel_nand_gas_t *)region->user;
|
||||||
|
return convert(parallel_nand_gas_write(
|
||||||
|
gas, (unsigned int)region->base + offset, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void parallel_nand_pcan_gas_region_init(pcan_gas_region_t *region,
|
||||||
|
parallel_nand_gas_t *gas)
|
||||||
|
{
|
||||||
|
region->base = (uint16_t)gas->base;
|
||||||
|
region->count = PNAND_GAS_REGION_REGS;
|
||||||
|
region->storage = 0;
|
||||||
|
region->read = read_reg;
|
||||||
|
region->write = write_reg;
|
||||||
|
region->flags = 0U;
|
||||||
|
region->user = gas;
|
||||||
|
region->name = "parallel-nand";
|
||||||
|
}
|
||||||
97
c/parallel-nand/tests/test_parallel_nand.c
Normal file
97
c/parallel-nand/tests/test_parallel_nand.c
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "parallel_nand.h"
|
||||||
|
#include "parallel_nand_gas.h"
|
||||||
|
|
||||||
|
typedef struct { char kind; unsigned int value; } event_t;
|
||||||
|
static event_t events[64];
|
||||||
|
static unsigned int event_count;
|
||||||
|
static pnand_octet_t input_data[32];
|
||||||
|
static unsigned int input_count;
|
||||||
|
static unsigned int input_pos;
|
||||||
|
|
||||||
|
static void record(char kind, unsigned int value)
|
||||||
|
{
|
||||||
|
events[event_count].kind = kind;
|
||||||
|
events[event_count].value = value;
|
||||||
|
event_count++;
|
||||||
|
}
|
||||||
|
static int fake_init(void) { return 0; }
|
||||||
|
static void fake_select(void) { record('S', 0); }
|
||||||
|
static void fake_deselect(void) { record('D', 0); }
|
||||||
|
static void fake_cmd(pnand_octet_t v) { record('C', v); }
|
||||||
|
static void fake_addr(pnand_octet_t v) { record('A', v); }
|
||||||
|
static void fake_write(pnand_octet_t v) { record('W', v); }
|
||||||
|
static pnand_octet_t fake_read(void)
|
||||||
|
{
|
||||||
|
return input_pos < input_count ? input_data[input_pos++] : 0xFFU;
|
||||||
|
}
|
||||||
|
static int fake_ready(void) { return 1; }
|
||||||
|
static void fake_wp(int enabled) { record('P', (unsigned int)enabled); }
|
||||||
|
static void fake_delay(unsigned long us) { (void)us; }
|
||||||
|
|
||||||
|
static const parallel_nand_port_t fake_port = {
|
||||||
|
fake_init, fake_select, fake_deselect, fake_cmd, fake_addr, fake_write,
|
||||||
|
fake_read, fake_ready, fake_wp, fake_delay
|
||||||
|
};
|
||||||
|
|
||||||
|
static void clear_events(void) { event_count = 0U; }
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
parallel_nand_t nand;
|
||||||
|
pnand_octet_t id[2];
|
||||||
|
pnand_octet_t bytes[2];
|
||||||
|
parallel_nand_gas_t gas;
|
||||||
|
unsigned int reg;
|
||||||
|
|
||||||
|
assert(parallel_nand_init(&nand, &fake_port,
|
||||||
|
¶llel_nand_mt29f1g08_geometry) == PNAND_OK);
|
||||||
|
assert(events[0].kind == 'D');
|
||||||
|
assert(events[1].kind == 'P' && events[1].value == 1U);
|
||||||
|
|
||||||
|
clear_events();
|
||||||
|
input_data[0] = 0x2CU;
|
||||||
|
input_data[1] = 0xF1U;
|
||||||
|
input_count = 2U;
|
||||||
|
input_pos = 0U;
|
||||||
|
assert(parallel_nand_read_id(&nand, 0U, id, 2U) == PNAND_OK);
|
||||||
|
assert(id[0] == 0x2CU && id[1] == 0xF1U);
|
||||||
|
assert(events[1].kind == 'C' && events[1].value == 0x90U);
|
||||||
|
assert(events[2].kind == 'A' && events[2].value == 0U);
|
||||||
|
|
||||||
|
clear_events();
|
||||||
|
input_data[0] = 0x12U;
|
||||||
|
input_data[1] = 0x34U;
|
||||||
|
input_count = 2U;
|
||||||
|
input_pos = 0U;
|
||||||
|
assert(parallel_nand_read_page_slice(&nand, 0x1234UL, 0x56UL,
|
||||||
|
bytes, 2UL) == PNAND_OK);
|
||||||
|
assert(bytes[0] == 0x12U && bytes[1] == 0x34U);
|
||||||
|
assert(events[1].kind == 'C' && events[1].value == 0x00U);
|
||||||
|
assert(events[2].kind == 'A' && events[2].value == 0x56U);
|
||||||
|
assert(events[3].kind == 'A' && events[3].value == 0x00U);
|
||||||
|
assert(events[4].kind == 'A' && events[4].value == 0x34U);
|
||||||
|
assert(events[5].kind == 'A' && events[5].value == 0x12U);
|
||||||
|
assert(events[6].kind == 'C' && events[6].value == 0x30U);
|
||||||
|
|
||||||
|
parallel_nand_gas_init(&gas, &nand, PNAND_GAS_DEFAULT_BASE);
|
||||||
|
assert(parallel_nand_gas_read(&gas, 0xE000U, ®) == PNAND_GAS_OK);
|
||||||
|
assert(reg == 0x4E44U);
|
||||||
|
assert(parallel_nand_gas_write(&gas, 0xE005U, 0x1234U) ==
|
||||||
|
PNAND_GAS_OK);
|
||||||
|
assert(parallel_nand_gas_write(&gas, 0xE006U, 0x0001U) ==
|
||||||
|
PNAND_GAS_OK);
|
||||||
|
assert(gas.selected_page == 0x00011234UL);
|
||||||
|
gas.main_data[0] = 0x34U;
|
||||||
|
gas.main_data[1] = 0x12U;
|
||||||
|
gas.status |= PNAND_GAS_STATUS_PAGE_VALID;
|
||||||
|
assert(parallel_nand_gas_read(&gas, 0xE100U, ®) == PNAND_GAS_OK);
|
||||||
|
assert(reg == 0x1234U);
|
||||||
|
assert(parallel_nand_gas_write(&gas, 0xE000U, 0U) ==
|
||||||
|
PNAND_GAS_READ_ONLY);
|
||||||
|
|
||||||
|
puts("parallel_nand: OK");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ set(SETPROTOCOL_V2_SOURCES
|
|||||||
src/set_can.c
|
src/set_can.c
|
||||||
src/set_firmware.c
|
src/set_firmware.c
|
||||||
src/set_telemetry.c
|
src/set_telemetry.c
|
||||||
|
src/set_plot.c
|
||||||
|
src/set_trends.c
|
||||||
|
src/set_spectrum.c
|
||||||
)
|
)
|
||||||
|
|
||||||
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
|
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
|
||||||
@@ -32,6 +35,9 @@ set(SETPROTOCOL_SOURCES
|
|||||||
|
|
||||||
add_library(setprotocol_static STATIC ${SETPROTOCOL_SOURCES})
|
add_library(setprotocol_static STATIC ${SETPROTOCOL_SOURCES})
|
||||||
target_include_directories(setprotocol_static PUBLIC include)
|
target_include_directories(setprotocol_static PUBLIC include)
|
||||||
|
if(NOT MSVC)
|
||||||
|
target_link_libraries(setprotocol_static PUBLIC m)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Совместимое имя цели для проектов, уже использовавших SET protocol v2.
|
# Совместимое имя цели для проектов, уже использовавших SET protocol v2.
|
||||||
add_library(set_protocol ALIAS setprotocol_static)
|
add_library(set_protocol ALIAS setprotocol_static)
|
||||||
@@ -40,6 +46,9 @@ option(SETP_BUILD_SHARED "Build stable host ABI shared library" ON)
|
|||||||
if(SETP_BUILD_SHARED)
|
if(SETP_BUILD_SHARED)
|
||||||
add_library(setprotocol SHARED ${SETPROTOCOL_SOURCES})
|
add_library(setprotocol SHARED ${SETPROTOCOL_SOURCES})
|
||||||
target_include_directories(setprotocol PUBLIC include)
|
target_include_directories(setprotocol PUBLIC include)
|
||||||
|
if(NOT MSVC)
|
||||||
|
target_link_libraries(setprotocol PRIVATE m)
|
||||||
|
endif()
|
||||||
target_compile_definitions(setprotocol PRIVATE PCAN_ABI_BUILD_DLL)
|
target_compile_definitions(setprotocol PRIVATE PCAN_ABI_BUILD_DLL)
|
||||||
set_target_properties(setprotocol PROPERTIES
|
set_target_properties(setprotocol PROPERTIES
|
||||||
C_VISIBILITY_PRESET hidden
|
C_VISIBILITY_PRESET hidden
|
||||||
@@ -66,6 +75,10 @@ option(SETP_BUILD_TESTS "Build host tests" ON)
|
|||||||
if(SETP_BUILD_TESTS)
|
if(SETP_BUILD_TESTS)
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
|
add_executable(test_plot tests/test_plot.c)
|
||||||
|
target_link_libraries(test_plot PRIVATE setprotocol_static)
|
||||||
|
add_test(NAME shared_plot COMMAND test_plot)
|
||||||
|
|
||||||
add_executable(test_set_protocol tests/test_set_protocol.c)
|
add_executable(test_set_protocol tests/test_set_protocol.c)
|
||||||
target_link_libraries(test_set_protocol PRIVATE setprotocol_static)
|
target_link_libraries(test_set_protocol PRIVATE setprotocol_static)
|
||||||
add_test(NAME set_protocol_v2 COMMAND test_set_protocol)
|
add_test(NAME set_protocol_v2 COMMAND test_set_protocol)
|
||||||
@@ -81,4 +94,10 @@ if(SETP_BUILD_TESTS)
|
|||||||
add_executable(test_abi tests/test_abi.c)
|
add_executable(test_abi tests/test_abi.c)
|
||||||
target_link_libraries(test_abi PRIVATE setprotocol_static)
|
target_link_libraries(test_abi PRIVATE setprotocol_static)
|
||||||
add_test(NAME stable_abi COMMAND test_abi)
|
add_test(NAME stable_abi COMMAND test_abi)
|
||||||
|
add_executable(test_trends tests/test_trends.c)
|
||||||
|
target_link_libraries(test_trends PRIVATE setprotocol_static)
|
||||||
|
add_test(NAME shared_trends COMMAND test_trends)
|
||||||
|
add_executable(test_spectrum tests/test_spectrum.c)
|
||||||
|
target_link_libraries(test_spectrum PRIVATE setprotocol_static)
|
||||||
|
add_test(NAME shared_spectrum COMMAND test_spectrum)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ multicast запрещены WRITE, FW_BEGIN и FW_ACTIVATE.
|
|||||||
|
|
||||||
## CAN
|
## CAN
|
||||||
|
|
||||||
|
Для существующего прикладного ProtoCAN доступен
|
||||||
|
[порт STM32 bxCAN](ports/stm32-bxcan/README.md), перенесённый из SETCAN.
|
||||||
|
Он сохраняет API `PROTOCAN_*`, использует HAL CAN/RTC/TIM и общий `pcan_id`.
|
||||||
|
Порт рассчитан на classic bxCAN; он не реализует сегментацию SETProtocol v2.
|
||||||
|
В проект устройства добавляются `ports/stm32-bxcan/protocan.c` и `src/pcan_id.c`,
|
||||||
|
а в include paths — `ports/stm32-bxcan` и `include`.
|
||||||
|
|
||||||
Не вызывайте прикладной router из ISR. ISR принимает CAN-кадры в очередь,
|
Не вызывайте прикладной router из ISR. ISR принимает CAN-кадры в очередь,
|
||||||
сегментация собирается в главном цикле, и только полный SETP-кадр передаётся
|
сегментация собирается в главном цикле, и только полный SETP-кадр передаётся
|
||||||
parser/router. Сборка должна иметь timeout, контроль номера сегмента и одного
|
parser/router. Сборка должна иметь timeout, контроль номера сегмента и одного
|
||||||
|
|||||||
@@ -7,12 +7,20 @@
|
|||||||
- совместимый ProtoCAN transport для существующих CAN-мостов;
|
- совместимый ProtoCAN transport для существующих CAN-мостов;
|
||||||
- SETGUI transport v1 для приборов переходного периода;
|
- SETGUI transport v1 для приборов переходного периода;
|
||||||
- стабильный host ABI для Python/JNI/других FFI;
|
- стабильный host ABI для Python/JNI/других FFI;
|
||||||
- GAS, кольцевые буферы, CRC и потоковые parser.
|
- GAS, кольцевые буферы, CRC и потоковые parser;
|
||||||
|
- host-анализ спектра: FFT, окна, фильтры и адаптеры JNI/ctypes.
|
||||||
|
|
||||||
Ядро не зависит от HAL, ОС или конкретного адаптера, не использует
|
Протокольное ядро не зависит от HAL, ОС или конкретного адаптера, не использует
|
||||||
динамическую память и не открывает COM/CAN само. SLCAN, SocketCAN, USB CDC,
|
динамическую память и не открывает COM/CAN само. SLCAN, SocketCAN, USB CDC,
|
||||||
TCP, UART/DMA и аппаратный CAN подключаются портами.
|
TCP, UART/DMA и аппаратный CAN подключаются портами.
|
||||||
|
|
||||||
|
Отдельный host-модуль `set_spectrum.c` использует ограниченный временный буфер
|
||||||
|
до 2×16384 `double`; он не нужен для wire-протокола/MCU.
|
||||||
|
Контракт и использование из обоих GUI: [GUI_SPECTRUM.md](docs/GUI_SPECTRUM.md).
|
||||||
|
|
||||||
|
Масштабирование, координаты и измерительные маркеры Android/SETGUI используют
|
||||||
|
общий `set_plot.c`: [границы модулей, ABI и проверки](docs/GUI_PLOT.md).
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
| Каталог | Назначение |
|
| Каталог | Назначение |
|
||||||
@@ -24,6 +32,7 @@ TCP, UART/DMA и аппаратный CAN подключаются портам
|
|||||||
| `src/` | общая реализация всех протокольных модулей |
|
| `src/` | общая реализация всех протокольных модулей |
|
||||||
| `ports/android` | JNI и Kotlin facade |
|
| `ports/android` | JNI и Kotlin facade |
|
||||||
| `ports/stm32f4` | UART/DMA port для legacy byte stream |
|
| `ports/stm32f4` | UART/DMA port для legacy byte stream |
|
||||||
|
| [`ports/stm32-bxcan`](ports/stm32-bxcan/README.md) | STM32 HAL bxCAN/RTC/TIM, прикладной ProtoCAN; бывший отдельный SETCAN |
|
||||||
| `docs/SETPROTOCOL.md` | архитектура, ABI, память и переносимость |
|
| `docs/SETPROTOCOL.md` | архитектура, ABI, память и переносимость |
|
||||||
| `docs/legacy` | нормативные документы ProtoCAN/SETGUI v1 |
|
| `docs/legacy` | нормативные документы ProtoCAN/SETGUI v1 |
|
||||||
| `PROTOCOL.md` | нормативный wire contract SET protocol v2 |
|
| `PROTOCOL.md` | нормативный wire contract SET protocol v2 |
|
||||||
|
|||||||
80
c/set-protocol/docs/GUI_PLOT.md
Normal file
80
c/set-protocol/docs/GUI_PLOT.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Общая работа с графиками Android и SETGUI
|
||||||
|
|
||||||
|
Численная логика находится в `include/set_plot.h` и `src/set_plot.c`.
|
||||||
|
Это модуль C99 без Qt, Android, транспорта, динамической памяти и глобального
|
||||||
|
состояния. Он собирается в существующую библиотеку SETProtocol; отдельная DLL
|
||||||
|
для графиков не требуется. Версия ABI графиков — `set_plot_abi_version() == 1`.
|
||||||
|
|
||||||
|
| Общее в templates | Адаптер приложения |
|
||||||
|
|---|---|
|
||||||
|
| Нормализованная область просмотра, масштаб 1…128, ограничение панорамирования | Размер поля и отступы в пикселях |
|
||||||
|
| Независимый масштаб X/Y с сохранением координаты под пальцами | События Compose или Qt, колесо, кнопки |
|
||||||
|
| Выбор преобладающей оси по изменению расстояния между пальцами | Накопление движения до порога; фиксация выбранной оси до конца жеста |
|
||||||
|
| Перевод значения в долю экрана и обратно, инверсия Y | Canvas/QPainter и оформление шкал |
|
||||||
|
| Перемещение маркера от начальной координаты, ограничение видимой областью | Захват линии пальцем/мышью, редактор положения |
|
||||||
|
| Разность B−A, D−C и множитель единиц, шаги шкалы 1/2/5 | Подписи, цвета, миллисекунды/герцы/единицы сигнала |
|
||||||
|
| Модели маркеров и их размещение в Kotlin/Python-портах | Жизненный цикл экрана, очистка и выбор источника |
|
||||||
|
|
||||||
|
Модель маркеров: A/B — координаты X и вертикальные линии во всю высоту поля;
|
||||||
|
C/D — координаты Y и горизонтальные линии. Они могут пересекаться: отрицательная
|
||||||
|
разность сохраняется. Начальные координаты — 1/3 и 2/3 диапазона. При изменении
|
||||||
|
масштаба уже установленные координаты сохраняются. «Маркеры в видимую область»
|
||||||
|
заново размещает их. Координаты и область просмотра времени/спектра независимы.
|
||||||
|
|
||||||
|
`set_plot_eval()` принимает операцию из `enum set_plot_operation`, точное число
|
||||||
|
входных `double` и буфер вывода. Контракт каждой операции указан в заголовке.
|
||||||
|
Возвращает число записанных элементов, 0 — неправильные аргументы. Некорректный
|
||||||
|
коэффициент масштабирования оставляет корректный viewport неизменным.
|
||||||
|
NaN/Inf и вырожденные диапазоны не передаются дальше в отрисовку.
|
||||||
|
FRACTION/VALUE не обрезают координаты: это позволяет правильно отсекать линии
|
||||||
|
по границе поля, а не создавать ложные горизонтальные участки на его краю.
|
||||||
|
DRAG, напротив, ограничивает положение маркера видимым диапазоном.
|
||||||
|
|
||||||
|
## Порты
|
||||||
|
|
||||||
|
- Android/JVM: `ports/android/kotlin/ru/setcorp/setprotocol/trends/NativePlot.kt`,
|
||||||
|
`PlotViewport.kt`, `TrendMarkers.kt`; тонкий JNI — `ports/android/set_plot_jni.c`.
|
||||||
|
Android использует `System.loadLibrary("setprotocol")`. Для JVM-тестов можно
|
||||||
|
передать абсолютный путь DLL/SO системным свойством `setplot.library`.
|
||||||
|
- Python: `python/protocan/plot.py`: `PlotMath(library)`, `Viewport`, `Bounds`,
|
||||||
|
`Markers`. Приложение передаёт уже загруженную SETProtocol `ctypes.CDLL`.
|
||||||
|
Зависимости — стандартная библиотека; Qt в этот модуль не импортируется.
|
||||||
|
|
||||||
|
Новые изменения не должны добавлять вторую реализацию численных операций
|
||||||
|
в Kotlin/Python или в приложение. Состояние и адаптация событий остаются в портах.
|
||||||
|
Добавление операции требует обновления C ABI, обоих портов и общих эталонов.
|
||||||
|
|
||||||
|
## Интеграция приложений
|
||||||
|
|
||||||
|
Android: `TrendGraph` использует общие viewport, bounds и маркеры, а Compose
|
||||||
|
рисует линии и панель. При измерениях время отображается в миллисекундах от
|
||||||
|
начала выбранного участка; сохранённые X-координаты включают абсолютное начало.
|
||||||
|
|
||||||
|
SETGUI: `ui/plot_interaction.py` адаптирует общий модуль к Qt. Его используют
|
||||||
|
`SignalPlot` (ТМС и датчики), `TrendPlot` (логи) и `SpectrumPlot`.
|
||||||
|
Колесо по вертикали масштабирует Y, Shift+колесо и горизонтальное колесо — X;
|
||||||
|
сенсорный pinch выбирает ось по движению. Временные графики хранят метку приёма
|
||||||
|
каждого отсчёта. Пропущенные значения не растягивают остальные серии:
|
||||||
|
все сигналы имеют единую шкалу времени. При повторном построении ТМС из журнала
|
||||||
|
используются исходные метки приёма. Дискретные дорожки имеют общую X-шкалу,
|
||||||
|
а маркеры уровня Y относятся к аналоговому полю.
|
||||||
|
|
||||||
|
Расчёт FFT не является частью этого модуля. Android вычисляет спектр через
|
||||||
|
`set_spectrum.c`; вкладка спектра SETGUI получает готовые уровни в дБмВ от
|
||||||
|
прибора. Общая интерактивная часть не меняет эти данные или единицы.
|
||||||
|
|
||||||
|
## Проверка и сборка
|
||||||
|
|
||||||
|
- CMake/CTest: `shared_plot` проверяет ABI, размеры буферов и координаты.
|
||||||
|
- `tests/fixtures/plot-v1.json` содержит общие численные эталоны. Их читают
|
||||||
|
`PlotContractTest` (фактический JNI) и `python/tests/test_plot.py` (ctypes).
|
||||||
|
- Host DLL/SO для Python: `python tools/build_host.py --output <путь>`.
|
||||||
|
- Для JVM: та же команда с `--java-home <JDK>` добавляет JNI-порт графиков.
|
||||||
|
Нужен C-компилятор (MSVC на Windows, cc/clang/gcc на Unix) и JDK с `jni.h`.
|
||||||
|
- Android-проект автоматически собирает host-библиотеку перед JVM-тестами;
|
||||||
|
интерпретатор Python можно задать свойством Gradle `setflash.python`.
|
||||||
|
- SETGUI проверяет события мыши, колеса и двух пальцев через QtTest без
|
||||||
|
физического прибора; численные тесты не требуют GUI.
|
||||||
|
|
||||||
|
Исходники должны приходить из одного коммита подмодуля `templates` в обоих
|
||||||
|
проектах. Локальная замена файлов без обновления gitlink не закрепляет версию.
|
||||||
53
c/set-protocol/docs/GUI_SPECTRUM.md
Normal file
53
c/set-protocol/docs/GUI_SPECTRUM.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Shared GUI spectrum
|
||||||
|
|
||||||
|
`include/set_spectrum.h` / `src/set_spectrum.c` implement the same numerical
|
||||||
|
pipeline for desktop and Android. CMake, `tools/build_host.py`, and the Android
|
||||||
|
NDK build include it. The wire protocol is unchanged. The host-only analyzer
|
||||||
|
allocates at most two FFT arrays (2×N doubles), releasing them before return.
|
||||||
|
|
||||||
|
The last N samples are used: the largest power of two within available data and
|
||||||
|
`max_size`, 16…16384. Timestamps are seconds; inputs are immutable. Fs is measured
|
||||||
|
from the selected endpoints. Non-monotonic/duplicate times and interval deviations
|
||||||
|
over 50% of the mean are rejected. Lesser jitter is linearly interpolated onto an
|
||||||
|
even grid (this can affect high-frequency amplitude; inspect returned jitter).
|
||||||
|
|
||||||
|
Processing order: interpolation, optional mean subtraction, filter, periodic
|
||||||
|
window, forward FFT, one-sided peak amplitude. Windows: rectangular, Hann,
|
||||||
|
Hamming, Blackman, Flat Top. Amplitudes are divided by window sum, doubled except
|
||||||
|
DC and Nyquist. Output is not RMS/PSD/dB. Coherent gain correction is exact for
|
||||||
|
bin-centered tones away from DC/Nyquist; off-bin tones still have scalloping.
|
||||||
|
|
||||||
|
Filters run before the window, on the unmodified sample copy. LP/HP are second
|
||||||
|
order Butterworth; band pass cascades HP and LP; notch uses Q=30.
|
||||||
|
Coefficients: [W3C Audio EQ Cookbook](https://www.w3.org/TR/audio-eq-cookbook/).
|
||||||
|
Initial state is the first sample's steady state, not a carried streaming state;
|
||||||
|
block-start transients may occur. Cutoffs must be strictly within (0, Fs/2).
|
||||||
|
|
||||||
|
## Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from protocan.spectrum import NativeSpectrum, Window, Filter
|
||||||
|
|
||||||
|
# lib = an existing ctypes.CDLL from protocan.native.NativeProtocol
|
||||||
|
spectrum = NativeSpectrum(lib).analyze(times_seconds, values,
|
||||||
|
window=Window.HANN, filter=Filter.LOW_PASS, high_hz=100,
|
||||||
|
max_size=4096, remove_mean=True)
|
||||||
|
plot(spectrum.frequencies, spectrum.amplitudes)
|
||||||
|
```
|
||||||
|
|
||||||
|
Adapter errors are ValueError, not empty/misleading arrays. No NumPy/Qt dependency.
|
||||||
|
|
||||||
|
## Android
|
||||||
|
|
||||||
|
`SpectrumAnalyzer.analyze(List<TrendPoint>, SpectrumOptions)` calls JNI using
|
||||||
|
relative seconds from millisecond timestamps. `TrendSpectrum` contains N, Fs,
|
||||||
|
maximum relative jitter, amplitudes or a displayable error. Call on a worker
|
||||||
|
dispatcher. `PlotViewport` provides axis-independent zoom/pan, focus anchoring,
|
||||||
|
128× limit and bounds clamping without any Compose dependency.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
Build CMake with tests and run CTest in Debug (assertions enabled). Python
|
||||||
|
`tests/test_spectrum.py` runs against an explicitly supplied `SETPROTOCOL_LIBRARY`:
|
||||||
|
independent direct DFT, amplitude correction, DC/Nyquist, off-bin leakage, filter
|
||||||
|
bands, timing rejection/interpolation, bounded tail selection and immutable inputs.
|
||||||
78
c/set-protocol/docs/GUI_TRENDS.md
Normal file
78
c/set-protocol/docs/GUI_TRENDS.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Общие тренды для Android GUI и SETGUI
|
||||||
|
|
||||||
|
Модуль задаёт совместимые настройки и числовые источники графиков. Он не
|
||||||
|
рисует экран, не открывает файлы или порты и не управляет прошивкой устройства.
|
||||||
|
|
||||||
|
Слои: GUI → Kotlin/Python adapter → `set_trends.c` → уже разобранные кадры.
|
||||||
|
Транспорт, таймер опроса, выбор файла, хранение настроек и Canvas/Qt остаются
|
||||||
|
в приложении. У ядра нет callbacks, динамической памяти или глобального состояния.
|
||||||
|
|
||||||
|
| Файл | Назначение и зависимости |
|
||||||
|
|---|---|
|
||||||
|
| `include/set_trends.h`, `src/set_trends.c` | C99: фильтрация GAS/raw CAN, signed/unsigned word, payload подписки SET GUI; использует `pcan_id` и ABI export macro |
|
||||||
|
| `ports/android/kotlin/ru/setcorp/setprotocol/trends/` | Модель, валидация JSON, ограниченная история, GAS_WATCH; JVM + org.json, без Android/Compose |
|
||||||
|
| `ports/android/setprotocol_jni.c` | Только преобразование JNI-аргументов |
|
||||||
|
| `python/protocan/trends.py` | Модель, JSON, история и `NativeTrends`; Python 3.9+, stdlib, без Qt |
|
||||||
|
| `tests/fixtures/trends-v1.json` | Один образец для тестов обоих GUI и обмена настройками |
|
||||||
|
|
||||||
|
## Формат файла
|
||||||
|
|
||||||
|
UTF-8 JSON: `format = "setflash-trends"`, `version = 1`, `profiles` — словарь.
|
||||||
|
Ключи профилей стабильны: `TMS2812`, `SET_V1`, `CAN_BRIDGE`, `GS_USB_CAN`,
|
||||||
|
`SLCAN`, `CANGAROO_SLCAN`, `BALZAM_CAN`. Приложение явно сопоставляет своё
|
||||||
|
подключение с этим ключом; импорт не меняет подключение.
|
||||||
|
|
||||||
|
Поля сигнала обязательны: `id`, `order`, `name`, `source`, `address`, `color`,
|
||||||
|
`visible`, `valueType`, `deviceType`, `device`, `byteOffset`, `extended`.
|
||||||
|
`id` уникален во всём файле. `order` — 1…9999, уникален внутри профиля.
|
||||||
|
`name` — непустое изменяемое имя (до 100 символов), `color` — `#RRGGBB`.
|
||||||
|
`visible` управляет только отрисовкой, не сбором. История и соединение в файл
|
||||||
|
не входят. Максимум 64 сигнала на профиль и 1 МиБ на файл. Неизвестные версии,
|
||||||
|
профили, источники и неверные значения отклоняются до замены настроек.
|
||||||
|
|
||||||
|
| Профиль / источник | Адрес и значение |
|
||||||
|
|---|---|
|
||||||
|
| TMS2812 / TMS_MEMORY | 32-битный адрес слова, CMD_PEEK; контроллер из подключения |
|
||||||
|
| SET_V1 / SET_GAS | 16-битный GAS; GAS_WATCH_SET `0x12`, DATA `0x13` |
|
||||||
|
| SET_V1 / SET_SENSOR | ROM DS18B20, 16 HEX-цифр (дефисы допустимы), °C |
|
||||||
|
| Современные CAN / CAN_GAS | GAS 0…65535, DeviceType 0…7, DeviceID 0…15 |
|
||||||
|
| Все CAN / CAN_RAW | CAN ID, STD/EXT, byteOffset 0…6; слово Little-endian |
|
||||||
|
|
||||||
|
Числовые адреса — десятичные либо HEX с `0x`. Регистры трактуются как
|
||||||
|
`UINT16` или `INT16`. Для RAW/CAN_GAS C-функция возвращает значение либо
|
||||||
|
`SET_TREND_NO_VALUE`, никогда не подставляет ноль вместо отсутствующего кадра.
|
||||||
|
|
||||||
|
CAN — пассивный приём, без записи GAS и автоматической отправки запросов.
|
||||||
|
GAS принимает только входящие FROM_DEVICE; TX/RTR/ошибки исключены.
|
||||||
|
SET GUI оформляет подписку в порядке адресов. ACK должен подтвердить весь
|
||||||
|
список: при частичном принятии нельзя определить пропущенные адреса, поэтому
|
||||||
|
строить график по смещённым индексам запрещено. При паузе порт приложения
|
||||||
|
отправляет подписку с периодом 0, когда транспорт доступен и не занят прошивкой.
|
||||||
|
|
||||||
|
## Быстрый старт SETGUI
|
||||||
|
|
||||||
|
```python
|
||||||
|
from protocan.native import NativeProtocol
|
||||||
|
from protocan.trends import TrendSignal, NativeTrends, encode_settings
|
||||||
|
|
||||||
|
core = NativeProtocol() # библиотека, пересобранная с set_trends.c
|
||||||
|
decoder = NativeTrends(core.lib)
|
||||||
|
signal = TrendSignal("current", source="CAN_GAS", address="0x1235")
|
||||||
|
text = encode_settings({"CAN_BRIDGE": [signal]})
|
||||||
|
value = decoder.can_value(signal, 0x1FD31234, 1, bytes([1, 0, 254, 255]))
|
||||||
|
assert value == 65534.0
|
||||||
|
# GUI сохраняет text выбранным способом и передаёт value своему графику.
|
||||||
|
```
|
||||||
|
|
||||||
|
Android подключает Kotlin-каталог напрямую через Gradle и C-файлы через NDK.
|
||||||
|
Приложение SETGUI может импортировать модуль через уже используемый путь
|
||||||
|
`third_party/templates/python`; в этом изменении его экран не изменяется.
|
||||||
|
После публикации новой ревизии templates оба проекта должны обновить сабмодуль
|
||||||
|
и пересобрать собственные native-библиотеки — вручную копировать реализации не нужно.
|
||||||
|
|
||||||
|
## Проверки
|
||||||
|
|
||||||
|
- CMake/CTest: `shared_trends`, плюс существующие protocol/ABI тесты.
|
||||||
|
- JVM: `TrendSettingsJsonTest`, `GuiGasWatchTest`; один JSON fixture с Python.
|
||||||
|
- Python: `PYTHONPATH=python python -m unittest discover -s python/tests`.
|
||||||
|
Для проверки настоящего C-декодера задайте `SETPROTOCOL_LIBRARY` путём DLL/SO.
|
||||||
@@ -48,7 +48,7 @@ uint16_t n = pcan_gas_read_block(&map, 0x0000, block, 4);
|
|||||||
Тип сообщения `PCAN_MSG_GAS` (`0b0011`). Адрес первого регистра лежит
|
Тип сообщения `PCAN_MSG_GAS` (`0b0011`). Адрес первого регистра лежит
|
||||||
в `MsgBody`, данные — до 4 регистров подряд, младшим байтом вперёд.
|
в `MsgBody`, данные — до 4 регистров подряд, младшим байтом вперёд.
|
||||||
Это ровно то, что делает `PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()`
|
Это ровно то, что делает `PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()`
|
||||||
в `SETCAN/Src/protocan.c`, поэтому обмен совместим с существующими
|
в `ports/stm32-bxcan/protocan.c` (бывший SETCAN), поэтому обмен совместим с существующими
|
||||||
устройствами.
|
устройствами.
|
||||||
|
|
||||||
| Кадр | DLC | Смысл |
|
| Кадр | DLC | Смысл |
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* Транспорт ProtoCAN (MsgType = 0b0011) кладёт в MsgBody адрес первого
|
* Транспорт ProtoCAN (MsgType = 0b0011) кладёт в MsgBody адрес первого
|
||||||
* регистра, а в данные - до 4 регистров подряд, младшим байтом вперёд.
|
* регистра, а в данные - до 4 регистров подряд, младшим байтом вперёд.
|
||||||
* Это ровно то, что делает PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()
|
* Это ровно то, что делает PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()
|
||||||
* в SETCAN/Src/protocan.c, поэтому обмен совместим с существующими
|
* в ports/stm32-bxcan/protocan.c, поэтому обмен совместим с существующими
|
||||||
* устройствами.
|
* устройствами.
|
||||||
*
|
*
|
||||||
* Запрос на чтение кодируется кадром GAS с DLC = 0: в исходном коде
|
* Запрос на чтение кодируется кадром GAS с DLC = 0: в исходном коде
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @file pcan_id.h
|
* @file pcan_id.h
|
||||||
* @brief Упаковка и разбор 29-битного идентификатора ProtoCAN.
|
* @brief Упаковка и разбор 29-битного идентификатора ProtoCAN.
|
||||||
*
|
*
|
||||||
* Раскладка (соответствует ProtoCanId_t из SETCAN/Inc/protocan.h):
|
* Раскладка (ProtoCanId_t из ports/stm32-bxcan/protocan.h, бывший SETCAN):
|
||||||
*
|
*
|
||||||
* биты 28 Priority
|
* биты 28 Priority
|
||||||
* 27 Route
|
* 27 Route
|
||||||
|
|||||||
35
c/set-protocol/include/set_plot.h
Normal file
35
c/set-protocol/include/set_plot.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/** @file set_plot.h
|
||||||
|
* @brief Toolkit-independent plot interaction math shared by JNI and ctypes.
|
||||||
|
* Coordinates are doubles in the caller's units. No allocation or global state.
|
||||||
|
*/
|
||||||
|
#ifndef SET_PLOT_H
|
||||||
|
#define SET_PLOT_H
|
||||||
|
#include "pcan_abi.h"
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum set_plot_operation {
|
||||||
|
SET_PLOT_TRANSFORM = 0, /* x,y,w,h, zoomX,zoomY, panX,panY, focusX,focusY -> x,y,w,h */
|
||||||
|
SET_PLOT_AXIS = 1, /* deltaX,deltaY,slop -> 0=none, 1=X, 2=Y */
|
||||||
|
SET_PLOT_FRACTION = 2, /* value,low,high,inverted -> fraction (not clamped) */
|
||||||
|
SET_PLOT_VALUE = 3, /* fraction,low,high,inverted -> value (not clamped) */
|
||||||
|
SET_PLOT_DRAG = 4, /* initial,deltaPixels,length,low,high,inverted -> clamped value */
|
||||||
|
SET_PLOT_TICK_STEP = 5, /* range,lengthPixels -> nice step */
|
||||||
|
SET_PLOT_DELTA = 6 /* A,B,multiplier -> (B-A)*multiplier */
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Version of this plot ABI, independently of the transport ABI. */
|
||||||
|
PCAN_ABI_API uint32_t set_plot_abi_version(void);
|
||||||
|
/** Evaluates one operation. Returns output count, or 0 for invalid arguments.
|
||||||
|
* TRANSFORM rejects malformed viewports; invalid gesture values return the
|
||||||
|
* unchanged viewport. Zoom is restricted to 1..128, with focus anchoring.
|
||||||
|
* Screen fractions increase downwards; set inverted=1 for the Y value axis.
|
||||||
|
*/
|
||||||
|
PCAN_ABI_API size_t set_plot_eval(uint32_t operation, const double *input,
|
||||||
|
size_t count, double *output, size_t capacity);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
34
c/set-protocol/include/set_spectrum.h
Normal file
34
c/set-protocol/include/set_spectrum.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/** Shared host-side FFT for Android/JNI and desktop/ctypes; no GUI dependencies. */
|
||||||
|
#ifndef SET_SPECTRUM_H
|
||||||
|
#define SET_SPECTRUM_H
|
||||||
|
#include "pcan_abi.h"
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define SET_SPECTRUM_MAX 16384U
|
||||||
|
enum { SET_WINDOW_RECT, SET_WINDOW_HANN, SET_WINDOW_HAMMING, SET_WINDOW_BLACKMAN, SET_WINDOW_FLATTOP };
|
||||||
|
enum { SET_FILTER_NONE, SET_FILTER_LOW_PASS, SET_FILTER_HIGH_PASS, SET_FILTER_BAND_PASS, SET_FILTER_NOTCH };
|
||||||
|
enum { SET_SPECTRUM_OK, SET_SPECTRUM_SHORT, SET_SPECTRUM_INVALID, SET_SPECTRUM_TIMING,
|
||||||
|
SET_SPECTRUM_CUTOFF, SET_SPECTRUM_MEMORY };
|
||||||
|
|
||||||
|
/** Analyze the last power-of-two block (16 <= N <= max_size).
|
||||||
|
* times are seconds, strictly increasing; values must be finite. Fs is measured
|
||||||
|
* from timestamps, never taken from the requested acquisition rate. Interval
|
||||||
|
* deviations >50% of the mean are rejected; smaller jitter is linearly resampled.
|
||||||
|
* Filter: second-order Butterworth LP/HP, their cascade for band pass, notch Q=30.
|
||||||
|
* low_hz: HP/band lower cutoff or notch center; high_hz: LP/band upper cutoff.
|
||||||
|
* Each block starts the causal filters at steady state for its first sample.
|
||||||
|
* Processing order: resample, optional mean removal, filter, periodic window, FFT.
|
||||||
|
* Output: one-sided PEAK amplitude, normalized by window sum, DC/Nyquist not doubled.
|
||||||
|
* Caller supplies max_size/2+1 amplitudes; meta = {N, Fs, max relative jitter}.
|
||||||
|
* Inputs are immutable. Scratch memory is bounded by 2*N doubles, allocated here.
|
||||||
|
*/
|
||||||
|
PCAN_ABI_API int set_spectrum_analyze(const double *times, const double *values, size_t count,
|
||||||
|
size_t max_size, int window, int filter, double low_hz, double high_hz, int remove_mean,
|
||||||
|
double *amplitudes, size_t capacity, double *meta);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
54
c/set-protocol/include/set_trends.h
Normal file
54
c/set-protocol/include/set_trends.h
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/** @file set_trends.h
|
||||||
|
* @brief Shared GUI trend decoding and GUI v1 GAS subscription payloads.
|
||||||
|
* No transport, rendering, allocation or global state. JNI/ctypes call the
|
||||||
|
* same exported functions; flags use the existing AA55 bridge ABI.
|
||||||
|
*/
|
||||||
|
#ifndef SET_TRENDS_H
|
||||||
|
#define SET_TRENDS_H
|
||||||
|
#include "pcan_abi.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define SET_TREND_CAN_GAS 1U
|
||||||
|
#define SET_TREND_CAN_RAW 2U
|
||||||
|
#define SET_TREND_NO_VALUE INT32_MIN
|
||||||
|
#define SET_TREND_WATCH_MAX 64U
|
||||||
|
|
||||||
|
/** Convert one wire word, without implementation-defined signed casts. */
|
||||||
|
PCAN_ABI_API int32_t set_trend_word_value(uint16_t word, uint8_t is_signed);
|
||||||
|
|
||||||
|
/** Return a decoded value or SET_TREND_NO_VALUE for a nonmatching/invalid frame.
|
||||||
|
* GAS requires Route=FROM_DEVICE, exact DeviceType/DeviceID, and an even DLC.
|
||||||
|
* TX echoes, RTR and error frames are always ignored. RAW offset is in bytes.
|
||||||
|
*/
|
||||||
|
PCAN_ABI_API int32_t set_trend_can_value(
|
||||||
|
uint8_t source, uint32_t address, uint8_t device_type, uint8_t device,
|
||||||
|
uint8_t byte_offset, uint8_t extended, uint8_t is_signed,
|
||||||
|
uint32_t can_id, uint8_t flags, const uint8_t *data, size_t size);
|
||||||
|
|
||||||
|
/** Encode period:u16 + count:u16 + addresses[count]:u16, little-endian.
|
||||||
|
* A zero period/count is an unsubscribe. Returns 0 on invalid arguments.
|
||||||
|
*/
|
||||||
|
PCAN_ABI_API size_t set_trend_watch_request(
|
||||||
|
uint16_t period_ms, const uint16_t *addresses, size_t count,
|
||||||
|
uint8_t *output, size_t capacity);
|
||||||
|
|
||||||
|
/** Accept only an exact acknowledgement of the entire ordered subscription.
|
||||||
|
* The device may omit unknown addresses. Partial acceptance must NOT be used:
|
||||||
|
* its acknowledgement has no address list, so index-to-address mapping is lost.
|
||||||
|
*/
|
||||||
|
PCAN_ABI_API int set_trend_watch_ack(
|
||||||
|
const uint8_t *payload, size_t size, uint16_t period_ms, size_t count);
|
||||||
|
|
||||||
|
/** Decode timestamp:u32 + count:u16 + words. Returns count, or -1 on error.
|
||||||
|
* Timestamp is device-local; GUI plot clocks should use their own monotonic time.
|
||||||
|
*/
|
||||||
|
PCAN_ABI_API int set_trend_watch_values(
|
||||||
|
const uint8_t *payload, size_t size, uint16_t *words, size_t capacity);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
@@ -8,6 +8,10 @@ LOCAL_SRC_FILES := \
|
|||||||
../../src/set_can.c \
|
../../src/set_can.c \
|
||||||
../../src/set_firmware.c \
|
../../src/set_firmware.c \
|
||||||
../../src/set_telemetry.c \
|
../../src/set_telemetry.c \
|
||||||
|
../../src/set_plot.c \
|
||||||
|
set_plot_jni.c \
|
||||||
|
../../src/set_trends.c \
|
||||||
|
../../src/set_spectrum.c \
|
||||||
../../src/gui_catalog.c \
|
../../src/gui_catalog.c \
|
||||||
../../src/gui_frame.c \
|
../../src/gui_frame.c \
|
||||||
../../src/pcan_abi.c \
|
../../src/pcan_abi.c \
|
||||||
@@ -19,5 +23,5 @@ LOCAL_SRC_FILES := \
|
|||||||
../../src/pcan_gas.c \
|
../../src/pcan_gas.c \
|
||||||
setprotocol_jni.c
|
setprotocol_jni.c
|
||||||
LOCAL_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden
|
LOCAL_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden
|
||||||
LOCAL_LDLIBS := -llog
|
LOCAL_LDLIBS := -llog -lm
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
|
|||||||
@@ -7,4 +7,15 @@ and include this `Android.mk` from the application NDK build.
|
|||||||
|
|
||||||
The JNI streaming parser returns fixed 15-byte records
|
The JNI streaming parser returns fixed 15-byte records
|
||||||
`SEQ | FLAGS | CAN_ID_LE | DLC | DATA[8]`. Dynamic allocation is confined to
|
`SEQ | FLAGS | CAN_ID_LE | DLC | DATA[8]`. Dynamic allocation is confined to
|
||||||
the Android adapter; the portable core remains allocation free.
|
the Android adapter for protocol operations; the portable protocol core remains allocation free.
|
||||||
|
|
||||||
|
`nativeSpectrum` calls the shared host DSP module `set_spectrum.c` (bounded heap
|
||||||
|
workspace, up to 2×16384 doubles). `trends/SpectrumAnalyzer` maps timestamps and
|
||||||
|
errors but does not duplicate FFT/filter math. Run it off the UI thread.
|
||||||
|
`trends/PlotViewport` is a toolkit-free normalized zoom/pan model.
|
||||||
|
|
||||||
|
`kotlin/ru/setcorp/setprotocol/update/FirmwareCatalog.kt` is a UI-independent
|
||||||
|
firmware release client. It reads the optional `firmware.releases` array from
|
||||||
|
the shared `update.json`, accepts only HTTPS assets, limits their size and
|
||||||
|
returns image bytes only after SHA-256 verification. Applications provide
|
||||||
|
their own UI and pass optional HTTP Basic credentials.
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ object MessageType {
|
|||||||
const val FIRMWARE_END = 0x0D
|
const val FIRMWARE_END = 0x0D
|
||||||
const val FIRMWARE_ABORT = 0x0E
|
const val FIRMWARE_ABORT = 0x0E
|
||||||
const val FIRMWARE_STATUS = 0x0F
|
const val FIRMWARE_STATUS = 0x0F
|
||||||
|
const val GAS_CATALOG = 0x11
|
||||||
|
const val GAS_WATCH_SET = 0x12
|
||||||
|
const val GAS_WATCH_DATA = 0x13
|
||||||
const val SENSOR_SCAN = 0x20
|
const val SENSOR_SCAN = 0x20
|
||||||
const val SENSOR_LIST = 0x21
|
const val SENSOR_LIST = 0x21
|
||||||
const val SENSOR_READ = 0x22
|
const val SENSOR_READ = 0x22
|
||||||
@@ -33,6 +36,9 @@ object MessageType {
|
|||||||
FIRMWARE_END -> "FIRMWARE_END"
|
FIRMWARE_END -> "FIRMWARE_END"
|
||||||
FIRMWARE_ABORT -> "FIRMWARE_ABORT"
|
FIRMWARE_ABORT -> "FIRMWARE_ABORT"
|
||||||
FIRMWARE_STATUS -> "FIRMWARE_STATUS"
|
FIRMWARE_STATUS -> "FIRMWARE_STATUS"
|
||||||
|
GAS_CATALOG -> "GAS_CATALOG"
|
||||||
|
GAS_WATCH_SET -> "GAS_WATCH_SET"
|
||||||
|
GAS_WATCH_DATA -> "GAS_WATCH_DATA"
|
||||||
SENSOR_SCAN -> "SENSOR_SCAN"
|
SENSOR_SCAN -> "SENSOR_SCAN"
|
||||||
SENSOR_LIST -> "SENSOR_LIST"
|
SENSOR_LIST -> "SENSOR_LIST"
|
||||||
SENSOR_READ -> "SENSOR_READ"
|
SENSOR_READ -> "SENSOR_READ"
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ object NativeSetProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
external fun nativeAbiVersion(): Int
|
external fun nativeAbiVersion(): Int
|
||||||
|
/** {status, N, Fs, jitter, amplitudes...}; status != 0 has no amplitudes. */
|
||||||
|
external fun nativeSpectrum(times: DoubleArray, values: DoubleArray, maxSize: Int,
|
||||||
|
window: Int, filter: Int, lowHz: Double, highHz: Double, removeMean: Boolean): DoubleArray?
|
||||||
|
external fun nativeTrendCanValue(
|
||||||
|
source: Int, address: Long, deviceType: Int, device: Int, byteOffset: Int,
|
||||||
|
extended: Boolean, signed: Boolean, canId: Long, flags: Int, input: ByteArray,
|
||||||
|
): Int
|
||||||
|
external fun nativeTrendWatchRequest(period: Int, addresses: IntArray): ByteArray?
|
||||||
|
external fun nativeTrendWatchValues(input: ByteArray): IntArray?
|
||||||
external fun nativePackId(
|
external fun nativePackId(
|
||||||
priority: Int,
|
priority: Int,
|
||||||
route: Int,
|
route: Int,
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package ru.setcorp.setprotocol.trends
|
||||||
|
|
||||||
|
import ru.setcorp.setprotocol.NativeSetProtocol
|
||||||
|
|
||||||
|
/** GUI v1 GAS contract, also exposed by python/protocan/gas_catalog.py. */
|
||||||
|
object GuiGasWatch {
|
||||||
|
const val SET = 0x12
|
||||||
|
const val DATA = 0x13
|
||||||
|
const val PERIOD_MS = 1000
|
||||||
|
|
||||||
|
fun request(periodMs: Int, addresses: List<Int>): ByteArray {
|
||||||
|
require(periodMs in 0..65535 && addresses.size <= 64 && addresses.all { it in 0..65535 })
|
||||||
|
if (NativeSetProtocol.available) return requireNotNull(NativeSetProtocol.nativeTrendWatchRequest(periodMs, addresses.toIntArray()))
|
||||||
|
return ByteArray(4 + addresses.size * 2).also { output ->
|
||||||
|
put16(output, 0, periodMs)
|
||||||
|
put16(output, 2, addresses.size)
|
||||||
|
addresses.forEachIndexed { index, address -> put16(output, 4 + index * 2, address) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateAck(payload: ByteArray, periodMs: Int, count: Int) {
|
||||||
|
require(payload.size == 4 && read16(payload, 0) == periodMs && read16(payload, 2) == count) {
|
||||||
|
"Прибор принял не все адреса GAS. Проверьте карту регистров; отображение по неполной подписке невозможно"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun values(payload: ByteArray, expectedCount: Int): List<Int> {
|
||||||
|
val values = if (NativeSetProtocol.available) {
|
||||||
|
requireNotNull(NativeSetProtocol.nativeTrendWatchValues(payload)) { "Повреждён GAS_WATCH_DATA" }.toList()
|
||||||
|
} else {
|
||||||
|
require(payload.size >= 6) { "GAS_WATCH_DATA короче заголовка" }
|
||||||
|
val count = read16(payload, 4)
|
||||||
|
require(count <= 64 && payload.size == 6 + count * 2) { "Неверная длина GAS_WATCH_DATA" }
|
||||||
|
List(count) { read16(payload, 6 + it * 2) }
|
||||||
|
}
|
||||||
|
require(values.size == expectedCount) { "Число значений GAS не соответствует подписке" }
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun read16(data: ByteArray, offset: Int): Int =
|
||||||
|
(data[offset].toInt() and 0xFF) or ((data[offset + 1].toInt() and 0xFF) shl 8)
|
||||||
|
private fun put16(data: ByteArray, offset: Int, value: Int) {
|
||||||
|
data[offset] = value.toByte()
|
||||||
|
data[offset + 1] = (value ushr 8).toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package ru.setcorp.setprotocol.trends
|
||||||
|
|
||||||
|
/** Shared C99 plot math, also used by the Python/Qt port. No UI dependency. */
|
||||||
|
internal object NativePlot {
|
||||||
|
init {
|
||||||
|
val hostLibrary = System.getProperty("setplot.library")
|
||||||
|
if (hostLibrary != null) System.load(hostLibrary) else System.loadLibrary("setprotocol")
|
||||||
|
}
|
||||||
|
private external fun evaluate(operation: Int, input: DoubleArray): DoubleArray?
|
||||||
|
fun call(operation: Int, vararg input: Double): DoubleArray =
|
||||||
|
requireNotNull(evaluate(operation, input)) { "Invalid plot operation $operation" }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PlotAxis { X, Y }
|
||||||
|
|
||||||
|
fun plotPinchAxis(deltaX: Double, deltaY: Double, slop: Double): PlotAxis? =
|
||||||
|
when (NativePlot.call(1, deltaX, deltaY, slop)[0].toInt()) {
|
||||||
|
1 -> PlotAxis.X; 2 -> PlotAxis.Y; else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PlotBounds(val left: Double, val right: Double, val bottom: Double, val top: Double) {
|
||||||
|
fun fraction(value: Double, horizontal: Boolean): Double = NativePlot.call(2, value,
|
||||||
|
if (horizontal) bottom else left, if (horizontal) top else right, if (horizontal) 1.0 else 0.0)[0]
|
||||||
|
fun value(fraction: Double, horizontal: Boolean): Double = NativePlot.call(3, fraction,
|
||||||
|
if (horizontal) bottom else left, if (horizontal) top else right, if (horizontal) 1.0 else 0.0)[0]
|
||||||
|
fun drag(initial: Double, delta: Double, length: Double, horizontal: Boolean): Double = NativePlot.call(4,
|
||||||
|
initial, delta, length, if (horizontal) bottom else left, if (horizontal) top else right,
|
||||||
|
if (horizontal) 1.0 else 0.0)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun plotTickStep(range: Double, pixels: Double): Double = NativePlot.call(5, range, pixels)[0]
|
||||||
|
fun plotDelta(a: Double, b: Double, multiplier: Double = 1.0): Double = NativePlot.call(6, a, b, multiplier)[0]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package ru.setcorp.setprotocol.trends
|
||||||
|
|
||||||
|
/** Normalized top-left viewport; independent of pixels, units, toolkit and samples. */
|
||||||
|
data class PlotViewport(val x: Double = 0.0, val y: Double = 0.0, val width: Double = 1.0, val height: Double = 1.0) {
|
||||||
|
fun transform(zoomX: Double = 1.0, zoomY: Double = 1.0, panX: Double = 0.0, panY: Double = 0.0,
|
||||||
|
focusX: Double = 0.5, focusY: Double = 0.5): PlotViewport {
|
||||||
|
val result = NativePlot.call(0, x, y, width, height, zoomX, zoomY, panX, panY, focusX, focusY)
|
||||||
|
return PlotViewport(result[0], result[1], result[2], result[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package ru.setcorp.setprotocol.trends
|
||||||
|
|
||||||
|
import ru.setcorp.setprotocol.NativeSetProtocol
|
||||||
|
|
||||||
|
/** Shared JNI decoder with a JVM-only reference fallback for tests/source builds. */
|
||||||
|
object TrendDecoder {
|
||||||
|
fun canValue(signal: TrendSignal, canId: Long, flags: Int, data: ByteArray): Double? {
|
||||||
|
if (NativeSetProtocol.available && signal.source in setOf(TrendSource.CAN_GAS, TrendSource.CAN_RAW)) {
|
||||||
|
val address = parseTrendAddress(signal.address, if (signal.source == TrendSource.CAN_GAS) 0xFFFF else 0x1FFF_FFFF) ?: return null
|
||||||
|
val value = NativeSetProtocol.nativeTrendCanValue(
|
||||||
|
if (signal.source == TrendSource.CAN_GAS) 1 else 2, address, signal.deviceType, signal.device,
|
||||||
|
signal.byteOffset, signal.extended, signal.valueType == TrendValueType.INT16,
|
||||||
|
canId, flags, data,
|
||||||
|
)
|
||||||
|
return value.takeIf { it != Int.MIN_VALUE }?.toDouble()
|
||||||
|
}
|
||||||
|
if (flags and 0x0E != 0 || data.size !in 2..8) return null
|
||||||
|
val extended = flags and 1 != 0
|
||||||
|
if (canId !in 0..(if (extended) 0x1FFF_FFFFL else 0x7FFL)) return null
|
||||||
|
val offset = when (signal.source) {
|
||||||
|
TrendSource.CAN_GAS -> {
|
||||||
|
if (!extended || data.size < 2 || data.size % 2 != 0) return null
|
||||||
|
if ((canId ushr 16) and 15L != 3L || (canId ushr 27) and 1L != 1L ||
|
||||||
|
((canId ushr 24) and 7L).toInt() != signal.deviceType ||
|
||||||
|
((canId ushr 20) and 15L).toInt() != signal.device) return null
|
||||||
|
val address = parseTrendAddress(signal.address, 0xFFFF)?.toInt() ?: return null
|
||||||
|
(address - (canId and 0xFFFF).toInt()) * 2
|
||||||
|
}
|
||||||
|
TrendSource.CAN_RAW -> {
|
||||||
|
if (extended != signal.extended || canId != parseTrendAddress(signal.address, 0x1FFF_FFFF)) return null
|
||||||
|
signal.byteOffset
|
||||||
|
}
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
if (offset < 0 || offset + 2 > data.size) return null
|
||||||
|
val word = (data[offset].toInt() and 0xFF) or ((data[offset + 1].toInt() and 0xFF) shl 8)
|
||||||
|
return signal.valueType.decode(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package ru.setcorp.setprotocol.trends
|
||||||
|
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
enum class TrendMarker(val title: String, val horizontal: Boolean) {
|
||||||
|
A("A", false), B("B", false), C("C", false), D("D", false),
|
||||||
|
E("E", true), F("F", true), G("G", true), H("H", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TrendMarkers(
|
||||||
|
val xEnabled: Boolean = true,
|
||||||
|
val yEnabled: Boolean = false,
|
||||||
|
val selected: TrendMarker = TrendMarker.A,
|
||||||
|
val a: Double? = null,
|
||||||
|
val b: Double? = null,
|
||||||
|
val c: Double? = null,
|
||||||
|
val d: Double? = null,
|
||||||
|
val e: Double? = null,
|
||||||
|
val f: Double? = null,
|
||||||
|
val g: Double? = null,
|
||||||
|
val h: Double? = null,
|
||||||
|
) {
|
||||||
|
fun value(marker: TrendMarker): Double? = when (marker) {
|
||||||
|
TrendMarker.A -> a; TrendMarker.B -> b; TrendMarker.C -> c; TrendMarker.D -> d
|
||||||
|
TrendMarker.E -> e; TrendMarker.F -> f; TrendMarker.G -> g; TrendMarker.H -> h
|
||||||
|
}
|
||||||
|
fun move(marker: TrendMarker, value: Double): TrendMarkers = when (marker) {
|
||||||
|
TrendMarker.A -> copy(a = value); TrendMarker.B -> copy(b = value)
|
||||||
|
TrendMarker.C -> copy(c = value); TrendMarker.D -> copy(d = value)
|
||||||
|
TrendMarker.E -> copy(e = value); TrendMarker.F -> copy(f = value)
|
||||||
|
TrendMarker.G -> copy(g = value); TrendMarker.H -> copy(h = value)
|
||||||
|
}
|
||||||
|
fun positioned(bounds: PlotBounds): TrendMarkers = copy(
|
||||||
|
a = a ?: bounds.value(0.2, false), b = b ?: bounds.value(0.4, false),
|
||||||
|
c = c ?: bounds.value(0.6, false), d = d ?: bounds.value(0.8, false),
|
||||||
|
e = e ?: bounds.value(0.2, true), f = f ?: bounds.value(0.4, true),
|
||||||
|
g = g ?: bounds.value(0.6, true), h = h ?: bounds.value(0.8, true))
|
||||||
|
fun reset(bounds: PlotBounds): TrendMarkers = copy(
|
||||||
|
a = null, b = null, c = null, d = null, e = null, f = null, g = null, h = null).positioned(bounds)
|
||||||
|
fun fraction(marker: TrendMarker, bounds: PlotBounds): Double = bounds.fraction(requireNotNull(value(marker)), marker.horizontal)
|
||||||
|
fun drag(marker: TrendMarker, delta: Double, length: Double, bounds: PlotBounds): TrendMarkers =
|
||||||
|
move(marker, bounds.drag(requireNotNull(value(marker)), delta, length, marker.horizontal)).copy(selected = marker)
|
||||||
|
fun hit(x: Double, y: Double, width: Double, height: Double, radius: Double, bounds: PlotBounds): TrendMarker? {
|
||||||
|
if (x !in 0.0..width || y !in 0.0..height) return null
|
||||||
|
return TrendMarker.entries.filter { enabled(it) && value(it) != null }.mapNotNull { marker ->
|
||||||
|
val fraction = fraction(marker, bounds)
|
||||||
|
val distance = abs(if (marker.horizontal) y - fraction * height else x - fraction * width)
|
||||||
|
if (fraction in 0.0..1.0 && distance <= radius) marker to distance else null
|
||||||
|
}.sortedWith(compareBy<Pair<TrendMarker, Double>> { it.second }
|
||||||
|
.thenBy { if (it.first == selected) 0 else 1 }).firstOrNull()?.first
|
||||||
|
}
|
||||||
|
fun enabled(marker: TrendMarker): Boolean = if (marker.horizontal) yEnabled else xEnabled
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package ru.setcorp.setprotocol.trends
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
enum class TrendSection { SIGNALS, CHART }
|
||||||
|
|
||||||
|
/** File-format IDs; host applications map their connection profiles to these IDs. */
|
||||||
|
enum class TrendProfile(val title: String) {
|
||||||
|
SET_V1("SET GUI v1"), TMS2812("TMS320F2812 / BALZAM"), CAN_BRIDGE("CAN ↔ RS485"),
|
||||||
|
GS_USB_CAN("CANgaroo / gs_usb"), SLCAN("SKLab SLCAN"), CANGAROO_SLCAN("CANgaroo / SLCAN"),
|
||||||
|
BALZAM_CAN("Старый CAN BALZAM"),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TrendSource(val title: String) {
|
||||||
|
TMS_MEMORY("Память TMS · CMD_PEEK"),
|
||||||
|
CAN_GAS("Регистр GAS · ProtoCAN"),
|
||||||
|
CAN_RAW("Слово из CAN-кадра"),
|
||||||
|
SET_SENSOR("Температура DS18B20"),
|
||||||
|
SET_GAS("Регистр GAS · SET GUI"),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TrendValueType(val title: String) {
|
||||||
|
UINT16("UInt16 · 0…65535"), INT16("Int16 · −32768…32767");
|
||||||
|
|
||||||
|
fun decode(word: Int): Double = when (this) {
|
||||||
|
UINT16 -> word.toDouble()
|
||||||
|
INT16 -> word.toShort().toDouble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TrendProfile.trendSources(): List<TrendSource> = when (this) {
|
||||||
|
TrendProfile.TMS2812 -> listOf(TrendSource.TMS_MEMORY)
|
||||||
|
TrendProfile.SET_V1 -> listOf(TrendSource.SET_GAS, TrendSource.SET_SENSOR)
|
||||||
|
TrendProfile.BALZAM_CAN -> listOf(TrendSource.CAN_RAW)
|
||||||
|
else -> listOf(TrendSource.CAN_GAS, TrendSource.CAN_RAW)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stable IDs keep samples attached to a signal when its name/order changes. */
|
||||||
|
data class TrendSignal(
|
||||||
|
val id: String = UUID.randomUUID().toString(),
|
||||||
|
val order: Int = 1,
|
||||||
|
val name: String = "Тренд 1",
|
||||||
|
val source: TrendSource = TrendSource.TMS_MEMORY,
|
||||||
|
val address: String = "0x00000100",
|
||||||
|
val color: String = "#2F91FF",
|
||||||
|
val visible: Boolean = true,
|
||||||
|
val valueType: TrendValueType = TrendValueType.UINT16,
|
||||||
|
val deviceType: Int = 7,
|
||||||
|
val device: Int = 13,
|
||||||
|
val byteOffset: Int = 0,
|
||||||
|
val multiplier: Double = 1.0,
|
||||||
|
val iq: Int = 0,
|
||||||
|
val extended: Boolean = true,
|
||||||
|
) {
|
||||||
|
fun validationError(profile: TrendProfile): String? = when {
|
||||||
|
id.isBlank() || id.length > 80 -> "Неверный идентификатор тренда"
|
||||||
|
order !in 1..9999 -> "Номер должен быть от 1 до 9999"
|
||||||
|
name.isBlank() || name.length > 100 -> "Имя должно содержать от 1 до 100 символов"
|
||||||
|
address.length > 64 -> "Адрес слишком длинный"
|
||||||
|
source !in profile.trendSources() -> "Источник несовместим с протоколом ${profile.title}"
|
||||||
|
!color.matches(Regex("#[0-9a-fA-F]{6}")) -> "Цвет задаётся как #RRGGBB"
|
||||||
|
source == TrendSource.SET_SENSOR -> if (normalizeRom(address).matches(Regex("[0-9A-F]{16}"))) null
|
||||||
|
else "ROM датчика должен содержать 16 HEX-цифр"
|
||||||
|
source == TrendSource.TMS_MEMORY && parseTrendAddress(address, 0xFFFF_FFFFL) == null ->
|
||||||
|
"Адрес памяти: 0…0xFFFFFFFF"
|
||||||
|
source in setOf(TrendSource.CAN_GAS, TrendSource.SET_GAS) && parseTrendAddress(address, 0xFFFF) == null ->
|
||||||
|
"Адрес GAS: 0…0xFFFF"
|
||||||
|
source == TrendSource.CAN_GAS && (deviceType !in 0..7 || device !in 0..15) ->
|
||||||
|
"Тип устройства: 0…7, номер устройства: 0…15"
|
||||||
|
source == TrendSource.CAN_RAW && parseTrendAddress(address, if (extended) 0x1FFF_FFFF else 0x7FF) == null ->
|
||||||
|
"CAN ID вне диапазона ${if (extended) "29" else "11"} бит"
|
||||||
|
source == TrendSource.CAN_RAW && byteOffset !in 0..6 -> "Смещение слова: 0…6 байт"
|
||||||
|
!multiplier.isFinite() -> "Множитель должен быть конечным числом"
|
||||||
|
iq !in 0..30 -> "IQ должен быть в диапазоне 0…30"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayValue(rawValue: Double): Double =
|
||||||
|
rawValue * multiplier / (1L shl iq).toDouble()
|
||||||
|
|
||||||
|
/** Only source changes invalidate the historical series. */
|
||||||
|
fun sameInput(other: TrendSignal): Boolean =
|
||||||
|
copy(
|
||||||
|
order = other.order,
|
||||||
|
name = other.name,
|
||||||
|
color = other.color,
|
||||||
|
visible = other.visible,
|
||||||
|
multiplier = other.multiplier,
|
||||||
|
iq = other.iq,
|
||||||
|
) == other
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val COLORS = listOf("#2F91FF", "#FFB547", "#52D6A4", "#F8798D", "#B79AFF", "#4AD9E8", "#E8DA68", "#E7ECF3")
|
||||||
|
fun new(profile: TrendProfile, existing: List<TrendSignal>): TrendSignal {
|
||||||
|
val order = (1..9999).first { number -> existing.none { it.order == number } }
|
||||||
|
val source = profile.trendSources().first()
|
||||||
|
return TrendSignal(
|
||||||
|
order = order, name = "Тренд $order", source = source,
|
||||||
|
address = when (source) {
|
||||||
|
TrendSource.TMS_MEMORY -> "0x00000100"
|
||||||
|
TrendSource.CAN_GAS -> "0x0000"
|
||||||
|
TrendSource.SET_GAS -> "0x0000"
|
||||||
|
TrendSource.CAN_RAW -> "0x00BA0010"
|
||||||
|
TrendSource.SET_SENSOR -> ""
|
||||||
|
},
|
||||||
|
color = COLORS[(order - 1) % COLORS.size],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseTrendAddress(value: String, max: Long): Long? {
|
||||||
|
val text = value.trim()
|
||||||
|
val hex = text.startsWith("0x", ignoreCase = true)
|
||||||
|
val digits = if (hex) text.drop(2) else text
|
||||||
|
if (digits.isEmpty() || !digits.all { if (hex) it in "0123456789abcdefABCDEF" else it in '0'..'9' }) return null
|
||||||
|
return digits.toLongOrNull(if (hex) 16 else 10)?.takeIf { it in 0..max }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun normalizeRom(value: String): String = value.trim().replace("-", "").replace(" ", "").uppercase()
|
||||||
|
|
||||||
|
data class TrendPoint(
|
||||||
|
val timestampMs: Long,
|
||||||
|
val value: Double,
|
||||||
|
/** Nanoseconds inside [timestampMs], preserving spacing above 1 kHz without overflowing epoch nanoseconds. */
|
||||||
|
val subMillisecondNanos: Int = 0,
|
||||||
|
) {
|
||||||
|
init { require(subMillisecondNanos in 0..999_999) }
|
||||||
|
fun nanosSince(other: TrendPoint): Long =
|
||||||
|
(timestampMs - other.timestampMs) * 1_000_000L + subMillisecondNanos - other.subMillisecondNanos
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TrendUiState(
|
||||||
|
val section: TrendSection = TrendSection.SIGNALS,
|
||||||
|
val settings: Map<TrendProfile, List<TrendSignal>> = emptyMap(),
|
||||||
|
val history: Map<String, List<TrendPoint>> = emptyMap(),
|
||||||
|
val running: Boolean = false,
|
||||||
|
val status: String = "Добавьте сигналы и откройте вкладку «График»",
|
||||||
|
val fileBusy: Boolean = false,
|
||||||
|
) {
|
||||||
|
fun signals(profile: TrendProfile): List<TrendSignal> = settings[profile].orEmpty().sortedBy { it.order }
|
||||||
|
|
||||||
|
fun append(values: Map<String, Double>, timestampMs: Long): TrendUiState {
|
||||||
|
if (!running) return this
|
||||||
|
val next = history.toMutableMap()
|
||||||
|
values.forEach { (id, value) ->
|
||||||
|
if (value.isFinite()) next[id] = (next[id].orEmpty() + TrendPoint(timestampMs, value)).takeLast(MAX_POINTS)
|
||||||
|
}
|
||||||
|
return copy(history = next)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_SIGNALS = 64
|
||||||
|
const val MAX_POINTS = 600
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package ru.setcorp.setprotocol.trends
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/** Versioned, human-readable configuration. Import is validated before replacing any state. */
|
||||||
|
object TrendSettingsJson {
|
||||||
|
const val MAX_FILE_BYTES = 1024 * 1024
|
||||||
|
|
||||||
|
fun encode(settings: Map<TrendProfile, List<TrendSignal>>): String {
|
||||||
|
validate(settings)
|
||||||
|
val profiles = JSONObject()
|
||||||
|
settings.forEach { (profile, signals) ->
|
||||||
|
profiles.put(profile.name, JSONArray().apply {
|
||||||
|
signals.sortedBy { it.order }.forEach { signal ->
|
||||||
|
put(JSONObject().apply {
|
||||||
|
put("id", signal.id)
|
||||||
|
put("order", signal.order)
|
||||||
|
put("name", signal.name)
|
||||||
|
put("source", signal.source.name)
|
||||||
|
put("address", signal.address)
|
||||||
|
put("color", signal.color)
|
||||||
|
put("visible", signal.visible)
|
||||||
|
put("valueType", signal.valueType.name)
|
||||||
|
put("deviceType", signal.deviceType)
|
||||||
|
put("device", signal.device)
|
||||||
|
put("byteOffset", signal.byteOffset)
|
||||||
|
put("multiplier", signal.multiplier)
|
||||||
|
put("iq", signal.iq)
|
||||||
|
put("extended", signal.extended)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return JSONObject().put("format", "setflash-trends").put("version", 1).put("profiles", profiles).toString(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decode(text: String): Map<TrendProfile, List<TrendSignal>> {
|
||||||
|
require(text.toByteArray(Charsets.UTF_8).size <= MAX_FILE_BYTES) { "Файл настроек больше 1 МБ" }
|
||||||
|
val root = JSONObject(text.removePrefix("\uFEFF"))
|
||||||
|
require(root.strictString("format") == "setflash-trends" && root.strictInt("version") == 1) {
|
||||||
|
"Неизвестный формат или версия настроек графиков"
|
||||||
|
}
|
||||||
|
val profiles = root.getJSONObject("profiles")
|
||||||
|
val result = profiles.keys().asSequence().associate { key ->
|
||||||
|
val profile = TrendProfile.entries.firstOrNull { it.name == key }
|
||||||
|
?: error("Неизвестный протокол: $key")
|
||||||
|
val array = profiles.getJSONArray(key)
|
||||||
|
require(array.length() <= TrendUiState.MAX_SIGNALS) { "Не более 64 сигналов на протокол" }
|
||||||
|
profile to (0 until array.length()).map { index ->
|
||||||
|
val item = array.getJSONObject(index)
|
||||||
|
TrendSignal(
|
||||||
|
id = item.strictString("id"), order = item.strictInt("order"), name = item.strictString("name"),
|
||||||
|
source = TrendSource.valueOf(item.strictString("source")), address = item.strictString("address"),
|
||||||
|
color = item.strictString("color"), visible = item.strictBoolean("visible"),
|
||||||
|
valueType = TrendValueType.valueOf(item.strictString("valueType")),
|
||||||
|
deviceType = item.strictInt("deviceType"), device = item.strictInt("device"),
|
||||||
|
byteOffset = item.strictInt("byteOffset"),
|
||||||
|
multiplier = if (item.has("multiplier")) item.strictDouble("multiplier") else 1.0,
|
||||||
|
iq = if (item.has("iq")) item.strictInt("iq") else 0,
|
||||||
|
extended = item.strictBoolean("extended"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validate(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JSONObject.strictString(key: String): String =
|
||||||
|
get(key).let { require(it is String) { "$key должен быть строкой" }; it }
|
||||||
|
|
||||||
|
private fun JSONObject.strictBoolean(key: String): Boolean =
|
||||||
|
get(key).let { require(it is Boolean) { "$key должен быть логическим флагом" }; it }
|
||||||
|
|
||||||
|
private fun JSONObject.strictInt(key: String): Int {
|
||||||
|
val value = get(key)
|
||||||
|
require(value is Int || value is Long) { "$key должен быть целым числом" }
|
||||||
|
val number = (value as Number).toLong()
|
||||||
|
require(number in Int.MIN_VALUE..Int.MAX_VALUE) { "$key вне диапазона" }
|
||||||
|
return number.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JSONObject.strictDouble(key: String): Double {
|
||||||
|
val value = get(key)
|
||||||
|
require(value is Number) { "$key должен быть числом" }
|
||||||
|
return value.toDouble().also { require(it.isFinite()) { "$key должен быть конечным числом" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validate(settings: Map<TrendProfile, List<TrendSignal>>) {
|
||||||
|
val allIds = settings.values.flatten().map { it.id }
|
||||||
|
require(allIds.distinct().size == allIds.size) { "Идентификаторы трендов не должны повторяться" }
|
||||||
|
settings.forEach { (profile, signals) ->
|
||||||
|
require(signals.size <= TrendUiState.MAX_SIGNALS) { "Не более 64 сигналов на протокол" }
|
||||||
|
require(signals.map { it.order }.distinct().size == signals.size) { "Номера трендов одного протокола должны различаться" }
|
||||||
|
signals.forEach { signal ->
|
||||||
|
require(signal.validationError(profile) == null) { "${signal.name}: ${signal.validationError(profile)}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package ru.setcorp.setprotocol.trends
|
||||||
|
|
||||||
|
import ru.setcorp.setprotocol.NativeSetProtocol
|
||||||
|
|
||||||
|
/** Enum order is the stable set_spectrum.h ABI; titles belong to this GUI adapter. */
|
||||||
|
enum class SpectrumWindow(val title: String) {
|
||||||
|
RECT("Прямоугольное"), HANN("Hann"), HAMMING("Hamming"), BLACKMAN("Blackman"), FLATTOP("Flat Top"),
|
||||||
|
}
|
||||||
|
enum class SpectrumFilter(val title: String) {
|
||||||
|
NONE("Без фильтра"), LOW_PASS("ФНЧ"), HIGH_PASS("ФВЧ"), BAND_PASS("Полосовой"), NOTCH("Режекторный"),
|
||||||
|
}
|
||||||
|
data class SpectrumOptions(
|
||||||
|
val window: SpectrumWindow = SpectrumWindow.HANN,
|
||||||
|
val filter: SpectrumFilter = SpectrumFilter.NONE,
|
||||||
|
val lowHz: Double = 10.0,
|
||||||
|
val highHz: Double = 100.0,
|
||||||
|
val removeMean: Boolean = true,
|
||||||
|
val maxSize: Int = 4096,
|
||||||
|
) {
|
||||||
|
fun validate() {
|
||||||
|
require(maxSize in 16..16384 && maxSize and (maxSize - 1) == 0) { "Размер FFT: степень 2 от 16 до 16384" }
|
||||||
|
if (filter in listOf(SpectrumFilter.HIGH_PASS, SpectrumFilter.BAND_PASS, SpectrumFilter.NOTCH))
|
||||||
|
require(lowHz.isFinite() && lowHz > 0) { "Частота должна быть больше 0" }
|
||||||
|
if (filter in listOf(SpectrumFilter.LOW_PASS, SpectrumFilter.BAND_PASS))
|
||||||
|
require(highHz.isFinite() && highHz > 0) { "Частота должна быть больше 0" }
|
||||||
|
if (filter == SpectrumFilter.BAND_PASS) require(lowHz < highHz) { "Нижняя частота должна быть меньше верхней" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TrendSpectrum(
|
||||||
|
val size: Int = 0, val sampleRate: Double = 0.0, val jitter: Double = 0.0,
|
||||||
|
val amplitudes: List<Double> = emptyList(), val error: String? = null,
|
||||||
|
) {
|
||||||
|
val binHz: Double get() = if (size > 0) sampleRate / size else 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Math is implemented once in C and used unchanged by JNI and ctypes. */
|
||||||
|
object SpectrumAnalyzer {
|
||||||
|
fun analyze(points: List<TrendPoint>, options: SpectrumOptions): TrendSpectrum {
|
||||||
|
options.validate()
|
||||||
|
if (points.size < 16) return TrendSpectrum(error = "Нужно минимум 16 точек")
|
||||||
|
if (!NativeSetProtocol.available) return TrendSpectrum(error = "Модуль FFT недоступен")
|
||||||
|
val count = minOf(points.size, options.maxSize)
|
||||||
|
val start = points.size - count
|
||||||
|
val origin = points[start]
|
||||||
|
val times = DoubleArray(count) { points[start + it].nanosSince(origin) / 1_000_000_000.0 }
|
||||||
|
val values = DoubleArray(count) { points[start + it].value }
|
||||||
|
val output = NativeSetProtocol.nativeSpectrum(times, values, options.maxSize,
|
||||||
|
options.window.ordinal, options.filter.ordinal, options.lowHz, options.highHz, options.removeMean)
|
||||||
|
?: return TrendSpectrum(error = "Не удалось вычислить FFT")
|
||||||
|
return decode(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun decode(output: DoubleArray): TrendSpectrum {
|
||||||
|
require(output.size >= 4)
|
||||||
|
val error = when (output[0].toInt()) {
|
||||||
|
0 -> null
|
||||||
|
1 -> "Нужно минимум 16 точек"
|
||||||
|
3 -> "Разрывы или неравномерные метки времени: выберите другой участок"
|
||||||
|
4 -> "Частота фильтра должна быть ниже Fs/2 = %.3f Гц".format(java.util.Locale.ROOT, output[2] / 2)
|
||||||
|
5 -> "Недостаточно памяти для FFT"
|
||||||
|
else -> "Некорректные данные FFT"
|
||||||
|
}
|
||||||
|
return TrendSpectrum(output[1].toInt(), output[2], output[3],
|
||||||
|
if (error == null) output.drop(4) else emptyList(), error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package ru.setcorp.setprotocol.update
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
/** Учётные данные HTTP Basic для закрытого каталога релизов. */
|
||||||
|
data class FirmwareCatalogCredentials(val login: String, val password: String)
|
||||||
|
|
||||||
|
/** Одна проверенная запись секции `firmware.releases` общего update.json. */
|
||||||
|
data class FirmwareRelease(
|
||||||
|
val product: String,
|
||||||
|
val versionName: String,
|
||||||
|
val versionCode: Long,
|
||||||
|
val imageUrl: String,
|
||||||
|
val fileName: String,
|
||||||
|
val sha256: String,
|
||||||
|
val notes: String,
|
||||||
|
val transport: String,
|
||||||
|
val baseAddress: Long?,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переиспользуемый клиент базы прошивок для Android-приложений SET.
|
||||||
|
*
|
||||||
|
* Не зависит от UI: читает общий manifest, валидирует записи, загружает образ
|
||||||
|
* только по HTTPS и возвращает байты после обязательной проверки SHA-256.
|
||||||
|
*/
|
||||||
|
class FirmwareCatalogClient(private val userAgent: String) {
|
||||||
|
fun readCatalog(manifestUrl: String, credentials: FirmwareCatalogCredentials?): List<FirmwareRelease> {
|
||||||
|
val connection = openChecked(URL(manifestUrl), "application/json", credentials)
|
||||||
|
val text = try {
|
||||||
|
connection.inputStream.bufferedReader().use { reader ->
|
||||||
|
reader.readText().also {
|
||||||
|
require(it.length <= MAX_MANIFEST_CHARS) { "Файл каталога прошивок слишком большой" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
connection.disconnect()
|
||||||
|
}
|
||||||
|
return parse(manifestUrl, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun download(
|
||||||
|
release: FirmwareRelease,
|
||||||
|
credentials: FirmwareCatalogCredentials?,
|
||||||
|
onProgress: (Float) -> Unit = {},
|
||||||
|
): ByteArray {
|
||||||
|
val connection = openChecked(URL(release.imageUrl), "application/octet-stream", credentials)
|
||||||
|
val total = connection.contentLengthLong
|
||||||
|
require(total < 0 || total <= MAX_FIRMWARE_BYTES) { "Файл прошивки превышает допустимый размер" }
|
||||||
|
return try {
|
||||||
|
val bytes = connection.inputStream.use { input ->
|
||||||
|
val output = java.io.ByteArrayOutputStream()
|
||||||
|
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
var received = 0L
|
||||||
|
while (true) {
|
||||||
|
val count = input.read(buffer)
|
||||||
|
if (count < 0) break
|
||||||
|
received += count
|
||||||
|
require(received <= MAX_FIRMWARE_BYTES) { "Файл прошивки превышает допустимый размер" }
|
||||||
|
output.write(buffer, 0, count)
|
||||||
|
if (total > 0) onProgress((received.toFloat() / total).coerceIn(0f, 1f))
|
||||||
|
}
|
||||||
|
output.toByteArray()
|
||||||
|
}
|
||||||
|
require(bytes.isNotEmpty()) { "Сервер вернул пустой файл прошивки" }
|
||||||
|
val actual = MessageDigest.getInstance("SHA-256").digest(bytes).toHex()
|
||||||
|
require(actual.equals(release.sha256, ignoreCase = true)) {
|
||||||
|
"SHA-256 загруженной прошивки не совпадает с каталогом"
|
||||||
|
}
|
||||||
|
onProgress(1f)
|
||||||
|
bytes
|
||||||
|
} finally {
|
||||||
|
connection.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openChecked(
|
||||||
|
initial: URL,
|
||||||
|
accept: String,
|
||||||
|
credentials: FirmwareCatalogCredentials?,
|
||||||
|
): HttpURLConnection {
|
||||||
|
var current = initial.requireHttps()
|
||||||
|
val credentialHost = current.host
|
||||||
|
for (redirect in 0..MAX_REDIRECTS) {
|
||||||
|
val connection = current.openConnection() as HttpURLConnection
|
||||||
|
connection.connectTimeout = CONNECT_TIMEOUT_MS
|
||||||
|
connection.readTimeout = READ_TIMEOUT_MS
|
||||||
|
connection.instanceFollowRedirects = false
|
||||||
|
connection.setRequestProperty("Accept", accept)
|
||||||
|
connection.setRequestProperty("User-Agent", userAgent)
|
||||||
|
if (credentials != null && current.host.equals(credentialHost, ignoreCase = true)) {
|
||||||
|
val basic = Base64.encodeToString(
|
||||||
|
"${credentials.login}:${credentials.password}".toByteArray(), Base64.NO_WRAP,
|
||||||
|
)
|
||||||
|
connection.setRequestProperty("Authorization", "Basic $basic")
|
||||||
|
}
|
||||||
|
val code = connection.responseCode
|
||||||
|
if (code in REDIRECT_CODES) {
|
||||||
|
val location = connection.getHeaderField("Location")
|
||||||
|
?: run { connection.disconnect(); error("Перенаправление без адреса") }
|
||||||
|
connection.disconnect()
|
||||||
|
require(redirect < MAX_REDIRECTS) { "Слишком много перенаправлений" }
|
||||||
|
current = URL(current, location).requireHttps()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (code !in 200..299) {
|
||||||
|
connection.disconnect()
|
||||||
|
error("Сервер каталога прошивок вернул HTTP $code")
|
||||||
|
}
|
||||||
|
return connection
|
||||||
|
}
|
||||||
|
error("Слишком много перенаправлений")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(manifestUrl: String, text: String): List<FirmwareRelease> {
|
||||||
|
require(text.length <= MAX_MANIFEST_CHARS) { "Файл каталога прошивок слишком большой" }
|
||||||
|
val root = JSONObject(text)
|
||||||
|
val firmware = root.optJSONObject("firmware")
|
||||||
|
?: error("В update.json не опубликован каталог прошивок")
|
||||||
|
val rows = firmware.optJSONArray("releases")
|
||||||
|
?: error("В каталоге отсутствует массив releases")
|
||||||
|
return buildList {
|
||||||
|
for (index in 0 until rows.length()) {
|
||||||
|
val row = rows.getJSONObject(index)
|
||||||
|
val product = row.optString("product", row.optString("device")).trim()
|
||||||
|
val versionName = row.optString("versionName", row.optString("version")).trim()
|
||||||
|
val versionCode = row.optLong("versionCode", -1)
|
||||||
|
val imageUrl = URL(URL(manifestUrl), row.getString("imageUrl")).toString()
|
||||||
|
URL(imageUrl).requireHttps()
|
||||||
|
val sha256 = row.getString("sha256").trim().lowercase()
|
||||||
|
val fileName = row.optString("fileName", URL(imageUrl).path.substringAfterLast('/')).trim()
|
||||||
|
val transport = row.optString("transport", "rs485").trim().lowercase()
|
||||||
|
require(product.isNotEmpty() && versionName.isNotEmpty() && versionCode >= 0) {
|
||||||
|
"Не заполнены обязательные поля прошивки №${index + 1}"
|
||||||
|
}
|
||||||
|
require(sha256.matches(Regex("[0-9a-f]{64}"))) { "Некорректный SHA-256 прошивки №${index + 1}" }
|
||||||
|
require(fileName.matches(Regex("[A-Za-zА-Яа-яЁё0-9._ -]+\\.(bin|hex)", RegexOption.IGNORE_CASE))) {
|
||||||
|
"Некорректное имя файла прошивки №${index + 1}"
|
||||||
|
}
|
||||||
|
require(transport in setOf("rs485", "can", "stm32")) { "Некорректный канал прошивки №${index + 1}" }
|
||||||
|
val baseAddress = row.opt("baseAddress")?.takeUnless { it == JSONObject.NULL }?.toString()?.let(::parseAddress)
|
||||||
|
add(FirmwareRelease(
|
||||||
|
product, versionName, versionCode, imageUrl, fileName, sha256,
|
||||||
|
row.optString("notes").trim(), transport, baseAddress,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}.sortedWith(compareBy<FirmwareRelease> { it.product.lowercase() }.thenByDescending { it.versionCode })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseAddress(value: String): Long =
|
||||||
|
if (value.startsWith("0x", ignoreCase = true)) value.drop(2).toLong(16) else value.toLong()
|
||||||
|
|
||||||
|
private fun URL.requireHttps(): URL = also {
|
||||||
|
require(protocol.equals("https", ignoreCase = true)) { "Обновления разрешены только по HTTPS" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
|
||||||
|
private const val CONNECT_TIMEOUT_MS = 15_000
|
||||||
|
private const val READ_TIMEOUT_MS = 60_000
|
||||||
|
private const val MAX_REDIRECTS = 5
|
||||||
|
private const val MAX_MANIFEST_CHARS = 128 * 1024
|
||||||
|
private const val MAX_FIRMWARE_BYTES = 128L * 1024 * 1024
|
||||||
|
private val REDIRECT_CODES = setOf(301, 302, 303, 307, 308)
|
||||||
|
}
|
||||||
|
}
|
||||||
22
c/set-protocol/ports/android/set_plot_jni.c
Normal file
22
c/set-protocol/ports/android/set_plot_jni.c
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#include <jni.h>
|
||||||
|
#include "set_plot.h"
|
||||||
|
|
||||||
|
JNIEXPORT jdoubleArray JNICALL
|
||||||
|
Java_ru_setcorp_setprotocol_trends_NativePlot_evaluate(JNIEnv *env, jobject self,
|
||||||
|
jint operation, jdoubleArray input) {
|
||||||
|
double values[10], result[4];
|
||||||
|
jsize n;
|
||||||
|
size_t count;
|
||||||
|
jdoubleArray output;
|
||||||
|
(void)self;
|
||||||
|
if (!input) return NULL;
|
||||||
|
n = (*env)->GetArrayLength(env, input);
|
||||||
|
if (n < 0 || n > 10) return NULL;
|
||||||
|
(*env)->GetDoubleArrayRegion(env, input, 0, n, values);
|
||||||
|
if ((*env)->ExceptionCheck(env)) return NULL;
|
||||||
|
count = set_plot_eval((uint32_t)operation, values, (size_t)n, result, 4);
|
||||||
|
if (!count) return NULL;
|
||||||
|
output = (*env)->NewDoubleArray(env, (jsize)count);
|
||||||
|
if (output) (*env)->SetDoubleArrayRegion(env, output, 0, (jsize)count, result);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
@@ -4,6 +4,95 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "setprotocol_abi.h"
|
#include "setprotocol_abi.h"
|
||||||
|
#include "set_trends.h"
|
||||||
|
#include "set_spectrum.h"
|
||||||
|
|
||||||
|
JNIEXPORT jdoubleArray JNICALL
|
||||||
|
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeSpectrum(
|
||||||
|
JNIEnv *env, jobject self, jdoubleArray times, jdoubleArray values, jint max_size,
|
||||||
|
jint window, jint filter, jdouble low_hz, jdouble high_hz, jboolean remove_mean)
|
||||||
|
{
|
||||||
|
(void)self;
|
||||||
|
if (times == NULL || values == NULL || max_size < 16 || max_size > (jint)SET_SPECTRUM_MAX ||
|
||||||
|
(max_size & (max_size - 1)) != 0) return NULL;
|
||||||
|
jsize count = (*env)->GetArrayLength(env, times);
|
||||||
|
if ((*env)->GetArrayLength(env, values) != count) return NULL;
|
||||||
|
/* Only the analyzed tail crosses JNI; allocations cannot grow with file length. */
|
||||||
|
jsize used = count < max_size ? count : max_size;
|
||||||
|
size_t bins = (size_t)max_size / 2 + 1;
|
||||||
|
double *buffer = (double *)calloc((size_t)used * 2 + bins + 4, sizeof(double));
|
||||||
|
if (buffer == NULL) return NULL;
|
||||||
|
double *t = buffer, *v = t + used, *out = v + used;
|
||||||
|
(*env)->GetDoubleArrayRegion(env, times, count - used, used, t);
|
||||||
|
(*env)->GetDoubleArrayRegion(env, values, count - used, used, v);
|
||||||
|
if ((*env)->ExceptionCheck(env)) { free(buffer); return NULL; }
|
||||||
|
int status = set_spectrum_analyze(t, v, (size_t)used, (size_t)max_size,
|
||||||
|
window, filter, low_hz, high_hz, remove_mean, out + 4, bins, out + 1);
|
||||||
|
out[0] = (double)status;
|
||||||
|
jsize length = status == SET_SPECTRUM_OK ? (jsize)out[1] / 2 + 5 : 4;
|
||||||
|
jdoubleArray result = (*env)->NewDoubleArray(env, length);
|
||||||
|
if (result != NULL) (*env)->SetDoubleArrayRegion(env, result, 0, length, out);
|
||||||
|
free(buffer);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendCanValue(
|
||||||
|
JNIEnv *env, jobject self, jint source, jlong address, jint device_type,
|
||||||
|
jint device, jint byte_offset, jboolean extended, jboolean is_signed,
|
||||||
|
jlong can_id, jint flags, jbyteArray input)
|
||||||
|
{
|
||||||
|
(void)self;
|
||||||
|
jsize size = (*env)->GetArrayLength(env, input);
|
||||||
|
if (size < 2 || size > 8) return SET_TREND_NO_VALUE;
|
||||||
|
jbyte data[8];
|
||||||
|
(*env)->GetByteArrayRegion(env, input, 0, size, data);
|
||||||
|
return set_trend_can_value((uint8_t)source, (uint32_t)address,
|
||||||
|
(uint8_t)device_type, (uint8_t)device, (uint8_t)byte_offset,
|
||||||
|
(uint8_t)extended, (uint8_t)is_signed, (uint32_t)can_id,
|
||||||
|
(uint8_t)flags, (const uint8_t *)data, (size_t)size);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jbyteArray JNICALL
|
||||||
|
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchRequest(
|
||||||
|
JNIEnv *env, jobject self, jint period, jintArray input)
|
||||||
|
{
|
||||||
|
(void)self;
|
||||||
|
jsize count = (*env)->GetArrayLength(env, input);
|
||||||
|
if (count > (jsize)SET_TREND_WATCH_MAX || period < 0 || period > 65535) return NULL;
|
||||||
|
jint values[SET_TREND_WATCH_MAX];
|
||||||
|
uint16_t addresses[SET_TREND_WATCH_MAX];
|
||||||
|
uint8_t output[4U + 2U * SET_TREND_WATCH_MAX];
|
||||||
|
(*env)->GetIntArrayRegion(env, input, 0, count, values);
|
||||||
|
for (jsize i = 0; i < count; ++i) {
|
||||||
|
if (values[i] < 0 || values[i] > 65535) return NULL;
|
||||||
|
addresses[i] = (uint16_t)values[i];
|
||||||
|
}
|
||||||
|
size_t size = set_trend_watch_request((uint16_t)period, addresses, (size_t)count, output, sizeof(output));
|
||||||
|
if (size == 0U) return NULL;
|
||||||
|
jbyteArray result = (*env)->NewByteArray(env, (jsize)size);
|
||||||
|
if (result != NULL) (*env)->SetByteArrayRegion(env, result, 0, (jsize)size, (const jbyte *)output);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jintArray JNICALL
|
||||||
|
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchValues(
|
||||||
|
JNIEnv *env, jobject self, jbyteArray input)
|
||||||
|
{
|
||||||
|
(void)self;
|
||||||
|
jsize size = (*env)->GetArrayLength(env, input);
|
||||||
|
if (size < 6 || size > (jsize)(6U + 2U * SET_TREND_WATCH_MAX)) return NULL;
|
||||||
|
jbyte payload[6U + 2U * SET_TREND_WATCH_MAX];
|
||||||
|
uint16_t words[SET_TREND_WATCH_MAX];
|
||||||
|
jint values[SET_TREND_WATCH_MAX];
|
||||||
|
(*env)->GetByteArrayRegion(env, input, 0, size, payload);
|
||||||
|
int count = set_trend_watch_values((const uint8_t *)payload, (size_t)size, words, SET_TREND_WATCH_MAX);
|
||||||
|
if (count < 0) return NULL;
|
||||||
|
for (int i = 0; i < count; ++i) values[i] = words[i];
|
||||||
|
jintArray result = (*env)->NewIntArray(env, count);
|
||||||
|
if (result != NULL) (*env)->SetIntArrayRegion(env, result, 0, count, values);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t *storage;
|
uint8_t *storage;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package ru.setcorp.setprotocol.trends
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class GuiGasWatchTest {
|
||||||
|
@Test fun requestMatchesPythonAndCContract() {
|
||||||
|
assertArrayEquals(byteArrayOf(-24, 3, 2, 0, 0x34, 0x12, -1, -1), GuiGasWatch.request(1000, listOf(0x1234, 0xFFFF)))
|
||||||
|
assertArrayEquals(byteArrayOf(0, 0, 0, 0), GuiGasWatch.request(0, emptyList()))
|
||||||
|
assertTrue(runCatching { GuiGasWatch.request(1, (0..64).toList()) }.isFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun partialAckMustNeverMislabelValues() {
|
||||||
|
GuiGasWatch.validateAck(byteArrayOf(-24, 3, 2, 0), 1000, 2)
|
||||||
|
assertTrue(runCatching { GuiGasWatch.validateAck(byteArrayOf(-24, 3, 1, 0), 1000, 2) }.isFailure)
|
||||||
|
assertTrue(runCatching { GuiGasWatch.validateAck(byteArrayOf(0, 0, 2, 0), 1000, 2) }.isFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun valuesRequireExactLengthAndCount() {
|
||||||
|
val payload = byteArrayOf(1, 2, 3, 4, 2, 0, 0x34, 0x12, -1, -1)
|
||||||
|
assertEquals(listOf(0x1234, 65535), GuiGasWatch.values(payload, 2))
|
||||||
|
assertTrue(runCatching { GuiGasWatch.values(payload, 1) }.isFailure)
|
||||||
|
assertTrue(runCatching { GuiGasWatch.values(payload.dropLast(1).toByteArray(), 2) }.isFailure)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user