Files
BL616-firmware/gui/MainForm.cs
T
Gromov_IAandClaude Opus 5.5 e736fab46a Прошивка моста BL616 для Tang Primer 25K и программатор TangProgrammer
- src/: прошивка встроенного отладчика BL616 — составное USB-устройство
  1209:C0CA: DirtyJTAG v3 (WinUSB через MS OS 2.0) + CDC-ACM UART к FPGA
  B3/C3; UART до 20 Мбод на DMA, JTAG до 30 МГц через аппаратный SPI;
- bin/: загрузчик Sipeed (0x0) и штатная прошивка 2025030317 для отката;
- gui/: TangProgrammer.exe (C# WinForms, .NET Framework 4) — загрузка FPGA
  в SRAM и во флеш-память, копия и стирание флеш-памяти FPGA, прошивка
  моста; светлая и тёмная тема;
- scripts/: setup, build, flash, build_gui, make_portable, а также
  make_release и publish_release для релизов Gitea;
- fpga_test/, tests/: тестовые проекты Gowin (loopback, stream) и тесты
  UART на Python;
- tools/openFPGALoader/: openFPGALoader 1.1.1 для Windows x64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:59:10 +03:00

2035 lines
86 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Tang Primer 25K programmer: a window around openFPGALoader (-c dirtyJtag)
// for loading FPGA bitstreams, plus flashing of the BL616 bridge firmware
// (what scripts\flash.ps1 does). Build with scripts\build_gui.ps1.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TangProgrammer
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
enum ToolKind { Ofl, BlFlash }
sealed class RunResult
{
public int ExitCode = -1;
public bool FailSeen;
public readonly List<string> Lines = new List<string>();
// openFPGALoader exits with 0 even when loading the SRAM fails
public bool Ok
{
get { return ExitCode == 0 && !FailSeen; }
}
public bool Has(string text)
{
return Lines.Any(l => l.IndexOf(text, StringComparison.OrdinalIgnoreCase) >= 0);
}
public string Find(Regex rx)
{
foreach (string l in Lines)
{
Match m = rx.Match(l);
if (m.Success)
return m.Groups[1].Value;
}
return null;
}
}
sealed class LogEntry
{
public string Text;
public Tone Tone;
public bool Bold;
}
sealed class MainForm : Form
{
const string AppTitle = "Tang Primer 25K — программатор";
// where the tools and the bridge firmware are: the source tree, or the
// portable copy made by scripts\make_portable.ps1
static readonly string[] BlFlashPaths =
{
@"third_party\bouffalo_sdk\tools\bflb_tools\bouffalo_flash_cube\BLFlashCommand.exe",
@"tools\bouffalo_flash_cube\BLFlashCommand.exe",
};
static readonly string[] BridgeFwPaths =
{
@"build\build_out\tang25k_debugger_bl616.bin",
@"firmware\tang25k_debugger_bl616.bin",
};
static readonly int[] FreqList = { 1, 2, 4, 6, 10, 15, 20, 30 }; // MHz; the firmware does up to 30
const int DefaultFreq = 6; // what the firmware starts with
const int MaxLogEntries = 4000;
// openFPGALoader: "Load SRAM: [===== ] 33.00%"
static readonly Regex OflProgressRx = new Regex(@"^(?<label>[^\[\]]*?)\s*:\s*\[[^\]]*\]\s*(?<pct>\d+(?:\.\d+)?)\s*%$");
// BLFlashCommand: "Load 2048/68976 {""progress"":2}"
static readonly Regex BlProgressRx = new Regex(@"""progress""\s*:\s*(?<pct>\d+)");
static readonly Regex BlLoadRx = new Regex(@"\bLoad\s+(?<done>\d+)\s*/\s*(?<total>\d+)");
static readonly Regex FailRx = new Regex(@"\bfail(ed|ure)?\b|^error\b|\berror:|write en: error|wait: error", RegexOptions.IgnoreCase);
// --detect -f: known chips print "Detected: <vendor> <model> <n> sectors", others the raw JEDEC bytes
static readonly Regex FlashDetectedRx = new Regex(@"Detected:\s*(.+?)\s+(\d+)\s+sectors");
static readonly Regex JedecIdRx = new Regex(@"^\s*Jedec ID\s*:\s*([0-9a-fA-F]{2})\s*$");
static readonly Regex JedecTypeRx = new Regex(@"^\s*memory type\s*:\s*([0-9a-fA-F]{2})\s*$");
static readonly Regex JedecCapRx = new Regex(@"^\s*memory capacity\s*:\s*([0-9a-fA-F]{2})\s*$");
static readonly Regex AnsiRx = new Regex(@"\x1B\[[0-9;?]*[A-Za-z]");
static readonly Regex ModelRx = new Regex(@"^\s*model\s+(\S+)");
static readonly Regex IdcodeRx = new Regex(@"^\s*idcode\s+0x([0-9a-fA-F]+)");
readonly string uiRoot; // project folder as the user sees it (D:\BL616)
readonly string root; // the same, local path if the drive maps back to this PC
readonly string oflExe;
readonly string blFlashExe;
readonly string bridgeFw;
readonly bool inSourceTree; // false in the portable copy (no scripts\ there)
readonly Settings settings;
TableLayoutPanel mainLayout;
Card headerCard, pageCard, logCard;
StatusDot dot;
Label lblTitle, lblDetail, lblFpga;
FlatButton btnTheme;
ContextMenuStrip themeMenu;
TabStrip tabStrip;
TableLayoutPanel pageFpga, pageFlash, pageBridge;
ComboBox cbFile, cbFreq;
Label lblFileInfo;
ThemedRadio rbSram, rbFlash;
ThemedCheck chkVerify, chkAuto;
FlatButton btnBrowse, btnProgram, btnDetect, btnReload;
Label lblFlashChip, lblFlashHelp;
ThemedRadio rbDumpConfig, rbDumpAll;
ThemedCheck chkDumpVerify;
FlatButton btnFlashDetect, btnDump, btnRestore, btnErase;
Label lblBridgeNow, lblBridgeFile, lblBridgeHelp;
FlatButton btnBridgeFlash, btnBridgeRestore;
Label lblStatus;
FlatProgress pb;
FlatButton btnCancel;
RichTextBox log;
Font logFont, logBold;
ToolTip tips;
// colors that depend on the theme
readonly List<Label> dimLabels = new List<Label>();
readonly List<KeyValuePair<Control, string>> nativeThemed = new List<KeyValuePair<Control, string>>();
Tone statusTone, fileInfoTone, bridgeFileTone;
int headerDot; // 0 off, 1 ok, 2 attention
BridgeState bridge;
string fpgaModel; // from --detect or from the last loaded file
string flashInfo; // from --detect -f
long flashSize;
RunResult lastRun;
bool scanning, rescan, fitting;
readonly System.Windows.Forms.Timer devTimer, drainTimer, reloadTimer;
FileSystemWatcher watcher;
DateTime loadedStamp; // write time of the file version that is in the FPGA
bool reloadPending;
// the running operation
bool busy, opRisky;
string opTitle;
CancellationTokenSource cts;
Stopwatch opWatch;
RunResult run;
ToolKind runKind;
LineSplitter splitOut, splitErr;
readonly ConcurrentQueue<KeyValuePair<string, bool>> outQueue = new ConcurrentQueue<KeyValuePair<string, bool>>();
readonly List<LogEntry> logEntries = new List<LogEntry>();
string progressLabel; // the last log line is this progress bar
int progressLineStart;
public MainForm()
{
uiRoot = FindProjectRoot();
root = uiRoot != null ? LocalPath.Resolve(uiRoot) : null;
if (root != null)
{
oflExe = Path.Combine(root, @"tools\openFPGALoader\bin\openFPGALoader.exe");
blFlashExe = FirstExisting(root, BlFlashPaths);
bridgeFw = FirstExisting(root, BridgeFwPaths);
inSourceTree = File.Exists(Path.Combine(root, @"scripts\build.ps1"));
}
else
{
oflExe = FindInPath("openFPGALoader.exe");
}
settings = Settings.Load();
using (Graphics g = Graphics.FromHwnd(IntPtr.Zero))
Ui.Scale = g.DpiX / 96f;
Text = AppTitle;
Font = new Font("Segoe UI", 9f);
AutoScaleMode = AutoScaleMode.None;
KeyPreview = true;
ClientSize = new Size(S(820), S(740));
MinimumSize = new Size(S(680), S(620));
StartPosition = FormStartPosition.CenterScreen;
if (!settings.Bounds.IsEmpty && Screen.AllScreens.Any(s => s.WorkingArea.IntersectsWith(settings.Bounds)))
{
StartPosition = FormStartPosition.Manual;
Bounds = settings.Bounds;
}
if (settings.Maximized)
WindowState = FormWindowState.Maximized;
try
{
using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream("TangProgrammer.app.ico"))
if (s != null)
Icon = new Icon(s);
}
catch (Exception)
{
}
BuildUi();
EnableFileDrop(this);
ApplyTheme();
devTimer = new System.Windows.Forms.Timer { Interval = 700 };
devTimer.Tick += (s, e) => { devTimer.Stop(); RefreshDevices(); };
drainTimer = new System.Windows.Forms.Timer { Interval = 50 };
drainTimer.Tick += (s, e) => DrainOutput();
reloadTimer = new System.Windows.Forms.Timer { Interval = 1500 };
reloadTimer.Tick += OnReloadTimer;
}
static int S(float px)
{
return Ui.S(px);
}
static Padding P(float left, float top, float right, float bottom)
{
return new Padding(S(left), S(top), S(right), S(bottom));
}
// ---------------------------------------------------------------- UI
void BuildUi()
{
tips = new ToolTip { AutoPopDelay = 20000, OwnerDraw = true };
tips.Popup += (s, e) =>
{
Size t = TextRenderer.MeasureText(tips.GetToolTip(e.AssociatedControl), Font);
e.ToolTipSize = new Size(t.Width + S(16), t.Height + S(10));
};
tips.Draw += (s, e) =>
{
Theme th = Theme.Current;
using (var b = new SolidBrush(th.MenuBg))
e.Graphics.FillRectangle(b, e.Bounds);
using (var pen = new Pen(th.MenuBorder))
e.Graphics.DrawRectangle(pen, 0, 0, e.Bounds.Width - 1, e.Bounds.Height - 1);
TextRenderer.DrawText(e.Graphics, e.ToolTipText, Font, new Point(S(8), S(5)), th.Text);
};
mainLayout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 5, Padding = P(14, 12, 14, 14) };
mainLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
mainLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
mainLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
mainLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, S(300))); // fitted to the pages in FitPages
mainLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
mainLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
mainLayout.Controls.Add(BuildHeader(), 0, 0);
mainLayout.Controls.Add(BuildTabStrip(), 0, 1);
mainLayout.Controls.Add(BuildPages(), 0, 2);
mainLayout.Controls.Add(BuildProgressRow(), 0, 3);
mainLayout.Controls.Add(BuildLog(), 0, 4);
Controls.Add(mainLayout);
}
Control BuildHeader()
{
headerCard = new Card
{
Dock = DockStyle.Fill, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink,
Padding = P(16, 12, 12, 12), Margin = P(0, 0, 0, 6),
};
var t = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 4, RowCount = 2, Margin = new Padding(0) };
t.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
t.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
t.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
t.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
t.RowStyles.Add(new RowStyle(SizeType.AutoSize));
t.RowStyles.Add(new RowStyle(SizeType.AutoSize));
dot = new StatusDot { Size = new Size(S(10), S(10)), Margin = P(0, 7, 12, 0), Anchor = AnchorStyles.Top };
lblTitle = new Label { AutoSize = true, Font = new Font("Segoe UI Semibold", 11f), Margin = P(0, 0, 0, 2) };
lblDetail = DimLabel(new Label { AutoSize = true, Margin = new Padding(0) });
lblFpga = new Label { AutoSize = true, Font = new Font("Segoe UI Semibold", 10f), Anchor = AnchorStyles.Right, Margin = P(12, 0, 14, 0) };
btnTheme = new FlatButton { Glyph = Glyph.Contrast, Size = new Size(S(34), S(34)), Anchor = AnchorStyles.Right, Margin = new Padding(0) };
btnTheme.Click += (s, e) =>
{
foreach (ToolStripMenuItem item in themeMenu.Items)
item.Checked = (string)item.Tag == settings.Theme;
themeMenu.Show(btnTheme, new Point(btnTheme.Width, btnTheme.Height + S(2)), ToolStripDropDownDirection.BelowLeft);
};
t.Controls.Add(dot, 0, 0);
t.Controls.Add(lblTitle, 1, 0);
t.Controls.Add(lblDetail, 1, 1);
t.Controls.Add(lblFpga, 2, 0);
t.SetRowSpan(lblFpga, 2);
t.Controls.Add(btnTheme, 3, 0);
t.SetRowSpan(btnTheme, 2);
headerCard.Controls.Add(t);
themeMenu = new ContextMenuStrip { Renderer = new MenuRenderer(), Font = Font };
themeMenu.Items.Add(ThemeItem("Как в Windows", "system"));
themeMenu.Items.Add(ThemeItem("Светлая", "light"));
themeMenu.Items.Add(ThemeItem("Тёмная", "dark"));
tips.SetToolTip(btnTheme, "Тема оформления");
tips.SetToolTip(lblFpga, "Модель FPGA: по последнему «Определить FPGA» или по загруженному файлу");
return headerCard;
}
ToolStripMenuItem ThemeItem(string text, string mode)
{
var item = new ToolStripMenuItem(text) { Tag = mode };
item.Click += (s, e) =>
{
settings.Theme = mode;
ApplyTheme();
SaveSettings();
};
return item;
}
Control BuildTabStrip()
{
tabStrip = new TabStrip { Dock = DockStyle.Fill, Font = new Font("Segoe UI", 10f), Margin = P(0, 2, 0, 2) };
tabStrip.Add("FPGA");
tabStrip.Add("Флеш-память FPGA");
tabStrip.Add("Прошивка моста (BL616)");
tabStrip.Height = tabStrip.GetPreferredSize(Size.Empty).Height; // the table row is sized by it
tabStrip.SelectedIndexChanged += (s, e) => ShowPage();
return tabStrip;
}
Control BuildPages()
{
pageCard = new Card { Dock = DockStyle.Fill, Padding = P(18, 16, 18, 14), Margin = new Padding(0) };
pageFpga = BuildFpgaPage();
pageFlash = BuildFlashPage();
pageFlash.Visible = false;
pageBridge = BuildBridgePage();
pageBridge.Visible = false;
pageCard.Controls.Add(pageFlash);
pageCard.Controls.Add(pageFpga);
pageCard.Controls.Add(pageBridge);
return pageCard;
}
void ShowPage()
{
int tab = tabStrip.SelectedIndex;
bool bridgeTab = tab == 2;
pageFpga.Visible = tab == 0;
pageFlash.Visible = tab == 1;
pageBridge.Visible = bridgeTab;
if (bridgeTab)
UpdateBridgeInfo();
}
Label DimLabel(Label l)
{
dimLabels.Add(l);
return l;
}
TableLayoutPanel NewGrid(int columns)
{
var t = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = columns, RowCount = 0, Margin = new Padding(0) };
t.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
t.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
for (int i = 2; i < columns; i++)
t.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
return t;
}
static void AddRow(TableLayoutPanel t, params Control[] cells)
{
int row = t.RowCount;
t.RowCount = row + 1;
t.RowStyles.Add(new RowStyle(SizeType.AutoSize));
for (int c = 0; c < cells.Length; c++)
if (cells[c] != null)
t.Controls.Add(cells[c], c, row);
}
// a control that takes the rest of the row
static void AddWide(TableLayoutPanel t, int column, Control c)
{
int row = t.RowCount;
t.RowCount = row + 1;
t.RowStyles.Add(new RowStyle(SizeType.AutoSize));
t.Controls.Add(c, column, row);
t.SetColumnSpan(c, t.ColumnCount - column);
}
static Label FieldLabel(string text, float top)
{
return new Label { Text = text, AutoSize = true, Margin = P(0, top, 16, 3) };
}
FlatButton MakeButton(string text, EventHandler click, bool accent)
{
var b = new FlatButton
{
Text = text, Accent = accent, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink,
Padding = P(14, 3, 14, 3), Margin = P(0, 3, 8, 3), MinimumSize = new Size(0, S(32)),
};
if (accent)
{
b.Font = new Font("Segoe UI Semibold", 9f);
b.MinimumSize = new Size(S(176), S(32));
}
b.Click += click;
return b;
}
ComboBox MakeCombo(ComboBoxStyle style)
{
var cb = new ComboBox { DropDownStyle = style };
nativeThemed.Add(new KeyValuePair<Control, string>(cb, "DarkMode_CFD"));
cb.HandleCreated += (s, e) => { if (Theme.Current.IsDark) Ui.SetNativeTheme(cb, "DarkMode_CFD"); };
return cb;
}
TableLayoutPanel BuildFpgaPage()
{
TableLayoutPanel t = NewGrid(3);
cbFile = MakeCombo(ComboBoxStyle.DropDown);
cbFile.Dock = DockStyle.Fill;
cbFile.Margin = P(0, 4, 8, 3);
cbFile.TextChanged += (s, e) => OnFileChanged();
btnBrowse = MakeButton("Обзор…", OnBrowse, false);
btnBrowse.Margin = P(0, 1, 0, 3);
AddRow(t, FieldLabel("Битстрим:", 7), cbFile, btnBrowse);
lblFileInfo = new Label { AutoSize = true, Margin = P(0, 1, 0, 10) };
AddWide(t, 1, lblFileInfo);
rbSram = new ThemedRadio { Text = "SRAM — сразу, до отключения питания", AutoSize = true, Checked = true, Margin = P(0, 2, 3, 2) };
rbFlash = new ThemedRadio { Text = "Flash — во флеш-память FPGA, сохранится после выключения", AutoSize = true, Margin = P(0, 2, 3, 1) };
chkVerify = new ThemedCheck { Text = "проверить после записи", AutoSize = true, Margin = P(26, 1, 3, 4) };
rbFlash.CheckedChanged += (s, e) => UpdateModeUi();
var modes = new FlowLayoutPanel { FlowDirection = FlowDirection.TopDown, WrapContents = false, AutoSize = true, Margin = new Padding(0) };
modes.Controls.Add(rbSram);
modes.Controls.Add(rbFlash);
modes.Controls.Add(chkVerify);
AddRow(t, FieldLabel("Куда:", 5), modes);
cbFreq = MakeCombo(ComboBoxStyle.DropDownList);
cbFreq.Width = S(110);
cbFreq.Anchor = AnchorStyles.Left;
cbFreq.Margin = P(0, 8, 3, 3);
foreach (int f in FreqList)
cbFreq.Items.Add(f + " МГц");
AddRow(t, FieldLabel("Частота JTAG:", 11), cbFreq);
chkAuto = new ThemedCheck { Text = "Загружать в SRAM автоматически, когда файл пересобран", AutoSize = true, Margin = P(0, 8, 3, 3) };
chkAuto.CheckedChanged += (s, e) => UpdateWatcher();
AddWide(t, 1, chkAuto);
var buttons = new FlowLayoutPanel { AutoSize = true, WrapContents = false, Margin = P(0, 14, 0, 0) };
btnProgram = MakeButton("Загрузить в SRAM", OnProgram, true);
btnDetect = MakeButton("Определить FPGA", OnDetect, false);
btnReload = MakeButton("Перезапуск из Flash", OnReloadFpga, false);
buttons.Controls.Add(btnProgram);
buttons.Controls.Add(btnDetect);
buttons.Controls.Add(btnReload);
AddWide(t, 0, buttons);
tips.SetToolTip(btnProgram, "F5");
tips.SetToolTip(btnBrowse, "Ctrl+O. Файл можно и просто перетащить в окно.");
tips.SetToolTip(btnDetect, "Найти FPGA в JTAG-цепочке и показать её модель");
tips.SetToolTip(btnReload, "FPGA заново загрузит конфигурацию из своей флеш-памяти,\n" +
"как после включения питания. Загруженное в SRAM пропадёт.");
tips.SetToolTip(chkAuto, "Удобно вместе с Gowin IDE: после каждой сборки новый .fs сразу попадает в FPGA.");
tips.SetToolTip(cbFreq, "При записи во Flash openFPGALoader сам выбирает частоту для GW5A.");
return t;
}
TableLayoutPanel BuildFlashPage()
{
TableLayoutPanel t = NewGrid(3);
lblFlashChip = new Label { AutoSize = true, Margin = P(0, 9, 8, 6), Text = "ещё не определена" };
btnFlashDetect = MakeButton("Определить", OnFlashDetect, false);
btnFlashDetect.Margin = P(0, 1, 0, 3);
AddRow(t, FieldLabel("Микросхема:", 9), lblFlashChip, btnFlashDetect);
rbDumpConfig = new ThemedRadio { Text = "конфигурация FPGA (первый 1 МБ)", AutoSize = true, Checked = true, Margin = P(0, 2, 3, 2) };
rbDumpAll = new ThemedRadio { Text = "вся флеш-память", AutoSize = true, Margin = P(0, 2, 3, 2) };
chkDumpVerify = new ThemedCheck { Text = "проверять копию: прочитать дважды и сравнить", AutoSize = true, Checked = true, Margin = P(0, 4, 3, 2) };
var opts = new FlowLayoutPanel { FlowDirection = FlowDirection.TopDown, WrapContents = false, AutoSize = true, Margin = P(0, 6, 0, 0) };
opts.Controls.Add(rbDumpConfig);
opts.Controls.Add(rbDumpAll);
opts.Controls.Add(chkDumpVerify);
AddRow(t, FieldLabel("Резервная копия:", 8), opts);
var buttons = new FlowLayoutPanel { AutoSize = true, WrapContents = false, Margin = P(0, 14, 0, 8) };
btnDump = MakeButton("Сохранить копию в файл…", OnDumpFlash, true);
btnRestore = MakeButton("Записать файл во флеш…", OnRestoreFlash, false);
btnErase = MakeButton("Стереть всю флеш-память…", OnEraseFlash, false);
buttons.Controls.Add(btnDump);
buttons.Controls.Add(btnRestore);
buttons.Controls.Add(btnErase);
AddWide(t, 0, buttons);
lblFlashHelp = DimLabel(new Label
{
AutoSize = true, Margin = P(0, 2, 0, 0),
Text = "Копия — это точный образ флеш-памяти (.bin). Её, как и .bin из Gowin IDE, можно записать обратно " +
"кнопкой «Записать файл во флеш…». На время этих операций FPGA останавливается, а потом " +
"перезапускается из флеш-памяти.",
});
AddWide(t, 0, lblFlashHelp);
tips.SetToolTip(btnFlashDetect, "Прочитать JEDEC ID микросхемы флеш-памяти");
tips.SetToolTip(btnErase, "Стирает всю микросхему. После этого FPGA не будет загружаться при включении питания.");
return t;
}
TableLayoutPanel BuildBridgePage()
{
TableLayoutPanel t = NewGrid(2);
lblBridgeNow = new Label { AutoSize = true, Margin = P(0, 3, 0, 6) };
AddRow(t, FieldLabel("Сейчас на плате:", 3), lblBridgeNow);
lblBridgeFile = new Label { AutoSize = true, Margin = P(0, 3, 0, 6) };
AddRow(t, FieldLabel("Прошивка моста:", 3), lblBridgeFile);
var buttons = new FlowLayoutPanel { AutoSize = true, WrapContents = false, Margin = P(0, 10, 0, 10) };
btnBridgeFlash = MakeButton("Записать прошивку моста", OnBridgeFlash, true);
btnBridgeRestore = MakeButton("Вернуть штатную прошивку Sipeed", OnBridgeRestore, false);
buttons.Controls.Add(btnBridgeFlash);
buttons.Controls.Add(btnBridgeRestore);
AddWide(t, 0, buttons);
lblBridgeHelp = DimLabel(new Label
{
AutoSize = true, Margin = P(0, 2, 0, 0),
Text = "Мост переводится в режим загрузчика сам, если на нём работает эта прошивка и её COM-порт " +
"не открыт в терминале. Иначе отключите USB-кабель и подключите снова, удерживая кнопку S3 " +
"(при штатной прошивке — замкнув тестовые точки TDO и 3V3 на нижней стороне платы).\n" +
"После записи переподключите USB, чтобы запустить новую прошивку. " +
"Штатная прошивка Sipeed нужна только для Gowin Programmer.",
});
AddWide(t, 0, lblBridgeHelp);
return t;
}
Control BuildProgressRow()
{
var t = new TableLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, ColumnCount = 3, RowCount = 1, Margin = P(0, 10, 0, 8) };
t.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
t.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, S(230)));
t.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
t.RowStyles.Add(new RowStyle(SizeType.AutoSize));
lblStatus = new Label { Dock = DockStyle.Fill, AutoEllipsis = true, TextAlign = ContentAlignment.MiddleLeft, Margin = P(2, 0, 0, 0) };
pb = new FlatProgress { Anchor = AnchorStyles.Left | AnchorStyles.Right, Height = S(10), Margin = P(10, 0, 12, 0) };
btnCancel = MakeButton("Отмена", OnCancel, false);
btnCancel.Margin = new Padding(0);
btnCancel.Enabled = false;
t.Controls.Add(lblStatus, 0, 0);
t.Controls.Add(pb, 1, 0);
t.Controls.Add(btnCancel, 2, 0);
tips.SetToolTip(btnCancel, "Esc");
return t;
}
Control BuildLog()
{
logFont = new Font("Consolas", 9.5f);
logBold = new Font("Consolas", 9.5f, FontStyle.Bold);
logCard = new Card { Dock = DockStyle.Fill, Padding = P(12, 8, 3, 8), Margin = new Padding(0) };
log = new RichTextBox
{
Dock = DockStyle.Fill, ReadOnly = true, BorderStyle = BorderStyle.None, Font = logFont,
DetectUrls = false, HideSelection = false, ScrollBars = RichTextBoxScrollBars.Vertical,
};
nativeThemed.Add(new KeyValuePair<Control, string>(log, "DarkMode_Explorer"));
log.HandleCreated += (s, e) => { if (Theme.Current.IsDark) Ui.SetNativeTheme(log, "DarkMode_Explorer"); };
var menu = new ContextMenuStrip { Renderer = new MenuRenderer(), Font = Font };
menu.Items.Add("Копировать", null, (s, e) =>
{
if (log.SelectionLength > 0)
Clipboard.SetText(log.SelectedText.Replace("\n", "\r\n"));
});
menu.Items.Add("Копировать всё", null, (s, e) =>
{
if (log.TextLength > 0)
Clipboard.SetText(log.Text.Replace("\n", "\r\n"));
});
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("Очистить", null, (s, e) =>
{
logEntries.Clear();
log.Clear();
progressLabel = null;
});
log.ContextMenuStrip = menu;
logCard.Controls.Add(log);
return logCard;
}
void ApplyTheme()
{
bool dark = settings.Theme == "dark" || (settings.Theme == "system" && Theme.SystemPrefersDark());
Theme th = dark ? Theme.Dark : Theme.Light;
Theme.Current = th;
BackColor = th.Window;
ForeColor = th.Text;
headerCard.BackColor = th.Card;
headerCard.Border = th.CardBorder;
pageCard.BackColor = th.Card;
pageCard.Border = th.CardBorder;
logCard.BackColor = th.LogBg;
logCard.Border = th.CardBorder;
log.BackColor = th.LogBg;
log.ForeColor = th.Text;
foreach (ComboBox cb in new[] { cbFile, cbFreq })
{
cb.BackColor = th.Input;
cb.ForeColor = th.Text;
}
foreach (Label l in dimLabels)
l.ForeColor = th.TextDim;
foreach (KeyValuePair<Control, string> nt in nativeThemed)
if (nt.Key.IsHandleCreated)
Ui.SetNativeTheme(nt.Key, dark ? nt.Value : null);
if (IsHandleCreated)
Ui.SetDarkTitleBar(this, dark);
dot.DotColor = headerDot == 1 ? th.DotOn : headerDot == 2 ? th.DotWarn : th.DotOff;
lblStatus.ForeColor = th.Of(statusTone);
lblFileInfo.ForeColor = th.Of(fileInfoTone);
lblBridgeFile.ForeColor = th.Of(bridgeFileTone);
RenderLog();
Invalidate(true);
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
Ui.SetDarkTitleBar(this, Theme.Current.IsDark);
}
// The page card gets exactly the height its pages need; the log takes the rest.
void FitPages()
{
int inner = pageCard.ClientSize.Width - pageCard.Padding.Horizontal;
if (inner <= 0)
return;
lblBridgeHelp.MaximumSize = new Size(inner, 0);
int h = 0;
lblFlashHelp.MaximumSize = new Size(inner, 0);
foreach (Control page in new Control[] { pageFpga, pageFlash, pageBridge })
h = Math.Max(h, page.GetPreferredSize(new Size(inner, 0)).Height);
float height = h + pageCard.Padding.Vertical;
if (Math.Abs(mainLayout.RowStyles[2].Height - height) > 0.5f)
mainLayout.RowStyles[2].Height = height;
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
foreach (string f in settings.Recent)
cbFile.Items.Add(f);
if (cbFile.Items.Count > 0)
cbFile.SelectedIndex = 0;
int fi = Array.IndexOf(FreqList, settings.FreqMHz);
cbFreq.SelectedIndex = fi >= 0 ? fi : Array.IndexOf(FreqList, DefaultFreq);
rbFlash.Checked = settings.Flash;
rbSram.Checked = !settings.Flash;
chkVerify.Checked = settings.Verify;
chkAuto.Checked = settings.AutoReload;
if (settings.Tab > 0 && settings.Tab < tabStrip.Count)
tabStrip.SelectedIndex = settings.Tab;
UpdateModeUi();
UpdateFileInfo();
ShowBridgeState();
FitPages();
RefreshDevices();
SetStatus("", Tone.Text);
if (oflExe == null || !File.Exists(oflExe))
LogLine("Не найден openFPGALoader.exe: он должен лежать в tools\\openFPGALoader\\bin рядом с программой.", Tone.Err);
else
LogLine("Выберите файл .fs (или перетащите его в окно) и нажмите «Загрузить».", Tone.Dim);
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
if (!IsHandleCreated || fitting || pageCard == null)
return;
fitting = true;
try
{
FitPages();
}
finally
{
fitting = false;
}
}
protected override void OnActivated(EventArgs e)
{
base.OnActivated(e);
// the file may have been rebuilt meanwhile
if (cbFile != null)
{
UpdateFileInfo();
UpdateBridgeInfo();
}
}
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == 0x0219 /* WM_DEVICECHANGE */ && devTimer != null)
{
devTimer.Stop();
devTimer.Start();
}
else if (m.Msg == 0x001A /* WM_SETTINGCHANGE */ && m.LParam != IntPtr.Zero && settings.Theme == "system" &&
Marshal.PtrToStringUni(m.LParam) == "ImmersiveColorSet")
{
// light/dark app mode switched in the Windows settings
if (Theme.SystemPrefersDark() != Theme.Current.IsDark)
ApplyTheme();
}
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.F5 && tabStrip.SelectedIndex == 0)
{
if (!busy)
OnProgram(this, EventArgs.Empty);
return true;
}
if (keyData == (Keys.Control | Keys.O))
{
if (!busy)
OnBrowse(this, EventArgs.Empty);
return true;
}
if (keyData == (Keys.Control | Keys.Tab) || keyData == (Keys.Control | Keys.Shift | Keys.Tab))
{
tabStrip.SelectedIndex = (tabStrip.SelectedIndex + 1) % tabStrip.Count;
return true;
}
if (keyData == Keys.Escape && busy)
{
OnCancel(this, EventArgs.Empty);
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (busy)
{
if (MessageBox.Show(this, "Операция ещё выполняется. Прервать её и закрыть программу?", AppTitle,
MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
{
e.Cancel = true;
return;
}
cts.Cancel();
}
string f = cbFile.Text.Trim();
if (f.Length > 0 && File.Exists(f))
AddRecent(f);
SaveSettings();
base.OnFormClosing(e);
}
void SaveSettings()
{
int fi = cbFreq.SelectedIndex;
settings.FreqMHz = fi >= 0 ? FreqList[fi] : DefaultFreq;
settings.Flash = rbFlash.Checked;
settings.Verify = chkVerify.Checked;
settings.AutoReload = chkAuto.Checked;
settings.Tab = tabStrip.SelectedIndex;
settings.Maximized = WindowState == FormWindowState.Maximized;
settings.Bounds = WindowState == FormWindowState.Normal ? Bounds : RestoreBounds;
settings.Save();
}
// ------------------------------------------------------- bitstream file
void OnBrowse(object sender, EventArgs e)
{
using (var d = new OpenFileDialog())
{
d.Title = "Битстрим FPGA";
d.Filter = "Битстрим Gowin (*.fs)|*.fs|Образ для флеш-памяти (*.bin)|*.bin|Все файлы (*.*)|*.*";
string cur = cbFile.Text.Trim();
try
{
if (cur.Length > 0 && File.Exists(cur))
{
d.InitialDirectory = Path.GetDirectoryName(Path.GetFullPath(cur));
d.FileName = Path.GetFileName(cur);
}
}
catch (Exception)
{
}
if (d.ShowDialog(this) == DialogResult.OK)
{
tabStrip.SelectedIndex = 0;
cbFile.Text = d.FileName;
}
}
}
void EnableFileDrop(Control c)
{
c.AllowDrop = true;
c.DragEnter += OnDragEnter;
c.DragDrop += OnDragDrop;
foreach (Control child in c.Controls)
EnableFileDrop(child);
}
void OnDragEnter(object sender, DragEventArgs e)
{
e.Effect = !busy && e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None;
}
void OnDragDrop(object sender, DragEventArgs e)
{
var files = e.Data.GetData(DataFormats.FileDrop) as string[];
if (busy || files == null || files.Length == 0 || !File.Exists(files[0]))
return;
tabStrip.SelectedIndex = 0;
cbFile.Text = files[0];
Activate();
}
void OnFileChanged()
{
UpdateFileInfo();
UpdateWatcher();
}
void UpdateFileInfo()
{
string f = cbFile.Text.Trim();
if (f.Length == 0)
{
SetFileInfo("Gowin кладёт его в папку проекта: impl\\pnr\\<проект>.fs", Tone.Dim);
return;
}
if (!File.Exists(f))
{
SetFileInfo("Файл не найден", Tone.Err);
return;
}
var info = new FileInfo(f);
string device = ReadFsDevice(f);
var parts = new List<string>();
if (device != null)
parts.Add(device);
parts.Add(FormatSize(info.Length));
parts.Add("изменён " + FormatTime(info.LastWriteTime));
string text = string.Join(" · ", parts);
bool mismatch = device != null && fpgaModel != null &&
!device.Equals(fpgaModel, StringComparison.OrdinalIgnoreCase);
if (mismatch)
text += " — а на плате " + fpgaModel + "!";
SetFileInfo(text, mismatch ? Tone.Err : Tone.Dim);
}
void SetFileInfo(string text, Tone tone)
{
if (lblFileInfo.Text != text)
lblFileInfo.Text = text;
fileInfoTone = tone;
lblFileInfo.ForeColor = Theme.Current.Of(tone);
}
// Gowin .fs files start with "//Device: GW5A-25" and friends
static string ReadFsDevice(string path)
{
try
{
using (var r = new StreamReader(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)))
{
for (int i = 0; i < 40; i++)
{
string line = r.ReadLine();
if (line == null || !line.StartsWith("//"))
break;
if (line.StartsWith("//Device:"))
return line.Substring(9).Trim();
}
}
}
catch (Exception)
{
}
return null;
}
void AddRecent(string file)
{
file = Path.GetFullPath(file);
settings.Recent.RemoveAll(x => string.Equals(x, file, StringComparison.OrdinalIgnoreCase));
settings.Recent.Insert(0, file);
if (settings.Recent.Count > 12)
settings.Recent.RemoveRange(12, settings.Recent.Count - 12);
}
void RememberFile(string file)
{
AddRecent(file);
cbFile.BeginUpdate();
cbFile.Items.Clear();
foreach (string r in settings.Recent)
cbFile.Items.Add(r);
cbFile.SelectedIndex = 0;
cbFile.EndUpdate();
SaveSettings();
}
void UpdateModeUi()
{
chkVerify.Enabled = rbFlash.Checked && !busy;
btnProgram.Text = rbFlash.Checked ? "Записать во Flash" : "Загрузить в SRAM";
}
int FreqHz()
{
int i = cbFreq.SelectedIndex;
return (i >= 0 ? FreqList[i] : DefaultFreq) * 1000000;
}
// openFPGALoader gets its arguments through the ANSI code page: copy
// files whose path does not fit into it to the temp folder first.
static string ToolPath(string file)
{
if (AnsiSafe(file))
return file;
string dir = Path.Combine(Path.GetTempPath(), "Tang25kProgrammer");
Directory.CreateDirectory(dir);
string name = Path.GetFileName(file);
string copy = Path.Combine(dir, AnsiSafe(name) ? name : "bitstream" + Path.GetExtension(file));
File.Copy(file, copy, true);
return copy;
}
static bool AnsiSafe(string s)
{
Encoding ansi = Encoding.GetEncoding(Encoding.Default.CodePage, EncoderFallback.ExceptionFallback,
DecoderFallback.ExceptionFallback);
try
{
ansi.GetBytes(s);
return true;
}
catch (EncoderFallbackException)
{
return false;
}
}
// ----------------------------------------------------- auto reload
void UpdateWatcher()
{
if (watcher != null)
{
watcher.EnableRaisingEvents = false;
watcher.Dispose();
watcher = null;
}
string f = cbFile.Text.Trim();
if (!chkAuto.Checked || f.Length == 0 || !File.Exists(f))
return;
try
{
string full = Path.GetFullPath(f);
watcher = new FileSystemWatcher(Path.GetDirectoryName(full), Path.GetFileName(full));
watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size;
watcher.SynchronizingObject = this;
watcher.Changed += (s, e) => KickReload();
watcher.Created += (s, e) => KickReload();
watcher.Renamed += (s, e) => KickReload();
watcher.EnableRaisingEvents = true;
loadedStamp = File.GetLastWriteTimeUtc(f);
}
catch (Exception ex)
{
LogLine("Не получается следить за файлом: " + ex.Message, Tone.Warn);
}
}
void KickReload()
{
reloadTimer.Stop();
reloadTimer.Start();
}
void OnReloadTimer(object sender, EventArgs e)
{
reloadTimer.Stop();
string f = cbFile.Text.Trim();
if (!chkAuto.Checked || f.Length == 0 || !File.Exists(f))
return;
UpdateFileInfo();
if (busy)
{
reloadPending = true;
return;
}
if (File.GetLastWriteTimeUtc(f) == loadedStamp)
return;
if (!WriteFinished(f))
{
KickReload();
return;
}
AutoReload();
}
async void AutoReload()
{
await ProgramFpga(false, true);
}
// nobody holds the file open for writing any more
static bool WriteFinished(string path)
{
try
{
using (new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
return true;
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
}
// ------------------------------------------------------- FPGA actions
async void OnProgram(object sender, EventArgs e)
{
await ProgramFpga(rbFlash.Checked, false);
}
async Task ProgramFpga(bool toFlash, bool auto)
{
if (busy || !CheckOfl())
return;
string file = cbFile.Text.Trim();
if (file.Length == 0 || !File.Exists(file))
{
if (!auto)
ShowWarn(file.Length == 0 ? "Сначала выберите файл битстрима (.fs)." : "Файл не найден:\n" + file);
return;
}
if (!toFlash && !file.EndsWith(".fs", StringComparison.OrdinalIgnoreCase))
{
if (!auto)
ShowWarn("В SRAM загружается только битстрим Gowin (.fs).\n" +
"Образ .bin можно записать во флеш-память: переключите «Куда» на Flash.");
return;
}
if (toFlash && MessageBox.Show(this,
"Записать «" + Path.GetFileName(file) + "» во флеш-память FPGA?\n\n" +
"Текущая конфигурация во флеш-памяти будет заменена. После записи FPGA сразу запустится с новой.",
AppTitle, MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
return;
RememberFile(file);
string path;
try
{
path = ToolPath(file);
}
catch (Exception ex)
{
ShowWarn("Не удалось прочитать файл:\n" + ex.Message);
return;
}
DateTime stamp = File.GetLastWriteTimeUtc(file);
string device = ReadFsDevice(file);
string args = "-c dirtyJtag --freq " + FreqHz() +
(toFlash ? " -f" + (chkVerify.Checked ? " --verify" : "") : "") +
" \"" + path + "\"";
BeginOp(toFlash ? "Запись во флеш-память FPGA" : auto ? "Файл пересобран — загрузка в SRAM" : "Загрузка в SRAM", toFlash);
RunResult r = toFlash ? await RunFlashTool(args) : await RunTool(oflExe, args, null, ToolKind.Ofl);
if (r.Ok)
{
loadedStamp = stamp;
if (device != null)
SetFpgaModel(device);
EndOp(true, (toFlash ? "Записано во флеш-память FPGA" : "Загружено в SRAM") + " за " + Seconds() + ".", null);
}
else if (toFlash)
{
EndOp(false, "Записать во флеш-память не удалось.", OflHint(r) ?? FlashBrokenHint);
}
else
{
EndOp(false, "Загрузить не удалось.", OflHint(r));
}
}
const string FlashBrokenHint =
"Флеш-память FPGA сейчас может быть записана не полностью, и FPGA не загрузится из неё при включении. " +
"Повторите запись.";
// A flash operation that failed half-way (seen once, right after a
// user design had been running in the FPGA) leaves the flash partly
// erased: try it once more before giving up.
async Task<RunResult> RunFlashTool(string args)
{
RunResult r = await RunTool(oflExe, args, null, ToolKind.Ofl);
if (!r.Ok && !cts.IsCancellationRequested && !IsPermanentError(r))
{
LogLine("Сбой при работе с флеш-памятью — пробую ещё раз…", Tone.Warn);
r = await RunTool(oflExe, args, null, ToolKind.Ofl);
}
return r;
}
// errors a second attempt won't fix
static bool IsPermanentError(RunResult r)
{
return (r.ExitCode == -1 && r.Lines.Count == 0) || r.Has("fails to open device") || r.Has("JTAG init failed") ||
r.Has("no device found") || r.Has("mismatch between target's idcode") || r.Has("can't parse file") ||
r.Has("incompatible file format") || r.Has("can't open") || r.Has("block protection is set");
}
// ------------------------------------------------------ FPGA flash
async void OnFlashDetect(object sender, EventArgs e)
{
if (busy || !CheckOfl())
return;
BeginOp("Определение флеш-памяти FPGA", false);
bool ok = await DetectFlash();
EndOp(ok, ok ? "Флеш-память: " + flashInfo + "." : "Определить флеш-память не удалось.",
ok ? null : OflHint(lastRun));
}
async Task<bool> DetectFlash()
{
lastRun = await RunTool(oflExe, "-c dirtyJtag --detect -f", null, ToolKind.Ofl);
if (!lastRun.Ok)
return false;
Match known = null;
foreach (string l in lastRun.Lines)
{
Match m = FlashDetectedRx.Match(l);
if (m.Success)
known = m;
}
string id = lastRun.Find(JedecIdRx), type = lastRun.Find(JedecTypeRx), cap = lastRun.Find(JedecCapRx);
if (known != null)
{
flashSize = long.Parse(known.Groups[2].Value) * 0x10000;
flashInfo = known.Groups[1].Value + ", " + FormatSize(flashSize);
}
else if (id != null && type != null && cap != null)
{
int c = Convert.ToInt32(cap, 16);
flashSize = c >= 0x10 && c <= 0x1F ? 1L << c : 0;
flashInfo = FlashVendor(Convert.ToInt32(id, 16)) + "JEDEC ID " + (id + type + cap).ToUpperInvariant() +
(flashSize > 0 ? ", " + FormatSize(flashSize) : "");
}
else
{
return false;
}
lblFlashChip.Text = flashInfo;
return true;
}
static string FlashVendor(int jedec)
{
switch (jedec)
{
case 0x0B: return "XTX, ";
case 0x20: return "XMC/Micron, ";
case 0x5E: return "Zbit, ";
case 0x68: return "Boya, ";
case 0x85: return "Puya, ";
case 0x9D: return "ISSI, ";
case 0xA1: return "Fudan, ";
case 0xC2: return "Macronix, ";
case 0xC8: return "GigaDevice, ";
case 0xEF: return "Winbond, ";
default: return "";
}
}
async void OnDumpFlash(object sender, EventArgs e)
{
if (busy || !CheckOfl())
return;
string target;
using (var d = new SaveFileDialog())
{
d.Title = "Резервная копия флеш-памяти FPGA";
d.Filter = "Образ флеш-памяти (*.bin)|*.bin|Все файлы (*.*)|*.*";
d.DefaultExt = "bin";
d.FileName = "fpga_flash_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm") + ".bin";
if (uiRoot != null && Directory.Exists(Path.Combine(uiRoot, "backup")))
d.InitialDirectory = Path.Combine(uiRoot, "backup");
if (d.ShowDialog(this) != DialogResult.OK)
return;
target = d.FileName;
}
await BackupFlash(target, rbDumpAll.Checked, chkDumpVerify.Checked);
}
async Task BackupFlash(string target, bool whole, bool verify)
{
BeginOp("Резервная копия флеш-памяти FPGA", false);
string message = null, hint = null;
bool ok = false;
try
{
long size = 0x100000;
if (whole)
{
if (flashSize <= 0)
{
LogLine("Сначала определяю объём флеш-памяти…", Tone.Text);
if (!await DetectFlash() || flashSize <= 0)
throw new IOException("не удалось определить объём флеш-памяти");
}
size = flashSize;
}
string dir = WorkDir();
string first = Path.Combine(dir, "dump_1.bin"), second = Path.Combine(dir, "dump_2.bin");
if (!await DumpTo(first, size))
throw new IOException("чтение не удалось");
if (verify)
{
LogLine("Проверка: читаю флеш-память ещё раз…", Tone.Text);
if (!await DumpTo(second, size))
throw new IOException("повторное чтение не удалось");
if (!FilesEqual(first, second))
{
LogLine("Два чтения не совпали — читаю в третий раз…", Tone.Warn);
string third = Path.Combine(dir, "dump_3.bin");
if (!await DumpTo(third, size))
throw new IOException("чтение не удалось");
if (FilesEqual(third, second))
first = second;
else if (!FilesEqual(third, first))
throw new IOException("три чтения дали разные данные — связь с флеш-памятью нестабильна");
}
}
File.Copy(first, target, true);
ok = true;
message = "Копия сохранена (" + FormatSize(size) + ", " + Seconds() + "): " + target;
}
catch (Exception ex)
{
if (cts.IsCancellationRequested)
message = "Прервано.";
else
{
message = "Сохранить копию не удалось: " + ex.Message + ".";
hint = OflHint(lastRun);
}
}
EndOp(ok, message, hint);
}
async Task<bool> DumpTo(string path, long size)
{
if (File.Exists(path))
File.Delete(path);
lastRun = await RunTool(oflExe, "-c dirtyJtag --dump-flash --file-size " + size + " \"" + path + "\"", null, ToolKind.Ofl);
return lastRun.Ok && File.Exists(path) && new FileInfo(path).Length == size;
}
async void OnRestoreFlash(object sender, EventArgs e)
{
if (busy || !CheckOfl())
return;
string file;
using (var d = new OpenFileDialog())
{
d.Title = "Образ для записи во флеш-память FPGA";
d.Filter = "Образ флеш-памяти (*.bin)|*.bin|Битстрим Gowin (*.fs)|*.fs|Все файлы (*.*)|*.*";
if (uiRoot != null && Directory.Exists(Path.Combine(uiRoot, "backup")))
d.InitialDirectory = Path.Combine(uiRoot, "backup");
if (d.ShowDialog(this) != DialogResult.OK)
return;
file = d.FileName;
}
if (MessageBox.Show(this, "Записать «" + Path.GetFileName(file) + "» (" + FormatSize(new FileInfo(file).Length) +
") во флеш-память FPGA с адреса 0?\n\nСодержимое этой области флеш-памяти будет заменено.",
AppTitle, MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
return;
await WriteImage(file);
}
async Task WriteImage(string file)
{
bool isFs = file.EndsWith(".fs", StringComparison.OrdinalIgnoreCase);
BeginOp("Запись файла во флеш-память FPGA", true);
string image;
try
{
// trailing 0xFF of a dump is erased flash anyway: no need to write it
image = isFs ? ToolPath(file) : TrimmedImage(file);
}
catch (Exception ex)
{
EndOp(false, "Не удалось подготовить файл: " + ex.Message, null);
return;
}
if (image == null)
{
EndOp(false, "В файле только 0xFF — записывать нечего.", "Чтобы очистить флеш-память, есть «Стереть всю флеш-память».");
return;
}
if (!isFs && new FileInfo(image).Length < new FileInfo(file).Length)
LogLine("Пустой хвост файла (0xFF) не записываю: осталось " + FormatSize(new FileInfo(image).Length) + ".", Tone.Dim);
RunResult r = await RunFlashTool("-c dirtyJtag -f --verify \"" + image + "\"");
if (r.Ok)
EndOp(true, "Записано и проверено за " + Seconds() + ".", null);
else
EndOp(false, "Записать во флеш-память не удалось.", OflHint(r) ?? FlashBrokenHint);
}
// copy of a .bin without its trailing 0xFF bytes; null if nothing is left
string TrimmedImage(string file)
{
byte[] data = File.ReadAllBytes(file);
int len = data.Length;
while (len > 0 && data[len - 1] == 0xFF)
len--;
if (len == 0)
return null;
string path = Path.Combine(WorkDir(), "image.bin");
using (var f = new FileStream(path, FileMode.Create, FileAccess.Write))
f.Write(data, 0, len);
return path;
}
async void OnEraseFlash(object sender, EventArgs e)
{
if (busy || !CheckOfl())
return;
if (MessageBox.Show(this, "Стереть всю флеш-память FPGA?\n\nFPGA перестанет загружаться при включении питания, " +
"пока во флеш-память не будет записана новая конфигурация. " +
"Если нужна резервная копия — сначала сохраните её.",
AppTitle, MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) != DialogResult.OK)
return;
BeginOp("Стирание флеш-памяти FPGA", true);
LogLine("Стирание всей микросхемы занимает до минуты.", Tone.Dim);
RunResult r = await RunFlashTool("-c dirtyJtag --bulk-erase");
if (r.Ok)
EndOp(true, "Флеш-память стёрта за " + Seconds() + ".", null);
else
EndOp(false, "Стереть флеш-память не удалось.",
r.Has("block protection is set") ? "Флеш-память защищена от записи (биты BP в регистре статуса)." : OflHint(r));
}
// scratch folder whose path openFPGALoader can take (ANSI code page)
string WorkDir()
{
string dir = Path.Combine(Path.GetTempPath(), "Tang25kProgrammer");
if (!AnsiSafe(dir) && root != null)
dir = Path.Combine(root, @"build\gui_tmp");
Directory.CreateDirectory(dir);
return dir;
}
static bool FilesEqual(string a, string b)
{
byte[] x = File.ReadAllBytes(a), y = File.ReadAllBytes(b);
if (x.Length != y.Length)
return false;
for (int i = 0; i < x.Length; i++)
if (x[i] != y[i])
return false;
return true;
}
async void OnDetect(object sender, EventArgs e)
{
if (busy || !CheckOfl())
return;
BeginOp("Поиск FPGA", false);
RunResult r = await RunTool(oflExe, "-c dirtyJtag --freq " + FreqHz() + " --detect", null, ToolKind.Ofl);
string model = r.Find(ModelRx);
string idcode = r.Find(IdcodeRx);
if (r.Ok && model != null)
{
SetFpgaModel(model);
EndOp(true, "Найдена FPGA " + model + " (IDCODE 0x" + (idcode ?? "").ToUpperInvariant().PadLeft(8, '0') + ").", null);
}
else if (r.Ok)
{
EndOp(false, "Известной FPGA в JTAG-цепочке нет.", idcode != null ? "IDCODE 0x" + idcode : null);
}
else
{
EndOp(false, "FPGA не найдена.", OflHint(r));
}
}
async void OnReloadFpga(object sender, EventArgs e)
{
if (busy || !CheckOfl())
return;
BeginOp("Перезапуск FPGA из флеш-памяти", false);
RunResult r = await RunTool(oflExe, "-c dirtyJtag --freq " + FreqHz() + " -r", null, ToolKind.Ofl);
if (r.Ok)
EndOp(true, "Готово: FPGA загрузила конфигурацию из своей флеш-памяти.", null);
else
EndOp(false, "Перезапустить FPGA не удалось.", OflHint(r));
}
void SetFpgaModel(string model)
{
fpgaModel = model;
lblFpga.Text = "FPGA: " + model;
UpdateFileInfo();
}
bool CheckOfl()
{
if (oflExe != null && File.Exists(oflExe))
return true;
ShowWarn("Не найден openFPGALoader.exe.\nОн должен лежать в tools\\openFPGALoader\\bin рядом с программой.");
return false;
}
string OflHint(RunResult r)
{
if (r == null)
return null;
if (r.Has("fails to open device") || r.Has("JTAG init failed") || r.Has("unable to open"))
{
if (bridge != null && !bridge.Present)
return "Мост не найден: подключите USB-C разъём JTAG/UART платы. На BL616 должна стоять прошивка моста (вкладка «Прошивка моста»).";
return "Не удалось открыть JTAG. Возможно, его занимает другая программа (например, ещё один openFPGALoader).";
}
if (r.Has("no device found"))
return "FPGA не отвечает по JTAG. Проверьте питание платы.";
if (r.Has("mismatch between target's idcode"))
return "Битстрим собран для другой микросхемы.";
if (r.Has("CRC check : FAIL"))
return "Контрольная сумма не совпала. Попробуйте частоту JTAG пониже.";
if (r.Has("can't parse file") || r.Has("incompatible file format") || r.Has("can't open"))
return "Не удалось прочитать файл — это точно битстрим Gowin (.fs)?";
return null;
}
// ------------------------------------------------ bridge firmware
async void OnBridgeFlash(object sender, EventArgs e)
{
await FlashBridge(false);
}
async void OnBridgeRestore(object sender, EventArgs e)
{
await FlashBridge(true);
}
async Task FlashBridge(bool restore)
{
if (busy)
return;
if (blFlashExe == null || !File.Exists(blFlashExe))
{
ShowWarn(inSourceTree
? "Не найден BLFlashCommand.exe из Bouffalo SDK.\nУстановите SDK: scripts\\setup.ps1"
: "Не найден BLFlashCommand.exe: он должен лежать в tools\\bouffalo_flash_cube рядом с программой.");
return;
}
if (!restore && !File.Exists(bridgeFw))
{
ShowWarn(inSourceTree
? "Прошивка моста ещё не собрана:\n" + bridgeFw + "\n\nСоберите её: scripts\\build.ps1"
: "Не найден файл прошивки моста:\n" + bridgeFw);
return;
}
string question = restore
? "Вернуть штатную прошивку Sipeed?\n\nОна нужна для Gowin Programmer. Эта программа и openFPGALoader " +
"с dirtyJtag перестанут работать, пока прошивка моста не будет записана снова."
: "Записать прошивку моста в BL616?";
if (MessageBox.Show(this, question, AppTitle, MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
return;
BeginOp(restore ? "Возврат штатной прошивки Sipeed" : "Запись прошивки моста", true);
string port = await FindBootloader();
if (port == null)
{
EndOp(false, "Загрузчик BL616 не появился.",
"Отключите USB-кабель и подключите снова, удерживая S3 (или замкнув TDO и 3V3), затем повторите.");
return;
}
LogLine("Загрузчик BL616 на " + port + ".", Tone.Text);
string args = "--interface=uart --baudrate=2000000 --port=" + port + " --chipname=bl616 --cpu_id= --config=" +
(restore ? "restore_prog_cfg.ini" : "flash_prog_cfg.ini");
RunResult r = await RunTool(blFlashExe, args, root, ToolKind.BlFlash);
if (r.ExitCode == 0)
{
EndOp(true, "Прошивка записана. Отключите и снова подключите USB-кабель, чтобы её запустить.", null);
MessageBox.Show(this, "Прошивка записана.\n\nОтключите USB-кабель платы и подключите снова, чтобы её запустить.",
AppTitle, MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
EndOp(false, "Записать прошивку не удалось (код " + r.ExitCode + ").",
"Мост можно прошить заново в любой момент: загрузчик BL616 находится в ПЗУ.");
}
}
async Task<string> FindBootloader()
{
BridgeState st = await ScanAsync();
if (st == null)
st = new BridgeState();
if (st.BootloaderPort != null)
return st.BootloaderPort;
if (st.Present && st.UartPort != null)
{
LogLine("Перевожу мост в режим загрузчика (" + st.UartPort + " на 1234 бод)…", Tone.Text);
string port = st.UartPort;
string err = await Task.Run(() => Native.PokeBaudRate(port, 1234));
if (err != null)
LogLine("Не удалось открыть " + port + ": " + err.TrimEnd('.', ' ') + ". Возможно, он открыт в терминале.", Tone.Warn);
string found = await WaitBootloader(15);
if (found != null)
return found;
}
if (cts.IsCancellationRequested)
return null;
LogLine("Жду загрузчик BL616: отключите USB-кабель и подключите снова, удерживая кнопку S3 " +
"(или замкнув тестовые точки TDO и 3V3 на нижней стороне платы).", Tone.Warn, true);
SetStatus("Переподключите USB, удерживая S3…", Tone.Warn);
return await WaitBootloader(120);
}
async Task<string> WaitBootloader(int seconds)
{
var sw = Stopwatch.StartNew();
while (sw.Elapsed.TotalSeconds < seconds && !cts.IsCancellationRequested)
{
BridgeState st = await ScanAsync();
if (st != null && st.BootloaderPort != null)
return st.BootloaderPort;
try
{
await Task.Delay(500, cts.Token);
}
catch (OperationCanceledException)
{
}
}
return null;
}
void UpdateBridgeInfo()
{
BridgeState b = bridge;
string now;
if (b == null)
now = "…";
else if (b.Present)
now = "прошивка моста " + (b.Version ?? "") + " (DirtyJTAG + UART" + (b.UartPort != null ? " на " + b.UartPort : "") + ")";
else if (b.BootloaderPort != null)
now = "загрузчик BL616 на " + b.BootloaderPort + " — можно записывать";
else if (b.SipeedStock)
now = "штатная прошивка Sipeed (эмуляция FT2232)";
else
now = "плата не подключена";
lblBridgeNow.Text = now;
string text;
if (bridgeFw == null)
{
text = "—";
bridgeFileTone = Tone.Text;
}
else if (File.Exists(bridgeFw))
{
var fi = new FileInfo(bridgeFw);
text = bridgeFw.Substring(root.Length).TrimStart('\\') + " · " + FormatSize(fi.Length) +
" · собрана " + FormatTime(fi.LastWriteTime);
bridgeFileTone = Tone.Text;
}
else
{
text = inSourceTree ? "не собрана — сначала выполните scripts\\build.ps1" : "файл не найден";
bridgeFileTone = Tone.Err;
}
lblBridgeFile.Text = text;
lblBridgeFile.ForeColor = Theme.Current.Of(bridgeFileTone);
}
// ---------------------------------------------------------- devices
async void RefreshDevices()
{
if (scanning)
{
rescan = true;
return;
}
scanning = true;
BridgeState st = await ScanAsync();
scanning = false;
if (st != null)
{
bridge = st;
ShowBridgeState();
}
if (rescan)
{
rescan = false;
RefreshDevices();
}
}
static async Task<BridgeState> ScanAsync()
{
try
{
return await Task.Run(() => Devices.Scan());
}
catch (Exception)
{
return null;
}
}
void ShowBridgeState()
{
BridgeState b = bridge;
if (b == null)
{
SetHeader(0, "Поиск платы…", "");
}
else if (b.Present && b.JtagDriverOk)
{
SetHeader(1, "Мост подключён",
"Прошивка моста " + (b.Version ?? "?") + " · JTAG: DirtyJTAG · UART к FPGA: " + (b.UartPort ?? "—"));
}
else if (b.Present)
{
SetHeader(2, "Мост подключён, но у JTAG нет драйвера",
"Переподключите USB-кабель. Не поможет — поставьте WinUSB на «DirtyJTAG» через Zadig.");
}
else if (b.BootloaderPort != null)
{
SetHeader(2, "BL616 в режиме загрузчика (" + b.BootloaderPort + ")",
"Можно записать прошивку моста. Для обычной работы переподключите USB.");
}
else if (b.SipeedStock)
{
SetHeader(2, "На мосту штатная прошивка Sipeed",
"Для этой программы нужна прошивка моста: вкладка «Прошивка моста (BL616)».");
}
else
{
SetHeader(0, "Плата не подключена", "Подключите USB-C разъём JTAG/UART платы Tang Primer 25K.");
}
if (b != null && !b.Present && fpgaModel != null)
{
fpgaModel = null;
lblFpga.Text = "";
UpdateFileInfo();
}
UpdateBridgeInfo();
}
void SetHeader(int dotState, string title, string detail)
{
Theme th = Theme.Current;
headerDot = dotState;
dot.DotColor = dotState == 1 ? th.DotOn : dotState == 2 ? th.DotWarn : th.DotOff;
lblTitle.Text = title;
lblDetail.Text = detail;
}
// ------------------------------------------------- running tools
void BeginOp(string title, bool risky)
{
busy = true;
opRisky = risky;
opTitle = title;
cts = new CancellationTokenSource();
opWatch = Stopwatch.StartNew();
SetBusyUi();
if (logEntries.Count > 0)
LogLine("", Tone.Text);
LogLine(title, Tone.Title, true);
SetStatus(title + "…", Tone.Text);
pb.State = BarState.Running;
pb.Marquee = true;
}
void EndOp(bool ok, string message, string hint)
{
bool cancelled = cts.IsCancellationRequested;
if (cancelled)
{
ok = false;
message = "Прервано.";
hint = null;
}
busy = false;
cts.Dispose();
cts = null;
SetBusyUi();
bool hadProgress = !pb.Marquee && pb.Value > 0;
pb.Marquee = false;
if (ok || !hadProgress)
pb.Value = 100;
pb.State = ok ? BarState.Ok : cancelled ? BarState.Paused : BarState.Error;
LogLine(message, ok ? Tone.Ok : Tone.Err, true);
if (hint != null)
LogLine(hint, Tone.Warn);
SetStatus(message, ok ? Tone.Ok : Tone.Err);
if (Form.ActiveForm != this)
Native.FlashTaskbar(this);
if (reloadPending)
{
reloadPending = false;
KickReload();
}
}
void SetBusyUi()
{
foreach (Control c in new Control[] { cbFile, btnBrowse, rbSram, rbFlash, cbFreq, btnProgram, btnDetect,
btnReload, btnFlashDetect, rbDumpConfig, rbDumpAll, chkDumpVerify,
btnDump, btnRestore, btnErase, btnBridgeFlash, btnBridgeRestore })
c.Enabled = !busy;
chkVerify.Enabled = !busy && rbFlash.Checked;
btnCancel.Enabled = busy;
}
void OnCancel(object sender, EventArgs e)
{
if (!busy || cts == null || cts.IsCancellationRequested)
return;
if (opRisky && MessageBox.Show(this, "Прервать запись? Записанное к этому моменту будет неполным — " +
"запись потом нужно будет повторить.", AppTitle,
MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
return;
if (cts != null)
{
cts.Cancel();
SetStatus("Прерываю…", Tone.Warn);
}
}
string Seconds()
{
return opWatch.Elapsed.TotalSeconds.ToString("0.0", CultureInfo.CurrentCulture) + " с";
}
async Task<RunResult> RunTool(string exe, string args, string workDir, ToolKind kind)
{
run = new RunResult();
runKind = kind;
splitOut = new LineSplitter();
splitErr = new LineSplitter();
LogLine("> " + Path.GetFileNameWithoutExtension(exe) + " " + args, Tone.Dim);
drainTimer.Start();
try
{
run.ExitCode = await ToolProcess.Run(exe, args, workDir,
(text, isErr) => outQueue.Enqueue(new KeyValuePair<string, bool>(text, isErr)), cts.Token);
}
catch (Exception ex)
{
run.ExitCode = -1;
LogLine("Не удалось запустить " + Path.GetFileName(exe) + ": " + ex.Message, Tone.Err);
}
drainTimer.Stop();
DrainOutput();
HandleSegment(splitOut.Flush(), false);
HandleSegment(splitErr.Flush(), true);
RunResult r = run;
run = null;
return r;
}
void DrainOutput()
{
if (run == null)
return;
KeyValuePair<string, bool> item;
while (outQueue.TryDequeue(out item))
{
LineSplitter sp = item.Value ? splitErr : splitOut;
foreach (string seg in sp.Feed(item.Key))
HandleSegment(seg, item.Value);
}
}
void HandleSegment(string seg, bool isErr)
{
string line = AnsiRx.Replace(seg, "").TrimEnd();
if (line.Trim().Length == 0)
return;
// stray debug print of openFPGALoader 1.1.x
if (runKind == ToolKind.Ofl && (line == "empty" || line == "pas empty"))
return;
run.Lines.Add(line);
string label;
int pct;
if (TryProgress(line, out label, out pct))
{
ShowProgress(label, pct);
return;
}
bool fail = runKind == ToolKind.Ofl && FailRx.IsMatch(line);
if (fail)
run.FailSeen = true;
LogLine(line, fail || isErr ? Tone.Err : Tone.Text);
}
bool TryProgress(string line, out string label, out int pct)
{
label = null;
pct = 0;
Match m = OflProgressRx.Match(line.Trim());
if (m.Success)
{
label = m.Groups["label"].Value.Trim();
pct = (int)double.Parse(m.Groups["pct"].Value, CultureInfo.InvariantCulture);
}
else if (runKind == ToolKind.BlFlash && (m = BlProgressRx.Match(line)).Success)
{
label = "Запись";
pct = int.Parse(m.Groups["pct"].Value);
}
else if (runKind == ToolKind.BlFlash && (m = BlLoadRx.Match(line)).Success)
{
long done = long.Parse(m.Groups["done"].Value), total = long.Parse(m.Groups["total"].Value);
if (total <= 0)
return false;
label = "Запись";
pct = (int)(done * 100 / total);
}
else
{
return false;
}
if (label.Length == 0)
label = "Прогресс";
pct = Math.Max(0, Math.Min(100, pct));
return true;
}
void ShowProgress(string label, int pct)
{
if (pb.Marquee)
pb.Marquee = false;
pb.Value = pct;
SetStatus(opTitle + " — " + pct + "%", Tone.Text);
string text = label + ": " + pct + "%";
if (progressLabel == label)
{
ReplaceLast(text);
}
else
{
int start = log.TextLength;
LogLine(text, Tone.Text);
progressLineStart = start;
progressLabel = label;
}
}
// ------------------------------------------------------------ log
void LogLine(string text, Tone tone, bool bold = false)
{
progressLabel = null;
var entry = new LogEntry { Text = text, Tone = tone, Bold = bold };
logEntries.Add(entry);
if (logEntries.Count > MaxLogEntries)
{
logEntries.RemoveRange(0, MaxLogEntries / 4);
RenderLog();
return;
}
AppendEntry(entry);
ScrollLogToEnd();
}
void AppendEntry(LogEntry e)
{
log.SelectionStart = log.TextLength;
log.SelectionLength = 0;
log.SelectionColor = Theme.Current.Of(e.Tone);
log.SelectionFont = e.Bold ? logBold : logFont;
log.SelectedText = e.Text + "\n";
}
// the last line is a progress bar: redraw it in place
void ReplaceLast(string text)
{
LogEntry last = logEntries[logEntries.Count - 1];
last.Text = text;
log.Select(progressLineStart, log.TextLength - progressLineStart);
log.SelectionColor = Theme.Current.Of(last.Tone);
log.SelectionFont = logFont;
log.SelectedText = text + "\n";
ScrollLogToEnd();
}
// after a theme change: the colors of the text are stored in the rich edit
void RenderLog()
{
if (log == null)
return;
bool redraw = log.IsHandleCreated;
if (redraw)
Ui.SetRedraw(log, false);
log.Clear();
for (int i = 0; i < logEntries.Count; i++)
{
if (i == logEntries.Count - 1)
progressLineStart = log.TextLength;
AppendEntry(logEntries[i]);
}
if (redraw)
Ui.SetRedraw(log, true);
ScrollLogToEnd();
}
void ScrollLogToEnd()
{
log.SelectionStart = log.TextLength;
log.SelectionLength = 0;
log.ScrollToCaret();
}
void SetStatus(string text, Tone tone)
{
statusTone = tone;
lblStatus.Text = text;
lblStatus.ForeColor = Theme.Current.Of(tone);
}
void ShowWarn(string text)
{
MessageBox.Show(this, text, AppTitle, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
// ----------------------------------------------------------- misc
static string FormatSize(long bytes)
{
if (bytes < 1024 * 1024)
return Math.Ceiling(bytes / 1024.0).ToString("0") + " КБ";
return (bytes / (1024.0 * 1024)).ToString("0.0") + " МБ";
}
static string FormatTime(DateTime t)
{
if (t.Date == DateTime.Today)
return "сегодня в " + t.ToString("HH:mm:ss");
if (t.Date == DateTime.Today.AddDays(-1))
return "вчера в " + t.ToString("HH:mm");
return t.ToString("dd.MM.yyyy HH:mm");
}
// the first of the paths (relative to dir) that exists, else the first one
static string FirstExisting(string dir, string[] paths)
{
foreach (string p in paths)
if (File.Exists(Path.Combine(dir, p)))
return Path.Combine(dir, p);
return Path.Combine(dir, paths[0]);
}
// the folder with tools\openFPGALoader: next to the exe or one level up
static string FindProjectRoot()
{
string dir = Path.GetDirectoryName(typeof(MainForm).Assembly.Location);
for (int i = 0; i < 3 && !string.IsNullOrEmpty(dir); i++)
{
if (File.Exists(Path.Combine(dir, @"tools\openFPGALoader\bin\openFPGALoader.exe")))
return dir;
dir = Path.GetDirectoryName(dir.TrimEnd('\\'));
}
return null;
}
static string FindInPath(string exe)
{
foreach (string dir in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(';'))
{
try
{
string p = Path.Combine(dir.Trim().Trim('"'), exe);
if (dir.Trim().Length > 0 && File.Exists(p))
return p;
}
catch (ArgumentException)
{
}
}
return null;
}
}
}