diff --git a/embassy-nrf/src/chips/nrf52840.rs b/embassy-nrf/src/chips/nrf52840.rs index 51c55cd4d..d3272b2e8 100644 --- a/embassy-nrf/src/chips/nrf52840.rs +++ b/embassy-nrf/src/chips/nrf52840.rs @@ -173,6 +173,9 @@ embassy_hal_internal::peripherals! { // I2S I2S, + + // Radio + RADIO, } impl_usb!(USBD, USBD, USBD); @@ -311,6 +314,8 @@ impl_saadc_input!(P0_31, ANALOG_INPUT7); impl_i2s!(I2S, I2S, I2S); +impl_radio!(RADIO, RADIO, RADIO); + embassy_hal_internal::interrupt_mod!( POWER_CLOCK, RADIO, diff --git a/embassy-nrf/src/lib.rs b/embassy-nrf/src/lib.rs index 358a7cc27..04a6293a4 100644 --- a/embassy-nrf/src/lib.rs +++ b/embassy-nrf/src/lib.rs @@ -45,6 +45,11 @@ pub mod buffered_uarte; pub mod gpio; #[cfg(feature = "gpiote")] pub mod gpiote; + +// TODO: tested on other chips +#[cfg(any(feature = "nrf52840"))] +pub mod radio; + #[cfg(any(feature = "nrf52832", feature = "nrf52833", feature = "nrf52840"))] pub mod i2s; pub mod nvmc; diff --git a/embassy-nrf/src/radio/ble.rs b/embassy-nrf/src/radio/ble.rs new file mode 100644 index 000000000..24dba582f --- /dev/null +++ b/embassy-nrf/src/radio/ble.rs @@ -0,0 +1,438 @@ +//! Radio driver implementation focused on Bluetooth Low-Energy transmission. + +use core::future::poll_fn; +use core::sync::atomic::{compiler_fence, Ordering}; +use core::task::Poll; + +use embassy_hal_internal::drop::OnDrop; +use embassy_hal_internal::{into_ref, PeripheralRef}; +pub use pac::radio::mode::MODE_A as Mode; +use pac::radio::pcnf0::PLEN_A as PreambleLength; +use pac::radio::state::STATE_A as RadioState; +pub use pac::radio::txpower::TXPOWER_A as TxPower; + +use crate::interrupt::typelevel::Interrupt; +use crate::radio::*; +use crate::util::slice_in_ram_or; + +/// RADIO error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Error { + /// Buffer was too long. + BufferTooLong, + /// Buffer was to short. + BufferTooShort, + /// The buffer is not in data RAM. It is most likely in flash, and nRF's DMA cannot access flash. + BufferNotInRAM, +} + +/// Radio driver. +pub struct Radio<'d, T: Instance> { + _p: PeripheralRef<'d, T>, +} + +impl<'d, T: Instance> Radio<'d, T> { + /// Create a new radio driver. + pub fn new( + radio: impl Peripheral

+ 'd, + _irq: impl interrupt::typelevel::Binding> + 'd, + ) -> Self { + into_ref!(radio); + + let r = T::regs(); + + r.pcnf1.write(|w| unsafe { + // It is 0 bytes long in a standard BLE packet + w.statlen() + .bits(0) + // MaxLen configures the maximum packet payload plus add-on size in + // number of bytes that can be transmitted or received by the RADIO. This feature can be used to ensure + // that the RADIO does not overwrite, or read beyond, the RAM assigned to the packet payload. This means + // that if the packet payload length defined by PCNF1.STATLEN and the LENGTH field in the packet specifies a + // packet larger than MAXLEN, the payload will be truncated at MAXLEN + // + // To simplify the implementation, It is setted as the maximum value + // and the length of the packet is controlled only by the LENGTH field in the packet + .maxlen() + .bits(255) + // Configure the length of the address field in the packet + // The prefix after the address fields is always appended, so is always 1 byte less than the size of the address + // The base address is truncated from the least significant byte if the BALEN is less than 4 + // + // BLE address is always 4 bytes long + .balen() + .bits(3) // 3 bytes base address (+ 1 prefix); + // Configure the endianess + // For BLE is always little endian (LSB first) + .endian() + .little() + // Data whitening is used to avoid long sequences of zeros or + // ones, e.g., 0b0000000 or 0b1111111, in the data bit stream. + // The whitener and de-whitener are defined the same way, + // using a 7-bit linear feedback shift register with the + // polynomial x7 + x4 + 1. + // + // In BLE Whitening shall be applied on the PDU and CRC of all + // Link Layer packets and is performed after the CRC generation + // in the transmitter. No other parts of the packets are whitened. + // De-whitening is performed before the CRC checking in the receiver + // Before whitening or de-whitening, the shift register should be + // initialized based on the channel index. + .whiteen() + .set_bit() + }); + + // Configure CRC + r.crccnf.write(|w| { + // In BLE the CRC shall be calculated on the PDU of all Link Layer + // packets (even if the packet is encrypted). + // It skips the address field + w.skipaddr() + .skip() + // In BLE 24-bit CRC = 3 bytes + .len() + .three() + }); + + // Ch map between 2400 MHZ .. 2500 MHz + // All modes use this range + r.frequency.write(|w| w.map().default()); + + // Configure shortcuts to simplify and speed up sending and receiving packets. + r.shorts.write(|w| { + // start transmission/recv immediately after ramp-up + // disable radio when transmission/recv is done + w.ready_start().enabled().end_disable().enabled() + }); + + // Enable NVIC interrupt + T::Interrupt::unpend(); + unsafe { T::Interrupt::enable() }; + + Self { _p: radio } + } + + fn state(&self) -> RadioState { + match T::regs().state.read().state().variant() { + Some(s) => s, + None => unreachable!(), + } + } + + #[allow(dead_code)] + fn trace_state(&self) { + match self.state() { + RadioState::DISABLED => trace!("radio:state:DISABLED"), + RadioState::RX_RU => trace!("radio:state:RX_RU"), + RadioState::RX_IDLE => trace!("radio:state:RX_IDLE"), + RadioState::RX => trace!("radio:state:RX"), + RadioState::RX_DISABLE => trace!("radio:state:RX_DISABLE"), + RadioState::TX_RU => trace!("radio:state:TX_RU"), + RadioState::TX_IDLE => trace!("radio:state:TX_IDLE"), + RadioState::TX => trace!("radio:state:TX"), + RadioState::TX_DISABLE => trace!("radio:state:TX_DISABLE"), + } + } + + /// Set the radio mode + /// + /// The radio must be disabled before calling this function + pub fn set_mode(&mut self, mode: Mode) { + assert!(self.state() == RadioState::DISABLED); + + let r = T::regs(); + r.mode.write(|w| w.mode().variant(mode)); + + r.pcnf0.write(|w| { + w.plen().variant(match mode { + Mode::BLE_1MBIT => PreambleLength::_8BIT, + Mode::BLE_2MBIT => PreambleLength::_16BIT, + Mode::BLE_LR125KBIT | Mode::BLE_LR500KBIT => PreambleLength::LONG_RANGE, + _ => unimplemented!(), + }) + }); + } + + /// Set the header size changing the S1's len field + /// + /// The radio must be disabled before calling this function + pub fn set_header_expansion(&mut self, use_s1_field: bool) { + assert!(self.state() == RadioState::DISABLED); + + let r = T::regs(); + + // s1 len in bits + let s1len: u8 = match use_s1_field { + false => 0, + true => 8, + }; + + r.pcnf0.write(|w| unsafe { + w + // Configure S0 to 1 byte length, this will represent the Data/Adv header flags + .s0len() + .set_bit() + // Configure the length (in bits) field to 1 byte length, this will represent the length of the payload + // and also be used to know how many bytes to read/write from/to the buffer + .lflen() + .bits(8) + // Configure the lengh (in bits) of bits in the S1 field. It could be used to represent the CTEInfo for data packages in BLE. + .s1len() + .bits(s1len) + }); + } + + /// Set initial data whitening value + /// Data whitening is used to avoid long sequences of zeros or ones, e.g., 0b0000000 or 0b1111111, in the data bit stream + /// On BLE the initial value is the channel index | 0x40 + /// + /// The radio must be disabled before calling this function + pub fn set_whitening_init(&mut self, whitening_init: u8) { + assert!(self.state() == RadioState::DISABLED); + + let r = T::regs(); + + r.datawhiteiv.write(|w| unsafe { w.datawhiteiv().bits(whitening_init) }); + } + + /// Set the central frequency to be used + /// It should be in the range 2400..2500 + /// + /// [The radio must be disabled before calling this function](https://devzone.nordicsemi.com/f/nordic-q-a/15829/radio-frequency-change) + pub fn set_frequency(&mut self, frequency: u32) { + assert!(self.state() == RadioState::DISABLED); + assert!((2400..=2500).contains(&frequency)); + + let r = T::regs(); + + r.frequency + .write(|w| unsafe { w.frequency().bits((frequency - 2400) as u8) }); + } + + /// Set the acess address + /// This address is always constants for advertising + /// And a random value generate on each connection + /// It is used to filter the packages + /// + /// The radio must be disabled before calling this function + pub fn set_access_address(&mut self, access_address: u32) { + assert!(self.state() == RadioState::DISABLED); + + let r = T::regs(); + + // Configure logical address + // The byte ordering on air is always least significant byte first for the address + // So for the address 0xAA_BB_CC_DD, the address on air will be DD CC BB AA + // The package order is BASE, PREFIX so BASE=0xBB_CC_DD and PREFIX=0xAA + r.prefix0 + .write(|w| unsafe { w.ap0().bits((access_address >> 24) as u8) }); + + // The base address is truncated from the least significant byte (because the BALEN is less than 4) + // So it shifts the address to the right + r.base0.write(|w| unsafe { w.bits(access_address << 8) }); + + // Don't match tx address + r.txaddress.write(|w| unsafe { w.txaddress().bits(0) }); + + // Match on logical address + // This config only filter the packets by the address, + // so only packages send to the previous address + // will finish the reception (TODO: check the explanation) + r.rxaddresses.write(|w| { + w.addr0() + .enabled() + .addr1() + .enabled() + .addr2() + .enabled() + .addr3() + .enabled() + .addr4() + .enabled() + }); + } + + /// Set the CRC polynomial + /// It only uses the 24 least significant bits + /// + /// The radio must be disabled before calling this function + pub fn set_crc_poly(&mut self, crc_poly: u32) { + assert!(self.state() == RadioState::DISABLED); + + let r = T::regs(); + + r.crcpoly.write(|w| unsafe { + // Configure the CRC polynomial + // Each term in the CRC polynomial is mapped to a bit in this + // register which index corresponds to the term's exponent. + // The least significant term/bit is hard-wired internally to + // 1, and bit number 0 of the register content is ignored by + // the hardware. The following example is for an 8 bit CRC + // polynomial: x8 + x7 + x3 + x2 + 1 = 1 1000 1101 . + w.crcpoly().bits(crc_poly & 0xFFFFFF) + }); + } + + /// Set the CRC init value + /// It only uses the 24 least significant bits + /// The CRC initial value varies depending of the PDU type + /// + /// The radio must be disabled before calling this function + pub fn set_crc_init(&mut self, crc_init: u32) { + assert!(self.state() == RadioState::DISABLED); + + let r = T::regs(); + + r.crcinit.write(|w| unsafe { w.crcinit().bits(crc_init & 0xFFFFFF) }); + } + + /// Set the radio tx power + /// + /// The radio must be disabled before calling this function + pub fn set_tx_power(&mut self, tx_power: TxPower) { + assert!(self.state() == RadioState::DISABLED); + + let r = T::regs(); + + r.txpower.write(|w| w.txpower().variant(tx_power)); + } + + /// Set buffer to read/write + /// + /// This method is unsound. You should guarantee that the buffer will live + /// for the life time of the transmission or if the buffer will be modified. + /// Also if the buffer is smaller than the packet length, the radio will + /// read/write memory out of the buffer bounds. + fn set_buffer(&mut self, buffer: &[u8]) -> Result<(), Error> { + slice_in_ram_or(buffer, Error::BufferNotInRAM)?; + + let r = T::regs(); + + // Here it consider that the length of the packet is + // correctly set in the buffer, otherwise it will send + // unowned regions of memory + let ptr = buffer.as_ptr(); + + // Configure the payload + r.packetptr.write(|w| unsafe { w.bits(ptr as u32) }); + + Ok(()) + } + + /// Send packet + /// If the length byte in the package is greater than the buffer length + /// the radio will read memory out of the buffer bounds + pub async fn transmit(&mut self, buffer: &[u8]) -> Result<(), Error> { + self.set_buffer(buffer)?; + + let r = T::regs(); + self.trigger_and_wait_end(move || { + // Initialize the transmission + // trace!("txen"); + r.tasks_txen.write(|w| w.tasks_txen().set_bit()); + }) + .await; + + Ok(()) + } + + /// Receive packet + /// If the length byte in the received package is greater than the buffer length + /// the radio will write memory out of the buffer bounds + pub async fn receive(&mut self, buffer: &mut [u8]) -> Result<(), Error> { + self.set_buffer(buffer)?; + + let r = T::regs(); + self.trigger_and_wait_end(move || { + // Initialize the transmission + // trace!("rxen"); + r.tasks_rxen.write(|w| w.tasks_rxen().set_bit()); + }) + .await; + + Ok(()) + } + + async fn trigger_and_wait_end(&mut self, trigger: impl FnOnce()) { + //self.trace_state(); + + let r = T::regs(); + let s = T::state(); + + // If the Future is dropped before the end of the transmission + // it disable the interrupt and stop the transmission + // to keep the state consistent + let drop = OnDrop::new(|| { + trace!("radio drop: stopping"); + + r.intenclr.write(|w| w.end().clear()); + r.events_end.reset(); + + r.tasks_stop.write(|w| w.tasks_stop().set_bit()); + + // The docs don't explicitly mention any event to acknowledge the stop task + while r.events_end.read().events_end().bit_is_clear() {} + + trace!("radio drop: stopped"); + }); + + // trace!("radio:enable interrupt"); + // Clear some remnant side-effects (TODO: check if this is necessary) + r.events_end.reset(); + + // Enable interrupt + r.intenset.write(|w| w.end().set()); + + compiler_fence(Ordering::SeqCst); + + // Trigger the transmission + trigger(); + // self.trace_state(); + + // On poll check if interrupt happen + poll_fn(|cx| { + s.end_waker.register(cx.waker()); + if r.events_end.read().events_end().bit_is_set() { + // trace!("radio:end"); + return core::task::Poll::Ready(()); + } + Poll::Pending + }) + .await; + + compiler_fence(Ordering::SeqCst); + r.events_disabled.reset(); // ACK + + // Everthing ends fine, so it disable the drop + drop.defuse(); + } + + /// Disable the radio + fn disable(&mut self) { + let r = T::regs(); + + compiler_fence(Ordering::SeqCst); + // If it is already disabled, do nothing + if self.state() != RadioState::DISABLED { + trace!("radio:disable"); + // Trigger the disable task + r.tasks_disable.write(|w| w.tasks_disable().set_bit()); + + // Wait until the radio is disabled + while r.events_disabled.read().events_disabled().bit_is_clear() {} + + compiler_fence(Ordering::SeqCst); + + // Acknowledge it + r.events_disabled.reset(); + } + } +} + +impl<'d, T: Instance> Drop for Radio<'d, T> { + fn drop(&mut self) { + self.disable(); + } +} diff --git a/embassy-nrf/src/radio/mod.rs b/embassy-nrf/src/radio/mod.rs new file mode 100644 index 000000000..03f967f87 --- /dev/null +++ b/embassy-nrf/src/radio/mod.rs @@ -0,0 +1,75 @@ +//! Integrated 2.4 GHz Radio +//! +//! The 2.4 GHz radio transceiver is compatible with multiple radio standards +//! such as 1Mbps, 2Mbps and Long Range Bluetooth Low Energy. + +#![macro_use] + +/// Bluetooth Low Energy Radio driver. +pub mod ble; + +use core::marker::PhantomData; + +use crate::{interrupt, pac, Peripheral}; + +/// Interrupt handler +pub struct InterruptHandler { + _phantom: PhantomData, +} + +impl interrupt::typelevel::Handler for InterruptHandler { + unsafe fn on_interrupt() { + let r = T::regs(); + let s = T::state(); + + if r.events_end.read().events_end().bit_is_set() { + s.end_waker.wake(); + r.intenclr.write(|w| w.end().clear()); + } + } +} + +pub(crate) mod sealed { + use embassy_sync::waitqueue::AtomicWaker; + + pub struct State { + /// end packet transmission or reception + pub end_waker: AtomicWaker, + } + impl State { + pub const fn new() -> Self { + Self { + end_waker: AtomicWaker::new(), + } + } + } + + pub trait Instance { + fn regs() -> &'static crate::pac::radio::RegisterBlock; + fn state() -> &'static State; + } +} + +macro_rules! impl_radio { + ($type:ident, $pac_type:ident, $irq:ident) => { + impl crate::radio::sealed::Instance for peripherals::$type { + fn regs() -> &'static pac::radio::RegisterBlock { + unsafe { &*pac::$pac_type::ptr() } + } + + fn state() -> &'static crate::radio::sealed::State { + static STATE: crate::radio::sealed::State = crate::radio::sealed::State::new(); + &STATE + } + } + impl crate::radio::Instance for peripherals::$type { + type Interrupt = crate::interrupt::typelevel::$irq; + } + }; +} + +/// Radio peripheral instance. +pub trait Instance: Peripheral

+ sealed::Instance + 'static + Send { + /// Interrupt for this peripheral. + type Interrupt: interrupt::typelevel::Interrupt; +}