ANT Slave Operation

Configure dev board as ANT slave and transfer data [VIDEO]

Platform Test Tools SAM3U2 Firmware nRF51422 Firmware Software
MPG1
MPG2
ANT USB Stick
USB RS-232 Converter
MPG User Code
ANT Slave SOLUTION
AP2 Emulator ANTware II
TeraTerm

Prerequisite Modules

Start

This module will setup and run the development board in Slave mode. Start with the main Master code branch.

We will write a program that will do the following:

  1. Show if a channel is open and searching (green), paired (blue), loss of sync (blinking blue) and closed(yellow)
  2. Display any NEW data from transmitting devices (repeated messages will be ignored)
  3. Respond to a special command that we define to set an LED to a certain color (the LCD back light on the ASCII board, or an RGB light on the dot matrix board)

*** The code shown here is only for the ASCII dev board to keep things clear. The solution on Github supports both development boards. ***

There are also some very important things to note:

  1. The default Broadcast message data for the ANTware Master is all zeroes except for the last byte which is a one-byte counter that increments every message (e.g. 00-00-00-00-00-00-00-xx)
  2. If you configure a Slave device with wildcard parameters, as soon as the Slave pairs with a Master the Slave will automatically update itself to the specific Channel ID of the Master. Even if the Slave channel is closed and then re-opened, the Channel ID will still be set to the last Master it paired with. There are very good reasons for this! If your application needs to reset back to wildcards, then simply resend the channel configuration message after the channel is closed.
  3. A Slave that is not paired will not provide regular messages to the host (unlike a Master that always sends messages at the Channel Period)

ANT_TICK and Events

Once a Slave has paired, the status of the connection is continuously updated with events to the host. The events are defined by ANT and are provided in the antdefines.h header file. The ones we care about are shown here:

//////////////////////////////////////////////
// Response / Event Codes
//////////////////////////////////////////////
#define RESPONSE_NO_ERROR                ((UCHAR)0x00)             
#define EVENT_RX_SEARCH_TIMEOUT          ((UCHAR)0x01)             
#define EVENT_RX_FAIL                    ((UCHAR)0x02)             
#define EVENT_RX_FAIL_GO_TO_SEARCH       ((UCHAR)0x08)

These events are forwarded to the application by ant_api through the ANT_TICK message. An ANT_TICK message is just an array of 8 bytes, but each byte is specified to mean something specific, per the definition in ant_api.h:

/*-------------------------------------------------------------------------
ANT_TICK communicates the message period to the application.  

MSG_NAME  MSG_ID     D_0      D_1      D_2     D_3     D_4     D_5     D_6
ANT_TICK   0xFF     EVENT    0xFF     0xFF    0xFF   MISSED  MISSED  MISSED
                    CODE                              MSG #   MSG #   MSG #
                                                      HIGH    MID     LOW
---------------------------------------------------------------------------*/

#define   MESSAGE_ANT_TICK                        (u8)0xFF

#define   ANT_TICK_MSG_ID_INDEX                   (u8)0
#define   ANT_TICK_MSG_EVENT_CODE_INDEX           (u8)1
#define   ANT_TICK_MSG_SENTINEL1_INDEX            (u8)2
#define   ANT_TICK_MSG_SENTINEL2_INDEX            (u8)3
#define   ANT_TICK_MSG_SENTINEL3_INDEX            (u8)4
#define   ANT_TICK_MSG_MISSED_HIGH_BYTE_INDEX     (u8)5
#define   ANT_TICK_MSG_MISSED_MID_BYTE_INDEX      (u8)6
#define   ANT_TICK_MSG_MISSED_LOW_BYTE_INDEX      (u8)7

What is most important is to understand that the ANT_TICK message contains the EVENT_CODE at index [1]. The index is assigned the name ANT_TICK_MSG_EVENT_CODE_INDEX so it is self-documenting. Therefore to get a copy of the current event code in an ANT_TICK message so you can react to it, do this:

u8CurrentEventCodeExample = G_au8AntApiCurrentData[ANT_TICK_MSG_EVENT_CODE_INDEX];

Understanding the ANT_TICK message is absolutely essential, not only for the specific reason of looking at events, but because propagating information like this in an embedded system happens all the time. As a designer, you assign meaning to otherwise meaningless bytes and build an entire protocol on which your system works.

