Incomplete integration of Custom Design Rules into interactive router

I’ve setup custom design rules for a 6 layer board with controlled impedances for differential pairs (PCIe Gen 4 and DisplayPort). I had hoped this would make routing a lot more streamline, sadly, a lot of the constraint seemingly are only for DRC and are not actively enforced during interactive routing, and some are erroneous.

What does work:
All clearances with simple conditions are applied during routing. This is already a big upgrade for multiple layers compared to before custom rules existed.

Problem 1:
insideArea always returns false during interactive routing but works during DRC, which means the typical application of fanout areas are not working.
Option 1:
rule /w big clearances
rule /w small clearances limited to insideArea(‘Fanout’)
insideArea = false => small clearances rule is never matched
Option 2:
rule /w small clearances
rule /w big clearances limited to !insideArea(‘Fanout’)
!insideArea = true => big clearances rule always matched

Workaround: Disable big clearances rules for all differential pairs and for all layers while working in fanout areas.

Problem 2:
The DRC subsystem knows what min and opt track widths and gaps my differential pairs should have, but there is absolutely NO integration with the router subsystem besides the clearances.
So you still have to manually setup all possible sizes for differential pairs, and switch between them as you switch between differential pair types and most annoyingly, layers, and if you get it wrong, the DRC will complain only later.
The netclass default is also harmful here, as this would differ between layers, so everything has to be done manually.
Optimally, optimal/minimum track widths and gaps should be communicated to the router just like the clearances. Additionally, via gaps that are set up in that differential pair setup should be inferred from those parameters as well (maybe from netclass via sizes + custom rule clearances).

Problem 3:
Rounded Corner Style are great and glad they are in KiCad now, but unfortunately the differential router just ignores them.
Workaround: Fillet each corner manually at the end

Incase you need it, I can try to attach my custom rules once I have permission to. It is supposed to reflect the manufacturing capabilities of the 6-layer jlcpcb stackup JLC06121H-3313 plus PCIe Gen4 and DisplayPort differential signalling.

Maybe I should open a bug report. This issue shows it SHOULD be able to fetch the correct width and gap from the rules. The issue is about it not doing it reliably, but at the end, after doing it, it DOES fetch it and show “(from rule DP inner)”. That is completely new behaviour that I never got so something must be wrong with my setup. I am on version 7.06, they’re on 6.08, so unless there’s been a major regression, it doesn’t work for me.

Here are the rules I use for ONE differential pair type (DisplayPort), for ONE layer only:

(rule "DP Diff Uncoupled L3"
	(condition "A.inDiffPair('/DP*')")
	(layer In4.Cu)
	(constraint track_width (min 0.123mm))
	(constraint diff_pair_uncoupled (max 5.0mm)))
(rule "DP Diff Pair L3"
	(condition "A.inDiffPair('/DP*') && AB.isCoupledDiffPair()")
	(layer In4.Cu)
	(constraint track_width (min 0.106mm))
	(constraint diff_pair_gap (min 0.20mm) (opt 0.21mm)))

If you spot an error that could cause this, please let me know

For completeness, here’s what I use for clearances (where the fanout area does not work):

(rule "DP Diff Clearance Outer Fanout"
	(condition "A.Type == 'Track' && A.inDiffPair('/DP*') && B.inDiffPair('*') && !AB.isCoupledDiffPair()")
	(layer outer)
	(constraint clearance (min 0.2mm)))
(rule "DP Diff Clearance Inner Fanout"
	(condition "A.Type == 'Track' && A.inDiffPair('/DP*') && B.inDiffPair('*') && !AB.isCoupledDiffPair()")
	(layer inner)
	(constraint clearance (min 0.2mm)))

(rule "DP Diff Clearance Outer"
	(condition "A.Type == 'Track' && A.inDiffPair('/DP*') && B.inDiffPair('*') && !AB.isCoupledDiffPair() && !A.insideArea('Fanout')")
	(layer outer)
	(constraint clearance (min 1.5mm)))
