STM32WB Bluetooth® LE – Heart Rate Sensor project migration to Azure RTOS ThreadX OS

1 Overview

This article presents the steps followed to port the BLE_HeartRate from ST Sequencer scheduler to ThreadX Real Time OS.
A simple but complete overview about ThreadX features is given, pointing to the official Microsoft documentation when a deeper knowledge is required.
A focus is made on how ThreadX was integrated inside STM32WB, and after and how the BLE Heart Rate application can be modified to replace Sequencer API with ThreadX API. Note that ThreadX integration on all STM32 family follows a common framework. This approach has been properly highlighted pointing to general STM32 ThreadX internal documentation when required to clarify specific aspects.

2 Azure RTOS ThreadX OS characteristics

Below is a list of ThreadX key features:

  • Picokernel, preemption-threshold, event-chaining unique features
  • Execution profiling and performance metrics
  • Totally available in source code (ANSI C and assembler)
  • Safety certifications (TÜV, MISRA, UL)
  • Integrated with other Azure RTOS components:
    • USBX (USB stack)
    • NETX (IPV4 stack + applications layers up to cloud connection) and NETXDUO (IPV6 stack + application layers up to cloud connection)
    • FILEX (FAT compatible file-system)
    • LEVELX (NAND and NOR flash wear leveling facilities)
    • GUIX (embedded GUI)
    • TRACEX (graphical host tool for real-time system events analysis)

Clearly one of the main advantages of ThreadX is the ability to integrate with other software components coming in the same Microsoft Azure RTOS ecosystem required to build a complete embedded application.

Fully detailed description of ThreadX OS features and benefits can be found at Microsoft AzureRTOS ThreadX documentation page [1].

Fully detailed description of all Azure RTOS components can be found at Microsoft Azure RTOS Ecosystem Overview [2].

2.1 ThreadX code organization

ThreadX folders organization

Main folders descriptions

  • cmake: original build system (not mandatory)
  • common: mcu architecture independent source code
  • common_modules: module feature [3]
  • common_smp: Symmetric Multi Processing feature [4]
  • docs: Just a dependency tree of Azure RTOS components
  • ports: mcu architecture dependent (M3, M4, M33,...)
  • ports_module: architecture dependent module code
  • ports_smp: architecture dependent smp code
  • samples: Microsoft example code (demo_threadx.c)
  • utility:
    • benchmarks: Thread-Metric test suite
    • execution_profile_kit: thread execution time tracker
    • low_power: Low power management files
    • rtos_compatibility_layers: adaptation layers (FreeRTOS™, Posix, OSEK)

Note that this layout represents the structure coming from the Microsoft ThreadX sources directory.
Some components not strictly required could be missing in this STM32WB example as features are simply not used.
For instance the "ports" folder contains only the code linked to Cortex®- M4 architecture and relevant to the selected IDE. And again "ports_module" is missing as ThreadX module feature is not used all.






2.2 ThreadX way of working

A typical ThreadX application could be similar to this one (courtesy of Microsoft):

#include "tx_api.h"
unsigned long my_thread_counter = 0;
TX_THREAD my_thread;
main( )
{
    /* Enter the ThreadX kernel. */
    tx_kernel_enter( );
}
void tx_application_define(void *first_unused_memory)
{
    /* Create my_thread! */
    tx_thread_create(&my_thread, "My Thread",
    my_thread_entry, 0x1234, first_unused_memory, 1024,
    3, 3, TX_NO_TIME_SLICE, TX_AUTO_START);
}
void my_thread_entry(ULONG thread_input)
{
    /* Enter into a forever loop. */
    while(1)
    {
        /* Increment thread counter. */
        my_thread_counter++;
        /* Sleep for 1 tick. */
        tx_thread_sleep(1);
    }
}

Consider that this is just the simplest skeleton to arrange a ThreadX based application. A more structured code organization could be present in this STM32WB example to cover specific needs.

tx_kernel_enter
This API is used to give the control to the OS, tx_application_define is called and, the OS scheduler is in charge to select the first thread (ready) to run.

tx_application_define
Here we have the creation of ThreadX resources (threads, semaphores, mutexes, events, queues, etc.). At least one thread must be created, other threads and resources can be created later. Pay attention to how this function is called. Interrupts are disabled before the call to this function (inside _tx_initialize_low_level) and reenabled just after (inside _tx_thread_schedule). So, do not put any code relying on interrupt management inside tx_application_define.

2.3 ThreadX key files

tx_api.h: C header file containing all public definitions, data structures, and service prototypes usable in the application.
For a list of all available API see the ThreadX API [5]

tx_port.h: C header file containing all development-tool and target-specific data definitions and structures.
For more details see the target integration details [6]

tx_user.h: Several configuration options can be set when building the ThreadX library and the application. These configurations can be used to enable smallest code size, fastest execution, performance evaluation, error checking, quality coding rules and generally speaking extra features. These options can be defined in the application source, on the command line or inside tx_user.h (in this latter case TX_INCLUDE_USER_DEFINE_FILE has to be defined). A "tx_user_sample.h" template is given by Microsoft for reference. In BLE_HeartRate_ThreadX example, this file is located inside Core/Inc folder. In this file we have also all the mapping, between ThreadX low-power macros and STM32WB specific functions to manage low-power features. According to the different development stages, specific options can be activated to enable/disable error or performance checking in ThreadX API, for instance:
- TX_DISABLE_ERROR_CHECKING
- TX_ENABLE_MULTI_ERROR_CHECKING
- TX_ENABLE_STACK_CHECKING
- All ENABLE_PERFORMANCE_INFO options (available for almost all ThreadX object)
Or to enable a MISRA compliancy in ThreadX
- TX_ENABLE_MISRA
For more information see the ThreadX configuration options[7]

tx_initialize_low_level.S: Low-level processor initialization, including setting up interrupt-vectors, setting up a periodic timer interrupt source, saving the system stack pointer for use in ISR processing later and finding the first available RAM memory address for tx_application_define

Description of initialization process (between processor reset and the entry point of the thread scheduling loop) and the ThreadX way of working is fully detailed in the ThreadX way of working [8]. Refer to the following chapter before porting your application to ThreadX.

2.4 Idle Scheduler behavior

Unlike FreeRTOS ThreadX has no a default idle thread to run when no threads are ready to run. We have this simple loop:

__tx_ts_wait:
    CPSID   i                                       // Disable interrupts
    LDR     r1, [r2]                                // Pickup the next thread to execute pointer
    STR     r1, [r0]                                // Store it in the current pointer
    CBNZ    r1, __tx_ts_ready                       // If non-NULL, a new thread is ready!
#ifdef TX_LOW_POWER
    PUSH    {r0-r3}
    BL      tx_low_power_enter                      // Possibly enter low power mode
    POP     {r0-r3}
#endif
#ifdef TX_ENABLE_WFI
    DSB                                             // Ensure no outstanding memory transactions
    WFI                                             // Wait for interrupt
    ISB                                             // Ensure pipeline is flushed
#endif
#ifdef TX_LOW_POWER
    PUSH    {r0-r3}
    BL      tx_low_power_exit                       // Exit low power mode
    POP     {r0-r3}
#endif
    CPSIE   i                                       // Enable interrupts
    B       __tx_ts_wait                            // Loop to continue waiting

ThreadX runs always the PendSV_Handler, which is fixed at the lowest interrupt priority (15). The MCU does not service any other interrupts of the lowest priority while no threads are ready to run.

To solve this limitation, we must avoid assigning the same lowest priority (15) to an interrupt in charge of wake up for the system. Alternatively we can manually create an idle thread that is always ready to run. This idle thread is then in charge to enter a lower-power mode. Take care with this approach the SysTick cannot wake the MCU.

3 Required adaptations to integrate ThreadX into BLE Heart Rate application

We can identify two different types of modifications:

STM32WB platform specific (how Threadx OS uses the underlying hardware resources):

  • microcontroller architecture
  • Development environment (IDE)
  • Memory allocation strategy used by the OS
  • Low power

BLE Heart Rate Application specific (how application uses ThreadX OS API):

  • Usage of ThreadX API according to the specific application logic

3.1 STM32WB platform specific integration

ThreadX is highly integrated to the hardware platform through a direct usage of assembler code so we should pay attention for the integration phase.
For a more generic description on ThreadX porting on STM32 family, see this page: ThreadX on STM32 family

3.1.1 Initialization code Integration

The most important role is played by tx_initialize_low_level.S placed in "Application/User/Core" of each ThreadX project.
In this file we can inform the OS at which clock frequency our microcontroller is running (through SYSTEM_CLOCK variable) and at which frequency a system tick interrupt is raised (through SYSTICK_CYCLES variables). Note that "Systick timer" (Cortex core IP) is used exclusively by the OS and "HW Timer 17" peripheral is used by the ST HAL drivers.

The memory allocation strategy can be static or dynamic.
The dynamic approach can be selected using the USE_DYNAMIC_MEMORY_ALLOCATION macro and properly setting the heap memory.
By default a static approach is used through a static array declared at application level.
Inside this file we have also all the low-level initialization code as required by ThreadX. This file is split into three sections to cover the three supported IDEs according to specific macro:
- __clang__ for Keil® (AC6)
- __IAR_SYSTEMS_ASM__ for IAR
- (__GNUC__) && !defined(__clang__) for CubeIDE

3.1.2 Scheduler integration

ThreadX scheduler is a pure assembler code, and so is strictly dependent on MCU architecture (M3/M4/M33/...) and compiler (IAR/Keil/GCC/...) Therefore, scheduler integration refers to a correct selection of assembly files according to architecture/toolchain. These files can be found in the "Middlewares/AzureRTOS/threadx/ports/<mcu_arc>/<compiler>" directory of each ThreadX project. In the STM32WB specific case <mcu_arch> is cortex_m4 and <compiler> is "iar" for IAR, "gnu" for CubeIDE or "keil" for Keil.

3.1.3 Low power integration

The low-power mechanism is implemented through a mix of assembler code already available in the scheduler and C helper functions (tx_low_power.c and tx_low_power.h files). Theses are already defined by ThreadX in the utility sections and do not need to be modified.
In our example these files can be found in "Middlewares/AzureRTOS/threadx/utility/low_power".
In ThreadX we have no idle task but an assembler loop (__tx_ts_wait) defined in the scheduler that can invoke low power routines if no task is ready to run. The low-power framework is fully customizable using specific macro and hook functions:

Threadx LP Macro Usage Implementation used in BLE_HeartRate_ThreadX
TX_LOW_POWER To enable low power support DEFINED
TX_ENABLE_WFI To enable WFI inside ThreadX scheduler NOT DEFINED
TX_LOW_POWER_TICKLESS To enable tickless mode in LP NOT DEFINED
TX_LOW_POWER_USER_ENTER Function to enter in LP UTIL_LPM_EnterLowPower
TX_LOW_POWER_USER_EXIT Function to exit from LP NOT DEFINED
TX_LOW_POWER_TIMER_SETUP Function to start a timer before entering in LP APP_BLE_ThreadX_Low_Power_Setup
TX_LOW_POWER_USER_TIMER_ADJUST Function to calculate time spent in LP APP_BLE_Threadx_Low_Power_Adjust_Ticks

3.2 BLE Heart Rate application specific integration

Inside "Application/User/Core" of your project these files should be present
- stm32wbxx_hal_timebase_tim.c, as SysTick is to be used by the Threadx and not anymore by STM32 HAL. By design TIM17 was chosen for HAL timebase.
- tx_initialize_low_level.S, for low-level initialization
- tx_user.h, for ThreadX OS configuration

ThreadX component must be present in "Middlewares/AzureRTOS" section:

Connectivity ThreadXComponents.png


3.2.1 ST Sequencer VS ThreadX

The sequencer executes registered functions one by one. It is able to:

  • Support up to 32 functions.
  • Request a function to be executed.
  • Pause/Resume the execution of a function.
  • Wait for a specific event (might be not blocking).
  • Priority on functions.

The sequencer is an optimized packaging of a while loop bare metal classic implementation. It does not intend to compete versus standard OSs but versus standard bare metal implementations. It allows avoiding race conditions, which are most of the time faced in bare metal implementation. This is especially when low-power mode is implemented.
For a detailed description of ST Sequencer, refer to:
Building wireless applications with STM32WB Series microcontrollers (AN5289)[9]

3.2.2 List of Sequencer API used in BLE Heart Rate application

void UTIL_SEQ_Run (UTIL_SEQ_bm_t mask_bm)

Requests the sequencer to execute functions that are pending and enabled in the mask mask_bm. Internal (not exposed API) function UTIL_SEQ_Idle is called when no activity is scheduled for execution (typically used to enter in low power)

void UTIL_SEQ_RegTask(UTIL_SEQ_bm_t task_id_bm, uint32_t flags, void (*task)( void ))

Registers a function (task) associated with a signal (task_id_bm) in the sequencer. The task_id_bm must have a single bit set.

void UTIL_SEQ_SetTask( UTIL_SEQ_bm_t task_id_bm,* UTIL_SEQ_PauseTask)

Requests the function associated with the task_id_bm to be executed. The task_prio is evaluated by the sequencer only when a function has finished. If several functions are pending at any one time, the one with the highest priority (0) is executed.

void UTIL_SEQ_PauseTask(UTIL_SEQ_bm_t task_id_bm )

Disables the sequencer to execute the function associated with task_id_bm.

void UTIL_SEQ_ResumeTask(UTIL_SEQ_bm_t task_id_bm )

Enables the sequencer to execute the function associated with task_id_bm.

void UTIL_SEQ_SetEvt(UTIL_SEQ_bm_t evt_id_bm )

Notifies the sequencer that the event evt_id_bm occurred (the event must have been first requested).

void UTIL_SEQ_WaitEvt(UTIL_SEQ_bm_t evt_id_bm )

Requests the sequencer to wait for a specific event evt_id_bm and does not return until the event is set with UTIL_SEQ_SetEvt().

3.2.3 ST Scheduler ThreadX API replacement strategy

In this application we have mapped the Sequencer API with ThreadX API according this table:

Sequencer ThreadX Description
UTIL_SEQ_RegTask tx_thread_create thread creation
UTIL_SEQ_SetTask tx_semaphore_put unlocking a thread waiting for a semaphore
UTIL_SEQ_SetEvt tx_semaphore_put unlocking a thread waiting for a semaphore
UTIL_SEQ_WaitEvt tx_semaphore_get locking a thread waiting for a semaphore
UTIL_SEQ_PauseTask tx_mutex_get locking a thread waiting for a mutex
UTIL_SEQ_ResumeTask tx_mutex_put unlocking a thread waiting for a mutex
UTIL_SEQ_Run tx_kernel_enter Launching the scheduler
UTIL_SEQ_Idle (Internal) __tx_ts_wait (Internal) Internal function automatically called by the scheduler when no task ready to be scheduled

3.2.4 ThreadX resources declaration

tx_application_define is the first function called by tx_kernel_enter.
ThreadX scheduler needs at least one thread that must be declared and ready to be executed. In this application, tx_application_define (app_entry.c file) declares the thread for managing the system host communication interface.
three other threads are declared to manage standard hci commands, advertising policy update and the heart rate measurement. Inside tx_application_define we find also the byte pool creation realized through a static allocation (a_memory_area array).
Note that MX_ThreadX_Init (main.c file) is just a wrapper for tx_kernel_enter.

3.2.5 Principal modifications description

The principal modifications and rationale are listed below.

3.2.5.1 Core\Src\main.c

A wrapper (MX_ThreadX_Init) to launch ThreadX scheduler has been used:

  ...
  /* Init code for STM32_WPAN */
  /* Containing all application initialization could be run before kernel launching */
  MX_APPE_Init();
  /* Launching ThreadX kernel */
  MX_ThreadX_Init();
  /* Infinite loop */
  while(1)
  {
  }
  ...

Timer17 has to be used for HAL timebase:

 
/**
  * @brief  Period elapsed callback in non blocking mode
  * @note   This function is called  when TIM17 interrupt took place, inside
  * HAL_TIM_IRQHandler(). It makes a direct call to HAL_IncTick() to increment
  * a global variable "uwTick" used as application time base.
  * @param  htim : TIM handle
  * @retval None
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */

  /* USER CODE END Callback 0 */
  if (htim->Instance == TIM17) {
    HAL_IncTick();
  }
  /* USER CODE BEGIN Callback 1 */

  /* USER CODE END Callback 1 */
}
3.2.5.2 Core\Src\app_entry.c

ThreadX API should be used instead of Sequencer API:

...
#include "tx_api.h"
...

ThreadX resources, memory declaration and system hci process function prototype:

...
/* USER CODE BEGIN PV */
CHAR* p_pointer = TX_NULL;
static TX_MUTEX     mutex_shci;
static TX_SEMAPHORE semaphore_shci;
static TX_SEMAPHORE semaphore_shci_tl_notify_async_evt;
static TX_THREAD    thread_ShciUserEvtProcess;
static TX_BYTE_POOL byte_pool_ble;
static UCHAR a_memory_area[DEMO_BYTE_POOL_SIZE];
/* USER CODE END PV */

/* Private functions prototypes-----------------------------------------------*/
static void thread_ShciUserEvtProcess_entry(ULONG thread_input);
...

A simple wrapper to start the scheduler and tx_application_define declaring the thread in charge of system hci commands management:

...
/**
  * @brief  MX_ThreadX_Init
  * @param  None
  * @retval None
  */
void MX_ThreadX_Init(void)
{
  /* USER CODE BEGIN  Before_Kernel_Start */

  /* USER CODE END  Before_Kernel_Start */

  tx_kernel_enter();

  /* USER CODE BEGIN  Kernel_Start_Error */

  /* USER CODE END  Kernel_Start_Error */
}
void tx_application_define(void* first_unused_memory)
{
  UNUSED(first_unused_memory);

  /* Here we should declare all the initial ThreadX resources
   * We should have at least one thread to be launched from the scheduler */

  /* Create a byte memory pool from which to allocate the thread stacks, 
   * using static memory coming from a_memory_area array  */
  tx_byte_pool_create(&byte_pool_ble, "byte pool 0", a_memory_area, DEMO_BYTE_POOL_SIZE);

  tx_mutex_create(&mutex_shci, "mutex_shci", TX_NO_INHERIT);
  tx_semaphore_create(&semaphore_shci, "semaphore_shci", 0);
  tx_semaphore_create(&semaphore_shci_tl_notify_async_evt,
                      "semaphore_shci_tl_notify_async_evt",
                      0);

  tx_byte_allocate(&byte_pool_ble, (VOID**) &p_pointer, DEMO_STACK_SIZE_LARGE, TX_NO_WAIT);
  tx_thread_create(&thread_ShciUserEvtProcess,
                   "thread_ShciUserEvtProcess",
                   thread_ShciUserEvtProcess_entry,
                   0,
                   p_pointer,
                   DEMO_STACK_SIZE_LARGE,
                   16,
                   16,
                   TX_NO_TIME_SLICE,
                   TX_AUTO_START);
}
...

Sytem hci commands have to be processed one by one. A mutex is used to mage this pacing mechanism:

...
static void APPE_SysStatusNot( SHCI_TL_CmdStatus_t status )
{
    switch (status) {
        case SHCI_TL_CmdBusy:
            tx_mutex_get(&mutex_shci, TX_WAIT_FOREVER);
            break;

        case SHCI_TL_CmdAvailable:
            tx_mutex_put(&mutex_shci);
            break;

        default:
            break;
    }
    return;
}
...

When a new shci event is notified we can call shci_user_evt_proc. Same semaphore sync mechanism is used for shci commands:

 
...
static void thread_ShciUserEvtProcess_entry(ULONG thread_input)
{
  UNUSED(thread_input);

  while (1) {
        tx_semaphore_get(&semaphore_shci_tl_notify_async_evt, TX_WAIT_FOREVER);
        shci_user_evt_proc();
    }
}

void shci_notify_asynch_evt(void* pdata)
{
  UNUSED(pdata);
  tx_semaphore_put(&semaphore_shci_tl_notify_async_evt);
	return;
}

void shci_cmd_resp_release(uint32_t flag)
{
  UNUSED(flag);
  tx_semaphore_put(&semaphore_shci);
	return;
}

void shci_cmd_resp_wait(uint32_t timeout)
{
  UNUSED(timeout);
  tx_semaphore_get(&semaphore_shci, TX_WAIT_FOREVER);
	return;
}
...
3.2.5.3 STM32_WPAN\App\app_ble.c

ThreadX API and limits.h (used for low power):

...
#include "tx_api.h"
...
#include "limits.h"
...

Low power timer structure used by ThreadX:

...
#if (CFG_LPM_SUPPORTED == 1)
typedef struct
{
  uint32_t LpTXTimeLeftOnEntry;
  uint8_t LpTXTimerThreadx_Id;
} LpTXTimerContext_t;
#endif
...

Low-power timer definiton

...
#if (CFG_LPM_SUPPORTED == 1)
static LpTXTimerContext_t LpTXTimerContext;
#endif
...

ThreadX resources definition for hci management
Prototype of functions used in the threads in charge of advertisement process and hci process:

...
static TX_MUTEX     mtx_hci;
static TX_SEMAPHORE sem_hci;
static TX_THREAD    thread_HciUserEvtProcess;
static TX_SEMAPHORE sem_HciUserEvtProcessSignal;
static TX_THREAD    thread_AdvUpdateProcess;
static TX_SEMAPHORE sem_AdvUpdateProcessSignal;


/* Private function prototypes -----------------------------------------------*/
static void thread_AdvUpdateProcess_entry(ULONG thread_input);
static void thread_HciUserEvtProcess_entry(ULONG thread_input);
...

Prototype of low-power timer callback:

/* USER CODE BEGIN PFP */
#if (CFG_LPM_SUPPORTED == 1)
static void APP_BLE_Threadx_LpTimerCb(void);
#endif
/* USER CODE END PFP */
...

APP_BLE_Init requires the pointer to byte pool memory for threads creation:

...
/* Functions Definition ------------------------------------------------------*/
void APP_BLE_Init(TX_BYTE_POOL* p_byte_pool)
{
  SHCI_CmdStatus_t status;
  /* USER CODE BEGIN APP_BLE_Init_1 */
...

ThreadX resources definition and thread creation for hci event processing

 ...
  CHAR* p_pointer;
  tx_mutex_create(&mtx_hci, "mtx_hci", TX_NO_INHERIT);
  tx_semaphore_create(&sem_hci, "sem_hci", 0);
  tx_semaphore_create(&sem_HciUserEvtProcessSignal, "sem_HciUserEvtProcessSignal", 0);
  tx_byte_allocate(p_byte_pool, (VOID**) &p_pointer, DEMO_STACK_SIZE_LARGE, TX_NO_WAIT);
  tx_thread_create(&thread_HciUserEvtProcess,
                   "thread_HciUserEvtProcess",
                   thread_HciUserEvtProcess_entry,
                   0,
                   p_pointer,
                   DEMO_STACK_SIZE_LARGE,
                   16,
                   16,
                   TX_NO_TIME_SLICE,
                   TX_AUTO_START);
...

ThreadX resources definition and thread creation for advertisement management:

  ...
  tx_semaphore_create(&sem_AdvUpdateProcessSignal, "sem_AdvUpdateProcessSignal", 0);
  tx_byte_allocate(p_byte_pool, (VOID**) &p_pointer, DEMO_STACK_SIZE_REDUCED, TX_NO_WAIT);
  tx_thread_create(&thread_AdvUpdateProcess,
                   "thread_AdvUpdateProcess",
                   thread_AdvUpdateProcess_entry,
                   0,
                   p_pointer,
                   DEMO_STACK_SIZE_REDUCED,
                   16,
                   16,
                   TX_NO_TIME_SLICE,
                   TX_AUTO_START);
 ...

HRSAPP_Init requires the pointer to byte pool memory for app specific thread creation.
A timer for ThreadX low power management is created.

 ...
  /**
   * Initialize HRS Application
   */
  HRSAPP_Init(p_byte_pool);

  /* USER CODE BEGIN APP_BLE_Init_3 */

  /* USER CODE END APP_BLE_Init_3 */

  /**
   * Create timer to handle the connection state machine
   */
  HW_TS_Create(CFG_TIM_PROC_ID_ISR, &(BleApplicationContext.Advertising_mgr_timer_Id), hw_ts_SingleShot, Adv_Mgr);

#if (CFG_LPM_SUPPORTED == 1)
  /**
   * Create low power timer to handle the user ThreadX timers
   */
  HW_TS_Create(CFG_TIM_PROC_ID_ISR, &(LpTXTimerContext.LpTXTimerThreadx_Id), hw_ts_SingleShot, APP_BLE_Threadx_LpTimerCb);
#endif
...

Functions to calculate how much time was spent in low power and to adjust the SysTick have been added:

...
#if (CFG_LPM_SUPPORTED == 1)

static void APP_BLE_Threadx_LpTimerCb( void )
{
  /**
  * Nothing to be done
  */
  return;
}

/**
 * @brief  Request to start a low power timer
 *
 * @param  tx_low_power_next_expiration: Number of ThreadX ticks
 * @retval None
 */
void APP_BLE_ThreadX_Low_Power_Setup(ULONG tx_low_power_next_expiration)
{
  uint64_t time;
  /* Timer was already created, here we need to start it */
  /* By default, see  tx_initialize_low_level.S, each tick is 10 ms */
  /* This function should be very similar to LpTimerStart used in freertos_port.c */
  /* Converts the number of FreeRTOS ticks into hw timer tick */
  if (tx_low_power_next_expiration > (ULLONG_MAX / 1e12)) /* Prevent overflow in else statement */
  {
    time = 0xFFFF0000; /* Maximum value equal to 24 days */
  }
  else
  {
    /* The result always fits in uint32_t and is always less than 0xFFFF0000 */
    time = tx_low_power_next_expiration * 1000000000000ULL;
    time = (uint64_t)( time /  ( CFG_TS_TICK_VAL_PS * TX_TIMER_TICK_PER_SECOND ));
  }

  HW_TS_Start(LpTXTimerContext.LpTXTimerThreadx_Id, (uint32_t)time);

  /**
   * There might be other timers already running in the timer server that may elapse
   * before this one.
   * Store how long before the next event so that on wakeup, it will be possible to calculate
   * how long the tick has been suppressed
   */
  LpTXTimerContext.LpTXTimeLeftOnEntry = HW_TS_RTC_ReadLeftTicksToCount( );

  return;
}
/**
 * @brief  Read how long the tick has been suppressed
 *
 * @param  None
 * @retval The number of tick rate (FreeRTOS tick)
 */
unsigned long APP_BLE_Threadx_Low_Power_Adjust_Ticks(void)
{
  uint64_t val_ticks, time_ps;
  uint32_t LpTimeLeftOnExit;

  LpTimeLeftOnExit = HW_TS_RTC_ReadLeftTicksToCount();
  /* This cannot overflow. Max result is ~ 1.6e13 */
  time_ps = (uint64_t)((CFG_TS_TICK_VAL_PS) * (uint64_t)(LpTXTimerContext.LpTXTimeLeftOnEntry - LpTimeLeftOnExit));

  /* time_ps can be less than 1 RTOS tick in following situations
   * a) MCU didn't go to STOP2 due to wake-up unrelated to Timer Server or woke up from STOP2 very shortly after.
   *    Advancing RTOS clock by 1 ThreadX tick doesn't hurt in this case.
   * b) APP_BLE_ThreadX_Low_Power_Setup(tx_low_power_next_expiration) was called with xExpectedIdleTime = 2 which is minimum value defined by configEXPECTED_IDLE_TIME_BEFORE_SLEEP.
   *    The xExpectedIdleTime is decremented by one RTOS tick to wake-up in advance.
   *    Ex: RTOS tick is 1ms, the timer Server wakes the MCU in ~977 us. RTOS clock should be advanced by 1 ms.
   * */
  if(time_ps <= (1e12 / TX_TIMER_TICK_PER_SECOND)) /* time_ps < RTOS tick */
  {
    val_ticks = 1;
  }
  else
  {
    /* Convert pS time into OS ticks */
    val_ticks = time_ps * TX_TIMER_TICK_PER_SECOND; /* This cannot overflow. Max result is ~ 1.6e16 */
    val_ticks = (uint64_t)(val_ticks / (1e12)); /* The result always fits in uint32_t */
  }

  /**
   * The system may have been out from another reason than the timer
   * Stop the timer after the elapsed time is calculated other wise, HW_TS_RTC_ReadLeftTicksToCount()
   * may return 0xFFFF ( TIMER LIST EMPTY )
   * It does not hurt stopping a timer that exists but is not running.
   */
  HW_TS_Stop(LpTXTimerContext.LpTXTimerThreadx_Id);

  return (unsigned long)val_ticks;
}
#endif
...

Adv_Mgr unlock the semaphore in charge of launch the thread to modify the advertisement policy.
hci events/commands/responses management through semaphore/mutex synchronization:

...
static void Adv_Mgr(void)
{
  /**
   * The code shall be executed in the background as an aci command may be sent
   * The background is the only place where the application can make sure a new aci command
   * is not sent if there is a pending one
   */
   tx_semaphore_put(&sem_AdvUpdateProcessSignal);

  return;
}

static void thread_AdvUpdateProcess_entry(ULONG argument)
{
  UNUSED(argument);

  for (;;){
            tx_semaphore_get(&sem_AdvUpdateProcessSignal, TX_WAIT_FOREVER);
            Adv_Update();
          }
}

static void Adv_Update(void)
{
  Adv_Request(APP_BLE_LP_ADV);

  return;
}

static void thread_HciUserEvtProcess_entry(ULONG argument)
{
  UNUSED(argument);

  for (;;) {
             tx_semaphore_get(&sem_HciUserEvtProcessSignal, TX_WAIT_FOREVER);
             hci_user_evt_proc();
           }
}

/* USER CODE BEGIN FD_SPECIFIC_FUNCTIONS */

/* USER CODE END FD_SPECIFIC_FUNCTIONS */
/*************************************************************
 *
 * WRAP FUNCTIONS
 *
 *************************************************************/
void hci_notify_asynch_evt(void* pdata)
{
  UNUSED(pdata);
  tx_semaphore_put(&sem_HciUserEvtProcessSignal);
  
  return;
}

void hci_cmd_resp_release(uint32_t flag)
{
  UNUSED(flag);
  tx_semaphore_put(&sem_hci);
  
  return;
}

void hci_cmd_resp_wait(uint32_t timeout)
{
  UNUSED(timeout);
  tx_semaphore_get(&sem_hci, TX_WAIT_FOREVER);
  
  return;
}

static void BLE_UserEvtRx(void *p_Payload)
{
  SVCCTL_UserEvtFlowStatus_t svctl_return_status;
  tHCI_UserEvtRxParam *p_param;

  p_param = (tHCI_UserEvtRxParam *)p_Payload;

  svctl_return_status = SVCCTL_UserEvtRx((void *)&(p_param->pckt->evtserial));
  if (svctl_return_status != SVCCTL_UserEvtFlowDisable)
  {
    p_param->status = HCI_TL_UserEventFlow_Enable;
  }
  else
  {
    p_param->status = HCI_TL_UserEventFlow_Disable;
  }

  return;
}

static void BLE_StatusNot(HCI_TL_CmdStatus_t Status)
{
  switch (Status)
  {
    case HCI_TL_CmdBusy:
      /**
       * All tasks that may send an aci/hci commands shall be listed here
       * This is to prevent a new command is sent while one is already pending
       */
      tx_mutex_get(&mtx_hci, TX_WAIT_FOREVER);
      /* USER CODE BEGIN HCI_TL_CmdBusy */

      /* USER CODE END HCI_TL_CmdBusy */
      break;

    case HCI_TL_CmdAvailable:
      /**
       * All tasks that may send an aci/hci commands shall be listed here
       * This is to prevent a new command is sent while one is already pending
       */
      tx_mutex_put(&mtx_hci);
      /* USER CODE BEGIN HCI_TL_CmdAvailable */

      /* USER CODE END HCI_TL_CmdAvailable */
      break;

    default:
      /* USER CODE BEGIN default */

      /* USER CODE END default */
      break;
  }

  return;
}
...
3.2.5.4 STM32_WPAN\App\hrs_app.c
...
#include "tx_api.h"
...
...
static TX_THREAD thread_HrsProcess;
static TX_SEMAPHORE sem_HrsProcessSignal;

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private functions prototypes-----------------------------------------------*/
static void HrMeas(void);
static void thread_HrsProcess_entry(ULONG argument);
static void HRSAPP_Measurement(void);
...
...
void HRSAPP_Init(TX_BYTE_POOL* p_byte_pool)
{
  CHAR* p_pointer;
  tx_semaphore_create(&sem_HrsProcessSignal, "sem_HrsProcessSignal", 0);
  tx_byte_allocate(p_byte_pool, (VOID**) &p_pointer, DEMO_STACK_SIZE_REDUCED, TX_NO_WAIT);
  tx_thread_create(&thread_HrsProcess,
                   "thread_HrsProcess",
                   thread_HrsProcess_entry,
                   0,
                   p_pointer,
                   DEMO_STACK_SIZE_REDUCED,
                   16,
                   16,
                   TX_NO_TIME_SLICE,
                   TX_AUTO_START);
/* USER CODE BEGIN HRSAPP_Init */
...
...
static void thread_HrsProcess_entry(ULONG argument)
{
  UNUSED(argument);

  for(;;)
  {
    tx_semaphore_get(&sem_HrsProcessSignal, TX_WAIT_FOREVER);
    HRSAPP_Measurement( );
  }
}
...
...
static void HrMeas( void )
{
  /**
   * Notifying a new measure is available
   */
  tx_semaphore_put(&sem_HrsProcessSignal);
/* USER CODE BEGIN HrMeas */

/* USER CODE END HrMeas */

  return;
}
...

4 References