Replicating pcbnew new for arrayed sheets

Many circuit designs can have repeated structures. Sadly, pcbnew doesn’t have any features to make the placement and routing any more productive.

Here is a demo of a script I wrote to help:

If you have X copies of the same sheet in eeschema, the script will allow you to place and route one of them and them apply that to the other sheets.

How’s it work?

If you call GetPath() on the modules in your design, you’ll get values like these (note the the path is only the /number/number part):

mod Q1 path /587DA765/58758821
mod Q2 path /5875D13C/58758821
mod Q3 path /5875D13D/58758821
mod Q4 path /5875D13E/58758821
mod R1 path /587DA765/5875882F
mod R2 path /587DA765/5875883D
mod R3 path /5875D13C/5875882F
mod R4 path /5875D13C/5875883D

The first number tells you which sheet instance the module belongs to. The second is an identifier for the module. So from the information above we can tell that these are all in the same sheet:

mod Q1 path /587DA765/58758821
mod R1 path /587DA765/5875882F
mod R2 path /587DA765/5875883D

Similarly, we know that these are the same transistor, just in different instances of the sheet:

mod Q1 path /587DA765/58758821
mod Q2 path /5875D13C/58758821
mod Q3 path /5875D13D/58758821
mod Q4 path /5875D13E/58758821

So if I have placed and routed sheet 587DA765, the script just needs to find all of the members of 5875D13C and simply adjust their positions by some constant increment.

The script can be found here in my github repository for kicad

Querying pcbnew to generate a basic svg file

In a previous post, I gave some basic query capabilies in pcbnew. In this post, I’ll use that to generate a simple svg file.

The result looks like this:

Not the most interesting layout and a bunch of detail is missing, but I was happy to get this far. The code is hopefully self-explanatory. After the last post, I don’t really know what I can add.

The code can be found in my github

Preliminaries

I need to get each of pcbnew’s layer colors. Some of the color names are modified to match svr colors. The real answer is to get the rgb values from colorrefs, but those aren’t exposed in python today

Edit Nov 15, 2017 RGB values are now available in the form of the COLOR4D structure.


colors = board.Colors()

c4 = colors.GetLayerColor(track.GetLayer())

