
# [nano RTOS](/) / [nordic](.) / softdevice

### Integration of Nordic SoftDevice into a BLE UART driver for nanoRTOS

#### Preface
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.

#### Environment
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](https://www.nordicsemi.com/Products/Development-software/nRF5-SDK) 17.1.0 and [SoftDevice s140](https://www.nordicsemi.com/Products/Development-software/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.

#### Memory layout and boot
The first goal was to start nanoRTOS on a [nRF52840dk](https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dk) 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.

#### Interrupt Vector Table
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.

#### Routing of SVC calls
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.

#### Peripheral protection
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:

https://docs.nordicsemi.com/bundle/sds_s140/page/SDS/s1xx/sd_resource_reqs/hw_block_interrupt_vector.html

The following peripherals are monitored:  
`CLOCK/POWER, RADIO, TIMER0, RTC0, TEMP, RNG, ECB, AAR/CCM, EGU5/SWI5, ACL/NVMC`

#### System timer
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.

#### NVMC and non-volatile memory
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.

#### BLE UART
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.

#### Boot: 2-way BLE-UART bridge for nRF52840 on Thingy:91
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}
```

#### nanoRTOS guidelines for UART
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.

#### Data event handler
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.

#### Link state events
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.

#### Init BLE stack
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.
``` C
// ***************************************************************************
// 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_EVENTS
	int_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;
}
```

#### Memory layout
``` Make
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
```

#### Redirect NRF\_LOG to STDERR
``` C
// 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, ...);
```

#### Headers included by the BLE UART driver
``` C
#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>
```

#### Include paths
``` Makefile
# 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
```

#### Source files
``` Makefile
$(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
```

#### nRF SDK susbset used
```
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
```

#### Linker script sections
``` ld
.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
```


#### © 2022-2026 Georgi Valkov
https://httpstorm.com/  
https://nanortos.com/  
<keyfunc>
	<span>g</span><span>v</span><span>a</span><span>l</span><span>k</span><span>o</span><span>v</span><span>&#64;</span><span>g</span><span>m</span><span>a</span><span>i</span><span>l</span><span>&#46;</span><span>c</span><span>o</span><span>m</span>
</keyfunc>
