392 lines
12 KiB
C++
392 lines
12 KiB
C++
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 2011-2013 Lorenzo Marcantonio <l.marcantonio@logossrl.com>
|
|
* 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 <confirm.h>
|
|
#include <gestfich.h>
|
|
#include <kiface_base.h>
|
|
#include <pcb_edit_frame.h>
|
|
#include <trigo.h>
|
|
#include <build_version.h>
|
|
#include <macros.h>
|
|
#include <wildcards_and_files_ext.h>
|
|
#include <locale_io.h>
|
|
#include <pcbnew.h>
|
|
#include <board.h>
|
|
#include <board_design_settings.h>
|
|
#include <footprint.h>
|
|
#include <pad.h>
|
|
#include <pcb_track.h>
|
|
#include <vector>
|
|
#include <cctype>
|
|
#include <math/util.h> // for KiROUND
|
|
#include <export_d356.h>
|
|
#include <wx/filedlg.h>
|
|
|
|
|
|
|
|
// 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 <D356_RECORD>& 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
|
|
|| pad->GetAttribute() == PAD_ATTRIB::CONN;
|
|
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 <D356_RECORD>& 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<PCB_VIA*>( 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<wxString, wxString> &aMap, std::set<wxString> &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<unsigned char>( 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 <D356_RECORD> &aRecords, FILE* aFile )
|
|
{
|
|
// Sanified and shorted network names and set of short names
|
|
std::map<wxString, wxString> d356_net_map;
|
|
std::set<wxString> 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( wxT( "The file %s could not be opened for writing." ), aFilename );
|
|
DisplayErrorMessage( m_parent, wxT( "Could not write IPC-356D file!" ), details );
|
|
return;
|
|
}
|
|
|
|
// This will contain everything needed for the 356 file
|
|
std::vector<D356_RECORD> 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 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() );
|
|
}
|