[ home ]

A tool helping with manual assembly of PCBs

This page describes a script for generating a Gerber-file containing indicators at component-locations. This can save a lot of time when manually populating proto-boards.

All this is probably only useful if you use KiCad, since it relies on the format of KiCad-generated centroid-files.


To manually assemble a non-trivial PCB with many components e.g. in case of a proto, it takes time to have to lookup the component-type/-value of each land to be populated, 'live', in the EDA-tool.

It would be better to have a map of locations of all components of a given type. I didn't know of a way to generate one using the KiCad suite of programs.


Existing Gerber-viewers (e.g. 'gerbview' or 'gerbv' that come with the KiCad resp. gEDA suite) can already display a stack of single-layer images. KiCad (or any other EDA-tool) can already generate its board-images as Gerber-files.

Therefore, I made a script to convert a KiCad centroid-file (X/Y/rotation) to a Gerber-image containing dots at the locations where components should be placed. This layer can then be overlayed onto e.g. the paste-layer image, to get a quick visual overview of where to place components.


  1. create a board in KiCad and generate Gerbers and a centroid-file for it
  2. make a plaintext file containing refdeses (is that a word?) of all components to generate indicators for
  3. run aforementioned script to generate a Gerber-file containing component-indicators
  4. simultaneoisly display this generated Gerber and e.g. the paste-layer of the board
  5. place the components at the locations shown by the indicators

The 'kicad-pos2gerb.pl' script itself at time of writing:

    use strict;
    use warnings;
    #   This is a tool for manually placing components on a PCB during assembly.
    #   A Gerber-file containing dots at every component-position is constructed, taking as input
    #   a KiCad position-file as generated by 'pcbnew' as well as a separate file containing
    #   a list of refdes, one per line.
    #   This generated Gerber-file, when viewed in a Gerber-viewer (e.g. 'gerbv' or 'gerbview')
    #   as an overlay onto the silk-/paste-/copper-layers, can speed up manually placing many
    #   components of the same type onto a PCB.
    # constants: accuracy-specifyers for Gerber encoding.
    my $num_dec  = 3;
    my $num_frac = 3;
    my $linenr = 0;
    my %refdes;   # key exists if refdes occurs in input-file; 
                  # value is x/y coordinate-pair if refdes occurred in pos-file
    sub exit_with_help { die "Use: <KiCad_position_filename> <refdes_list_filename> <gerber_output_filename>\n" }
    sub open_file 
        my ( $fname, $rw ) = @_; 
        $rw ||= "<";
        open my $fh, $rw, $fname or die "cannot open file '$fname'"; 
    sub strip_ws { my ( $s ) = @_; $s =~ s/^\s*(.*)[\s\n]*$/$1/; $s; }
    sub read_refdeses
        my $fh = open_file $_[ 0 ];
        foreach my $r (<$fh>)
            $r = strip_ws $r;
            $refdes{ $r } = undef;
        close $fh;
    sub process_posfile_line
        my ( $p ) = @_;
        # Assume only the 'value' field can contain whitespace. KiCad unfortunately
        # uses whitespace as field-separator, and clips fields that are too long.
        #           ref     val         pkg     posx    posy    rot     side
        $p =~ /^\s*(\S+)\s+(\S.*\S*)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$/ or die "syntax error in position-file line $linenr\n";
        my ( $refdes, $x, $y ) = ( $1, $4, $5 );
        $refdes{ $refdes } = { x => $x, y => $y } if exists $refdes{ $refdes };
    sub process_posfile
        my $fh = open_file $_[ 0 ];
        foreach my $p (<$fh>)
            $p = strip_ws $p;
            next if $p =~ /^#/;
            next if $p =~ /^\s*$/;
            process_posfile_line $p;
        close $fh;
    sub write_gerber_header
        my ( $fh ) = @_;
        my $fmt = "$num_dec$num_frac";
        print $fh "%FSLAX${fmt}Y${fmt}*%\n";   # specify num format, omit leading zeroes, abs positions
        print $fh "%MOMM*%\n";                 # units are millimeters
        print $fh "%ADD10C,1*%\n";             # 1mm-dia solid circle as position-indicator
        print $fh "%LPD*%\n";                  # start new dark layer (I think this is redundant)
        print $fh "D10*\n";                    # select our aperture (I think this is redundant too)
    sub write_gerber_footer { my $fh = $_[ 0 ]; print $fh "M02*\n" }
    sub gerber_numeral
        my ( $f ) = @_;
        my $s = sprintf "%$num_dec.${num_frac}f", $f;
        $s =~ s/\.//;
    sub write_gerber_pos_indicator
        my ( $fh, $x, $y ) = @_;
        # (D3 = flash selected aperture at given coordinates)
        print $fh "X" . gerber_numeral( $x ) . "Y" . gerber_numeral( $y ) . "D3*\n";
    sub generate_gerber
        my $fh = open_file $_[ 0 ], ">";
        write_gerber_header $fh;
        foreach my $r ( keys %refdes )
            my $c = $refdes{ $r };
            if ( $c ) { write_gerber_pos_indicator $fh, $c->{ x }, $c->{ y }     }
            else      { print "warning: refdes '$r' not in pos-file - ignored\n" }
        write_gerber_footer $fh;
        close $fh;
    my $pos_fname    = $ARGV[ 0 ] or exit_with_help;
    my $refdes_fname = $ARGV[ 1 ] or exit_with_help;
    my $gerber_fname = $ARGV[ 2 ] or exit_with_help;
    read_refdeses   $refdes_fname;
    process_posfile $pos_fname;
    generate_gerber $gerber_fname;

