17 Commits

Author SHA1 Message Date
874d2e1dfc Объединить разработки CAN и STM32 в master 2026-09-15 15:57:48 +03:00
698632ebf3 Сохранить текущие изменения NAND и LED перед объединением веток 2026-09-15 15:57:06 +03:00
b0f3731554 Добавить HTML-справочник CAN v1 и v2 2026-09-06 03:03:59 +03:00
9ad0322c71 Добавить порт SETProtocol v2 для TMS320F2812 2026-09-06 02:14:17 +03:00
1511719bb9 Расширить общий протокол CAN_Bal_2812 2026-09-06 01:06:30 +03:00
0ce464499f Добавить общую модель конструктора ProtoCAN для Android 2026-09-06 00:11:25 +03:00
27ef6fcfdf Merge remote-tracking branch 'origin/codex/trend-display-scale' into HEAD 2026-09-05 11:43:08 +03:00
235d5d81a2 Share PM67 upload protocol through C core 2026-09-05 11:37:03 +03:00
59f847e900 Добавить множитель и IQ в Python-тренды 2026-09-05 03:45:05 +03:00
286e454464 Добавить выпуск прошивок из Keil и CCS 2026-09-05 03:16:16 +03:00
e691dfc337 Добавить публикацию каталога прошивок 2026-09-05 03:05:17 +03:00
f5f15f6a04 Сохранить совместимость ABI графиков 2026-09-05 02:44:08 +03:00
be066a57eb Дополнить общий контракт границ осей 2026-09-05 02:43:07 +03:00
b1f7b965f4 Расширить общие API графиков и GAS обмена 2026-09-05 02:37:11 +03:00
78d3f6690b Align Android parser statistics with C core 2026-09-04 21:10:02 +03:00
6a82b309cc Move PM35 protocol into shared C core 2026-09-04 21:06:40 +03:00
2316a5a26a Добавить Android API протокола ПМ35 2026-09-04 20:39:04 +03:00
110 changed files with 5426 additions and 936 deletions

View File

@@ -62,6 +62,27 @@ G431 и G474 намеренно используют один исходник
этой задачи совместимы. Раздельными остаются конфигурация платы, startup,
linker script и выбранный CMSIS device define.
## Выбор готового порта parallel NAND
| Целевой МК | Реализация шины | Каталог порта |
|---|---|---|
| TMS320F2812 | XINTF Zone 6 | `c/parallel-nand/ports/tms320f2812` |
| STM32F103RCT6 | GPIO bit-bang, поскольку FSMC недоступен в LQFP64 | `c/parallel-nand/ports/stm32f103rc` |
| STM32F103ZET6 | FSMC NAND Bank 3 | `c/parallel-nand/ports/stm32f103ze` |
| STM32F407VET6 | FSMC NAND Bank 2 | `c/parallel-nand/ports/stm32f407ve` |
| STM32G474CEU6 | GPIO bit-bang для UFQFPN48 | `c/parallel-nand/ports/stm32g474ce` |
Ядро поддерживает профили Micron `MT29F1G08ABADA` и Hynix
`HY27UF084G2M`. В сборку добавляются `src/parallel_nand.c`, при необходимости
`src/parallel_nand_gas.c` или `src/parallel_nand_pcan_gas.c`, и ровно один
аппаратный порт. Конкретная распиновка, требуемые модули HAL и пример
инициализации приведены в README выбранного порта.
Для `PROGRAM`/`ERASE` линия `WP#` должна управляться портом. Готовые порты
STM32F103RC, STM32F103ZE и STM32G474CE объявляют поддержку записи. Базовые
схемы TMS320F2812 и STM32F407VE фиксируют `WP#` аппаратно и безопасно
возвращают `PNAND_ERROR_WRITE_PROTECTED`, пока плата и порт не доработаны.
Рабочий пример полноценного G474-порта находится в соседнем проекте
`candleLight_fw`: `src/device/device_g4.c` отвечает за clock/GPIO,
`src/can/m_can.c` — за FDCAN, а `include/config.h` — за привязку платы
@@ -77,6 +98,7 @@ USB gs_usb и конкретной разводки адаптера; перен
| дисплей ST7789 | `c/st7789` | SPI, DC, CS, RESET, подсветка и задержка |
| кнопки | `c/keypad` | настройка GPIO и чтение уровней |
| меню | `c/menu` | только привязка painter к дисплею |
| индикация состояний | `c/led-indicator` | готовый STM32 HAL-порт или callbacks GPIO и выбранного таймера |
| EEPROM FT24C256 | `c/eeprom-ft24c256` | I2C write/write-read и задержка |
| CAN SETTINGS | `c/can-sensor` | bxCAN для F103 либо FDCAN для G431/G474 |

View File

