This article describes the process of using the SoftDevice outside of nRF SDK. It covers sharing of interrupts, SVC calls and peripherals between OS and SoftDevice.
I should start by saying that nanoRTOS is extremely flexible which allowed me to perform integrations in a very clean way, that would otherwise not be possible. I started with nRF5 SDK 17.1.0 and SoftDevice s140 7.3.0. After getting familiar with the examples and testing them, I settled on examples/ble_peripheral/ble_app_uart
as a good starting point.
The first goal was to start nanoRTOS on a nRF52840dk board with SoftDevice programmed. This version of SoftDevice reserves the following memory ranges: RAM 0x20000000-0x20002ae8
and FLASH 0x00000000-0x00027000
. The application is expected to start with a vector table at address 0x00027000
. So I moved all FLASH and RAM addresses to where the SoftDevice expects them to be. And the system started. As a good measure I analysed the SoftDevice boot. Every detail is important when integrating an external module into the OS, because the roles change: in the SDK, the SoftDevice is a host and the app relays on it. In nanoRTOS, the OS owns all hardware and the SoftDevice is a guest. I don't have the source, but disassembly and monitoring the special function registers allowed me to achieve a pretty good integration. I used that a lot.
By default nanoRTOS relocates its vector table to RAM. This allows drivers and apps to configure and handle any interrupt source. To support the SoftDevice, all unused interrupts are replaced with the corresponding handler found in the SoftDevice vector table at address 0x00000000
. This essentially creates a zero-latency pass through where the CPU jumps directly to the SoftDevice handler.
nanoRTOS uses the SVC
interrupt to perform context switch and other synchronous services, so these calls must be handled directly by the operating system. For flexibility, unused SVC
calls can be forwarded transparently to an external handler, such as the SoftDevice. This is configured during board init.
When the SoftDevice is enabled, the Memory Watch Unit MWU
interrupt is configured with a higher priority than SVC
. MWU is configured to monitor access to protected peripherals. If access to a protected peripheral is detected from code outside the SoftDevice region, the MWU handler will crash the system by calling SVC 254
. Other parts of the SoftDevice use SVC 255
when a RADIO
interrupt is not handled in time. SVC calls from a high priority interrupt are not possible, because they are synchronous and need to preempt the context which triggered them, so the CPU enters a hard-fault. This design is intended to discourage user apps from accessing peripherals in use by the SoftDevice, and prevent undefined behaviour. More on that here:
The following peripherals are monitored:
CLOCK/POWER, RADIO, TIMER0, RTC0, TEMP, RNG, ECB, AAR/CCM, EGU5/SWI5, ACL/NVMC
In order for the system to keep an accurate timestamp and wake tasks with microsecond precision, while also being power efficient, a so called tick-less timer is used. Instead of waking frequently on fixed time intervals, nanoRTOS wakes only when a task needs to continue. On Nordic CPUs however, SysTick stops during power save (WFE
, WFI
), so a backup time source is needed during sleep. This can be any available RTC
or TIMER
. It is crucial to know exactly when power save begins and ends, in order to keep the timestamp accurate. This is achieved using the POWER
peripheral, which can use the Programmable Peripheral Interconnect PPI
to start and stop the backup timer and then notify the operating system to adjust the timestamp. Due to its critical role in time management, the POWER
peripheral is reconfigured after SoftDevice startup and nanoRTOS takes control of it. Care is taken to not interfere with any SoftDevice operation. MWU
protection is removed for this peripheral.
While FLASH writes are short enough and don't interfere with the RADIO, erasing a FLASH page is a lengthy process that would disrupt communication even when partial erase is performed with the shortest interval of 1ms. For optimal performance, FLASH writes are direct. Erase requests use SoftDevice API when the SoftDevice is enabled and fallback to direct NVMC
access otherwise. MWU
protection is removed for this peripheral.
With the environment all set for nanoRTOS and the SoftDeivce to coexist, each having direct access to its own resources, it's time to wrap it up as a UART driver. Why UART? It's the base for all communications. UARTs can be encapsulated as IO streams, so read
, write
and printf
can use them. The logging system starts pre-boot and is safe to use in any context.
The examples/ble_peripheral/ble_app_uart
app is as a good starting point. I copied sdk_config.h
from pca10056/s140/config
to the nrf52840
board directory, and defined any required compiler macros. nrf_log.h
is also copied to the board directory and modified to use stderr
from the nanoRTOS logging system. Next, starting with an empty source file for the driver, I added all includes from main.c
. The compiler provides guidance for any missing dependencies. Once all required headers and paths were set, the empty driver compiled successfully. Step by step, I added all required init and driver code, along with some source files. To keep things clean, any unused code and headers were removed. Some linker script sections had to be added too. And it works.
The following startup and bridge messages were sent over BLE. The sensor data is from nRF9160.
# nrf52840 starting... # nano RTOS (C) 2022-2024 nanortos.com # BLE to STD IN/OUT bridge example STD 16 ALT 0 adxl362={ "x" : - 6 . 4 , "y" : - 1 . 9 , "z" : - 6 . 6 , "t" : 20 . 7 } adxl362={ "x" : - 6 . 1 , "y" : - 1 . 9 , "z" : - 6 . 5 , "t" : 21 . 1 } adxl362={ "x" : - 6 . 1 , "y" : - 1 . 9 , "z" : - 6 . 8 , "t" : 19 . 5 } bh1749={ "r" : 9 , "g" : 9 , "b" : 6 , "i" : 14 } battery={ "v" : 3600 , "l" : 3 , "c" : 0 } bme680={ "temp" : 21 . 917 , "pressure" : 100072 , "humidity" : 58 . 521 , "gas" : 810247 }
All IO requests are background synchronous. This combines the simplicity of synchronous code with the performance of asynchronous background activities. A receive request returns immediately if data is available, otherwise it blocks, and the driver will signal the task to continue, when one or more bytes are received. A send request queues all data and returns immediately. It blocks only when the queue is full, in which case the driver will notify the task when it can continue. The driver sends and receives data in the background, meanwhile the task is free to perform more work and IO requests. All API is available in pre-boot and interrupt context. As a result, messages sent before nanoRTOS starts or sent from an interrupt context are guaranteed to be delivered to a BLE client, even if no client is connected at the time the message was sent. For packet devices such as BLE, printf
messages are sent in one packet for improved performance.
Just like any UART interrupt handler, the NUS data handler runs in interrupt context and notifies when data has been sent or received. BLE_NUS_EVT_TX_RDY
is a good time to continue sending in the background, and in the rare event that a task had been blocked due to TX queue full, it is signalled to continue. Usually sending tasks are not blocked when a send fails with NRF_ERROR_RESOURCES
. If all data has been queued, it will be sent in the background, so blocking is not needed. On BLE_NUS_EVT_RX_DATA
, if a task is waiting to receive one or more bytes, they are consumed (from the receive queue and incoming packet), and the task is signalled to continue. Any remaining data is saved to the receive queue.
When a BLE client enables notifications, a BLE_NUS_EVT_COMM_STARTED
event is used to update the link state to online
, while BLE_NUS_EVT_COMM_STOPPED
notifies that we can no longer send data offline
, either because the client disconnected or disabled notifications. When offline, data can only be queued, and not sent. If the send queue is full, some of the old data is dropped and replaced with new data. Send calls never block in offline mode, since this would stall other activities indefinitely.
Since we are building outside of the nRF SDK build environment, some interrupt handlers have to be installed manually. We also need to disable the CLOCK/POWER
interrupt, because the SoftDevice checks if it is enabled and will trigger a crash. Its state is restored later. The priority of RADIO
and MWU
interrupts is decreased to ensure we can handle sleep exit events first, and keep the system timestamp accurate. SOC observers are defined in nrf_sdh_soc.c
and used to receive FLASH erase events. Since all symbols in this module are declared static
and cannot be referenced externally, the compiler will optimise it out. To workaround this, a dummy variable nrf_sdh_soc_ref
is added to the module and referenced here.
// *************************************************************************** // Function: nrf_sd_init_stack // Description: initialise the SoftDevice // Parameters: - // Return values: - // Comments: initialise the SoftDevice and the BLE event interrupt // Context: any // *************************************************************************** static ret_code_t nrf_sd_init_stack( void ) { // install interrupt handlers int_set_vector ( SWI0_EGU0_IRQn, SWI0_EGU0_IRQHandler); int_set_vector ( SWI2_EGU2_IRQn, SWI2_EGU2_IRQHandler); #if BYPASS_MWU_EVENTSint_set_vector ( MWU_IRQn, mwu_handler_raw); #endif // disable clock/power interrupt, save priority int_state_t int_state= 0 ; uint8_t int_prio= 0 ; int_source_disable_nested ( POWER_CLOCK_IRQn, & int_state); int_source_get_priority ( POWER_CLOCK_IRQn, & int_prio); ret_code_t err_code= nrf_sdh_enable_request(); if ( err_code != NRF_SUCCESS) { BLE_ERROR_HANDLER( err_code); return err_code; } // configure the BLE stack using the default settings. // fetch the start address of the application RAM. uint32_t ram_start= 0 ; err_code= nrf_sdh_ble_default_cfg_set( APP_BLE_CONN_CFG_TAG, & ram_start); if ( err_code != NRF_SUCCESS) { BLE_ERROR_HANDLER( err_code); return err_code; } // enable BLE stack err_code= nrf_sdh_ble_enable(& ram_start); if ( err_code != NRF_SUCCESS) { BLE_ERROR_HANDLER( err_code); return err_code; } // register a handler for BLE events NRF_SDH_BLE_OBSERVER( m_ble_observer, APP_BLE_OBSERVER_PRIO, nrf_sd_ble_evt_handler, NULL ); NRF_SDH_SOC_OBSERVER( m_soc_observer, APP_SOC_OBSERVER_PRIO, nrf_sd_sys_evt_handler, NULL ); // force the magic section from nrf_sdh_soc.c to be included // otherwise the module is excluded from the firmware and // SOC events do not fire extern volatile const uint32_t nrf_sdh_soc_ref; ( void ) nrf_sdh_soc_ref; // restore clock/power interrupt, and priority int_source_set_priority ( MWU_IRQn, 0x4 ); int_source_set_priority ( RADIO_IRQn, 0x2 ); int_source_set_priority ( POWER_CLOCK_IRQn, int_prio); int_source_enable_nested ( POWER_CLOCK_IRQn, int_state); return err_code; }
ifeq ($( VARIANT_SOFTDEVICE ), 0) # memory layout FLASH ROM_START:= 0x00000000 RAM_START:= 0x20000000 RAM_SIZE:= 0x00040000 ROM_SIZE:= 0x00100000 RAM_END:= 0x20040000 ROM_END:= $( ROM_SIZE) else # memory layout with softdevice ROM_START:= 0x00027000 RAM_START:= 0x20002ae8 RAM_SIZE:= 0x0003d518 ROM_SIZE:= 0x000d9000 RAM_END:= 0x20040000 ROM_END:= 0x00100000 endif
// nrf_log.h #ifndef STDERR_FILENO #define STDERR_FILENO 2 // standard error file descriptor #endif #define eprintf (...) dprintf ( STDERR_FILENO , __VA_ARGS__ ) // when SD is not set during build, the SoftDevice is diabled // positive values enable the SoftDevice and specify the log level // make SD=1 enables logging of errors // make SD=2 enables logging of warnings, and so on #if VARIANT_SOFTDEVICE >= 1 #define NRF_LOG_ERROR(...) eprintf ( __VA_ARGS__ ); eprintf ( "\n\n" ) #else #define NRF_LOG_ERROR(...) //- NRF_LOG_INTERNAL_ERROR(__VA_ARGS__) #endif #if VARIANT_SOFTDEVICE >= 2 #define NRF_LOG_WARNING(...) eprintf ( __VA_ARGS__ ); eprintf ( "\n" ) #else #define NRF_LOG_WARNING(...) //- NRF_LOG_INTERNAL_WARNING(__VA_ARGS__) #endif …int printf ( const char * restrict format, ...); int dprintf ( int fd, const char * restrict format, ...);
#include < nordic_common. h> #include < nrf. h> #include < ble_hci. h> #include < ble_advdata. h> #include < ble_advertising. h> #include < ble_conn_params. h> #include < nrf_sdh. h> #include < nrf_sdh_soc. h> #include < nrf_sdh_ble. h> #include < nrf_ble_gatt. h> #include < nrf_ble_qwr. h> #include < app_timer. h> #include < ble_nus. h> #include < nrf_log. h> #include < nrfx_rtc. h> #include < nrfx_swi. h> // advice to SDK creators: use relative include paths // for better portability and readability, example #include < modules/ nrfx/ drivers/ include/ nrfx_swi. h>
# otherwise a lot of include paths have to be added to the compiler $( ROOT)/ include/ arch/ nordic$( PATH_NORDIC)/ components/ ble/ ble_advertising$( PATH_NORDIC)/ components/ ble/ ble_link_ctx_manager$( PATH_NORDIC)/ components/ ble/ ble_services/ ble_nus$( PATH_NORDIC)/ components/ ble/ common$( PATH_NORDIC)/ components/ ble/ nrf_ble_gatt$( PATH_NORDIC)/ components/ ble/ nrf_ble_qwr$( PATH_NORDIC)/ components/ libraries/ atomic$( PATH_NORDIC)/ components/ libraries/ atomic_flags$( PATH_NORDIC)/ components/ libraries/ balloc$( PATH_NORDIC)/ components/ libraries/ delay$( PATH_NORDIC)/ components/ libraries/ experimental_section_vars$( PATH_NORDIC)/ components/ libraries/ log$( PATH_NORDIC)/ components/ libraries/ log/ src$( PATH_NORDIC)/ components/ libraries/ memobj$( PATH_NORDIC)/ components/ libraries/ mutex$( PATH_NORDIC)/ components/ libraries/ strerror$( PATH_NORDIC)/ components/ libraries/ timer$( PATH_NORDIC)/ components/ libraries/ util$( PATH_NORDIC)/ components/ softdevice/ common$( PATH_NORDIC)/ components/ softdevice/ s140/ headers$( PATH_NORDIC)/ components/ softdevice/ s140/ headers/ nrf52$( PATH_NORDIC)/ integration/ nrfx$( PATH_NORDIC)/ integration/ nrfx/ legacy$( PATH_NORDIC)/ modules/ nrfx$( PATH_NORDIC)/ modules/ nrfx/ drivers$( PATH_NORDIC)/ modules/ nrfx/ drivers/ include
$( PATH_NORDIC)/ components/ ble/ ble_advertising/ ble_advertising. c$( PATH_NORDIC)/ components/ ble/ ble_link_ctx_manager/ ble_link_ctx_manager. c$( PATH_NORDIC)/ components/ ble/ ble_services/ ble_nus/ ble_nus. c$( PATH_NORDIC)/ components/ ble/ common/ ble_advdata. c$( PATH_NORDIC)/ components/ ble/ common/ ble_conn_params. c$( PATH_NORDIC)/ components/ ble/ common/ ble_conn_state. c$( PATH_NORDIC)/ components/ ble/ common/ ble_srv_common. c$( PATH_NORDIC)/ components/ ble/ nrf_ble_gatt/ nrf_ble_gatt. c$( PATH_NORDIC)/ components/ ble/ nrf_ble_qwr/ nrf_ble_qwr. c$( PATH_NORDIC)/ components/ libraries/ atomic/ nrf_atomic. c$( PATH_NORDIC)/ components/ libraries/ atomic_flags/ nrf_atflags. c$( PATH_NORDIC)/ components/ libraries/ experimental_section_vars/ nrf_section_iter. c$( PATH_NORDIC)/ components/ libraries/ strerror/ nrf_strerror. c$( PATH_NORDIC)/ components/ libraries/ timer/ app_timer. c$( PATH_NORDIC)/ components/ libraries/ util/ app_error. c$( PATH_NORDIC)/ components/ libraries/ util/ app_error_weak. c$( PATH_NORDIC)/ components/ libraries/ util/ app_error_handler_gcc. c$( PATH_NORDIC)/ components/ libraries/ util/ app_util_platform. c$( PATH_NORDIC)/ components/ softdevice/ common/ nrf_sdh. c$( PATH_NORDIC)/ components/ softdevice/ common/ nrf_sdh_ble. c$( PATH_NORDIC)/ components/ softdevice/ common/ nrf_sdh_soc. c$( PATH_NORDIC)/ modules/ nrfx/ drivers/ src/ nrfx_swi. c
src/ sdk/ nordic ├── components │ ├── ble │ │ ├── ble_advertising │ │ │ ├── ble_advertising. c │ │ │ └── ble_advertising. h │ │ ├── ble_link_ctx_manager │ │ │ ├── ble_link_ctx_manager. c │ │ │ └── ble_link_ctx_manager. h │ │ ├── ble_services │ │ │ └── ble_nus │ │ │ ├── ble_nus. c │ │ │ └── ble_nus. h │ │ ├── common │ │ │ ├── ble_advdata. c │ │ │ ├── ble_advdata. h │ │ │ ├── ble_conn_params. c │ │ │ ├── ble_conn_params. h │ │ │ ├── ble_conn_state. c │ │ │ ├── ble_conn_state. h │ │ │ ├── ble_srv_common. c │ │ │ └── ble_srv_common. h │ │ ├── nrf_ble_gatt │ │ │ ├── nrf_ble_gatt. c │ │ │ └── nrf_ble_gatt. h │ │ └── nrf_ble_qwr │ │ ├── nrf_ble_qwr. c │ │ └── nrf_ble_qwr. h │ ├── libraries │ │ ├── atomic │ │ │ ├── nrf_atomic. c │ │ │ ├── nrf_atomic. h │ │ │ └── nrf_atomic_internal. h │ │ ├── atomic_flags │ │ │ ├── nrf_atflags. c │ │ │ └── nrf_atflags. h │ │ ├── balloc │ │ │ └── nrf_balloc. h │ │ ├── delay │ │ │ └── nrf_delay. h │ │ ├── experimental_section_vars │ │ │ ├── nrf_section. h │ │ │ ├── nrf_section_iter. c │ │ │ └── nrf_section_iter. h │ │ ├── log │ │ │ ├── nrf_log_backend_interface. h │ │ │ ├── nrf_log_ctrl. h │ │ │ ├── nrf_log_instance. h │ │ │ ├── nrf_log_types. h │ │ │ └── src │ │ │ └── nrf_log_ctrl_internal. h │ │ ├── memobj │ │ │ └── nrf_memobj. h │ │ ├── mutex │ │ │ └── nrf_mtx. h │ │ ├── strerror │ │ │ ├── nrf_strerror. c │ │ │ └── nrf_strerror. h │ │ ├── timer │ │ │ ├── app_timer. c │ │ │ └── app_timer. h │ │ └── util │ │ ├── app_error. c │ │ ├── app_error. h │ │ ├── app_error_handler_gcc. c │ │ ├── app_error_weak. c │ │ ├── app_error_weak. h │ │ ├── app_util. h │ │ ├── app_util_platform. c │ │ ├── app_util_platform. h │ │ ├── nordic_common. h │ │ ├── nrf_assert. h │ │ ├── sdk_common. h │ │ ├── sdk_errors. h │ │ ├── sdk_macros. h │ │ ├── sdk_os. h │ │ └── sdk_resources. h │ └── softdevice │ ├── common │ │ ├── nrf_sdh. c │ │ ├── nrf_sdh. h │ │ ├── nrf_sdh_ble. c │ │ ├── nrf_sdh_ble. h │ │ ├── nrf_sdh_soc. c │ │ └── nrf_sdh_soc. h │ └── s140 │ └── headers │ ├── ble. h │ ├── ble_err. h │ ├── ble_gap. h │ ├── ble_gatt. h │ ├── ble_gattc. h │ ├── ble_gatts. h │ ├── ble_hci. h │ ├── ble_l2cap. h │ ├── ble_ranges. h │ ├── ble_types. h │ ├── nrf52 │ │ └── nrf_mbr. h │ ├── nrf_error. h │ ├── nrf_error_sdm. h │ ├── nrf_error_soc. h │ ├── nrf_nvic. h │ ├── nrf_sd_def. h │ ├── nrf_sdm. h │ ├── nrf_soc. h │ └── nrf_svc. h ├── integration │ └── nrfx │ ├── legacy │ │ └── apply_old_config. h │ ├── nrfx_config. h │ └── nrfx_glue. h └── modules └── nrfx ├── drivers │ ├──include │ │ ├── nrfx_rtc. h │ │ └── nrfx_swi. h │ ├── nrfx_common. h │ ├── nrfx_errors. h │ └── src │ └── nrfx_swi. c ├── hal │ └── nrf_rtc. h ├── nrfx. h └── soc ├── nrfx_atomic. h ├── nrfx_coredep. h ├── nrfx_irqs. h └── nrfx_irqs_nrf52840. h
. sdh_ble_observers _endinit: { PROVIDE ( __start_sdh_ble_observers= .); KEEP (*( SORT (. sdh_ble_observers*))) PROVIDE ( __stop_sdh_ble_observers= .); } > flash. sdh_soc_observers: { PROVIDE ( __start_sdh_soc_observers= .); KEEP (*( SORT (. sdh_soc_observers*))) PROVIDE ( __stop_sdh_soc_observers= .); } > flash. sdh_req_observers: { PROVIDE ( __start_sdh_req_observers= .); KEEP (*( SORT (. sdh_req_observers*))) PROVIDE ( __stop_sdh_req_observers= .); } > flash. sdh_state_observers: { PROVIDE ( __start_sdh_state_observers= .); KEEP (*( SORT (. sdh_state_observers*))) PROVIDE ( __stop_sdh_state_observers= .); } > flash. sdh_stack_observers: { PROVIDE ( __start_sdh_stack_observers= .); KEEP (*( SORT (. sdh_stack_observers*))) PROVIDE ( __stop_sdh_stack_observers= .); } > flash. pwr_mgmt_data: { PROVIDE ( __start_pwr_mgmt_data= .); KEEP (*( SORT (. pwr_mgmt_data*))) PROVIDE ( __stop_pwr_mgmt_data= .); } > flash
https://httpstorm.com/
https://nanortos.com/