DCC Stepper Controller – A4988 Nano – Example Sketch

At the end of this guide, you should be able to drive a stepper motor from your Model Railway DCC Controller…

This is for the Arduino Nano Model Railway DCC Stepper Motor Controller board, not the UNO version.

At the end is a video showing it working.

To start off with, we need to get a number of things ready….

Lets get ready…..

A Cup of tea…. cant get started without one of these…

Your completed DCC Stepper Controller board

A USB Cable

A Computer / Laptop running Linux / Mac / Windows ( which ever you prefer )

A Stepper Motor – i bought mine from https://coolcomponents.co.uk

A power source – 12v. An old BT Homehub supplied mine 🙂

A Hall effect Sensor – easy to find on ebay – stick this to the outside of the turn table well – this will be our Home position – our reference point.

A small strong magnet – also easy to find on ebay – stick these to one end of the turn table – we will use them to trigger the hall sensor.

Arduino IDE

The first thing you will need to do, if you have not done already is to install the Arduino IDE ( Integrated Development Environment ). Arduino have a complete Getting Started guide that you can follow here.

This will install the standard Arduino IDE, which is great and a good starting point. However there are other IDE’s available. I use VisualMicro, which is an extension to Microsoft’s Visual Studio. It is much quicker an compiling the sketches, and offers intellisense when writing code.

The example Sketch

The example sketch was created with the help of Alex Shepherd from Nrma – Thanks Alex.

In order to compile the example sketch, you will need to download the following libraries

AccelStepper – the stepper motor driver. You can install it from the Library Manager in the Arduino IDE..

or you can download it from here.

The NmraDcc Arduino library can be found in the Arduino Library Manager or downloaded from here: https://github.com/mrrwa/LocoNet/archive/master.zip .

You can of course use the MynaBay DCC Library  which can be downloaded here… https://github.com/MynaBay/DCC_Decoder, but for my example i’m using the NmraDcc library.

So firstly, Ill show the sketch and then describe the important parts…

// DCC Stepper Motor Controller ( A4988 ) Example
// See: https://www.dccinterface.com/how-to/assemblyguide/
// 
// Author: Alex Shepherd 2017-12-04
// 
// This example requires two Arduino Libraries:
//
// 1) The AccelStepper library from: http://www.airspayce.com/mikem/arduino/AccelStepper/index.html
//
// 2) The NmraDcc Library from: http://mrrwa.org/download/
//
// Both libraries can be installed via the Arduino IDE Library Manager 
//

#include <AccelStepper.h>
#include <NmraDcc.h>

// Uncomment to enable Powering-Off the Stepper if its not running 
#define STEPPER_ENABLE_PIN 6

// Home Position Sensor Input
#define HOME_SENSOR_PIN 3

typedef struct
{
  int address;
  int stationFront;
  int stationBack;
}
DCCAccessoryAddress;
DCCAccessoryAddress gAddresses[2];

// for a 1.8 deg stepper, there are 200 full steps
#define FULL_STEPS_PER_REVOLUTION 200

// Uncomment the lime below for the Driver Board Settings
//#define FULL_TURN_STEPS (FULL_STEPS_PER_REVOLUTION)     // full steps
//#define FULL_TURN_STEPS (FULL_STEPS_PER_REVOLUTION * 2) // 1/2 steps
#define FULL_TURN_STEPS (FULL_STEPS_PER_REVOLUTION * 4) // 1/4 steps
//#define FULL_TURN_STEPS (FULL_STEPS_PER_REVOLUTION * 8) // 1/8 steps
//#define FULL_TURN_STEPS (FULL_STEPS_PER_REVOLUTION * 16) // 1/16 steps

// home location
#define entryStation FULL_TURN_STEPS / 3

volatile bool bInterruptDetected = false;
bool bHomePositionFound = false;

// Now we'll wrap the stepper in an AccelStepper object
AccelStepper stepper1(1, 4, 5);

NmraDcc  Dcc ;
uint16_t lastAddr = 0xFFFF ;
uint8_t lastDirection = 0xFF;
//
// Decoder Init
//
void ConfigureStations()
{
  // this is home
  gAddresses[0].address = 200;
  gAddresses[0].stationFront = entryStation;
  gAddresses[0].stationBack = entryStation + (FULL_TURN_STEPS / 2);

  gAddresses[1].address = 201;
  gAddresses[1].stationFront = ((FULL_TURN_STEPS / 3) * 2);
  gAddresses[1].stationBack =  ((FULL_TURN_STEPS / 3) * 2) + (FULL_TURN_STEPS / 2);
}

