5. User Guides

5.1. The BLE Framework

Figure 24 depicts the general architectural scheme used.

../_images/046.png

Figure 24 BLE Framework architecture

Using a top-down approach, the layers that build-up the BLE Framework functionality can be identified as the following:

  1. The BLE service framework provides BLE services that the application can use “out-of-the-box”, using simple initialization functions and callbacks for the various BLE service events (like the change of an alert level attribute). The functionality of the BLE service framework is built on top of the Dialog BLE API library. The BLE service API header files can be found under <sdk_root_directory>/sdk/interfaces/ble/services/include. The BLE service callbacks are executed by the application task that uses the BLE service framework.

  2. The Dialog BLE API is a set of functions that can be used to initiate BLE operations or respond to BLE events. The API header files can be found under the path <sdk_root_directory>/sdk/interfaces/ble/api/include. The API functions can be used to send messages (commands or replies to events) to the BLE manager, either by directly calling the BLE manager command handler or by using queues between the application task and the BLE manager task. The BLE API is called in the context of the application.

  3. The BLE manager provides the interface to the BLE functionality of the chip. Application tasks that are based on BLE functionality use the Dialog BLE API to interface with the BLE manager. The BLE manager is a task that stands between the application and the BLE adapter. It uses the BLE adapter to interface with the BLE Stack. The BLE manager uses a Generic Transport Layer (GTL) interface to communicate with the BLE adapter through a command and event queue. The BLE manager is a separate system task.

  4. The BLE adapter is the system task that provides the interface to the BLE Stack. It runs the BLE Stack internal scheduler, receives the commands or the replies to events from the BLE manager, and passes BLE events to the BLE manager. BLE core functionality is implemented by the BLE adapter task.

  5. The BLE Stack is the software stack that implements both the BLE Host and the Low Energy (LE) controller (Link Layer). The BLE Host includes the Logical Link Control and Adaptation Protocol (L2CAP), the Security Manager Protocol (SMP), the Attribute Protocol (ATT), the Generic Attribute Profile (GATT) and the Generic Access Profile (GAP). The BLE Stack API header files, for the DA1469x, can be found under <sdk_root_directory>/sdk/interfaces/ble/stack/da14690/include. The BLE Stack software is run under the BLE adapter’s task context.

5.1.1. Developing BLE Applications

One of the main goals of the SmartSnippets™ DA1469x SDK is to simplify the development of BLE applications and achieve a fast time to market. The SmartSnippets™ DA1469x SDK separates the application logic from the BLE Stack implementation and provides a clean and powerful API to interact with the BLE capabilities of the device. The BLE API provides an easy way to configure the BLE device, start air operations and set up the attribute database of a GATT server. The BLE service API provides access to predefined Bluetooth SIG profiles with the definition of only a few callback functions.

The Proximity Reporter (pxp_reporter) application is the most typical of the BLE applications that are included in the SmartSnippets™ DA1469x SDK. It is a complete and solid example of a BLE application developed on top of the SmartSnippets™ DA1469x SDK. It uses both the Dialog BLE API and the BLE service framework to implement the functionality of a BLE profile.

However, it may not be the simplest example or the best starting point to become familiar with the development of a BLE application from scratch. Instead, there are BLE projects specifically created to serve as starting points for specific BLE applications such as beacons (ble_adv_demo) or specific roles such as a generic peripheral (ble_peripheral) or central (ble_central) device.

The goal of this section is to introduce the various options and examples that exist in the SmartSnippets™ DA1469x SDK which can be used as building blocks for many applications. After a short introduction on where the API header files can be found, each section describes the functionality they implement along with guidance on how they differ from each other. This information is essential when developing a BLE application from scratch.

5.1.2. The BLE API header files

5.1.2.1. Dialog BLE API

All demos and services API can be found in [Ref_04].

In most projects these API header files are symbolically linked to files located in

<sdk_root_directory>/sdk/ble/api/include.

The API functions are declared across several header files depending on their functionality:

  • Common API (ble_common.h): Functions used for operations, not specific to a specific BLE device software component. For example:

Table 15 API Functions of the common BLE device software component

Function

Description

ble_register_app()

Register the application to the BLE Framework so that it can receive BLE event notifications.

ble_enable()

Enable the BLE module.

ble_reset()

Reset the BLE module.

ble_central_start()

Start the device as a BLE central. This is actually a helper function, since it uses API calls ble_enable() and ble_gap_role_set().

ble_peripheral_start()

Start the device as a BLE peripheral. This is also a helper function that uses ble_enable() and ble_gap_role_set().

ble_get_event()

Get a BLE event from the BLE event queue.

ble_has_event()

Check if there is an event pending at the BLE event queue.

ble_handle_event_default()

Used to define handling of events that are not handled by the added services or the application defined handlers.

  • GAP & L2CAP APIs (ble_gap.h/ble_l2cap.h): Covers a wide range of operations, like

    • Device parameters configuration: device role, MTU size, device name exposed in the GAP service attribute, etc.

    • Air operations: Advertise, scan, connect, respond to connection requests, initiate or respond to connection parameters update, etc.

    • Security operations: Initiate and respond to a pairing or bonding procedure, set the security level, unpair, etc.

Table 16 GAP and L2CAP API functions

Function

Description

BLE device configuration

ble_gap_role_get()

Get the GAP role currently set.

ble_gap_role_set()

Set the device GAP roles (central, peripheral, observer, broadcaster).

ble_gap_mtu_size_get()

Get the MTU size currently set.

ble_gap_mtu_size_set()

Set the MTU size to be used in MTU exchange transactions.

ble_gap_channel_map_get()

Get the currently set channel map of the device (the device has to be configured as central).

ble_gap_channel_map_set()

Set the channel map of the device (device has to be configured as central).

ble_gap_address_get()

Get the currently used BD address of the device.

ble_gap_address_set()

Set the BD address of the device.

ble_gap_device_name_get()

Get the device name used in the respective attribute of GAP service.

ble_gap_device_name_set()

Set the device name used in the respective attribute of GAP service.

ble_gap_appearance_get()

Get the appearance used in the respective attribute of GAP service.

ble_gap_appearance_set()

Set the appearance used in the respective attribute of GAP service.

ble_gap_per_pref_conn_params_get()

Get the peripheral preferred connection parameters used in the respective attribute of GAP service.

ble_gap_per_pref_conn_params_set()

Set the peripheral preferred connection parameters used in the respective attribute of GAP service.

ble_gap_get_io_cap()

Get the I/O capabilities of the device.

ble_gap_set_io_cap()

Set the I/O capabilities of the device (combined with the peer’s I/O capabilities, this will determine which pairing algorithm will be used).

ble_gap_data_length_set()

Set the data length to be used for transmission on new connections.

Advertising

ble_gap_adv_start()

Start advertising.

ble_gap_adv_stop()

Stop advertising.

ble_gap_adv_data_set()

Set the Advertising Data and Scan Response Data used.

ble_gap_adv_ad_struct_set()

Set Advertising Data and Scan Response Data using ::gap_adv_ad_struct_t type.

ble_gap_adv_data_get()

Get currently used Advertising Data and Scan Response Data.

ble_gap_adv_intv_get()

Get the currently set advertising interval.

ble_gap_adv_intv_set()

Set the advertising interval (has to be done prior to ble_gap_adv_start()).

ble_gap_adv_chnl_map_get()

Get the advertising channel map currently set.

ble_gap_adv_chnl_map_set()

Set the advertising channel map (has to be done prior to ble_gap_adv_start()).

ble_gap_adv_mode_get()

Get the discoverability mode used for advertising.

ble_gap_adv_mode_set()

Set the discoverability mode used for advertising (has to be done prior to ble_gap_adv_start()).

ble_gap_adv_set_permutation()

Set the order of the primary advertising channels (Bluetooth Core v5.1)

ble_gap_adv_filt_policy_get()

Get the filtering policy used for advertising.

ble_gap_adv_filt_policy_set()

Set the filtering policy used for advertising.

ble_gap_adv_direct_address_get()

Get the peer address used for directed advertising.

ble_gap_adv_direct_address_set()

Set the peer address used for directed advertising (has to be done prior to ble_gap_adv_start()).

Scanning

ble_gap_scan_start()

Start scanning for devices.

ble_gap_scan_stop()

Stop scanning for devices.

Connection management

ble_gap_scan_params_get()

Get the scan parameters used for future connections.

ble_gap_scan_params_set()

Set the scan parameters used for future connections.

ble_gap_connect()

Initiate a direct connection to a device.

ble_gap_connect_ce()

Initiate a direct connection with an application-defined minimum and maximum connection event length

ble_gap_connect_cancel()

Cancel an initiated connection procedure.

ble_gap_disconnect()

Initiate a disconnection procedure on an established link.

ble_gap_peer_version_get()

Get peer’s version on an established connection.

ble_gap_peer_features_get()

Get peer’s features on an established connection.

ble_gap_conn_rssi_get()

Get the RSSI of a connection.

ble_gap_conn_param_update()

Initiate a connection parameter update or update request procedure (depending on the role set and peer’s supported features).

ble_gap_conn_param_update_reply()

Reply to a connection parameter update request.

ble_gap_data_length_set()

Set the data length used for transmission on a specified connection.

ble_gap_phy_get()

Get the transmitter and receiver PHY (default preferred or for a specified connection).

ble_gap_phy_set()

Set PHY used for RX and TX (default or for a given connection).

Security

ble_gap_pair()

Start pairing.

ble_gap_pair_reply()

Respond to a pairing request.

ble_gap_passkey_reply()

Respond to a passkey request.

ble_gap_numeric_reply()

Respond to a numeric comparison request (LE Secure Connections only).

ble_gap_get_sec_level()

Get link security level.

ble_gap_set_sec_level()

Set link security level.

ble_gap_unpair()

Unpair device (will also remove the related bond data from BLE storage).

ble_gap_address_resolve()

Resolve a BD address using the set of IRKs stored in BLE storage.

Power Control

ble_gap_local_tx_power_get()

Get the current and maximum transmit power levels of the local device, on the ACL connection, for the indicated PHY.

ble_gap_remote_tx_power_get()

Get the TX power level used by the remote device, on the ACL connection, for the indicated PHY.

ble_gap_path_loss_report_params_set()

Set the path loss threshold reporting parameters.

ble_gap_path_loss_report_en()

Enable or disable path loss reporting.

ble_gap_tx_power_report_en()

Enable or disable reporting of TX power level in the local and remote device for the ACL connection.

ble_gap_rf_path_compensation_set()

Indicate the RF path gain or loss between the RF transceiver and the antenna contributed by intermediate components.

Helper functions

ble_gap_get_connected()

Get list of connected devices.

ble_gap_get_bonded()

Get list of bonded devices.

ble_gap_get_devices()

Return list of known devices based on filter.

ble_gap_get_device_by_addr()

Get the device object, given the device address.

ble_gap_get_device_by_conn_idx()

Get device object, given the connection index.

ble_gap_is_bonded()

Get bond state of device (by connection index).

ble_gap_is_addr_bonded()

Get bond state of device (by address).

Expert functions

ble_gap_skip_latency()

Temporarily ignore the connection latency.

  • GATT server API (ble_gatts.h): Set up the attribute database, set attribute values, notify/indicate characteristic values, initiate MTU exchanges, respond to write and read requests, etc.

Table 17 GATT server API

Function

Description

ble_gatts_add_service()

Add a new GATT service to the ATT database. Subsequent calls to ble_gatts_add_include(), ble_gatts_add_characteristic() and ble_gatts_add_descriptor() will add attributes to the service added by this call.

ble_gatts_add_include()

Add an included service declaration to the service added by ble_gatts_add_service().

ble_gatts_add_characteristic()

Add a characteristic declaration to the service added by ble_gatts_add_service().

ble_gatts_add_descriptor()

Add a descriptor declaration to the service added by ble_gatts_add_service().

ble_gatts_register_service()

Add to the ATT database all attributes previously added to the service.

ble_gatts_enable_service()

Enable service in database.

ble_gatts_disable_service()

Disable service in database.

ble_gatts_get_characteristic_prop()

Read current characteristic properties and permissions.

ble_gatts_set_characteristic_prop()

Set characteristic properties and permissions.

ble_gatts_get_value()

Get attribute value.

ble_gatts_set_value()

Set attribute value.

ble_gatts_read_cfm()

Confirmation response to an attribute read request.

ble_gatts_write_cfm()

Confirmation response to an attribute write request.

ble_gatts_prepare_write_cfm()

Confirmation response to an attribute prepare write request.

ble_gatts_send_event()

Send a characteristic value notification or indication.

ble_gatts_service_changed_ind()

Send indication of the Service Changed Characteristic.

ble_gatts_get_num_attr()

Calculate the number of attributes required for a service.

  • GATT client API (ble_gattc.h): Used by a device configured as a GATT client to discover the services, characteristics, etc. of a peer device, read or write its attributes, initiate MTU exchanges, confirm the reception of indications, etc.

Table 18 GATT client API

Function

Description

ble_gattc_browse()

Browse services on a remote GATT server.

ble_gattc_browse_range()

Browse services on remote GATT server in a given range.

ble_gattc_discover_svc()

Discover services on a remote GATT server.

ble_gattc_discover_include()

Discover included services on a remote GATT server.

ble_gattc_discover_char()

Discover characteristics on a remote GATT server.

ble_gattc_discover_desc()

Discover descriptors on a remote GATT server.

ble_gattc_read()

Read a characteristic value or a characteristic descriptor from the remote GATT server, depending on the attribute handle.

ble_gattc_write()

Write a characteristic value or a characteristic descriptor to the remote GATT server, depending on the attribute handle.

ble_gattc_write_no_resp()

Write attribute to remote GATT server without response.

ble_gattc_write_prepare()

Prepare long/reliable write to remote GATT server.

ble_gattc_write_execute()

Execute long/reliable write to remote GATT server.

ble_gattc_indication_cfm()

Send confirmation for received indication.

ble_gattc_get_mtu()

Get current TX MTU of peer.

ble_gattc_exchange_mtu()

Exchange MTU.

Note

Several GAP configuration functions must be called before the attribute database is created, because modifying the device’s configuration can clear the attribute database created up to that point. This is noted in the Doxygen headers of the configuration functions that can have this effect.

5.1.2.2. Dialog BLE service API

The BLE service API header files are in <sdk_root_directory>/sdk/interfaces/ble/services/include. In most projects the API header files are symbolically linked to <sdk_root_directory>/sdk/ble/services/include.

The services-specific API, callback function prototypes and definitions are included in each service’s header file. The services implemented are the following:

Table 19 Header files for the BLE services

Header file

Description

bas.h

Battery Service

bcs.h

Body Composition Service

ble_service.h

Services handling routines API

bms.h

Bond Management Service

cts.h

Current Time Service

dis.h

Device Information Service

dlg_debug.h

Dialog Debug Service

dlg_suota.h

Dialog SUOTA Service

hids.h

Human Interface Device Service

hrs.h

Heart Rate Service

ias.h

Immediate Alert Service

lls.h

Link Loss Service

scps.h

Scan Parameter Service

sps.h

Serial Port Service

svc_defines.h

Common definitions for all services

svc_types.h

Common characteristic common

tps.h

Tx Power Service

uds.h

User Data Service

wss.h

Weight Scale Service

All services have an initialization function defined. This function is called with arguments that vary for different services.

The most common argument is a pointer to one or more callback functions that should be called upon a service-specific event. For example, the prototype for the initialization function of the Immediate Alert Service (ias.h) is the following:

Code 23 Initialization code for Immediate Alert Service
ble_service_t *ias_init(ias_alert_level_cb_t alert_level_cb)

Function ias_init() has only one argument. It is a pointer to the callback function that will be called when a peer device has modified the value of the Immediate Alert Level characteristic. This callback function is part of the user application code and should provide the application handling required for the change to the Immediate Alert Level.

The return value from all initialization functions is the created service’s handle which is used to reference the service in the application. To understand how the service interacts with the BLE Framework it is useful to know what this handle represents. The handle is a pointer to a generic structure (ble_service_t) that defines how the service should interact with the framework. Within each service there is an internal service definition (XXX_service_t) as shown in Figure 25. This contains the generic service structure plus a set of handles, one for each GATT characteristic that the service implements. This XXX_service_t structure is populated by XXX_init() for that service. The start_h and end_h handles will contain the start and end handles of the attributes for this service within the overall GATT table provided by the GATT server. So, when a GATT client requests a Service Discovery from the server these represent the start and end handles that the client would use to access service XXX.

../_images/058.png

Figure 25 Structure of a service handle

The set of optional callbacks allow each service to specify if it wants to do some specific handling on a certain event received by the BLE Framework. If the service wants to be informed when another BLE device has connected to this device then it can define its own handle_connected_evt() function and plug it into the connected_evt callback. Each service declares its handle_connected_evt() function as static in xxx.c and by convention in the SmartSnippets™ SDK they all share the same function names in each service.

As each service is initialized and thus added to the BLE services framework with ble_service_add(), its generic services structure is added to a structure of supported services as shown in Figure 26.

../_images/059.png

Figure 26 Structure of supported services

Now that the main internal services structure has been explained, it is easier to follow how the service initialization defines how the service operates.

Within the BLE service framework, the main event handler is ble_service_handle_event() which is shown below.

Code 24 Handle BLE events using BLE service framework
bool ble_service_handle_event(const ble_evt_hdr_t *evt)
{
     switch (evt->evt_code) {
     case BLE_EVT_GAP_CONNECTED:
             connected_evt((const ble_evt_gap_connected_t *) evt);
             return false; // make it "not handled" so app can handle
     case BLE_EVT_GAP_DISCONNECTED:
             disconnected_evt((const ble_evt_gap_disconnected_t *) evt);
             return false; // make it "not handled" so app can handle
     case BLE_EVT_GATTS_READ_REQ:
             return read_req((const ble_evt_gatts_read_req_t *) evt);
     case BLE_EVT_GATTS_WRITE_REQ:
             return write_req((const ble_evt_gatts_write_req_t *) evt);
     case BLE_EVT_GATTS_PREPARE_WRITE_REQ:
             return prepare_write_req((const ble_evt_gatts_prepare_write_req_t *) evt);
     case BLE_EVT_GATTS_EVENT_SENT:
             return event_sent((const ble_evt_gatts_event_sent_t *) evt);
     }

     return false;
}

Each of these sub-handlers inside ble_service_handle_event() search throughout the added services to find one that has defined a behavior for this event. There are two types of events that are handled differently.

5.1.2.3. Connection Orientated Events

The connection and disconnection events are potentially of interest to all registered services, so all services can be informed. The cleanup on shutdown is handled in the same way.

For example a connection event will call the BLE service’s statically defined connected_evt() function (sdk/interfaces/ble/services/src/ble_service.c). This will go through all the services registered to the BLE service framework to check for services that have registered to connection events during the service initialization. For each such service the connected_evt callback will be called.

5.1.2.4. Attribute Orientated Events

These are events that have to do with a given attribute handle. As each attribute is related to a unique service the first step in each of these handlers is to identify which service the attribute belongs to. For example a write request on a specific attribute will call the BLE service’s statically defined write_req() function (sdk/interfaces/ble/services/src/ble_service.c) as shown below. This will first identify which service owns that attribute with find_service_by_handle(). Then if it has a write_req callback defined it executes the callback.

Code 25 Example of code for the Write Request
static bool write_req(const ble_evt_gatts_write_req_t *evt)
{
        ble_service_t *svc = find_service_by_handle(evt->handle);

        if (!svc) {
                return false;
        }

        if (svc->write_req) {
                svc->write_req(svc, evt);
        }

        return true;
}

An example of this flow is the Write No Response procedure that can be applied to the Immediate Alert Level characteristic of the Immediate Alert Service. When a GATT client requests a write to that characteristic it will trigger the write_req() sub-handler under ble_service_handle_event().

The write_req() sub-handler will use find_service_by_handle() to see if any of the added services are registered for that characteristic. It will match it with the Immediate Alert Service (IAS) and as the IAS has registered a Write Request handler the IAS handle_write_req() will be called (sdk/interfaces/ble/services/src/ias.c).

Code 26 Example of code that handle the Write Request and match it with the appropriate instance
static void handle_write_req(ble_service_t *svc, const ble_evt_gatts_write_req_t *evt)
{
        ia_service_t *ias = (ia_service_t *) svc;
        att_error_t err = ATT_ERROR_OK;

        if (evt->length == 1) {
                uint8_t level;

                /*
                * This is write-only characteristic so we don't need to store value written
                * anywhere, can just handle it here and reply.
                */

                level = get_u8(evt->value);

                if (level > 2) {
                        err = ATT_ERROR_APPLICATION_ERROR;
                } else if (ias->cb) {
                        ias->cb(evt->conn_idx, level);
                }
        }

        ble_gatts_write_cfm(evt->conn_idx, evt->handle, err);
}

By calling ias->cb() function, this handler actually calls the application supplied callback function passed as an argument when ias_init() was called by the application. Finally, it sends a Write Confirmation to update the value at the attribute database maintained in the BLE Stack.

This is only an example of the way the BLE service framework translates BLE events to BLE service events. Different events in different services can have different levels of complexity, but most of the times this complexity is contained within the BLE service API. The aim is that the application only needs to call the service’s initialization function and define the appropriate callback functions to handle all service’s events.

In addition, some services define additional service-specific API calls. For instance, the Heart Rate Service implementation defines an API to trigger notifications of the Heart Rate Measurement characteristic, using functions hrs_notify_measurement() and hrs_notify_measurement_all() (the first is used to notify the characteristic to a specified peer, while the second is used to notify the characteristic to all subscribed peers). Some services also define some internal helper functions that are used to manipulate characteristic values, and some services require attribute initial values as arguments of the initialization function.

The BLE service API adds another layer to the general BLE API. Together the BLE adapter, BLE manager, BLE API library and BLE service framework results in the BLE Framework.

The BLE services API provides an off the shelf solution to implement an application using many of the common adopted BLE services. The underlying BLE API and GATT server API can be used to create other adopted services or even custom services using the BLE services as a template.

The following sections will provide an overview of a generic application and then will describe in detail several of the example BLE projects included in the SmartSnippets™ DA1469x SDK:

Table 20 BLE projects included in the SmartSnippets™ DA1469x SDK

BLE projects

General description

ble_adv

The simplest BLE project available in the SmartSnippets™ DA1469x SDK, which does not use the BLE service framework and exposes only the GAP and GATT services.

ble_peripheral

A project that can be used as a template for developing BLE peripheral applications. The project includes some of the services implemented under the BLE service framework.

ble_suota_client

This application is a SUOTA 1.2 client implementation and allows to update SUOTA-enabled devices over the air, using a simple serial console interface.

5.1.2.5. Configuring the project

In each project the BLE Framework and BSP are configured via a set of custom config files that set the defines and macros used in that project. These files are found in the config directory of each project.

In the case of the ble_adv project these file are config/custom_config_qspi.h and config/custom_config_ram.h (for QSPI and RAM build configurations respectively).

Any definitions set in these file will override the default SDK values which are found in the following files:

sdk/bsp/config/bsp_defaults.h

sdk/interfaces/ble/config/ble_config.h

sdk/free_rtos/include/FreeRTOSconfig.h

5.1.2.6. BLE application structure

All the BLE application projects in the SmartSnippets™ DA1469x SDK have a similar structure. This involves several FreeRTOS tasks at different priority levels to ensure that the overall system’s real time requirements are met.

The BLE application is implemented in a FreeRTOS task that is created by the system_init() function. system_init() runs at the highest priority at startup and is also responsible for starting the BLE manager and BLE adapter tasks.

The application task has the following flow:

  1. Device initialization and configuration: Start-up BLE, setting device role, device name, appearance and other device parameters.

  2. Attribute database creation (GATT server devices): Creation of services and attributes using the BLE service framework. This must be done after (1) to prevent deletion of the attribute database.

  3. Air operation configuration and initiation: BLE peripheral devices usually end-up advertising and BLE central devices end-up scanning and/or attempting to connect to another device.

  4. The infinite for(;;) loop, which is the application’s event handling loop. The application has been set-up as desired and now it is waiting for BLE events to respond to, like connections or write requests.

    • The BLE adapter (ad_ble_task) must have a higher priority than the application task(s) because it runs the BLE Host stack scheduler and it handles time critical tasks.

    • Most of the BLE applications use the FreeRTOS task notifications mechanism to block. ble_adv_demo is the simplest application and is the only project that does not use this mechanism. Instead, it just blocks on the BLE manager’s event queue.

    • In addition to the BLE-related functionality most projects also use other system modules, like software timers or sensors. In this case, the application usually defines callback functions to be triggered on these system events or interrupts. These callback functions should use task notifications to unblock the application task which can then handle the event or interrupt in the application task’s context (under its for(;;) loop).

Warning

Calling a BLE API function inside a callback function triggered on a timer expiry will execute the BLE API function in the timer’s task context. Calling other functions in the callback functions can also have implications on real time performance or in corrupting the small stack used by the timer task.

5.1.3. BLE Security

The Bluetooth specification defines the security options for device authentication and link encryption. These aspects of security are handled seamlessly by the BLE Framework. The API in Table 21 is able to set-up security, initiate pairing, do a security request or set-up encryption using previously exchanged keys. Most details of the procedures will be handled internally by the BLE Framework and the application will be notified only if intervention is needed or when the procedure is completed. These options will be described in detail in sections Section 5.1.3.2 and Section 5.1.3.5.

The generation and storage of the security keys and other bonding information is also handled by the BLE Framework. Persistent storage can also be used to enable storage of the security keys and bonding data info in the flash. The BLE Framework can then retrieve this information after a power cycle and use it to restore connections with previously bonded devices. This is described in Section 5.1.5.

5.1.3.1. LE Secure

LE Secure Connections pairing is supported and enabled by default by the SDK using the API described in sections Section 5.1.3.2 LE Secure Connections pairing will be used if the connected peer supports the feature without the need for the application to specifically request it. If the combination of the devices’ capabilities result in a numeric comparison pairing algorithm (introduced and used for the LE Secure Connections pairing), the application will be notified of a numeric comparison request during pairing by the reception of a BLE_EVT_GAP_NUMERIC_REQUEST event and should respond using ble_gap_numeric_reply() function.

If the application needs to use only LE Legacy Pairing and disable LE Secure Connections support in the SDK, it should define dg_configBLE_SECURE_CONNECTIONS macro to 0 in the application config file.

5.1.3.2. Functions

Table 21 summarizes the API functions that can be used by the application to set-up security features.

Table 21 BLE Security API functions

API call

Description

ble_gap_pair()

Initiate a pairing or bonding procedure with a connected peer. Depending on whether the device is master or slave on the connection, this call will result either in a pairing or a security request respectively.

ble_gap_pair_reply()

