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