@@ -13,6 +13,7 @@
templates/
c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты
python/ модули на чистом Python 3.9+, только stdlib
tools/ общие инструменты сборки и выпуска прошивок
```
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
@@ -30,18 +31,19 @@ templates/
| [`c/st7789`](c/st7789) | TFT ST7789V по SPI, RGB565, текст без кадрового буфера | `stdint.h` | SPI-запись, линия DC, задержка |
| [`c/keypad`](c/keypad) | шесть кнопок: антидребезг, автоповтор, удержание, очередь событий | `stdint.h` | чтение уровня кнопки, время в мс |
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `stdint.h` | заливка прямоугольника, вывод строки |
| [`c/led-indicator`](c/led-indicator) | неблокирующие режимы LED: работа, активность, предупреждение и ошибка | C99 | логическая запись канала, время выбранного таймера — **STM32F1/F4/G4 HAL-порт в комплекте** |
| [`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`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android, STM32F4 и TMS320F2812-порты в комплекте** |
| [`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/set-protocol/ports/tms320f2812`](c/set-protocol/ports/tms320f2812) | SETProtocol v2 firmware service по segmented classic CAN | `set_protocol`, `set_can`, `set_firmware` | CAN TX, Flash erase/write/read, optional signature policy и reboot |
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
| [`c/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 в комплекте** |
| [`c/parallel-nand`](c/parallel-nand) | parallel NAND x8: raw read/program/erase, профили Micron MT29F1G08 и Hynix HY27UF084G2M, bad-block marker и банковое окно GAS для SETGUI | C99, опционально `set-protocol/pcan_gas` | **порты TMS320F2812, STM32F103RC/ZE, STM32F407VE и STM32G474CEU6; запись требует управляемого WP#** |
### Python
@@ -50,6 +52,12 @@ templates/
| [`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 |
### Инструменты
| Инструмент | Что делает |
|---|---|
| [`tools/firmware-publish`](tools/firmware-publish) | единый BAT и конфигурации для проверки и публикации `.hex` Keil / `.bin` CCS 12 в каталоге SETGUI |
Общие тренды для Android GUI и SETGUI: [формат JSON, C99-ядро и адаптеры](c/set-protocol/docs/GUI_TRENDS.md).
Общие масштабирование и маркеры: [C99, JNI и Python/Qt](c/set-protocol/docs/GUI_PLOT.md).

80
RULES.md Normal file
View File

@@ -0,0 +1,80 @@
# Правила общего кроссплатформенного кода
Этот репозиторий — единственный источник общих алгоритмов для `GUI_Android`,
`SETGUI`, прошивок и будущих GUI. Копирование одной реализации между Kotlin,
Python, C# или другим языком запрещено.
## 1. Граница C-ядра и порта
В `c/set-protocol` на C99 обязательно размещаются:
- форматы кадров и идентификаторов, CRC/checksum, endian-преобразования;
- построение команд, разбор и проверка ответов;
- автоматы обмена, сегментация, повтор, таймаутные состояния без системных часов;
- общие вычисления, таблицы и каталоги, влияющие на поведение протокола;
- проверка образов прошивки и других бинарных форматов.
Порт на языке GUI содержит только:
- вызовы C через стабильный ABI (`ctypes`, JNI, P/Invoke, Swift FFI и т. п.);
- преобразование C-структур в модели языка без повторения алгоритма;
- работу с USB, COM, Bluetooth, SocketCAN и API операционной системы;
- жизненный цикл, потоки, разрешения, хранение настроек и UI;
- локализованный текст и чисто визуальные преобразования.
Порт не вычисляет CRC, не собирает wire-пакет и не разбирает его поля заново.
Если для функции C-ядро недоступно, приложение сообщает об ошибке сборки или
загрузки. Алгоритмический fallback на языке GUI запрещён: он снова создаёт две
версии протокола.
## 2. Разделение контроллеров
Профили контроллеров нельзя сливать по совпадению названия транспорта:
- **ПМ67 / TMS320F2812** — основной контроллер, собственные RS и CAN;
- **ПМ35 / TMS320F28335 periph** — отдельный контроллер и отдельный CAN для
настроечного терминала, а также собственный прямой RS232/485-протокол.
Выбор профиля выполняется в GUI, но выбранный профиль вызывает свой отдельный
модуль C-ядра. Наличие одной CAN-линии не даёт права удалить или подменить
другую.
## 3. Порядок изменения протокола
1. Добавить или изменить публичный заголовок и реализацию в
`c/set-protocol/include` и `c/set-protocol/src`.
2. Зафиксировать эталонные байты и ошибочные случаи в C-тесте.
3. При необходимости расширить `pcan_abi.h`, сохраняя бинарную совместимость.
4. Добавить тонкие порты в `ports/<platform>` и `python/`; в них не должно быть
второго кодека.
5. Одними и теми же векторами проверить C, Python и Android/JVM.
6. Собрать SETGUI и Android с одним commit submodule `templates`.
Изменение только в одном GUI считается незавершённым. Сначала меняется
`templates`, затем оба потребителя обновляют ссылку submodule на проверенный
commit.
## 4. Требования к C-ядру
- C99, без зависимости от GUI и конкретной ОС.
- Буферы и их размеры передаются явно; владение памятью остаётся у вызывающего.
- Для MCU основная логика не требует heap, исключений или файловой системы.
- Endian и размеры целых задаются через `stdint.h`, структуры wire-формата не
передаются через ABI без явного стабильного представления.
- Экспорт shared library идёт через `PCAN_ABI_API`; существующие символы не
меняют смысл и сигнатуру.
- Ошибки возвращаются детерминированным кодом и тестируются наряду с успехом.
## 5. Проверка на ревью
Изменение нельзя принимать, если ответ «да» хотя бы на один вопрос:
- появился одинаковый CRC/parser/builder в двух языках;
- UI знает byte offset, endian или служебный байт wire-протокола;
- Python и Kotlin содержат одинаковую таблицу команд, влияющую на обмен;
- добавлен тихий fallback, поведение которого отличается от C;
- обновлён один GUI без обновления и теста `templates`;
- ПМ67 и ПМ35 сведены к одному соединению или одному состоянию контроллера.
Текущее состояние и очередь переноса перечислены в
[`doc/CROSS_PLATFORM_AUDIT.md`](doc/CROSS_PLATFORM_AUDIT.md).

View File

@@ -1,17 +0,0 @@
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()

View File

@@ -1,145 +0,0 @@
# 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

@@ -1,108 +0,0 @@
#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

@@ -1,32 +0,0 @@
#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

View File

@@ -1,164 +0,0 @@
#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;
}

View File

@@ -1,69 +0,0 @@
#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

@@ -1,19 +0,0 @@
#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

@@ -1,228 +0,0 @@
#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,30 @@
cmake_minimum_required(VERSION 3.13)
project(led_indicator C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_library(led_indicator STATIC led_indicator.c)
target_include_directories(led_indicator PUBLIC .)
if(MSVC)
target_compile_options(led_indicator PRIVATE /W4)
else()
target_compile_options(led_indicator PRIVATE -Wall -Wextra -Wpedantic)
endif()
option(LED_INDICATOR_BUILD_TESTS "Собирать тесты LED indicator" ON)
if(LED_INDICATOR_BUILD_TESTS)
enable_testing()
add_executable(test_led_indicator tests/test_led_indicator.c)
target_link_libraries(test_led_indicator PRIVATE led_indicator)
add_test(NAME led_indicator COMMAND test_led_indicator)
add_executable(test_led_indicator_stm32_port
tests/test_stm32_port.c
ports/stm32-hal/led_indicator_stm32_hal.c)
target_include_directories(test_led_indicator_stm32_port PRIVATE
. ports/stm32-hal tests/fakes)
target_link_libraries(test_led_indicator_stm32_port PRIVATE led_indicator)
add_test(NAME led_indicator_stm32_port COMMAND test_led_indicator_stm32_port)
endif()

126
c/led-indicator/README.md Normal file
View File

@@ -0,0 +1,126 @@
# LED Indicator
Неблокирующая C99-библиотека для индикации состояний `STARTUP`, `WORK`,
`ACTIVITY`, `WARNING`, `ERROR` и `CRITICAL` с разными рисунками и частотами.
Один экземпляр обслуживает несколько светодиодов.
Ядро не знает о GPIO, PWM, сдвиговом регистре, RTOS и модели таймера. Порт
передаёт логическое состояние канала наружу и читает монотонное время в
миллисекундах. В библиотеке нет задержек, динамической памяти, прерываний и
изменяемого глобального состояния.
```text
приложение -> LedIndicator_SetMode/Process -> ядро -> write/now_ms -> GPIO/PWM/expander + TIM
```
## Файлы
| Файл | Назначение | Зависимости |
|---|---|---|
| `led_indicator.h/.c` | режимы, шаблоны и планировщик | C99, `stdint.h` |
| `ports/stm32-hal` | GPIO и выбранный аппаратный TIM | STM32 HAL |
| `tests/test_led_indicator.c` | host-тесты, включая переполнение `uint32_t` | libc |
## Встроенные режимы
| Режим | Сигнал |
|---|---|
| `OFF` / `ON` | постоянно выключен / включён |
| `STARTUP` | три коротких импульса, затем постоянно включён |
| `WORK` | 100 мс включён, 900 мс выключен |
| `ACTIVITY` | 50/50 мс |
| `WARNING` | 250/250 мс |
| `ERROR` | два импульса и пауза |
| `CRITICAL` | три импульса и пауза |
Частоты не зашиты в алгоритм. Скопируйте встроенную таблицу и измените
`duration_ms` до инициализации:
```c
static LedIndicator_Pattern patterns[LED_INDICATOR_MODE_COUNT];
LedIndicator_CopyDefaultPatterns(patterns, LED_INDICATOR_MODE_COUNT);
patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] = 50U;
patterns[LED_INDICATOR_MODE_WORK].duration_ms[1] = 1950U;
config.patterns = patterns;
config.pattern_count = LED_INDICATOR_MODE_COUNT;
```
Таблица должна существовать всё время работы экземпляра. До восьми шагов
описываются длительностями и битовой маской `levels`; шаблон может повторяться
или после одного прохода перейти в `final_level`.
## Контракт порта
```c
void write(void *context, uint8_t channel, uint8_t logical_on);
uint32_t now_ms(void *context);
```
`write` получает именно логический уровень. Инверсию active-low, управление
PWM или запись общего регистра расширителя выполняет порт. Переполнение
32-битной миллисекундной метки обработано разностью беззнаковых чисел.
Если время уже есть в планировщике приложения, `now_ms` можно оставить `NULL`
и вызывать варианты `LedIndicator_SetModeAt`/`LedIndicator_ProcessAt`.
## Быстрый старт без привязки к HAL
```c
static LedIndicator led;
static LedIndicator_Channel state[2];
LedIndicator_Config cfg;
LedIndicator_Port port = {Board_LedWrite, Board_TimerMs, &board};
LedIndicator_ConfigDefault(&cfg);
LedIndicator_Init(&led, state, 2U, &port, &cfg);
LedIndicator_SetMode(&led, 0U, LED_INDICATOR_MODE_WORK);
LedIndicator_SetMode(&led, 1U, LED_INDICATOR_MODE_ERROR);
for (;;) {
LedIndicator_Process(&led);
}
```
`SetMode` идемпотентен: повторный вызов того же режима в каждом проходе цикла
не начинает рисунок заново. Для нового импульса события служит
`LedIndicator_RestartMode`.
## STM32F103, STM32F4, STM32G431 и STM32G474
Порт `ports/stm32-hal` принимает конкретный `TIM_HandleTypeDef *`; библиотека
не использует `HAL_GetTick()` и не занимает SysTick. Настройте TIM как
free-running, запустите его и передайте частоту счётчика после prescaler.
Скопируйте подходящий `led_indicator_stm32_hal_config.*.template.h` в каталог
платы под именем `led_indicator_stm32_hal_config.h`.
```c
static const LedIndicator_Stm32HalOutput outputs[] = {
{STATUS_GPIO_Port, STATUS_Pin, 0U},
{ERROR_GPIO_Port, ERROR_Pin, 1U}
};
static LedIndicator_Stm32HalPort hw;
static LedIndicator_Port port;
HAL_TIM_Base_Start(&htim6); /* CNT = 1 кГц в данном примере. */
LedIndicator_Stm32HalPortInit(&hw, &htim6, 1000U,
outputs, 2U, &port);
```
`LedIndicator_Process()` должен вызываться чаще, чем переполняется выбранный
TIM. Для 16-битного CNT на 1 кГц это не реже одного раза за 65 секунд. Сам
таймер и его prescaler/period задаются в CubeMX или board-порте, а не в ядре.
Для К1921ВК028 и C28x используется тот же основной порт: `now_ms` возвращает
счётчик, увеличиваемый обработчиком выбранного TIMER/CPU Timer, а `write`
обращается к GPIO SDK. Такое разделение оставляет номер таймера и выводы в
проекте конкретной платы.
## Проверка
```sh
cmake -S c/led-indicator -B build/led-indicator
cmake --build build/led-indicator
ctest --test-dir build/led-indicator --output-on-failure
```

View File

@@ -0,0 +1,246 @@
#include "led_indicator.h"
#include <limits.h>
#define LEVELS_1 0x01U
#define LEVELS_10 0x01U
#define LEVELS_1010 0x05U
#define LEVELS_101010 0x15U
static const LedIndicator_Pattern g_default_patterns[LED_INDICATOR_MODE_COUNT] = {
{{0U}, 0U, 1U, 1U, 0U},
{{0U}, LEVELS_1, 1U, 1U, 1U},
{{100U, 100U, 100U, 100U, 100U, 100U}, LEVELS_101010, 6U, 0U, 1U},
{{100U, 900U}, LEVELS_10, 2U, 1U, 0U},
{{50U, 50U}, LEVELS_10, 2U, 1U, 0U},
{{250U, 250U}, LEVELS_10, 2U, 1U, 0U},
{{100U, 100U, 100U, 700U}, LEVELS_1010, 4U, 1U, 0U},
{{100U, 100U, 100U, 100U, 100U, 700U}, LEVELS_101010, 6U, 1U, 0U}
};
static uint8_t pattern_valid(const LedIndicator_Pattern *pattern)
{
size_t i;
uint32_t total = 0U;
if ((pattern == NULL) || (pattern->step_count == 0U) ||
(pattern->step_count > LED_INDICATOR_MAX_STEPS)) {
return 0U;
}
if (pattern->step_count == 1U) {
return 1U;
}
for (i = 0U; i < pattern->step_count; ++i) {
if ((pattern->duration_ms[i] == 0U) ||
(UINT32_MAX - total < pattern->duration_ms[i])) {
return 0U;
}
total += pattern->duration_ms[i];
}
return (uint8_t)(total != 0U);
}
static uint32_t pattern_period(const LedIndicator_Pattern *pattern)
{
size_t i;
uint32_t total = 0U;
for (i = 0U; i < pattern->step_count; ++i) {
total += pattern->duration_ms[i];
}
return total;
}
static uint8_t pattern_level(const LedIndicator_Pattern *pattern, uint32_t elapsed)
{
uint32_t position = elapsed;
uint32_t period;
size_t i;
if (pattern->step_count == 1U) {
return (uint8_t)(pattern->levels & 1U);
}
period = pattern_period(pattern);
if (pattern->repeat != 0U) {
position %= period;
} else if (position >= period) {
return (uint8_t)(pattern->final_level != 0U);
}
for (i = 0U; i < pattern->step_count; ++i) {
if (position < pattern->duration_ms[i]) {
return (uint8_t)((pattern->levels >> i) & 1U);
}
position -= pattern->duration_ms[i];
}
return (uint8_t)(pattern->final_level != 0U);
}
static void write_if_changed(LedIndicator *instance, size_t channel, uint8_t level,
uint8_t force)
{
LedIndicator_Channel *state = &instance->channels[channel];
level = (uint8_t)(level != 0U);
if ((force != 0U) || (state->output_level != level)) {
state->output_level = level;
instance->port.write(instance->port.context, (uint8_t)channel, level);
}
}
void LedIndicator_ConfigDefault(LedIndicator_Config *config)
{
if (config != NULL) {
config->patterns = g_default_patterns;
config->pattern_count = LED_INDICATOR_MODE_COUNT;
}
}
size_t LedIndicator_CopyDefaultPatterns(LedIndicator_Pattern *patterns, size_t capacity)
{
size_t i;
size_t count = (size_t)LED_INDICATOR_MODE_COUNT;
if (patterns == NULL) {
return 0U;
}
if (capacity < count) {
count = capacity;
}
for (i = 0U; i < count; ++i) {
patterns[i] = g_default_patterns[i];
}
return count;
}
uint8_t LedIndicator_ValidateConfig(const LedIndicator_Config *config)
{
size_t i;
if ((config == NULL) || (config->patterns == NULL) ||
(config->pattern_count == 0U) ||
(config->pattern_count > (size_t)LED_INDICATOR_MODE_COUNT)) {
return 0U;
}
for (i = 0U; i < config->pattern_count; ++i) {
if (pattern_valid(&config->patterns[i]) == 0U) {
return 0U;
}
}
return 1U;
}
uint8_t LedIndicator_Init(LedIndicator *instance,
LedIndicator_Channel *channels,
size_t channel_count,
const LedIndicator_Port *port,
const LedIndicator_Config *config)
{
size_t i;
if ((instance == NULL) || (channels == NULL) || (channel_count == 0U) ||
(channel_count > 256U) || (port == NULL) || (port->write == NULL) ||
(LedIndicator_ValidateConfig(config) == 0U)) {
return 0U;
}
instance->port = *port;
instance->patterns = config->patterns;
instance->pattern_count = config->pattern_count;
instance->channels = channels;
instance->channel_count = channel_count;
instance->initialized = 1U;
for (i = 0U; i < channel_count; ++i) {
channels[i].mode = LED_INDICATOR_MODE_OFF;
channels[i].started_ms = 0U;
channels[i].output_level = 0U;
channels[i].initialized = 1U;
write_if_changed(instance, i, 0U, 1U);
}
return 1U;
}
uint8_t LedIndicator_SetModeAt(LedIndicator *instance, size_t channel,
LedIndicator_Mode mode, uint32_t now_ms)
{
if ((instance == NULL) || (instance->initialized == 0U) ||
(channel >= instance->channel_count) || ((size_t)mode >= instance->pattern_count)) {
return 0U;
}
if (instance->channels[channel].mode == mode) {
return 1U;
}
return LedIndicator_RestartModeAt(instance, channel, mode, now_ms);
}
uint8_t LedIndicator_RestartModeAt(LedIndicator *instance, size_t channel,
LedIndicator_Mode mode, uint32_t now_ms)
{
LedIndicator_Channel *state;
const LedIndicator_Pattern *pattern;
if ((instance == NULL) || (instance->initialized == 0U) ||
(channel >= instance->channel_count) || ((size_t)mode >= instance->pattern_count)) {
return 0U;
}
state = &instance->channels[channel];
state->mode = mode;
state->started_ms = now_ms;
pattern = &instance->patterns[(size_t)mode];
write_if_changed(instance, channel, pattern_level(pattern, 0U), 0U);
return 1U;
}
uint8_t LedIndicator_SetMode(LedIndicator *instance, size_t channel,
LedIndicator_Mode mode)
{
if ((instance == NULL) || (instance->port.now_ms == NULL)) {
return 0U;
}
return LedIndicator_SetModeAt(instance, channel, mode,
instance->port.now_ms(instance->port.context));
}
uint8_t LedIndicator_RestartMode(LedIndicator *instance, size_t channel,
LedIndicator_Mode mode)
{
if ((instance == NULL) || (instance->port.now_ms == NULL)) {
return 0U;
}
return LedIndicator_RestartModeAt(instance, channel, mode,
instance->port.now_ms(instance->port.context));
}
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms)
{
size_t i;
if ((instance == NULL) || (instance->initialized == 0U)) {
return;
}
for (i = 0U; i < instance->channel_count; ++i) {
const LedIndicator_Channel *state = &instance->channels[i];
const LedIndicator_Pattern *pattern = &instance->patterns[(size_t)state->mode];
const uint32_t elapsed = now_ms - state->started_ms;
write_if_changed(instance, i, pattern_level(pattern, elapsed), 0U);
}
}
void LedIndicator_Process(LedIndicator *instance)
{
if ((instance != NULL) && (instance->port.now_ms != NULL)) {
LedIndicator_ProcessAt(instance, instance->port.now_ms(instance->port.context));
}
}
LedIndicator_Mode LedIndicator_GetMode(const LedIndicator *instance, size_t channel)
{
if ((instance == NULL) || (instance->initialized == 0U) ||
(channel >= instance->channel_count)) {
return LED_INDICATOR_MODE_OFF;
}
return instance->channels[channel].mode;
}

View File

@@ -0,0 +1,117 @@
/**
* @file led_indicator.h
* @brief Неблокирующая индикация состояния устройства на одном или нескольких LED.
*/
#ifndef LED_INDICATOR_H
#define LED_INDICATOR_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define LED_INDICATOR_MAX_STEPS 8U
typedef enum {
LED_INDICATOR_MODE_OFF = 0,
LED_INDICATOR_MODE_ON,
LED_INDICATOR_MODE_STARTUP,
LED_INDICATOR_MODE_WORK,
LED_INDICATOR_MODE_ACTIVITY,
LED_INDICATOR_MODE_WARNING,
LED_INDICATOR_MODE_ERROR,
LED_INDICATOR_MODE_CRITICAL,
LED_INDICATOR_MODE_COUNT
} LedIndicator_Mode;
/** Один период сигнала. Бит N в levels задаёт уровень шага N. */
typedef struct {
uint32_t duration_ms[LED_INDICATOR_MAX_STEPS];
uint8_t levels;
uint8_t step_count;
uint8_t repeat;
uint8_t final_level;
} LedIndicator_Pattern;
/**
* Аппаратный порт. write получает логический уровень, поэтому active-low,
* GPIO, PWM и регистры расширителя обрабатываются за границей ядра.
*/
typedef struct {
void (*write)(void *context, uint8_t channel, uint8_t on);
uint32_t (*now_ms)(void *context);
void *context;
} LedIndicator_Port;
typedef struct {
LedIndicator_Mode mode;
uint32_t started_ms;
uint8_t output_level;
uint8_t initialized;
} LedIndicator_Channel;
typedef struct {
const LedIndicator_Pattern *patterns;
size_t pattern_count;
} LedIndicator_Config;
typedef struct {
LedIndicator_Port port;
const LedIndicator_Pattern *patterns;
LedIndicator_Channel *channels;
size_t pattern_count;
size_t channel_count;
uint8_t initialized;
} LedIndicator;
/** Заполняет конфигурацию встроенными шаблонами режимов. */
void LedIndicator_ConfigDefault(LedIndicator_Config *config);
/** Копирует встроенные шаблоны в изменяемую таблицу; возвращает число записей. */
size_t LedIndicator_CopyDefaultPatterns(LedIndicator_Pattern *patterns, size_t capacity);
/** Проверяет таблицу шаблонов. */
uint8_t LedIndicator_ValidateConfig(const LedIndicator_Config *config);
/**
* Инициализирует экземпляр и немедленно выключает все его каналы.
* Массив channels принадлежит приложению и должен жить столько же, сколько instance.
*/
uint8_t LedIndicator_Init(LedIndicator *instance,
LedIndicator_Channel *channels,
size_t channel_count,
const LedIndicator_Port *port,
const LedIndicator_Config *config);
/** Назначает режим, используя время порта. Повтор того же режима не сбрасывает фазу. */
uint8_t LedIndicator_SetMode(LedIndicator *instance, size_t channel,
LedIndicator_Mode mode);
/** Назначает режим с явно переданной меткой времени. Повтор не сбрасывает фазу. */
uint8_t LedIndicator_SetModeAt(LedIndicator *instance, size_t channel,
LedIndicator_Mode mode, uint32_t now_ms);
/** Принудительно запускает режим с первого шага, используя время порта. */
uint8_t LedIndicator_RestartMode(LedIndicator *instance, size_t channel,
LedIndicator_Mode mode);
/** Принудительно запускает режим с первого шага в указанное время. */
uint8_t LedIndicator_RestartModeAt(LedIndicator *instance, size_t channel,
LedIndicator_Mode mode, uint32_t now_ms);
/** Обновляет выходы, используя время порта. Вызывать в главном цикле. */
void LedIndicator_Process(LedIndicator *instance);
/** Обновляет выходы с явно переданной меткой времени. */
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms);
LedIndicator_Mode LedIndicator_GetMode(const LedIndicator *instance, size_t channel);
#ifdef __cplusplus
}
#endif
#endif /* LED_INDICATOR_H */

View File

@@ -0,0 +1,65 @@
#include "led_indicator_stm32_hal.h"
static void stm32_write(void *context, uint8_t channel, uint8_t on)
{
LedIndicator_Stm32HalPort *adapter = (LedIndicator_Stm32HalPort *)context;
const LedIndicator_Stm32HalOutput *output;
GPIO_PinState state;
if ((adapter == NULL) || ((size_t)channel >= adapter->output_count)) {
return;
}
output = &adapter->outputs[channel];
state = ((on != 0U) ^ (output->active_low != 0U)) ? GPIO_PIN_SET : GPIO_PIN_RESET;
HAL_GPIO_WritePin(output->gpio, output->pin, state);
}
static uint32_t stm32_now_ms(void *context)
{
LedIndicator_Stm32HalPort *adapter = (LedIndicator_Stm32HalPort *)context;
const uint32_t current = __HAL_TIM_GET_COUNTER(adapter->timer);
const uint32_t reload = __HAL_TIM_GET_AUTORELOAD(adapter->timer);
uint32_t delta;
uint64_t scaled;
if (current >= adapter->last_counter) {
delta = current - adapter->last_counter;
} else if (reload == UINT32_MAX) {
delta = current - adapter->last_counter;
} else {
delta = (reload - adapter->last_counter) + 1U + current;
}
adapter->last_counter = current;
scaled = (uint64_t)adapter->remainder + ((uint64_t)delta * 1000ULL);
adapter->accumulated_ms += (uint32_t)(scaled / adapter->timer_tick_hz);
adapter->remainder = (uint32_t)(scaled % adapter->timer_tick_hz);
return adapter->accumulated_ms;
}
uint8_t LedIndicator_Stm32HalPortInit(LedIndicator_Stm32HalPort *adapter,
TIM_HandleTypeDef *timer,
uint32_t timer_tick_hz,
const LedIndicator_Stm32HalOutput *outputs,
size_t output_count,
LedIndicator_Port *port)
{
if ((adapter == NULL) || (timer == NULL) || (timer_tick_hz == 0U) ||
(outputs == NULL) || (output_count == 0U) || (output_count > 256U) ||
(port == NULL)) {
return 0U;
}
adapter->timer = timer;
adapter->outputs = outputs;
adapter->output_count = output_count;
adapter->timer_tick_hz = timer_tick_hz;
adapter->last_counter = __HAL_TIM_GET_COUNTER(timer);
adapter->accumulated_ms = 0U;
adapter->remainder = 0U;
port->write = stm32_write;
port->now_ms = stm32_now_ms;
port->context = adapter;
return 1U;
}

View File

@@ -0,0 +1,39 @@
/**
* @file led_indicator_stm32_hal.h
* @brief Порт LED Indicator для STM32F1/F4/G4 HAL и выбранного TIM.
*/
#ifndef LED_INDICATOR_STM32_HAL_H
#define LED_INDICATOR_STM32_HAL_H
#include "led_indicator.h"
#include "led_indicator_stm32_hal_config.h"
typedef struct {
GPIO_TypeDef *gpio;
uint16_t pin;
uint8_t active_low;
} LedIndicator_Stm32HalOutput;
typedef struct {
TIM_HandleTypeDef *timer;
const LedIndicator_Stm32HalOutput *outputs;
size_t output_count;
uint32_t timer_tick_hz;
uint32_t last_counter;
uint32_t accumulated_ms;
uint32_t remainder;
} LedIndicator_Stm32HalPort;
/**
* Создаёт порт на уже настроенном и запущенном таймере.
* timer_tick_hz — частота изменения CNT после prescaler, например 1000 Гц.
*/
uint8_t LedIndicator_Stm32HalPortInit(LedIndicator_Stm32HalPort *adapter,
TIM_HandleTypeDef *timer,
uint32_t timer_tick_hz,
const LedIndicator_Stm32HalOutput *outputs,
size_t output_count,
LedIndicator_Port *port);
#endif /* LED_INDICATOR_STM32_HAL_H */

View File

@@ -0,0 +1,6 @@
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
#define LED_INDICATOR_STM32_HAL_CONFIG_H
#include "stm32f1xx_hal.h"
#endif

View File

@@ -0,0 +1,6 @@
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
#define LED_INDICATOR_STM32_HAL_CONFIG_H
#include "stm32f4xx_hal.h"
#endif

View File

@@ -0,0 +1,6 @@
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
#define LED_INDICATOR_STM32_HAL_CONFIG_H
#include "stm32g4xx_hal.h"
#endif

View File

@@ -0,0 +1,27 @@
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
#define LED_INDICATOR_STM32_HAL_CONFIG_H
#include <stdint.h>
typedef enum {
GPIO_PIN_RESET = 0,
GPIO_PIN_SET
} GPIO_PinState;
typedef struct {
uint16_t last_pin;
GPIO_PinState last_state;
uint32_t writes;
} GPIO_TypeDef;
typedef struct {
uint32_t counter;
uint32_t autoreload;
} TIM_HandleTypeDef;
#define __HAL_TIM_GET_COUNTER(handle) ((handle)->counter)
#define __HAL_TIM_GET_AUTORELOAD(handle) ((handle)->autoreload)
void HAL_GPIO_WritePin(GPIO_TypeDef *gpio, uint16_t pin, GPIO_PinState state);
#endif

View File

@@ -0,0 +1,84 @@
#include "led_indicator.h"
#include <stdio.h>
typedef struct {
uint32_t now;
uint8_t outputs[3];
uint32_t writes[3];
} FakePort;
static void fake_write(void *context, uint8_t channel, uint8_t on)
{
FakePort *fake = (FakePort *)context;
fake->outputs[channel] = on;
fake->writes[channel]++;
}
static uint32_t fake_now(void *context)
{
return ((FakePort *)context)->now;
}
#define CHECK(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \
#condition); \
return 1; \
} \
} while (0)
int main(void)
{
LedIndicator indicator;
LedIndicator_Channel channels[3];
LedIndicator_Config config;
LedIndicator_Pattern patterns[LED_INDICATOR_MODE_COUNT];
FakePort fake = {0};
LedIndicator_Port port = {fake_write, fake_now, &fake};
LedIndicator_ConfigDefault(&config);
CHECK(LedIndicator_ValidateConfig(&config) != 0U);
CHECK(LedIndicator_CopyDefaultPatterns(patterns, LED_INDICATOR_MODE_COUNT) ==
LED_INDICATOR_MODE_COUNT);
patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] = 25U;
CHECK(patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] == 25U);
CHECK(LedIndicator_Init(&indicator, channels, 3U, &port, &config) != 0U);
CHECK(fake.writes[0] == 1U && fake.writes[1] == 1U && fake.writes[2] == 1U);
CHECK(LedIndicator_SetMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
CHECK(fake.outputs[0] == 1U);
fake.now = 99U;
CHECK(LedIndicator_SetMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
LedIndicator_Process(&indicator);
CHECK(fake.outputs[0] == 1U);
fake.now = 100U;
LedIndicator_Process(&indicator);
CHECK(fake.outputs[0] == 0U);
fake.now = 1000U;
LedIndicator_Process(&indicator);
CHECK(fake.outputs[0] == 1U);
fake.now = 1050U;
CHECK(LedIndicator_RestartMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
fake.now = 1150U;
LedIndicator_Process(&indicator);
CHECK(fake.outputs[0] == 0U);
CHECK(LedIndicator_SetModeAt(&indicator, 1U, LED_INDICATOR_MODE_ERROR,
0xFFFFFFF0UL) != 0U);
LedIndicator_ProcessAt(&indicator, 0x00000054UL);
CHECK(fake.outputs[1] == 0U); /* 100 ms после старта, переход во второй шаг. */
LedIndicator_ProcessAt(&indicator, 0x000000B8UL);
CHECK(fake.outputs[1] == 1U); /* 200 ms после старта, второй импульс. */
CHECK(LedIndicator_SetModeAt(&indicator, 2U, LED_INDICATOR_MODE_STARTUP, 10U) != 0U);
LedIndicator_ProcessAt(&indicator, 610U);
CHECK(fake.outputs[2] == 1U); /* Однократный startup завершён постоянным ON. */
CHECK(LedIndicator_SetMode(&indicator, 3U, LED_INDICATOR_MODE_ON) == 0U);
CHECK(LedIndicator_GetMode(&indicator, 0U) == LED_INDICATOR_MODE_WORK);
puts("led indicator tests passed");
return 0;
}

View File

@@ -0,0 +1,52 @@
#include "led_indicator_stm32_hal.h"
#include <stdio.h>
void HAL_GPIO_WritePin(GPIO_TypeDef *gpio, uint16_t pin, GPIO_PinState state)
{
gpio->last_pin = pin;
gpio->last_state = state;
gpio->writes++;
}
#define CHECK(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \
#condition); \
return 1; \
} \
} while (0)
int main(void)
{
GPIO_TypeDef gpio_a = {0};
GPIO_TypeDef gpio_b = {0};
TIM_HandleTypeDef timer = {100U, 65535U};
const LedIndicator_Stm32HalOutput outputs[] = {
{&gpio_a, 0x0001U, 0U},
{&gpio_b, 0x0080U, 1U}
};
LedIndicator_Stm32HalPort adapter;
LedIndicator_Port port;
CHECK(LedIndicator_Stm32HalPortInit(&adapter, &timer, 10000U,
outputs, 2U, &port) != 0U);
port.write(port.context, 0U, 1U);
CHECK(gpio_a.last_pin == 0x0001U && gpio_a.last_state == GPIO_PIN_SET);
port.write(port.context, 1U, 1U);
CHECK(gpio_b.last_pin == 0x0080U && gpio_b.last_state == GPIO_PIN_RESET);
timer.counter = 109U;
CHECK(port.now_ms(port.context) == 0U);
timer.counter = 110U;
CHECK(port.now_ms(port.context) == 1U);
timer.counter = 65530U;
(void)port.now_ms(port.context);
timer.counter = 4U;
CHECK(port.now_ms(port.context) == 6544U);
puts("led indicator STM32 port tests passed");
return 0;
}

View File

@@ -1,13 +1,16 @@
# parallel-nand
Переносимый C99-драйвер асинхронной parallel NAND x8 и окно управления через
общее адресное пространство GAS. Первый профиль — Micron `MT29F1G08ABADA`:
2048+64 байта, 64 страницы в блоке, 1024 блока.
общее адресное пространство GAS. Есть профили Micron `MT29F1G08ABADA` и
Hynix `HY27UF084G2M` (512 MiB, 2048+64 байта, 64 страницы в блоке,
4096 блоков, ID `AD DC 80 95`).
Подготовлены аппаратные порты:
- `TMS320F2812` — XINTF Zone 6;
- `STM32F407VET6` — аппаратный FSMC NAND Bank 2;
- `STM32F103RCT6` — GPIO bit-bang, так как в LQFP64 FSMC недоступен;
- `STM32F103ZET6` — аппаратный FSMC NAND Bank 3;
- `STM32G474CEU6` — GPIO bit-bang для корпуса UFQFPN-48.
Ядро не зависит от HAL и транспорта GUI. STM32 HAL используется только в
@@ -55,6 +58,12 @@ void memory_init(void)
}
```
Для Hynix на STM32F103ZET6 используйте
`parallel_nand_port_stm32f103ze()` и
`parallel_nand_hy27uf084g2m_geometry`; распиновка и ограничения приведены в
[`ports/stm32f103ze/README.md`](ports/stm32f103ze/README.md). Порт RCT6
с GPIO bit-bang сохранён как отдельный вариант для корпуса LQFP64.
Диспетчер общего адресного пространства вызывает
`parallel_nand_gas_read()`/`parallel_nand_gas_write()` для адресов
`0xE000..0xE51F`. Для STM32 с `c/set-protocol/pcan_gas` дополнительно включить
@@ -67,10 +76,28 @@ void memory_init(void)
[docs/MEMORY_GAS.md](docs/MEMORY_GAS.md). Электрические соединения — в README
соответствующего порта и [docs/EMPTY_BOARD_PINOUT.md](docs/EMPTY_BOARD_PINOUT.md).
## PROGRAM и ERASE
- `parallel_nand_program_page()` записывает полную main-область и, при
необходимости, полную OOB-область страницы.
- `parallel_nand_erase_block()` стирает один физический блок.
- Перед изменением ядро проверяет заводской bad-block marker, на время операции
снимает `WP#`, ждёт `R/B#`, проверяет fail-бит статуса и снова включает
защиту при любом результате.
- Порт обязан объявить `write_supported = 1`; иначе возвращается
`PNAND_ERROR_WRITE_PROTECTED`. Это защищает платы, где `WP#` постоянно
соединён с GND.
- Через GAS разрушительные команды требуют записи `0xA55A` в `CONFIRM`
непосредственно перед `PROGRAM_PAGE` или `ERASE_BLOCK`.
NAND можно программировать только из `1` в `0`; для повторной записи сначала
стирается весь блок. Не стирайте заводские bad-блоки и не используйте raw-запись
как файловую систему без ECC, wear leveling и защиты загрузочных блоков.
## Текущие границы
- Реализовано безопасное чтение, RESET, READ ID, STATUS и bad-block marker.
- PROGRAM/ERASE намеренно отсутствуют; ядро включает аппаратную защиту `WP#`.
- Реализованы чтение, программирование страницы, стирание блока, RESET,
READ ID, STATUS и проверка bad-block marker.
- ECC пока не исправляет данные. GUI получает физические main/OOB и должен
помечать дамп как raw.
- В полном дампе bad-блоки не пропускаются: сохраняется физический порядок.
@@ -86,3 +113,5 @@ void memory_init(void)
5. Записать номер страницы в `0xE005/0xE006`, затем `3` в `0xE004`.
6. Дождаться `READY | PAGE_VALID` в `0xE002`.
7. Прочитать main из `0xE100..0xE4FF`, OOB из `0xE500..0xE51F`.
8. PROGRAM/ERASE проверять только на заведомо расходном блоке после сохранения
полного raw-дампа.