Reply to a received BLE_EVT_GAP_PAIR_REQ event. This event will only be received by a peripheral device when the central peer has initiated a pairing procedure, so this function should only be called by a peripheral application and only after a BLE_EVT_GAP_PAIR_REQ event has been received.

ble_gap_passkey_reply()

Reply to a received BLE_EVT_GAP_PASSKEY_REQUEST event. This event will be received if the combination of the devices’ input/output capabilities results in a passkey entry pairing algorithm. The application should use this function to submit the passkey for the pairing procedure to proceed.

ble_gap_numeric_reply()

Reply to a received BLE_EVT_GAP_NUMERIC_REQUEST event. This event will be received if the combination of the devices’ input/output capabilities results in a numeric comparison pairing algorithm. The application should use this function to accept or reject the numeric key for the pairing procedure to proceed. This should be only used if LE Secure Connections are enabled.

ble_gap_set_sec_level()

Set the security level for a connection. If the device is already bonded, the existing Long Term Key (LTK) will be used to set-up encryption. If the device is not bonded, a pairing or a security request will be triggered (depending on whether the device is master or slave on the connection) with the bond flag set to false.

ble_gap_get_sec_level()

Get the security level currently established on a connection.

ble_gap_unpair()

Unpair a previously paired or bonded device. This will also remove security keys and bonding data info currently present in BLE storage.

5.1.3.3. Events

Table 22 describes the BLE events related to security that may be received by the application and the proper API functions to respond to them.

Table 22 BLE Security API events

Event

Argument

Description

BLE_EVT_GAP_PAIR_REQ

ble_evt_gap_pair_req_t

Pairing request received by a connected peer. Member <bond> indicates if the peer has requested a bond (that is, exchange of long term security keys). The application should use ble_gap_pair_reply() to respond to this request.

BLE_EVT_GAP_PAIR_COMPLETED

ble_evt_gap_pair_completed_t

A previously requested pairing procedure has been completed. Member <status> indicates the completion status of the procedure, while members <bond> and <MITM> indicate if a bond was established with the peer and if MITM (Man In The Middle) protection has been enabled on the connected link.

BLE_EVT_GAP_SECURITY_REQUEST

ble_evt_gap_security_request_t

Security request received by a connected peripheral peer. Members <bond> and <MITM> indicate if a bond and MITM protection have been requested by the peer. The application may use ble_gap_pair() to initiate pairing with the peer.

BLE_EVT_GAP_PASSKEY_NOTIFY

ble_evt_gap_passkey_notify_t

A passkey has been generated during a pairing procedure. This event will be received if the application has display capability. Member <passkey> contains the passkey that should be displayed to the user and entered by the peer for the pairing procedure to continue.

BLE_EVT_GAP_PASSKEY_REQUEST

ble_evt_gap_passkey_request_t

A passkey has been requested during a pairing procedure. This event will be received if the application has keyboard capability. The application should use ble_gap_passkey_reply() to respond to this request using the passkey entered by the user.

BLE_EVT_GAP_NUMERIC_REQUEST

ble_evt_gap_numeric_request_t

A numeric comparison has been requested during a pairing procedure. This event will be received if the application has keyboard or Yes/No and display capability. The application should use ble_gap_numeric_reply() to respond to this request using the accept or reject input entered by the user.

BLE_EVT_GAP_SEC_LEVEL_CHANGED

ble_evt_gap_sec_level_changed_t

The security level has been changed on an established link. Member <level> contains the security level that has been reached. This will be received after a pairing or an encryption procedure has been successfully completed.

BLE_EVT_GAP_SET_SEC_LEVEL_FAILED

ble_evt_gap_set_sec_level_failed_t

Setting the security level on an established link using ble_gap_set_sec_level() has failed. Member <status> indicates the reason for the failure. This will be received after an initiated encryption procedure has been unsuccessful. This may indicate that pairing should be requested again for the connected peer (for example, the peer may have lost the previously exchanged security keys).

5.1.3.4. Macros

Table 23 contains the configuration macros related to BLE security.

Table 23 BLE Security API macros

Macro

Default

Description

dg_configBLE_SECURE_CONNECTIONS

1

Set to 1 to use LE Secure Connections pairing if the peer supports the feature or to 0 to always use LE Legacy Pairing.

dg_configBLE_PAIR_INIT_KEY_DIST

GAP_KDIST_ENCKEY | GAP_KDIST_IDKEY | GAP_KDIST_SIGNKEY

Set the security keys requested to be distributed by the pairing initiator during a pairing feature exchange procedure.

dg_configBLE_PAIR_RESP_KEY_DIST

GAP_KDIST_ENCKEY | GAP_KDIST_IDKEY | GAP_KDIST_SIGNKEY

Set the security keys requested to be distributed by the pairing responder during a pairing feature exchange procedure.

5.1.3.5. Message Sequence Charts (MSCs)

5.1.3.5.1. Central
../_images/047.png

Figure 27 Central Pairing Just Works

../_images/048.png

Figure 28 Central Bonding Just Works

../_images/049.png

Figure 29 Central Bonding Passkey Entry (Central Display)

../_images/050.png

Figure 30 Central Bonding Passkey Entry (Peripheral Display)

../_images/051.png

Figure 31 Central Bonding Numeric Comparison (Secure Connections Only)

5.1.3.5.2. Peripheral
../_images/052.png

Figure 32 Peripheral Pairing Just Works

../_images/053.png

Figure 33 Peripheral Bonding Just Works

../_images/054.png

Figure 34 Peripheral Bonding Passkey Entry (Peripheral Display)

../_images/055.png

Figure 35 Peripheral Bonding Passkey Entry (Central Display)

../_images/056.png

Figure 36 Peripheral Bonding Numeric Comparison (Secure Connections Only)

5.1.4. BLE Power Control

The Bluetooth specification defines the way by which a device can request a remote device to make a specified change in its TX power level on a given PHY. The response of the peer device contains a value that indicates an acceptable reduction in the power level that allows the local device to further reduce its transmit power level to the minimum level possible. The local and remote devices can also share their current transmit power levels. This way they can also calculate the link path loss between them.

This 5.2 BLE Feature is supported and enabled by default by the SDK using the API described in section Section 5.1.4.2

5.1.4.1. RSSI Golden Range

The BLE specification defines that the radio receiver can have an RSSI Golden Range that it prefers the incoming signal to remain within. This functionality is supported and so the local controller adjusts automatically the peer’s TX power to bring RSSI to its preferred value inside this Golden Range.

The local device continuously monitors the RSSI and if it is below the minimum or above maximum acceptable values of the RSSI it uses the Power Control Request procedure to request an increase or decrease the TX power level of the peer device. This procedure is done automatically by the local device. The application can be informed about the changes in the local and the remote TX power levels by using the ble_gap_tx_power_report_en() command. The changes in the TX power level are then reported to the application by the BLE_EVT_GAP_TX_PWR_REPORT event.

The default values of the RSSI Golden Range can be seen in Table 26.

5.1.4.2. Functions

Table 24 summarizes the API functions that can be used by the application to use the LE Power Control feature.

Table 24 BLE Power Control API functions

API call

Description

ble_gap_local_tx_power_get()

Get the current and maximum transmit power levels of the local device, on the ACL connection, for the indicated PHY.

ble_gap_remote_tx_power_get()

Get the TX power level used by the remote device, on the ACL connection for the indicated PHY.

ble_gap_path_loss_report_params_set()

Set the path loss threshold reporting parameters

ble_gap_path_loss_report_en()

Enable or disable path loss reporting.

ble_gap_tx_power_report_en()

Enable or disable reporting of TX power level in the local and remote device for the ACL connection.

ble_gap_rf_path_compensation_set()

Indicate the RF path gain or loss between the RF transceiver and the antenna contributed by intermediate components.

5.1.4.3. Events

Table 25 describes the BLE events related to LE Power Control that may be received by the application.

Table 25 BLE Power Control API events

Event

Argument

Description

BLE_EVT_GAP_LOCAL_TX_PWR

ble_evt_gap_local_tx_pwr_t

Reading of the local transmit power has been completed. Member <phy> indicates the PHY, member <curr_tx_pwr_lvl> indicates the current transmit power level (dBm) while member <max_tx_pwr_lvl> indicates the maximum transmit power level.

BLE_EVT_GAP_TX_PWR_REPORT

ble_evt_gap_tx_pwr_report_t

Reports that the local or remote transmit power has changed or that a ble_gap_remote_tx_power_get() command has been completed. Member <reason> indicates the reason the of event and device (local or remote) , member <phy> indicates the PHY involved (which might not be the current transmit PHY for the device), member <tx_pwr_lvl> indicates the value of TX power level (dBm), member <tx_pwr_lvl_flag> indicates if the TX power level is min or max while the member <delta> indicates the actual change in the TX power level.

BLE_EVT_GAP_PATH_LOSS_THRES

ble_evt_gap_path_loss_thres_t

Reports a path loss threshold crossing. Member <curr_path_loss> indicates the current path loss value while member <zone_enter> indicates which zone path loss has entered.

5.1.4.4. Macros

Table 26 contains the configuration macros related to BLE Power Control.

Table 26 BLE Power Control API macros

Macro

Default

Description

dg_configBLE_GOLDEN_RANGE_LOW

-70

Sets the lower RSSI value (dBm) of the Golden Range.

dg_configBLE_GOLDEN_RANGE_UP

-40

Sets the upper RSSI value (dBm) of the Golden Range.

dg_configBLE_GOLDEN_RANGE_PREF

-55

Sets the preferred RSSI value (dBm) inside the Golden Range (dBm).

dg_configBLE_PCLE_MIN_TX_PWR_IDX

GAP_TX_POWER_MINUS_26_dBm

Sets the Minimum TX Power index used in LE Power Control (dBm). The available TX power levels can be seen in Table 27

dg_configBLE_PCLE_MAX_TX_PWR_IDX

GAP_TX_POWER_MAX

Sets the Maximum TX Power index used in LE Power Control (dBm). The available TX power levels can be seen in Table 27

5.1.4.5. Supported TX Power Levels

Table 27 lists the available TX power levels. The corresponding gap_tx_power_t enumeration is located in the sdk/ble/api/include/ble_gap.h.

Table 27 TX power levels

Tx Power level

Corresponding index

GAP_TX_POWER_MAX

17

GAP_TX_POWER_6_dBm

17

GAP_TX_POWER_5_dBm

16

GAP_TX_POWER_4_5_dBm

15

GAP_TX_POWER_4_dBm

14

GAP_TX_POWER_3_dBm

13

GAP_TX_POWER_2_dBm

12

GAP_TX_POWER_1_5_dBm

11

GAP_TX_POWER_0_dBm

10

GAP_TX_POWER_MINUS_1_dBm

9

GAP_TX_POWER_MINUS_2_dBm

8

GAP_TX_POWER_MINUS_3_dBm

7

GAP_TX_POWER_MINUS_6_dBm

6

GAP_TX_POWER_MINUS_8_dBm

5

GAP_TX_POWER_MINUS_12_dBm

4

GAP_TX_POWER_MINUS_18_dBm

3

GAP_TX_POWER_MINUS_22_dBm

2

GAP_TX_POWER_MINUS_26_dBm

1

GAP_TX_POWER_MINUS_50_dBm

0

GAP_TX_POWER_MIN

0

5.1.5. BLE Storage

BLE Storage is the module that implements storage functionality for information related to connected and bonded peers, like security keys, CCC descriptors configuration and application-defined values. BLE Storage can maintain the list of connected and bonded devices both in RAM and in persistent storage (for example, in the flash). By default, devices are managed in RAM only and persistent storage must be explicitly enabled in the application’s configuration by defining macro CONFIG_BLE_STORAGE. Device data is then stored using Non-Volatile Memory Storage (NVMS) on the generic partition (see Section Section 4.2 for details).

Two kinds of data are stored:

  • Device pairing data (exchanged keys and related information).

  • Application-defined data managed using the BLE storage API (only the values with the ‘persistent’ flag set are stored in flash, for example CCC descriptor values).

Persistent storage can be enabled by the application by adding the following entries in the application’s custom configuration file:

Code 27 Enable BLE persistent storage
// enable BLE persistent storage
#define CONFIG_BLE_STORAGE

// enable Flash and NVMS adapters with VES (required by BLE persistent storage)
#define dg_configFLASH_ADAPTER                  1
#define dg_configNVMS_ADAPTER                   1
#define dg_configNVMS_VES                       1

The maximum number of bonded devices can be set using the defaultBLE_MAX_BONDED macro (defined to 8 by default). If the application attempts to bond more devices than its allowed, an error will be returned. This error should be handled by the application. It can then either unpair one of the currently bonded devices (using ble_gap_unpair() API function) or perform pairing without bonding.

Technical details on the BLE Storage implementation can be found in the following readme file:

<sdk_root_directory>/sdk/interfaces/ble/readme.md

5.1.7. LE Data Packet Length Extension

For Bluetooth Core versions 4.0 and 4.1 the maximum Packet Data Unit (PDU) size was 27 octets. Bluetooth Core version 4.2 introduced an important enhancement, namely LE Data Packet Length Extension, which allows for the PDU size to be anywhere between 27 and 251 octets. This means that, for example, the L2CAP layer can now fill up to 247 octets of higher layer data packets in every L2CAP PDU compared to 21 octets with previous Bluetooth Core versions. This significant increase (more than 10 times) in the number of octets of user data sent per packet allows devices to transfer data up to 2.5 times faster than with previous versions. This will be of great benefit to applications that might require transferring large amounts of data such as Over-the-Air (OTA) firmware updates or downloading large data logs from sensors.

For the default PDU size to be extended on an established connection, the Data Length Update procedure must be performed. According to this control procedure, the LL_LENGTH_REQ and LL_LENGTH_RSP PDUs must be exchanged by the connected devices so that each is notified of its peer device’s capabilities. Each device uses these PDUs to report its maximum receive data channel and maximum transmit data channel PDU payload length and PDU time. After this update procedure, the PDU size for each direction of the connection’s data path is set by both controllers to the minimum of the values exchanged.

The DA1469x supports the LE Data Length Extension feature, so the values for the Receive and Transmit Data Length are set by default to the maximum allowed, which is 251 octets. The DA1469x controller when configured as a BLE central device will initiate a Data Length Update upon a new connection if the peer device’s controller supports this feature. The BLE Manager will use the values defined by dg_configBLE_DATA_LENGTH_RX_MAX and dg_configBLE_DATA_LENGTH_TX_MAX macros for this initial Data Length Update negotiation.

5.1.7.1. Functions

Table 31 LE Data Length Functions – ble_gap.h

Function

Description

ble_gap_data_length_set()

Set the maximum Transmit data length and time for an existing connection or the preferred Transmit data length for future connections (that is, the Transmit data length to be used in future data length update negotiations). Connection data length change will be signaled using BLE_EVT_GAP_DATA_LENGTH_CHANGED event.

5.1.7.2. Macros

Table 32 LE Data Length Definitions

Macro

Default

Description

dg_configBLE_DATA_LENGTH_RX_MAX

251

Set the maximum Receive Data Channel PDU Payload Length. Unless ble_gap_data_length_set() is used by the application, this will define the Receive data length present in the LE Data Length Update negotiations done by the device.

dg_configBLE_DATA_LENGTH_TX_MAX

251

Set the maximum Transmit Data Channel PDU Payload Length. Unless ble_gap_data_length_set() is used by the application, this will define the Transmit data length present in the LE Data Length Update negotiations done by the device.

5.1.7.3. Events

Table 33 LE Data Length Events – fetched using ble_get_event() - ble_gap.h

Event

Argument

Description

BLE_EVT_GAP_DATA_LENGTH_CHANGED

ble_evt_gap_data_length_changed_t

Data Length changed for specified connection. Members <rx_length>, <rx_time>, <tx_length> and <tx_time> specify the values obtained after an LE Data Length Update negotiation (each direction’s data length is typically set to the minimum of the values reported by the connected devices).

BLE_EVT_GAP_DATA_LENGTH_SET_FAILED

ble_evt_gap_data_length_set_failed_t

Data Length Set operation failed. Member <status> indicates the reason the set operation failed.

5.1.8. 2M PHY

Bluetooth Core versions up to 4.2 supported only the mandatory bit rate of 1 megabit per second (Mb/s), which is referred to as the LE 1M PHY. Bluetooth Core version 5.0, which is implemented in Dialog DA1469x product family, also supports the optional bit rate of 2 Mb/s, which is referred to as the LE 2M PHY. This feature is implemented by the LE controller and it can be configured using the API described in Section 5.1.8.1 and Section 5.1.8.2, although additional configuration is not mandatory; the LE controller will handle PHY update procedures seamlessly and will inform the application of any changes in the PHY configuration regardless of which side initiated the PHY update procedure.

5.1.8.1. Functions

Table 34 LE 2M Functions – ble_gap.h

Function

Description

ble_gap_phy_get()

Get the transmitter and receiver PHY preferences set for an existing connection or for all future connections. The changes in PHY configuration for a given connection will be signaled using BLE_EVT_GAP_PHY_CHANGED event.

ble_gap_phy_set()

Set the transmitter and receiver PHY preferences for an existing connection or for all future connections. Completion of the PHY set operation will be signaled using BLE_EVT_GAP_PHY_SET_COMPLETED event and possible change in PHY configuration for a given connection will be signaled using BLE_EVT_GAP_PHY_CHANGED event.

5.1.8.2. Events

Table 35 LE 2M Events – fetched using ble_get_event() - ble_gap.h

Event

Argument

Description

BLE_EVT_GAP_PHY_CHANGED

ble_evt_gap_phy_changed_t

PHY configuration changed for the specified connection. Members <tx_phy> and <rx_phy> specify the current configuration for the transmitter and received PHY respectively. This event is received only after a change in one or both of the transmitter and receiver PHY configurations.

BLE_EVT_GAP_PHY_SET_COMPLETED

ble_evt_gap_phy_set_completed_t

PHY set operation has completed. This will be received following a ble_gap_phy_set() call. Member <status> indicates the status of the set operation.

5.1.9. NVPARAM fields

Table 36 shows the Non-Volatile memory parameters which can be found in <sdk_root_directory>/sdk/middleware/adapters/include/platform_nvparam.h

Table 36 NVPARAM fields

Tag

Offset

Length

NVPARAM_BLE_PLATFORM_BD_ADDRESS

0x01

6 bytes array

NVPARAM_BLE_PLATFORM_LPCLK_DRIFT

0x07

16bit value

NVPARAM_BLE_PLATFORM_EXT_WAKEUP_TIME

0x0D

16bit value

NVPARAM_BLE_PLATFORM_OSC_WAKEUP_TIME

0x0E

16bit value

NVPARAM_BLE_PLATFORM_RM_WAKEUP_TIME

0x0F

16bit value

NVPARAM_BLE_PLATFORM_SLEEP_ENABLE

0x11

1 byte

NVPARAM_BLE_PLATFORM_EXT_WAKEUP_ENABLE

0x12

1 byte

NVPARAM_BLE_PLATFORM_BLE_CA_TIMER_DUR

0x40

16bit value

NVPARAM_BLE_PLATFORM_BLE_CRA_TIMER_DUR

0x41

1 byte

NVPARAM_BLE_PLATFORM_BLE_CA_MIN_RSSI

0x42

1 byte

NVPARAM_BLE_PLATFORM_BLE_CA_NB_PKT

0x43

16bit value

NVPARAM_BLE_PLATFORM_BLE_CA_NB_BAD_PKT

0x44

16bit value

NVPARAM_BLE_PLATFORM_IRK

0x80

16 bytes array

5.1.10. Considerations on BLE Task Priorities

The BLE Software in the SDK consists of three modules:

  1. BLE manager: Provides the interface to the BLE functionality of the chip. The application task uses the BLE API to interface with the BLE manager. The BLE manager is a task that stands between the application and the BLE adapter. It uses the BLE adapter to interface with the BLE Stack.

  2. BLE adapter: The system task that provides the interface to the BLE Stack. It runs the BLE Stack internal scheduler, receives the commands or the replies to events from the BLE manager, and passes BLE Stack events to the BLE manager. BLE core functionality is implemented by the BLE adapter task.

  3. BLE Stack: The software stack that implements the Link Layer and the BLE Host stack, specifically the Logical Link Control and Adaptation Protocol (L2CAP), the Security Manager Protocol (SMP), the Attribute Protocol (ATT), the Generic Attribute Profile (GATT) and the Generic Access Profile (GAP).

Note

The BLE Stack software is run under the BLE adapter’s task context, which instantiates and initializes the stack.

The two BLE system tasks have by default a higher priority than application tasks in the SDK.

Note

Application developers should always make sure BLE adapter and BLE manager tasks always have a higher priority than application tasks.

The BLE adapter runs the BLE Stack scheduler, which dispatches all the messages between the BLE Stack’s different layers and calls the appropriate handlers. For example, when an application uses API ble_gatts_send_event() to send a GATT notification, this will result in a propagation of messages between the BLE manager, the BLE adapter and several BLE Stack’s internal layers until it reaches a transmission buffer and, eventually, the air.

5.1.11. BLE tasks timing requirements

In DA1469x the LE controller runs independently from the application. However, the BLE system tasks (BLE adapter, BLE manager) still need to run regularly to make sure BLE data are transmitted and handled in a timely manner. When the application is not making any BLE API calls and no data are exchanged with the peer, the BLE adapter will typically block until it is needed to either retrieve a message from or send a command to the BLE Host.

In some scenarios the BLE manager and the BLE adapter will communicate with messages without notifying the application. For example, upon connection with a peer that uses a resolvable private address, the BLE manager will attempt to resolve it using known devices IRKs. In this case the BLE manager and BLE adapter will have more running slots.

There are also other cases when the BLE framework will require a reply from the application when, for example, a pair request or a write request is received from the peer. Again, in these cases the BLE adapter and BLE manager will have to run more times in a period between two connection events.

5.1.12. Attribute operations

As the Attribute protocol is strict when an attribute request such as a read or a write request is received, the BLE Stack’s GATT layer will switch to a busy state for as long as the request is not completed/handled. In the case of a write request or a read request of an attribute whose value is to be provided by the application, then the application will have to confirm these operations using ble_gatts_read_cfm() or ble_gatts_write_cfm() respectively (after receiving BLE_EVT_GATTS_READ_REQ or BLE_EVT_GATTS_WRITE_REQ). In this case, other GATT operations, such as notification sending, will be queued until this request is confirmed.

5.1.13. BLE Application Examples

5.1.13.1. Advertising Application

The simplest BLE project in the SmartSnippets™ DA1469x SDK is ble_adv demo which is found in the folder <sdk_root_directory>/projects/dk_apps/demos/ble_adv. The application starts the device as a peripheral, sets the device name and advertising data and starts advertising. Code 28 is an extract from main.c.

Code 28 Set BLE device
// Start BLE module as a peripheral device
ble_peripheral_start();

// Set device name
ble_gap_device_name_set("Dialog ADV Demo", ATT_PERM_READ);

// Set advertising data
ble_gap_adv_data_set(sizeof(adv_data), adv_data, 0, NULL);

// Start advertising
ble_gap_adv_start(GAP_CONN_MODE_UNDIRECTED);

No BLE service is added, and the ones exposed are just GAP and GATT services. The infinite loop that implements the lifetime behavior of the application uses just ble_get_event(true) to block indefinitely on the BLE manager’s event queue. As soon as an event is posted there, the task unblocks and handles it using a switch case. Code 29 is located in main.c.

Code 29 Example of event handle
for (;;) {
             ble_evt_hdr_t *hdr;

             /* notify watchdog on each loop */
             sys_watchdog_notify(wdog_id);

             /* suspend watchdog while blocking on ble_get_event() */
             sys_watchdog_suspend(wdog_id);

             /*
              * Wait for a BLE event - this task will block
              * indefinitely until something is received.
              */
             hdr = ble_get_event(true);

             /* resume watchdog */
             sys_watchdog_notify_and_resume(wdog_id);

             if (!hdr) {
                     continue;
             }

             switch (hdr->evt_code) {
             case BLE_EVT_GAP_CONNECTED:
                     handle_evt_gap_connected((ble_evt_gap_connected_t *) hdr);
                     break;
             case BLE_EVT_GAP_DISCONNECTED:
                     handle_evt_gap_disconnected((ble_evt_gap_disconnected_t *) hdr);
                     break;
             case BLE_EVT_GAP_PAIR_REQ:
             {
                     ble_evt_gap_pair_req_t *evt = (ble_evt_gap_pair_req_t *) hdr;
                     ble_gap_pair_reply(evt->conn_idx, true, evt->bond);
                     break;
             }
             default:
                     ble_handle_event_default(hdr);
                     break;
             }

             // Free event buffer
             OS_FREE(hdr);
     }

Since the BLE service framework is not used, the only events handled by the application are the three events handled by the switch case: connection, disconnection and pair request. This makes sense for this application as its only purpose is to start a connectable advertising, restart it in case of a disconnection and respond to pair requests from devices that require pairing/bonding upon connection.

Running this project will result in an advertising BLE peripheral device exposing GAP and GATT services. GAP service attributes can be read using any standard BLE central device.

The ble_peripheral project is a good starting point for developing BLE peripheral applications. It is found in folder <sdk_root_directory>/projects/dk_apps/features/ble_peripheral. Unlike other example projects, it does not implement a specific profile, but instead exposes several BLE services via a GATT server.

The application’s initialization is similar to other projects that implement BLE peripheral applications. It uses the BLE service framework to instantiate several BLE services:

  • Battery Service (multiple instance)

  • Current Time Service

  • Device Information Service

  • Scan Parameter Service

  • Dialog Debug Service

  • Custom User Service

In addition to Bluetooth SIG-adopted services, ble_peripheral project instantiates two more services, Dialog Debug Service and a custom user service.

The Dialog Debug Service can be used to interact with the services that the application exposes using a Control Point characteristic to write commands and receive notifications from. A detailed description of the ways to interact with the Dialog Debug Service is included in the readme.md file inside the project’s folder.

The custom user service does not define any specific functionality other than using 128-bit UUIDs for services, characteristics and descriptors. This custom service, referred to as myservice in the project source code, is an example of implementing a custom service using BLE API calls to create its attribute database. No specific functionality is defined when one of these attributes is read or written. More details on how to create and use custom services will be given in section Section 5.1.16.

After the attribute database is created, the device will end-up advertising and it will wait for a connection event.

The ble_peripheral project uses the BLE service framework to handle service events, the application also defines handlers for connection, advertising completion and pair request events. The ble_peripheral project stands in terms of its completeness somewhere between the ble_adv demo and an application implementing a full BLE profile like the pxp_reporter.

The services the project will expose can be configured using the file config/ble_peripheral_config.h.

5.1.14. BLE profile projects

In addition to the projects described in the previous sections, there are several application projects that implement BLE profiles. These projects are more complex and provide a full implementation of BLE applications. As such they provide a good reference on how to combine the BLE functionality with several OS mechanisms, GPIO handling and interfacing with external sensors.

