diff --git a/oneware-extension.json b/oneware-extension.json
index f191484..ebd5761 100644
--- a/oneware-extension.json
+++ b/oneware-extension.json
@@ -86,6 +86,15 @@
"target": "all"
}
]
+ },
+ {
+ "version": "1.0.2",
+ "minStudioVersion": "1.0.19",
+ "targets": [
+ {
+ "target": "all"
+ }
+ ]
}
]
}
diff --git a/src/OneWare.Quartus/Helper/QsfFile.cs b/src/OneWare.Quartus/Helper/QsfFile.cs
index 074e064..d289d58 100644
--- a/src/OneWare.Quartus/Helper/QsfFile.cs
+++ b/src/OneWare.Quartus/Helper/QsfFile.cs
@@ -14,6 +14,15 @@ public partial class QsfFile(string[] lines)
[GeneratedRegex(@"set_global_assignment\s-name\s\w+_FILE\s(.+)")]
private static partial Regex RemoveFileAssignmentRegex();
+
+ ///
+ /// Matches: set_instance_assignment -name NAME "quoted value" -to signal [-entity entity]
+ /// or: set_instance_assignment -name NAME unquoted -to signal [-entity entity]
+ /// Groups: 1=name, 2=quoted-value (may be empty), 3=unquoted-value, 4=signal, 5=entity
+ ///
+ [GeneratedRegex(@"set_instance_assignment\s+-name\s+(\w+)\s+(?:""([^""]*)""|(\S+))\s+-to\s+(\w+)(?:\s+-entity\s+(\w+))?",
+ RegexOptions.IgnoreCase)]
+ private static partial Regex InstanceAssignmentRegex();
public List Lines { get; private set; } = lines.ToList();
@@ -89,7 +98,7 @@ public void RemoveGlobalAssignment(string name)
public IEnumerable<(string,string)> GetLocationAssignments()
{
- foreach (var line in lines)
+ foreach (var line in Lines)
{
var match = LocationAssignmentRegex().Match(line);
if (!match.Success) continue;
@@ -117,6 +126,54 @@ public void RemoveLocationAssignments()
var regex = RemoveLocationAssignmentRegex();
Lines = Lines.Where(x => !regex.IsMatch(x)).ToList();
}
+
+ // ── Instance assignments (set_instance_assignment) ────────────────────────
+
+ ///
+ /// Returns all set_instance_assignment lines parsed as
+ /// (Name, Value, Signal, Entity?).
+ ///
+ public IEnumerable<(string Name, string Value, string Signal, string? Entity)> GetInstanceAssignments()
+ {
+ foreach (var line in Lines)
+ {
+ var match = InstanceAssignmentRegex().Match(line);
+ if (!match.Success) continue;
+
+ var name = match.Groups[1].Value;
+ // Group 2 = quoted value, group 3 = unquoted value
+ var value = match.Groups[2].Success ? match.Groups[2].Value : match.Groups[3].Value;
+ var signal = match.Groups[4].Value;
+ var entity = match.Groups[5].Success ? match.Groups[5].Value : (string?)null;
+
+ yield return (name, value, signal, entity);
+ }
+ }
+
+ ///
+ /// Writes a set_instance_assignment line.
+ /// Values that contain spaces are automatically quoted.
+ ///
+ public void AddInstanceAssignment(string name, string value, string signal, string? entity = null)
+ {
+ var quotedValue = value.Contains(' ') ? $"\"{value}\"" : value;
+ var line = $"set_instance_assignment -name {name} {quotedValue} -to {signal}";
+ if (entity != null) line += $" -entity {entity}";
+ Lines.Add(line);
+ }
+
+ ///
+ /// Removes all set_instance_assignment lines whose -name matches
+ /// (case-insensitive).
+ ///
+ public void RemoveInstanceAssignmentsByName(string name)
+ {
+ // Match the property name as a whole word after -name
+ var regex = new Regex(
+ @$"set_instance_assignment\s.*-name\s+{Regex.Escape(name)}(\s|$)",
+ RegexOptions.IgnoreCase);
+ Lines = Lines.Where(x => !regex.IsMatch(x)).ToList();
+ }
public void AddFile(string relativePath)
{
diff --git a/src/OneWare.Quartus/OneWare.Quartus.csproj b/src/OneWare.Quartus/OneWare.Quartus.csproj
index ed9adf5..19f0d9d 100644
--- a/src/OneWare.Quartus/OneWare.Quartus.csproj
+++ b/src/OneWare.Quartus/OneWare.Quartus.csproj
@@ -1,7 +1,7 @@
- 1.0.1
+ 1.0.2
net10.0
enable
enable
@@ -15,8 +15,8 @@
-
-
+
+
diff --git a/src/OneWare.Quartus/QuartusToolchain.cs b/src/OneWare.Quartus/QuartusToolchain.cs
index 417a79e..fbfeaac 100644
--- a/src/OneWare.Quartus/QuartusToolchain.cs
+++ b/src/OneWare.Quartus/QuartusToolchain.cs
@@ -2,6 +2,7 @@
using OneWare.Essentials.Services;
using OneWare.Quartus.Helper;
using OneWare.Quartus.Services;
+using OneWare.UniversalFpgaProjectSystem.Fpga;
using OneWare.UniversalFpgaProjectSystem.Models;
using OneWare.UniversalFpgaProjectSystem.Parser;
using OneWare.UniversalFpgaProjectSystem.Services;
@@ -11,11 +12,44 @@ namespace OneWare.Quartus;
public class QuartusToolchain(QuartusService quartusService, ILogger logger) : IFpgaToolchain
{
public const string ToolchainId = "quartus";
-
+
public string Id => ToolchainId;
public string Name => "Quartus";
+ ///
+ /// Per-pin properties exposed in the pin planner for every Quartus project.
+ ///
+ public IEnumerable PinProperties =>
+ [
+ new PinPropertyDefinition(
+ "IO_STANDARD",
+ "IO Standard",
+ PinPropertyType.ComboBox,
+ [
+ "",
+ "1.0 V",
+ "1.2 V",
+ "1.5 V",
+ "1.8 V",
+ "2.5 V",
+ "3.3-V LVCMOS",
+ "LVTTL",
+ "LVCMOS",
+ "LVDS",
+ "LVDS_E_3R",
+ "True Differential Signaling",
+ "High Speed Differential I/O",
+ "Differential 1.8-V SSTL Class I",
+ "Differential 1.8-V SSTL Class II"
+ ]),
+ new PinPropertyDefinition(
+ "WEAK_PULL_UP_RESISTOR",
+ "Weak Pull-Up",
+ PinPropertyType.ComboBox,
+ ["", "ON", "OFF"])
+ ];
+
public void OnProjectCreated(UniversalFpgaProjectRoot project)
{
//TODO Add gitignore defaults
@@ -27,15 +61,37 @@ public void LoadConnections(UniversalFpgaProjectRoot project, FpgaModel fpga)
{
var qsfPath = QsfHelper.GetQsfPath(project);
var qsf = QsfHelper.ReadQsf(qsfPath);
-
+
+ // Step 1: load pin ↔ node location assignments
foreach (var (pin, node) in qsf.GetLocationAssignments())
{
+ if (!fpga.PinModels.TryGetValue(pin, out var pinModel)) continue;
+ if (!fpga.NodeModels.TryGetValue(node, out var nodeModel)) continue;
- if(!fpga.PinModels.TryGetValue(pin, out var pinModel)) return;
- if(!fpga.NodeModels.TryGetValue(node, out var nodeModel)) return;
-
fpga.Connect(pinModel, nodeModel);
}
+
+ // Step 2: load per-pin instance assignments (IO_STANDARD, WEAK_PULL_UP_RESISTOR, …)
+ // Keyed by node name → list of (propertyName, value)
+ var instanceAssignments = new Dictionary>(
+ StringComparer.OrdinalIgnoreCase);
+
+ foreach (var (name, value, signal, _) in qsf.GetInstanceAssignments())
+ {
+ if (!instanceAssignments.TryGetValue(signal, out var list))
+ instanceAssignments[signal] = list = new List<(string, string)>();
+ list.Add((name, value));
+ }
+
+ // Apply instance assignments to the corresponding pin model (via connected node)
+ foreach (var nodeModel in fpga.NodeModels.Values)
+ {
+ if (nodeModel.ConnectedPin == null) continue;
+ if (!instanceAssignments.TryGetValue(nodeModel.Node.Name, out var assignments)) continue;
+
+ foreach (var (name, value) in assignments)
+ nodeModel.ConnectedPin.SetPinPropertyValue(name, value);
+ }
}
catch (Exception e)
{
@@ -47,16 +103,39 @@ public void SaveConnections(UniversalFpgaProjectRoot project, FpgaModel fpga)
{
try
{
+ var topEntity = project.TopEntity != null
+ ? Path.GetFileNameWithoutExtension(project.TopEntity)
+ : null;
+
var qsfPath = QsfHelper.GetQsfPath(project);
var qsf = QsfHelper.ReadQsf(qsfPath);
- //Add Connections
+ // ── Location assignments ──────────────────────────────────────────
qsf.RemoveLocationAssignments();
foreach (var (_, pinModel) in fpga.PinModels.Where(x => x.Value.ConnectedNode != null))
- {
qsf.AddLocationAssignment(pinModel.Pin.Name, pinModel.ConnectedNode!.Node.Name);
+
+ // ── Instance assignments (per-pin properties) ─────────────────────
+ // Prefer properties declared in hardware JSON; fall back to toolchain's own list
+ var effectiveProperties = fpga.Fpga.AllowedPinProperties.Count > 0
+ ? (IEnumerable)fpga.Fpga.AllowedPinProperties
+ : PinProperties;
+
+ // Remove old managed assignments before re-adding
+ foreach (var propDef in effectiveProperties)
+ qsf.RemoveInstanceAssignmentsByName(propDef.Key);
+
+ foreach (var (_, pinModel) in fpga.PinModels.Where(x => x.Value.ConnectedNode != null))
+ {
+ var nodeName = pinModel.ConnectedNode!.Node.Name;
+ foreach (var propDef in effectiveProperties)
+ {
+ var value = pinModel.GetPinPropertyValue(propDef.Key);
+ if (!string.IsNullOrEmpty(value))
+ qsf.AddInstanceAssignment(propDef.Key, value, nodeName, topEntity);
+ }
}
-
+
QsfHelper.WriteQsf(qsfPath, qsf);
}
catch (Exception e)
diff --git a/tests/OneWare.Quartus.UnitTests/OneWareQuartusTests.cs b/tests/OneWare.Quartus.UnitTests/OneWareQuartusTests.cs
index 8dea010..2383d4d 100644
--- a/tests/OneWare.Quartus.UnitTests/OneWareQuartusTests.cs
+++ b/tests/OneWare.Quartus.UnitTests/OneWareQuartusTests.cs
@@ -1,14 +1,100 @@
-using Xunit;
+using System.Collections.Generic;
+using System.Linq;
+using OneWare.Quartus.Helper;
+using Xunit;
namespace OneWare.Quartus.UnitTests;
public class OneWareQuartusTests
{
- //Add your unit tests here
-
[Fact]
public void LoadLibrary()
{
Assert.True(true);
}
+
+ // ── QsfFile: instance assignments ─────────────────────────────────────────
+
+ [Fact]
+ public void QsfFile_AddAndGetInstanceAssignment_QuotedValue()
+ {
+ var qsf = new QsfFile([]);
+ qsf.AddInstanceAssignment("IO_STANDARD", "3.3-V LVCMOS", "led0", "top");
+
+ var assignments = qsf.GetInstanceAssignments().ToList();
+ Assert.Single(assignments);
+ Assert.Equal("IO_STANDARD", assignments[0].Name);
+ Assert.Equal("3.3-V LVCMOS", assignments[0].Value);
+ Assert.Equal("led0", assignments[0].Signal);
+ Assert.Equal("top", assignments[0].Entity);
+ }
+
+ [Fact]
+ public void QsfFile_AddAndGetInstanceAssignment_UnquotedValue()
+ {
+ var qsf = new QsfFile([]);
+ qsf.AddInstanceAssignment("WEAK_PULL_UP_RESISTOR", "ON", "btn0");
+
+ var assignments = qsf.GetInstanceAssignments().ToList();
+ Assert.Single(assignments);
+ Assert.Equal("WEAK_PULL_UP_RESISTOR", assignments[0].Name);
+ Assert.Equal("ON", assignments[0].Value);
+ Assert.Equal("btn0", assignments[0].Signal);
+ Assert.Null(assignments[0].Entity);
+ }
+
+ [Fact]
+ public void QsfFile_ParseExistingInstanceAssignmentLines()
+ {
+ var qsf = new QsfFile([
+ "set_instance_assignment -name IO_STANDARD \"1.2 V\" -to clk -entity seg_test",
+ "set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to io96_3a_pb0 -entity seg_test",
+ "set_instance_assignment -name IO_STANDARD \"3.3-V LVCMOS\" -to io96_3a_led0 -entity seg_test"
+ ]);
+
+ var assignments = qsf.GetInstanceAssignments().ToList();
+ Assert.Equal(3, assignments.Count);
+
+ Assert.Equal(("IO_STANDARD", "1.2 V", "clk", "seg_test"), assignments[0]);
+ Assert.Equal(("WEAK_PULL_UP_RESISTOR", "ON", "io96_3a_pb0", "seg_test"), assignments[1]);
+ Assert.Equal(("IO_STANDARD", "3.3-V LVCMOS", "io96_3a_led0", "seg_test"), assignments[2]);
+ }
+
+ [Fact]
+ public void QsfFile_RemoveInstanceAssignmentsByName_RemovesOnlyMatchingName()
+ {
+ var qsf = new QsfFile([
+ "set_instance_assignment -name IO_STANDARD \"3.3-V LVCMOS\" -to led0 -entity top",
+ "set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to btn0 -entity top",
+ "set_instance_assignment -name IO_STANDARD \"1.2 V\" -to clk -entity top"
+ ]);
+
+ qsf.RemoveInstanceAssignmentsByName("IO_STANDARD");
+
+ var remaining = qsf.GetInstanceAssignments().ToList();
+ Assert.Single(remaining);
+ Assert.Equal("WEAK_PULL_UP_RESISTOR", remaining[0].Name);
+ }
+
+ [Fact]
+ public void QsfFile_RoundTrip_LocationAndInstanceAssignments()
+ {
+ var qsf = new QsfFile([]);
+ // Simulate SaveConnections output
+ qsf.AddLocationAssignment("AG21", "led");
+ qsf.AddInstanceAssignment("IO_STANDARD", "3.3-V LVCMOS", "led", "top");
+ qsf.AddLocationAssignment("AD5", "clk");
+ qsf.AddInstanceAssignment("IO_STANDARD", "1.2 V", "clk", "top");
+
+ // Re-read via GetLocationAssignments / GetInstanceAssignments
+ var locations = qsf.GetLocationAssignments().ToList();
+ Assert.Equal(2, locations.Count);
+ Assert.Contains(("AG21", "led"), locations);
+ Assert.Contains(("AD5", "clk"), locations);
+
+ var instances = qsf.GetInstanceAssignments().ToList();
+ Assert.Equal(2, instances.Count);
+ Assert.Contains(instances, i => i.Name == "IO_STANDARD" && i.Value == "3.3-V LVCMOS" && i.Signal == "led");
+ Assert.Contains(instances, i => i.Name == "IO_STANDARD" && i.Value == "1.2 V" && i.Signal == "clk");
+ }
}
\ No newline at end of file