20 Commits

Author SHA1 Message Date
96ce6f8759 feat(ds18b20-ds2480): добавь драйвер датчиков через UART-мост 2026-09-15 16:57:33 +03:00
20dbc2c721 fix(set-protocol): сохрани совместимость порта с ARMCC 2026-09-07 08:42:02 +03:00
e95338d781 fix(set-protocol): убери libc из порта DevBoard F407 2026-09-07 08:41:26 +03:00
daa3466a41 feat(set-protocol): добавь порт F407 для DevBoard_V1 2026-09-07 08:32:02 +03:00
c9cf797132 Добавить HTML-справочник CAN v1 и v2 2026-09-06 03:09:53 +03:00
de5dc18a43 Merge pull request 'Расширить графики и общие протоколы CAN и Periph28335' (#4) from feature/plot-can-periph-updates into master
Reviewed-on: #4
2026-09-04 19:18:54 +03:00
d44d57a7aa Убрать лишнюю строку в Legacy CAN API 2026-09-04 19:17:27 +03:00
b972b6f33c Добавить канал TMS в каталог прошивок 2026-09-04 19:15:48 +03:00
454baeed98 Добавить общий протокол Periph28335 2026-09-04 19:15:48 +03:00
01ffc0e496 Добавить общий Python API старого CAN terminal 2026-09-04 19:15:47 +03:00
3c4ac9963d Добавить общий API старого CAN terminal 2026-09-04 19:15:47 +03:00
d9eb7dd9ad feat(plot): lock axes and calculate marker levels in dB 2026-09-04 19:15:47 +03:00
f6787163dc feat(plot): configure one or two marker pairs per axis 2026-09-04 19:15:47 +03:00
5f8fc07f2a Merge pull request 'Добавить общие графики и драйвер parallel NAND' (#3) from feature/shared-plot-and-parallel-nand into master
Reviewed-on: #3
2026-09-04 12:39:12 +03:00
c8565d868d Убрать лишнюю строку в модели маркеров 2026-09-04 12:36:56 +03:00
1523640414 Добавить общий Python-парсер каталога прошивок 2026-09-04 12:31:42 +03:00
fa6f615e81 Добавить безопасный клиент каталога прошивок Android 2026-09-04 12:30:37 +03:00
392817432b Поддержать настройки трендов в Python 3.8 2026-09-04 12:23:07 +03:00
20850b26df Добавить переносимый драйвер parallel NAND 2026-09-04 12:22:21 +03:00
e163d9ba55 Добавить общие графики, декодер KONOR и порт STM32 bxCAN 2026-09-04 12:22:17 +03:00
112 changed files with 10315 additions and 33 deletions

View File

@@ -33,17 +33,25 @@ templates/
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
| [`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-ds2480`](c/ds18b20-ds2480) | DS18B20 через DS2480B: поиск ROM, температура, EEPROM, паразитное питание | C99 | UART 9600 8N1, сброс моста, задержка |
| [`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/stm32f407-devboard-v1`](c/set-protocol/ports/stm32f407-devboard-v1) | доступ SETGUI к Modbus-регистрам F407 через CAN485 DevBoard_V1 | `pcan_modbus_server`, STM32 HAL CAN | bxCAN FIFO0 и callbacks карты регистров |
| [`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/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/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/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.
@@ -94,7 +102,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/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |

View File

@@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.13)
project(ds18b20_ds2480 C)
add_library(ds18b20_ds2480 STATIC ds2480.c ds18b20_ds2480.c)
target_include_directories(ds18b20_ds2480 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(ds18b20_ds2480 PUBLIC c_std_99)
if(MSVC)
target_compile_options(ds18b20_ds2480 PRIVATE /W4)
else()
target_compile_options(ds18b20_ds2480 PRIVATE -Wall -Wextra -Wpedantic)
endif()
option(DS18B20_DS2480_BUILD_TESTS "Build host tests" ON)
if(DS18B20_DS2480_BUILD_TESTS)
enable_testing()
add_executable(test_ds18b20_ds2480 tests/test_ds18b20_ds2480.c)
target_link_libraries(test_ds18b20_ds2480 PRIVATE ds18b20_ds2480)
add_test(NAME ds18b20_ds2480 COMMAND test_ds18b20_ds2480)
endif()

145
c/ds18b20-ds2480/README.md Normal file
View File

@@ -0,0 +1,145 @@
# DS18B20 через DS2480B
Переносимая библиотека C99: поиск DS18B20 на общей шине, температура,
разрешение 912 бит, TH/TL и сохранение в EEPROM через UART-мост DS2480B.
Поддерживаются внешнее и паразитное питание: преобразование и запись EEPROM
используют strong pullup, включаемый мостом сразу после последнего бита команды.
Ядро не зависит от HAL, CMSIS, ОС или существующей GPIO-библиотеки `ds18b20`.
Динамической памяти и глобального состояния нет. Каждый UART-мост имеет свой
`ds2480`, каждый обход — свой `ds2480_search`.
```text
Приложение → ds18b20_ds2480 → ds2480 → callbacks UART/задержки → платформа
```
## Файлы
| Файл | Назначение | Зависимости |
|---|---|---|
| `ds2480.h`, `ds2480.c` | Калибровка, reset, обмен битами/байтами, поиск ROM, CRC, strong pullup | `stdint.h`, callbacks |
| `ds18b20_ds2480.h`, `ds18b20_ds2480.c` | Команды термометра, проверка scratchpad, знаковая температура | `ds2480`, `string.h` |
| `examples/read_first.c` | Полный цикл для первого найденного DS18B20 | ядро, порт приложения |
| `tests/test_ds18b20_ds2480.c` | Модель UART-моста и нескольких устройств 1-Wire | ядро, стандартная библиотека C |
## Контракт порта
```c
int prepare(void *user);
int write(void *user, const uint8_t *data, uint32_t size, uint32_t timeout_ms);
int read(void *user, uint8_t *data, uint32_t size, uint32_t timeout_ms);
void delay_ms(void *user, uint32_t ms);
```
Первые три callbacks возвращают `0` при успехе, иначе ошибку.
`prepare` аппаратно сбрасывает DS2480B или формирует UART BREAK не короче 2 мс,
настраивает **9600 бод, 8N1**, выдерживает минимум 2 мс после сброса и очищает RX
и ошибки UART. Эта функция должна иметь собственный конечный таймаут.
Нельзя просто посылать `C1` работающему мосту вместо сброса.
`write` передаёт ровно `size` байтов и ждёт окончания передачи; `read` получает
ровно `size` байтов. Обе операции ограничены `timeout_ms`; при частичном обмене
возвращается ошибка. Приём должен работать уже во время передачи: ответ может
появиться до вызова `read`. `delay_ms` не должна возвращаться раньше заданного
времени (учтите округление системного тика).
Управлять одним UART извне одновременно с библиотекой нельзя. В RTOS блокировка
нужна на всю операцию верхнего уровня, включая поиск и ожидание преобразования.
Тайминги 1-Wire формирует мост, запрещать прерывания на время слотов не требуется.
## Быстрый старт
Функции `prepare`, `write`, `read`, `delay_ms` и `uart_context` предоставляет плата.
Следующий код располагается внутри функции приложения:
```c
ds2480 bus;
ds2480_search search = {{0}, 0, 0};
ds2480_port port = {uart_context, prepare, write, read, delay_ms};
int16_t raw;
ds2480_status status = ds2480_init(&bus, &port, 20);
if (status != DS2480_OK) return;
status = ds18b20_ds2480_next(&bus, &search);
if (status != DS2480_OK) return;
status = ds18b20_ds2480_convert(&bus, search.rom);
if (status != DS2480_OK) return;
status = ds18b20_ds2480_temperature(&bus, search.rom, &raw);
if (status != DS2480_OK) return;
float celsius = raw / 16.0f;
(void)celsius;
```
Вызывайте `next` повторно с тем же курсором до `DS2480_DONE`, сохраняя каждый
найденный ROM в памяти приложения. Лимита количества датчиков в ядре нет.
Пустая шина возвращает `DS2480_NO_PRESENCE`; обрыв поиска и ошибки CRC не
маскируются под успешное завершение. Для нового обхода обнулите курсор.
`convert(bus, NULL)` одновременно запускает все устройства — допустимо только
на шине исключительно с DS18B20. Затем читайте каждое по ROM. Для паразитного
питания суммарный ток должен укладываться в возможности моста; при необходимости
преобразуйте по одному датчику. Преобразование блокирует вызов на 750 мс плюс
UART-обмен. EEPROM использует выдержку 12 мс, её содержимое после перезапуска
этой функцией не проверяется. Чтение температуры само преобразование не запускает.
Значение 85 °C после включения нельзя отличить от настоящих 85 °C без завершённого
преобразования; не считайте его заведомой ошибкой.
## Ошибки и ограничения
- `IO` — таймаут/ошибка порта, `PROTOCOL` — неожиданный ответ DS2480B.
После них экземпляр не готов к работе: повторите `ds2480_init`.
- `SHORT` — короткое замыкание, `NO_PRESENCE` — нет presence.
- `CRC` — неверная контрольная сумма, `DATA` — недопустимые данные или
неподтверждённая запись. Выход температуры/scratchpad при ошибке не меняется.
- При ошибке во время strong pullup ядро вызывает `prepare`, чтобы прекратить
импульс, и остаётся неготовым. При отказе самого порта снятие питания гарантировать
невозможно; восстановление UART/моста остаётся задачей приложения.
Используется стандартная скорость 1-Wire и только Command Mode: байт передаётся
восьмью командами Single Bit. Поэтому любые значения, включая `E3`, передаются
без экранирования. Это простая реализация для опроса температуры; повышенные
скорости UART, Overdrive и Search Accelerator пока не реализованы. Параметры
таймингов длинной линии остаются заводскими. Аппаратная проверка обязательна
для выбранной топологии/нагрузки. Протокол проверен по документации **DS2480B**;
старые ревизии DS2480 без суффикса B отдельно не проверялись.
## Подключение и тесты
Добавьте `ds2480.c`, `ds18b20_ds2480.c` и путь к заголовкам в сборку прошивки.
Номер UART, GPIO и библиотеку платформы выбирает приложение. Готовый порт
для конкретной платы пока не входит в библиотеку.
```cmake
set(DS18B20_DS2480_BUILD_TESTS OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/templates/c/ds18b20-ds2480)
target_link_libraries(firmware PRIVATE ds18b20_ds2480)
```
Отдельная сборка хостовых тестов:
```sh
cmake -S . -B build
cmake --build build --config Debug
ctest --test-dir build -C Debug --output-on-failure
```
Без CMake, из каталога библиотеки:
```sh
clang -std=c99 -Wall -Wextra -Wpedantic -Werror -I . ds2480.c ds18b20_ds2480.c tests/test_ds18b20_ds2480.c -o build/test.exe
./build/test.exe
```
Тесты моделируют калибровку без ответа, несколько ROM с развилками, другие
семейства, адресацию, все разрешения, отрицательную температуру, CRC, пустую
и замкнутую шину, ошибки транспорта, strong pullup и восстановление после ошибок.
Это программная модель; испытания на физическом DS2480B пока не проводились.
## Использование
Добавлена в сабмодуль `templates` проекта `john103C6T6NewVer`, ветка `ds2480`.
В рабочую прошивку ещё не включена: для этого требуется порт выбранного UART.
## Источники
- [DS2480B datasheet, таблицы команд и ответов](https://www.analog.com/media/en/technical-documentation/data-sheets/ds2480b.pdf)
- [DS18B20 datasheet, команды, питание и scratchpad](https://www.analog.com/media/en/technical-documentation/data-sheets/ds18b20.pdf)

View File

@@ -0,0 +1,108 @@
#include "ds18b20_ds2480.h"
#include <string.h>
static ds2480_status write_byte(ds2480 *bus, uint8_t value)
{
uint8_t echoed;
ds2480_status status = ds2480_byte(bus, value, &echoed);
if (status != DS2480_OK) return status;
return echoed == value ? DS2480_OK : DS2480_DATA;
}
static ds2480_status select_rom(ds2480 *bus, const uint8_t *rom)
{
uint8_t i;
ds2480_status status;
if (rom && (rom[0] != 0x28 || ds2480_crc8(rom, 8))) return DS2480_ARGUMENT;
status = ds2480_reset(bus);
if (status != DS2480_OK) return status;
status = write_byte(bus, rom ? 0x55 : 0xCC);
if (status != DS2480_OK || !rom) return status;
for (i = 0; i < 8; ++i) {
status = write_byte(bus, rom[i]);
if (status != DS2480_OK) return status;
}
return DS2480_OK;
}
ds2480_status ds18b20_ds2480_next(ds2480 *bus, ds2480_search *search)
{
ds2480_status status;
do {
status = ds2480_search_next(bus, search);
if (status != DS2480_OK) return status;
} while (search->rom[0] != 0x28);
return DS2480_OK;
}
ds2480_status ds18b20_ds2480_convert(ds2480 *bus, const uint8_t rom[8])
{
ds2480_status status = select_rom(bus, rom);
if (status != DS2480_OK) return status;
return ds2480_power_byte(bus, 0x44, 750);
}
ds2480_status ds18b20_ds2480_read(ds2480 *bus, const uint8_t rom[8], uint8_t scratchpad[9])
{
uint8_t data[9], i;
ds2480_status status;
if (!rom || !scratchpad) return DS2480_ARGUMENT;
status = select_rom(bus, rom);
if (status != DS2480_OK) return status;
status = write_byte(bus, 0xBE);
if (status != DS2480_OK) return status;
for (i = 0; i < 9; ++i) {
status = ds2480_byte(bus, 0xFF, &data[i]);
if (status != DS2480_OK) return status;
}
if (ds2480_crc8(data, 9)) return DS2480_CRC;
/* All-zero data has valid CRC but cannot be a DS18B20 scratchpad. */
if ((data[4] & 0x9F) != 0x1F) return DS2480_DATA;
memcpy(scratchpad, data, sizeof data);
return DS2480_OK;
}
ds2480_status ds18b20_ds2480_temperature(ds2480 *bus, const uint8_t rom[8], int16_t *raw)
{
uint8_t data[9], undefined;
uint16_t value;
int32_t signed_value;
ds2480_status status;
if (!raw) return DS2480_ARGUMENT;
status = ds18b20_ds2480_read(bus, rom, data);
if (status != DS2480_OK) return status;
undefined = (uint8_t)(3 - ((data[4] >> 5) & 3));
value = (uint16_t)((uint16_t)data[0] | ((uint16_t)data[1] << 8));
value &= (uint16_t)~((1U << undefined) - 1U);
signed_value = value & 0x8000 ? (int32_t)value - 65536 : (int32_t)value;
if (signed_value < -55 * 16 || signed_value > 125 * 16) return DS2480_DATA;
*raw = (int16_t)signed_value;
return DS2480_OK;
}
ds2480_status ds18b20_ds2480_configure(ds2480 *bus, const uint8_t rom[8],
int8_t th, int8_t tl, uint8_t resolution, uint8_t save)
{
uint8_t data[9], config;
ds2480_status status;
if (!rom || resolution < 9 || resolution > 12) return DS2480_ARGUMENT;
config = (uint8_t)(0x1F | ((resolution - 9) << 5));
status = select_rom(bus, rom);
if (status != DS2480_OK) return status;
status = write_byte(bus, 0x4E);
if (status != DS2480_OK) return status;
status = write_byte(bus, (uint8_t)th);
if (status != DS2480_OK) return status;
status = write_byte(bus, (uint8_t)tl);
if (status != DS2480_OK) return status;
status = write_byte(bus, config);
if (status != DS2480_OK) return status;
status = ds18b20_ds2480_read(bus, rom, data);
if (status != DS2480_OK) return status;
if (data[2] != (uint8_t)th || data[3] != (uint8_t)tl || data[4] != config)
return DS2480_DATA;
if (!save) return DS2480_OK;
status = select_rom(bus, rom);
if (status != DS2480_OK) return status;
return ds2480_power_byte(bus, 0x48, 12);
}

View File

@@ -0,0 +1,32 @@
#ifndef DS18B20_DS2480_H
#define DS18B20_DS2480_H
#include "ds2480.h"
#ifdef __cplusplus
extern "C" {
#endif
/** Find next DS18B20, skipping other families; same cursor rules as bridge. */
ds2480_status ds18b20_ds2480_next(ds2480 *bus, ds2480_search *search);
/** Start conversion and hold strong pullup for 750 ms (all resolutions).
* rom=NULL broadcasts: use only on a bus containing exclusively DS18B20.
* Supports external and parasite power within the bridge's current budget.
*/
ds2480_status ds18b20_ds2480_convert(ds2480 *bus, const uint8_t rom[8]);
/** Read and validate all 9 scratchpad bytes. Output is unchanged on error.
* rom is mandatory, must have family 0x28 and valid CRC.
*/
ds2480_status ds18b20_ds2480_read(ds2480 *bus, const uint8_t rom[8], uint8_t scratchpad[9]);
/** Read last conversion in signed 1/16 Celsius units; masks undefined bits
* for 9/10/11-bit resolution. Call convert first: power-on 85 C is ambiguous.
* Output is unchanged on error. Does not start a conversion itself.
*/
ds2480_status ds18b20_ds2480_temperature(ds2480 *bus, const uint8_t rom[8], int16_t *raw);
/** Set TH/TL and resolution (9..12); verify by reading back.
* save!=0 copies to EEPROM with 12 ms strong pullup; avoid frequent writes.
*/
ds2480_status ds18b20_ds2480_configure(ds2480 *bus, const uint8_t rom[8],
int8_t th, int8_t tl, uint8_t resolution, uint8_t save);
#ifdef __cplusplus
}
#endif
#endif

164
c/ds18b20-ds2480/ds2480.c Normal file
View File

@@ -0,0 +1,164 @@
#include "ds2480.h"
static ds2480_status fault(ds2480 *bus, ds2480_status status)
{
bus->ready = 0;
return status;
}
static ds2480_status exchange(ds2480 *bus, uint8_t command, uint8_t *reply)
{
if (!bus || !reply) return DS2480_ARGUMENT;
if (!bus->ready) return DS2480_NOT_READY;
if (bus->port.write(bus->port.user, &command, 1, bus->timeout_ms) ||
bus->port.read(bus->port.user, reply, 1, bus->timeout_ms))
return fault(bus, DS2480_IO);
return DS2480_OK;
}
ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout_ms)
{
uint8_t calibration = 0xC1, reply;
ds2480_status status;
ds2480_port copy;
if (!bus) return DS2480_ARGUMENT;
if (!port || !port->prepare || !port->write || !port->read ||
!port->delay_ms || !timeout_ms) {
bus->ready = 0;
return DS2480_ARGUMENT;
}
copy = *port; /* Also permit reinitialization with &bus->port. */
bus->ready = 0;
bus->port = copy;
bus->timeout_ms = timeout_ms;
if (copy.prepare(copy.user) ||
copy.write(copy.user, &calibration, 1, timeout_ms)) return DS2480_IO;
/* First C1 calibrates only: there is NO response byte. */
copy.delay_ms(copy.user, 2);
bus->ready = 1;
/* Strong pullup duration = infinite, command-mode operations only. */
status = exchange(bus, 0x3F, &reply);
if (status != DS2480_OK) return status;
if (reply != 0x3E) return fault(bus, DS2480_PROTOCOL);
return DS2480_OK;
}
ds2480_status ds2480_reset(ds2480 *bus)
{
uint8_t reply;
ds2480_status status = exchange(bus, 0xC1, &reply);
if (status != DS2480_OK) return status;
/* DS2480B revision pattern from table 2; bit 5 is unspecified. */
if ((reply & 0xDC) != 0xCC) return fault(bus, DS2480_PROTOCOL);
switch (reply & 3) {
case 0: return DS2480_SHORT;
case 3: return DS2480_NO_PRESENCE;
default: return DS2480_OK; /* Ordinary or alarming presence. */
}
}
static ds2480_status slot(ds2480 *bus, uint8_t bit, uint8_t power, uint8_t *received)
{
uint8_t reply, command = (uint8_t)(0x81 | (bit ? 0x10 : 0) | (power ? 2 : 0));
ds2480_status status = exchange(bus, command, &reply);
if (status != DS2480_OK) return status;
if ((reply & 0xFC) != (command & 0xFC) ||
((reply & 3) != 0 && (reply & 3) != 3))
return fault(bus, DS2480_PROTOCOL);
*received = reply & 1;
return DS2480_OK;
}
ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received)
{
if (!received) return DS2480_ARGUMENT;
return slot(bus, bit, 0, received);
}
ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received)
{
uint8_t i, bit, result = 0;
ds2480_status status;
if (!received) return DS2480_ARGUMENT;
for (i = 0; i < 8; ++i) {
status = ds2480_bit(bus, (uint8_t)((value >> i) & 1), &bit);
if (status != DS2480_OK) return status;
result |= (uint8_t)(bit << i);
}
*received = result;
return DS2480_OK;
}
ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms)
{
uint8_t i, bit, reply, echoed = 0;
ds2480_status status;
if (!bus || !hold_ms || hold_ms > 1000) return DS2480_ARGUMENT;
for (i = 0; i < 8; ++i) {
status = slot(bus, (uint8_t)((value >> i) & 1), (uint8_t)(i == 7), &bit);
if (status != DS2480_OK) {
/* A lost reply may leave an infinite pulse active: reset hardware. */
if (i == 7 && !bus->ready) (void)bus->port.prepare(bus->port.user);
return status;
}
echoed |= (uint8_t)(bit << i);
}
bus->port.delay_ms(bus->port.user, hold_ms);
status = exchange(bus, 0xF1, &reply);
if (status == DS2480_OK && reply != 0xEC && reply != 0xEF)
status = fault(bus, DS2480_PROTOCOL);
if (status != DS2480_OK) (void)bus->port.prepare(bus->port.user);
if (status == DS2480_OK && echoed != value) return DS2480_DATA;
return status;
}
uint8_t ds2480_crc8(const uint8_t *data, uint32_t size)
{
uint8_t crc = 0, bit;
uint32_t i;
for (i = 0; i < size; ++i) {
crc ^= data[i];
for (bit = 0; bit < 8; ++bit)
crc = (uint8_t)((crc >> 1) ^ ((crc & 1) ? 0x8C : 0));
}
return crc;
}
ds2480_status ds2480_search_next(ds2480 *bus, ds2480_search *search)
{
ds2480_search next;
ds2480_status status;
uint8_t pos, a, b, direction, ignored, last_zero = 0;
if (!bus || !search || search->discrepancy > 64) return DS2480_ARGUMENT;
if (search->done) return DS2480_DONE;
next = *search;
status = ds2480_reset(bus);
if (status != DS2480_OK) return status;
status = ds2480_byte(bus, 0xF0, &ignored);
if (status != DS2480_OK) return status;
for (pos = 1; pos <= 64; ++pos) {
uint8_t index = (uint8_t)((pos - 1) / 8);
uint8_t mask = (uint8_t)(1U << ((pos - 1) % 8));
status = ds2480_bit(bus, 1, &a);
if (status != DS2480_OK) return status;
status = ds2480_bit(bus, 1, &b);
if (status != DS2480_OK) return status;
if (a && b) return DS2480_DATA; /* Devices disappeared mid-search. */
if (a != b) direction = a;
else {
direction = pos < search->discrepancy ? (uint8_t)!!(search->rom[index] & mask)
: (uint8_t)(pos == search->discrepancy);
if (!direction) last_zero = pos;
}
if (direction) next.rom[index] |= mask;
else next.rom[index] &= (uint8_t)~mask;
status = ds2480_bit(bus, direction, &ignored);
if (status != DS2480_OK) return status;
}
if (!next.rom[0]) return DS2480_DATA;
if (ds2480_crc8(next.rom, 8)) return DS2480_CRC;
next.discrepancy = last_zero;
next.done = (uint8_t)(last_zero == 0);
*search = next;
return DS2480_OK;
}

69
c/ds18b20-ds2480/ds2480.h Normal file
View File

@@ -0,0 +1,69 @@
#ifndef DS2480_H
#define DS2480_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
DS2480_OK = 0, DS2480_DONE, DS2480_NO_PRESENCE, DS2480_SHORT,
DS2480_IO, DS2480_PROTOCOL, DS2480_CRC, DS2480_ARGUMENT,
DS2480_NOT_READY, DS2480_DATA
} ds2480_status;
/** Blocking UART callbacks: 0 = success, nonzero = error/timeout.
* prepare resets the bridge (BREAK >= 2 ms or hardware reset), sets UART
* to 9600 8N1, waits >= 2 ms after reset, clears RX/errors before returning.
* write waits for physical TX completion; read receives exactly size bytes.
* Both enforce timeout_ms. RX must remain enabled while transmitting.
* delay_ms waits AT LEAST the requested duration. No callback may reenter.
*/
typedef struct {
void *user;
int (*prepare)(void *user);
int (*write)(void *user, const uint8_t *data, uint32_t size, uint32_t timeout_ms);
int (*read)(void *user, uint8_t *data, uint32_t size, uint32_t timeout_ms);
void (*delay_ms)(void *user, uint32_t ms);
} ds2480_port;
/** One instance per UART/bridge; serialize access for the whole operation. */
typedef struct {
ds2480_port port;
uint32_t timeout_ms;
uint8_t ready;
} ds2480;
/** Independent ROM search cursor. Zero-initialize before each enumeration. */
typedef struct {
uint8_t rom[8];
uint8_t discrepancy;
uint8_t done;
} ds2480_search;
/** Reset/calibrate DS2480B and verify configuration. Empty bus is allowed.
* On IO/protocol errors call init again before further transactions.
*/
ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout_ms);
/** Standard-speed reset; distinguish short, empty bus and transport failure. */
ds2480_status ds2480_reset(ds2480 *bus);
/** Exchange one slot (write 1 to read); received must be non-NULL. */
ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received);
/** Exchange a byte, LSB first, in command mode (eight UART transactions). */
ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received);
/** Write a byte and start strong pullup immediately after its last slot.
* Blocks for hold_ms (1..1000), then terminates the pulse and consumes reply.
*/
ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms);
/** CRC8 Dallas/Maxim. data must address size bytes (NULL allowed for size=0). */
uint8_t ds2480_crc8(const uint8_t *data, uint32_t size);
/** Search ALL families. OK yields ROM with valid CRC; DONE ends enumeration.
* Cursor changes only on success. After an error retry or zero it to restart.
*/
ds2480_status ds2480_search_next(ds2480 *bus, ds2480_search *search);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,19 @@
#include "ds18b20_ds2480.h"
/* board_port supplies the four callbacks documented in ds2480.h.
* Blocking example: includes bridge reset, scan and 750 ms conversion.
*/
ds2480_status ds18b20_example_read_first(const ds2480_port *board_port, int16_t *raw)
{
ds2480 bus;
ds2480_search search = {{0}, 0, 0};
ds2480_status status;
if (!raw) return DS2480_ARGUMENT;
status = ds2480_init(&bus, board_port, 20);
if (status != DS2480_OK) return status;
status = ds18b20_ds2480_next(&bus, &search);
if (status != DS2480_OK) return status;
status = ds18b20_ds2480_convert(&bus, search.rom);
if (status != DS2480_OK) return status;
return ds18b20_ds2480_temperature(&bus, search.rom, raw);
}

View File

@@ -0,0 +1,228 @@
#include "ds18b20_ds2480.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s\n", __LINE__, #x); exit(1); } } while (0)
enum { ROM_COMMAND, MATCH, FUNCTION, SEARCH, READ, CONFIG };
typedef struct {
uint8_t rom[3][8], scratch[9];
unsigned count, active, state, phase, position, bits, value, match_index;
unsigned prepared, calibrated, pending, reply, pulse, hold, convert, copy;
unsigned fail_read, fail_write, fail_prepare, corrupt_reply, reset_reply;
unsigned fail_power, fail_stop, reject_config;
} fake;
static void crc_scratch(fake *f) { f->scratch[8] = ds2480_crc8(f->scratch, 8); }
static void setup(fake *f)
{
unsigned i;
static const uint8_t scratch[9] = {0x91, 0x01, 0x4B, 0x46, 0x7F, 0xFF, 0x0C, 0x10, 0x70};
memset(f, 0, sizeof *f);
f->count = 3;
f->reset_reply = 0xCD;
memcpy(f->scratch, scratch, 9);
for (i = 0; i < 3; ++i) {
f->rom[i][0] = i == 2 ? 0x10 : 0x28;
f->rom[i][1] = (uint8_t)(i + 1);
f->rom[i][7] = ds2480_crc8(f->rom[i], 7);
}
}
static void byte_in(fake *f, uint8_t byte)
{
unsigned i;
if (f->state == ROM_COMMAND) {
if (byte == 0xF0) { f->state = SEARCH; f->position = f->phase = 0; }
else if (byte == 0x55) { f->state = MATCH; f->match_index = 0; }
else { CHECK(byte == 0xCC); f->state = FUNCTION; }
} else if (f->state == MATCH) {
for (i = 0; i < f->count; ++i)
if (f->rom[i][f->match_index] != byte) f->active &= ~(1U << i);
if (++f->match_index == 8) f->state = FUNCTION;
} else if (f->state == FUNCTION) {
CHECK(f->active != 0);
switch (byte) {
case 0xBE: f->state = READ; f->position = 0; break;
case 0x4E: f->state = CONFIG; f->position = 2; break;
case 0x44: ++f->convert; break;
case 0x48: ++f->copy; break;
default: CHECK(0);
}
} else {
CHECK(f->state == CONFIG);
if (!f->reject_config) f->scratch[f->position] = byte;
if (++f->position == 5) { crc_scratch(f); f->state = FUNCTION; }
}
}
static uint8_t wire_bit(fake *f, uint8_t bit)
{
unsigned i, result = 1;
if (f->state == SEARCH) {
for (i = 0; i < f->count; ++i) {
unsigned v = (f->rom[i][f->position / 8] >> (f->position % 8)) & 1;
if (!(f->active & (1U << i))) continue;
if (f->phase < 2) result &= f->phase ? !v : v;
else if (v != bit) f->active &= ~(1U << i);
}
if (++f->phase == 3) { f->phase = 0; ++f->position; }
return (uint8_t)result;
}
if (f->state == READ) {
CHECK(f->position < 72 && bit == 1);
result = (f->scratch[f->position / 8] >> (f->position % 8)) & 1;
++f->position;
return (uint8_t)result;
}
f->value |= (unsigned)bit << f->bits;
if (++f->bits == 8) {
uint8_t byte = (uint8_t)f->value;
f->bits = f->value = 0;
byte_in(f, byte);
}
return bit;
}
static int prepare(void *user)
{
fake *f = user;
++f->prepared;
f->pending = f->pulse = f->calibrated = 0;
return (int)f->fail_prepare;
}
static int transmit(void *user, const uint8_t *data, uint32_t size, uint32_t timeout)
{
fake *f = user;
uint8_t command = *data, bit;
CHECK(size == 1 && timeout == 20 && !f->pending);
if (f->fail_write) return -1;
if (!f->calibrated) { CHECK(command == 0xC1); f->calibrated = 1; return 0; }
CHECK(!f->pulse || command == 0xF1);
if (command == 0x3F) f->reply = 0x3E;
else if (command == 0xC1) {
f->reply = f->count ? f->reset_reply : 0xCF;
f->state = ROM_COMMAND; f->bits = f->value = 0;
f->active = (1U << f->count) - 1;
} else if (command == 0xF1) {
CHECK(f->pulse && (f->hold == 750 || f->hold == 12));
f->pulse = 0; f->reply = 0xEC;
if (f->fail_stop) f->fail_read = 1;
} else {
CHECK(command == 0x81 || command == 0x91 || command == 0x83 || command == 0x93);
bit = wire_bit(f, (uint8_t)((command >> 4) & 1));
f->reply = (command & 0xFC) | (bit ? 3U : 0U);
if (command & 2) {
CHECK(f->bits == 0 && (f->convert || f->copy));
f->pulse = 1; f->hold = 0;
if (f->fail_power) f->fail_read = 1;
}
}
f->pending = 1;
return 0;
}
static int receive(void *user, uint8_t *data, uint32_t size, uint32_t timeout)
{
fake *f = user;
CHECK(size == 1 && timeout == 20 && f->pending);
if (f->fail_read) return -1;
*data = (uint8_t)(f->corrupt_reply ? 0 : f->reply);
f->pending = 0;
return 0;
}
static void delay(void *user, uint32_t ms)
{
fake *f = user;
if (f->pulse) f->hold += ms;
else CHECK(ms == 2);
}
static void init(fake *f, ds2480 *bus)
{
ds2480_port port = { f, prepare, transmit, receive, delay };
CHECK(ds2480_init(bus, &port, 20) == DS2480_OK);
CHECK(!f->pending);
}
int main(void)
{
fake f, other;
ds2480 bus, second;
ds2480_search search = {{0}, 0, 0}, saved;
uint8_t bit, scratch[9], rom[8];
int16_t raw = 999;
unsigned found = 0, i;
setup(&f); init(&f, &bus);
CHECK(ds2480_crc8(f.scratch, 9) == 0); /* Datasheet vector, not generated. */
while (ds18b20_ds2480_next(&bus, &search) == DS2480_OK) {
if (!memcmp(search.rom, f.rom[0], 8)) found |= 1;
else if (!memcmp(search.rom, f.rom[1], 8)) found |= 2;
else CHECK(0);
}
CHECK(found == 3 && search.done);
CHECK(ds18b20_ds2480_next(&bus, &search) == DS2480_DONE);
CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_OK);
CHECK(f.convert == 1 && f.hold == 750 && !f.pulse && !f.pending);
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK && raw == 401);
CHECK(ds18b20_ds2480_convert(&bus, f.rom[1]) == DS2480_OK && f.active == 2);
for (i = 9; i <= 12; ++i) {
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], -5, -20, (uint8_t)i, 1) == DS2480_OK);
CHECK(f.hold == 12 && !f.pulse && !f.pending);
f.scratch[0] = 0x5F; f.scratch[1] = 0xFF; crc_scratch(&f);
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK);
CHECK(raw == (i == 9 ? -168 : i == 10 ? -164 : i == 11 ? -162 : -161));
}
CHECK(f.copy == 4);
f.reject_config = 1;
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], 1, 2, 9, 1) == DS2480_DATA && f.copy == 4);
f.scratch[8] ^= 1; raw = 999;
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_CRC && raw == 999);
memset(f.scratch, 0, 9);
CHECK(ds18b20_ds2480_read(&bus, f.rom[0], scratch) == DS2480_DATA);
memset(f.scratch, 0xFF, 9);
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_CRC);
memcpy(rom, f.rom[0], 8); rom[7] ^= 1;
CHECK(ds18b20_ds2480_convert(&bus, rom) == DS2480_ARGUMENT);
CHECK(ds18b20_ds2480_read(&bus, NULL, scratch) == DS2480_ARGUMENT);
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], 0, 0, 8, 0) == DS2480_ARGUMENT);
CHECK(ds2480_bit(&bus, 1, NULL) == DS2480_ARGUMENT);
f.reset_reply = 0xEC; CHECK(ds2480_reset(&bus) == DS2480_SHORT);
f.reset_reply = 0xEE; CHECK(ds2480_reset(&bus) == DS2480_OK);
f.count = 0; CHECK(ds2480_reset(&bus) == DS2480_NO_PRESENCE);
memset(&search, 0, sizeof search);
CHECK(ds2480_search_next(&bus, &search) == DS2480_NO_PRESENCE);
setup(&f); init(&f, &bus); f.count = 1; f.rom[0][7] ^= 1;
saved = search;
CHECK(ds2480_search_next(&bus, &search) == DS2480_CRC);
CHECK(!memcmp(&search, &saved, sizeof search));
f.fail_read = 1; CHECK(ds2480_reset(&bus) == DS2480_IO);
CHECK(ds2480_reset(&bus) == DS2480_NOT_READY);
setup(&f); init(&f, &bus); f.corrupt_reply = 1;
CHECK(ds2480_reset(&bus) == DS2480_PROTOCOL && !bus.ready);
setup(&f); init(&f, &bus); CHECK(ds2480_reset(&bus) == DS2480_OK); f.corrupt_reply = 1;
CHECK(ds2480_bit(&bus, 1, &bit) == DS2480_PROTOCOL);
setup(&f); init(&f, &bus); f.fail_write = 1;
CHECK(ds2480_reset(&bus) == DS2480_IO && !bus.ready);
setup(&f); init(&f, &bus); f.fail_power = 1;
CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_IO);
CHECK(!f.pulse && !bus.ready && f.prepared == 2);
setup(&f); init(&f, &bus); f.fail_stop = 1;
CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_IO);
CHECK(!f.pulse && !bus.ready && f.prepared == 2);
setup(&f); init(&f, &bus);
setup(&other); init(&other, &second);
CHECK(ds18b20_ds2480_convert(&second, other.rom[1]) == DS2480_OK);
CHECK(f.convert == 0 && other.convert == 1);
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK && raw == 401);
CHECK(ds2480_init(&bus, &bus.port, 20) == DS2480_OK);
f.fail_prepare = 1;
CHECK(ds2480_init(&bus, &bus.port, 20) == DS2480_IO && !bus.ready);
CHECK(ds2480_init(&bus, NULL, 20) == DS2480_ARGUMENT);
puts("DS18B20/DS2480: all host tests passed");
return 0;
}

View 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
View 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(),
&parallel_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`.

View 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,710 кОм к 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 —
общий 14,7 мкФ. Сигналы не должны превышать питание NAND. Для трасс длиннее
нескольких сантиметров предусмотреть последовательные резисторы 2247 Ом у
источника на `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)

View 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#` удерживается активным аппаратным портом.

View 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

View 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

View 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

View 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

View 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. Начальные тайминги
консервативны; финальные значения подтвердить осциллографом.

View File

@@ -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;
}

View File

@@ -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

View 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,710 кОм к 3,3 В. Порт использует STM32Cube
HAL только для инициализации, а циклы NAND выполняет прямым доступом к GPIO.
Выбранные `PB3/PB4` нельзя одновременно использовать для trace/JTAG; SWD на
`PA13/PA14` остаётся доступен. Если эти пины нужны проекту, измените таблицу и
операции шины: текущая оптимизация рассчитана на непрерывные `PB0..PB7`.

View File

@@ -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;
}

View File

@@ -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

View 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,710 кОм.
Адреса C28x задаются в 16-битных словах: data `0x100000`, command
`0x100001`, address `0x100002`. Используется только младший байт XINTF.
`WP#` в базовом варианте аппаратный: для режима только чтения соединить с GND
через 10 кОм; если позже понадобится PROGRAM/ERASE, заменить на GPIO и реализовать
`tms_set_wp()`.
Задержка в порте приблизительная. После выбора частоты CPU проверить импульсы
осциллографом и при необходимости изменить `PNAND_TMS_CPU_MHZ`.

View File

@@ -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;
}