The implemented profiles are located under <sdk_root_directory>/projects/dk_apps/ble_profiles and listed below:

  • Apple Media Service (AMS) - Client role

  • HID over GATT Profile (HOGP) – Device role (hogp_device)

  • HID over GATT Profile (HOGP) – Host role (hogp_host)

  • Heart Rate Profile – Sensor role (hrp_sensor)

  • Heart Rate Profile – Collector role (hrp_collector)

  • Proximity Profile – Reporter role (pxp_reporter) located under <sdk_root_directory>/projects/dk_apps/demos

  • Weight Scale Profile – Weight Scale role (wsp_weightscale)

  • Apple Notification Center Service (ANCS) - Notification Consumer (NC) role (ancs)

  • Blood Pressure Profile (BLP) – Blood Pressure Sensor role (blp_sensor)

  • Bond Management Service (BMS)

  • Cycling Speed And Cadence collector (CSCP)

  • Health Thermometer Profile – Thermometer role (htp_thermometer)

5.1.15. Using adopted BLE services

Table 37 summarizes the API header files of the BLE services implemented by the SmartSnippets™ DA1469x SDK. These files can be found under <sdk_root_directory>/sdk/interfaces/ble/services/include. The developer can use these APIs to add these services to another project.

Table 37 BLE service API header files

File name

Description

ble_service.h

BLE service framework API:

  • Add service to framework

  • Handle event using BLE service framework

  • Elevate permission

  • Get number of attributes in a service

  • Add included services

bas.h

Battery Service – BAS

bcs.h

Body Composition Service – BCS

bms.h

Bond Management Service – BMS

cts.h

Current Time Service – CTS

dis.h

Device Information Service – DIS

dlg_debug.h

Dialog Debug Service

dlg_suota.h

Dialog SUOTA Service

hids.h

Human Interface Device Service – HID

hrs.h

Heart Rate Service – HRS

ias.h

Immediate Alert Service – IAS

lls.h

Link Loss Service – LLS

scps.h

Scan Parameters Service – ScPS

sps.h

Serial Port Service – SPS

tps.h

Tx Power Service – TPS

uds.h

User Data Service – UDS

wss.h

Weight Scale Service – WSS

5.1.16. Adding a custom service

The following code segments provide an overview of the initialization required to create a new custom service called XXX. It requires the files xxx.c and xxx.h to be created. A good example to base these on is the dlg_mls service in the Multi-Link demo. This provides a single write only characteristic in the service.

Each service needs a structure containing both the generic ble_service_t structure and any callbacks and characteristic handles required by the service. In the example below for service XXX there is one callback and one characteristic defined.

Code 30 Structure definition for XXX service
typedef struct {
     ble_service_t svc;          // Core BLE service structure
     xxx_cb_t cb;                // Callback provided by app to xxx
                                 // service to process an event
     uint16_t xxx_char1_val_h;   // Declare handle for each characteristic
                                 // that can be read or written
} xxx_service_t;

The requirements of the initialization function xxx_init() are illustrated below. They key information here is the comments which are explaining what each line is doing.

Code 31 Initialization function for XXX service
xxx_service_t* xxx_init(callback1){
// Allocate and initialise xxx_service_t structure
// Define any callback functions required by the service, write only in this case
xxx->svc.write_req = <this services write request handler>
// Create primary service UUID with either 16 or 128 bit value
uuid=ble_uuid_from_string() or uuid=ble_uuid_create16()
// add PRIMARY service with X attributes
num_attrs=X
ble_gatts_add_service(&uuid, GATT_SERVICE_PRIMARY, num_attrs)
// Create characteristic 1 for this service and allocate handle for it in GATT table
ble_gatts_add_characteristic(&uuid, GATT property, ATT permissions, size,0, NULL, &xxx->xxx_char1_h)
// Set start_h and pass in null terminated variable length list of all characteristic handles in the service
ble_gatts_register_service(&xxx->svc.start_h, &xxx->xxx_char1_h,0);
// Calculate end handle for service based on number of attributes in service
xxx->svc.end_h= xxx->svc.start_h + num_attrs;
// add the passed in callback function to service structure
xxx->xxx_cb1=callback1;
// add newly created service to ble framework
ble_service_add(&xxx->svc);
// and return handle for the service to the application
return &xxx->svc
}

5.1.17. Extending BLE functionality

The Dialog BLE API can be used to develop BLE applications. The API header files are located in folder <sdk_root_directory>/sdk/interfaces/ble/api/include. They are documented in [Ref_04] and are summarized in Table 38.

Table 38 Dialog BLE API header files

File name

Description

ble_att.h

Attribute Protocol API: Mostly definitions.

ble_attribdb.h

Helper to manage complex attributes database.

ble_bufops.h

Helpers to put and get data from BLE buffers.

ble_common.h

Common API: Functions used for operations not specific to a certain BLE Host software component

ble_gap.h

GAP API:

  • Device parameters configuration: device role, MTU size, device name exposed in the GAP service attribute, etc.

  • Air operations: Advertise, scan, connect, respond to connection requests, initiate or respond to connection parameters update, etc.

  • Security operations: Initiate and respond to a pairing or bonding procedure, set the security level, unpair, etc.

ble_gatt.h

Common definitions for GATT API

ble_gattc.h

GATT client API:

  • Discover services, characteristics, etc. of a peer device

  • Read or write a peer device’s attributes

  • Initiate MTU exchanges

  • Confirm the reception of indications

ble_gattc_util.h

GATT client utilities

ble_gatts.h

GATT server API:

  • Set up the attribute database

  • Set attribute values

  • Notify/indicate characteristic values

  • Initiate MTU exchanges

  • Respond to write and read requests

ble_l2cap

BLE L2CAP API.

ble_storage.h

BLE persistent storage API.

ble_uuid.h

BLE UUID declarations and helper functions.

5.2. Software Upgrade Over The Air (SUOTA)

5.2.1. Introduction

The SmartSnippets™ DA1469x SDK allows the user to update the software of the device wirelessly, using the Software Upgrade Over The Air (SUOTA) procedure.

When an update procedure is initiated from an Android or iOS device, a new firmware image is first transferred to the Flash memory and then the device reboots to complete the update. The final verification and activation of the new image is performed by the ROM boot loader on the next reboot. For more information see chapter Section 5.3.1.

The SUOTA GATT server runs on the DA1469x device and the GATT client on the Android or iOS device running the SUOTA application.

5.2.2. SUOTA service description

This section gives a brief description of the SUOTA service, responsible for performing software upgrades over BLE. A detailed service characteristic description is given on Table 39.

Table 39 SUOTA service characteristics

Characteris tic

SUOTA Version

(SUOTA_VERS ION definition)

Access

Size

Description

MEM_DEV

since version v1.3

READ

WRITE

4

Using this characteristic the GATT client is able to send commands to the SUOTA service. Some of the most commonly used commands are the following:

  • SPOTAR_IMG_SPI_FLASH( 0x13): Prepare for SUOTA. Image is going to be stored to the FLASH memory.

  • SPOTAR_REBOOT(0xFD): Reboot the device.

  • SPOTAR_IMG_END(0xFE): Client sent the whole image. SUOTA service is allowed to perform CRC calculations and other sanity tests to verify that the image transfer was successful.

GPIO MAP

READ

WRITE

4

Used to specify GPIO map of external FLASH device. Currently not applicable.

MEM_INFO

READ

4

Stores the total number of bytes received until now.

PATCH_LEN

since version v1.3

READ

WRITE

2

Specifies the number of bytes that when received, will trigger a notification back to the GATT client. This is meant to be used for flow control. The exact value is set by the GATT client during SUOTA. The notification is generated from the “STATUS” characteristic

PATCH_DATA

since version v1.3

READ

WRITE

WRITE_NO_RE SP

Exact size is specified by PATCH_DATA CHAR_SIZE

This is the endpoint to which SUOTA image data are sent. The exact size is specified by the “PATCH_DATA CHAR_SIZE” characteristic, and different values (23 – 509) can be used depending on the throughput requirements

STATUS

since version v1.3

READ

NOTIFY

1

This characteristic is used to notify the GATT client of the status of the SUOTA process. Status notifications are sent to indicate error conditions (for example bad command, or CRC) or to allow flow control during SUOTA process.

L2CAP_PSM

since version v1.3

READ

2

This is an optional characteristic that, if exists, indicates that the SUOTA service supports both SUOTA over GATT and SUOTA over L2CAP CoC. The value indicates the dynamic L2CAP channel on which the SUOTA service is listening for connections. The absence of this characteristic indicates that only SUOTA over GATT is supported.

VERSION

since version v1.3

READ

1

Indicates the version of the SUOTA service. The value is retrieved from the “SUOTA_VERSION” definition.

MTU

since version v1.3

READ

2

Stores the current value of the MTU, which is going to be either 23 (default), or a bigger value, if MTU exchange took place. This value can be used by the GATT client to retrieve the MTU size (if such API is not available on its side) and write with optimal rate to the “PATCH_DATA” characteristic.

PATCH_DATA CHAR_SIZE

since version v1.3

READ

2

Specifies the size of the “PATCH_DATA” characteristic.

CCC

READ

WRITE

1

Client Characteristic Configuration Allows the GATT client to enable notifications from the “STATUS” source.

Once the SUOTA service is discovered on the remote device and the GATT client has enabled notifications by writing the CCC characteristic, the SUOTA procedure can be started by issuing the SPOTAR_IMG_SPI_FLASH command. The write command executes successfully only if:

  • No more than one device is currently connected to the SUOTA enabled device

  • The application hosted in the SUOTA enabled device allows the upgrade to take place

  • There is enough memory to allocate the internal working buffers

If any of the above restrictions is violated, then command fails and an error notification is sent back to the GATT client (status SUOTA_SRV_EXIT). After a successful command execution the service is able to receive data either over GATT or L2CAP CoC layer (if the L2CAP_PSM characteristic is available).

The GATT client can use the value of the characteristic MTU to perform ATT write commands to the characteristic PATCH_DATA with optimal size if the GATT Client (mobile phone) has no API to find the optimal packet size. It is also possible for the GATT client to retrieve the size of the PATCH_DATA characteristic by reading the PATCH_DATA_CHAR_SIZE characteristic.

Following this, the GATT client should specify the value of the patch_len variable by writing the PATCH_LEN characteristic. PATCH_LEN specifies the number of bytes that once received, will trigger a notification back to the GATT client. This kind of flow control could be used to avoid flooding the SUOTA enabled device with too much image data. The bigger the value, the better the throughput, since notifications are going to be generated less frequently and therefore the number of missed connection events (where flow has stopped waiting for the notification) is decreased.

For example, if patch_len is set to 500 bytes, notifications will be sent to the GATT client when byte ranges 1-500, 501-1000, 1001 – 1500 etc. are received. Following the Bluetooth low energy specification, the maximum number of bytes that can be written to the PATCH_DATA characteristic with a single ATT write command is the minimum of MTU – 3 and the size of the PATCH_DATA characteristic.

When the whole image has been sent, the GATT client should issue the SPOTAR_IMG_END command to indicate this to the SUOTA service. The service is going to perform some sanity checks to verify that image transfer took place without errors, and then it is going to generate the appropriate status notification (SUOTA_CMP_OK on success, SUOTA_APP_ERROR or SUOTA_CRC_ERR on error).

Finally, the GATT client could issue an SPOTAR_REBOOT command to force a device reboot. This step is optional, but it is highly recommended.

Note

The PATCH_DATA, PATCH_DATA_CHAR_SIZE and PATCH_LEN characteristics are only relevant when SUOTA over GATT is taking place. When L2CAP CoC are used, a connection should be established to the L2CAP_PSM channel via L2CAP CoC and the flow is controlled using L2CAP credits. SUOTA service assigns enough credits to ensure that flow won’t stop during the upgrade. Notifications relevant to the PATCH_LEN characteristic are not sent during image transfer, but all other notifications are still valid.

5.2.3. SUOTA Flow

Figure 38 and Figure 39 illustrate the abstract FW update steps. Figure 40 illustrates in detail the flow diagram of the SUOTA.

The final verification and activation of the new image is performed by the ROM boot loader on the next reboot. Figure Figure 58 illustrates the booting sequence that shows at which step the acceptance/rejection of a FW image happens.

../_images/112.png

Figure 38 Flash layout after initial programming during production

../_images/113.png

Figure 39 Flash image layout after SUOTA image written in the destination partition

../_images/114.png

Figure 40 Flash image layout after reboot and activation of the new image

5.2.4. Performing SUOTA upgrade using a mobile phone

Note

The following procedure applies when using Android or iOS devices

5.2.4.1. Preparing the SUOTA enabled device

  1. Build the pxp_reporter application using the DA1469x-00-Release_QSPI_SUOTA configuration.

  2. Erase the Flash memory of DA1469x using the erase_qspi_jtag script.

  3. Download the pxp_reporter binary to DA1469x using the program_qspi_jtag script.

Press the K2 (Reset) button on the DK daughter board. PXP Reporter application should start running.

5.2.4.2. Preparing the SUOTA image

A SUOTA image is a binary file with a proper header that can be sent to a target device from an Android or iOS device.

To prepare a SUOTA image using PXP Reporter demo application, perform the following steps:

  1. Import the following two projects into SmartSnippets™ Studio from these locations.

    • python_scripts: <sdk_root_directory>\utilities

    • pxp_reporter: <sdk_root_directory>\projects\dk_apps\demos

  2. Build the pxp_reporter application using the DA1469x-00-Release_QSPI_SUOTA configuration. A new image named pxp_reporter.img is also created under DA1469x-00-Release_QSPI_SUOTA folder.

5.2.4.3. Perform the SUOTA using the mobile application

  1. Download the Dialog SUOTA application from Google PlayStore or Apple App Store.

  2. Copy pxp_reporter.img to an Android phone or tablet or to an iOS device and placed into the SUOTA folder. The folder is automatically created, if it does not exist, on the device by running the “Dialog Suota” application. On Android it is located at the root directory of the “Internal Storage” drive.

  3. Launch the Dialog SUOTA application on the Android phone and select the DA1469x device you want to update.

../_images/076.png

Figure 41 Device selection

  1. Select Update device.

../_images/077.png

Figure 42 Update device

  1. Select the appropriate image file – this is a list of the files in the SUOTA directory.

../_images/078.png

Figure 43 Image file

Note

The screens shown in Figure 44, Figure 45 have no effect for the Da1469x product.

  1. Touch Send to device on the touchscreen

../_images/079.png

Figure 44 Parameter settings for SPI

../_images/080.png

Figure 45 Parameter settings for I2C

  1. Wait until the process is completed. When the image is uploaded, a dialog box pops up asking for a device reboot. Select OK.

../_images/081.png

Figure 46 Uploading the image file

../_images/082.png

Figure 47 Reboot device

  1. Press Close to return to the main menu.

../_images/083.png

Figure 48 When file upload is finished, press “Close”

5.2.5. Performing SUOTA upgrade using two DA1469x devices

This section describes the procedure for performing SUOTA using two DA1469x devices.

  • One acting as the BLE central. It performs as the SUOTA image transmitter running ble_suota_client application. The SUOTA image will be stored in the NVMS_BIN_PART NVMS partition.

  • One acting as the BLE peripheral. It performs as the SUOTA image receiver, running pxp_reporter application.

Using this setup, it is possible to test both SUOTA methods (over GATT and over L2CAP Connection-Oriented Channels) without using any phone. The image to be transferred is stored in the NVMS_BIN_PART partition in the Flash memory of the BLE central device.

5.2.5.1. Preparing the SUOTA enabled device pxp_reporter

Follow the steps described in Section 5.2.4.1

As soon as program_qspi_jtag is executed, a new window pops up asking to choose which device is the target; select the appropriate device.

../_images/094.png

Figure 49 Selecting target device

5.2.5.2. Preparing the SUOTA update image

See chapter Section 5.2.4.2

5.2.5.3. Preparing the ble_suota_client device

5.2.5.3.1. Building the ble_suota_client application

Import the following projects using SmartSnippets™ Studio from the following locations:

  • python_scripts: <sdk_root_directory>\utilities

  • ble_suota_client: <sdk_root_directory>\projects\dk_apps\features

and select the following build configuration for the ble_suota_client project:

  • ble_suota_client in DA1469x-00-Release_QSPI configuration.

To build and install the ble_suota_client, follow the procedure below:

  1. Build the project ble_suota_client by executing DA1469x-00-Release_QSPI

  2. Erase the flash memory of the device by executing the erase_qspi_jtag script.

  3. Execute the program_qspi_jtag script to program the QSPI Flash memory.

As soon as the script is executed, a new window pops up asking to choose which of the DKs is the target; select the appropriate device.

../_images/087.png

Figure 50 Selecting the target device

5.2.5.3.2. Storing the SUOTA update image in the ble_suota_client device

Note

The SUOTA update image was created in step Preparing the SUOTA update image

Use cli_programmer to download the binary pxp_reporter.img to the device.

> <SDK_ROOT_PATH>\binaries\cli_programmer.exe <SERIAL_PORT> write_qspi 0x300000 <SDK_ROOT_PATH>\projects\dk_apps\demos\pxp_reporter\DA1469x-Debug_QSPI_SUOTA\pxp_reporter.img

5.2.5.3.3. Performing the software upgrade procedure

When the previous procedure has finished, the two DA1469x devices are ready to communicate. To perform SUOTA on the PXP Reporter device, the following steps should be followed:

  • A serial terminal is needed to connect to the ble_suota_client device and perform the SUOTA. In the example below, “Real Term” is used for this purpose. Configure the serial terminal as follows:

../_images/131.png

Figure 51 Configuring the serial port

  • Connect to the serial port and press the K2 RESET button.

../_images/130.png

Figure 52 Connecting to the serial port

Information regarding the image stored in the NVMS_BIN_PART partition are displayed during boot

  • In the serial terminal of the ble_suota_client device, give the following command:

    > scan start

../_images/120.png

Figure 53 Scanning for available devices

The ble_suota_client device starts scanning for available devices immediately (Figure 53). In this in example, from the devices listed Figure 53, the pxp_reporter device, is the device with sequence number [01].

  • As soon as the pxp_reporter device is found, the scanning operation can be stopped with the following command:

    > scan stop

  • A connection to the pxp_reporter device can be initiated with the command:

    > connect 1

The first argument of the “connect” command refers to the device index on the scan result list. Once a connection is established, the application automatically queries the remote device for available services and device information. The characteristic values of the Device Information Service (DIS) are read. The following output is printed on the terminal:

../_images/121.png

Figure 54 Connecting to loader device

The presence of “L2CAP PSM” indicates that the remote device supports SUOTA and over-L2CAP COC.

  • To update a device supporting L2CAP COC over L2CAP, issue the update command. To update the same device over GATT, issue the update gatt command. If the remote device does not support L2CAP COC (“L2CAP PSM” is not displayed), both update and update gatt commands begin SUOTA over GATT.

    > update gatt

../_images/122.png

Figure 55 Updating with new image the loader device

After the image transfer has been completed, the remote device disconnects and reboot as shown in Figure 56.

../_images/123.png

Figure 56 Transfer complete

When the pxp_reporter device reboots, execution of the new image begins.

The software upgrade has finished. Now the pxp_reporter device should start advertising as <Dialog PX Reporter>. To verify that, perfom the scan operation again by issuing the scan start command.

> scan start

../_images/108.png

Figure 57 Verifying that loader is running PX Reporter

5.3. Booting

The Boot ROM code is executed by SYSCPU (Cortex-M33) after a POR or HW Reset. As an additional capability it can be executed after wakeup when the RESET_ON_WAKEUP feature is configured.

Four different booting flavors are supported:

  • Boot from cached QSPI FLASH without any security features, configuration script (CS) in OTP

  • Boot from cached QSPI FLASH without any security features, configuration script (CS) in FLASH

  • Boot from cached QSPI FLASH with security features, configuration script (CS) in OTP

  • Boot from UART without FLASH or security features

In case neither Flash nor UART is available, the booter remains in an infinent loop.

The Configuration Script (CS) is a table of up to 256 32-bit entries. System can boot without CS but if it exists it is stored either in Flash or OTP. The booter expects CS to start at address 0x00000000 of flash or at 0x00000C00 of OTP. It contains information related to booter, like Product header location, the development mode flag, entries which can be used by SDK or application software to apply some values to specific registers (address value pair) or to use them as trim values for a subsystem. The address value pair entry in CS is a way to enable the secure features of the booter. For extra protection in secure boot mode CS is stored to OTP in order to be difficult to change the security related entries. For more info about CS valid entries, please refer to the DA1469x datasheet chapter “System Overview”, paragraph “OTP”, section “Configuration Script”.

5.3.1. Boot flow

The boot flow is divided in five separate phases

  • Initialization: The booter takes care of configuring power domains and peripheral clocks. The boot code resets the OTP (One Time Programmable) - and Flash memory in order to be able to access them. It enables the Development Mode by default. In Development Mode the booter enables the debugger and it is also possible to update the devices firmware by UART. Development mode can be deactivated adding the corresponding entry in CS.

  • Run Configuration Script: The booter searches for the CS in the beginning of Flash or address 0x00000C00 of OTP. If CS is found it starts parsing it and applies its directives, otherwise continues to next state.

  • Retrieve application code: This phase depends on Development mode.

    Development mode enabled
    The booter will enable debugger and try to boot form UART. If it fails to retrieve an application from UART it will try to locate a valid product header. If it finds a product header will continue to next phase otherwise will try to boot from UART again. In case there is no valid product header the booter will enter an infinite loop. This loop is active until it boots form UART or a debugger is connected.

    Development mode disabled
    In this case both debugger and booting from UART feature are disabled. The booter tries to locate and parse the Product and image headers from FLASH. If these headers are invalid, HW Reset is performed.

  • Device administration: The booter will first check if there is any pending update. In case of update it will validate the new firmware image and if the validation succeeds the booter will accept the update. Otherwise the booter will reject the update, trigger a HW Reset and boot the old image. In case there is no pending update the booter will validate the active (old) image. If validation fails the booter will trigger again a HW reset. Image validation depends on secure mode. If the image is no secure then the booter checks for ‘Qq’ in the beginning of the image header. If FW image is secure then booter has to do the below additional checks.

    1. Checks if image header contains the security section (0xAA22).

    2. Checks if image header contains the administration section (0xAA44)

    3. Checks if is able to extract public keys, encryption keys, signature verification value and NONCE.

    4. Checks if the public key used for the signature verification is not revoked.

    5. Checks if application encryption key is not revoked.

    6. Checks if signature value calculated using Ed25519 algorithm is the correct one.

    In case any of the above checks fail, then the booter will trigger a HW reset.

  • Load image: This is the final booting phase in which the actual FW image is loaded. This phase is divided in the following steps:
    1. Flash QSPI controller configuration retrieving information from the Product header.

    2. In case the secure feature is enabled, the decryption key is fetched through DMA to QSPI controller. QSPI controller supports image decryption on the fly.

    3. Interrupt Vector Table (IVT) is copied to RAM

    4. Cache controller is configured to point to the beginning of IVT

    5. QSPI FLASH address is remapped to address 0x0

    6. SW_RESET is triggered

    If any error occurs during this phase a HW reset will be triggered by the booter.

For more details please refer to Figure 58.

../_images/003.png

Figure 58 BootROM flow

5.3.2. FLASH Layout

The bootROM expects at least three different section in FLASH to be able to boot. These are shown in : Figure 59.

../_images/004.png

Figure 59 FLASH layout

Configuration Script is an optional region.

Product header starts with “Pp” and contains information about the address of the active and updated FW image, QSPI controller and the Flash device setup and the CRC code used to verify Product header integrity. There are two Product Headers, Primary and Backup which are identical. The reason is that we want to protect system from a possible Product header corruption. The booter first does a CRC check of the primary header and if is successful it parses it, otherwise does a CRC check of the backup header. In case both headers are corrupted the booter continues as there is no Product Header and behaves as described above in boot flow chapter in paragraph Retrieve application code. If only primary is corrupted, the booter copies backup header to primary header. If active and update FW image addresses are different, the booter will trigger the update procedure. An update image is considered valid when all the checks described in Device Administration phase are successful. An update is considered successful when the booter executes successfully all the boot flow phases described above. * Product Header fields:

  1. Flash Programmed identifier “Pp” Product header identifier

  2. Active FW Image Address address in Flash of the active FW Image header

  3. Update FW Image Address address in Flash of the update FW Image header

  4. Flash BURSTCMDA reg value value written in the QSPIC_BURSTCMDA_REG of the QSPI controller

  5. Flash BURSTCMDB reg value value written in the QSPIC_BURSTCMDB_REG of the QSPI controller

  6. 0xAA11 Flash config section identifier

  7. Length of Flash Config Section size of Flash config section

  8. Flash Write Config reg Command sequence commands for QSPI controller

Image header region starts with “Qq” and is divided into three sections, common, security and administration.

  • Common section fields:

  1. Image identifier (“Qq”) Image header identifier

  2. size the FW image size

  3. CRC CRC value for image integrity check

  4. Timestamp number of seconds passed since epoch (1/1/1970)

  5. IVT offset from the beginning of the Image header to the start of the IVT.

  • Security section fields:

  1. 0xAA22 security section identifier

  2. Length of security section size of security section

  3. Index to ECC key signature key index

  4. Index to Sym. key FW decryption key

  5. NONCE used in FW decryption algorithm

  6. Length of security section size in bytes of the signature value

  7. Signature signature value used for signature verification

  • Administration sections fields:

  1. 0xAA44 administration section identifier

  2. Length of administration section size of administration section

  3. 0xAA55 key revocation section identifier

  4. Length of the key revocation record size of key revocation record

  5. Key Type type of the key(eg ECC, Sym., User data key)

  6. Key Index key index

  7. next key records if are available

For security reasons the key revocation status is stored in a specific area in OTP per key type.

IVT and executable is the section where the IVT and application are stored. In case the boot is secure this section is encrypted.

5.3.3. OTP Layout

The OTP consists of several different segments. None of these segments needed in case of no secure boot. In the below table there is a description of the segments.

Table 40 OTP Layout

Segment

Bytes

Description

OTP Address

1

1024

Configuration Script

~100 registers write operations

0x00000C00

2

256

QSPI FW Decryption Keys Area – Payload

write/read protected when secure mode enabled in CS

Secure mode connects those (8 * 256-bits) keys to QSPI Controller

0x00000B00

3

256

User Data Encryption Keys – Payload

Write/Read protected when secure mode enabled in CS.

Secure mode connects those (8 * 256-bits) keys to AES engine

0x00000A00

4

32

QSPI FW Decryption Keys Area – Index 8 entries for 8 256-bit keys

0x000009E0

5

32

User Data Encryption Keys – Index