View File

@@ -30,7 +30,7 @@ TSOP-48 (суффикс `WP`). На второй фотографии NAND —
| `CLE` | 16 | фиксация команды |
| `ALE` | 17 | фиксация адреса |
| `WE#` | 18 | строб команды/адреса от МК |
| `WP#` | 19 | для read-only 10 кОм на GND; для записи — GPIO и pull-down |
| `WP#` | 19 | GPIO МК и pull-down 10 кОм; только для чтения допустим GND через 10 кОм |
| `I/O0` | 29 | двунаправленная шина, бит 0 |
| `I/O1` | 30 | двунаправленная шина, бит 1 |
| `I/O2` | 31 | двунаправленная шина, бит 2 |
@@ -69,10 +69,12 @@ TSOP-48 (суффикс `WP`). На второй фотографии NAND —
## Первый запуск
1. Не устанавливать NAND и проверить 3,3 В, отсутствие КЗ и уровни `CE#/WE#/RE#`.
2. Установить NAND, оставить `WP#` в нуле и выполнить RESET `FFh`.
2. Установить NAND, проверить pull-down и уровень `WP# = 0`, выполнить RESET `FFh`.
3. Прочитать ID `90h`: ожидаемые первые байты для профиля — `2C F1`.
4. Считать одну страницу два раза и сравнить main/OOB побайтно.
5. Только после устойчивого чтения запускать полный дамп через GAS.
6. PROGRAM/ERASE проверять на расходном блоке после сохранения raw-дампа;
убедиться осциллографом, что `WP#` поднимается только на время операции.
## Источники для проверки footprint