View File

@@ -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

View 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;
}

View 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;
}

View 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";
}

View 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,
&parallel_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, &reg) == 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, &reg) == PNAND_GAS_OK);
assert(reg == 0x1234U);
assert(parallel_nand_gas_write(&gas, 0xE000U, 0U) ==
PNAND_GAS_READ_ONLY);
puts("parallel_nand: OK");
return 0;
}

View File

@@ -10,10 +10,14 @@ set(SETPROTOCOL_V2_SOURCES
src/set_can.c
src/set_firmware.c
src/set_telemetry.c
src/set_plot.c
src/set_trends.c
src/set_spectrum.c
)
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
set(SETPROTOCOL_LEGACY_SOURCES
src/balsam_can.c
src/gui_catalog.c
src/gui_frame.c
src/pcan_abi.c
@@ -22,6 +26,7 @@ set(SETPROTOCOL_LEGACY_SOURCES
src/pcan_gas.c
src/pcan_id.c
src/pcan_link.c
src/pcan_modbus_server.c
src/pcan_ring.c
)
@@ -32,6 +37,9 @@ set(SETPROTOCOL_SOURCES
add_library(setprotocol_static STATIC ${SETPROTOCOL_SOURCES})
target_include_directories(setprotocol_static PUBLIC include)
if(NOT MSVC)
target_link_libraries(setprotocol_static PUBLIC m)
endif()
# Совместимое имя цели для проектов, уже использовавших SET protocol v2.
add_library(set_protocol ALIAS setprotocol_static)
@@ -40,6 +48,9 @@ option(SETP_BUILD_SHARED "Build stable host ABI shared library" ON)
if(SETP_BUILD_SHARED)
add_library(setprotocol SHARED ${SETPROTOCOL_SOURCES})
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)
set_target_properties(setprotocol PROPERTIES
C_VISIBILITY_PRESET hidden
@@ -66,6 +77,10 @@ option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS)
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)
target_link_libraries(test_set_protocol PRIVATE setprotocol_static)
add_test(NAME set_protocol_v2 COMMAND test_set_protocol)
@@ -74,6 +89,10 @@ if(SETP_BUILD_TESTS)
target_link_libraries(test_transport PRIVATE setprotocol_static)
add_test(NAME legacy_transport COMMAND test_transport)
add_executable(test_modbus_server tests/test_modbus_server.c)
target_link_libraries(test_modbus_server PRIVATE setprotocol_static)
add_test(NAME protocan_modbus_server COMMAND test_modbus_server)
add_executable(test_gui tests/test_gui.c)
target_link_libraries(test_gui PRIVATE setprotocol_static)
add_test(NAME legacy_gui COMMAND test_gui)
@@ -81,4 +100,13 @@ if(SETP_BUILD_TESTS)
add_executable(test_abi tests/test_abi.c)
target_link_libraries(test_abi PRIVATE setprotocol_static)
add_test(NAME stable_abi COMMAND test_abi)
add_executable(test_balsam_can tests/test_balsam_can.c)
target_link_libraries(test_balsam_can PRIVATE setprotocol_static)
add_test(NAME legacy_balsam_can COMMAND test_balsam_can)
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()