Initialization

To set up the user task, set the ANT channel parameters up with mostly wild card values.

userapp.h

/* Required constants for ANT channel configuration */
#define ANT_CHANNEL_USERAPP             (u8)0                 
#define ANT_SERIAL_LO_USERAPP           (u8)0                 
#define ANT_SERIAL_HI_USERAPP           (u8)0                 
#define ANT_DEVICE_TYPE_USERAPP         (u8)0                 
#define ANT_TRANSMISSION_TYPE_USERAPP   (u8)0                 
#define ANT_CHANNEL_PERIOD_LO_USERAPP   (u8)0x00              
#define ANT_CHANNEL_PERIOD_HI_USERAPP   (u8)0x20              
#define ANT_FREQUENCY_USERAPP           (u8)50                
#define ANT_TX_POWER_USERAPP            RADIO_TX_POWER_0DBM

userapp.c
Expose the global variables required to access the ant_api.c data:

/* Existing variables (defined in other files -- should all contain the "extern" keyword) */
extern AntSetupDataType G_stAntSetupData;                         /* From ant.c */

extern u32 G_u32AntApiCurrentDataTimeStamp;                       /* From ant_api.c */
extern AntApplicationMessageType G_eAntApiCurrentMessageClass;    /* From ant_api.c */
extern u8 G_au8AntApiCurrentData[ANT_APPLICATION_MESSAGE_BYTES];  /* From ant_api.c */

Also add some debug counters that will be useful to see how the system is behaving:

/************************************************************************
Global variable definitions with scope limited to this local application.
Variable names shall start with "UserApp_" and be declared as static.
*************************************************************************/
static u32 UserApp_u32DataMsgCount = 0;   /* ANT_DATA packets received */
static u32 UserApp_u32TickMsgCount = 0;   /* ANT_TICK packets received */

Update UserAppInitialize to show the welcome message and configure the channel:

void UserAppInitialize(void)
{
  u8 au8WelcomeMessage[] = "ANT SLAVE DEMO";
  u8 au8Instructions[] = "B0 toggles radio";
  
  /* Clear screen and place start messages */
  LCDCommand(LCD_CLEAR_CMD);
  LCDMessage(LINE1_START_ADDR, au8WelcomeMessage); 
  LCDMessage(LINE2_START_ADDR, au8Instructions); 

  /* Start with LED0 in RED state = channel is not configured */
  LedOn(RED);
  
 /* Configure ANT for this application */
  G_stAntSetupData.AntChannel          = ANT_CHANNEL_USERAPP;
  G_stAntSetupData.AntSerialLo         = ANT_SERIAL_LO_USERAPP;
  G_stAntSetupData.AntSerialHi         = ANT_SERIAL_HI_USERAPP;
  G_stAntSetupData.AntDeviceType       = ANT_DEVICE_TYPE_USERAPP;
  G_stAntSetupData.AntTransmissionType = ANT_TRANSMISSION_TYPE_USERAPP;
  G_stAntSetupData.AntChannelPeriodLo  = ANT_CHANNEL_PERIOD_LO_USERAPP;
  G_stAntSetupData.AntChannelPeriodHi  = ANT_CHANNEL_PERIOD_HI_USERAPP;
  G_stAntSetupData.AntFrequency        = ANT_FREQUENCY_USERAPP;
  G_stAntSetupData.AntTxPower          = ANT_TX_POWER_USERAPP;
  
  /* If good initialization, set state to Idle */
  if( AntChannelConfig(ANT_SLAVE) )
  {
    /* Channel is configured, so change LED to yellow */
    LedOff(RED);
    LedOn(YELLOW);
    UserApp_StateMachine = UserAppSM_Idle;
  }
  else
  {
    /* The task isn't properly initialized, so shut it down and don't run */
    LedBlink(RED, LED_4HZ);
    UserApp_StateMachine = UserAppSM_Error;
  }

} /* end UserAppInitialize() */

The user_app state machine

Managing an ANT slave channel offers new challenges because the task must be able to react to different conditions depending on the ANT radio state. Trying to manage everything in a single state would be difficult. The state diagram below shows how the system will progress through four mainstates. To keep things as clear as possible, the conditions that hold a state are not explicitly shown.

AntSlaveStateMachine

