Software - ejbergdk/loconet-routectrl3 GitHub Wiki

Getting the software

This repository uses submodules. Add --recurse-submodules when cloning the project:

git clone --recurse-submodules https://github.com/ejbergdk/loconet-routectrl3.git

If you have already cloned this repo without --recurse-submodules, run this command:

git submodule update --init --recursive

Building the software

Using Microchip Studio 7

The easiest way to build the software (on Windows), is with Microchip Studio 7. If you don't have it installed already, when asked, you only need to install support for AVR 8-bit MCU using AVR-GCC. There is no need to install anything else (apart from a device pack after the setup is complete).
Once Studio 7 is up and running, make sure you have the appropriate Device Pack installed within Studio 7 (Tools -> Device Pack Manager -> Install the newest AVR-Dx_DFP pack). When installed, restart Studio 7 (only needed after installing new device packs).

Open the project solution file loconet-routectrl3.atsln in Studio 7, and you should be all set.

Other ways

If running on anything else than Windows, you are more or less on your own. Hopefully you know how to do this already. You should use AVR-GCC, and you'll need to know this:
This project uses two linker script augmentation files (routetables.ld and lib/avr-shell-cmd/cmdtables.ld) that you'll need to inform the linker about. This is done with the command line option -Wl,-T <linkerfile>.

Preprocessor macro defines

Some preprocessor macros must be set globally (with the -D command line option, or in Studio 7 Debug -> routectrl3 Properties -> Toolchain -> AVR/GNU C Compiler -> Symbols). Others are optional. Here is a list of the most important ones:

Macro Description
F_CPU=24000000UL Mandatory Set to the CPU clock frequency. This project runs at 24 MHz
EXTXTAL Set only if your hardware has the optional 32.768 kHz crystal mounted. Note: The Curiosity Nano board does have such a crystal
LNPACKET_SIZE_MAX=6 Set this to limit the Loconet packet length to max 6 bytes. This saves ram space. Don't do it if you need support for long Loconet packets
LNPACKET_CNT=16 Set to determine the maximum number of Loconet packets that can be processed at the same time. Defaults to 8 if not set
LNSTAT Set to collect statistical data on Loconet communication. Read stat with shell command ln s
LNECHO If set, all Loconet packets sent from this module will be received and processed as if it was sent from any other module on the Loconet bus. If not set, packets sent from this module will not cause a reception of them
ROUTE_DEBUG If set, debug info about route handling is printed on the serial shell. Handy when writing/testing new routes
FEEDBACK_ADR_MAX=192 If set, limits the feedback address range to the given number. This can save some ram. Defaults to the full 4096 addresses if not set, which uses 512 bytes ram
SW_ADR_MAX=400 If set, limits the switch address range to the given number. This can save some ram. Defaults to the full 2048 addresses if not set, which uses 256 bytes ram
MAXROUTES=250 Defines how many routes are supported. Don't make this number too large, or you'll waste both ram and cpu processing power. Defaults to 200 if not set

Brief overview

Once you have cloned the repository and set up all of the above, you should be able to build the code without errors. If you then put it on an AVR, you'll find that it doesn't really do anything. This is not an error. This project (or at least the main branch, which is the one you should use) is an empty framework, ready for you to fill in whatever you want it to do on your train layout.

First you should verify that the code is actually running on the AVR. Connect a USB-serial (logic-level, not RS-232) cable to the serial pins. If you've used the Curiosity Nano board, the serial connection is already included in the debugger USB cable. Open a serial terminal and connect with 115200 baud, 8N1, and you should get a simple text shell. Type help to get a list of all the debug commands available.

Now you are ready to add the functionality you want. I recommend you add one or more new .c files that'll hold your code. This will make it easier to get future updates from this repo, without destroying your additions.

Send a switch request

The most common action you likely is going to do, is to send a switch request to the Loconet bus. First you need to include route.h:
#include "route.h"
This include has most of the functionality you'll need to create routes and send commands to your layout. To send a switch request, use route_send_sw(uint16_t adr, bool opt);
Example: Set switch (or signal) 28 to green:
route_send_sw(28, SW_G);
You can send many such requests right after each other. The requests will be queued up and sent with proper intervals in order not to overflow your command central. The opt parameter determines the switch direction (or signal aspect). You can use SW_R and SW_G.

Send a feedback report

Similarly as sending a switch request, you can just as easily send a feedback report using route_send_fb(uint16_t adr, bool opt);
Example: Report that feedback address 42 is occupied:
route_send_fb(42, FB_OCCUPIED);
Here, opt can be FB_OCCUPIED and FB_FREE.

Event driven programming

Being able to send switch and feedback packets onto the Loconet bus is all fine, but we haven't talked about WHEN to do it. The general idea behind how to use this route controller, is that it only does some actions as a response to input coming from your layout. That could be incoming feedback from contact tracks, a control panel (such as Uhlenbrock Track Control) or even as commands typed in the debug shell.

Do an action when receiving a feedback report

A simple example will be setting a signal to red when a train passes it. For this we need to listen to feedback reports from contact tracks on the layout. We can do that by including fb_handler.h. With this we can create callback functions that will be called when a given feedback address is activated. We need to write a function with all the actions we want performed, and then we need to indicate that this function is to be called when a given feedback report is received. This sounds complicated, but it is not too bad. An example will show it much better than I can describe it in words:

Lets say we have a signal on address 56, and we want this to go to red when a train hits contact track 49. This is written like this:

#include "fb_handler.h"
#include "route.h"

// Function with the actions to do
static void lets_do_this(uint16_t num)
{
    // Set signal 56 to red
    route_send_sw(56, SW_R);
}

