/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2004-2023 KiCad Developers. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include /* Differential pair gap/coupling test. Errors generated: - DRCE_DIFF_PAIR_GAP_OUT_OF_RANGE - DRCE_DIFF_PAIR_UNCOUPLED_LENGTH_TOO_LONG - DRCE_TOO_MANY_VIAS Todo: - arc support. - improve recognition of coupled segments (now anything that's parallel is considered coupled, causing DRC errors on meanders) */ namespace test { class DRC_TEST_PROVIDER_DIFF_PAIR_COUPLING : public DRC_TEST_PROVIDER { public: DRC_TEST_PROVIDER_DIFF_PAIR_COUPLING () : m_board( nullptr ) { } virtual ~DRC_TEST_PROVIDER_DIFF_PAIR_COUPLING() { } virtual bool Run() override; virtual const wxString GetName() const override { return wxT( "diff_pair_coupling" ); }; virtual const wxString GetDescription() const override { return wxT( "Tests differential pair coupling" ); } private: BOARD* m_board; }; }; static bool commonParallelProjection( SEG p, SEG n, SEG &pClip, SEG& nClip ) { SEG n_proj_p( p.LineProject( n.A ), p.LineProject( n.B ) ); int64_t t_a = 0; int64_t t_b = p.TCoef( p.B ); int64_t tproj_a = p.TCoef( n_proj_p.A ); int64_t tproj_b = p.TCoef( n_proj_p.B ); if( t_b < t_a ) std::swap( t_b, t_a ); if( tproj_b < tproj_a ) std::swap( tproj_b, tproj_a ); if( t_b <= tproj_a ) return false; if( t_a >= tproj_b ) return false; int64_t t[4] = { 0, p.TCoef( p.B ), p.TCoef( n_proj_p.A ), p.TCoef( n_proj_p.B ) }; std::vector tv( t, t + 4 ); std::sort( tv.begin(), tv.end() ); // fixme: awful and disgusting way of finding 2 midpoints int64_t pLenSq = p.SquaredLength(); VECTOR2I dp = p.B - p.A; pClip.A.x = p.A.x + rescale( (int64_t)dp.x, tv[1], pLenSq ); pClip.A.y = p.A.y + rescale( (int64_t)dp.y, tv[1], pLenSq ); pClip.B.x = p.A.x + rescale( (int64_t)dp.x, tv[2], pLenSq ); pClip.B.y = p.A.y + rescale( (int64_t)dp.y, tv[2], pLenSq ); nClip.A = n.LineProject( pClip.A ); nClip.B = n.LineProject( pClip.B ); return true; } static bool commonParallelProjection( const PCB_ARC& p, const PCB_ARC& n, SHAPE_ARC &pClip, SHAPE_ARC& nClip ) { VECTOR2I p_center = p.GetCenter(); VECTOR2I n_center = n.GetCenter(); double p_radius = p.GetRadius(); double n_radius = n.GetRadius(); VECTOR2I p_start( p.GetStart() ); VECTOR2I p_end( p.GetEnd() ); if( !p.IsCCW() ) std::swap( p_start, p_end ); VECTOR2I n_start( n.GetStart() ); VECTOR2I n_end( n.GetEnd() ); if( !n.IsCCW() ) std::swap( n_start, n_end ); SHAPE_ARC p_arc( p_start, p.GetMid(), p_end, 0 ); SHAPE_ARC n_arc( n_start, n.GetMid(), n_end, 0 ); EDA_ANGLE p_start_angle = p_arc.GetStartAngle(); // Rotate the arcs to a common 0 starting angle p_arc.Rotate( -p_start_angle, p_center ); n_arc.Rotate( -p_start_angle, n_center ); EDA_ANGLE p_end_angle = p_arc.GetEndAngle(); EDA_ANGLE n_start_angle = n_arc.GetStartAngle(); EDA_ANGLE n_end_angle = n_arc.GetEndAngle(); EDA_ANGLE clip_total_angle; EDA_ANGLE clip_start_angle; if( n_start_angle > p_end_angle ) { // n is fully outside of p if( n_end_angle > p_end_angle ) return false; // n starts before angle 0 and ends in the middle of p clip_total_angle = n_end_angle + p_start_angle; clip_start_angle = p_start_angle; } else { clip_start_angle = n_start_angle + p_start_angle; // n is fully inside of p if( n_end_angle < p_end_angle ) clip_total_angle = n_end_angle - n_start_angle; else // n starts after 0 and ends after p clip_total_angle = p_end_angle - n_start_angle; } // One arc starts exactly where the other ends if( clip_total_angle == ANGLE_0 ) return false; VECTOR2I n_start_pt = n_center + VECTOR2I( KiROUND( n_radius ), 0 ); VECTOR2I p_start_pt = p_center + VECTOR2I( KiROUND( p_radius ), 0 ); RotatePoint( n_start_pt, n_center, clip_start_angle ); RotatePoint( p_start_pt, p_center, clip_start_angle ); pClip = SHAPE_ARC( p_center, p_start_pt, clip_total_angle ); nClip = SHAPE_ARC( n_center, n_start_pt, clip_total_angle ); return true; } struct DIFF_PAIR_KEY { bool operator<( const DIFF_PAIR_KEY& b ) const { if( netP < b.netP ) { return true; } else if( netP > b.netP ) { return false; } else // netP == b.netP { if( netN < b.netN ) return true; else if( netN > b.netN ) return false; else if( gapRuleName.IsEmpty() ) return gapRuleName < b.gapRuleName; else return uncoupledRuleName < b.uncoupledRuleName; } } int netP, netN; wxString gapRuleName; wxString uncoupledRuleName; std::optional> gapConstraint; DRC_RULE* gapRule; std::optional> uncoupledConstraint; DRC_RULE* uncoupledRule; }; struct DIFF_PAIR_COUPLED_SEGMENTS { SEG coupledN; SEG coupledP; bool isArc; SHAPE_ARC coupledArcN; SHAPE_ARC coupledArcP; PCB_TRACK* parentN; PCB_TRACK* parentP; int computedGap; PCB_LAYER_ID layer; bool couplingFailMin; bool couplingFailMax; DIFF_PAIR_COUPLED_SEGMENTS() : isArc( false ), parentN( nullptr ), parentP( nullptr ), computedGap( 0 ), layer( UNDEFINED_LAYER ), couplingFailMin( false ), couplingFailMax( false ) {} }; struct DIFF_PAIR_ITEMS { std::set itemsP, itemsN; std::vector coupled; int totalCoupled; int totalLengthN; int totalLengthP; }; static void extractDiffPairCoupledItems( DIFF_PAIR_ITEMS& aDp ) { for( BOARD_CONNECTED_ITEM* itemP : aDp.itemsP ) { PCB_TRACK* sp = dyn_cast( itemP ); std::optional bestCoupled; int bestGap = std::numeric_limits::max(); if( !sp ) continue; for ( BOARD_CONNECTED_ITEM* itemN : aDp.itemsN ) { PCB_TRACK* sn = dyn_cast ( itemN ); if( !sn ) continue; if( ( sn->GetLayerSet() & sp->GetLayerSet() ).none() ) continue; SEG ssp ( sp->GetStart(), sp->GetEnd() ); SEG ssn ( sn->GetStart(), sn->GetEnd() ); // Segments that are ~ 1 IU in length per side are approximately parallel (tolerance is 1 IU) // with everything and their parallel projection is < 1 IU, leading to bad distance calculations if( ssp.SquaredLength() > 2 && ssn.SquaredLength() > 2 && ssp.ApproxParallel(ssn) ) { DIFF_PAIR_COUPLED_SEGMENTS cpair; bool coupled = commonParallelProjection( ssp, ssn, cpair.coupledP, cpair.coupledN ); if( coupled ) { cpair.parentP = sp; cpair.parentN = sn; cpair.layer = sp->GetLayer(); cpair.computedGap = (cpair.coupledP.A - cpair.coupledN.A).EuclideanNorm(); cpair.computedGap -= ( sp->GetWidth() + sn->GetWidth() ) / 2; if( cpair.computedGap < bestGap ) { bestGap = cpair.computedGap; bestCoupled = cpair; } } } } if( bestCoupled ) { auto excludeSelf = [&]( BOARD_ITEM* aItem ) { if( aItem == bestCoupled->parentN || aItem == bestCoupled->parentP ) return false; if( aItem->Type() == PCB_TRACE_T || aItem->Type() == PCB_VIA_T || aItem->Type() == PCB_ARC_T ) { BOARD_CONNECTED_ITEM* bci = static_cast( aItem ); if( bci->GetNetCode() == bestCoupled->parentN->GetNetCode() || bci->GetNetCode() == bestCoupled->parentP->GetNetCode() ) { return false; } } return true; }; SHAPE_SEGMENT checkSegStart( bestCoupled->coupledP.A, bestCoupled->coupledN.A ); SHAPE_SEGMENT checkSegEnd( bestCoupled->coupledP.B, bestCoupled->coupledN.B ); DRC_RTREE* tree = bestCoupled->parentP->GetBoard()->m_CopperItemRTreeCache.get(); // check if there's anything in between the segments suspected to be coupled. If // there's nothing, assume they are really coupled. if( !tree->CheckColliding( &checkSegStart, sp->GetLayer(), 0, excludeSelf ) && !tree->CheckColliding( &checkSegEnd, sp->GetLayer(), 0, excludeSelf ) ) { aDp.coupled.push_back( *bestCoupled ); } } } for( BOARD_CONNECTED_ITEM* itemP : aDp.itemsP ) { PCB_ARC* sp = dyn_cast( itemP ); std::optional bestCoupled; int bestGap = std::numeric_limits::max(); if( !sp ) continue; for ( BOARD_CONNECTED_ITEM* itemN : aDp.itemsN ) { PCB_ARC* sn = dyn_cast ( itemN ); if( !sn ) continue; if( ( sn->GetLayerSet() & sp->GetLayerSet() ).none() ) continue; // Segments that are ~ 1 IU in length per side are approximately parallel (tolerance is 1 IU) // with everything and their parallel projection is < 1 IU, leading to bad distance calculations if( sp->GetLength() > 2 && sn->GetLength() > 2 && ( sp->GetCenter() - sn->GetCenter() ).SquaredEuclideanNorm() < 4 ) { DIFF_PAIR_COUPLED_SEGMENTS cpair; cpair.isArc = true; bool coupled = commonParallelProjection( *sp, *sn, cpair.coupledArcP, cpair.coupledArcN ); if( coupled ) { cpair.parentP = sp; cpair.parentN = sn; cpair.layer = sp->GetLayer(); cpair.computedGap = KiROUND( std::abs( cpair.coupledArcP.GetRadius() - cpair.coupledArcN.GetRadius() ) ); cpair.computedGap -= ( sp->GetWidth() + sn->GetWidth() ) / 2; if( cpair.computedGap < bestGap ) { bestGap = cpair.computedGap; bestCoupled = cpair; } } } } if( bestCoupled ) { auto excludeSelf = [&] ( BOARD_ITEM *aItem ) { if( aItem == bestCoupled->parentN || aItem == bestCoupled->parentP ) return false; if( aItem->Type() == PCB_TRACE_T || aItem->Type() == PCB_VIA_T || aItem->Type() == PCB_ARC_T ) { BOARD_CONNECTED_ITEM* bci = static_cast( aItem ); if( bci->GetNetCode() == bestCoupled->parentN->GetNetCode() || bci->GetNetCode() == bestCoupled->parentP->GetNetCode() ) { return false; } } return true; }; SHAPE_SEGMENT checkSegStart( bestCoupled->coupledP.A, bestCoupled->coupledN.A ); SHAPE_SEGMENT checkSegEnd( bestCoupled->coupledP.B, bestCoupled->coupledN.B ); DRC_RTREE* tree = bestCoupled->parentP->GetBoard()->m_CopperItemRTreeCache.get(); // check if there's anything in between the segments suspected to be coupled. If // there's nothing, assume they are really coupled. if( !tree->CheckColliding( &checkSegStart, sp->GetLayer(), 0, excludeSelf ) && !tree->CheckColliding( &checkSegEnd, sp->GetLayer(), 0, excludeSelf ) ) { aDp.coupled.push_back( *bestCoupled ); } } } } bool test::DRC_TEST_PROVIDER_DIFF_PAIR_COUPLING::Run() { m_board = m_drcEngine->GetBoard(); int epsilon = m_board->GetDesignSettings().GetDRCEpsilon(); std::map dpRuleMatches; auto evaluateDpConstraints = [&]( BOARD_ITEM *item ) -> bool { DIFF_PAIR_KEY key; BOARD_CONNECTED_ITEM* citem = static_cast( item ); NETINFO_ITEM* refNet = citem->GetNet(); if( refNet && DRC_ENGINE::IsNetADiffPair( m_board, refNet, key.netP, key.netN ) ) { drc_dbg( 10, wxT( "eval dp %p\n" ), item ); const DRC_CONSTRAINT_T constraintsToCheck[] = { DIFF_PAIR_GAP_CONSTRAINT, MAX_UNCOUPLED_CONSTRAINT }; for( int i = 0; i < 2; i++ ) { DRC_CONSTRAINT constraint = m_drcEngine->EvalRules( constraintsToCheck[ i ], item, nullptr, item->GetLayer() ); if( constraint.IsNull() || constraint.GetSeverity() == RPT_SEVERITY_IGNORE ) continue; drc_dbg( 10, wxT( "cns %d item %p\n" ), constraintsToCheck[i], item ); DRC_RULE* parentRule = constraint.GetParentRule(); wxString ruleName = parentRule ? parentRule->m_Name : constraint.GetName(); switch( constraintsToCheck[i] ) { case DIFF_PAIR_GAP_CONSTRAINT: key.gapConstraint = constraint.GetValue(); key.gapRule = parentRule; key.gapRuleName = ruleName; break; case MAX_UNCOUPLED_CONSTRAINT: key.uncoupledConstraint = constraint.GetValue(); key.uncoupledRule = parentRule; key.uncoupledRuleName = ruleName; break; default: break; } if( refNet->GetNetCode() == key.netN ) dpRuleMatches[key].itemsN.insert( citem ); else dpRuleMatches[key].itemsP.insert( citem ); } } return true; }; m_board->GetConnectivity()->GetFromToCache()->Rebuild( m_board ); forEachGeometryItem( { PCB_TRACE_T, PCB_VIA_T, PCB_ARC_T }, LSET::AllCuMask(), evaluateDpConstraints ); drc_dbg( 10, wxT( "dp rule matches %d\n" ), (int) dpRuleMatches.size() ); reportAux( wxT( "DPs evaluated:" ) ); for( auto& [ key, itemSet ] : dpRuleMatches ) { NETINFO_ITEM *niP = m_board->GetNetInfo().GetNetItem( key.netP ); NETINFO_ITEM *niN = m_board->GetNetInfo().GetNetItem( key.netN ); assert( niP ); assert( niN ); wxString nameP = niP->GetNetname(); wxString nameN = niN->GetNetname(); reportAux( wxString::Format( wxT( "Rule '%s', DP: (+) %s - (-) %s" ), key.gapRuleName, nameP, nameN ) ); extractDiffPairCoupledItems( itemSet ); itemSet.totalCoupled = 0; itemSet.totalLengthN = 0; itemSet.totalLengthP = 0; drc_dbg(10, wxT( " coupled prims : %d\n" ), (int) itemSet.coupled.size() ); for( BOARD_CONNECTED_ITEM* item : itemSet.itemsN ) { if( PCB_TRACK* track = dynamic_cast( item ) ) itemSet.totalLengthN += track->GetLength(); } for( BOARD_CONNECTED_ITEM* item : itemSet.itemsP ) { if( PCB_TRACK* track = dynamic_cast( item ) ) itemSet.totalLengthP += track->GetLength(); } for( DIFF_PAIR_COUPLED_SEGMENTS& dp : itemSet.coupled ) { int length = dp.coupledN.Length(); wxCHECK2( dp.parentN && dp.parentP, continue ); std::shared_ptr overlay = m_drcEngine->GetDebugOverlay(); if( overlay ) { overlay->SetIsFill(false); overlay->SetIsStroke(true); overlay->SetStrokeColor( RED ); overlay->SetLineWidth( 100000 ); overlay->Line( dp.coupledP ); overlay->SetStrokeColor( BLUE ); overlay->Line( dp.coupledN ); } drc_dbg( 10, wxT( " len %d gap %d l %d\n" ), length, dp.computedGap, dp.parentP->GetLayer() ); if( key.gapConstraint ) { if( key.gapConstraint->HasMin() && key.gapConstraint->Min() >= 0 && ( dp.computedGap < key.gapConstraint->Min() - epsilon ) ) { dp.couplingFailMin = true; } if( key.gapConstraint->HasMax() && key.gapConstraint->Max() >= 0 && ( dp.computedGap > key.gapConstraint->Max() + epsilon ) ) { dp.couplingFailMax = true; } } if( !dp.couplingFailMin && !dp.couplingFailMax ) itemSet.totalCoupled += length; } int totalLen = std::max( itemSet.totalLengthN, itemSet.totalLengthP ); reportAux( wxString::Format( wxT( " - coupled length: %s, total length: %s" ), MessageTextFromValue( itemSet.totalCoupled ), MessageTextFromValue( totalLen ) ) ); int totalUncoupled = totalLen - itemSet.totalCoupled; bool uncoupledViolation = false; if( key.uncoupledConstraint && ( !itemSet.itemsP.empty() || !itemSet.itemsN.empty() ) ) { const MINOPTMAX& val = *key.uncoupledConstraint; if( val.HasMax() && val.Max() >= 0 && totalUncoupled > val.Max() ) { auto drce = DRC_ITEM::Create( DRCE_DIFF_PAIR_UNCOUPLED_LENGTH_TOO_LONG ); wxString msg = formatMsg( _( "(%s maximum uncoupled length %s; actual %s)" ), key.uncoupledRuleName, val.Max(), totalUncoupled ); drce->SetErrorMessage( drce->GetErrorText() + wxS( " " ) + msg ); BOARD_CONNECTED_ITEM* item = nullptr; auto p_it = itemSet.itemsP.begin(); auto n_it = itemSet.itemsN.begin(); if( p_it != itemSet.itemsP.end() ) { item = *p_it; drce->AddItem( *p_it ); p_it++; } if( n_it != itemSet.itemsN.end() ) { item = *n_it; drce->AddItem( *n_it ); n_it++; } while( p_it != itemSet.itemsP.end() ) drce->AddItem( *p_it++ ); while( n_it != itemSet.itemsN.end() ) drce->AddItem( *n_it++ ); uncoupledViolation = true; drce->SetViolatingRule( key.uncoupledRule ); reportViolation( drce, item->GetPosition(), item->GetLayer() ); } } if( key.gapConstraint && ( uncoupledViolation || !key.uncoupledConstraint ) ) { for( DIFF_PAIR_COUPLED_SEGMENTS& dp : itemSet.coupled ) { wxCHECK2( dp.parentP && dp.parentN, continue ); if( ( dp.couplingFailMin || dp.couplingFailMax ) ) { // We have a candidate violation, now we need to re-query for a constraint // given the actual items, because there may be a location-based rule in play. DRC_CONSTRAINT constraint = m_drcEngine->EvalRules( DIFF_PAIR_GAP_CONSTRAINT, dp.parentP, dp.parentN, dp.parentP->GetLayer() ); MINOPTMAX val = constraint.GetValue(); if( !val.HasMin() || val.Min() < 0 || dp.computedGap >= val.Min() ) dp.couplingFailMin = false; if( !val.HasMax() || val.Max() < 0 || dp.computedGap <= val.Max() ) dp.couplingFailMax = false; if( !dp.couplingFailMin && !dp.couplingFailMax ) continue; auto drcItem = DRC_ITEM::Create( DRCE_DIFF_PAIR_GAP_OUT_OF_RANGE ); wxString msg; if( dp.couplingFailMin ) { msg = formatMsg( _( "(%s minimum gap %s; actual %s)" ), key.gapRuleName, val.Min(), dp.computedGap ); } else if( dp.couplingFailMax ) { msg = formatMsg( _( "(%s maximum gap %s; actual %s)" ), key.gapRuleName, val.Max(), dp.computedGap ); } drcItem->SetErrorMessage( drcItem->GetErrorText() + wxS( " " ) + msg ); BOARD_CONNECTED_ITEM* item = nullptr; if( dp.parentP ) { item = dp.parentP; drcItem->AddItem( dp.parentP ); } if( dp.parentN ) { item = dp.parentN; drcItem->AddItem( dp.parentN ); } drcItem->SetViolatingRule( key.gapRule ); reportViolation( drcItem, item->GetPosition(), item->GetLayer() ); } } } } reportRuleStatistics(); return true; } namespace detail { static DRC_REGISTER_TEST_PROVIDER dummy; }