5. User Guides
5.1. The BLE Framework
Figure 24 depicts the general architectural scheme used.
Using a top-down approach, the layers that build-up the BLE Framework functionality can be identified as the following:
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.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.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.
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.
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:
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.
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 |
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.
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.
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:
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:
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
.
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.
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.
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.
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
).
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:
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:
Device initialization and configuration: Start-up BLE, setting device role, device name, appearance and other device parameters.
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.
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.
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.
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_gap_passkey_reply() |
Reply to a received |
ble_gap_numeric_reply() |
Reply to a received |
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.
Event |
Argument |
Description |
---|---|---|
BLE_EVT_GAP_PAIR_REQ |
ble_evt_gap_pair_req_t |
Pairing request received by a connected peer. Member |
BLE_EVT_GAP_PAIR_COMPLETED |
ble_evt_gap_pair_completed_t |
A previously requested pairing procedure has been completed. Member |
BLE_EVT_GAP_SECURITY_REQUEST |
ble_evt_gap_security_request_t |
Security request received by a connected peripheral peer. Members |
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 |
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_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_EVT_GAP_SEC_LEVEL_CHANGED |
ble_evt_gap_sec_level_changed_t |
The security level has been changed on an established link. Member |
BLE_EVT_GAP_SET_SEC_LEVEL_FAILED |
ble_evt_gap_set_sec_level_failed_t |
Setting the security level on an established link using |
5.1.3.4. Macros
Table 23 contains the configuration macros related to BLE security.
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
5.1.3.5.2. Peripheral
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.
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.
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 |
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_EVT_GAP_PATH_LOSS_THRES |
ble_evt_gap_path_loss_thres_t |
Reports a path loss threshold crossing. Member |
5.1.4.4. Macros
Table 26 contains the configuration macros related to BLE Power Control.
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
.
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:
// 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.6. Logical Link Control and Adaptation Layer Protocol
The Logical Link Control and Adaptation Layer Protocol, referred to as L2CAP provides connection-oriented and connectionless data services to upper layer protocols with protocol multiplexing capability and segmentation and reassembly operation. As referred in [Ref_02], L2CAP permits higher level protocols and applications to transmit and receive upper layer data packets (L2CAP Service Data Units, SDU) up to 64 kilobytes in length. L2CAP also permits per-channel flow control and retransmission.
The L2CAP layer provides logical channels, named L2CAP channels, which are multiplexed over one or more logical links. Each one of the endpoints of an L2CAP channel is referred to by a channel identifier (CID).
L2CAP channels may operate in one of five different modes as selected for each L2CAP channel. The modes are:
Basic L2CAP Mode (equivalent to L2CAP specification in Bluetooth v1.1)
Flow Control Mode
Retransmission Mode
Enhanced Retransmission Mode
Streaming Mode
LE Credit Based Flow Control Mode
The null CID (0x0000
) is never used as destination endpoint. Identifiers
from 0x0001
to 0x003F
are reserved for specific L2CAP functions. These
channels are referred to as Fixed Channels.
CID 0x0004
is used by the ATT, CID 0x0006
is used by the SMP while CID
is used by the signaling channel.
As referred above, the connection-oriented data channels represent a connection between two devices, where a CID, combined with the logical link, identifies each endpoint of the channel.
Figure 37 illustrates the format of the L2CAP PDU in basic mode.
Summarizing:
L2CAP implementations transfer data between upper layer protocols and the lower layer protocol.
L2CAP maps channels to Controller logical links, which in turn run over Controller physical links. All logical links going between a local Controller and remote Controller run over a single physical link.
L2CAP is packet-based but follows a communication model based on channels. A channel represents a data flow between L2CAP entities in remote devices. Channels may be connection-oriented or connectionless. Fixed channels other than the L2CAP connectionless channel (CID
0x0002
) and the two L2CAP signaling channels (CIDs0x0001
and0x0005
) are considered connection-oriented. All channels with dynamically assigned CIDs are connection-oriented.
5.1.6.1. Credit-Based Flow Control
The Credit-Based Flow Control is an L2CAP mode of operation that when used, allows both devices involved in the LE connection to have complete control over how many packets the peer device is allowed to send. This is achieved by the use of credits that represent the absolute maximum number of LE frames that the device is willing to accept at a particular moment. The sending entity may send only as many LE-frames as it has credits. If the credit count reaches zero the transmission must stop. If more frames are sent the connection will be closed.
5.1.6.2. Functions
To establish a LE-Credit Based L2CAP connection, the initiator should send a LE Credit-Based connection request, specifying parameters like the Protocol Service Multiplexer (PSM), Maximum Transmission Unit (MTU), Maximum Payload Size (MPS), and the initial number of credits that the remote peer has to send data. The responding device should respond with a LE Credit-Based Connection Response specifying its own MTU, MPS and initial credits value. PSM is 2-byte odd number that can be used to support multiple implementations of a protocol. The valid range for PSM is between 0x80 - 0xFF
. The fixed SIG assigned PSM values are between 0x00
and 0x7F
. MTU represents the maximum size of data that the service above L2CAP can send to the remote peer (Maximum size of an SDU – Service Data Unit). MPS is the maximum payload size that an L2CAP entity can receive from the lower layer, and it is equivalent to the maximum PDU payload size. Each MPS corresponds to one credit. One SDU (of size MTU) can be fragmented to one or more PDUs (of size MPS). If the SDU length field value exceeds the receiver’s MTU, the receiver shall disconnect the channel. If the payload length of any LE-frame exceeds the receiver’s MPS, the receiver shall disconnect the channel. If the sum of the payload lengths for the LE-frames exceeds the specified SDU length, the receiver shall disconnect the channel. After the LE Credit-Based connection request and response frames are received or exchanged, the two entities agree to use the minimum values of MTU and MPS.
As an example, consider two devices that use the following values during the Credit-Based connection request/response procedure:
Device A Connection Request |
Device B Connection Response |
|
---|---|---|
PSM |
0x80 |
- (Field not available on Connection Response) |
MTU |
100 |
250 |
MPS |
50 |
23 |
Initial Credits |
10 |
20 |
In this scenario, device B can receive PDUs of size at most 23, and SDUs of size at most 250. On the other hand, device A can receive PDUs of size at most 100, and SDUs of size at most 50. Device A can send 20 PDUs to device B, and should stop until device B updates the available credits. This update could be performed any time during the connection. If device A was to transmit an SDU of size 100 bytes (MTU), it would be fragmented to 5 PDUs of data sizes 21, 23, 23, 23 and 10 respectively. This transmission would consume 5 credits (one credit for every PDU that could be received from device B). Note that the usable payload size of the first PDU is 2 bytes less than the value of MPS as the first LE-frame contains a 2-byte field specifying the total length of the SDU. This is true only for the first frame. In the same manner, the transmission of a 23-byte SDU would require two credits and two PDUs of size 21 and 2 bytes respectively whereas the transmission of a 21-byte SDU would require just one PDU. Maximum SDU length (MTU) can be specified using the ble_gap_mtu_size_set()
API call.
API call |
Description |
---|---|
ble_l2cap_connect() |
Create a l2cap connection oriented channel with remote peer. Connection establishment will be signaled using |
ble_l2cap_listen() |
Create a connection oriented channel listening for incoming connections. Incoming connection will be signaled using |
ble_l2cap_listen_defer_setup() |
Create a connection oriented channel listening for incoming connections. Incoming connection will be signaled using |
ble_l2cap_connection_cfm() |
Accept or reject incoming connection. Accepted connection will be signaled using |
ble_l2cap_stop_listen() |
Stop listening for incoming connections. |
ble_l2cap_disconnect() |
Disconnect an established L2CAP channel. |
ble_l2cap_send() |
Send data on channel, response code is signaled using the |
ble_l2cap_add_credits() |
Provide additional credits to remote peer. |
5.1.6.3. Events
Event |
Argument |
Description |
---|---|---|
BLE_EVT_L2CAP_CONNECTED |
ble_evt_l2cap_connected_t |
Channel connected. Members |
BLE_EVT_L2CAP_CONNECTION_FAILED |
ble_evt_l2cap_connection_failed_t |
Channel connection failed. Member |
BLE_EVT_L2CAP_CONNECTION_REQ |
ble_evt_l2cap_connection_req_t |
Request connection. Members refer to connection parameters e.g. |
BLE_EVT_L2CAP_DISCONNECTED |
ble_evt_l2cap_disconnected_t |
Channel disconnected. Member |
BLE_EVT_L2CAP_SENT |
ble_evt_l2cap_sent_t |
Data sent on channel. |
BLE_EVT_L2CAP_REMOTE_CREDITS_CHANGED |
ble_evt_l2cap_credit_changed_t |
Available remote credits changed on channel. |
BLE_EVT_L2CAP_DATA_IND |
ble_evt_l2cap_data_ind_t |
Data received on channel. |
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
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 |
5.1.7.2. Macros
Macro |
Default |
Description |
---|---|---|
dg_configBLE_DATA_LENGTH_RX_MAX |
251 |
Set the maximum Receive Data Channel PDU Payload Length. Unless |
dg_configBLE_DATA_LENGTH_TX_MAX |
251 |
Set the maximum Transmit Data Channel PDU Payload Length. Unless |
5.1.7.3. Events
Event |
Argument |
Description |
---|---|---|
BLE_EVT_GAP_DATA_LENGTH_CHANGED |
ble_evt_gap_data_length_changed_t |
Data Length changed for specified connection. Members |
BLE_EVT_GAP_DATA_LENGTH_SET_FAILED |
ble_evt_gap_data_length_set_failed_t |
Data Length Set operation failed. Member |
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
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_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 |
5.1.8.2. Events
Event |
Argument |
Description |
---|---|---|
BLE_EVT_GAP_PHY_CHANGED |
ble_evt_gap_phy_changed_t |
PHY configuration changed for the specified connection. Members |
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 |
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
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:
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.
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.
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.
// 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.
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 roleHID 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.
File name |
Description |
---|---|
ble_service.h |
BLE service framework API:
|
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.
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.
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.
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:
|
ble_gatt.h |
Common definitions for GATT API |
ble_gattc.h |
GATT client API:
|
ble_gattc_util.h |
GATT client utilities |
ble_gatts.h |
GATT server API:
|
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.
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:
|
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.
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
Build the pxp_reporter application using the
DA1469x-00-Release_QSPI_SUOTA
configuration.Erase the Flash memory of DA1469x using the
erase_qspi_jtag
script.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:
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
Build the pxp_reporter application using the
DA1469x-00-Release_QSPI_SUOTA
configuration. A new image namedpxp_reporter.img
is also created underDA1469x-00-Release_QSPI_SUOTA
folder.
5.2.4.3. Perform the SUOTA using the mobile application
Download the Dialog SUOTA application from Google PlayStore or Apple App Store.
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.Launch the Dialog SUOTA application on the Android phone and select the DA1469x device you want to update.
Select Update device.
Select the appropriate image file – this is a list of the files in the SUOTA directory.
Touch Send to device on the touchscreen
Wait until the process is completed. When the image is uploaded, a dialog box pops up asking for a device reboot. Select OK.
Press Close to return to the main menu.
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.
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:
Build the project ble_suota_client by executing
DA1469x-00-Release_QSPI
Erase the flash memory of the device by executing the
erase_qspi_jtag
script.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.
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:
Connect to the serial port and press the
K2
RESET button.
In the serial terminal of the
ble_suota_client
device, give the following command:> scan start
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:
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
After the image transfer has been completed, the remote device disconnects and reboot as shown in Figure 56.
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
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.
Checks if image header contains the security section (
0xAA22
).Checks if image header contains the administration section (
0xAA44
)Checks if is able to extract public keys, encryption keys, signature verification value and NONCE.
Checks if the public key used for the signature verification is not revoked.
Checks if application encryption key is not revoked.
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:
Flash QSPI controller configuration retrieving information from the
Product header
.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.
Interrupt Vector Table (IVT) is copied to RAM
Cache controller is configured to point to the beginning of IVT
QSPI FLASH address is remapped to address 0x0
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.
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.
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:
Flash Programmed identifier
“Pp” Product header identifierActive FW Image Address
address in Flash of the active FW Image headerUpdate FW Image Address
address in Flash of the update FW Image headerFlash BURSTCMDA reg value
value written in the QSPIC_BURSTCMDA_REG of the QSPI controllerFlash BURSTCMDB reg value
value written in the QSPIC_BURSTCMDB_REG of the QSPI controller0xAA11
Flash config section identifierLength of Flash Config Section
size of Flash config sectionFlash 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:
Image identifier
(“Qq”) Image header identifiersize
the FW image sizeCRC
CRC value for image integrity checkTimestamp
number of seconds passed since epoch (1/1/1970)IVT
offset from the beginning of theImage header
to the start of the IVT.
Security section fields:
0xAA22
security section identifierLength of security section
size of security sectionIndex to ECC key
signature key indexIndex to Sym. key
FW decryption keyNONCE
used in FW decryption algorithmLength of security section
size in bytes of the signature valueSignature
signature value used for signature verification
Administration sections fields:
0xAA44
administration section identifierLength of administration section
size of administration section0xAA55
key revocation section identifierLength of the key revocation record
size of key revocation recordKey Type
type of the key(eg ECC, Sym., User data key)Key Index
key indexnext 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.
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:
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):
Reset_Handler
insdk\bsp\startup\startup_da1469x.S
starts execution.SystemInitPre()
insdk\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.
Reset_Handler
insdk\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.
SystemInit()
insdk\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.
__START
is called, which initializes some libc structures and then callsmain()
.
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.
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
is0x1600
.FLASH_REGION_OFFSET
is0x900
. Since this field is in 32-bit words, the actual offset in bytes is0x2400
.FLASH_REGION_SIZE
is0x6
. 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:
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:
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:
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 |
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):
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 |
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 ( |
device_type |
The Flash JEDEC device type ( |
device_density |
The Flash JEDEC device density ( |
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 |
read_erase_progress_opcode |
The opcode to use to check if erase is in progress (Usually the Read Status Reg opcode |
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 ( |
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” ( |
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 |
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:
Copy and rename the template header file, or an existing driver file.
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.
Define the proper
JEDEC ID
values for the Manufacturer code, the device type and the device density.Verify that the
suspend
,resume
,exit power-down
,enter power-down
,fast read
,write enable
,read
commands are valid for the new device type.Guard the header file using an
#if
preprocessor macro that checks for the specific driver selection.
#if (dg_configFLASH_MANUFACTURER_ID == WINBOND_ID && dg_configFLASH_DEVICE_TYPE == W25Q32FW && dg_configFLASH_DENSITY == W25Q_32Mb_SIZE)
Define any other driver-specific macros that are needed (like timings etc).
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 theuCode
format.Declare the constant struct instance of type
qspi_flash_config_t
, namedflash_<device name>_config
, and initialize it with proper values. Please note that this must be declared asconst
.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.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.The function
is_suspended()
should read the flash Status Register and return true if Erase or Write is suspended on the device.If Continuous Read Mode (sometimes referred to as Performance or Burst Mode) is used, make sure to set
send_once
to 1, and setextra_byte
to a proper value for the flash to keep working in this mode. This is device-specific.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) andaddress_size
.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) orDA1469x_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 theProduct Header
FLASH section) are loaded from theutilities\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 toutilities\python_scripts\qspi\program_qspi_config.py
as follows:python program_qspi_config.py --prod_id DA1469x-00 -fc custom_flash_configurations.xml
After executing
program_qspi_config.py
, a newprogram_qspi.xml
file will be generated inside theutilities\python_scripts\qspi
path. Since this is the file used whenprogram_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:
Verify that the application boots by using SmartSnippets™ Studio Power Profiler and a cell phone to connect to the device.
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.
Repeat steps 1 and 2 by changing the application clock to 96 MHz (change
sysclk_XTAL32M
inmain.c
tosysclk_PLL96
)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.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.
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.
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
).
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 uCodeSensor 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.
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 |
---|---|
|
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 |
---|---|
|
Register a uCode-Block to a PDC event. |
|
Unregister a uCode-Block from a PDC event. Note: The uCode ID to be passed as argument is returned when calling |
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:
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 inhw_pdc.h
).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.
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.
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.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).
An example code demonstrating the registration of three SNC uCodes with different configuration parameters is presented in Code 36.
// @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:
A uCode registration request is sent to the Sensor Node Adapter task with the uCode context and the required configuration.
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.
The SNC queues for the application communicating with the particular uCode are created, based on the given configuration.
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:
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.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.
Memory allocation and creation of the uCode.
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:
The SNC-main-uCode is halted.
The uCode is removed from the uCode list.
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.
Any pending CM33-to-SNC notifications for the particular uCode are cleared.
The SNC-main-uCode is resumed again.
The destructor function of the uCode is called, so that its allocated resources can be freed.
The SNC queues (if any) of the uCode are also deleted.
// @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 |
---|---|
|
Enable uCode execution. |
|
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 |
---|---|
|
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. |
An illustration of the above processes comprising the SNC/CM33 notification exchange mechanism is presented in Figure 67.
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 |
---|---|
|
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.
// @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 |
---|---|
|
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.
// @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 |
---|---|
|
Check if a uCode-Block’s SNC-to-CM33 queue or CM33-to-SNC queue is empty. |
|
Check if a uCode-Block’s SNC-to-CM33 queue or CM33-to-SNC queue is full. |
|
Return the number of free (not written/allocated) chunks in a uCode-Block’s SNC-to-CM33 queue or CM33-to-SNC queue. |
|
Return the number of allocated (written) chunks in a uCode-Block’s SNC-to-CM33 queue or CM33-to-SNC queue. |
|
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).
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.
// @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.
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);
}}
}
}
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.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()
andSNC_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, namelySNC_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.
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.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)); } }
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.
#include "sys_usb.h"
.
.
sys_usb_init();
#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.
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.
#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
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.
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 applicationsys_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.
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.
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 eventUBE_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 eventUBE_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 eventUBE_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.
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).
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.
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 |
---|---|
|
Configure the Haptic Driver for driving the specific actuator in use and initialize the Haptic LLD’s internal data |
|
Get/set the drive level |
|
Get/set the polarity |
|
Get/set the half period |
|
Get/set the Haptic Driver state (Idle/Active) |
|
Deactivate the Haptic Driver completely |
|
Convert frequency to half period |
|
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 |
---|---|
|
Maximum nominal duty cycle of the drive signal that is supported by the actuator |
|
Maximum absolute duty cycle of the drive signal that is supported by the actuator |
|
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) |
|
Callback function to be fired on interrupt |
|
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. Thehw_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).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, theHW_HAPTIC_CONV_VOLT_TO_DUTYCYCLE()
function-like macro can be used. Theduty_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:
Depending on the type of actuator that is used, either
dg_configUSE_HW_ERM
ordg_configUSE_HW_LRA
should be defined to 1, in the appropriatecustom_config_xxx.h
file. (In case of activating both configuration options, then by default the LRA functionality will be enabled.)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 theHW_HAPTIC_CONV_VOLT_TO_DUTYCYCLE()
function-like macro.If
interrupt_cb
is set to point to a specific callback function, this callback function should be implemented appropriately to perform haptic processing.The
hw_haptic_init()
function should be called by passing a pointer to thehaptic_config_t
instance. The Haptic Driver gets initialized and enters Idle state.The
hw_haptic_get/set_drive_level()
,hw_haptic_get/set_polarity()
andhw_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. Thehw_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()
andhw_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.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 |
---|---|
|
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 |
---|---|
|
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 |
---|---|
|
Initialize SmartDrive™ algorithm |
|
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 |
---|---|
|
Initialize the Haptics Algorithm Library |
|
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:
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.An instance of
haptics_lib_params
must first be created and the respective configuration parameters should be set according to the actuator in use.The
haptic_lib_init()
function should be called before putting the Haptic Driver into Active state.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 |
---|---|
|
Initialize the Waveform Memory Decoder |
|
Trigger start/stop of playback of sequence in waveform memory |
|
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:
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.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.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 |
---|---|
|
Open a haptic operating session (Configure the Haptic Driver and the related SW modules) |
|
Apply a new haptic configuration |
|
Set the drive level (directly or indirectly, depending on the current drive mode) |
|
Set the drive polarity |
|
Set the half period |
|
Set the Haptic Driver into Active or Idle state |
|
Set the current drive mode of the haptic operation (i.e. enable/disable Overdrive and Frequency Control parts of the Haptics Algorithm Library). |
|
Play one of the waveform sequences stored in the waveform memory |
|
Stop the currently played waveform sequence |
|
Update haptic drive parameters (to be used as the interrupt Handler’s callback function) |
|
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 |
---|---|
|
Pointer to the the Haptic LLD’s configuration parameter structure ( |
|
Pointer to the Haptics Algorithm Library’s parameter structure ( |
|
Pointer to the the Adapter’s Waveform Memory configuration parameter structure ( |
The Adapter’s waveform memory configuration structure, ad_haptic_wm_conf_t
, consists of the following parameters:
Parameter |
Description |
---|---|
|
Pointer to the array containing all the available haptic patterns in the encoded DA7280 Waveform Memory format |
|
Specifies whether the amplitude values of each snippet will be interpreted as signed or unsigned |
|
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:
Depending on the type of actuator that is used, either
dg_configUSE_HW_ERM
ordg_configUSE_HW_LRA
should be defined to 1, in the appropriatecustom_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.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 theinterrupt_cb
parameter of thehaptic_config_t
instance should point to thead_haptic_update_drive_parameters()
function (or a custom callback function that should call thead_haptic_update_drive_parameters()
itself).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 untilad_haptic_close()
is called.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).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 thead_haptic_set_state()
function) or by playing one of the available waveform patterns using thead_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.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 thead_haptic_play_wm_sequence()
function). If this blocking is not desired, then thead_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].)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).
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.1. Automatic flashing of the application image via SEGGER JLink
As part of the development process, it sometimes may be more convenient to have the application image automatically flashed onto the DA1469x whenever the code is modified and needs to be debugged or tested. This can be achieved by setting up Smart Snippets Studio to automatically build and/or flash the application binary whenever a debugging session is started.
Note
This feature is experimental and is not fully supported. If the
following instructions do not produce the expected results, please
use cli_programmer
to flash images onto the DA1469x.
Note
This option is only provided for software development purposes and must not be used for production purposes.
Note
SEGGER JLink software version 6.20 or later is required.
5.11.2. Toolset preparation
In Smart Snippets Studio import the project
segger_flash_loader
located in the sdk, undersdk/bsp/system/loaders
.Build the project in configuration
DA1469x-00_Release
.Copy the produced file
segger_flash_loader.elf
inside the SEGGER JLink installation folder assegger_flash_loader_da1469x.elf
.Make sure that an appropriate JLink version is installed and selected in Smart Snippets Studio.
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.)
<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
Add the following entry in the
custom_config_qspi.h
file of the project that you want to flash.
#define dg_configUSE_SEGGER_FLASH_LOADER ( 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.