Files
BL616-firmware/gui/MainForm.cs
T

2035 lines
86 KiB
C#
Raw Normal View History

// 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;
}
}
}