We can consider both the Idle and ChannelOpen states “steady states” since the radio is in a known state at those times and we expect a button press or other action to change states. That is not to say that things are not happening. In the ChannelOpen state, the system will continually be reading messages, monitoring the channel, and updating the LCD. WaitChannelOpen and WaitChannelClose are transitory states because we should never be there for more than about one second while ANT opens or closes a channel. Therefore both these states use a timeout counter to ensure the system does not get stuck while waiting for the external device to do something.

Hardly any of the possible problems that could come up are addressed in this example code. A robust system would handle every error case, provide debugging information, and do its best to get the radio back to a known, working state if anything went wrong. Of course that would require substantially more code and testing — something you would be expected to do if this were a commercial product.

To implement the new states, copy the following framework into user_app.c and do not forget to add the required function prototypes in user_app.h for each new state.

Idle State
This state looks for BUTTON0. When pressed, it queues OpenChannel, sets up the LEDs for the next state, initializes the timeout counter and sets the state machine to execute WaitChannelOpen on the next cycle. In most cases, you always set up the static conditions for the next state as you exit out of the current state.

static void UserAppSM_Idle(void)
{
  /* Look for BUTTON 0 to open channel */
  if(WasButtonPressed(BUTTON0))
  {
    /* Got the button, so complete one-time actions before next state */
    ButtonAcknowledge(BUTTON0);
    
    /* Queue open channel and change LED0 from yellow to blinking green to indicate channel is opening */
    AntOpenChannel();

    LedOff(YELLOW);
    LedBlink(GREEN, LED_2HZ);
    
    /* Set timer and advance states */
    UserApp_u32Timeout = G_u32SystemTime1ms;
    UserApp_StateMachine = UserAppSM_WaitChannelOpen;
  }
} /* end UserAppSM_Idle() */

WaitChannelOpen State
This state allows the system time to open the ANT channel. When AntRadioStatus returns ANT_OPEN the state can advance. Since the system is waiting on an external event, UserApp_u32Timeout is monitored and will kick the task back to Idle if the timeout occurs.

static void UserAppSM_WaitChannelOpen(void)
{
  /* Monitor the channel status to check if channel is opened */
  if(AntRadioStatus() == ANT_OPEN)
  {
    LedOn(GREEN);
    UserApp_StateMachine = UserAppSM_ChannelOpen;
  }
  
  /* Check for timeout */
  if( IsTimeUp(&UserApp_u32Timeout, TIMEOUT_VALUE) )
  {
    AntCloseChannel();
    LedOff(GREEN);
    LedOn(YELLOW);
    UserApp_StateMachine = UserAppSM_Idle;
  }
} /* end UserAppSM_WaitChannelOpen() */

ChannelOpen State
The most interesting part of this application is when the channel is open. To get ready for all of the functionality of this state, add the following variables:

  static u8 u8LastState = 0xff;
  static u8 au8TickMessage[] = "EVENT x\n\r";  /* "x" at index [6] will be replaced by the current code */
  static u8 au8DataContent[] = "xxxxxxxxxxxxxxxx";
  static u8 au8LastAntData[ANT_APPLICATION_MESSAGE_BYTES] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
  static u8 au8TestMessage[] = {0, 0, 0, 0, 0xA5, 0, 0, 0};
  bool bGotNewData;

The ChannelOpen state has two conditions for exit, the first being a button press:

  /* Check for BUTTON0 to close channel */
  if(WasButtonPressed(BUTTON0))
  {
    /* Got the button, so complete one-time actions before next state */
    ButtonAcknowledge(BUTTON0);
    
    /* Queue close channel, initialize the u8LastState variable and change LED to blinking green to indicate channel is closing */
    AntCloseChannel();
    u8LastState = 0xff;
    LedOff(YELLOW);
    LedOff(BLUE);
    LedBlink(GREEN, LED_2HZ);
    
    /* Set timer and advance states */
    UserApp_u32Timeout = G_u32SystemTime1ms;
    UserApp_StateMachine = UserAppSM_WaitChannelClose;
  } /* end if(WasButtonPressed(BUTTON0)) */