View File

@@ -36,6 +36,13 @@ multicast запрещены WRITE, FW_BEGIN и FW_ACTIVATE.
## 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-кадры в очередь,
сегментация собирается в главном цикле, и только полный SETP-кадр передаётся
parser/router. Сборка должна иметь timeout, контроль номера сегмента и одного

View File

@@ -7,12 +7,20 @@
- совместимый ProtoCAN transport для существующих CAN-мостов;
- SETGUI transport v1 для приборов переходного периода;
- стабильный host ABI для Python/JNI/других FFI;
- GAS, кольцевые буферы, CRC и потоковые parser.
- GAS, кольцевые буферы, CRC и потоковые parser;
- host-анализ спектра: FFT, окна, фильтры и адаптеры JNI/ctypes.
Ядро не зависит от HAL, ОС или конкретного адаптера, не использует
Протокольное ядро не зависит от HAL, ОС или конкретного адаптера, не использует
динамическую память и не открывает COM/CAN само. SLCAN, SocketCAN, USB CDC,
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/` | общая реализация всех протокольных модулей |
| `ports/android` | JNI и Kotlin facade |
| `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/legacy` | нормативные документы ProtoCAN/SETGUI v1 |
| `PROTOCOL.md` | нормативный wire contract SET protocol v2 |

View 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 и оформление шкал |
| Перемещение маркера от начальной координаты, ограничение видимой областью | Захват линии пальцем/мышью, редактор положения |
| Разность BA, DC и множитель единиц, шаги шкалы 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 не закрепляет версию.

View 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.

View 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.

View File

@@ -48,7 +48,7 @@ uint16_t n = pcan_gas_read_block(&map, 0x0000, block, 4);
Тип сообщения `PCAN_MSG_GAS` (`0b0011`). Адрес первого регистра лежит
в `MsgBody`, данные — до 4 регистров подряд, младшим байтом вперёд.
Это ровно то, что делает `PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()`
в `SETCAN/Src/protocan.c`, поэтому обмен совместим с существующими
в `ports/stm32-bxcan/protocan.c` (бывший SETCAN), поэтому обмен совместим с существующими
устройствами.
| Кадр | DLC | Смысл |

View File

@@ -0,0 +1,61 @@
/**
* @file balsam_can.h
* @brief Legacy Balsam 167 extended-CAN register frames.
*
* The wire layout is taken from Balsam_167_periph/Source/Internal/ecan.c:
* one big-endian address/mask word followed by three big-endian register words.
*/
#ifndef BALSAM_CAN_H
#define BALSAM_CAN_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define BALSAM_CAN_BASE_ID 0x00BA0000UL
#define BALSAM_CAN_NODE_COUNT 13U
#define BALSAM_CAN_DATA_OFFSET 0x10U
#define BALSAM_CAN_DLC 8U
#define BALSAM_CAN_REGISTER_COUNT 3U
typedef enum {
BALSAM_CAN_TO_NODE = 0,
BALSAM_CAN_FROM_NODE = 1
} balsam_can_direction_t;
typedef struct {
uint8_t device;
uint8_t direction;
uint8_t present_mask;
uint16_t start_address;
uint16_t values[BALSAM_CAN_REGISTER_COUNT];
} balsam_can_frame_t;
/** Return non-zero for the command/data/terminal IDs used by Balsam 167. */
int balsam_can_is_id(uint32_t can_id);
/**
* Decode an 8-byte Balsam frame. Returns 1 on success, 0 for another CAN ID,
* -1 for invalid arguments and -2 for a Balsam ID with a non-8-byte payload.
*/
int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
balsam_can_frame_t *output);
/** Human-readable Russian name of a Balsam device. */
const char *balsam_can_device_name(uint8_t device);
/**
* Format the register name from the Balsam 167 data table into output.
* Returns the required length (excluding NUL); an empty string means unknown.
*/
size_t balsam_can_register_name(uint8_t device, uint16_t address,
char *output, size_t output_size);
#ifdef __cplusplus
}
#endif
#endif /* BALSAM_CAN_H */

View File

@@ -45,6 +45,14 @@ typedef struct {
uint8_t payload[PCAN_ABI_GUI_PAYLOAD_MAX];
} pcan_abi_gui_frame_t;
typedef struct {
uint8_t device;
uint8_t direction;
uint8_t present_mask;
uint16_t start_address;
uint16_t values[3];
} pcan_abi_balsam_frame_t;
PCAN_ABI_API uint32_t pcan_abi_version(void);
PCAN_ABI_API uint32_t pcan_abi_id_pack(uint8_t priority, uint8_t route,
@@ -58,6 +66,13 @@ PCAN_ABI_API void pcan_abi_id_unpack(uint32_t raw, uint8_t *priority,
PCAN_ABI_API uint16_t pcan_abi_crc16(const uint8_t *data, size_t size);
PCAN_ABI_API int pcan_abi_balsam_decode(uint32_t can_id,
const uint8_t *data, size_t size,
pcan_abi_balsam_frame_t *output);
PCAN_ABI_API const char *pcan_abi_balsam_device_name(uint8_t device);
PCAN_ABI_API size_t pcan_abi_balsam_register_name(
uint8_t device, uint16_t address, char *output, size_t output_size);
PCAN_ABI_API size_t pcan_abi_frame_encode(uint8_t sequence, uint8_t flags,
uint32_t can_id,
const uint8_t *data, uint8_t dlc,

View File

@@ -10,7 +10,7 @@
* Транспорт ProtoCAN (MsgType = 0b0011) кладёт в MsgBody адрес первого
* регистра, а в данные - до 4 регистров подряд, младшим байтом вперёд.
* Это ровно то, что делает PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()
* в SETCAN/Src/protocan.c, поэтому обмен совместим с существующими
* в ports/stm32-bxcan/protocan.c, поэтому обмен совместим с существующими
* устройствами.
*
* Запрос на чтение кодируется кадром GAS с DLC = 0: в исходном коде

View File

@@ -2,7 +2,7 @@
* @file pcan_id.h
* @brief Упаковка и разбор 29-битного идентификатора ProtoCAN.
*
* Раскладка (соответствует ProtoCanId_t из SETCAN/Inc/protocan.h):
* Раскладка (ProtoCanId_t из ports/stm32-bxcan/protocan.h, бывший SETCAN):
*
* биты 28 Priority
* 27 Route

View File

@@ -0,0 +1,60 @@
/**
* @file pcan_modbus_server.h
* @brief Modbus register windows transported in one classic ProtoCAN frame.
*/
#ifndef PCAN_MODBUS_SERVER_H
#define PCAN_MODBUS_SERVER_H
#include <stdbool.h>
#include <stdint.h>
#include "pcan_frame.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
PCAN_MODBUS_COIL = 0x4U,
PCAN_MODBUS_DISCRETE = 0x5U,
PCAN_MODBUS_HOLDING = 0x6U,
PCAN_MODBUS_INPUT = 0x7U
} pcan_modbus_bank_t;
typedef bool (*pcan_modbus_read_fn)(void *user, pcan_modbus_bank_t bank,
uint16_t address, uint16_t *value);
typedef bool (*pcan_modbus_write_fn)(void *user, pcan_modbus_bank_t bank,
uint16_t address, uint16_t value);
typedef struct {
uint8_t device_type;
uint8_t device_id;
pcan_modbus_read_fn read;
pcan_modbus_write_fn write;
void *user;
uint32_t requests;
uint32_t responses;
uint32_t rejected;
} pcan_modbus_server_t;
typedef enum {
PCAN_MODBUS_NOT_FOR_US = 0,
PCAN_MODBUS_HANDLED_NO_RESPONSE,
PCAN_MODBUS_RESPONSE
} pcan_modbus_result_t;
bool pcan_modbus_server_init(pcan_modbus_server_t *server,
uint8_t device_type, uint8_t device_id,
pcan_modbus_read_fn read,
pcan_modbus_write_fn write, void *user);
/** Zero DLC reads; data writes COIL/HOLDING. Register words are little-endian. */
pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
const pcan_frame_t *request,
pcan_frame_t *response);
#ifdef __cplusplus
}
#endif
#endif /* PCAN_MODBUS_SERVER_H */

View File

@@ -23,6 +23,7 @@
#include "pcan_gas.h"
#include "pcan_id.h"
#include "pcan_link.h"
#include "pcan_modbus_server.h"
#include "pcan_ring.h"
#endif /* PROTOCAN_TRANSPORT_H */

View File

@@ -0,0 +1,36 @@
/** @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 */
SET_PLOT_DB_DELTA = 7 /* A,B -> 20*log10(abs(B/A)); zero is invalid */
};
/** 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

View 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

View 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

View File

@@ -5,6 +5,8 @@
#ifndef SETPROTOCOL_H
#define SETPROTOCOL_H
#include "balsam_can.h"
/* Основной SET protocol v2. */
#include "set_protocol.h"
#include "set_can.h"

View File

@@ -8,6 +8,11 @@ LOCAL_SRC_FILES := \
../../src/set_can.c \
../../src/set_firmware.c \
../../src/set_telemetry.c \
../../src/set_plot.c \
set_plot_jni.c \
../../src/set_trends.c \
../../src/set_spectrum.c \
../../src/balsam_can.c \
../../src/gui_catalog.c \
../../src/gui_frame.c \
../../src/pcan_abi.c \
@@ -19,5 +24,5 @@ LOCAL_SRC_FILES := \
../../src/pcan_gas.c \
setprotocol_jni.c
LOCAL_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden
LOCAL_LDLIBS := -llog
LOCAL_LDLIBS := -llog -lm
include $(BUILD_SHARED_LIBRARY)

View File

@@ -7,4 +7,20 @@ and include this `Android.mk` from the application NDK build.
The JNI streaming parser returns fixed 15-byte records
`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.
`legacycan/LegacyCanTerminal.kt` contains the two historical CAN_terminal wire
formats, the complete Projects.ini node/command catalog, and shared codecs for
register writes and command frames. Android UI code must use this module instead
of reproducing the Delphi byte rotation or CAN-ID routing rules.
`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.

View File

@@ -10,6 +10,9 @@ object MessageType {
const val FIRMWARE_END = 0x0D
const val FIRMWARE_ABORT = 0x0E
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_LIST = 0x21
const val SENSOR_READ = 0x22
@@ -33,6 +36,9 @@ object MessageType {
FIRMWARE_END -> "FIRMWARE_END"
FIRMWARE_ABORT -> "FIRMWARE_ABORT"
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_LIST -> "SENSOR_LIST"
SENSOR_READ -> "SENSOR_READ"

Some files were not shown because too many files have changed in this diff Show More