View File

@@ -18,6 +18,8 @@ parallel_nand_gas
общее ядро parallel_nand
├─ TMS320F2812: XINTF
├─ STM32F103RC: GPIO
├─ STM32F103ZE: FSMC
├─ STM32F407VE: FSMC
└─ STM32G474CE: GPIO
```
@@ -77,7 +79,7 @@ ARM.
| Адрес | Доступ | Название | Значение |
|---:|:---:|---|---|
| `E000` | R | `SIGNATURE` | `0x4E44` (`ND`) |
| `E001` | R | `VERSION` | версия окна, сейчас 1 |
| `E001` | R | `VERSION` | версия окна, сейчас 2 |
| `E002` | R | `STATUS` | состояние операции |
| `E003` | R | `ERROR` | код `PNAND_*`, 0 = нет ошибки |
| `E004` | W | `COMMAND` | команда окна |
@@ -92,8 +94,9 @@ ARM.
| `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 |
| `E010` | W | `CONFIRM` | `0xA55A` непосредственно перед разрушительной командой |
| `E100..E4FF` | R/W | `PAGE_DATA` | 1024 регистра main, два байта LE |
| `E500..E51F` | R/W | `PAGE_OOB` | 32 регистра OOB, два байта LE |
Между управляющими регистрами и окнами данных есть зарезервированные адреса.
Чтение блока через них не выполняется: каждый запрос должен начинаться внутри
@@ -107,6 +110,8 @@ ARM.
| 1 | `0x0002` | `BUSY` |
| 2 | `0x0004` | `PAGE_VALID` |
| 3 | `0x0008` | `BAD_BLOCK` |
| 4 | `0x0010` | `WRITE_SUPPORTED`, порт способен управлять `WP#` |
| 5 | `0x0020` | `BUFFER_DIRTY`, буфер изменён после чтения/очистки |
| 15 | `0x8000` | `ERROR`, подробность в `E003` |
GUI читает окно данных только при `READY=1`, `PAGE_VALID=1`, `ERROR=0`.
@@ -118,6 +123,9 @@ GUI читает окно данных только при `READY=1`, `PAGE_VALI
| 1 | `RESET` | сброс NAND и ожидание Ready |
| 2 | `READ_ID` | обновляет `E00B/E00C` |
| 3 | `READ_PAGE` | загружает main/OOB выбранной страницы |
| 4 | `CLEAR_BUFFER` | заполняет main/OOB значением `0xFF` для подготовки записи |
| 5 | `PROGRAM_PAGE` | программирует выбранную страницу из буфера |
| 6 | `ERASE_BLOCK` | стирает блок, содержащий выбранную страницу |
Запись номера страницы и запуск — две отдельные операции. Сначала GUI пишет
`PAGE_LO/PAGE_HI`, затем записывает `READ_PAGE` в `COMMAND`. Нельзя отправлять
@@ -172,6 +180,28 @@ GUI хранит номер последней полностью записан
## PROGRAM/ERASE
Запись и стирание не включены намеренно. Они требуют отдельного разрешения,
проверки ECC, защиты загрузочных блоков и двухфазного подтверждения через GAS.
До добавления этих функций `WP#` удерживается активным аппаратным портом.
Разрушительные команды доступны только если установлен флаг
`WRITE_SUPPORTED`. Ядро проверяет bad-block marker и отказывается изменять
заводской плохой блок. `WP#` снимается только внутри операции и включается
обратно после проверки status fail-бита, тайм-аута или другой ошибки.
Для программирования очищенной страницы:
1. Записать `PAGE_LO/PAGE_HI`.
2. Выполнить `CLEAR_BUFFER`.
3. Записать необходимые main/OOB-регистры. Незаписанные байты останутся
`0xFF`; OOB лучше не менять без определённой ECC-разметки.
4. Записать `0xA55A` в `CONFIRM`.
5. Следующей записью без промежуточных операций отправить `PROGRAM_PAGE`.
6. Проверить `READY`, отсутствие `ERROR` и изменение `GENERATION`.
Для стирания записать любую страницу целевого блока в `PAGE_LO/PAGE_HI`, затем
`0xA55A` в `CONFIRM` и сразу `ERASE_BLOCK`. Изменение страницы, буфера или
другого управляющего регистра сбрасывает подтверждение. После успешного
стирания `PAGE_VALID` очищается.
Повторное программирование не может превратить `0` обратно в `1`: для этого
требуется стирание всего блока. Перед первым тестом сохраните полный raw-дамп,
исключите загрузочные блоки и используйте заведомо расходный исправный блок.
Библиотека не реализует ECC, wear leveling, журналирование и восстановление
после пропадания питания.

View File