(rule "DP Diff Clearance Inner"
	(condition "A.Type == 'Track' && A.inDiffPair('/DP*') && B.inDiffPair('*') && !AB.isCoupledDiffPair() && !A.insideArea('Fanout')")
	(layer inner)
	(constraint clearance (min 1.0mm)))

I found the issue for Problem 2, I had to set opt and max values in addition to the min values for both track width and gap. I think the interactive router would benefit from handling that, or at least updating the docs to make it very clear all of it is needed.

Two related problem remain, when starting from pads is takes the uncoupled rule, but it doesn’t switch to coupled rules after they are parallel. Workaround: Create a via pair with ‘V’, delete connection, start from the via again, for some reason that is immediately using the coupled rules.
The other minor problem is that unless “copy track parameters” toggle in the toolbar is set, continuing a differential pair doesn’t work, presumably it tries to use rules other than the coupled rule and thus doesn’t know the proper gap.

I’ve found that the term “uncoupled” is used for two different things in the docs…
For diff_pair_gap and diff_pair_uncoupled, the meaning is “Coupled tracks are segments that are parallel to each other.”.
For isCoupledDiffPair, it seemingly “Returns true if the two objects being tested are part of the same differential pair but are opposite polarities. For example, returns true if A is in net /USB+ and B is in net /USB-.”
This is not made very clear, so I attempted to set a different track width for when the tracks are not parallel (and thus, not coupled, in the first sense).

I merged these rules now, but isCoupledDiffPair still does NOT return true when I start from a pad (see minor issue 1 in last post). Or it just doesn’t check against the custom rules properly when starting to route - which also matches the second minor issue.
This bug has been mentioned in the previously linked issue, so perhaps it has not been fixed yet.
Maybe there’s a missing updateSizesAfterLayerSwitch call on the active layer to make sure they use the correct parameters when starting the routing.

Both items must be totally inside the area. intersectsArea may be better. However, it doesn’t change width/gap in the midst of drawing a track and crossing the line. You have to start inside the area and cut the trace just next to the border of it, then do the real fanning out.

But IMO the starting width should work automatically inside the rule area, otherwise the whole system is almost useless.

1 Like

That’s too much to say, but it’s still more clumsy than necessary. I tried with 7.99. The rules work well, but you have to change the track width manually to the narrower width. When starting a new segment, KiCad should change to “opt” width if it’s found in the rules and can applied to the segment when it’s started. Maybe this is a possible wishlist item, but we have to be careful when thinking about how the interactive router “should” work – some things may feel simple but may be logically impossible or very difficult to implement.

The arrows show the effective clearances of the tracks, and they are changed and applied when a new segment is started. It even seems to be possible to start from outside the area and continue inside it – there’s a small bug so that the gray clearance area is the larger one even when the rule area is entered, but when the track is fixed, it shows like in the screenshot.

(rule fanout1
(condition "A.Type == 'Track' && A.intersectsArea('fan')")
(constraint clearance (min 0.19mm))
)
(rule fanout1
(condition "A.Type == 'Track' && A.intersectsArea('fan')")
(constraint track_width (min 0.24mm) (opt 0.25mm) (max 0.26mm))
)
1 Like

Oh, I didn’t know about that. I’m following this documentation, which only mentions insideArea and describes it like you do intersectsArea.
Sadly, neither insideArea nor intersectsArea seem to apply to small segments fully inside the area - also when both tested are inside. Though as far as my understanding goes, if I only test A.insideArea, it should only require A to be fully (or, in that documentation version, partially) in the area.

Following is some debugging, but unfortunately I need to split it because I can only upload one image per post :confused:
If you only care about the result, I think I found the problem with the implementation, and will open an issue. Read the conclusion at the end

So I simplified my whole custom rule sheet to this:

(version 1)
(rule "Normal"
	(condition "A.Type == 'Track'")
	(constraint clearance (min 1.5mm)))
(rule "Fanout"
	(condition "A.Type == 'Track' && A.intersectsArea('Fanout')")
	(constraint clearance (min 0.2mm)))

However, the clearance of 1.5mm is applied everywhere, I cannot even start routes:


Now if I comment out the Normal rule, it DOES apply the Fanout rule, so my previous assumption that intersectsArea/insideArea returns false is not true. This is also supported by the fact that… the outline wires ARE correct! Didn’t notice before because there were so many rules.
But for some reason, Normal takes precedence for interactive routing. In the Docs it says the lower rules take precedence, since it’s evaluated bottom up.

I reversed the order, and sure enough, the outlines show that the clearances are now 1.5mm, and the interactive router agrees:


SO, new conjecture: The interactive router somehow skips rules with area-based commands.

Aaaand that reminds me of the code I read yesterday, and it becomes clear. updateSizesAfterLayerSwitch (which I assume is the code also used for other interactive routing tasks) creates a dummy track to test on, but seemingly does NOT put the location where I actually want to route, so all rules in the interactive router are completely location-unaware. Yikes.

This should be the cause for most issues I’m seeing then - so I made another test.

(version 1)
(rule "Normal"
	(condition "A.Type == 'Track'")
	(constraint track_width (min 0.2mm) (opt 0.2mm) (max 0.2mm))
	(constraint clearance (min 0.3mm)))
(rule "Fanout"
	(condition "A.Type == 'Track' && A.intersectsArea('Fanout')")
	(constraint track_width (min 0.3mm) (opt 0.3mm) (max 0.3mm))
	(constraint clearance (min 0.2mm)))

The netclass values are set to 0.1mm track width and 0.1mm clearance.
And then started a differential pair from a pad as you see here:


And voila, the toolbar shows correct values, but the routing chose the netclass values, despite them violating custom rules. In fact, I cannot continue that differential pair, because then it DOES seem to check for the rules properly.

I then tested my previous workaround and placed a via immediately after the pads. Now ofc the vias are also violating DRC, so I had to place them farther apart manually, so I could start from these vias - and it still defaults to the netclass values:

Then, pressing + and then - to switch layers back to the top layers, it finally does trigger a custom rule check (updateSizesAfterLayerSwitch), but as is set in the code, it only reads the track width and differential pair gap.


Well - I didn’t set up a differential pair gap, so it used the default I set in the netclass, 0.1mm - and that violates the clearance, which has NOT been updated.

Adding a gap of 0.3mm works, but since it doesn’t update the clearance, it DOES let me route in a way that violates the clearance (0.2mm for are rule, 0.3mm for normal rule, but it uses netclass of 0.1mm):

So to conclude:
updateSizesAfterLayerSwitch is the key. But currently, it is both insufficient in two ways, and not called often enough.
It needs to be called whenever a routing starts, so that it is updated to the current track circumstances, current layer and current area rules. And it does need to check the current location with the dummy track, not just any part of the layer. And finally, it needs to update ALL values relevant to routing, and should also mitigate conflicts (e.g. differential gap < clearance). I do not know enough to understand how this is handled currently, sadly, e.g. in regards to updating the clearance in regards to other elements - I doubt they check all possible combinations (e.g. this track to via of every type, of pads, etc.), so perhaps there is more work to be done checking the current track whenever such a situation occurs. Or it indeed needs to update the nearby elements with a temporary clearance for these specific circumstances (which it might do already, just wrong?). I will open an issue. (EDIT: Here it is)

2 Likes

Now when you mention, I was partially wrong. It was originally “insideArea” which was deprecated in favor of “intersectsArea” and “enclosedByArea” because the first one was ambiguous and did only one thing – probably it’s synonym for intersectsArea.

That’s what I would assume, too.

Notice that I tried with the development version 7.99 which may have enhancements compared with the stable 7.0.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.