Introduction: Model Train Controller With RP2040 Pico and Micropython

During a conversation with a friend on how to replace his (very) old speed and direction controller of his N-scale model trains, this project was born. We had a short chat, did some internet research and he came up with following list of requirements:

  • while turning a knob, control speed and direction and have a stop in the middle position
  • use big knobs so kids could turn it easily
  • have some indication lights for power, direction, stop position and overload
  • for safety switch off power on the tracks while in stop and during overload condition. Overload could be e.g. a short circuit on the rails. (The rails do supply the power to the train engines)
  • control of two independent rails

Supplies

If you know my other instructables, I like the Raspberry Pi Pico boards, because the Pico board with the RP2040 MCU has lots of memory, GPIO, ADC, PIO, SPI, I2C and processing power. So this instructable will use a Raspberry Pi Pico board as well. Basically you can use any micro-controller board, which has at least 12 GPIO, 2 ADC inputs, SPI and can run micropython. If you do not use a RP2040 based board, check esp. the ADC's input voltage range and accuracy. Adjust hardware and program accordingly, in case it is different than the RP2040's ADCs.

To power N-scale engines a maximum of 14V is needed. I re-use an old 15V laptop power supply. A 18V laptop power supply would work as well. Current requirement for an N-scale engine is approx. 50mA, so an old laptop power supply could support multiple tracks (check your engine's specification).

The bill of material is attached.

Step 1: Train Controller Design

With above requirements the technical design was sketched quickly. I need a user interface with two big knobs, few LEDs to show status, two digitally controlled power supplies and a track controller to switch on/off power, change direction and detect an overload condition - all controlled by a Raspberry Pi RP2040 Pico and micropython.

To get an idea of the power requirements I did some measurements with an adjustable power supply. The N-scale engines' operation voltage is 5V to 14V (very slow to fast), consuming 50mA of current, while break free voltage from stop position is ~7V.

Few remarks:

For quite a while I was interested in digital potentiometers, so they became part of the implementation.

I am using a modular design, so the individual building blocks can be leveraged for other projects.

This instructable assumes you have some basic knowledge on how to use a RP2040 Pico board, esp. how to load micro-python and python modules to the board. In case you need help installing and starting micropython or programs on the Pico I have added a step "Development Environment" at the end of the instructable with some recommendations and links.

Step 2: Driver for Digital Potentiometer

For this project I selected the microchip MCP41xxx/42xxx digital potentiometer family. This family is controlled by a SPI interface and is available with 10k, 50k and 100k resistance, with single or dual potentiometers in a package. The datasheet is attached for your convenience. For the train controller I use the MCP42010 dual 10k potentiometer in a 14 pin DIP package to control the output voltage of the two adjustable power supplies. SOIC packages are available, too, if you prefer surface mount devices.

I could not find a micropython driver for this device, so I implemented a device driver (mcp42xxx.py) and wrote a short test program (MCP42010_test.py), both attached to this step.

To execute the test, load micropython v1.20 to the Pico and download both files from this step.

Test setup:

HW SPI interface 0 is used with default pins (SPI TX - GP19, SPI SCK - GP18, SCI CS- GP17). Though MCP41/42xxx's maximum SPI clock frequency is 10MHz, we use 400kHz for testing on a bread board.


Bread board HW setup: Pin numbers in brackets [] are Pin numbers of the MCP42010.

Connect the VDD pin [14] and both A pins [7, 8] of the MCP42010 to 3.3V of the Pico board (Pin 36, 3V3_out). Connect the VSS pin [4] and both B Pins [5, 10] to ground. Connect RS [11] and SHDN [12] pins together and connect them to 3.3V using a 1k resistor.

Connect the MCP42010 SPI interface to the Pico: SCK pin [2] to GP18, SI [3] to GP19 and CS [1] to GP17.

Add a 100nF ceramic capacitor across the power supply pins of the MCP42010.

Connect a voltmeter between a wiper pin [6 or 9] and ground.


Test execution:

Use a Terminal or your favorite editor (e.g. Thonny) to connect to the Pico board. Start the test program MCP42xxx_test.py.

E.g. at the REPL type: >>> import MCP42xxx_test

Upon start of the program MCP42xxx_test.py the user is asked to provide input which of the two potentiometers should be tested. You can test one potentiometer at a time or you could test both at the same time.

Next a loop will be started where the user enters a number between 0 and 255 to set the wiper value. Once you pressed ENTER, you can measure the resulting voltage at the wiper pin [6 or 9 or both] . You should measure voltages between 0V and 3.3V.

Ctrl-C ends the program and both potentiometers are reset (disabled) before the program ends.


Remarks:

  • MCP42xxx_test requires module mcp42xxx.py in the same directory as the test program.
  • Upon power up the MCP41/42xxx ICs initialize with a wiper value of 128, which results in ~1.65V at the wiper pins (1/2 of 3.3V).
  • Please have a look to the picture attached to this step in case you need a visual for connecting the devices.
  • In case you need to debug, I added a picture of the SPI signal. The top signal (yellow) is SCK (clock), the bottom signal (blue) shows the corresponding SI signal (data). There are always two bytes transferred. The first byte is the command byte and the second byte contains the data. Refer to the datasheet for more details.

Step 3: Digitally Adjustable Power Supply

For the power supplies I use the LM317 adjustable voltage regulator. This IC is available from many sources in THC or SMD packages and features complete set of protections (current limiting, thermal shutdown and SOA).

The MCP42010 is used to adjust the output voltage. As we operate the digital potentiometer with the 3.3V supply of the RP2040 Pico, we need to amplify the wiper voltage with an Op-Amp. Please refer to the schematic picture.

I use the LM358 dual Op-Amp. Resistors R2, R3 and R4, R5 are used to divide the output voltage of the LM317s to the 0V to 3.3V range. The output voltage is feeding the inverting input of the Op-Amp, the voltage of the digital potentiometer is feeding the non-inverting input.

With this setup the output voltage can be adjusted between 2.4V and 13.8V using a 15V power supply. (You could use an 18V laptop power supply to reach the full 14V in case needed.)

Although the train consumes 50mA only, I decided to use the LM317 in a TO220 package, which has two advantages. (1) I do not need a heat sink and (2) if later-on more power is needed, a heat sink can be easily fitted to the TO220 package.

Add the parts for one power supply (U4,U5,R2,R3,C6,C8) to another bread board and connect it with bread board of the MC42010 test setup (see picture and schematics). The MCP42xxx_test program can now be used to test the digitally adjustable power supply.

Remarks:

  • It's a good idea to connect Pin 6 of U4 to ground, if you do not use the second power supply (U6,R4,R5,C9), while testing.
  • The lowest output voltage is 2.4V, this is due to the fact that the Op-Amp's output does not swing fully to ground (0V) and the lowest output voltage of the LM317 is 1.2V, even if the adjust Terminal is at 0V.
  • You can use other Op-Amps in this circuit, but you need to use Op-Amps which can deal with input voltages down to Gnd (rail to rail input Op-Amp).

Step 4: Track Controller Hardware

The track controller takes care of switching track power on/off (enable) and reverse the polarity (direction). I decided to use AZ820 signal relays for this task as the currents are low and these relays are readily available.

Two relays are used, one to switch track power on/off and the other to reverse power, which reverses direction of the train engine.

Each relay is switched on/off by a transistor controlled by a GPIO pin of the RP2040. So direction and power on/off is controlled by software.

To ensure safety a power overload shutdown is implemented in hardware. A resistor (see R7 in attached picture) is used to sense the current supplied to the rails. Once an output overload is detected the J-Fet Q2 is activated and pulls the Base of transistor Q3 to ground, which disables current flow through relay K2. K2 will move to its idle position, which turns off track power.

The circuit to detect an overload condition is described in the following step.

Remark:

The output overload signal is forwarded to the RP2040, so we can decide in SW how to handle an overload condition.

Step 5: Track Overload Detection

Although we manage almost all functionality with software, a short circuit of the power lines needs to be detected and acted upon, even if the software is not running. I am using a circuit for overload detection, which was published by Felix Geering already in 2002. I modified the original schematic to add an overload reset circuit (R8,Q4), which can be controlled by the RP2040 and added a filter circuit, as I expect some digital noise from the RP2040 and I want to avoid this noise to activate the overload circuit.

Brief explanation of the functionality (please refer to the attached schematics).

D1 creates a reference voltage of approx. 0.6V, connected to Op-Amp U7B's inverting input. The reference voltage is compared to the voltage of the non-inverting input, which is the voltage across the output current sense resistor R7 (see Step4). The voltage across R7 is proportional to the track current. The value of R7 is set to 2 Ohm. If the current exceeds 300mA (six times the normal operating current), the voltage across R7 exceeds the reference voltage of 0.6V (overload condition). The output voltage of U7B swings positive and the output of U7A swings positive as well (=output overload detected signal).

U7A has positive feedback from its output to the non-inverting input (feedback resistor R12 and R13), which keeps the output positive, even if the cause for the overload condition is removed, so power stays off. The overload signal is forwarded to the RP2040 and the overload can be managed by the software.

An overload reset signal from RP2040's GPIO turns on Q4 and resets the output of U7A. This removes the overload condition, so power can be switched on again.

R13 and C11 create a low pass filter with a time constant of 10us to suppress digital noise. The filter is added to avoid that spikes from digital signals e.g. noise on the power lines to the Op-Amp would cause the overload detection to switch on or off the overload detected signal inadvertently.

Step 6: Schematics and Board Layout

See attached pictures for Pin assignment of RP2040 Pico board mapped to function and connection to peripherals. The schematics is attached as pdf file.

The schematics is split into two sheets. Sheet one contains the schematics for the Pico, the two potentiometers, the user turns to set speed and direction, and the adjustable power supplies.

Sheet two contains the track control hardware and overload detection. The Raspberry Pi Pico's RP2040 CPU has enough processing power to manage multiple tracks. The attached schematics has two identical track control blocks to control two independent rails.

Although I typically do prototypes by PCB milling, eventually for this project I decided to order a PCB. The picture attached shows the layout. To make reproduction easy I opted to use through hole components. If you use SMDs, the PCB can be much smaller.


Step 7: User Interface and Functionality

User Interface:

Potentiometers are used to set speed and direction, with a STOP position in the middle. Speed is increased if you turn the knob from the STOP position to the end points. (Remark: "Knob" is used synonymous for the wiper position of the potentiometer).

From STOP turning the knob clockwise results in increased speed and forward direction, turning the knob counterclockwise from STOP results in increased speed and reverse direction.

Description of the functionality:

On Power-on the controller program intitializes the hardware and waits until both knobs are in the "STOP" position. The "Stop-LED" is blinking until the middle position is reached. If both potentiometers are at the STOP position the main control loop starts.

As long as the knob stays in the STOP position the Stop-LED is turned on and rail power is off.

If the knob is turned, the controller reads the position of the knob and calculates direction and power setting. With the result the direction is set, the adjustable power supply is set to the calculated voltage and power for the rail is turned on.

If an overload is detected, e.g. by shorting the power of the rails, the rail power is switched off. The "Overload-LED" is turned on.

The overload is detected by the controller. The controller requires the knob to be turned to the STOP posistion. This is indicated by blinking the Stop-LED.

Once the knob is in STOP position, the controller resets the overload condition (overload-LED turns off) and the user can turn the knob to turn on track power. If the overload cause is not removed, power is switch off again.

LED pattern [color]:

Stop-LED ON [yellow]: knob in stop position

Stop-LED BLINKING [yellow]: move the knob to stop position

Forward-LED[green] and Reverse-LED [green]: show the direction, LED brightness is proportional to rails voltage

Overload LED-ON [red]: overload detected

Step 8: Train Control Software

Attached picture shows the high level control structure. If multiple track controllers are used, they are processed in a serial manner. The second picture shows the control for two independent tracks.

The software is implemented using modular design. I use three blocks: (1) the configuration, (2) the drivers and the (3) main program.

(1) The file cfg.py contains all configurations and definitions. The definitions related to the hardware are e.g. the GPIO Pins, the SPI interface and the ADCs. If you want to change e.g. GPIO pins, do the chages in cfg.py. Additionally the user interface configurations are defined here, these are e.g. the dead band values left and right of the mid point of the potentiometers for the stop position. As well the status of the track controller is maintained in cfg.py. Additionally all constants needed for the calculations are defined in the configuration file. This speeds up program execution, as the calculations are done only once, when the python interpreter starts, though for this application speed is not critical at all. Finally you set the debug level in cfg.py (see Step "Testing").

(2) I use two drivers, one for the digital potentiometer (mcp42xxx.py) and one for the track control (track_controller.py). Both drivers are implemented using classes to support re-use and easy extension to multiple tracks.

(3) The main program is t_control.py. It contains all program logic, reading the user input (potentiometers), setting speed and direction and handles overload and exception conditions.

During testing I added a fourth "debug block", a module to output controller status to a terminal program via the USB connection.

Overview of files attached and their function:

  • cfg.py module with all definition related to hardware, UI and debug level
  • mcp42xxx.py driver for the digital potentiometer family MCP41xxx/MCP42xxx
  • track_controller.py driver for the track control hardware
  • print_status.py module to output status over USB to terminal, if in debug mode 1
  • t_control.py main program
  • main.py program to autostart t_control at power on

Step 9: Testing Hardware and Software

One Remark upfront. To ease PCB layout I have changed the SPI CS Pin from GPIO17 to GPIO20. If you test with MCP42010_test.py change the CS_pin to 20 (row 28 in MCP42010_test.py)


Test with Bread boards:

If you followed the instructable Step by Step you should have built two bread boards up to now (see Step 3). One with the RP2040 Pico board and the MCP42010 digital potentiometer and another one with the digital controlled power Supply. Add a relay and a few components (from schematics: K2, D4, Q2, Q3, R10) to the power supply bread board and build a third bread board with the overload detection circuit (see schematics). Now we can test most of the functions for one track.

Connect all bread boards together using the schematics and the picture as reference.


Test with the final hardware:

If you made a PCB and have soldered all parts, you can start the test with the PCB. I am using a 12V PC fan for testing, as I do not have a N-scale train engine. The fan consumes ~130mA, which is a bit more than a N-scale engine, but is fine as it does not trigger the overload circuit.


Debugging:

For testing it is always good to switch debug on. This is done by setting the variable debug in the file cfg.py. Take your favorite editor and set the debug level. I implemented three debug levels:

  • debug = 0 : no debug output
  • debug = 1 : track controller status is printed to a terminal over USB
  • debug = 2 : all (detailed) debug output is sent via USB to a terminal

I would recommend to start with debug level 1. This provides you with the status output of the two track controllers. You get direction, power on/off, speed (0...255), and overload condition printed to your terminal.

This way you can see what the controller does if you turn the knob. With a voltmeter, you can measure the output voltage.

To test the overload functionality connect two resistors in series to the output. E.g. one 68 Ohm and one 33 Ohm resistor . With maximum voltage (14V) you have ~140mA current. If you now short the the 68 Ohm resistors, current increases to ~400mA and you can test the overload functionality. Caution: the resistors should be capable of dissipating 2 Watt and they might get hot if connected for a longer period of time! Same for the LM317s, if they do not have a heat sink attached. So keep the overload test short.

As the control loop runs fast, you get a lot of debug output, if debug = 2, and the screen gets cluttered quickly. I have implemented a 500ms delay to the control loop, to slow it down. The delay is automatically activated if debug=2. If you like to change the delay, or even add a stop after each loop, e.g. by adding an `input("press enter to continue")` statement, go to the end of the control loop in t_control.py to do your changes.


Loading and starting the software:

If not already done, you need to load micropython version 1.2 or later to your Pico board. Then download the files attached to Step 8 "Train Control Software", except main.py. You download main.py later, once you are done with testing and everything works fine.

Connect with a terminal to the Pico and start the train controller by importing the module t_control.

>>> import t_control

Refer to Step 7 "User Interface" and test the power supplies, direction, overload condition, LEDs working....

If everything works as expected, you can automate startup of the train controller by downloading the file main.py, which contains basically only one line. This line imports t_control.

If you need help for working with micropython, I have added some recommendations and links in the last Step of this instructable. If you want to do changes, I recommend to use mpremote as decribed in the last step.


Few Insights to the configuration file cfg.py: (variable names in italics)

The configuration file allows you to tailor the train controller to your needs. You find comments in cfg.py explaining the function of each section. Few tips here to get you started:

  • All Pin, ADC, SPI assignments are done in cfg.py, if you use e.g. different Pins or a different MCU, change the definitions in the respective sections of cfg.py.
  • debug: set variable debug to set debug level, already described earlier in this Step
  • dead band value: the stop position is in the mid position of the potentiometers (pot). The variable dead_band_value is a percentage of the maximum value of the pot, actually ADCmax, the maximum ADC reading of the pots wiper voltage. This value is applied to the left and right of the the mid position of the pot (ADCmax/2). If this stop position is hard to hit, increase the deab_band_value, if the STOP position is to wide, decrease the value. Initially it is set to 0.1, which is 10% of the ADCmax.
  • break free voltage: typically there is some inertia to a train engine, which creates a need to apply a higher voltage to break free from stop to run (compared to the lowest voltage the engine would still move). If you observe that you need to turn the knob quite far until the train starts increase break_free or break_free_time or both. If the train jumpstarts instead of smoothly moving to a low speed, reduce break_free or break_free_time. For one N-scale engine I did measurements and created a graph (picture attached) to find the right values for the train engine's operation voltages. you need to know minum and maximum voltages for movement and the break free voltage when the engine starts to move from a stop.

Step 10: Final Assembly

Building the housing

I did design a housing and a front panel to fit the PCB, the LEDs and the potentiometers. My friend's daughter got to choose the colors. She chose red color for the housing and blue for the front panel. I like her choices ;-).

I 3D-printed the housing. On the back of the housing I drilled holes for 4mm jacks and a fuse holder. The laptop power supply can source 5A of current and I did not want to run any risk of shorting the power, so I added a 1A fuse.

The front panel is milled from blue acrylic.

Final assembly:

Mount the PCB with screws to the bottom of the housing. Add wires from the PCB to the jacks, the fuse, the power supply, the potentiometers and the LEDs. Use the schematics as reference.

I did mount the LEDs to the front panel using hot glue. The LEDs should just sit a bit higher than the front panel's surface. For the potentiometers I used the screws supplied with the pots and added big knobs.

Finally screw the front panel to the housing.

Remark: Both track controllers have a connon ground. So if you want to run two tracks with opposite polarity, make sure you isolate the rails accordingly, otherwise you will get a short circuit.

Step 11: Next Steps and Improvement Ideas

As always, I appreciate your feedback, questions, improvement ideas.

During implementation and testing some ideas / improvements have been identified, which could be realized in a next revision or by a maker implementing this instructable.

  • An LED chain or multi-color LED could be added to provide user feedback (direction, power setting, overload condition), or even add a small display.
  • Relays could be replaced by power mosfets
  • The voltage of the digitally controlled power supply could be read by another ADC. This way a closed loop control of the output could be achieved (if needed).
  • An interface could be built to enable running a control program on a PC or a Raspberry Pi. This would enable automation.
  • A PCB with one or two track controller would enable to add as many of these track controllers as you have rails.
  • If you run out of GPIOs you could use a port expander to the track controller PCB, such as the MCP23017. This IC is available with I2C or SPI interface and provides 16 GPIOs.

Step 12: Development Environment

I am using Micropython as "operating system", please find the step by step guide getting started with micropython on raspberry pi pico.


Load the software to the rp2040 PicoW:

To load Micropython version 1.20 or higher onto the RP2040 PicoW board, please follow the instructions on the Micropython download Website.

When you connect to the rp2040 picoW board, after successfully downloading micropython should start and you get a message similar to below with the three chevrons >>> being the micropython REPL prompt:

MicroPython v1.19.1 on 2022-09-14; Raspberry Pi Pico W with RP2040

Type "help()" for more information.

>>>


A recommendation for a development environment:

You certainly can use your favorite IDE (Thonny, MU Editor, ...) to edit and download micropython programs.

If you intend to further develop the modules in this instructabe, e.g. add new functionality, I'd like to recommend the tool "mpremote". The documentation says: "The mpremote command line tool provides an integrated set of utilities to remotely interact with and automate a MicroPython device over a serial connection."

In short: mpremote enables you to do all the editing work in a working directory on your PC with your favorite editor (e.g. I like to use sublime text 4).

When you are ready to test you open a terminal window, change directory to your working directory and enter the command: mpremote mount . (do not forget the dot "." , which indicates the current directory)

This mounts your working directory to micropython on the RP2040. You should get the REPL promt >>> of micropython. You can import and start your micropython programs from your working directory and get all the program output in your terminal window.

The major benefit: should you need to correct an error, or like to continue programming, you can do that with your editor. To reload your program you do a soft reset of micropython (at the REPL >>> you hit CTRL-D) and import your program again.

This avoids that you have to download the program to the rp2040 flash memory all the time you do a change. Eventually, when you are done with your development, you download the final program and you can run it independent of a PC.

To test the digital potentiometer for example you would copy the files from Step 3 to your working directory, open a terminal window and enter $ mpremote mount . and at the micropython chevrons >>> import MCP42010.py