- 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.
2035 lines
86 KiB
C#
2035 lines
86 KiB
C#
// 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;
|
||
}
|
||
}
|
||
}
|