- 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.
434 lines
16 KiB
C#
434 lines
16 KiB
C#
// Helpers of the programmer GUI: USB device discovery, running the
|
|
// command-line tools, settings, a few Win32 calls.
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Management;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using Microsoft.Win32.SafeHandles;
|
|
|
|
namespace TangProgrammer
|
|
{
|
|
// What Windows sees on USB (see README: installation and restore)
|
|
sealed class BridgeState
|
|
{
|
|
public bool Present; // this firmware: DirtyJTAG + CDC UART, 1209:C0CA
|
|
public string Version; // from bcdDevice, e.g. "1.00"
|
|
public string UartPort; // COM port of its CDC interface
|
|
public bool JtagDriverOk = true;
|
|
public string BootloaderPort; // BL616 ROM USB bootloader "Bouffalo CDC DEMO", 349B:6160
|
|
public bool SipeedStock; // stock Sipeed firmware emulates an FT2232, 0403:6010
|
|
}
|
|
|
|
static class Devices
|
|
{
|
|
static readonly Regex ComRx = new Regex(@"\((COM\d+)\)");
|
|
static readonly Regex RevRx = new Regex(@"REV_(\d\d)(\d\d)");
|
|
|
|
public static BridgeState Scan()
|
|
{
|
|
var st = new BridgeState();
|
|
const string query = "SELECT Name, PNPDeviceID, HardwareID, ConfigManagerErrorCode " +
|
|
"FROM Win32_PnPEntity WHERE PNPDeviceID LIKE 'USB%'";
|
|
using (var searcher = new ManagementObjectSearcher(query))
|
|
using (ManagementObjectCollection found = searcher.Get())
|
|
{
|
|
foreach (ManagementBaseObject o in found)
|
|
{
|
|
using (o)
|
|
{
|
|
string id = Convert.ToString(o["PNPDeviceID"]).ToUpperInvariant();
|
|
Match com = ComRx.Match(Convert.ToString(o["Name"]));
|
|
if (id.Contains("VID_1209&PID_C0CA"))
|
|
{
|
|
st.Present = true;
|
|
if (id.Contains("&MI_00"))
|
|
st.JtagDriverOk = Convert.ToUInt32(o["ConfigManagerErrorCode"]) == 0;
|
|
else if (com.Success)
|
|
st.UartPort = com.Groups[1].Value;
|
|
var hwIds = o["HardwareID"] as string[];
|
|
if (st.Version == null && hwIds != null)
|
|
{
|
|
foreach (string hw in hwIds)
|
|
{
|
|
Match m = RevRx.Match(hw.ToUpperInvariant());
|
|
if (m.Success)
|
|
{
|
|
st.Version = int.Parse(m.Groups[1].Value) + "." + m.Groups[2].Value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (id.Contains("VID_349B&PID_6160"))
|
|
{
|
|
if (com.Success)
|
|
st.BootloaderPort = com.Groups[1].Value;
|
|
}
|
|
else if (id.Contains("VID_0403&PID_6010"))
|
|
{
|
|
st.SipeedStock = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return st;
|
|
}
|
|
}
|
|
|
|
// A drive mapped to a share of this very PC (D: -> \\THISPC\Projects) sends
|
|
// every tool launch through SMB; use the folder behind the share instead,
|
|
// like Resolve-LocalPath in scripts\common.ps1.
|
|
static class LocalPath
|
|
{
|
|
[DllImport("mpr.dll", CharSet = CharSet.Unicode)]
|
|
static extern int WNetGetConnection(string localName, StringBuilder remoteName, ref int length);
|
|
|
|
public static string Resolve(string path)
|
|
{
|
|
string full = Path.GetFullPath(path);
|
|
try
|
|
{
|
|
if (full.Length < 3 || full[1] != ':')
|
|
return full;
|
|
var unc = new StringBuilder(512);
|
|
int len = unc.Capacity;
|
|
if (WNetGetConnection(full.Substring(0, 2), unc, ref len) != 0)
|
|
return full;
|
|
Match m = Regex.Match(unc.ToString(), @"^\\\\([^\\]+)\\([^\\]+)\\?(.*)$");
|
|
if (!m.Success)
|
|
return full;
|
|
string host = m.Groups[1].Value;
|
|
if (!host.Equals(Environment.MachineName, StringComparison.OrdinalIgnoreCase) &&
|
|
!host.Equals("localhost", StringComparison.OrdinalIgnoreCase) && host != "127.0.0.1")
|
|
return full;
|
|
string query = "SELECT Path FROM Win32_Share WHERE Name='" + m.Groups[2].Value.Replace("'", "\\'") + "'";
|
|
using (var searcher = new ManagementObjectSearcher(query))
|
|
using (ManagementObjectCollection found = searcher.Get())
|
|
{
|
|
foreach (ManagementBaseObject o in found)
|
|
{
|
|
string share = Convert.ToString(o["Path"]);
|
|
if (share.Length == 0)
|
|
continue;
|
|
string local = Path.Combine(Path.Combine(share, m.Groups[3].Value), full.Substring(3));
|
|
if (Directory.Exists(local) || File.Exists(local))
|
|
return local;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
return full;
|
|
}
|
|
}
|
|
|
|
// Runs a console tool without a console window and streams its output
|
|
// (stdout and stderr chunks, as they arrive) to a callback on a worker thread.
|
|
static class ToolProcess
|
|
{
|
|
public static Task<int> Run(string exe, string args, string workDir,
|
|
Action<string, bool> output, CancellationToken ct)
|
|
{
|
|
var psi = new ProcessStartInfo(exe, args)
|
|
{
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
StandardOutputEncoding = Encoding.Default,
|
|
StandardErrorEncoding = Encoding.Default,
|
|
WorkingDirectory = workDir ?? Path.GetDirectoryName(exe),
|
|
};
|
|
Process p = Process.Start(psi);
|
|
Thread outPump = Pump(p.StandardOutput, false, output);
|
|
Thread errPump = Pump(p.StandardError, true, output);
|
|
return Task.Run(() =>
|
|
{
|
|
using (p)
|
|
using (ct.Register(() => Task.Run(() => KillTree(p))))
|
|
{
|
|
p.WaitForExit();
|
|
outPump.Join();
|
|
errPump.Join();
|
|
return p.ExitCode;
|
|
}
|
|
});
|
|
}
|
|
|
|
static Thread Pump(StreamReader reader, bool isErr, Action<string, bool> output)
|
|
{
|
|
var t = new Thread(() =>
|
|
{
|
|
var buf = new char[4096];
|
|
try
|
|
{
|
|
int n;
|
|
while ((n = reader.Read(buf, 0, buf.Length)) > 0)
|
|
output(new string(buf, 0, n), isErr);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
});
|
|
t.IsBackground = true;
|
|
t.Start();
|
|
return t;
|
|
}
|
|
|
|
// BLFlashCommand is a one-file PyInstaller exe: the real work happens in a child
|
|
static void KillTree(Process p)
|
|
{
|
|
try
|
|
{
|
|
if (p.HasExited)
|
|
return;
|
|
var psi = new ProcessStartInfo("taskkill.exe", "/T /F /PID " + p.Id)
|
|
{
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
};
|
|
using (Process k = Process.Start(psi))
|
|
k.WaitForExit(5000);
|
|
if (!p.HasExited)
|
|
p.Kill();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
// Splits console output into lines. A lone '\r' also ends a line: that is
|
|
// how progress bars redraw themselves.
|
|
sealed class LineSplitter
|
|
{
|
|
readonly StringBuilder cur = new StringBuilder();
|
|
|
|
public List<string> Feed(string chunk)
|
|
{
|
|
var lines = new List<string>();
|
|
foreach (char ch in chunk)
|
|
{
|
|
if (ch == '\r' || ch == '\n')
|
|
{
|
|
if (cur.Length > 0)
|
|
lines.Add(cur.ToString());
|
|
cur.Clear();
|
|
}
|
|
else
|
|
{
|
|
cur.Append(ch);
|
|
}
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
public string Partial
|
|
{
|
|
get { return cur.ToString(); }
|
|
}
|
|
|
|
public string Flush()
|
|
{
|
|
string rest = cur.ToString();
|
|
cur.Clear();
|
|
return rest;
|
|
}
|
|
}
|
|
|
|
sealed class Settings
|
|
{
|
|
public readonly List<string> Recent = new List<string>();
|
|
public int FreqMHz = 6;
|
|
public bool Flash;
|
|
public bool Verify = true;
|
|
public bool AutoReload;
|
|
public int Tab;
|
|
public string Theme = "system"; // system | light | dark
|
|
public Rectangle Bounds;
|
|
public bool Maximized;
|
|
|
|
static string FilePath
|
|
{
|
|
get
|
|
{
|
|
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
"Tang25kProgrammer", "settings.ini");
|
|
}
|
|
}
|
|
|
|
public static Settings Load()
|
|
{
|
|
var s = new Settings();
|
|
try
|
|
{
|
|
if (!File.Exists(FilePath))
|
|
return s;
|
|
foreach (string line in File.ReadAllLines(FilePath, Encoding.UTF8))
|
|
{
|
|
int eq = line.IndexOf('=');
|
|
if (eq <= 0)
|
|
continue;
|
|
string key = line.Substring(0, eq).Trim();
|
|
string val = line.Substring(eq + 1).Trim();
|
|
switch (key)
|
|
{
|
|
case "recent":
|
|
if (val.Length > 0)
|
|
s.Recent.Add(val);
|
|
break;
|
|
case "freq":
|
|
int.TryParse(val, out s.FreqMHz);
|
|
break;
|
|
case "flash":
|
|
s.Flash = val == "1";
|
|
break;
|
|
case "verify":
|
|
s.Verify = val == "1";
|
|
break;
|
|
case "auto":
|
|
s.AutoReload = val == "1";
|
|
break;
|
|
case "tab":
|
|
int.TryParse(val, out s.Tab);
|
|
break;
|
|
case "theme":
|
|
if (val == "light" || val == "dark" || val == "system")
|
|
s.Theme = val;
|
|
break;
|
|
case "maximized":
|
|
s.Maximized = val == "1";
|
|
break;
|
|
case "bounds":
|
|
string[] p = val.Split(',');
|
|
if (p.Length == 4)
|
|
s.Bounds = new Rectangle(int.Parse(p[0]), int.Parse(p[1]), int.Parse(p[2]), int.Parse(p[3]));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
return s;
|
|
}
|
|
|
|
public void Save()
|
|
{
|
|
try
|
|
{
|
|
var sb = new StringBuilder();
|
|
foreach (string r in Recent)
|
|
sb.AppendLine("recent=" + r);
|
|
sb.AppendLine("freq=" + FreqMHz);
|
|
sb.AppendLine("flash=" + (Flash ? 1 : 0));
|
|
sb.AppendLine("verify=" + (Verify ? 1 : 0));
|
|
sb.AppendLine("auto=" + (AutoReload ? 1 : 0));
|
|
sb.AppendLine("tab=" + Tab);
|
|
sb.AppendLine("theme=" + Theme);
|
|
sb.AppendLine("maximized=" + (Maximized ? 1 : 0));
|
|
if (!Bounds.IsEmpty)
|
|
sb.AppendLine("bounds=" + Bounds.X + "," + Bounds.Y + "," + Bounds.Width + "," + Bounds.Height);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(FilePath));
|
|
File.WriteAllText(FilePath, sb.ToString(), Encoding.UTF8);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
static class Native
|
|
{
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
struct FLASHWINFO
|
|
{
|
|
public uint cbSize;
|
|
public IntPtr hwnd;
|
|
public uint dwFlags;
|
|
public uint uCount;
|
|
public uint dwTimeout;
|
|
}
|
|
|
|
[DllImport("user32.dll")]
|
|
static extern bool FlashWindowEx(ref FLASHWINFO fi);
|
|
|
|
// blink the taskbar button until the window is activated
|
|
public static void FlashTaskbar(Form f)
|
|
{
|
|
var fi = new FLASHWINFO
|
|
{
|
|
cbSize = (uint)Marshal.SizeOf(typeof(FLASHWINFO)),
|
|
hwnd = f.Handle,
|
|
dwFlags = 3 /* FLASHW_ALL */ | 12 /* FLASHW_TIMERNOFG */,
|
|
};
|
|
FlashWindowEx(ref fi);
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
struct DCB
|
|
{
|
|
public int DCBlength;
|
|
public uint BaudRate;
|
|
public uint Flags;
|
|
public ushort wReserved;
|
|
public ushort XonLim;
|
|
public ushort XoffLim;
|
|
public byte ByteSize;
|
|
public byte Parity;
|
|
public byte StopBits;
|
|
public byte XonChar;
|
|
public byte XoffChar;
|
|
public byte ErrorChar;
|
|
public byte EofChar;
|
|
public byte EvtChar;
|
|
public ushort wReserved1;
|
|
}
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
static extern SafeFileHandle CreateFile(string name, uint access, uint share, IntPtr security,
|
|
uint disposition, uint flags, IntPtr template);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
static extern bool GetCommState(SafeFileHandle h, ref DCB dcb);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
static extern bool SetCommState(SafeFileHandle h, ref DCB dcb);
|
|
|
|
// Opens the serial port, sets the baud rate and closes it again. The
|
|
// bridge firmware reboots into the ROM bootloader on 1234 baud, so the
|
|
// device vanishes while the port is open: System.IO.Ports does not
|
|
// survive that, plain handles do. Returns null or the error text.
|
|
public static string PokeBaudRate(string port, uint baud)
|
|
{
|
|
using (SafeFileHandle h = CreateFile(@"\\.\" + port, 0xC0000000 /* GENERIC_READ | GENERIC_WRITE */,
|
|
0, IntPtr.Zero, 3 /* OPEN_EXISTING */, 0, IntPtr.Zero))
|
|
{
|
|
if (h.IsInvalid)
|
|
return new Win32Exception(Marshal.GetLastWin32Error()).Message;
|
|
var dcb = new DCB();
|
|
dcb.DCBlength = Marshal.SizeOf(typeof(DCB));
|
|
if (!GetCommState(h, ref dcb))
|
|
return new Win32Exception(Marshal.GetLastWin32Error()).Message;
|
|
dcb.BaudRate = baud;
|
|
if (dcb.ByteSize == 0)
|
|
dcb.ByteSize = 8;
|
|
// may report an error: the device can be gone before the call returns
|
|
SetCommState(h, ref dcb);
|
|
Thread.Sleep(200);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|