Skip to content
/ hpcb Public

PCB creation using a Haskell DSL (domain specific language), Kicad format output

License

Notifications You must be signed in to change notification settings

iemxblog/hpcb

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HPCB

Create a PCB programmatically (instead of using a GUI) (like OpenSCAD for 3d modelling)

Quick start : an LED, a resistor, and a pin header

Install Haskell and Kicad :

$ sudo apt install haskell-platform kicad

Create a new haskell project :

$ mkdir led
$ cd led
$ cabal sandbox init
$ cabal init

Fill in all the informations needed for the project. Then add Hpcb as a dependency in led.cabal. Example below :

-- Initial led.cabal generated by cabal init.  For further documentation, 
-- see http://haskell.org/cabal/users-guide/

name:                led
version:             0.1.0.0
-- synopsis:            
-- description:         
license:             MIT
license-file:        LICENSE
author:              iemxblog
maintainer:          iemxblog@gmail.com
-- copyright:           
-- category:            
build-type:          Simple
extra-source-files:  ChangeLog.md
cabal-version:       >=1.10

executable led
  main-is:             Main.hs
  -- other-modules:       
  -- other-extensions:    
  build-depends:
    base >=4.9 && <4.10,
    hpcb
  -- hs-source-dirs:      
  default-language:    Haskell2010

Install Hpcb like this (it will be installed in the sandbox) :

cabal install --only-dependencies

Edit Main.hs and copy/paste the following code :

module Main (
  main
) where

import Hpcb
import Data.Monoid

outline :: Circuit
outline =
  rectangle w h
  # translate (V2 (w/2-2.54) (h/2-2.54*1.5))
  # layer EdgeCuts
  where (w, h) = (4*2.54, 3.5*2.54)

ledBoard :: Circuit
ledBoard = (
  pinHeaderFromNets "JP1" ["VCC", "GND"]
  <> led_805 "D1" "RED" # rotate 180 # translate (V2 (2.54*2) (-1.27))
  <> r805 "R1" "330" # rotate 180 # translate (V2 (2.54*2) 2.54)
  <> outline
  )
  # connect (net "VCC") (pinName "D1" "A")
  # connect (pinName "D1" "K") (pin "R1" 1)
  # connect (net "GND") (pin "R1" 2)

main :: IO ()
main = runCircuit ledBoard

Then produce the board file like this :

cabal build
./dist/build/led/led > led.kicad_pcb

Then open it with Kicad like this :

pcbnew led.kicad_pcb

And here is the result :

led

What does it do ?

  • Places components, translates, rotates them (and composes translations and rotations)
  • Makes electrical connections (ratsnest, but not routing)
  • Outputs a Kicad PCB file

What doesn't it do (yet) ?

  • Routing
  • Flipping components (putting them on the back side of a board)

How to

Connect components

4 functions are available to make connections : connect, net, pin, and pinName.

Here are some examples :

Example 1

Create a resistor named "R1", of value "10k" and connect its pin number 1 to net "GND".

resistor = r805 "R1" "10k" # connect (net "GND") (pin "R1" 1)

Example 2

Create an LED named "D1", of value "RED", and connect its cathode named "K" to net "GND"

led = led_805 "D1" "RED" # connect (net "GND") (pinName "D1" "K")

Example 3

Create an ATMega328p named "U1", and connect all its VCC pins to net "VCC", and all its GND pins to net "GND".

mcu =
  atmega328p_au "U1"
  # connect (net "VCC") (pinName "VCC")
  # connect (net "VCC") (pinName "AVCC")
  # connect (net "GND") (pinName "U1" "GND")

Example 4

Create 2 leds, an connect their 2 anodes together.

leds =
  ( led_805 "D1" "RED"
  <> led_805 "D2" "GREEN"
  )
  # connect (pinName "D1" "A") (pinName "D2" "A")

Pitfalls

Out of scope components

leds =
  led_805 "D1" "RED"
  <> led_805 "D2" "GREEN" # connect (pinName "D1" "A") (pinName "D2" "A")

This example will return an error saying that it cannot find component D1. It is normal because operator # has a higher precedence than <>, so "pinName" looks only in the circuit returned by this function :

led_805 "D2" "GREEN"

And there is no component named "D1".

Overwriting Nets

In this circuit, pin 1 of resistor R1 won't be connected to pin "SDA" of the atmega328p_au. The reason is that the SDA net associated with pin ADC4 of component U1 is overwritten on the following line.

mcu =
  atmega328p_au "U1"
  # connect (net "SDA") (pinName "U1" "ADC4")
  # connect (net "ADC4") (pinName "U1" "ADC4")  -- net SDA is overwritten by net ADC4


r =
  r805 "R1" "1k"
  # connect (net "VCC") (pin "R1" 1)
  # connect (net "SDA") (pin "R1" 2)    -- This pin won't be connected to pin ADC4 of component U1, because its net has been overwritten

circuit = mcu <> r

Should overwriting be forbidden in a future version ? Or should we find another solution to this problem, that will cause errors not seen by users ? If you want to give some feedback about this, send me an email at iemxblog@gmail.com.

For contributors

TODO

Documentation Tests New footprints Flipping components Routing

Coordinate system

In the Kicad file format, a footprint has a location and an orientation. So when we apply a transformation to a component, we have to keep track of its orientation.

The coordinate system used in Hpcb is inpired by homogeneous coordinates. Here are the coordinates of some footprint (a column vector) :

