Learning to Develop KiCad (4) - analysis of length calculation of Length Tuning Tool

All my series of “Learning to Develop KiCad” posts are listed in FAQ

Index:

Following is a translation of my Chinese article

commit revision: 9db1dd5ec5f20d301af076301b8853b7a133eed5

Calling Traces

When activated Length Tuning Tool, via the calling to Go inside DRAWING_TOOL::setTransitions (pcbnew/tools/drawing_tool.cpp), bound to DRAWING_TOOL::PlaceTuningPattern, in which there is an event loop handler.
When a cursor hovers over a track, Length Tuning Tool shows the length of the whole track, the source code goes here ( still inside DRAWING_TOOL::PlaceTuningPattern):

if( evt->IsMotion ) {
// omitted code ...
    updateHoverStatus();
} // omitted more code

Inside updateHoverStatus(), by calling PCB_TUNING_PATTERN::Update(), the code goes into

router->StartRouting()

it will go (for single traces) inside pcbnew/router/pns_meander_placer.cpp

MEANDER_PLACER::Start

Now, it goes into the core algorithm of length calculation.

Analysis of algorithm of length calculation

The core code is here in pcbnew/router/pns_meander_placer.cpp

TOPOLOGY topo( m_world ); 
m_tunedPath = topo.AssembleTuningPath( m_initialSegment, &m_startPad_n, &m_endPad_n );

Inheritance Relationships

In source code, by calling:

PNS::ITEM::Kind()

to determine the actual object type of current PNS::ITEM * pointer.

PNS::ITEM

Base class for PNS router board items.
Implements the shared properties of all PCB items net, spanned layers, geometric shape and reference to owning model.

PNS::NODE

pcbnew/router/pns_node.h

Keep the router “world” - i.e. all the tracks, vias, solids in a hierarchical and indexed way.

image

PNS::JOINT

A 2D point on a given set of layers and belonging to a certain net, that links together a number of board items. A hash table of joints is used by the router to follow connectivity between the items.

VIAs, PADs, junction of two segments all can be a JOINT. It defines multiple methods which can determine the type of this JOINT.

  96     /**
  97      * Checks if a joint connects two segments of the same net, layer, and width.
  98      * @param aAllowLockedSegs will consider joints between locked and unlocked segments as trivial
  99      * @return true if the joint is a trivial line corner
 100      */
 101     bool IsLineCorner( bool aAllowLockedSegs = false ) const;
 150     bool IsNonFanoutVia() const;
 172     bool IsStitchingVia() const;
 177     bool IsTraceWidthChange() const;

PNS::LINE

Represents a track on a PCB, connecting two non-trivial joints (that is, vias, pads, junctions between multiple traces or two traces different widths and combinations of these). PNS_LINEs are NOT stored in the model (NODE). Instead, they are assembled on-the-fly, based on a via/pad/segment that belongs to/starts/ends them.
A LINE may have a VIA attached at its end (i.e. the last point) - this is used by via dragging/force propagation stuff.

PNS::TOPOLOGY

TOPOLOGY is used to calculate topology properties of a NODE (world).

PNS::TOPOLOGY::AssembleTrivialPath

Assemble a trivial path between two joints given a starting item

Analysis

Sample PCB

Simplified content of the PCB file


