Generate ice40 FPGA pin mapping from schematic

I’d like to use KiCad to design a PCB with a FPGA. One of the great things of a FPGA is that (almost) all IO pins of the FPGA are equivalent, so I can swap pins until the pcb layout is as simple as can be. At this point KiCad knows how the FPGA is connected. See this minimal example:

Now I would like KiCad to generate “io.pcf” so the FPGA toolchain (symbiflow) knows how the IO is connected:

#generated by KiCad from playground.kicad_sch
#io.pcf for ICE40UP5K-SG48ITR (U1)

set_io FPGA_LED1 23
set_io FPGA_LED2 31
set_io FPGA_LED3 35
set_io FPGA_LED4 43

Is this possible in KiCad?
If I want to code this myself in KiCad (or in a python extention), where should I begin?

Is this a big FPGA, or just some 50 pins as your screenshot suggests?
For 50 pins some manual renaming is probably quicker then trying to automate it.

If you’re looking for ways to automate for (future) bigger projects there are some ways.

First, have a look at the SKiDL Project. SKiDL is a python project that can create a “schematic equivalent” in code and can also generate a KiCad netlist. I see it as a sort of VHDL for schematic design.

Another way of automation is to write a Python script to generate custom schematic symbols for your FPGA, and then also generate a file for your FPGA pin mapping. I do not know much of python, but a few years ago I experimented with it and I wrote a python program that generated a KiCad library for some 40 schematic symbols for connectors in an afternoon. The description of KiCad’s file formats is useful if you want to go this way: https://kicad.org/help/file-formats/

A few years ago I saw a project that can copy text from pin data from a .pdf datasheet into a spreadsheet, which can then be modified and then be turned into a KiCad Schematic symbol by a script.

There are quite a lot of side projects and scripts around KiCad. You can find an overview of more then 70 of those side projects on:

A word of caution:
KiCad V6 is getting close to release (Few months, maybe half a year) and it has a completely different schematics file format based on S-expressions. It seems unwise to invest a lot of time now in automation for KiCad V5.

Look also at

For now it will be the FPGA with the most logic cells and the biggest pitch I can find, so it’s easy to assemble by hand. It also has to have an open toolchain. If there are better options than this FPGA, please let me know.

You are correct that it probably costs way more time the first time around to automate this. Especially because I’m a beginner in python programming. But as I’m a sloppy human, I will make quite a lot of mistakes in the process of manual mapping, and the ease of mind that there’s no error in the mapping is worth the effort in my opinion.

The PCB is the one who has the final say in the ideal connections between the FPGA and the rest of the components, so I need a route from PCB to schematic to the FPGA.

Thanks for the link. I’ll take a look. If I have to write my own plugin, I’ll submit it there.

Agreed. I’m running 5.99 for exactly this reason.

That’s very cool. It looks like that solves one of the problems.

Finally I’ve let Eeschema generate a netlist. As far as I can see it contains all the information needed to generate the file I want. This segment tells me the FPGA is U1:

(comp (ref "U1")
      (value "ICE40UP5K-SG48ITR")
      (footprint "Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.6x5.6mm")
      (datasheet "http://www.latticesemi.com/Products/FPGAandCPLD/iCE40Ultra")
      (libsource (lib "FPGA_Lattice") (part "ICE40UP5K-SG48ITR") (description "iCE40 UltraPlus FPGA, 5280 LUTs, 1.2V, 48-pin QFN"))
      (property (name "Sheet name") (value ""))
      (property (name "Sheet file") (value "playground.kicad_sch"))
      (sheetpath (names "/") (tstamps "/"))
      (tstamp "1b277448-4846-40e8-8836-2f8c2575715e")))

Knowing that, I can search through all the net’s and gather the ones connected to U1, remove special characters (like “/” “(” and “)” ) and dump those in my file:

(net (code "1") (name "/FPGA_LED1")
      (node (ref "R1") (pin "1"))
      (node (ref "U1") (pin "23") (pinfunction "IOT_37a")))

That would generate this line:
set_io FPGA_LED1 23

This is a net without net lable:

(net (code "14") (name "Net-(U1-Pad13)")
      (node (ref "U1") (pin "13") (pinfunction "IOB_24a")))

That would generate this line:
set_io Net-U1-Pad13 13