Depending on the Slave channel configuration, a Slave will timeout searching for a Master after a certain amount of time and automatically close the channel. The default timeout is about 30 seconds. Regardless, the code can handle this with AntRadioStatus().

  /* A slave channel can close on its own, so explicitly check channel status */
  if(AntRadioStatus() != ANT_OPEN)
  {
    LedBlink(GREEN, LED_2HZ);
    LedOff(BLUE);
    u8LastState = 0xff;
    
    UserApp_u32Timeout = G_u32SystemTime1ms;
    UserApp_StateMachine = UserAppSM_WaitChannelClose;
  } /* if(AntRadioStatus() != ANT_OPEN) */

Lastly, the state must process the ANT messages that will always be arriving at the message period. Each data message (Broadcast or Acknowledged) results in data being added to the ANT data buffer, and also an ANT_TICK with a RESPONSE_NO_ERROR event. However, if a message is missed, there is no new data message but there is still an ANT_TICK message with EVENT_RX_FAIL as the code. The framework is set up to be ready to code the handling of these messages. Notice that we have added the debug message counters to track the number of ANT_DATA and ANT_TICK messages received.

  /* Always check for ANT messages */
  if( AntReadData() )
  {
     /* New data message: check what it is */
    if(G_eAntApiCurrentMessageClass == ANT_DATA)
    {
      UserApp_u32DataMsgCount++;
    } /* end if(G_eAntApiCurrentMessageClass == ANT_DATA) */

    else if(G_eAntApiCurrentMessageClass == ANT_TICK)
    {
      UserApp_u32TickMsgCount++;
    } /* end else if(G_eAntApiCurrentMessageClass == ANT_TICK) */
  } /* end AntReadData() */

WaitChannelClose State
This state allows the system time to close the ANT Channel. When AntRadioStatus() returns ANT_CLOSED the state can return to Idle. Since the system is waiting on an external event, UserApp_u32Timeout is monitored and will kick the task to error if the timeout occurs.

static void UserAppSM_WaitChannelClose(void)
{
  /* Monitor the channel status to check if channel is closed */
  if(AntRadioStatus() == ANT_CLOSED)
  {
    LedOff(GREEN);
    LedOn(YELLOW);

    UserApp_StateMachine = UserAppSM_Idle;
  }
  
  /* Check for timeout */
  if( IsTimeUp(&UserApp_u32Timeout, TIMEOUT_VALUE) )
  {
    LedOff(GREEN);
    LedOff(YELLOW);
    LedBlink(RED, LED_4HZ);

    UserApp_StateMachine = UserAppSM_Error;
  }
    
} /* end UserAppSM_WaitChannelClose() */

Build the code to make sure there are no errors. Test that ChannelOpen works as expected when a channel is open with no Master sending in ANTware, but also when the Master channel is not broadcasting so the system times out.

ANT_TICK Processing

Now we add some intelligence to look into the ANT_TICK messages and respond to them appropriately. All we want to do is update the state of the LEDs and for debugging we will print out the event that occurred. Only new events will be displayed otherwise we will get way too many event messages that are not useful in this case.

    else if(G_eAntApiCurrentMessageClass == ANT_TICK)
    {
      UserApp_u32TickMsgCount++;

      /* Look at the TICK contents to check the event code and respond only if it's different */
      if(u8LastState != G_au8AntApiCurrentData[ANT_TICK_MSG_EVENT_CODE_INDEX])
      {
        /* The state changed so update u8LastState and queue a debug message */
        u8LastState = G_au8AntApiCurrentData[ANT_TICK_MSG_EVENT_CODE_INDEX];
        au8TickMessage[6] = HexToASCIICharUpper(u8LastState);
        DebugPrintf(au8TickMessage);

        /* Parse u8LastState to update LED status */
        switch (u8LastState)
        {
          /* If we are synced with a device, blue is solid */
          case RESPONSE_NO_ERROR:
          {
            LedOff(GREEN);
            LedOn(BLUE);
            break;
          }

          /* If we are paired but missing messages, blue blinks */
          case EVENT_RX_FAIL:
          {
            LedOff(GREEN);
            LedBlink(BLUE, LED_2HZ);
            break;
          }

          /* If we drop to search, LED is green */
          case EVENT_RX_FAIL_GO_TO_SEARCH:
          {
            LedOff(BLUE);
            LedOn(GREEN);
            break;
          }

          /* If the search times out, the channel should automatically close */
          case EVENT_RX_SEARCH_TIMEOUT:
          {
            DebugPrintf("Search timeout\r\n");
            break;
          }

          default:
          {
            DebugPrintf("Unexpected Event\r\n");
            break;
          }
        } /* end switch (G_au8AntApiCurrentData) */
      } /* end if (u8LastState ...) */
    } /* end else if(G_eAntApiCurrentMessageClass == ANT_TICK) */

