Tech: Programming an Arduino to talk Seatalk

Warning: This blog post contains code!
SmartConroller test bench.
Recently my Raymarine "Smart Controller" remote control stopped working. It simply refused to connect to the wireless base station, complaining "ALARM: NO LINK" every time it was turned on. Fortunately it was under warranty so I promptly sent it back to Raymarine to be fixed. When it came back I thought it would be a good idea to test it before going to trouble to drive to my boat and re-install it.

The SmartController wireless base station and remote control are Seatalk devices. I cobbled together a simple test bench using a prototyping breadboard, supplied 12V and connected both devices. Voila! The remote control now connected to the base station.

To confirm full functionality though, I needed a way of sending Seatalk data to the remote. Previously I've done a bit of NMEA 0183 programming with my Arduino, testing my MR-350 GPS. Both NMEA 0183 and Seatalk are serial protocols but that is where the similarities end. NMEA 0183 is a simple text-based protocol, whereas Seatalk is binary protocol. Seatalk is a proprietary protocol developed by Raymarine, which has been reverse engineered. SeaTalk is weird in that it uses 9-bit symbols instead of the usual 8-bit bytes. The most-significant bit (0x100) is set to 1 for the first symbol of every new message, and 0 thereafter. To learn more, read Thomas Knauf's Seatalk reference, the Bible of Seatalk.

The good news is that the Arduino Uno's UART supports 9-bit serial transmission. The bad news is that this support is not available in any standard library. Fortunately this forum post pointed to a modified version of the Arduino HardwareSerial code by Nick Gammon. I could not find any Arduino code for sending Seatalk messages though so I decided to write my own.

Note: I also used the handy XScopes Xprotolab mini oscilloscope (bottom right in photo) for measuring voltages and waveforms.

Here's the gist of how my code works:

First, I defined all the field types used by Seatalk as follows:

  enum FieldType {
    Null =   0,     // terminates a datagram spec
    Cmd =    1,     // command byte
    Att =    2,     // attribute byte
    Nibble = 3,     // data nibble (4 bits)
    Byte =   4,     // data byte (8 bits)
    Int =    5,     // data int (16 bits)
    Int10 =  6      // data int scaled by 10 (NOT 10 ints)
  };

Next, I defined Seatalk datagram specifications as arrays of alternating field types and field values, terminated by Null.

For example, Speed Through Water (STW) is defined as follows:

  uint16_t stwSpec[] = { Cmd, 0x20, Att, 0x01, Int10, 0, Null };
  #define STW_SPEED 5

The #define is the position in the array of the speed value.

Depth Below Transducer (DBT) requires 3 fields:
  uint16_t dbtSpec[] = { Cmd, 0x00, Att, 0x02, Nibble, 0, Nibble, 0, Int10, 0, Null };
  #define DBT_Y 5
  #define DBT_Z 7
  #define DBT_DEPTH 9

The first nibble at position 5 (DBT_Y) defines the units. The second nibble at position 7 (DBT_Z) optionally defines alarms.

Compass Heading (HDG) is even more complicated:

  uint16_t hdgSpec[] = { Cmd, 0x89, Nibble, 0, Nibble, 0x2, Byte, 0, Byte, 0,
                         Nibble, 0x2, Nibble, 0, Null };
  #define HDG_U  3
  #define HDG_VW 7
  #define HDG_XY 9
  #define HDG_Z 13

The heading value is calculated from 3 different fields as follows:
  heading = (HDG_U & 0x3) * 90 + (HDG_VW & 0x3F) * 2 + (HDG_U & 0xC) / 2

I case you're wondering where the "U", "VW", "XY", etc. all come from, I'm using the same symbols as in Knauf's Seatalk reference.

The following function does the work of transmitting any Seatalk datagram:

  boolean writeDatagram(uint16_t data[]) {
    // send a datagram; last element of data must be Null
    for (int ii = 0; ; ii += 2) {
      switch (data[ii]) {
      case Null:
        return true;

      case Cmd:
        // NB: set command bit
        Serial.write9bit(data[ii + 1] | 0x0100);     
        break;

      case Nibble:
        // NB: nibbles come in pairs, with most significant first
        if (data[ii + 2] != Nibble) return false;
        Serial.write9bit((data[ii + 1] << 4) | (data[ii + 3] & 0x0f));
        ii += 2; // skip over next nibble which we've already consumed
        break;

      case Att:
      case Byte:
        Serial.write9bit(data[ii + 1] & 0x00ff); // mask high byte
        break;

      case Int:
      case Int10:
        Serial.write9bit(data[ii + 1] & 0x00ff); // LSB first
        Serial.write9bit(data[ii + 1]  >> 8);    // MSB second
        break;

      default:
        return false;
      }
    }
    return true;
  }

Finally, here's the code to send Seatalk messages for depth, speed and heading:

    // DBT = 8.1 m
    seatalk::dbtSpec[DBT_Y] = 4; // specify meters
    seatalk::dbtSpec[DBT_DEPTH] = (uint16_t)(M_TO_FT(8.1) * 10);
    seatalk::writeDatagram(seatalk::dbtSpec);

    // STW = 6.2 knots
    seatalk::stwSpec[STW_SPEED] = (uint16_t)(6.2 * 10);           
    seatalk::writeDatagram(seatalk::stwSpec);

    // HDG = 291 (= 3 * 90 + 10 * 2 + 2/2)   
    seatalk::hdgSpec[HDG_U] = 0xB;
    seatalk::hdgSpec[HDG_VW] = 10;
    seatalk::writeDatagram(seatalk::hdgSpec);

The full source code for my Arduino program ("sketch") along with the modified HardwareSerial files can be found here.  Copy HardwareSerial files over those in your Arduino installation (in cores/arduino) then restart the Arduino IDE.

The most satisfying thing about coding, is the joy of working code.  I even have photos to prove it!

 

Regarding the hardware interface, note that the Seatalk bus rests at 12V, whereas Arduino uses 5V. A non-inverting 5V~12V level shifter, such as this, is therefore advisable.

OVER.

PS More Arduino Arriba blog posts and an Arduino-based autopilot simulator.