Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions oneware-extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@
"target": "all"
}
]
},
{
"version": "1.0.2",
"minStudioVersion": "1.0.19",
"targets": [
{
"target": "all"
}
]
}
]
}
59 changes: 58 additions & 1 deletion src/OneWare.Quartus/Helper/QsfFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/// <summary>
/// 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
/// </summary>
[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<string> Lines { get; private set; } = lines.ToList();

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -117,6 +126,54 @@ public void RemoveLocationAssignments()
var regex = RemoveLocationAssignmentRegex();
Lines = Lines.Where(x => !regex.IsMatch(x)).ToList();
}

// ── Instance assignments (set_instance_assignment) ────────────────────────

/// <summary>
/// Returns all <c>set_instance_assignment</c> lines parsed as
/// (Name, Value, Signal, Entity?).
/// </summary>
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);
}
}

/// <summary>
/// Writes a <c>set_instance_assignment</c> line.
/// Values that contain spaces are automatically quoted.
/// </summary>
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);
}

/// <summary>
/// Removes all <c>set_instance_assignment</c> lines whose <c>-name</c> matches
/// <paramref name="name"/> (case-insensitive).
/// </summary>
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)
{
Expand Down
6 changes: 3 additions & 3 deletions src/OneWare.Quartus/OneWare.Quartus.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Version>1.0.1</Version>
<Version>1.0.2</Version>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand All @@ -15,8 +15,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="OneWare.Essentials" Version="1.0.0" Private="false" ExcludeAssets="runtime;Native" />
<PackageReference Include="OneWare.UniversalFpgaProjectSystem" Version="1.0.0" Private="false" ExcludeAssets="runtime;Native" />
<PackageReference Include="OneWare.Essentials" Version="1.0.19" Private="false" ExcludeAssets="runtime;Native" />
<PackageReference Include="OneWare.UniversalFpgaProjectSystem" Version="1.0.19" Private="false" ExcludeAssets="runtime;Native" />
</ItemGroup>

<Target Name="GenerateCompatibilityFile" AfterTargets="Build">
Expand Down
95 changes: 87 additions & 8 deletions src/OneWare.Quartus/QuartusToolchain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";

/// <summary>
/// Per-pin properties exposed in the pin planner for every Quartus project.
/// </summary>
public IEnumerable<PinPropertyDefinition> 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
Expand All @@ -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<string, List<(string Name, string Value)>>(
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)
{
Expand All @@ -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<PinPropertyDefinition>)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)
Expand Down
92 changes: 89 additions & 3 deletions tests/OneWare.Quartus.UnitTests/OneWareQuartusTests.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading