An In-Depth Analysis of the BLE Observer Pattern Callback Mechanism

Osprey Talks Microcontrollers Source: Osprey Talks Microcontrollers

The nRF5 SDK has updated the event callback mechanism starting from version 14, introducing the Observer Pattern to decouple different BLE layers from the callback functions for BLE events.

This mechanism utilizes Flash sections, combining function calls in RAM with operations in Flash, which is a novel idea.

This article attempts to understand and trace the entire callback process and write a piece of code to validate our thoughts.

1. Introduction to the Observer Pattern

In the world of object-oriented programming, there are many well-known design patterns, one of which is the Observer Pattern. It addresses the problem of a one-to-many dependency between objects. When the state of a central object changes, all objects that depend on it are notified and updated automatically.

An In-Depth Analysis of the BLE Observer Pattern Callback Mechanism

In the Observer Pattern, there are several roles: Observer, Subject, and Publisher.

Multiple observers can independently subscribe to a subject. When the subject receives data pushed by the publisher, it notifies each observer to handle the data.

To implement the Observer Pattern, the observer side needs to implement a subscription function, passing its handle and callback function to the subject. The host side should maintain a list of all subscribed observer handles and callback functions. When notification is needed, it traverses the handles in the list and executes each corresponding callback function. The publisher simply exposes an interface to send data to the subject.

Furthermore, in the code, the handle and callback function should be encapsulated into a structure for easy parameter passing. The observer prepares a function to save this structure into a list, called subscription. The subject prepares a function to read this list, traverse to obtain each structure, and execute the callback, called notification.

Designing this list is crucial.

An In-Depth Analysis of the BLE Observer Pattern Callback Mechanism

The simplest way is to prepare a memory array, where the subscription function writes to the array, and the notification function reads from it.

The nRF5 SDK chose another method, using Flash sections.

2. Introduction to Flash Sections

This article uses the SEGGER Embedded Studio development tool, which uses the arm gcc compiler at the backend, requiring its linker file (.ld) and map file (.map).

A Flash section refers to a specified space in Flash, including a starting address and space length, and setting a section name that begins with a dot (.).

Commonly mentioned sections in C language development include the code section .text, constant section .rodata, etc.

An In-Depth Analysis of the BLE Observer Pattern Callback Mechanism

Using the __attribute__ keyword, a variable can be assigned a section name. The following code places the variable my_var in the section .my_section:

static my_type_t my_var __attribute__ ((section(".my_section"))) __attribute__((used)) = 
{
    .handler    = handler,
    .p_context  = NULL
};

It is also necessary to set the starting address and length of this section in the memory layout file. Compiling the SES project will generate a linker file (.ld), which may contain code like the following:

.sdh_ble_observers ALIGN(__pwr_mgmt_data_end__ , 4) : AT(ALIGN(__pwr_mgmt_data_end__ , 4))
{
__sdh_ble_observers_start__ = .;
__start_sdh_ble_observers =   __sdh_ble_observers_start__;
KEEP(*(SORT(.sdh_ble_observers*)))
}
__sdh_ble_observers_end__ = __sdh_ble_observers_start__ + SIZEOF(.sdh_ble_observers);
__sdh_ble_observers_size__ = SIZEOF(.sdh_ble_observers);

Where xx_start__ indicates the starting address, xx_size__ indicates the length, and xx_end__ indicates the ending address.

Note a key line: KEEP(*(SORT(.sdh_ble_observers*))).

This line uses a wildcard, where the asterisk at the end of .sdh_ble_observers* indicates any character, so we might see section names like .sdh_ble_observers1 in the code. SORT indicates that these wildcard-matched sections are sorted in ascending order by name.

Looking at the map file, we can see records like:

.sdh_ble_observers
                0x0000000000030ae4       0x30
                0x0000000000030ae4                __sdh_ble_observers_start__ = .
                0x0000000000030ae4                __start_sdh_ble_observers = __sdh_ble_observers_start__
 *(SORT_BY_NAME(.sdh_ble_observers*))
 .sdh_ble_observers0
                0x0000000000030ae4        0x8 Output/ble_app_blinky_pca10040_s132 Debug/Obj/ble_conn_state.o
 .sdh_ble_observers1
                0x0000000000030aec        0x8 Output/ble_app_blinky_pca10040_s132 Debug/Obj/main.o
 .sdh_ble_observers1
                0x0000000000030af4        0x8 Output/ble_app_blinky_pca10040_s132 Debug/Obj/ble_conn_params.o
 .sdh_ble_observers2
                0x0000000000030afc       0x10 Output/ble_app_blinky_pca10040_s132 Debug/Obj/main.o

In the map file, we see multiple similarly named sections .sdh_ble_observers[0, 1, 2], arranged together, with their Flash addresses contiguous, and their lengths summing up to the length of the .sdh_ble_observers section.

We can consider .sdh_ble_observers* as sub-sections of .sdh_ble_observers.

