← Back to team overview

kicad-developers team mailing list archive

[PATCH] [RFC] Exporter for Mentor Hyperlynx

 

Hi,

We needed to do some signal/power integrity simulations on one of our
Kicad designs and in order to do that, we needed to convert a Kicad PCB
to Hyperlynx format. Luckily, the format is simple, all text and well
documented in [1], so here comes a patch that adds a Hyperlynx exporter.

Notes:
- since Kicad doesn't have a concept of board stackup (permittivities,
loss tangent, dielectric types, etc.), the exporter writes a dummy
stackup. Edit it to match the PCB spec in Hyperlynx.
- no support for offset pad holes, slotted pad holes,
trapezoid/polygonal pads (it seems HL format doesn't support such
features or I need to figure out how to emulate them).
- no support for thermal pads (yet)
- no error reporting.

Looking forward to your feedback & wish you happy testing,
Tom

[1] http://www.ibis.org/birds/bird33.txt
>From e966c63a6f00959e359be36bfc0b8206e01ed4bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tomasz=20W=C5=82ostowski?= <tomasz.wlostowski@xxxxxxx>
Date: Thu, 4 Apr 2019 18:07:12 +0200
Subject: [PATCH] pcbnew: Initial support for Mentor Hyperlynx export

---
 pcbnew/CMakeLists.txt                         |   1 +
 pcbnew/dialogs/dialog_export_idf.cpp          |   1 +
 pcbnew/exporters/export_hyperlynx.cpp         | 547 ++++++++++++++++++
 pcbnew/menubar_pcb_editor.cpp                 |   5 +
 pcbnew/pcb_edit_frame.cpp                     |  29 +
 pcbnew/pcb_edit_frame.h                       |   7 +
 pcbnew/pcbnew_id.h                            |   1 +
 qa/CMakeLists.txt                             |   1 +
 qa/hyperlynx_export/CMakeLists.txt            |  64 ++
 qa/hyperlynx_export/test_hyperlynx_export.cpp |  61 ++
 10 files changed, 717 insertions(+)
 create mode 100644 pcbnew/exporters/export_hyperlynx.cpp
 create mode 100644 qa/hyperlynx_export/CMakeLists.txt
 create mode 100644 qa/hyperlynx_export/test_hyperlynx_export.cpp

diff --git a/pcbnew/CMakeLists.txt b/pcbnew/CMakeLists.txt
index 3bbdd8723..9f0f5eac1 100644
--- a/pcbnew/CMakeLists.txt
+++ b/pcbnew/CMakeLists.txt
@@ -193,6 +193,7 @@ set( PCBNEW_IMPORT_GFX
     )
 
 set( PCBNEW_EXPORTERS
+    exporters/export_hyperlynx.cpp
     exporters/export_d356.cpp
     exporters/export_footprint_associations.cpp
     exporters/export_gencad.cpp
diff --git a/pcbnew/dialogs/dialog_export_idf.cpp b/pcbnew/dialogs/dialog_export_idf.cpp
index e9e16bfbc..882224b7c 100644
--- a/pcbnew/dialogs/dialog_export_idf.cpp
+++ b/pcbnew/dialogs/dialog_export_idf.cpp
@@ -229,3 +229,4 @@ void PCB_EDIT_FRAME::OnExportIDF3( wxCommandEvent& event )
         return;
     }
 }
