Прошивка моста 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.
This commit is contained in:
2026-10-02 14:59:10 +03:00
commit 8bc1583805
57 changed files with 6439 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
* text=auto
*.bin binary
*.exe binary
*.dll binary
*.ico binary
*.fs binary
*.zip binary
+20
View File
@@ -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
+38
View File
@@ -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 на плате не использовать.
+26
View File
@@ -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)
+10
View File
@@ -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
+284
View File
@@ -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 и счётчики байт.
+16
View File
@@ -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.
+10
View File
@@ -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
+17
View File
@@ -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
+9
View File
@@ -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
+5
View File
@@ -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;
+8
View File
@@ -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
+9
View File
@@ -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
+5
View File
@@ -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;
+40
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+433
View File
@@ -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
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+28
View File
@@ -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>
+35
View File
@@ -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.
+10
View File
@@ -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
+19
View File
@@ -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
}
+25
View File
@@ -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
+42
View File
@@ -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
+72
View File
@@ -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
+80
View File
@@ -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
+56
View File
@@ -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
+91
View File
@@ -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
+15
View File
@@ -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
+14
View File
@@ -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
+92
View File
@@ -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
View File
@@ -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
View File
@@ -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
+38
View File
@@ -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
View File
@@ -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);
}
+39
View File
@@ -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
View File
@@ -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();
}
}
+154
View File
@@ -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;
}
+29
View File
@@ -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
+513
View File
@@ -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);
}
+32
View File
@@ -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
+60
View File
@@ -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
View File
@@ -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);
}
+36
View File
@@ -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
+88
View File
@@ -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()
+59
View File
@@ -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()
+19
View File
@@ -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.