This looks very doable to me.

Going “backward” from the PCB to the schematic is a bit problematic. “Back annotation” has never been a strong point of KiCad.
It’s probably easier to draw a part of the tracks on the PCB, then decide to what pins the final connections go. Enter those connections in the schematic (or whatever alternative you use), then update the PCB and finalize the connections.

I have no experience with FPGA’s myself, so I can’t help you there.

If you put the labels directly on the pins you can do this by parsing the .sch file. For inspiration how to do this in python, look at the already mentioned swap pins action plugin.

Be aware that the plugins made for v5.1.x may not work with v5.99 due to the change on the eeschema file format. Surely the plugins will be updated once the v6 is released.

Yup, I forgot to mention, my plugin supports only 5.1.x branch. And it will take a while until I start migrating them to V6. And even then I’ll start with higher priority plugins.

In the .kicad_sch file (made by KiCad v5.99) the information about what net is connected to what pin of the FPGA is not so easy to obtain. First you need to figure out there the 5 parts of the FPGA are placed in the schematic from this part of the file:

(symbol (lib_id "FPGA_Lattice:ICE40UP5K-SG48ITR") (at 165.1 34.29 0) (unit 4)
    (in_bom yes) (on_board yes)
    (uuid "e8966ff8-798c-4f44-abf7-133b83162563")
    (property "Reference" "U1" (id 0) (at 171.45 33.02 0)
      (effects (font (size 1.27 1.27)) (justify left))
    )
    (property "Value" "ICE40UP5K-SG48ITR" (id 1) (at 171.45 34.29 0)
      (effects (font (size 1.27 1.27)) (justify left))
    )
    (property "Footprint" "Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.6x5.6mm" (id 2) (at 165.1 68.58 0)
      (effects (font (size 1.27 1.27)) hide)
    )
    (property "Datasheet" "http://www.latticesemi.com/Products/FPGAandCPLD/iCE40Ultra" (id 3) (at 154.94 8.89 0)
      (effects (font (size 1.27 1.27)) hide)
    )
  )

  (symbol (lib_id "FPGA_Lattice:ICE40UP5K-SG48ITR") (at 142.24 64.77 0) (unit 3)
    (in_bom yes) (on_board yes)
    (uuid "e34fcc4f-fe4b-4392-8980-b86d88bc0732")
    (property "Reference" "U1" (id 0) (at 151.13 60.96 0)
      (effects (font (size 1.27 1.27)) (justify left))
    )
    (property "Value" "ICE40UP5K-SG48ITR" (id 1) (at 151.13 63.5 0)
      (effects (font (size 1.27 1.27)) (justify left))
    )
    (property "Footprint" "Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.6x5.6mm" (id 2) (at 142.24 99.06 0)
      (effects (font (size 1.27 1.27)) hide)
    )
    (property "Datasheet" "http://www.latticesemi.com/Products/FPGAandCPLD/iCE40Ultra" (id 3) (at 132.08 39.37 0)
      (effects (font (size 1.27 1.27)) hide)
    )
  )

  (symbol (lib_id "FPGA_Lattice:ICE40UP5K-SG48ITR") (at 68.58 58.42 0) (unit 1)
    (in_bom yes) (on_board yes)
    (uuid "1b277448-4846-40e8-8836-2f8c2575715e")
    (property "Reference" "U1" (id 0) (at 77.47 55.88 0)
      (effects (font (size 1.27 1.27)) (justify left))
    )
    (property "Value" "ICE40UP5K-SG48ITR" (id 1) (at 77.47 58.42 0)
      (effects (font (size 1.27 1.27)) (justify left))
    )
    (property "Footprint" "Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.6x5.6mm" (id 2) (at 68.58 92.71 0)
      (effects (font (size 1.27 1.27)) hide)
    )
    (property "Datasheet" "http://www.latticesemi.com/Products/FPGAandCPLD/iCE40Ultra" (id 3) (at 58.42 33.02 0)
      (effects (font (size 1.27 1.27)) hide)
    )
  )

  (symbol (lib_id "FPGA_Lattice:ICE40UP5K-SG48ITR") (at 109.22 59.69 0) (unit 2)
    (in_bom yes) (on_board yes)
    (uuid "a664fa94-5c1a-4e98-9fb4-8a989926cba8")
    (property "Reference" "U1" (id 0) (at 109.22 87.63 0))
    (property "Value" "ICE40UP5K-SG48ITR" (id 1) (at 109.22 90.17 0))
    (property "Footprint" "Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.6x5.6mm" (id 2) (at 109.22 93.98 0)
      (effects (font (size 1.27 1.27)) hide)
    )
    (property "Datasheet" "http://www.latticesemi.com/Products/FPGAandCPLD/iCE40Ultra" (id 3) (at 99.06 34.29 0)
      (effects (font (size 1.27 1.27)) hide)
    )
  )