+
diff --git a/pcbnew/exporters/export_hyperlynx.cpp b/pcbnew/exporters/export_hyperlynx.cpp
new file mode 100644
index 000000000..5f5c678dd
--- /dev/null
+++ b/pcbnew/exporters/export_hyperlynx.cpp
@@ -0,0 +1,547 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 CERN
+ *
+ * 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
+ */
+
+#include <kiface_i.h>
+#include <pcb_edit_frame.h>
+#include <pcbnew.h>
+
+#include <class_board.h>
+#include <class_board_item.h>
+#include <class_drawsegment.h>
+#include <class_edge_mod.h>
+#include <class_module.h>
+#include <class_track.h>
+#include <class_zone.h>
+#include <cstdio>
+#include <vector>
+
+static double iu2hyp( double iu )
+{
+    return iu / 1e9 / 0.0254;
+}
+
+class PAD_STACK
+{
+public:
+    PAD_STACK( BOARD* aBoard, const D_PAD* aPad );
+    PAD_STACK( BOARD* aBoard, const VIA* aVia );
+    ~PAD_STACK(){};
+
+
+    bool isThrough() const
+    {
+        return m_type == PAD_ATTRIB_HOLE_NOT_PLATED || m_type == PAD_ATTRIB_STANDARD;
+    }
+
+    bool operator==( const PAD_STACK& other ) const
+    {
+        if( m_shape != other.m_shape )
+            return false;
+
+        if( m_type != other.m_type )
+            return false;
+
+        if( isThrough() && other.isThrough() && m_drill != other.m_drill )
+            return false;
+
+        if( m_sx != other.m_sx )
+            return false;
+
+        if( m_sy != other.m_sy )
+            return false;
+
+        if( m_layers != other.m_layers )
+            return false;
+
+        if( m_angle != other.m_angle )
+            return false;
+
+        return true;
+    }
+
+    bool isSMD() const
+    {
+        return m_type == PAD_ATTRIB_SMD;
+    }
+
+    PCB_LAYER_ID getSMDLayer() const
+    {
+        for( auto l : LSET::AllCuMask().Seq() )
+            if( m_layers[l] )
+                return l;
+        return F_Cu;
+    }
+
+    void SetId( int id )
+    {
+        m_id = id;
+    }
+
+    int GetId() const
+    {
+        return m_id;
+    }
+
+    void FormatPadShape(
+            int indentLevel, std::shared_ptr<FILE_OUTPUTFORMATTER> fmt, const wxString& layerName )
+    {
+        int shapeId;
+
+        switch( m_shape )
+        {
+        case PAD_SHAPE_CIRCLE:
+        case PAD_SHAPE_OVAL: shapeId = 0; break;
+        case PAD_SHAPE_ROUNDRECT: shapeId = 2; break;
+        case PAD_SHAPE_RECT: shapeId = 1; break;
+        default: wxLogDebug( wxT( "Unsupported pad shape %d\n" ), m_shape ); return;
+        }
+
+        fmt->Print( indentLevel, "(\"%s\", %d, %.9f, %.9f, %.1f, M)\n",
+                (const char*) layerName.c_str(), shapeId, iu2hyp( m_sx ), iu2hyp( m_sy ), m_angle );
+    }
+
+    bool isEmpty() const
+    {
+        LSET layerMask = LSET::AllCuMask() & m_board->GetEnabledLayers();
+        LSET outLayers = m_layers & layerMask;
+
+        return outLayers.none();
+    }
+
+    void Format( int indentLevel, std::shared_ptr<FILE_OUTPUTFORMATTER> fmt )
+    {
+        LSET layerMask = LSET::AllCuMask() & m_board->GetEnabledLayers();
+        LSET outLayers = m_layers & layerMask;
+
+        if( outLayers.none() )
+            return;
+
+        fmt->Print( 0, "{PADSTACK=%d, %.9f\n", m_id, iu2hyp( m_drill ) );
+
+        if( outLayers == layerMask )
+        {
+            FormatPadShape( indentLevel + 1, fmt, wxT( "MDEF" ) );
+        }
+        else
+        {
+            for( auto l : outLayers.Seq() )
+            {
+                FormatPadShape( indentLevel + 1, fmt, m_board->GetLayerName( l ) );
+            }
+        }
+
+
+        fmt->Print( 0, "}\n\n" );
+    }
+
+
+private:
+    BOARD*      m_board;
+    int         m_id;
+    int         m_drill;
+    PAD_SHAPE_T m_shape;
+    int         m_sx, m_sy;
+    double      m_angle;
+    LSET        m_layers;
+    PAD_ATTR_T  m_type;
+};
+
+class HYPERLYNX_EXPORTER
+{
+public:
+    HYPERLYNX_EXPORTER(){};
+    ~HYPERLYNX_EXPORTER(){};
+
+    void SetOutputFilename( const wxFileName& aPath )
+    {
+        m_outputFilePath = aPath;
+    }
+
+    void SetBoard( BOARD* aBoard )
+    {
+        m_board = aBoard;
+    }
+
+    bool Run();
+
+    PAD_STACK* addPadStack( PAD_STACK stack )
+    {
+        for( auto p : m_padStacks )
+        {
+            if( *p == stack )
+                return p;
+        }
+
+        stack.SetId( m_padStacks.size() );
+        m_padStacks.push_back( new PAD_STACK( stack ) );
+
+        return m_padStacks.back();
+    }
+
+
+private:
+    bool                           generateHeaders();
+    bool                           writeBoardInfo();
+    bool                           writeStackupInfo();
+    bool                           writeDevices();
+    bool                           writePadStacks();
+    bool                           writeNets();
+    bool                           writeNetObjects( const std::vector<BOARD_ITEM*>& aObjects );
+    const std::vector<BOARD_ITEM*> collectNetObjects( int netcode );
+
+    std::vector<PAD_STACK*>           m_padStacks;
+    std::map<BOARD_ITEM*, PAD_STACK*> m_padMap;
+
+
+    BOARD*                                m_board;
+    wxFileName                            m_outputFilePath;
+    std::shared_ptr<FILE_OUTPUTFORMATTER> m_out;
+    int                                   m_polyId;
+};
+
+
+PAD_STACK::PAD_STACK( BOARD* aBoard, const D_PAD* aPad )
+{
+    m_board = aBoard;
+    m_sx = aPad->GetSize().x;
+    m_sy = aPad->GetSize().y;
+    //printf("sx %d sy %d\n", m_sx, m_sy );
+    m_angle = 180.0 - ( aPad->GetOrientation() / 10.0 );
+    if( m_angle < 0.0 )
+        m_angle += 360.0;
+    m_layers = aPad->GetLayerSet();
+    m_drill = aPad->GetDrillSize().x;
+    m_shape = aPad->GetShape();
+    m_type = PAD_ATTRIB_STANDARD;
+}
+
+PAD_STACK::PAD_STACK( BOARD* aBoard, const VIA* aVia )
+{
+    m_board = aBoard;
+    m_sx = m_sy = aVia->GetWidth();
+    //printf("sx %d sy %d\n", m_sx, m_sy );
+    m_angle = 0;
+    m_layers = LSET::AllCuMask();
+    m_drill = aVia->GetDrillValue();
+    m_shape = PAD_SHAPE_CIRCLE;
+    m_type = PAD_ATTRIB_STANDARD;
+}
+
+bool HYPERLYNX_EXPORTER::generateHeaders()
+{
+    m_out->Print( 0, "{VERSION=2.14}\n" );
+    m_out->Print( 0, "{UNITS=ENGLISH LENGTH}\n\n" );
+    return true;
+}
+
+
+bool HYPERLYNX_EXPORTER::writeBoardInfo()
+{
+    SHAPE_POLY_SET outlines;
+    wxString       errText;
+    wxPoint        errLoc;
+
+    m_out->Print( 0, "{BOARD \"%s\"\n", (const char*) m_board->GetFileName().c_str() );
+
+    if( !m_board->GetBoardPolygonOutlines( outlines, &errText, &errLoc ) )
+    {
+        return false;
+    }
+
+    for( int o = 0; o < outlines.OutlineCount(); o++ )
+    {
+        const auto& outl = outlines.COutline( o );
+        for( int i = 0; i < outl.SegmentCount(); i++ )
+        {
+            const auto& s = outl.CSegment( i );
+            m_out->Print( 1, "(PERIMETER_SEGMENT X1=%.9f Y1=%.9f X2=%.9f Y2=%.9f)\n",
+                    iu2hyp( s.A.x ), iu2hyp( s.A.y ), iu2hyp( s.B.x ), iu2hyp( s.B.y ) );
+        }
+    }
+    m_out->Print( 0, "}\n\n" );
+
+    return true;
+}
+
+bool HYPERLYNX_EXPORTER::writeStackupInfo()
+{
+    auto layers = m_board->GetDesignSettings().GetEnabledLayers().CuStack();
+
+    m_out->Print( 0, "{STACKUP\n" );
+
+    for( auto l : layers )
+    {
+        const auto name = m_board->GetLayerName( l );
+        m_out->Print( 1, "(SIGNAL T=0.002284 P=0.000000 C=1.724e-8 L=\"%s\" M=COPPER)\n",
+                (const char*) name.c_str() );
+        if( l != B_Cu )
+        {
+            m_out->Print( 1, "(DIELECTRIC T=0.007087 C=3.660000 L=\"DE_%s\" M=FR4)\n",
+                    (const char*) name.c_str() );
+        }
+    }
+    m_out->Print( 0, "}\n\n" );
+
+    return true;
+}
+
+bool HYPERLYNX_EXPORTER::writeDevices()
+{
+    m_out->Print( 0, "{DEVICES\n" );
+
+    for( auto mod : m_board->Modules() )
+    {
+        wxString ref = mod->GetReference();
+        auto     layerName = m_board->GetLayerName( mod->GetLayer() );
+
+        if( ref.IsEmpty() )
+            ref = wxT( "EMPTY" );
+
+        m_out->Print( 1, "(? REF=\"%s\" L=\"%s\")\n", (const char*) ref.c_str(),
+                (const char*) layerName.c_str() );
+    }
+    m_out->Print( 0, "}\n\n" );
+
+    return true;
+}
+
+bool HYPERLYNX_EXPORTER::writePadStacks()
+{
+    for( auto mod : m_board->Modules() )
+    {
+        for( auto pad : mod->Pads() )
+        {
+            auto ps = addPadStack( PAD_STACK( m_board, pad ) );
+            m_padMap[pad] = ps;
+        }
+    }
+
+    for( auto trk : m_board->Tracks() )
+    {
+        if( VIA* via = dyn_cast<VIA*>( trk ) )
+        {
+            auto ps = addPadStack( PAD_STACK( m_board, via ) );
+            m_padMap[via] = ps;
+        }
+    }
+
+    for( auto pstack : m_padStacks )
+        pstack->Format( 0, m_out );
+
+    return true;
+}
+
+bool HYPERLYNX_EXPORTER::writeNetObjects( const std::vector<BOARD_ITEM*>& aObjects )
+{
+
+    for( auto item : aObjects )
+    {
+        if( D_PAD* pad = dyn_cast<D_PAD*>( item ) )
+        {
+            auto pstackIter = m_padMap.find( pad );
+            if( pstackIter != m_padMap.end() )
+            {
+                wxString ref = pad->GetParent()->GetReference();
+                if( ref.IsEmpty() )
+                    ref = wxT( "EMPTY" );
+
+                auto padName = pad->GetName();
+
+                if( padName.IsEmpty() )
+                    padName = wxT( "1" );
+
+
+                m_out->Print( 1, "(PIN X=%.10f Y=%.10f R=\"%s.%s\" P=%d)\n",
+                        iu2hyp( pad->GetPosition().x ), iu2hyp( pad->GetPosition().y ),
+                        (const char*) ref.c_str(), (const char*) padName.c_str(),
+                        pstackIter->second->GetId() );
+            }
+        }
+        else if( VIA* via = dyn_cast<VIA*>( item ) )
+        {
+            auto pstackIter = m_padMap.find( via );
+            if( pstackIter != m_padMap.end() )
+            {
+                m_out->Print( 1, "(VIA X=%.10f Y=%.10f P=%d)\n", iu2hyp( via->GetPosition().x ),
+                        iu2hyp( via->GetPosition().y ), pstackIter->second->GetId() );
+            }
+        }
+        else if( TRACK* track = dyn_cast<TRACK*>( item ) )
+        {
+            const auto layerName = m_board->GetLayerName( track->GetLayer() );
+            m_out->Print( 1, "(SEG X1=%.10f Y1=%.10f X2=%.10f Y2=%.10f W=%.10f L=\"%s\")\n",
+                    iu2hyp( track->GetStart().x ), iu2hyp( track->GetStart().y ),
+                    iu2hyp( track->GetEnd().x ), iu2hyp( track->GetEnd().y ),
+                    iu2hyp( track->GetWidth() ), (const char*) layerName.c_str() );
+        }
+        else if( ZONE_CONTAINER* zone = dyn_cast<ZONE_CONTAINER*>( item ) )
+        {
+            const auto     layerName = m_board->GetLayerName( zone->GetLayer() );
+            SHAPE_POLY_SET filledShape = zone->GetFilledPolysList();
+
+            filledShape.Simplify( SHAPE_POLY_SET::PM_FAST );
+
+            for( int i = 0; i < filledShape.OutlineCount(); i++ )
+            {
+                const auto& outl = filledShape.COutline( i );
+
+                auto p0 = outl.CPoint( 0 );
+                m_out->Print( 1, "{POLYGON T=POUR L=\"%s\" ID=%d X=%.10f Y=%.10f\n",
+                        (const char*) layerName.c_str(), m_polyId, iu2hyp( p0.x ), iu2hyp( p0.y ) );
+
+                for( int v = 0; v < outl.PointCount(); v++ )
+                {
+                    m_out->Print( 2, "(LINE X=%.10f Y=%.10f)\n", iu2hyp( outl.CPoint( v ).x ),
+                            iu2hyp( outl.CPoint( v ).y ) );
+                }
+
+                m_out->Print( 2, "(LINE X=%.10f Y=%.10f)\n", iu2hyp( p0.x ), iu2hyp( p0.y ) );
+
+                m_out->Print( 1, "}\n" );
+
+                for( int h = 0; h < filledShape.HoleCount( i ); h++ )
+                {
+                    const auto& holeShape = filledShape.CHole( i, h );
+                    auto        ph0 = holeShape.CPoint( 0 );
+
+                    m_out->Print( 1, "{POLYVOID ID=%d X=%.10f Y=%.10f\n", m_polyId, iu2hyp( ph0.x ),
+                            iu2hyp( ph0.y ) );
+
+                    for( int v = 0; v < holeShape.PointCount(); v++ )
+                    {
+                        m_out->Print( 2, "(LINE X=%.10f Y=%.10f)\n",
+                                iu2hyp( holeShape.CPoint( v ).x ),
+                                iu2hyp( holeShape.CPoint( v ).y ) );
+                    }
+                    m_out->Print( 2, "(LINE X=%.10f Y=%.10f)\n", iu2hyp( ph0.x ), iu2hyp( ph0.y ) );
+
+                    m_out->Print( 1, "}\n" );
+                }
+
+                m_polyId++;
+            }
+        }
+    }
+
+    return true;
+}
+
+const std::vector<BOARD_ITEM*> HYPERLYNX_EXPORTER::collectNetObjects( int netcode )
+{
+    std::vector<BOARD_ITEM*> rv;
+
+    auto check = [&]( BOARD_CONNECTED_ITEM* item ) -> bool {
+        if( ( item->GetLayerSet() & LSET::AllCuMask() ).none() )
+            return false;
+        if( item->GetNetCode() == netcode || ( netcode < 0 && item->GetNetCode() <= 0 ) )
+            return true;
+        return false;
+    };
+
+    for( auto mod : m_board->Modules() )
+    {
+        for( auto pad : mod->Pads() )
+        {
+            if( check( pad ) )
+                rv.push_back( pad );
+        }
+    }
+
+    for( auto item : m_board->Tracks() )
+        if( check( item ) )
+            rv.push_back( item );
+
+    for( int i = 0; i < m_board->GetAreaCount(); i++ )
+    {
+        auto zone = m_board->GetArea( i );
+        if( check( zone ) )
+            rv.push_back( zone );
+    }
+    return rv;
+}
+
+bool HYPERLYNX_EXPORTER::writeNets()
+{
+    m_polyId = 1;
+
+    for( const auto netInfo : m_board->GetNetInfo() )
+    {
+        int netcode = netInfo->GetNet();
+
+        //printf( " netCode %d '%s' \n", netcode, (const char*) netInfo->GetNetname().c_str() );
+
+        bool isNullNet = netInfo->GetNet() <= 0 || netInfo->GetNetname().IsEmpty();
+        if( isNullNet )
+            continue;
+
+        auto netObjects = collectNetObjects( netcode );
+        if( netObjects.size() )
+        {
+
+            m_out->Print( 0, "{NET \"%s\"\n", (const char*) netInfo->GetNetname().c_str() );
+            writeNetObjects( netObjects );
+            m_out->Print( 0, "}\n\n" );
+        }
+    }
+
+    auto nullNetObjects = collectNetObjects( -1 );
+
+    //printf( "Null net objects: %d\n", nullNetObjects.size() );
+
+    int idx = 0;
+
+    for( auto item : nullNetObjects )
+    {
+        m_out->Print( 0, "{NET \"EmptyNet%d\"\n", idx );
+        writeNetObjects( { item } );
+        m_out->Print( 0, "}\n\n" );
+        idx++;
+    }
+
+    return true;
+}
+
+bool HYPERLYNX_EXPORTER::Run()
+{
+    LOCALE_IO toggle; // toggles on, then off, the C locale.
+
+    m_out.reset( new FILE_OUTPUTFORMATTER( m_outputFilePath.GetFullPath() ) );
+
+    generateHeaders();
+    writeBoardInfo();
+    writeStackupInfo();
+    writeDevices();
+    writePadStacks();
+    writeNets();
+
+    return true;
+}
+
+
+bool ExportBoardToHyperlynx( BOARD* aBoard, const wxFileName& aPath )
+{
+    HYPERLYNX_EXPORTER exporter;
+    exporter.SetBoard( aBoard );
+    exporter.SetOutputFilename( aPath );
+    return exporter.Run();
+}
diff --git a/pcbnew/menubar_pcb_editor.cpp b/pcbnew/menubar_pcb_editor.cpp
index 422456245..3e0711920 100644
--- a/pcbnew/menubar_pcb_editor.cpp
+++ b/pcbnew/menubar_pcb_editor.cpp
@@ -970,4 +970,9 @@ void prepareExportMenu( wxMenu* aParentMenu )
                  _( "&Footprint Association (.cmp) File..." ),
                  _( "Export footprint association file (*.cmp) for schematic back annotation" ),
                  KiBitmap( create_cmp_file_xpm ) );
