1421 words
7 minutes
DelphiScript for Altium Designer
2025-06-04

I agree that Altium Designer is THE most intuitive PCB designing tool among a short list of what I have ever used. But its potential is significantly downgraded by an absolutely TERRIFYING API system, which inexplicably uses the zombie-like DelphiScript as its programming language.

I just can’t figure out why the Altium picked DelphiScript from all those options, and I don’t actually want to learn anything more about it. It’s just an old crap. But you have to deal with it since the mighty Altium seems to be happy with it.

Here records the development of a script that fans out MCUs according to CubeMX. The version for release can be found here:

tsaoo
/
EETools
Waiting for api.github.com...
00K
0K
0K
Waiting...

Or refer to the document on this site.

API Documents#

Altium Design Software API Reference that describes the structure of the API system. It is pretty simple in fact, divided into subsystems of Schematic, PCB ,and Library, etc.

Schematic API System Interface List of all accessible functions. List for other subsystems may be found in the parent directory.

DelphiScript API Examples that are useful for understanding common practice. The examples can be also found in the official Github repo 👇

Altium-Designer-addons
/
scripts-libraries
Waiting for api.github.com...
00K
0K
0K
Waiting...

Coding#

Main Function#

Function Declaration#

procedure FanOutMCUPins;
var
  CurrentSchDoc: ISch_Document;                   // from which the "Run Script" command is executed

  CompIterator: ISch_Iterator;                     // used to iterate components on CurrentSchDoc
  Component: ISch_Component;                      // component iterated by CompIterator
  PinIterator: ISch_Iterator;                     // used to iterate pin within Component
  Pin: ISch_Pin;                                  // pin iterated by PinIterator
  PinDesignator: String;                          // designator of Pin
  PinFullDesignator: String;                      // full designator with part id (i.e. U1A-A4), used to check whether the iterated pin belongs to 
                                                      // the current part of Component
  PinFunction: String;                            // function of Pin, read from PinDictionary

  Wire: ISch_Wire;                                // the fanout wire to be created
  StartX, StartY, EndX, EndY: Integer;            // the absolute location of Wire
  NetLabel: ISch_Netlabel;                        // labeling the function of the pin connected to Wire
  NetLabelJustification: TTestJustification;      // justification of NetLabel

  IsFound: Boolean;                               // flag indicating whether an MCU component is found
  i: Integer;

begin
  ...
  end;

Here the main function FanOutMCUPins has no parameters, if you want any, define it as:

procedure FanOutMCUPins(x, y: Integer)

In DelphiScript you have to declare every variable at the beginning of a function. Global variables are declared at the beginning of the whole script. That’s also what FanOutMCUPins does. Notice that even the counting variables for looping must also be declared in advance, like the i here.

Here, CurrentSchDoc is a ISch_Document object. The focused document at the time when the script is called will be read into it later. ISch_Iterator is an abstract executor that scan through specified objects within a specified parent. Other stuff are self-explanatory.

Searching for Pins#

Then the script searches for pins of the desired component.

// within the '...' of the former code block
  LoadPinoutCSV;        

  if SchServer = nil then
  begin
    ShowMessage('SchServer not found, aborted!');
    Exit;
  end;

  CurrentSchDoc := SchServer.GetCurrentSchDocument;
  if CurrentSchDoc = nil then
  begin
    ShowMessage('Current document is not SchDoc, aborted!');
    Exit;
  end;

  CompIterator := CurrentSchDoc.SchIterator_Create;
  CompIterator.AddFilter_ObjectSet(MkSet(eSchComponent));      // let CompIterator read the component objects only

  IsFound := False;
  Component := CompIterator.FirstSchObject;                    // iterate the components
  while Component <> nil do
  begin
    if (Component.Comment <> nil) and (Copy(Component.Comment.Text, 1, Length(MCUType)) = MCUType) then
    begin
      IsFound := True;

      PinIterator := Component.SchIterator_Create;
      PinIterator.AddFilter_ObjectSet(MkSet(ePin));
      Pin := PinIterator.FirstSchObject;                      // iterate the pins
      while Pin <> nil do
      begin
        ...
      end;
    end;
    Component := CompIterator.NextSchObject;
  end;

  if not IsFound then
  begin
    ShowMessage('Not Found!');
  end;

  PinDictionary.Free;

It first checks if the currently focused document is a schematic. If so, it creates a ISch_Iterator under the schematic document to iterate the children of it, and applies a filter of eSchComponent enumeration type to specify the searching region as components.

At Line 23, the comment parameter of every component on the schematic is compared with the model name of the desired MCU, to locate the corresponding component. When found, at Line 27, another ISch_Iterator is created to scan for every pin of the component.

Adding Fan-Out Wire#

// within the '...' of the former code block
        PinFullDesignator := Pin.FullDesignator;
        if (Component.IsMultiPartComponent) and ( Ord(Copy(PinFullDesignator, Pos('-',PinFullDesignator) - 1, 1)) - 64 <> Component.CurrentPartID) then      // 'A'=64, part A = part 1
        begin
          Pin := PinIterator.NextSchObject;
          continue
        end;
          
        PinDesignator := Pin.Designator;
        PinFunction := PinDictionary.Values[PinDesignator];   // read function of the pin
        if PinFunction = '' then                              // filter out pins with no function
        begin
          Pin := PinIterator.NextSchObject;
          continue
        end;

Here is where the true ugliness of the API system surfaces. I’m speechless.