(x)
(y)
(o)
(1)

"x" and "y" are the coordinates of the footprint. "o" is its orientation. Here is how we make a translation :

(x')      (1  0   0   tx)  (x)
(y')  =   (0  1   0   ty)  (y)
(o')      (0  0   1   0 )  (o)
(1 )      (0  0   0   1 )  (1)

Here is how we make a rotation :

(x')      (cos a  sin a   0   0)  (x)
(y')  =   (-sin a cos a   0   0)  (y)
(o')      (0      0       1   a)  (o)
(1 )      (0      0       0   1)  (1)

How to make a new footprint

Let's take an example. This is Kicad file format's representation of a resitor (805 package) :

  (module Resistors_SMD:R_0805 (layer F.Cu) (tedit 58AADA8F) (tstamp 59B14E88)
    (at -6.35 2.54)
    (descr "Resistor SMD 0805, reflow soldering, Vishay (see dcrcw.pdf)")
    (tags "resistor 0805")
    (attr smd)
    (fp_text reference REF** (at 0 -1.65) (layer F.SilkS)
      (effects (font (size 1 1) (thickness 0.15)))
    )
    (fp_text value R_0805 (at 0 1.75) (layer F.Fab)
      (effects (font (size 1 1) (thickness 0.15)))
    )
    (fp_text user %R (at 0 -1.65) (layer F.Fab)
      (effects (font (size 1 1) (thickness 0.15)))
    )
    (fp_line (start -1 0.62) (end -1 -0.62) (layer F.Fab) (width 0.1))
    (fp_line (start 1 0.62) (end -1 0.62) (layer F.Fab) (width 0.1))
    (fp_line (start 1 -0.62) (end 1 0.62) (layer F.Fab) (width 0.1))
    (fp_line (start -1 -0.62) (end 1 -0.62) (layer F.Fab) (width 0.1))
    (fp_line (start 0.6 0.88) (end -0.6 0.88) (layer F.SilkS) (width 0.12))
    (fp_line (start -0.6 -0.88) (end 0.6 -0.88) (layer F.SilkS) (width 0.12))
    (fp_line (start -1.55 -0.9) (end 1.55 -0.9) (layer F.CrtYd) (width 0.05))
    (fp_line (start -1.55 -0.9) (end -1.55 0.9) (layer F.CrtYd) (width 0.05))
    (fp_line (start 1.55 0.9) (end 1.55 -0.9) (layer F.CrtYd) (width 0.05))
    (fp_line (start 1.55 0.9) (end -1.55 0.9) (layer F.CrtYd) (width 0.05))
    (pad 1 smd rect (at -0.95 0) (size 0.7 1.3) (layers F.Cu F.Paste F.Mask))
    (pad 2 smd rect (at 0.95 0) (size 0.7 1.3) (layers F.Cu F.Paste F.Mask))
    (model Resistors_SMD.3dshapes/R_0805.wrl
      (at (xyz 0 0 0))
      (scale (xyz 1 1 1))
      (rotate (xyz 0 0 0))
    )
  )

We just have to translate it using Hpcb functions, like fpText, fpRectangle, fpLine, pad, etc. And here is the result :

-- | SMD Resistor, 805 package (2012 metric)
r805 :: String      -- ^ Reference
        -> String   -- ^ Value
        -> Circuit
r805 ref val = footprint ref "R_805" $
  fpText "reference" ref defaultEffects # translate (V2 0 (-1.65)) # layer FSilkS
  <> fpText "value" val defaultEffects # translate (V2 0 1.65) # layer FFab
  <> fpRectangle 2.0 1.25 # layer FFab # width 0.1
  <> fpRectangle 3.2 2.0 # layer FCrtYd # width 0.05
  <> (
    fpLine (V2 0.6 0.875) (V2 (-0.6) 0.875)
    <> fpLine (V2 (-0.6) (-0.875)) (V2 0.6 (-0.875))
  ) # layer FSilkS # width 0.15
  <> (
    pad 1 SMD Rect (V2 0.7 1.3) (newNet ref 1) # translate (V2 (-0.95) 0)
    <> pad 2 SMD Rect (V2 0.7 1.3) (newNet ref 2) # translate (V2 0.95 0)
  ) # layers [FCu, FPaste, FMask]

Instantiate a new component from an existing footprint

Let's take an example : we will create a new component named lm358n from the existing footprint soic_8. We give the value "LM358N" to the component, and we just use the function "names" to assign names to pins, and that's all. A pin can have multiple names, as you can see for pin 4, which has 2 names.

lm358n :: String
          -> Circuit
lm358n ref = soic_8 ref "LM358N" # names ref [
    (1, ["OUTA"]),
    (2, ["-INA"]),
    (3, ["+INA"]),
    (4, ["GND", "V-"]),
    (5, ["+INB"]),
    (6, ["-INB"]),
    (7, ["OUTB"]),
    (8, ["V+"])
  ]

And then we will be able to use "connect" and "pinName" functions to connect those pins.

Links

Interesting projects made by other people

Skidl

SKiDL is a module that extends Python with the ability to design electronic circuits.

PCBmodE

PCBmodE is a printed circuit board design Python script that creates an SVG from JSON input files, and then creates Gerber and Excellon files for manufacturing.

Kicad file format description

Kicad file format

About

PCB creation using a Haskell DSL (domain specific language), Kicad format output

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published