Sequencer operation - Francerigo/STM32WL55JC1_end_node GitHub Wiki

Sequencer Overview

The sequencer provides a robust and efficient framework for executing tasks in the background while ensuring low-power operation when the system is idle. It also implements a mechanism to prevent race conditions and includes an event-handling feature, allowing functions to wait for specific events triggered by interrupts.

This approach optimizes both CPU usage and power consumption, making it ideal for applications that follow a "run to completion" execution model.

The sequencer provides the following features:

  1. Support up to 32 tasks and 32 events
  2. Task registration and execution
  3. Wait for an event and set event
  4. Task priority setting
  5. Race condition safe low-power entry

To use the sequencer, the application must perform the following:

  1. Set the maximum number of supported functions, by defining a value for UTIL_SEQ_CONF_TASK_NBR .
  2. Register a function to be supported by the sequencer with UTIL_SEQ_RegTask() .
  3. Start the sequencer by calling UTIL_SEQ_Run() to run a background while loop.
  4. Call UTIL_SEQ_SetTask() when a function needs to be executed.

The sequencer utility is located in Utilities > sequencer > stm32_seq.c

Configuration

The sequencer tasks and event IDs are configured in the utilities_def.h file, located at:

Path: Core > Inc > utilities_def.h

Note: Do not remove any existing task or event IDs.

Each task must be assigned a unique Task ID (a number between 0 and 31). The following code snippet defines the structure for task registration:

typedef enum
{
  CFG_SEQ_Task_LmHandlerProcess,
  CFG_SEQ_Task_LoRaSendOnTxTimerOrButtonEvent,
  CFG_SEQ_Task_LoRaStoreContextEvent,
  CFG_SEQ_Task_LoRaStopJoinEvent,
  /* USER CODE BEGIN CFG_SEQ_Task_Id_t */

  /* USER CODE END CFG_SEQ_Task_Id_t */
  CFG_SEQ_Task_NBR
} CFG_SEQ_Task_Id_t;

  • CFG_SEQ_Task_LmHandlerProcess = 0
  • CFG_SEQ_Task_LoRaSendOnTxTimerOrButtonEvent = 1
  • CFG_SEQ_Task_LoRaStoreContextEvent = 2
  • CFG_SEQ_Task_LoRaStopJoinEvent = 3

Register a Task

In LoRaWAN > App > lora_app.c different functions interface with the sequencer operation. When executing the main, MX_LoRaWAN_Init(); calls LoRaWAN_Init(void), which registers the tasks by proceeding as follows:

void LoRaWAN_Init(void)
{
  ...

  UTIL_SEQ_RegTask((1 << CFG_SEQ_Task_LmHandlerProcess), UTIL_SEQ_RFU, LmHandlerProcess);
  UTIL_SEQ_RegTask((1 << CFG_SEQ_Task_LoRaSendOnTxTimerOrButtonEvent), UTIL_SEQ_RFU, SendTxData);
  UTIL_SEQ_RegTask((1 << CFG_SEQ_Task_LoRaStoreContextEvent), UTIL_SEQ_RFU, StoreContext);
  UTIL_SEQ_RegTask((1 << CFG_SEQ_Task_LoRaStopJoinEvent), UTIL_SEQ_RFU, StopJoin);

  ...
}

The function which is called is:

void UTIL_SEQ_RegTask(UTIL_SEQ_bm_t TaskId_bm, uint32_t Flags, void (*Task)( void ))
{
  (void)Flags;
  UTIL_SEQ_ENTER_CRITICAL_SECTION();

  TaskCb[SEQ_BitPosition(TaskId_bm)] = Task;

  UTIL_SEQ_EXIT_CRITICAL_SECTION();

  return;
}

Let's analyze the three input parameters to the function void UTIL_SEQ_RegTask(UTIL_SEQ_bm_t TaskId_bm, uint32_t Flags, void (*Task)( void )):

  • The first parameter is related to Task IDs.
  • By doing 1 << your_task_ID, the result is a uint32_t bitmask created by shifting the binary value 1 to the left by the task ID.

In this case:

  • CFG_SEQ_Task_LmHandlerProcess = 0: (1 << 0) equals 1 (0x0001 in hexadecimal, 00000000000000000000000000000001 in binary).
  • CFG_SEQ_Task_LoRaSendOnTxTimerOrButtonEvent = 1: (1 << 1) equals 2 (0x0002, 0000000000000000000000000000010).
  • CFG_SEQ_Task_LoRaStoreContextEvent = 2: (1 << 2) equals 4 (0x0004, 0000000000000000000000000000100).
  • CFG_SEQ_Task_LoRaStopJoinEvent = 3: (1 << 3) equals 8 (0x0008, 0000000000000000000000000001000).

This bit mask indicates which task is being registered, with each bit corresponding to a specific task.

  • The second (UTIL_SEQ_RFU) is defined as 0 and serves as flag.

The Flags parameter is present to allow for future expansion or configuration options. In the current implementation, it's deliberately not used as indicated by the line (void)Flags; but it exists so that if later versions of the sequencer require additional flags to modify task behavior.

  • Finally, the third parameter is a pointer to the Task function, which feeds TaskCb[SEQ_BitPosition(TaskId_bm)] = Task;.

SEQ_BitPosition(TaskId_bm) returns the position of the first bit set to 1, and is implemented as:

/**
 * @brief return the position of the first bit set to 1
 * @param Value 32 bit value
 * @retval bit position
 */
uint8_t SEQ_BitPosition(uint32_t Value)
{
uint8_t n = 0U;
uint32_t lvalue = Value;

  if ((lvalue & 0xFFFF0000U) == 0U)  { n  = 16U; lvalue <<= 16U;  }
  if ((lvalue & 0xFF000000U) == 0U)  { n +=  8U; lvalue <<=  8U;  }
  if ((lvalue & 0xF0000000U) == 0U)  { n +=  4U; lvalue <<=  4U;  }

  n += SEQ_clz_table_4bit[lvalue >> (32-4)];

  return (uint8_t)(31U-n);
}

The number of the first bit different from 0, which was set by the bitmask we calculated, is used as index for the buffer TaskCb, defined as static void (*TaskCb[UTIL_SEQ_CONF_TASK_NBR])( void );. This declaration creates a static array of function pointers named TaskCb. As it's static, the array has internal linkage, so it is only visible within the file where it's declared. It is an array with a number of UTIL_SEQ_CONF_TASK_NBR elements. Each element is a pointer to a function that takes no arguments (void) and returns void.

Set a Task

Whenever an interrupt occurs, the sequencer calls the function UTIL_SEQ_SetTask(), which marks a specific task as pending and assigns it a priority. Each task is identified by a bit in a 32-bit bitmask and must be associated with a priority level.

void UTIL_SEQ_SetTask( UTIL_SEQ_bm_t TaskId_bm , uint32_t Task_Prio )
{
  UTIL_SEQ_ENTER_CRITICAL_SECTION( );

  TaskSet |= TaskId_bm;
  TaskPrio[Task_Prio].priority |= TaskId_bm;

  UTIL_SEQ_EXIT_CRITICAL_SECTION( );

  return;
}

The function first enters a critical section to ensure that the update to shared variables is atomic.

  • TaskSet is a global 32-bit bitmask (of type UTIL_SEQ_bm_t) where each bit represents whether a task is pending (1 if the Task is pending, 0 if not).

  • The array TaskPrio holds task priority information. Each element corresponds to a specific priority level and is of type UTIL_SEQ_Priority_t. The structure UTIL_SEQ_Priority_t contains two fields, as its defined as:

typedef struct
{
  uint32_t priority;    /*!<bit field of the enabled task.          */
  uint32_t round_robin; /*!<mask on the allowed task to be running. */
} UTIL_SEQ_Priority_t;

By executing TaskPrio[Task_Prio].priority |= TaskId_bm;, the function marks the task as pending within the priority level provided by Task_Prio.

More in detail: we have an array TaskPrio, composed of two-fielded elements. We are taking the .priority 32 bit value in position Task_Prio (e.g., if Task_Prio = 1, we are looking into the UTIL_SEQ_Priority_t; element inserted at index 1). By doing an OR with the bitmask (containing which tasks are to be executed), we set the .priority field equal to which tasks need to be executed at that priority. A graphical representation is provided here.

sequencer

The function to set tasks is called through interrupts, which may be due to the internal timer or to external events, depending on what the user has set. Let's analyze the case in which they are called by the internal timer interrupt.

static void OnTxTimerEvent(void *context)
{
  /* USER CODE BEGIN OnTxTimerEvent_1 */

  /* USER CODE END OnTxTimerEvent_1 */
  UTIL_SEQ_SetTask((1 << CFG_SEQ_Task_LoRaSendOnTxTimerOrButtonEvent), CFG_SEQ_Prio_0);

  /*Wait for next tx slot*/
  UTIL_TIMER_Start(&TxTimer);
  /* USER CODE BEGIN OnTxTimerEvent_2 */

  /* USER CODE END OnTxTimerEvent_2 */
}

This function is called because inside LoRaWAN_Init a timer is set as:

...
if (EventType == TX_ON_TIMER)
  {
    /* send every time timer elapses */
    UTIL_TIMER_Create(&TxTimer, TxPeriodicity, UTIL_TIMER_ONESHOT, OnTxTimerEvent, NULL);
    UTIL_TIMER_Start(&TxTimer);
  }
...
  • When OnTxTimerEvent is called, it calls UTIL_SEQ_SetTask((1 << CFG_SEQ_Task_LoRaSendOnTxTimerOrButtonEvent), CFG_SEQ_Prio_0);.
  • It sets the bit associated with the task sendTxData (which has TaskID CFG_SEQ_Task_LoRaSendOnTxTimerOrButtonEvent) in the 32 bit value .priority, in the TaskPrio array in position CFG_SEQ_Prio_0, which is 0 ( = highest priority). the .round_robin field is not yet modified.
  • At this point we are ok to start the task execution!

Execute a Task

Let's assume the main starts executing. After the initialization phase, the process shifts to

  while (1)
  {
    /* USER CODE END WHILE */
    MX_LoRaWAN_Process();

    /* USER CODE BEGIN 3 */
  }

The function MX_LoRaWAN_Process(); is implemented as:

  void MX_LoRaWAN_Process(void)
{
  /* USER CODE BEGIN MX_LoRaWAN_Process_1 */

  /* USER CODE END MX_LoRaWAN_Process_1 */
  UTIL_SEQ_Run(UTIL_SEQ_DEFAULT);
  /* USER CODE BEGIN MX_LoRaWAN_Process_2 */

  /* USER CODE END MX_LoRaWAN_Process_2 */
}
  • UTIL_SEQ_Run(UTIL_SEQ_DEFAULT); starts the sequencer operation.

UTIL_SEQ_DEFAULT is the default value used to start the scheduling. This informs the sequencer that all tasks registered shall be considered. It is defined as #define UTIL_SEQ_DEFAULT (~0U).

  • The sequencer runs through:
void UTIL_SEQ_Run( UTIL_SEQ_bm_t Mask_bm )
{
  uint32_t counter;
  UTIL_SEQ_bm_t current_task_set;
  UTIL_SEQ_bm_t super_mask_backup;
  UTIL_SEQ_bm_t local_taskset;
  UTIL_SEQ_bm_t local_evtset;
  UTIL_SEQ_bm_t local_taskmask;
  UTIL_SEQ_bm_t local_evtwaited;

  /*
   * When this function is nested, the mask to be applied cannot be larger than the first call
   * The mask is always getting smaller and smaller
   * A copy is made of the mask set by UTIL_SEQ_Run() in case it is called again in the task
   */
  super_mask_backup = SuperMask;
  SuperMask &= Mask_bm;

  /*
   * There are two independent mask to check:
   * TaskMask that comes from UTIL_SEQ_PauseTask() / UTIL_SEQ_ResumeTask
   * SuperMask that comes from UTIL_SEQ_Run
   * If the waited event is there, exit from  UTIL_SEQ_Run() to return to the
   * waiting task
   */
  local_taskset = TaskSet;
  local_evtset = EvtSet;
  local_taskmask = TaskMask;
  local_evtwaited =  EvtWaited;
  while(((local_taskset & local_taskmask & SuperMask) != 0U) && ((local_evtset & local_evtwaited)==0U))
  {
    counter = 0U;
    /*
     * When a flag is set, the associated bit is set in TaskPrio[counter].priority mask depending
     * on the priority parameter given from UTIL_SEQ_SetTask()
     * The while loop is looking for a flag set from the highest priority maskr to the lower
     */
    while((TaskPrio[counter].priority & local_taskmask & SuperMask)== 0U)
    {
      counter++;
    }

    current_task_set = TaskPrio[counter].priority & local_taskmask & SuperMask;

    /*
     * The round_robin register is a mask of allowed flags to be evaluated.
     * The concept is to make sure that on each round on UTIL_SEQ_Run(), if two same flags are always set,
     * the sequencer does not run always only the first one.
     * When a task has been executed, The flag is removed from the round_robin mask.
     * If on the next UTIL_SEQ_RUN(), the two same flags are set again, the round_robin mask will mask out the first flag
     * so that the second one can be executed.
     * Note that the first flag is not removed from the list of pending task but just masked by the round_robin mask
     *
     * In the check below, the round_robin mask is reinitialize in case all pending tasks haven been executed at least once
     */
    if ((TaskPrio[counter].round_robin & current_task_set) == 0U)
    {
      TaskPrio[counter].round_robin = UTIL_SEQ_ALL_BIT_SET;
    }

  /*
   * Read the flag index of the task to be executed
	 * Once the index is read, the associated task will be executed even though a higher priority stack is requested
	 * before task execution.
	 */
    CurrentTaskIdx = (SEQ_BitPosition(current_task_set & TaskPrio[counter].round_robin));

    /*
     * remove from the roun_robin mask the task that has been selected to be executed
     */
    TaskPrio[counter].round_robin &= ~(1U << CurrentTaskIdx);

    UTIL_SEQ_ENTER_CRITICAL_SECTION( );
    /* remove from the list or pending task the one that has been selected to be executed */
    TaskSet &= ~(1U << CurrentTaskIdx);
    /* remove from all priority mask the task that has been selected to be executed */
    for (counter = UTIL_SEQ_CONF_PRIO_NBR; counter != 0U; counter--)
    {
      TaskPrio[counter - 1U].priority &= ~(1U << CurrentTaskIdx);
    }
    UTIL_SEQ_EXIT_CRITICAL_SECTION( );

    /* Execute the task */
    TaskCb[CurrentTaskIdx]( );

    local_taskset = TaskSet;
    local_evtset = EvtSet;
    local_taskmask = TaskMask;
    local_evtwaited = EvtWaited;
  }

  /* the set of CurrentTaskIdx to no task running allows to call WaitEvt in the Pre/Post ilde context */
  CurrentTaskIdx = UTIL_SEQ_NOTASKRUNNING;
  UTIL_SEQ_PreIdle( );

  UTIL_SEQ_ENTER_CRITICAL_SECTION_IDLE( );
  local_taskset = TaskSet;
  local_evtset = EvtSet;
  local_taskmask = TaskMask;
  if ((local_taskset & local_taskmask & SuperMask) == 0U)
  {
    if ((local_evtset & EvtWaited)== 0U)
    {
      UTIL_SEQ_Idle( );
    }
  }
  UTIL_SEQ_EXIT_CRITICAL_SECTION_IDLE( );

  UTIL_SEQ_PostIdle( );

  /* restore the mask from UTIL_SEQ_Run() */
  SuperMask = super_mask_backup;

  return;
}

Let's analyze it step by step.

Variables declaration.

  • counter: used as an index or loop counter.
  • current_task_set: holds the bitmask of tasks eligible for execution at a given priority.
  • super_mask_backup: a backup of the global "super mask".
  • local_taskset, local_evtset, local_taskmask, local_evtwaited: local copies of global variables that define the current state of pending tasks, events, and their masks.

Backup and Update the Global Super Mask

super_mask_backup = SuperMask;
SuperMask &= Mask_bm;
  • Backup: The current global mask (SuperMask) is saved. At first, static UTIL_SEQ_bm_t SuperMask = UTIL_SEQ_ALL_BIT_SET;, where #define UTIL_SEQ_ALL_BIT_SET (~0U).
  • Update: The passed-in mask (Mask_bm) is ANDed with SuperMask.

At first, both Mask_bm and SuperMask are defined as ~0U (which means NOT 0U -> 0xFFFFFFFF). Therefore, the AND operation just returns ~0U at the first iteration. Otherwise, if we would have passed a Mask_bm different from ~0U, the SuperMask value updates to the value of Mask_bm.

Take Local Snapshots of Global State

local_taskset = TaskSet; 
local_evtset = EvtSet; 
local_taskmask = TaskMask; 
local_evtwaited = EvtWaited; 

This creates local copies of the pending tasks and events, ensuring that the function works with a consistent snapshot of the system state. At first, TaskSet = 0U, EvtSet = 0U, TaskMask = ~0U, EvtWaited = 0U;

Main Processing Loop Condition

while (((local_taskset & local_taskmask & SuperMask) != 0U) && ((local_evtset & local_evtwaited) == 0U))

The loop continues while:

  • There is at least one pending task allowed by both TaskMask and SuperMask.
  • No event that is being waited for has occurred (i.e., EvtSet does not contain the awaited event bits).

Right after initialization:

  • At first, local_taskset = 0000.., local_taskmask = 1111.., SuperMask = 1111..
  • At first, local_evtset = 0000.., local_evtwaited = 0000..
  • Therefore, at first the loop is not executed, because no tasks are to be executed.

As soon as an interrupt occurs, a Task is set for execution (through UTIL_SEQ_SetTask). TaskSet contains a bitmask representing all the tasks that are currently pending execution.

Each bit in local_taskset corresponds to a specific task; if a bit is set (i.e., it is 1), it indicates that the corresponding task is scheduled to run. This means that, for example, having TaskSet = 0000000000000000000000000000010 means having to execute the task with TaskID = 2, which is sendTxData.

