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

Revision as of 05:18, 6 July 2022 by Registered User

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):

 1 #include "tx_api.h"
 2 unsigned long my_thread_counter = 0;
 3 TX_THREAD my_thread;
 4 main( )
 5 {
 6     /* Enter the ThreadX kernel. */
 7     tx_kernel_enter( );
 8 }
 9 void tx_application_define(void *first_unused_memory)
10 {
11     /* Create my_thread! */
12     tx_thread_create(&my_thread, "My Thread",
13     my_thread_entry, 0x1234, first_unused_memory, 1024,
14     3, 3, TX_NO_TIME_SLICE, TX_AUTO_START);
15 }
16 void my_thread_entry(ULONG thread_input)
17 {
18     /* Enter into a forever loop. */
19     while(1)
20     {
21         /* Increment thread counter. */
22         my_thread_counter++;
23         /* Sleep for 1 tick. */
24         tx_thread_sleep(1);
25     }
26 }

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:

141   ...
142   /* Init code for STM32_WPAN */
143   /* Containing all application initialization could be run before kernel launching */
144   MX_APPE_Init();
145   /* Launching ThreadX kernel */
146   MX_ThreadX_Init();
147   /* Infinite loop */
148   while(1)
149   {
150   }
151   ...

Timer17 has to be used for HAL timebase:

492  
493 /**
494   * @brief  Period elapsed callback in non blocking mode
495   * @note   This function is called  when TIM17 interrupt took place, inside
496   * HAL_TIM_IRQHandler(). It makes a direct call to HAL_IncTick() to increment
497   * a global variable "uwTick" used as application time base.
498   * @param  htim : TIM handle
499   * @retval None
500   */
501 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
502 {
503   /* USER CODE BEGIN Callback 0 */
504 
505   /* USER CODE END Callback 0 */
506   if (htim->Instance == TIM17) {
507     HAL_IncTick();
508   }
509   /* USER CODE BEGIN Callback 1 */
510 
511   /* USER CODE END Callback 1 */
512 }
3.2.5.2 Core\Src\app_entry.c

ThreadX API should be used instead of Sequencer API:

27 ...
28 #include "tx_api.h"
29 ...

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

64 ...
65 /* USER CODE BEGIN PV */
66 CHAR* p_pointer = TX_NULL;
67 static TX_MUTEX     mutex_shci;
68 static TX_SEMAPHORE semaphore_shci;
69 static TX_SEMAPHORE semaphore_shci_tl_notify_async_evt;
70 static TX_THREAD    thread_ShciUserEvtProcess;
71 static TX_BYTE_POOL byte_pool_ble;
72 static UCHAR a_memory_area[DEMO_BYTE_POOL_SIZE];
73 /* USER CODE END PV */
74 
75 /* Private functions prototypes-----------------------------------------------*/
76 static void thread_ShciUserEvtProcess_entry(ULONG thread_input);
77 ...

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

154 ...
155 /**
156   * @brief  MX_ThreadX_Init
157   * @param  None
158   * @retval None
159   */
160 void MX_ThreadX_Init(void)
161 {
162   /* USER CODE BEGIN  Before_Kernel_Start */
163 
164   /* USER CODE END  Before_Kernel_Start */
165 
166   tx_kernel_enter();
167 
168   /* USER CODE BEGIN  Kernel_Start_Error */
169 
170   /* USER CODE END  Kernel_Start_Error */
171 }
172 void tx_application_define(void* first_unused_memory)
173 {
174   UNUSED(first_unused_memory);
175 
176   /* Here we should declare all the initial ThreadX resources
177    * We should have at least one thread to be launched from the scheduler */
178 
179   /* Create a byte memory pool from which to allocate the thread stacks, 
180    * using static memory coming from a_memory_area array  */
181   tx_byte_pool_create(&byte_pool_ble, "byte pool 0", a_memory_area, DEMO_BYTE_POOL_SIZE);
182 
183   tx_mutex_create(&mutex_shci, "mutex_shci", TX_NO_INHERIT);
184   tx_semaphore_create(&semaphore_shci, "semaphore_shci", 0);
185   tx_semaphore_create(&semaphore_shci_tl_notify_async_evt,
186                       "semaphore_shci_tl_notify_async_evt",
187                       0);
188 
189   tx_byte_allocate(&byte_pool_ble, (VOID**) &p_pointer, DEMO_STACK_SIZE_LARGE, TX_NO_WAIT);
190   tx_thread_create(&thread_ShciUserEvtProcess,
191                    "thread_ShciUserEvtProcess",
192                    thread_ShciUserEvtProcess_entry,
193                    0,
194                    p_pointer,
195                    DEMO_STACK_SIZE_LARGE,
196                    16,
197                    16,
198                    TX_NO_TIME_SLICE,
199                    TX_AUTO_START);
200 }
201 ...

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

