/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2011-2013 Lorenzo Marcantonio * Copyright (C) 2004-2022 KiCad Developers, see change_log.txt for contributors. * * 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 2 * 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, you may find one here: * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * or you may search the http://www.gnu.org website for the version 2 license, * or you may write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ /** * @file export_d356.cpp * @brief Export IPC-D-356 test format */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for KiROUND #include #include // Compute the access code for a pad. Returns -1 if there is no copper static int compute_pad_access_code( BOARD *aPcb, LSET aLayerMask ) { // Non-copper is not interesting here aLayerMask &= LSET::AllCuMask(); if( !aLayerMask.any() ) return -1; // Traditional TH pad if( aLayerMask[F_Cu] && aLayerMask[B_Cu] ) return 0; // Front SMD pad if( aLayerMask[F_Cu] ) return 1; // Back SMD pad if( aLayerMask[B_Cu] ) return aPcb->GetCopperLayerCount(); // OK, we have an inner-layer only pad (and I have no idea about // what could be used for); anyway, find the first copper layer // it's on for( int layer = In1_Cu; layer < B_Cu; ++layer ) { if( aLayerMask[layer] ) return layer + 1; } // This shouldn't happen return -1; } /* Convert and clamp a size from IU to decimils */ static int iu_to_d356(int iu, int clamp) { int val = KiROUND( iu / ( IU_PER_MILS / 10 ) ); if( val > clamp ) return clamp; if( val < -clamp ) return -clamp; return val; } /* Extract the D356 record from the footprints (pads) */ static void build_pad_testpoints( BOARD *aPcb, std::vector & aRecords ) { VECTOR2I origin = aPcb->GetDesignSettings().GetAuxOrigin(); for( FOOTPRINT* footprint : aPcb->Footprints() ) { for( PAD* pad : footprint->Pads() ) { D356_RECORD rk; rk.access = compute_pad_access_code( aPcb, pad->GetLayerSet() ); // It could be a mask only pad, we only handle pads with copper here if( rk.access != -1 ) { rk.netname = pad->GetNetname(); rk.pin = pad->GetNumber(); rk.refdes = footprint->GetReference(); rk.midpoint = false; // XXX MAYBE need to be computed (how?) const VECTOR2I& drill = pad->GetDrillSize(); rk.drill = std::min( drill.x, drill.y ); rk.hole = (rk.drill != 0); rk.smd = pad->GetAttribute() == PAD_ATTRIB::SMD; rk.mechanical = ( pad->GetAttribute() == PAD_ATTRIB::NPTH ); rk.x_location = pad->GetPosition().x - origin.x; rk.y_location = origin.y - pad->GetPosition().y; rk.x_size = pad->GetSize().x; // Rule: round pads have y = 0 if( pad->GetShape() == PAD_SHAPE::CIRCLE ) rk.y_size = 0; else rk.y_size = pad->GetSize().y; rk.rotation = - pad->GetOrientation().AsDegrees(); if( rk.rotation < 0 ) rk.rotation += 360; // the value indicates which sides are *not* accessible rk.soldermask = 3; if( pad->GetLayerSet()[F_Mask] ) rk.soldermask &= ~1; if( pad->GetLayerSet()[B_Mask] ) rk.soldermask &= ~2; aRecords.push_back( rk ); } } } } /* Compute the access code for a via. In D-356 layers are numbered from 1 up, where '1' is the 'primary side' (usually the component side); '0' means 'both sides', and other layers follows in an unspecified order */ static int via_access_code( BOARD *aPcb, int top_layer, int bottom_layer ) { // Easy case for through vias: top_layer is component, bottom_layer is // solder, access code is 0 if( (top_layer == F_Cu) && (bottom_layer == B_Cu) ) return 0; // Blind via, reachable from front if( top_layer == F_Cu ) return 1; // Blind via, reachable from bottom if( bottom_layer == B_Cu ) return aPcb->GetCopperLayerCount(); // It's a buried via, accessible from some inner layer // (maybe could be used for testing before laminating? no idea) return bottom_layer + 1; // XXX is this correct? } /* Extract the D356 record from the vias */ static void build_via_testpoints( BOARD *aPcb, std::vector & aRecords ) { VECTOR2I origin = aPcb->GetDesignSettings().GetAuxOrigin(); // Enumerate all the track segments and keep the vias for( auto track : aPcb->Tracks() ) { if( track->Type() == PCB_VIA_T ) { PCB_VIA *via = static_cast( track ); NETINFO_ITEM *net = track->GetNet(); D356_RECORD rk; rk.smd = false; rk.hole = true; if( net ) rk.netname = net->GetNetname(); else rk.netname = wxEmptyString; rk.refdes = wxT("VIA"); rk.pin = wxT(""); rk.midpoint = true; // Vias are always midpoints rk.drill = via->GetDrillValue(); rk.mechanical = false; PCB_LAYER_ID top_layer, bottom_layer; via->LayerPair( &top_layer, &bottom_layer ); rk.access = via_access_code( aPcb, top_layer, bottom_layer ); rk.x_location = via->GetPosition().x - origin.x; rk.y_location = origin.y - via->GetPosition().y; rk.x_size = via->GetWidth(); rk.y_size = 0; // Round so height = 0 rk.rotation = 0; rk.soldermask = 3; // XXX always tented? aRecords.push_back( rk ); } } } /* Add a new netname to the d356 canonicalized list */ static const wxString intern_new_d356_netname( const wxString &aNetname, std::map &aMap, std::set &aSet ) { wxString canon; for( size_t ii = 0; ii < aNetname.Len(); ++ii ) { // Rule: we can only use the standard ASCII, control excluded wxUniChar ch = aNetname[ii]; if( ch > 126 || !std::isgraph( static_cast( ch ) ) ) ch = '?'; canon += ch; } // Rule: only uppercase (unofficial, but known to give problems // otherwise) canon.MakeUpper(); // Rule: maximum length is 14 characters, otherwise we keep the tail if( canon.size() > 14 ) { canon = canon.Right( 14 ); } // Check if it's still unique if( aSet.count( canon ) ) { // Nope, need to uniquify it, trim it more and add a number wxString base( canon ); if( base.size() > 10 ) { base = base.Right( 10 ); } int ctr = 0; do { ++ctr; canon = base; canon << '#' << ctr; } while ( aSet.count( canon ) ); } // Register it aMap[aNetname] = canon; aSet.insert( canon ); return canon; } /* Write all the accumuled data to the file in D356 format */ void IPC356D_WRITER::write_D356_records( std::vector &aRecords, FILE* aFile ) { // Sanified and shorted network names and set of short names std::map d356_net_map; std::set d356_net_set; for( unsigned i = 0; i < aRecords.size(); i++ ) { D356_RECORD &rk = aRecords[i]; // Try to sanify the network name (there are limits on this), if // not already done. Also 'empty' net are marked as N/C, as // specified. wxString d356_net( wxT( "N/C" ) ); if( !rk.netname.empty() ) { d356_net = d356_net_map[rk.netname]; if( d356_net.empty() ) d356_net = intern_new_d356_netname( rk.netname, d356_net_map, d356_net_set ); } // Choose the best record type int rktype; if( rk.smd ) rktype = 327; else { if( rk.mechanical ) rktype = 367; else rktype = 317; } // Operation code, signal and component fprintf( aFile, "%03d%-14.14s %-6.6s%c%-4.4s%c", rktype, TO_UTF8(d356_net), TO_UTF8(rk.refdes), rk.pin.empty()?' ':'-', TO_UTF8(rk.pin), rk.midpoint?'M':' ' ); // Hole definition if( rk.hole ) { fprintf( aFile, "D%04d%c", iu_to_d356( rk.drill, 9999 ), rk.mechanical ? 'U':'P' ); } else fprintf( aFile, " " ); // Test point access fprintf( aFile, "A%02dX%+07dY%+07dX%04dY%04dR%03d", rk.access, iu_to_d356( rk.x_location, 999999 ), iu_to_d356( rk.y_location, 999999 ), iu_to_d356( rk.x_size, 9999 ), iu_to_d356( rk.y_size, 9999 ), rk.rotation ); // Soldermask fprintf( aFile, "S%d\n", rk.soldermask ); } } void IPC356D_WRITER::Write( const wxString& aFilename ) { FILE* file = nullptr; LOCALE_IO toggle; // Switch the locale to standard C if( ( file = wxFopen( aFilename, wxT( "wt" ) ) ) == nullptr ) { wxString details; details.Printf( "The file %s could not be opened for writing.", aFilename ); DisplayErrorMessage( m_parent, "Could not write IPC-356D file!", details ); return; } // This will contain everything needed for the 356 file std::vector d356_records; build_via_testpoints( m_pcb, d356_records ); build_pad_testpoints( m_pcb, d356_records ); // Code 00 AFAIK is ASCII, CUST 0 is decimils/degrees // CUST 1 would be metric but gerbtool simply ignores it! fprintf( file, "P CODE 00\n" ); fprintf( file, "P UNITS CUST 0\n" ); fprintf( file, "P arrayDim N\n" ); write_D356_records( d356_records, file ); fprintf( file, "999\n" ); fclose( file ); } void PCB_EDIT_FRAME::GenD356File( wxCommandEvent& aEvent ) { wxFileName fn = GetBoard()->GetFileName(); wxString msg, ext, wildcard; ext = IpcD356FileExtension; wildcard = IpcD356FileWildcard(); fn.SetExt( ext ); wxString pro_dir = wxPathOnly( Prj().GetProjectFullName() ); wxFileDialog dlg( this, _( "Export D-356 Test File" ), pro_dir, fn.GetFullName(), wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT ); if( dlg.ShowModal() == wxID_CANCEL ) return; IPC356D_WRITER writer( GetBoard(), this ); writer.Write( dlg.GetPath() ); }