Compare commits
36 Commits
codex/ump-
...
874d2e1dfc
| Author | SHA1 | Date | |
|---|---|---|---|
| 874d2e1dfc | |||
| 698632ebf3 | |||
| 20dbc2c721 | |||
| e95338d781 | |||
| daa3466a41 | |||
| c9cf797132 | |||
| b0f3731554 | |||
| 9ad0322c71 | |||
| 1511719bb9 | |||
| 0ce464499f | |||
| 27ef6fcfdf | |||
| 235d5d81a2 | |||
| 59f847e900 | |||
| 286e454464 | |||
| e691dfc337 | |||
| f5f15f6a04 | |||
| be066a57eb | |||
| b1f7b965f4 | |||
| 78d3f6690b | |||
| 6a82b309cc | |||
| 2316a5a26a | |||
| de5dc18a43 | |||
| d44d57a7aa | |||
| b972b6f33c | |||
| 454baeed98 | |||
| 01ffc0e496 | |||
| 3c4ac9963d | |||
| d9eb7dd9ad | |||
| f6787163dc | |||
| 5f8fc07f2a | |||
| c8565d868d | |||
| 1523640414 | |||
| fa6f615e81 | |||
| 392817432b | |||
| 20850b26df | |||
| e163d9ba55 |
@@ -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 |
|
||||
|
||||
|
||||
19
README.md
19
README.md
@@ -13,6 +13,7 @@
|
||||
templates/
|
||||
c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты
|
||||
python/ модули на чистом Python 3.9+, только stdlib
|
||||
tools/ общие инструменты сборки и выпуска прошивок
|
||||
```
|
||||
|
||||
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
|
||||
@@ -30,20 +31,35 @@ 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/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 read/program/erase, профили Micron MT29F1G08 и Hynix HY27UF084G2M, bad-block marker и банковое окно GAS для SETGUI | C99, опционально `set-protocol/pcan_gas` | **порты TMS320F2812, STM32F103RC/ZE, STM32F407VE и STM32G474CEU6; запись требует управляемого WP#** |
|
||||
|
||||
### Python
|
||||
|
||||
| Модуль | Что делает | Зависимости |
|
||||
|---|---|---|
|
||||
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
|
||||
| [`python/protocan/trends.py`](python/protocan/trends.py) | общие настройки графиков, ограниченная история, ctypes-декодер GAS/raw CAN | stdlib, опционально SETProtocol DLL/SO |
|
||||
|
||||
### Инструменты
|
||||
|
||||
| Инструмент | Что делает |
|
||||
|---|---|
|
||||
| [`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).
|
||||
|
||||
Кодировщики `c/set-protocol` и `python/protocan` дают побайтово
|
||||
одинаковый результат — это зафиксировано эталонами в тестах на C.
|
||||
@@ -94,7 +110,6 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
|
||||
|
||||
| Код | Почему не переносится | Что можно вытащить |
|
||||
|---|---|---|
|
||||
| `SETCAN/Src/protocan.c` | 59 вызовов `HAL_*`, привязка к `CAN_HandleTypeDef`, `RTC_HandleTypeDef`, `NVIC_SystemReset()` | разбиение длинных посылок на кадры и проверка даты — чистая логика, нужны два интерфейса: «отправить кадр» и «часы». Раскладка идентификатора уже вынесена в `pcan_id` |
|
||||
| `1921vk028/drivers/user/src/one_wire_user.c` | аппаратный 1-Wire `OWI_*` из `plib028`, мигание светодиодом прямо внутри опроса датчика | логика DS18B20 уже есть в `c/ds18b20` — переносится порт, а не алгоритм |
|
||||
| `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
|
||||
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |
|
||||
|
||||
80
RULES.md
Normal file
80
RULES.md
Normal 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).
|
||||
30
c/led-indicator/CMakeLists.txt
Normal file
30
c/led-indicator/CMakeLists.txt
Normal 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
126
c/led-indicator/README.md
Normal 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
|
||||
```
|
||||
246
c/led-indicator/led_indicator.c
Normal file
246
c/led-indicator/led_indicator.c
Normal 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;
|
||||
}
|
||||
117
c/led-indicator/led_indicator.h
Normal file
117
c/led-indicator/led_indicator.h
Normal 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 */
|
||||
65
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.c
Normal file
65
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.c
Normal 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;
|
||||
}
|
||||
39
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.h
Normal file
39
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.h
Normal 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 */
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||
#define LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||
|
||||
#include "stm32f1xx_hal.h"
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||
#define LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||
|
||||
#include "stm32f4xx_hal.h"
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||
#define LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||
|
||||
#include "stm32g4xx_hal.h"
|
||||
|
||||
#endif
|
||||
27
c/led-indicator/tests/fakes/led_indicator_stm32_hal_config.h
Normal file
27
c/led-indicator/tests/fakes/led_indicator_stm32_hal_config.h
Normal 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
|
||||
84
c/led-indicator/tests/test_led_indicator.c
Normal file
84
c/led-indicator/tests/test_led_indicator.c
Normal 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;
|
||||
}
|
||||
52
c/led-indicator/tests/test_stm32_port.c
Normal file
52
c/led-indicator/tests/test_stm32_port.c
Normal 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;
|
||||
}
|
||||
26
c/parallel-nand/CMakeLists.txt
Normal file
26
c/parallel-nand/CMakeLists.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(parallel_nand C)
|
||||
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
add_library(parallel_nand
|
||||
src/parallel_nand.c
|
||||
src/parallel_nand_gas.c
|
||||
)
|
||||
target_include_directories(parallel_nand PUBLIC
|
||||
include
|
||||
)
|
||||
|
||||
add_library(parallel_nand_pcan_gas src/parallel_nand_pcan_gas.c)
|
||||
target_include_directories(parallel_nand_pcan_gas PUBLIC
|
||||
include
|
||||
../set-protocol/include
|
||||
)
|
||||
target_link_libraries(parallel_nand_pcan_gas PUBLIC parallel_nand)
|
||||
|
||||
add_executable(test_parallel_nand tests/test_parallel_nand.c)
|
||||
target_link_libraries(test_parallel_nand PRIVATE parallel_nand)
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME parallel_nand_core COMMAND test_parallel_nand)
|
||||
117
c/parallel-nand/README.md
Normal file
117
c/parallel-nand/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# parallel-nand
|
||||
|
||||
Переносимый C99-драйвер асинхронной parallel NAND x8 и окно управления через
|
||||
общее адресное пространство 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 используется только в
|
||||
конкретных портах. `parallel_nand_gas` реализует переносимую 16-битную карту
|
||||
GAS и компилируется в том числе TI C28x, где `char` имеет 16 бит.
|
||||
|
||||
## Подключение
|
||||
|
||||
В проект добавить:
|
||||
|
||||
```text
|
||||
c/parallel-nand/src/parallel_nand.c
|
||||
c/parallel-nand/src/parallel_nand_gas.c
|
||||
```
|
||||
|
||||
Пути заголовков:
|
||||
|
||||
```text
|
||||
c/parallel-nand/include
|
||||
c/parallel-nand/ports/<нужный-порт>
|
||||
```
|
||||
|
||||
И один аппаратный порт, например:
|
||||
|
||||
```text
|
||||
c/parallel-nand/ports/stm32g474ce/parallel_nand_port_stm32g474ce.c
|
||||
```
|
||||
|
||||
Минимальная инициализация:
|
||||
|
||||
```c
|
||||
static parallel_nand_t nand;
|
||||
static parallel_nand_gas_t nand_gas;
|
||||
|
||||
void memory_init(void)
|
||||
{
|
||||
int rc = parallel_nand_init(&nand,
|
||||
parallel_nand_port_stm32g474ce(),
|
||||
¶llel_nand_mt29f1g08_geometry);
|
||||
if (rc != PNAND_OK) {
|
||||
/* Оставить диагностику доступной GUI и сообщить ошибку платы. */
|
||||
}
|
||||
|
||||
parallel_nand_gas_init(&nand_gas, &nand, PNAND_GAS_DEFAULT_BASE);
|
||||
}
|
||||
```
|
||||
|
||||
Для 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` дополнительно включить
|
||||
`src/parallel_nand_pcan_gas.c`, создать `pcan_gas_region_t` и вызвать
|
||||
`parallel_nand_pcan_gas_region_init()`. Этот адаптер намеренно не нужен TMS.
|
||||
Для полного дампа предпочтительны пакетные `READ_REGISTERS/WRITE_REGISTERS`,
|
||||
а classic CAN GAS оставлен для совместимости и коротких диагностических чтений.
|
||||
|
||||
Доступ GUI и алгоритм полного дампа описаны в
|
||||
[docs/MEMORY_GAS.md](docs/MEMORY_GAS.md). Электрические соединения — в README
|
||||
соответствующего порта и [docs/EMPTY_BOARD_PINOUT.md](docs/EMPTY_BOARD_PINOUT.md).
|
||||
|
||||
## 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.
|
||||
- ECC пока не исправляет данные. GUI получает физические main/OOB и должен
|
||||
помечать дамп как raw.
|
||||
- В полном дампе bad-блоки не пропускаются: сохраняется физический порядок.
|
||||
- Для другой NAND нужно передать другую `parallel_nand_geometry_t` и проверить
|
||||
команды/маркер по её datasheet.
|
||||
|
||||
## Проверка первого запуска
|
||||
|
||||
1. Запустить `parallel_nand_init()`.
|
||||
2. Через GAS проверить `0xE000 = 0x4E44`.
|
||||
3. Записать `2` в `0xE004` (`READ_ID`).
|
||||
4. Проверить `0xE00B = 0x002C`, `0xE00C = 0x00F1`.
|
||||
5. Записать номер страницы в `0xE005/0xE006`, затем `3` в `0xE004`.
|
||||
6. Дождаться `READY | PAGE_VALID` в `0xE002`.
|
||||
7. Прочитать main из `0xE100..0xE4FF`, OOB из `0xE500..0xE51F`.
|
||||
8. PROGRAM/ERASE проверять только на заведомо расходном блоке после сохранения
|
||||
полного raw-дампа.
|
||||
83
c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md
Normal file
83
c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Подключение parallel NAND на пустой плате
|
||||
|
||||
Документ рассчитан на асинхронную NAND x8 `MT29F1G08ABADA` 3,3 В в корпусе
|
||||
TSOP-48 (суффикс `WP`). На второй фотографии NAND — длинная микросхема справа
|
||||
с маркировкой `29F1G08ABADA`. Микросхема Micron `D9MDK` сверху — DDR2 SDRAM,
|
||||
не NAND. На первой фотографии `AM29LV800BT` — parallel NOR, а не NAND.
|
||||
|
||||
Уточнение по ёмкости с фотографий:
|
||||
|
||||
- `K6R4008V1D` — SRAM 4 Mbit (512 KiB), не Flash;
|
||||
- `AM29LV800BT` — parallel NOR 8 Mbit (1 MiB);
|
||||
- `MT29F1G08ABADA` — parallel NAND 1 Gbit, то есть 128 MiB main area.
|
||||
|
||||
Поэтому текущий NAND-профиль рассчитан на 1-Gbit чип со второй платы. Для
|
||||
другой плотности меняется `parallel_nand_geometry_t`; GAS и GUI-команды
|
||||
остаются теми же.
|
||||
|
||||
Не переносите номера выводов TSOP-48 на вариант `H4` VFBGA-63. Для BGA нужно
|
||||
использовать ball map конкретного полного part number.
|
||||
|
||||
## TSOP-48, x8
|
||||
|
||||
| NAND | Вывод | Назначение |
|
||||
|---|---:|---|
|
||||
| `R/B#` | 7 | open-drain Ready/Busy, подтяжка 4,7–10 кОм к 3,3 В |
|
||||
| `RE#` | 8 | строб чтения от МК |
|
||||
| `CE#` | 9 | выбор кристалла от МК |
|
||||
| `VCC` | 12, 37 | питание 3,3 В |
|
||||
| `VSS` | 13, 36 | земля |
|
||||
| `CLE` | 16 | фиксация команды |
|
||||
| `ALE` | 17 | фиксация адреса |
|
||||
| `WE#` | 18 | строб команды/адреса от МК |
|
||||
| `WP#` | 19 | GPIO МК и pull-down 10 кОм; только для чтения допустим GND через 10 кОм |
|
||||
| `I/O0` | 29 | двунаправленная шина, бит 0 |
|
||||
| `I/O1` | 30 | двунаправленная шина, бит 1 |
|
||||
| `I/O2` | 31 | двунаправленная шина, бит 2 |
|
||||
| `I/O3` | 32 | двунаправленная шина, бит 3 |
|
||||
| `I/O4` | 41 | двунаправленная шина, бит 4 |
|
||||
| `I/O5` | 42 | двунаправленная шина, бит 5 |
|
||||
| `I/O6` | 43 | двунаправленная шина, бит 6 |
|
||||
| `I/O7` | 44 | двунаправленная шина, бит 7 |
|
||||
| `VCC1` | 34, 39 | соединить с 3,3 В для совместимости корпуса/ONFI |
|
||||
| `VSS1` | 25, 48 | соединить с GND для совместимости корпуса/ONFI |
|
||||
| `DNU` | 38, 47 | оставить неподключёнными |
|
||||
| `NC` | остальные | не подключать |
|
||||
|
||||
У каждого VCC/VCC1 поставить 100 нФ на ближайший VSS/VSS1; рядом с NAND —
|
||||
общий 1–4,7 мкФ. Сигналы не должны превышать питание NAND. Для трасс длиннее
|
||||
нескольких сантиметров предусмотреть последовательные резисторы 22–47 Ом у
|
||||
источника на `WE#`, `RE#`, `CLE`, `ALE` и при необходимости на данных.
|
||||
|
||||
## Соединение с МК
|
||||
|
||||
| NAND | TMS320F2812 | STM32F407VET6 | STM32G474CEU6 |
|
||||
|---|---|---|---|
|
||||
| `I/O0..7` | `XD0..XD7` | `PD14,PD15,PD0,PD1,PE7..PE10` | `PB0..PB7` |
|
||||
| `CLE` | `XA0` | `PD11/FSMC_A16` | `PA0` |
|
||||
| `ALE` | `XA1` | `PD12/FSMC_A17` | `PA1` |
|
||||
| `WE#` | `XWE` | `PD5/FSMC_NWE` | `PA2` |
|
||||
| `RE#` | `XRD` | `PD4/FSMC_NOE` | `PA3` |
|
||||
| `CE#` | `XZCS6AND7` | `PD7/FSMC_NE2` | `PA4` |
|
||||
| `WP#` | GND через 10 кОм | GND через 10 кОм | `PA5` и pull-down 10 кОм |
|
||||
| `R/B#` | `GPIOE0` | `PD6/FSMC_NWAIT` | `PA6` |
|
||||
|
||||
Подробные номера выводов корпуса МК и ограничения находятся в README каждого
|
||||
каталога `ports/`. Перед изготовлением платы нужно сверить выбранный корпус и
|
||||
полный part number NAND с актуальным datasheet.
|
||||
|
||||
## Первый запуск
|
||||
|
||||
1. Не устанавливать NAND и проверить 3,3 В, отсутствие КЗ и уровни `CE#/WE#/RE#`.
|
||||
2. Установить NAND, проверить pull-down и уровень `WP# = 0`, выполнить RESET `FFh`.
|
||||
3. Прочитать ID `90h`: ожидаемые первые байты для профиля — `2C F1`.
|
||||
4. Считать одну страницу два раза и сравнить main/OOB побайтно.
|
||||
5. Только после устойчивого чтения запускать полный дамп через GAS.
|
||||
6. PROGRAM/ERASE проверять на расходном блоке после сохранения raw-дампа;
|
||||
убедиться осциллографом, что `WP#` поднимается только на время операции.
|
||||
|
||||
## Источники для проверки footprint
|
||||
|
||||
- [Micron: каталог SLC NAND](https://www.micron.com/products/storage/nand-flash/slc-nand/part-catalog)
|
||||
- [MT29F1G08ABADA: signal assignments и геометрия](https://helpdesk.trx.pl/pliki/SERWER/Micron-MT29F1G08ABADAWP-IT%20D-datasheet.pdf)
|
||||
- [Micron FBGA part decoder](https://in.micron.com/sales-support/design-tools/fbga-parts-decoder)
|
||||
207
c/parallel-nand/docs/MEMORY_GAS.md
Normal file
207
c/parallel-nand/docs/MEMORY_GAS.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Работа с parallel NAND через GAS
|
||||
|
||||
## Архитектура
|
||||
|
||||
Внешняя NAND значительно больше общего адресного пространства GAS. Например,
|
||||
`MT29F1G08ABADA` содержит 128 МиБ main area, а GAS имеет только 65536
|
||||
16-битных регистров. Поэтому NAND представлена не линейным диапазоном, а
|
||||
банковым окном одной физической страницы.
|
||||
|
||||
```text
|
||||
SETGUI
|
||||
│ GUI protocol v1: READ_REGISTERS / WRITE_REGISTERS
|
||||
▼
|
||||
карта GAS 0x0000..0xFFFF
|
||||
│ 0xE000: управление, 0xE100: main, 0xE500: OOB
|
||||
▼
|
||||
parallel_nand_gas
|
||||
▼
|
||||
общее ядро parallel_nand
|
||||
├─ TMS320F2812: XINTF
|
||||
├─ STM32F103RC: GPIO
|
||||
├─ STM32F103ZE: FSMC
|
||||
├─ STM32F407VE: FSMC
|
||||
└─ STM32G474CE: GPIO
|
||||
```
|
||||
|
||||
GUI работает одинаково со всеми МК. Тип процессора влияет только на выбранный
|
||||
файл из `ports/`.
|
||||
|
||||
Встроенный проект должен направлять весь диапазон `0xE000..0xE51F` в
|
||||
`parallel_nand_gas_read()` и `parallel_nand_gas_write()`. Их аргументы имеют
|
||||
тип `unsigned int`: на C28x это нативное 16-битное слово, а на ARM функции
|
||||
явно оставляют только младшие 16 бит. Так карта и wire-формат совпадают без
|
||||
предположения, что C `char` всегда восьмибитный.
|
||||
|
||||
Для STM32, использующего карту `pcan_gas`, готов адаптер:
|
||||
|
||||
```c
|
||||
#include "parallel_nand_pcan_gas.h"
|
||||
|
||||
static pcan_gas_region_t nand_region;
|
||||
parallel_nand_pcan_gas_region_init(&nand_region, &nand_gas);
|
||||
```
|
||||
|
||||
Для TMS320F2812 переносимый обработчик вызывается прямо из существующего GAS
|
||||
диспетчера:
|
||||
|
||||
```c
|
||||
if (address >= nand_gas.base &&
|
||||
address < nand_gas.base + PNAND_GAS_REGION_REGS) {
|
||||
return parallel_nand_gas_read(&nand_gas, address, value);
|
||||
}
|
||||
```
|
||||
|
||||
Если тип результата диспетчера отличается, статусы `PNAND_GAS_OK`,
|
||||
`PNAND_GAS_NO_REG`, `PNAND_GAS_READ_ONLY` и `PNAND_GAS_REJECTED`
|
||||
преобразуются в его локальные статусы так же, как это сделано в
|
||||
`parallel_nand_pcan_gas.c`.
|
||||
|
||||
## Транспорт до GUI
|
||||
|
||||
Карта GAS не привязана к физическому каналу. Рекомендуемый путь для полного
|
||||
дампа — используемые SETGUI сообщения `WRITE_REGISTERS (0x0A)` и
|
||||
`READ_REGISTERS (0x09)`: они позволяют передавать крупные блоки поверх USB,
|
||||
UART, RS-485 или Ethernet. Адаптер `pcan_gas` нужен, когда теми же регистрами
|
||||
обмениваются по classic CAN; там один кадр несёт только четыре регистра, и
|
||||
полный 128-MiB дамп будет значительно медленнее.
|
||||
|
||||
Устройство не должно собирать весь дамп в RAM. Буферизуется только одна
|
||||
страница. Из-за переносимости на C28x каждый octet хранится в `unsigned short`,
|
||||
поэтому статические буферы GAS занимают примерно 4,2 KiB RAM и на TMS, и на
|
||||
ARM.
|
||||
|
||||
## Карта GAS
|
||||
|
||||
Базовый адрес по умолчанию — `0xE000`. Его можно изменить при вызове
|
||||
`parallel_nand_gas_init()`; все смещения ниже останутся прежними.
|
||||
|
||||
| Адрес | Доступ | Название | Значение |
|
||||
|---:|:---:|---|---|
|
||||
| `E000` | R | `SIGNATURE` | `0x4E44` (`ND`) |
|
||||
| `E001` | R | `VERSION` | версия окна, сейчас 2 |
|
||||
| `E002` | R | `STATUS` | состояние операции |
|
||||
| `E003` | R | `ERROR` | код `PNAND_*`, 0 = нет ошибки |
|
||||
| `E004` | W | `COMMAND` | команда окна |
|
||||
| `E005` | R/W | `PAGE_LO` | младшие 16 бит физической страницы |
|
||||
| `E006` | R/W | `PAGE_HI` | старшие 16 бит физической страницы |
|
||||
| `E007` | R | `PAGE_MAIN` | main bytes, для MT29 = 2048 |
|
||||
| `E008` | R | `PAGE_OOB` | OOB bytes, для MT29 = 64 |
|
||||
| `E009` | R | `PAGES_PER_BLOCK` | для MT29 = 64 |
|
||||
| `E00A` | R | `BLOCKS` | для MT29 = 1024 |
|
||||
| `E00B` | R | `MANUFACTURER_ID` | ожидается `0x2C` |
|
||||
| `E00C` | R | `DEVICE_ID` | ожидается `0xF1` |
|
||||
| `E00D` | R | `BAD_BLOCK` | 1 для физически плохого блока |
|
||||
| `E00E` | R | `GENERATION_LO` | счётчик загруженных страниц, low |
|
||||
| `E00F` | R | `GENERATION_HI` | счётчик загруженных страниц, high |
|
||||
| `E010` | W | `CONFIRM` | `0xA55A` непосредственно перед разрушительной командой |
|
||||
| `E100..E4FF` | R/W | `PAGE_DATA` | 1024 регистра main, два байта LE |
|
||||
| `E500..E51F` | R/W | `PAGE_OOB` | 32 регистра OOB, два байта LE |
|
||||
|
||||
Между управляющими регистрами и окнами данных есть зарезервированные адреса.
|
||||
Чтение блока через них не выполняется: каждый запрос должен начинаться внутри
|
||||
существующего участка.
|
||||
|
||||
## STATUS
|
||||
|
||||
| Бит | Маска | Смысл |
|
||||
|---:|---:|---|
|
||||
| 0 | `0x0001` | `READY` |
|
||||
| 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`.
|
||||
|
||||
## COMMAND
|
||||
|
||||
| Значение | Команда | Результат |
|
||||
|---:|---|---|
|
||||
| 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`. Нельзя отправлять
|
||||
один блок, начинающийся с `COMMAND`: команда выполнилась бы до обновления номера
|
||||
страницы.
|
||||
|
||||
## Последовательность полного дампа
|
||||
|
||||
1. Прочитать `E000..E00F`, проверить подпись, версию и геометрию.
|
||||
2. Выполнить `READ_ID`; для микросхемы с фотографии проверить `2C F1`.
|
||||
3. Рассчитать `page_count = PAGES_PER_BLOCK * BLOCKS`.
|
||||
4. Для каждой физической страницы от 0 до `page_count - 1`:
|
||||
- записать номер страницы в `E005/E006`;
|
||||
- записать `3` в `E004`;
|
||||
- опрашивать `E002`, пока не установлен `READY`;
|
||||
- проверить `ERROR`, `BAD_BLOCK` и изменение `GENERATION`;
|
||||
- прочитать `E100..E4FF`;
|
||||
- прочитать `E500..E51F`;
|
||||
- сразу дописать данные и метаданные в файлы.
|
||||
5. Не пропускать bad-блоки: иначе файл потеряет соответствие физическим
|
||||
страницам микросхемы.
|
||||
|
||||
GUI protocol v1 допускает payload до 512 байт. Ответ `READ_REGISTERS` содержит
|
||||
четырёхбайтовый заголовок диапазона, поэтому за один запрос следует читать не
|
||||
более 254 GAS-регистров. Страница main читается пятью запросами: четыре раза по
|
||||
254 регистра и последний раз 8 регистров. OOB читается одним запросом на 32
|
||||
регистра.
|
||||
|
||||
## Возобновление
|
||||
|
||||
GUI хранит номер последней полностью записанной страницы. После разрыва связи
|
||||
он повторно проверяет подпись/ID/геометрию и продолжает со следующей страницы.
|
||||
Страница считается готовой только после записи main, OOB и записи её статуса в
|
||||
метаданные.
|
||||
|
||||
Рекомендуемые выходные файлы:
|
||||
|
||||
- `dump.bin` — 2048 байт main каждой физической страницы;
|
||||
- `dump.oob` — 64 байта OOB каждой физической страницы;
|
||||
- `dump.json` — ID, геометрия, bad-блоки, ошибки и последняя готовая страница.
|
||||
|
||||
Для `MT29F1G08ABADA` размеры завершённого дампа: `dump.bin` = 134217728 байт,
|
||||
`dump.oob` = 4194304 байта.
|
||||
|
||||
## ECC
|
||||
|
||||
Текущая версия шаблона отдаёт raw main/OOB и не заявляет исправление ошибок.
|
||||
Статус bad-блока определяется по первому байту OOB первых двух страниц блока.
|
||||
Перед использованием дампа как рабочего образа нужно добавить ECC-политику для
|
||||
точного чипа и формата OOB. Само окно GAS при этом менять не требуется: можно
|
||||
добавить новые флаги состояния в свободные биты `STATUS`.
|
||||
|
||||
## PROGRAM/ERASE
|
||||
|
||||
Разрушительные команды доступны только если установлен флаг
|
||||
`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, журналирование и восстановление
|
||||
после пропадания питания.
|
||||
93
c/parallel-nand/include/parallel_nand.h
Normal file
93
c/parallel-nand/include/parallel_nand.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#ifndef PARALLEL_NAND_H
|
||||
#define PARALLEL_NAND_H
|
||||
|
||||
#include "parallel_nand_port.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PNAND_MT29F1G08_PAGE_MAIN 2048UL
|
||||
#define PNAND_MT29F1G08_PAGE_OOB 64UL
|
||||
#define PNAND_MT29F1G08_PAGES_PER_BLOCK 64UL
|
||||
#define PNAND_MT29F1G08_BLOCKS 1024UL
|
||||
#define PNAND_MT29F1G08_COLUMN_CYCLES 2U
|
||||
#define PNAND_MT29F1G08_ROW_CYCLES 2U
|
||||
#define PNAND_MT29F1G08_PAGE_TOTAL \
|
||||
(PNAND_MT29F1G08_PAGE_MAIN + PNAND_MT29F1G08_PAGE_OOB)
|
||||
|
||||
#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_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 {
|
||||
unsigned long page_main_size;
|
||||
unsigned long page_oob_size;
|
||||
unsigned long pages_per_block;
|
||||
unsigned long block_count;
|
||||
unsigned int column_cycles;
|
||||
unsigned int row_cycles;
|
||||
unsigned long ready_timeout_us;
|
||||
} parallel_nand_geometry_t;
|
||||
|
||||
typedef struct {
|
||||
const parallel_nand_port_t *port;
|
||||
parallel_nand_geometry_t geometry;
|
||||
} parallel_nand_t;
|
||||
|
||||
extern const parallel_nand_geometry_t parallel_nand_mt29f1g08_geometry;
|
||||
extern const parallel_nand_geometry_t parallel_nand_hy27uf084g2m_geometry;
|
||||
|
||||
int parallel_nand_init(parallel_nand_t *nand,
|
||||
const parallel_nand_port_t *port,
|
||||
const parallel_nand_geometry_t *geometry);
|
||||
int parallel_nand_reset(parallel_nand_t *nand);
|
||||
int parallel_nand_read_status(parallel_nand_t *nand, pnand_octet_t *status);
|
||||
int parallel_nand_read_id(parallel_nand_t *nand, pnand_octet_t address,
|
||||
pnand_octet_t *id, unsigned int count);
|
||||
int parallel_nand_read_page(parallel_nand_t *nand, unsigned long page,
|
||||
pnand_octet_t *main_data,
|
||||
unsigned long main_capacity,
|
||||
pnand_octet_t *oob,
|
||||
unsigned long oob_capacity);
|
||||
int parallel_nand_read_page_slice(parallel_nand_t *nand, unsigned long page,
|
||||
unsigned long column, pnand_octet_t *out,
|
||||
unsigned long count);
|
||||
int parallel_nand_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);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
93
c/parallel-nand/include/parallel_nand_gas.h
Normal file
93
c/parallel-nand/include/parallel_nand_gas.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#ifndef PARALLEL_NAND_GAS_H
|
||||
#define PARALLEL_NAND_GAS_H
|
||||
|
||||
#include "parallel_nand.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PNAND_GAS_DEFAULT_BASE 0xE000U
|
||||
#define PNAND_GAS_REGION_REGS 0x0520U
|
||||
#define PNAND_GAS_DATA_OFFSET 0x0100U
|
||||
#define PNAND_GAS_DATA_REGS 1024U
|
||||
#define PNAND_GAS_OOB_OFFSET 0x0500U
|
||||
#define PNAND_GAS_OOB_REGS 32U
|
||||
|
||||
enum {
|
||||
PNAND_GAS_REG_SIGNATURE = 0x0000,
|
||||
PNAND_GAS_REG_VERSION = 0x0001,
|
||||
PNAND_GAS_REG_STATUS = 0x0002,
|
||||
PNAND_GAS_REG_ERROR = 0x0003,
|
||||
PNAND_GAS_REG_COMMAND = 0x0004,
|
||||
PNAND_GAS_REG_PAGE_LO = 0x0005,
|
||||
PNAND_GAS_REG_PAGE_HI = 0x0006,
|
||||
PNAND_GAS_REG_PAGE_MAIN = 0x0007,
|
||||
PNAND_GAS_REG_PAGE_OOB = 0x0008,
|
||||
PNAND_GAS_REG_PAGES_PER_BLOCK = 0x0009,
|
||||
PNAND_GAS_REG_BLOCKS = 0x000A,
|
||||
PNAND_GAS_REG_MANUFACTURER_ID = 0x000B,
|
||||
PNAND_GAS_REG_DEVICE_ID = 0x000C,
|
||||
PNAND_GAS_REG_BAD_BLOCK = 0x000D,
|
||||
PNAND_GAS_REG_GENERATION_LO = 0x000E,
|
||||
PNAND_GAS_REG_GENERATION_HI = 0x000F,
|
||||
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_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 {
|
||||
parallel_nand_t *nand;
|
||||
unsigned int base;
|
||||
unsigned int status;
|
||||
int error;
|
||||
unsigned long selected_page;
|
||||
unsigned long generation;
|
||||
unsigned long cached_block;
|
||||
int cached_bad_block;
|
||||
int block_cache_valid;
|
||||
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];
|
||||
} parallel_nand_gas_t;
|
||||
|
||||
void parallel_nand_gas_init(parallel_nand_gas_t *gas,
|
||||
parallel_nand_t *nand, unsigned int base);
|
||||
|
||||
/* Portable GAS callbacks. Values and addresses are exactly 16 bits on wire. */
|
||||
typedef enum {
|
||||
PNAND_GAS_OK = 0,
|
||||
PNAND_GAS_NO_REG,
|
||||
PNAND_GAS_READ_ONLY,
|
||||
PNAND_GAS_REJECTED
|
||||
} parallel_nand_gas_result_t;
|
||||
|
||||
parallel_nand_gas_result_t parallel_nand_gas_read(
|
||||
parallel_nand_gas_t *gas, unsigned int address, unsigned int *value);
|
||||
parallel_nand_gas_result_t parallel_nand_gas_write(
|
||||
parallel_nand_gas_t *gas, unsigned int address, unsigned int value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
11
c/parallel-nand/include/parallel_nand_pcan_gas.h
Normal file
11
c/parallel-nand/include/parallel_nand_pcan_gas.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#ifndef PARALLEL_NAND_PCAN_GAS_H
|
||||
#define PARALLEL_NAND_PCAN_GAS_H
|
||||
|
||||
#include "parallel_nand_gas.h"
|
||||
#include "pcan_gas.h"
|
||||
|
||||
/* Adapter for targets where c/set-protocol pcan_gas is available. */
|
||||
void parallel_nand_pcan_gas_region_init(pcan_gas_region_t *region,
|
||||
parallel_nand_gas_t *gas);
|
||||
|
||||
#endif
|
||||
30
c/parallel-nand/include/parallel_nand_port.h
Normal file
30
c/parallel-nand/include/parallel_nand_port.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef PARALLEL_NAND_PORT_H
|
||||
#define PARALLEL_NAND_PORT_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Один octet хранится в целом типе: это важно для C28x с 16-битным char. */
|
||||
typedef unsigned short pnand_octet_t;
|
||||
|
||||
typedef struct {
|
||||
int (*init)(void);
|
||||
void (*select)(void);
|
||||
void (*deselect)(void);
|
||||
void (*write_command)(pnand_octet_t value);
|
||||
void (*write_address)(pnand_octet_t value);
|
||||
void (*write_data)(pnand_octet_t value);
|
||||
pnand_octet_t (*read_data)(void);
|
||||
int (*is_ready)(void);
|
||||
void (*set_write_protect)(int enabled);
|
||||
void (*delay_us)(unsigned long us);
|
||||
/* Nonzero only when set_write_protect(0) physically releases WP#. */
|
||||
int write_supported;
|
||||
} parallel_nand_port_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
51
c/parallel-nand/ports/stm32f103rc/README.md
Normal file
51
c/parallel-nand/ports/stm32f103rc/README.md
Normal 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 нФ, рядом с чипом —
|
||||
1–4,7 мкФ. `R/B#` имеет открытый сток и требует внешней подтяжки 4,7–10 кОм
|
||||
к 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(),
|
||||
¶llel_nand_hy27uf084g2m_geometry);
|
||||
}
|
||||
```
|
||||
|
||||
После успешной инициализации прочитайте четыре байта ID и ожидайте
|
||||
`AD DC 80 95`. Порт поддерживает PROGRAM/ERASE: ядро держит `WP#` в активном
|
||||
низком уровне между операциями и поднимает его только на время изменения NAND.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
62
c/parallel-nand/ports/stm32f103ze/README.md
Normal file
62
c/parallel-nand/ports/stm32f103ze/README.md
Normal 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,7–10 кОм к 3,3 В.
|
||||
Питание NAND — 3,3 В; возле каждой пары питания нужен 100 нФ, рядом с
|
||||
микросхемой — 1–4,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(),
|
||||
¶llel_nand_hy27uf084g2m_geometry);
|
||||
}
|
||||
```
|
||||
|
||||
Начальные тайминги намеренно консервативны. После запуска проверьте
|
||||
осциллографом длительности `WE#`/`RE#` и время установки данных. Ожидаемый ID
|
||||
HY27UF084G2M: `AD DC 80 95`.
|
||||
|
||||
Порт поддерживает PROGRAM/ERASE. `WP#` на `PG8` остаётся активным между
|
||||
операциями и снимается ядром только на время программирования или стирания.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
31
c/parallel-nand/ports/stm32f407ve/README.md
Normal file
31
c/parallel-nand/ports/stm32f407ve/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Порт STM32F407VET6
|
||||
|
||||
Используется аппаратный FSMC NAND Bank 2, шина x8.
|
||||
|
||||
| NAND | STM32F407VE | LQFP-100 |
|
||||
|---|---|---:|
|
||||
| `I/O0` | `PD14/FSMC_D0` | 61 |
|
||||
| `I/O1` | `PD15/FSMC_D1` | 62 |
|
||||
| `I/O2` | `PD0/FSMC_D2` | 81 |
|
||||
| `I/O3` | `PD1/FSMC_D3` | 82 |
|
||||
| `I/O4` | `PE7/FSMC_D4` | 38 |
|
||||
| `I/O5` | `PE8/FSMC_D5` | 39 |
|
||||
| `I/O6` | `PE9/FSMC_D6` | 40 |
|
||||
| `I/O7` | `PE10/FSMC_D7` | 41 |
|
||||
| `RE#` | `PD4/FSMC_NOE` | 85 |
|
||||
| `WE#` | `PD5/FSMC_NWE` | 86 |
|
||||
| `R/B#` | `PD6/FSMC_NWAIT` | 87 |
|
||||
| `CE#` | `PD7/FSMC_NE2` | 88 |
|
||||
| `CLE` | `PD11/FSMC_A16` | 58 |
|
||||
| `ALE` | `PD12/FSMC_A17` | 59 |
|
||||
|
||||
Порт использует STM32Cube HAL. В `stm32f4xx_hal_conf.h` включить
|
||||
`HAL_NAND_MODULE_ENABLED`, добавить `stm32f4xx_hal_nand.c` и
|
||||
`stm32f4xx_ll_fsmc.c`, включить FSMC и не создавать второй
|
||||
`NAND_HandleTypeDef` для Bank 2. Начальные тайминги
|
||||
консервативны; финальные значения подтвердить осциллографом.
|
||||
|
||||
В базовой схеме `WP#` аппаратно зафиксирован в активном уровне, поэтому порт
|
||||
объявляет `write_supported = 0`, а PROGRAM/ERASE возвращают
|
||||
`PNAND_ERROR_WRITE_PROTECTED`. Для записи назначьте отдельный GPIO с внешним
|
||||
pull-down, реализуйте управление `WP#` и включите признак поддержки порта.
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "parallel_nand_port_stm32f407ve.h"
|
||||
#include "stm32f4xx_hal.h"
|
||||
#include "stm32f4xx_hal_nand.h"
|
||||
|
||||
#define PNAND_F407_BANK2_BASE 0x70000000UL
|
||||
#define PNAND_F407_CMD_AREA (1UL << 16)
|
||||
#define PNAND_F407_ADDR_AREA (1UL << 17)
|
||||
|
||||
static NAND_HandleTypeDef hnand;
|
||||
|
||||
static int f407_init(void)
|
||||
{
|
||||
GPIO_InitTypeDef io;
|
||||
FSMC_NAND_PCC_TimingTypeDef common;
|
||||
FSMC_NAND_PCC_TimingTypeDef attribute;
|
||||
|
||||
__HAL_RCC_GPIOD_CLK_ENABLE();
|
||||
__HAL_RCC_GPIOE_CLK_ENABLE();
|
||||
__HAL_RCC_FSMC_CLK_ENABLE();
|
||||
|
||||
io.Mode = GPIO_MODE_AF_PP;
|
||||
io.Pull = GPIO_NOPULL;
|
||||
io.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
|
||||
io.Alternate = GPIO_AF12_FSMC;
|
||||
io.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5 |
|
||||
GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_11 | GPIO_PIN_12 |
|
||||
GPIO_PIN_14 | GPIO_PIN_15;
|
||||
HAL_GPIO_Init(GPIOD, &io);
|
||||
io.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10;
|
||||
HAL_GPIO_Init(GPIOE, &io);
|
||||
|
||||
hnand.Instance = FSMC_NAND_DEVICE;
|
||||
hnand.Init.NandBank = FSMC_NAND_BANK2;
|
||||
hnand.Init.Waitfeature = FSMC_NAND_PCC_WAIT_FEATURE_DISABLE;
|
||||
hnand.Init.MemoryDataWidth = FSMC_NAND_PCC_MEM_BUS_WIDTH_8;
|
||||
hnand.Init.EccComputation = FSMC_NAND_ECC_DISABLE;
|
||||
hnand.Init.ECCPageSize = FSMC_NAND_ECC_PAGE_SIZE_2048BYTE;
|
||||
hnand.Init.TCLRSetupTime = 2U;
|
||||
hnand.Init.TARSetupTime = 2U;
|
||||
|
||||
common.SetupTime = 2U;
|
||||
common.WaitSetupTime = 5U;
|
||||
common.HoldSetupTime = 3U;
|
||||
common.HiZSetupTime = 2U;
|
||||
attribute = common;
|
||||
return HAL_NAND_Init(&hnand, &common, &attribute) == HAL_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
static void f407_select(void) { /* FSMC_NCE2 активируется доступом. */ }
|
||||
static void f407_deselect(void) { }
|
||||
static void f407_write_command(pnand_octet_t value)
|
||||
{
|
||||
*(__IO uint8_t *)(PNAND_F407_BANK2_BASE | PNAND_F407_CMD_AREA) =
|
||||
(uint8_t)value;
|
||||
}
|
||||
static void f407_write_address(pnand_octet_t value)
|
||||
{
|
||||
*(__IO uint8_t *)(PNAND_F407_BANK2_BASE | PNAND_F407_ADDR_AREA) =
|
||||
(uint8_t)value;
|
||||
}
|
||||
static void f407_write_data(pnand_octet_t value)
|
||||
{
|
||||
*(__IO uint8_t *)PNAND_F407_BANK2_BASE = (uint8_t)value;
|
||||
}
|
||||
static pnand_octet_t f407_read_data(void)
|
||||
{
|
||||
return *(__IO uint8_t *)PNAND_F407_BANK2_BASE;
|
||||
}
|
||||
static int f407_is_ready(void)
|
||||
{
|
||||
return HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_6) == GPIO_PIN_SET;
|
||||
}
|
||||
static void f407_set_wp(int enabled)
|
||||
{
|
||||
/* WP# аппаратный в базовой схеме. */
|
||||
(void)enabled;
|
||||
}
|
||||
static void f407_delay_us(unsigned long us)
|
||||
{
|
||||
uint32_t cycles = (SystemCoreClock / 3000000UL) * (uint32_t)us;
|
||||
while (cycles-- != 0U) __NOP();
|
||||
}
|
||||
|
||||
static const parallel_nand_port_t f407_port = {
|
||||
f407_init, f407_select, f407_deselect,
|
||||
f407_write_command, f407_write_address, f407_write_data,
|
||||
f407_read_data, f407_is_ready, f407_set_wp, f407_delay_us,
|
||||
0
|
||||
};
|
||||
|
||||
const parallel_nand_port_t *parallel_nand_port_stm32f407ve(void)
|
||||
{
|
||||
return &f407_port;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef PARALLEL_NAND_PORT_STM32F407VE_H
|
||||
#define PARALLEL_NAND_PORT_STM32F407VE_H
|
||||
|
||||
#include "parallel_nand_port.h"
|
||||
|
||||
const parallel_nand_port_t *parallel_nand_port_stm32f407ve(void);
|
||||
|
||||
#endif
|
||||
24
c/parallel-nand/ports/stm32g474ce/README.md
Normal file
24
c/parallel-nand/ports/stm32g474ce/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Порт STM32G474CEU6
|
||||
|
||||
У UFQFPN-48 нет выведенного FSMC, поэтому используется GPIO bit-bang.
|
||||
|
||||
| NAND | STM32G474CEU6 | UFQFPN-48 |
|
||||
|---|---|---:|
|
||||
| `I/O0..I/O7` | `PB0..PB7` | 17, 18, 19, 41, 42, 43, 44, 45 |
|
||||
| `CLE` | `PA0` | 8 |
|
||||
| `ALE` | `PA1` | 9 |
|
||||
| `WE#` | `PA2` | 10 |
|
||||
| `RE#` | `PA3` | 11 |
|
||||
| `CE#` | `PA4` | 12 |
|
||||
| `WP#` | `PA5` | 13 |
|
||||
| `R/B#` | `PA6` | 14 |
|
||||
|
||||
`R/B#` требует внешней подтяжки 4,7–10 кОм к 3,3 В. Порт использует STM32Cube
|
||||
HAL только для инициализации, а циклы NAND выполняет прямым доступом к GPIO.
|
||||
|
||||
Выбранные `PB3/PB4` нельзя одновременно использовать для trace/JTAG; SWD на
|
||||
`PA13/PA14` остаётся доступен. Если эти пины нужны проекту, измените таблицу и
|
||||
операции шины: текущая оптимизация рассчитана на непрерывные `PB0..PB7`.
|
||||
|
||||
Порт поддерживает PROGRAM/ERASE через управляемый `WP#` на `PA5`. Внешний
|
||||
pull-down сохраняет защиту до инициализации GPIO и при сбросе МК.
|
||||
@@ -0,0 +1,135 @@
|
||||
#include "parallel_nand_port_stm32g474ce.h"
|
||||
#include "stm32g4xx_hal.h"
|
||||
|
||||
/* Пустая плата STM32G474CEU6: PB0..PB7 образуют непрерывную x8 шину. */
|
||||
#define DATA_MASK 0x00FFU
|
||||
#define CLE_PIN GPIO_PIN_0
|
||||
#define ALE_PIN GPIO_PIN_1
|
||||
#define WE_PIN GPIO_PIN_2
|
||||
#define RE_PIN GPIO_PIN_3
|
||||
#define CE_PIN GPIO_PIN_4
|
||||
#define WP_PIN GPIO_PIN_5
|
||||
#define RB_PIN GPIO_PIN_6
|
||||
|
||||
static int data_is_output;
|
||||
|
||||
static void data_input(void)
|
||||
{
|
||||
if (data_is_output) {
|
||||
GPIOB->MODER &= ~0x0000FFFFUL;
|
||||
data_is_output = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void data_output(void)
|
||||
{
|
||||
if (!data_is_output) {
|
||||
GPIOB->MODER = (GPIOB->MODER & ~0x0000FFFFUL) | 0x00005555UL;
|
||||
data_is_output = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void short_delay(void)
|
||||
{
|
||||
__NOP(); __NOP(); __NOP(); __NOP();
|
||||
__NOP(); __NOP(); __NOP(); __NOP();
|
||||
}
|
||||
|
||||
static int g474_init(void)
|
||||
{
|
||||
GPIO_InitTypeDef io;
|
||||
__HAL_RCC_GPIOA_CLK_ENABLE();
|
||||
__HAL_RCC_GPIOB_CLK_ENABLE();
|
||||
|
||||
io.Pin = DATA_MASK;
|
||||
io.Mode = GPIO_MODE_OUTPUT_PP;
|
||||
io.Pull = GPIO_NOPULL;
|
||||
io.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
|
||||
HAL_GPIO_Init(GPIOB, &io);
|
||||
data_is_output = 1;
|
||||
data_input();
|
||||
|
||||
/* Set inactive levels before enabling the pins as outputs. */
|
||||
GPIOA->BSRR = WE_PIN | RE_PIN | CE_PIN |
|
||||
((uint32_t)(CLE_PIN | ALE_PIN | WP_PIN) << 16);
|
||||
io.Pin = CLE_PIN | ALE_PIN | WE_PIN | RE_PIN | CE_PIN | WP_PIN;
|
||||
io.Mode = GPIO_MODE_OUTPUT_PP;
|
||||
io.Pull = GPIO_NOPULL;
|
||||
io.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
|
||||
HAL_GPIO_Init(GPIOA, &io);
|
||||
io.Pin = RB_PIN;
|
||||
io.Mode = GPIO_MODE_INPUT;
|
||||
io.Pull = GPIO_NOPULL;
|
||||
HAL_GPIO_Init(GPIOA, &io);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void g474_select(void) { GPIOA->BSRR = (uint32_t)CE_PIN << 16; }
|
||||
static void g474_deselect(void) { GPIOA->BSRR = CE_PIN; }
|
||||
|
||||
static void write_cycle(pnand_octet_t value)
|
||||
{
|
||||
data_output();
|
||||
GPIOB->BSRR = (uint32_t)DATA_MASK << 16;
|
||||
GPIOB->BSRR = (uint32_t)value & DATA_MASK;
|
||||
short_delay();
|
||||
GPIOA->BSRR = (uint32_t)WE_PIN << 16;
|
||||
short_delay();
|
||||
GPIOA->BSRR = WE_PIN;
|
||||
short_delay();
|
||||
}
|
||||
|
||||
static void g474_write_command(pnand_octet_t value)
|
||||
{
|
||||
GPIOA->BSRR = CLE_PIN | ((uint32_t)ALE_PIN << 16);
|
||||
write_cycle(value);
|
||||
GPIOA->BSRR = (uint32_t)CLE_PIN << 16;
|
||||
}
|
||||
|
||||
static void g474_write_address(pnand_octet_t value)
|
||||
{
|
||||
GPIOA->BSRR = ALE_PIN | ((uint32_t)CLE_PIN << 16);
|
||||
write_cycle(value);
|
||||
GPIOA->BSRR = (uint32_t)ALE_PIN << 16;
|
||||
}
|
||||
|
||||
static void g474_write_data(pnand_octet_t value)
|
||||
{
|
||||
GPIOA->BSRR = ((uint32_t)(CLE_PIN | ALE_PIN) << 16);
|
||||
write_cycle(value);
|
||||
}
|
||||
|
||||
static pnand_octet_t g474_read_data(void)
|
||||
{
|
||||
pnand_octet_t value;
|
||||
data_input();
|
||||
GPIOA->BSRR = ((uint32_t)(CLE_PIN | ALE_PIN | RE_PIN) << 16);
|
||||
short_delay();
|
||||
value = (pnand_octet_t)(GPIOB->IDR & DATA_MASK);
|
||||
GPIOA->BSRR = RE_PIN;
|
||||
short_delay();
|
||||
return value;
|
||||
}
|
||||
|
||||
static int g474_is_ready(void) { return (GPIOA->IDR & RB_PIN) != 0U; }
|
||||
static void g474_set_wp(int enabled)
|
||||
{
|
||||
GPIOA->BSRR = enabled ? ((uint32_t)WP_PIN << 16) : WP_PIN;
|
||||
}
|
||||
static void g474_delay_us(unsigned long us)
|
||||
{
|
||||
uint32_t cycles = (SystemCoreClock / 3000000UL) * (uint32_t)us;
|
||||
while (cycles-- != 0U) __NOP();
|
||||
}
|
||||
|
||||
static const parallel_nand_port_t g474_port = {
|
||||
g474_init, g474_select, g474_deselect,
|
||||
g474_write_command, g474_write_address, g474_write_data,
|
||||
g474_read_data, g474_is_ready, g474_set_wp, g474_delay_us,
|
||||
1
|
||||
};
|
||||
|
||||
const parallel_nand_port_t *parallel_nand_port_stm32g474ce(void)
|
||||
{
|
||||
return &g474_port;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef PARALLEL_NAND_PORT_STM32G474CE_H
|
||||
#define PARALLEL_NAND_PORT_STM32G474CE_H
|
||||
|
||||
#include "parallel_nand_port.h"
|
||||
|
||||
const parallel_nand_port_t *parallel_nand_port_stm32g474ce(void);
|
||||
|
||||
#endif
|
||||
22
c/parallel-nand/ports/tms320f2812/README.md
Normal file
22
c/parallel-nand/ports/tms320f2812/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Порт TMS320F2812
|
||||
|
||||
Порт использует XINTF Zone 6:
|
||||
|
||||
- `XD0..XD7` — NAND `I/O0..I/O7`;
|
||||
- `XA0` — `CLE`;
|
||||
- `XA1` — `ALE`;
|
||||
- `XWE` — `WE#`;
|
||||
- `XRD` — `RE#`;
|
||||
- `XZCS6AND7` — `CE#`;
|
||||
- `GPIOE0/XINT1` — `R/B#` с внешней подтяжкой 4,7–10 кОм.
|
||||
|
||||
Адреса C28x задаются в 16-битных словах: data `0x100000`, command
|
||||
`0x100001`, address `0x100002`. Используется только младший байт XINTF.
|
||||
|
||||
`WP#` в базовом варианте аппаратный и соединён с GND через 10 кОм. Поэтому
|
||||
порт объявляет `write_supported = 0`, а PROGRAM/ERASE возвращают
|
||||
`PNAND_ERROR_WRITE_PROTECTED`. Для записи нужно перенести `WP#` на GPIO,
|
||||
реализовать `tms_set_wp()` и установить признак поддержки в структуре порта.
|
||||
|
||||
Задержка в порте приблизительная. После выбора частоты CPU проверить импульсы
|
||||
осциллографом и при необходимости изменить `PNAND_TMS_CPU_MHZ`.
|
||||
@@ -0,0 +1,66 @@
|
||||
#include "parallel_nand_port_tms320f2812.h"
|
||||
#include "DSP281x_Device.h"
|
||||
|
||||
/* C28x addresses are 16-bit word addresses. XA0/XA1 drive CLE/ALE. */
|
||||
#define PNAND_TMS_DATA_REG (*(volatile unsigned int *)0x100000UL)
|
||||
#define PNAND_TMS_CMD_REG (*(volatile unsigned int *)0x100001UL)
|
||||
#define PNAND_TMS_ADDR_REG (*(volatile unsigned int *)0x100002UL)
|
||||
|
||||
#ifndef PNAND_TMS_CPU_MHZ
|
||||
#define PNAND_TMS_CPU_MHZ 150UL
|
||||
#endif
|
||||
|
||||
static void tms_delay_us(unsigned long us)
|
||||
{
|
||||
volatile unsigned long loops = us * (PNAND_TMS_CPU_MHZ / 5UL);
|
||||
while (loops-- != 0UL) asm(" NOP");
|
||||
}
|
||||
|
||||
static int tms_init(void)
|
||||
{
|
||||
EALLOW;
|
||||
GpioMuxRegs.GPEMUX.bit.XINT1_XBIO_GPIOE0 = 0;
|
||||
GpioMuxRegs.GPEDIR.bit.GPIOE0 = 0;
|
||||
|
||||
XintfRegs.XINTCNF2.bit.XTIMCLK = 1;
|
||||
XintfRegs.XINTCNF2.bit.WRBUFF = 0;
|
||||
XintfRegs.XINTCNF2.bit.CLKOFF = 0;
|
||||
XintfRegs.XINTCNF2.bit.CLKMODE = 1;
|
||||
XintfRegs.XTIMING6.bit.XWRLEAD = 3;
|
||||
XintfRegs.XTIMING6.bit.XWRACTIVE = 7;
|
||||
XintfRegs.XTIMING6.bit.XWRTRAIL = 3;
|
||||
XintfRegs.XTIMING6.bit.XRDLEAD = 3;
|
||||
XintfRegs.XTIMING6.bit.XRDACTIVE = 7;
|
||||
XintfRegs.XTIMING6.bit.XRDTRAIL = 3;
|
||||
XintfRegs.XTIMING6.bit.USEREADY = 0;
|
||||
XintfRegs.XTIMING6.bit.READYMODE = 0;
|
||||
XintfRegs.XTIMING6.bit.XSIZE = 3;
|
||||
EDIS;
|
||||
asm(" RPT #7 || NOP");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void tms_select(void) { /* XZCS6AND7 формируется каждым доступом. */ }
|
||||
static void tms_deselect(void) { }
|
||||
static void tms_write_command(pnand_octet_t v) { PNAND_TMS_CMD_REG = v & 0xFFU; }
|
||||
static void tms_write_address(pnand_octet_t v) { PNAND_TMS_ADDR_REG = v & 0xFFU; }
|
||||
static void tms_write_data(pnand_octet_t v) { PNAND_TMS_DATA_REG = v & 0xFFU; }
|
||||
static pnand_octet_t tms_read_data(void) { return PNAND_TMS_DATA_REG & 0xFFU; }
|
||||
static int tms_is_ready(void) { return GpioDataRegs.GPEDAT.bit.GPIOE0 != 0; }
|
||||
static void tms_set_wp(int enabled)
|
||||
{
|
||||
/* Шаблон платы фиксирует WP# аппаратно. См. README порта. */
|
||||
(void)enabled;
|
||||
}
|
||||
|
||||
static const parallel_nand_port_t tms_port = {
|
||||
tms_init, tms_select, tms_deselect,
|
||||
tms_write_command, tms_write_address, tms_write_data,
|
||||
tms_read_data, tms_is_ready, tms_set_wp, tms_delay_us,
|
||||
0
|
||||
};
|
||||
|
||||
const parallel_nand_port_t *parallel_nand_port_tms320f2812(void)
|
||||
{
|
||||
return &tms_port;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef PARALLEL_NAND_PORT_TMS320F2812_H
|
||||
#define PARALLEL_NAND_PORT_TMS320F2812_H
|
||||
|
||||
#include "parallel_nand_port.h"
|
||||
|
||||
const parallel_nand_port_t *parallel_nand_port_tms320f2812(void);
|
||||
|
||||
#endif
|
||||
306
c/parallel-nand/src/parallel_nand.c
Normal file
306
c/parallel-nand/src/parallel_nand.c
Normal file
@@ -0,0 +1,306 @@
|
||||
#include "parallel_nand.h"
|
||||
|
||||
#define PNAND_CMD_READ_1 0x00U
|
||||
#define PNAND_CMD_READ_2 0x30U
|
||||
#define PNAND_CMD_READ_ID 0x90U
|
||||
#define PNAND_CMD_READ_STATUS 0x70U
|
||||
#define PNAND_CMD_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,
|
||||
PNAND_MT29F1G08_PAGES_PER_BLOCK,
|
||||
PNAND_MT29F1G08_BLOCKS,
|
||||
PNAND_MT29F1G08_COLUMN_CYCLES,
|
||||
PNAND_MT29F1G08_ROW_CYCLES,
|
||||
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 &&
|
||||
port->deselect != 0 && port->write_command != 0 &&
|
||||
port->write_address != 0 && port->read_data != 0 &&
|
||||
port->is_ready != 0 && port->set_write_protect != 0 &&
|
||||
port->delay_us != 0;
|
||||
}
|
||||
|
||||
static int geometry_valid(const parallel_nand_geometry_t *g)
|
||||
{
|
||||
return g != 0 && g->page_main_size != 0UL &&
|
||||
g->pages_per_block != 0UL && g->block_count != 0UL &&
|
||||
g->column_cycles != 0U && g->row_cycles != 0U &&
|
||||
g->ready_timeout_us != 0UL;
|
||||
}
|
||||
|
||||
unsigned long parallel_nand_page_count(const parallel_nand_t *nand)
|
||||
{
|
||||
if (nand == 0) return 0UL;
|
||||
return nand->geometry.pages_per_block * nand->geometry.block_count;
|
||||
}
|
||||
|
||||
static int wait_ready(parallel_nand_t *nand)
|
||||
{
|
||||
unsigned long elapsed = 0UL;
|
||||
while (!nand->port->is_ready()) {
|
||||
if (elapsed >= nand->geometry.ready_timeout_us)
|
||||
return PNAND_ERROR_TIMEOUT;
|
||||
nand->port->delay_us(1UL);
|
||||
elapsed++;
|
||||
}
|
||||
return PNAND_OK;
|
||||
}
|
||||
|
||||
int parallel_nand_reset(parallel_nand_t *nand)
|
||||
{
|
||||
if (nand == 0 || !port_valid(nand->port)) return PNAND_ERROR_ARGUMENT;
|
||||
nand->port->select();
|
||||
nand->port->write_command(PNAND_CMD_RESET);
|
||||
nand->port->deselect();
|
||||
/* R/B# may not fall until tWB after the command cycle. */
|
||||
nand->port->delay_us(1UL);
|
||||
return wait_ready(nand);
|
||||
}
|
||||
|
||||
int parallel_nand_init(parallel_nand_t *nand,
|
||||
const parallel_nand_port_t *port,
|
||||
const parallel_nand_geometry_t *geometry)
|
||||
{
|
||||
if (nand == 0 || !port_valid(port) || !geometry_valid(geometry))
|
||||
return PNAND_ERROR_ARGUMENT;
|
||||
nand->port = port;
|
||||
nand->geometry = *geometry;
|
||||
if (port->init() != 0) return PNAND_ERROR_PORT;
|
||||
port->deselect();
|
||||
/* WP# stays asserted except for the shortest possible mutation window. */
|
||||
port->set_write_protect(1);
|
||||
return parallel_nand_reset(nand);
|
||||
}
|
||||
|
||||
int parallel_nand_read_status(parallel_nand_t *nand, pnand_octet_t *status)
|
||||
{
|
||||
if (nand == 0 || status == 0) return PNAND_ERROR_ARGUMENT;
|
||||
nand->port->select();
|
||||
nand->port->write_command(PNAND_CMD_READ_STATUS);
|
||||
*status = nand->port->read_data() & 0xFFU;
|
||||
nand->port->deselect();
|
||||
return PNAND_OK;
|
||||
}
|
||||
|
||||
int parallel_nand_read_id(parallel_nand_t *nand, pnand_octet_t address,
|
||||
pnand_octet_t *id, unsigned int count)
|
||||
{
|
||||
unsigned int i;
|
||||
if (nand == 0 || id == 0 || count == 0U) return PNAND_ERROR_ARGUMENT;
|
||||
nand->port->select();
|
||||
nand->port->write_command(PNAND_CMD_READ_ID);
|
||||
nand->port->write_address(address & 0xFFU);
|
||||
for (i = 0U; i < count; i++) id[i] = nand->port->read_data() & 0xFFU;
|
||||
nand->port->deselect();
|
||||
return PNAND_OK;
|
||||
}
|
||||
|
||||
static int begin_page_read(parallel_nand_t *nand, unsigned long page,
|
||||
unsigned long column)
|
||||
{
|
||||
unsigned int i;
|
||||
unsigned long value;
|
||||
unsigned long total = nand->geometry.page_main_size +
|
||||
nand->geometry.page_oob_size;
|
||||
if (page >= parallel_nand_page_count(nand) || column >= total)
|
||||
return PNAND_ERROR_RANGE;
|
||||
|
||||
nand->port->select();
|
||||
nand->port->write_command(PNAND_CMD_READ_1);
|
||||
value = column;
|
||||
for (i = 0U; i < nand->geometry.column_cycles; i++) {
|
||||
nand->port->write_address((pnand_octet_t)(value & 0xFFUL));
|
||||
value >>= 8;
|
||||
}
|
||||
value = page;
|
||||
for (i = 0U; i < nand->geometry.row_cycles; i++) {
|
||||
nand->port->write_address((pnand_octet_t)(value & 0xFFUL));
|
||||
value >>= 8;
|
||||
}
|
||||
nand->port->write_command(PNAND_CMD_READ_2);
|
||||
nand->port->deselect();
|
||||
nand->port->delay_us(1UL); /* Cover tWB before polling R/B#. */
|
||||
return wait_ready(nand);
|
||||
}
|
||||
|
||||
int parallel_nand_read_page_slice(parallel_nand_t *nand, unsigned long page,
|
||||
unsigned long column, pnand_octet_t *out,
|
||||
unsigned long count)
|
||||
{
|
||||
unsigned long i;
|
||||
unsigned long total;
|
||||
int result;
|
||||
if (nand == 0 || out == 0 || count == 0UL) return PNAND_ERROR_ARGUMENT;
|
||||
total = nand->geometry.page_main_size + nand->geometry.page_oob_size;
|
||||
if (column >= total || count > total - column) return PNAND_ERROR_RANGE;
|
||||
result = begin_page_read(nand, page, column);
|
||||
if (result != PNAND_OK) return result;
|
||||
nand->port->select();
|
||||
for (i = 0UL; i < count; i++) out[i] = nand->port->read_data() & 0xFFU;
|
||||
nand->port->deselect();
|
||||
return PNAND_OK;
|
||||
}
|
||||
|
||||
int parallel_nand_read_page(parallel_nand_t *nand, unsigned long page,
|
||||
pnand_octet_t *main_data,
|
||||
unsigned long main_capacity,
|
||||
pnand_octet_t *oob,
|
||||
unsigned long oob_capacity)
|
||||
{
|
||||
unsigned long i;
|
||||
int result;
|
||||
if (nand == 0 || main_data == 0 || oob == 0)
|
||||
return PNAND_ERROR_ARGUMENT;
|
||||
if (main_capacity < nand->geometry.page_main_size ||
|
||||
oob_capacity < nand->geometry.page_oob_size)
|
||||
return PNAND_ERROR_BUFFER;
|
||||
result = begin_page_read(nand, page, 0UL);
|
||||
if (result != PNAND_OK) return result;
|
||||
nand->port->select();
|
||||
for (i = 0UL; i < nand->geometry.page_main_size; i++)
|
||||
main_data[i] = nand->port->read_data() & 0xFFU;
|
||||
for (i = 0UL; i < nand->geometry.page_oob_size; i++)
|
||||
oob[i] = nand->port->read_data() & 0xFFU;
|
||||
nand->port->deselect();
|
||||
return PNAND_OK;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
unsigned long first_page;
|
||||
pnand_octet_t marker;
|
||||
int result;
|
||||
if (nand == 0 || is_bad == 0) return PNAND_ERROR_ARGUMENT;
|
||||
if (block >= nand->geometry.block_count) return PNAND_ERROR_RANGE;
|
||||
first_page = block * nand->geometry.pages_per_block;
|
||||
result = parallel_nand_read_page_slice(nand, first_page,
|
||||
nand->geometry.page_main_size, &marker, 1UL);
|
||||
if (result != PNAND_OK) return result;
|
||||
if ((marker & 0xFFU) != 0xFFU) {
|
||||
*is_bad = 1;
|
||||
return PNAND_OK;
|
||||
}
|
||||
result = parallel_nand_read_page_slice(nand, first_page + 1UL,
|
||||
nand->geometry.page_main_size, &marker, 1UL);
|
||||
if (result != PNAND_OK) return result;
|
||||
*is_bad = ((marker & 0xFFU) != 0xFFU);
|
||||
return PNAND_OK;
|
||||
}
|
||||
263
c/parallel-nand/src/parallel_nand_gas.c
Normal file
263
c/parallel-nand/src/parallel_nand_gas.c
Normal file
@@ -0,0 +1,263 @@
|
||||
#include "parallel_nand_gas.h"
|
||||
|
||||
static unsigned int pack_pair(const pnand_octet_t *data, unsigned int index)
|
||||
{
|
||||
return (unsigned int)((data[index * 2U] & 0xFFU) |
|
||||
((data[index * 2U + 1U] & 0xFFU) << 8));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
unsigned int offset;
|
||||
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,
|
||||
offset - PNAND_GAS_DATA_OFFSET);
|
||||
return PNAND_GAS_OK;
|
||||
}
|
||||
if (offset >= PNAND_GAS_OOB_OFFSET &&
|
||||
offset < PNAND_GAS_OOB_OFFSET + PNAND_GAS_OOB_REGS) {
|
||||
if ((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,
|
||||
offset - PNAND_GAS_OOB_OFFSET);
|
||||
return PNAND_GAS_OK;
|
||||
}
|
||||
switch (offset) {
|
||||
case PNAND_GAS_REG_SIGNATURE: *value = 0x4E44U; break; /* ND */
|
||||
case PNAND_GAS_REG_VERSION: *value = 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;
|
||||
case PNAND_GAS_REG_PAGE_LO: *value = (unsigned int)gas->selected_page & 0xFFFFU; break;
|
||||
case PNAND_GAS_REG_PAGE_HI: *value = (unsigned int)(gas->selected_page >> 16) & 0xFFFFU; break;
|
||||
case PNAND_GAS_REG_PAGE_MAIN: *value = (unsigned int)g->page_main_size; break;
|
||||
case PNAND_GAS_REG_PAGE_OOB: *value = (unsigned int)g->page_oob_size; break;
|
||||
case PNAND_GAS_REG_PAGES_PER_BLOCK: *value = (unsigned int)g->pages_per_block; break;
|
||||
case PNAND_GAS_REG_BLOCKS: *value = (unsigned int)g->block_count; break;
|
||||
case PNAND_GAS_REG_MANUFACTURER_ID: *value = (unsigned int)gas->id[0]; break;
|
||||
case PNAND_GAS_REG_DEVICE_ID: *value = (unsigned int)gas->id[1]; break;
|
||||
case PNAND_GAS_REG_BAD_BLOCK:
|
||||
*value = (gas->status & PNAND_GAS_STATUS_BAD_BLOCK) != 0U; break;
|
||||
case PNAND_GAS_REG_GENERATION_LO: *value = (unsigned int)gas->generation & 0xFFFFU; break;
|
||||
case PNAND_GAS_REG_GENERATION_HI: *value = (unsigned int)(gas->generation >> 16) & 0xFFFFU; break;
|
||||
case PNAND_GAS_REG_CONFIRM: *value = 0U; break;
|
||||
default: return PNAND_GAS_NO_REG;
|
||||
}
|
||||
*value &= 0xFFFFU;
|
||||
return PNAND_GAS_OK;
|
||||
}
|
||||
|
||||
static void finish(parallel_nand_gas_t *gas, int result)
|
||||
{
|
||||
gas->error = result;
|
||||
gas->status &= (0xFFFFU ^ PNAND_GAS_STATUS_BUSY);
|
||||
gas->status |= PNAND_GAS_STATUS_READY | persistent_status(gas);
|
||||
if (result != PNAND_OK) gas->status |= PNAND_GAS_STATUS_ERROR;
|
||||
}
|
||||
|
||||
static parallel_nand_gas_result_t run_command(parallel_nand_gas_t *gas,
|
||||
unsigned int command)
|
||||
{
|
||||
int result = PNAND_OK;
|
||||
int bad = 0;
|
||||
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) {
|
||||
result = parallel_nand_read_id(gas->nand, 0U, gas->id, 8U);
|
||||
} else if (command == PNAND_GAS_CMD_READ_PAGE) {
|
||||
unsigned long block;
|
||||
if (gas->selected_page >= parallel_nand_page_count(gas->nand)) {
|
||||
finish(gas, PNAND_ERROR_RANGE);
|
||||
return PNAND_GAS_REJECTED;
|
||||
}
|
||||
result = parallel_nand_read_page(gas->nand, gas->selected_page,
|
||||
gas->main_data, PNAND_MT29F1G08_PAGE_MAIN,
|
||||
gas->oob, PNAND_MT29F1G08_PAGE_OOB);
|
||||
if (result == PNAND_OK) {
|
||||
gas->status |= PNAND_GAS_STATUS_PAGE_VALID;
|
||||
gas->generation++;
|
||||
block = gas->selected_page /
|
||||
gas->nand->geometry.pages_per_block;
|
||||
if (!gas->block_cache_valid || gas->cached_block != block) {
|
||||
result = parallel_nand_is_bad_block(gas->nand, block, &bad);
|
||||
if (result == PNAND_OK) {
|
||||
gas->cached_block = block;
|
||||
gas->cached_bad_block = bad;
|
||||
gas->block_cache_valid = 1;
|
||||
}
|
||||
} else {
|
||||
bad = gas->cached_bad_block;
|
||||
}
|
||||
if (result == PNAND_OK && bad)
|
||||
gas->status |= PNAND_GAS_STATUS_BAD_BLOCK;
|
||||
}
|
||||
} else 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_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);
|
||||
return result == PNAND_OK ? PNAND_GAS_OK : PNAND_GAS_REJECTED;
|
||||
}
|
||||
|
||||
parallel_nand_gas_result_t parallel_nand_gas_write(
|
||||
parallel_nand_gas_t *gas, unsigned int address, unsigned int value)
|
||||
{
|
||||
unsigned int offset;
|
||||
if (gas == 0 || address < gas->base) return PNAND_GAS_REJECTED;
|
||||
offset = address - gas->base;
|
||||
if (offset >= PNAND_GAS_REGION_REGS) return PNAND_GAS_NO_REG;
|
||||
if ((gas->status & PNAND_GAS_STATUS_BUSY) != 0U)
|
||||
return PNAND_GAS_REJECTED;
|
||||
value &= 0xFFFFU;
|
||||
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:
|
||||
gas->destructive_armed = 0;
|
||||
return PNAND_GAS_READ_ONLY;
|
||||
}
|
||||
}
|
||||
|
||||
void parallel_nand_gas_init(parallel_nand_gas_t *gas,
|
||||
parallel_nand_t *nand, unsigned int base)
|
||||
{
|
||||
unsigned int i;
|
||||
gas->nand = nand;
|
||||
gas->base = base;
|
||||
gas->status = PNAND_GAS_STATUS_READY | 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;
|
||||
}
|
||||
43
c/parallel-nand/src/parallel_nand_pcan_gas.c
Normal file
43
c/parallel-nand/src/parallel_nand_pcan_gas.c
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "parallel_nand_pcan_gas.h"
|
||||
|
||||
static pcan_gas_status_t convert(parallel_nand_gas_result_t result)
|
||||
{
|
||||
switch (result) {
|
||||
case PNAND_GAS_OK: return PCAN_GAS_OK;
|
||||
case PNAND_GAS_NO_REG: return PCAN_GAS_NO_REG;
|
||||
case PNAND_GAS_READ_ONLY: return PCAN_GAS_READ_ONLY;
|
||||
default: return PCAN_GAS_REJECTED;
|
||||
}
|
||||
}
|
||||
|
||||
static pcan_gas_status_t read_reg(const pcan_gas_region_t *region,
|
||||
uint16_t offset, uint16_t *value)
|
||||
{
|
||||
parallel_nand_gas_t *gas = (parallel_nand_gas_t *)region->user;
|
||||
unsigned int portable_value;
|
||||
parallel_nand_gas_result_t result = parallel_nand_gas_read(
|
||||
gas, (unsigned int)region->base + offset, &portable_value);
|
||||
if (result == PNAND_GAS_OK) *value = (uint16_t)portable_value;
|
||||
return convert(result);
|
||||
}
|
||||
|
||||
static pcan_gas_status_t write_reg(const pcan_gas_region_t *region,
|
||||
uint16_t offset, uint16_t value)
|
||||
{
|
||||
parallel_nand_gas_t *gas = (parallel_nand_gas_t *)region->user;
|
||||
return convert(parallel_nand_gas_write(
|
||||
gas, (unsigned int)region->base + offset, value));
|
||||
}
|
||||
|
||||
void parallel_nand_pcan_gas_region_init(pcan_gas_region_t *region,
|
||||
parallel_nand_gas_t *gas)
|
||||
{
|
||||
region->base = (uint16_t)gas->base;
|
||||
region->count = PNAND_GAS_REGION_REGS;
|
||||
region->storage = 0;
|
||||
region->read = read_reg;
|
||||
region->write = write_reg;
|
||||
region->flags = 0U;
|
||||
region->user = gas;
|
||||
region->name = "parallel-nand";
|
||||
}
|
||||
272
c/parallel-nand/tests/test_parallel_nand.c
Normal file
272
c/parallel-nand/tests/test_parallel_nand.c
Normal file
@@ -0,0 +1,272 @@
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include "parallel_nand.h"
|
||||
#include "parallel_nand_gas.h"
|
||||
|
||||
typedef struct { char kind; unsigned int value; } event_t;
|
||||
static event_t events[256];
|
||||
static unsigned int event_count;
|
||||
static pnand_octet_t input_data[32];
|
||||
static unsigned int input_count;
|
||||
static unsigned int input_pos;
|
||||
|
||||
static void record(char kind, unsigned int value)
|
||||
{
|
||||
events[event_count].kind = kind;
|
||||
events[event_count].value = value;
|
||||
event_count++;
|
||||
}
|
||||
static int fake_init(void) { return 0; }
|
||||
static void fake_select(void) { record('S', 0); }
|
||||
static void fake_deselect(void) { record('D', 0); }
|
||||
static void fake_cmd(pnand_octet_t v) { record('C', v); }
|
||||
static void fake_addr(pnand_octet_t v) { record('A', v); }
|
||||
static void fake_write(pnand_octet_t v) { record('W', v); }
|
||||
static pnand_octet_t fake_read(void)
|
||||
{
|
||||
return input_pos < input_count ? input_data[input_pos++] : 0xFFU;
|
||||
}
|
||||
static int fake_ready(void) { return 1; }
|
||||
static void fake_wp(int enabled) { record('P', (unsigned int)enabled); }
|
||||
static void fake_delay(unsigned long us) { (void)us; }
|
||||
|
||||
static const parallel_nand_port_t fake_port = {
|
||||
fake_init, fake_select, fake_deselect, fake_cmd, fake_addr, fake_write,
|
||||
fake_read, fake_ready, fake_wp, fake_delay, 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,
|
||||
¶llel_nand_mt29f1g08_geometry) == PNAND_OK);
|
||||
assert(events[0].kind == 'D');
|
||||
assert(events[1].kind == 'P' && events[1].value == 1U);
|
||||
|
||||
clear_events();
|
||||
input_data[0] = 0x2CU;
|
||||
input_data[1] = 0xF1U;
|
||||
input_count = 2U;
|
||||
input_pos = 0U;
|
||||
assert(parallel_nand_read_id(&nand, 0U, id, 2U) == PNAND_OK);
|
||||
assert(id[0] == 0x2CU && id[1] == 0xF1U);
|
||||
assert(events[1].kind == 'C' && events[1].value == 0x90U);
|
||||
assert(events[2].kind == 'A' && events[2].value == 0U);
|
||||
|
||||
clear_events();
|
||||
input_data[0] = 0x12U;
|
||||
input_data[1] = 0x34U;
|
||||
input_count = 2U;
|
||||
input_pos = 0U;
|
||||
assert(parallel_nand_read_page_slice(&nand, 0x1234UL, 0x56UL,
|
||||
bytes, 2UL) == PNAND_OK);
|
||||
assert(bytes[0] == 0x12U && bytes[1] == 0x34U);
|
||||
assert(events[1].kind == 'C' && events[1].value == 0x00U);
|
||||
assert(events[2].kind == 'A' && events[2].value == 0x56U);
|
||||
assert(events[3].kind == 'A' && events[3].value == 0x00U);
|
||||
assert(events[4].kind == 'A' && events[4].value == 0x34U);
|
||||
assert(events[5].kind == 'A' && events[5].value == 0x12U);
|
||||
assert(events[6].kind == 'C' && events[6].value == 0x30U);
|
||||
|
||||
assert(parallel_nand_init(&nand, &fake_port,
|
||||
¶llel_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, ®) == PNAND_GAS_OK);
|
||||
assert(reg == 0x4E44U);
|
||||
assert(parallel_nand_gas_read(&gas, 0xE001U, ®) == 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) ==
|
||||
PNAND_GAS_OK);
|
||||
assert(gas.selected_page == 0x00011234UL);
|
||||
gas.main_data[0] = 0x34U;
|
||||
gas.main_data[1] = 0x12U;
|
||||
gas.status |= PNAND_GAS_STATUS_PAGE_VALID;
|
||||
assert(parallel_nand_gas_read(&gas, 0xE100U, ®) == PNAND_GAS_OK);
|
||||
assert(reg == 0x1234U);
|
||||
assert(parallel_nand_gas_write(&gas, 0xE000U, 0U) ==
|
||||
PNAND_GAS_READ_ONLY);
|
||||
|
||||
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, ®) == 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;
|
||||
}
|
||||
@@ -10,10 +10,17 @@ set(SETPROTOCOL_V2_SOURCES
|
||||
src/set_can.c
|
||||
src/set_firmware.c
|
||||
src/set_telemetry.c
|
||||
src/set_plot.c
|
||||
src/set_trends.c
|
||||
src/set_spectrum.c
|
||||
)
|
||||
|
||||
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
|
||||
set(SETPROTOCOL_LEGACY_SOURCES
|
||||
src/balsam_can.c
|
||||
src/set_crc.c
|
||||
src/periph28335.c
|
||||
src/tms2812.c
|
||||
src/gui_catalog.c
|
||||
src/gui_frame.c
|
||||
src/pcan_abi.c
|
||||
@@ -22,6 +29,7 @@ set(SETPROTOCOL_LEGACY_SOURCES
|
||||
src/pcan_gas.c
|
||||
src/pcan_id.c
|
||||
src/pcan_link.c
|
||||
src/pcan_modbus_server.c
|
||||
src/pcan_ring.c
|
||||
)
|
||||
|
||||
@@ -32,6 +40,9 @@ set(SETPROTOCOL_SOURCES
|
||||
|
||||
add_library(setprotocol_static STATIC ${SETPROTOCOL_SOURCES})
|
||||
target_include_directories(setprotocol_static PUBLIC include)
|
||||
if(NOT MSVC)
|
||||
target_link_libraries(setprotocol_static PUBLIC m)
|
||||
endif()
|
||||
|
||||
# Совместимое имя цели для проектов, уже использовавших SET protocol v2.
|
||||
add_library(set_protocol ALIAS setprotocol_static)
|
||||
@@ -40,6 +51,9 @@ option(SETP_BUILD_SHARED "Build stable host ABI shared library" ON)
|
||||
if(SETP_BUILD_SHARED)
|
||||
add_library(setprotocol SHARED ${SETPROTOCOL_SOURCES})
|
||||
target_include_directories(setprotocol PUBLIC include)
|
||||
if(NOT MSVC)
|
||||
target_link_libraries(setprotocol PRIVATE m)
|
||||
endif()
|
||||
target_compile_definitions(setprotocol PRIVATE PCAN_ABI_BUILD_DLL)
|
||||
set_target_properties(setprotocol PROPERTIES
|
||||
C_VISIBILITY_PRESET hidden
|
||||
@@ -66,6 +80,10 @@ option(SETP_BUILD_TESTS "Build host tests" ON)
|
||||
if(SETP_BUILD_TESTS)
|
||||
enable_testing()
|
||||
|
||||
add_executable(test_plot tests/test_plot.c)
|
||||
target_link_libraries(test_plot PRIVATE setprotocol_static)
|
||||
add_test(NAME shared_plot COMMAND test_plot)
|
||||
|
||||
add_executable(test_set_protocol tests/test_set_protocol.c)
|
||||
target_link_libraries(test_set_protocol PRIVATE setprotocol_static)
|
||||
add_test(NAME set_protocol_v2 COMMAND test_set_protocol)
|
||||
@@ -74,6 +92,10 @@ if(SETP_BUILD_TESTS)
|
||||
target_link_libraries(test_transport PRIVATE setprotocol_static)
|
||||
add_test(NAME legacy_transport COMMAND test_transport)
|
||||
|
||||
add_executable(test_modbus_server tests/test_modbus_server.c)
|
||||
target_link_libraries(test_modbus_server PRIVATE setprotocol_static)
|
||||
add_test(NAME protocan_modbus_server COMMAND test_modbus_server)
|
||||
|
||||
add_executable(test_gui tests/test_gui.c)
|
||||
target_link_libraries(test_gui PRIVATE setprotocol_static)
|
||||
add_test(NAME legacy_gui COMMAND test_gui)
|
||||
@@ -81,4 +103,26 @@ if(SETP_BUILD_TESTS)
|
||||
add_executable(test_abi tests/test_abi.c)
|
||||
target_link_libraries(test_abi PRIVATE setprotocol_static)
|
||||
add_test(NAME stable_abi COMMAND test_abi)
|
||||
add_executable(test_balsam_can tests/test_balsam_can.c)
|
||||
target_link_libraries(test_balsam_can PRIVATE setprotocol_static)
|
||||
add_test(NAME legacy_balsam_can COMMAND test_balsam_can)
|
||||
add_executable(test_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)
|
||||
add_executable(test_spectrum tests/test_spectrum.c)
|
||||
target_link_libraries(test_spectrum PRIVATE setprotocol_static)
|
||||
add_test(NAME shared_spectrum COMMAND test_spectrum)
|
||||
endif()
|
||||
|
||||
@@ -36,6 +36,18 @@ multicast запрещены WRITE, FW_BEGIN и FW_ACTIVATE.
|
||||
|
||||
## CAN
|
||||
|
||||
Для существующего прикладного ProtoCAN доступен
|
||||
[порт STM32 bxCAN](ports/stm32-bxcan/README.md), перенесённый из SETCAN.
|
||||
Он сохраняет API `PROTOCAN_*`, использует HAL CAN/RTC/TIM и общий `pcan_id`.
|
||||
Порт рассчитан на classic bxCAN; он не реализует сегментацию SETProtocol v2.
|
||||
В проект устройства добавляются `ports/stm32-bxcan/protocan.c` и `src/pcan_id.c`,
|
||||
а в include paths — `ports/stm32-bxcan` и `include`.
|
||||
|
||||
Для 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, контроль номера сегмента и одного
|
||||
|
||||
@@ -7,12 +7,26 @@
|
||||
- совместимый ProtoCAN transport для существующих CAN-мостов;
|
||||
- SETGUI transport v1 для приборов переходного периода;
|
||||
- стабильный host ABI для Python/JNI/других FFI;
|
||||
- GAS, кольцевые буферы, CRC и потоковые parser.
|
||||
- GAS, кольцевые буферы, CRC и потоковые parser;
|
||||
- host-анализ спектра: FFT, окна, фильтры и адаптеры JNI/ctypes.
|
||||
|
||||
Ядро не зависит от HAL, ОС или конкретного адаптера, не использует
|
||||
Протокольное ядро не зависит от HAL, ОС или конкретного адаптера, не использует
|
||||
динамическую память и не открывает COM/CAN само. SLCAN, SocketCAN, USB CDC,
|
||||
TCP, UART/DMA и аппаратный CAN подключаются портами.
|
||||
|
||||
Отдельный host-модуль `set_spectrum.c` использует ограниченный временный буфер
|
||||
до 2×16384 `double`; он не нужен для wire-протокола/MCU.
|
||||
Контракт и использование из обоих GUI: [GUI_SPECTRUM.md](docs/GUI_SPECTRUM.md).
|
||||
|
||||
Масштабирование, координаты и измерительные маркеры Android/SETGUI используют
|
||||
общий `set_plot.c`: [границы модулей, ABI и проверки](docs/GUI_PLOT.md).
|
||||
|
||||
Прямой терминал ПМ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).
|
||||
|
||||
## Структура
|
||||
|
||||
| Каталог | Назначение |
|
||||
@@ -24,6 +38,7 @@ TCP, UART/DMA и аппаратный CAN подключаются портам
|
||||
| `src/` | общая реализация всех протокольных модулей |
|
||||
| `ports/android` | JNI и Kotlin facade |
|
||||
| `ports/stm32f4` | UART/DMA port для legacy byte stream |
|
||||
| [`ports/stm32-bxcan`](ports/stm32-bxcan/README.md) | STM32 HAL bxCAN/RTC/TIM, прикладной ProtoCAN; бывший отдельный SETCAN |
|
||||
| `docs/SETPROTOCOL.md` | архитектура, ABI, память и переносимость |
|
||||
| `docs/legacy` | нормативные документы ProtoCAN/SETGUI v1 |
|
||||
| `PROTOCOL.md` | нормативный wire contract SET protocol v2 |
|
||||
|
||||
83
c/set-protocol/docs/GUI_PLOT.md
Normal file
83
c/set-protocol/docs/GUI_PLOT.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Общая работа с графиками Android и SETGUI
|
||||
|
||||
Численная логика находится в `include/set_plot.h` и `src/set_plot.c`.
|
||||
Это модуль C99 без Qt, Android, транспорта, динамической памяти и глобального
|
||||
состояния. Он собирается в существующую библиотеку SETProtocol; отдельная DLL
|
||||
для графиков не требуется. Версия ABI графиков — `set_plot_abi_version() == 1`;
|
||||
новые операции добавляются без изменения существующих значений и сигнатур.
|
||||
|
||||
| Общее в templates | Адаптер приложения |
|
||||
|---|---|
|
||||
| Нормализованная область просмотра, масштаб 1…128, ограничение панорамирования | Размер поля и отступы в пикселях |
|
||||
| Независимый масштаб X/Y с сохранением координаты под пальцами | События Compose или Qt, колесо, кнопки |
|
||||
| Выбор преобладающей оси по изменению расстояния между пальцами | Накопление движения до порога; фиксация выбранной оси до конца жеста |
|
||||
| Перевод значения в долю экрана и обратно, инверсия Y | Canvas/QPainter и оформление шкал |
|
||||
| Перемещение маркера от начальной координаты, ограничение видимой областью | Захват линии пальцем/мышью, редактор положения |
|
||||
| Разность B−A, D−C и множитель единиц, шаги шкалы 1/2/5 | Подписи, цвета, миллисекунды/герцы/единицы сигнала |
|
||||
| Проверка абсолютных границ X/Y для фиксации осей | Диалог ввода и хранение отдельных границ времени/FFT |
|
||||
| Модели маркеров и их размещение в Kotlin/Python-портах | Жизненный цикл экрана, очистка и выбор источника |
|
||||
|
||||
Модель маркеров: A/B — координаты X и вертикальные линии во всю высоту поля;
|
||||
C/D — координаты Y и горизонтальные линии. Они могут пересекаться: отрицательная
|
||||
разность сохраняется. Начальные координаты — 1/3 и 2/3 диапазона. При изменении
|
||||
масштаба уже установленные координаты сохраняются. «Маркеры в видимую область»
|
||||
заново размещает их. Координаты и область просмотра времени/спектра независимы.
|
||||
|
||||
`set_plot_eval()` принимает операцию из `enum set_plot_operation`, точное число
|
||||
входных `double` и буфер вывода. Контракт каждой операции указан в заголовке.
|
||||
Возвращает число записанных элементов, 0 — неправильные аргументы. Некорректный
|
||||
коэффициент масштабирования оставляет корректный viewport неизменным.
|
||||
NaN/Inf и вырожденные диапазоны не передаются дальше в отрисовку.
|
||||
FRACTION/VALUE не обрезают координаты: это позволяет правильно отсекать линии
|
||||
по границе поля, а не создавать ложные горизонтальные участки на его краю.
|
||||
DRAG, напротив, ограничивает положение маркера видимым диапазоном.
|
||||
|
||||
## Порты
|
||||
|
||||
- Android/JVM: `ports/android/kotlin/ru/setcorp/setprotocol/trends/NativePlot.kt`,
|
||||
`PlotViewport.kt`, `TrendMarkers.kt`; тонкий JNI — `ports/android/set_plot_jni.c`.
|
||||
Android использует `System.loadLibrary("setprotocol")`. Для JVM-тестов можно
|
||||
передать абсолютный путь DLL/SO системным свойством `setplot.library`.
|
||||
- Python: `python/protocan/plot.py`: `PlotMath(library)`, `Viewport`, `Bounds`,
|
||||
`Markers`. Приложение передаёт уже загруженную SETProtocol `ctypes.CDLL`.
|
||||
Зависимости — стандартная библиотека; Qt в этот модуль не импортируется.
|
||||
|
||||
Новые изменения не должны добавлять вторую реализацию численных операций
|
||||
в Kotlin/Python или в приложение. Состояние и адаптация событий остаются в портах.
|
||||
Добавление операции требует обновления C ABI, обоих портов и общих эталонов.
|
||||
|
||||
## Интеграция приложений
|
||||
|
||||
Android: `TrendGraph` использует общие viewport, bounds и маркеры, а Compose
|
||||
рисует линии и панель. При измерениях время отображается в миллисекундах от
|
||||
начала выбранного участка; сохранённые X-координаты включают абсолютное начало.
|
||||
|
||||
SETGUI: `ui/plot_interaction.py` адаптирует общий модуль к Qt. Его используют
|
||||
`SignalPlot` (ТМС и датчики), `TrendPlot` (логи) и `SpectrumPlot`.
|
||||
Колесо по вертикали масштабирует Y, Shift+колесо и горизонтальное колесо — X;
|
||||
сенсорный pinch выбирает ось по движению. Временные графики хранят метку приёма
|
||||
каждого отсчёта. Пропущенные значения не растягивают остальные серии:
|
||||
все сигналы имеют единую шкалу времени. При повторном построении ТМС из журнала
|
||||
используются исходные метки приёма. Дискретные дорожки имеют общую X-шкалу,
|
||||
а маркеры уровня Y относятся к аналоговому полю.
|
||||
|
||||
Расчёт FFT не является частью этого модуля. Android и SETGUI вычисляют спектр через
|
||||
`set_spectrum.c`; там же находится общий поиск доминирующего узкополосного пика,
|
||||
а трёхсекундный таймер его отображения остаётся состоянием GUI. Вкладка спектра SETGUI получает готовые уровни в дБмВ от
|
||||
прибора. Общая интерактивная часть не меняет эти данные или единицы.
|
||||
|
||||
## Проверка и сборка
|
||||
|
||||
- CMake/CTest: `shared_plot` проверяет ABI, размеры буферов и координаты.
|
||||
- `tests/fixtures/plot-v1.json` содержит общие численные эталоны. Их читают
|
||||
`PlotContractTest` (фактический JNI) и `python/tests/test_plot.py` (ctypes).
|
||||
- Host DLL/SO для Python: `python tools/build_host.py --output <путь>`.
|
||||
- Для JVM: та же команда с `--java-home <JDK>` добавляет JNI-порт графиков.
|
||||
Нужен C-компилятор (MSVC на Windows, cc/clang/gcc на Unix) и JDK с `jni.h`.
|
||||
- Android-проект автоматически собирает host-библиотеку перед JVM-тестами;
|
||||
интерпретатор Python можно задать свойством Gradle `setflash.python`.
|
||||
- SETGUI проверяет события мыши, колеса и двух пальцев через QtTest без
|
||||
физического прибора; численные тесты не требуют GUI.
|
||||
|
||||
Исходники должны приходить из одного коммита подмодуля `templates` в обоих
|
||||
проектах. Локальная замена файлов без обновления gitlink не закрепляет версию.
|
||||
53
c/set-protocol/docs/GUI_SPECTRUM.md
Normal file
53
c/set-protocol/docs/GUI_SPECTRUM.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Shared GUI spectrum
|
||||
|
||||
`include/set_spectrum.h` / `src/set_spectrum.c` implement the same numerical
|
||||
pipeline for desktop and Android. CMake, `tools/build_host.py`, and the Android
|
||||
NDK build include it. The wire protocol is unchanged. The host-only analyzer
|
||||
allocates at most two FFT arrays (2×N doubles), releasing them before return.
|
||||
|
||||
The last N samples are used: the largest power of two within available data and
|
||||
`max_size`, 16…16384. Timestamps are seconds; inputs are immutable. Fs is measured
|
||||
from the selected endpoints. Non-monotonic/duplicate times and interval deviations
|
||||
over 50% of the mean are rejected. Lesser jitter is linearly interpolated onto an
|
||||
even grid (this can affect high-frequency amplitude; inspect returned jitter).
|
||||
|
||||
Processing order: interpolation, optional mean subtraction, filter, periodic
|
||||
window, forward FFT, one-sided peak amplitude. Windows: rectangular, Hann,
|
||||
Hamming, Blackman, Flat Top. Amplitudes are divided by window sum, doubled except
|
||||
DC and Nyquist. Output is not RMS/PSD/dB. Coherent gain correction is exact for
|
||||
bin-centered tones away from DC/Nyquist; off-bin tones still have scalloping.
|
||||
|
||||
Filters run before the window, on the unmodified sample copy. LP/HP are second
|
||||
order Butterworth; band pass cascades HP and LP; notch uses Q=30.
|
||||
Coefficients: [W3C Audio EQ Cookbook](https://www.w3.org/TR/audio-eq-cookbook/).
|
||||
Initial state is the first sample's steady state, not a carried streaming state;
|
||||
block-start transients may occur. Cutoffs must be strictly within (0, Fs/2).
|
||||
|
||||
## Python
|
||||
|
||||
```python
|
||||
from protocan.spectrum import NativeSpectrum, Window, Filter
|
||||
|
||||
# lib = an existing ctypes.CDLL from protocan.native.NativeProtocol
|
||||
spectrum = NativeSpectrum(lib).analyze(times_seconds, values,
|
||||
window=Window.HANN, filter=Filter.LOW_PASS, high_hz=100,
|
||||
max_size=4096, remove_mean=True)
|
||||
plot(spectrum.frequencies, spectrum.amplitudes)
|
||||
```
|
||||
|
||||
Adapter errors are ValueError, not empty/misleading arrays. No NumPy/Qt dependency.
|
||||
|
||||
## Android
|
||||
|
||||
`SpectrumAnalyzer.analyze(List<TrendPoint>, SpectrumOptions)` calls JNI using
|
||||
relative seconds from millisecond timestamps. `TrendSpectrum` contains N, Fs,
|
||||
maximum relative jitter, amplitudes or a displayable error. Call on a worker
|
||||
dispatcher. `PlotViewport` provides axis-independent zoom/pan, focus anchoring,
|
||||
128× limit and bounds clamping without any Compose dependency.
|
||||
|
||||
## Checks
|
||||
|
||||
Build CMake with tests and run CTest in Debug (assertions enabled). Python
|
||||
`tests/test_spectrum.py` runs against an explicitly supplied `SETPROTOCOL_LIBRARY`:
|
||||
independent direct DFT, amplitude correction, DC/Nyquist, off-bin leakage, filter
|
||||
bands, timing rejection/interpolation, bounded tail selection and immutable inputs.
|
||||
80
c/set-protocol/docs/GUI_TRENDS.md
Normal file
80
c/set-protocol/docs/GUI_TRENDS.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Общие тренды для Android GUI и SETGUI
|
||||
|
||||
Модуль задаёт совместимые настройки и числовые источники графиков. Он не
|
||||
рисует экран, не открывает файлы или порты и не управляет прошивкой устройства.
|
||||
|
||||
Слои: GUI → Kotlin/Python adapter → `set_trends.c` → уже разобранные кадры.
|
||||
Транспорт, таймер опроса, выбор файла, хранение настроек и Canvas/Qt остаются
|
||||
в приложении. У ядра нет callbacks, динамической памяти или глобального состояния.
|
||||
|
||||
| Файл | Назначение и зависимости |
|
||||
|---|---|
|
||||
| `include/set_trends.h`, `src/set_trends.c` | C99: фильтрация GAS/raw CAN, signed/unsigned word, payload подписки SET GUI; использует `pcan_id` и ABI export macro |
|
||||
| `ports/android/kotlin/ru/setcorp/setprotocol/trends/` | Модель, валидация JSON, ограниченная история, GAS_WATCH; JVM + org.json, без Android/Compose |
|
||||
| `ports/android/setprotocol_jni.c` | Только преобразование JNI-аргументов |
|
||||
| `python/protocan/trends.py` | Модель, JSON, история и `NativeTrends` (word/CAN/GAS request/ack/data); Python 3.9+, stdlib, без Qt |
|
||||
| `tests/fixtures/trends-v1.json` | Один образец для тестов обоих GUI и обмена настройками |
|
||||
|
||||
## Формат файла
|
||||
|
||||
UTF-8 JSON: `format = "setflash-trends"`, `version = 1`, `profiles` — словарь.
|
||||
Ключи профилей стабильны: `TMS2812`, `SET_V1`, `CAN_BRIDGE`, `GS_USB_CAN`,
|
||||
`SLCAN`, `CANGAROO_SLCAN`, `BALZAM_CAN`. Приложение явно сопоставляет своё
|
||||
подключение с этим ключом; импорт не меняет подключение.
|
||||
|
||||
Поля сигнала обязательны: `id`, `order`, `name`, `source`, `address`, `color`,
|
||||
`visible`, `valueType`, `deviceType`, `device`, `byteOffset`, `extended`.
|
||||
`id` уникален во всём файле. `order` — 1…9999, уникален внутри профиля.
|
||||
`name` — непустое изменяемое имя (до 100 символов), `color` — `#RRGGBB`.
|
||||
`visible` управляет только отрисовкой, не сбором. История и соединение в файл
|
||||
не входят. Максимум 64 сигнала на профиль и 1 МиБ на файл. Неизвестные версии,
|
||||
профили, источники и неверные значения отклоняются до замены настроек.
|
||||
|
||||
| Профиль / источник | Адрес и значение |
|
||||
|---|---|
|
||||
| TMS2812 / TMS_MEMORY | 32-битный адрес слова, CMD_PEEK; контроллер из подключения |
|
||||
| SET_V1 / SET_GAS | 16-битный GAS; GAS_WATCH_SET `0x12`, DATA `0x13` |
|
||||
| SET_V1 / SET_SENSOR | ROM DS18B20, 16 HEX-цифр (дефисы допустимы), °C |
|
||||
| Современные CAN / CAN_GAS | GAS 0…65535, DeviceType 0…7, DeviceID 0…15 |
|
||||
| Все CAN / CAN_RAW | CAN ID, STD/EXT, byteOffset 0…6; слово Little-endian |
|
||||
|
||||
Числовые адреса — десятичные либо HEX с `0x`. Регистры трактуются как
|
||||
`UINT16` или `INT16`. Для RAW/CAN_GAS C-функция возвращает значение либо
|
||||
`SET_TREND_NO_VALUE`, никогда не подставляет ноль вместо отсутствующего кадра.
|
||||
|
||||
CAN — пассивный приём, без записи GAS и автоматической отправки запросов.
|
||||
GAS принимает только входящие FROM_DEVICE; TX/RTR/ошибки исключены.
|
||||
Полный `GAS_WATCH_DATA`, включая 32-битную метку прибора, разбирается функцией
|
||||
`set_trend_watch_decode`; UI не знает смещений полей и endian.
|
||||
SET GUI оформляет подписку в порядке адресов. ACK должен подтвердить весь
|
||||
список: при частичном принятии нельзя определить пропущенные адреса, поэтому
|
||||
строить график по смещённым индексам запрещено. При паузе порт приложения
|
||||
отправляет подписку с периодом 0, когда транспорт доступен и не занят прошивкой.
|
||||
|
||||
## Быстрый старт SETGUI
|
||||
|
||||
```python
|
||||
from protocan.native import NativeProtocol
|
||||
from protocan.trends import TrendSignal, NativeTrends, encode_settings
|
||||
|
||||
core = NativeProtocol() # библиотека, пересобранная с set_trends.c
|
||||
decoder = NativeTrends(core.lib)
|
||||
signal = TrendSignal("current", source="CAN_GAS", address="0x1235")
|
||||
text = encode_settings({"CAN_BRIDGE": [signal]})
|
||||
value = decoder.can_value(signal, 0x1FD31234, 1, bytes([1, 0, 254, 255]))
|
||||
assert value == 65534.0
|
||||
# GUI сохраняет text выбранным способом и передаёт value своему графику.
|
||||
```
|
||||
|
||||
Android подключает Kotlin-каталог напрямую через Gradle и C-файлы через NDK.
|
||||
Приложение SETGUI может импортировать модуль через уже используемый путь
|
||||
`third_party/templates/python`; в этом изменении его экран не изменяется.
|
||||
После публикации новой ревизии templates оба проекта должны обновить сабмодуль
|
||||
и пересобрать собственные native-библиотеки — вручную копировать реализации не нужно.
|
||||
|
||||
## Проверки
|
||||
|
||||
- CMake/CTest: `shared_trends`, плюс существующие protocol/ABI тесты.
|
||||
- JVM: `TrendSettingsJsonTest`, `GuiGasWatchTest`; один JSON fixture с Python.
|
||||
- Python: `PYTHONPATH=python python -m unittest discover -s python/tests`.
|
||||
Для проверки настоящего C-декодера задайте `SETPROTOCOL_LIBRARY` путём DLL/SO.
|
||||
@@ -48,7 +48,7 @@ uint16_t n = pcan_gas_read_block(&map, 0x0000, block, 4);
|
||||
Тип сообщения `PCAN_MSG_GAS` (`0b0011`). Адрес первого регистра лежит
|
||||
в `MsgBody`, данные — до 4 регистров подряд, младшим байтом вперёд.
|
||||
Это ровно то, что делает `PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()`
|
||||
в `SETCAN/Src/protocan.c`, поэтому обмен совместим с существующими
|
||||
в `ports/stm32-bxcan/protocan.c` (бывший SETCAN), поэтому обмен совместим с существующими
|
||||
устройствами.
|
||||
|
||||
| Кадр | DLC | Смысл |
|
||||
|
||||
61
c/set-protocol/include/balsam_can.h
Normal file
61
c/set-protocol/include/balsam_can.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @file balsam_can.h
|
||||
* @brief CAN_Bal_2812 extended-CAN register space.
|
||||
*
|
||||
* 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
|
||||
#define BALSAM_CAN_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define BALSAM_CAN_BASE_ID 0x00BA0000UL
|
||||
#define BALSAM_CAN_NODE_COUNT 13U
|
||||
#define BALSAM_CAN_DATA_OFFSET 0x10U
|
||||
#define BALSAM_CAN_DLC 8U
|
||||
#define BALSAM_CAN_REGISTER_COUNT 3U
|
||||
|
||||
typedef enum {
|
||||
BALSAM_CAN_TO_NODE = 0,
|
||||
BALSAM_CAN_FROM_NODE = 1
|
||||
} balsam_can_direction_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t device;
|
||||
uint8_t direction;
|
||||
uint8_t present_mask;
|
||||
uint16_t start_address;
|
||||
uint16_t values[BALSAM_CAN_REGISTER_COUNT];
|
||||
} balsam_can_frame_t;
|
||||
|
||||
/** Return non-zero for the command/data/terminal IDs used by CAN_Bal_2812. */
|
||||
int balsam_can_is_id(uint32_t can_id);
|
||||
|
||||
/**
|
||||
* Decode an 8-byte Balsam frame. Returns 1 on success, 0 for another CAN ID,
|
||||
* -1 for invalid arguments and -2 for a Balsam ID with a non-8-byte payload.
|
||||
*/
|
||||
int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||
balsam_can_frame_t *output);
|
||||
|
||||
/** Human-readable Russian name of a CAN_Bal_2812 device. */
|
||||
const char *balsam_can_device_name(uint8_t device);
|
||||
|
||||
/**
|
||||
* 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,
|
||||
char *output, size_t output_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BALSAM_CAN_H */
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user