@@ -16,13 +16,31 @@ extern "C" {
#define PNAND_MT29F1G08_PAGE_TOTAL \
(PNAND_MT29F1G08_PAGE_MAIN + PNAND_MT29F1G08_PAGE_OOB)
#define PNAND_HY27UF084G2M_PAGE_MAIN 2048UL
#define PNAND_HY27UF084G2M_PAGE_OOB 64UL
#define PNAND_HY27UF084G2M_PAGES_PER_BLOCK 64UL
#define PNAND_HY27UF084G2M_BLOCKS 4096UL
#define PNAND_HY27UF084G2M_COLUMN_CYCLES 2U
#define PNAND_HY27UF084G2M_ROW_CYCLES 3U
#define PNAND_HY27UF084G2M_MANUFACTURER_ID 0xADU
#define PNAND_HY27UF084G2M_DEVICE_ID 0xDCU
#define PNAND_HY27UF084G2M_ID3 0x80U
#define PNAND_HY27UF084G2M_ID4 0x95U
#define PNAND_HY27UF084G2M_PAGE_TOTAL \
(PNAND_HY27UF084G2M_PAGE_MAIN + PNAND_HY27UF084G2M_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
PNAND_ERROR_BUFFER = -5,
PNAND_ERROR_WRITE_PROTECTED = -6,
PNAND_ERROR_PROGRAM_FAILED = -7,
PNAND_ERROR_ERASE_FAILED = -8,
PNAND_ERROR_BAD_BLOCK = -9,
PNAND_ERROR_CONFIRMATION = -10
} parallel_nand_result_t;
typedef struct {
@@ -41,6 +59,7 @@ typedef struct {
} parallel_nand_t;
extern const parallel_nand_geometry_t parallel_nand_mt29f1g08_geometry;
extern const parallel_nand_geometry_t parallel_nand_hy27uf084g2m_geometry;
int parallel_nand_init(parallel_nand_t *nand,
const parallel_nand_port_t *port,
@@ -57,6 +76,12 @@ int parallel_nand_read_page(parallel_nand_t *nand, unsigned long page,
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_program_page(parallel_nand_t *nand, unsigned long page,
const pnand_octet_t *main_data,
unsigned long main_count,
const pnand_octet_t *oob,
unsigned long oob_count);
int parallel_nand_erase_block(parallel_nand_t *nand, unsigned long block);
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);

View File

@@ -30,20 +30,28 @@ enum {
PNAND_GAS_REG_DEVICE_ID = 0x000C,
PNAND_GAS_REG_BAD_BLOCK = 0x000D,
PNAND_GAS_REG_GENERATION_LO = 0x000E,
PNAND_GAS_REG_GENERATION_HI = 0x000F
PNAND_GAS_REG_GENERATION_HI = 0x000F,
PNAND_GAS_REG_CONFIRM = 0x0010
};
enum {
PNAND_GAS_CMD_NONE = 0,
PNAND_GAS_CMD_RESET = 1,
PNAND_GAS_CMD_READ_ID = 2,
PNAND_GAS_CMD_READ_PAGE = 3
PNAND_GAS_CMD_READ_PAGE = 3,
PNAND_GAS_CMD_CLEAR_BUFFER = 4,
PNAND_GAS_CMD_PROGRAM_PAGE = 5,
PNAND_GAS_CMD_ERASE_BLOCK = 6
};
#define PNAND_GAS_CONFIRM_MAGIC 0xA55AU
#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_WRITE_SUPPORTED 0x0010U
#define PNAND_GAS_STATUS_BUFFER_DIRTY 0x0020U
#define PNAND_GAS_STATUS_ERROR 0x8000U
typedef struct {
@@ -56,6 +64,7 @@ typedef struct {
unsigned long cached_block;
int cached_bad_block;
int block_cache_valid;
int destructive_armed;
pnand_octet_t id[8];
pnand_octet_t main_data[PNAND_MT29F1G08_PAGE_MAIN];
pnand_octet_t oob[PNAND_MT29F1G08_PAGE_OOB];

View File

@@ -19,6 +19,8 @@ typedef struct {
int (*is_ready)(void);
void (*set_write_protect)(int enabled);
void (*delay_us)(unsigned long us);
/* Nonzero only when set_write_protect(0) physically releases WP#. */
int write_supported;
} parallel_nand_port_t;
#ifdef __cplusplus

View File

@@ -0,0 +1,51 @@
# Порт STM32F103RCT6 для HY27UF084G2M
У `STM32F103RCT6` корпус LQFP64. Согласно datasheet STM32F103xC/D/E, функция
FSMC в этом корпусе недоступна, поэтому порт выполняет асинхронные NAND-циклы
через GPIO.
## Соединения
| HY27UF084G2M | STM32F103RCT6 | LQFP64 |
|---|---|---:|
| `I/O0..I/O7` | `PB0..PB7` | 26, 27, 28, 55, 56, 57, 58, 59 |
| `CLE` | `PC0` | 8 |
| `ALE` | `PC1` | 9 |
| `WE#` | `PC2` | 10 |
| `RE#` | `PC3` | 11 |
| `CE#` | `PC4` | 24 |
| `WP#` | `PC5` | 25 |
| `R/B#` | `PC6` | 37 |
Питание NAND — 3,3 В. На каждый вывод питания нужен 100 нФ, рядом с чипом —
14,7 мкФ. `R/B#` имеет открытый сток и требует внешней подтяжки 4,710 кОм
к 3,3 В. Не оставляйте `WP#` неподключённым.
`PB2` также является `BOOT1`: NAND держит I/O в высокоимпедансном состоянии
до командного цикла, но штатную конфигурацию загрузки платы всё равно нужно
обеспечить подтяжкой. `PB3/PB4` заняты JTAG; порт отключает JTAG и сохраняет
SWD на `PA13/PA14`.
## Подключение к проекту Cube/HAL
Добавьте `parallel_nand_port_stm32f103rc.c`, путь к его заголовку и включите
GPIO HAL. Инициализация:
```c
#include "parallel_nand.h"
#include "parallel_nand_port_stm32f103rc.h"
static parallel_nand_t nand;
int nand_init(void)
{
return parallel_nand_init(
&nand,
parallel_nand_port_stm32f103rc(),
&parallel_nand_hy27uf084g2m_geometry);
}
```
После успешной инициализации прочитайте четыре байта ID и ожидайте
`AD DC 80 95`. Порт поддерживает PROGRAM/ERASE: ядро держит `WP#` в активном
низком уровне между операциями и поднимает его только на время изменения NAND.

View File

@@ -0,0 +1,143 @@
#include "parallel_nand_port_stm32f103rc.h"
#include "stm32f1xx_hal.h"
/* STM32F103RCT6 LQFP64: FSMC is unavailable, so use GPIO bit-bang. */
#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->CRL = 0x44444444UL; /* PB0..PB7: floating input. */
data_is_output = 0;
}
}
static void data_output(void)
{
if (!data_is_output) {
GPIOB->CRL = 0x33333333UL; /* PB0..PB7: 50 MHz push-pull. */
data_is_output = 1;
}
}
static void short_delay(void)
{
__NOP(); __NOP(); __NOP(); __NOP();
}
static int f103rc_init(void)
{
GPIO_InitTypeDef io;
__HAL_RCC_AFIO_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
/* Latch inactive control levels while pins are still inputs. */
GPIOC->BSRR = WE_PIN | RE_PIN | CE_PIN |
((uint32_t)(CLE_PIN | ALE_PIN | WP_PIN) << 16);
/* PB3/PB4 carry I/O3/I/O4. Keep SWD, release only the JTAG pins. */
__HAL_AFIO_REMAP_SWJ_NOJTAG();
GPIOB->BSRR = DATA_MASK;
GPIOB->CRL = 0x33333333UL;
data_is_output = 1;
data_input();
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_HIGH;
HAL_GPIO_Init(GPIOC, &io);
io.Pin = RB_PIN;
io.Mode = GPIO_MODE_INPUT;
io.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOC, &io);
return 0;
}
static void f103rc_select(void) { GPIOC->BRR = CE_PIN; }
static void f103rc_deselect(void) { GPIOC->BSRR = CE_PIN; }
static void write_cycle(pnand_octet_t value)
{
data_output();
GPIOB->BRR = DATA_MASK;
GPIOB->BSRR = (uint32_t)value & DATA_MASK;
short_delay();
GPIOC->BRR = WE_PIN;
short_delay();
GPIOC->BSRR = WE_PIN;
short_delay();
}
static void f103rc_write_command(pnand_octet_t value)
{
GPIOC->BSRR = CLE_PIN;
GPIOC->BRR = ALE_PIN;
write_cycle(value);
GPIOC->BRR = CLE_PIN;
}
static void f103rc_write_address(pnand_octet_t value)
{
GPIOC->BSRR = ALE_PIN;
GPIOC->BRR = CLE_PIN;
write_cycle(value);
GPIOC->BRR = ALE_PIN;
}
static void f103rc_write_data(pnand_octet_t value)
{
GPIOC->BRR = CLE_PIN | ALE_PIN;
write_cycle(value);
}
static pnand_octet_t f103rc_read_data(void)
{
pnand_octet_t value;
data_input();
GPIOC->BRR = CLE_PIN | ALE_PIN | RE_PIN;
short_delay();
value = (pnand_octet_t)(GPIOB->IDR & DATA_MASK);
GPIOC->BSRR = RE_PIN;
short_delay();
return value;
}
static int f103rc_is_ready(void) { return (GPIOC->IDR & RB_PIN) != 0U; }
static void f103rc_set_wp(int enabled)
{
if (enabled) GPIOC->BRR = WP_PIN;
else GPIOC->BSRR = WP_PIN;
}
static void f103rc_delay_us(unsigned long us)
{
uint32_t cycles = (SystemCoreClock / 3000000UL) * (uint32_t)us;
while (cycles-- != 0U) __NOP();
}
static const parallel_nand_port_t f103rc_port = {
f103rc_init, f103rc_select, f103rc_deselect,
f103rc_write_command, f103rc_write_address, f103rc_write_data,
f103rc_read_data, f103rc_is_ready, f103rc_set_wp, f103rc_delay_us,
1
};
const parallel_nand_port_t *parallel_nand_port_stm32f103rc(void)
{
return &f103rc_port;
}

View File

@@ -0,0 +1,16 @@
#ifndef PARALLEL_NAND_PORT_STM32F103RC_H
#define PARALLEL_NAND_PORT_STM32F103RC_H
#include "parallel_nand_port.h"
#ifdef __cplusplus
extern "C" {
#endif
const parallel_nand_port_t *parallel_nand_port_stm32f103rc(void);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,62 @@
# Порт STM32F103ZET6 для HY27UF084G2M
Порт использует аппаратный контроллер `FSMC NAND Bank 3`, 8-битную шину и
окно памяти `0x70000000`. Сигналы `CE#`, `WE#`, `RE#`, `CLE` и `ALE`
формируются FSMC. Защита записи `WP#` управляется отдельным GPIO `PG8`.
## Соединения
| HY27UF084G2M | STM32F103ZET6 | LQFP144 | Функция FSMC |
|---|---|---:|---|
| `I/O0` | `PD14` | 85 | `D0` |
| `I/O1` | `PD15` | 86 | `D1` |
| `I/O2` | `PD0` | 114 | `D2` |
| `I/O3` | `PD1` | 115 | `D3` |
| `I/O4` | `PE7` | 58 | `D4` |
| `I/O5` | `PE8` | 59 | `D5` |
| `I/O6` | `PE9` | 60 | `D6` |
| `I/O7` | `PE10` | 63 | `D7` |
| `CLE` | `PD11` | 80 | `A16/CLE` |
| `ALE` | `PD12` | 81 | `A17/ALE` |
| `WE#` | `PD5` | 119 | `NWE` |
| `RE#` | `PD4` | 118 | `NOE` |
| `CE#` | `PD7` | 123 | `NCE2` |
| `R/B#` | `PD6` | 122 | вход GPIO (`NWAIT`) |
| `WP#` | `PG8` | 93 | выход GPIO |
`R/B#` имеет открытый сток и требует внешней подтяжки 4,710 кОм к 3,3 В.
Питание NAND — 3,3 В; возле каждой пары питания нужен 100 нФ, рядом с
микросхемой — 14,7 мкФ. Все номера выводов MCU указаны для вида сверху.
## Подключение к STM32Cube HAL
В `stm32f1xx_hal_conf.h` должны быть включены `HAL_NAND_MODULE_ENABLED` и
`HAL_GPIO_MODULE_ENABLED`. В проект добавляются:
- `parallel_nand_port_stm32f103ze.c`;
- `stm32f1xx_hal_nand.c`;
- `stm32f1xx_ll_fsmc.c`.
Инициализация переносимой библиотеки:
```c
#include "parallel_nand.h"
#include "parallel_nand_port_stm32f103ze.h"
static parallel_nand_t nand;
int nand_init(void)
{
return parallel_nand_init(
&nand,
parallel_nand_port_stm32f103ze(),
&parallel_nand_hy27uf084g2m_geometry);
}
```
Начальные тайминги намеренно консервативны. После запуска проверьте
осциллографом длительности `WE#`/`RE#` и время установки данных. Ожидаемый ID
HY27UF084G2M: `AD DC 80 95`.
Порт поддерживает PROGRAM/ERASE. `WP#` на `PG8` остаётся активным между
операциями и снимается ядром только на время программирования или стирания.

View File

@@ -0,0 +1,184 @@
#include "parallel_nand_port_stm32f103ze.h"
#include "stm32f1xx_hal.h"
#include "stm32f1xx_hal_nand.h"
/* STM32F103 names this block FSMC NAND Bank 3. Its HAL memory window is
* NAND_DEVICE1; each access makes FSMC assert PD7/FSMC_NCE2 automatically. */
#define PNAND_F103ZE_DATA_ADDRESS 0x70000000UL
#define PNAND_F103ZE_COMMAND_AREA (1UL << 16U)
#define PNAND_F103ZE_ADDRESS_AREA (1UL << 17U)
/* WP# is not generated by FSMC, so it is controlled as an ordinary GPIO. */
#define PNAND_F103ZE_WP_PORT GPIOG
#define PNAND_F103ZE_WP_PIN GPIO_PIN_8
/* HAL owns the FSMC registers through this handle during initialization. */
static NAND_HandleTypeDef nand_handle;
/** Configure all FSMC signals that belong to GPIO port D. */
static void configure_fsmc_gpio_d(void)
{
GPIO_InitTypeDef pin = {0};
/* D0..D3, NOE, NWE, NCE2, CLE and ALE are driven by FSMC. On STM32F1
* alternate-function output mode is selected without an AF number. */
pin.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5 |
GPIO_PIN_7 | GPIO_PIN_11 | GPIO_PIN_12 |
GPIO_PIN_14 | GPIO_PIN_15;
pin.Mode = GPIO_MODE_AF_PP;
pin.Pull = GPIO_NOPULL;
pin.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOD, &pin);
/* R/B# is read directly. Its open-drain output needs an external
* 4.7..10 kOhm pull-up to 3.3 V. */
pin.Pin = GPIO_PIN_6;
pin.Mode = GPIO_MODE_INPUT;
pin.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOD, &pin);
}
/** Configure FSMC data bits D4..D7 on GPIO port E. */
static void configure_fsmc_gpio_e(void)
{
GPIO_InitTypeDef pin = {0};
pin.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10;
pin.Mode = GPIO_MODE_AF_PP;
pin.Pull = GPIO_NOPULL;
pin.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOE, &pin);
}
/** Configure the explicit write-protect output and assert protection. */
static void configure_write_protect(void)
{
GPIO_InitTypeDef pin = {0};
/* Set the safe active-low level before changing the pad to an output. */
HAL_GPIO_WritePin(PNAND_F103ZE_WP_PORT, PNAND_F103ZE_WP_PIN,
GPIO_PIN_RESET);
pin.Pin = PNAND_F103ZE_WP_PIN;
pin.Mode = GPIO_MODE_OUTPUT_PP;
pin.Pull = GPIO_NOPULL;
pin.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(PNAND_F103ZE_WP_PORT, &pin);
}
/** Initialize the STM32F1 HAL NAND layer with conservative 72 MHz timings. */
static int f103ze_init(void)
{
FSMC_NAND_PCC_TimingTypeDef common_timing = {0};
FSMC_NAND_PCC_TimingTypeDef attribute_timing = {0};
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
__HAL_RCC_FSMC_CLK_ENABLE();
configure_fsmc_gpio_d();
configure_fsmc_gpio_e();
configure_write_protect();
nand_handle.Instance = FSMC_NAND_DEVICE;
nand_handle.Init.NandBank = FSMC_NAND_BANK3;
nand_handle.Init.Waitfeature = FSMC_NAND_PCC_WAIT_FEATURE_DISABLE;
nand_handle.Init.MemoryDataWidth = FSMC_NAND_PCC_MEM_BUS_WIDTH_8;
nand_handle.Init.EccComputation = FSMC_NAND_ECC_DISABLE;
nand_handle.Init.ECCPageSize = FSMC_NAND_ECC_PAGE_SIZE_2048BYTE;
nand_handle.Init.TCLRSetupTime = 2U;
nand_handle.Init.TARSetupTime = 2U;
/* One FSMC tick is about 13.9 ns at HCLK=72 MHz. These deliberately
* relaxed values are suitable for first bring-up and signal probing. */
common_timing.SetupTime = 2U;
common_timing.WaitSetupTime = 5U;
common_timing.HoldSetupTime = 3U;
common_timing.HiZSetupTime = 2U;
attribute_timing = common_timing;
return HAL_NAND_Init(&nand_handle, &common_timing, &attribute_timing) ==
HAL_OK
? 0
: -1;
}
static void f103ze_select(void)
{
/* A memory-window access asserts NCE2; no separate software GPIO exists. */
}
static void f103ze_deselect(void)
{
/* NCE2 is released automatically when the FSMC access finishes. */
}
static void f103ze_write_command(pnand_octet_t value)
{
*(__IO uint8_t *)(PNAND_F103ZE_DATA_ADDRESS |
PNAND_F103ZE_COMMAND_AREA) = (uint8_t)value;
__DSB(); /* Complete the external bus write before the next NAND cycle. */
}
static void f103ze_write_address(pnand_octet_t value)
{
*(__IO uint8_t *)(PNAND_F103ZE_DATA_ADDRESS |
PNAND_F103ZE_ADDRESS_AREA) = (uint8_t)value;
__DSB();
}
static void f103ze_write_data(pnand_octet_t value)
{
*(__IO uint8_t *)PNAND_F103ZE_DATA_ADDRESS = (uint8_t)value;
__DSB();
}
static pnand_octet_t f103ze_read_data(void)
{
uint8_t value = *(__IO uint8_t *)PNAND_F103ZE_DATA_ADDRESS;
__DSB();
return (pnand_octet_t)value;
}
static int f103ze_is_ready(void)
{
return HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_6) == GPIO_PIN_SET;
}
static void f103ze_set_write_protect(int enabled)
{
/* NAND WP# is active low: RESET protects, SET permits program/erase. */
HAL_GPIO_WritePin(PNAND_F103ZE_WP_PORT, PNAND_F103ZE_WP_PIN,
enabled ? GPIO_PIN_RESET : GPIO_PIN_SET);
}
static void f103ze_delay_us(unsigned long us)
{
/* Approximate delay used only around reset/status polling. The factor
* accounts for loop instructions in addition to the NOP. */
uint32_t cycles = (SystemCoreClock / 3000000UL) * (uint32_t)us;
while (cycles-- != 0U) {
__NOP();
}
}
static const parallel_nand_port_t f103ze_port = {
f103ze_init,
f103ze_select,
f103ze_deselect,
f103ze_write_command,
f103ze_write_address,
f103ze_write_data,
f103ze_read_data,
f103ze_is_ready,
f103ze_set_write_protect,
f103ze_delay_us,
1
};
const parallel_nand_port_t *parallel_nand_port_stm32f103ze(void)
{
return &f103ze_port;
}