// Set up a monitor on feedback address 49 occupied, and call the function above when it happens
FEEDBACK_OCC(49, lets_do_this);

This is all that is needed. Let's go through it step by step:

  • First we include the two .h files needed. If you don't know C programming, you only do this once at the top of your .c file.
  • Then we define a new function that has the actions we want to be done. In this case setting signal 56 to red. The name of the function can be whatever you like, but it must be a static void function and have a single uint16_t parameter. The parameter (here called num) will have the feedback address that caused the function to be called. This isn't used in this simple example, but it can be useful in more advanced scenarios where several different feedback addresses can call the same function.
  • Finally FEEDBACK_OCC tells the compiler that we want the function "lets_do_this" to be called when contact track 49 becomes occupied. Similarly, if you want something to happen when the track is freed instead, you use FEEDBACK_FREE instead.

Do an action when receiving a switch request

You can also do actions when receiving a switch request, just like when receiving a feedback report as described above. You need to include sw_handler.h and use SWITCH_REQ instead of FEEDBACK_OCC/FEEDBACK_FREE. Let's have a look at an example:

#include "route.h"
#include "sw_handler.h"

// Function with the actions to do
static void switch17_actions(uint16_t adr, bool dir)
{
    // Add whatever actions you like to perform here
}

// Set up a monitor on switch address 17, and call the function above when it happens
SWITCH_REQ(17, switch17_actions);

At first glance, this example is very similar to the previous feedback example, but there are a few important differences.
The function must now take two parameters: A uint16_t holding the address that caused the function to be called, and a bool containing the direction the switch was commanded (SW_R / SW_G). Unlike for the feedback handler, which has separate FEEDBACK_OCC and FEEDBACK_FREE macros, the SWITCH_REQ macro calls the same function for both SW_R and SW_G, hence the need for the extra parameter.

The route concept

Now we've reached the core functionality of this project: Routes.
A route is the process of moving a train from one specific spot to another specific spot on your layout, preferably without colliding with another train along the way. If all possible movements on the layout are defined as routes, then we can say which routes aren't allowed to be active at the same time, thereby preventing collisions.

A route goes through some steps when used:

  • First, a route must be requested from somewhere. This can be by automation (a contact track) or by manual input from a control panel.
  • When a route is requested, it starts to monitor conflicting routes, called constraints.
  • Once all conflicting routes are inactive, the requested route is activated. This trigger a function callback, where you have defined what needs to happen to make the train go through this route.
  • When some sort of feedback (usually a contact track) indicates that the train has passed through the route, it should free the route. The route then trigger another function callback, where you can clean up signals and so on.
  • Routes can also make a cancel callback, trigger other routes and perform delayed actions, but that is only used for more advanced setups. Let's not worry about this now.

Route example

Consider this simple station with two platforms (P1 and P2), one entry (A) and one exit (B):

station1

(s are signals, w are switches/turnouts and K are contact tracks, all with their respective addresses)

There are 4 possible routes:

  1. From A to P1
  2. From A to P2
  3. From P1 to B
  4. From P2 to B

We will have a closer look at route 1, going from A to platform P1.
First we write the actions that should be performed when activating and freeing the route into two functions, and then defining the route with its constraints.

static void route1_act(void)
{
    // Set switches to get from A to P1
    route_send_sw(10, SW_G);
    route_send_sw(9, SW_G);
    // Set signal at A to green
    route_send_sw(103, SW_G);
}

static void route1_free(void)
{
    // Set signal at A to red
    route_send_sw(103, SW_R);
}

ROUTE(1, route1_act, route1_free, NULL, 2, 3);

There is a lot going on here, but it isn't as bad as it looks.

  • The first function (route1_act) contains what needs to happen when the route is activated. Two switches are set to get the train from A, through w10 and w9 to P1. Then sets the signal s103 to green to allow the train to move.
  • The second function (route1_free) is called when the route is freed. It just sets signal s103 back to red.
  • Lastly, the route is defined with the ROUTE macro. It takes several parameters:
    • First parameter is the route number. It can be from 0 to 199, or higher if you have changed the MAXROUTES define as described earlier.
    • Second parameter is the function you want to run when this route is activated.
    • Third parameter is the function you want to run when this route is freed.
    • Fourth parameter is the function you want to run when this route is cancelled. We don't use that in this example, and instead of a function, you can write NULL to indicate there is no function to call.
    • The rest of the parameters are the constraints. It is a list of routes, that conflicts with this one. Here we won't allow route 1 to activate if route 2 or 3 is already active. You can add as many routes as you like to the constraint list. You can also omit the constraint list entirely.

That was the definition of route 1, but we are not done yet. We still need to activate and free the route.
Activation could come from a control panel. Let's say that a button on such a panel sends feedback 501 when pressed.
Freeing should happen as soon as the train has moved past all switches along the route. In this example that is when the train has passed contact track K41.

// Feedback 501
static void fb501occupied(uint16_t num)
{
    // Control panel wants to activate route 1
    route_request(1);
}

FEEDBACK_OCC(501, fb501occupied);

// Contact track K41 is freed
static void k41free(uint16_t num)
{
    // Train has passed K41. Free route 1
    route_free(1);
}

FEEDBACK_FREE(41, k41free);

And now we are done. Route 1 should now work as intended. Now we just need to do similar work for routes 2, 3 and 4, and that is it for this small station.

Of course a real model train layout is going to be bigger and much more complex than this. To inspire you (or scare you off), this is the route and feedback definitions for my 4.5 meter long layout:
ctrl_routetables.c
ctrl_feedbacktables.c

⚠️ **GitHub.com Fallback** ⚠️