8 entries for 8 256-bit keys

0x000009C0

6

256

Signature Keys Area – Payload

0x000008C0

7

32

Signature Keys Area – Index

0x000008A0

8

2208

Customer Application Area (Secondary bootloader, binaries, …)

0x00000000

5.4. Build configurations and startup procedure

This section provides a brief description of the supported project build configurations and the startup procedure of the SmartSnippets™ DA1469x SDK.

5.4.1. Build configurations

SmartSnippets™ DA1469x SDK projects by default support the following two types of build configurations:

Table 41 Build Configurations

Build Configuration

Location of code and read-only data

Cache enabled

Description

RAM

RAM

No

Program is loaded directly to RAM.

QSPI

Flash (QSPI mode)

Yes

Program runs in-place from QSPI flash.

5.4.1.1. QSPI build configuration

In this mode, the code will be placed in FLASH whereas variables will be placed in RAM. It is also possible to move specific functions to RAM using the __RETAINED_CODE macro. This could be useful when a particular function implements time critical functionality. A program built for flash cached mode is written into QSPI flash memory with the CLI programmer tool. Along with the application image, the QSPI Flash must be also programmed with valid Product and Image Headers. After a hard reset, the booter reads these headers to set up the external FLASH memory and prepare the application image before jumping to it.

After hard reset, ROM is remapped to address 0x0 and the booter starts executing. At first, the booter will read the Product and Image headers (located to FLASH) to verify that a valid firmware image is programmed to Flash. It will also program the QSPI FLASH controller according to the contents of the relevant fields in the Product Header to enable QSPI Auto mode and execution in place (XIP). Before starting executing the programmed firmware image, the booter sets up the cache controller and copies application’s IVT (Interrupt Vector Table) from FLASH to RAM. SYS_CTRL_REG[REMAP_INTVECT] is enabled so that the virtual addresses 0 - 0x1FF are mapped to the beginning of RAM, i.e. memory area 0x00000000 0x000001FF is mapped to memory area 0x20000000 0x200001FF. This ensures that the IVT is always located in RAM for quick access. SYS_CTRL_REG[REMAP_ADR0] is set to 2 (FLASH is remapped to virtual address 0x0) and a software reset is applied. CPU fetches the location of Reset_Handler from RAM (since SYS_CTRL_REG[REMAP_INTVECT] is enabled), and the Reset_Handler starts executing from FLASH.

A program built with the QSPI build configuration can be executed using the QSPI_DA1469x debug configuration using the SmartSnippets™ Studio. It is also possible to execute the image be pressing the Reset button on the development kit, after programming it on the Flash.

5.4.1.2. RAM build configuration

In this mode, both code and data are placed in RAM. A program built with the RAM build configuration can be loaded to RAM either directly using the ELF file and J-Link debugger or be first converted to a raw binary and then written to RAM with the CLI programmer tool. The RAM mode is used only for debugging purposes as it avoids the step of programming the QSPI flash. After loading the program, SYS_CTRL_REG[REMAP_ADR0] must be configured so that RAM is mapped to address 0x00000000. After a soft reset is issued, the written program starts execution. The RAM mode does not rely on the boot loader. Therefore, the Product and Image headers in QSPI Flash don’t affect the firmware execution.

A program built with the RAM build configuration can be executed using the RAM_DA1469x debug configuration using the SmartSnippets™ Studio. It is also possible to load and execute the image using the CLI programmer tool. In both cases SYS_CTRL_REG[REMAP_ADR0] is configured appropriately by the corresponding tool.

5.4.2. Startup procedure

The startup code is the part of the program that runs after reset and before entering main(). Briefly, it consists of the following steps, as shown in Figure 60 (please consult the startup code within the SmartSnippets™ DA1469x SDK for details):

../_images/startup_procedure.png

Figure 60 Startup procedure

  1. Reset_Handler in sdk\bsp\startup\startup_da1469x.S starts execution.

  2. SystemInitPre() in sdk\bsp\startup\system_da1469x.c gets called, to do the following:

    • Enable debugger (if the corresponding macro, dg_configENABLE_DEBUGGER, is enabled).

    • Set dividers of the AMBA High Speed Bus and Peripheral Bus.

    • Check IC version compatibility with SW.

    • Check alignment of copy and zero tables.

    • Bring the pad latches, the memory controller, the PDC and the peripherals’ clocks to a well known initial state.

Note

No variable initialization should take place here, since copy & zero tables have not yet been initialized yet and any modifications to variables will be discarded. For the same reason, functions that initialize or are using initialized variables should not be called from here.

  1. Reset_Handler in sdk\bsp\startup\startup_da1469x.S resumes execution.

    • Copy code and data to RAM according to the .copy.table section.

    • Initialize certain memory areas to zero according to the .zero.table section.

  2. SystemInit() in sdk\bsp\startup\system_da1469.c gets called, to do the following:

    • Configure interrupt priorities.

    • Configure the QSPI Flash and Cache. This is done to fine-tune the configuration applied by the booter code for the particular flash model.

    • Read Trim and Calibration Section (TCS) values from One-Time Programmable (OTP) memory

    • Configure the power domain controller (PDC).

    • Bring power domains to a well known initial state.

    • Activate BOD protection.

  3. __START is called, which initializes some libc structures and then calls main().

5.5. Memory Layout

This section gives a brief description on the available program and data memories. Information for the memory layout is given in Figure 61 and in Table 42 below.

../_images/da1469x_system_mem.png

Figure 61 System Memory

Table 42 Memory Layout

Memory Device

Start Address | End Address

Size (bytes)

Size (KiB)

Remapped Devices

0x00000000

0x00800000

0x800000

8192

ROM

0x00900000

0x00920000

0x020000

128

OTP

0x10080000

0x10081000

0x001000

4

RAM (Code Interface)

0x00800000

0x00880000

0x080000

512

RAM (System Interface)

0x20000000

0x20080000

0x080000

512

QSPI FLASH (Code Interface, cached)

0x16000000

0x16800000

0x800000

8192

QSPI FLASH (System Interface, uncached)

0x36000000

0x36800000

0x800000

8192

5.5.1. ROM

The 128KiB ROM is mapped to address 0x900000 and is only used for executing the booter code. This is the first piece of code that will be executed after a HW reset, before jumping to the application.

5.5.2. OTP

The 4KiB one-time programmable (OTP) memory is accessible at address 0x10080000 and its main purpose is to store information (Product Header and Configuration Script) that will be used by the booter before jumping to the application.

5.5.3. System RAM

The RAM (up to 512KiB) is mapped to addresses 0x80000 (Code Interface) and 0x20000000 (System Interface). Accesses to RAM are never cached. In BLE projects, some memory is used by the CMAC CPU. The system RAM comprises 8 RAM cells (64KiB in size, each), which can be indpendently configured to not retain their content during sleep, to decrease power consumption. It is also possible for a RAM cell to be completely disabled in case it is not needed by the application.

5.5.4. External QSPI Flash

The external FLASH memory (up to 8MiB) is mapped to addresses 0x16000000 (Code Interface) and 0x36000000 (System Interface). Accesses to the external FLASH memory are only cached when performed through the Code Interface (or the Remapped Region) and are performed transparently thanks to the use of the internal QSPI FLASH controller. The BLE Host and application code are located here.

5.5.5. Remapped Region

Depending on the value of SYS_CTRL_REG[REMAP_ADR0] register field, accesses to the address region [0x0, 0x7FFFFF] can be mapped to

  • The ROM (0x900000)

  • The system RAM (0x80000)

  • The External QSPI Flash address range (0x16000000)

When SYS_CTRL_REG[REMAP_INTVECT] is set to 1, the address range [0x0, 0x1FF] is mapped to System RAM [0x800000, 0x80001FF] address range, allowing to have the interrupt vector table in RAM while executing from FLASH.

When FLASH is remapped to address 0x0 (SYS_CTRL_REG[REMAP_ADR0] is set to 2) and the cache is enabled, the effective FLASH address for an address in the range [0x0, 0x7FFFFF] used by the CPU or the Debugger depends on the value of CACHE_FLASH_REG:

  • FLASH_REGION_SIZE (CACHE_FLASH_REG[31:16]) is used to split the FLASH region in equal-sized subregions.

  • FLASH_REGION_OFFSET (CACHE_FLASH_REG[15:4]) defines the offset for each subregion.

  • FLASH_REGION_BASE (CACHE_FLASH_REG[2:0]) defines the FLASH region base.

The effective FLASH address for a remapped address can be calculated as follows:

(CACHE_FLASH_REG[FLASH_REGION_SIZE] << 16) + (CACHE_FLASH_REG[FLASH_REGION_OFFSET] << 2) + ADDR

This enables an application image compiled to execute from address 0x0, to be placed in different FLASH locations to allow firmware update.

Consider for example the case where CACHE_FLASH_REG has the value 0x16009006:

  • FLASH_REGION_BASE is 0x1600.

  • FLASH_REGION_OFFSET is 0x900. Since this field is in 32-bit words, the actual offset in bytes is 0x2400.

  • FLASH_REGION_SIZE is 0x6. This value corresponds to a flash region size of 0.5MiB (0x80000).

Any CPU or Debugger access to address 0x1000 will be remapped to FLASH address 0x16002400 + 0x1000. Since CACHE_FLASH_REG[FLASH_REGION_BASE] is 0x6 (0.5 MiB), the valid address range that the CPU is allowed to access through the remapped region will be [0x0, 0x7FFFF - 0x2400], which translates to the [0x16002400, 0x1607FFFF] FLASH range. Trying to access an address outside this region will give undefined results. The same restrictions apply also to the code memory range, e.g. the CPU will be able to access only addresses [0x16000000, 0x1607FFFF] using the code memory range, 0x16000000 to 0x167FFFFF. Note though, that the address range [0x16000000, 0x160023FF] will be accessed uncached. However, the debugger can access the whole code memory range, 0x16000000 to 0x167FFFFF, regardless of the CACHE_FLASH_REG configuration.

The CACHE_FLASH_REG is configured to the proper value during booter execution, depending on the product and application FW headers.

Both the CPU and the debugger are able to access the whole FLASH address range without restrictions using the uncached system memory range, 0x36000000 to 0x367FFFFF.

5.6. Non Volatile Memory Storage (NVMS)

The SmartSnippets™ DA1469x SDK defines a software layer for Non Volatile Memory Storage management. It is essential for the developer to have a clear understanding of the requirements for the following elements that could use non-volatile storage:

  • System Parameters that need to be stored in NVM (e.g. device address)

  • Firmware upgrade (dual images)

  • Application specific binaries (e.g. pre-recorded audio messages)

  • Logging (e.g. event logs)

  • Application data (e.g. sensor values, statistics, authentication keys)

For each storage type a corresponding, dedicated region is allocated in the Flash partition table. Each region is identified by a partition ID. When the NVMS Adapter makes read/write accesses to storage, it uses the partition ID and an offset. Additional details can be found in the NVMS Adapter section, Section 4.2.4.

The SmartSnippets™ DA1469x SDK defines the following FLASH partitions (in a non-SUOTA build) to manage the storage:

  • (FW) Firmware Image Region

  • (PARAMS) Parameters Region

  • (BIN) Binaries Region

  • (LOG) Logging of events or values

  • (DATA) Generic data Region, Statistics etc.

The exact Memory mapping depends on the actual Flash device (i.e. size, sector size) used on the board. This mapping needs to be defined at compile-time in the file <sdk_root_directory>/bsp/config/4M/partition_table.h (or <sdk_root_directory>/bsp/config/4M/suota/partition_table.h when SUOTA is used). A default partition table is provided with the SmartSnippets™ DA1469x SDK, which fits in the DK QSPI Flash (4MiB with sectors of 4KiB), the actual definition of which is shown in Code 32:

Code 32 NVMS mapping
     PARTITION2( NVMS_PRODUCT_HEADER_PART  , 0 )
     PARTITION2( NVMS_FIRMWARE_PART        , 0 )
     PARTITION2( NVMS_GENERIC_PART         , PARTITION_FLAG_VES )
     PARTITION2( NVMS_PLATFORM_PARAMS_PART , PARTITION_FLAG_READ_ONLY )
     PARTITION2( NVMS_PARAM_PART           , 0 )
     PARTITION2( NVMS_LOG_PART             , 0 )
     PARTITION2( NVMS_BIN_PART             , 0 )
     PARTITION2( NVMS_PARTITION_TABLE      , PARTITION_FLAG_READ_ONLY )

When SUOTA is implemented, an additional partition is required so that both the current and the updated image can be stored, as shown in Code 33:

Code 33 NVMS mapping (SUOTA)
     PARTITION2( NVMS_PRODUCT_HEADER_PART  , 0 )
     PARTITION2( NVMS_FW_EXEC_PART         , 0 )
     PARTITION2( NVMS_GENERIC_PART         , PARTITION_FLAG_VES )
     PARTITION2( NVMS_PLATFORM_PARAMS_PART , PARTITION_FLAG_READ_ONLY )
     PARTITION2( NVMS_PARAM_PART           , 0 )
     PARTITION2( NVMS_FW_UPDATE_PART       , 0 )
     PARTITION2( NVMS_LOG_PART             , 0 )
     PARTITION2( NVMS_PARTITION_TABLE      , PARTITION_FLAG_READ_ONLY )

Note

NVMS_PARTITION_TABLE should be always located at the offset defined by the PARTITION_TABLE_ADDR configuration macro.

5.7. QSPI FLASH Support

This section describes the QSPI Flash support in SmartSnippets™ DA1469x SDK and the steps the user needs to follow in order to add support for new Flash types.

The SDK supports by default three different flash devices:

  • Macronix: MX25U3235F, 32Mbit

  • Winbond: W25Q32FW, 32Mbit

  • Gigadevice: GD25LE32, 32Mbit

These three devices have been tested with the SDK release using the modes of operation listed below. The default device is the Macronix MX25U3235F. This device is mounted on both the ProDK and USB-Kit boards. Another device can be selected by changing the macros shown in section Section 5.7.4.

Section Section 5.7.7 explains how to add support for other flash devices that have the same boot sequence as the three supported devices. The user will need to check carefully the Flash command set and verify correct read/write/erase operation at the desired clock speed.

5.7.1. Modes of operation and configuration

The SmartSnippets™ DA1469x SDK supports two modes of operation: Autodetect mode and Manual mode. The Autodetect mode can detect the flash type at runtime, while the Manual mode requires explicitly declaring the flash used in the project at compile time.

Note

The Manual Mode is the default and recommended mode. The Autodetect Mode will greatly increase code size and Retained RAM usage, and may prevent the project fitting in RAM.

5.7.2. Autodetect Mode

The Autodetect mode detects the flash that is used at runtime, and selects the proper flash driver to use. The Autodetect mode can only detect among the flash devices officially supported by the SmartSnippets™ DA1469x SDK. If no match is found, a default driver will be used (which may or may not work).

Since the Autodetect mode needs to select the driver to use at runtime, it has the code for all the drivers in the binary. It also keeps the selected driver’s configuration parameters in Retained RAM. Therefore, the Autodetect mode is NOT recommended for production builds.

5.7.3. Manual Mode

The Manual mode simply consists of a hardcoded declaration of the flash driver to use. Therefore, only the code of the selected driver is compiled in the binary, and there is no need to retain the driver parameters in Retained RAM, since the compiler optimizes them out. This mode is suitable for Production builds.

5.7.4. Flash Configuration

The Flash subsystem is configured using the macros shown in Table 43, which must be defined in the config/custom_config_qspi.h file of the project:

Table 43 Macros for the configuration of the Flash subsystem

qspi_flash_config_t field

Description

dg_configFLASH_AUTODETECT

Default: 0. This macro, if set, enables the Autodetect Mode. Please note, that the use of this macro is NOT recommended.

dg_configFLASH_HEADER_FILE

This macro must be defined as a string that denotes the header file to use for the specific flash driver. E.g. qspi_mx25u323.h, qspi_qd25le32.h, qspi_w25q32fw.h. This header file must be either one of the qspi_<part_nr>.h header files found in <sdk_root_directory>/sdk/bsp/memory/include, or a header file under the project’s folder, as long as this path is in the compiler’s include search path (see the document section Section 5.7.7 about adding support for new flash devices).

When the system is in Manual Mode (dg_configFLASH_AUTODETECT == 0), which is the default, all the macros above are defined in sdk/config/bsp_defaults.h to enable the default flash used, which is the Macronix MX25U3235F.

5.7.5. Code Structure

The QSPI Flash access functionality is implemented in qspi_automode.c and qspi_automode.h file. Common command definitions and functions needed for all devices are declared in qspi_common.h. Device specific code is defined in header files named as qspi_<flash device name>.h.

The code in qspi_automode.c (and in some other parts of the SmartSnippets™ DA1469x SDK), calls device-specific functions and uses device-specific values to properly initialize the flash device. Each driver header file provides an instance of the structure qspi_flash_config_t to the main driver, containing all the device-specific function pointers and variables.

5.7.6. The flash configuration structure

Each driver header file must provide its own instance of qspi_flash_config_t. Please note that this instance must be named with a unique name, like flash_<device name>_config, since all the device header files are included in the qspi_automode.c file. Therefore, there is a single global namespace. Also, please note that the struct instance must be declared as const so that the compiler can optimize references to it.

The qspi_flash_config_t structure, as shown in Table 44, has the following fields (see <sdk_root_directory>/sdk/memory/include qspi_common.h for more information):

Table 44 The qspi_flash_config_t structure

qspi_flash_config_t field

Description

initialize

Pointer to the flash-specific initialization function.

is_suspended

Pointer to a flash-specific function that checks if flash is in erase/program suspend state.

sys_clk_cfg

Pointer to a flash-specific function that performs Flash configuration when system clock is changed (e.g. change dummy bytes or QSPIC clock divider).

get_dummy_bytes

Pointer to a flash-specific function that returns the number of dummy bytes currently needed (it may change when the clock changes).

manufacturer_id

The Flash JEDEC vendor ID (Cmd 0x9F, 1st byte). This and the device_type and device_density are needed for flash autodetection, when in Autodetect mode.

device_type

The Flash JEDEC device type (Cmd 0x9F, 2nd byte).

device_density

The Flash JEDEC device density (Cmd 0x9F, 3rd byte).

erase_opcode

The Flash erase opcode.

erase_suspend_opcode

The Flash erase suspend opcode.

erase_resume_opcode

The Flash erase resume opcode.

page_program_opcode

The Flash page program opcode. For PSRAM memories this is the write opcode.

page_qpi_program_opcode

The Flash QPI page program opcode to use.

quad_page_program_address

If true, the address will be transmitted in QUAD mode when writing a page. Otherwise, it will be transmitted in serial mode.

read_erase_progress_opcode

The opcode to use to check if erase is in progress (Usually the Read Status Reg opcode 0x5).

enter_qpi_opcode

The Flash opcode for entering QPI mode.

erase_in_progress_bit

The bit to check when reading the erase progress.

erase_in_progress_bit_high_level

The active state (true: high, false: low) of the bit above.

send_once

If set to 1, the “Performance mode” (or “burst”, or “continuous”; differs per vendor) will be used for read accesses. In this mode, the read opcode is only sent once, and subsequent accesses only transfer the address.

extra_byte

The extra byte to transmit, when in “Performance mode” (send_once is 1), that tells the flash that it should stay in this continuous, performance mode.

address_size

Whether the flash works in 24- or 32-bit addressing mode.

memory_size

Maximum capacity, memory size of selected device in Mbits.

break_seq_size

Whether the break sequence, that puts the flash out of the continuous mode, is one or two bytes long (the break byte is 0xFF).

ucode_wakeup

The QSPIC microcode to use to set up the flash on wakeup. This is automatically used by the QSPI Controller after wakeup, and before CPU starts code execution. This is different, based on whether the flash was active, in deep power down or off while the system was sleeping.

power_down_delay

The maximum time required in usec, after the Power Down command, in order for the flash memory to enter into the deep power down mode.

release_power_down_delay

The maximum time required in usec, after the Release Power Down command, in order for the flash memory to exit from the deep power down mode.

power_up_delay

The maximum time required in usec, to power up the flash memory.

suspend_delay_us

The minimum time required in usec, between an erase/program suspend command and the moment when the memory is ready to accept the next consecutive command.

resume_delay_us

The minimum time required in usec between an erase/program resume command and the moment when the memory is ready to accept the next consecutive command.

reset_delay_us

The minimum time required in usec, between a reset command and the moment when the memory is ready to accept the next consecutive command.

read_cs_idle_delay_ns

The minimum time required in nsec, that the CS signal has to stay in idle state between two consecutive read commands. Also referred as “read CS deselect time”.

erase_cs_idle_delay_ns

The minimum time required in nsec that the CS signal has to stay in idle state between a write enable, erase, erase suspend or erase resume command and the next command. Also referred as “erase/write CS deselect time”.

is_ram

True if device is RAM, false if device is Flash.

qpi_mode

True if the device operates in QPI bus mode. Applicable to PSRAM memories only (QSPIC2).

burst_len

The length of the wrapping burst that the external memory device is capable to implement. Applicable to PSRAM memories only (QSPIC2).

cs_active_time_max_us

The maximum time in usec, that the QSPIC2 CS signal is allowed to stay active. Applicable to PSRAM memories only (QSPIC2).

In Autodetect mode, these structures reside in the .rodata section of the code. Upon the initialization of the flash subsystem, the JEDEC ID (command 0x9F) of the flash is read to identify the attached device. The obtained JEDEC ID helps selecting the appropriate flash_<flash device>_config structure, and copying it to the Retained RAM. From this point onward, the structure is used where necessary to all flash operations.

On the other hand, in Manual mode no JEDED ID is read, and thus no structure is copied to the Retained RAM. As a substitute, the constant pointer flash_config is directly assigned to the specific constant flash_<device name>_config structure, which resides in the header file of the selected device driver. The compiler then optimizes out the entire structure.

5.7.7. Adding support for a new flash device

The SmartSnippets™ DA1469x SDK driver subsystem currently supports a specific set of QSPI flash devices. It provides, however, the capability to add support for other flash devices as well.

Each device driver must have its own header file that should be named qspi_<device name>.h. The programmer can either use the qspi_XXX_template.h, or start with an existing driver file.

Warning

The new flash driver file should be placed inside the project’s path, in a folder that is in the compiler’s include search path (an obvious choice is the config folder, but others can be used as well). This is recommended so that potential SDK upgrades will not interfere with the project-specific flash driver implementation.

Common code among flash families or vendors can be factored out in common header file per family/vendor. There are currently such common header files, like qspi_macronix.h and qspi_winbond.h. However, this is NOT necessary; moreover, it is the responsibility of the device driver header file to include the common header file, if needed.

Note

A custom flash driver can ONLY be used in Manual mode, which means that the macros described in Table 43 MUST be defined in config/custom_config_qspi.h.

The following steps are usually needed to create the new flash driver:

  1. Copy and rename the template header file, or an existing driver file.

  2. Rename all the functions and variables appropriately. It is important to remember that all the drivers reside in the same namespace and so all function and variable names must be unique.

  3. Define the proper JEDEC ID values for the Manufacturer code, the device type and the device density.

  4. Verify that the suspend, resume, exit power-down, enter power-down, fast read, write enable, read commands are valid for the new device type.

  5. Guard the header file using an #if preprocessor macro that checks for the specific driver selection.

Code 34 Example flash guard macros
#if (dg_configFLASH_MANUFACTURER_ID == WINBOND_ID && dg_configFLASH_DEVICE_TYPE == W25Q32FW && dg_configFLASH_DENSITY == W25Q_32Mb_SIZE)
  1. Define any other driver-specific macros that are needed (like timings etc).

  2. Define the constant wakeup microcode arrays that will be needed, per configuration mode that will be supported (dg_configFLASH_POWER_OFF, dg_configFLASH_POWER_DOWN or none of them). The microcode will be copied during the driver initialization in a special memory in the QSPI controller, and will be used after system wakeup to initialize the QSPI (since the CPU isn’t yet running code at this time). Please see [Ref_01] for the uCode format.

  3. Declare the constant struct instance of type qspi_flash_config_t, named flash_<device name>_config, and initialize it with proper values. Please note that this must be declared as const.

  4. Extend the function flash_<device name>_initialize() if needed, e.g. to write some special QSPI configuration registers or the QUAD enable bit. Otherwise, leave empty.

  5. Extend the function flash_<device name>_sys_clock_cfg() if needed. This can include modifying the dummy bytes when the system (and hence the QSPI) clock changes, or changing the QSPI clock divider (if, for example, the flash device cannot cope with 96MHz). Otherwise, leave empty.

  6. The function is_suspended() should read the flash Status Register and return true if Erase or Write is suspended on the device.

  7. If Continuous Read Mode (sometimes referred to as Performance or Burst Mode) is used, make sure to set send_once to 1, and set extra_byte to a proper value for the flash to keep working in this mode. This is device-specific.

  8. If the flash supports 32-bit addressing, make sure to use the proper uCode for wakeup. Also set page_program_opcode, erase_opcode, break_seq_size (this should also take into consideration whether the device will be working in Continuous Read mode as well) and address_size.

  9. If the address, during write, will be provided in QUAD mode, set quad_page_program_address to true.

Note

The SmartSnippets™ DA1469x SDK supports reading in QUAD I/O mode (where the address and data are read in QUAD mode, and only the command is transferred in serial mode), both in Continuous Read and normal mode.

5.7.8. Working with a new flash device

Read/Erase/Program of new QSPI flash devices should be done using SmartSnippets™ Studio standard procedure (check the “General Installation and Debugging Procedure” page of [Ref_04]).

Before working with new QSPI flash devices the following steps are required:

  • Support for the new flash is added to the SDK as described in paragraph Section 5.7.7.

  • SDK uartboot project (the secondary bootloader used by SDK flash programming tools) is built with support for the new QSPI flash as described in paragraph Section 5.7.4.

  • SDK cli_programmer project (the tool used in the SDK for accessing flash) is re-built in DA1469x_Release_static_linux (if working in Linux) or DA1469x_Release_static_win32 (if working in Windows) configuration, as described in the “CLI programmer application” page of [Ref_04].

  • When the program_qspi_config tool is run through SmartSnippets™ Studio, the available FLASH devices along with their burst commands (that will be placed in the Product Header FLASH section) are loaded from the utilities\python_scripts\qspi\flash_configurations.xml XML file. To avoid changing the default SDK XML file, a new one should be created at a given path that must be then provided to utilities\python_scripts\qspi\program_qspi_config.py as follows:

    Code 35 Passing custom FLASH configuration file to program_qspi_config.py
    python program_qspi_config.py --prod_id DA1469x-00 -fc custom_flash_configurations.xml
    

    After executing program_qspi_config.py, a new program_qspi.xml file will be generated inside the utilities\python_scripts\qspi path. Since this is the file used when program_qspi_config tool is run from within SmartSnippets™ Studio to program a new image to the device, the product header is going to be programmed according to the specifications of the new FLASH device.

Note

SmartSnippets™ Toolbox only supports read/erase/programming of the default supported QSPI flash devices. Therefore, it is not recommended to be used with new flash devices.

5.7.9. Testing a new flash device

To test the flash driver, use the PXP Reporter demo application, and configure the new flash driver in its custom_config_qspi.h and custom_config_qspi_suota.h files. Do the following tests:

  1. Verify that the application boots by using SmartSnippets™ Studio Power Profiler and a cell phone to connect to the device.

  2. Verify that the application continues working after the system starts going to sleep (after ~8 seconds), that the cell phone can connect to the device and that it can maintain the connection for a while.

  3. Repeat steps 1 and 2 by changing the application clock to 96 MHz (change sysclk_XTAL32M in main.c to sysclk_PLL96)

  4. Repeat steps 1, 2 and 3 and change dg_configPOWER_1V8_SLEEP to 0 (in flashes where this makes sense) and (separately), dg_configFLASH_POWER_DOWN to 1, to test the supported wake up sequence driver modes.

  5. Repeat steps 1 through 4 using the SUOTA Configuration of the PXP Reporter application. This will test the write/erase functionality of the driver.

5.8. Sensor Node Controller Programming Framework

5.8.1. Introduction - SeNIS

The Sensor Node Controller (SNC) is a sophisticated hardware state machine capable of executing a very limited particular set of instructions while operating autonomously (without the rest of the system being wοken up) and dissipating minimal power.

The SNC Instruction Set (named SeNIS), is summarized in Table 45. It allows operations such as

  • polling sensor status bits

  • comparing register to memory address contents (values)

  • transferring data from communication interfaces to system RAM

  • branching on comparisons

etc. and is therefore enough for creating small programs (named uCodes) for manipulating the communication controllers and the sensors connected to them.

Table 45 Sensor Node Instruction Set (SeNIS) Overview

Instruction

Description

WADAD

Operand1, Operand2

Store the contents of the Register/RAM address* defined by the value in Operand2 to the address* defined by the value in Operand1.

WADVA

Operand1, Operand2

Store the value in Operand2 to the address* defined by the value in Operand1.

TOBRE

Operand1, Operand2

XOR the contents of the Register/RAM address defined by the value in Operand1 with the mask defined in Operand2. If the mask contains “1” at a specific bit place, then this bit’s value is toggled.

RDCBI

Operand1, Operand2

Read and compare the contents of the Register/RAM address defined by the value in Operand1 with “1” in the specific bit position and set EQUALHIGH_FLAG=true.

RDCGR

Operand1, Operand2

Compare the contents of the Register/RAM address defined by the value in Operand1 with the contents of the address defined by the value in Operand2 and set GREATERVAL_FLAG=true.

COBR

Operand1, Operand2

Branch to the address* defined by Operand1 according to EQUALHIGH_FLAG or GREATERVAL_FLAG or for a specific number of times (depending on the value in Operand2).

INC

Operand1

Increment the contents of the RAM address defined by the value in Operand1 either by 1 or by 4.

DEL

Operand1

Start a delay of a number of ticks defined by the value in Operand1, where a tick the minimum interval of an 8-bit timer running on the low power clock (32 kHz). After timer expires, program execution continues.

SLP

Designate the end of SNC execution. A signal pulse to PDC is automatically generated in order to set the system to sleep and power down SNC.

NOP

No operation.

* Depending on the addressing mode that is selected for the specific command, the Operand’s value can be either
  • an address to the System Ram or a Register (direct addressing) or

  • a pointer to a memory space in which the requested address resides (indirect addressing).

Details regarding the architecture and functionality of the SNC (e.g. the addressing mode selection) can be found in [Ref_01].

5.8.2. Sensor Node Controller Programming Model Overview

In order to facilitate the development of DA1469x applications integrating both OS (FreeRTOS) tasks and SNC uCodes, a Sensor Node Controller Programming Framework is provided by the SmartSnippets™ DA1469x SDK.

../_images/fig_SNC_3.png

Figure 62 Sensor Node Controller Programming Model overview

More specifically, as shown in Figure 62, the supported SNC programming model can be summarized as follows:

  • A DA1469x application includes processes executed by OS (FreeRTOS) tasks (i.e. CM33 execution context) and SNC uCodes (i.e. SNC execution context) in parallel.

  • An SNC uCode is registered to or unregistered from the DA1469x system by the SNC Adapter, which creates a list of SNC uCodes, each triggered by a specific PDC event.

  • Each SNC uCode is registered to a PDC event entry, so that it can be executed when the event is triggered.

  • Execution priorities for the registered SNC uCodes per triggered PDC event entry are defined.

  • The SNC Adapter employs a special SNC uCode (i.e. SNC-main uCode) which implements the scheduling of the registered SNC uCodes execution based on the respective uCode list, and it controls the SNC HW module using its low level driver.

  • A set of SeNIS-based construct C preprocessor macros are defined, resulting in a set of assembly and C-like language constructs to be used for SNC uCode development.

  • A set of low level driver SNC uCodes are provided which can be used in order to drive communication peripherals such as SPI, I2C etc.

  • A mechanism is provided for FreeRTOS tasks and SNC uCodes comprising an application to exchange:

    • Notifications

    • Data (i.e. SNC Queues) (if dg_configUSE_SNC_QUEUES set to “1”)

  • A mechanism is provided for debugging SNC uCodes, using SNC breakpoints and step-by-step debugging regions (if dg_configUSE_SNC_DEBUGGER set to “1”).

  • An SNC Emulator may be used instead of the SNC HW module in order to improve and ease SNC uCode debugging process (if dg_configUSE_HW_SENSOR_NODE_EMU set to “1”).

5.8.3. Sensor Node Controller Adapter

As presented in Section Section 5.8.2, part of the SNC Programming Framework is the SNC Adapter, implemented in ad_snc.c, through which SNC uCodes can be easily integrated in a DA1469x software system. This is accomplished by hiding the complexity of controlling the underlying system (i.e. registers, IRQs etc.), implementing respective mechanisms for registering/unregistering, configuring and communicating with the SNC uCodes (more specifically, the SNC uCode-Blocks (see Section Section 5.8.4)) in SYSCPU execution context (i.e. CM33 - FreeRTOS tasks), where the main operations of the system reside and coordinate all processes and data manipulation in order to support a set of applications employing the capabilities of the DA1469x ecosystem.

An overview of the functionality supported by the SNC Adapter is shown in Figure 63, while more details for each of the presented processes, corresponding to separate sets of functions in the SNC Adapter API (defined in ad_snc.h), are given in the following subsections. Further technical information regarding the usage of the API (functions parameters, structures, enumerations etc.) can be found in [Ref_04].

In order to enable the supported functions, dg_configSNC_ADAPTER macro must be defined and set to “1” in the project’s configuration settings (config/custom_config_xxx.h).

../_images/fig_SNC_4.png

Figure 63 Using SNC Adapter to register and communicate with an SNC uCode

Regarding the main elements involved in the implementation of the SNC Adapter, they can be listed as follows:

  • List of registered PDC events and uCodes: It is the list of uCodes (i.e. uCode-Blocks) which have been registered to the SNC Adapter, thus executing on SNC when PDC events are triggered. The defined priorities for the uCodes and the PDC events to which the uCodes are registered, determines the structure of that list, essentially resulting in a multi-level list per PDC event and priority as shown in Figure 64.

  • SNC-main-uCode: It is the uCode which executes first when SNC wakes-up by a triggered PDC event. It checks the status of the uCode list and drives execution flow through each of the registered uCodes, based on the PDC event that has been triggered and the PDC event they are registered to. The execution of the uCodes is non-preemptive, therefore in order to register or unregister a uCode, which means updating the respective list of uCodes, requires to halt and resume the SNC execution in SNC-main-uCode context.

  • Sensor Node Adapter task: It handles the registration and un-registration of SNC uCodes to the SNC Adapter. Its stack size may be adapted accordingly, depending on the size of the SNC uCodes comprising an application. Upon its initiation, it adds a PDC event entry in order to support SNC-to-CM33 notifications, creates the SNC-main-uCode and registers the starting address of the latter as the SNC base address, which is the memory address from where SNC starts execution of SeNIS commands.

  • Sensor Node Adapter IRQ task: It handles the notifications being sent through the Sensor_Node_Handler() IRQ handler function by the SNC uCodes to the OS tasks running on SYSCPU, by changing the execution flow from ISR context to OS context and dispatching it through the respective callback functions registered for each uCode

  • Sensor Node Emulator task: It is the OS task which executes the SW Instructions-FSM of the SNC Emulator (see Section Section 5.8.6) concurrently (not in parallel, as SNC HW does) to the rest of the OS tasks of the system.

  • SNC queues: They are special queues used for supporting data exchange between processes executing in SYSCPU (CM33) context and uCodes executing in SNC context. They implement a circular buffer which can be handled in a FIFO manner, where the included data is organized in chunks. An SNC queue chunk has header (preamble and timestamp field) and body, and in the typical case corresponds to the maximum memory space required for writing or reading data being exchanged with SYSCPU processes (i.e. FreeRTOS tasks) when an SNC uCode is executed. The structure of an SNC queue is shown in Figure 65.

../_images/fig_SNC_5.png

Figure 64 Structure of the multi-level priority-list of the registered PDC events and uCodes to the SNC Adapter

../_images/fig_SNC_6.png

Figure 65 Structure of an SNC queue

5.8.3.1. Initializing the SNC Adapter

The SNC Adapter has to be initialized before being used, by calling ad_snc_init() API function. As in the cases of all the software adapters provided by the SDK, this is performed when the system is initialized after power-up, calling pm_system_init() function of the system power manager component implemented in sys_power_mgr.h|c.

Function

Description

ad_snc_init()

Initialize the SNC Adapter.

More specifically, when initializing the SNC Adapter the following are performed:

  • Initialization of resources such as MUTEXs, events and queues employed by the OS tasks utilized for the SNC Adapter implementation.

  • Initialization of the maintained list of registered PDC events and SNC uCodes.

  • Creation and initiation of the SNC Adapter OS tasks (see Section Section 5.8.3.2), namely Sensor Node Adapter task (at its initiation the SNC-main-uCode is created), Sensor Node Adapter IRQ task and Sensor Node Emulator task (if SNC Emulator is enabled - see Section Section 5.8.6).

5.8.3.2. Registering/Unregistering uCodes

Having initialized the SNC Adapter, it is ready for registering and unregistering uCodes, accordingly. Following the paradigm of creating and deleting OS tasks that is adopted throughout the SDK and the applications implementation in SYSCPU execution context, the SNC uCodes can be created and registered to a PDC event, so that they can execute when the particular event is triggered, or they can be unregistered from a PDC event and deleted when they are not needed any more, freeing their resources. Those two operations are provided by the API functions ad_snc_ucode_register() and ad_snc_ucode_unregister(), respectively.

Function

Description

ad_snc_ucode_register()

Register a uCode-Block to a PDC event.

ad_snc_ucode_unregister()

Unregister a uCode-Block from a PDC event.

Note: The uCode ID to be passed as argument is returned when calling ad_snc_ucode_register() in order to register the uCode-Block.

Note

A SNC uCode that has been registered, should also be enabled in order to be executed. Thus, after ad_snc_ucode_register(), ad_snc_ucode_enable() function should be called (see Section Section 5.8.3.3) for enabling the uCode execution.

In case of registering an SNC uCode, it is necessary to pass the address of its context data structure (i.e. ucode_ctx), as well as its configuration (i.e. cfg) regarding the PDC event that triggers its execution, the PDC event and uCode priorities, and if required, the notifications-to-SYSCPU callback and the configuration of SNC queues for exchanging data with SYSCPU execution context processes. Regarding the address of the context data structure, as described in Section Section 5.8.4.2, it is acquired using SNC_UCODE_CTX() macro when the name of the uCode is given as argument. This is enabled when a uCode definition (i.e. SNC_UCODE_BLOCK_DEF()) or declaration (i.e. SNC_UCODE_BLOCK_DECL()) is provided within a scope that is visible to the point where the particular macro is used. Given the uCode context data structure address, the ad_snc_ucode_register() function allocates, based on the configuration structure passed as argument, all appropriate resources required for controlling and manipulating the uCode in both SYSCPU and SNC execution contexts, as well as for communicating with the SNC queues, if defined, and it finally returns the uCode ID that has been assigned, so that the uCode can be identified uniquely. The configuration structure may include the following:

  1. PDC LUT event entry (i.e. .pdc_entry): It defines the PDC event entry that is to be added/registered to the PDC LUT, so that SNC can wake-up and execute the registered uCode when the event is triggered. The entry can be defined using the HW_PDC_LUT_ENTRY_VAL(trig_select, trig_id, wakeup_master, flags) macro (provided in hw_pdc.h).

  2. PDC event priority (i.e. .pdc_evt_pr): It defines the priority at which the PDC event will be handled in case of multiple events being triggered at the same time and indicated as pending to the corresponding PDC event pending register for the SNC (i.e. PDC_PENDING_SNC_REG). A value from AD_SNC_PDC_EVT_PRIORITY enumeration can be selected, defining a PDC event either with priority 1 to 3 or no priority. However, only one PDC event can be registered for priorities 1 to 3, while the priority cannot change when registering a similar PDC event (i.e. same trigger type, trigger ID and master to wake-up) to ones that have been already registered.

  3. uCode priority (i.e. .ucode_pr): It defines the priority of the uCode among the rest of the uCodes registered to a specific PDC event entry, indicating to the SNC Adapter the order at which the uCodes must be called when the particular PDC event is triggered. A value from AD_SNC_UCODE_PRIORITY enumeration can be selected, defining a uCode with priority 1 to 15 or no priority.

  4. Application notification callback (i.e. .cb): It defines the callback to be called when a notification is sent from the particular uCode using SNC_CM33_NOTIFY() macro.

  5. SNC-to-CM33 and CM33-to-SNC data exchange queues configurations (i.e. .snc_to_cm33_queue_cfg and .cm33_to_snc_queue_cfg): They are configuration structures defining SNC/CM33 data exchange queues allocated for a uCode (see also Sections Section 5.8.3.5 and Section 5.8.3.6), whose starting addresses are stored in the respective uCode context data structure members snc_ucode_context_t->SNC_to_CM33_data_queue and snc_ucode_context_t->CM33_to_SNC_data_queue. The attributes included in the configuration structures of the SNC queues are as follows (also presented in Figure 66):

    • Maximum chunk size (i.e. .max_chunk_bytes): It defines the maximum size of data in bytes that can be stored into the 32-bit word (uint32_t) elements of a chunk comprising the SNC queue. The actual size of the chunk is determined by the element weight. Some padding 32-bit word elements are added to the end of the chunk in order to deal with data sizes which are not multiples of the element weight defined for the SNC queue.

    • Number of chunks (i.e. .num_of_chunks): It defines the number of data chunks comprising the SNC queue. If it is equal to zero, then no SNC queue is created.

    • Element weight (i.e. .element_weight): It defines the “weight” or the size (in bytes) of data that reside in the LSB part of a 32-bit word element comprising the SNC queue data chunks, resembling the way data are written in 32-bit word elements, which can be accessed by SNC.

    • Enable timestamping (i.e. .enable_data_timestamp): If set, it enables timestamping in the headers of the data chunks comprising the SNC queues.

    • Enable swapping of pushed CM33 data (i.e. .swap_pushed_data_bytes): If set, it changes the endianness of the data being pushed/written within CM33 execution context to the CM33-to-SNC queue, by swapping the position of data bytes per 32-bit word element as implied by the element weight attribute (i.e. .element_weight). For example, if a 16-bit data word (stored in the 2 LSBs of a 32-bit word element - SNC_QUEUE_ELEMENT_SIZE_HWORD), 0x00001122, is to be written to the SNC queue with swapping enabled, it will finally be written 0x00002211. (Typical use case: writing data that are to be finally sent over an SPI bus using 16- or 32-bit SPI word size mode).

    • Enable swapping of popped CM33 data (i.e. .swap_popped_data_bytes): If set, it changes the endianness of the data being popped/read within CM33 execution context from the SNC-to-CM33 queue, by swapping the position of data bytes per 32-bit word element as implied by the element weight attribute (i.e. .element_weight). For example, if a 32-bit data word (stored in a 32-bit word element - SNC_QUEUE_ELEMENT_SIZE_WORD), 0x11223344, is to be read from the SNC queue with swapping enabled, it will finally be read 0x44332211. (Typical use case: reading data that have been received over an SPI bus using 16- or 32-bit SPI word size mode).

../_images/fig_SNC_7.png

Figure 66 SNC queues configuration attributes

An example code demonstrating the registration of three SNC uCodes with different configuration parameters is presented in Code 36.

Code 36 Example code registering SNC uCodes
// @file foo_header_file_exposing_ucode_1.h
// @file foo_header_file_exposing_ucode_1.h
...             // Other API functions
SNC_UCODE_BLOCK_DECL(foo_ucode1);   // Declaration of foo_ucode1 uCode-Block
// EOF

// @file foo_header_file_exposing_ucodes_2and3.h
...             // Other API functions
SNC_UCODE_BLOCK_DECL(foo_ucode2);   // Declaration of foo_ucode2 uCode-Block
SNC_UCODE_BLOCK_DECL(foo_ucode3);   // Declaration of foo_ucode3 uCode-Block
// EOF

// @file foo_src_file_registering_ucodes.c
#include "foo_header_file_exposing_ucode_1.h"
#include "foo_header_file_exposing_ucode_2and3.h"
...

// foo_ucode1 notification callback
void foo_ucode1_cb(void) {
    OS_TASK_NOTIFY(OSTask_handling_ucode1_notif, UCODE1_NOTIF, OS_NOTIFY_SET_BITS);
}

// foo_ucode3 notification callback
void foo_ucode3_cb(void) {
    OS_TASK_NOTIFY(OSTask_handling_ucode3_notif, UCODE3_NOTIF, OS_NOTIFY_SET_BITS);
}

void foo_func_register_ucodes123(void) {
    ...
    // Register foo_ucode1 with priority 2 to the system through the SNC Adapter,
    // for an RTC PDC event entry with priority 0, sending notifications through
    // foo_ucode1_cb and without requiring any SNC queues
    ad_snc_ucode_cfg_t ucode1_cfg = {
        .pdc_entry =
            HW_PDC_LUT_ENTRY_VAL(HW_PDC_TRIG_SELECT_PERIPHERAL,
                                 HW_PDC_PERIPH_TRIG_ID_RTC_TIMER,
                                 HW_PDC_MASTER_SNC,
                                 0),
        .pdc_evnt_pr = AD_SNC_PDC_EVT_PR_0,
        .ucode_pr = AD_SNC_UCODE_PR_2,
        .cb = foo_ucode1_cb,
    };
    uint32_t foo_ucode1_id =
        ad_snc_ucode_register(&ucode1_cfg, SNC_UCODE_CTX(foo_ucode1));
    
    // At this point:
    //     uCode list -> pdc_event_pr_0_list -> foo_ucode1

    // Register foo_ucode2 with priority 2 to the system through the SNC Adapter,
    // for an RTC PDC event entry with priority 0 requiring also XTAL32M to be
    // enabled, without sending notifications and requiring any SNC queues 
    ad_snc_ucode_cfg_t ucode2_cfg = {
        .pdc_entry =
            HW_PDC_LUT_ENTRY_VAL(HW_PDC_TRIG_SELECT_PERIPHERAL,
                                 HW_PDC_PERIPH_TRIG_ID_RTC_TIMER,
                                 HW_PDC_MASTER_SNC,
                                 HW_PDC_LUT_ENTRY_EN_XTAL),
        .pdc_evnt_pr = AD_SNC_PDC_EVT_PR_0,
        .ucode_pr = AD_SNC_UCODE_PR_1,
    };
    uint32_t foo_ucode2_id =
        ad_snc_ucode_register(&ucode2_cfg, SNC_UCODE_CTX(foo_ucode2));
    
    // At this point:
    //     uCode list -> pdc_event_pr_0_list -> foo_ucode2 -> foo_ucode1

    // Register foo_ucode3 with priority 5 to the system through the SNC Adapter,
    // for a GPIO 0_17 Wake-Up PDC event entry with priority 1, sending notifications
    // through foo_ucode3_cb, timestamping enabled and an SNC-to-CM33 data exchange
    // queue of 4 chunks, with chunk size 40 bytes, where 16-bit data are stored per 
    // 32-bit word element and swapped when acquired in SYSCPU execution context
    // (i.e. using ad_snc_queue_pop() function)
    ad_snc_ucode_cfg_t ucode3_cfg = {
        .pdc_entry =
            HW_PDC_LUT_ENTRY_VAL(HW_GPIO_PORT_0,
                                 HW_GPIO_PIN_17,
                                 HW_PDC_MASTER_SNC,
                                 0),
        .pdc_evnt_pr = AD_SNC_PDC_EVT_PR_1,
        .ucode_pr = AD_SNC_UCODE_PR_5,
        
        .snc_to_cm33_queue_cfg.max_chunk_bytes = 40,
        .snc_to_cm33_queue_cfg.num_of_chunks = 4,
        .snc_to_cm33_queue_cfg.element_weight = SNC_QUEUE_ELEMENT_SIZE_HWORD,
        .snc_to_cm33_queue_cfg.enable_data_timestamp = true,
        .snc_to_cm33_queue_cfg.swap_popped_data_bytes = true,
    };
    uint32_t foo_ucode3_id =
        ad_snc_ucode_register(&ucode3_cfg, SNC_UCODE_CTX(foo_ucode3));
    
    // At this point:
    //     uCode list -> pdc_event_pr_1_list -> foo_ucode3
    //                   pdc_event_pr_0_list -> foo_ucode2 -> foo_ucode1
}
...

// EOF

As far as the operations being performed when registering a uCode are concerned, the following steps can be identified:

  1. A uCode registration request is sent to the Sensor Node Adapter task with the uCode context and the required configuration.

  2. The uCode configuration parameters are validated:

    • If invalid configuration parameter values are passed or the maximum number of registered uCodes has been reached or the maximum number of PDC events per priority has been reached, AD_SNC_INVALID_UCODE_ID is returned.

  3. The SNC queues for the application communicating with the particular uCode are created, based on the given configuration.

  4. The constructor function of the uCode is called, which allocates the appropriate memory space, initializes the uCode context data structure, builds the SeNIS-based code defined by the C preprocessor macro constructs (described in Section Section 5.8.5) and stores the produced SeNIS commands into the memory space. In essence the constructor function is called three times, applying a different “building context” each time, as follows:

    1. Calculation of the required memory space size to be used for determining the uCode addresses to which the defined labels (i.e. when using SENIS_label() construct macro - see Section Section 5.8.5) in the uCode implementation refer to.

    2. Memory allocation and determination of the uCode labels, as well as calculation of the required memory space size to which the uCode will be stored.

    3. Memory allocation and creation of the uCode.

  5. The SNC-main-uCode is halted, the list of the registered PDC events and uCodes is updated accordingly, and finally SNC-main-uCode is resumed again, having now access to the updated list of PDC event entries and uCodes to handle.

On the other hand, when unregistering an SNC uCode, it is necessary only to pass the uCode ID that is returned when registering the uCode, i.e. when calling ad_snc_register_ucode() function. The SNC Adapter searches for the particular uCode entry in the uCode list based on the provided uCode ID, it removes the uCode entry, and if the corresponding PDC event entry is finally left without any uCode entries, the PDC event entry is removed, too. An example code demonstrating the registration and un-registration of an SNC uCode is presented in Code 37 (extending the example in Code 36). The operations being performed when unregistering a uCode can be summarized to the following:

  1. The SNC-main-uCode is halted.

  2. The uCode is removed from the uCode list.

  3. The PDC event entry to which the particular uCode is registered, is removed from the uCode list if there are no other uCodes left that are registered to that PDC event entry, otherwise it is updated accordingly, so that it can include the required flags by the remaining uCodes.

  4. Any pending CM33-to-SNC notifications for the particular uCode are cleared.

  5. The SNC-main-uCode is resumed again.

  6. The destructor function of the uCode is called, so that its allocated resources can be freed.

  7. The SNC queues (if any) of the uCode are also deleted.

Code 37 Example code registering and unregistering an SNC uCode
// @file foo_header_file_exposing_ucode_4.h
...             // Other API functions
SNC_UCODE_BLOCK_DECL(foo_ucode4);   // Declaration of foo_ucode4 uCode-Block
// EOF

// @file foo_src_file_registering_ucodes.c

...

#include "foo_header_file_exposing_ucode_4.h"

_SNC_RETAINED static uint32_t foo_ucode4_id;

void foo_func_register_ucode4(void) {
    ...
    // Assuming foo_func_register_ucodes123() has been called earlier
    // At this point:
    //     uCode list -> pdc_event_pr_1_list -> foo_ucode3
    //                pdc_event_pr_0_list -> foo_ucode2 -> foo_ucode1

    // Register foo_ucode4 with priority 1 to the system through the SNC Adapter,
    // for an RTC PDC event entry with priority 3 (it is finally ignored, since
    // already defined with priority 0), without sending notifications and requiring
    // any SNC queues
    ad_snc_ucode_cfg_t ucode4_cfg = {
        .pdc_entry =
            HW_PDC_LUT_ENTRY_VAL(HW_PDC_TRIG_SELECT_PERIPHERAL,
                                 HW_PDC_PERIPH_TRIG_ID_RTC_TIMER,
                                 HW_PDC_MASTER_SNC,
                                 0),
        .pdc_evnt_pr = AD_SNC_PDC_EVT_PR_3,
        .ucode_pr = AD_SNC_UCODE_PR_1,
    };
    foo_ucode4_id =
        ad_snc_ucode_register(&ucode4_cfg, SNC_UCODE_CTX(foo_ucode4));
    
    // At this point:
    //     uCode list -> pdc_event_pr_1_list -> foo_ucode3
    //                  pdc_event_pr_0_list -> foo_ucode2 -> foo_ucode4 -> foo_ucode1
}

bool foo_func_unregister_ucode4(void) {
    return ad_snc_ucode_unregister(foo_ucode4_id);

    // At this point:
    //     uCode list -> pdc_event_pr_1_list -> foo_ucode3
    //                  pdc_event_pr_0_list -> foo_ucode2 -> foo_ucode1
}
...

// EOF

5.8.3.3. Enabling/Disabling uCodes

When registering an SNC uCode, it is initially disabled. This means that, even if the corresponding PDC event is triggered, the SNC execution flow is not driven through the particular SNC uCode.

In order to control the execution of a registered SNC uCode, two API functions are provided, namely ad_snc_ucode_enable() and ad_snc_ucode_disable(), allowing for enabling and disabling the SNC uCode execution, respectively.

Function

Description

ad_snc_ucode_enable()

Enable uCode execution.

ad_snc_ucode_disable()

Disable uCode execution.

5.8.3.4. Notifying and Receiving Notifications from uCodes

SNC uCodes can be considered as parallel tasks to the concurrent OS tasks (i.e. FreeRTOS tasks) executing in SYSCPU (CM33) context. Therefore the SNC Programming Framework extends the notifications exchanged between the OS tasks to notifications also exchanged between OS tasks and SNC uCodes in a unified way.

In order to support SNC-to-CM33 notifications, as it has been already mentioned in the previous sections, a callback function (i.e. ad_snc_ucode_cfg_t->cb) is provided for each registered uCode. That callback is called when SNC_CM33_NOTIFY() is called in the SNC execution context of the uCode, showing resemblance to the OS_TASK_NOTIFY() macro defined in the OS abstraction layer API (i.e. osal.h) for notifying each other the OS tasks. More specifically, the Sensor Node Adapter IRQ task handles the notifications that are sent by the uCodes, therefore processing within the particular callbacks must be kept short (i.e. using only an OS_TASK_NOTIFY() macro that sends the notification to the targeted OS task, accordingly), so that the relatively small stack size of the task cannot be exceeded.

In the opposite way, since the SNC uCodes are executed when a PDC event is triggered, notifications can be sent from an OS task to an SNC uCode by force-triggering the PDC event the uCode is registered to. This is performed when calling the SNC Adapter API function ad_snc_pdc_set_pending(), which takes as argument the uCode ID returned when registering a uCode, and triggers the respective PDC event entry in the PDC LUT, accordingly. Considering, though, the fact that more than one SNC uCodes can be registered to the same PDC event or a PDC event can be triggered also by the source it is related to (i.e. GPIO, timer etc.), an SNC API macro can be used (typically as first statement) in the uCodes’ implementation, namely SNC_CHECK_CM33_NOTIF_PENDING(), with which such a CM33-to-SNC notification can be identified and each uCode return or proceed with its execution, accordingly.

Function

Description

ad_snc_pdc_set_pending()

Set a PDC LUT event entry where a uCode has been registered as pending.

Note: It will force-trigger the execution of all uCodes that have been registered to the PDC event being set as pending. SNC_CHECK_CM33_NOTIF_PENDING() macro function can be used in SNC execution context in order to check if the implied notification targets the particular uCode.

An illustration of the above processes comprising the SNC/CM33 notification exchange mechanism is presented in Figure 67.

../_images/fig_SNC_8.png

Figure 67 Notifications exchange between OS task and SNC uCode

5.8.3.5. Pushing Data to uCode CM33-to-SNC Queue

In SYSCPU execution context, in order to push a specific size of data to the CM33-to-SNC queue of a uCode defined by the given uCode ID, and add also a timestamp that implies some time reference to accompany them when they are processed, ad_snc_queue_push() function can be used. In fact, since the SNC queues are organized in chunks, each time the ad_snc_queue_push() function is called, the given data will be written to the next free/available chunk, resulting in “partially” empty data storage per chunk in the SNC queue in case the given data size is less than the maximum one defined for the chunk on the configuration of the SNC queue (i.e. when registering a uCode, setting the attribute snc_queue_config_t->max_chunk_bytes). On the other hand, if the given size is greater than the maximum chunk size, the function returns “false”. The same happens if the SNC queue is full and there are no free chunks to write data to, a status of the SNC queue which is returned when calling ad_snc_queue_is_full() function (see Section Section 5.8.3.7).

Function

Function

ad_snc_queue_push()

Push data into a uCode-Block’s CM33-to-SNC queue.

Regarding the timestamp attribute, if timestamping is enabled in the SNC queue configuration (i.e. snc_queue_config_t->enable_data_timestamp = true), then a time reference value can be added to the respective header field of the data chunk being written in the SNC queue. That timestamp can be either a capture time value for event on a GPIO (using hw_timer_get_capture[1|2|3|4]() function in hw_timer.h) or an RTC time value (using hw_rtc_get_time_bcd() function in hw_rtc.h) or even a simple counter value.

Finally, the data to be pushed to the SNC queue are “unpacked” and “swapped” before being stored to the chunk’s 32-bit word elements that are accessed by SNC, based on the element weight (i.e. snc_queue_config_t->element_weight) and the corresponding indication for swapping the pushed data (i.e. snc_queue_config_t->swap_pushed_data_bytes), respectively, that are configured for the SNC queue when registering the uCode. More specifically, assuming that a specific number of bytes are pushed to a uCode’s CM33-to-SNC queue, what is finally written to the SNC queue chunks based on the configured element weight is presented below:

  • If the selected element weight is 1 byte (i.e. SNC_QUEUE_ELEMENT_SIZE_BYTE), then each byte is stored into a different 32-bit word element of a chunk.

  • If the selected element weight is 2 bytes (i.e. SNC_QUEUE_ELEMENT_SIZE_HWORD), then each set of 2 bytes is stored into a different 32-bit word element of a chunk, and if the total number of bytes is not a multiple of 2, the last byte is stored into a separate 32-bit word element.

  • If the selected element weight is 4 bytes (i.e. SNC_QUEUE_ELEMENT_SIZE_WORD), then each set of 4 bytes is stored into a different 32-bit word element of a chunk, and if the total number of bytes is not a multiple of 4, each of the last bytes is stored into a separate 32-bit word element.

If swapping is also enabled, then the endianness of the pushed data is also changed before stored into a 32-bit word element, by swapping the written bytes into the LSBs. A representative example of what is finally stored into an SNC queue when pushing data to its chunks for different configured element weights is shown in Figure 68, while some typical usage cases of the particular API function are presented in Code 38.

../_images/fig_SNC_9.png

Figure 68 uCode CM33-to-SNC queue data representation when pushing data in CM33 context

Code 38 Example code pushing data to uCode CM33-to-SNC queues in CM33 context
// @file foo_header_file_exposing_ucodes.h
...             // Other API functions
SNC_UCODE_BLOCK_DECL(foo_ucode1);   // Declaration of foo_ucode1 uCode-Block
SNC_UCODE_BLOCK_DECL(foo_ucode2);   // Declaration of foo_ucode2 uCode-Block
// EOF

// @file foo_src_file_pushing_data_to_ucode_CM33toSNC_queues.c
#include "foo_header_file_exposing_ucodes.h"
...
_SNC_RETAINED static uint32_t foo_ucode1_id;
_SNC_RETAINED static uint32_t foo_ucode2_id;

void foo_func_register_ucodes(void) {
    ...    // Initialization of the uCodes

    // Register foo_ucode1 to the system through the SNC Adapter, with CM33-to-SNC
    // data exchange queue of 3 chunks, with timestamping enabled and chunk size
    // 4 bytes, where 16-bit data are stored per 32-bit word element, and not swapped
    // when stored to the SNC queue chunks in SYSCPU execution context (i.e. using
    // ad_snc_queue_push() function)
    ad_snc_ucode_cfg_t ucode1_cfg = {
...    // Rest of the configuration parameters
        .cm33_to_snc_queue_cfg.max_chunk_bytes = 4,
        .cm33_to_snc_queue_cfg.num_of_chunks = 3,
        .cm33_to_snc_queue_cfg.element_weight = SNC_QUEUE_ELEMENT_SIZE_HWORD,
        .cm33_to_snc_queue_cfg.enable_data_timestamp = true,
        .cm33_to_snc_queue_cfg.swap_pushed_data_bytes = false,
    };
    uint32_t foo_ucode1_id =
        ad_snc_ucode_register(&ucode1_cfg, SNC_UCODE_CTX(foo_ucode1));

    // Register foo_ucode2 to the system through the SNC Adapter, with CM33-to-SNC
    // data exchange queue of 2 chunks, with timestamping disabled and chunk size
    // 11 bytes, where 32-bit data are stored per 32-bit word element, and swapped
    // when stored to the SNC queue chunks in SYSCPU execution context (i.e. using
    // ad_snc_queue_push() function)
    ad_snc_ucode_cfg_t ucode2_cfg = {
        ...    // Rest of the configuration parameters
        .cm33_to_snc_queue_cfg.max_chunk_bytes = 11,
        .cm33_to_snc_queue_cfg.num_of_chunks = 2,
        .cm33_to_snc_queue_cfg.element_weight = SNC_QUEUE_ELEMENT_SIZE_WORD,
        .cm33_to_snc_queue_cfg.enable_data_timestamp = false,
        .cm33_to_snc_queue_cfg.swap_pushed_data_bytes = true,
    };
    uint32_t foo_ucode2_id =
        ad_snc_ucode_register(&ucode2_cfg, SNC_UCODE_CTX(foo_ucode2));

    // At this point:
    //     SNC_UCODE_CTX(foo_ucode1)->CM33_to_SNC_data_queue ->
    //  (free) chunk[0] -> 0(timestamp) {0, 0}
    //  (free) chunk[1] -> 0(timestamp) {0, 0}
    //  (free) chunk[2] -> 0(timestamp) {0, 0}
    //     SNC_UCODE_CTX(foo_ucode2)->CM33_to_SNC_data_queue ->
    //  (free) chunk[0] -> {0, 0, 0, 0(padding), 0(padding)}
    //  (free) chunk[1] -> {0, 0, 0, 0(padding), 0(padding)}
    
    // Enable foo_ucode1
    ad_snc_ucode_enable(foo_ucode1_id);

    // Enable foo_ucode2
    ad_snc_ucode_enable(foo_ucode2_id);
}

_SNC_RETAINED static uint16_t data_to_ucode1[] = {
    0x0201, 0x0403, 0x0605
};

_SNC_RETAINED static uint8_t data_to_ucode2[] = {
    0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
    0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B
};

void foo_FreeRTOS_task_func()       // Definition of FreeRTOS task function
                                    // communicating with foo_ucode1 and foo_ucode2
{
    foo_func_register_ucodes();     // Initialize and register uCodes

    for (;;) {
        ...
        // Push the first 2 bytes of data_to_ucode1 to foo_ucode1's CM33-to-SNC queue
        // and set timestamp to 1
        ad_snc_queue_push(foo_ucode1_id, (uint8_t*)&data_to_ucode1[0], 2, 1);
        // At this point:
        //     SNC_UCODE_CTX(foo_ucode1)->CM33_to_SNC_data_queue ->
        //(written) chunk[0] -> 1(timestamp) {0x00000201, 0}
        //   (free) chunk[1] -> 0(timestamp) {0, 0}
        //   (free) chunk[2] -> 0(timestamp) {0, 0}
        // Push the next 4 bytes of data_to_ucode1 to foo_ucode1's CM33-to-SNC queue
        // and set timestamp to 2
        ad_snc_queue_push(foo_ucode1_id, (uint8_t*)&data_to_ucode1[1], 4, 2);
        // At this point:
        //     SNC_UCODE_CTX(foo_ucode1)->CM33_to_SNC_data_queue ->
        //(written) chunk[0] -> 1(timestamp) {0x00000201, 0}
        //(written) chunk[1] -> 2(timestamp) {0x00000403, 0x00000605}
        //   (free) chunk[2] -> 0(timestamp) {0, 0}

        // Push the first 9 bytes of data_to_ucode2 to foo_ucode2's CM33-to-SNC queue
        ad_snc_queue_push(foo_ucode2_id, (uint8_t*)&data_to_ucode2[0], 9, 0);
        // At this point:
        //     SNC_UCODE_CTX(foo_ucode2)->CM33_to_SNC_data_queue ->
        //(written) chunk[0] -> {0x01020304, 0x05060708, 0x00000009, 0, 0}
        //   (free) chunk[1] -> {0, 0}

        // Push the next 7 bytes of data_to_ucode2 to foo_ucode2's CM33-to-SNC queue
        ad_snc_queue_push(foo_ucode2_id, (uint8_t*)&data_to_ucode2[9], 7, 0);
        // At this point:
        //     SNC_UCODE_CTX(foo_ucode2)->CM33_to_SNC_data_queue ->
        //(written) chunk[0] -> {0x01020304, 0x05060708, 0x00000009, 0, 0}
        //(written) chunk[1] -> {0x0A0B1112, 0x00000013, 0x00000014, 0x00000015, 0}
        ...
    }
}

// EOF 

5.8.3.6. Popping Data from uCode SNC-to-CM33 Queue

In SYSCPU execution context, in order to pop the data stored to the SNC-to-CM33 queue of a uCode defined by the given uCode ID, acquiring also their size and the timestamp that may accompany them, ad_snc_queue_pop() function can be used. In fact, since the SNC queues are organized in chunks, each time the ad_snc_queue_pop() function is called, the given data will be read from the next chunk with written data, while their size in bytes (maximum value equal to snc_queue_config_t->max_chunk_bytes), which is indicated in the chunk header, will be returned/stored to address indicated by the size parameter. If there is no chunk with written data, the function returns “false”, a status of the SNC queue which is also returned when calling ad_snc_queue_is_empty() function (see Section Section 5.8.3.7).

Function

Description

ad_snc_queue_pop()

Pop data from a uCode-Block’s SNC-to-CM33 queue.

Regarding the timestamp attribute, if timestamping is enabled in the SNC queue configuration (i.e. snc_queue_config_t->enable_data_timestamp = true), then the time reference value which is stored to the respective header field of the data chunk being read from the SNC queue is returned and handled accordingly.

Finally, the data stored by the SNC into the 32-bit word elements of the chunk are “packed” and “swapped” when copied/popped to the address indicated by the given data parameter, based on the element weight (i.e. snc_queue_config_t->element_weight) and the corresponding indication for swapping the popped data (i.e. snc_queue_config_t->swap_popped_data_bytes), respectively, that are configured for the SNC queue when registering the uCode. More specifically, assuming a specific number of bytes are popped from a chunk in a uCode’s SNC-to-CM33 queue, what is finally read from the SNC queue and stored to the given data address parameter based on the configured element weight is presented below:

  • If the selected element weight is 1 byte (i.e. SNC_QUEUE_ELEMENT_SIZE_BYTE), then the LSB is read from each 32-bit word element of a chunk and stored to the next byte of the destination buffer implied by the given data address parameter.

  • If the selected element weight is 2 bytes (i.e. SNC_QUEUE_ELEMENT_SIZE_HWORD), then the two LSBs are read from each 32-bit word element of a chunk and stored to the next 2 bytes of the destination buffer. In case the total number of bytes in the chunk is not a multiple of 2, the last byte is stored into a separate 32-bit word element, therefore only the LSB is read and stored to the next byte of the destination buffer.

  • If the selected element weight is 4 bytes (i.e. SNC_QUEUE_ELEMENT_SIZE_WORD), then all four bytes are read from each 32-bit word element of a chunk and stored to the next 4 bytes of the destination buffer. In case the total number of bytes in the chunk is not a multiple of 4, the last bytes are stored into a separate 32-bit word element, therefore only the LSB is read from each of those 32-bit word elements and stored to the next bytes of the destination buffer.

If swapping is also enabled, then the endianness of the popped data is also changed before stored into the destination buffer, by swapping the LSBs of each of the read 32-bit word elements from the SNC queue. A representative example of what is finally read from an SNC queue and stored to a destination buffer when popping data from the SNC queue chunks for different configured element weights is shown in Figure 69, while some typical usage cases of the particular API function are presented in Code 39.

../_images/fig_SNC_10.png

Figure 69 Data representation in a destination buffer when popping data from a uCode SNC-to-CM33 queue in CM33 context

Code 39 Example code popping data from uCode SNC-to-CM33 queues in CM33 context
// @file foo_header_file_exposing_ucodes.h
...             // Other API functions
SNC_UCODE_BLOCK_DECL(foo_ucode1);   // Declaration of foo_ucode1 uCode-Block
SNC_UCODE_BLOCK_DECL(foo_ucode2);   // Declaration of foo_ucode2 uCode-Block
// EOF

// @file foo_src_file_popping_data_from_ucode_SNCtoCM33_queues.c
#include "foo_header_file_exposing_ucodes.h"
...
_SNC_RETAINED static uint32_t foo_ucode1_id;
_SNC_RETAINED static uint32_t foo_ucode2_id;

void foo_func_register_ucodes(void) {
    ...    // Initialization of the uCodes

    // Register foo_ucode1 to the system through the SNC Adapter, with SNC-to-CM33
    // data exchange queue of 3 chunks, with timestamping enabled and chunk size
    // 4 bytes, where 8-bit data are stored per 32-bit word element, and not swapped
    // when read from the SNC queue chunks in SYSCPU execution context (i.e. using
    // ad_snc_queue_pop() function)
    ad_snc_ucode_cfg_t ucode1_cfg = {
        ...    // Rest of the configuration parameters
        .snc_to_cm33_queue_cfg.max_chunk_bytes = 4,
        .snc_to_cm33_queue_cfg.num_of_chunks = 3,
        .snc_to_cm33_queue_cfg.element_weight = SNC_QUEUE_ELEMENT_SIZE_BYTE,
        .snc_to_cm33_queue_cfg.enable_data_timestamp = true,
        .snc_to_cm33_queue_cfg.swap_popped_data_bytes = false,
    };
    uint32_t foo_ucode1_id =
        ad_snc_ucode_register(&ucode1_cfg, SNC_UCODE_CTX(foo_ucode1));

    // Register foo_ucode2 to the system through the SNC Adapter, with SNC-to-CM33
    // data exchange queue of 2 chunks, with timestamping disabled and chunk size
    // 12 bytes, where 32-bit data are stored per 32-bit word element, and swapped
    // when read from the SNC queue chunks in SYSCPU execution context (i.e. using
    // ad_snc_queue_pop() function)
    ad_snc_ucode_cfg_t ucode2_cfg = {
        ...    // Rest of the configuration parameters
        .snc_to_cm33_queue_cfg.max_chunk_bytes = 12,
        .snc_to_cm33_queue_cfg.num_of_chunks = 2,
        .snc_to_cm33_queue_cfg.element_weight = SNC_QUEUE_ELEMENT_SIZE_WORD,
        .snc_to_cm33_queue_cfg.enable_data_timestamp = false,
        .snc_to_cm33_queue_cfg.swap_poppped_data_bytes = true,
    };
    uint32_t foo_ucode2_id =
        ad_snc_ucode_register(&ucode2_cfg, SNC_UCODE_CTX(foo_ucode2));

    // Enable foo_ucode1
    ad_snc_ucode_enable(foo_ucode1_id);

    // Enable foo_ucode2
    ad_snc_ucode_enable(foo_ucode2_id);
}

_SNC_RETAINED static struct {
    uint32_t timestamp;
    uint32_t size;
    uint16_t data[2];
} data_from_ucode1[3] = {0};    // Definition of data_from_ucode1 destination buffer

_SNC_RETAINED static struct {
    uint32_t size;
    uint32_t data[3];
} data_from_ucode2[3] = {0};    // Definition of data_from_ucode2 destination buffer

void foo_FreeRTOS_task_func()   // Definition of FreeRTOS task function
                                // communicating with foo_ucode1 and foo_ucode2
{
    foo_func_register_ucodes(); // Initialize and register uCodes

    for (;;) {
        ...
        // Assuming at this point SNC uCodes have stored data to SNC-to-CM33 queues:
        //     SNC_UCODE_CTX(foo_ucode1)->SNC_to_CM33_data_queue ->
        //(written) chunk[0] -> 1(timestamp) {0x00000001, 0x00000002, 0x00000003, 0}
        //(written) chunk[1] -> 2(timestamp) {0x00000004, 0x00000005, 0, 0}
        //   (free) chunk[2] -> 0(timestamp) {0, 0, 0, 0}
        //     SNC_UCODE_CTX(foo_ucode2)->SNC_to_CM33_data_queue ->
        //(written) chunk[0] -> {0x01020304, 0x05060708, 0x00000009, 0, 0}
        //(written) chunk[1] -> {0x10111213, 0x00000014, 0x00000015, 0x00000016, 0}

        // Pop the first chunk of foo_ucode1's SNC-to-CM33 queue and store it to
        // data_from_ucode1, and acquire also its size and its timestamp
        ad_snc_queue_pop(foo_ucode1_id, (uint8_t*)&data_from_ucode1[0]->data,
                         &data_from_ucode1[0]->size, &data_from_ucode1[0]->timestamp);
        // At this point:
        //     SNC_UCODE_CTX(foo_ucode1)->SNC_to_CM33_data_queue ->
        //   (free) chunk[0] -> 1(timestamp) {0x00000001, 0x00000002, 0x00000003, 0}
        //(written) chunk[1] -> 2(timestamp) {0x00000004, 0x00000005, 0, 0}
        //   (free) chunk[2] -> 0(timestamp) {0, 0, 0, 0}
        //
        //     data_from_ucode1[0] -> {1, 3, {0x0201, 0x0003}}
        //     data_from_ucode1[1] -> {0, 0, {0, 0}}
        //     data_from_ucode1[2] -> {0, 0, {0, 0}}

        // Pop the next chunk of foo_ucode1's SNC-to-CM33 queue and store it to
        // data_from_ucode1, and acquire also its size and its timestamp
        ad_snc_queue_pop(foo_ucode1_id, (uint8_t*)&data_from_ucode1[1]->data,
                         &data_from_ucode1[1]->size, &data_from_ucode1[1]->timestamp);
        // At this point:
        //     SNC_UCODE_CTX(foo_ucode1)->SNC_to_CM33_data_queue ->
        //   (free) chunk[0] -> 1(timestamp) {0x00000001, 0x00000002, 0x00000003, 0}
        //   (free) chunk[1] -> 2(timestamp) {0x00000004, 0x00000005, 0, 0}
        //   (free) chunk[2] -> 0(timestamp) {0, 0, 0, 0}
        //
        //     data_from_ucode1[0] -> {1, 3, {0x0201, 0x0003}}
        //     data_from_ucode1[1] -> {2, 2, {0x0504, 0}}
        //     data_from_ucode1[2] -> {0, 0, {0, 0}}

        // Pop the first chunk of foo_ucode2's SNC-to-CM33 queue and store it to
        // data_from_ucode2, and acquire also its size
        ad_snc_queue_pop(foo_ucode2_id, (uint8_t*)&data_to_ucode2[0]->data,
                         &data_from_ucode2[0]->size, NULL);
        // At this point:
        //     SNC_UCODE_CTX(foo_ucode1)->SNC_to_CM33_data_queue ->
        //   (free) chunk[0] -> {0x01020304, 0x05060708, 0x00000009, 0, 0}
        //(written) chunk[1] -> {0x10111213, 0x00000014, 0x00000015, 0x00000016, 0}
        //
        //     data_from_ucode2[0] -> {9, {0x04030201, 0x08070605, 0x00000009}}
        //     data_from_ucode2[1] -> {0, {0, 0, 0}}
        //     data_from_ucode2[2] -> {0, {0, 0, 0}}

        // Pop the next chunk of foo_ucode2's SNC-to-CM33 queue and store it to
        // data_from_ucode2, and acquire also its size
        ad_snc_queue_pop(foo_ucode2_id, (uint8_t*)&data_to_ucode2[1]->data,
                         &data_from_ucode2[1]->size, NULL);
        // At this point:
        //     SNC_UCODE_CTX(foo_ucode1)->SNC_to_CM33_data_queue ->
        //   (free) chunk[0] -> {0x01020304, 0x05060708, 0x00000009, 0, 0}
        //   (free) chunk[1] -> {0x10111213, 0x00000014, 0x00000015, 0x00000016, 0}
        //
        //     data_from_ucode2[0] -> {9, {0x04030201, 0x08070605, 0x00000009}}
        //     data_from_ucode2[1] -> {7, {0x12121110, 0x00161514, 0}}
        //     data_from_ucode2[2] -> {0, {0, 0, 0}}
        ...
    }
}

// EOF 

5.8.3.7. Acquiring the Status of uCode SNC Queues

In SYSCPU execution context, in order to check the status of an SNC-to-CM33 queue or CM33-to-SNC queue of a uCode defined by the given uCode ID, and accordingly proceed to actions related to pushing or popping data from the queues, a set of API functions are provided:

Function

Description

ad_snc_queue_is_empty()

Check if a uCode-Block’s SNC-to-CM33 queue or CM33-to-SNC queue is empty.

ad_snc_queue_is_full()

Check if a uCode-Block’s SNC-to-CM33 queue or CM33-to-SNC queue is full.

ad_snc_queue_get_free_chunks()

Return the number of free (not written/allocated) chunks in a uCode-Block’s SNC-to-CM33 queue or CM33-to-SNC queue.

ad_snc_queue_get_alloc_chunks()

Return the number of allocated (written) chunks in a uCode-Block’s SNC-to-CM33 queue or CM33-to-SNC queue.

ad_snc_queue_get_cur_chunk_bytes()

Return the number of bytes in the current chunk (i.e. the next to be popped) of a uCode-Block’s SNC-to-CM33 queue or CM33-to-SNC queue.

More specifically:

  • In order to check if an SNC queue is empty, for example before popping a chunk from an SNC-to-CM33 queue by calling ad_snc_queue_pop() function, ad_snc_queue_is_empty() function can be used. An SNC queue is empty when no chunks have been written yet (or the next chunk to be read has not been written yet, as implied by the underlying implementation).

  • In order to check if an SNC queue is full, for example before pushing data to a CM33-to-SNC queue by calling ad_snc_queue_push() function, ad_snc_queue_is_full() function can be used. An SNC queue is full when all chunks of the SNC queue have been written (or the next chunk to be written is already written, as implied by the underlying implementation).

  • In order to read the number of free chunks in an SNC queue, for example before pushing multiple times data to a CM33-to-SNC queue in different chunks by calling ad_snc_queue_push() function, ad_snc_queue_get_free_chunks() function can be used. As free chunk is considered a chunk that has not been written yet.

  • In order to read the number of allocated/written chunks in an SNC queue, for example before popping multiple chunks from an SNC-to-CM33 queue by calling ad_snc_queue_pop() function, ad_snc_queue_get_alloc_chunks() function can be used. As allocated chunk is considered a chunk that has been written.

  • In order to read the number of written bytes in the next chunk to be popped in an SNC queue, for example for allocating the appropriate memory space for the destination buffer before popping a chunk from an SNC-to-CM33 queue by calling ad_snc_queue_pop() function, ad_snc_queue_get_cur_chunk_bytes() function can be used. As current chunk is considered the next chunk to be popped from an SNC queue.

5.8.4. Adding uCodes to CM33-SNC Applications

The underlying SeNIS-based programming framework defines three different types of uCodes, corresponding to three different “uCode roles” implementing the total logic being executed on SNC. In the common case, when developing applications that combine/are expanded to both SYSCPU and SNC execution contexts, only the uCode-Block role is used, while the other two roles (i.e. Function-uCode and SNC-main-uCode) are mainly used throughout the underlying SNC system architecture implementing mechanisms related to scheduling of uCodes execution, driving system peripherals (e.g. SPI, I2C), exchanging notifications between SYSCPU and SNC, etc.