+
+    AddMenuItem( aParentMenu, ID_GEN_EXPORT_FILE_HYPERLYNX,
+                 _( "&Hyperlynx..." ), _( "Hyperlynx export" ),
+                 KiBitmap( export_step_xpm ) );
+
 }
diff --git a/pcbnew/pcb_edit_frame.cpp b/pcbnew/pcb_edit_frame.cpp
index b4db36fb5..369e63a75 100644
--- a/pcbnew/pcb_edit_frame.cpp
+++ b/pcbnew/pcb_edit_frame.cpp
@@ -128,6 +128,7 @@ BEGIN_EVENT_TABLE( PCB_EDIT_FRAME, PCB_BASE_FRAME )
     EVT_MENU( ID_GEN_EXPORT_FILE_VRML, PCB_EDIT_FRAME::OnExportVRML )
     EVT_MENU( ID_GEN_EXPORT_FILE_IDF3, PCB_EDIT_FRAME::OnExportIDF3 )
     EVT_MENU( ID_GEN_EXPORT_FILE_STEP, PCB_EDIT_FRAME::OnExportSTEP )
+    EVT_MENU( ID_GEN_EXPORT_FILE_HYPERLYNX, PCB_EDIT_FRAME::OnExportHyperlynx )
 
     EVT_MENU( ID_GEN_IMPORT_SPECCTRA_SESSION,PCB_EDIT_FRAME::ImportSpecctraSession )
     EVT_MENU( ID_GEN_IMPORT_SPECCTRA_DESIGN, PCB_EDIT_FRAME::ImportSpecctraDesign )
