Прошивка моста 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 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
* text=auto
|
||||||
|
|
||||||
|
*.bin binary
|
||||||
|
*.exe binary
|
||||||
|
*.dll binary
|
||||||
|
*.ico binary
|
||||||
|
*.fs binary
|
||||||
|
*.zip binary
|
||||||
+20
@@ -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
|
||||||
@@ -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 на плате не использовать.
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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 и счётчики байт.
|
||||||
@@ -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
|
||||||
|
```
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
+793
@@ -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<string> items = new List<string>();
|
||||||
|
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; } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2034
File diff suppressed because it is too large
Load Diff
+433
@@ -0,0 +1,433 @@
|
|||||||
|
// Helpers of the programmer GUI: USB device discovery, running the
|
||||||
|
// command-line tools, settings, a few Win32 calls.
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Management;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
|
||||||
|
namespace TangProgrammer
|
||||||
|
{
|
||||||
|
// What Windows sees on USB (see README: installation and restore)
|
||||||
|
sealed class BridgeState
|
||||||
|
{
|
||||||
|
public bool Present; // this firmware: DirtyJTAG + CDC UART, 1209:C0CA
|
||||||
|
public string Version; // from bcdDevice, e.g. "1.00"
|
||||||
|
public string UartPort; // COM port of its CDC interface
|
||||||
|
public bool JtagDriverOk = true;
|
||||||
|
public string BootloaderPort; // BL616 ROM USB bootloader "Bouffalo CDC DEMO", 349B:6160
|
||||||
|
public bool SipeedStock; // stock Sipeed firmware emulates an FT2232, 0403:6010
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Devices
|
||||||
|
{
|
||||||
|
static readonly Regex ComRx = new Regex(@"\((COM\d+)\)");
|
||||||
|
static readonly Regex RevRx = new Regex(@"REV_(\d\d)(\d\d)");
|
||||||
|
|
||||||
|
public static BridgeState Scan()
|
||||||
|
{
|
||||||
|
var st = new BridgeState();
|
||||||
|
const string query = "SELECT Name, PNPDeviceID, HardwareID, ConfigManagerErrorCode " +
|
||||||
|
"FROM Win32_PnPEntity WHERE PNPDeviceID LIKE 'USB%'";
|
||||||
|
using (var searcher = new ManagementObjectSearcher(query))
|
||||||
|
using (ManagementObjectCollection found = searcher.Get())
|
||||||
|
{
|
||||||
|
foreach (ManagementBaseObject o in found)
|
||||||
|
{
|
||||||
|
using (o)
|
||||||
|
{
|
||||||
|
string id = Convert.ToString(o["PNPDeviceID"]).ToUpperInvariant();
|
||||||
|
Match com = ComRx.Match(Convert.ToString(o["Name"]));
|
||||||
|
if (id.Contains("VID_1209&PID_C0CA"))
|
||||||
|
{
|
||||||
|
st.Present = true;
|
||||||
|
if (id.Contains("&MI_00"))
|
||||||
|
st.JtagDriverOk = Convert.ToUInt32(o["ConfigManagerErrorCode"]) == 0;
|
||||||
|
else if (com.Success)
|
||||||
|
st.UartPort = com.Groups[1].Value;
|
||||||
|
var hwIds = o["HardwareID"] as string[];
|
||||||
|
if (st.Version == null && hwIds != null)
|
||||||
|
{
|
||||||
|
foreach (string hw in hwIds)
|
||||||
|
{
|
||||||
|
Match m = RevRx.Match(hw.ToUpperInvariant());
|
||||||
|
if (m.Success)
|
||||||
|
{
|
||||||
|
st.Version = int.Parse(m.Groups[1].Value) + "." + m.Groups[2].Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (id.Contains("VID_349B&PID_6160"))
|
||||||
|
{
|
||||||
|
if (com.Success)
|
||||||
|
st.BootloaderPort = com.Groups[1].Value;
|
||||||
|
}
|
||||||
|
else if (id.Contains("VID_0403&PID_6010"))
|
||||||
|
{
|
||||||
|
st.SipeedStock = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A drive mapped to a share of this very PC (D: -> \\THISPC\Projects) sends
|
||||||
|
// every tool launch through SMB; use the folder behind the share instead,
|
||||||
|
// like Resolve-LocalPath in scripts\common.ps1.
|
||||||
|
static class LocalPath
|
||||||
|
{
|
||||||
|
[DllImport("mpr.dll", CharSet = CharSet.Unicode)]
|
||||||
|
static extern int WNetGetConnection(string localName, StringBuilder remoteName, ref int length);
|
||||||
|
|
||||||
|
public static string Resolve(string path)
|
||||||
|
{
|
||||||
|
string full = Path.GetFullPath(path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (full.Length < 3 || full[1] != ':')
|
||||||
|
return full;
|
||||||
|
var unc = new StringBuilder(512);
|
||||||
|
int len = unc.Capacity;
|
||||||
|
if (WNetGetConnection(full.Substring(0, 2), unc, ref len) != 0)
|
||||||
|
return full;
|
||||||
|
Match m = Regex.Match(unc.ToString(), @"^\\\\([^\\]+)\\([^\\]+)\\?(.*)$");
|
||||||
|
if (!m.Success)
|
||||||
|
return full;
|
||||||
|
string host = m.Groups[1].Value;
|
||||||
|
if (!host.Equals(Environment.MachineName, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!host.Equals("localhost", StringComparison.OrdinalIgnoreCase) && host != "127.0.0.1")
|
||||||
|
return full;
|
||||||
|
string query = "SELECT Path FROM Win32_Share WHERE Name='" + m.Groups[2].Value.Replace("'", "\\'") + "'";
|
||||||
|
using (var searcher = new ManagementObjectSearcher(query))
|
||||||
|
using (ManagementObjectCollection found = searcher.Get())
|
||||||
|
{
|
||||||
|
foreach (ManagementBaseObject o in found)
|
||||||
|
{
|
||||||
|
string share = Convert.ToString(o["Path"]);
|
||||||
|
if (share.Length == 0)
|
||||||
|
continue;
|
||||||
|
string local = Path.Combine(Path.Combine(share, m.Groups[3].Value), full.Substring(3));
|
||||||
|
if (Directory.Exists(local) || File.Exists(local))
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs a console tool without a console window and streams its output
|
||||||
|
// (stdout and stderr chunks, as they arrive) to a callback on a worker thread.
|
||||||
|
static class ToolProcess
|
||||||
|
{
|
||||||
|
public static Task<int> Run(string exe, string args, string workDir,
|
||||||
|
Action<string, bool> output, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo(exe, args)
|
||||||
|
{
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
StandardOutputEncoding = Encoding.Default,
|
||||||
|
StandardErrorEncoding = Encoding.Default,
|
||||||
|
WorkingDirectory = workDir ?? Path.GetDirectoryName(exe),
|
||||||
|
};
|
||||||
|
Process p = Process.Start(psi);
|
||||||
|
Thread outPump = Pump(p.StandardOutput, false, output);
|
||||||
|
Thread errPump = Pump(p.StandardError, true, output);
|
||||||
|
return Task.Run(() =>
|
||||||
|
{
|
||||||
|
using (p)
|
||||||
|
using (ct.Register(() => Task.Run(() => KillTree(p))))
|
||||||
|
{
|
||||||
|
p.WaitForExit();
|
||||||
|
outPump.Join();
|
||||||
|
errPump.Join();
|
||||||
|
return p.ExitCode;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static Thread Pump(StreamReader reader, bool isErr, Action<string, bool> output)
|
||||||
|
{
|
||||||
|
var t = new Thread(() =>
|
||||||
|
{
|
||||||
|
var buf = new char[4096];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int n;
|
||||||
|
while ((n = reader.Read(buf, 0, buf.Length)) > 0)
|
||||||
|
output(new string(buf, 0, n), isErr);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.IsBackground = true;
|
||||||
|
t.Start();
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BLFlashCommand is a one-file PyInstaller exe: the real work happens in a child
|
||||||
|
static void KillTree(Process p)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (p.HasExited)
|
||||||
|
return;
|
||||||
|
var psi = new ProcessStartInfo("taskkill.exe", "/T /F /PID " + p.Id)
|
||||||
|
{
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
};
|
||||||
|
using (Process k = Process.Start(psi))
|
||||||
|
k.WaitForExit(5000);
|
||||||
|
if (!p.HasExited)
|
||||||
|
p.Kill();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits console output into lines. A lone '\r' also ends a line: that is
|
||||||
|
// how progress bars redraw themselves.
|
||||||
|
sealed class LineSplitter
|
||||||
|
{
|
||||||
|
readonly StringBuilder cur = new StringBuilder();
|
||||||
|
|
||||||
|
public List<string> Feed(string chunk)
|
||||||
|
{
|
||||||
|
var lines = new List<string>();
|
||||||
|
foreach (char ch in chunk)
|
||||||
|
{
|
||||||
|
if (ch == '\r' || ch == '\n')
|
||||||
|
{
|
||||||
|
if (cur.Length > 0)
|
||||||
|
lines.Add(cur.ToString());
|
||||||
|
cur.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cur.Append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Partial
|
||||||
|
{
|
||||||
|
get { return cur.ToString(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Flush()
|
||||||
|
{
|
||||||
|
string rest = cur.ToString();
|
||||||
|
cur.Clear();
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Settings
|
||||||
|
{
|
||||||
|
public readonly List<string> Recent = new List<string>();
|
||||||
|
public int FreqMHz = 6;
|
||||||
|
public bool Flash;
|
||||||
|
public bool Verify = true;
|
||||||
|
public bool AutoReload;
|
||||||
|
public int Tab;
|
||||||
|
public string Theme = "system"; // system | light | dark
|
||||||
|
public Rectangle Bounds;
|
||||||
|
public bool Maximized;
|
||||||
|
|
||||||
|
static string FilePath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"Tang25kProgrammer", "settings.ini");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Settings Load()
|
||||||
|
{
|
||||||
|
var s = new Settings();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(FilePath))
|
||||||
|
return s;
|
||||||
|
foreach (string line in File.ReadAllLines(FilePath, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
int eq = line.IndexOf('=');
|
||||||
|
if (eq <= 0)
|
||||||
|
continue;
|
||||||
|
string key = line.Substring(0, eq).Trim();
|
||||||
|
string val = line.Substring(eq + 1).Trim();
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case "recent":
|
||||||
|
if (val.Length > 0)
|
||||||
|
s.Recent.Add(val);
|
||||||
|
break;
|
||||||
|
case "freq":
|
||||||
|
int.TryParse(val, out s.FreqMHz);
|
||||||
|
break;
|
||||||
|
case "flash":
|
||||||
|
s.Flash = val == "1";
|
||||||
|
break;
|
||||||
|
case "verify":
|
||||||
|
s.Verify = val == "1";
|
||||||
|
break;
|
||||||
|
case "auto":
|
||||||
|
s.AutoReload = val == "1";
|
||||||
|
break;
|
||||||
|
case "tab":
|
||||||
|
int.TryParse(val, out s.Tab);
|
||||||
|
break;
|
||||||
|
case "theme":
|
||||||
|
if (val == "light" || val == "dark" || val == "system")
|
||||||
|
s.Theme = val;
|
||||||
|
break;
|
||||||
|
case "maximized":
|
||||||
|
s.Maximized = val == "1";
|
||||||
|
break;
|
||||||
|
case "bounds":
|
||||||
|
string[] p = val.Split(',');
|
||||||
|
if (p.Length == 4)
|
||||||
|
s.Bounds = new Rectangle(int.Parse(p[0]), int.Parse(p[1]), int.Parse(p[2]), int.Parse(p[3]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (string r in Recent)
|
||||||
|
sb.AppendLine("recent=" + r);
|
||||||
|
sb.AppendLine("freq=" + FreqMHz);
|
||||||
|
sb.AppendLine("flash=" + (Flash ? 1 : 0));
|
||||||
|
sb.AppendLine("verify=" + (Verify ? 1 : 0));
|
||||||
|
sb.AppendLine("auto=" + (AutoReload ? 1 : 0));
|
||||||
|
sb.AppendLine("tab=" + Tab);
|
||||||
|
sb.AppendLine("theme=" + Theme);
|
||||||
|
sb.AppendLine("maximized=" + (Maximized ? 1 : 0));
|
||||||
|
if (!Bounds.IsEmpty)
|
||||||
|
sb.AppendLine("bounds=" + Bounds.X + "," + Bounds.Y + "," + Bounds.Width + "," + Bounds.Height);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(FilePath));
|
||||||
|
File.WriteAllText(FilePath, sb.ToString(), Encoding.UTF8);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Native
|
||||||
|
{
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
struct FLASHWINFO
|
||||||
|
{
|
||||||
|
public uint cbSize;
|
||||||
|
public IntPtr hwnd;
|
||||||
|
public uint dwFlags;
|
||||||
|
public uint uCount;
|
||||||
|
public uint dwTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
static extern bool FlashWindowEx(ref FLASHWINFO fi);
|
||||||
|
|
||||||
|
// blink the taskbar button until the window is activated
|
||||||
|
public static void FlashTaskbar(Form f)
|
||||||
|
{
|
||||||
|
var fi = new FLASHWINFO
|
||||||
|
{
|
||||||
|
cbSize = (uint)Marshal.SizeOf(typeof(FLASHWINFO)),
|
||||||
|
hwnd = f.Handle,
|
||||||
|
dwFlags = 3 /* FLASHW_ALL */ | 12 /* FLASHW_TIMERNOFG */,
|
||||||
|
};
|
||||||
|
FlashWindowEx(ref fi);
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
struct DCB
|
||||||
|
{
|
||||||
|
public int DCBlength;
|
||||||
|
public uint BaudRate;
|
||||||
|
public uint Flags;
|
||||||
|
public ushort wReserved;
|
||||||
|
public ushort XonLim;
|
||||||
|
public ushort XoffLim;
|
||||||
|
public byte ByteSize;
|
||||||
|
public byte Parity;
|
||||||
|
public byte StopBits;
|
||||||
|
public byte XonChar;
|
||||||
|
public byte XoffChar;
|
||||||
|
public byte ErrorChar;
|
||||||
|
public byte EofChar;
|
||||||
|
public byte EvtChar;
|
||||||
|
public ushort wReserved1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
|
static extern SafeFileHandle CreateFile(string name, uint access, uint share, IntPtr security,
|
||||||
|
uint disposition, uint flags, IntPtr template);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
static extern bool GetCommState(SafeFileHandle h, ref DCB dcb);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
static extern bool SetCommState(SafeFileHandle h, ref DCB dcb);
|
||||||
|
|
||||||
|
// Opens the serial port, sets the baud rate and closes it again. The
|
||||||
|
// bridge firmware reboots into the ROM bootloader on 1234 baud, so the
|
||||||
|
// device vanishes while the port is open: System.IO.Ports does not
|
||||||
|
// survive that, plain handles do. Returns null or the error text.
|
||||||
|
public static string PokeBaudRate(string port, uint baud)
|
||||||
|
{
|
||||||
|
using (SafeFileHandle h = CreateFile(@"\\.\" + port, 0xC0000000 /* GENERIC_READ | GENERIC_WRITE */,
|
||||||
|
0, IntPtr.Zero, 3 /* OPEN_EXISTING */, 0, IntPtr.Zero))
|
||||||
|
{
|
||||||
|
if (h.IsInvalid)
|
||||||
|
return new Win32Exception(Marshal.GetLastWin32Error()).Message;
|
||||||
|
var dcb = new DCB();
|
||||||
|
dcb.DCBlength = Marshal.SizeOf(typeof(DCB));
|
||||||
|
if (!GetCommState(h, ref dcb))
|
||||||
|
return new Win32Exception(Marshal.GetLastWin32Error()).Message;
|
||||||
|
dcb.BaudRate = baud;
|
||||||
|
if (dcb.ByteSize == 0)
|
||||||
|
dcb.ByteSize = 8;
|
||||||
|
// may report an error: the device can be gone before the call returns
|
||||||
|
SetCommState(h, ref dcb);
|
||||||
|
Thread.Sleep(200);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="TangProgrammer.app"/>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 10 / 11 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0"
|
||||||
|
processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
Tang Primer 25K — программатор (переносная версия)
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Запуск: TangProgrammer.exe, двойной щелчок. Устанавливать ничего не нужно.
|
||||||
|
|
||||||
|
Требования
|
||||||
|
----------
|
||||||
|
* Windows 10 или 11, 64-бит (.NET Framework 4.8 в них уже есть).
|
||||||
|
* Папку целиком держите в любом месте, куда можно писать (рабочий стол,
|
||||||
|
D:\...), но не в Program Files: программа прошивки моста создаёт файлы
|
||||||
|
рядом с собой.
|
||||||
|
* Gowin IDE на этом компьютере нужна только для сборки своих .fs.
|
||||||
|
|
||||||
|
Драйверы
|
||||||
|
--------
|
||||||
|
Ставить ничего не нужно. При первом подключении платы Windows сама установит
|
||||||
|
WinUSB для JTAG («DirtyJTAG») и драйвер COM-порта UART — подождите 10–30 секунд.
|
||||||
|
Прошивка моста хранится в самой плате (в BL616), переносить её не нужно.
|
||||||
|
|
||||||
|
Если архив скачан из интернета или пришёл почтой
|
||||||
|
-----------------------------------------------
|
||||||
|
До распаковки: правой кнопкой по zip → «Свойства» → отметить «Разблокировать»
|
||||||
|
→ OK. Иначе Windows может показать «Система Windows защитила ваш компьютер»
|
||||||
|
(программа не подписана) — тогда «Подробнее» → «Выполнить в любом случае».
|
||||||
|
|
||||||
|
Что внутри
|
||||||
|
----------
|
||||||
|
TangProgrammer.exe сама программа
|
||||||
|
tools\openFPGALoader\ openFPGALoader 1.1.1: загрузка FPGA, флеш-память FPGA
|
||||||
|
tools\bouffalo_flash_cube\ BLFlashCommand — только для вкладки «Прошивка моста»
|
||||||
|
firmware\, *.ini прошивка моста, загрузчик и штатная прошивка Sipeed
|
||||||
|
(тоже только для вкладки «Прошивка моста»)
|
||||||
|
|
||||||
|
Настройки программы (тема, последние файлы) хранятся отдельно для каждого
|
||||||
|
пользователя в %LOCALAPPDATA%\Tang25kProgrammer.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[cfg]
|
||||||
|
# 0: no erase, 1: programmed section erase, 2: chip erase
|
||||||
|
erase = 1
|
||||||
|
skip_mode = 0x0, 0x0
|
||||||
|
boot2_isp_mode = 0
|
||||||
|
|
||||||
|
# Original Sipeed debugger firmware (FT2232 emulation), version 2025030317
|
||||||
|
[FW]
|
||||||
|
filedir = ./bin/bl616_fpga_partner_25kDock.bin
|
||||||
|
address = 0x0
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Build the firmware: build\build_out\tang25k_debugger_bl616.bin
|
||||||
|
param([switch]$Clean)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
. (Join-Path $PSScriptRoot 'common.ps1')
|
||||||
|
|
||||||
|
$root = Get-ProjectRoot
|
||||||
|
Set-BuildEnv $root
|
||||||
|
Push-Location $root
|
||||||
|
try {
|
||||||
|
if ($Clean) {
|
||||||
|
Remove-Item -Recurse -Force build -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
& make.exe CHIP=bl616 BOARD=bl616dk
|
||||||
|
if ($LASTEXITCODE) { throw "build failed ($LASTEXITCODE)" }
|
||||||
|
$bin = Get-Item build\build_out\tang25k_debugger_bl616.bin
|
||||||
|
Write-Host ("OK: {0} ({1} bytes)" -f $bin.FullName, $bin.Length) -ForegroundColor Green
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Build TangProgrammer.exe (the GUI for openFPGALoader and the bridge firmware)
|
||||||
|
# with the C# compiler that comes with Windows (.NET Framework 4.x): nothing
|
||||||
|
# has to be installed.
|
||||||
|
#
|
||||||
|
# .\scripts\build_gui.ps1 -> TangProgrammer.exe in the project root
|
||||||
|
# .\scripts\build_gui.ps1 -Out some.exe -> elsewhere
|
||||||
|
param([string]$Out)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
|
$src = Join-Path $root 'gui'
|
||||||
|
if (-not $Out) { $Out = Join-Path $root 'TangProgrammer.exe' }
|
||||||
|
$csc = Join-Path $env:WINDIR 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'
|
||||||
|
if (-not (Test-Path $csc)) {
|
||||||
|
$csc = Join-Path $env:WINDIR 'Microsoft.NET\Framework\v4.0.30319\csc.exe'
|
||||||
|
}
|
||||||
|
if (-not (Test-Path $csc)) { throw '.NET Framework 4 C# compiler (csc.exe) not found' }
|
||||||
|
|
||||||
|
$sources = Get-ChildItem (Join-Path $src '*.cs') | ForEach-Object { $_.FullName }
|
||||||
|
& $csc /nologo /target:winexe /optimize+ /codepage:65001 "/out:$Out" `
|
||||||
|
"/win32icon:$src\app.ico" "/win32manifest:$src\app.manifest" "/resource:$src\app.ico,TangProgrammer.app.ico" `
|
||||||
|
/r:System.dll /r:System.Drawing.dll /r:System.Windows.Forms.dll /r:System.Management.dll `
|
||||||
|
$sources
|
||||||
|
if ($LASTEXITCODE) { throw "csc failed ($LASTEXITCODE) - is $Out running?" }
|
||||||
|
Write-Host "OK: $Out" -ForegroundColor Green
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Shared helpers for build/flash scripts.
|
||||||
|
|
||||||
|
# A mapped drive that points back to a share on this very PC (D: -> \\THISPC\Projects)
|
||||||
|
# makes every compiler launch go through SMB and antivirus scanning: builds
|
||||||
|
# get ~50x slower. Translate such paths to the local folder behind the share.
|
||||||
|
function Resolve-LocalPath([string]$Path) {
|
||||||
|
$full = (Resolve-Path $Path).ProviderPath
|
||||||
|
$drive = Get-PSDrive -Name $full.Substring(0, 1) -ErrorAction SilentlyContinue
|
||||||
|
if ($drive -and $drive.DisplayRoot -match '^\\\\([^\\]+)\\([^\\]+)') {
|
||||||
|
if ($Matches[1] -ieq $env:COMPUTERNAME) {
|
||||||
|
$share = Get-CimInstance Win32_Share -Filter "Name='$($Matches[2])'" -ErrorAction SilentlyContinue
|
||||||
|
if ($share) {
|
||||||
|
return Join-Path $share.Path $full.Substring(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $full
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectRoot {
|
||||||
|
return Resolve-LocalPath (Join-Path $PSScriptRoot '..')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-BuildEnv([string]$Root) {
|
||||||
|
$sdk = Join-Path $Root 'third_party\bouffalo_sdk'
|
||||||
|
$tc = Join-Path $Root 'third_party\toolchain_gcc_t-head_windows\bin'
|
||||||
|
if (-not (Test-Path $sdk) -or -not (Test-Path $tc)) {
|
||||||
|
throw "SDK/toolchain not found in $Root\third_party - run scripts\setup.ps1 first"
|
||||||
|
}
|
||||||
|
$env:BL_SDK_BASE = $sdk
|
||||||
|
$env:PATH = "$tc;$sdk\tools\make;$sdk\tools\ninja;$sdk\tools\cmake\bin;$env:SystemRoot\System32;$env:SystemRoot"
|
||||||
|
}
|
||||||
|
|
||||||
|
# COM port name(s) of present devices whose instance id matches the pattern
|
||||||
|
function Find-ComPort([string]$IdPattern) {
|
||||||
|
Get-CimInstance Win32_PnPEntity -Filter "PNPClass='Ports'" -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.PNPDeviceID -match $IdPattern -and $_.Name -match '\((COM\d+)\)' } |
|
||||||
|
ForEach-Object { [regex]::Match($_.Name, '\((COM\d+)\)').Groups[1].Value }
|
||||||
|
}
|
||||||
|
|
||||||
|
$BootromPortId = 'VID_349B&PID_6160' # BL616 ROM USB bootloader "Bouffalo CDC DEMO"
|
||||||
|
$FirmwarePortId = 'VID_1209&PID_C0CA&MI_01' # this firmware's CDC UART
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Flash the BL616 on the Tang Primer 25K Dock.
|
||||||
|
#
|
||||||
|
# .\scripts\flash.ps1 this firmware (Sipeed loader @0x0 + firmware @0x20000)
|
||||||
|
# .\scripts\flash.ps1 -Restore original Sipeed debugger firmware
|
||||||
|
#
|
||||||
|
# The BL616 has to be in its ROM USB bootloader ("Bouffalo CDC DEMO"). The
|
||||||
|
# script gets it there by itself when this firmware is already running
|
||||||
|
# (serial port opened at 1234 baud). Otherwise, unplug the board and plug it
|
||||||
|
# back while either
|
||||||
|
# - holding the S3 button (this firmware is installed), or
|
||||||
|
# - shorting the TDO test point to 3V3 (bottom side, always works).
|
||||||
|
param(
|
||||||
|
[switch]$Restore,
|
||||||
|
[string]$Port
|
||||||
|
)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
. (Join-Path $PSScriptRoot 'common.ps1')
|
||||||
|
|
||||||
|
$root = Get-ProjectRoot
|
||||||
|
$sdk = Join-Path $root 'third_party\bouffalo_sdk'
|
||||||
|
$tool = Join-Path $sdk 'tools\bflb_tools\bouffalo_flash_cube\BLFlashCommand.exe'
|
||||||
|
$cfg = if ($Restore) { 'restore_prog_cfg.ini' } else { 'flash_prog_cfg.ini' }
|
||||||
|
|
||||||
|
if (-not $Restore -and -not (Test-Path (Join-Path $root 'build\build_out\tang25k_debugger_bl616.bin'))) {
|
||||||
|
throw 'firmware not built yet: run scripts\build.ps1'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Wait-BootromPort([int]$Seconds) {
|
||||||
|
$deadline = (Get-Date).AddSeconds($Seconds)
|
||||||
|
do {
|
||||||
|
$p = @(Find-ComPort $BootromPortId)
|
||||||
|
if ($p.Count) { return $p[0] }
|
||||||
|
Start-Sleep -Milliseconds 300
|
||||||
|
} while ((Get-Date) -lt $deadline)
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Port) {
|
||||||
|
$Port = @(Find-ComPort $BootromPortId) | Select-Object -First 1
|
||||||
|
}
|
||||||
|
if (-not $Port) {
|
||||||
|
$fw = @(Find-ComPort $FirmwarePortId) | Select-Object -First 1
|
||||||
|
if ($fw) {
|
||||||
|
Write-Host "Firmware found on $fw, rebooting it into the ROM bootloader..."
|
||||||
|
try {
|
||||||
|
$sp = New-Object System.IO.Ports.SerialPort $fw, 1234
|
||||||
|
$sp.Open()
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
$sp.Close()
|
||||||
|
} catch {
|
||||||
|
Write-Warning "could not open ${fw}: $_ (is a terminal using it?)"
|
||||||
|
}
|
||||||
|
$Port = Wait-BootromPort 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $Port) {
|
||||||
|
Write-Host 'Waiting for the BL616 ROM bootloader (Bouffalo CDC DEMO)...' -ForegroundColor Yellow
|
||||||
|
Write-Host ' unplug the board, then plug it back while holding S3'
|
||||||
|
Write-Host ' (or while shorting the TDO test point to 3V3 on the bottom side)'
|
||||||
|
$Port = Wait-BootromPort 120
|
||||||
|
}
|
||||||
|
if (-not $Port) { throw 'BL616 bootloader port not found' }
|
||||||
|
|
||||||
|
Write-Host "Flashing $cfg via $Port" -ForegroundColor Cyan
|
||||||
|
Push-Location $root
|
||||||
|
try {
|
||||||
|
& $tool --interface=uart --baudrate=2000000 --port=$Port --chipname=bl616 --cpu_id= --config=$cfg
|
||||||
|
if ($LASTEXITCODE) { throw "BLFlashCommand failed ($LASTEXITCODE)" }
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
Write-Host 'Done. Unplug and plug the board back (power cycle) to start the new firmware.' -ForegroundColor Green
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Make a portable copy of TangProgrammer for another PC: the program and only
|
||||||
|
# the tools it runs (no SDK, no toolchain, no sources).
|
||||||
|
#
|
||||||
|
# .\scripts\make_portable.ps1 -> dist\TangProgrammer\ and dist\TangProgrammer.zip
|
||||||
|
#
|
||||||
|
# The FPGA and FPGA-flash tabs need TangProgrammer.exe + tools\openFPGALoader.
|
||||||
|
# The bridge firmware tab also needs BLFlashCommand (for flashing over the USB
|
||||||
|
# bootloader only the exe and chips\ are used, not the 200+ MB of debug probe
|
||||||
|
# tools around it), the firmware images and the flash configs.
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
|
$dist = Join-Path $root 'dist'
|
||||||
|
$out = Join-Path $dist 'TangProgrammer'
|
||||||
|
$cube = Join-Path $root 'third_party\bouffalo_sdk\tools\bflb_tools\bouffalo_flash_cube'
|
||||||
|
$bridgeFw = Join-Path $root 'build\build_out\tang25k_debugger_bl616.bin'
|
||||||
|
|
||||||
|
if (Test-Path $out) { Remove-Item -Recurse -Force $out }
|
||||||
|
New-Item -ItemType Directory -Force (Join-Path $out 'tools') | Out-Null
|
||||||
|
|
||||||
|
& (Join-Path $PSScriptRoot 'build_gui.ps1') -Out (Join-Path $out 'TangProgrammer.exe')
|
||||||
|
Copy-Item -Recurse (Join-Path $root 'tools\openFPGALoader') (Join-Path $out 'tools\openFPGALoader')
|
||||||
|
|
||||||
|
if (Test-Path (Join-Path $cube 'BLFlashCommand.exe')) {
|
||||||
|
$dst = Join-Path $out 'tools\bouffalo_flash_cube'
|
||||||
|
New-Item -ItemType Directory -Force $dst | Out-Null
|
||||||
|
Copy-Item (Join-Path $cube 'BLFlashCommand.exe') $dst
|
||||||
|
Copy-Item -Recurse (Join-Path $cube 'chips') $dst
|
||||||
|
} else {
|
||||||
|
Write-Warning 'Bouffalo SDK is not installed (scripts\setup.ps1): the bridge firmware tab will not work in the copy'
|
||||||
|
}
|
||||||
|
|
||||||
|
$fwDir = Join-Path $out 'firmware'
|
||||||
|
New-Item -ItemType Directory -Force $fwDir | Out-Null
|
||||||
|
Copy-Item (Join-Path $root 'bin\bl616_bootloader_0x20000_nano20k_signed.bin') $fwDir
|
||||||
|
Copy-Item (Join-Path $root 'bin\bl616_fpga_partner_25kDock.bin') $fwDir
|
||||||
|
if (Test-Path $bridgeFw) {
|
||||||
|
Copy-Item $bridgeFw $fwDir
|
||||||
|
} else {
|
||||||
|
Write-Warning 'bridge firmware is not built (scripts\build.ps1): it is missing from the copy'
|
||||||
|
}
|
||||||
|
|
||||||
|
# the same configs as in the project root, pointing at firmware\
|
||||||
|
Set-Content -Encoding ASCII (Join-Path $out 'flash_prog_cfg.ini') @'
|
||||||
|
[cfg]
|
||||||
|
# 0: no erase, 1: programmed section erase, 2: chip erase
|
||||||
|
erase = 1
|
||||||
|
skip_mode = 0x0, 0x0
|
||||||
|
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 = ./firmware/bl616_bootloader_0x20000_nano20k_signed.bin
|
||||||
|
address = 0x0
|
||||||
|
|
||||||
|
[FW]
|
||||||
|
filedir = ./firmware/tang25k_debugger_bl616.bin
|
||||||
|
address = 0x20000
|
||||||
|
'@
|
||||||
|
Set-Content -Encoding ASCII (Join-Path $out 'restore_prog_cfg.ini') @'
|
||||||
|
[cfg]
|
||||||
|
# 0: no erase, 1: programmed section erase, 2: chip erase
|
||||||
|
erase = 1
|
||||||
|
skip_mode = 0x0, 0x0
|
||||||
|
boot2_isp_mode = 0
|
||||||
|
|
||||||
|
# Original Sipeed debugger firmware (FT2232 emulation), version 2025030317
|
||||||
|
[FW]
|
||||||
|
filedir = ./firmware/bl616_fpga_partner_25kDock.bin
|
||||||
|
address = 0x0
|
||||||
|
'@
|
||||||
|
|
||||||
|
Copy-Item (Join-Path $root 'gui\portable_readme.txt') (Join-Path $out 'README.txt')
|
||||||
|
|
||||||
|
$zip = Join-Path $dist 'TangProgrammer.zip'
|
||||||
|
if (Test-Path $zip) { Remove-Item -Force $zip }
|
||||||
|
Compress-Archive -Path $out -DestinationPath $zip
|
||||||
|
$size = (Get-ChildItem -Recurse $out | Measure-Object -Sum Length).Sum
|
||||||
|
Write-Host ("OK: {0} ({1:N1} MB), {2} ({3:N1} MB)" -f $out, ($size / 1MB), $zip, ((Get-Item $zip).Length / 1MB)) -ForegroundColor Green
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Collect the files of a release into dist\release\v<version>\:
|
||||||
|
#
|
||||||
|
# TangProgrammer-v<version>-win64.zip portable programmer (make_portable.ps1)
|
||||||
|
# TangProgrammer.exe the program alone
|
||||||
|
# tang25k_debugger_bl616.bin bridge firmware, @0x20000
|
||||||
|
# bl616_bootloader_0x20000_nano20k_signed.bin Sipeed loader, @0x0
|
||||||
|
# bl616_fpga_partner_25kDock.bin original Sipeed firmware, @0x0
|
||||||
|
# fpga_test_bitstreams-GW5A-25.zip fpga_test\*\impl\pnr\*.fs, if built
|
||||||
|
# SHA256SUMS.txt
|
||||||
|
#
|
||||||
|
# .\scripts\build.ps1
|
||||||
|
# .\scripts\make_release.ps1 version from FW_VERSION_STR in src\sys.h
|
||||||
|
param([string]$Version)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
|
if (-not $Version) {
|
||||||
|
$m = Select-String -Path (Join-Path $root 'src\sys.h') -Pattern '#define\s+FW_VERSION_STR\s+"([^"]+)"'
|
||||||
|
if (-not $m) { throw 'FW_VERSION_STR not found in src\sys.h' }
|
||||||
|
$Version = $m.Matches[0].Groups[1].Value
|
||||||
|
}
|
||||||
|
$tag = 'v' + $Version.TrimStart('v')
|
||||||
|
|
||||||
|
$bridgeFw = Join-Path $root 'build\build_out\tang25k_debugger_bl616.bin'
|
||||||
|
if (-not (Test-Path $bridgeFw)) { throw 'bridge firmware is not built: run scripts\build.ps1' }
|
||||||
|
if (-not (Test-Path (Join-Path $root 'third_party\bouffalo_sdk\tools\bflb_tools\bouffalo_flash_cube\BLFlashCommand.exe'))) {
|
||||||
|
throw 'Bouffalo SDK is not installed (scripts\setup.ps1): the portable copy needs BLFlashCommand'
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = Join-Path $root "dist\release\$tag"
|
||||||
|
if (Test-Path $out) { Remove-Item -Recurse -Force $out }
|
||||||
|
New-Item -ItemType Directory -Force $out | Out-Null
|
||||||
|
|
||||||
|
& (Join-Path $PSScriptRoot 'make_portable.ps1')
|
||||||
|
Copy-Item (Join-Path $root 'dist\TangProgrammer.zip') (Join-Path $out "TangProgrammer-$tag-win64.zip")
|
||||||
|
Copy-Item (Join-Path $root 'dist\TangProgrammer\TangProgrammer.exe') $out
|
||||||
|
|
||||||
|
Copy-Item $bridgeFw $out
|
||||||
|
Copy-Item (Join-Path $root 'bin\bl616_bootloader_0x20000_nano20k_signed.bin') $out
|
||||||
|
Copy-Item (Join-Path $root 'bin\bl616_fpga_partner_25kDock.bin') $out
|
||||||
|
|
||||||
|
$fs = @(Get-ChildItem (Join-Path $root 'fpga_test\*\impl\pnr\*.fs') -ErrorAction SilentlyContinue)
|
||||||
|
if ($fs.Count) {
|
||||||
|
Compress-Archive -Path ($fs | ForEach-Object { $_.FullName }) -DestinationPath (Join-Path $out 'fpga_test_bitstreams-GW5A-25.zip')
|
||||||
|
} else {
|
||||||
|
Write-Warning 'no test bitstreams: build them with gw_sh build.tcl in fpga_test\loopback and fpga_test\stream'
|
||||||
|
}
|
||||||
|
|
||||||
|
# sha256sum format, so that "sha256sum -c SHA256SUMS.txt" works
|
||||||
|
$sums = Get-ChildItem $out -File | Sort-Object Name | ForEach-Object {
|
||||||
|
(Get-FileHash -Algorithm SHA256 $_.FullName).Hash.ToLower() + ' ' + $_.Name
|
||||||
|
}
|
||||||
|
[IO.File]::WriteAllText((Join-Path $out 'SHA256SUMS.txt'), ($sums -join "`n") + "`n")
|
||||||
|
|
||||||
|
Get-ChildItem $out -File | Sort-Object Name | Format-Table Name, @{ n = 'KB'; e = { [math]::Ceiling($_.Length / 1KB) }; a = 'right' } -AutoSize
|
||||||
|
Write-Host "OK: $out" -ForegroundColor Green
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Publish dist\release\v<version>\ (made by make_release.ps1) as a Gitea
|
||||||
|
# release: the release text is the version's section of CHANGELOG.md. The tag
|
||||||
|
# has to be pushed already. Re-running uploads only the files that are missing.
|
||||||
|
#
|
||||||
|
# Needs a Gitea access token with the write:repository scope
|
||||||
|
# (Settings -> Applications -> Generate New Token):
|
||||||
|
#
|
||||||
|
# $env:GITEA_TOKEN = '<token>'
|
||||||
|
# .\scripts\publish_release.ps1 version from src\sys.h
|
||||||
|
# .\scripts\publish_release.ps1 -Version 1.0.1
|
||||||
|
param(
|
||||||
|
[string]$Version,
|
||||||
|
[string]$Token = $env:GITEA_TOKEN,
|
||||||
|
[string]$Remote = 'origin'
|
||||||
|
)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
|
if (-not $Version) {
|
||||||
|
$m = Select-String -Path (Join-Path $root 'src\sys.h') -Pattern '#define\s+FW_VERSION_STR\s+"([^"]+)"'
|
||||||
|
if (-not $m) { throw 'FW_VERSION_STR not found in src\sys.h' }
|
||||||
|
$Version = $m.Matches[0].Groups[1].Value
|
||||||
|
}
|
||||||
|
$Version = $Version.TrimStart('v')
|
||||||
|
$tag = 'v' + $Version
|
||||||
|
$dir = Join-Path $root "dist\release\$tag"
|
||||||
|
if (-not (Test-Path $dir)) { throw "$dir not found: run scripts\make_release.ps1 first" }
|
||||||
|
if (-not $Token) { throw 'no token: set $env:GITEA_TOKEN (Gitea: Settings -> Applications -> Generate New Token, scope write:repository)' }
|
||||||
|
|
||||||
|
# http://server/owner/repo.git -> http://server/api/v1/repos/owner/repo
|
||||||
|
$url = (& git -C $root remote get-url $Remote).Trim()
|
||||||
|
if ($url -notmatch '^(https?://[^/]+)/(.+?)(\.git)?/?$') { throw "not an http(s) remote: $url" }
|
||||||
|
$api = "$($Matches[1])/api/v1/repos/$($Matches[2])"
|
||||||
|
|
||||||
|
# the release text: the lines under "## v<version>" up to the next "## "
|
||||||
|
$notes = New-Object System.Collections.Generic.List[string]
|
||||||
|
$inside = $false
|
||||||
|
foreach ($line in [IO.File]::ReadAllLines((Join-Path $root 'CHANGELOG.md'), [Text.Encoding]::UTF8)) {
|
||||||
|
if ($line -match '^## ') {
|
||||||
|
if ($inside) { break }
|
||||||
|
$inside = $line -match "^## v?$([regex]::Escape($Version))\b"
|
||||||
|
} elseif ($inside) {
|
||||||
|
$notes.Add($line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $inside) { throw "no '## $tag' section in CHANGELOG.md" }
|
||||||
|
$body = ($notes -join "`n").Trim()
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.Net.Http
|
||||||
|
$http = New-Object System.Net.Http.HttpClient
|
||||||
|
$http.Timeout = [TimeSpan]::FromMinutes(10)
|
||||||
|
$http.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue 'token', $Token
|
||||||
|
|
||||||
|
function Invoke-Gitea([string]$Method, [string]$Path, [System.Net.Http.HttpContent]$Content) {
|
||||||
|
$req = New-Object System.Net.Http.HttpRequestMessage ([System.Net.Http.HttpMethod]::new($Method)), ($api + $Path)
|
||||||
|
if ($Content) { $req.Content = $Content }
|
||||||
|
$resp = $http.SendAsync($req).GetAwaiter().GetResult()
|
||||||
|
$text = $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
||||||
|
$code = [int]$resp.StatusCode
|
||||||
|
if ($code -ge 400 -and $code -ne 404) { throw "$Method $Path -> $code $text" }
|
||||||
|
return [pscustomobject]@{ Code = $code; Json = $(if ($text) { $text | ConvertFrom-Json } else { $null }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Invoke-Gitea GET "/tags/$tag").Code -eq 404) {
|
||||||
|
throw "tag $tag is not on the server: git tag -a $tag -m $tag; git push $Remote $tag"
|
||||||
|
}
|
||||||
|
|
||||||
|
$r = Invoke-Gitea GET "/releases/tags/$tag"
|
||||||
|
if ($r.Code -eq 404) {
|
||||||
|
$json = ConvertTo-Json @{ tag_name = $tag; name = $tag; body = $body; draft = $false; prerelease = $false }
|
||||||
|
$r = Invoke-Gitea POST '/releases' (New-Object System.Net.Http.StringContent $json, ([Text.Encoding]::UTF8), 'application/json')
|
||||||
|
Write-Host "Created release $tag"
|
||||||
|
} else {
|
||||||
|
Write-Host "Release $tag exists, adding missing files"
|
||||||
|
}
|
||||||
|
$release = $r.Json
|
||||||
|
$have = @($release.assets | ForEach-Object { $_.name })
|
||||||
|
|
||||||
|
foreach ($f in Get-ChildItem $dir -File | Sort-Object Name) {
|
||||||
|
if ($have -contains $f.Name) {
|
||||||
|
Write-Host " = $($f.Name) (already there)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$file = [System.Net.Http.ByteArrayContent]::new([IO.File]::ReadAllBytes($f.FullName))
|
||||||
|
$file.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::new('application/octet-stream')
|
||||||
|
$form = New-Object System.Net.Http.MultipartFormDataContent
|
||||||
|
$form.Add($file, 'attachment', $f.Name)
|
||||||
|
Invoke-Gitea POST "/releases/$($release.id)/assets?name=$([Uri]::EscapeDataString($f.Name))" $form | Out-Null
|
||||||
|
Write-Host (" + {0} ({1:N0} KB)" -f $f.Name, ($f.Length / 1KB))
|
||||||
|
}
|
||||||
|
Write-Host "OK: $($release.html_url)" -ForegroundColor Green
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Fetch the Bouffalo SDK and the T-Head RISC-V toolchain into third_party\.
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
. (Join-Path $PSScriptRoot 'common.ps1')
|
||||||
|
|
||||||
|
$root = Get-ProjectRoot
|
||||||
|
$tp = Join-Path $root 'third_party'
|
||||||
|
New-Item -ItemType Directory -Force $tp | Out-Null
|
||||||
|
|
||||||
|
if (-not (Test-Path (Join-Path $tp 'bouffalo_sdk'))) {
|
||||||
|
git clone --depth 1 --branch v2.3.16 https://github.com/bouffalolab/bouffalo_sdk.git (Join-Path $tp 'bouffalo_sdk')
|
||||||
|
}
|
||||||
|
if (-not (Test-Path (Join-Path $tp 'toolchain_gcc_t-head_windows'))) {
|
||||||
|
git clone --depth 1 https://github.com/bouffalolab/toolchain_gcc_t-head_windows.git (Join-Path $tp 'toolchain_gcc_t-head_windows')
|
||||||
|
}
|
||||||
|
Write-Host 'SDK and toolchain are in place.' -ForegroundColor Green
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Picked up by the SDK through __has_include("board_overlay.h"): it replaces
|
||||||
|
* bsp/board/bl616dk/board.h and disables bl616dk/board.c. The board init for
|
||||||
|
* the Tang Primer 25K Dock lives in board_tang25k.c.
|
||||||
|
*/
|
||||||
|
#ifndef BOARD_OVERLAY_H
|
||||||
|
#define BOARD_OVERLAY_H
|
||||||
|
|
||||||
|
#include "board_gpio.h"
|
||||||
|
#include "board_flash_psram.h"
|
||||||
|
|
||||||
|
void board_init(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Board init for the BL616 on the Tang Primer 25K Dock.
|
||||||
|
*
|
||||||
|
* Differences from the SDK bl616dk board:
|
||||||
|
* - 26 MHz crystal (bl616dk assumes 40 MHz),
|
||||||
|
* - the flash is left at the clock set by the boot ROM/bootloader
|
||||||
|
* (board_set_flash_80m() recalibrates the PLL with 40 MHz values),
|
||||||
|
* - UART and SPI run from the 160 MHz MUXPLL for accurate high baud rates,
|
||||||
|
* - no PSRAM, no wireless.
|
||||||
|
*/
|
||||||
|
#include "bflb_uart.h"
|
||||||
|
#include "bflb_gpio.h"
|
||||||
|
#include "bflb_clock.h"
|
||||||
|
#include "bflb_irq.h"
|
||||||
|
#include "board.h"
|
||||||
|
#include "bl616_glb.h"
|
||||||
|
#include "mm.h"
|
||||||
|
#include "hw.h"
|
||||||
|
|
||||||
|
extern void bflb_uart_set_console(struct bflb_device_s *dev);
|
||||||
|
|
||||||
|
extern uint32_t __HeapBase;
|
||||||
|
extern uint32_t __HeapLimit;
|
||||||
|
extern uint32_t _heap_wifi_start;
|
||||||
|
extern uint32_t _heap_wifi_size;
|
||||||
|
|
||||||
|
static void ATTR_CLOCK_SECTION __attribute__((noinline)) system_clock_init(void)
|
||||||
|
{
|
||||||
|
GLB_Power_On_XTAL_And_PLL_CLK(GLB_XTAL_26M, GLB_PLL_WIFIPLL | GLB_PLL_AUPLL);
|
||||||
|
GLB_Set_MCU_System_CLK(GLB_MCU_SYS_CLK_TOP_WIFIPLL_320M);
|
||||||
|
HBN_Set_MCU_XCLK_Sel(HBN_MCU_XCLK_XTAL);
|
||||||
|
CPU_Set_MTimer_CLK(ENABLE, BL_MTIMER_SOURCE_CLOCK_MCU_XCLK, Clock_System_Clock_Get(BL_SYSTEM_CLOCK_XCLK) / 1000000 - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void peripheral_clock_init(void)
|
||||||
|
{
|
||||||
|
PERIPHERAL_CLOCK_SEC_ENABLE();
|
||||||
|
PERIPHERAL_CLOCK_DMA0_ENABLE();
|
||||||
|
PERIPHERAL_CLOCK_UART0_ENABLE();
|
||||||
|
PERIPHERAL_CLOCK_UART1_ENABLE();
|
||||||
|
PERIPHERAL_CLOCK_SPI0_ENABLE();
|
||||||
|
PERIPHERAL_CLOCK_TIMER0_1_WDG_ENABLE();
|
||||||
|
PERIPHERAL_CLOCK_USB_ENABLE();
|
||||||
|
|
||||||
|
GLB_Set_UART_CLK(ENABLE, HBN_UART_CLK_MUXPLL_160M, 0);
|
||||||
|
GLB_Set_SPI_CLK(ENABLE, GLB_SPI_CLK_MCU_MUXPLL_160M, 0);
|
||||||
|
GLB_Set_USB_CLK_From_WIFIPLL(1);
|
||||||
|
GLB_Swap_MCU_SPI_0_MOSI_With_MISO(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void console_init(void)
|
||||||
|
{
|
||||||
|
struct bflb_device_s *gpio = bflb_device_get_by_name("gpio");
|
||||||
|
struct bflb_device_s *uart0 = bflb_device_get_by_name("uart0");
|
||||||
|
struct bflb_uart_config_s cfg = {
|
||||||
|
.baudrate = 2000000,
|
||||||
|
.direction = UART_DIRECTION_TX,
|
||||||
|
.data_bits = UART_DATA_BITS_8,
|
||||||
|
.stop_bits = UART_STOP_BITS_1,
|
||||||
|
.parity = UART_PARITY_NONE,
|
||||||
|
.bit_order = UART_LSB_FIRST,
|
||||||
|
.flow_ctrl = 0,
|
||||||
|
.tx_fifo_threshold = 7,
|
||||||
|
.rx_fifo_threshold = 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
bflb_gpio_uart_init(gpio, PIN_CONSOLE_TX, GPIO_UART_FUNC_UART0_TX);
|
||||||
|
bflb_uart_init(uart0, &cfg);
|
||||||
|
bflb_uart_set_console(uart0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ram_heap_init(void)
|
||||||
|
{
|
||||||
|
mem_manager_init();
|
||||||
|
mm_register_heap(MM_HEAP_OCRAM_0, "OCRAM", MM_ALLOCATOR_TLSF, &__HeapBase,
|
||||||
|
(size_t)&__HeapLimit - (size_t)&__HeapBase);
|
||||||
|
mm_register_heap(MM_HEAP_WRAM_0, "WRAM", MM_ALLOCATOR_TLSF, &_heap_wifi_start,
|
||||||
|
(uintptr_t)&_heap_wifi_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void board_init(void)
|
||||||
|
{
|
||||||
|
uintptr_t flag = bflb_irq_save();
|
||||||
|
|
||||||
|
system_clock_init();
|
||||||
|
peripheral_clock_init();
|
||||||
|
bflb_irq_initialize();
|
||||||
|
console_init();
|
||||||
|
ram_heap_init();
|
||||||
|
|
||||||
|
bflb_irq_restore(flag);
|
||||||
|
}
|
||||||
+426
@@ -0,0 +1,426 @@
|
|||||||
|
/*
|
||||||
|
* DirtyJTAG protocol on USB interface 0 (EP 0x01 OUT / 0x82 IN).
|
||||||
|
*
|
||||||
|
* CMD_INFO answers "DJTAG3": from then on CMD_XFER carries a 16 bit bit
|
||||||
|
* count (openFPGALoader sends up to 4000 bits = 503 bytes, one HS packet).
|
||||||
|
* Before CMD_INFO the DirtyJTAG v2 framing (8 bit count + EXTEND_LENGTH) is
|
||||||
|
* used, so tools that never ask for the version keep working.
|
||||||
|
*
|
||||||
|
* OUT transfers are queued in a small ring of buffers and executed by the
|
||||||
|
* main loop, so the host can push the next transfer while the current one
|
||||||
|
* is being shifted out. A command split across two buffers is carried over.
|
||||||
|
* Read data is collected per buffer and sent back as one IN transfer.
|
||||||
|
*/
|
||||||
|
#include <string.h>
|
||||||
|
#include "bflb_irq.h"
|
||||||
|
#include "usbd_core.h"
|
||||||
|
#include "usb_desc.h"
|
||||||
|
#include "djtag.h"
|
||||||
|
#include "jtag_io.h"
|
||||||
|
#include "uart_bridge.h"
|
||||||
|
#include "sys.h"
|
||||||
|
|
||||||
|
enum {
|
||||||
|
CMD_STOP = 0x00,
|
||||||
|
CMD_INFO = 0x01,
|
||||||
|
CMD_FREQ = 0x02,
|
||||||
|
CMD_XFER = 0x03,
|
||||||
|
CMD_SETSIG = 0x04,
|
||||||
|
CMD_GETSIG = 0x05,
|
||||||
|
CMD_CLK = 0x06,
|
||||||
|
CMD_SETVOLTAGE = 0x07,
|
||||||
|
CMD_GOTOBOOTLOADER = 0x08,
|
||||||
|
};
|
||||||
|
|
||||||
|
#define NO_READ 0x80 /* CMD_XFER */
|
||||||
|
#define EXTEND_LENGTH 0x40 /* CMD_XFER, v2 framing */
|
||||||
|
#define READOUT 0x80 /* CMD_CLK */
|
||||||
|
|
||||||
|
#define JBUF_NUM 8
|
||||||
|
#define JBUF_SIZE 512
|
||||||
|
#define RSP_SIZE 1024
|
||||||
|
#define XFER_MAX_BITS 4096
|
||||||
|
#define CMD_MAX_LEN (3 + XFER_MAX_BITS / 8)
|
||||||
|
#define CMD_BAD 0xFFFFFFFFu
|
||||||
|
|
||||||
|
struct __attribute__((packed)) fw_info {
|
||||||
|
char magic[4]; /* "T25D" */
|
||||||
|
uint8_t info_version;
|
||||||
|
uint8_t usb_high_speed;
|
||||||
|
uint16_t size;
|
||||||
|
char fw_version[12];
|
||||||
|
char build[24];
|
||||||
|
uint32_t uptime_ticks; /* 8 kHz */
|
||||||
|
uint32_t jtag_commands;
|
||||||
|
uint32_t jtag_xfer_bits;
|
||||||
|
uint32_t jtag_freq_khz;
|
||||||
|
uint32_t jtag_protocol_errors;
|
||||||
|
uint32_t uart_baud_requested;
|
||||||
|
uint32_t uart_baud_actual;
|
||||||
|
uint32_t uart_rx_bytes;
|
||||||
|
uint32_t uart_rx_dropped;
|
||||||
|
uint32_t uart_tx_bytes;
|
||||||
|
uint32_t uart_rx_resets;
|
||||||
|
};
|
||||||
|
|
||||||
|
static USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t jbuf[JBUF_NUM][JBUF_SIZE];
|
||||||
|
static USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t rsp[2][RSP_SIZE];
|
||||||
|
|
||||||
|
/* staging area: an incomplete command followed by the next buffer */
|
||||||
|
static uint8_t work[CMD_MAX_LEN + JBUF_SIZE];
|
||||||
|
static uint32_t carry_len;
|
||||||
|
|
||||||
|
/* filled by the OUT interrupt, consumed by the main loop */
|
||||||
|
static volatile uint16_t jlen[JBUF_NUM];
|
||||||
|
static volatile uint32_t jwr, jrd;
|
||||||
|
static volatile bool out_armed;
|
||||||
|
static uint16_t out_mps = 64;
|
||||||
|
static volatile bool configured;
|
||||||
|
static volatile bool in_busy;
|
||||||
|
static volatile uint32_t session;
|
||||||
|
|
||||||
|
static uint32_t seen_session;
|
||||||
|
static uint8_t rsp_cur;
|
||||||
|
static uint32_t rsp_len;
|
||||||
|
static bool proto_v3;
|
||||||
|
static struct djtag_stats stats;
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- USB */
|
||||||
|
|
||||||
|
/* interrupt context, or main loop with interrupts off */
|
||||||
|
static void out_arm(void)
|
||||||
|
{
|
||||||
|
if (!out_armed && configured && (jwr - jrd) < JBUF_NUM) {
|
||||||
|
out_armed = true;
|
||||||
|
/* one packet at a time: transfers are not always ended by a short packet */
|
||||||
|
usbd_ep_start_read(0, JTAG_OUT_EP, jbuf[jwr % JBUF_NUM], out_mps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void jtag_out_cb(uint8_t busid, uint8_t ep, uint32_t nbytes)
|
||||||
|
{
|
||||||
|
(void)busid;
|
||||||
|
(void)ep;
|
||||||
|
|
||||||
|
if (nbytes == 0) {
|
||||||
|
/* zero length packet: the BL616 driver keeps the read armed */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out_armed = false;
|
||||||
|
jlen[jwr % JBUF_NUM] = (uint16_t)nbytes;
|
||||||
|
jwr++;
|
||||||
|
out_arm();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void jtag_in_cb(uint8_t busid, uint8_t ep, uint32_t nbytes)
|
||||||
|
{
|
||||||
|
(void)busid;
|
||||||
|
(void)ep;
|
||||||
|
(void)nbytes;
|
||||||
|
in_busy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rsp_flush(void)
|
||||||
|
{
|
||||||
|
if (rsp_len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t s = session;
|
||||||
|
while (in_busy) {
|
||||||
|
/* the host has not read the previous answer yet */
|
||||||
|
if (s != session || !configured) {
|
||||||
|
rsp_len = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* No zero length packet after a response that fills whole packets:
|
||||||
|
* DirtyJTAG hosts read exactly the expected length, and a ZLP requested
|
||||||
|
* on BL616 while data is still in the FIFO is not reliable.
|
||||||
|
*/
|
||||||
|
in_busy = true;
|
||||||
|
usbd_ep_start_write(0, JTAG_IN_EP, rsp[rsp_cur], rsp_len);
|
||||||
|
rsp_cur ^= 1;
|
||||||
|
rsp_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t *rsp_reserve(uint32_t n)
|
||||||
|
{
|
||||||
|
if (rsp_len + n > RSP_SIZE) {
|
||||||
|
rsp_flush();
|
||||||
|
}
|
||||||
|
uint8_t *p = &rsp[rsp_cur][rsp_len];
|
||||||
|
rsp_len += n;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ parsing */
|
||||||
|
|
||||||
|
static uint32_t xfer_bits(const uint8_t *c)
|
||||||
|
{
|
||||||
|
if (proto_v3) {
|
||||||
|
return ((uint32_t)c[1] << 8) | c[2];
|
||||||
|
}
|
||||||
|
return c[1] + ((c[0] & EXTEND_LENGTH) ? 256 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* full length of the command at c, 0 if more bytes are needed to tell */
|
||||||
|
static uint32_t cmd_length(const uint8_t *c, uint32_t avail)
|
||||||
|
{
|
||||||
|
switch (c[0] & 0x0F) {
|
||||||
|
case CMD_STOP:
|
||||||
|
case CMD_INFO:
|
||||||
|
case CMD_GETSIG:
|
||||||
|
case CMD_GOTOBOOTLOADER:
|
||||||
|
return 1;
|
||||||
|
case CMD_SETVOLTAGE:
|
||||||
|
return 2;
|
||||||
|
case CMD_FREQ:
|
||||||
|
case CMD_SETSIG:
|
||||||
|
case CMD_CLK:
|
||||||
|
return 3;
|
||||||
|
case CMD_XFER: {
|
||||||
|
uint32_t hdr = proto_v3 ? 3 : 2;
|
||||||
|
if (avail < hdr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
uint32_t bits = xfer_bits(c);
|
||||||
|
if (bits > XFER_MAX_BITS) {
|
||||||
|
return CMD_BAD;
|
||||||
|
}
|
||||||
|
return hdr + (bits + 7) / 8;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return CMD_BAD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cmd_exec(const uint8_t *c)
|
||||||
|
{
|
||||||
|
stats.commands++;
|
||||||
|
|
||||||
|
switch (c[0] & 0x0F) {
|
||||||
|
case CMD_STOP:
|
||||||
|
rsp_flush();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_INFO:
|
||||||
|
memcpy(rsp_reserve(10), "DJTAG3\n\0\0\0", 10);
|
||||||
|
proto_v3 = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_FREQ:
|
||||||
|
stats.freq_khz = jtag_io_set_freq_khz(((uint32_t)c[1] << 8) | c[2]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_XFER: {
|
||||||
|
uint32_t bits = xfer_bits(c);
|
||||||
|
const uint8_t *tdi = c + (proto_v3 ? 3 : 2);
|
||||||
|
uint8_t *tdo = (c[0] & NO_READ) ? NULL : rsp_reserve((bits + 7) / 8);
|
||||||
|
|
||||||
|
jtag_io_xfer(tdi, tdo, bits);
|
||||||
|
stats.xfer_bits += bits;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CMD_SETSIG:
|
||||||
|
jtag_io_setsig(c[1], c[2]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_GETSIG:
|
||||||
|
*rsp_reserve(1) = jtag_io_getsig();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_CLK: {
|
||||||
|
bool tdo = jtag_io_clk(c[2], (c[1] & SIG_TMS) != 0, (c[1] & SIG_TDI) != 0);
|
||||||
|
if (c[0] & READOUT) {
|
||||||
|
*rsp_reserve(1) = tdo ? 1 : 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CMD_SETVOLTAGE:
|
||||||
|
/* fixed 3.3 V I/O */
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_GOTOBOOTLOADER:
|
||||||
|
rsp_flush();
|
||||||
|
sys_request_isp_reboot();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void process(const uint8_t *buf, uint32_t len)
|
||||||
|
{
|
||||||
|
const uint8_t *p = buf;
|
||||||
|
uint32_t n = len;
|
||||||
|
|
||||||
|
if (carry_len) {
|
||||||
|
memcpy(work + carry_len, buf, len);
|
||||||
|
p = work;
|
||||||
|
n = carry_len + len;
|
||||||
|
carry_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t pos = 0;
|
||||||
|
while (pos < n) {
|
||||||
|
uint32_t need = cmd_length(p + pos, n - pos);
|
||||||
|
|
||||||
|
if (need == CMD_BAD) {
|
||||||
|
/* like the reference firmware: drop the rest of this buffer */
|
||||||
|
stats.protocol_errors++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (need == 0 || pos + need > n) {
|
||||||
|
carry_len = n - pos;
|
||||||
|
memmove(work, p + pos, carry_len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cmd_exec(p + pos);
|
||||||
|
pos += need;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void djtag_poll(void)
|
||||||
|
{
|
||||||
|
if (seen_session != session) {
|
||||||
|
seen_session = session;
|
||||||
|
carry_len = 0;
|
||||||
|
rsp_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (jrd != jwr) {
|
||||||
|
uint32_t i = jrd % JBUF_NUM;
|
||||||
|
|
||||||
|
process(jbuf[i], jlen[i]);
|
||||||
|
rsp_flush();
|
||||||
|
sys_activity();
|
||||||
|
|
||||||
|
uintptr_t flag = bflb_irq_save();
|
||||||
|
jrd++;
|
||||||
|
out_arm();
|
||||||
|
bflb_irq_restore(flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------ vendor requests */
|
||||||
|
|
||||||
|
static int djtag_vendor_handler(uint8_t busid, struct usb_setup_packet *setup, uint8_t **data, uint32_t *len)
|
||||||
|
{
|
||||||
|
(void)busid;
|
||||||
|
|
||||||
|
switch (setup->bRequest) {
|
||||||
|
case VREQ_REBOOT_ISP:
|
||||||
|
sys_request_isp_reboot();
|
||||||
|
*len = 0;
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case VREQ_GET_INFO: {
|
||||||
|
struct fw_info info;
|
||||||
|
struct uart_stats us;
|
||||||
|
|
||||||
|
memset(&info, 0, sizeof(info));
|
||||||
|
memcpy(info.magic, "T25D", 4);
|
||||||
|
info.info_version = 2;
|
||||||
|
info.usb_high_speed = usbd_get_port_speed(0) == USB_SPEED_HIGH;
|
||||||
|
info.size = sizeof(info);
|
||||||
|
strncpy(info.fw_version, FW_VERSION_STR, sizeof(info.fw_version) - 1);
|
||||||
|
strncpy(info.build, __DATE__ " " __TIME__, sizeof(info.build) - 1);
|
||||||
|
info.uptime_ticks = sys_ticks();
|
||||||
|
info.jtag_commands = stats.commands;
|
||||||
|
info.jtag_xfer_bits = stats.xfer_bits;
|
||||||
|
info.jtag_freq_khz = stats.freq_khz;
|
||||||
|
info.jtag_protocol_errors = stats.protocol_errors;
|
||||||
|
uart_bridge_get_stats(&us);
|
||||||
|
info.uart_baud_requested = us.baud_requested;
|
||||||
|
info.uart_baud_actual = us.baud_actual;
|
||||||
|
info.uart_rx_bytes = us.rx_bytes;
|
||||||
|
info.uart_rx_dropped = us.rx_dropped;
|
||||||
|
info.uart_tx_bytes = us.tx_bytes;
|
||||||
|
info.uart_rx_resets = us.rx_resets;
|
||||||
|
|
||||||
|
memcpy(*data, &info, sizeof(info));
|
||||||
|
*len = sizeof(info);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
case VREQ_PEEK: {
|
||||||
|
/* debug: read one 32 bit word from a peripheral register */
|
||||||
|
uint32_t addr = ((uint32_t)setup->wIndex << 16) | setup->wValue;
|
||||||
|
uint32_t val;
|
||||||
|
|
||||||
|
if ((addr & 3) || !((addr >= 0x20000000 && addr < 0x30000000) ||
|
||||||
|
(addr >= 0x62FC0000 && addr < 0x63040000))) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
val = *(volatile uint32_t *)(uintptr_t)addr;
|
||||||
|
memcpy(*data, &val, 4);
|
||||||
|
*len = 4;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
case VREQ_POKE: {
|
||||||
|
/* debug: write one 32 bit word (in the data stage) to a peripheral register */
|
||||||
|
uint32_t addr = ((uint32_t)setup->wIndex << 16) | setup->wValue;
|
||||||
|
uint32_t val;
|
||||||
|
|
||||||
|
if ((addr & 3) || addr < 0x20000000 || addr >= 0x30000000 || setup->wLength != 4) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(&val, *data, 4);
|
||||||
|
*(volatile uint32_t *)(uintptr_t)addr = val;
|
||||||
|
*len = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct usbd_interface jtag_intf = {
|
||||||
|
.vendor_handler = djtag_vendor_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct usbd_endpoint jtag_out_ep = {
|
||||||
|
.ep_addr = JTAG_OUT_EP,
|
||||||
|
.ep_cb = jtag_out_cb,
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct usbd_endpoint jtag_in_ep = {
|
||||||
|
.ep_addr = JTAG_IN_EP,
|
||||||
|
.ep_cb = jtag_in_cb,
|
||||||
|
};
|
||||||
|
|
||||||
|
void djtag_usb_init(uint8_t busid)
|
||||||
|
{
|
||||||
|
stats.freq_khz = jtag_io_set_freq_khz(6000);
|
||||||
|
usbd_add_interface(busid, &jtag_intf);
|
||||||
|
usbd_add_endpoint(busid, &jtag_out_ep);
|
||||||
|
usbd_add_endpoint(busid, &jtag_in_ep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void djtag_on_reset(void)
|
||||||
|
{
|
||||||
|
configured = false;
|
||||||
|
out_armed = false;
|
||||||
|
in_busy = false;
|
||||||
|
session++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void djtag_on_configured(void)
|
||||||
|
{
|
||||||
|
out_mps = usbd_get_ep_mps(0, JTAG_OUT_EP);
|
||||||
|
configured = true;
|
||||||
|
out_armed = false;
|
||||||
|
in_busy = false;
|
||||||
|
session++;
|
||||||
|
out_arm();
|
||||||
|
}
|
||||||
|
|
||||||
|
void djtag_get_stats(struct djtag_stats *st)
|
||||||
|
{
|
||||||
|
*st = stats;
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#ifndef DJTAG_H
|
||||||
|
#define DJTAG_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
struct djtag_stats {
|
||||||
|
uint32_t commands;
|
||||||
|
uint32_t xfer_bits;
|
||||||
|
uint32_t freq_khz;
|
||||||
|
uint32_t protocol_errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* registers interface 0 + endpoints, call before usbd_initialize */
|
||||||
|
void djtag_usb_init(uint8_t busid);
|
||||||
|
|
||||||
|
void djtag_on_reset(void);
|
||||||
|
void djtag_on_configured(void);
|
||||||
|
|
||||||
|
/* main loop: executes queued JTAG command buffers */
|
||||||
|
void djtag_poll(void);
|
||||||
|
|
||||||
|
void djtag_get_stats(struct djtag_stats *st);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Tang Primer 25K Dock: BL616 <-> GW5A-25 wiring (schematic Tang_25K_60033).
|
||||||
|
*/
|
||||||
|
#ifndef HW_H
|
||||||
|
#define HW_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define CPU_HZ 320000000u
|
||||||
|
|
||||||
|
/* JTAG to the FPGA. TCK/TDO/TDI sit on SPI0 SCLK/MISO/MOSI pin slots. */
|
||||||
|
#define PIN_JTAG_TMS 0
|
||||||
|
#define PIN_JTAG_TCK 1
|
||||||
|
#define PIN_JTAG_TDO 2 /* also the BL616 boot strap pin */
|
||||||
|
#define PIN_JTAG_TDI 3
|
||||||
|
|
||||||
|
/* UART to the FPGA */
|
||||||
|
#define PIN_UART_RX 10 /* <- FPGA C3 (uart_tx in Sipeed examples) */
|
||||||
|
#define PIN_UART_TX 11 /* -> FPGA B3 (uart_rx in Sipeed examples) */
|
||||||
|
|
||||||
|
#define PIN_BUTTON 12 /* S3, active low */
|
||||||
|
#define PIN_LED 20 /* LED5, active low */
|
||||||
|
#define PIN_CONSOLE_TX 21 /* not connected on the Dock, debug log only */
|
||||||
|
|
||||||
|
/* GLB fast GPIO registers for pins 0..31 */
|
||||||
|
#define GPIO_IN_REG (*(volatile uint32_t *)0x20000AC4) /* GLB_GPIO_CFG128 */
|
||||||
|
#define GPIO_SET_REG (*(volatile uint32_t *)0x20000AEC) /* GLB_GPIO_CFG138 */
|
||||||
|
#define GPIO_CLR_REG (*(volatile uint32_t *)0x20000AF4) /* GLB_GPIO_CFG140 */
|
||||||
|
#define GPIO_CFG_REG(p) (*(volatile uint32_t *)(0x200008C4 + 4 * (p)))
|
||||||
|
|
||||||
|
static inline uint32_t cpu_cycles(void)
|
||||||
|
{
|
||||||
|
uint32_t c;
|
||||||
|
__asm volatile("csrr %0, mcycle" : "=r"(c));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
+329
@@ -0,0 +1,329 @@
|
|||||||
|
/*
|
||||||
|
* JTAG engine for the Tang Primer 25K BL616.
|
||||||
|
*
|
||||||
|
* TMS = GPIO0, TCK = GPIO1, TDO = GPIO2, TDI = GPIO3. GPIO1..3 are also the
|
||||||
|
* SPI0 SCLK/MISO/MOSI pin slots, so long TDI/TDO shifts are done by SPI0 in
|
||||||
|
* mode 0 (data set up before the rising edge, TDO sampled on the rising
|
||||||
|
* edge, SCLK idles low), which is exactly JTAG timing. TMS stays a plain GPIO
|
||||||
|
* and is not touched during a shift. Everything else is bit-banged through
|
||||||
|
* the GLB set/clear/input registers.
|
||||||
|
*/
|
||||||
|
#include "bflb_gpio.h"
|
||||||
|
#include "bflb_spi.h"
|
||||||
|
#include "bflb_clock.h"
|
||||||
|
#include "hardware/spi_reg.h"
|
||||||
|
#include "jtag_io.h"
|
||||||
|
#include "hw.h"
|
||||||
|
|
||||||
|
#define M_TMS (1u << PIN_JTAG_TMS)
|
||||||
|
#define M_TCK (1u << PIN_JTAG_TCK)
|
||||||
|
#define M_TDO (1u << PIN_JTAG_TDO)
|
||||||
|
#define M_TDI (1u << PIN_JTAG_TDI)
|
||||||
|
|
||||||
|
#define JTAG_MAX_KHZ 30000u
|
||||||
|
#define JTAG_DEFAULT_KHZ 6000u
|
||||||
|
#define SPI_MIN_BYTES 4u /* shorter shifts are cheaper to bit-bang */
|
||||||
|
#define SPI_FIFO_INFLIGHT 24u /* bytes in flight, below the 32 byte RX FIFO */
|
||||||
|
|
||||||
|
#define PIN_DRV (GPIO_FLOAT | GPIO_SMT_EN | GPIO_DRV_2)
|
||||||
|
|
||||||
|
static uintptr_t spi_base;
|
||||||
|
static uint32_t spi_clk_hz;
|
||||||
|
|
||||||
|
static uint32_t half_cycles; /* CPU cycles per half TCK period (bit-bang) */
|
||||||
|
static uint32_t spi_prd0; /* SPI_PRD_0 value for the current frequency */
|
||||||
|
static bool spi_usable; /* frequency is within the SPI divider range */
|
||||||
|
static uint32_t last_edge; /* cycle stamp of the last SETSIG TCK edge */
|
||||||
|
|
||||||
|
/* GPIO_CFGx values for GPIO and SPI function, captured at init */
|
||||||
|
static uint32_t cfg_tck_gpio, cfg_tck_spi;
|
||||||
|
static uint32_t cfg_tdi_gpio, cfg_tdi_spi;
|
||||||
|
static uint32_t cfg_tdo_gpio, cfg_tdo_spi;
|
||||||
|
|
||||||
|
static inline void wait_until(uint32_t deadline)
|
||||||
|
{
|
||||||
|
while ((int32_t)(cpu_cycles() - deadline) < 0) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t spi_reg(uint32_t off)
|
||||||
|
{
|
||||||
|
return *(volatile uint32_t *)(spi_base + off);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void spi_reg_wr(uint32_t off, uint32_t val)
|
||||||
|
{
|
||||||
|
*(volatile uint32_t *)(spi_base + off) = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
void jtag_io_init(void)
|
||||||
|
{
|
||||||
|
struct bflb_device_s *gpio = bflb_device_get_by_name("gpio");
|
||||||
|
struct bflb_device_s *spi = bflb_device_get_by_name("spi0");
|
||||||
|
|
||||||
|
/* SPI master first, so SCLK is already idling low when the pins move */
|
||||||
|
struct bflb_spi_config_s cfg = {
|
||||||
|
.freq = 1000000,
|
||||||
|
.role = SPI_ROLE_MASTER,
|
||||||
|
.mode = SPI_MODE0,
|
||||||
|
.data_width = SPI_DATA_WIDTH_8BIT,
|
||||||
|
.bit_order = SPI_BIT_MSB,
|
||||||
|
.byte_order = SPI_BYTE_LSB,
|
||||||
|
.tx_fifo_threshold = 0,
|
||||||
|
.rx_fifo_threshold = 0,
|
||||||
|
};
|
||||||
|
bflb_spi_init(spi, &cfg);
|
||||||
|
spi_base = spi->reg_base;
|
||||||
|
spi_clk_hz = bflb_clk_get_peripheral_clock(BFLB_DEVICE_TYPE_SPI, 0);
|
||||||
|
/* continuous mode: no chip-select handling or gaps between frames */
|
||||||
|
spi_reg_wr(SPI_CONFIG_OFFSET, spi_reg(SPI_CONFIG_OFFSET) | SPI_CR_SPI_M_CONT_EN);
|
||||||
|
|
||||||
|
/* capture the SPI function configuration of the three shift pins */
|
||||||
|
bflb_gpio_init(gpio, PIN_JTAG_TCK, GPIO_FUNC_SPI0 | GPIO_ALTERNATE | PIN_DRV);
|
||||||
|
bflb_gpio_init(gpio, PIN_JTAG_TDI, GPIO_FUNC_SPI0 | GPIO_ALTERNATE | PIN_DRV);
|
||||||
|
bflb_gpio_init(gpio, PIN_JTAG_TDO, GPIO_FUNC_SPI0 | GPIO_ALTERNATE | PIN_DRV);
|
||||||
|
cfg_tck_spi = GPIO_CFG_REG(PIN_JTAG_TCK);
|
||||||
|
cfg_tdi_spi = GPIO_CFG_REG(PIN_JTAG_TDI);
|
||||||
|
cfg_tdo_spi = GPIO_CFG_REG(PIN_JTAG_TDO);
|
||||||
|
|
||||||
|
/* then the plain GPIO configuration, which is the resting state */
|
||||||
|
GPIO_CLR_REG = M_TCK | M_TDI;
|
||||||
|
GPIO_SET_REG = M_TMS;
|
||||||
|
bflb_gpio_init(gpio, PIN_JTAG_TCK, GPIO_OUTPUT | PIN_DRV);
|
||||||
|
bflb_gpio_init(gpio, PIN_JTAG_TDI, GPIO_OUTPUT | PIN_DRV);
|
||||||
|
bflb_gpio_init(gpio, PIN_JTAG_TMS, GPIO_OUTPUT | PIN_DRV);
|
||||||
|
bflb_gpio_init(gpio, PIN_JTAG_TDO, GPIO_INPUT | GPIO_FLOAT | GPIO_SMT_EN);
|
||||||
|
cfg_tck_gpio = GPIO_CFG_REG(PIN_JTAG_TCK);
|
||||||
|
cfg_tdi_gpio = GPIO_CFG_REG(PIN_JTAG_TDI);
|
||||||
|
cfg_tdo_gpio = GPIO_CFG_REG(PIN_JTAG_TDO);
|
||||||
|
GPIO_CLR_REG = M_TCK | M_TDI;
|
||||||
|
GPIO_SET_REG = M_TMS;
|
||||||
|
|
||||||
|
last_edge = cpu_cycles();
|
||||||
|
jtag_io_set_freq_khz(JTAG_DEFAULT_KHZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t jtag_io_set_freq_khz(uint32_t khz)
|
||||||
|
{
|
||||||
|
if (khz == 0) {
|
||||||
|
khz = 1;
|
||||||
|
}
|
||||||
|
if (khz > JTAG_MAX_KHZ) {
|
||||||
|
khz = JTAG_MAX_KHZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
half_cycles = CPU_HZ / (2000u * khz);
|
||||||
|
|
||||||
|
/* SPI SCLK = spi_clk / (ph0 + ph1), each phase 1..256 SPI clocks */
|
||||||
|
uint32_t total = (spi_clk_hz / 1000u + khz - 1) / khz;
|
||||||
|
if (total < 2) {
|
||||||
|
total = 2;
|
||||||
|
}
|
||||||
|
spi_usable = (spi_clk_hz != 0) && (total <= 512);
|
||||||
|
if (spi_usable) {
|
||||||
|
uint32_t ph0 = total / 2;
|
||||||
|
uint32_t ph1 = total - ph0;
|
||||||
|
spi_prd0 = ((ph0 - 1) << SPI_CR_SPI_PRD_D_PH_0_SHIFT) |
|
||||||
|
((ph1 - 1) << SPI_CR_SPI_PRD_D_PH_1_SHIFT);
|
||||||
|
/* start/stop phases at the minimum: CS is not routed anyway */
|
||||||
|
spi_reg_wr(SPI_PRD_0_OFFSET, spi_prd0);
|
||||||
|
spi_reg_wr(SPI_PRD_1_OFFSET, spi_reg(SPI_PRD_1_OFFSET) & ~SPI_CR_SPI_PRD_I_MASK);
|
||||||
|
return spi_clk_hz / 1000u / total;
|
||||||
|
}
|
||||||
|
return khz;
|
||||||
|
}
|
||||||
|
|
||||||
|
void jtag_io_setsig(uint8_t mask, uint8_t value)
|
||||||
|
{
|
||||||
|
uint32_t set = 0, clr = 0;
|
||||||
|
|
||||||
|
if (mask & SIG_TDI) {
|
||||||
|
if (value & SIG_TDI) {
|
||||||
|
set |= M_TDI;
|
||||||
|
} else {
|
||||||
|
clr |= M_TDI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mask & SIG_TMS) {
|
||||||
|
if (value & SIG_TMS) {
|
||||||
|
set |= M_TMS;
|
||||||
|
} else {
|
||||||
|
clr |= M_TMS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(mask & SIG_TCK)) {
|
||||||
|
GPIO_SET_REG = set;
|
||||||
|
GPIO_CLR_REG = clr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Keep at least half a TCK period between edges. Compare the elapsed
|
||||||
|
* time, not deadlines: mcycle wraps every 13.4 s, and a deadline taken
|
||||||
|
* from an edge seconds ago would look like it is still in the future.
|
||||||
|
*/
|
||||||
|
if (cpu_cycles() - last_edge < half_cycles) {
|
||||||
|
wait_until(last_edge + half_cycles);
|
||||||
|
}
|
||||||
|
if (value & SIG_TCK) {
|
||||||
|
/* data first, then the rising edge */
|
||||||
|
GPIO_SET_REG = set;
|
||||||
|
GPIO_CLR_REG = clr;
|
||||||
|
GPIO_SET_REG = M_TCK;
|
||||||
|
} else {
|
||||||
|
GPIO_CLR_REG = M_TCK;
|
||||||
|
GPIO_SET_REG = set;
|
||||||
|
GPIO_CLR_REG = clr;
|
||||||
|
}
|
||||||
|
last_edge = cpu_cycles();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t jtag_io_getsig(void)
|
||||||
|
{
|
||||||
|
uint32_t in = GPIO_IN_REG;
|
||||||
|
uint8_t sig = 0;
|
||||||
|
|
||||||
|
if (in & M_TDO) {
|
||||||
|
sig |= SIG_TDO;
|
||||||
|
}
|
||||||
|
if (in & M_TCK) {
|
||||||
|
sig |= SIG_TCK;
|
||||||
|
}
|
||||||
|
if (in & M_TDI) {
|
||||||
|
sig |= SIG_TDI;
|
||||||
|
}
|
||||||
|
if (in & M_TMS) {
|
||||||
|
sig |= SIG_TMS;
|
||||||
|
}
|
||||||
|
return sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TDO is sampled at the end of the TCK high phase: it changed on the previous
|
||||||
|
* falling edge, so this gives it a full period to settle through the GPIO
|
||||||
|
* input synchronizer.
|
||||||
|
*/
|
||||||
|
static void __attribute__((noinline)) ATTR_TCM_SECTION bitbang_shift(const uint8_t *tdi, uint8_t *tdo, uint32_t nbits)
|
||||||
|
{
|
||||||
|
const uint32_t half = half_cycles;
|
||||||
|
uint32_t t = cpu_cycles();
|
||||||
|
uint32_t in = 0, out = 0;
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < nbits; i++) {
|
||||||
|
uint32_t mask = 0x80u >> (i & 7);
|
||||||
|
|
||||||
|
if ((i & 7) == 0) {
|
||||||
|
in = tdi[i >> 3];
|
||||||
|
}
|
||||||
|
if (in & mask) {
|
||||||
|
GPIO_SET_REG = M_TDI;
|
||||||
|
} else {
|
||||||
|
GPIO_CLR_REG = M_TDI;
|
||||||
|
}
|
||||||
|
t += half;
|
||||||
|
wait_until(t);
|
||||||
|
GPIO_SET_REG = M_TCK;
|
||||||
|
t += half;
|
||||||
|
wait_until(t);
|
||||||
|
if (GPIO_IN_REG & M_TDO) {
|
||||||
|
out |= mask;
|
||||||
|
}
|
||||||
|
GPIO_CLR_REG = M_TCK;
|
||||||
|
|
||||||
|
if (((i & 7) == 7) || (i == nbits - 1)) {
|
||||||
|
if (tdo) {
|
||||||
|
tdo[i >> 3] = (uint8_t)out;
|
||||||
|
}
|
||||||
|
out = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_edge = cpu_cycles();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __attribute__((noinline)) ATTR_TCM_SECTION spi_shift(const uint8_t *tdi, uint8_t *tdo, uint32_t nbytes)
|
||||||
|
{
|
||||||
|
uint32_t tx = 0, rx = 0;
|
||||||
|
|
||||||
|
spi_reg_wr(SPI_PRD_0_OFFSET, spi_prd0);
|
||||||
|
spi_reg_wr(SPI_FIFO_CONFIG_0_OFFSET,
|
||||||
|
spi_reg(SPI_FIFO_CONFIG_0_OFFSET) | SPI_TX_FIFO_CLR | SPI_RX_FIFO_CLR);
|
||||||
|
|
||||||
|
/* TCK is low and SCLK idles low, so the hand-over is glitch free */
|
||||||
|
GPIO_CFG_REG(PIN_JTAG_TDO) = cfg_tdo_spi;
|
||||||
|
GPIO_CFG_REG(PIN_JTAG_TDI) = cfg_tdi_spi;
|
||||||
|
GPIO_CFG_REG(PIN_JTAG_TCK) = cfg_tck_spi;
|
||||||
|
|
||||||
|
while (rx < nbytes) {
|
||||||
|
uint32_t fifo = spi_reg(SPI_FIFO_CONFIG_1_OFFSET);
|
||||||
|
uint32_t rx_cnt = (fifo & SPI_RX_FIFO_CNT_MASK) >> SPI_RX_FIFO_CNT_SHIFT;
|
||||||
|
uint32_t tx_free = (fifo & SPI_TX_FIFO_CNT_MASK) >> SPI_TX_FIFO_CNT_SHIFT;
|
||||||
|
|
||||||
|
while (rx_cnt--) {
|
||||||
|
uint8_t b = (uint8_t)spi_reg(SPI_FIFO_RDATA_OFFSET);
|
||||||
|
if (tdo) {
|
||||||
|
tdo[rx] = b;
|
||||||
|
}
|
||||||
|
rx++;
|
||||||
|
}
|
||||||
|
while (tx_free-- && (tx < nbytes) && ((tx - rx) < SPI_FIFO_INFLIGHT)) {
|
||||||
|
spi_reg_wr(SPI_FIFO_WDATA_OFFSET, tdi[tx++]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (spi_reg(SPI_BUS_BUSY_OFFSET)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
GPIO_CFG_REG(PIN_JTAG_TCK) = cfg_tck_gpio;
|
||||||
|
GPIO_CFG_REG(PIN_JTAG_TDI) = cfg_tdi_gpio;
|
||||||
|
GPIO_CFG_REG(PIN_JTAG_TDO) = cfg_tdo_gpio;
|
||||||
|
last_edge = cpu_cycles();
|
||||||
|
}
|
||||||
|
|
||||||
|
void jtag_io_xfer(const uint8_t *tdi, uint8_t *tdo, uint32_t nbits)
|
||||||
|
{
|
||||||
|
uint32_t nbytes = nbits / 8;
|
||||||
|
|
||||||
|
if (spi_usable && (nbytes >= SPI_MIN_BYTES)) {
|
||||||
|
spi_shift(tdi, tdo, nbytes);
|
||||||
|
tdi += nbytes;
|
||||||
|
if (tdo) {
|
||||||
|
tdo += nbytes;
|
||||||
|
}
|
||||||
|
nbits &= 7;
|
||||||
|
}
|
||||||
|
if (nbits) {
|
||||||
|
bitbang_shift(tdi, tdo, nbits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ATTR_TCM_SECTION jtag_io_clk(uint32_t count, bool tms, bool tdi)
|
||||||
|
{
|
||||||
|
const uint32_t half = half_cycles;
|
||||||
|
bool tdo = false;
|
||||||
|
|
||||||
|
GPIO_SET_REG = (tms ? M_TMS : 0) | (tdi ? M_TDI : 0);
|
||||||
|
GPIO_CLR_REG = (tms ? 0 : M_TMS) | (tdi ? 0 : M_TDI);
|
||||||
|
|
||||||
|
uint32_t t = cpu_cycles();
|
||||||
|
while (count--) {
|
||||||
|
t += half;
|
||||||
|
wait_until(t);
|
||||||
|
GPIO_SET_REG = M_TCK;
|
||||||
|
t += half;
|
||||||
|
wait_until(t);
|
||||||
|
tdo = (GPIO_IN_REG & M_TDO) != 0;
|
||||||
|
GPIO_CLR_REG = M_TCK;
|
||||||
|
}
|
||||||
|
last_edge = cpu_cycles();
|
||||||
|
return tdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void jtag_io_release(void)
|
||||||
|
{
|
||||||
|
struct bflb_device_s *gpio = bflb_device_get_by_name("gpio");
|
||||||
|
|
||||||
|
bflb_gpio_init(gpio, PIN_JTAG_TCK, GPIO_INPUT | GPIO_FLOAT);
|
||||||
|
bflb_gpio_init(gpio, PIN_JTAG_TDI, GPIO_INPUT | GPIO_FLOAT);
|
||||||
|
bflb_gpio_init(gpio, PIN_JTAG_TMS, GPIO_INPUT | GPIO_FLOAT);
|
||||||
|
bflb_gpio_init(gpio, PIN_JTAG_TDO, GPIO_INPUT | GPIO_FLOAT);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Low level JTAG engine: GPIO bit-bang plus SPI0 for long shifts.
|
||||||
|
*/
|
||||||
|
#ifndef JTAG_IO_H
|
||||||
|
#define JTAG_IO_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* DirtyJTAG signal bits */
|
||||||
|
#define SIG_TCK (1 << 1)
|
||||||
|
#define SIG_TDI (1 << 2)
|
||||||
|
#define SIG_TDO (1 << 3)
|
||||||
|
#define SIG_TMS (1 << 4)
|
||||||
|
#define SIG_TRST (1 << 5)
|
||||||
|
#define SIG_SRST (1 << 6)
|
||||||
|
|
||||||
|
void jtag_io_init(void);
|
||||||
|
|
||||||
|
/* Returns the frequency actually used, in kHz. */
|
||||||
|
uint32_t jtag_io_set_freq_khz(uint32_t khz);
|
||||||
|
|
||||||
|
void jtag_io_setsig(uint8_t mask, uint8_t value);
|
||||||
|
uint8_t jtag_io_getsig(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shift nbits through TDI/TDO with TMS unchanged. Bits are MSB first inside
|
||||||
|
* each byte (DirtyJTAG order). tdo may be NULL; otherwise (nbits + 7) / 8
|
||||||
|
* bytes are written. TCK is low before and after.
|
||||||
|
*/
|
||||||
|
void jtag_io_xfer(const uint8_t *tdi, uint8_t *tdo, uint32_t nbits);
|
||||||
|
|
||||||
|
/* count TCK pulses with fixed TMS/TDI; returns TDO sampled on the last one */
|
||||||
|
bool jtag_io_clk(uint32_t count, bool tms, bool tdi);
|
||||||
|
|
||||||
|
/* Release all JTAG pins (inputs), e.g. before rebooting. */
|
||||||
|
void jtag_io_release(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Tang Primer 25K onboard debugger firmware for the BL616.
|
||||||
|
*
|
||||||
|
* USB composite device:
|
||||||
|
* - DirtyJTAG v3 (interface 0) for openFPGALoader -c dirtyJtag
|
||||||
|
* - CDC-ACM serial port bridged to the FPGA UART (B3/C3)
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "board.h"
|
||||||
|
#include "usbd_core.h"
|
||||||
|
#include "djtag.h"
|
||||||
|
#include "jtag_io.h"
|
||||||
|
#include "uart_bridge.h"
|
||||||
|
#include "usb_desc.h"
|
||||||
|
#include "sys.h"
|
||||||
|
|
||||||
|
#define USB_REG_BASE 0x20072000
|
||||||
|
|
||||||
|
static void usbd_event_handler(uint8_t busid, uint8_t event)
|
||||||
|
{
|
||||||
|
(void)busid;
|
||||||
|
|
||||||
|
switch (event) {
|
||||||
|
case USBD_EVENT_RESET:
|
||||||
|
sys_set_usb_configured(false);
|
||||||
|
djtag_on_reset();
|
||||||
|
uart_bridge_on_reset();
|
||||||
|
break;
|
||||||
|
case USBD_EVENT_CONFIGURED:
|
||||||
|
djtag_on_configured();
|
||||||
|
uart_bridge_on_configured();
|
||||||
|
sys_set_usb_configured(true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
board_init();
|
||||||
|
printf("\r\nTang Primer 25K debugger " FW_VERSION_STR " (" __DATE__ " " __TIME__ ")\r\n");
|
||||||
|
|
||||||
|
sys_init();
|
||||||
|
jtag_io_init();
|
||||||
|
uart_bridge_init();
|
||||||
|
|
||||||
|
usb_desc_init(0);
|
||||||
|
djtag_usb_init(0); /* interface 0 */
|
||||||
|
uart_bridge_usb_init(0); /* interfaces 1, 2 */
|
||||||
|
usbd_initialize(0, USB_REG_BASE, usbd_event_handler);
|
||||||
|
|
||||||
|
sys_tick_start();
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
djtag_poll();
|
||||||
|
sys_poll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include "bflb_gpio.h"
|
||||||
|
#include "bflb_timer.h"
|
||||||
|
#include "bflb_mtimer.h"
|
||||||
|
#include "bflb_efuse.h"
|
||||||
|
#include "bflb_irq.h"
|
||||||
|
#include "bl616_hbn.h"
|
||||||
|
#include "usbd_core.h"
|
||||||
|
#include "sys.h"
|
||||||
|
#include "hw.h"
|
||||||
|
#include "jtag_io.h"
|
||||||
|
#include "uart_bridge.h"
|
||||||
|
|
||||||
|
#define TICK_HZ 8000u
|
||||||
|
#define ACTIVITY_BLINK (TICK_HZ / 25) /* 40 ms */
|
||||||
|
#define ISP_REBOOT_DELAY (TICK_HZ / 20) /* let the control transfer finish */
|
||||||
|
|
||||||
|
extern int bl_sys_reset_por(void);
|
||||||
|
|
||||||
|
static struct bflb_device_s *gpio;
|
||||||
|
static struct bflb_device_s *timer0;
|
||||||
|
|
||||||
|
static volatile uint32_t ticks;
|
||||||
|
static volatile uint32_t led_blink_until;
|
||||||
|
static volatile bool usb_configured;
|
||||||
|
static volatile bool isp_requested;
|
||||||
|
static volatile uint32_t isp_request_tick;
|
||||||
|
|
||||||
|
static char serial[17];
|
||||||
|
|
||||||
|
static inline void led_write(bool on)
|
||||||
|
{
|
||||||
|
/* LED5 sits between 3V3 and GPIO20 */
|
||||||
|
if (on) {
|
||||||
|
GPIO_CLR_REG = 1u << PIN_LED;
|
||||||
|
} else {
|
||||||
|
GPIO_SET_REG = 1u << PIN_LED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __attribute__((noreturn)) reboot_to_isp(void)
|
||||||
|
{
|
||||||
|
printf("rebooting into the ROM bootloader\r\n");
|
||||||
|
jtag_io_release();
|
||||||
|
usbd_deinitialize(0); /* soft disconnect, the host sees the detach */
|
||||||
|
bflb_mtimer_delay_ms(100);
|
||||||
|
HBN_Set_User_Boot_Config(1);
|
||||||
|
bl_sys_reset_por();
|
||||||
|
while (1) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sys_init(void)
|
||||||
|
{
|
||||||
|
uint8_t id[8] = { 0 };
|
||||||
|
|
||||||
|
gpio = bflb_device_get_by_name("gpio");
|
||||||
|
|
||||||
|
GPIO_SET_REG = 1u << PIN_LED;
|
||||||
|
bflb_gpio_init(gpio, PIN_LED, GPIO_OUTPUT | GPIO_FLOAT | GPIO_DRV_0);
|
||||||
|
led_write(false);
|
||||||
|
|
||||||
|
bflb_efuse_get_chipid(id);
|
||||||
|
/* same order as the "chip id" printed by the Bouffalo flash tools */
|
||||||
|
snprintf(serial, sizeof(serial), "%02X%02X%02X%02X%02X%02X",
|
||||||
|
id[0], id[1], id[2], id[3], id[4], id[5]);
|
||||||
|
|
||||||
|
/* S3 held while powering up: go straight to the ROM bootloader */
|
||||||
|
bflb_gpio_init(gpio, PIN_BUTTON, GPIO_INPUT | GPIO_PULLUP | GPIO_SMT_EN);
|
||||||
|
bflb_mtimer_delay_ms(5);
|
||||||
|
uint32_t pressed_ms = 0;
|
||||||
|
while (!bflb_gpio_read(gpio, PIN_BUTTON) && pressed_ms < 3000) {
|
||||||
|
bflb_mtimer_delay_ms(10);
|
||||||
|
pressed_ms += 10;
|
||||||
|
led_write((pressed_ms / 100) & 1);
|
||||||
|
}
|
||||||
|
if (pressed_ms >= 100) {
|
||||||
|
reboot_to_isp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void timer_isr(int irq, void *arg)
|
||||||
|
{
|
||||||
|
(void)irq;
|
||||||
|
(void)arg;
|
||||||
|
|
||||||
|
if (!bflb_timer_get_compint_status(timer0, TIMER_COMP_ID_0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bflb_timer_compint_clear(timer0, TIMER_COMP_ID_0);
|
||||||
|
|
||||||
|
uint32_t now = ++ticks;
|
||||||
|
|
||||||
|
uart_bridge_tick();
|
||||||
|
|
||||||
|
if ((int32_t)(now - led_blink_until) < 0) {
|
||||||
|
led_write(!usb_configured);
|
||||||
|
} else {
|
||||||
|
led_write(usb_configured);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sys_tick_start(void)
|
||||||
|
{
|
||||||
|
struct bflb_timer_config_s cfg = {
|
||||||
|
.counter_mode = TIMER_COUNTER_MODE_PROLOAD,
|
||||||
|
.clock_source = TIMER_CLKSRC_XTAL,
|
||||||
|
.clock_div = 25, /* 26 MHz / 26 = 1 MHz */
|
||||||
|
.trigger_comp_id = TIMER_COMP_ID_0,
|
||||||
|
.comp0_val = 1000000u / TICK_HZ - 1,
|
||||||
|
.comp1_val = 0xFFFFFFFF,
|
||||||
|
.comp2_val = 0xFFFFFFFF,
|
||||||
|
.preload_val = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
timer0 = bflb_device_get_by_name("timer0");
|
||||||
|
bflb_timer_init(timer0, &cfg);
|
||||||
|
bflb_irq_attach(timer0->irq_num, timer_isr, NULL);
|
||||||
|
bflb_irq_enable(timer0->irq_num);
|
||||||
|
bflb_timer_start(timer0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sys_poll(void)
|
||||||
|
{
|
||||||
|
if (isp_requested && (ticks - isp_request_tick) >= ISP_REBOOT_DELAY) {
|
||||||
|
reboot_to_isp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sys_set_usb_configured(bool configured)
|
||||||
|
{
|
||||||
|
usb_configured = configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sys_activity(void)
|
||||||
|
{
|
||||||
|
led_blink_until = ticks + ACTIVITY_BLINK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sys_request_isp_reboot(void)
|
||||||
|
{
|
||||||
|
isp_request_tick = ticks;
|
||||||
|
isp_requested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sys_serial(void)
|
||||||
|
{
|
||||||
|
return serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t sys_ticks(void)
|
||||||
|
{
|
||||||
|
return ticks;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#ifndef SYS_H
|
||||||
|
#define SYS_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define FW_VERSION_STR "1.0.0"
|
||||||
|
|
||||||
|
/* LED, S3 button (held at power-up: reboot into the ROM USB bootloader) */
|
||||||
|
void sys_init(void);
|
||||||
|
|
||||||
|
/* 8 kHz housekeeping tick, also drives the UART bridge */
|
||||||
|
void sys_tick_start(void);
|
||||||
|
|
||||||
|
/* main loop housekeeping */
|
||||||
|
void sys_poll(void);
|
||||||
|
|
||||||
|
void sys_set_usb_configured(bool configured);
|
||||||
|
void sys_activity(void);
|
||||||
|
|
||||||
|
/* reboot into the BL616 ROM USB bootloader ("Bouffalo CDC DEMO") a bit later */
|
||||||
|
void sys_request_isp_reboot(void);
|
||||||
|
|
||||||
|
/* chip-unique USB serial number */
|
||||||
|
const char *sys_serial(void);
|
||||||
|
|
||||||
|
uint32_t sys_ticks(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
/*
|
||||||
|
* CDC-ACM <-> UART1 bridge.
|
||||||
|
*
|
||||||
|
* FPGA -> PC: UART1 RX is drained byte by byte by a circular DMA into a
|
||||||
|
* 64 KB ring, so nothing is lost no matter how late the CPU looks at it.
|
||||||
|
* The 8 kHz tick and the IN-complete interrupt move whatever is new to the
|
||||||
|
* CDC IN endpoint. When the host stops reading, the oldest data is dropped.
|
||||||
|
*
|
||||||
|
* PC -> FPGA: OUT packets land in a queue of 512 byte buffers that a second
|
||||||
|
* DMA channel feeds to the UART TX FIFO. When the queue is full the OUT
|
||||||
|
* endpoint is simply not re-armed: the host gets NAKs, nothing is lost.
|
||||||
|
*
|
||||||
|
* All state below is touched only from interrupt context (USB, DMA, timer),
|
||||||
|
* which all run at the same level and therefore never preempt each other.
|
||||||
|
*/
|
||||||
|
#include <string.h>
|
||||||
|
#include "bflb_uart.h"
|
||||||
|
#include "bflb_gpio.h"
|
||||||
|
#include "bflb_dma.h"
|
||||||
|
#include "bflb_clock.h"
|
||||||
|
#include "bflb_irq.h"
|
||||||
|
#include "bl616_glb.h"
|
||||||
|
#include "hardware/uart_reg.h"
|
||||||
|
#include "hardware/dma_reg.h"
|
||||||
|
#include "usbd_core.h"
|
||||||
|
#include "usbd_cdc_acm.h"
|
||||||
|
#include "usb_desc.h"
|
||||||
|
#include "uart_bridge.h"
|
||||||
|
#include "sys.h"
|
||||||
|
#include "hw.h"
|
||||||
|
|
||||||
|
#define RX_RING_SIZE (64u * 1024u) /* power of two */
|
||||||
|
#define RX_RING_MASK (RX_RING_SIZE - 1)
|
||||||
|
#define RX_SEG_SIZE 2048u
|
||||||
|
#define RX_SEG_NUM (RX_RING_SIZE / RX_SEG_SIZE)
|
||||||
|
#define RX_GUARD (8u * 1024u)
|
||||||
|
#define IN_CHUNK_MAX 2048u
|
||||||
|
|
||||||
|
/*
|
||||||
|
* One buffer per USB packet: usbser.sys writes in multiples of the packet
|
||||||
|
* size without a terminating zero length packet, so a read longer than one
|
||||||
|
* packet could wait forever for the end of a transfer.
|
||||||
|
*/
|
||||||
|
#define TX_BUF_NUM 32u
|
||||||
|
#define TX_BUF_SIZE 512u
|
||||||
|
|
||||||
|
/* 160 MHz / 8 = 20 Mbaud is the fastest setting accepted */
|
||||||
|
#define UART_DIV_MIN 8u
|
||||||
|
|
||||||
|
#define TICK_HZ 8000u /* uart_bridge_tick() rate */
|
||||||
|
|
||||||
|
static ATTR_NOCACHE_NOINIT_RAM_SECTION __attribute__((aligned(64))) uint8_t rx_ring[RX_RING_SIZE];
|
||||||
|
static ATTR_NOCACHE_NOINIT_RAM_SECTION __attribute__((aligned(16))) struct bflb_dma_channel_lli_pool_s rx_lli[RX_SEG_NUM];
|
||||||
|
static ATTR_NOCACHE_NOINIT_RAM_SECTION __attribute__((aligned(16))) struct bflb_dma_channel_lli_pool_s tx_lli[1];
|
||||||
|
/* the USB VDMA wants aligned buffers, so IN data goes through a bounce buffer */
|
||||||
|
static USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t in_buf[2][IN_CHUNK_MAX];
|
||||||
|
static uint8_t in_buf_idx;
|
||||||
|
static USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t tx_buf[TX_BUF_NUM][TX_BUF_SIZE];
|
||||||
|
|
||||||
|
static struct bflb_device_s *uart;
|
||||||
|
static struct bflb_device_s *rx_dma;
|
||||||
|
static struct bflb_device_s *tx_dma;
|
||||||
|
static uintptr_t uart_base;
|
||||||
|
static uint32_t uart_clk;
|
||||||
|
static volatile uint32_t *rx_dma_dst;
|
||||||
|
|
||||||
|
/* FPGA -> PC */
|
||||||
|
static uint32_t rx_rd; /* ring offset of the oldest byte not yet sent */
|
||||||
|
static uint32_t rx_last_wr; /* DMA write offset seen by the last update */
|
||||||
|
static uint32_t rx_pending; /* bytes between rx_rd and rx_last_wr */
|
||||||
|
static bool in_busy;
|
||||||
|
static uint16_t in_mps; /* CDC IN/OUT max packet size of the current connection */
|
||||||
|
static uint16_t out_mps;
|
||||||
|
|
||||||
|
/* PC -> FPGA: buffers [tx_rd, tx_wr) are full, tx_wr is the one being filled */
|
||||||
|
static uint16_t tx_len[TX_BUF_NUM];
|
||||||
|
static uint32_t tx_wr, tx_rd;
|
||||||
|
static bool tx_dma_busy;
|
||||||
|
static bool out_armed;
|
||||||
|
|
||||||
|
static bool configured;
|
||||||
|
static bool dtr;
|
||||||
|
static struct cdc_line_coding line_coding = { 115200, 0, 0, 8 };
|
||||||
|
static struct uart_stats stats;
|
||||||
|
|
||||||
|
static uint32_t rx_wd_limit;
|
||||||
|
static uint32_t rx_wd_ticks;
|
||||||
|
static uint32_t rx_wd_bytes;
|
||||||
|
|
||||||
|
static void tx_dma_done(void *arg);
|
||||||
|
|
||||||
|
static void dma_dummy_isr(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- UART */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* (Re)configure UART1 from scratch. The BL616 receiver can lock up for good
|
||||||
|
* (URX busy, no more bytes) when its bit period changes in the middle of an
|
||||||
|
* incoming frame; clearing/setting the enable bits, as the SDK does, does not
|
||||||
|
* get it out. A peripheral reset does, so every real change of the line
|
||||||
|
* parameters goes through one. The DMA channels are left running: they just
|
||||||
|
* wait for the next request.
|
||||||
|
*/
|
||||||
|
static void uart_apply_line_coding(const struct cdc_line_coding *lc)
|
||||||
|
{
|
||||||
|
uint32_t baud = lc->dwDTERate ? lc->dwDTERate : 115200;
|
||||||
|
uint32_t div = (uart_clk + baud / 2) / baud;
|
||||||
|
uint32_t bits = lc->bDataBits;
|
||||||
|
uint32_t tx, rx, fifo;
|
||||||
|
|
||||||
|
if (div < UART_DIV_MIN) {
|
||||||
|
div = UART_DIV_MIN;
|
||||||
|
}
|
||||||
|
if (div > 0x10000) {
|
||||||
|
div = 0x10000;
|
||||||
|
}
|
||||||
|
if (bits < 5 || bits > 8) {
|
||||||
|
bits = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLB_AHB_MCU_Software_Reset(GLB_AHB_MCU_SW_UART1);
|
||||||
|
|
||||||
|
putreg32(((div - 1) << 16) | (div - 1), uart_base + UART_BIT_PRD_OFFSET);
|
||||||
|
putreg32(0, uart_base + UART_DATA_CONFIG_OFFSET);
|
||||||
|
putreg32(0xFFFFFFFF, uart_base + UART_INT_MASK_OFFSET);
|
||||||
|
|
||||||
|
/* DMA requests as soon as there is one byte / one free slot */
|
||||||
|
fifo = getreg32(uart_base + UART_FIFO_CONFIG_1_OFFSET);
|
||||||
|
fifo &= ~(UART_TX_FIFO_TH_MASK | UART_RX_FIFO_TH_MASK);
|
||||||
|
putreg32(fifo, uart_base + UART_FIFO_CONFIG_1_OFFSET);
|
||||||
|
|
||||||
|
tx = getreg32(uart_base + UART_UTX_CONFIG_OFFSET);
|
||||||
|
rx = getreg32(uart_base + UART_URX_CONFIG_OFFSET);
|
||||||
|
tx |= UART_CR_UTX_FRM_EN;
|
||||||
|
tx &= ~(UART_CR_UTX_BIT_CNT_D_MASK | UART_CR_UTX_BIT_CNT_P_MASK |
|
||||||
|
UART_CR_UTX_PRT_EN | UART_CR_UTX_PRT_SEL);
|
||||||
|
rx &= ~(UART_CR_URX_BIT_CNT_D_MASK | UART_CR_URX_PRT_EN | UART_CR_URX_PRT_SEL);
|
||||||
|
|
||||||
|
tx |= (bits - 1) << UART_CR_UTX_BIT_CNT_D_SHIFT;
|
||||||
|
rx |= (bits - 1) << UART_CR_URX_BIT_CNT_D_SHIFT;
|
||||||
|
|
||||||
|
/* stop bits field counts half bits minus one: 1 -> 1, 1.5 -> 2, 2 -> 3 */
|
||||||
|
switch (lc->bCharFormat) {
|
||||||
|
case 1:
|
||||||
|
tx |= UART_STOP_BITS_1_5 << UART_CR_UTX_BIT_CNT_P_SHIFT;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
tx |= UART_STOP_BITS_2 << UART_CR_UTX_BIT_CNT_P_SHIFT;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
tx |= UART_STOP_BITS_1 << UART_CR_UTX_BIT_CNT_P_SHIFT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* mark/space parity is not supported by the hardware: sent as none */
|
||||||
|
if (lc->bParityType == 1) {
|
||||||
|
tx |= UART_CR_UTX_PRT_EN | UART_CR_UTX_PRT_SEL;
|
||||||
|
rx |= UART_CR_URX_PRT_EN | UART_CR_URX_PRT_SEL;
|
||||||
|
} else if (lc->bParityType == 2) {
|
||||||
|
tx |= UART_CR_UTX_PRT_EN;
|
||||||
|
rx |= UART_CR_URX_PRT_EN;
|
||||||
|
}
|
||||||
|
|
||||||
|
putreg32(UART_DMA_TX_EN | UART_DMA_RX_EN, uart_base + UART_FIFO_CONFIG_0_OFFSET);
|
||||||
|
putreg32(tx | UART_CR_UTX_EN, uart_base + UART_UTX_CONFIG_OFFSET);
|
||||||
|
putreg32(rx | UART_CR_URX_EN, uart_base + UART_URX_CONFIG_OFFSET);
|
||||||
|
|
||||||
|
stats.baud_requested = baud;
|
||||||
|
stats.baud_actual = uart_clk / div;
|
||||||
|
|
||||||
|
/* receiver watchdog: 100 ms plus 30 frame times */
|
||||||
|
rx_wd_limit = TICK_HZ / 10 + (TICK_HZ * 300u) / baud;
|
||||||
|
rx_wd_ticks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* receiver busy, yet no byte for a long time: it is stuck, reset it */
|
||||||
|
static void rx_watchdog(void)
|
||||||
|
{
|
||||||
|
if ((getreg32(uart_base + UART_STATUS_OFFSET) & UART_STS_URX_BUS_BUSY) &&
|
||||||
|
stats.rx_bytes == rx_wd_bytes) {
|
||||||
|
if (++rx_wd_ticks > rx_wd_limit) {
|
||||||
|
uart_apply_line_coding(&line_coding);
|
||||||
|
stats.rx_resets++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rx_wd_bytes = stats.rx_bytes;
|
||||||
|
rx_wd_ticks = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void uart_bridge_init(void)
|
||||||
|
{
|
||||||
|
struct bflb_device_s *gpio = bflb_device_get_by_name("gpio");
|
||||||
|
struct bflb_uart_config_s cfg = {
|
||||||
|
.baudrate = 115200,
|
||||||
|
.direction = UART_DIRECTION_TXRX,
|
||||||
|
.data_bits = UART_DATA_BITS_8,
|
||||||
|
.stop_bits = UART_STOP_BITS_1,
|
||||||
|
.parity = UART_PARITY_NONE,
|
||||||
|
.bit_order = UART_LSB_FIRST,
|
||||||
|
.flow_ctrl = 0,
|
||||||
|
/* DMA requests as soon as there is one byte / one free slot */
|
||||||
|
.tx_fifo_threshold = 0,
|
||||||
|
.rx_fifo_threshold = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
bflb_gpio_uart_init(gpio, PIN_UART_TX, GPIO_UART_FUNC_UART1_TX);
|
||||||
|
bflb_gpio_uart_init(gpio, PIN_UART_RX, GPIO_UART_FUNC_UART1_RX);
|
||||||
|
|
||||||
|
uart = bflb_device_get_by_name("uart1");
|
||||||
|
bflb_uart_init(uart, &cfg);
|
||||||
|
uart_base = uart->reg_base;
|
||||||
|
uart_clk = bflb_clk_get_peripheral_clock(BFLB_DEVICE_TYPE_UART, 1);
|
||||||
|
uart_apply_line_coding(&line_coding);
|
||||||
|
|
||||||
|
/* RX: endless DMA into the ring */
|
||||||
|
struct bflb_dma_channel_config_s rx_cfg = {
|
||||||
|
.direction = DMA_PERIPH_TO_MEMORY,
|
||||||
|
.src_req = DMA_REQUEST_UART1_RX,
|
||||||
|
.dst_req = DMA_REQUEST_NONE,
|
||||||
|
.src_addr_inc = DMA_ADDR_INCREMENT_DISABLE,
|
||||||
|
.dst_addr_inc = DMA_ADDR_INCREMENT_ENABLE,
|
||||||
|
.src_burst_count = DMA_BURST_INCR1,
|
||||||
|
.dst_burst_count = DMA_BURST_INCR1,
|
||||||
|
.src_width = DMA_DATA_WIDTH_8BIT,
|
||||||
|
.dst_width = DMA_DATA_WIDTH_8BIT,
|
||||||
|
};
|
||||||
|
struct bflb_dma_channel_lli_transfer_s seg[RX_SEG_NUM];
|
||||||
|
|
||||||
|
rx_dma = bflb_device_get_by_name("dma0_ch0");
|
||||||
|
bflb_dma_channel_init(rx_dma, &rx_cfg);
|
||||||
|
/* the shared DMA ISR calls every channel's handler without a NULL check */
|
||||||
|
bflb_dma_channel_irq_attach(rx_dma, dma_dummy_isr, NULL);
|
||||||
|
bflb_dma_channel_tcint_mask(rx_dma, true);
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < RX_SEG_NUM; i++) {
|
||||||
|
seg[i].src_addr = DMA_ADDR_UART1_RDR;
|
||||||
|
seg[i].dst_addr = (uint32_t)(uintptr_t)&rx_ring[i * RX_SEG_SIZE];
|
||||||
|
seg[i].nbytes = RX_SEG_SIZE;
|
||||||
|
}
|
||||||
|
int used = bflb_dma_channel_lli_reload(rx_dma, rx_lli, RX_SEG_NUM, seg, RX_SEG_NUM);
|
||||||
|
bflb_dma_channel_lli_link_head(rx_dma, rx_lli, used);
|
||||||
|
rx_dma_dst = (volatile uint32_t *)(rx_dma->reg_base + DMA_CxDSTADDR_OFFSET);
|
||||||
|
rx_rd = rx_last_wr = 0;
|
||||||
|
|
||||||
|
/* TX: one DMA transfer per filled buffer */
|
||||||
|
struct bflb_dma_channel_config_s tx_cfg = {
|
||||||
|
.direction = DMA_MEMORY_TO_PERIPH,
|
||||||
|
.src_req = DMA_REQUEST_NONE,
|
||||||
|
.dst_req = DMA_REQUEST_UART1_TX,
|
||||||
|
.src_addr_inc = DMA_ADDR_INCREMENT_ENABLE,
|
||||||
|
.dst_addr_inc = DMA_ADDR_INCREMENT_DISABLE,
|
||||||
|
.src_burst_count = DMA_BURST_INCR1,
|
||||||
|
.dst_burst_count = DMA_BURST_INCR1,
|
||||||
|
.src_width = DMA_DATA_WIDTH_8BIT,
|
||||||
|
.dst_width = DMA_DATA_WIDTH_8BIT,
|
||||||
|
};
|
||||||
|
tx_dma = bflb_device_get_by_name("dma0_ch1");
|
||||||
|
bflb_dma_channel_init(tx_dma, &tx_cfg);
|
||||||
|
bflb_dma_channel_irq_attach(tx_dma, tx_dma_done, NULL);
|
||||||
|
|
||||||
|
bflb_uart_link_rxdma(uart, true);
|
||||||
|
bflb_uart_link_txdma(uart, true);
|
||||||
|
bflb_dma_channel_start(rx_dma);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------- FPGA -> PC */
|
||||||
|
|
||||||
|
static inline uint32_t rx_wr_offset(void)
|
||||||
|
{
|
||||||
|
uint32_t off = *rx_dma_dst - (uint32_t)(uintptr_t)rx_ring;
|
||||||
|
|
||||||
|
/* right at the end of the last segment the address points past the ring */
|
||||||
|
return (off >= RX_RING_SIZE) ? 0 : off;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rx_update(void)
|
||||||
|
{
|
||||||
|
uint32_t wr = rx_wr_offset();
|
||||||
|
uint32_t delta = (wr - rx_last_wr) & RX_RING_MASK;
|
||||||
|
|
||||||
|
if (delta == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rx_last_wr = wr;
|
||||||
|
rx_pending += delta;
|
||||||
|
stats.rx_bytes += delta;
|
||||||
|
|
||||||
|
if (rx_pending > RX_RING_SIZE - RX_GUARD) {
|
||||||
|
/* host is not reading: keep the newest half */
|
||||||
|
uint32_t drop = rx_pending - RX_RING_SIZE / 2;
|
||||||
|
rx_rd = (rx_rd + drop) & RX_RING_MASK;
|
||||||
|
rx_pending -= drop;
|
||||||
|
stats.rx_dropped += drop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rx_submit(void)
|
||||||
|
{
|
||||||
|
if (in_busy || !configured || rx_pending == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t n = (rx_pending < IN_CHUNK_MAX) ? rx_pending : IN_CHUNK_MAX;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Never end a transfer on a packet boundary: the host only hands data to
|
||||||
|
* the application after a short packet. The usual cure, a zero length
|
||||||
|
* packet, is not reliable on BL616 (requested while the last data packet
|
||||||
|
* still sits in the FIFO it gets lost), so keep one byte for the next
|
||||||
|
* transfer instead.
|
||||||
|
*/
|
||||||
|
if (in_mps && (n % in_mps) == 0) {
|
||||||
|
n--;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t first = RX_RING_SIZE - rx_rd;
|
||||||
|
|
||||||
|
if (first > n) {
|
||||||
|
first = n;
|
||||||
|
}
|
||||||
|
/* alternate buffers: never touch the one the last transfer came from */
|
||||||
|
uint8_t *buf = in_buf[in_buf_idx];
|
||||||
|
in_buf_idx ^= 1;
|
||||||
|
|
||||||
|
memcpy(buf, &rx_ring[rx_rd], first);
|
||||||
|
if (n > first) {
|
||||||
|
memcpy(buf + first, &rx_ring[0], n - first);
|
||||||
|
}
|
||||||
|
rx_rd = (rx_rd + n) & RX_RING_MASK;
|
||||||
|
rx_pending -= n;
|
||||||
|
|
||||||
|
in_busy = true;
|
||||||
|
usbd_ep_start_write(0, CDC_IN_EP, buf, n);
|
||||||
|
sys_activity();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cdc_in_cb(uint8_t busid, uint8_t ep, uint32_t nbytes)
|
||||||
|
{
|
||||||
|
(void)busid;
|
||||||
|
(void)ep;
|
||||||
|
(void)nbytes;
|
||||||
|
|
||||||
|
in_busy = false;
|
||||||
|
rx_update();
|
||||||
|
rx_submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uart_bridge_tick(void)
|
||||||
|
{
|
||||||
|
rx_update();
|
||||||
|
rx_watchdog();
|
||||||
|
rx_submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------- PC -> FPGA */
|
||||||
|
|
||||||
|
static void out_arm(void)
|
||||||
|
{
|
||||||
|
if (out_armed || !configured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((tx_wr - tx_rd) < TX_BUF_NUM) {
|
||||||
|
out_armed = true;
|
||||||
|
usbd_ep_start_read(0, CDC_OUT_EP, tx_buf[tx_wr % TX_BUF_NUM], out_mps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tx_kick(void)
|
||||||
|
{
|
||||||
|
if (tx_dma_busy || tx_rd == tx_wr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t i = tx_rd % TX_BUF_NUM;
|
||||||
|
struct bflb_dma_channel_lli_transfer_s tr = {
|
||||||
|
.src_addr = (uint32_t)(uintptr_t)tx_buf[i],
|
||||||
|
.dst_addr = DMA_ADDR_UART1_TDR,
|
||||||
|
.nbytes = tx_len[i],
|
||||||
|
};
|
||||||
|
|
||||||
|
tx_dma_busy = true;
|
||||||
|
bflb_dma_channel_lli_reload(tx_dma, tx_lli, 1, &tr, 1);
|
||||||
|
bflb_dma_channel_start(tx_dma);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tx_dma_done(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
tx_dma_busy = false;
|
||||||
|
tx_rd++;
|
||||||
|
tx_kick();
|
||||||
|
out_arm();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cdc_out_cb(uint8_t busid, uint8_t ep, uint32_t nbytes)
|
||||||
|
{
|
||||||
|
(void)busid;
|
||||||
|
(void)ep;
|
||||||
|
|
||||||
|
if (nbytes == 0) {
|
||||||
|
/* zero length packet: the BL616 driver keeps the read armed */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out_armed = false;
|
||||||
|
tx_len[tx_wr % TX_BUF_NUM] = (uint16_t)nbytes;
|
||||||
|
tx_wr++;
|
||||||
|
stats.tx_bytes += nbytes;
|
||||||
|
tx_kick();
|
||||||
|
sys_activity();
|
||||||
|
out_arm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------- CDC control */
|
||||||
|
|
||||||
|
void usbd_cdc_acm_set_line_coding(uint8_t busid, uint8_t intf, struct cdc_line_coding *lc)
|
||||||
|
{
|
||||||
|
(void)busid;
|
||||||
|
(void)intf;
|
||||||
|
|
||||||
|
if (lc->dwDTERate == UART_MAGIC_ISP_BAUD) {
|
||||||
|
/* "1200 bps touch" style: firmware update without opening the case */
|
||||||
|
sys_request_isp_reboot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* terminals send the same settings again and again: only act on changes */
|
||||||
|
if (memcmp(lc, &line_coding, sizeof(line_coding)) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
line_coding = *lc;
|
||||||
|
uart_apply_line_coding(&line_coding);
|
||||||
|
}
|
||||||
|
|
||||||
|
void usbd_cdc_acm_get_line_coding(uint8_t busid, uint8_t intf, struct cdc_line_coding *lc)
|
||||||
|
{
|
||||||
|
(void)busid;
|
||||||
|
(void)intf;
|
||||||
|
*lc = line_coding;
|
||||||
|
}
|
||||||
|
|
||||||
|
void usbd_cdc_acm_set_dtr(uint8_t busid, uint8_t intf, bool state)
|
||||||
|
{
|
||||||
|
(void)busid;
|
||||||
|
(void)intf;
|
||||||
|
|
||||||
|
if (state && !dtr) {
|
||||||
|
/* port just opened: drop what piled up while nobody listened */
|
||||||
|
rx_update();
|
||||||
|
stats.rx_dropped += rx_pending;
|
||||||
|
rx_rd = rx_last_wr;
|
||||||
|
rx_pending = 0;
|
||||||
|
}
|
||||||
|
dtr = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
void usbd_cdc_acm_set_rts(uint8_t busid, uint8_t intf, bool state)
|
||||||
|
{
|
||||||
|
(void)busid;
|
||||||
|
(void)intf;
|
||||||
|
(void)state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- USB */
|
||||||
|
|
||||||
|
static struct usbd_interface cdc_intf_comm;
|
||||||
|
static struct usbd_interface cdc_intf_data;
|
||||||
|
|
||||||
|
static struct usbd_endpoint cdc_in_ep = {
|
||||||
|
.ep_addr = CDC_IN_EP,
|
||||||
|
.ep_cb = cdc_in_cb,
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct usbd_endpoint cdc_out_ep = {
|
||||||
|
.ep_addr = CDC_OUT_EP,
|
||||||
|
.ep_cb = cdc_out_cb,
|
||||||
|
};
|
||||||
|
|
||||||
|
void uart_bridge_usb_init(uint8_t busid)
|
||||||
|
{
|
||||||
|
usbd_add_interface(busid, usbd_cdc_acm_init_intf(busid, &cdc_intf_comm));
|
||||||
|
usbd_add_interface(busid, usbd_cdc_acm_init_intf(busid, &cdc_intf_data));
|
||||||
|
usbd_add_endpoint(busid, &cdc_in_ep);
|
||||||
|
usbd_add_endpoint(busid, &cdc_out_ep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uart_bridge_on_reset(void)
|
||||||
|
{
|
||||||
|
configured = false;
|
||||||
|
in_busy = false;
|
||||||
|
out_armed = false;
|
||||||
|
dtr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uart_bridge_on_configured(void)
|
||||||
|
{
|
||||||
|
configured = true;
|
||||||
|
in_busy = false;
|
||||||
|
in_mps = usbd_get_ep_mps(0, CDC_IN_EP);
|
||||||
|
out_mps = usbd_get_ep_mps(0, CDC_OUT_EP);
|
||||||
|
out_armed = false;
|
||||||
|
out_arm();
|
||||||
|
rx_submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uart_bridge_get_stats(struct uart_stats *st)
|
||||||
|
{
|
||||||
|
uintptr_t flag = bflb_irq_save();
|
||||||
|
*st = stats;
|
||||||
|
bflb_irq_restore(flag);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#ifndef UART_BRIDGE_H
|
||||||
|
#define UART_BRIDGE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* opening the serial port at this baud rate reboots into the ROM bootloader */
|
||||||
|
#define UART_MAGIC_ISP_BAUD 1234
|
||||||
|
|
||||||
|
struct uart_stats {
|
||||||
|
uint32_t baud_requested;
|
||||||
|
uint32_t baud_actual;
|
||||||
|
uint32_t rx_bytes; /* FPGA -> PC, received by the UART */
|
||||||
|
uint32_t rx_dropped; /* lost because the host did not read */
|
||||||
|
uint32_t tx_bytes; /* PC -> FPGA */
|
||||||
|
uint32_t rx_resets; /* receiver lock-ups recovered by the watchdog */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* UART1 + DMA; starts receiving immediately */
|
||||||
|
void uart_bridge_init(void);
|
||||||
|
|
||||||
|
/* registers the CDC-ACM interfaces/endpoints, call before usbd_initialize */
|
||||||
|
void uart_bridge_usb_init(uint8_t busid);
|
||||||
|
|
||||||
|
void uart_bridge_on_reset(void);
|
||||||
|
void uart_bridge_on_configured(void);
|
||||||
|
|
||||||
|
/* called from the 8 kHz tick interrupt */
|
||||||
|
void uart_bridge_tick(void);
|
||||||
|
|
||||||
|
void uart_bridge_get_stats(struct uart_stats *st);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* CherryUSB configuration for the Tang Primer 25K BL616 debugger firmware.
|
||||||
|
*/
|
||||||
|
#ifndef CHERRYUSB_CONFIG_H
|
||||||
|
#define CHERRYUSB_CONFIG_H
|
||||||
|
|
||||||
|
/* ================ USB common Configuration ================ */
|
||||||
|
|
||||||
|
#define CONFIG_USB_PRINTF(...) printf(__VA_ARGS__)
|
||||||
|
|
||||||
|
#ifndef CONFIG_USB_DBG_LEVEL
|
||||||
|
#define CONFIG_USB_DBG_LEVEL USB_DBG_WARNING
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CONFIG_USB_ALIGN_SIZE
|
||||||
|
#define CONFIG_USB_ALIGN_SIZE 4
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* USB VDMA does not go through the D-cache */
|
||||||
|
#define USB_NOCACHE_RAM_SECTION __attribute__((section(".noncacheable")))
|
||||||
|
|
||||||
|
/* ================= USB Device Stack Configuration ================ */
|
||||||
|
|
||||||
|
#ifndef CONFIG_USBDEV_REQUEST_BUFFER_LEN
|
||||||
|
#define CONFIG_USBDEV_REQUEST_BUFFER_LEN 512
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define CONFIG_USBDEV_ADVANCE_DESC
|
||||||
|
|
||||||
|
/* ================ USB Device Port Configuration ================ */
|
||||||
|
|
||||||
|
#ifndef CONFIG_USBDEV_MAX_BUS
|
||||||
|
#define CONFIG_USBDEV_MAX_BUS 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* EP0 + EP1..EP4 have FIFOs on BL616; EP5 is used as a FIFO-less CDC notify EP */
|
||||||
|
#ifndef CONFIG_USBDEV_EP_NUM
|
||||||
|
#define CONFIG_USBDEV_EP_NUM 6
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Host-side defines: bflb_usb_v2.c includes usbh_core.h even in device-only
|
||||||
|
* builds, so these have to exist.
|
||||||
|
*/
|
||||||
|
#define CONFIG_USB_EHCI_HCOR_RESERVED_DISABLE
|
||||||
|
#define CONFIG_USBHOST_MAX_RHPORTS 1
|
||||||
|
#define CONFIG_USBHOST_MAX_EXTHUBS 1
|
||||||
|
#define CONFIG_USBHOST_MAX_EHPORTS 4
|
||||||
|
#define CONFIG_USBHOST_MAX_INTERFACES 8
|
||||||
|
#define CONFIG_USBHOST_MAX_INTF_ALTSETTINGS 8
|
||||||
|
#define CONFIG_USBHOST_MAX_ENDPOINTS 4
|
||||||
|
#define CONFIG_USBHOST_DEV_NAMELEN 16
|
||||||
|
#define CONFIG_USBHOST_MAX_BUS 1
|
||||||
|
#define CONFIG_USBHOST_PIPE_NUM 10
|
||||||
|
#define CONFIG_USB_EHCI_FRAME_LIST_SIZE 1024
|
||||||
|
#define CONFIG_USB_EHCI_QH_NUM CONFIG_USBHOST_PIPE_NUM
|
||||||
|
#define CONFIG_USB_EHCI_QTD_NUM 3
|
||||||
|
#define CONFIG_USB_EHCI_ITD_NUM 20
|
||||||
|
|
||||||
|
#endif
|
||||||
+190
@@ -0,0 +1,190 @@
|
|||||||
|
/*
|
||||||
|
* USB descriptors: composite device
|
||||||
|
* interface 0 vendor class, DirtyJTAG (WinUSB bound via MS OS 2.0)
|
||||||
|
* interfaces 1-2 CDC-ACM UART bridge (IAD)
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "usbd_core.h"
|
||||||
|
#include "usb_cdc.h"
|
||||||
|
#include "usb_desc.h"
|
||||||
|
#include "sys.h"
|
||||||
|
|
||||||
|
#define STR_IDX_JTAG 4
|
||||||
|
#define STR_IDX_UART 5
|
||||||
|
|
||||||
|
#define CONFIG_TOTAL_LEN (9 + (9 + 7 + 7) + 8 + (9 + 5 + 5 + 4 + 5 + 7) + (9 + 7 + 7))
|
||||||
|
|
||||||
|
#define MAX_POWER_MA 500
|
||||||
|
|
||||||
|
/* Interval: 2^(n-1) microframes at HS, n ms at FS. The EP never sends anyway. */
|
||||||
|
#define CDC_INT_INTERVAL 0x0A
|
||||||
|
|
||||||
|
#define CONFIG_BODY(mps) \
|
||||||
|
/* interface 0: DirtyJTAG */ \
|
||||||
|
USB_INTERFACE_DESCRIPTOR_INIT(JTAG_INTF, 0x00, 0x02, 0xFF, 0x00, 0x00, STR_IDX_JTAG), \
|
||||||
|
USB_ENDPOINT_DESCRIPTOR_INIT(JTAG_OUT_EP, USB_ENDPOINT_TYPE_BULK, mps, 0x00), \
|
||||||
|
USB_ENDPOINT_DESCRIPTOR_INIT(JTAG_IN_EP, USB_ENDPOINT_TYPE_BULK, mps, 0x00), \
|
||||||
|
/* IAD for the CDC function */ \
|
||||||
|
0x08, USB_DESCRIPTOR_TYPE_INTERFACE_ASSOCIATION, CDC_COMM_INTF, 0x02, \
|
||||||
|
USB_DEVICE_CLASS_CDC, CDC_ABSTRACT_CONTROL_MODEL, CDC_COMMON_PROTOCOL_NONE, STR_IDX_UART, \
|
||||||
|
/* interface 1: CDC communication */ \
|
||||||
|
USB_INTERFACE_DESCRIPTOR_INIT(CDC_COMM_INTF, 0x00, 0x01, USB_DEVICE_CLASS_CDC, \
|
||||||
|
CDC_ABSTRACT_CONTROL_MODEL, CDC_COMMON_PROTOCOL_NONE, STR_IDX_UART), \
|
||||||
|
0x05, CDC_CS_INTERFACE, CDC_FUNC_DESC_HEADER, WBVAL(CDC_V1_10), \
|
||||||
|
0x05, CDC_CS_INTERFACE, CDC_FUNC_DESC_CALL_MANAGEMENT, 0x00, CDC_DATA_INTF, \
|
||||||
|
0x04, CDC_CS_INTERFACE, CDC_FUNC_DESC_ABSTRACT_CONTROL_MANAGEMENT, 0x02, \
|
||||||
|
0x05, CDC_CS_INTERFACE, CDC_FUNC_DESC_UNION, CDC_COMM_INTF, CDC_DATA_INTF, \
|
||||||
|
USB_ENDPOINT_DESCRIPTOR_INIT(CDC_INT_EP, USB_ENDPOINT_TYPE_INTERRUPT, 0x0008, CDC_INT_INTERVAL), \
|
||||||
|
/* interface 2: CDC data */ \
|
||||||
|
USB_INTERFACE_DESCRIPTOR_INIT(CDC_DATA_INTF, 0x00, 0x02, CDC_DATA_INTERFACE_CLASS, 0x00, 0x00, 0), \
|
||||||
|
USB_ENDPOINT_DESCRIPTOR_INIT(CDC_OUT_EP, USB_ENDPOINT_TYPE_BULK, mps, 0x00), \
|
||||||
|
USB_ENDPOINT_DESCRIPTOR_INIT(CDC_IN_EP, USB_ENDPOINT_TYPE_BULK, mps, 0x00)
|
||||||
|
|
||||||
|
static const uint8_t device_descriptor[] = {
|
||||||
|
/* bcdUSB 2.10 so that Windows asks for the BOS / MS OS 2.0 descriptors */
|
||||||
|
USB_DEVICE_DESCRIPTOR_INIT(USB_2_1, 0xEF, 0x02, 0x01, USBD_VID, USBD_PID, USBD_BCD_DEVICE, 0x01),
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t config_hs[] = {
|
||||||
|
USB_CONFIG_DESCRIPTOR_INIT(CONFIG_TOTAL_LEN, 0x03, 0x01, USB_CONFIG_BUS_POWERED, MAX_POWER_MA / 2),
|
||||||
|
CONFIG_BODY(512),
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t config_fs[] = {
|
||||||
|
USB_CONFIG_DESCRIPTOR_INIT(CONFIG_TOTAL_LEN, 0x03, 0x01, USB_CONFIG_BUS_POWERED, MAX_POWER_MA / 2),
|
||||||
|
CONFIG_BODY(64),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* "other speed" = what the device would look like at the speed it is not running at */
|
||||||
|
static const uint8_t other_speed_of_hs[] = {
|
||||||
|
USB_OTHER_SPEED_CONFIG_DESCRIPTOR_INIT(CONFIG_TOTAL_LEN, 0x03, 0x01, USB_CONFIG_BUS_POWERED, MAX_POWER_MA / 2),
|
||||||
|
CONFIG_BODY(64),
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t other_speed_of_fs[] = {
|
||||||
|
USB_OTHER_SPEED_CONFIG_DESCRIPTOR_INIT(CONFIG_TOTAL_LEN, 0x03, 0x01, USB_CONFIG_BUS_POWERED, MAX_POWER_MA / 2),
|
||||||
|
CONFIG_BODY(512),
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t device_qualifier[] = {
|
||||||
|
USB_DEVICE_QUALIFIER_DESCRIPTOR_INIT(USB_2_0, 0xEF, 0x02, 0x01, 0x01),
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* MS OS 2.0 descriptor set: binds WinUSB to interface 0 only, so libusb
|
||||||
|
* (openFPGALoader) can open it without Zadig. The CDC function keeps the
|
||||||
|
* inbox usbser.sys driver.
|
||||||
|
*/
|
||||||
|
#define MSOS20_TOTAL_LEN 178
|
||||||
|
|
||||||
|
static const uint8_t msos20_descriptor_set[] = {
|
||||||
|
/* set header: wLength, wDescriptorType, dwWindowsVersion (8.1), wTotalLength */
|
||||||
|
0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, WBVAL(MSOS20_TOTAL_LEN),
|
||||||
|
/* configuration subset header: configuration index 0 */
|
||||||
|
0x08, 0x00, 0x01, 0x00, 0x00, 0x00, WBVAL(MSOS20_TOTAL_LEN - 10),
|
||||||
|
/* function subset header: first interface 0 */
|
||||||
|
0x08, 0x00, 0x02, 0x00, JTAG_INTF, 0x00, WBVAL(MSOS20_TOTAL_LEN - 10 - 8),
|
||||||
|
/* compatible ID "WINUSB" */
|
||||||
|
0x14, 0x00, 0x03, 0x00,
|
||||||
|
'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
/* registry property: DeviceInterfaceGUIDs, REG_MULTI_SZ */
|
||||||
|
0x84, 0x00, 0x04, 0x00, 0x07, 0x00, 0x2A, 0x00,
|
||||||
|
'D', 0, 'e', 0, 'v', 0, 'i', 0, 'c', 0, 'e', 0, 'I', 0, 'n', 0, 't', 0, 'e', 0, 'r', 0,
|
||||||
|
'f', 0, 'a', 0, 'c', 0, 'e', 0, 'G', 0, 'U', 0, 'I', 0, 'D', 0, 's', 0, 0, 0,
|
||||||
|
0x50, 0x00,
|
||||||
|
'{', 0, '9', 0, '9', 0, '0', 0, 'F', 0, 'A', 0, '7', 0, '0', 0, '5', 0, '-', 0,
|
||||||
|
'4', 0, '1', 0, 'C', 0, 'A', 0, '-', 0, '4', 0, '6', 0, '0', 0, 'E', 0, '-', 0,
|
||||||
|
'B', 0, '1', 0, 'A', 0, '4', 0, '-', 0, '6', 0, '1', 0, 'C', 0, '8', 0, '9', 0,
|
||||||
|
'1', 0, '5', 0, 'C', 0, 'B', 0, '7', 0, '5', 0, '5', 0, '}', 0, 0, 0, 0, 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t bos_descriptor[] = {
|
||||||
|
/* BOS header */
|
||||||
|
0x05, 0x0F, WBVAL(5 + 28), 0x01,
|
||||||
|
/* platform capability: MS OS 2.0, UUID D8DD60DF-4589-4CC7-9CD2-659D9E648A9F */
|
||||||
|
0x1C, 0x10, 0x05, 0x00,
|
||||||
|
0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C,
|
||||||
|
0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F,
|
||||||
|
0x00, 0x00, 0x03, 0x06, /* dwWindowsVersion 8.1 */
|
||||||
|
WBVAL(MSOS20_TOTAL_LEN),
|
||||||
|
MS_VENDOR_CODE,
|
||||||
|
0x00, /* bAltEnumCode */
|
||||||
|
};
|
||||||
|
|
||||||
|
_Static_assert(sizeof(config_hs) == CONFIG_TOTAL_LEN, "config descriptor length");
|
||||||
|
_Static_assert(sizeof(other_speed_of_hs) == CONFIG_TOTAL_LEN, "other speed descriptor length");
|
||||||
|
_Static_assert(sizeof(msos20_descriptor_set) == MSOS20_TOTAL_LEN, "MS OS 2.0 descriptor length");
|
||||||
|
_Static_assert(sizeof(bos_descriptor) == 5 + 28, "BOS descriptor length");
|
||||||
|
|
||||||
|
static const char langid_string[] = { 0x09, 0x04 };
|
||||||
|
|
||||||
|
static const uint8_t *device_descriptor_callback(uint8_t speed)
|
||||||
|
{
|
||||||
|
(void)speed;
|
||||||
|
return device_descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const uint8_t *config_descriptor_callback(uint8_t speed)
|
||||||
|
{
|
||||||
|
return (speed == USB_SPEED_HIGH) ? config_hs : config_fs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const uint8_t *device_quality_descriptor_callback(uint8_t speed)
|
||||||
|
{
|
||||||
|
(void)speed;
|
||||||
|
return device_qualifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const uint8_t *other_speed_descriptor_callback(uint8_t speed)
|
||||||
|
{
|
||||||
|
return (speed == USB_SPEED_HIGH) ? other_speed_of_hs : other_speed_of_fs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *string_descriptor_callback(uint8_t speed, uint8_t index)
|
||||||
|
{
|
||||||
|
(void)speed;
|
||||||
|
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
return langid_string;
|
||||||
|
case 1:
|
||||||
|
return "OpenTang";
|
||||||
|
case 2:
|
||||||
|
return "Tang Primer 25K Debugger (DirtyJTAG + UART)";
|
||||||
|
case 3:
|
||||||
|
return sys_serial();
|
||||||
|
case STR_IDX_JTAG:
|
||||||
|
return "DirtyJTAG";
|
||||||
|
case STR_IDX_UART:
|
||||||
|
return "Tang UART";
|
||||||
|
default:
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct usb_msosv2_descriptor msosv2_descriptor = {
|
||||||
|
.compat_id = msos20_descriptor_set,
|
||||||
|
.compat_id_len = sizeof(msos20_descriptor_set),
|
||||||
|
.vendor_code = MS_VENDOR_CODE,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const struct usb_bos_descriptor bos = {
|
||||||
|
.string = bos_descriptor,
|
||||||
|
.string_len = sizeof(bos_descriptor),
|
||||||
|
};
|
||||||
|
|
||||||
|
static const struct usb_descriptor composite_descriptor = {
|
||||||
|
.device_descriptor_callback = device_descriptor_callback,
|
||||||
|
.config_descriptor_callback = config_descriptor_callback,
|
||||||
|
.device_quality_descriptor_callback = device_quality_descriptor_callback,
|
||||||
|
.other_speed_descriptor_callback = other_speed_descriptor_callback,
|
||||||
|
.string_descriptor_callback = string_descriptor_callback,
|
||||||
|
.msosv2_descriptor = &msosv2_descriptor,
|
||||||
|
.bos_descriptor = &bos,
|
||||||
|
};
|
||||||
|
|
||||||
|
void usb_desc_init(uint8_t busid)
|
||||||
|
{
|
||||||
|
usbd_desc_register(busid, &composite_descriptor);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#ifndef USB_DESC_H
|
||||||
|
#define USB_DESC_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* pid.codes allocation of the DirtyJTAG project, what openFPGALoader looks for */
|
||||||
|
#define USBD_VID 0x1209
|
||||||
|
#define USBD_PID 0xC0CA
|
||||||
|
#define USBD_BCD_DEVICE 0x0100 /* firmware 1.00 */
|
||||||
|
|
||||||
|
#define JTAG_INTF 0
|
||||||
|
#define CDC_COMM_INTF 1
|
||||||
|
#define CDC_DATA_INTF 2
|
||||||
|
|
||||||
|
/*
|
||||||
|
* BL616 has one 512 byte FIFO per endpoint number 1..4, shared by IN and OUT,
|
||||||
|
* so every bulk endpoint gets its own number. EP5 has no FIFO: it only NAKs,
|
||||||
|
* which is fine for the CDC notification endpoint that is never written.
|
||||||
|
* openFPGALoader hardcodes 0x01/0x82 on interface 0.
|
||||||
|
*/
|
||||||
|
#define JTAG_OUT_EP 0x01
|
||||||
|
#define JTAG_IN_EP 0x82
|
||||||
|
#define CDC_IN_EP 0x83
|
||||||
|
#define CDC_OUT_EP 0x04
|
||||||
|
#define CDC_INT_EP 0x85
|
||||||
|
|
||||||
|
/* vendor requests */
|
||||||
|
#define MS_VENDOR_CODE 0x20 /* MS OS 2.0 descriptor set */
|
||||||
|
#define VREQ_REBOOT_ISP 0x41 /* OUT, no data: reboot into the BL616 ROM USB bootloader */
|
||||||
|
#define VREQ_GET_INFO 0x42 /* IN: struct fw_info */
|
||||||
|
#define VREQ_PEEK 0x43 /* IN: 4 bytes read from register (wIndex << 16 | wValue) */
|
||||||
|
#define VREQ_POKE 0x44 /* OUT: 4 bytes written to register (wIndex << 16 | wValue) */
|
||||||
|
|
||||||
|
void usb_desc_init(uint8_t busid);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
UART bridge test with fpga_test/loopback loaded into the FPGA (uart_tx = uart_rx).
|
||||||
|
|
||||||
|
Writes random data at several baud rates and checks that exactly the same
|
||||||
|
bytes come back; prints the sustained full-duplex throughput.
|
||||||
|
|
||||||
|
python tests/uart_loopback_test.py --port COM12
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import serial
|
||||||
|
|
||||||
|
BAUDS = [9600, 115200, 460800, 921600, 1000000, 2000000, 3000000, 4000000,
|
||||||
|
5000000, 6000000, 8000000, 10000000, 12000000]
|
||||||
|
|
||||||
|
|
||||||
|
def run(port, baud, nbytes, fmt):
|
||||||
|
data = os.urandom(nbytes)
|
||||||
|
rx = bytearray()
|
||||||
|
bytesize, parity, stopbits = fmt
|
||||||
|
with serial.Serial(port, baud, bytesize=bytesize, parity=parity,
|
||||||
|
stopbits=stopbits, timeout=0.05) as s:
|
||||||
|
if bytesize < 8:
|
||||||
|
data = bytes(b & ((1 << bytesize) - 1) for b in data)
|
||||||
|
time.sleep(0.05)
|
||||||
|
s.reset_input_buffer()
|
||||||
|
done = threading.Event()
|
||||||
|
|
||||||
|
def reader():
|
||||||
|
idle_since = None
|
||||||
|
while len(rx) < len(data):
|
||||||
|
chunk = s.read(65536)
|
||||||
|
if chunk:
|
||||||
|
rx.extend(chunk)
|
||||||
|
idle_since = None
|
||||||
|
elif done.is_set():
|
||||||
|
idle_since = idle_since or time.time()
|
||||||
|
if time.time() - idle_since > 0.5:
|
||||||
|
break
|
||||||
|
|
||||||
|
t = threading.Thread(target=reader)
|
||||||
|
t0 = time.time()
|
||||||
|
t.start()
|
||||||
|
for i in range(0, len(data), 4096):
|
||||||
|
s.write(data[i:i + 4096])
|
||||||
|
s.flush()
|
||||||
|
done.set()
|
||||||
|
t.join(timeout=60 + nbytes * 12 / baud)
|
||||||
|
dt = time.time() - t0
|
||||||
|
ok = bytes(rx) == data
|
||||||
|
first_bad = next((i for i, (a, b) in enumerate(zip(rx, data)) if a != b), None)
|
||||||
|
return ok, len(rx), dt, first_bad
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--port', required=True)
|
||||||
|
ap.add_argument('--bytes', type=int, default=0, help='bytes per test (default: ~1 s of data)')
|
||||||
|
ap.add_argument('--baud', type=int, action='append', help='test only these baud rates')
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
fails = 0
|
||||||
|
for baud in args.baud or BAUDS:
|
||||||
|
n = args.bytes or max(4096, min(2_000_000, baud // 10))
|
||||||
|
ok, got, dt, bad = run(args.port, baud, n, (8, serial.PARITY_NONE, serial.STOPBITS_ONE))
|
||||||
|
rate = got / dt / 1024
|
||||||
|
status = 'OK ' if ok else 'FAIL'
|
||||||
|
extra = '' if ok else f' (received {got}/{n}, first mismatch at {bad})'
|
||||||
|
print(f'{status} {baud:>9} baud 8N1 {n:>8} bytes {rate:8.1f} KiB/s{extra}', flush=True)
|
||||||
|
fails += not ok
|
||||||
|
|
||||||
|
for fmt, name in [((7, serial.PARITY_EVEN, serial.STOPBITS_ONE), '7E1'),
|
||||||
|
((8, serial.PARITY_ODD, serial.STOPBITS_TWO), '8O2')]:
|
||||||
|
ok, got, dt, bad = run(args.port, 115200, 4096, fmt)
|
||||||
|
print(f'{"OK " if ok else "FAIL"} 115200 baud {name}', flush=True)
|
||||||
|
fails += not ok
|
||||||
|
|
||||||
|
print('ALL PASSED' if fails == 0 else f'{fails} FAILED')
|
||||||
|
sys.exit(1 if fails else 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
FPGA -> PC stream test with fpga_test/stream loaded into the FPGA: the FPGA
|
||||||
|
sends a byte counter at exactly 10 Mbaud (1 MB/s) from its own 50 MHz clock.
|
||||||
|
Checks absolute baud accuracy and that no byte is lost or corrupted.
|
||||||
|
|
||||||
|
--switches N first reopens the port N times at other baud rates while the
|
||||||
|
stream keeps coming (the BL616 receiver used to lock up on such changes).
|
||||||
|
|
||||||
|
python tests/uart_stream_test.py --port COM12 --seconds 20 --switches 10
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import serial
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--port', required=True)
|
||||||
|
ap.add_argument('--baud', type=int, default=10_000_000)
|
||||||
|
ap.add_argument('--seconds', type=float, default=10)
|
||||||
|
ap.add_argument('--switches', type=int, default=0)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
for i in range(args.switches):
|
||||||
|
other = random.choice([9600, 115200, 921600, 3_000_000, 12_000_000])
|
||||||
|
with serial.Serial(args.port, other, timeout=0.05) as s:
|
||||||
|
time.sleep(random.uniform(0.02, 0.2))
|
||||||
|
s.read(65536)
|
||||||
|
|
||||||
|
with serial.Serial(args.port, args.baud, timeout=0.1) as s:
|
||||||
|
s.set_buffer_size(rx_size=1 << 20)
|
||||||
|
time.sleep(0.3)
|
||||||
|
s.reset_input_buffer()
|
||||||
|
expect = None
|
||||||
|
total = errors = 0
|
||||||
|
t0 = time.time()
|
||||||
|
while time.time() - t0 < args.seconds:
|
||||||
|
buf = s.read(65536)
|
||||||
|
for b in buf:
|
||||||
|
if expect is not None and b != expect:
|
||||||
|
errors += 1
|
||||||
|
expect = (b + 1) & 0xFF
|
||||||
|
total += len(buf)
|
||||||
|
dt = time.time() - t0
|
||||||
|
|
||||||
|
rate = total / dt
|
||||||
|
print(f'received {total} bytes in {dt:.1f} s = {rate / 1e6:.3f} MB/s '
|
||||||
|
f'({rate * 10 / 1e6:.2f} Mbaud of 8N1 payload), sequence errors: {errors}')
|
||||||
|
ok = errors == 0 and rate > 0.97e6 * args.baud / 10_000_000
|
||||||
|
print('PASS' if ok else 'FAIL')
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# openFPGALoader 1.1.1 для Windows x64
|
||||||
|
|
||||||
|
Готовая сборка, чтобы `TangProgrammer.exe` и команды из README работали сразу
|
||||||
|
после клонирования. Программа ищет её по пути `tools\openFPGALoader\bin`.
|
||||||
|
|
||||||
|
Нужна версия **не ниже 1.1.0**: в ней исправлена работа DirtyJTAG с GW5A.
|
||||||
|
|
||||||
|
Откуда взято:
|
||||||
|
|
||||||
|
* `openFPGALoader.exe` — из релиза v1.1.1 проекта
|
||||||
|
<https://github.com/trabucayre/openFPGALoader/releases/tag/v1.1.1>
|
||||||
|
(`mingw-w64-ucrt-x86_64-openFPGALoader-ci-1-any.pkg.tar.zst`);
|
||||||
|
* DLL — из пакетов MSYS2 UCRT64 (<https://repo.msys2.org/mingw/ucrt64/>):
|
||||||
|
`libftdi-1.5-13`, `libusb-1.0.30-1`, `zlib-1.3.2-2`, `gcc-libs-16.2.0-4`,
|
||||||
|
`libwinpthread-14.0.0.r426.g4564ee4b5-1`.
|
||||||
|
|
||||||
|
Лицензии: openFPGALoader — Apache-2.0; libftdi, libusb — LGPL-2.1;
|
||||||
|
libgcc/libstdc++ — GPL-3.0 с исключением GCC Runtime Library; winpthreads — MIT;
|
||||||
|
zlib — zlib License.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user