And that's all.

NB: personal reminders only beyond this point :-)

Some reminders for myself: additional things interacting with parts-database

get part-IDs for an assembly:

    sql ~/proj/046/db/046.db 'select childpartid from assemblies where parentpartid = 1267 group by childpartid order by count( childpartid ) desc' | sed 1,2d

(the 'sed 1,2d' are used to remove column-header and blank line, contained in the output at the time of writing)

Generate gerbers for each component-type:

    for p in $( cat partids.txt ); do 
      echo ---$p---
      sql $db "select refdes from assemblies where parentpartid = $assy and childpartid = $p" | sed 1,2d > p$p.txt
      kicad-pos2gerb.pl mainboard-sig1.pos p$p.txt tmp.pho
      N=$( grep 'D3\*$' tmp.pho | wc -l | tr -d ' ' )
      mv tmp.pho p${p}_$N.pho
      echo "  $N"

(the 'N' is the number of components of that part-ID; easy to be able to see as the layer-name while viewing the Gerber. Through-hole components generally don't occur in the centroid-file, so 'N' will be 0 for part-IDs corresponding to through-hole components.)

View generated and paste-layer Gerbers:

View generated Gerbers each corresponding to a single component-type, one by one:

    for a in $( ls -S p*.pho ); do 
        ./mk_gerbv_proj.pl mainboard-F_Paste.pho $a > tmp.gerbv
        gerbv -p tmp.gerbv

The 'mk_gerbv_proj.pl' script at the time of writing:

    use strict;
    use warnings;
    #   this script creates a gerbv (gEDA Gerber viewer) project consisting of 2 layers; one is 
    #   meant to be a generated layer consisting component-location indicators, and the other is some
    #   image of the board, e.g. front paste layer.
    #   The component-placement layer (the topmost one in the layer-stack) is shown in a bright colour,
    #   while the board-layer is shown in a dark colour.
    #   The project contents (just a few lines) are printed on stdout on succes. 
    my $boardlayer_fname = $ARGV[ 0 ] or die "use board layer as 1st argument\n";
    my $poslayer_fname   = $ARGV[ 1 ] or die "use position-indicator layer as 2nd argument\n";
    print "(gerbv-file-version! \"2.0A\")\n";
    print "(define-layer! 1 (cons 'filename \"$boardlayer_fname\")(cons 'visible #t)(cons 'color #(16912 16912 16912)))\n";
    print "(define-layer! 0 (cons 'filename \"$poslayer_fname\")(cons 'visible #t)(cons 'color #(65535 0 0)))\n";
    print "(set-render-type! 0)\n";

(Using a gerbv project-file instead of just calling it with the 2 Gerber's filenames as arguments, allows one to specify colours for each layer. I have no idea if this is possible without using a project-file.)