How to access data within a section? One way is to directly call the variable name, like my_var above, and another way is to use the section name to index into the sub-section contents. The SDK provides a function library for section operations, nrf_section_iter. If a section name is known, the following code can be used to obtain the contents of its sub-sections:

nrf_section_iter_t  iter;
for (nrf_section_iter_init(&iter, &my_section);
        nrf_section_iter_get(&iter) != NULL;
        nrf_section_iter_next(&iter))
{
    my_type_t     * p_section;
    p_section = (my_type_t*)nrf_section_iter_get(&iter);
}

3. BLE Event Callback

Taking the SDK15.1/ble_app_blinky project as an example, we will trace the calling logic of its BLE callback events.

In main.c –> ble_stack_init(), the following is called:

NRF_SDH_BLE_OBSERVER(m_ble_observer, APP_BLE_OBSERVER_PRIO, ble_evt_handler, NULL);

Where ble_evt_handler is the BLE event callback function we set.

NRF_SDH_BLE_OBSERVER is a highly complex nested macro, which after dissection, transforms the code into the following form:

static nrf_sdh_ble_evt_observer_t m_ble_observer __attribute__ ((section(".sdh_ble_observers3"))) __attribute__((used)) =
{
    .handler    =ble_evt_handler,
    .p_context  = NULL
};

This code defines a structure variable in the .sdh_ble_observers3 section and sets the callback function as a parameter.

So where is ble_evt_handler() called? We find it in nrf_sdh_ble.c -> nrf_sdh_ble_evts_poll(), where we see the key code:

nrf_section_iter_t  iter;
for (nrf_section_iter_init(&iter, &sdh_ble_observers);
        nrf_section_iter_get(&iter) != NULL;
        nrf_section_iter_next(&iter))
{
    nrf_sdh_ble_evt_observer_t * p_observer;
    nrf_sdh_ble_evt_handler_t    handler;

    p_observer = (nrf_sdh_ble_evt_observer_t *)nrf_section_iter_get(&iter);
    handler    = p_observer->handler;

    handler(p_ble_evt, p_observer->p_context);
}

This is exactly what we analyzed above, obtaining all sub-section contents by section name and then executing their callback functions.

Still in that file, we further find the key code:

NRF_SDH_STACK_OBSERVER(m_nrf_sdh_ble_evts_poll, NRF_SDH_BLE_STACK_OBSERVER_PRIO) =
{
    .handler   = nrf_sdh_ble_evts_poll,
    .p_context = NULL,
};

Similar to above, this is a nested macro, which after dissection, yields the following code:

static nrf_sdh_stack_observer_t m_nrf_sdh_ble_evts_poll __attribute__ ((section(".sdh_stack_observers2"))) __attribute__((used)) =
{
    .handler    =nrf_sdh_ble_evts_poll,
    .p_context  = NULL
};

Where is nrf_sdh_ble_evts_poll() called? We find it in nrf_sdh.c -> nrf_sdh_evts_poll(), where we see the key code:

for (nrf_section_iter_init(&iter, &sdh_stack_observers);
        nrf_section_iter_get(&iter) != NULL;
        nrf_section_iter_next(&iter))
{
    nrf_sdh_stack_observer_t    * p_observer;
    nrf_sdh_stack_evt_handler_t   handler;

    p_observer = (nrf_sdh_stack_observer_t *) nrf_section_iter_get(&iter);
    handler    = p_observer->handler;

    handler(p_observer->p_context);
}

Furthermore, we see the calling location of this function:

void SD_EVT_IRQHandler(void)
{
    nrf_sdh_evts_poll();
}

SD_EVT_IRQHandler is the interrupt handler for BLE events. Whenever the chip generates a BLE event, it enters this interrupt handler. By following the tracing logic above in reverse, we can reach the initial ble_evt_handler callback function.

Thus, we have clarified the jump logic of the BLE event callback.

4. Some Details

(1) What is SD_EVT_IRQHandler?

It is the BLE event interrupt.

After multiple redefinitions and jumps, we find its original name: SWI2_EGU2_IRQHandler.

In the ses_startup_nrf52.s file, we see it is an interrupt vector:

/* External Interrupts */
  .word   POWER_CLOCK_IRQHandler
  .word   RADIO_IRQHandler
  .word   UARTE0_UART0_IRQHandler
// ....
  .word   COMP_LPCOMP_IRQHandler
  .word   SWI0_EGU0_IRQHandler
  .word   SWI1_EGU1_IRQHandler
  .word   SWI2_EGU2_IRQHandler
  .word   SWI3_EGU3_IRQHandler

Why does it represent the BLE event interrupt?

In the chip manual’s Memory section, we find the Instantiation subsection, which lists all interrupt vector addresses:

An In-Depth Analysis of the BLE Observer Pattern Callback Mechanism

