commit e736fab46ac8d310dd5cd35c5e8f25bbde5dbbdd Author: gromov_ia <1keyten1@gmail.com> Date: Fri Oct 2 14:59:10 2026 +0300 Прошивка моста BL616 для Tang Primer 25K и программатор TangProgrammer - src/: прошивка встроенного отладчика BL616 — составное USB-устройство 1209:C0CA: DirtyJTAG v3 (WinUSB через MS OS 2.0) + CDC-ACM UART к FPGA B3/C3; UART до 20 Мбод на DMA, JTAG до 30 МГц через аппаратный SPI; - bin/: загрузчик Sipeed (0x0) и штатная прошивка 2025030317 для отката; - gui/: TangProgrammer.exe (C# WinForms, .NET Framework 4) — загрузка FPGA в SRAM и во флеш-память, копия и стирание флеш-памяти FPGA, прошивка моста; светлая и тёмная тема; - scripts/: setup, build, flash, build_gui, make_portable, а также make_release и publish_release для релизов Gitea; - fpga_test/, tests/: тестовые проекты Gowin (loopback, stream) и тесты UART на Python; - tools/openFPGALoader/: openFPGALoader 1.1.1 для Windows x64. Co-Authored-By: Claude Opus 5.5 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..65df984 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +* text=auto + +*.bin binary +*.exe binary +*.dll binary +*.ico binary +*.fs binary +*.zip binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f86734 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# SDK и тулчейн: scripts\setup.ps1 скачивает их сюда (~2.4 ГБ) +/third_party + +# Сборка прошивки моста, программы и релиза +build/ +build_log.txt +dist/ +/TangProgrammer.exe +*.pdb + +# Дампы флеш-памяти конкретной платы +backup/ + +# Вывод Gowin для тестовых битстримов (fpga_test\*\build.tcl) +fpga_test/*/impl/ +fpga_test/*/*.gprj* + +# Python +__pycache__/ +*.pyc diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..87c097c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +# Изменения + +## v1.0.0 — 2026-10-01 + +Первый выпуск. + +**Прошивка моста BL616** (`tang25k_debugger_bl616.bin`, версия 1.0.0) + +* Составное USB-устройство `1209:C0CA`: DirtyJTAG v3 (WinUSB ставится сам, + без Zadig) + CDC-ACM UART к пинам FPGA B3/C3; работают одновременно. +* UART от 300 бод до 20 Мбод, 5–8 бит, чётность none/odd/even, 1/1.5/2 + стоп-бита. Проверено 9600 … 12 Мбод в обе стороны без потерь, поток + FPGA → ПК 10 Мбод непрерывно 30 с без ошибок. +* JTAG до 30 МГц, длинные сдвиги через аппаратный SPI. Загрузка GW5A-25 в + SRAM ≈ 2 с при 6 МГц, запись во флеш-память FPGA с проверкой ≈ 8 с. +* Обход особенностей BL616: зависание приёмника UART при смене скорости + посреди байта, запись `usbser.sys` без завершающего ZLP, потеря ZLP на IN. +* Переход в загрузчик без перемычки: открыть COM-порт моста на 1234 бод или + держать S3 при подключении USB. + +Пишется вместе с загрузчиком Sipeed: `bl616_bootloader_0x20000_nano20k_signed.bin` +по адресу `0x0`, прошивка моста по адресу `0x20000`. Откат — +`bl616_fpga_partner_25kDock.bin` (штатная Sipeed 2025030317) по адресу `0x0`. + +**Программатор TangProgrammer** (Windows 10/11 x64, .NET Framework 4) + +* FPGA: загрузка `.fs` в SRAM и во флеш-память, автозагрузка при пересборке + в Gowin IDE, перезапуск из флеш-памяти. +* Флеш-память FPGA: определение микросхемы, резервная копия с двойным чтением, + запись `.bin`/`.fs`, стирание. +* Прошивка моста BL616 и возврат штатной прошивки Sipeed. +* Светлая и тёмная тема. + +**Тестовые битстримы** для GW5A-25: `loopback.fs` (петля UART) и `stream.fs` +(поток 10 Мбод) — к `tests\uart_loopback_test.py` и `tests\uart_stream_test.py`. + +**Ограничения:** Gowin Programmer с этой прошивкой не работает (только +openFPGALoader); `--unprotect-flash` с флеш-памятью XTX на плате не использовать. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0467537 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.15) + +find_package(bouffalo_sdk REQUIRED HINTS $ENV{BL_SDK_BASE}) + +# The SDK adds -save-temps/-fstack-usage to every object. They only produce +# extra files and make Windows builds (antivirus scanning) many times slower. +get_target_property(_sdk_opts sdk_intf_lib INTERFACE_COMPILE_OPTIONS) +list(REMOVE_ITEM _sdk_opts "-save-temps=obj" "-fstack-usage") +set_target_properties(sdk_intf_lib PROPERTIES INTERFACE_COMPILE_OPTIONS "${_sdk_opts}") + +# src/ holds board_overlay.h, which replaces the SDK bl616dk board.c/board.h +sdk_add_include_directories(src) +sdk_add_compile_definitions(-DCONFIG_USB_HS) + +target_sources(app PRIVATE + src/board_tang25k.c + src/usb_desc.c + src/djtag.c + src/jtag_io.c + src/uart_bridge.c + src/sys.c +) + +sdk_set_main_file(src/main.c) + +project(tang25k_debugger) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9b46cfb --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +SDK_DEMO_PATH ?= $(abspath .) +BL_SDK_BASE ?= $(SDK_DEMO_PATH)/third_party/bouffalo_sdk + +export BL_SDK_BASE + +CHIP ?= bl616 +BOARD ?= bl616dk +CROSS_COMPILE ?= riscv64-unknown-elf- + +include $(BL_SDK_BASE)/project.build diff --git a/README.md b/README.md new file mode 100644 index 0000000..c76baf9 --- /dev/null +++ b/README.md @@ -0,0 +1,284 @@ +# Прошивка отладчика BL616 для Tang Primer 25K + +Замена штатной прошивки встроенного отладчика (BL616) на плате **Sipeed Tang +Primer 25K Dock**. Вместо эмуляции FT2232 плата становится составным +USB-устройством из двух независимых функций: + +| Функция | Что это | Драйвер Windows | +|---|---|---| +| **JTAG** | DirtyJTAG v3 (`1209:C0CA`, интерфейс 0) для `openFPGALoader -c dirtyJtag` | WinUSB, ставится сам (MS OS 2.0) — Zadig не нужен | +| **UART** | CDC-ACM, обычный COM-порт к пинам FPGA **B3/C3** | встроенный `usbser.sys` | + +Оба работают одновременно: можно грузить битстрим, не закрывая терминал. + +## Скачать + +Готовые файлы — в [релизах](http://gitea.vector.ru/Gromov_IA/BL616-firmware/releases): + +| Файл | Что это | +|---|---| +| `TangProgrammer-<версия>-win64.zip` | **программатор с окном** — распаковать и запустить, внутри всё нужное (openFPGALoader, BLFlashCommand, прошивки моста) | +| `TangProgrammer.exe` | только программа — для клона этого репозитория (рядом должна быть папка `tools\`) | +| `tang25k_debugger_bl616.bin` | прошивка моста, пишется по адресу `0x20000` | +| `bl616_bootloader_0x20000_nano20k_signed.bin` | загрузчик Sipeed, пишется по адресу `0x0` вместе с прошивкой моста | +| `bl616_fpga_partner_25kDock.bin` | штатная прошивка Sipeed (`0x0`) — для отката | +| `fpga_test_bitstreams-GW5A-25.zip` | тестовые битстримы `loopback.fs` и `stream.fs` для проверки UART | +| `SHA256SUMS.txt` | контрольные суммы | + +Чтобы только пользоваться платой, достаточно архива `TangProgrammer-…-win64.zip`: +он прошивает и мост (один раз), и FPGA. SDK и сборка не нужны. + +## Что получилось (проверено на плате) + +| | | +|---|---| +| UART, скорости | любые от 300 бод до 20 Мбод (делитель от 160 МГц); проверено 9600 … **12 Мбод в обе стороны без единой потери** | +| UART, формат | 5–8 бит, чётность none/odd/even, 1 / 1.5 / 2 стоп-бита | +| UART, поток FPGA → ПК | 10 Мбод непрерывно (1 МБ/с) 30 с — 0 ошибок; точность частоты подтверждена генератором FPGA | +| UART, ПК → FPGA | управление потоком по USB (NAK), данные не теряются при любой скорости | +| UART, буфер | кольцевой DMA-буфер 64 КБ на приём, 16 КБ на передачу | +| JTAG | `--detect` за ~40 мс; загрузка GW5A-25 в SRAM ≈ 2 с при 6 МГц | +| JTAG, частота | до 30 МГц; длинные сдвиги идут через аппаратный SPI BL616 | + +Стресс-тест: сотни прогонов петли с произвольными скоростями и объёмами — без ошибок. + +## Как это устроено + +**Загрузка.** У BL616 на этих платах прожжён eFuse: ROM запускает с адреса +`0x0` только образ, зашифрованный ключом Sipeed. Поэтому по `0x0` пишется +загрузчик Sipeed (`bin/bl616_bootloader_0x20000_nano20k_signed.bin`, +выпущен Sipeed для Nano 20K; ключ у всех плат серии общий — проверено +сравнением шифротекстов штатных прошивок), а он безусловно передаёт +управление нашей прошивке по `0x20000`. + +**Распиновка BL616** (схема `Tang_25K_60033`): + +| Сигнал | BL616 | FPGA | +|---|---|---| +| TMS | GPIO0 | B1 | +| TCK | GPIO1 | C1 | +| TDO | GPIO2 (он же BOOT) | A2 | +| TDI | GPIO3 | A3 | +| UART TX (ПК → FPGA) | GPIO11 | **B3** (`uart_rx` в FPGA) | +| UART RX (FPGA → ПК) | GPIO10 | **C3** (`uart_tx` в FPGA) | +| светодиод LED5 | GPIO20 | — | +| кнопка S3 | GPIO12 | — | + +Кварц BL616 на Dock — **26 МГц** (а не 40 МГц, как в SDK по умолчанию). + +**Почему штатный UART работал плохо, а этот нет** — коротко о найденных +особенностях BL616, которые обходит прошивка: + +* приёмник UART BL616 намертво зависает, если сменить скорость посреди + приходящего байта (типичная ситуация: FPGA уже шлёт данные, а терминал + открывается на другой скорости); сброс бита EN, как делает SDK, не + помогает — прошивка делает полный программный сброс блока UART1 при + каждой реальной смене параметров и держит сторожевой таймер; +* `usbser.sys` пишет порциями, кратными 512 байтам, без завершающего + нулевого пакета — приём ведётся строго по одному пакету; +* нулевой пакет (ZLP) на IN, запрошенный пока прошлый пакет ещё в FIFO, + теряется — передачи к ПК всегда заканчиваются коротким пакетом. + +## Установка (один раз) + +1. Закройте Gowin Programmer / BouffaloLabDevCube, если они открыты. +2. Отключите USB-C (JTAG/UART) от платы. +3. Замкните тестовые точки **TDO** и **3V3** (нижняя сторона Dock, левый + верхний угол), подключите USB, разомкните. Появится устройство + «Bouffalo CDC DEMO» (COM-порт). +4. Прошейте мост одним из способов: + * **TangProgrammer** из релиза: вкладка «Прошивка моста (BL616)» → + «Записать прошивку моста»; + * из исходников (нужны `scripts\setup.ps1` и `scripts\build.ps1`): + ```powershell + .\scripts\flash.ps1 + ``` + *(пишет загрузчик Sipeed из `bin\` и собранную прошивку моста + `build\build_out\tang25k_debugger_bl616.bin`)*; + * вручную, BouffaloLabDevCube или BLFlashCommand: файлы из релиза, + `bl616_bootloader_0x20000_nano20k_signed.bin` по адресу `0x0` и + `tang25k_debugger_bl616.bin` по адресу `0x20000`. +5. Переподключите USB. Появятся «DirtyJTAG» и новый COM-порт. + +Перед первой прошивкой стоит сохранить полный дамп флеш-памяти BL616 +(плата в загрузчике, как в шаге 3): +```powershell +.\third_party\bouffalo_sdk\tools\bflb_tools\bouffalo_flash_cube\BLFlashCommand.exe --interface=uart --baudrate=2000000 --port=COMxx --chipname=bl616 --cpu_id= --flash --read --start=0x0 --len=0x400000 --file=backup\flash_full_4M.bin +``` +Папка `backup\` в git не попадает. + +## Обновление прошивки + +```powershell +.\scripts\build.ps1 +.\scripts\flash.ps1 +``` +`flash.ps1` сам переводит работающую прошивку в загрузчик (открывает её +COM-порт на «магической» скорости **1234 бод**) и прошивает. После записи +**переподключите USB** — флаг «загрузиться в ISP» живёт до снятия питания. + +Другие способы попасть в загрузчик: держать **S3** при подключении USB +(если эта прошивка уже стоит) или перемычка TDO→3V3 (работает всегда). + +## Возврат штатной прошивки Sipeed + +```powershell +.\scripts\flash.ps1 -Restore +``` +(или в TangProgrammer: «Вернуть штатную прошивку Sipeed»). Пишет `bin/bl616_fpga_partner_25kDock.bin` (версия 2025030317, sha256 +`1be5c1f9…aacd`) — ровно то, что стояло на плате. + +## Программатор с окном: `TangProgrammer.exe` + +Замена Gowin Programmer поверх openFPGALoader, без консоли. Готовая программа — +архив `TangProgrammer-…-win64.zip` из релиза; в клоне репозитория её собирает +`.\scripts\build_gui.ps1` (появляется `TangProgrammer.exe` в корне, рядом с +`tools\`). Ничего ставить не нужно (.NET Framework 4 есть в Windows). Вверху окна — состояние моста +(подключён / загрузчик BL616 / штатная прошивка / нет платы), оно +обновляется само при подключении USB. + +**Вкладка «FPGA»** +* битстрим `.fs` выбирается кнопкой «Обзор…» (Ctrl+O), из списка последних или + перетаскиванием в окно; под полем — модель FPGA из файла, размер и время + сборки (предупреждение, если файл собран не для GW5A-25); +* «Загрузить в SRAM» (F5) — до отключения питания; «Записать во Flash» — во + внешнюю флеш-память FPGA, с проверкой; +* «Загружать в SRAM автоматически, когда файл пересобран» — для работы вместе с + Gowin IDE: после каждой сборки новый `.fs` сразу попадает в FPGA; +* «Определить FPGA», «Перезапуск из Flash» (FPGA заново грузится из своей + флеш-памяти, как после включения питания). + +**Вкладка «Флеш-память FPGA»** — определение микросхемы (на этой плате XTX, +JEDEC `0B4017`, 8 МБ), резервная копия в `.bin` (первый 1 МБ с конфигурацией +или вся микросхема; по умолчанию читается дважды и сравнивается), запись +`.bin`/`.fs` во флеш (пустой хвост 0xFF не пишется) и стирание всей микросхемы. +Время на этой плате: запись `.fs` с проверкой ≈ 8 с, копия 1 МБ ≈ 3 с на одно +чтение, стирание ≈ 13 с. + +**Вкладка «Прошивка моста (BL616)»** — то же, что `scripts\flash.ps1`: записать +прошивку моста или вернуть штатную Sipeed. + +Справа вверху — тема: как в Windows, светлая или тёмная. Настройки хранятся в +`%LOCALAPPDATA%\Tang25kProgrammer\settings.ini`. Пересборка программы: +`.\scripts\build_gui.ps1` (исходники — в `gui\`). + +Если запись во флеш-память FPGA прервалась с ошибкой, программа сама повторяет +её один раз: недописанная флеш-память означает, что FPGA не загрузится при +включении. + +**На другой компьютер** — архив из релиза или свой: +`.\scripts\make_portable.ps1` собирает `dist\TangProgrammer\` и +`dist\TangProgrammer.zip` (≈ 14 МБ) — программа, openFPGALoader, BLFlashCommand +(только exe и `chips\` вместо 238 МБ папки SDK), прошивки моста и памятка +`README.txt`. Распаковать в любую папку, куда можно писать (не в Program Files), +и запустить `TangProgrammer.exe`. Нужна Windows 10/11 x64; драйверы для платы +Windows ставит сама. + +## Использование из консоли + +JTAG — нужен **openFPGALoader ≥ 1.1.0** (в 1.1.0 исправлена работа DirtyJTAG +с GW5A). Готовая сборка для Windows лежит в `tools\openFPGALoader\bin`. + +```powershell +$ofl = ".\tools\openFPGALoader\bin\openFPGALoader.exe" +& $ofl -c dirtyJtag --detect # GW5A-25, idcode 0x0001281b +& $ofl -c dirtyJtag design.fs # загрузить битстрим в SRAM (до отключения питания) +& $ofl -c dirtyJtag --freq 15M design.fs # загрузить в SRAM быстрее +& $ofl -c dirtyJtag -f design.fs # прошить битстрим во внешнюю флеш-память FPGA (навсегда) +& $ofl -c dirtyJtag --dump-flash --file-size 1048576 copy.bin # копия первого 1 МБ флеш-памяти FPGA +& $ofl -c dirtyJtag -f copy.bin # записать образ обратно (с адреса 0) +& $ofl -c dirtyJtag --bulk-erase # стереть всю флеш-память FPGA +``` +`--unprotect-flash` с этой микросхемой флеш-памяти не использовать: её нет в +справочнике openFPGALoader 1.1.1, и эта функция на ней падает. Защита записи +(биты BP) на плате и так снята. + +UART — любой терминал или pyserial на появившемся COM-порту, скорость и +формат задаются как обычно. При открытии порта (DTR=1) накопленные за время +простоя данные сбрасываются, как у FTDI. + +**Gowin Programmer эта прошивка не поддерживает** (он работает только с FTDI). +Для него — `flash.ps1 -Restore`. + +## Сборка из исходников + +```powershell +.\scripts\setup.ps1 # Bouffalo SDK v2.3.16 + тулчейн T-Head в third_party\ +.\scripts\build.ps1 # build\build_out\tang25k_debugger_bl616.bin +``` +Если проект лежит на сетевом диске, который смотрит на эту же машину +(например `D:` → `\\PC\Projects`), скрипты сами собирают по локальному пути +— через SMB сборка в десятки раз медленнее. + +## Тесты + +`fpga_test\` — два тестовых проекта Gowin (готовые `.fs` есть в релизе): + +* `loopback` — `uart_tx = uart_rx`, петля на любой скорости; +* `stream` — FPGA сама шлёт счётчик на ровно 10 Мбод. + +Сборка — из папки проекта, например: +```powershell +cd fpga_test\loopback; & C:\Gowin\Gowin_V1.9.12_x64\IDE\bin\gw_sh.exe build.tcl; cd ..\.. +``` + +```powershell +& $ofl -c dirtyJtag fpga_test\loopback\impl\pnr\loopback.fs +python tests\uart_loopback_test.py --port COM14 +& $ofl -c dirtyJtag fpga_test\stream\impl\pnr\stream.fs +python tests\uart_stream_test.py --port COM14 --seconds 30 --switches 10 +``` + +## Выпуск релиза + +1. Поднять `FW_VERSION_STR` в `src/sys.h` и описать изменения в `CHANGELOG.md`. +2. Собрать прошивку и тестовые битстримы, затем файлы релиза: + ```powershell + .\scripts\build.ps1 + .\scripts\make_release.ps1 # dist\release\v<версия>\ + SHA256SUMS.txt + ``` +3. Закоммитить, поставить тег и отправить: + ```powershell + git tag -a v1.0.0 -m "v1.0.0" + git push origin main v1.0.0 + ``` +4. Опубликовать релиз с файлами — нужен токен Gitea с правом + `write:repository` (Настройки → Приложения → Создать токен): + ```powershell + $env:GITEA_TOKEN = "<токен>" + .\scripts\publish_release.ps1 + ``` + Или вручную: «Релизы» → «Новый релиз» → тег, текст из `CHANGELOG.md`, + файлы из `dist\release\v<версия>\`. + +## Ограничения + +* Gowin Programmer не работает (только openFPGALoader). +* Чётность mark/space не поддерживается аппаратно (передаётся как «нет»). +* DTR/RTS и BREAK не выведены на FPGA — на плате нет таких линий. +* Нулевой пакет на JTAG-IN не отправляется: клиенты DirtyJTAG читают ответ + известной длины. + +## Структура + +``` +src/ прошивка (main, JTAG, UART-мост, USB-дескрипторы, плата) +gui/ исходники TangProgrammer.exe (C# WinForms, .NET Framework 4) +bin/ загрузчик Sipeed и штатная прошивка для отката +scripts/ setup / build / flash / build_gui / make_portable / make_release / publish_release +fpga_test/ тестовые проекты Gowin (loopback, stream) +tests/ тесты UART на Python (pyserial) +tools/ openFPGALoader 1.1.1 для Windows +``` + +Не в git (создаются скриптами): + +``` +third_party/ Bouffalo SDK и тулчейн (setup.ps1) +build/ сборка прошивки моста: build\build_out\tang25k_debugger_bl616.bin (build.ps1) +dist/ переносная версия программы и файлы релиза (make_portable.ps1, make_release.ps1) +backup/ дампы флеш-памяти своей платы +``` + +Диагностика: vendor-запрос `0x42` к устройству (`bmRequestType=0xC0`) +возвращает версию, частоту JTAG, скорость UART и счётчики байт. diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 0000000..aa00cda --- /dev/null +++ b/bin/README.md @@ -0,0 +1,16 @@ +# Образы Sipeed для BL616 + +Не собираются из этого репозитория — взяты у Sipeed как есть. Нужны для +прошивки моста (`flash_prog_cfg.ini`, `restore_prog_cfg.ini`). + +| Файл | Адрес | Зачем | +|---|---|---| +| `bl616_bootloader_0x20000_nano20k_signed.bin` | `0x0` | загрузчик, зашифрованный ключом Sipeed: ROM с прожжённым eFuse запускает только такие образы. Безусловно передаёт управление на `0x20000`, где лежит наша прошивка | +| `bl616_fpga_partner_25kDock.bin` | `0x0` | штатная прошивка отладчика Tang Primer 25K Dock (эмуляция FT2232), версия 2025030317 — для отката (`scripts\flash.ps1 -Restore`) | + +SHA-256: + +``` +1b5d0ed698a3f2bf0b9d4f72d242868da993e737bbea0c8288267b69b73fbc12 bl616_bootloader_0x20000_nano20k_signed.bin +1be5c1f99270ccc483b5570bec01d877e22902c8b402141f6eeb4ac5b745aacd bl616_fpga_partner_25kDock.bin +``` diff --git a/bin/bl616_bootloader_0x20000_nano20k_signed.bin b/bin/bl616_bootloader_0x20000_nano20k_signed.bin new file mode 100644 index 0000000..7742abe Binary files /dev/null and b/bin/bl616_bootloader_0x20000_nano20k_signed.bin differ diff --git a/bin/bl616_fpga_partner_25kDock.bin b/bin/bl616_fpga_partner_25kDock.bin new file mode 100644 index 0000000..925908e Binary files /dev/null and b/bin/bl616_fpga_partner_25kDock.bin differ diff --git a/defconfig b/defconfig new file mode 100644 index 0000000..5a11545 --- /dev/null +++ b/defconfig @@ -0,0 +1,10 @@ +CONFIG_CHERRYUSB =y +CONFIG_CHERRYUSB_DEVICE =y +CONFIG_CHERRYUSB_DEVICE_CDC_ACM =y + +CONFIG_NEWLIB =n +CONFIG_NEWLIB_STANDARD =n +CONFIG_VSNPRINTF_NANO =y +CONFIG_BFLB_LOG =n +CONFIG_COREDUMP =n +CONFIG_TLSF =y diff --git a/flash_prog_cfg.ini b/flash_prog_cfg.ini new file mode 100644 index 0000000..cad7b0e --- /dev/null +++ b/flash_prog_cfg.ini @@ -0,0 +1,17 @@ +[cfg] +# 0: no erase, 1: programmed section erase, 2: chip erase +erase = 1 +# skip mode set first para is skip addr, second para is skip len, multi-segment region with ; separated +skip_mode = 0x0, 0x0 +# 0: not use isp mode, #1: isp mode +boot2_isp_mode = 0 + +# Sipeed-encrypted stage-1 loader: the eFuse secure boot only runs images +# encrypted with Sipeed's key at 0x0. It unconditionally jumps to 0x20000. +[bootloader] +filedir = ./bin/bl616_bootloader_0x20000_nano20k_signed.bin +address = 0x0 + +[FW] +filedir = ./build/build_out/tang25k_debugger_$(CHIPNAME).bin +address = 0x20000 diff --git a/fpga_test/loopback/build.tcl b/fpga_test/loopback/build.tcl new file mode 100644 index 0000000..ed04842 --- /dev/null +++ b/fpga_test/loopback/build.tcl @@ -0,0 +1,9 @@ +# gw_sh build.tcl (run from this directory) +set_device -name GW5A-25A GW5A-LV25MG121NC1/I0 +add_file -type verilog loopback.v +add_file -type cst loopback.cst +set_option -top_module loopback +set_option -output_base_name loopback +set_option -use_sspi_as_gpio 1 +set_option -use_cpu_as_gpio 1 +run all diff --git a/fpga_test/loopback/loopback.cst b/fpga_test/loopback/loopback.cst new file mode 100644 index 0000000..5f2b374 --- /dev/null +++ b/fpga_test/loopback/loopback.cst @@ -0,0 +1,5 @@ +IO_LOC "uart_rx" B3; +IO_LOC "uart_tx" C3; + +IO_PORT "uart_rx" IO_TYPE=LVCMOS33 PULL_MODE=UP BANK_VCCIO=3.3; +IO_PORT "uart_tx" IO_TYPE=LVCMOS33 PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; diff --git a/fpga_test/loopback/loopback.v b/fpga_test/loopback/loopback.v new file mode 100644 index 0000000..2a75873 --- /dev/null +++ b/fpga_test/loopback/loopback.v @@ -0,0 +1,8 @@ +// UART loopback for testing the BL616 bridge: everything the PC sends to the +// serial port comes straight back, at any baud rate / format. +module loopback ( + input wire uart_rx, // B3, driven by BL616 GPIO11 (TX) + output wire uart_tx // C3, read by BL616 GPIO10 (RX) +); + assign uart_tx = uart_rx; +endmodule diff --git a/fpga_test/stream/build.tcl b/fpga_test/stream/build.tcl new file mode 100644 index 0000000..6e6efb4 --- /dev/null +++ b/fpga_test/stream/build.tcl @@ -0,0 +1,9 @@ +# gw_sh build.tcl (run from this directory) +set_device -name GW5A-25A GW5A-LV25MG121NC1/I0 +add_file -type verilog stream.v +add_file -type cst stream.cst +set_option -top_module stream +set_option -output_base_name stream +set_option -use_sspi_as_gpio 1 +set_option -use_cpu_as_gpio 1 +run all diff --git a/fpga_test/stream/stream.cst b/fpga_test/stream/stream.cst new file mode 100644 index 0000000..d519643 --- /dev/null +++ b/fpga_test/stream/stream.cst @@ -0,0 +1,5 @@ +IO_LOC "clk" E2; +IO_LOC "uart_tx" C3; + +IO_PORT "clk" IO_TYPE=LVCMOS33 PULL_MODE=NONE BANK_VCCIO=3.3; +IO_PORT "uart_tx" IO_TYPE=LVCMOS33 PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; diff --git a/fpga_test/stream/stream.v b/fpga_test/stream/stream.v new file mode 100644 index 0000000..9eca7a0 --- /dev/null +++ b/fpga_test/stream/stream.v @@ -0,0 +1,40 @@ +// Continuous UART stream from the FPGA at exactly 10 Mbaud, 8N1 +// (50 MHz / 5 clocks per bit), payload is a free-running byte counter. +// Checks absolute baud accuracy of the BL616 and sustained RX throughput: +// every received byte must be the previous one + 1. +module stream ( + input wire clk, // E2, 50 MHz + output reg uart_tx = 1'b1 // C3, read by BL616 GPIO10 (RX) +); + localparam CLKS_PER_BIT = 5; + + reg [2:0] div = 0; + reg [3:0] bitn = 0; // 0 start, 1..8 data, 9 stop + reg [7:0] data = 0; + reg [7:0] shreg = 0; + + always @(posedge clk) begin + if (div == CLKS_PER_BIT - 1) begin + div <= 0; + case (bitn) + 4'd0: begin + uart_tx <= 1'b0; + shreg <= data; + bitn <= 1; + end + 4'd9: begin + uart_tx <= 1'b1; + data <= data + 8'd1; + bitn <= 0; + end + default: begin + uart_tx <= shreg[0]; + shreg <= {1'b0, shreg[7:1]}; + bitn <= bitn + 4'd1; + end + endcase + end else begin + div <= div + 3'd1; + end + end +endmodule diff --git a/gui/Controls.cs b/gui/Controls.cs new file mode 100644 index 0000000..e05c28c --- /dev/null +++ b/gui/Controls.cs @@ -0,0 +1,793 @@ +// Light/dark palette and the owner-drawn controls of the programmer GUI. +// Stock WinForms controls (tabs, buttons, check boxes, progress bar) are +// drawn by the light Windows theme only, so these paint themselves. +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using Microsoft.Win32; + +namespace TangProgrammer +{ + enum Tone { Text, Dim, Ok, Err, Warn, Title } + + sealed class Theme + { + public bool IsDark; + public Color Window, Card, CardBorder, LogBg; + public Color Text, TextDim, TextDisabled; + public Color Button, ButtonHover, ButtonPressed, ButtonBorder, ButtonDisabled; + public Color Accent, AccentHover, AccentPressed, AccentText, AccentDisabled, AccentTextDisabled; + public Color ToggleFill, ToggleBorder; + public Color Input, Track; + public Color Ok, Err, Warn, Title; + public Color DotOn, DotWarn, DotOff; + public Color MenuBg, MenuHover, MenuBorder; + + public static readonly Theme Light = new Theme + { + Window = Rgb(0xF3F3F3), Card = Rgb(0xFFFFFF), CardBorder = Rgb(0xE3E3E3), LogBg = Rgb(0xFFFFFF), + Text = Rgb(0x1B1B1B), TextDim = Rgb(0x626262), TextDisabled = Rgb(0xA3A3A3), + Button = Rgb(0xFBFBFB), ButtonHover = Rgb(0xF3F3F3), ButtonPressed = Rgb(0xEBEBEB), + ButtonBorder = Rgb(0xD3D3D3), ButtonDisabled = Rgb(0xF5F5F5), + Accent = Rgb(0x0067C0), AccentHover = Rgb(0x1975C5), AccentPressed = Rgb(0x3183CA), + AccentText = Rgb(0xFFFFFF), AccentDisabled = Rgb(0xC5C5C5), AccentTextDisabled = Rgb(0xFFFFFF), + ToggleFill = Rgb(0xFAFAFA), ToggleBorder = Rgb(0x8A8A8A), + Input = Rgb(0xFFFFFF), Track = Rgb(0xE0E0E0), + Ok = Rgb(0x0F7B0F), Err = Rgb(0xC42B1C), Warn = Rgb(0x9D5D00), Title = Rgb(0x005FB8), + DotOn = Rgb(0x13A10E), DotWarn = Rgb(0xE8911A), DotOff = Rgb(0xA0A0A0), + MenuBg = Rgb(0xFFFFFF), MenuHover = Rgb(0xEDEDED), MenuBorder = Rgb(0xD5D5D5), + }; + + public static readonly Theme Dark = new Theme + { + IsDark = true, + Window = Rgb(0x202020), Card = Rgb(0x2B2B2B), CardBorder = Rgb(0x3A3A3A), LogBg = Rgb(0x252525), + Text = Rgb(0xEDEDED), TextDim = Rgb(0xA6A6A6), TextDisabled = Rgb(0x6E6E6E), + Button = Rgb(0x373737), ButtonHover = Rgb(0x3E3E3E), ButtonPressed = Rgb(0x333333), + ButtonBorder = Rgb(0x484848), ButtonDisabled = Rgb(0x313131), + Accent = Rgb(0x4CC2FF), AccentHover = Rgb(0x48B1E8), AccentPressed = Rgb(0x42A1D2), + AccentText = Rgb(0x000000), AccentDisabled = Rgb(0x454545), AccentTextDisabled = Rgb(0x9A9A9A), + ToggleFill = Rgb(0x262626), ToggleBorder = Rgb(0x9E9E9E), + Input = Rgb(0x383838), Track = Rgb(0x474747), + Ok = Rgb(0x6CCB5F), Err = Rgb(0xFF99A4), Warn = Rgb(0xFCE100), Title = Rgb(0x60CDFF), + DotOn = Rgb(0x6CCB5F), DotWarn = Rgb(0xF7B955), DotOff = Rgb(0x6E6E6E), + MenuBg = Rgb(0x2C2C2C), MenuHover = Rgb(0x3D3D3D), MenuBorder = Rgb(0x484848), + }; + + public static Theme Current = Light; + + static Color Rgb(int rgb) + { + return Color.FromArgb((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); + } + + public Color Of(Tone tone) + { + switch (tone) + { + case Tone.Dim: return TextDim; + case Tone.Ok: return Ok; + case Tone.Err: return Err; + case Tone.Warn: return Warn; + case Tone.Title: return Title; + default: return Text; + } + } + + // "Choose your app mode" in the Windows settings + public static bool SystemPrefersDark() + { + try + { + using (RegistryKey k = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")) + { + object v = k == null ? null : k.GetValue("AppsUseLightTheme"); + return v is int && (int)v == 0; + } + } + catch (Exception) + { + return false; + } + } + } + + static class Ui + { + public static float Scale = 1f; + + public static int S(float px) + { + return (int)Math.Round(px * Scale); + } + + public static GraphicsPath Round(RectangleF r, float radius) + { + var p = new GraphicsPath(); + float d = Math.Min(radius * 2, Math.Min(r.Width, r.Height)); + if (d < 1f) + { + p.AddRectangle(r); + return p; + } + p.AddArc(r.X, r.Y, d, d, 180, 90); + p.AddArc(r.Right - d, r.Y, d, d, 270, 90); + p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90); + p.AddArc(r.X, r.Bottom - d, d, d, 90, 90); + p.CloseFigure(); + return p; + } + + public static Color ParentBack(Control c) + { + return c.Parent != null ? c.Parent.BackColor : c.BackColor; + } + + [DllImport("uxtheme.dll", CharSet = CharSet.Unicode)] + static extern int SetWindowTheme(IntPtr hwnd, string subAppName, string subIdList); + + [DllImport("dwmapi.dll")] + static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int value, int size); + + [DllImport("user32.dll")] + static extern IntPtr SendMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam); + + // dark scroll bars and combo boxes of native controls (Windows 10 1809+); + // null restores the default look + public static void SetNativeTheme(Control c, string themeClass) + { + SetWindowTheme(c.Handle, themeClass, null); + c.Invalidate(); + } + + public static void SetDarkTitleBar(Form f, bool dark) + { + int v = dark ? 1 : 0; + // DWMWA_USE_IMMERSIVE_DARK_MODE: 20, was 19 before Windows 10 20H1 + if (DwmSetWindowAttribute(f.Handle, 20, ref v, 4) != 0) + DwmSetWindowAttribute(f.Handle, 19, ref v, 4); + } + + public static void SetRedraw(Control c, bool on) + { + SendMessage(c.Handle, 0x000B /* WM_SETREDRAW */, (IntPtr)(on ? 1 : 0), IntPtr.Zero); + if (on) + c.Invalidate(); + } + } + + // White (or dark) rounded panel with a thin border + sealed class Card : Panel + { + public Color Border = Color.Gray; + + public Card() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + Graphics g = e.Graphics; + g.Clear(Ui.ParentBack(this)); + g.SmoothingMode = SmoothingMode.AntiAlias; + using (GraphicsPath p = Ui.Round(new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f), Ui.S(7))) + using (var fill = new SolidBrush(BackColor)) + using (var pen = new Pen(Border)) + { + g.FillPath(fill, p); + g.DrawPath(pen, p); + } + } + } + + // Text tabs with an accent underline (replaces the TabControl, which can't be dark) + sealed class TabStrip : Control + { + readonly List items = new List(); + int selected, hot = -1; + Font bold; + + public event EventHandler SelectedIndexChanged; + + public TabStrip() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + SetStyle(ControlStyles.Selectable, false); + } + + public void Add(string text) + { + items.Add(text); + Invalidate(); + } + + public int SelectedIndex + { + get { return selected; } + set + { + if (value == selected || value < 0 || value >= items.Count) + return; + selected = value; + Invalidate(); + if (SelectedIndexChanged != null) + SelectedIndexChanged(this, EventArgs.Empty); + } + } + + public int Count + { + get { return items.Count; } + } + + Font Bold + { + get + { + if (bold == null || bold.Size != Font.Size) + bold = new Font("Segoe UI Semibold", Font.Size); + return bold; + } + } + + int ItemWidth(int i) + { + return TextRenderer.MeasureText(items[i], Bold).Width + Ui.S(24); + } + + Rectangle ItemRect(int i) + { + int x = 0; + for (int k = 0; k < i; k++) + x += ItemWidth(k); + return new Rectangle(x, 0, ItemWidth(i), Height); + } + + int HitTest(Point p) + { + for (int i = 0; i < items.Count; i++) + if (ItemRect(i).Contains(p)) + return i; + return -1; + } + + public override Size GetPreferredSize(Size proposed) + { + int w = 0; + for (int i = 0; i < items.Count; i++) + w += ItemWidth(i); + return new Size(w, Font.Height + Ui.S(16)); + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + } + + protected override void OnPaint(PaintEventArgs e) + { + Theme th = Theme.Current; + Graphics g = e.Graphics; + g.Clear(Ui.ParentBack(this)); + int line = Ui.S(3); + for (int i = 0; i < items.Count; i++) + { + Rectangle r = ItemRect(i); + bool sel = i == selected; + Color c = sel || i == hot ? th.Text : th.TextDim; + TextRenderer.DrawText(g, items[i], sel ? Bold : Font, new Rectangle(r.X, 0, r.Width, r.Height - line), + c, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | + TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix); + if (sel) + { + int w = TextRenderer.MeasureText(items[i], Bold).Width - Ui.S(6); + var u = new RectangleF(r.X + (r.Width - w) / 2f, r.Bottom - line - Ui.S(1), w, line); + g.SmoothingMode = SmoothingMode.AntiAlias; + using (GraphicsPath p = Ui.Round(u, line / 2f)) + using (var b = new SolidBrush(th.Accent)) + g.FillPath(b, p); + g.SmoothingMode = SmoothingMode.None; + } + } + } + + protected override void OnMouseMove(MouseEventArgs e) + { + int h = HitTest(e.Location); + if (h != hot) + { + hot = h; + Cursor = h >= 0 && h != selected ? Cursors.Hand : Cursors.Default; + Invalidate(); + } + base.OnMouseMove(e); + } + + protected override void OnMouseLeave(EventArgs e) + { + hot = -1; + Invalidate(); + base.OnMouseLeave(e); + } + + protected override void OnMouseDown(MouseEventArgs e) + { + int h = HitTest(e.Location); + if (h >= 0 && e.Button == MouseButtons.Left) + { + SelectedIndex = h; + Cursor = Cursors.Default; + } + base.OnMouseDown(e); + } + + protected override void Dispose(bool disposing) + { + if (disposing && bold != null) + bold.Dispose(); + base.Dispose(disposing); + } + } + + enum Glyph { None, Contrast } + + // Rounded button in the colors of the current theme; Accent = the main action + sealed class FlatButton : Button + { + public bool Accent; + public Glyph Glyph; + bool hover, pressed; + + public FlatButton() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + FlatStyle = FlatStyle.Flat; + FlatAppearance.BorderSize = 0; + UseVisualStyleBackColor = false; + } + + public override Size GetPreferredSize(Size proposed) + { + Size t = TextRenderer.MeasureText(Text, Font, Size.Empty, TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix); + return new Size(Math.Max(MinimumSize.Width, t.Width + Padding.Horizontal + Ui.S(8)), + Math.Max(MinimumSize.Height, t.Height + Padding.Vertical + Ui.S(6))); + } + + protected override void OnMouseEnter(EventArgs e) + { + hover = true; + Invalidate(); + base.OnMouseEnter(e); + } + + protected override void OnMouseLeave(EventArgs e) + { + hover = pressed = false; + Invalidate(); + base.OnMouseLeave(e); + } + + protected override void OnMouseDown(MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + pressed = true; + Invalidate(); + } + base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseEventArgs e) + { + pressed = false; + Invalidate(); + base.OnMouseUp(e); + } + + protected override void OnEnabledChanged(EventArgs e) + { + hover = pressed = false; + Invalidate(); + base.OnEnabledChanged(e); + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + } + + protected override void OnPaint(PaintEventArgs e) + { + Theme th = Theme.Current; + Graphics g = e.Graphics; + g.Clear(Ui.ParentBack(this)); + Color bg, fg, border; + if (Accent) + { + bg = !Enabled ? th.AccentDisabled : pressed ? th.AccentPressed : hover ? th.AccentHover : th.Accent; + fg = Enabled ? th.AccentText : th.AccentTextDisabled; + border = bg; + } + else + { + bg = !Enabled ? th.ButtonDisabled : pressed ? th.ButtonPressed : hover ? th.ButtonHover : th.Button; + fg = Enabled ? th.Text : th.TextDisabled; + border = th.ButtonBorder; + } + g.SmoothingMode = SmoothingMode.AntiAlias; + using (GraphicsPath p = Ui.Round(new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f), Ui.S(4))) + using (var b = new SolidBrush(bg)) + using (var pen = new Pen(border)) + { + g.FillPath(b, p); + g.DrawPath(pen, p); + } + if (Focused && ShowFocusCues) + { + using (GraphicsPath p = Ui.Round(new RectangleF(2.5f, 2.5f, Width - 5f, Height - 5f), Ui.S(3))) + using (var pen = new Pen(fg)) + g.DrawPath(pen, p); + } + if (Glyph == Glyph.Contrast) + { + float d = Math.Min(Width, Height) * 0.46f; + var r = new RectangleF((Width - d) / 2f, (Height - d) / 2f, d, d); + using (var b = new SolidBrush(fg)) + g.FillPie(b, r.X, r.Y, r.Width, r.Height, 90, 180); + using (var pen = new Pen(fg, Math.Max(1.2f, Ui.Scale * 1.3f))) + g.DrawEllipse(pen, r); + return; + } + g.SmoothingMode = SmoothingMode.None; + TextRenderer.DrawText(g, Text, Font, ClientRectangle, fg, + TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | + TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix | TextFormatFlags.EndEllipsis); + } + } + + static class TogglePainter + { + const TextFormatFlags Flags = TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix; + + static int Box(Control c) + { + return Math.Max(13, c.Font.Height); + } + + public static Size Measure(ButtonBase c) + { + Size t = TextRenderer.MeasureText(c.Text, c.Font, Size.Empty, Flags); + int box = Box(c); + return new Size(box + Ui.S(8) + t.Width + Ui.S(2), Math.Max(box, t.Height) + Ui.S(4)); + } + + public static void Paint(ButtonBase c, Graphics g, bool on, bool radio, bool hover, bool focus) + { + Theme th = Theme.Current; + g.Clear(Ui.ParentBack(c)); + int box = Box(c); + var r = new RectangleF(0.5f, (c.Height - box) / 2f + 0.5f, box - 1f, box - 1f); + Color accent = !c.Enabled ? th.AccentDisabled : hover ? th.AccentHover : th.Accent; + Color border = !c.Enabled ? th.TextDisabled : hover ? th.Text : th.ToggleBorder; + g.SmoothingMode = SmoothingMode.AntiAlias; + if (radio) + { + if (on) + { + using (var b = new SolidBrush(accent)) + g.FillEllipse(b, r); + float d = box * 0.4f; + using (var b = new SolidBrush(th.AccentText)) + g.FillEllipse(b, r.X + (r.Width - d) / 2f, r.Y + (r.Height - d) / 2f, d, d); + } + else + { + using (var b = new SolidBrush(th.ToggleFill)) + g.FillEllipse(b, r); + using (var pen = new Pen(border)) + g.DrawEllipse(pen, r); + } + } + else + { + using (GraphicsPath p = Ui.Round(r, box * 0.2f)) + { + if (on) + { + using (var b = new SolidBrush(accent)) + g.FillPath(b, p); + using (var pen = new Pen(th.AccentText, Math.Max(1.5f, box * 0.12f))) + { + pen.StartCap = pen.EndCap = LineCap.Round; + pen.LineJoin = LineJoin.Round; + g.DrawLines(pen, new[] + { + new PointF(r.X + box * 0.25f, r.Y + box * 0.52f), + new PointF(r.X + box * 0.43f, r.Y + box * 0.70f), + new PointF(r.X + box * 0.76f, r.Y + box * 0.32f), + }); + } + } + else + { + using (var b = new SolidBrush(th.ToggleFill)) + g.FillPath(b, p); + using (var pen = new Pen(border)) + g.DrawPath(pen, p); + } + } + } + g.SmoothingMode = SmoothingMode.None; + var tr = new Rectangle(box + Ui.S(8), 0, Math.Max(0, c.Width - box - Ui.S(8)), c.Height); + TextRenderer.DrawText(g, c.Text, c.Font, tr, c.Enabled ? th.Text : th.TextDisabled, + Flags | TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis); + if (focus) + { + Size t = TextRenderer.MeasureText(c.Text, c.Font, Size.Empty, Flags); + ControlPaint.DrawFocusRectangle(g, new Rectangle(tr.X - 1, (c.Height - t.Height) / 2, t.Width + 2, t.Height), + th.Text, Ui.ParentBack(c)); + } + } + } + + sealed class ThemedCheck : CheckBox + { + bool hover; + + public ThemedCheck() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + } + + public override Size GetPreferredSize(Size proposed) + { + return TogglePainter.Measure(this); + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + } + + protected override void OnPaint(PaintEventArgs e) + { + TogglePainter.Paint(this, e.Graphics, Checked, false, hover, Focused && ShowFocusCues); + } + + protected override void OnMouseEnter(EventArgs e) + { + hover = true; + Invalidate(); + base.OnMouseEnter(e); + } + + protected override void OnMouseLeave(EventArgs e) + { + hover = false; + Invalidate(); + base.OnMouseLeave(e); + } + } + + sealed class ThemedRadio : RadioButton + { + bool hover; + + public ThemedRadio() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + } + + public override Size GetPreferredSize(Size proposed) + { + return TogglePainter.Measure(this); + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + } + + protected override void OnPaint(PaintEventArgs e) + { + TogglePainter.Paint(this, e.Graphics, Checked, true, hover, Focused && ShowFocusCues); + } + + protected override void OnMouseEnter(EventArgs e) + { + hover = true; + Invalidate(); + base.OnMouseEnter(e); + } + + protected override void OnMouseLeave(EventArgs e) + { + hover = false; + Invalidate(); + base.OnMouseLeave(e); + } + } + + enum BarState { Running, Ok, Error, Paused } + + // Thin rounded progress bar; Marquee = "busy, amount unknown" + sealed class FlatProgress : Control + { + int value; + bool marquee; + BarState state; + float phase; + readonly Timer timer = new Timer { Interval = 15 }; + + public FlatProgress() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + SetStyle(ControlStyles.Selectable, false); + timer.Tick += (s, e) => + { + phase += 0.009f; + if (phase > 1f) + phase -= 1f; + Invalidate(); + }; + } + + public int Value + { + get { return value; } + set { this.value = Math.Max(0, Math.Min(100, value)); Invalidate(); } + } + + public bool Marquee + { + get { return marquee; } + set { marquee = value; phase = 0; timer.Enabled = value; Invalidate(); } + } + + public BarState State + { + get { return state; } + set { state = value; Invalidate(); } + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + } + + protected override void OnPaint(PaintEventArgs e) + { + Theme th = Theme.Current; + Graphics g = e.Graphics; + g.Clear(Ui.ParentBack(this)); + g.SmoothingMode = SmoothingMode.AntiAlias; + float h = Math.Min(Height, Ui.S(6)); + var track = new RectangleF(0, (Height - h) / 2f, Width, h); + using (GraphicsPath p = Ui.Round(track, h / 2f)) + using (var b = new SolidBrush(th.Track)) + g.FillPath(b, p); + RectangleF bar; + if (marquee) + { + float seg = Width * 0.28f; + // ease in-out sweep across the track + float t = (float)(0.5 - Math.Cos(phase * Math.PI) / 2); + bar = RectangleF.Intersect(new RectangleF(-seg + t * (Width + seg), track.Y, seg, h), track); + } + else + { + bar = new RectangleF(0, track.Y, Width * value / 100f, h); + } + if (bar.Width < 0.5f) + return; + Color fill = state == BarState.Ok ? th.Ok : state == BarState.Error ? th.Err : + state == BarState.Paused ? th.Warn : th.Accent; + using (GraphicsPath p = Ui.Round(bar, h / 2f)) + using (var b = new SolidBrush(fill)) + g.FillPath(b, p); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + timer.Dispose(); + base.Dispose(disposing); + } + } + + sealed class StatusDot : Control + { + Color dotColor = Color.Silver; + + public StatusDot() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true); + } + + public Color DotColor + { + get { return dotColor; } + set { dotColor = value; Invalidate(); } + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + } + + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.Clear(Ui.ParentBack(this)); + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + using (var b = new SolidBrush(dotColor)) + e.Graphics.FillEllipse(b, 0.5f, 0.5f, Width - 1.5f, Height - 1.5f); + } + } + + // Context menus in the colors of the current theme + sealed class MenuRenderer : ToolStripProfessionalRenderer + { + public MenuRenderer() : base(new MenuColors()) + { + RoundedEdges = false; + } + + protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) + { + e.TextColor = e.Item.Enabled ? Theme.Current.Text : Theme.Current.TextDisabled; + base.OnRenderItemText(e); + } + + protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e) + { + Rectangle r = e.ImageRectangle; + float s = Math.Min(r.Width, r.Height); + float x = r.X + (r.Width - s) / 2f, y = r.Y + (r.Height - s) / 2f; + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + using (var pen = new Pen(Theme.Current.Text, Math.Max(1.5f, s * 0.11f))) + { + pen.StartCap = pen.EndCap = LineCap.Round; + pen.LineJoin = LineJoin.Round; + e.Graphics.DrawLines(pen, new[] + { + new PointF(x + s * 0.22f, y + s * 0.52f), + new PointF(x + s * 0.42f, y + s * 0.72f), + new PointF(x + s * 0.78f, y + s * 0.30f), + }); + } + e.Graphics.SmoothingMode = SmoothingMode.None; + } + + sealed class MenuColors : ProfessionalColorTable + { + public MenuColors() + { + UseSystemColors = false; + } + + static Theme T + { + get { return Theme.Current; } + } + + public override Color ToolStripDropDownBackground { get { return T.MenuBg; } } + public override Color ImageMarginGradientBegin { get { return T.MenuBg; } } + public override Color ImageMarginGradientMiddle { get { return T.MenuBg; } } + public override Color ImageMarginGradientEnd { get { return T.MenuBg; } } + public override Color MenuBorder { get { return T.MenuBorder; } } + public override Color MenuItemBorder { get { return T.MenuHover; } } + public override Color MenuItemSelected { get { return T.MenuHover; } } + public override Color MenuItemSelectedGradientBegin { get { return T.MenuHover; } } + public override Color MenuItemSelectedGradientEnd { get { return T.MenuHover; } } + public override Color SeparatorDark { get { return T.MenuBorder; } } + public override Color SeparatorLight { get { return T.MenuBg; } } + public override Color CheckBackground { get { return T.MenuBg; } } + public override Color CheckSelectedBackground { get { return T.MenuHover; } } + public override Color CheckPressedBackground { get { return T.MenuHover; } } + } + } +} diff --git a/gui/MainForm.cs b/gui/MainForm.cs new file mode 100644 index 0000000..24f22c2 --- /dev/null +++ b/gui/MainForm.cs @@ -0,0 +1,2034 @@ +// Tang Primer 25K programmer: a window around openFPGALoader (-c dirtyJtag) +// for loading FPGA bitstreams, plus flashing of the BL616 bridge firmware +// (what scripts\flash.ps1 does). Build with scripts\build_gui.ps1. +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace TangProgrammer +{ + static class Program + { + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new MainForm()); + } + } + + enum ToolKind { Ofl, BlFlash } + + sealed class RunResult + { + public int ExitCode = -1; + public bool FailSeen; + public readonly List Lines = new List(); + + // openFPGALoader exits with 0 even when loading the SRAM fails + public bool Ok + { + get { return ExitCode == 0 && !FailSeen; } + } + + public bool Has(string text) + { + return Lines.Any(l => l.IndexOf(text, StringComparison.OrdinalIgnoreCase) >= 0); + } + + public string Find(Regex rx) + { + foreach (string l in Lines) + { + Match m = rx.Match(l); + if (m.Success) + return m.Groups[1].Value; + } + return null; + } + } + + sealed class LogEntry + { + public string Text; + public Tone Tone; + public bool Bold; + } + + sealed class MainForm : Form + { + const string AppTitle = "Tang Primer 25K — программатор"; + // where the tools and the bridge firmware are: the source tree, or the + // portable copy made by scripts\make_portable.ps1 + static readonly string[] BlFlashPaths = + { + @"third_party\bouffalo_sdk\tools\bflb_tools\bouffalo_flash_cube\BLFlashCommand.exe", + @"tools\bouffalo_flash_cube\BLFlashCommand.exe", + }; + static readonly string[] BridgeFwPaths = + { + @"build\build_out\tang25k_debugger_bl616.bin", + @"firmware\tang25k_debugger_bl616.bin", + }; + static readonly int[] FreqList = { 1, 2, 4, 6, 10, 15, 20, 30 }; // MHz; the firmware does up to 30 + const int DefaultFreq = 6; // what the firmware starts with + const int MaxLogEntries = 4000; + + // openFPGALoader: "Load SRAM: [===== ] 33.00%" + static readonly Regex OflProgressRx = new Regex(@"^(?