399 ...
400 static void APPE_SysStatusNot( SHCI_TL_CmdStatus_t status )
401 {
402     switch (status) {
403         case SHCI_TL_CmdBusy:
404             tx_mutex_get(&mutex_shci, TX_WAIT_FOREVER);
405             break;
406 
407         case SHCI_TL_CmdAvailable:
408             tx_mutex_put(&mutex_shci);
409             break;
410 
411         default:
412             break;
413     }
414     return;
415 }
416 ...

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

645  
646 ...
647 static void thread_ShciUserEvtProcess_entry(ULONG thread_input)
648 {
649   UNUSED(thread_input);
650 
651   while (1) {
652         tx_semaphore_get(&semaphore_shci_tl_notify_async_evt, TX_WAIT_FOREVER);
653         shci_user_evt_proc();
654     }
655 }
656 
657 void shci_notify_asynch_evt(void* pdata)
658 {
659   UNUSED(pdata);
660   tx_semaphore_put(&semaphore_shci_tl_notify_async_evt);
661 	return;
662 }
663 
664 void shci_cmd_resp_release(uint32_t flag)
665 {
666   UNUSED(flag);
667   tx_semaphore_put(&semaphore_shci);
668 	return;
669 }
670 
671 void shci_cmd_resp_wait(uint32_t timeout)
672 {
673   UNUSED(timeout);
674   tx_semaphore_get(&semaphore_shci, TX_WAIT_FOREVER);
675 	return;
676 }
677 ...
3.2.5.3 STM32_WPAN\App\app_ble.c

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

31 ...
32 #include "tx_api.h"
33 ...
34 #include "limits.h"
35 ...

Low power timer structure used by ThreadX:

171 ...
172 #if (CFG_LPM_SUPPORTED == 1)
173 typedef struct
174 {
175   uint32_t LpTXTimeLeftOnEntry;
176   uint8_t LpTXTimerThreadx_Id;
177 } LpTXTimerContext_t;
178 #endif
179 ...

Low-power timer definiton

250 ...
251 #if (CFG_LPM_SUPPORTED == 1)
252 static LpTXTimerContext_t LpTXTimerContext;
253 #endif
254 ...

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

255 ...
256 static TX_MUTEX     mtx_hci;
257 static TX_SEMAPHORE sem_hci;
258 static TX_THREAD    thread_HciUserEvtProcess;
259 static TX_SEMAPHORE sem_HciUserEvtProcessSignal;
260 static TX_THREAD    thread_AdvUpdateProcess;
261 static TX_SEMAPHORE sem_AdvUpdateProcessSignal;
262 
263 
264 /* Private function prototypes -----------------------------------------------*/
265 static void thread_AdvUpdateProcess_entry(ULONG thread_input);
266 static void thread_HciUserEvtProcess_entry(ULONG thread_input);
267 ...

Prototype of low-power timer callback:

277 /* USER CODE BEGIN PFP */
278 #if (CFG_LPM_SUPPORTED == 1)
279 static void APP_BLE_Threadx_LpTimerCb(void);
280 #endif
281 /* USER CODE END PFP */
282 ...

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

289 ...
290 /* Functions Definition ------------------------------------------------------*/
291 void APP_BLE_Init(TX_BYTE_POOL* p_byte_pool)
292 {
293   SHCI_CmdStatus_t status;
294   /* USER CODE BEGIN APP_BLE_Init_1 */
295 ...

ThreadX resources definition and thread creation for hci event processing

341  ...
342   CHAR* p_pointer;
343   tx_mutex_create(&mtx_hci, "mtx_hci", TX_NO_INHERIT);
344   tx_semaphore_create(&sem_hci, "sem_hci", 0);
345   tx_semaphore_create(&sem_HciUserEvtProcessSignal, "sem_HciUserEvtProcessSignal", 0);
346   tx_byte_allocate(p_byte_pool, (VOID**) &p_pointer, DEMO_STACK_SIZE_LARGE, TX_NO_WAIT);
347   tx_thread_create(&thread_HciUserEvtProcess,
348                    "thread_HciUserEvtProcess",
349                    thread_HciUserEvtProcess_entry,
350                    0,
351                    p_pointer,
352                    DEMO_STACK_SIZE_LARGE,
353                    16,
354                    16,
355                    TX_NO_TIME_SLICE,
356                    TX_AUTO_START);
357 ...

ThreadX resources definition and thread creation for advertisement management:

390   ...
391   tx_semaphore_create(&sem_AdvUpdateProcessSignal, "sem_AdvUpdateProcessSignal", 0);
392   tx_byte_allocate(p_byte_pool, (VOID**) &p_pointer, DEMO_STACK_SIZE_REDUCED, TX_NO_WAIT);
393   tx_thread_create(&thread_AdvUpdateProcess,
394                    "thread_AdvUpdateProcess",
395                    thread_AdvUpdateProcess_entry,
396                    0,
397                    p_pointer,
398                    DEMO_STACK_SIZE_REDUCED,
399                    16,
400                    16,
401                    TX_NO_TIME_SLICE,
402                    TX_AUTO_START);
403  ...

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

415  ...
416   /**
417    * Initialize HRS Application
418    */
419   HRSAPP_Init(p_byte_pool);
420 
421   /* USER CODE BEGIN APP_BLE_Init_3 */
422 
423   /* USER CODE END APP_BLE_Init_3 */
424 
425   /**
426    * Create timer to handle the connection state machine
427    */
428   HW_TS_Create(CFG_TIM_PROC_ID_ISR, &(BleApplicationContext.Advertising_mgr_timer_Id), hw_ts_SingleShot, Adv_Mgr);
429 
430 #if (CFG_LPM_SUPPORTED == 1)
431   /**
432    * Create low power timer to handle the user ThreadX timers
433    */
434   HW_TS_Create(CFG_TIM_PROC_ID_ISR, &(LpTXTimerContext.LpTXTimerThreadx_Id), hw_ts_SingleShot, APP_BLE_Threadx_LpTimerCb);
435 #endif
436 ...

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

824 ...
825 #if (CFG_LPM_SUPPORTED == 1)
826 
827 static void APP_BLE_Threadx_LpTimerCb( void )
828 {
829   /**
830   * Nothing to be done
831   */
832   return;
833 }
834 
835 /**
836  * @brief  Request to start a low power timer
837  *
838  * @param  tx_low_power_next_expiration: Number of ThreadX ticks
839  * @retval None
840  */
841 void APP_BLE_ThreadX_Low_Power_Setup(ULONG tx_low_power_next_expiration)
842 {
843   uint64_t time;
844   /* Timer was already created, here we need to start it */
845   /* By default, see  tx_initialize_low_level.S, each tick is 10 ms */
846   /* This function should be very similar to LpTimerStart used in freertos_port.c */
847   /* Converts the number of FreeRTOS ticks into hw timer tick */
848   if (tx_low_power_next_expiration > (ULLONG_MAX / 1e12)) /* Prevent overflow in else statement */
849   {
850     time = 0xFFFF0000; /* Maximum value equal to 24 days */
851   }
852   else
853   {
854     /* The result always fits in uint32_t and is always less than 0xFFFF0000 */
855     time = tx_low_power_next_expiration * 1000000000000ULL;
856     time = (uint64_t)( time /  ( CFG_TS_TICK_VAL_PS * TX_TIMER_TICK_PER_SECOND ));
857   }
858 
859   HW_TS_Start(LpTXTimerContext.LpTXTimerThreadx_Id, (uint32_t)time);
860 
861   /**
862    * There might be other timers already running in the timer server that may elapse
863    * before this one.
864    * Store how long before the next event so that on wakeup, it will be possible to calculate
865    * how long the tick has been suppressed
866    */
867   LpTXTimerContext.LpTXTimeLeftOnEntry = HW_TS_RTC_ReadLeftTicksToCount( );
868 
869   return;
870 }
871 /**
872  * @brief  Read how long the tick has been suppressed
873  *
874  * @param  None
875  * @retval The number of tick rate (FreeRTOS tick)
876  */
877 unsigned long APP_BLE_Threadx_Low_Power_Adjust_Ticks(void)
878 {
879   uint64_t val_ticks, time_ps;
880   uint32_t LpTimeLeftOnExit;
881 
882   LpTimeLeftOnExit = HW_TS_RTC_ReadLeftTicksToCount();
883   /* This cannot overflow. Max result is ~ 1.6e13 */
884   time_ps = (uint64_t)((CFG_TS_TICK_VAL_PS) * (uint64_t)(LpTXTimerContext.LpTXTimeLeftOnEntry - LpTimeLeftOnExit));
885 
886   /* time_ps can be less than 1 RTOS tick in following situations
887    * a) MCU didn't go to STOP2 due to wake-up unrelated to Timer Server or woke up from STOP2 very shortly after.
888    *    Advancing RTOS clock by 1 ThreadX tick doesn't hurt in this case.
889    * 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.
890    *    The xExpectedIdleTime is decremented by one RTOS tick to wake-up in advance.
891    *    Ex: RTOS tick is 1ms, the timer Server wakes the MCU in ~977 us. RTOS clock should be advanced by 1 ms.
892    * */
893   if(time_ps <= (1e12 / TX_TIMER_TICK_PER_SECOND)) /* time_ps < RTOS tick */
894   {
895     val_ticks = 1;
896   }
897   else
898   {
899     /* Convert pS time into OS ticks */
900     val_ticks = time_ps * TX_TIMER_TICK_PER_SECOND; /* This cannot overflow. Max result is ~ 1.6e16 */
901     val_ticks = (uint64_t)(val_ticks / (1e12)); /* The result always fits in uint32_t */
902   }
903 
904   /**
905    * The system may have been out from another reason than the timer
906    * Stop the timer after the elapsed time is calculated other wise, HW_TS_RTC_ReadLeftTicksToCount()
907    * may return 0xFFFF ( TIMER LIST EMPTY )
908    * It does not hurt stopping a timer that exists but is not running.
909    */
910   HW_TS_Stop(LpTXTimerContext.LpTXTimerThreadx_Id);
911 
912   return (unsigned long)val_ticks;
913 }
914 #endif
915 ...

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:

1402 ...
1403 static void Adv_Mgr(void)
1404 {
1405   /**
1406    * The code shall be executed in the background as an aci command may be sent
1407    * The background is the only place where the application can make sure a new aci command
1408    * is not sent if there is a pending one
1409    */
1410    tx_semaphore_put(&sem_AdvUpdateProcessSignal);
1411 
1412   return;
1413 }
1414 
1415 static void thread_AdvUpdateProcess_entry(ULONG argument)
1416 {
1417   UNUSED(argument);
1418 
1419   for (;;){
1420             tx_semaphore_get(&sem_AdvUpdateProcessSignal, TX_WAIT_FOREVER);
1421             Adv_Update();
1422           }
1423 }
1424 
1425 static void Adv_Update(void)
1426 {
1427   Adv_Request(APP_BLE_LP_ADV);
1428 
1429   return;
1430 }
1431 
1432 static void thread_HciUserEvtProcess_entry(ULONG argument)
1433 {
1434   UNUSED(argument);
1435 
1436   for (;;) {
1437              tx_semaphore_get(&sem_HciUserEvtProcessSignal, TX_WAIT_FOREVER);
1438              hci_user_evt_proc();
1439            }
1440 }
1441 
1442 /* USER CODE BEGIN FD_SPECIFIC_FUNCTIONS */
1443 
1444 /* USER CODE END FD_SPECIFIC_FUNCTIONS */
1445 /*************************************************************
1446  *
1447  * WRAP FUNCTIONS
1448  *
1449  *************************************************************/
1450 void hci_notify_asynch_evt(void* pdata)
1451 {
1452   UNUSED(pdata);
1453   tx_semaphore_put(&sem_HciUserEvtProcessSignal);
1454   
1455   return;
1456 }
1457 
1458 void hci_cmd_resp_release(uint32_t flag)
1459 {
1460   UNUSED(flag);
1461   tx_semaphore_put(&sem_hci);
1462   
1463   return;
1464 }
1465 
1466 void hci_cmd_resp_wait(uint32_t timeout)
1467 {
1468   UNUSED(timeout);
1469   tx_semaphore_get(&sem_hci, TX_WAIT_FOREVER);
1470   
1471   return;
1472 }
1473 
1474 static void BLE_UserEvtRx(void *p_Payload)
1475 {
1476   SVCCTL_UserEvtFlowStatus_t svctl_return_status;
1477   tHCI_UserEvtRxParam *p_param;
1478 
1479   p_param = (tHCI_UserEvtRxParam *)p_Payload;
1480 
1481   svctl_return_status = SVCCTL_UserEvtRx((void *)&(p_param->pckt->evtserial));
1482   if (svctl_return_status != SVCCTL_UserEvtFlowDisable)
1483   {
1484     p_param->status = HCI_TL_UserEventFlow_Enable;
1485   }
1486   else
1487   {
1488     p_param->status = HCI_TL_UserEventFlow_Disable;
1489   }
1490 
1491   return;
1492 }
1493 
1494 static void BLE_StatusNot(HCI_TL_CmdStatus_t Status)
1495 {
1496   switch (Status)
1497   {
1498     case HCI_TL_CmdBusy:
1499       /**
1500        * All tasks that may send an aci/hci commands shall be listed here
1501        * This is to prevent a new command is sent while one is already pending
1502        */
1503       tx_mutex_get(&mtx_hci, TX_WAIT_FOREVER);
1504       /* USER CODE BEGIN HCI_TL_CmdBusy */
1505 
1506       /* USER CODE END HCI_TL_CmdBusy */
1507       break;
1508 
1509     case HCI_TL_CmdAvailable:
1510       /**
1511        * All tasks that may send an aci/hci commands shall be listed here
1512        * This is to prevent a new command is sent while one is already pending
1513        */
1514       tx_mutex_put(&mtx_hci);
1515       /* USER CODE BEGIN HCI_TL_CmdAvailable */
1516 
1517       /* USER CODE END HCI_TL_CmdAvailable */
1518       break;
1519 
1520     default:
1521       /* USER CODE BEGIN default */
1522 
1523       /* USER CODE END default */
1524       break;
1525   }
1526 
1527   return;
1528 }
1529 ...
3.2.5.4 STM32_WPAN\App\hrs_app.c
28 ...
29 #include "tx_api.h"
30 ...
69 ...
70 static TX_THREAD thread_HrsProcess;
71 static TX_SEMAPHORE sem_HrsProcessSignal;
72 
73 /* USER CODE BEGIN PV */
74 
75 /* USER CODE END PV */
76 
77 /* Private functions prototypes-----------------------------------------------*/
78 static void HrMeas(void);
79 static void thread_HrsProcess_entry(ULONG argument);
80 static void HRSAPP_Measurement(void);
81 ...
143 ...
144 void HRSAPP_Init(TX_BYTE_POOL* p_byte_pool)
145 {
146   CHAR* p_pointer;
147   tx_semaphore_create(&sem_HrsProcessSignal, "sem_HrsProcessSignal", 0);
148   tx_byte_allocate(p_byte_pool, (VOID**) &p_pointer, DEMO_STACK_SIZE_REDUCED, TX_NO_WAIT);
149   tx_thread_create(&thread_HrsProcess,
150                    "thread_HrsProcess",
151                    thread_HrsProcess_entry,
152                    0,
153                    p_pointer,
154                    DEMO_STACK_SIZE_REDUCED,
155                    16,
156                    16,
157                    TX_NO_TIME_SLICE,
158                    TX_AUTO_START);
159 /* USER CODE BEGIN HRSAPP_Init */
160 ...
200 ...
201 static void thread_HrsProcess_entry(ULONG argument)
202 {
203   UNUSED(argument);
204 
205   for(;;)
206   {
207     tx_semaphore_get(&sem_HrsProcessSignal, TX_WAIT_FOREVER);
208     HRSAPP_Measurement( );
209   }
210 }
211 ...
233 ...
234 static void HrMeas( void )
235 {
236   /**
237    * Notifying a new measure is available
238    */
239   tx_semaphore_put(&sem_HrsProcessSignal);
240 /* USER CODE BEGIN HrMeas */
241 
242 /* USER CODE END HrMeas */
243 
244   return;
245 }
246 ...

4 References