Z-Scale controller Part IV: Turnout controller firmware

In my previous article, I described the electronics involved in controlling turnouts. This article will cover the firmware part.

Hardware SPI on the AVR

The AVR processor that is used on the Arduino is, like most recent microcontrollers, capable of talking to other chips using the SPI protocol, and relies on dedicated hardware to do this. The main advantage of using the built-in capabilities of the AVR rather than just bitbanging IOs, is speed, simplicity, the the ability to read data while we write on the bus.

The documentation for using SPI on the Arduino is located on their site. All functionality is provided through a dedicated library, as usual.

One caveat with SPI, is that the concept of SPI covers many variations: SPI is, at the core, simply a way to talk to a chip using a synchronous serial link: one clock, one data out, one data in, and a ‘chip select’ pin. Depending on the specifics of the chip you are talking to, you might find that that particular chip expects an idle high or idle low clock signal, or that bit transitions on the data bus occur on raising or falling edges on the clock. Last, some chips expect data with Least Significant bit first, others with Most Significant Bit first. Fortunately, the AVR SPI hardware covers all those, and the Arduino SPI library implements this with no hassle.

This all translates in the following for the MC33880, as the code below shows. First of all, let’s define some constants at the beginning of the firmware:

// SPI Connection to the shift register/relay driver
const int spi_mosi  = 11;
const int spi_miso  = 12;
const int spi_clock = 13;
const int spi_ss    =  6;

Then setup() will initialize the SPI bus as follows:

   // Initialise the SPI library
   SPI.setBitOrder(MSBFIRST); // 33880 Uses MSB First transmission
   SPI.begin();
   SPI.setDataMode(SPI_MODE1);

   pinMode(spi_ss,OUTPUT);
   digitalWrite(spi_ss,HIGH); // Deselect chips
   delay(3);

Power-On self test

Reading the MC33880 data sheet, Freescale recommends to check bus integrity at power up, which sounds like a good idea: we don’t want to end up in situation where accessories would be turned on outside of our control, since that could cause them to get damaged. I therefore implemented a simple mechanism to do this bus integrity test at reset:

   // SPI Integrity check: transmit 2 bytes to each chip. 1st received byte is the
   // fault output, 2nd received byte should be same as 1st sent byte
   digitalWrite(spi_ss,LOW);
   byte val = SPI.transfer(0xaa); // Test pattern (10101010)
   val = SPI.transfer(0xaa);      // send same pattern to second chip
   val = SPI.transfer(0); // turn all outputs off
   // (Test hiside, should be 170)
   post_result |= (val!=170) ? SPI_INTEGRITY_FAULT : 0;
   val = SPI.transfer(0); // finish turning all outputs off (second chip)
   //(Test lowside, should be 170)
   post_result |= (val!=170) ? SPI_INTEGRITY_FAULT : 0;
   digitalWrite(spi_ss,HIGH); // all outputs are now turned off

At the end of this sequence, the post_result global variable will contain the result of this test, and the rest of the firmware will be able to report issues, as well as modify its behaviour, for instance by refusing to control accessories in case the test did not pass.

Accessory Command function

All we need to do now, is to write an “accessory command” function. I decided to number accessories from 1 to 16, and used a concept of “port” for each accessory number, which corresponds to both coils of a turnout.

As discussed in the previous article, I am also using a “remapping” function to take into account the fact that the physical connectors are not wired in linear order.

Also, a “remap_faults” function updates the contents of the firmware’s “accessory_map” array which keeps track on what port is connected to an accessory or not, based on what the 33880 detected.

// Using the fault register value and bank number,
// update our connected accessory map
void remap_faults(byte bank, byte faults) {
  byte v;
  for (int i=0; i<8; i++) {
    v = faults & (1<<i);
    if (v>0) {
      accessory_map[bank*8+i] = 0;
    } else {
      accessory_map[bank*8+i] = 1;
    }
  }
}

And last, the turnout control function itself. This version supports the concept of turning an accessory on and forcing it off, it might not be present in the final version of the firmware, and I will probably replace it by a “short pulse” and “long pulse” concept. In the mean time, the code below is amply commented.

The most important thing to keep in mind when reading the code below, is that since both 33880 chips are connected one after another, we always need to write two bytes on the bus after selecting the chips and before deselecting them. And when we do this, we receive the status of the high side then low side chip in the process.

/**
 * Set a turnout
 * Arguments:
 * address: turnout number (starts at ONE)
 * dir: true or false (straight or not)
 * op : one of OP_PULSE, OP_ON, OP_OFF
 */
void accessoryCommand(int address, int port, int op)
{
  byte response;
  address--; // we start at one in the command, this is more human friendly.
  // Compute the turnout bank we should enable
  byte bank = address / turnout_banks;
  byte bankio = 1 << highside_map[bank];

  // Compute the actual I/O we should pulse
  int io = 1 << (lowside_map[address%turnout_banks*2+port]);

    if (op == OP_ON) {
      // Save timestamp & accessory ID:
      accessory_on_id = address;
      accessory_on_port = port;
      accessory_on_timestamp = millis();
    }

   // Enable the accessory bank (highside driver)
   digitalWrite(spi_ss,LOW);
   response = SPI.transfer(bankio); // Highside driver value (bank select)
   response = SPI.transfer(0);      // Lowside: all zeroes for now (all accessories off)
   digitalWrite(spi_ss,HIGH); // Turn on outputs
   delay(1); // Required to give the 33880 time to do open circuit fault detection (has to be > 300us)
   // Send pulse (lowside driver)
   digitalWrite(spi_ss,LOW); // Upon transistion to low, fault status is udpated in the
                              // 33880 register: the next two SPI commands will return those,
                              // see below.
   if (op != OP_OFF) {
     response = SPI.transfer(bankio); // Highside driver value
     response = SPI.transfer(io);     // Lowside driver value
     // This response value tells up which pins are connected to
     // an accessory and which are not:
     // a connected pin will be zero, an unconnected one will be 1 ("Open")
     remap_faults(bank,response);

 #ifdef DEBUG
    aJsonObject *msg = aJson.createObject();
    aJson.addNumberToObject(msg, "bank", bank);
    aJson.addNumberToObject(msg, "bankio", bankio);
    aJson.addNumberToObject(msg, "io", io);
    aJson.addNumberToObject(msg, "faults", response);
    aJson.print(msg, &serial_stream);
    Serial.println();
    aJson.deleteItem(msg);
#endif

   }
    if (op == OP_PULSE) {
        digitalWrite(spi_ss,HIGH);    // Updates outputs + fault status
        delay(turnoutPulse);
        digitalWrite(spi_ss,LOW);
   }
   if (op != OP_ON) {
     response = SPI.transfer(0);      // Switch off Highside
     //response is now Highside fault status after lowside select
     response = SPI.transfer(0);      // Switch off Lowside
     //response is now Lowside fault status after lowside select
   }
   digitalWrite(spi_ss,HIGH);
}

What next

Let’s start now to take a look at the UI of this project: part IVb does just that.

Leave a Reply

Your email address will not be published. Required fields are marked *