r = int(round(c4.r*255)

g = int(round(c4.g*255)),
b = int(round(c4.b*255))

The colornames map below won’t work anymore.

End edit

colornames = {
 pcbnew.BLACK: 'BLACK', 
 pcbnew.DARKDARKGRAY: 'DARKSLATEGRAY', # 'DARKDARKGRAY', 
 pcbnew.DARKGRAY: 'DARKGRAY', 
 pcbnew.LIGHTGRAY: 'LIGHTGRAY', 
 pcbnew.WHITE: 'WHITE', 
 pcbnew.LIGHTYELLOW: 'LIGHTYELLOW', 
 pcbnew.DARKBLUE: 'DARKBLUE', 
 pcbnew.DARKGREEN: 'DARKGREEN', 
 pcbnew.DARKCYAN: 'DARKCYAN', 
 pcbnew.DARKRED: 'DARKRED', 
 pcbnew.DARKMAGENTA: 'DARKMAGENTA', 
 pcbnew.DARKBROWN: 'MAROON', # 'DARKBROWN', 
 pcbnew.BLUE: 'BLUE', 
 pcbnew.GREEN: 'GREEN', 
 pcbnew.CYAN: 'CYAN', 
 pcbnew.RED: 'RED', 
 pcbnew.MAGENTA: 'MAGENTA', 
 pcbnew.BROWN: 'BROWN', 
 pcbnew.LIGHTBLUE: 'LIGHTBLUE', 
 pcbnew.LIGHTGREEN: 'LIGHTGREEN', 
 pcbnew.LIGHTCYAN: 'LIGHTCYAN', 
 pcbnew.LIGHTRED: 'INDIANRED', # 'LIGHTRED', 
 pcbnew.LIGHTMAGENTA: 'LIGHTPINK', # 'LIGHTMAGENTA', 
 pcbnew.YELLOW: 'YELLOW', 
 pcbnew.PUREBLUE: 'MEDIUMBLUE', # 'PUREBLUE', 
 pcbnew.PUREGREEN: 'LAWNGREEN', # 'PUREGREEN', 
 pcbnew.PURECYAN: 'DARKTURQUOISE', # 'PURECYAN', 
 pcbnew.PURERED: 'FIREBRICK', # 'PURERED', 
 pcbnew.PUREMAGENTA: 'DARKORCHID', # PUREMAGENTA', 
 pcbnew.PUREYELLOW: 'KHAKI', # PUREYELLOW' 
}

padshapes = {
 pcbnew.PAD_SHAPE_CIRCLE: "PAD_SHAPE_CIRCLE",
 pcbnew.PAD_SHAPE_OVAL: "PAD_SHAPE_OVAL",
 pcbnew.PAD_SHAPE_RECT: "PAD_SHAPE_RECT",
 pcbnew.PAD_SHAPE_TRAPEZOID: "PAD_SHAPE_TRAPEZOID" 
}
# new in the most recent kicad code
if hasattr(pcbnew, 'PAD_SHAPE_ROUNDRECT'):
 padshapes[pcbnew.PAD_SHAPE_ROUNDRECT] = "PAD_SHAPE_ROUNDRECT",

Now we can get to it. Need to get the board, get the boundary coordinates, and set the scale factor

board = pcbnew.GetBoard()
boardbbox = board.ComputeBoundingBox()
boardxl = boardbbox.GetX()
boardyl = boardbbox.GetY()
boardwidth = boardbbox.GetWidth()
boardheight = boardbbox.GetHeight()

# the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm)
# the coordinate 121550000 corresponds to 121.550000

SCALE = 1000000.0

Writing an SVG

Here is where things happen. I’m sure if kicad include the svgwrite package or if it picks it up from my python install area. I did need to install it to python to do some experiments before doing anything in Kicad.

Edit Nov 15, 2017 

Due to kicad’s change to color management (in favor of RGB over colornames), the code below is not uptodate. Go to github for the latest working version.

End edit

import sys, os
import svgwrite

print("working in the dir " + os.getcwd())
name = "output.svg"
# A4 is approximately 21x29
dwg = svgwrite.Drawing(name, size=('21cm', '29cm'), profile='full', debug=True)

dwg.viewbox(width=boardwidth, height=boardheight, minx=boardxl, miny=boardyl)
background = dwg.add(dwg.g(id='bg', stroke='white'))
background.add(dwg.rect(insert=(boardxl, boardyl), size=(boardwidth, boardheight), fill='white'))

svglayers = {}
for colorcode, colorname in colornames.items():
 layer = dwg.add(dwg.g(id='layer_'+colorname, stroke=colorname.lower(), stroke_linecap="round"))
 svglayers[colorcode] = layer

alltracks = board.GetTracks() 
for track in alltracks:
 # print("{}->{}".format(track.GetStart(), track.GetEnd()))
 # print("{},{}->{},{} width {} layer {}".format(track.GetStart().x/SCALE, track.GetStart().y/SCALE,
 # track.GetEnd().x/SCALE, track.GetEnd().y/SCALE,
 # track.GetWidth()/SCALE,
 # track.GetLayer()) 
 # )
 layercolor = board.GetLayerColor(track.GetLayer())
 svglayers[layercolor].add(dwg.line(start=(track.GetStart().x,
 track.GetStart().y),
 end=(track.GetEnd().x,
 track.GetEnd().y),
 stroke_width=track.GetWidth()
 ))


svgpads = dwg.add(dwg.g(id='pads', stroke='red',fill='orange'))
allpads = board.GetPads()

for pad in allpads:
 mod = pad.GetParent()
 name = pad.GetPadName()
 if (0):
 print("pad {}({}) on {}({}) at {},{} shape {} size {},{}"
 .format(name,
 pad.GetNet().GetNetname(),
 mod.GetReference(),
 mod.GetValue(),
 pad.GetPosition().x, pad.GetPosition().y,
 padshapes[pad.GetShape()],
 pad.GetSize().x, pad.GetSize().y
 ))
 if (pad.GetShape() == pcbnew.PAD_SHAPE_RECT):
 if ((pad.GetOrientationDegrees()==270) | (pad.GetOrientationDegrees()==90)):
 svgpads.add(dwg.rect(insert=(pad.GetPosition().x-pad.GetSize().x/2,
 pad.GetPosition().y-pad.GetSize().y/2),
 size=(pad.GetSize().y, pad.GetSize().x)))
 else:
 svgpads.add(dwg.rect(insert=(pad.GetPosition().x-pad.GetSize().x/2,
 pad.GetPosition().y-pad.GetSize().y/2),
 size=(pad.GetSize().x, pad.GetSize().y)))
 elif (pad.GetShape() == pcbnew.PAD_SHAPE_CIRCLE):
 svgpads.add(dwg.circle(center=(pad.GetPosition().x, pad.GetPosition().y),
 r=pad.GetSize().x))
 elif (pad.GetShape() == pcbnew.PAD_SHAPE_OVAL):
 svgpads.add(dwg.ellipse(center=(pad.GetPosition().x, pad.GetPosition().y),
 r=(pad.GetSize().x/2, pad.GetSize().y/2)))
 else:
 print("unknown pad shape {}({})".format(pad.GetShape(), padshapes[pad.GetShape()]))


 
dwg.save()

 

modifying pcbnew layout from python

I my previous post, I talked about querying for information about your layout. In this one I’ll show you how to create your own wires/vias from python. I also cover moving modules. Most of the code is self-explanatory but I find it helpful to have sample “cookbook” code.

The main thing to keep in mind when creating new objects is that even though you have to pass a board pointer to the constructors, you still have to call board.Add(obj). Also, you have to add it before setting the net. If you try to set the net before, it’ll do nothing.

Remember that the units are 1E-6mm. So if you have a mm value multiply it by a million.

Layers

First, let’s generate our layer mapping

layertable = {}

numlayers = pcbnew.LAYER_ID_COUNT
for i in range(numlayers):
 layertable[i] = board.GetLayerName(i)
 print("{} {}".format(i, board.GetLayerName(i)))

Add a track

track = pcbnew.TRACK(board)
track.SetStart(pcbnew.wxPoint(136144000, 95504000))
track.SetEnd(pcbnew.wxPoint(176144000, 95504000))
track.SetWidth(1614400)
track.SetLayer(layertable["F.Cu"])

board.Add(track)
track.SetNet(clknet)

Add a via

In this case, I’m going to copy an existing via. Note that there is also the clone method, but doing it this way you’ll know how to generate a via from scratch.

There isn’t yet a direct way to query a via for its layers. The way I work around this is by looping through all layers and calling IsOnLayer This is one of the reasons I’m showing how to copy an existing via.

The via types will be one of these:

  • pcbnew.VIA_THROUGH
  • pcbnew.VIA_BLIND_BURIED
  • pcbnew.VIA_MICROVIA

The width if the via is the diameter. If you forget to set this, you’ll get funny behavior where via disappears from the display when you zoom in.

newvia = pcbnew.VIA(board)
# need to add before SetNet will work, 
# so just doing it first
board.Add(newvia)

toplayer=-1
bottomlayer=pcbnew.LAYER_ID_COUNT
for l in range(pcbnew.LAYER_ID_COUNT):
   if not track.IsOnLayer(l):
      continue
   toplayer = max(toplayer, l)
   bottomlayer = min(bottomlayer, l)

# now that I have the top and bottom layers, I tell the new
# via
newvia.SetLayerPair(toplayer, bottomlayer)
newvia.SetPosition(pcbnew.wxPoint(track.GetPosition().x+offset[0],
                                  track.GetPosition().y+offset[1]))
newvia.SetViaType(oldvia.GetViaType())
newvia.SetWidth(oldvia.GetWidth())
newvia.SetNet(tonet)

Moving a module

I haven’t tried creating a new module yet. I prefer to let the netlist importer do this for me. I do, however, find it useful to be able to move modules. They all come in on top of each other. There are a variety of placement algorithms one might want to implement. 1

Note that the orientation is degrees*10.0

peer.SetPosition(pcbnew.wxPoint(newx, newy))
peer.SetOrientation(180*10)

Class diagram

I’ve created a class diagram to help me remember. Click to enlarge. Also available in my github


  1. In my previous professional life, one of the more interesting ones I saw was using linear programming. You start with everything in the middle. You create a set of equations representing net connectivity as well as cell overlaps. Solve the equations. Repeat. It was good for a couple hundred thousand cells. Much more than what PCB requires. Simulated Annealing would likely be easier here.

The basics of scripting in pcbnew

I’ve found that, so far, 1 I’m able to do all of the layout queries and manipulations I’ve wanted to do.

Note that these examples don’t work on the latest release as of Feb 2017 (4.0.5). You want one of the nightly builds, also in the kicad download area. Or you can build it

The interface is lacking some consistency but it’s fine if you have a map of the classes (click for a larger version or download it from here):

 

In this post, I’ll focus on querying a board for information about it’s contents. The code can be found in this github repo.

Getting started

To invoke any of these examples, you’ll want pcbnew’s scripting window. Tools->scripting console

You’ll probably want these in your scripts

import pcbnew

# most queries start with a board
board = pcbnew.GetBoard()

Nets

Want to know all of the nets in your board?
Nets can be looked up in two ways:

  • by name
  • by netcode – a unique integer identifier for your net.

If you run this code:

# returns a dictionary netcode:netinfo_item
netcodes = board.GetNetsByNetcode()

# list off all of the nets in the board.
for netcode, net in netcodes.items():
    print("netcode {}, name {}".format(netcode, net.GetNetname()))

# here's another way of doing the same thing.
print("here's the other way to do it")
nets = board.GetNetsByName()
for netname, net in nets.items():
    print("method2 netcode {}, name{}".format(net.GetNet(), netname))

# maybe you just want a single net
# the find method returns an iterator to all matching nets.
# the value of an iterator is a tuple: name, netinfo
clknet = nets.find("/clk").value()[1]
clkclass = clknet.GetNetClass()

print("net {} is on netclass {}".format(clknet.GetNetname(),
 clkclass))

You’ll get something like this:

netcode 49, name /ihg
netcode 50, name /ihh
netcode 51, name /data_in
netcode 52, name /data_out
netcode 53, name /clk
here's the other way to do it
method2 netcode 0, name
method2 netcode 23, name+5V
method2 netcode 53, name/clk
method2 netcode 55, name/data_contd
method2 netcode 51, name/data_in

Physical dimensions

The coordinate space of kicad_pcb is in mm. At the beginning of this wiki about Kicad’s Board_File_Format

“All physical units are in mils (1/1000th inch) unless otherwise noted.”

Then later in historical notes, it says,

“As of 2013, the PCBnew application creates ‘.kicad_pcb’ files that begin with (kicad_pcb (version 3)”. All distances are in millimeters.

In short, for the data that I’ve recently created, the internal coordinate space of pcbnew is 10E-6 mm. (a millionth of a mm)
For example, the coordinate 121550000 corresponds to 121.550000mm

SCALE = 1000000.0

boardbbox = board.ComputeBoundingBox()
boardxl = boardbbox.GetX()
boardyl = boardbbox.GetY()
boardwidth = boardbbox.GetWidth()
boardheight = boardbbox.GetHeight()

print("this board is at position {},{} {} wide and {} high".format(boardxl,
    boardyl,
    boardwidth,
    boardheight))

Modules/Pads

Each of your placed modules can be found with its reference name. The module connection points are pads, of course.

# generate a LUT with shape integers to a string
padshapes = {
    pcbnew.PAD_SHAPE_CIRCLE: "PAD_SHAPE_CIRCLE",
    pcbnew.PAD_SHAPE_OVAL: "PAD_SHAPE_OVAL",
    pcbnew.PAD_SHAPE_RECT: "PAD_SHAPE_RECT",
    pcbnew.PAD_SHAPE_TRAPEZOID: "PAD_SHAPE_TRAPEZOID"
}
# new in the most recent kicad code
if hasattr(pcbnew, 'PAD_SHAPE_ROUNDRECT'):
    padshapes[pcbnew.PAD_SHAPE_ROUNDRECT] = "PAD_SHAPE_ROUNDRECT",

modref = "U1"
mod = board.FindModuleByReference(modref)
for pad in mod.Pads():
    print("pad {}({}) on {}({}) at {},{} shape {} size {},{}"
        .format(pad.GetPadName(),
                pad.GetNet().GetNetname(),
                mod.GetReference(),
                mod.GetValue(),
                pad.GetPosition().x, pad.GetPosition().y,
                padshapes[pad.GetShape()],
                pad.GetSize().x, pad.GetSize().y
    ))

Gives you this:

pad 1(/ilb) on U1(74HC595) at 127635000,106520000 shape PAD_SHAPE_RECT size 1500000,600000
pad 2(/ilc) on U1(74HC595) at 126365000,106520000 shape PAD_SHAPE_RECT size 1500000,600000
pad 3(/ild) on U1(74HC595) at 125095000,106520000 shape PAD_SHAPE_RECT size 1500000,600000

Layer

Most of the pcb data is on a layer. pcbnew stores layers as numbers. Here we can print them all out

layertable = {}

numlayers = pcbnew.LAYER_ID_COUNT
   for i in range(numlayers):
       layertable[i] = board.GetLayerName(i)
       print("{} {}".format(i, board.GetLayerName(i)))

Which gives you this:

0 F.Cu
1 In1.Cu
2 In2.Cu
3 In3.Cu
...

Tracks (wires and vias)

A wire is stored in a TRACK object. Vias are in a class derived from TRACK. Let’s list some of them.

# clk net was defined above as was SCALE
clktracks = board.TracksInNet(clknet.GetNet())
   for track in clktracks:
       print("{},{}->{},{} width {} layer {}".format(track.GetStart().x/SCALE,
             track.GetStart().y/SCALE,
             track.GetEnd().x/SCALE,
             track.GetEnd().y/SCALE,
             track.GetWidth()/SCALE,
             layertable[track.GetLayer()]))

And here are the wires

121.92,97.79->126.492,97.79 width 0.25 layer F.Cu
127.762,100.33->125.984,100.33 width 0.25 layer F.Cu
127.762,99.314->127.762,100.33 width 0.25 layer B.Cu

So that’s how to query data. In my next post, I’ll talk about making changes


  1. thanks to recent work by Kicad contributor/developer Dick Hollenbeck