@@ -1369,3 +1370,31 @@ void PCB_EDIT_FRAME::LockModule( MODULE* aModule, bool aLocked )
         }
     }
 }
+
+bool ExportBoardToHyperlynx( BOARD* aBoard, const wxFileName& aPath );
+
+void PCB_EDIT_FRAME::OnExportHyperlynx( wxCommandEvent& event )
+{
+    wxString    wildcard =  wxT("*.hyp");
+    wxFileName  fn = GetBoard()->GetFileName();
+
+    fn.SetExt( wxT("hyp") );
+
+    wxFileDialog dlg( this,
+            _( "Export Hyperlynx Layout" ),
+            fn.GetPath(),
+            fn.GetFullName(),
+            wildcard,
+            wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+            );
+
+    if( dlg.ShowModal() != wxID_OK )
+        return;
+
+    fn = dlg.GetPath();
+
+    // always enforce filename extension, user may not have entered it.
+    fn.SetExt( wxT("hyp") );
+
+    ExportBoardToHyperlynx( GetBoard(), fn );
+}
diff --git a/pcbnew/pcb_edit_frame.h b/pcbnew/pcb_edit_frame.h
index a916893b2..cc6f5f89a 100644
--- a/pcbnew/pcb_edit_frame.h
+++ b/pcbnew/pcb_edit_frame.h
@@ -1044,6 +1044,13 @@ public:
      */
     void OnExportIDF3( wxCommandEvent& event );
 