Finding the Highest Priority Task

counter = 0U;
while ((TaskPrio[counter].priority & local_taskmask & SuperMask) == 0U)
{
  counter++;
}

The loop scans through the priority levels (starting at the highest priority, which is 0) until it finds a level where the bitmask (priority field) indicates that at least one task is pending.

Determining the Eligible Task(s) at This Priority Level

current_task_set = TaskPrio[counter].priority & local_taskmask & SuperMask;

Combines the current priority level’s tasks with the task mask (which is TaskSet) and the super mask (which was put equal to the bitmask) to obtain the set of tasks that are eligible for execution.

  • TaskPrio[counter].priority contains a bitmask representing the subset of tasks present at that priority
  • local_taskmask contains the bitmask including all tasks.

Round-Robin Mask Reset

if ((TaskPrio[counter].round_robin & current_task_set) == 0U)
{
  TaskPrio[counter].round_robin = UTIL_SEQ_ALL_BIT_SET;
}

The round-robin mask is used to ensure fairness when multiple tasks of the same priority are pending.

  • When a round_robin bit is set, it means the task can be executed, and viceversa
  • If all tasks at this priority have already been executed (i.e., none are currently allowed by the round-robin mask), it resets the round-robin mask so that tasks can be executed again in a new round.

Selecting the Task to Execute

CurrentTaskIdx = (SEQ_BitPosition(current_task_set & TaskPrio[counter].round_robin));
  • SEQ_BitPosition finds the bit index of the first eligible task in the bitmask (taking the round-robin restrictions into account).
  • This index (CurrentTaskIdx) identifies which task function to execute next.

Updating the Round-Robin Mask

TaskPrio[counter].round_robin &= ~(1U << CurrentTaskIdx);

After selecting a task, its corresponding bit is cleared in the round-robin mask so that the same task isn't immediately re-selected within the same round.

Removing the Task from the Pending List (Critical Section)

UTIL_SEQ_ENTER_CRITICAL_SECTION();
TaskSet &= ~(1U << CurrentTaskIdx);
for (counter = UTIL_SEQ_CONF_PRIO_NBR; counter != 0U; counter--)
{
  TaskPrio[counter - 1U].priority &= ~(1U << CurrentTaskIdx);
}
UTIL_SEQ_EXIT_CRITICAL_SECTION();

A critical section is entered to ensure atomicity. The selected task is removed from the global pending task set (TaskSet). It is also removed from all priority masks to ensure it isn’t run again until it’s re-registered. The critical section is then exited.

Executing the Selected Task

TaskCb[CurrentTaskIdx]( );

The task function, whose pointer is stored in the TaskCb array at index CurrentTaskIdx, is called. This executes the task.

Update Local State After Task Execution

local_taskset = TaskSet;
local_evtset = EvtSet;
local_taskmask = TaskMask;
local_evtwaited = EvtWaited;

The local copies of the state are refreshed to capture any changes made during task execution.

After All Eligible Tasks Have Run (i.e., when the loop condition is no longer met):

Once the loop condition is no longer met (either no tasks remain or an awaited event has occurred), the function sets:

CurrentTaskIdx = UTIL_SEQ_NOTASKRUNNING;

This signals that no task is currently running.

Pre-Idle Processing

UTIL_SEQ_PreIdle();

This callback allows the application to perform any necessary actions before entering an idle state.

Entering Idle State (If No Tasks or Events Are Pending)

UTIL_SEQ_ENTER_CRITICAL_SECTION_IDLE();
local_taskset = TaskSet;
local_evtset = EvtSet;
local_taskmask = TaskMask;
if ((local_taskset & local_taskmask & SuperMask) == 0U)
{
  if ((local_evtset & EvtWaited) == 0U)
  {
    UTIL_SEQ_Idle();
  }
}
UTIL_SEQ_EXIT_CRITICAL_SECTION_IDLE();

A critical section ensures that checking for pending tasks or events is done atomically. If there are no pending tasks (after applying the masks) and no awaited events, the sequencer calls UTIL_SEQ_Idle() to put the system into an idle state.

Post-Idle Processing

UTIL_SEQ_PostIdle();

After coming out of the idle state, a post-idle callback is invoked, which can perform any cleanup or further processing.

Restoring the Global Super Mask

SuperMask = super_mask_backup;

The original SuperMask is restored to its previous value, ensuring that the global state is consistent for any outer (or future) calls.

Function Exit

The function then returns, having processed all pending tasks and handled the idle state as necessary.