For a multi-part component (e.g. U1A and U1B), every part will be iterated once by CompIterator, and for every part, EVERY pin of the component will be iterated by PinIterator, no matter the pin belongs to the current part or not. For those pins not belonged to the current part, its Location will be read as the topleft pin of this part, which means overlapping. I.e., suppose both U1A and U1B matches the MCU’s model name, when the iterator goes to U1A, pins of U1B will also be scanned, but with wrong positions.

The solution is to check the full designator of every pin, and see if the part id (e.g. A/B/C…) meets with CurrentPartID of Component. In the above-mentioned example, when the iterators goes to U1A, pins with a parent designator ending with “B” will be skipped.

With the right pins found, the next step is to add fan-out wires and net name labels 👇

//following the former code block
        StartX := Pin.Location.X;                             // location of the root of the pin
        StartY := Pin.Location.Y;
        if Pin.Orientation = eRight then                      // eRight actually means the pin is pointing to the left (CRAZY)
        begin
          StartX := StartX - Pin.PinLength;                   // find the end of the pin
          EndX := StartX - MilsToCoord(FanOutLength);         // calculate the vertex of funout wire
          EndY := StartY;
          NetLabelJustification := eJustify_BottomLeft;
        end
        else if Pin.Orientation = eLeft then
        begin
          StartX := StartX + Pin.PinLength;
          EndX := StartX + MilsToCoord(FanOutLength);
          EndY := StartY;
          NetLabelJustification := eJustify_BottomRight;
        end;

        Wire := SchServer.SchObjectFactory(eWire, eCreate_GlobalCopy);        // create and configure the wire
        Wire.Location := Point(StartX, StartY);
        Wire.InsertVertex := 1;
        Wire.SetState_Vertex(1, Point(StartX, StartY));
        Wire.InsertVertex := 2;
        Wire.SetState_Vertex(2, Point(EndX, EndY));
        CurrentSchDoc.RegisterSchObjectInContainer(Wire);

        NetLabel := SchServer.SchObjectFactory(eNetlabel,eCreate_GlobalCopy);   // create and configure the net label
        NetLabel.Location := Point(EndX, EndY);
        NetLabel.Text := PinFunction;
        NetLabel.SetState_Justification(NetLabelJustification);
        CurrentSchDoc.RegisterSchObjectInContainer(NetLabel);

        Pin := PinIterator.NextSchObject;

This part is straightforward. First it calculate the start and end location of each fan-out wire, according to a user-defined constant FanOutLength and the orientation of each pin. The only thing that needs attention is that Pin.Location.X/Y is given in absolute coordinate format (something like pixel location), the MilsToCoord function should be called to converter the length in mils to coordinate format.

Then it creates a wire by adding the vertexes one-by-one, and registers the wire to the current schematic document.

After that a net label is put on the fan-out wire. The physical net and the wire is then automatically connected in the net list, just like what you manually do in Altium Designer.

Read Net List from CSV#

procedure LoadPinoutCSV;
var
  CSVLine: String;
  CSVFile: TStringList;
  LineComponents: TStringList;
  i: Integer;
begin
  PinDictionary := TStringList.Create;

  CSVFile := TStringList.Create;
  LineComponents := TStringList.Create;

  CSVFile.LoadFromFile(PinOutFilePath);

  for i := 1 to CSVFile.Count - 1 do
  begin
    CSVLine := CSVFile[i];
    LineComponents.StrictDelimiter := True;
    LineComponents.Delimiter := ','; 
    LineComponents.DelimitedText := CSVLine;
    if LineComponents[3] <> '' then
    PinDictionary.Add(LineComponents[0] + '=' + LineComponents[3]);
  end;

  CSVFile.Free;
  LineComponents.Free;
  end;
end;

This part read the CSV pin description file exported from cubeMX into a string dictionary. Once again, DelphiScript (perhaps Pascal too) shows its insanity. The way to add an item to the dictionary is to pass a “key=value” formatted string to Add function, then it will parse the string. I have literally no idea why they made it so complex. What’s wrong with PinDictionary[key] := value ??

(To make things worse, working with := makes me feel like writing verilog.)

Graphics UI#

The Altium API system accepts a .dfm file with the same name as the .pas, to serve as the GUI description script. The basic format is something like:

object form1: TForm1
    object Button_RunClick: TButton
        Left = 10
        Top = 230
        Width = 480
        Height = 100
        Margins.Left = 5
        Margins.Top = 5
        Margins.Right = 5
        Margins.Bottom = 5
        Caption = 'Run'
        Default = True
        ModalResult = 1
        TabOrder = 0
        OnClick = Button_RunClick
    end;
end;

The above code block defines a button. The OnClick parameter indicates the callback function of a clicking event. To use the callback function, add this to the .pas script 👇

procedure TForm1.Button_RunClick(Sender: TObject);
begin
  PinOutFilePath := Box_Path.Text;
  FanOutLength := StrToInt(Box_FanOutLength.Text);
  MCUType := Box_MCUType.Text;
  if CheckBox_DebugMode.State = cbChecked then          // not sure if it is the only way to do an if-else
  begin                                                 // Delphi is fucking nonsense to me
    DebugModeEnabled := True;
  end
  else
  begin
    DebugModeEnabled := False;
  end;

  FanOutMCUPins;
  Close;
end;
DelphiScript for Altium Designer
http://tsaoo.github.io/resrvplot/posts/cad/delphiscript-for-altium-designer/
Author
Zhiyang Cao
Published at
2025-06-04
License
CC BY-NC-SA 4.0