Prox / RFID
Ladder Logic
[interfacing] †
Tube Joints
Key Code From Photo
SolveSpace (3d CAD)
SketchFlat (2d CAD)
Resume / Consulting
Contact Me

LDmicro: Ladder Logic for PIC and AVR

(also in: Italiano, Deutsch, Português, Русский)

Quick summary: I wrote a compiler that starts with a ladder diagram and generates native PIC16 or AVR code. Features include:

  • digital inputs and outputs
  • timers (TON, TOF, RTO)
  • counters (CTU, CTD, `circular counters' for use like a sequencer)
  • analog inputs, analog (PWM) outputs
  • integer variables and arithmetic instructions
  • easy-to-use serial communications, to a PC, LCD, or other device
  • shift registers, look-up tables
  • EEPROM variables, whose values are not forgotten when you lose power
  • simulator, to test your program before you generate PIC/AVR code

This program is free software; source code and executables are available for download.


PLCs are often programmed in ladder logic. This is because PLCs originally replaced relay control systems, and forty years later, we still haven't quite let go. A PLC, like any microprocessor, executes a list of instructions in sequence. Ladder logic tools abstract this; you can program the PLC by wiring up relay contacts and coils on-screen, and the PLC runtime will simulate the circuit that you've drawn. Some of the relay contacts can be tied to input signals from the real world; some of the coils can be tied to outputs. That way you can make your simulated circuit interact with other devices, and actually control things. That is the point.

Actually it's more general than that, because you can incorporate timers and counters and arithmetic operations that you couldn't (easily) perform with just relays. The circuit concept is still useful though, partly just because it's intuitive, but also because it abstracts the concurrency issues. It looks like this:

         ||       Xa               Xb              Yout       ||
       1 ||-------] [------+-------] [------+-------( )-------||
         ||                |                |                 ||
         ||                |       Xc       |                 ||
         ||                +-------]/[------+                 ||

This is a simple piece of combinational logic. There are three input terms, Xa, Xb, and Xc. There is one output term, Yout. The expression is Yout := Xa and (Xb or (not Xc)). This makes sense if you think of Xa and Xb as normally open relay contacts, Xc as normally closed relay contacts, and Yout as a relay coil. Of course it gets more complicated than that:

         ||                                                   ||
         ||                                      Asetpoint    ||
       1 ||-------------------------------------{READ ADC}----||
         ||                                                   ||
         ||                                    Atemperature   ||
         ||-------------------------------------{READ ADC}----||
         ||                                                   ||
         ||                                                   ||
         ||                                                   ||
         ||                                                   ||
         ||                        {SUB  min_temp  :=}        ||
       2 ||------------------------{ Asetpoint - 20  }--------||
         ||                                                   ||
         ||                        {ADD  max_temp  :=}        ||
         ||------------------------{ Asetpoint + 20  }--------||
         ||                                                   ||
         ||                                                   ||
         ||                                                   ||
         ||                                                   ||
         ||[Atemperature >]                       Yheater     ||
       3 ||[ max_temp     ]+------------------------(R)-------||
         ||                |                                  ||
         ||     Xenable    |                                  ||
         ||-------]/[------+                                  ||
         ||                                                   ||
         ||[Atemperature <]      Xenable          Yheater     ||
         ||[ min_temp     ]--------] [--------------(S)-------||
         ||                                                   ||
         ||                                                   ||
         ||                                                   ||
         ||                                                   ||
         ||                       {SUB  check_temp  :=}       ||
       4 ||-----------------------{ Asetpoint - 30    }-------||
         ||                                                   ||
         ||                                                   ||
         ||                                                   ||
         ||                                                   ||
         ||[Atemperature >]                       Yis_hot     ||
       5 ||[ check_temp   ]-------------------------( )-------||
         ||                                                   ||
         ||                                                   ||
         ||                                                   ||
         ||                                                   ||
         ||                                                   ||

This is for a simple thermostat. There are two analog inputs; one of them is for the setpoint, so that it might, for example, be connected to a pot that the user turns to select the desired temperature. The other provides the temperature measurement; it might be a semiconductor temperature sensor, or a platinum RTD with suitable interfacing circuitry. There is a digital output, Yheater. That might control a heating element, through a suitable switch (a TRIAC, or a relay, or a solid-state relay, or whatever).

We close the loop with a simple hysteretic (bang-bang) controller. We have selected plus or minus 20 ADC units of hysteresis. That means that when the temperature falls below (setpoint - 20), we turn on the heater, and when it climbs above (setpoint + 20), we turn the heater off.

I chose to add a few small frills. First, there is an enable input: the heater is forced off when Xenable is low. I also added an indicator light, Yis_hot, to indicate that the temperature is within regulation. This compares against a threshold slightly colder than (setpoint - 20), so that the light does not flicker with the normal cycling of the thermostat.

This is a trivial example, but it should be clear that the language is quite expressive. Ladder logic is not a general-purpose programming language, but it is Turing-complete, accepted in industry, and, for a limited class of (mostly control-oriented) problems, surprisingly convenient.

A Ladder Logic Compiler for PIC16 and AVR

Modern sub-3.00 USD microcontrollers probably have about the computing power of a PLC circa 1975. They therefore provide more than enough MIPS to run reasonably complex ladder logic with a cycle time of a few milliseconds. I think PLCs usually have some sort of runtime that's kind of like an interpreter or a virtual machine, but if we're doing simple logic on a processor without much memory then a compiler might be a better idea.

So I wrote a compiler. You start with an empty rung. You can add contacts (inputs) and coils (outputs) and more complicated structures to build up your program. Timers (TON, TOF, RTO) are supported. The max/min durations depend on the cycle time of the `PLC,' which is configurable; timers can count from milliseconds to tens of minutes. There are counters and arithmetic operations (plus, minus, times, div).

Circuit elements may be added in series or in parallel with existing elements. An I/O list is built from the ladder logic drawn. You can have internal relays (Rfoo), for which memory is automatically allocated, or inputs (Xfoo) and outputs (Yfoo), to which you must assign a pin on the microcontroller. The selection of pins available depends on the microcontroller. I have tried to support the most popular PICs and AVRs (see below).

You can edit the program in graphical form:

Then you can test the program by simulating it in real time. The program appears on screen with the energized (true) branches highlighted, which makes it easy to debug. The state of all variables is shown at the bottom of the screen in the I/O list.

Once the program works in simulation you can assign pins to the inputs and outputs and generate PIC or AVR code. The code generator isn't all that difficult. If you realize that a parallel circuit is an OR and a series circuit is an AND, it's a second-year CS assignment, and not a very long one. The editor is actually much more challenging. It would take some work to make a smart compiler, though. For the AVR a good register allocator would provide a major speedup. If you wanted to get very fancy then you could apply standard logic reduction algorithms, and maybe state reduction too. That would be much more difficult. The timers screw things up anyways.

Even ignoring all that, my code generator for the AVR is very poor. The AVR back end still generates PIC code...for example, it does not really take advantage of the fact that the AVR has more than one register. Some of the code that it generates is just embarrassingly bad. The PIC back end is better, but not really great. None of this matters much until you're trying to run dozens of rungs of logic with fast cycle times.

I support the A/D converter, PWM unit, and UART on those microcontrollers that provide them. That means that you can write ladder logic that reads analog inputs, and that sends and receives characters over serial (for example to a PC, if you add a suitable level-shifter like a MAX232, or to a character LCD). It is possible to send arbitrary strings over serial, as well as the values of integer variables, as ASCII text. Finally, I now support `preserved variables' on devices with EEPROM; you can indicate that a particular variable should be auto-saved to nonvolatile memory whenever it changes, so that its value is persistent across power-on resets.

Limitations, and Disclaimer

Of course a microcontroller with this software can't do everything that a PLC will. Most PLC environments offer more features and predefined blocks than my tool does. The PLC hardware is better too; usually the inputs and outputs are designed to withstand incredible electrical abuse. You can get a PIC16F877 on a carrier board for ten or twenty USD, though, and you'll pay a fair bit more for a PLC with the same capabilities.

So far I have received very few bug reports compared to the number of people with questions or feature requests. There is still great possibility for defects, especially in the targets for microcontrollers that I do not physically have (and therefore cannot test). Certainly do not use LDmicro for anything safety-critical, or anything that would break something expensive if it failed.

As noted above, the code that it generates is far from optimal. Also, not all of the data RAM in PIC16 devices is available to the ladder logic program. This is because I have not implemented very much support for all the paging nonsense. I do, however, support the program memory paging, which is necessary to access program memory locations in the PIC16 beyond 2k.


This software is tested under all versions from Windows 2000 to Windows 10. Unconfirmed reports suggest that it works under WINE. The download is a .exe file; there are no other files required, so there is no installation program. Save it somewhere on your computer and just run it, and it will work. The manual is included in the .exe file, but you can download it separately if you want.

The compiler generates Intel IHEX files. Most of the programming software that I have seen expects this. Of course you need some sort of programming gadget to get the hex file into the chip. For the AVRs, I recommend an AVRISP mkII, which is available from various distributors. For the PICs, I recommend Microchip's PICkit 2 or 3, which is available from their web store. Both of these are officially supported, connect over USB, and cost less than 40 USD.

It should generally be possible to use code generated by LDmicro with a bootloader. Most AVR parts have special fuses (BOOTRST, BOOTSZx) that will need to be configured for whatever bootloader you are using. The PIC16 parts don't have any specific hardware support for a bootloader, but LDmicro generates code with the correct format to allow the bootloader to rewrite the reset vector.

I would appreciate any bug reports. The following chips are supported:

  • PIC16F628(A)
  • PIC16F88
  • PIC16F819
  • PIC16F877(A)
  • PIC16F876(A)
  • PIC16F887
  • PIC16F886
  • ATmega128
  • ATmega64
  • ATmega162
  • ATmega32
  • ATmega16
  • ATmega8

It is also possible to generate C code from the ladder program. That is less convenient, but you could use it on any processor for which you have a C compiler.

LDmicro can generate interpretable byte code. If you are willing to write an interpreter then you can use this to run your ladder code on any kind of target. There is not very much documentation on this, but I provide a sample interpreter in mostly-portable C.

You can download a build of my last stable release:

LDmicro is now maintained by Ihor Nehrutsa, who has added support for many new processors and other features. You can get more recent builds from his repo:

The source code, and various other files, are also available for download. This program may be distributed and modified under the terms of the GPL Version 3.

Older releases are also available:

(right-click to save any of these files)

Please report any defects. This is free software, with no department in charge of quality control. I do not even have the hardware to test many of the targets myself. A bug that is not reported is unlikely to ever be fixed.

I have a tutorial, in which I describe how to enter a simple ladder diagram, simulate it, and then generate an IHEX file and program it into a PIC. That is probably the easiest way to get started with this software.

If you have questions about LDmicro, then ask them on the forum.

January 2016, Portland, OR; thanks to G. Mariani and A. Pedrazzi for Italian translations; H. U. Noell for German; D. Corteletti and O. da Rocha for Portuguese; M. Vaufleury for French; J. Pascual for Spanish; A. Akici for Turkish.