Mistake on this page?
Report an issue in GitHub or email us

Connectivity

The network socket API

New device

The NetworkSocketAPI is designed to make porting new devices as easy as possible and only requires a handful of methods for a minimal implementation.

A new device must implement a NetworkInterface, with the naming convention of DeviceInterface - where Device is a unique name that represents the device or network processor.

The DeviceInterface should also inherit one of the following (unless it is an abstract device):

The NetworkInterface implementation provides the following methods:

    /** Get the internally stored IP address
    /return     IP address of the interface or null if not yet connected
     */
    virtual const char *get_ip_address() = 0;

    /** Get the stack this interface is bound to
    /return     The stack this interface is bound to or null if not yet connected
     */
    virtual NetworkStack * get_stack(void) = 0;

    /** Free NetworkInterface resources
     */
    virtual ~NetworkInterface() {};

NetworkStack

The Network-Socket-API (NSAPI) provides a TCP/UDP API on top of any IP based network interface. With the NSAPI, you can write applications and libraries that use TCP/UDP Sockets without regard to the type of IP connectivity. In addition to providing the TCP/UDP API, the NSAPI includes virtual base classes for the different IP interface types.

Class hierarchy

All network-socket API implementations inherit from two classes: a NetworkStack and a communication specific subclass of NetworkInterface.

NetworkInterface Class

The current NetworkInterface subclasses are CellularInterface, EthernetInterface, MeshInterface and WiFiInterface. Your communication interface is a subclass of one of these, as well as the NetworkStack. For example, the ESP8266Interface inheritance structure looks like this:

Class

There are three pure virtual methods in the NetworkInterface class.

  • connect() - to connect the interface to the network.
  • disconnect() - to disconnect the interface from the network.
  • get_stack() - to return the underlying NetworkStack object.

Each subclass has distinct pure virtual methods. Visit their class references (linked above) to determine those you must implement.

NetworkStack class

NetworkStack provides a common interface that hardware shares. By implementing the NetworkStack, you can use a class as a target for instantiating network sockets.

NetworkStack provides these functions. Look for the function signature like declarator virt-specifier(optional) = 0 to determine which functions are pure virtual and which you must override in your child class.

Errors

Many functions of NetworkStack and NetworkInterface have return types of nsapi_error_t, which is a type used to represent error codes. You can see a list of these return codes here. You can view the integer values the error macros in this file. A negative error code indicates failure, while 0 indicates success.

The connect() method

High-level API calls to an implementation of a network-socket API are identical across networking protocols. The only difference is the interface object constructor and the method through which you connect to the network. For example, a Wi-Fi connection requires an SSID and password, a cellular connection requires an APN and Ethernet doesn't require any credentials. Only the connect method syntax of the derived classes reflects these differences. The intended design allows the user to change out the connectivity of the app by adding a new library and changing the API call for connecting to the network.

Below is a demonstration with the code that sends an HTTP request over Ethernet:

    EthernetInterface net;
    int return_code = net.connect();
    if (return_code < 0)
        printf("Error connecting to network %d\r\n", return_code);

    // Open a socket on the network interface, and create a TCP connection to api.ipify.org
    TCPSocket socket;
    socket.open(&net);
    socket.connect("api.ipify.org", 80);
    char *buffer = new char[256];

    // Send an HTTP request
    strcpy(buffer, "GET / HTTP/1.1\r\nHost: api.ipify.org\r\n\r\n");
    int scount = socket.send(buffer, strlen(buffer));

    // Recieve an HTTP response and print out the response line
    int rcount = socket.recv(buffer, 256);

    // Close the socket to return its memory and bring down the network interface
    socket.close();
    delete[] buffer;

    // Bring down the ethernet interface
    net.disconnect();

To change the connectivity to ESP8266 Wi-Fi, change these lines:

    EthernetInterface net;
    int return_code = net.connect();

To:

    ESP8266Interface net(TX_PIN, RX_PIN);
    int return_code = net.connect("my_ssid", "my_password");

Testing

When adding a new connectivity class, you can use mbed test to verify your implementation.

  1. Make a new Mbed OS project: mbed new [directory_name], where directory name is the name you'd like to use as your testing directory.

  2. Move into that folder: cd [directory_name].

  3. Add your library to the project: mbed add [driver URL] or copy the driver files to this directory.

  4. You need to create a JSON test configuration file using the following format:

    {
        "config": {
            "header-file": {
                "help" : "String for including your driver header file",
                "value" : "\"EthernetInterface.h\""
            },
            "object-construction" : {
                "value" : "new EthernetInterface()"
            },
            "connect-statement" : {
                "help" : "Must use 'net' variable name",
                "value" : "((EthernetInterface *)net)->connect()"
            },
            "echo-server-addr" : {
                "help" : "IP address of echo server",
                "value" : "\"195.34.89.241\""
            },
            "echo-server-port" : {
                "help" : "Port of echo server",
                "value" : "7"
            },
            "tcp-echo-prefix" : {
                "help" : "Some servers send a prefix before echoed message",
                "value" : "\"u-blox AG TCP/UDP test service\\n\""
            }
        }
    }
    

    The configuration values you need to replace are header-file, object-construction and connect-statement.

    • header-file - Replace EthernetInterface.h with the correct header file of your class
    • object-construction - Replace EthernetInterface() with the syntax for your class' object construction.
    • connect-statement - Replace EthernetInterface* with a pointer type to your class and connect() to match your class' connect function signature.
  5. Save the content as a new JSON file.

  6. Run the following command to execute the tests: mbed test -m [MCU] -t [toolchain] -n mbed-os-tests-netsocket* --test-config path/to/config.json

  7. Use -vv for very verbose to view detailed test output.

Case Study: ESP8266 Wi-Fi component

This example ports a driver for the ESP8266 Wi-Fi module to the NSAPI.

Required methods

Because ESP8266 is a Wi-Fi component, choose WiFiInterface as the NetworkworkInterface parent class.

WiFiInterface defines the following pure virtual functions:

  • set_credentials(const char *ssid, const char *pass, nsapi_security_t security).
  • set_channel(uint8_t channel).
  • get_rssi().
  • connect(const char *ssid, const char *pass, nsapi_security_t security, uint8_t channel).
  • connect().
  • disconnect().
  • scan(WiFiAccessPoint *res, nsapi_size_t count).

Additionally, WiFiInterface parent class NetworkInterface introduces NetworkStack *get_stack() as a pure virtual function.

You must also use NetworkStack as a parent class of the interface. You've already explored the pure virtual methods here.

Implementing connect()

Because a Wi-Fi connection requires an SSID and password, you need to implement a connect function that doesn't have these as a parameter.

One of the WiFiInterface pure virtual functions is set_credentials(const char *ssid, const char *pass, nsapi_security_t security). Implement set_credentials to store the SSID and password in private class variables. When you call connect() with no SSID and password, it is assumed that set_credentials has been called.

The next step is to implement this with the connect() method.

This is the first method that needs to interact with the Wi-Fi chip. You need to do some configuration to get the chip in a state where you can open sockets. You need to send some AT commands to the chip to accomplish this.

The AT commands you want to send are:

  1. AT+CWMODE=3 - This sets the Wi-Fi mode of the chip to 'station mode' and 'SoftAP mode', where it acts as a client connection to a Wi-Fi network, as well as a Wi-Fi access point.
  2. AT+CIPMUX=1 - This allows the chip to have multiple socket connections open at once.
  3. AT+CWDHCP=1,1 - To enable DHCP.
  4. AT+CWJAP=[ssid,password] - To connect to the network.
  5. AT+CIFSR - To query your IP address and ensure that the network assigned you one through DHCP.
Sending AT Commands