Comparing this list with the above interrupt vector definitions, we find they correspond one-to-one, strictly arranged in order. Therefore, the position of SWI2_EGU2_IRQHandler indicates the interrupt vector for SWI2 and EGU2, regardless of its name.

Note that SWI2 and EGU2 share the same vector address, so they share an interrupt vector, hence the vector name is written as SWI2_EGU2_IRQHandler.

(2) Why index twice?

In the nrf_sdh_evts_poll function, nrf_sdh_ble_evts_poll() is called, and then our ble_evt_handler is called. Why index twice?

Upon careful examination of the code, we find that nrf_sdh_evts_poll handles both BLE and SOC events, while ble_evt_handler only handles BLE events.

This is because SWI2 and EGU2 share an interrupt vector, and they provide not only BLE event interrupts but also SOC-related interrupts, such as clock events.

(3) What is APP_BLE_OBSERVER_PRIO?

It represents the priority.

As mentioned earlier, the .ld file uses SORT to arrange all sub-sections in ascending order. Subsections with smaller priority values are placed first, while those with larger values are placed later. When indexing sub-section contents, higher priority (smaller value) callback functions are always executed first, followed by lower priority (larger value) callback functions. The execution order of callbacks with the same priority cannot be determined.

(4) Observer Role

In the analysis above, NRF_SDH_BLE_OBSERVER signifies the subscription function, and the BLE handling in main.c acts as an observer.

The SDK further encapsulates subscription functions into macro forms like BLE_XXX_DEF(). For example, the subscription function macro for GATT:

NRF_BLE_GATT_DEF(_name)

Many BLE libraries provide subscription function macros, and when using them, one only needs to declare them in main.c.

General BLE subscription function macros:
#define BLE_ADVERTISING_DEF(_name)
#define BLE_DB_DISCOVERY_DEF(_name)
#define BLE_LINK_CTX_MANAGER_DEF()
#define NRF_BLE_SCAN_DEF(_name)
#define NRF_BLE_GATT_DEF(_name)
#define NRF_BLE_QWR_DEF(_name)

BLE Profile subscription function macros:
#define BLE_BAS_DEF(_name)
#define BLE_BPS_DEF(_name)
#define BLE_CSCS_DEF(_name)
#define BLE_GLS_DEF(_name)
#define BLE_HIDS_DEF()
#define BLE_HRS_DEF(_name)
#define BLE_HTS_DEF(_name)
#define BLE_LBS_DEF(_name)
...

If we create a custom profile, we should also provide a subscription function macro like this.

nrf_sdh_ble_evts_poll and nrf_sdh_evts_poll act as notification functions, while nrf_sdh.c and nrf_sdh_ble.c serve as the subject roles.

The publisher is the chip, and the SD_EVT_IRQHandler interrupt is the interface through which the publisher pushes data to the subject.

5. Validation

Let’s try to write a piece of code to validate this section operation observer pattern.

First, define a section: syq_sections

typedef void (*syq_handler_t)(uint8_t const evt_code, void * p_context);


typedef struct
{
    syq_handler_t         handler;      //!< BLE event handler.
    void *                p_context;    //!< A parameter to the event handler.
} const syq_type_t;


NRF_SECTION_SET_DEF(syq_sections, syq_type_t, NRF_SDH_BLE_OBSERVER_PRIO_LEVELS);

Set three different priority section variables:

void syq_handler1(uint8_t const evt_code, void * p_context)
{
    NRF_LOG_INFO("handler1 is triggered");
}


static syq_type_t m_syq_1 __attribute__ ((section(".syq_sections1"))) __attribute__((used)) = 
{
    .handler    = syq_handler1,
    .p_context  = NULL
};


void syq_handler2(uint8_t const evt_code, void * p_context)
{
    NRF_LOG_INFO("handler2 is triggered");
}


static syq_type_t m_syq_2 __attribute__ ((section(".syq_sections2"))) __attribute__((used)) = 
{
    .handler    = syq_handler2,
    .p_context  = NULL
};


void syq_handler3(uint8_t const evt_code, void * p_context)
{
    NRF_LOG_INFO("handler3 is triggered");
}


static syq_type_t m_syq_3 __attribute__ ((section(".syq_sections3"))) __attribute__((used)) = 
{
    .handler    = syq_handler3,
    .p_context  = NULL
};

In the main function, execute the indexing:

nrf_section_iter_t  iter;
for (nrf_section_iter_init(&iter, &syq_sections);
        nrf_section_iter_get(&iter) != NULL;
        nrf_section_iter_next(&iter)) {
    syq_type_t * p_observer;
    syq_handler_t    handler;

    p_observer = (syq_type_t *)nrf_section_iter_get(&iter);
    handler    = p_observer->handler;

    handler(1, p_observer->p_context);
}

This way, we can sequentially execute three different priority callback functions, and the printed results are as follows:

An In-Depth Analysis of the BLE Observer Pattern Callback Mechanism

Using this approach, we have implemented a simple observer pattern.

Leave a Comment