View File

@@ -0,0 +1,16 @@
#ifndef PARALLEL_NAND_PORT_STM32F103ZE_H
#define PARALLEL_NAND_PORT_STM32F103ZE_H
#include "parallel_nand_port.h"
#ifdef __cplusplus
extern "C" {
#endif
const parallel_nand_port_t *parallel_nand_port_stm32f103ze(void);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -24,3 +24,8 @@
`stm32f4xx_ll_fsmc.c`, включить FSMC и не создавать второй
`NAND_HandleTypeDef` для Bank 2. Начальные тайминги
консервативны; финальные значения подтвердить осциллографом.
В базовой схеме `WP#` аппаратно зафиксирован в активном уровне, поэтому порт
объявляет `write_supported = 0`, а PROGRAM/ERASE возвращают
`PNAND_ERROR_WRITE_PROTECTED`. Для записи назначьте отдельный GPIO с внешним
pull-down, реализуйте управление `WP#` и включите признак поддержки порта.

View File

@@ -84,7 +84,8 @@ static void f407_delay_us(unsigned long us)
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
f407_read_data, f407_is_ready, f407_set_wp, f407_delay_us,
0
};
const parallel_nand_port_t *parallel_nand_port_stm32f407ve(void)

View File

@@ -19,3 +19,6 @@ HAL только для инициализации, а циклы NAND выпо
Выбранные `PB3/PB4` нельзя одновременно использовать для trace/JTAG; SWD на
`PA13/PA14` остаётся доступен. Если эти пины нужны проекту, измените таблицу и
операции шины: текущая оптимизация рассчитана на непрерывные `PB0..PB7`.
Порт поддерживает PROGRAM/ERASE через управляемый `WP#` на `PA5`. Внешний
pull-down сохраняет защиту до инициализации GPIO и при сбросе МК.

View File

@@ -125,7 +125,8 @@ static void g474_delay_us(unsigned long us)
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
g474_read_data, g474_is_ready, g474_set_wp, g474_delay_us,
1
};
const parallel_nand_port_t *parallel_nand_port_stm32g474ce(void)

View File

@@ -13,9 +13,10 @@
Адреса C28x задаются в 16-битных словах: data `0x100000`, command
`0x100001`, address `0x100002`. Используется только младший байт XINTF.
`WP#` в базовом варианте аппаратный: для режима только чтения соединить с GND
через 10 кОм; если позже понадобится PROGRAM/ERASE, заменить на GPIO и реализовать
`tms_set_wp()`.
`WP#` в базовом варианте аппаратный и соединён с GND через 10 кОм. Поэтому
порт объявляет `write_supported = 0`, а PROGRAM/ERASE возвращают
`PNAND_ERROR_WRITE_PROTECTED`. Для записи нужно перенести `WP#` на GPIO,
реализовать `tms_set_wp()` и установить признак поддержки в структуре порта.
Задержка в порте приблизительная. После выбора частоты CPU проверить импульсы
осциллографом и при необходимости изменить `PNAND_TMS_CPU_MHZ`.

View File

@@ -56,7 +56,8 @@ static void tms_set_wp(int 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
tms_read_data, tms_is_ready, tms_set_wp, tms_delay_us,
0
};
const parallel_nand_port_t *parallel_nand_port_tms320f2812(void)

View File