That gives you the designator (U1), the value (ICE40UP5K-SG48ITR), and 4 locations.
Next this snipped tells you where all the pins of the FPGA are, relative to the 4 locations obtained above:

(symbol "FPGA_Lattice:ICE40UP5K-SG48ITR" (in_bom yes) (on_board yes)
      (property "Reference" "U" (id 0) (at -8.89 -29.21 0)
        (effects (font (size 1.27 1.27)))
      )
      (property "Value" "ICE40UP5K-SG48ITR" (id 1) (at 0 -31.75 0)
        (effects (font (size 1.27 1.27)))
      )
      (property "Footprint" "Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.6x5.6mm" (id 2) (at 0 -34.29 0)
        (effects (font (size 1.27 1.27)) hide)
      )
      (property "Datasheet" "http://www.latticesemi.com/Products/FPGAandCPLD/iCE40Ultra" (id 3) (at -10.16 25.4 0)
        (effects (font (size 1.27 1.27)) hide)
      )
      (property "ki_locked" "" (id 4) (at 0 0 0)
        (effects (font (size 1.27 1.27)))
      )
      (property "ki_keywords" "FPGA programmable logic" (id 5) (at 0 0 0)
        (effects (font (size 1.27 1.27)) hide)
      )
      (property "ki_description" "iCE40 UltraPlus FPGA, 5280 LUTs, 1.2V, 48-pin QFN" (id 6) (at 0 0 0)
        (effects (font (size 1.27 1.27)) hide)
      )
      (property "ki_fp_filters" "QFN*7x7mm*P0.5mm*EP5.6x5.6mm*" (id 7) (at 0 0 0)
        (effects (font (size 1.27 1.27)) hide)
      )
      (symbol "ICE40UP5K-SG48ITR_1_1"
        (rectangle (start -7.62 25.4) (end 7.62 -27.94)
          (stroke (width 0.254)) (fill (type background))
        )
        (pin bidirectional line (at -10.16 12.7 0) (length 2.54)
          (name "IOT_37a" (effects (font (size 1.27 1.27))))
          (number "23" (effects (font (size 1.27 1.27))))
        )
        (pin bidirectional line (at -10.16 15.24 0) (length 2.54)
          (name "IOT_36b" (effects (font (size 1.27 1.27))))
          (number "25" (effects (font (size 1.27 1.27))))
        )

After figuring out the real position of the pins of the FPGA, you then have to see if there’s a line connected to those pins, and if there’s lines connected to those lines from this snippet:

  (wire (pts (xy 15.24 45.72) (xy 15.24 74.93))
    (stroke (width 0) (type solid) (color 0 0 0 0))
  )
  (wire (pts (xy 26.67 55.88) (xy 26.67 74.93))
    (stroke (width 0) (type solid) (color 0 0 0 0))
  )
  (wire (pts (xy 38.1 66.04) (xy 38.1 74.93))
    (stroke (width 0) (type solid) (color 0 0 0 0))
  )
  (wire (pts (xy 46.99 71.12) (xy 58.42 71.12))
    (stroke (width 0) (type solid) (color 0 0 0 0))
  )
  (wire (pts (xy 46.99 74.93) (xy 46.99 71.12))
    (stroke (width 0) (type solid) (color 0 0 0 0))
  )
  (wire (pts (xy 58.42 45.72) (xy 15.24 45.72))
    (stroke (width 0) (type solid) (color 0 0 0 0))
  )
  (wire (pts (xy 58.42 55.88) (xy 26.67 55.88))
    (stroke (width 0) (type solid) (color 0 0 0 0))
  )
  (wire (pts (xy 58.42 66.04) (xy 38.1 66.04))
    (stroke (width 0) (type solid) (color 0 0 0 0))
  )
  (wire (pts (xy 165.1 44.45) (xy 165.1 49.53))
    (stroke (width 0) (type solid) (color 0 0 0 0))
  )

And finally you need to see if the location of the lables is on the location of the wires from this snipped:

  (label "FPGA_LED1" (at 30.48 45.72 180)
    (effects (font (size 1.27 1.27)) (justify right bottom))
  )
  (label "FPGA_LED2" (at 38.1 55.88 180)
    (effects (font (size 1.27 1.27)) (justify right bottom))
  )
  (label "FPGA_LED3" (at 48.26 66.04 180)
    (effects (font (size 1.27 1.27)) (justify right bottom))
  )
  (label "FPGA_LED4" (at 57.15 71.12 180)
    (effects (font (size 1.27 1.27)) (justify right bottom))
  )

So this approach basically forces you to duplicate all the code that’s already in the KiCad netlist generator. I think it’s much more efficient (in terms of development effort) to ask eeschema to generate the netlist, and then parse that netlist file. This way Eeschema also deals with the differences between schematics drawn in v5 and v6, so the python script does not have to know about that.

This does raise the question how eeschema can be called from python, so it reads the schematic, and generate a netlist file.

This also raise the question if the netlist from eeschema v5 and from eeschema v6 are the same.

Maybe it’s better to make a new netlist generator that can be added to eeschema (eeschema -> file->export->export netlist->add generator). Where can I find examples how to do that?

I wonder if at this point it wouldn’t make more sense to have the scanning done from within PCBNew and have the Python program access the netlist (well, internal board data model) there. Granted, that requires a 2 step process, push schematic to PCB then run plugin. But, since the OP was imagining that FPGA final pin assignment won’t happen until at least a little bit of iteration between the schematic and PCB for ideal pin assignment, the PCB should be updated from the schematic to have routing checked out before the (final) “io.pcf” is generated for the FPGA toolchain.

I agree this would be the most correct way to do it, but I don’t know a way to access the internal board data model inside of pcbnew.

I also can’t find a way to export a netlist from pcbnew. I guess that leaves us with this procedure:

  1. Finish routing the PCB in pcbnew.
  2. pcbnew->tools->update schematic from PCB
  3. pcbnew->tools->switch to schematic editor
  4. eeschema->file->export netlist-> PCBnew -> default format -> export netlist
  5. parse that netlist into the pcf file.
    This procedure requires a minimal programming effort, and can also be done before the PCB is routed. (and it can be repeated every time the PCB is changed)

Yeah, I agree for your use case (generate info for FPGA tool) it seems better to start with the netlist.

But for initial schematics and to ease the layout, swap pins plugin is the only tool to help you with the layout. But since you are on 5.99 you can not use it, so you’ll have to swap pins manually.

As for writing a tool to help you with either of these two tasks, you should be aware that eeschema should get a python API in V6 (gitlab issue). What this API will inclulde is at the moment unknown. Maybe you’ll be able to export a netlist or maybe you won’t. But I would not count on this. You might want to write this as an issue on gitlab and link it to the #2077

And for the rest of your reply, how one should proceed, this is more or less the same as what is done in swap pins plugin:

  1. get the pin numbers and footprint reference in pcbnew
  2. parse the schematics to find the sheet where the symbol with this reference is on (it is slightly more complicate for multi unit symbols with different units on different sheets), and get the symbol name
  3. find the symbol in the -cache.lib
  4. parse the symbol to find relative pin positions with respect to symbol origin
  5. parse the schematics to find absolute pin positions
  6. find labels at the pin positions
  7. swap labels (or in case of only one label and the other pin empty, move label)

The link you posted says:
@ jeffyoung changed milestone to %7.0 1 month ago

[Edit]
Oops, that github topic has been bouncing arond a bit between KiCad V6 and V7 a few times and I did see the “ff-exception” but was not aware of it’s meaning.

Yeah, but then Seth changed it to %6. There was some back and forth when the feature freeze was announced and a couple issues/meges got tagged with ff-exception (feature freeze exception) meaning that they can (I do not dare say will) be solved/merged in V6

IIRC Seth said that changing it to v7 was a mistake in the first place, and I’ve got the impression that it’s planned and decided to be in v6 without question.