+    /**
+     * Function OnExportHyperlynx
+     * will export the current BOARD to a Hyperlynx HYP file.
+     */
+    void OnExportHyperlynx( wxCommandEvent& event );
+
+
     /**
      * Function Export_IDF3
      * Creates an IDF3 compliant BOARD (*.emn) and LIBRARY (*.emp) file.
diff --git a/pcbnew/pcbnew_id.h b/pcbnew/pcbnew_id.h
index dfe402af5..0d18e2e00 100644
--- a/pcbnew/pcbnew_id.h
+++ b/pcbnew/pcbnew_id.h
@@ -273,6 +273,7 @@ enum pcbnew_ids
     ID_GEN_EXPORT_FILE_IDF3,
     ID_GEN_EXPORT_FILE_VRML,
     ID_GEN_EXPORT_FILE_STEP,
+    ID_GEN_EXPORT_FILE_HYPERLYNX,
     ID_GEN_EXPORT_SPECCTRA,
     ID_GEN_EXPORT_FILE_GENCADFORMAT,
     ID_GEN_EXPORT_FILE_MODULE_REPORT,
diff --git a/qa/CMakeLists.txt b/qa/CMakeLists.txt
index 5822170df..eab06c313 100644
--- a/qa/CMakeLists.txt
+++ b/qa/CMakeLists.txt
@@ -19,6 +19,7 @@ add_subdirectory( unit_test_utils )
 
 # Unit tests
 add_subdirectory( common )
+add_subdirectory( hyperlynx_export )
 add_subdirectory( pcbnew )
 add_subdirectory( eeschema )
 
diff --git a/qa/hyperlynx_export/CMakeLists.txt b/qa/hyperlynx_export/CMakeLists.txt
new file mode 100644
index 000000000..0e279b370
--- /dev/null
+++ b/qa/hyperlynx_export/CMakeLists.txt
@@ -0,0 +1,64 @@
+# This program source code file is part of KiCad, a free EDA CAD application.
+#
+# Copyright (C) 2018 KiCad Developers, see CHANGELOG.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
+
+if( BUILD_GITHUB_PLUGIN )
+    set( GITHUB_PLUGIN_LIBRARIES github_plugin )
+endif()
+
+include_directories( ../../include
+                     ../../common
+                     ../../pcbnew
+                     ../.. )
+
+add_executable( test_hyperlynx_export
+    # stuff from common which is needed...why?
+    ../../common/colors.cpp
+    ../../common/observable.cpp
+
+    # The main test entry points
+    test_hyperlynx_export.cpp
+
+    # Older CMakes cannot link OBJECT libraries
+    # https://cmake.org/pipermail/cmake/2013-November/056263.html
+    $<TARGET_OBJECTS:pcbnew_kiface_objects>
+)
+
+target_link_libraries( test_hyperlynx_export
+    3d-viewer
+    connectivity
+    pcbcommon
+    pnsrouter
+    pcad2kicadpcb
+    legacy_wx
+    gal
+    common
+    qa_utils
+    lib_dxf
+    idf3
+    unit_test_utils
+    ${wxWidgets_LIBRARIES}
+    ${GITHUB_PLUGIN_LIBRARIES}
+    ${GDI_PLUS_LIBRARIES}
+    ${PYTHON_LIBRARIES}
+    ${Boost_LIBRARIES}      # must follow GITHUB
+    ${PCBNEW_EXTRA_LIBS}    # -lrt must follow Boost
+)
+
diff --git a/qa/hyperlynx_export/test_hyperlynx_export.cpp b/qa/hyperlynx_export/test_hyperlynx_export.cpp
new file mode 100644
index 000000000..479c25446
--- /dev/null
+++ b/qa/hyperlynx_export/test_hyperlynx_export.cpp
@@ -0,0 +1,61 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2018 KiCad Developers, see CHANGELOG.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
+ */
+
+/**
+ * Main file for the pcbnew tests to be compiled
+ */
+
+#include <wx/wx.h>
+#include <class_board.h>
+#include <io_mgr.h>
+#include <kicad_plugin.h>
+
+bool ExportBoardToHyperlynx( BOARD* aBoard, const wxFileName& aPath );
+
+BOARD* loadBoard( const std::string& filename )
+{
+    PLUGIN::RELEASER pi( new PCB_IO );
+    BOARD* brd = nullptr;
+
+    try
+    {
+        brd = pi->Load( wxString( filename.c_str() ), NULL, NULL );
+    }
+    catch( const IO_ERROR& ioe )
+    {
+        wxString msg = wxString::Format( _( "Error loading board.\n%s" ),
+                ioe.Problem() );
+
+        printf( "%s\n", (const char*) msg.mb_str() );
+        return nullptr;
+    }
+    return brd;
+
+}
+
+int main( int argc, char* argv[] )
+{
+    auto brd = loadBoard("test.kicad_pcb");
+    ExportBoardToHyperlynx( brd, wxString( "test.hyp" ) );
+    return 0;
+}
\ No newline at end of file
-- 
2.17.1


Follow ups