MCU Coding Tips

Please factor in the following information when writing your MCU code:

Don’t Forget to Call afLib::loop()

This is very basic, but critical: when you write MCU code using afLib, you must call afLib::loop() to give afLib’s state machine time to execute. Without this, afLib won’t run, and your code will not interact properly with the Afero Platform components.

You should definitely make a call to afLib::loop in your code’s loop() method; and you can call it elsewhere, as well, if you need to ensure that afLib is executing frequently enough to support your application.

#include <SPI.h>
#include <iafLib.h>
#include <ArduinoSPI.h>
#include "profile/device-description.h"

// Pin Defines assume Arduino Uno
#define CS_PIN                    10  
#define INT_PIN                   2

boolean shouldGetAttr = true;
iafLib *aflib;

bool attrSetHandler(uint8_t requestId, const uint16_t attributeId, const uint16_t valueLen, const uint8_t *value) {
  // Any application-specific actions
  return true;
}

void attrNotifyHandler(const uint8_t requestId, const uint16_t attributeId, const uint16_t valueLen, const uint8_t *value) {
  // Any application-specific actions
}

void ISRWrapper() {
  // Pass the MCU interrupt on to afLib
  aflib->mcuISR();
}

void setup() {
  Serial.begin(115200); 
  // Initialize afLib
  ArduinoSPI *arduinoSPI = new ArduinoSPI(CS_PIN);
  aflib = iafLib::create(digitalPinToInterrupt(INT_PIN), ISRWrapper, attrSetHandler, attrNotifyHandler, &Serial, arduinoSPI);
}

void loop() {
  // Any application-specific actions

  // CRITICAL: Give afLib processing time by calling aflib->loop() in sketch’s loop()
  aflib->loop();
}

Notes:

  • afLib requires time slices to process attribute writes.
  • aflib->loop() performs small amounts of work every time it’s called. A multi-threaded process is emulated on a single-threaded MCU.
  • Call aflib->loop() as often as possible in your code. If afLib has little to do, it returns quickly. Some complex calls, like setAttribute, require multiple loop() calls to finish.
  • When afLib is finished with an important task, you’ll get a notify event:
    • attrSetHandler - afLib has completed sending data to ASR.
    • attrNotifyHandler - afLib has completed receiving data from ASR.
  • Make sure afLib has time to complete its work. When doing multiple setAttribute calls, ideally wait for attrSetHandler callback before doing additional writes.
  • If you write faster than afLib can process, you can fill its queue and the return code will be:
    afERROR_QUEUE_OVERFLOW   -4  // Queue is full
  • Always check the return code from afLib calls. Check for return of afSUCCESS.

Robust afLib::setAttribute*() Calls

All forms of afLib::setAttribute() provide a return value to indicate whether the set request has been successfully enqueued. Checking this return value and re-trying can make your use of setAttribute*() more robust. Here’s a code snippet from the afBlink example sketch:

void attrNotifyHandler(const uint8_t requestId, const uint16_t attributeId, const uint16_t valueLen, const uint8_t *value) {

    switch (attributeId) {
        // Update the state of the LED based on the actual attribute value.
        case AF_MODULO_LED:
            moduloLEDIsOn = (*value == 0);
            break;

            // Allow the button on Modulo to control our blinking state.
        case AF_MODULO_BUTTON: {
            uint16_t *buttonValue = (uint16_t *) value;
            if (moduleButtonValue != *buttonValue) {
                moduleButtonValue = *buttonValue;
                blinking = !blinking;
                if (aflib->setAttributeBool(AF_BLINK, blinking) != afSUCCESS) {
                    Serial.println("Could not set BLINK");
                }
            }
        }
            break;
// snip //
        default:
            break;
    }
}

void setModuloLED(bool on) {
    if (moduloLEDIsOn != on) {
        int16_t attrVal = on ? LED_ON : LED_OFF; // Modulo LED is active low
        while (aflib->setAttribute16(AF_MODULO_LED, attrVal) != afSUCCESS) {
            Serial.println("Could not set LED");
            aflib->loop();
        }
        moduloLEDIsOn = on;
    }
}

Notes:

  • In both examples, the author compares the return value from setAttribute() to afSUCCESS.
  • In the first case, a non-zero return value triggers the print of useful debugging output.
  • In the second case, the setAttribute() call retries until afSUCCESS is returned. Note also that the author calls afLib::loop() within the re-try loop, to ensure that afLib is getting adequate processing time regardless of any unexpected delays.

Handle Reboot Requests