Build the code and set a break point on UserApp_u32TickMsgCount++. Run through this several times to understand what is happening with the EVENT_CODES, u8LastState, and the switch. This will show you how halting the host processor will also cause some trouble for ANT since the system will stop processing messages. Set break points in different parts of the switch statement and experiment with turning the Master on and off in ANTware to see how the Slave behaves.

Reading and responding to new data

Now add code to process ANT_DATA. Use bGotNewData to track if the message data is new since there is no need to update repeated data. All 8 data bytes must be tracked and checked, which is why we set up the au8LastAntData[] variable. If there is new data, update it to the LCD and reply back with a message to the Master.

    if(G_eAntApiCurrentMessageClass == ANT_DATA)
    {
      UserApp_u32DataMsgCount++;
      
      /* Check if the new data is the same as the old data and update as we go */
      bGotNewData = FALSE;
      for(u8 i = 0; i < ANT_APPLICATION_MESSAGE_BYTES; i++)
      {
        if(G_au8AntApiCurrentData[i] != au8LastAntData[i])
        {
          bGotNewData = TRUE;
          au8LastAntData[i] = G_au8AntApiCurrentData[i];

          au8DataContent[2 * i] = HexToASCIICharUpper(G_au8AntApiCurrentData[i] / 16);
          au8DataContent[2*i+1] = HexToASCIICharUpper(G_au8AntApiCurrentData[i] % 16); 
        }
      }
      
      if(bGotNewData)
      {
        /* We got new data: show on LCD */
        LCDClearChars(LINE2_START_ADDR, 20); 
        LCDMessage(LINE2_START_ADDR, au8DataContent); 


        /* Update our local message counter and send the message back */
        au8TestMessage[7]++;
        if(au8TestMessage[7] == 0)
        {
          au8TestMessage[6]++;
          if(au8TestMessage[6] == 0)
          {
            au8TestMessage[5]++;
          }
        }
        AntQueueBroadcastMessage(au8TestMessage);

      } /* end if(bGotNewData) */
    } /* end if(G_eAntApiCurrentMessageClass == ANT_DATA) */

Build and run this code and make sure you see the Master's messages updating on the LCD and the Slave's response message appearing in ANTware.
AntMasterGotData

Lastly, write code after the Slave sends the response message to look for a special Master message that starts with 0xA5. If the data packet contains this in byte 0, then look at bytes 1, 2, and 3. Turn on LEDs corresponding to each byte. This shows how easy it is to use the data that is sent in ANT.

      if(bGotNewData)
      {
        ...

        AntQueueBroadcastMessage(au8TestMessage);

        /* Check for a special packet and respond */
        if(G_au8AntApiCurrentData[0] == 0xA5)
        {
          LedOff(LCD_RED);
          LedOff(LCD_GREEN);
          LedOff(LCD_BLUE);
          
          if(G_au8AntApiCurrentData[1] == 1)
          {
            LedOn(LCD_RED);
          }
          
          if(G_au8AntApiCurrentData[2] == 1)
          {
            LedOn(LCD_GREEN);
          }

          if(G_au8AntApiCurrentData[3] == 1)
          {
            LedOn(LCD_BLUE);
          }
        }

      } /* end if(bGotNewData) */

AntSlaveDemo

You now have essentially everything you need to run an ANT wireless ultra low power embedded system. There are still a great many things to learn as far as designing and implementing a product, but much of that will depend on your specific implementation. The following exercises are good first steps to fortifying what you have learned and will let you build your experience with ANT:

  1. Send the full data string out the serial port for logging and display
  2. Show the received signal strength indicator value
  3. Have setable channel ID, period and frequency via the debug port or the user interface on the development board
  4. Offer scanning channel which accesses extended data to show all ANT devices in the vicinity and report the sender's information
[STATUS: RELEASED. LAST UPDATE: 2016-MAR-03]

*** ENGENUICS WILL NOT BE ABLE TO SHIP PRODUCTS BETWEEN MAY 26 AND JUNE 26, 2017 ***