You can use the AT command parser to send AT commands and parse their responses. The AT command parser operates with a BufferedSerial object that provides software buffers and interrupt driven TX and RX for Serial.

ESP8266Interface uses an underlying interface called ESP8266 to handle the communication with the Wi-Fi modem. ESP8266 maintains an instance of AT command parser to handle communcation with the module. An instance of ESP8266 is in a private ESP8266Interface class variable _esp. In turn, ESP8266 maintains an instance of AT command parser called _parser.

To send AT commands 1-2, there is an ESP8266 method called startup(int mode). Use the AT command parser's send and recv functions to accomplish this.

The necessary code is:


bool ESP8266::startup(int mode)
{
    ...

    bool success =
        && _parser.send("AT+CWMODE=%d", mode)
        && _parser.recv("OK")
        && _parser.send("AT+CIPMUX=1")
        && _parser.recv("OK");

    ...

The parser's send function returns true if the command succesully sent to the Wi-Fi chip. The recv function returns true if you receive the specified text. In the code example above, sending two commands and receiving the expected OK responses determines success.

Return values

So far, our connect method looks something like:

int ESP8266Interface::connect()
{
    if (!_esp.startup(3)) {
        return X;

If this !_esp.startup(3) evaluates to true, something went wrong when configuring the chip, and you should return an error code.

The NSAPI provides a set of error code return values for network operations. They documentation is here.

Of them, the most appropriate is NSAPI_ERROR_DEVICE_ERROR. So replace X in the return statement with NSAPI_ERROR_DEVICE_ERROR.

Finishing

You implemented similar methods to startup in ESP8266 to send AT commands 3-5. Then, you used them to determine the success of the connect() method. You can find the completed implementation here.

Implementing socket_open

The NetworkStack parent class dictates that you implement the functionality of opening a socket. This is the method signature in the interface:

int ESP8266Interface::socket_open(void **handle, nsapi_protocol_t proto)

This method doesn't necessitate any AT commands. The purpose is to create a socket in software and store the information in the handle parameter for use in other socket operations.

The ESP8266 module can only handle 5 open sockets, so you want to ensure that you don't open a socket when none are available. In the header file, use this macro for convenience: #define ESP8266_SOCKET_COUNT 5. Use a private class variable array to keep track of open sockets bool _ids[ESP8266_SOCKET_COUNT]. In socket_open, first iterate over _ids, and look for an element in the array whose value is false.

So far, the method looks like this:

int ESP8266Interface::socket_open(void **handle, nsapi_protocol_t proto)
{
    // Look for an unused socket
    int id = -1;

    for (int i = 0; i < ESP8266_SOCKET_COUNT; i++) {
        if (!_ids[i]) {
            id = i;
            _ids[i] = true;
            break;
        }
    }

    if (id == -1) {
        return NSAPI_ERROR_NO_SOCKET;
    }

    ...

After you've determined that you have an open socket, you want to store some information in the handle parameter. We've created a struct to store information about the socket that will be necessary for network operations:

struct esp8266_socket {
    int id; // Socket ID number
    nsapi_protocol_t proto; // TCP or UDP
    bool connected; // Is it connected to a server?
    SocketAddress addr; // The address that it is connected to
};

Create one of these, store some information in it and then point the handle at it:

int ESP8266Interface::socket_open(void **handle, nsapi_protocol_t proto)
{
    ...
    struct esp8266_socket *socket = new struct esp8266_socket;
    if (!socket) {
        return NSAPI_ERROR_NO_SOCKET;
    }

    socket->id = id; // store the open ID we found above
    socket->proto = proto; // TCP or UDP as specified in parameter
    socket->connected = false; // default state not connected

    *handle = socket;
    return 0; // success

See the full implementation here.

Implementing socket_connect

The NetworkStack parent class dictates that you implement the functionality of connecting a socket to a remote server. This is the method signature in the interface:

int ESP8266Interface::socket_connect(void *handle, const SocketAddress &addr)

In this case, the handle is one that has been assigned in the socket_open method.

You can cast the void pointer to an esp8266_socket pointer. Do this in the body of socket_connect:

int ESP8266Interface::socket_connect(void *handle, const SocketAddress &addr)
{
    struct esp8266_socket *socket = (struct esp8266_socket *)handle;
    _esp.setTimeout(ESP8266_MISC_TIMEOUT);

    const char *proto = (socket->proto == NSAPI_UDP) ? "UDP" : "TCP";
    if (!_esp.open(proto, socket->id, addr.get_ip_address(), addr.get_port())) {
        return NSAPI_ERROR_DEVICE_ERROR;
    }

    socket->connected = true;
    return 0;
}

Focus on this line: !_esp.open(proto, socket->id, addr.get_ip_address(), addr.get_port().

Access the socket ID and socket protocol from the members of esp8266_socket. Access the IP address and port of the server with the SocketAddress addr parameter.

This method sends the AT command for opening a socket to the Wi-Fi module and is defined as follows:

bool ESP8266::open(const char *type, int id, const char* addr, int port)
{
    //IDs only 0-4
    if(id > 4) {
        return false;
    }

    return _parser.send("AT+CIPSTART=%d,\"%s\",\"%s\",%d", id, type, addr, port)
        && _parser.recv("OK");
}

In this instance, use the AT command parser to send AT+CIPSTART=[id],[TCP or UDP], [address] to the module. Expect to receive a response of OK. Only return true if you succesfully send the command AND receive an OK response.

Implementing socket_attach

The NetworkStack parent class dictates that you implement the functionality of registering a callback on state change of the socket. This is the method signature in the interface:

void ESP8266Interface::socket_attach(void *handle, void (*callback)(void *), void *data)

Call the specified callback on state changes, such as when the socket can recv/send/accept successfully.

ESP8266 can have up to five open sockets. You need to keep track of all their callbacks. This struct holds the callback as well as the data of these callbacks. It is stored as a private class variable _cbs:

struct {
    void (*callback)(void *);
    void *data;
} _cbs[ESP8266_SOCKET_COUNT];

The attach method is:

void ESP8266Interface::socket_attach(void *handle, void (*callback)(void *), void *data)
{
    struct esp8266_socket *socket = (struct esp8266_socket *)handle;    
    _cbs[socket->id].callback = callback;
    _cbs[socket->id].data = data;
}

Store the information in the _cbs struct for use on state changes. There is a method event() to call socket callbacks. It looks like this:

void ESP8266Interface::event() {
    for (int i = 0; i < ESP8266_SOCKET_COUNT; i++) {
        if (_cbs[i].callback) {
            _cbs[i].callback(_cbs[i].data);
        }
    }
}

Look for sockets that have callbacks. Then, call them with the specified data!

Know when to trigger these events. You've used the ESP8266 class object, _esp, to attach a callback on a Serial RX event like so: _esp.attach(this, &ESP8266Interface::event). The _esp attach function creates _serial.attach(func), which attaches the function to the underlying UARTSerial RX event. Whenever the radio receives something, consider that a state change, and invoke any attach callbacks. A common use case is to attach socket_recv to a socket, so the socket can receive data asynchronously without blocking.

Testing

  • Make a new Mbed project - mbed new esp8266-driver-test.
  • Move into project folder - cd esp8266-driver-test.
  • Add ESP8266 driver - mbed add esp8266-driver.
  • Make a configuration file called esp8266_config.json with the following contents:
{
    "config": {
        "header-file": {
            "help" : "String for including your driver header file",
            "value" : "\"ESP8266Interface.h\""
        },
        "object-construction" : {
            "value" : "new ESP8266Interface(D1, D0)"
        },
        "connect-statement" : {
            "help" : "Must use 'net' variable name",
            "value" : "((ESP8266Interface *)net)->connect(\"my_ssid\", \"my_password\")"
        },
        "echo-server-addr" : {
            "help" : "IP address of echo server",
            "value" : "\"195.34.89.241\""
        },
        "echo-server-port" : {
            "help" : "Port of echo server",
            "value" : "7"
        },
        "tcp-echo-prefix" : {
            "help" : "Some servers send a prefix before echoed message",
            "value" : "\"u-blox AG TCP/UDP test service\\n\""
        }
    }
}
  • Run tests - mbed test -m [mcu] -t [toolchain] -n mbed-os-tests-netsocket* --test-config esp8266_config.json.
  • View test results:
mbedgt: test suite report:
+--------------+---------------+--------------------------------------------+--------+--------------------+-------------+
| target       | platform_name | test suite                                 | result | elapsed_time (sec) | copy_method |
+--------------+---------------+--------------------------------------------+--------+--------------------+-------------+
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-connectivity       | OK     | 32.24              | shell       |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-gethostbyname      | OK     | 24.01              | shell       |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-ip_parsing         | OK     | 14.31              | shell       |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-socket_sigio       | OK     | 29.23              | shell       |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-tcp_echo           | OK     | 51.39              | shell       |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-tcp_hello_world    | OK     | 21.03              | shell       |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-udp_dtls_handshake | OK     | 19.65              | shell       |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-udp_echo           | OK     | 23.22              | shell       |
+--------------+---------------+--------------------------------------------+--------+--------------------+-------------+
mbedgt: test suite results: 8 OK
mbedgt: test case report:
+--------------+---------------+--------------------------------------------+----------------------------------------+--------+--------+--------+--------------------+
| target       | platform_name | test suite                                 | test case                              | passed | failed | result | elapsed_time (sec) |
+--------------+---------------+--------------------------------------------+----------------------------------------+--------+--------+--------+--------------------+
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-connectivity       | Bringing the network up and down       | 1      | 0      | OK     | 7.61               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-connectivity       | Bringing the network up and down twice | 1      | 0      | OK     | 10.74              |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-gethostbyname      | DNS literal                            | 1      | 0      | OK     | 0.09               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-gethostbyname      | DNS preference literal                 | 1      | 0      | OK     | 0.1                |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-gethostbyname      | DNS preference query                   | 1      | 0      | OK     | 0.13               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-gethostbyname      | DNS query                              | 1      | 0      | OK     | 0.15               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-ip_parsing         | Hollowed IPv6 address                  | 1      | 0      | OK     | 0.06               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-ip_parsing         | Left-weighted IPv4 address             | 1      | 0      | OK     | 0.07               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-ip_parsing         | Left-weighted IPv6 address             | 1      | 0      | OK     | 0.06               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-ip_parsing         | Null IPv4 address                      | 1      | 0      | OK     | 0.04               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-ip_parsing         | Null IPv6 address                      | 1      | 0      | OK     | 0.05               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-ip_parsing         | Right-weighted IPv4 address            | 1      | 0      | OK     | 0.06               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-ip_parsing         | Right-weighted IPv6 address            | 1      | 0      | OK     | 0.06               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-ip_parsing         | Simple IPv4 address                    | 1      | 0      | OK     | 0.05               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-ip_parsing         | Simple IPv6 address                    | 1      | 0      | OK     | 0.04               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-socket_sigio       | Socket Attach Test                     | 1      | 0      | OK     | 2.04               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-socket_sigio       | Socket Detach Test                     | 1      | 0      | OK     | 6.26               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-socket_sigio       | Socket Reattach Test                   | 1      | 0      | OK     | 1.36               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-tcp_echo           | TCP echo                               | 1      | 0      | OK     | 6.24               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-tcp_hello_world    | TCP hello world                        | 1      | 0      | OK     | 7.34               |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-udp_dtls_handshake | UDP DTLS handshake                     | 1      | 0      | OK     | 5.9                |
| K64F-GCC_ARM | K64F          | mbed-os-tests-netsocket-udp_echo           | UDP echo                               | 1      | 0      | OK     | 9.75               |
+--------------+---------------+--------------------------------------------+----------------------------------------+--------+--------+--------+--------------------+
mbedgt: test case results: 22 OK
mbedgt: completed in 217.24 sec

CellularInterface

This section guidelines and details porting a cellular device driver to Mbed OS. It first describes the building blocks of your new cellular interface and then gives step-by-step instructions on how to port.

Quick peek

You can implement a cellular network interface in different ways depending on your requirements and physical setup. For example:

Case 1: An implementation using Mbed OS provided network stacks (PPP mode)

  • Pros
    • A well-established network stack with full Mbed OS support.
    • Simple operation and implementation because the inherent network stack provides all socket APIs.
    • Needs less maintenance because the IP stack handles the bulk of the work in data mode. Command mode is turned off as soon as the device enters data mode.
  • Cons
    • Heavier memory consumption.
    • Bigger footprint on flash.
    • Multiplexing command mode and data mode is not yet available.

Case 2: An implementation using on-chip network stacks (AT only mode)

  • Pros
    • Lighter memory footprint.
    • Lighter flash footprint.
  • Cons
    • Needs to provide a chip-specific interface between AT-sockets and Mbed OS NSAPI sockets.
    • Subtle variations in different on-chip network stacks and NSAPI implementations make maintenance difficult and require more testing.
    • Limited capabilities in some instances.

Case 3: Modem present on target board

  • This refers to the case when the cellular modem is bundled with the target board.
  • Target board must provide an implementation of the onboard_modem_API. For example, the target port for u-blox C027 Mbed Enabled IoT starter kit provides an implementation of onboard_modem_api.
  • Following Mbed OS conventions, drivers for on-board modules may become part of the Mbed OS tree.
  • OnboardCellularInterface ties together onboard_modem_api.h with the generic PPPCellularInterface to provide a complete driver. At present, only UART connection type is handled.

Case 4: Modem attached as a daughter board (Arduino shield)

  • This refers to the case when the cellular modem comes as a plug-in module or an external shield (for example, with an Arduino form factor).
  • Following Mbed OS conventions, drivers for plug-in modules come as a library with an application. For example, they are not part of the Mbed OS tree.
  • If the port inherits from the generic modem driver that Arm Mbed OS, the structure might look like this:

No matter your setup, Mbed OS provides ample framework. You can list common infrastructure shared between above-mentioned cases as:

a) Onboard modem API

Only valid for onboard modem types. In other words, Case 3 is applicable. A hardware abstraction layer is between a cellular modem and an Mbed OS cellular driver. This API provides basic framework for initializing and uninitializing hardware, as well as turning the modem on or off. For example:

/** Sets the modem up for powering on
 *  modem_init() will be equivalent to plugging in the device, i.e.,
 *  attaching power or serial port.
 *  Layout of modem_t is implementation dependent
 */
void modem_init(modem_t *obj);

b) A device type file handle

We have enhanced the existing FileHandle API to make it more usable for devices - it now supports nonblocking operation, SIGIO-style event notification and polling (see below). This makes a cellular interface implementation independent of underlying physical interface between the cellular modem and MCU, for example Serial UART, USB and so on.

FileHandle _fh;

In case of a UART type of device, Mbed OS provides an implementation of serial device type FileHandle with software buffering.

FileHandle * _fh = new UARTSerial(TX_PIN, RX_PIN, BAUDRATE);

UARTSerial replaces Serial (which is a file handle not suitable for background use and which doesn't provide buffering) and BufferedSerial (an external library class which does not use the FileHandle abstraction).

c) An AT command parser

An AT command parser that takes in a file handle and subsequently reads and writes to the user provided file handle.

ATCmdParser *_at = new ATCmdParser(_fh);

d) Polling mechanism for file handles

A mechanism to multiplex input and output over a set of file handles (file descriptors). poll() examines every file handle provided for any events registered for that particular file handle.

/**
* Where fhs is an array of pollfh structs carrying FileHandle(s) and bit mask of events.
* nhfs is the number of file handles.
* timout is the amount of time to block poll, i.e., waiting for an event
*/
int poll(pollfh fhs[], unsigned nfhs, int timeout);

e) PPP interface for network stacks

Only valid when Case 1 is applicable. This provides an interface for cellular drivers to underlying framework provided by the network stack. This in effect means that the driver itself does not depend on a certain network stack. In other words, it talks to any network stack providing this standard PPP interface. For example:

/** Connect to a PPP pipe
 *
 *  @param stream       Pointer to a device type file handle (descriptor)
 *  @param status_cb    Optional, user provided callback for connection status
 *  @param uname        Optional, username for the connection
 *  @param pwd          Optional, password for the connection
 *
 *  @return             0 on success, negative error code on failure
 */
nsapi_error_t nsapi_ppp_connect(FileHandle *stream, Callback<void(nsapi_error_t)> status_cb=0, const char *uname=0, const char *pwd=0);

The application activating the appropriate network stack feature, and ensuring it has PPP enabled via JSON config, determines which network stack is used for PPP modems. As of Mbed OS 5.5, LWIP provides IPv4 over PPP, but not IPv6. Nanostack does not provide PPP.

Step-by-step porting process

Providing onboard modem API

Only valid when Case 3 is applicable.

  1. Update mbed-os/targets/targets.json This file defines all the target platforms that Mbed OS supports. If Mbed OS supports your specific target, an entry for your target is in this file. Define a global macro in your target description that tells the build system that your target has a modem and the data connection type is attached with MCU.

For example,

    "MY_TARGET_007": {
        "supported_form_factors": ["ARDUINO"],
        "core": "Cortex-M3",
        "supported_toolchains": ["ARM", "uARM", "GCC_ARM", "GCC_CR", "IAR"],
        "extra_labels": ["LABEL", "ANOTHER_LABEL"],
        "config": {
            "modem_is_on_board": {
                "help": "Value: Tells the build system that the modem is on-board as oppose to a plug-in shield/module.",
                "value": 1,
                "macro_name": "MODEM_ON_BOARD"
            },
            "modem_data_connection_type": {
                "help": "Value: Defines how the modem is wired up to the MCU, e.g., data connection can be a UART or USB and so forth.",
                "value": 1,
                "macro_name": "MODEM_ON_BOARD_UART"
            }
        },
        "macros": ["TARGET_007"],
        "inherits": ["TargetBond"],
        "device_has": ["ETHERNET", "SPI"],
        "device_name": "JamesBond"
    },
  1. Use standard pin names. A standard naming conventions for pin names is required for standard modem pins in your target's 'targets/TARGET_FAMILY/YOUR_TARGET/PinNames.h'. An example is shown below for full UART capable modem. If any of these pins is not connected physically, mark it 'NC'. Also indicate pin polarity.
typedef enum {

	MDMTXD = P0_15, // Transmit Data
	MDMRXD = P0_16, // Receive Data
	MDMCTS = P0_17, // Clear to Send
	MDMDCD = P0_18, // Data Carrier Detect
	MDMDSR = P0_19, // Data Set Ready
	MDMDTR = P0_20, // Data Terminal Ready
	MDMRI  = P0_21, // Ring Indicator
	MDMRTS = P0_22, // Request to Send

} PinName;

#define ACTIVE_HIGH_POLARITY    1
#define ACTIVE_LOW_POLARITY     0

#define MDM_PIN_POLARITY            ACTIVE_HIGH_POLARITY

The current implementation does not use all pins, but you must define all of them.

  1. Implement onboard_modem_api.h Provide an implementation of onboard_modem_api.h. We provide an example implementation.
Providing module modem API

Only valid when Case 4 is applicable.

  • If the modem is already ready to use via the UART, it may be possible to use UARTCellularInterface directly. Just pass its constructor the necessary pin information for the module connected to your board.

  • If you require custom power and reset controls, create a custom class derived from UARTCellularInterface, which overrides the protected modem_init() methods.

  • If using a different connection type, you must provide access to the connection by implementing the FileHandle API, and then you can pass your file handle for that connection to PPPCellularInterface. Either use it directly, or derive from it, and pass a file handle to its constructor in the same manner as UARTCellularInterface.

Providing an implementation using on-chip network stacks (AT only mode)

Only valid when Case 1 is applicable.

  • This is the most complex case - the bulk of the work is implementing the NSAPI socket and network interfaces. The driver implementation derives from CellularBase to provide both the NetworkInterface API and the standard cellular API. Further layering to abstract connection type may be appropriate, as for the PPP case.

  • Use a file handle, such as UARTSerial, to provide the raw data connection; then you can use ATCmdParser to handle connection logic and the data flow of the socket API, assuming that you use AT commands to control the sockets.

  • An onboard implementation can use onboard_modem_api.h in the same manner as a PPP driver to access power controls - this could be shared with a PPP implementation.

Port verification testing

Once you have your target and driver port ready, you can verify your implementation by running port verification tests on your system. You must have mbed-greentea installed for this.

  • For onboard modem types:

    1. Copy contents of this folder in your implementation directory. For example, netsocket/cellular/YOUR_IMPLEMENTATION/TESTS/unit_tests/default/.
    2. Rename OnboardCellularInterface everywhere in the main.cpp with your Class. (This could be a derived class from already provided APIs, as this subsection mentions.)
    3. Make an empty test application with the fork of mbed-os where your implementation resides.
    4. Create a .json file in the root directory of your application, and copy the contents of template_mbed_app.txt into it.
    5. Now from the root of your application, enter this command:
    $ mbed test --compile-list
    
    1. Look for the name of of your test suite matching to the directory path.
    2. Run tests with the command:
    mbed test -n YOUR_TEST_SUITE_NAME
    

For more information on the mbed-greentea testing suite, please visit its documentation.

Porting new RF driver for 6LoWPAN Stack

Device drivers are a set of functions for providing PHY layer devices for the 6LoWPAN stack:

  • registering the device.
  • receiving function.
  • a set of device controlling functions.

How Nanostack runs inside Mbed OS

The Mbed OS port of Nanostack consist of a few helper modules that provide easier API for users and Platform API for working inside the operating system.

Nanostack inside Mbed OS

  • Mbed Mesh API controls and initializes Nanostack on Mbed OS.
    • Security settings.
    • Channel configuration.
    • Connection and reconnection logic.
  • nanostack-hal-mbed-cmsis-rtos implements Platform API for Mbed OS.
    • An internal event handler is initialized when the stack starts.
    • The event handler is running in its own thread. Not visible for users.
  • NanostackInterface class implements the network stack abstration for the socket layer.

In Mbed OS, Socket API hides the differences between the networking stacks. Users will only use one of its high level APIs:

  • UDPSocket.
  • TCPSocket.
  • TCPServer.

Sockets in Mbed OS

For an example of a simple application using Nanostack, see Example mesh application for Mbed OS.

For more information, see the documentation of the Socket API.

Providing RF driver for Mbed OS applications

For Mbed OS 5, the RF driver implements the NanostackRfPhy API. MeshInterfaceNanostack requires the driver object to be provided when initializing.

NanostackRfPhy

Applications use only LoWPANNDInterface, ThreadInterface or NanostackEthernetInterface directly to set up the network and provide a driver. Rest of the classes provide an abstration between Nanostack and Socket layers of Mbed OS.

See NanostackRfPhy.h for an up-to-date header file and API.

Device Driver API

The 6LoWPAN stack uses Device Driver API to communicate with different physical layer drivers. The 6LoWPAN stack supports different device types for PHY layer and special cases where raw IPv6 datagrams are forwarded to a driver.

The driver must first be registered with the 6LoWPAN stack using the phy_device_driver_s structure defined in section PHY device driver register. This structure defines all the functions that the stack uses when calling a device driver. When the device driver must call the driver API from the stack, it uses the ID number received in the registration phase to distinct between different devices. The following sections define the contents of the driver structures and API interfaces that the driver can use.

How to create a new RF driver

The following steps describe how you can create a new RF driver:

  1. Read through the section Example RF driver. You can use this example code as your starting point.

  2. Fill in the actual transceiver-specific parts of the RF driver.

  3. Register the driver to the 6LoWPAN stack on your application. You can use the example node applications with your driver.

  4. Create a MAC that is suitable for your purpose (802.15.4, Ethernet or serial).

  5. Implement the NanostackRfPhy API.

  6. Check with a RF sniffer tool that you can see RF packets transmitted when you start your device. The 6LoWPAN bootstrap should start with IEEE 802.15.4 Beacon Request packets.

  7. Verify the functionality of your implementation using the Nanostack RF driver test application. (This is currently only available to Mbed OS Partners.)

Worker thread for Mbed OS

Nanostack's interfaces use mutexes for protecting the access from multiple threads. In Mbed OS, the mutex cannot be used from an interrupt. The same applies to all APIs that have internal locking and multithread support. Therefore, each driver must implement their own worker thread to handle the interrupt requests.

Example: Use worked thread and signals from an interrupt

// Signals from interrupt routines
#define SIG_RADIO       1
#define SIG_TIMER       2

// Worker thread
Thread irq_thread;

// Interrupt routines
static void rf_interrupt(void)
{
    irq_thread.signal_set(SIG_RADIO);
}

static void rf_timer_signal(void)
{
    irq_thread.signal_set(SIG_TIMER);
}


// Worker thread
void rf_worker_thread(void)
{
    for (;;) {
        osEvent event = irq_thread.signal_wait(0);
        if (event.status != osEventSignal) {
            continue;
        }

        if (event.value.signals & SIG_RADIO) {
            rf_process_irq();
        }
        if (event.value.signals & SIG_TIMER) {
            rf_process_timer();
        }
    }
}

...
// Somewhere in the initialization code
irq_thread.start(rf_if_irq_task);

RF driver states

Figure 11-1 below shows the basic states of the RF driver.

The basic states in more detail:

State Description
DOWN This is the initial state of the driver. The radio is not used in this state.
RX ACTIVE In this state, the driver has the radio turned on and it can receive a packet or ACK from the radio. The driver can also go from this state to the TX ACTIVE state to transmit a packet.
TX ACTIVE In this state, the driver will try to start a transmission:
1. It must first check that it is not currently busy doing something else.
2. It must check that the channel is free.
3. Finally, it can try to transmit the packet.


SNIFFER This mode can be implemented to enable using the device as a packet sniffer. In this state, the RX is always on and the received packets are sent to the application layer but nothing is transmitted back.
ED SCAN This mode can be implemented to enable energy scan. It enables scanning the energy from channels one by one and nothing else.
ANY STATE This state represents all the states in the state machine.

Note: The driver initialization and registration using the function arm_net_phy_register must be performed before the driver is functional.

For more details on the TX process, see Figure 4-1.

Figure 4-1 RF driver states

scan

In sniffer mode, the device only receives packets, never ACKs or sends them.

The following commands are received as a parameter of the function state_control defined in the struct of type phy_device_driver_s:

  • PHY_INTERFACE_UP
  • PHY_INTERFACE_DOWN
  • PHY_INTERFACE_RESET
  • PHY_INTERFACE_RX_ENERGY_STATE
  • PHY_INTERFACE_SNIFFER_STATE

The following commands are received as a parameter of the function extension defined in the struct of type phy_device_driver_s:

  • PHY_EXTENSION_READ_CHANNEL_ENERGY
  • PHY_EXTENSION_SET_CHANNEL

Figure 4-2 describes the TX process.

The following describes the states in more detail:

State Description
CCA PROCESS In the Clear Channel Assessment (CCA) process, the radio checks that the channel is free before it starts sending anything to avoid collisions. Before starting the actual CCA process, the driver checks that it is not currently receiving a packet from the radio, in which case the CCA process fails.
SEND PACKET In this state, the driver commands the radio to send the data given to the driver as a parameter from the function tx defined in the struct of type phy_device_driver_s.

Figure 4-2 TX process

tx

PHY device driver register

This function is for the dynamic registration of a PHY device driver. The 6LoWPAN stack allocates its own device driver list internally. This list is used when an application creates network interfaces to a specific PHY driver.

To register a PHY driver to the stack:

int8_t arm_net_phy_register(phy_device_driver_s *phy_driver);

PHY data RX API

This is a callback that is a part of the device driver structure and initialized by the stack when a driver is registered.

The driver calls this function to push the received data from a PHY to the stack:

typedef int8_t arm_net_phy_rx_fn(const uint8_t *data_ptr, uint16_t data_len, uint8_t link_quality, int8_t dbm, int8_t driver_id);

PHY data TX done API

This is a callback that is a part of the device driver structure and initialized by the stack when a driver is registered.

The driver calls this function when it has completed a transmit attempt:

typedef int8_t arm_net_phy_tx_done_fn(int8_t driver_id, uint8_t tx_handle, phy_link_tx_status_e status, uint8_t cca_retry, uint8_t tx_retry);

When the PHY device handles the CSMA-CA and auto-retry, the stack needs to know the total number of CCA attempts or TX attempts made in case of error. The stack retries the CCA phase 8 times and the TX attempt 4 times. These may be handled by the hardware.

If the CSMA-CA is handled by the hardware, the cca_retry should return a value larger than 7 if returning PHY_LINK_CCA_FAIL status to the stack. If the total number of CCA retries is less than 8, the stack initiates a new CCA phase.

When the hardware handles the auto-retry mode, the error cases should report the number of TX attempts made in the tx_retry parameter. If the total number of retries is less that 4, the stack initiates a retransmission.

PHY driver structure and enumeration definitions

This section introduces driver API specific structures and enumerations.

PHY TX process status code

This enumeration defines the PHY TX process status code:

typedef enum phy_link_tx_status_e
{
	PHY_LINK_TX_DONE,
	PHY_LINK_TX_DONE_PENDING,
	PHY_LINK_TX_SUCCESS,
	PHY_LINK_TX_FAIL,
	PHY_LINK_CCA_FAIL
} phy_link_tx_status_e;
Parameter Description
TX_DONE TX process is Ready and ACK RX.
TX_DONE_PENDING TX process is OK with an ACK pending flag.
TX_SUCCESS MAC TX complete MAC will make a decision to enter a wait ack or TX Done state.
TX_FAIL The link TX process fails.
CCA_FAIL RF link CCA process fails.
PHY interface control types

This enumeration defines the PHY interface control types:

typedef enum phy_interface_state_e
{
	PHY_INTERFACE_RESET,
	PHY_INTERFACE_DOWN,
	PHY_INTERFACE_UP
} phy_interface_state_e;
Parameter Description
RESET Resets a PHY driver and sets it to idle.
DOWN Disables the PHY interface driver (RF radio disabled).
UP Enables the PHY interface driver (RF radio receiver ON).
PHY device driver

This PHY device driver structure comprises the following members:

typedef struct phy_device_driver_s
{
	phy_link_type_e link_type;
	driver_data_request_e data_request_layer;
	uint8_t *PHY_MAC;
	uint16_t phy_MTU;                                               /**< Define MAX PHY layer MTU size. */
	char * driver_description;
	uint16_t phy_MTU;
	uint8_t phy_tail_length;
	uint8_t phy_header_length;
	int8_t (*state_control)(phy_interface_state_e, uint8_t);
	int8_t (*tx)(uint8_t *,uint16_t,uint8_t, data_protocol_e);
	int8_t (*address_write)(phy_address_type_e ,uint8_t *);
	int8_t (*extension)(phy_extension_type_e,uint8_t *);
	const phy_device_channel_page_s *phy_channel_pages;

	//Upper layer callbacks, set with arm_net_phy_init();
	arm_net_phy_rx *phy_rx_cb;
	arm_net_phy_tx_done *phy_tx_done_cb;
	//Virtual upper layer rx/tx functions
	arm_net_virtual_rx *arm_net_virtual_rx_cb;
	arm_net_virtual_tx *arm_net_virtual_tx_cb;
	uint16_t tunnel_type;
} phy_device_driver_s;
Member Description
link_type Defines the device driver type.
data_request_layer Defines the interface Data OUT protocol.
PHY_MAC A pointer to a 48-bit or 64-bit MAC address.
PHY_MTU The size of the maximum transmission unit.
driver_description A short driver-specific description in Null-terminated string format.
phy_MTU The maximum MTU size of the physical layer.
phy_tail_length The tail length used by the PHY driver.
phy_header_length The header length used by the PDU PHY driver.
state_control A function pointer to the interface state control.
tx A function pointer to the interface TX functionality.
address_write A function pointer to the interface address writing (PAN ID, short address).
extension A function pointer to the interface extension control.
phy_channel_pages This pointer must be set only when the interface type is:
NET_INTERFACE_WIFI
NET_INTERFACE_RF_6LOWPAN
NET_INTERFACE_RF_ZIGBEEIP


phy_rx_cb A function pointer to the upper layer RX callback. Must be initialized to NULL, is set by MAC layer.
phy_tx_done_cb A function pointer to the upper layer TX callback. Must be initialized to NULL, is set by MAC layer.
arm_net_virtual_rx_cb A function pointer to the upper layer RX callback. Only needed by a virtual RF driver! Must be initialized to NULL, is set by MAC layer or virtual RF driver.
arm_net_virtual_tx_cb A function pointer to the upper layer tx callback. Only needed by virtual RF driver! Must be initialized to NULL, is set by MAC layer or virtual RF driver
tunnel_type TUN driver type this is only valid when link type is PHY_TUN
PHY device channel page information

This structure defines the PHY device channel page information and comprises the following members:

typedef struct phy_device_channel_page_s
{
	channel_page_e channel_page;
	const phy_rf_channel_configuration_s *rf_channel_configuration;
} phy_device_channel_page_s;
Member Description
channel_page The supported channel page(s).
rf_channel_configuration The used RF configuration for the channel page.

This enumeration defines the PHY device link types:

typedef enum phy_link_type_e
{
	PHY_LINK_ETHERNET_TYPE,
	PHY_LINK_15_4_2_4GHZ_TYPE,
	PHY_LINK_15_4_SUBGHZ_TYPE,
	PHY_LINK_TUN,
	PHY_LINK_SLIP,
} phy_link_type_e;
Parameter Description
ETHERNET_TYPE The standard IEEE 802 Ethernet type.
15_4_2_4GHZ_TYPE The standard 802.15.4 2.4GHz radio.
15_4_SUBGHZ_TYPE The standard 802.15.4 sub-1GHz radio 868/915MHz.
TUN The Linux virtual TUN interface.
SLIP The SLIP interface.
PHY device RF channel configuration

This structure defines the PHY device RF configuration:

typedef struct phy_rf_channel_configuration_s
{
	uint32_t channel_0_center_frequency;
	uint32_t channel_spacing;
	uint32_t datarate;
	uint16_t number_of_channels;
	phy_modulation_e modulation;
} phy_rf_channel_configuration_s;
Member Description
channel_0_center_frequency The first channel center frequency.
channel_spacing The RF channel spacing.
datarate The RF datarate.
number_of_channels The number of supported channels.
modulation The RF modulation method.
PHY device RF modulation methods

This enumeration defines the PHY device RF modulation methods:

typedef enum phy_modulation_e
{
	M_OFDM,
	M_OQPSK,
	M_BPSK,
	M_GFSK,
	M_UNDEFINED
} phy_modulation_e;
Parameter Description
M_OFDM The OFDM modulation method.
M_OQPSK The OQPSK modulation method.
M_BPSK The BPSK modulation method.
M_GFSK The GFSK modulation method.
M_UNDEFINED The RF modulation method undefined.

Example RF driver

The following code example is not a complete driver but shows you how to use the API to create a RF driver.

static uint8_t mac_address[8];
static phy_device_driver_s device_driver;
static int8_t rf_radio_driver_id = -1;

const phy_rf_channel_configuration_s phy_2_4ghz = {2405000000, 5000000, 250000, 16, M_OQPSK};
const phy_rf_channel_configuration_s phy_subghz = {868300000, 2000000, 250000, 11, M_OQPSK};

static phy_device_channel_page_s phy_channel_pages[] = {
	{CHANNEL_PAGE_0, &phy_2_4ghz},
	{CHANNEL_PAGE_0, NULL}
};

int8_t rf_device_register(void)
{
    /* Do some initialization */
    rf_init();
    /* Set pointer to MAC address */
    device_driver.PHY_MAC = mac_address;
    /* Set driver Name */
    device_driver.driver_description = "Example";

    if(subghz_radio) /* Configuration for Sub GHz Radio */
    {
        /*Type of RF PHY is SubGHz*/
        device_driver.link_type = PHY_LINK_15_4_SUBGHZ_TYPE;
        phy_channel_pages[0].channel_page = CHANNEL_PAGE_2;
        phy_channel_pages[0].rf_channel_configuration = &phy_subghz;
    }
    else /* Configuration for 2.4 GHz Radio */
    {
        /*Type of RF PHY is 2.4 GHz*/
        device_driver.link_type = PHY_LINK_15_4_2_4GHZ_TYPE;
        phy_channel_pages[0].channel_page = CHANNEL_PAGE_0;
        phy_channel_pages[0].rf_channel_configuration = &phy_2_4ghz;
    }

    /*Maximum size of payload is 127*/
    device_driver.phy_MTU = 127;
    /*No header in PHY*/
    device_driver.phy_header_length = 0;
    /*No tail in PHY*/
    device_driver.phy_tail_length = 0;

    /*Set up driver functions*/
    device_driver.address_write = &rf_address_write;
    device_driver.extension = &rf_extension;
    device_driver.state_control = &rf_interface_state_control;
    device_driver.tx = &rf_start_cca;
    /*Set supported channel pages*/
    device_driver.phy_channel_pages = phy_channel_pages;
    //Nullify rx/tx callbacks
    device_driver.phy_rx_cb = NULL;
    device_driver.phy_tx_done_cb = NULL;
    device_driver.arm_net_virtual_rx_cb = NULL;
    device_driver.arm_net_virtual_tx_cb = NULL;

    /*Register device driver*/
    rf_radio_driver_id = arm_net_phy_register(&device_driver);

    return rf_radio_driver_id;
}

void rf_handle_rx_end(void)
{
    uint8_t rf_lqi;
    int8_t rf_rssi;
    uint16_t rf_buffer_len;
    uint8_t *rf_buffer;

    /* Get received data */
    rf_buffer_len = rf_get_rf_buffer(rf_buffer);
    if(!rf_buffer_len)
        return;

    /* If waiting for ACK, check here if the packet is an ACK to a message previously sent */

    /* Get link information */
    rf_rssi = rf_get_rssi();
    rf_lqi = rf_get_lqi();

    /* Note: Checksum of the packet must be checked and removed before entering here */

    /* Send received data and link information to the network stack */
    if( device_driver.phy_rx_cb ){
    	device_driver.phy_rx_cb(rf_buffer, rf_buffer_len, rf_lqi, rf_rssi, rf_radio_driver_id);
    }
}

int8_t rf_start_cca(uint8_t *data_ptr, uint16_t data_length, uint8_t tx_handle, data_protocol_e data_protocol)
{
    /*Check if transmitter is busy*/
    if(transmitter_busy)
    {
        /*Return busy*/
        return -1;
    }
    else
    {
        /*Check if transmitted data needs to be ACKed*/
        if(*data_ptr & 0x20)
            need_ack = 1;
        else
            need_ack = 0;
        /*Store the sequence number for ACK handling*/
        tx_sequence = *(data_ptr + 2);

        /* Store date and start CCA process here */
        /* When the CCA process is ready send the packet */
        /* Note: Before sending the packet you need to calculate and add a checksum to it, unless done automatically by the radio */
    }

    /*Return success*/
    return 0;
}

static int8_t rf_interface_state_control(phy_interface_state_e new_state, uint8_t rf_channel)
{
    int8_t ret_val = 0;
    switch (new_state)
    {
        /*Reset PHY driver and set to idle*/
        case PHY_INTERFACE_RESET:
            rf_reset();
            break;
        /*Disable PHY Interface driver*/
        case PHY_INTERFACE_DOWN:
            rf_shutdown();
            break;
        /*Enable PHY Interface driver*/
        case PHY_INTERFACE_UP:
            rf_channel_set(rf_channel);
            rf_receive();
            break;
        /*Enable wireless interface ED scan mode*/
        case PHY_INTERFACE_RX_ENERGY_STATE:
            break;
        /*Enable Sniffer state*/
        case PHY_INTERFACE_SNIFFER_STATE:
            rf_setup_sniffer(rf_channel);
            break;
    }
    return ret_val;
}

static int8_t rf_extension(phy_extension_type_e extension_type, uint8_t *data_ptr)
{
    switch (extension_type)
    {
        /*Control MAC pending bit for Indirect data transmission*/
        case PHY_EXTENSION_CTRL_PENDING_BIT:
        /*Return frame pending status*/
        case PHY_EXTENSION_READ_LAST_ACK_PENDING_STATUS:
            *data_ptr = rf_if_last_acked_pending();
            break;
        /*Set channel, used for setting channel for energy scan*/
        case PHY_EXTENSION_SET_CHANNEL:
            break;
        /*Read energy on the channel*/
        case PHY_EXTENSION_READ_CHANNEL_ENERGY:
            *data_ptr = rf_get_channel_energy();
            break;
        /*Read status of the link*/
        case PHY_EXTENSION_READ_LINK_STATUS:
            *data_ptr = rf_get_link_status();
            break;
    }
    return 0;
}

static int8_t rf_address_write(phy_address_type_e address_type, uint8_t *address_ptr)
{

    switch (address_type)
    {
        /*Set 48-bit address*/
        case PHY_MAC_48BIT:
            /* Not used in this example */
            break;
        /*Set 64-bit address*/
        case PHY_MAC_64BIT:
            rf_set_mac_address(address_ptr);
            break;
        /*Set 16-bit address*/
        case PHY_MAC_16BIT:
            rf_set_short_adr(address_ptr);
            break;
        /*Set PAN Id*/
        case PHY_MAC_PANID:
            rf_set_pan_id(address_ptr);
            break;
    }

    return 0;
}

MAC API porting

Nanostack has a lower level API for the IEEE 802.15.4-2006 MAC standard. This enables developers to support different MACs, be it SW or HW based solution. Nanostack offers SW MAC that you can use when your board does not have 15.4 MAC available.

SW MAC

Nanostack includes an IEEE 802.15.4 based SW MAC class. You can use SW MAC when your board does not support MAC. To use the SW MAC service you must have a working RF driver registered to Nanostack. To create SW MAC, call the following function:

ns_sw_mac_create()

This creates a SW MAC class and sets a callback function to be used by Nanostack.

Note: You must not call ns_sw_mac_create() more than once!

Initializing SW MAC

Deploy SW MAC as follows:

  1. Call arm_net_phy_register() to register the configured RF driver class to Nanostack.
  2. Call ns_sw_mac_create() to create SW MAC with needed list sizes.
    • a sleepy device needs only 1-4 as the size of the device_decription_table_size.
    • the minimum and recommended key_description_table_size for the Thread stack is 4. (2 for 6LoWPAN)
    • the recommended value for key_lookup_size is 1 and for key_usage_size 3.
  3. Call arm_nwk_interface_lowpan_init() to create Nanostack with the created SW MAC class. Nanostack will initialize SW MAC before using it.

Example

See a simple code snippet for creating SW MAC with 16 as neighbour table size with three key descriptions:

int8_t generate_6lowpan_interface(int8_t rf_phy_device_register_id)
{
    mac_description_storage_size_t storage_sizes;
    storage_sizes.device_decription_table_size = 16;
    storage_sizes.key_description_table_size = 3;
    storage_sizes.key_lookup_size = 1;
    storage_sizes.key_usage_size = 3;
    mac_api_t *mac_api = ns_sw_mac_create(rf_phy_device_register_id, &storage_sizes);
    if (!mac_api) {
        tr_error("Mac create fail!");
        return -1;
    }
    return arm_nwk_interface_lowpan_init(mac_api, "6LoWPAN_ROUTER");
}

Enabling FHSS

SW MAC supports FHSS. To enable it, you need to do the following:

  1. Call arm_net_phy_register() to register the configured RF driver class to Nanostack.
  2. Call ns_sw_mac_create() to create SW MAC with needed list sizes.
  3. Call ns_fhss_create() to configure and define the FHSS class.
  4. Call ns_sw_mac_fhss_register() to register FHSS to SW MAC.
  5. Call arm_nwk_interface_lowpan_init() to create Nanostack with the created SW MAC class.

IEEE 802.15.4 MAC sublayer APIs

The stack uses the IEEE 802.15.4 defined MAC management service entity (MLME-SAP) and MAC data service (MCPS-SAP) interfaces. MAC API follows MCPS and MLME primitives defined by the IEEE 802.15.4-2006 standard.

The following primitives are used in MAC layer:

Primitive Description
Request Request made by service user.
Confirm MAC layer response to earlier request.
Indication Indication event from MAC to service user.
Response Service user's response to received indication.

MAC API is defined in the following header files:

  • mac_api.h Main header which defines a transparent MAC API for Nanostack to use.
  • mlme.h Definitions for MLME-SAP primitives.
  • mac_mcps.h Definitions for MCPS-SAP primitives.
  • mac_common_defines.h Definitions for common MAC constants.

MCPS-SAP interface

MCPS-SAP defines 802.15.4 data flow API with the following primitives:

Primitive Description
MCPS-DATA-REQ Data request primitive to MAC.
MCPS-DATA-CONF MAC generated confirmation for ongoing MCPS-DATA-REQ.
MCPS-DATA-IND MAC generated data indication event.
MCPS-PURGE-REQ Cancel ongoing MCPS-DATA-REQ from MAC.
MCPS-PURGE-CONF Confirmation from MAC to MCPS-PURGE-REQ operation.

MLME-SAP interface

MLME-SAP defines a set of different management primitives and this chapter introduces both supported and unsupported primitives in Nanostack.

Supported MLME APIs

MLME-SAP primitives used by Nanostack:

Primitive Description
MLME-BEACON-NOTIFY MAC generated event for received beacons.
MLME-GET-REQ Request information about a specified PAN Information Base (PIB) attribute.
MLME-GET-CONF MAC generated response to MLME-GET-REQ.
MLME-RESET-REQ Request to reset MAC to idle state and clean data queues.
MLME-SCAN-REQ Start MAC scan process. Orphan scan is not supported.
MLME-SCAN-CONF Result of the scan made by MLME-SCAN-REQ.
MLME-COMM-STATUS-IND MAC generated indication about the communications status.
MLME-SET-REQ Request to write data into a specified PIB attribute.
MLME-SET-CONF MAC generated response to MLME-SET-REQ.
MLME-START-REQ Starts or enables MAC with specified options. Nanostack uses this also for RFD device.
MLME-SYNCH-LOSS-IND Indicate syncronization loss from wireless PAN. Only used by SW MAC when FHSS is in use!
MLME-POLL-REQ Request MAC to do data poll to parent.
Unsupported MLME APIs

Unsupported MLME-SAP primitives:

Primitive Support planned Description
MLME-ASSOCIATE-REQ Not yet Start MAC association process.
MLME-ASSOCIATE-CONF Not yet MAC association process confirmation status.
MLME-ASSOCIATE-IND Not yet MAC association indication to indicate the reception of assocation request.
MLME-ASSOCIATE-RES Not yet MAC association response for indication.
MLME-DISASSOCIATE-REQ Not yet MAC disassociation request from service user.
MLME-DISASSOCIATE-IND Not yet MAC disassociation indication event to service user.
MLME-DISASSOCIATE-CONF Not yet MAC disassociation confirmation when the disassociation request is handled.
MLME-GTS-REQ Not yet MAC Guaranteed Time Slot (GTS) request.
MLME-GTS-IND Not yet MAC GTS allocate event indication.
MLME-GTS-CONF Not yet MAC GTS request confirmation.
MLME-ORPHAN-IND Not yet Service user indicated by orphaned device.
MLME-ORPHAN-RES Not yet Service user response to orphan indication event.
MLME-RESET-CONF Yes MAC reset confirmation.
MLME-RX-ENABLE-REQ Yes Enable (or disable) RX receiver for a specified time.
MLME-RX-ENABLE-CONF Yes Confirmation for MLME-RX-ENABLE-REQ.
MLME-START-CONF Yes Confirmation for MLME start request.
MLME-SYNCH-REQ Not yet Request MAC to synchronize with coordinator.

MAC API class introduction

This chapter introduces MAC mesh interface mac_api_s. It is a structure that defines the function callbacks needed by a service user.

The base class defines the functions for two-way communications between an external MAC and service user. The application creates a mac_api_s object by calling the MAC adapter's create function. The newly created object is then passed to Nanostack which initializes its own callback functions by calling mac_api_initialize() function. A service user operates MAC by calling MLME or MCPS primitive functions.

The MAC API class structure mac_api_t is defined as below:

typedef struct mac_api_s {
    //Service user defined initialization function which is called when Nanostack takes MAC into use
    mac_api_initialize              *mac_initialize;
    //MAC adapter function callbacks for MLME & MCPS SAP
    mlme_request                    *mlme_req;
    mcps_data_request               *mcps_data_req;
    mcps_purge_request                  *mcps_purge_req;
    //Service user defined function callbacks
    mcps_data_confirm               *data_conf_cb;
    mcps_data_indication                *data_ind_cb;
    mcps_purge_confirm                  *purge_conf_cb;
    mlme_confirm                        *mlme_conf_cb;
    mlme_indication                     *mlme_ind_cb;
    //MAC extension API for service user
    mac_ext_mac64_address_set           *mac64_set;
    mac_ext_mac64_address_get           *mac64_get;
    mac_storage_decription_sizes_get *mac_storage_sizes_get;
    int8_t                              parent_id;
    uint16_t                            phyMTU;
};
Member Description
mac_initialize MAC initialize function called by Nanostack.
mlme_req MLME request function to use MLME-SAP commands, MAC defines.
mcps_data_req MCPS data request function to use, MAC defines.
mcps_purge_req MCPS purge request function to use, MAC defines.
mcps_data_confirm MCPS data confirm callback function, service user defines.
data_ind_cb MCPS data indication callback function, service user defines.
purge_conf_cb MCPS purge confirm callback function, service user defines.
mlme_conf_cb MLME confirm callback function, service user defines.
mlme_ind_cb MLME indication callback function, service user defines.
mac64_set MAC extension function to set mac64 address.
mac64_get MAC extension function to get mac64 address.
mac_storage_sizes_get Getter function to query data storage sizes from MAC.
parent_id Service user ID used to indentify the MAC service user. Optional.
phyMTU Maximum Transmission Unit (MTU) used by MAC. Standard 802.15.4 MAC must set 127.

MAC API standard extensions

This chapter introduces MAC API standard extensions.

MAC 64-bit address set and get

NanoStack uses 64-bit address set and get. There are two 64-bit addresses available:

  • NVM EUI64.
  • dynamic 64-bit address used at the link layer.

Thread generates a random MAC64 after commissioning. Therefore, MAC and the RF driver must support updating of radio's dynamic 64-bit address anytime.

Address set and get support two different 64-bit addresses:

Address enumeration type Description
MAC_EXTENDED_READ_ONLY A unique EUI64.
MAC_EXTENDED_DYNAMIC Dynamic 64-bit address. Same as EUI64 after boot.
MAC max storage information get

Usually, HW MAC and SW MAC have static keys and neighbour list sizes. Nanostack always asks the max size to limit its neighbour table size. The service user must define the mac_storage_sizes_get() function returning the following values:

  • MAC Device description list size (must be > 1).
  • MAC Key description list size (must be > 1).

Note: The Key description list size must at least 4 if using Thread!

MLME attribute extension

Nanostack uses MLME attribute extensions which have to be ported to the HW MAC adapter. To configure the extensions, use the MLME-SET-REQ command.

Enumeration type Value Description
macLoadBalancingBeaconTx 0xfd Trigger to MAC layer to send a beacon. Called by the load balancer module periodically.
macLoadBalancingAcceptAnyBeacon 0xfe Configure MAC layer to accept beacons from other networks. Enabled by load balancer, default value is False. Value size boolean, true=enable, false=disable.
macThreadForceLongAddressForBeacon 0xff The Thread standard forces beacon source address to have an extended 64-bit address.
Thread Sleepy End Device (SED) keepalive extension

Thread 1.1 stack defines that sleepy end device data poll process must enable neighbour table keepalive functionality as well. When SED finishes data polling succesfully, it updates its parents keepalive value in a neighbour table. A service user at a parent device does not have a standard mechanism to indicate the data polling event. Therefore, the MAC layer must generate an MLME-COMM-STATUS indication callback with status MLME_DATA_POLL_NOTIFICATION.

Enumeration extension for MLME communication status enumeration:

Enumeration type Value Description
MLME_DATA_POLL_NOTIFICATION 0xff Thread requirement for MLME-COMM-STATUS to start indicating the successful data poll events.

HW MAC

To use HW MAC, you need to create an adapter class that links function calls between Nanostack and HW MAC. To create the adapter class, you need to implement the functions defined in the mac_api_s structure. When HW MAC generates an event the adapter must handle it and do a parameter adaptation before calling the correct function from the mac_api_s structure. You may need the same parameter adaptation for requests from Nanostack to HW MAC.

Note: Function calls from Nanostack to HW MAC must be non-blocking in the adapter layer!

Important Information for this Arm website

This site uses cookies to store information on your computer. By continuing to use our site, you consent to our cookies. If you are not happy with the use of these cookies, please review our Cookie Policy to learn how they can be disabled. By disabling cookies, some features of the site will not work.