// This function is called whenever a normal DCC Turnout Packet is received
void notifyDccAccTurnoutOutput( uint16_t Addr, uint8_t Direction, uint8_t OutputPower )
{
  Serial.print("notifyDccAccTurnoutOutput: ") ;
  Serial.print(Addr,DEC) ;
  Serial.print(',');
  Serial.print(Direction,DEC) ;
  Serial.print(',');
  Serial.println(OutputPower, HEX) ;

  for (int i = 0; i < (sizeof(gAddresses) / sizeof(DCCAccessoryAddress)) ; i++)
  {
    if ((Addr == gAddresses[i].address) && ((Addr != lastAddr) || (Direction != lastDirection)) && OutputPower)
    {
      lastAddr = Addr ;
      lastDirection = Direction ;
      
      Serial.print(F("Moving to Station : "));
      Serial.println(i, DEC);

#ifdef STEPPER_ENABLE_PIN
      stepper1.enableOutputs();
#endif
      if (Direction)
      {
        Serial.print(F("Moving to Front Position : "));
        Serial.println(gAddresses[i].stationFront, DEC);
        stepper1.moveTo(gAddresses[i].stationFront);
        break;
      }
      else
      {
        Serial.print(F("Moving to Back Position : "));
        Serial.println(gAddresses[i].stationBack, DEC);
        stepper1.moveTo(gAddresses[i].stationBack);
        break;
      }
    }
  }
};

bool lastIsRunningState ; 

void setupStepperDriver()
{
#ifdef STEPPER_ENABLE_PIN
  stepper1.setPinsInverted(false, false, true); // Its important that these commands are in this order
  stepper1.setEnablePin(STEPPER_ENABLE_PIN);    // otherwise the Outputs are NOT enabled initially
#endif
   
  stepper1.setMaxSpeed(1000.0);
  stepper1.setAcceleration(1000);
  stepper1.setSpeed(600);
 
  lastIsRunningState = stepper1.isRunning();
}

void moveToHomePosition()
{
  pinMode(HOME_SENSOR_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(HOME_SENSOR_PIN), interruptEvent, RISING);

  bInterruptDetected = false;
 
  Serial.println(F("Performing 2 complete turns to find home."));
  stepper1.move(FULL_TURN_STEPS * 2);
}

void interruptEvent()
{
  detachInterrupt(digitalPinToInterrupt(HOME_SENSOR_PIN));
  bInterruptDetected = true;
}

void setupDCCDecoder()
{
  Serial.println(F("Setting up DCC Decorder..."));

  // Setup which External Interrupt, the Pin it's associated with that we're using and enable the Pull-Up 
  Dcc.pin(0, 2, 1);
  
  // Call the main DCC Init function to enable the DCC Receiver
  Dcc.init( MAN_ID_DIY, 10, CV29_ACCESSORY_DECODER | CV29_OUTPUT_ADDRESS_MODE, 0 );
}

void setup()
{
  Serial.begin(115200);
  while(!Serial);   // Wait for the USB Device to Enumerate

  Serial.println(F("Example Turntable Program - www.dccinterface.com"));

  ConfigureStations();
  
  setupStepperDriver();

  Serial.println(F("Finding home...."));
  moveToHomePosition();
}

void loop()
{
  if (bInterruptDetected)
  {
    bInterruptDetected = false;
    bHomePositionFound = true;

    Serial.println(F("Found Home - Ssetting Current Position to 0"));

    stepper1.setCurrentPosition(0);

    Serial.print("Moving to position ");
    Serial.println(entryStation, DEC);
#ifdef STEPPER_ENABLE_PIN
    stepper1.enableOutputs();
#endif
    stepper1.moveTo(entryStation);

    setupDCCDecoder();
  }

  if (bHomePositionFound)
  {
    // You MUST call the NmraDcc.process() method frequently from the Arduino loop() function for correct library operation
    Dcc.process();
  }

  // Process the Stepper Library
  stepper1.run();

#ifdef STEPPER_ENABLE_PIN
  if(stepper1.isRunning() != lastIsRunningState)
  {
    lastIsRunningState = stepper1.isRunning();
    if(!lastIsRunningState)
    {
      stepper1.disableOutputs();
      Serial.println(F("Disable Stepper Outputs"));
    }
  }
#endif  
}

So, firstly, as we want to run this from DCC we want to set up the helper structures


typedef struct
{
  int address;
  int stationFront;
  int stationBack;
}
DCCAccessoryAddress;
DCCAccessoryAddress gAddresses[2];

 

Data Structures

In this we define a structure to hold the address and the station positions. Then we declare an array of the struct type with two elements – one for each turntable bridge position.

 

Declaring the Stepper Motor as an Accelerator Object

// Now we'll wrap the stepper in an AccelStepper object
AccelStepper stepper1(1, 4, 5);

Here we declare the stepper1 object as an AccelStepper object. For the Nano version we set the STEP parameter as 4 ( pin 4 on Arduino Nano ) and the DIR parameter to 5 ( pin 5 on the Arduino Nano). This is Important and is different on the UNO version.

This gives the turn table an acceleration up to full speed when starting off, and deceleration when approaching the target turntable track.

 

Main parts of the Code.

There are 3 main parts of the code – Setup, Loop and BasicAccDecoderPacket_Handler.

  • Setup() -> this is where we set up the stepper motor and the the interrupt , and start the turntable moving to find the home position.
  • Loop() -> Once “home” is found, we then set up the Arduino as a DCC Decoder. We also call the stepper library run() command and the DCC library loop() command in order for each library to process.
  • BasicAccDecoderPacket_Handler() -> This is where we listen to the accessory decoder packets and react to them – turn the table to the position we require.

 

SetupStepperDriver.

This is where we set up the Stepper Motor Driver.

 

MoveToHomePosition

Here we set up D3 ( pin 3 ) with an internal pull up, and tell the arduino that “interruptEvent()” is a call back function – when it detects the interrupt – call this function – our function.

We then tell the stepper motor to make two complete turns – in order to find the home position. Home will be found when the magnets on the turn table bridge trigger the hall sensor.

Once we have set the turn table moving, the code moves to the main loop()

 

InterruptEvent

This is the method that will be called when the Arduino detects the hall sensor triggering in response to the magnet moving within range. We set the bool variable “bInterruptDetected” to true at this point – a boolean variable stores “True” or “False”.

 

Loop

The first thing is to check if bInterruptDetected is true – this means we have found out home position.

If we have found home, we detach the interrupt, reset the stepper motors home position [stepper1.setCurrentPosition(0);] and then tell the stepper motor library to move to the entry position – in our example position 8000. Remember – we are in 1/8th steps microstep mode, with 1 complete turn being 12800 steps ( microsteps ).

We now set up the arduino as a DCC decorder – so we tell it which method to use as a call back – [BasicAccDecoderPacket_Handler()] , and we configure the gAddresses[] array that we will use as positional information.

We also now know the home position has been found, so we allow the DCC library to process packets using the [DCC.Loop()] command.

Finally, regardless of interrupt status, or home being found, we let the stepper motor library process [stepper1.run()]

 

BasicAccDecoderPacket_Handler

Here is were we listen to , and react to the DCC accessory addresses that are being sent from our DCC Command centre.

We perform a bit of manipulation of the [address] and [data] variables so that we have the address as an integer. An Integer data type holds whole numbers, such as 1,2,10, 1000 up to 32767.

Then we loop through the gAddresses[2] array from the first element to the last element

(We use for (int i = 0; i<(int)(sizeof(gAddresses) / sizeof(gAddresses[0])); i++) so that if we increase the number of stations, we dont have to constantly rewrite sections of code )

If an address is detected that we are interested in ( if (address == gAddresses[i].address) ) we then check its “Enabled” state – if this was a point it would be “Normal” or “Thrown” – one state of another. We use this to move the turn table bridge to the forward or back position.

The next step is to tell the stepper motor to move to ( stepper1.moveTo(gAddresses[i].stationFront); ) the position we require.

And thats about it.

This is an example sketch, with a lot of scope for refinement and customisation. It should get you started on journey to writing your own sketch.

If you have any questions or need help, please email us on support@dccinterface.com

Here is a video showing it working under DCC control.

Thank you and have fun.