(net 0 "")
(footprint "TestPoint:TestPoint_Pad_D4.0mm"
    (at 113.34 54.19)
    (pad "1" smd circle
           (at 0 0)
           (size 4 4)
           (layers "F.Cu" "F.Mask")
           (uuid "6baca6a6-e3ec-422e-8255-1ec74843a0d0")
    )
)
(footprint "TestPoint:TestPoint_Pad_D4.0mm"
    (layer "F.Cu")
    (uuid "a1b08514-55a3-404d-8cf1-e425fa312934")
    (at 129.07 54.21)
    (pad "1" smd circle
            (at 0 0)
            (size 4 4)
            (layers "F.Cu" "F.Mask")
            (uuid "efe3ecd5-3fc1-4341-b837-57f85992cbfe")
    )
)

    (segment
        (start 124.14 54.21)
        (end 129.07 54.21)
        (width 0.2)
        (layer "F.Cu")
        (net 0)
        (uuid "14e13181-32a3-4a99-92ee-58914e592582")
    )
    (segment
        (start 113.34 54.19)
        (end 117.7 54.19)
        (width 0.2)
        (layer "F.Cu")
        (net 0)
        (uuid "7e88cb31-5757-4ba5-8a5f-ddf7daf03ebf")
    )
    (segment
        (start 117.7 54.19)
        (end 117.71 54.2)
        (width 0.2)
        (layer "F.Cu")
        (net 0)
        (uuid "98dd8262-83c0-425a-9f5d-b12bb95a1462")
    )
    (via
        (at 117.71 54.2)
        (size 0.6)
        (drill 0.3)
        (layers "F.Cu" "B.Cu")
        (net 0)
        (uuid "40af5aa0-cd97-40bd-94fe-1d4d088d00be")
    )
    (via
        (at 124.14 54.21)
        (size 0.6)
        (drill 0.3)
        (layers "F.Cu" "B.Cu")
        (net 0)
        (uuid "674e89f5-457e-46a1-86c6-b68c22d485b1")
    )
    (segment
        (start 117.71 54.2)
        (end 124.13 54.2)
        (width 0.2)
        (layer "B.Cu")
        (net 0)
        (uuid "4140aa44-32a2-49f1-a53c-69d33340e76c")
    )
    (segment
        (start 124.13 54.2)
        (end 124.14 54.21)
        (width 0.2)
        (layer "B.Cu")
        (net 0)
        (uuid "7df491dc-65ca-42dc-bb4c-245e1d145486")
    )

The details of the file format see this doc.

Starting pcbnew by GDB, and set a breakpoint at TOPOLOGY::AssembleTuningPath

Then activated Length Tuning Tool, hover the cursor over the center track like this:

It will break at the breakpoint:


(gdb) list 332
327
328         return path;
329     }
330
331
332     const ITEM_SET TOPOLOGY::AssembleTuningPath( ITEM* aStart, SOLID** aStartPad, SOLID** aEndPad )
333     {
334         std::pair<const JOINT*, const JOINT*> joints;
335         ITEM_SET initialPath = AssembleTrivialPath( aStart, &joints, true );
336
(gdb) p *aStart
$2 = {<PNS::OWNABLE_ITEM> = {m_owner = 0x5555579ff000}, <PNS::ITEM_OWNER> = {_vptr.ITEM_OWNER = 0x7fffead8ef80 <vtable for PNS::SEGMENT+16>},
  m_kind = PNS::ITEM::SEGMENT_T, m_parent = 0x555556932280, m_layers = {m_start = 31, m_end = 31}, m_movable = true, m_net = 0x55555694e980, m_marker = 0,
  m_rank = -1, m_routable = true, m_isVirtual = false, m_isFreePad = false, m_isCompoundShapePrimitive = false}
(gdb) p *(SEGMENT *)aStart
$3 = {<PNS::LINKED_ITEM> = {<PNS::ITEM> = {<PNS::OWNABLE_ITEM> = {m_owner = 0x5555579ff000}, <PNS::ITEM_OWNER> = {
        _vptr.ITEM_OWNER = 0x7fffead8ef80 <vtable for PNS::SEGMENT+16>}, m_kind = PNS::ITEM::SEGMENT_T, m_parent = 0x555556932280, m_layers = {m_start = 31,
        m_end = 31}, m_movable = true, m_net = 0x55555694e980, m_marker = 0, m_rank = -1, m_routable = true, m_isVirtual = false, m_isFreePad = false,
      m_isCompoundShapePrimitive = false}, <No data fields>}, m_seg = {<SHAPE> = {<SHAPE_BASE> = {
        _vptr.SHAPE_BASE = 0x7fffeadec950 <vtable for SHAPE_SEGMENT+16>, m_type = SH_SEGMENT}, static MIN_PRECISION_IU = 4}, m_seg = {A = {x = 117710000,
        y = 54200000}, B = {x = 124130000, y = 54200000}, m_index = -1}, m_width = 200000}}

According to the value of m_seg, we can match inside data in kicad pcb file

 (segment
        (start 117.71 54.2)
        (end 124.13 54.2)
        (width 0.2)
        (layer "B.Cu")
        (net 0)
        (uuid "4140aa44-32a2-49f1-a53c-69d33340e76c")
    )

Step over statement, goes into TOPOLOGY::AssembleTrivialPath


(gdb) list
275
276     const ITEM_SET TOPOLOGY::AssembleTrivialPath( ITEM* aStart,
277                                                   std::pair<const JOINT*, const JOINT*>* aTerminalJoints,
278                                                   bool aFollowLockedSegments )
279     {
280         ITEM_SET        path;
281         LINKED_ITEM*    seg = nullptr;
282
283         if( aStart->Kind() == ITEM::VIA_T )
284         {
(gdb) n
281         LINKED_ITEM*    seg = nullptr;
(gdb)
283         if( aStart->Kind() == ITEM::VIA_T )
(gdb)
302         else if( aStart->OfKind( ITEM::SEGMENT_T | ITEM::ARC_T ) )
(gdb)
304             seg = static_cast<LINKED_ITEM*>( aStart );
(gdb)
307         if( !seg )

Now it is the simplest situation of Length Tuning Tool, which selected one segment.
Next, goes into NODE::AssembleLine

PNS::NODE::AssembleLine

Follow the joint map to assemble a line connecting two non-trivial joints starting from segment aSeg.
It first defines MaxVerts, which limits the max vertex which the algorithm can handle.

int i_start = MaxVerts / 2;
int i_end   = i_start + 1;

It shows, the algorithm constructs an array of a PNS::LINE, starts at the center of the array. The array defines left is from the i_start to 0, right is from i_end.

PNS::NODE::followLine

Scan the joint map, forming a line starting from segment (current)


(gdb) list
958
959     void NODE::followLine( LINKED_ITEM* aCurrent, bool aScanDirection, int& aPos, int aLimit,
960                            VECTOR2I* aCorners, LINKED_ITEM** aSegments, bool* aArcReversed,
961                            bool& aGuardHit, bool aStopAtLockedJoints, bool aFollowLockedSegments )
962     {
963         bool prevReversed = false;
964
965         const VECTOR2I guard = aCurrent->Anchor( aScanDirection );
966
967         for( int count = 0 ; ; ++count )

Here the algorithm starts at one point of the segment, scans from the center of the array, along with one direction.

Before we see PNS::SEGMENT has two points: A, B, here aCurrent is one PNS::SEGMENT, aCurrent->Anchor() calls implementation defines in pns_segment.h, ie, when aScanDirector == false, returns A, else returns B:


 107     virtual VECTOR2I Anchor( int n ) const override 52 b.refs|1 ref
 108     {
 109         if( n == 0 )
 110             return m_seg.GetSeg().A;
 111         else
 112             return m_seg.GetSeg().B;
 113     }

Next is the actual algorithm of construction of the scan.

(gdb) l 974
967         for( int count = 0 ; ; ++count )
968         {
969             const VECTOR2I p  = aCurrent->Anchor( aScanDirection ^ prevReversed );
970             const JOINT*   jt = FindJoint( p, aCurrent );
971
972             wxCHECK( jt, /* void */ );
973
974             aCorners[aPos]     = jt->Pos();
975             aSegments[aPos]    = aCurrent;
976             aArcReversed[aPos] = false;
977
978             if( aCurrent->Kind() == ITEM::ARC_T )
979             {
980                 if( ( aScanDirection && jt->Pos() == aCurrent->Anchor( 0 ) )
981                         || ( !aScanDirection && jt->Pos() == aCurrent->Anchor( 1 ) ) )

NODE::JOINT


1206    const JOINT* NODE::FindJoint( const VECTOR2I& aPos, int aLayer, NET_HANDLE aNet ) const
1207    {
1208        JOINT::HASH_TAG tag;
1209
1210        tag.net = aNet;
1211        tag.pos = aPos;
1212
1213        JOINT_MAP::const_iterator f = m_joints.find( tag ), end = m_joints.end();
1214
1215        if( f == end && !isRoot() )
1216        {
1217            end = m_root->m_joints.end();
1218            f = m_root->m_joints.find( tag );    // m_root->FindJoint(aPos, aLayer, aNet);
1219        }
1220
1221        if( f == end )
1222            return nullptr;
1223
1224        while( f != end )
1225        {
1226            if( f->second.Layers().Overlaps( aLayer ) )
1227                return &f->second;
1228
1229            ++f;
1230        }
1231
1232        return nullptr;
1233    }

I didn’t research how m_joints constructed.
When executed at line number 1221, the result of f is:


(gdb) p f->first
$21 = {pos = {x = 117710000, y = 54200000}, net = 0x55555694e980}

(gdb) p (JOINT)f->second
$50 = {<PNS::ITEM> = {<PNS::OWNABLE_ITEM> = {m_owner = 0x0}, <PNS::ITEM_OWNER> = {_vptr.ITEM_OWNER = 0x7fffead8f258 <vtable for PNS::JOINT+16>},
    m_kind = PNS::ITEM::JOINT_T, m_parent = 0x0, m_layers = {m_start = 0, m_end = 31}, m_movable = true, m_net = 0x0, m_marker = 0, m_rank = -1,
    m_routable = true, m_isVirtual = false, m_isFreePad = false, m_isCompoundShapePrimitive = false}, m_tag = {pos = {x = 117710000, y = 54200000},
    net = 0x55555694e980}, m_linkedItems = {<PNS::ITEM_OWNER> = {_vptr.ITEM_OWNER = 0x7fffead8eef0 <vtable for PNS::ITEM_SET+16>},
    m_items = std::vector of length 3, capacity 4 = {0x55555f2502d0, 0x555555c55900, 0x55555f1ee550}}, m_locked = false}
(gdb) p *(((JOINT)f->second).m_linkedItems.m_items[0])
$42 = {<PNS::OWNABLE_ITEM> = {m_owner = 0x5555579ff000}, <PNS::ITEM_OWNER> = {_vptr.ITEM_OWNER = 0x7fffead8ef80 <vtable for PNS::SEGMENT+16>},
  m_kind = PNS::ITEM::SEGMENT_T, m_parent = 0x55555f1c7de0, m_layers = {m_start = 0, m_end = 0}, m_movable = true, m_net = 0x55555694e980, m_marker = 0,
  m_rank = -1, m_routable = true, m_isVirtual = false, m_isFreePad = false, m_isCompoundShapePrimitive = false}
(gdb) p *(((JOINT)f->second).m_linkedItems.m_items[1])
$43 = {<PNS::OWNABLE_ITEM> = {m_owner = 0x5555579ff000}, <PNS::ITEM_OWNER> = {_vptr.ITEM_OWNER = 0x7fffead90258 <vtable for PNS::VIA+16>},
  m_kind = PNS::ITEM::VIA_T, m_parent = 0x55555f085f50, m_layers = {m_start = 0, m_end = 31}, m_movable = true, m_net = 0x55555694e980, m_marker = 0,
  m_rank = -1, m_routable = true, m_isVirtual = false, m_isFreePad = false, m_isCompoundShapePrimitive = false}
(gdb) p *(((JOINT)f->second).m_linkedItems.m_items[2])
$44 = {<PNS::OWNABLE_ITEM> = {m_owner = 0x5555579ff000}, <PNS::ITEM_OWNER> = {_vptr.ITEM_OWNER = 0x7fffead8ef80 <vtable for PNS::SEGMENT+16>},
  m_kind = PNS::ITEM::SEGMENT_T, m_parent = 0x555556932280, m_layers = {m_start = 31, m_end = 31}, m_movable = true, m_net = 0x55555694e980, m_marker = 0,
  m_rank = -1, m_routable = true, m_isVirtual = false, m_isFreePad = false, m_isCompoundShapePrimitive = false}

Print again after type conversion:

(gdb) p *(((JOINT)f->second).m_linkedItems.m_items[0])
$42 = {<PNS::OWNABLE_ITEM> = {m_owner = 0x5555579ff000}, <PNS::ITEM_OWNER> = {_vptr.ITEM_OWNER = 0x7fffead8ef80 <vtable for PNS::SEGMENT+16>},
  m_kind = PNS::ITEM::SEGMENT_T, m_parent = 0x55555f1c7de0, m_layers = {m_start = 0, m_end = 0}, m_movable = true, m_net = 0x55555694e980, m_marker = 0,
  m_rank = -1, m_routable = true, m_isVirtual = false, m_isFreePad = false, m_isCompoundShapePrimitive = false}
(gdb) p *(((JOINT)f->second).m_linkedItems.m_items[1])
$43 = {<PNS::OWNABLE_ITEM> = {m_owner = 0x5555579ff000}, <PNS::ITEM_OWNER> = {_vptr.ITEM_OWNER = 0x7fffead90258 <vtable for PNS::VIA+16>},
  m_kind = PNS::ITEM::VIA_T, m_parent = 0x55555f085f50, m_layers = {m_start = 0, m_end = 31}, m_movable = true, m_net = 0x55555694e980, m_marker = 0,
  m_rank = -1, m_routable = true, m_isVirtual = false, m_isFreePad = false, m_isCompoundShapePrimitive = false}
(gdb) p *(((JOINT)f->second).m_linkedItems.m_items[2])
$44 = {<PNS::OWNABLE_ITEM> = {m_owner = 0x5555579ff000}, <PNS::ITEM_OWNER> = {_vptr.ITEM_OWNER = 0x7fffead8ef80 <vtable for PNS::SEGMENT+16>},
  m_kind = PNS::ITEM::SEGMENT_T, m_parent = 0x555556932280, m_layers = {m_start = 31, m_end = 31}, m_movable = true, m_net = 0x55555694e980, m_marker = 0,
  m_rank = -1, m_routable = true, m_isVirtual = false, m_isFreePad = false, m_isCompoundShapePrimitive = false}

Now we match kicad pcb file, the three items are:


    (segment
        (start 117.7 54.19)
        (end 117.71 54.2)
        (width 0.2)
        (layer "F.Cu")
        (net 0)
        (uuid "98dd8262-83c0-425a-9f5d-b12bb95a1462")
    )

    (via
        (at 117.71 54.2)
        (size 0.6)
        (drill 0.3)
        (layers "F.Cu" "B.Cu")
        (net 0)
        (uuid "40af5aa0-cd97-40bd-94fe-1d4d088d00be")
    )

    (segment
        (start 117.71 54.2)
        (end 124.13 54.2)
        (width 0.2)
        (layer "B.Cu")
        (net 0)
        (uuid "4140aa44-32a2-49f1-a53c-69d33340e76c")
    )

Matching to PCB here

Save current JOINT information, and advanced one position ( forward +1, backward -1 ):

 974         aCorners[aPos]     = jt->Pos();
 975         aSegments[aPos]    = aCurrent;
 976         aArcReversed[aPos] = false;
 985         aPos += ( aScanDirection ? 1 : -1 );
  998         if( locked || !jt->IsLineCorner( aFollowLockedSegments ) || aPos < 0 || aPos == aLimit )
  999             break;

Here it checks jt is not LineCorner, then break ( because PNS::LINE is a line which connected two ‘non trivial’ line).
And here because IsLineCorener, the algorithm stops constructing more lines when more than two traces conntected to one JOINT


 1028     followLine( aSeg, false, i_start, MaxVerts, corners.data(), segs.data(), arcReversed.data(),
 1029                 guardHit, aStopAtLockedJoints, aFollowLockedSegments );
 1030
 1031     if( !guardHit )
 1032     {
 1033         followLine( aSeg, true, i_end, MaxVerts, corners.data(), segs.data(), arcReversed.data(),
 1034                     guardHit, aStopAtLockedJoints, aFollowLockedSegments );
 1035     }

Because guardHit is not set to true, now execute second followLine to find the other end (JOINT) of the LINE.

In the end, these ITEMs are selected

After two executions of followLine, then the algorithm will construct the final LINE (variable named pl) by returned corners and segments.

 1044     for( int i = i_start + 1; i < i_end; i++ ) 5 refs
 1045     {
 1046         const VECTOR2I& p  = corners[i]; 1 ref
 1047         LINKED_ITEM*    li = segs[i]; 11 refs
 1048
 1049         if( !li || li->Kind() != ITEM::ARC_T )
 1050             line.Append( p );
 1051
 1052         if( li && prev_seg != li )

The code later also process special cases of ARCs.

Notice, the i starts from i_start + 1, so it will skip first position, so when there is a third segments connected to one JOINT which causes algorithm ( construction ) stops, the LINE in the right side will not include this VIA ( or JOINT ).

back to pcbnew/router/pns_topology.cpp

 312     LINE l = m_world->AssembleLine( seg, nullptr, false, aFollowLockedSegments );
 314     path.Add( l );

The path ( which is returned as m_tunedPath ) will append the first LINE.

Then from this LINE l, the algorithm continues to construct to left and right ( the LEFT and RIGHT is logical, not in the drawing ):

319     followTrivialPath( &l, false, path, &jointB, aFollowLockedSegments );
320     followTrivialPath( &l, true, path, &jointA, aFollowLockedSegments );

PNS::TOPOLOGY::followTrivialPath

This logic is similar to followLine, it executes multiple times of NODE::AssembleLine.
The condition check:

 216         if( !visited.insert( last ).second
 217             || ( !jt->IsNonFanoutVia() && !jt->IsTraceWidthChange() ) )
 218         {
 219             if( aTerminalJoint )
 220                 *aTerminalJoint = jt;
 221
 222             return false;
 223         }

When a JOINT is NonFanoutVia, which is a VIA connected by two trivial SEGMENTs, the alglrithm stops construction.

Next is calculation of the length

PNS::MEANDER_PLACER::origPathLength

  94 long long int MEANDER_PLACER::origPathLength() const 2 refs|2 derived
  95 {
  96     return m_padToDieLength + lineLength( m_tunedPath, m_startPad_n, m_endPad_n );
  97 }

PNS::MEANDER_PLACER_BASE::lineLength

 335     /**
 336      * If there is a start pad but the pad's layers do not overlap the first track layer, then there must be a
 337      * fanout via on the line.  If there isn't, we still need to have the via back to the pad, so count the distance
 338      * in the line tuning
 339      */
 340     start_via = aStartPad && ( !aStartPad->LayersOverlap( start_item ) );
 341     end_via = aEndPad && ( !aEndPad->LayersOverlap( end_item ) );

Here, start_via is set only when aStartPad and start_item is not in same layer; end_via is set only when aEndPad and end_item is not in same layer. and the stackup height is added according to start_via and end_via is set or not.

At last, it is the calculation of sum up all LINEs

 343     for( int idx = 0; idx < aLine.Size(); idx++ ) 7 refs
 344     {
 345         const ITEM* item = aLine[idx]; 2 refs
 346
 347         if( const LINE* l = dyn_cast<const LINE*>( item ) ) 2 refs
 348         {
 349             total += l->CLine().Length();
 350         }
 351         else if( item->OfKind( ITEM::VIA_T ) && idx > 0 && idx < aLine.Size() - 1 )
 352         {
 353             int layerPrev = aLine[idx - 1]->Layer(); 2 refs
 354             int layerNext = aLine[idx + 1]->Layer(); 2 refs
 355
 356             if( layerPrev != layerNext )
 357                 total += m_router->GetInterface()->StackupHeight( layerPrev, layerNext );
 358         }
 359     }

Notice here, it will add a height between two layers when it finds a VIA.

1 Like

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