Your MCU code will often use the attrNotifyHandler() callback to take action when an attribute value has been changed by a call to setAttribute(). Your primary concern will be changes to attributes you define, but your code must also watch for changes to the system attribute AF_SYSTEM_ASR_STATE. This attribute can have one of four values:

  • 0 = Rebooted
  • 1 = Linked
  • 2 = Updating
  • 3 = Update Ready to Apply (Reboot Requested)

AF_SYSTEM_ASR_STATE will have value 3 (Reboot Requested) whenever ASR receives an Over-the-Air (OTA) firmware update. When this happens, ASR sends an update message with that attribute ID and value, and your MCU code is responsible for recognizing this case and triggering a reboot. You code should do this calling setAttribute() for the attribute AF_SYSTEM_COMMAND, with value 1. Here’s a snippet:

#define MODULE_STATE_REBOOTED       0
#define MODULE_STATE_LINKED         1
#define MODULE_STATE_UPDATING       2
#define MODULE_STATE_UPDATE_READY   3

#define MODULE_COMMAND_REBOOT       1

void attrNotifyHandler(const uint8_t requestId, const uint16_t attributeId, const uint16_t valueLen, const uint8_t *value) {

    switch (attributeId) {
        // snip //

        case AF_SYSTEM_ASR_STATE:
            Serial.print("ASR state: ");
            switch (value[0]) {
                case MODULE_STATE_REBOOTED:
                    Serial.println("Rebooted");
                    break;

                case MODULE_STATE_LINKED:
                    Serial.println("Linked");
                    break;

                case MODULE_STATE_UPDATING:
                    Serial.println("Updating");
                    break;

                case MODULE_STATE_UPDATE_READY:
                    Serial.println("Update ready - rebooting");
                    aflib->setAttribute32(AF_SYSTEM_COMMAND, MODULE_COMMAND_REBOOT);
        }
                    break;

                default:
                    break;
            }
            break;

        default:
            break;
    }
}

Our attrNotifyHandler() checks the supplied attribute ID, looking for AF_SYSTEM_ASR_STATE. If we have received an update message for that attribute ID, we check the attribute value. For values 0-2, this example just prints debug information, but if the value is 3 (MODULE_STATE_UPDATE_READY), then we trigger a reboot of ASR by calling setAttribute32() for the AF_SYSTEM_COMMAND attribute, with value MODULE_COMMAND_REBOOT. Note that in this case, the reboot was triggered as soon as requested, but in a more complicated project you might wait until you’ve completed an ongoing operation, or are in some idle state or similar.

Useful Debugging Methods

This isn’t a tip so much as a couple of useful methods that you can copy/paste into your code. You can see these methods used in the afBlink example.

#define ATTR_PRINT_HEADER_LEN     60
#define ATTR_PRINT_MAX_VALUE_LEN  512
#define ATTR_PRINT_BUFFER_LEN     (ATTR_PRINT_HEADER_LEN + ATTR_PRINT_MAX_VALUE_LEN)

char attr_print_buffer[ATTR_PRINT_BUFFER_LEN];

void getPrintAttrHeader(const char *sourceLabel, const char *attrLabel, const uint16_t attributeId, const uint16_t valueLen) {
    memset(attr_print_buffer, 0, ATTR_PRINT_BUFFER_LEN);
    snprintf(attr_print_buffer, ATTR_PRINT_BUFFER_LEN, "%s id: %s len: %05d value: ", sourceLabel, attrLabel, valueLen);
}

void printAttrBool(const char *sourceLabel, const char *attrLabel, const uint16_t attributeId, const uint16_t valueLen, const uint8_t *value) {
    getPrintAttrHeader(sourceLabel, attrLabel, attributeId, valueLen);
    strcat(attr_print_buffer, *value == 1 ? "true" : "false");
    Serial.println(attr_print_buffer);
}

void printAttr8(const char *sourceLabel, const char *attrLabel, const uint8_t attributeId, const uint16_t valueLen, const uint8_t *value) {
    getPrintAttrHeader(sourceLabel, attrLabel, attributeId, valueLen);
    char intStr[6];
    strcat(attr_print_buffer, itoa(*((uint8_t *)value), intStr, 10));
    Serial.println(attr_print_buffer);
}

void printAttr16(const char *sourceLabel, const char *attrLabel, const uint16_t attributeId, const uint16_t valueLen, const uint8_t *value) {
    getPrintAttrHeader(sourceLabel, attrLabel, attributeId, valueLen);
    char intStr[6];
    strcat(attr_print_buffer, itoa(*((uint16_t *)value), intStr, 10));
    Serial.println(attr_print_buffer);
}

