Overview#
The programming model applies only to projects that are generated by Simplicity Studio v5's project configuration tool. As of this release this is only applicable to Bluetooth and Proprietary SDKs. This section explains the details of how a project generated by Simplicity Studio v5's project configuration tool works, and how you integrate your application to it. This covers both new projects and projects that are migrated to the new SDK version.
The programming model covers several important areas such as hardware initialization, software initialization, and periodic action processing. It covers both bare metal and kernel based applications. For instance, in case of a bare metal application, this programming model will allow you to add processing action functions to the main loop.
If you are migrating an existing application to this new version, be aware that this programming model now handles some hardware initializations (hardware clocks, DCDC, etc) and some software initializations (kernel, stacks, etc) that you may have had to do manually with previous versions.
Using this programming model for your application is important as it will ensure proper hardware initialization and proper SDK software modules initialization. Using this programming model will also ensure proper power management, as power management has to be a centralized service (power requirements can come from different requesters). For more information on power management, refer to Power Manager section.
Note: The SDK Programming model is compatible with SiSDK 2025.6 and greater versions.
Features#
Initialization#
In a bare metal application, a single function, sl_main_init(),
is provided to you. This function initializes all the SDK modules that
are selected in your project.
With a RTOS kernel, the main() function is executed as a task. The function
sl_main_second_stage_init() is used to initialize the SDK modules present in
your project.
Note:
System Setup (sl_main)is the new standard for the initialization sequence replacingSystem Setup (sl_system), differing in some points. If you want to migrate an sl_system application to sl_main, refer to theSL System to SL Main Migration Guide.
Action Processing#
Some of the SDK modules (mostly services and wireless stacks) have actions to process periodically. Functions are also provided to help you with that. The action processing however differs depending on if you are using a kernel or if you have a bare metal application.
Bare Metal#
If your application is bare metal, a function called
sl_main_process_action() is provided to you. This function contains all the
SDK modules "step" functions that must be called in every iteration of the super
loop. You must call this function from your super loop.
Note: For series 3 devices, bare metal is not supported. A RTOS is mandatory for the SiSDK.
When Using a Kernel (RTOS)#
If you selected a kernel in your project, the SDK modules that have to process
periodic actions will automatically create task(s) during initialization. Your
main() function is called in the start task as the kernel is already started.
The start task priority is set to the maximum during initialization`. The start
task will terminate by default, but it can be used if desired.
You should do your permanent memory allocations in app_permanent_memory_alloc(),
which is called before the kernel is started but after it was initialized.
This will improve the memory placement and could lead to lower power consumption.
Then, you can create the tasks for your application in app_init().
SDK Component Catalog#
The SDK component catalog is a simple header file named
sl_component_catalog.h that you can include from anywhere in your project.
This file contains #define entries for the different SDK components available
in your project. This allows you to enable/disable portions of code depending
on whether a SDK component is present in your project or not.
Usage Examples#
These are examples of main functions that use the functions described above
that you can use as a reference. A main.c template file is added automatically
to the project during the first generation, but it can be freely be customized
without any risk of being overridden.
Bare Metal#
#include "sl_main_init.h"
#include "sl_main_process_action.h"
#include "sl_component_catalog.h"
#ifdef SL_CATALOG_POWER_MANAGER_PRESENT
#include "sl_power_manager.h"
#endif
void app_init(void)
{
// TODO: Add required initializations for your application.
}
void app_process_action(void)
{
// TODO: Add required "step" actions for your application.
}
int main(void)
{
// Initialize Silicon Labs SDK modules.
sl_main_init();
// Initialization for your application.
app_init();
while (1) {
// Process periodic actions of your application.
app_process_action();
// Process periodic actions of Silicon Labs SDK modules.
sl_main_process_action();
#ifdef SL_CATALOG_POWER_MANAGER_PRESENT
// Let the CPU go to sleep if the system allows it.
sl_power_manager_sleep();
#endif
}
}When Using a Kernel (RTOS)#
#include "sl_main_init.h"
#include "sl_main_init_memory.h"
#include "sl_main_kernel.h"
void app_init_early(void)
{
// This function is called before the kernel is initialized.
// TODO: Add required initializations for your application.
}
void app_permanent_memory_alloc(void)
{
// This function is called before the kernel is started,
// but after the kernel is initialized.
// TODO: Do the permanent memory allocations for your application.
// TODO: Create permanent kernel objects for your application.
}
void app_init(void)
{
// TODO: Add required initializations for your application.
// TODO: Create kernel tasks for your application.
}
int main(void)
{
// Initialize Silicon Labs SDK modules.
sl_main_second_stage_init();
// Initialization for your application.
app_init();
// The start task is deleted when exiting this function.
// You can also not let this task end and use it.
}Initializing your Device#
When generating your project on a Silicon Labs kit, the project configuration
tools will automatically add two components called Automatic Device Initialization
and Clock Manager. These components provide basic hardware configuration
functionalities. These functions will automatically be called by sl_main_init().
When generating a project for a custom board, the Automatic Device Initialization
and Clock Manager modules can be manually selected.
You can customize these basic configurations by modifying the configurations
available in the Device Initialization software component. For more
information on this component, see
Device Initialization.
The Clock Manager is also fully configurable, refer to
Clock Manager.
Atomic Section General Considerations#
Overview#
The sl_core module provides two types of basic critical sections (interrupt
masking), the critical sections, and the atomic sections. The critical sections
will always mask all the interrupts (except the non-maskable ones) by using the
PRIMASK register. On the other hand, the atomic sections can mask the
interrupts by using either the PRIMASK or BASEPRI register.
Using the BASEPRI register allows you to only mask interrupts based on their
interrupt priority. That is to say, only interrupts with equal or lower
priority (higher number) than the BASEPRI register value will be masked in
an atomic section.
The default value for the BASEPRI register used to mask interrupts in an
atomic section is 3. Otherwise, the BASEPRI value is kept to 0 to allow all
interrupts.
The benefit of the atomic sections over the critical sections is that it won't mask interrupts that you would consider of very high priority and for which you want to have minimal latency. However, the atomic sections can obviously not be used to protect access to resources that would be accessed by these very high priority interrupt's handlers.
SiSDK-aware vs. non-SiSDK-aware Interrupt Considerations#
The Silicon Labs SDK uses interrupt priority levels to distinguish between SiSDK-aware
and non-SiSDK-aware interrupts. The threshold is defined by the BASEPRI register value.
When the SDK needs to protect its internal resources (including kernel resources when using
an RTOS like Micrium OS or FreeRTOS), it masks interrupts using atomic sections. This means
any interrupt with priority higher than BASEPRI (lower numerical value) will NOT be masked
and can execute at any time. However, you CANNOT use any SiSDK APIs from these very high
priority interrupt handlers (you cannot post a semaphore, call Power Manager, use NVM3,
or interact with most SDK services).
If you need to call SiSDK APIs in response to a high-priority interrupt, you can use the
software-triggered KERNEL interrupts (e.g., KERNEL0_IRQn, KERNEL1_IRQn) as a deferred
processing mechanism. From your high-priority handler, trigger a KERNEL interrupt (which
must be configured with a priority equal to or lower than BASEPRI), and perform the SiSDK
API calls from that KERNEL interrupt handler instead.
Interrupt priorities MUST be configured either manually or automatically using the
Interrupt manager module. This applies to both bare metal
and RTOS configurations. When using FreeRTOS on an MCU that supports BASEPRI, FreeRTOS
uses the configMAX_SYSCALL_INTERRUPT_PRIORITY setting to configure BASEPRI to
protect its internal resources from concurrent access.
Interrupt Manager Module#
The Interrupt Manager component initializes all interrupt handler
priorities via the sl_interrupt_manager_init() function. The Interrupt Manager
module is a dependency of the sl_main module and is added automatically to your
project if the sl_main module is selected.
Silicon Labs devices currently use 4 bits in the NVIC register to encode a priority;
therefore, the priority range is 0-15. This may vary by device. Check your device's
header file for the __NVIC_PRIO_BITS value.
The Interrupt manager module defines the following value:
Define | Default Value |
|---|---|
| 5 |
sl_interrupt_manager_init() will therefore initialize all interrupt priorities
(except fault handler) to SL_INTERRUPT_MANAGER_DEFAULT_PRIORITY. Since the default
priority is lower than the BASEPRI value used in atomic sections, every
interrupt is disabled by default during an atomic section. When the interrupt manager service
initialization has finished, it is up to your application to call sl_interrupt_manager_set_irq_priority()
to change the default priority of an Interrupt Handler according to its needs.
For 4-bit priority devices (priority range 0-15):
Priority < BASEPRI: Non-SiSDK-aware. Not masked by atomic sections. Cannot call any SiSDK APIs.
Priority = BASEPRI: Threshold for atomic sections (SiSDK protection boundary).
Priority > BASEPRI: SiSDK-aware. Masked by atomic sections. Can safely call SiSDK APIs.
Default BASEPRI: Currently 3 (configurable, may change in future SDK versions).
Default interrupt priority: 5 (
SL_INTERRUPT_MANAGER_DEFAULT_PRIORITY).PendSV priority: 15 (lowest, used for kernel context switching when using RTOS).
Any modification to an interrupt priority should be done carefully since it can introduce behavior changes in your application difficult to pinpoint.
Common use case - NVM3 and flash operations: When using NVM3 for non-volatile storage, flash page erase and re-packing operations make the flash memory temporarily unreadable, introducing latency of several milliseconds. During this time, any code or data located in flash cannot be accessed. If you have time-critical interrupts that must execute without being affected by this flash latency:
Configure the interrupt with priority higher than BASEPRI (lower numerical value). All interrupts above BASEPRI must have their handlers in RAM.
Place the interrupt handler code in RAM using
SL_CODE_RAM.Ensure any data accessed by the handler is also in RAM.
Do NOT call any SiSDK APIs from the handler.
Note that ALL interrupt handlers configured with priority higher than BASEPRI must have their code in RAM, as any of them could be triggered during flash operations. It is essential to ensure every non-SiSDK-aware interrupt handler is properly placed in RAM to avoid execution failures.
Interrupts configured this way will not be masked by atomic sections and will execute immediately even during NVM3 re-packing, with no flash access latency.
Memory considerations#
The interrupt manager can relocate the interrupt vector table in RAM and switch the interrupt
vector table base address of the core to point to it. This can be achieved by adding the
component Interrupt Manager: Interrupt Vector Table in RAM to your project.
The interrupt vector table is by default located in RAM for Series 3 devices that use external
flash because the latency of the external FLASH can slow down the execution if the instruction
cache misses. For this reason, it is also recommended to place your interrupt routines and
time-critical code in RAM using the macro SL_CODE_RAM. The component ICACHE Disable can be
used to disable the instruction cache in testing. This can help to identify what code fails when
a cache miss happens and would need to be put in RAM.
Example of a high-priority interrupt handler in RAM:
#include "sl_interrupt_manager.h"
#include "em_device.h"
// Place ISR in RAM to execute during flash operations
SL_CODE_RAM void TIMER0_IRQHandler(void)
{
uint32_t flags = TIMER0->IF;
TIMER0->IF_CLR = flags; // Clear flags immediately
// Handle time-critical interrupt (no SiSDK API calls)
// ... process interrupt ...
}
void setup_critical_timer(void)
{
// Set to priority 0 (not masked during flash erase/re-packing)
sl_interrupt_manager_set_irq_priority(TIMER0_IRQn, 0);
// Enable the interrupt
sl_interrupt_manager_enable_irq(TIMER0_IRQn);
}