Regarding the uCodes typically comprising an application, hereafter referring to them as uCode-Blocks or SNC uCodes or just uCodes, they are small programs running on SNC that are implemented using a list of SeNIS-based C preprocessor constructs, as described in later sections, having as special “attribute” that their execution is coupled and can be triggered by a PDC event (e.g. GPIO, timer, RTC etc.) to which they have been registered through the SNC Adapter, as mentioned in Section Section 5.8.3.2. In essence, they constitute the starting points for the execution of the operations that are related to a specific event and have to be executed over SNC, and therefore all together produce the total behavior of the SNC with respect to the control and manipulation of both internal resources and peripherals and sensors driven/accessed by SNC.

5.8.4.1. Defining a uCode

Every uCode-Block has a unique uCode ID, which is produced upon its registration to the system through the SNC Adapter, both inbound and outbound notifications from/to SYSCPU are supported, and data can be exchanged with SYSCPU using a predefined mechanism which is based on inbound and outbound data queues implementing circular buffers and typical FIFO operation features, as presented in Section Section 5.8.3. In order to define a uCode-Block and its corresponding SNC execution context, which is comprised of a sequence of SeNIS commands stored in RAM, as well as data structures handled by the particular commands and the rest of the system supporting SNC uCode integration in a combined SYSCPU-SNC execution context, a C preprocessor macro must be used, called SNC_UCODE_BLOCK_DEF(), where the name of the uCode-Block is passed as argument. As shown in Code 40, inside the block with the curly braces (i.e. “{ … }”) defined after such macro-functions, the implementation of the uCode can be defined using the SeNIS-based C macro constructs (Section Section 5.8.5).

Code 40 SNC uCode-Block definition
SNC_UCODE_BLOCK_DEF(foo_ucode)    // Definition of foo_ucode uCode-Block
{
    ...         // uCode implementation - SeNIS-based statements
}

5.8.4.2. Declaring and Referencing a uCode

In order to reference the context of a uCode in a different scope with respect to where it is defined (when using SNC_UCODE_BLOCK_DEF() definition macro), it can be used another macro-function provided by the SNC programming framework, called SNC_UCODE_BLOCK_DECL(), which defines, in essence, a declaration of the uCode context. That uCode declaration is necessary to be provided (e.g. in a header file through which the context of the uCode is to be exposed) in the typical use case where a uCode needs to be registered to the system through the SNC Adapter, allowing for the use of the SNC_UCODE_CTX() macro for acquiring the context of the uCode, as presented in Section Section 5.8.3.2 and demonstrated in Code 41.

Code 41 SNC uCode-Block declaration and context reference
// @file foo_header_file_exposing_ucode_ctx.h

...             // Other API functions

SNC_UCODE_BLOCK_DECL(foo_ucode);   // Declaration of foo_ucode uCode-Block

// EOF


// @file foo_src_file_registering_ucode.c

...

    // Somewhere, at a specific implementation point:
    // Register foo_ucode to the system through the SNC Adapter with the given
    // configuration (i.e. foo_ucode_cfg)
    uint32_t foo_ucode_id =
        ad_snc_ucode_register(&foo_ucode_cfg, SNC_UCODE_CTX(foo_ucode)); 

...

// EOF

5.8.5. Sensor Node Controller Software Development Kit

5.8.5.1. SeNIS-based Language Constructs

As described in the previous sections, SNC is a sophisticated hardware state machine implementing a very short list of instructions, which however are rather enough for creating small programs addressing typical operations related to sensors and peripherals control and data acquisition. The particular instruction set promotes an assembly-way-of-thinking/programming, by covering typical assembly operations related to:

  • copying memory and register address contents (WADAD)

  • writing memory and register address contents (WADVA)

  • setting flags based on a condition (RDCBI and RDCGR)

  • performing arithmetic and bitwise operations (INC and TOBRE)

  • branching (COBR)

Based on the SNC instruction set (SeNIS), the SNC Programming Framework provides a set of programming constructs, for creating uCodes. Some of them offer a convenient way to directly use SeNIS, and some others offer a set of higher level C-like operations.

All of these SeNIS-based language constructs are provided in SeNIS.h and snc.h header files, which must be included in order to implement a program executing on SNC (uCode).

More details regarding the usage of these constructs can be found in [Ref_04].

An example uCode using SeNIS-based language constructs and the respective “equivalent” implementation using C language constructs are shown in Code 42 and Code 43, respectively, presenting an overview of how a uCode is developed using the particular SeNIS-based programming framework.

Code 42 Example SNC uCode using SeNIS-based language constructs
uint32_t foo_var1 = 0;                 // Definition of the variable foo_var1
uint32_t foo_buffer[10];               // Definition of the array foo_buffer
uint32_t* foo_var2_ptr;                // Definition of the variable foo_var2_ptr
uint32_t inRangeAndLTmax;              // Definition of the variable inRangeAndLTmax

#define MAX_THRESHOLD_VALUE    300     // Definition of a maximum threshold value
#define MIN_THRESHOLD_VALUE    100     // Definition of a minimum threshold value

SNC_UCODE_BLOCK_DEF(foo_ucode)         // Definition of foo_ucode uCode-Block
{
    // Writing a value (i.e. false) to inRangeAndLTmax variable
    SENIS_assign(da(&inRangeAndLTmax), false);

    // Writing a value (i.e. 5) to foo_var1 variable
    SENIS_assign(da(&foo_var1), 5);

    // Writing a value (i.e. the address of the 6th element of foo_buffer array)
    // to foo_var2_ptr variable
    SENIS_assign(da(&foo_var2_ptr), &foo_buffer[5]);

    // While foo_var1 is less than ("<") 10, ...
    SENIS_while (da(&foo_var1), LT, 10) {

        // increment foo_var1 by 1, ...
        SENIS_inc1(da(&foo_var1));

        // if the value in the pointed element of foo_buffer array by foo_var2_ptr
        // variable is greater than or equal to (">=") MAX_THRESHOLD_VALUE, ... 
        SENIS_if (ia(&foo_var2_ptr), GTEQ, MAX_THRESHOLD_VALUE) {

            // then write false to inRangeAndLTmax and ...
            SENIS_assign(da(&inRangeAndLTmax), false);

            // break the while loop, ...
            SENIS_break;

        // otherwise, ...
        SENIS_else {

            // if the value in the pointed element is less than ("<")
            // MIN_THRESHOLD_VALUE, ...
            SENIS_if (ia(&foo_var2_ptr), LT, MIN_THRESHOLD_VALUE) {

                // continue with the next element
                SENIS_inc4(da(&foo_var2_ptr));
                SENIS_continue;
            }

            // otherwise, increment foo_var2_ptr by 4 to point to the next 
            // element of foo_buffer array, and ...
            SENIS_inc4(da(&foo_var2_ptr));

            // write true to inRangeAndLTmax
            SENIS_assign(da(&inRangeAndLTmax), true);
        }}
    }
}
Code 43 C language equivalent implementation of the example SNC uCode shown in Code 42
uint32_t foo_var1 = 0;                 // Definition of the variable foo_var1
uint32_t foo_buffer[10];               // Definition of the array foo_buffer
uint32_t* foo_var2_ptr;                // Definition of the variable foo_var2_ptr
uint32_t inRangeAndLTmax;              // Definition of the variable inRangeAndLTmax

#define MAX_THRESHOLD_VALUE    300     // Definition of a maximum threshold value
#define MIN_THRESHOLD_VALUE    100     // Definition of a minimum threshold value

void foo_ucode(void)                   // Definition of foo_ucode uCode-Block
{
    // Writing a value (i.e. false) to inRangeAndLTmax variable
    inRangeAndLTmax = false;

    // Writing a value (i.e. 5) to foo_var1 variable
    foo_var1 = 5;

    // Writing a value (i.e. the address of the 6th element of foo_buffer array)
    // to foo_var2_ptr variable
    foo_var2_ptr = &foo_buffer[5]);

    // While foo_var1 is less than ("<") 10, ...
    while (foo_var1 < 10) {

        // increment foo_var1 by 1, ...
        foo_var1++;

        // if the value in the pointed element of foo_buffer array by foo_var2_ptr
        // variable is greater than or equal to (">=") MAX_THRESHOLD_VALUE, ... 
        if (*foo_var2_ptr >= MAX_THRESHOLD_VALUE) {

            // then write false to inRangeAndLTmax and ...
            inRangeAndLTmax = false;

            // break the while loop, ...
            break;

        // otherwise, ...
        } else {

            // if the value in the pointed element is less than ("<")
            // MIN_THRESHOLD_VALUE, ...
            if (*foo_var2_ptr < MIN_THRESHOLD_VALUE) {

                // continue with the next element
                foo_var2_ptr++;
                continue;
            }

            // otherwise, increment foo_var2_ptr by 4 to point to the next 
            // element of foo_buffer array, and ...
            foo_var2_ptr++;

            // write true to inRangeAndLTmax
            inRangeAndLTmax = true;
        }
    }
}

5.8.5.2. SNC System Functions & Drivers

In addition, in correspondence to the API functions provided by the SDK in order to control the DA1469x system core HW components/modules and peripherals, and acquire and manipulate their data efficiently in SYSCPU execution context, a set of API macro functions are also provided to perform similar operations in SNC execution context, which shall be used when developing applications including both FreeRTOS tasks and SNC uCodes. These SNC-context macro functions (found in the respective snc_hw_xxx.h files and documented in [Ref_04]) focus on:

  • System control (snc.h & snc_hw_sys.h)

  • GPIO control and configuration (snc_hw_gpio.h)

  • SPI interface control and data transfer (snc_hw_spi.h)

  • I2C interface control and data transfer (snc_hw_i2c.h)

  • GPADC control and data acquisition (snc_hw_gpadc.h)

5.8.5.3. SNC Queues

As already presented in Section Section 5.8.3, the SNC Programming Framework provides a mechanism for data exchange between OS tasks and SNC uCodes; the SNC queues. In this context, a set of API macro functions are provided in snc_queues.h (documented in [Ref_04]), implementing

  • Acquisition of the address from where data can start being written into a chunk of the SNC-to-CM33 queue

  • Pushing data to a chunk of the SNC-to-CM33 queue

  • Acquisition of the address from where data can start being read from a chunk of the CM33-to-SNC queue

  • Popping data from a chunk of the CM33-to-SNC queue

  • Acquiring the Status of uCode SNC Queues

  • Resetting the SNC-to-CM33 queue data

In order to enable the SNC Queues mechanism, dg_configUSE_SNC_QUEUES needs to be defined and set to “1” in the configuration of the project.

5.8.6. Sensor Node Controller Debugging

One of the most important aspects of programming is “debugging”. Therefore, except for the SYSCPU code, a developer must be able to debug the SNC uCodes integrated in a DA1469x system application. The SNC Programming Framework extends the debugging process to SNC execution context, allowing for complete and system-as-a-whole debugging; not debugging each CPU core separately, which may fail to detect faulty system behavior when the cores operate in parallel.

In essence, the provided debugging mechanism applies to both SNC and SYSCPU execution contexts at the same time. That is, when debugging an SNC uCode and its execution is stopped, for example, at a specific SeNIS command in its implementation, then the SYSCPU execution is stopped, too. In this way, the whole SYSCPU-SNC system is stopped, and its state (i.e. variables, registers etc.) can remain unchanged in order to be checked for logic errors and in general faulty system behavior.

In order to enable the SNC debugging mechanism, dg_configUSE_SNC_DEBUGGER needs to be defined and set to “1” in the configuration of the project. In this way, all respective SNC debugging construct macros, presented in the following subsections, are defined in snc_debug.h. Further technical information regarding the usage of the SNC Debugging API can be found in [Ref_04].

The SNC Programming Framework provides the following debugging solutions:

SNC breakpoints & Step-by-Step Debugging

SNC breakpoints are software implementations of breakpoints which “bind” the execution of SNC and SYSCPU by sending special SNC-to-CM33 notifications when the execution flow reaches the particular places in the uCode where those SeNIS-based constructs are defined. More specifically, when SNC executes the SeNIS commands implied by those constructs, SNC execution flow is paused, a corresponding to the SNC breakpoints CM33 execution flow is created which passes through special macro constructs or in general code where a typical CM33 (HW) breakpoint can be placed, and finally SNC execution is resumed from where it has stopped, as presented in Figure 70 for the SNC_BKPT() construct macro. Therefore, having “injected a piece of CM33 execution flow” into the SNC execution flow, the latter can be indirectly stopped when placing a CM33 breakpoint in that CM33 execution flow part.

../_images/fig_SNC_19.png

Figure 70 SNC breakpoint implementation for pausing SNC execution with CM33 breakpoints

It is important to be noted that SNC breakpoints are software breakpoints which are implemented with SeNIS commands in a uCode, thus being created at uCode-build-time as part of the uCode implementation. Furthermore, when adding SNC breakpoint construct macros in a uCode implementation, even if no CM33 (HW) breakpoint is placed, the CM33 execution flow part that is produced by the SNC breakpoint still executes, that is, CM33 wakes-up whenever an SNC breakpoint “executes” in SNC context.

In SNC execution context, Step-by-Step debugging can be supported over a set of SeNIS-based construct macros, by defining a region in a uCode implementation using SNC_STEP_BY_STEP_BEGIN() and SNC_STEP_BY_STEP_END() construct macros. Furthermore, different groups of SNC Breakpoints and Step-by-Step regions can de defined, so that they can be enabled and disabled separately.

SNC Emulator

In order to allow for full flexibility when debugging applications with SNC uCodes, the SNC Emulator is provided by the Sensor Node Controller Programming Framework, which is enabled if dg_configUSE_HW_SENSOR_NODE_EMU is defined and set to “1” in the configuration of the project. More specifically, the SNC Emulator implements in software the same I_FSM implemented in HW in the SNC module, and it uses an OS task to execute the SeNIS commands of the SNC uCodes comprising an application, called Sensor Node Emulator task. In this way, all SNC uCodes are executed in CM33 context, concurrently (not in parallel) with the rest of the OS tasks comprising an application, but in exactly the same way in terms of the implemented logic as they are executed by the SNC HW.

Since SeNIS commands execution by the SNC Emulator is performed in CM33 context, appropriate underlying mechanisms are implemented that allow for CM33 breakpoints to be placed to every SeNIS-based construct macro or SNC API macro function call in SNC uCode implementations, essentially translating directly the CM33 breakpoints to SNC ones. Regarding step-by-step debugging, it can be enabled in the same way it is performed for SNC step-by-step debugging region groups of SNC breakpoints, by placing an CM33 breakpoint at the position of a special group being defined in snc.h for SNC Emulator breakpoints, namely SNC_BKPT_GROUP_EMU() macro (with implied group name: snc_bkpt_group_emu), thus enabling (setting) all SNC Emulator breakpoints throughout the whole SW implementation related to SNC uCodes.

It is important to be noted that executing SeNIS commands in SNC Emulator (CM33) context is significantly slower than in SNC context, therefore SNC Emulator must be used only at the first stages of SNC uCodes development, or generally for verifying their correct functionality in non-time critical execution cases of an application. Furthermore, the memory space used for the SNC uCode in heap must be doubled to allow for CM33 breakpoint to SNC (Emulator) breakpoint “translation” throughout the whole SW implementation related to SNC uCodes.

In Figure 71, it is shown an illustration of what happens when placing a CM33 breakpoint (i.e. in Smart Snippets Studio IDE) at the position of a SeNIS-based construct macro or at the position where SNC_BKPT_GROU_EMU() macro is defined.

../_images/fig_SNC_22.png

Figure 71 Using SNC Emulator breakpoints and step-by-step debugging in Smart Snippets Studio

SNC Assertion

In SNC execution context, in order to create an assertion on a variable value, SNC_ASSERT() macro can be used. If the given condition is evaluated to false, both CM33 and SNC execution will be blocked forever.

Regarding the conditional expression, it can be only:

  • A Boolean expression implied by one operand (i.e. op1) that can be either a uCode-build-time value or the contents of a Register/RAM address, thus evaluating to “true” when the implied value is equal to “true” or non-zero.

  • A Boolean expression implied by two operands and a relational operator defining a relationship between the two operands that may refer either to a uCode-build-time value or the contents of a Register/RAM address, thus evaluating to “true” when the defined relationship holds.

In Code 44, it is presented an example code demonstrating a usage case for SNC_ASSERT() macro, while an illustration of what happens when the conditional expression is evaluated to “false” (i.e. in Smart Snippets Studio IDE), is shown in Figure 72.

Code 44 Example SNC uCode using SNC_ASSERT() macro for defining an SNC assertion
uint32_t foo_mode = 3;            // Definition of the variable foo_mode
uint32_t foo_flags;               // Definition of the variable foo_flags
uint32_t foo_stat_cnt = 10;       // Definition of the variable foo_stat_cnt

SNC_UCODE_BLOCK_DEF(foo_ucode)    // Definition of foo_ucode uCode-Block
{
    // Initialize foo_flags to 0x0000
    SENIS_assign(da(&foo_flags), 0x0000);

    // If foo_mode (direct addr.) is equal to 3, ...
    SENIS_if(da(&foo_mode), EQ, 3) {
        // set/toggle bit-15 of foo_flags (direct addr.), ...
        SENIS_xor(da(&foo_flags), 1 << 15);
    // otherwise, ...
    SENIS_else {
        // if foo_mode (direct addr.) is equal to 4, ...
        SENIS_if(da(&foo_mode), EQ, 4) {
            // set/toggle bit-15 of foo_flags (direct addr.)
            SENIS_xor(da(&foo_flags), 1 << 13);
        }
    }}

    // foo_mode (direct addr.) must be greater than 0.
    SNC_ASSERT(da(&foo_flags), GT, 0);

    // If foo_flags (direct addr.) has bit 13 set, ...
    SENIS_if(da(&foo_flags), BIT, 13) {
            // increment foo_stat_cnt (direct addr.) by 1
            SENIS_inc1(da(&foo_stat_cnt));
    }
}
../_images/fig_SNC_23.png

Figure 72 Using SNC assertion in Smart Snippets Studio

5.9. The USB Framework

5.9.1. Introduction

This section describes the USB support in the SmartSnippets™ DA1469x SDK and the necessary steps to add USB support to a user application. The USB port can be used as a charging port and/or an interface to a host machine.

5.9.2. SYS_USB

In order to enable the USB port, the user must first add sys_usb to the application, and set the relative configuration macro.

Code 45 Adding sys_usb to a project
#include "sys_usb.h"
.
.
sys_usb_init();
Code 46 Enabling sys_usb
#define dg_configUSE_SYS_USB (1)

The following diagram illustrates the sys_usb initialization procedure, when the device is configured to use both the USB charger and the USB stack.

Please note that any one of the two features can be used independent of each other.

../_images/usb_init.png

Figure 73 SYS_USB initialization

5.9.3. The USB Charger

For information on how to use the USB charger of the DA1469x please refer to chapter Section 4.1.3.

5.9.4. USB Data Connection

To enable the device to be able to enumerate and communicate with a connected host via the USB port, macro dg_configUSE_USB_ENUMERATION must be set.

Code 47 Enabling USB Enumeration
#define dg_configUSE_USB_ENUMERATION (1)

5.9.4.1. The SDK USB Stack

The SmartSnippets™ DA1469x SDK includes a complete USB stack implementation, based on the emUSB stack, version 3.12a, provided by SEGGER.

To add the USB stack to a user project, link the library object usb_lib_da1469x to the application. The necessary header files are located under

<sdk_root_directory>/sdk/interfaces/usb/include.

In order to configure and use the USB stack, please refer to the SEGGER emUSB user manual, or check the example applications provided with the SDK, under

<sdk_root_directory>/projects/dk_apps/features/usb_cdc_vmsd

and

<sdk_root_directory/projects/dk_apps/features/usb_cdc.

5.9.4.2. Using a different USB Stack

A different USB stack than the one provided with the SDK may be used. In that case, please check chapter Section 5.9.4.3 to see which callback functions are necessary to be implemented by the user-provided USB Stack.

5.9.4.3. Callback Functions / Hooks

The application may implement the following hook functions in order to be notified about USB events

Table 46 USB Hook Functions

Function

Description

sys_usb_ext_hook_attach()

This function is called when the device is physically attached to the port. In case the USB Charger is used by the application, then this function is called after the Charger has finished with the port detection procedure.

sys_usb_ext_hook_begin_enumeration()

This function is called when the USB Host has signaled the device that it must begin the enumeration procedure.

sys_usb_ext_hook_detach

This function is called when the device has been physically disconnected from the USB port

In case the application is not using the USB stack provided by the SDK, then it needs to implement the following callback functions.

Table 47 USB callback functions

Function

Description

hw_usb_ep_rx_read_by_driver()

This function will indicate if the USB driver should read the Rx data or if the Rx data is read by something else.

hw_usb_ep_get_rx_buffer()

This function will be called in order to get a buffer to store the data received.

hw_usb_ep_rx_done()

This function will be called when a receive operation has been completed.

hw_usb_ep_tx_done()

This function will be called when a transmit operation has been completed.

hw_usb_ep_nak()

This function will be called when a NAK has been received for an endpoint.

hw_usb_bus_event()

This function will be called to indicate bus events.

hw_usb_bus_frame()

This function will be called with the current USB frame number.

5.9.5. USB Events

5.9.5.1. VBUS Attach

This event signals that the device has been physically attached to a USB port.

  • Upon receiving the interrupt, sys_usb sets the device power mode to active and calls the application sys_usb_ext_hook_attach() hook, if it is implemented.

  • sys_usb then enables the USB pads and programs the USB interrupts.

  • If the application is configured with charger support, the charger subsystem is notified.

  • Afterwards, the USB low level driver is initialized and the device is attached to the USB BUS.

  • Finally, application hook sys_usb_ext_hook_begin_enumeration() is called, if it is implemented.

At this point the device should be ready to begin USB enumeration when triggered by the host.

The following diagram illustrates the USB attach procedure.

../_images/usb_attach.png

Figure 74 USB VBUS Attach

5.9.5.2. VBUS Detach

This event signals that the device has been physically detached from the USB port.

  • The USB pads are disabled

  • sys_usb restores the power mode that was used before the device was attached to the USB port.

  • If the application is configured with charger support, the charger subsystem is notified.

  • The device is detached from the USB BUS.

  • sys_usb stops using the PLL clock.

  • Finally, application hook sys_usb_ext_hook_detach() is called, if it is implemented.

The following diagram illustrates the USB detach procedure.

../_images/usb_detach.png

Figure 75 USB VBUS Detach

5.9.5.3. USB Reset / Suspend / Resume

USB Reset

This interrupt indicates that the device must begin the enumeration procedure.

  • sys_usb switches the device power mode to active and starts using the PLL clock.

  • The USB interrupts are programmed accordingly.

  • The USB stack is notified of this event via callback hw_usb_bus_event(), with event UBE_RESET.

Please note that the application is not directly notified by sys_usb, but instead via the USB stack.

USB Suspend

This interrupt signals that the USB host has been suspended and the device should lower the power consumption.

  • If the application is configured with charger support, the charger subsystem is notified.

  • The USB stack is notified of this event via callback hw_usb_bus_event(), with event UBE_SUSPEND.

  • The USB interrupts are programmed accordingly.

  • If the application is configured with USB_SUSPEND_MODE_IDLE, sys_usb switches the power mode to idle.

  • sys_usb stops using the PLL clock.

Please note that the application is not directly notified by sys_usb, but instead via the USB stack.

USB Resume

This interrupt signals that the USB Host has exited the suspend state and has resumed operation.

  • sys_usb starts using the PLL clock.

  • The USB stack is notified of this event via callback hw_usb_bus_event(), with event UBE_RESUME.

  • The USB interrupts are programmed accordingly.

  • If the application is configured with USB_SUSPEND_MODE_IDLE, sys_usb switches the power mode to active.

  • If the application is configured with charger support, the charger subsystem is notified.

Please note that the application is not directly notified by sys_usb, but instead via the USB stack.

The following diagram illustrates the USB interrupt handling procedure.

../_images/usb_irq_handling.png

Figure 76 USB Interrupt Handling

5.9.6. USB Suspend Modes

In response to a suspend signal from the USB host, the application may switch to an idle power state, so as to decrease its power consumption. Whether this happens or not is controlled by the macro configuration parameter dg_configUSB_SUSPEND_MODE.

Curently for the DA1469x two suspend modes are supported:

USB_SUSPEND_MODE_NONE indicates that the suspend signal will have no effect on the system’s power state.

USB_SUSPEND_MODE_IDLE indicates that the suspend signal will cause the system’s power state to switch to idle.

5.9.7. USB Power Consumption Considerations

In order for a device to be USB-IF certified, among other parameters, it needs to exhibit a low current draw from the USB host, when the host is suspended. The USB framework attempts to keep the power consumption to a minimum when the device is connected to a suspended host. It is however necessary for the user application as well to go into a power-saving mode when the host is suspended.

5.10. Haptic (LRA/ERM) Support

The SmartSnippets™ DA1469x SDK includes a set of software modules supporting the use of the Haptic Driver/Controller hardware module (available in the DA14697 and DA14699 variants of the DA1469x chipset family) together with external actuators, such as LRAs or ERMs, for producing haptic effects (in order to provide haptic feedback (e.g. typing on a touchscreen) as well as alerts/notifications to the user).

5.10.1. Introduction

5.10.1.1. Background theory

The most common types of actuators currently used in the haptic technology domain are the LRAs and ERMs. They are electromechanical devices that respond to an input voltage signal by performing a periodic mechanical movement. This movement creates a vibration effect and is characterized by a certain intensity (strength of vibration) and frequency (corresponding usually also to a certain acoustic tone). By varying the intensity and/or frequency of vibration over time, we can create a specific haptic waveform pattern (or simply haptic pattern).

5.10.1.1.1. ERM

An ERM (Eccentric Rotating Mass) motor consists of a DC motor with an off-center mass attached to its shaft. By applying a DC voltage signal to the motor leads, the unbalanced mass rotates and the centrifugal force causes a periodic displacement of the motor-mass system. The speed of rotation depends on the amplitude of the applied voltage and the motor-mass system characteristics, and affects both the intensity and frequency of the vibration (so the two vibration characteristics are correlated).

5.10.1.1.2. LRA

An LRA (Linear Resonant Actuator) consists of a magnetic mass attached to a spring and a voice coil, the axis of which is aligned to the the axis of the spring. The electrical current flowing through the coil induces a magnetic field that causes a displacement of the magnetic mass. So, by applying an AC voltage signal to the voice coil leads, we can create a linear periodic displacement of the magnetic mass. The movement of the mass with respect to the voice coil (and more specifically the speed of this movement) induces a Back-electro-motive force (BEMF) which has a sinusoidal shape (following the shape of the mechanical movement) and is reflected on a sinusoidal component in the electrical current of the LRA. By varying the amplitude and the frequency of the applied AC voltage signal we can control both the intensity and frequency of the vibration independently (unlike ERMs). However, as described in the next subsection, the drive frequency affects both vibration characteristics.

