nrf-softdevice

repository·master·Indexed 43 days ago

https://github.com/embassy-rs/nrf-softdevice

High-level, async/await Rust bindings for Nordic Semiconductor's proprietary SoftDevice Bluetooth stacks. Supports SoftDevice models s112, s113, s122, s132, and s140 across various nRF52 chips. Provides attribute macros like #[gatt_server], #[gatt_service], and #[gatt_client] for defining BLE GATT structures, as well as low-level raw bindings via nrf-softdevice-s1xx.

Tokens
15.1K
Snippets
46
Records
63
Agent score
56%

What's inside nrf-softdevice

  1. Manage critical sections to avoid SoftDevice assertion failures

    master

    The SoftDevice reserves certain interrupts. Using standard methods like cortex_m::interrupt::free to disable all interrupts will cause the SoftDevice to fail with "assertion failed" errors.

    To safely implement critical sections:

    1. Enable the critical-section-impl feature in nrf-softdevice.
    2. Use the critical-section crate's critical_section::with instead of cortex_m::interrupt::free.
    3. Use embassy_sync::blocking_mutex::CriticalSectionMutex instead of cortex_m::interrupt::Mutex.
    4. Ensure no third-party libraries you use internally call cortex_m::interrupt::free.
  2. Understand low-level raw SoftDevice bindings

    master

    The nrf-softdevice-s1xx crates provide low-level, 1-1 bindings to the SoftDevice C headers. These are generated via bindgen and use inline assembly (svc instructions) to minimize overhead.

    These bindings are intended for low-level access and match the C header signatures closely, typically wrapping a single svc call.

    #[inline(always)]
    pub unsafe fn sd_ble_gap_connect(
          p_peer_addr: *const ble_gap_addr_t,
          p_scan_params: *const ble_gap_scan_params_t,
          p_conn_params: *const ble_gap_conn_params_t,
          conn_cfg_tag: u8,
    ) -> u32 {
        let ret: u32;
        core::arch::asm!("svc 140",
            inout("r0") p_peer_addr => res,
            inout("r1") p_scan_params => _,
            inout("r2") p_conn_params => _,
            inout("r3") conn_cfg_tag => _,
            lateout("r12") _,
        );
        ret
    }
  3. Flash the SoftDevice to your hardware

    master

    The SoftDevice is a closed-source binary that must be flashed to the device separately from your application. This is a one-time operation (or required after a full chip erase).

    Using probe-rs:

    1. Erase flash: probe-rs erase --chip nrf52840_xxAA (use --allow-erase-all if necessary).
    2. Flash SoftDevice: probe-rs download --verify --binary-format hex --chip nRF52840_xxAA <SOFTDEVICE_FILE>.hex

    Using nrfjprog: Flash the SoftDevice with: nrfjprog --family NRF52 --chiperase --verify --program <SOFTDEVICE_FILE>.hex

    # Example using probe-rs
    probe-rs erase --chip nrf52840_xxAA
    probe-rs download --verify --binary-format hex --chip nRF52840_xxAA s140_nrf52_7.X.X_softdevice.hex
    
    # Example using nrfjprog
    nrfjprog --family NRF52 --chiperase --verify --program s140_nrf52_7.0.1_softdevice.hex
  4. Configure nrf-softdevice Cargo features

    master

    To use the nrf-softdevice crate, you must enable exactly one SoftDevice model feature and exactly one supported nRF chip model feature in your Cargo.toml.

    Supported SoftDevices:

    • s112 (peripheral only)
    • s113 (peripheral only)
    • s122 (central only)
    • s132 (central and peripheral)
    • s140 (central and peripheral)

    Supported nRF Chips:

    • nrf52805, nrf52810, nrf52811, nrf52820, nrf52832, nrf52833, nrf52840.
  5. Set interrupt priorities to avoid SoftDevice conflicts

    master

    The SoftDevice reserves interrupt priority levels 0, 1, and 4. You must never use these levels for your application interrupts. Since the default priority is often 0, you must explicitly set priorities for every interrupt you enable.

    Setting priority for a peripheral:

    use embassy_nrf::interrupt::{self, InterruptExt};
    
    interrupt::SPIM3.set_priority(interrupt::Priority::P3);

    Configuring Embassy-nrf priorities: If using embassy-nrf with gpiote or time-driver-rtc1, you must adjust the embassy_nrf::config::Config to use non-reserved priorities (e.g., P2):

    let mut config = embassy_nrf::config::Config::default();
    config.gpiote_interrupt_priority = Priority::P2;
    config.time_interrupt_priority = Priority::P2;
    let peripherals = embassy_nrf::init(config);
    use embassy_nrf::interrupt::{self, InterruptExt};
    
    interrupt::SPIM3.set_priority(interrupt::Priority::P3);
    let mut spim = spim::Spim::new(p.SPI3, Irqs, p.P0_13, p.P0_16, p.P0_15, config);
  6. Configure memory.x for SoftDevice flash and RAM offsets

    master

    Because the SoftDevice occupies the bottom of the flash and specific RAM regions, you must adjust your memory.x file to move your application's start address and reduce the available length.

    1. Determine the SoftDevice size (e.g., S132 v7.3 uses 152K / 0x26000).
    2. Set FLASH.ORIGIN to the SoftDevice size and FLASH.LENGTH to Total Flash - SoftDevice Size.
    3. Set RAM.ORIGIN to the SoftDevice RAM end address and RAM.LENGTH to Total RAM - SoftDevice RAM used.

    Example for NRF52832 with S132 7.3.0:

    MEMORY
    {
      FLASH : ORIGIN = 0x00000000 + 152K, LENGTH = 512K - 152K
      RAM : ORIGIN = 0x20000000 + 44K, LENGTH = 64K - 44K
    }
    MEMORY
    {
      /* NOTE 1 K = 1 KiBi = 1024 bytes */
      /* These values correspond to the NRF52832 with SoftDevices S132 7.3.0 */
      FLASH : ORIGIN = 0x00000000 + 152K, LENGTH = 512K - 152K
      RAM : ORIGIN = 0x20000000 + 44K, LENGTH = 64K - 44K
    }
  7. Implement a GATT Server

    master

    To create a GATT server, you must implement the Server trait. This trait allows you to handle various BLE events such as writes, deferred reads/writes, and notification completions. You then use the run function to start the server loop for a specific connection.

    Key methods to implement:

    • on_write: Handles standard characteristic writes.
    • on_deferred_read: Required if any characteristic has the deferred_read flag set.
    • on_deferred_write: Required if any characteristic has the deferred_write flag set.
    • on_notify_tx_complete: Callback for when notifications are successfully transmitted.
    • on_indicate_confirm: Callback for when an indication is confirmed by the client.
    impl Server for MyServer {
        type Event = MyEvent;
    
        fn on_write(&self, conn: &Connection, handle: u16, op: WriteOp, offset: usize, data: &[u8]) -> Option<Self::Event> {
            // Handle write logic
            None
        }
    
        // Implement other required methods...
    }
    
    // To run the server:
    // embassy_executor::Spawner::spawn(run(conn, &server, |event| {
    //     match event {
    //         MyEvent::SomeEvent => { ... }
    //     }
    // })).await;
  8. Implement a GATT Client using the `Client` trait

    master

    To interact with a GATT server, you must implement the Client trait. This trait defines how your application discovers services and handles asynchronous events like notifications and indications.

    Key methods to implement:

    • uuid(): Returns the Uuid of the service you want to discover.
    • new_undiscovered(conn): Creates a new instance of your client in an uninitialized state.
    • discovered_characteristic(&mut self, characteristic, descriptors): Called during the discovery process for every characteristic found. Use this to store handles for the characteristics you care about.
    • discovery_complete(&mut self): Called after discovery finishes. You must verify that all required characteristics were found; if not, return Err(DiscoverError::ServiceIncomplete).
    • on_hvx(&self, conn, type_, handle, data): Handles incoming HvxType::Notification or HvxType::Indication events. It returns an Option<Self::Event> which is then passed to your event loop.
    impl Client for MyGattClient {
        type Event = MyEvent;
    
        fn uuid() -> Uuid {
            Uuid::from_u16(0x180D) // e.g., Heart Rate Service
        }
    
        fn new_undiscovered(conn: Connection) -> Self {
            Self { conn, heart_rate_handle: None }
        }
    
        fn discovered_characteristic(&mut self, characteristic: &Characteristic, _descriptors: &[Descriptor]) {
            if characteristic.uuid == Some(Uuid::from_u16(0x2A37)) {
                self.heart_rate_handle = Some(characteristic.handle_value);
            }
        }
    
        fn discovery_complete(&mut self) -> Result<(), DiscoverError> {
            if self.heart_rate_handle.is_none() {
                return Err(DiscoverError::ServiceIncomplete);
            }
            Ok(())
        }
    
        fn on_hvx(&self, _conn: &Connection, type_: HvxType, handle: u16, data: &[u8]) -> Option<Self::Event> {
            if handle == self.heart_rate_handle.unwrap() {
                Some(MyEvent::HeartRateUpdate(data.to_vec()))
            } else {
                None
            }
        }
    }
  9. Select advertisement types

    master

    Choose between ConnectableAdvertisement and NonconnectableAdvertisement based on your device's role:

    ConnectableAdvertisement

    Used when you want to accept connections. Types include:

    • ScannableUndirected: Allows scanning for more data.
    • NonscannableDirected: Targeted at a specific Address.
    • Extended... variants: Available on s132 or s140 SoftDevices for extended advertising features.

    NonconnectableAdvertisement

    Used for broadcasting only. Types include:

    • ScannableUndirected: Allows scanning for more data.
    • NonscannableUndirected: Pure broadcast.
    • Extended... variants: Available on s132 or s140 for extended advertising.
  10. Configure BLE security using the SecurityHandler trait

    master

    To manage BLE security, bonding, and IO capabilities (like passkey entry or display), you must implement the SecurityHandler trait. This trait allows the SoftDevice to delegate security-related decisions and user interactions to your application code.

    Key Responsibilities

    1. IO Capabilities: Define what hardware your device has via io_capabilities(). This determines how the pairing process proceeds (e.g., DisplayOnly vs KeyboardOnly).
    2. Bonding: Implement can_bond() to allow key storage and on_bonded() to actually save the EncryptionInfo, MasterId, and IdentityKey provided by the SoftDevice.
    3. Passkey Handling: If your device has a display or keyboard, implement display_passkey() (to show a code) or enter_passkey() (to accept a code from a remote device).
    4. Key Management: Implement get_key() (for peripherals) or get_peripheral_key() (for centrals, requires ble-central feature) to retrieve stored LTKs for reconnection.
    5. GATT Server Integration: If using ble-gatt-server, implement save_sys_attrs() and load_sys_attrs() to persist GATTS system attributes associated with a bond.
    impl SecurityHandler for MySecurityManager {
        fn io_capabilities(&self) -> IoCapabilities {
            IoCapabilities::DisplayYesNo
        }
    
        fn can_bond(&self, _conn: &Connection) -> bool {
            true
        }
    
        fn on_bonded(&self, _conn: &Connection, master_id: MasterId, key: EncryptionInfo, peer_id: IdentityKey) {
            // TODO: Save key and IDs to flash/storage
        }
    
        fn display_passkey(&self, passkey: &[u8; 6]) {
            // TODO: Show passkey on your device's screen
        }
    }
  11. Configure nrf-softdevice features

    master

    The nrf-softdevice crate requires specific feature flags to be enabled in your Cargo.toml to match your hardware and desired functionality. If you do not provide a valid combination, the crate will fail to compile.

    Required Feature Groups

    1. BLE Role: You must activate at least one of:

      • ble-central
      • ble-peripheral
      • ble-l2cap (Note: availability depends on the SoftDevice)
    2. SoftDevice Version: You must activate exactly one of the following:

      • s112
      • s113
      • s122
      • s132
      • s140
    3. Chip Model: You must activate exactly one of the following:

      • nrf52805
      • nrf52810
      • nrf52811
      • nrf52820
      • nrf52832
      • nrf52833
      • nrf52840

    Compatibility Matrix

    The following table shows which SoftDevice versions support which BLE roles across different chips:

    SoftDeviceCentralPeripheralL2CAP-CoCSupported Chips
    s112XnRF52805, 52810, 52811, 52820, 52832, 52833
    s113XXnRF52805, 52810, 52811, 52820, 52832, 52833, 52840
    s122XnRF52820, 52833
    s132XXnRF52811, 52832, 52840
    s140XXnRF52832, 52833, 52840

    Note: 'X' indicates support. If you select a combination not listed in the matrix, the crate will trigger a compile_error!.