void printAttr32(const char *sourceLabel, const char *attrLabel, const uint16_t attributeId, const uint16_t valueLen, const uint8_t *value) {
    getPrintAttrHeader(sourceLabel, attrLabel, attributeId, valueLen);
    char intStr[11];
    strcat(attr_print_buffer, itoa(*((uint32_t *)value), intStr, 10));
    Serial.println(attr_print_buffer);
}

void printAttrHex(const char *sourceLabel, const char *attrLabel, const uint16_t attributeId, const uint16_t valueLen, const uint8_t *value) {
    getPrintAttrHeader(sourceLabel, attrLabel, attributeId, valueLen);
    for (int i = 0; i < valueLen; i++) {
        strcat(attr_print_buffer, String(value[i], HEX).c_str());
    }
    Serial.println(attr_print_buffer);
}

void printAttrStr(const char *sourceLabel, const char *attrLabel, const uint16_t attributeId, const uint16_t valueLen, const uint8_t *value) {
    getPrintAttrHeader(sourceLabel, attrLabel, attributeId, valueLen);
    int len = strlen(attr_print_buffer);
    for (int i = 0; i < valueLen; i++) {
        attr_print_buffer[len + i] = (char)value[i];
    }
    Serial.println(attr_print_buffer);
}

void printAttribute(const char *label, const uint16_t attributeId, const uint16_t valueLen, const uint8_t *value) {
    switch (attributeId) {
        case AF_BLINK:
            printAttrBool(label, "AF_BLINK", attributeId, valueLen, value);
            break;

        case AF_MODULO_LED:
            printAttr16(label, "AF_MODULO_LED", attributeId, valueLen, value);
            break;

        case AF_GPIO_0_CONFIGURATION:
            printAttrHex(label, "AF_GPIO_0_CONFIGURATION", attributeId, valueLen, value);
            break;

        case AF_MODULO_BUTTON:
            printAttr16(label, "AF_MODULO_BUTTON", attributeId, valueLen, value);
            break;

        case AF_GPIO_3_CONFIGURATION:
            printAttrHex(label, "AF_GPIO_3_CONFIGURATION", attributeId, valueLen, value);
            break;

        case AF_BOOTLOADER_VERSION:
            printAttrHex(label, "AF_BOOTLOADER_VERSION", attributeId, valueLen, value);
            break;

        case AF_SOFTDEVICE_VERSION:
            printAttrHex(label, "AF_SOFTDEVICE_VERSION", attributeId, valueLen, value);
            break;

        case AF_APPLICATION_VERSION:
            printAttrHex(label, "AF_APPLICATION_VERSION", attributeId, valueLen, value);
            break;

        case AF_PROFILE_VERSION:
            printAttrHex(label, "AF_PROFILE_VERSION", attributeId, valueLen, value);
            break;

        case AF_SYSTEM_ASR_STATE:
            printAttr8(label, "AF_SYSTEM_ASR_STATE", attributeId, valueLen, value);
            break;

        case AF_SYSTEM_LOW_POWER_WARN:
            printAttr8(label, "AF_ATTRIBUTE_LOW_POWER_WARN", attributeId, valueLen, value);
            break;

        case AF_SYSTEM_REBOOT_REASON:
            printAttrStr(label, "AF_REBOOT_REASON", attributeId, valueLen, value);
            break;

        case AF_SYSTEM_MCU_INTERFACE:
            printAttr8(label, "AF_SYSTEM_MCU_INTERFACE", attributeId, valueLen, value);
            break;

        case AF_SYSTEM_LINKED_TIMESTAMP:
            printAttr32(label, "AF_SYSTEM_LINKED_TIMESTAMP", attributeId, valueLen, value);
            break;
    }
}

Here’s a brief example usage snippet:

void attrNotifyHandler(const uint8_t requestId, const uint16_t attributeId, const uint16_t valueLen, const uint8_t *value) {
    printAttribute("attrNotifyHandler", attributeId, valueLen, value);
    // snip //
}

Watch Your Memory Usage

If you’ve worked much with Arduino, this tip is probably not news to you: Memory is limited, and if your MCU code exceeds available memory, your output can be quite puzzling. We have often encountered bizarre – but seemingly error-free – behavior from a sketch running on Uno, only to find that the same code runs entirely as expected on Mega or Teensy. This is particularly useful to remember in the context of the Useful Debugging Methods provided above; use of those methods requires significant memory, which you should keep in mind.

See Also

Tutorial - Lesson 3