5.10.1.1.3. LRA Frequency Control

As every mass-spring oscillating system, LRAs have a frequency-dependent response, which usually gets maximized at a certain frequency and (depending on the type of the LRA) may decline very rapidly as the drive frequency moves away from it. This resonant frequency may also vary due to environmental or other conditions (temperature, pressure, mechanical coupling etc.), so it is important to be able to follow this change and adjust the frequency of the drive voltage signal accordingly.

5.10.1.1.4. Overdrive

ERMs and LRAs have a mechanical response time which impacts how long they take to reach full speed/oscillation after a drive signal is applied (start time) and come to rest after the drive signal is removed (stop time). A slow mechanical response time can limit the transient impact of haptic patterns causing them to be muddied and less distinct. Start/stop times can be reduced however by accelerating/braking the actuator by overdriving it temporarily until it reaches its target speed/oscillation amplitude. In the case of acceleration, the actuator is temporarily driven with a higher drive voltage and in the case of braking, it is driven with a lower or negative drive voltage.

5.10.1.2. DA1469x Haptic Driver

The Haptic Driver and Controller hardware module of DA1469x (henceforth simply Haptic Driver) provides a DC or AC differential drive voltage signal suitable for driving haptic actuators (see Figure 77).

../_images/haptic_driver_output.png

Figure 77 Haptic Driver output voltage signal and configurable drive parameters

The drive signal is formed by a 250kHz PWM signal of either constant (DC) or alternating polarity (AC).

The duty cycle of the PWM signal is run-time configurable and is perceived by the actuator as the amplitude of the drive signal. It therefore directly affects the vibration intensity response of the haptic actuator, and in the case of ERM it also affects the vibration frequency. Since each haptic actuator is characterized by a maximum supported voltage (corresponding to a maximum duty cycle of the PWM signal), the term drive level can be used for describing the duty cycle of the drive signal as a percentage of the maximum duty cycle supported by the actuator.

The amplitude of the PWM signal is not configurable and depends on the supply voltage of the DA1469x device and also on the Haptic Driver’s load (the actuator’s impedance).

The frequency of the polarity switching in the AC drive case, and more specifically the half period of the polarity switching, is also run-time configurable and is perceived by the actuator (LRA) as the frequency of the AC drive signal. It directly affects the vibration frequency of the LRA but it also affects the vibration intensity due to the resonating behaviour of the LRA (as described above).

Apart from the duty cycle and the frequency, the automatic polarity switching pattern of the drive signal can also be inverted during run-time. This can be used for braking the haptic actuator (in case a negative drive voltage needs to be applied). This state of the automatic polarity switching pattern (normal or inverted) is henceforth simply called polarity (not to be confused with the instant polarity of the drive signal).

The three configurable parameters described above (half period, drive level, polarity) are henceforth simply called drive parameters.

The Haptic Driver is also capable of continuously measuring the electrical current flowing through the actuator circuit by sampling the voltage across a series sense resistor using an ADC. More specifically, within every half period of the drive signal, it stores 8 samples of the electrical current (i-samples) at equidistant times and, if configured appropriately, also generates an interrupt request right after storing one specific (out of the eight) sample (e.g. after capturing the 7th sample at every half period). By servicing those interrupt requests, the drive parameters can be dynamically updated (possibly also by reading and analyzing the i-samples), either for producing a specific desired haptic pattern and/or for ensuring efficient driving using the Overdrive and Frequency Control principles described in the previous section.

This mechanism of dynamic update of the drive parameters will be henceforth referred to as haptic processing. If the haptic processing takes into account the current state of the Haptic Driver and the actuator’s response (i.e. drive parameters, instant polarity, i-samples) we have the case of closed loop (haptic) processing. Otherwise, we have an open loop (haptic) processing case. More information (incl. Figure 79) is provided in Section Section 5.10.2.1.

At any time, the drive signal can be temporarily switched off. In this case, the interrupts are also disabled and the Haptic Driver is said to be in Idle state, since no power is delivered to the actuator but the drive parameters and any other configuration settings are retained. Otherwise (if the drive signal is switched on), the Haptic Driver is said to be Active state.

More information about the Haptic Driver hardware module can be found in the relevant section of [Ref_01].

5.10.2. SmartSnippets™ DA1469x SDK Haptic Software modules

The haptic software support provided by the SmartSnippets™ DA1469x SDK consists of the following SW modules, as well as example applications (lra and erm) that make use of them:

  • The Haptic Low Level SW Driver, which provides low level abstraction as well as interrupt handling of the Haptic Driver hardware module

  • The Haptics Algorithm Library, which can be used for efficient driving of LRAs (based on the Frequency Control and Overdrive principles)

  • The Waveform Memory Decoder, which can be used for decoding encoded haptic patterns

  • The Haptic Adapter, which provides an integrated API to the application for safe and abstracted use of the other haptic SW modules in order to simply produce various haptic effects.

A brief overview of the structure of the separate modules and their interactions is shown in Figure 78.

../_images/haptic_architecture.png

Figure 78 Haptic SDK Support overview

It is recommended to make use of the supported haptic functionality via the Haptic Adapter API and not directly use the API of the other haptic SW modules. However, the configuration parameter structure of the Adapter (required for its initial configuration) makes use also of the parameter structures of the other SW modules, so some basic understanding of the functionality of all related SW modules is recommended. For this reason, some description of each SW module (and not only the Haptic Adapter) and its usage is provided in the following sections. The lra application project is recommended to be used as an example usage reference.

5.10.2.1. Haptic Low Level SW Driver

The Haptic Low Level SW Driver (Haptic LLD) (sdk/bsp/peripherals/src/hw_haptic.c and bsp/peripherals/include/hw_haptic.h) interfaces directly to the Haptic Driver hardware module. Its primary purpose is to provide an abstraction of the module’s functionality to the higher layers of the SDK and to the application layer in order to allow them to initialize and control the module more easily, without requiring detailed knowledge of the associated registers, their fields and their usage. It also handles any interrupts generated by the module.

The provided functions (or function-like macros) of the Haptic LLD API are the following:

Function

Description

hw_haptic_init()

Configure the Haptic Driver for driving the specific actuator in use and initialize the Haptic LLD’s internal data

hw_haptic_get/set_drive_level()

Get/set the drive level

hw_haptic_get/set_polarity()

Get/set the polarity

hw_haptic_get/set_half_period()

Get/set the half period

hw_haptic_get/set_state()

Get/set the Haptic Driver state (Idle/Active)

hw_haptic_shutdown()

Deactivate the Haptic Driver completely

HW_HAPTIC_CONV_FREQ_TO_HALFPERIOD()

Convert frequency to half period

HW_HAPTIC_CONV_VOLT_TO_DUTYCYCLE()

Convert voltage to duty cycle

The Haptic LLD’s configuration parameter structure, haptic_config_t, passed (by reference) to hw_haptic_init(), consists of the following parameters:

Parameter

Description

duty_cycle_nom_max

Maximum nominal duty cycle of the drive signal that is supported by the actuator

duty_cycle_abs_max

Maximum absolute duty cycle of the drive signal that is supported by the actuator

half_period

Initial half period of the drive signal in case of an LRA (should be set according to its nominal/initial resonant frequency) - or just half period of the interrupt requests in the ERM case (optional)

interrupt_cb

Callback function to be fired on interrupt

signal_out

Initial polarity of the DC Drive signal (ERM only)

Notes:

  • The interrupt_cb parameter should point to a callback function that is desired to be called by the Interrupt Handler on every interrupt request of the Haptic Driver. If set to NULL, then the interrupt requests are disabled. The hw_haptic_init() function configures the Haptic Driver to generate interrupt requests after capturing the 7th sample at every half period. The Interrupt Handler calls the callback function by feeding it with the current drive parameters, the instant polarity and the eight i-samples that correspond to the current half period. The callback function is then allowed to perform haptic processing and update the drive parameters accordingly (see Figure 79).

    ../_images/haptic_processing.png

    Figure 79 Open and closed-loop haptic processing mechanism.

    The choice of this specific sample index (7th) enables the callback function to have both an adequate view of the shape of the LRA’s electrical current during the current half period of the drive signal as well as enough time to update the drive parameters before the beginning of the new half period.

  • The duty_cycle_nom_max parameter corresponds to the maximum nominal RMS voltage supported by the actuator. It can be extracted from the actuator’s datasheet. In order to easily convert the RMS voltage value to a duty cycle value, the HW_HAPTIC_CONV_VOLT_TO_DUTYCYCLE() function-like macro can be used. The duty_cycle_abs_max parameter corresponds to the maximum absolute RMS voltage supported by the actuator. It is the maximum RMS voltage that is supported for short time periods (i.e. transient, e.g. when accelerating/braking the actuator) so it should be greater than the nominal RMS voltage value. This is not always provided by the datasheet. In this case, it could be approximated as 10% above the maximum nominal RMS voltage.

  • The half_period parameter defines the initial half period of the drive signal and apparently also the initial frequency of the interrupt requests (if enabled).

    It is mandatory only in the LRA case and should be set according to the LRA’s initial resonant frequency, as specified in the actuator’s datasheet.

    In the ERM case, the parameter is optional and it only defines the frequency of the interrupt requests (if enabled). If not set, the default value of 200Hz is used. (Although there is no automatic polarity switching in this case, the i-samples are still captured in the same way as in the AC case, based on the current setting of the half period related register.)

  • The Haptic Driver HW module features also a hardware mechanism for ensuring that the electrical current of the actuator is constant within each half period. This is done by monitoring the i-samples and using a HW loop filter to continuously adjust the duty cycle of the PWM signal. If this mechanism is used, the hardware module is said to operate in constant current mode. Otherwise it operates in constant duty cycle mode. Only the latter one (constant duty cycle) however is used and supported by the SDK.

5.10.2.1.1. Programming model

As explained earlier, it is recommended to use the Haptic Adapter API and not directly use that of the other haptic SW modules. Nevertheless, the programming model of using the Haptic LLD is described below for completeness and in order to provide a better understanding of the functionality of each SW module and their interactions:

  1. Depending on the type of actuator that is used, either dg_configUSE_HW_ERM or dg_configUSE_HW_LRA should be defined to 1, in the appropriate custom_config_xxx.h file. (In case of activating both configuration options, then by default the LRA functionality will be enabled.)

  2. An instance of haptic_config_t must first be created and the respective configuration parameters should be set according to the actuator in use by taking the actuator characteristics (from its datasheet) into account and possibly using the HW_HAPTIC_CONV_VOLT_TO_DUTYCYCLE() function-like macro.

  3. If interrupt_cb is set to point to a specific callback function, this callback function should be implemented appropriately to perform haptic processing.

  4. The hw_haptic_init() function should be called by passing a pointer to the haptic_config_t instance. The Haptic Driver gets initialized and enters Idle state.

  5. The hw_haptic_get/set_drive_level(), hw_haptic_get/set_polarity() and hw_haptic_get/set_half_period() functions can be used anytime (as long as the previous steps have been completed) for reading or dynamically setting the drive parameters. The hw_haptic_set_state() function should also be called at anytime for enabling the drive signal (or disabling it, e.g. in case of long gaps between pulses/patterns).

    Note

    If closed loop processing is used, use of the hw_haptic_set_drive_level(), hw_haptic_set_polarity() and hw_haptic_set_half_period() functions is not recommended as, in this case, any changes to the drive parameters should ideally be synchronized with the start/end of the half period, in order to avoid step changes in the sampled electrical current, which may have a negative impact on the closed loop processing algorithms (e.g. overdrive, frequency control). So in this case, any updates on the drive parameters should only be applied within the context of the Interrupt Handler’s callback function.

  6. When all intended haptic activities have been performed, the Haptic Driver can be completely shut down (for minimizing power consumption) using the hw_haptic_shutdown() function.

5.10.2.2. Haptics Algorithm Library

The Haptics Algorithm Library (sdk/middleware/haptics/include/{curve_fitting.h, lra_frequency_control.h, smart_drive.h, haptics_lib.h} & sdk/middleware/haptics/lib) consists of a set of algorithmic modules that can be used in the context of haptic processing (within the Interrupt Handler’s callback function) for efficient real-time control of the drive parameters. It serves more specifically for overdriving LRAs and for tracking/following their resonant frequency.

A very basic description of each module, as well as, a list of the functions of the related API is provided below. Each module consists also of a parameter structure that should be initialized appropriately. However, a list or description of these parameter structures is not provided here, since it is out of the scope of this brief User Guide and since more detailed information about the functionality and usage of the Haptics Algorithm Library can be found in its respective documentation (sdk/middleware/haptics/README_haptics_lib.md, also easily accessed from the main page of [Ref_04]).

5.10.2.2.1. Curve Fitter

The Curve Fitter module analyzes the i-samples and extracts the dc_offset of the electrical current as well as the amplitude and phase (phase offset with respect to the drive signal) of the BEMF component of the electrical current.

Function

Description

curve_fitter_process()

Process the i-samples and calculate the dc_offset, and the amplitude & phase of the BEMF

5.10.2.2.2. Automatic Frequency Control

When the LRA is driven on its resonant frequency, its mechanical oscillation is in phase with the drive signal. Otherwise, there is a phase offset which is reflected on the phase of the BEMF. The Automatic Frequency Control module processes the BEMF phase (as provided by the Curve Fitter module) and calculates the new appropriate half period of the drive signal (corresponding to the resonant frequency), so that the phase offset disappears.

Function

Description

lra_afc_process()

Calculate the new half period, based on the phase of the BEMF and the current half period

5.10.2.2.3. SmartDrive™

The SmartDrive™ module implements the Overdrive principle for LRAs. It calculates the appropriate drive level so that the LRA reaches a desired vibration intensity level (target level) in minimum time. This is done by taking into account the amplitude of the BEMF component of the electrical current (as provided by the Curve Fitter).

Function

Description

smart_drive_init()

Initialize SmartDrive™ algorithm

smart_drive_process()

Calculate the new required drive level, based on the amplitude of the BEMF, the current drive level and the target level

5.10.2.2.4. Haptics_lib

A wrapper module is also included for efficient usage of the library and also for providing some additional functionality not covered by the individual modules. It is recommended to use this module instead of directly employing the API of the individual ones.

Function

Description

haptics_lib_init()

Initialize the Haptics Algorithm Library

haptics_lib_process()

Process the i-samples, the target level and the current hardware state of the Haptic Driver and update the drive parameters accordingly

5.10.2.2.5. Programming model

As explained earlier, it is recommended to use the Haptic Adapter API and not directly use that of the other haptic SW modules. Nevertheless, the appropriate steps in case of using only the Haptics Algorithm Library together with the Haptic LLD is briefly described below (for completeness and in order to provide a better understanding of the functionality of each SW module and their interactions):

Apart from the steps described in the Haptic LLD section, the following additional steps should be performed:

  1. The Interrupt Handler’s callback function should be implemented to call the haptics_lib_process() function with the current drive parameters, the instant polarity and the i-samples (as provided by the Interrupt Handler), as well as the current target level.

  2. An instance of haptics_lib_params must first be created and the respective configuration parameters should be set according to the actuator in use.

  3. The haptic_lib_init() function should be called before putting the Haptic Driver into Active state.

  4. The target level should be set (according to the desired vibration intensity level).

5.10.2.3. Waveform Memory Decoder

A haptic pattern can be represented by a sequence of drive amplitude - drive frequency pairs over time. This sequence can be encoded in a form that allows it to be stored in memory without occupying too much space.

The Waveform Memory Decoder SW module (sdk/middleware/haptics/include/wm_decoder.h & sdk/middleware/haptics/src/wm_decoder.c) decodes haptic patterns stored in a byte array in the same format as the one used by Dialog’s DA7280 Haptic Driver IC. The format is described in detail in the “Waveform Memory” section of the IC’s datasheet). It stores the patterns in the form of piecewise linear segments, snippets, frames and eventually sequences. Each sequence of the encoded DA7280 waveform memory format corresponds therefore to an encoded haptic pattern and is henceforth referred to as waveform sequence. The set of the encoded haptic patterns encoded in the array makes up the so-called waveform memory. Hence, the Waveform Memory Decoder decodes the waveform memory in order to provide us with the amplitude-frequency pair of a specific waveform sequence at a given time t (with respect to the beginning of the waveform sequence).

More details about the functionality and usage of the Waveform Memory Decoder can be found in its respective documentation (sdk/middleware/haptics/README_wm_decoder.md), which can also be easily accessed from the main page of [Ref_04].

The functions provided by the module’s API are the following:

Function

Description

wm_decoder_init()

Initialize the Waveform Memory Decoder

wm_decoder_trigger_sequence()

Trigger start/stop of playback of sequence in waveform memory

wm_decoder_update()

Update state of Waveform Memory Decoder according to current time

5.10.2.3.1. Programming model

As explained earlier, it is recommended to use the Haptic Adapter API and not directly use of that of the other haptic SW modules. Nevertheless, the appropriate steps in case of using only the Waveform Memory Decoder together with the Haptic LLD are briefly described below (for completeness and in order to provide a better understanding of the functionality of each SW module and their interactions):

Apart from the steps described in the Haptic LLD section, the following additional steps should be performed:

  1. The Interrupt Handler’s callback function should be implemented to call the wm_decoder_update() function by feeding the current time (as read by a free running 32-bit timer) in order to get the current amplitude-frequency pair (whenever a waveform sequence playback has been triggered). Then, the drive parameters should be updated based on the current amplitude and frequency pair.

  2. The wm_decoder_init() function should be called for initializing the Waveform Memory Decoder. If needed, the timer used in step 1 should also be configured/initialized.

  3. The wm_decoder_trigger_sequence() function should be called by feeding it with the current time (read by the same free running 32-bit timer used in step 1) and the index of the desired waveform sequence to be played.

The above steps can be used both with LRAs and ERMs. However, in the ERM case, the decoded frequency will not have any noticeable effect as it will just change the frequency of the interrupts. As explained earlier, the actual vibration frequency of an ERM is correlated with its vibration intensity, so it actually depends on the decoded amplitude.

5.10.2.4. Haptic Adapter

The Haptic Adapter (sdk/middleware/adapters/src/ad_haptic.c and sdk/middleware/adapters/include/ad_haptic.h) is a middleware abstraction layer module that interacts with all of the other haptic SW modules as well as the application layer, integrating their functionality so that they can operate together as a whole to provide a complete haptic solution. It provides a convenient way to use the Haptic LLD together with both the Haptics Algorithm Library and the Waveform Memory Decoder and, at the same time, it ensures power stability (i.e. that the appropriate power domains are on during the haptic operations) and provides arbitration among multiple tasks.

Function

Description

ad_haptic_open()

Open a haptic operating session (Configure the Haptic Driver and the related SW modules)

ad_haptic_reconfig()

Apply a new haptic configuration

ad_haptic_set_drive_level()

Set the drive level (directly or indirectly, depending on the current drive mode)

ad_haptic_set_polarity()

Set the drive polarity

ad_haptic_set_half_period()

Set the half period

ad_haptic_set_state()

Set the Haptic Driver into Active or Idle state

ad_haptic_set_drive_mode()

Set the current drive mode of the haptic operation (i.e. enable/disable Overdrive and Frequency Control parts of the Haptics Algorithm Library).

ad_haptic_play_wm_sequence()

Play one of the waveform sequences stored in the waveform memory

ad_haptic_stop_wm_sequence()

Stop the currently played waveform sequence

ad_haptic_update_drive_parameters()

Update haptic drive parameters (to be used as the interrupt Handler’s callback function)

ad_haptic_close()

Terminate the haptic operating session

The Adapter’s configuration parameter structure, ad_haptic_controller_conf_t, passed (by reference) to ad_haptic_open() and ad_haptic_reconfig(), has the following members:

Parameter

Description

drv

Pointer to the the Haptic LLD’s configuration parameter structure (haptic_config_t) instance

lib

Pointer to the Haptics Algorithm Library’s parameter structure (haptics_lib_params) instance

wm

Pointer to the the Adapter’s Waveform Memory configuration parameter structure (ad_haptic_wm_conf_t) instance

The Adapter’s waveform memory configuration structure, ad_haptic_wm_conf_t, consists of the following parameters:

Parameter

Description

data

Pointer to the array containing all the available haptic patterns in the encoded DA7280 Waveform Memory format

accel_en

Specifies whether the amplitude values of each snippet will be interpreted as signed or unsigned

timebase

Specifies whether the minimum timebase of the waveform sequences is 1.36ms or 5.44 ms

Note: The Adapter can be used for both LRAs and ERMs. However, since the Automatic Frequency Control and SmartDrive™ modules are only applicable to LRAs, there is no point in having a Haptics Algorithm library configuration in this case. As a result, the lib member of the ad_haptic_controller_conf_t instance should be set to NULL in the ERM case.

Programming model:

  1. Depending on the type of actuator that is used, either dg_configUSE_HW_ERM or dg_configUSE_HW_LRA should be defined to 1, in the appropriate custom_config_xxx.h file. (In case of activating both configuration options, then by default the LRA functionality will be enabled.) Apart from that, dg_configHAPTIC_ADAPTER should be defined to 1.

  2. An instance of ad_haptic_controller_conf_t should be created and all of the respective parameters should be initialized. This implies that instances for all the individual configuration structures as well as the waveform memory array should also be created and initialized prior to this. If any of the provided haptic processing SW modules (Haptics Algorithm libray & Waveform Memory Decoder) are intended to be used, then the interrupt_cb parameter of the haptic_config_t instance should point to the ad_haptic_update_drive_parameters() function (or a custom callback function that should call the ad_haptic_update_drive_parameters() itself).

  3. The ad_haptic_open() function should be called in order to open a haptic operating session based on the configuration parameters as set in step 2. The returned handle should be stored, as it has to be used in the adapter’s other function calls. The system is now prevented from going to sleep until ad_haptic_close() is called.

  4. The ad_haptic_set_drive_mode() function should be called (if necessary) in order to enable or disable the OverDrive and Frequency Control features (as implemented by the Haptics Algorithm Library).

  5. The intended haptic effect can be produced either by manually setting the drive parameters using the ad_haptic_set_drive_level/polarity/half_period() functions (and by setting the Haptic Driver to Active state (if not already) via the ad_haptic_set_state() function) or by playing one of the available waveform patterns using the ad_haptic_play_wm_sequence() function. In the latter case, the Haptic Driver goes into Active state automatically at the beginning of the waveform sequence playback.

  6. Steps 4 and 5 should be repeated, if necessary, in order to produce more haptic effects. Also, in case a haptic effect needs to be produced with a different configuration (e.g. with different Haptics Algorithm Library parameters or with a different waveform memory array that contains a different set of haptic patterns), the ad_haptic_reconfig() function can be used for changing the Adapter’s configuration (before continuing again with 3 and 4).

    If any of the functions used in steps 4-6 is called while a waveform sequence playback is already in progress, the function will block until the ongoing playback is finished. An exception for this is the case of an urgent waveform playback request while a non-urgent waveform playback is in progress (see urgency parameter of the ad_haptic_play_wm_sequence() function). If this blocking is not desired, then the ad_haptic_stop_wm_sequence() function can be used in order to stop the ongoing sequence playback immediately. More details can be found in the Adapter’s header file (with the same content being also very conveniently presented in [Ref_04].)

  7. ad_haptic_close() should be called in order to terminate the haptic operating session.

A flow diagram showing the Adapter’s provided functions and their usage is provided in Figure 80 (the “ad_haptic_” prefix of the function names is apparently omitted).

../_images/haptic_adapter_api_flow_diagram.png

Figure 80 Haptic Adapter - Function usage flow diagram

5.10.2.5. Example applications

The SmartSnippets™ DA1469x SDK includes two example applications that demonstrate the usage of the Haptic Driver and the related software support described above.

5.10.2.5.1. lra

The lra application (projects/dk_apps/features/lra/) demonstrates the functionality provided by the Haptic Adapter API by producing various haptic effects using an LRA. By pressing the K1 button of the DA1469x Pro Development Kit (ProDK) board, a different haptic effect is produced (either by applying a specific haptic pattern, or by driving the LRA at a constant drive level and, in each case, while operating on a specific drive mode).

5.10.2.5.2. erm

The erm application (projects/dk_apps/features/erm/) drives an ERM motor at a constant (compile-time) configurable drive level in order to create a constant vibration effect. By pressing the K1 button of the ProDK, the drive signal is switched on or off (by setting the Haptic Driver into Active or Idle mode respectively). Due to the limited application’s functionality, the Haptic LLD API is employed directly (although, as explained above, it is generally strongly recommended to use the Haptic Adapter API).

More information can be found in the applications’ README.md files, which can also be easily accessed from the main page of [Ref_04].

5.11. Using SEGGER Open Flashloader

5.11.2. Toolset preparation

  1. In Smart Snippets Studio import the project segger_flash_loader located in the sdk, under sdk/bsp/system/loaders.

  2. Build the project in configuration DA1469x-00_Release.

  3. Copy the produced file segger_flash_loader.elf inside the SEGGER JLink installation folder as segger_flash_loader_da1469x.elf.

  4. Make sure that an appropriate JLink version is installed and selected in Smart Snippets Studio.

  5. In the SEGGER JLink installation folder, edit file JLinkDevices.xml, and add the following segment inside the <Databases> segment. (For more information on this, see Chapter ‘Open Flashloader/Adding a new device’ of the J-Link / J-Trace User Guide.)

Code 48 Adding DA1469x to the list of devices
<Device>
  <ChipInfo Vendor="Dialog Semiconductor" Name="DA1469x" WorkRAMAddr="0x810000" WorkRAMSize="0x10000" Core="JLINK_CORE_CORTEX_M33" />
  <FlashBankInfo Name="win. Flash" BaseAddr="0x0" MaxSize="0x100000" Loader="segger_flash_loader_da1469x.elf" LoaderType="FLASH_ALGO_TYPE_OPEN" />
</Device>

5.11.3. Project preparation

  1. Add the following entry in the custom_config_qspi.h file of the project that you want to flash.

Code 49 Produce SEGGER loader compatible images
#define  dg_configUSE_SEGGER_FLASH_LOADER       ( 1 )
  1. In SmartSnippetsStudio, modify the QSPI_DA1469x debug configuration:
    • Under Tab Main, enable auto build, if you want the project to automatically build before starting a debugging session.

    • Under Tab Debugger, set device name to DA1469x.

    • Under Tab Startup, enable load executable.

5.11.4. Flashing an image

Select the DA1469x debug configuration. The project will automatically begin to build, and once finished, the project image will be flashed onto the DA1469x and the debugger will be attached.