@@ -4,8 +4,15 @@
#define PNAND_CMD_READ_2 0x30U
#define PNAND_CMD_READ_ID 0x90U
#define PNAND_CMD_READ_STATUS 0x70U
#define PNAND_CMD_PROGRAM_1 0x80U
#define PNAND_CMD_PROGRAM_2 0x10U
#define PNAND_CMD_ERASE_1 0x60U
#define PNAND_CMD_ERASE_2 0xD0U
#define PNAND_CMD_RESET 0xFFU
#define PNAND_STATUS_FAIL 0x01U
#define PNAND_STATUS_WP 0x80U
const parallel_nand_geometry_t parallel_nand_mt29f1g08_geometry = {
PNAND_MT29F1G08_PAGE_MAIN,
PNAND_MT29F1G08_PAGE_OOB,
@@ -16,6 +23,16 @@ const parallel_nand_geometry_t parallel_nand_mt29f1g08_geometry = {
1000000UL
};
const parallel_nand_geometry_t parallel_nand_hy27uf084g2m_geometry = {
PNAND_HY27UF084G2M_PAGE_MAIN,
PNAND_HY27UF084G2M_PAGE_OOB,
PNAND_HY27UF084G2M_PAGES_PER_BLOCK,
PNAND_HY27UF084G2M_BLOCKS,
PNAND_HY27UF084G2M_COLUMN_CYCLES,
PNAND_HY27UF084G2M_ROW_CYCLES,
1000000UL
};
static int port_valid(const parallel_nand_port_t *port)
{
return port != 0 && port->init != 0 && port->select != 0 &&
@@ -57,6 +74,8 @@ int parallel_nand_reset(parallel_nand_t *nand)
nand->port->select();
nand->port->write_command(PNAND_CMD_RESET);
nand->port->deselect();
/* R/B# may not fall until tWB after the command cycle. */
nand->port->delay_us(1UL);
return wait_ready(nand);
}
@@ -70,7 +89,7 @@ int parallel_nand_init(parallel_nand_t *nand,
nand->geometry = *geometry;
if (port->init() != 0) return PNAND_ERROR_PORT;
port->deselect();
/* Ядро пока только читает: аппаратно запрещаем PROGRAM/ERASE. */
/* WP# stays asserted except for the shortest possible mutation window. */
port->set_write_protect(1);
return parallel_nand_reset(nand);
}
@@ -122,6 +141,7 @@ static int begin_page_read(parallel_nand_t *nand, unsigned long page,
}
nand->port->write_command(PNAND_CMD_READ_2);
nand->port->deselect();
nand->port->delay_us(1UL); /* Cover tWB before polling R/B#. */
return wait_ready(nand);
}
@@ -167,6 +187,101 @@ int parallel_nand_read_page(parallel_nand_t *nand, unsigned long page,
return PNAND_OK;
}
static void write_address_cycles(parallel_nand_t *nand,
unsigned long value,
unsigned int count)
{
unsigned int i;
for (i = 0U; i < count; i++) {
nand->port->write_address((pnand_octet_t)(value & 0xFFUL));
value >>= 8;
}
}
static int mutation_supported(const parallel_nand_t *nand)
{
return nand != 0 && port_valid(nand->port) &&
nand->port->write_data != 0 &&
nand->port->write_supported != 0;
}
static int finish_mutation(parallel_nand_t *nand, int failure_result)
{
pnand_octet_t status = 0U;
int result = wait_ready(nand);
if (result == PNAND_OK)
result = parallel_nand_read_status(nand, &status);
nand->port->set_write_protect(1);
if (result != PNAND_OK) return result;
if ((status & PNAND_STATUS_WP) == 0U)
return PNAND_ERROR_WRITE_PROTECTED;
return (status & PNAND_STATUS_FAIL) != 0U ? failure_result : PNAND_OK;
}
int parallel_nand_program_page(parallel_nand_t *nand, unsigned long page,
const pnand_octet_t *main_data,
unsigned long main_count,
const pnand_octet_t *oob,
unsigned long oob_count)
{
unsigned long i;
unsigned long block;
int bad = 0;
int result;
if (nand == 0 || main_data == 0) return PNAND_ERROR_ARGUMENT;
if (!mutation_supported(nand)) return PNAND_ERROR_WRITE_PROTECTED;
if (page >= parallel_nand_page_count(nand)) return PNAND_ERROR_RANGE;
if (main_count != nand->geometry.page_main_size)
return PNAND_ERROR_BUFFER;
if ((oob == 0 && oob_count != 0UL) ||
(oob != 0 && oob_count != nand->geometry.page_oob_size))
return PNAND_ERROR_BUFFER;
block = page / nand->geometry.pages_per_block;
result = parallel_nand_is_bad_block(nand, block, &bad);
if (result != PNAND_OK) return result;
if (bad) return PNAND_ERROR_BAD_BLOCK;
nand->port->set_write_protect(0);
nand->port->delay_us(1UL);
nand->port->select();
nand->port->write_command(PNAND_CMD_PROGRAM_1);
write_address_cycles(nand, 0UL, nand->geometry.column_cycles);
write_address_cycles(nand, page, nand->geometry.row_cycles);
for (i = 0UL; i < main_count; i++)
nand->port->write_data(main_data[i] & 0xFFU);
for (i = 0UL; i < oob_count; i++)
nand->port->write_data(oob[i] & 0xFFU);
nand->port->write_command(PNAND_CMD_PROGRAM_2);
nand->port->deselect();
nand->port->delay_us(1UL); /* Cover tWB before polling R/B#. */
return finish_mutation(nand, PNAND_ERROR_PROGRAM_FAILED);
}
int parallel_nand_erase_block(parallel_nand_t *nand, unsigned long block)
{
unsigned long first_page;
int bad = 0;
int result;
if (nand == 0) return PNAND_ERROR_ARGUMENT;
if (!mutation_supported(nand)) return PNAND_ERROR_WRITE_PROTECTED;
if (block >= nand->geometry.block_count) return PNAND_ERROR_RANGE;
result = parallel_nand_is_bad_block(nand, block, &bad);
if (result != PNAND_OK) return result;
if (bad) return PNAND_ERROR_BAD_BLOCK;
first_page = block * nand->geometry.pages_per_block;
nand->port->set_write_protect(0);
nand->port->delay_us(1UL);
nand->port->select();
nand->port->write_command(PNAND_CMD_ERASE_1);
write_address_cycles(nand, first_page, nand->geometry.row_cycles);
nand->port->write_command(PNAND_CMD_ERASE_2);
nand->port->deselect();
nand->port->delay_us(1UL); /* Cover tWB before polling R/B#. */
return finish_mutation(nand, PNAND_ERROR_ERASE_FAILED);
}
int parallel_nand_is_bad_block(parallel_nand_t *nand, unsigned long block,
int *is_bad)
{

View File

@@ -6,6 +6,20 @@ static unsigned int pack_pair(const pnand_octet_t *data, unsigned int index)
((data[index * 2U + 1U] & 0xFFU) << 8));
}
static void unpack_pair(pnand_octet_t *data, unsigned int index,
unsigned int value)
{
data[index * 2U] = (pnand_octet_t)(value & 0xFFU);
data[index * 2U + 1U] = (pnand_octet_t)((value >> 8) & 0xFFU);
}
static unsigned int persistent_status(const parallel_nand_gas_t *gas)
{
return gas->nand->port->write_supported != 0
? PNAND_GAS_STATUS_WRITE_SUPPORTED
: 0U;
}
parallel_nand_gas_result_t parallel_nand_gas_read(
parallel_nand_gas_t *gas, unsigned int address, unsigned int *value)
{
@@ -13,11 +27,16 @@ parallel_nand_gas_result_t parallel_nand_gas_read(
const parallel_nand_geometry_t *g;
if (gas == 0 || value == 0 || address < gas->base)
return PNAND_GAS_REJECTED;
/* Confirmation authorizes exactly the immediately following write. */
gas->destructive_armed = 0;
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 ((unsigned long)(offset - PNAND_GAS_DATA_OFFSET) * 2UL >=
g->page_main_size)
return PNAND_GAS_NO_REG;
if ((gas->status & PNAND_GAS_STATUS_PAGE_VALID) == 0U)
return PNAND_GAS_REJECTED;
*value = pack_pair(gas->main_data,
@@ -26,6 +45,9 @@ parallel_nand_gas_result_t parallel_nand_gas_read(
}
if (offset >= PNAND_GAS_OOB_OFFSET &&
offset < PNAND_GAS_OOB_OFFSET + PNAND_GAS_OOB_REGS) {
if ((unsigned long)(offset - PNAND_GAS_OOB_OFFSET) * 2UL >=
g->page_oob_size)
return PNAND_GAS_NO_REG;
if ((gas->status & PNAND_GAS_STATUS_PAGE_VALID) == 0U)
return PNAND_GAS_REJECTED;
*value = pack_pair(gas->oob,
@@ -34,7 +56,7 @@ parallel_nand_gas_result_t parallel_nand_gas_read(
}
switch (offset) {
case PNAND_GAS_REG_SIGNATURE: *value = 0x4E44U; break; /* ND */
case PNAND_GAS_REG_VERSION: *value = 0x0001U; break;
case PNAND_GAS_REG_VERSION: *value = 0x0002U; 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;
@@ -50,6 +72,7 @@ parallel_nand_gas_result_t parallel_nand_gas_read(
*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;
case PNAND_GAS_REG_CONFIRM: *value = 0U; break;
default: return PNAND_GAS_NO_REG;
}
*value &= 0xFFFFU;
@@ -60,7 +83,7 @@ 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;
gas->status |= PNAND_GAS_STATUS_READY | persistent_status(gas);
if (result != PNAND_OK) gas->status |= PNAND_GAS_STATUS_ERROR;
}
@@ -69,7 +92,11 @@ static parallel_nand_gas_result_t run_command(parallel_nand_gas_t *gas,
{
int result = PNAND_OK;
int bad = 0;
gas->status = PNAND_GAS_STATUS_BUSY;
int armed = gas->destructive_armed;
unsigned int i;
unsigned int previous_status = gas->status;
gas->destructive_armed = 0;
gas->status = PNAND_GAS_STATUS_BUSY | persistent_status(gas);
if (command == PNAND_GAS_CMD_RESET) {
result = parallel_nand_reset(gas->nand);
} else if (command == PNAND_GAS_CMD_READ_ID) {
@@ -101,8 +128,59 @@ static parallel_nand_gas_result_t run_command(parallel_nand_gas_t *gas,
if (result == PNAND_OK && bad)
gas->status |= PNAND_GAS_STATUS_BAD_BLOCK;
}
} else if (command == PNAND_GAS_CMD_CLEAR_BUFFER) {
for (i = 0U; i < PNAND_MT29F1G08_PAGE_MAIN; i++)
gas->main_data[i] = 0xFFU;
for (i = 0U; i < PNAND_MT29F1G08_PAGE_OOB; i++)
gas->oob[i] = 0xFFU;
gas->status |= PNAND_GAS_STATUS_PAGE_VALID |
PNAND_GAS_STATUS_BUFFER_DIRTY;
gas->generation++;
} else if (command == PNAND_GAS_CMD_PROGRAM_PAGE) {
if (!armed ||
(previous_status & PNAND_GAS_STATUS_PAGE_VALID) == 0U) {
gas->status |= previous_status &
(PNAND_GAS_STATUS_PAGE_VALID |
PNAND_GAS_STATUS_BUFFER_DIRTY |
PNAND_GAS_STATUS_BAD_BLOCK);
finish(gas, PNAND_ERROR_CONFIRMATION);
return PNAND_GAS_REJECTED;
}
result = parallel_nand_program_page(
gas->nand, gas->selected_page,
gas->main_data, gas->nand->geometry.page_main_size,
gas->oob, gas->nand->geometry.page_oob_size);
gas->status |= PNAND_GAS_STATUS_PAGE_VALID;
if (result == PNAND_OK) {
gas->generation++;
gas->block_cache_valid = 0;
} else {
gas->status = PNAND_GAS_STATUS_READY;
gas->status |= PNAND_GAS_STATUS_BUFFER_DIRTY;
}
} else if (command == PNAND_GAS_CMD_ERASE_BLOCK) {
unsigned long block;
if (!armed ||
gas->selected_page >= parallel_nand_page_count(gas->nand)) {
finish(gas, !armed ? PNAND_ERROR_CONFIRMATION :
PNAND_ERROR_RANGE);
return PNAND_GAS_REJECTED;
}
block = gas->selected_page / gas->nand->geometry.pages_per_block;
result = parallel_nand_erase_block(gas->nand, block);
if (result == PNAND_OK) {
gas->status &= (0xFFFFU ^ (PNAND_GAS_STATUS_PAGE_VALID |
PNAND_GAS_STATUS_BUFFER_DIRTY |
PNAND_GAS_STATUS_BAD_BLOCK));
gas->generation++;
gas->block_cache_valid = 0;
} else {
gas->status |= previous_status &
(PNAND_GAS_STATUS_PAGE_VALID |
PNAND_GAS_STATUS_BUFFER_DIRTY |
PNAND_GAS_STATUS_BAD_BLOCK);
}
} else {
gas->status = PNAND_GAS_STATUS_READY | persistent_status(gas);
return PNAND_GAS_REJECTED;
}
finish(gas, result);
@@ -119,18 +197,47 @@ parallel_nand_gas_result_t parallel_nand_gas_write(
if ((gas->status & PNAND_GAS_STATUS_BUSY) != 0U)
return PNAND_GAS_REJECTED;
value &= 0xFFFFU;
if (offset >= PNAND_GAS_DATA_OFFSET &&
offset < PNAND_GAS_DATA_OFFSET + PNAND_GAS_DATA_REGS) {
unsigned int index = offset - PNAND_GAS_DATA_OFFSET;
if ((gas->status & PNAND_GAS_STATUS_PAGE_VALID) == 0U ||
(unsigned long)index * 2UL >= gas->nand->geometry.page_main_size)
return PNAND_GAS_REJECTED;
gas->destructive_armed = 0;
unpack_pair(gas->main_data, index, value);
gas->status |= PNAND_GAS_STATUS_BUFFER_DIRTY;
return PNAND_GAS_OK;
}
if (offset >= PNAND_GAS_OOB_OFFSET &&
offset < PNAND_GAS_OOB_OFFSET + PNAND_GAS_OOB_REGS) {
unsigned int index = offset - PNAND_GAS_OOB_OFFSET;
if ((gas->status & PNAND_GAS_STATUS_PAGE_VALID) == 0U ||
(unsigned long)index * 2UL >= gas->nand->geometry.page_oob_size)
return PNAND_GAS_REJECTED;
gas->destructive_armed = 0;
unpack_pair(gas->oob, index, value);
gas->status |= PNAND_GAS_STATUS_BUFFER_DIRTY;
return PNAND_GAS_OK;
}
switch (offset) {
case PNAND_GAS_REG_COMMAND: return run_command(gas, value);
case PNAND_GAS_REG_CONFIRM:
gas->destructive_armed = value == PNAND_GAS_CONFIRM_MAGIC;
return gas->destructive_armed ? PNAND_GAS_OK : PNAND_GAS_REJECTED;
case PNAND_GAS_REG_PAGE_LO:
gas->destructive_armed = 0;
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->destructive_armed = 0;
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;
default:
gas->destructive_armed = 0;
return PNAND_GAS_READ_ONLY;
}
}
@@ -140,12 +247,17 @@ void parallel_nand_gas_init(parallel_nand_gas_t *gas,
unsigned int i;
gas->nand = nand;
gas->base = base;
gas->status = PNAND_GAS_STATUS_READY;
gas->status = PNAND_GAS_STATUS_READY | persistent_status(gas);
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;
gas->destructive_armed = 0;
for (i = 0U; i < 8U; i++) gas->id[i] = 0U;
for (i = 0U; i < PNAND_MT29F1G08_PAGE_MAIN; i++)
gas->main_data[i] = 0xFFU;
for (i = 0U; i < PNAND_MT29F1G08_PAGE_OOB; i++)
gas->oob[i] = 0xFFU;
}

View File

@@ -5,7 +5,7 @@
#include "parallel_nand_gas.h"
typedef struct { char kind; unsigned int value; } event_t;
static event_t events[64];
static event_t events[256];
static unsigned int event_count;
static pnand_octet_t input_data[32];
static unsigned int input_count;
@@ -33,17 +33,35 @@ 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
fake_read, fake_ready, fake_wp, fake_delay, 1
};
static void clear_events(void) { event_count = 0U; }
static unsigned int find_event(unsigned int start, char kind,
unsigned int value)
{
unsigned int i;
for (i = start; i < event_count; i++) {
if (events[i].kind == kind && events[i].value == value) return i;
}
return event_count;
}
int main(void)
{
parallel_nand_t nand;
pnand_octet_t id[2];
pnand_octet_t bytes[2];
pnand_octet_t main_data[2] = {0xAAU, 0x55U};
pnand_octet_t oob[2] = {0xFFU, 0xFEU};
parallel_nand_gas_t gas;
parallel_nand_geometry_t small_geometry = {
2UL, 2UL, 2UL, 4UL, 2U, 2U, 100UL
};
parallel_nand_port_t read_only_port;
parallel_nand_t read_only_nand;
unsigned int pos;
unsigned int reg;
assert(parallel_nand_init(&nand, &fake_port,
@@ -76,9 +94,116 @@ int main(void)
assert(events[5].kind == 'A' && events[5].value == 0x12U);
assert(events[6].kind == 'C' && events[6].value == 0x30U);
assert(parallel_nand_init(&nand, &fake_port,
&parallel_nand_hy27uf084g2m_geometry) == PNAND_OK);
assert(parallel_nand_page_count(&nand) == 262144UL);
assert(PNAND_HY27UF084G2M_MANUFACTURER_ID == 0xADU);
assert(PNAND_HY27UF084G2M_DEVICE_ID == 0xDCU);
clear_events();
input_data[0] = 0xABU;
input_count = 1U;
input_pos = 0U;
assert(parallel_nand_read_page_slice(&nand, 0x23456UL, 0x78UL,
bytes, 1UL) == PNAND_OK);
assert(bytes[0] == 0xABU);
assert(events[2].kind == 'A' && events[2].value == 0x78U);
assert(events[3].kind == 'A' && events[3].value == 0x00U);
assert(events[4].kind == 'A' && events[4].value == 0x56U);
assert(events[5].kind == 'A' && events[5].value == 0x34U);
assert(events[6].kind == 'A' && events[6].value == 0x02U);
assert(events[7].kind == 'C' && events[7].value == 0x30U);
assert(parallel_nand_init(&nand, &fake_port, &small_geometry) ==
PNAND_OK);
clear_events();
input_data[0] = 0xFFU;
input_data[1] = 0xFFU;
input_data[2] = 0x80U;
input_count = 3U;
input_pos = 0U;
assert(parallel_nand_program_page(&nand, 3UL, main_data, 2UL,
oob, 2UL) == PNAND_OK);
pos = find_event(0U, 'P', 0U);
assert(pos < event_count);
pos = find_event(pos + 1U, 'C', 0x80U);
assert(pos < event_count);
pos = find_event(pos + 1U, 'W', 0xAAU);
assert(pos < event_count);
pos = find_event(pos + 1U, 'W', 0x55U);
assert(pos < event_count);
pos = find_event(pos + 1U, 'W', 0xFFU);
assert(pos < event_count);
pos = find_event(pos + 1U, 'W', 0xFEU);
assert(pos < event_count);
pos = find_event(pos + 1U, 'C', 0x10U);
assert(pos < event_count);
pos = find_event(pos + 1U, 'C', 0x70U);
assert(pos < event_count);
assert(events[event_count - 1U].kind == 'P' &&
events[event_count - 1U].value == 1U);
clear_events();
input_data[0] = 0xFFU;
input_data[1] = 0xFFU;
input_data[2] = 0x80U;
input_count = 3U;
input_pos = 0U;
assert(parallel_nand_erase_block(&nand, 2UL) == PNAND_OK);
pos = find_event(0U, 'P', 0U);
pos = find_event(pos + 1U, 'C', 0x60U);
assert(pos < event_count);
pos = find_event(pos + 1U, 'A', 0x04U);
assert(pos < event_count);
pos = find_event(pos + 1U, 'C', 0xD0U);
assert(pos < event_count);
assert(events[event_count - 1U].kind == 'P' &&
events[event_count - 1U].value == 1U);
clear_events();
input_data[0] = 0xFFU;
input_data[1] = 0xFFU;
input_data[2] = 0x81U;
input_count = 3U;
input_pos = 0U;
assert(parallel_nand_program_page(&nand, 0UL, main_data, 2UL,
0, 0UL) ==
PNAND_ERROR_PROGRAM_FAILED);
assert(events[event_count - 1U].kind == 'P' &&
events[event_count - 1U].value == 1U);
clear_events();
input_data[0] = 0xFFU;
input_data[1] = 0xFFU;
input_data[2] = 0x00U;
input_count = 3U;
input_pos = 0U;
assert(parallel_nand_program_page(&nand, 0UL, main_data, 2UL,
0, 0UL) ==
PNAND_ERROR_WRITE_PROTECTED);
assert(events[event_count - 1U].kind == 'P' &&
events[event_count - 1U].value == 1U);
clear_events();
input_data[0] = 0x00U;
input_count = 1U;
input_pos = 0U;
assert(parallel_nand_erase_block(&nand, 0UL) ==
PNAND_ERROR_BAD_BLOCK);
assert(find_event(0U, 'P', 0U) == event_count);
read_only_port = fake_port;
read_only_port.write_supported = 0;
assert(parallel_nand_init(&read_only_nand, &read_only_port,
&small_geometry) == PNAND_OK);
assert(parallel_nand_erase_block(&read_only_nand, 0UL) ==
PNAND_ERROR_WRITE_PROTECTED);
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_read(&gas, 0xE001U, &reg) == PNAND_GAS_OK);
assert(reg == 0x0002U);
assert(parallel_nand_gas_write(&gas, 0xE005U, 0x1234U) ==
PNAND_GAS_OK);
assert(parallel_nand_gas_write(&gas, 0xE006U, 0x0001U) ==
@@ -92,6 +217,56 @@ int main(void)
assert(parallel_nand_gas_write(&gas, 0xE000U, 0U) ==
PNAND_GAS_READ_ONLY);
assert((gas.status & PNAND_GAS_STATUS_WRITE_SUPPORTED) != 0U);
assert(parallel_nand_gas_write(&gas, 0xE005U, 0U) == PNAND_GAS_OK);
assert(parallel_nand_gas_write(&gas, 0xE006U, 0U) == PNAND_GAS_OK);
assert(parallel_nand_gas_write(&gas, 0xE004U,
PNAND_GAS_CMD_CLEAR_BUFFER) ==
PNAND_GAS_OK);
assert(parallel_nand_gas_write(&gas, 0xE100U, 0x1234U) ==
PNAND_GAS_OK);
assert(gas.main_data[0] == 0x34U && gas.main_data[1] == 0x12U);
assert(parallel_nand_gas_write(&gas, 0xE004U,
PNAND_GAS_CMD_PROGRAM_PAGE) ==
PNAND_GAS_REJECTED);
assert(gas.error == PNAND_ERROR_CONFIRMATION);
assert(parallel_nand_gas_write(&gas, 0xE010U,
PNAND_GAS_CONFIRM_MAGIC) ==
PNAND_GAS_OK);
assert(parallel_nand_gas_read(&gas, 0xE002U, &reg) == PNAND_GAS_OK);
assert(parallel_nand_gas_write(&gas, 0xE004U,
PNAND_GAS_CMD_PROGRAM_PAGE) ==
PNAND_GAS_REJECTED);
assert(gas.error == PNAND_ERROR_CONFIRMATION);
input_data[0] = 0xFFU;
input_data[1] = 0xFFU;
input_data[2] = 0x80U;
input_count = 3U;
input_pos = 0U;
assert(parallel_nand_gas_write(&gas, 0xE010U,
PNAND_GAS_CONFIRM_MAGIC) ==
PNAND_GAS_OK);
assert(parallel_nand_gas_write(&gas, 0xE004U,
PNAND_GAS_CMD_PROGRAM_PAGE) ==
PNAND_GAS_OK);
assert((gas.status & PNAND_GAS_STATUS_BUFFER_DIRTY) == 0U);
assert(parallel_nand_gas_write(&gas, 0xE005U, 2U) == PNAND_GAS_OK);
input_data[0] = 0xFFU;
input_data[1] = 0xFFU;
input_data[2] = 0x80U;
input_count = 3U;
input_pos = 0U;
assert(parallel_nand_gas_write(&gas, 0xE010U,
PNAND_GAS_CONFIRM_MAGIC) ==
PNAND_GAS_OK);
assert(parallel_nand_gas_write(&gas, 0xE004U,
PNAND_GAS_CMD_ERASE_BLOCK) ==
PNAND_GAS_OK);
assert((gas.status & PNAND_GAS_STATUS_PAGE_VALID) == 0U);
puts("parallel_nand: OK");
return 0;
}

View File

@@ -18,6 +18,9 @@ set(SETPROTOCOL_V2_SOURCES
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
set(SETPROTOCOL_LEGACY_SOURCES
src/balsam_can.c
src/set_crc.c
src/periph28335.c
src/tms2812.c
src/gui_catalog.c
src/gui_frame.c
src/pcan_abi.c
@@ -103,6 +106,19 @@ if(SETP_BUILD_TESTS)
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_periph28335 tests/test_periph28335.c)
target_link_libraries(test_periph28335 PRIVATE setprotocol_static)
add_test(NAME shared_periph28335 COMMAND test_periph28335)
add_executable(test_tms2812 tests/test_tms2812.c)
target_link_libraries(test_tms2812 PRIVATE setprotocol_static)
add_test(NAME shared_tms2812 COMMAND test_tms2812)
add_executable(test_tms2812_boot
tests/test_tms2812_boot.c
ports/tms320f2812/setp_tms2812_boot.c
)
target_include_directories(test_tms2812_boot PRIVATE ports/tms320f2812)
target_link_libraries(test_tms2812_boot PRIVATE setprotocol_static)
add_test(NAME setp_tms2812_boot COMMAND test_tms2812_boot)
add_executable(test_trends tests/test_trends.c)
target_link_libraries(test_trends PRIVATE setprotocol_static)
add_test(NAME shared_trends COMMAND test_trends)

View File

@@ -43,6 +43,11 @@ multicast запрещены WRITE, FW_BEGIN и FW_ACTIVATE.
В проект устройства добавляются `ports/stm32-bxcan/protocan.c` и `src/pcan_id.c`,
а в include paths — `ports/stm32-bxcan` и `include`.
Для TMS320F2812 доступен SETProtocol v2 firmware service
[`ports/tms320f2812`](ports/tms320f2812/README.md). Он использует общую
сегментацию `set_can`, собирается без TI headers и получает eCAN, Flash и reset
через callbacks проекта платы.
Не вызывайте прикладной router из ISR. ISR принимает CAN-кадры в очередь,
сегментация собирается в главном цикле, и только полный SETP-кадр передаётся
parser/router. Сборка должна иметь timeout, контроль номера сегмента и одного

View File

@@ -21,6 +21,12 @@ TCP, UART/DMA и аппаратный CAN подключаются портам
Масштабирование, координаты и измерительные маркеры Android/SETGUI используют
общий `set_plot.c`: [границы модулей, ABI и проверки](docs/GUI_PLOT.md).
Прямой терминал ПМ35/TMS320F28335 использует единый wire contract
`Set_Terminal_28335`: функции MODBUS RTU `03/06`, 128 регистров и CRC16.
Эталонные адаптеры находятся в
[`python/protocan/periph28335.py`](../../python/protocan/periph28335.py) и
[`ports/android/kotlin/.../periph28335`](ports/android/kotlin/ru/setcorp/setprotocol/periph28335).
## Структура
| Каталог | Назначение |

View File

@@ -3,7 +3,8 @@
Численная логика находится в `include/set_plot.h` и `src/set_plot.c`.
Это модуль C99 без Qt, Android, транспорта, динамической памяти и глобального
состояния. Он собирается в существующую библиотеку SETProtocol; отдельная DLL
для графиков не требуется. Версия ABI графиков — `set_plot_abi_version() == 1`.
для графиков не требуется. Версия ABI графиков — `set_plot_abi_version() == 1`;
новые операции добавляются без изменения существующих значений и сигнатур.
| Общее в templates | Адаптер приложения |
|---|---|
@@ -13,6 +14,7 @@
| Перевод значения в долю экрана и обратно, инверсия Y | Canvas/QPainter и оформление шкал |
| Перемещение маркера от начальной координаты, ограничение видимой областью | Захват линии пальцем/мышью, редактор положения |
| Разность BA, DC и множитель единиц, шаги шкалы 1/2/5 | Подписи, цвета, миллисекунды/герцы/единицы сигнала |
| Проверка абсолютных границ X/Y для фиксации осей | Диалог ввода и хранение отдельных границ времени/FFT |
| Модели маркеров и их размещение в Kotlin/Python-портах | Жизненный цикл экрана, очистка и выбор источника |
Модель маркеров: A/B — координаты X и вертикальные линии во всю высоту поля;
@@ -59,8 +61,9 @@ SETGUI: `ui/plot_interaction.py` адаптирует общий модуль к
используются исходные метки приёма. Дискретные дорожки имеют общую X-шкалу,
а маркеры уровня Y относятся к аналоговому полю.
Расчёт FFT не является частью этого модуля. Android вычисляет спектр через
`set_spectrum.c`; вкладка спектра SETGUI получает готовые уровни в дБмВ от
Расчёт FFT не является частью этого модуля. Android и SETGUI вычисляют спектр через
`set_spectrum.c`; там же находится общий поиск доминирующего узкополосного пика,
а трёхсекундный таймер его отображения остаётся состоянием GUI. Вкладка спектра SETGUI получает готовые уровни в дБмВ от
прибора. Общая интерактивная часть не меняет эти данные или единицы.
## Проверка и сборка

View File

@@ -12,7 +12,7 @@
| `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 |
| `python/protocan/trends.py` | Модель, JSON, история и `NativeTrends` (word/CAN/GAS request/ack/data); Python 3.9+, stdlib, без Qt |
| `tests/fixtures/trends-v1.json` | Один образец для тестов обоих GUI и обмена настройками |
## Формат файла
@@ -44,6 +44,8 @@ UTF-8 JSON: `format = "setflash-trends"`, `version = 1`, `profiles` — слов
CAN — пассивный приём, без записи GAS и автоматической отправки запросов.
GAS принимает только входящие FROM_DEVICE; TX/RTR/ошибки исключены.
Полный `GAS_WATCH_DATA`, включая 32-битную метку прибора, разбирается функцией
`set_trend_watch_decode`; UI не знает смещений полей и endian.
SET GUI оформляет подписку в порядке адресов. ACK должен подтвердить весь
список: при частичном принятии нельзя определить пропущенные адреса, поэтому
строить график по смещённым индексам запрещено. При паузе порт приложения

View File

@@ -1,8 +1,8 @@
/**
* @file balsam_can.h
* @brief Legacy Balsam 167 extended-CAN register frames.
* @brief CAN_Bal_2812 extended-CAN register space.
*
* The wire layout is taken from Balsam_167_periph/Source/Internal/ecan.c:
* The wire layout is used by BALZAM Src/balzam_7/CanSetupBalzam7.c:
* one big-endian address/mask word followed by three big-endian register words.
*/
#ifndef BALSAM_CAN_H
@@ -34,7 +34,7 @@ typedef struct {
uint16_t values[BALSAM_CAN_REGISTER_COUNT];
} balsam_can_frame_t;
/** Return non-zero for the command/data/terminal IDs used by Balsam 167. */
/** Return non-zero for the command/data/terminal IDs used by CAN_Bal_2812. */
int balsam_can_is_id(uint32_t can_id);
/**
@@ -44,11 +44,11 @@ int balsam_can_is_id(uint32_t can_id);
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. */
/** Human-readable Russian name of a CAN_Bal_2812 device. */
const char *balsam_can_device_name(uint8_t device);
/**
* Format the register name from the Balsam 167 data table into output.
* Format the register name from the CAN_Bal_2812 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,

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