nRF51 iBeacon in Ada
Annotated source code for an iBeacon in Ada
iBeacon format is pretty straight forward and can be nicely represented with an Ada record. For this demo we just set the default record values to our packet values. Multibyte fields are little-endian (from the BLE core spec).
with Interfaces; package Ibeacon is use Interfaces; type Mac_Type is array (0 .. 5) of Unsigned_8; type Apu_Header_Type is array (0 .. 1) of Unsigned_8; type Manuf_Data_Header is array (0 .. 3) of Unsigned_8; type UUID_Type is array (0 .. 15) of Unsigned_8; type Ibeacon_Packet is record Header : Unsigned_8 := 16#42#; Radio_Length : Unsigned_8 := 16#24#; Mac : Mac_Type := (16#FE#, 16#CA#, 16#EF#, 16#BE#, 16#AD#, 16#DE#); Flags_Length : Unsigned_8 := 2; Flags_Type : Unsigned_8 := 1; Flags_Content : Unsigned_8 := 6; Data_Length : Unsigned_8 := 16#1A#; Data_Type : Unsigned_8 := 16#FF#; -- Manuf. Spec Data Data_Header : Manuf_Data_Header := (16#4C#, 16#00#, 16#02#, 16#15#); UUID : UUID_Type := (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); Major : Unsigned_16 := 0; Minor : Unsigned_16 := 0; Power : Integer_8 := -70; end record; end Ibeacon;
with Interfaces; with Ibeacon; with nrf51; use nrf51; with nrf51.CLOCK; with nrf51.GPIO; with nrf51.Interrupts; with nrf51.RADIO; with System.Storage_Elements;
All the peripheral drivers are included in the zfp runtime for nrf51
package body Radio is use Interfaces; Ble_Access_Address : constant Unsigned_32 := 16#8E89_BED6#; Packet : Ibeacon.Ibeacon_Packet; subtype Channel_Number is UInt7 range 0 .. 39; -- subtype Data_Channel_Number is Channel_Number range 0 .. 36; subtype Advertising_Channel_Number is Channel_Number range 37 .. 39; subtype Adv_Channel_Index is Integer range 1 .. 3; -- type Data_Channel_Index is new Integer range 1 .. 37; -- nRF51 Radio Peripheral represents frequncy as 2400 + Ble_Frequency subtype Ble_Frequency is UInt7 range 2 .. 80; -- type Data_Channel is -- record -- Channel : Data_Channel_Number; -- Frequency : Ble_Frequency; -- end record; type Advertising_Channel is record Channel : Advertising_Channel_Number; Frequency : Ble_Frequency; end record; Advertising_Channels : constant array (1 .. 3) of Advertising_Channel := ((37, 2), (38, 26), (39, 80)); Current_Adv_Channel_Index : Adv_Channel_Index := 1;
The preamble to the module defines some standard BLE constants and variables in the format expected by the nrf51 RADIO peripheral. I've included some (commented out) definitions for the BLE data channels, but iBeacon only requires the advertising channels.
I define a record for the channels that includes the channel number with the frequency. This is useful because BLE specifies data whitening before transmission and the whitening polynomial is based on channel number. Fundamentally, anytime we need the frequency, we'll also need the channel number to modify whitening.
procedure Send; procedure RADIO_IRQHandler; pragma Export (C, RADIO_IRQHandler, "RADIO_IRQHandler"); procedure POWER_CLOCK_IRQHandler; pragma Export (C, POWER_CLOCK_IRQHandler, "POWER_CLOCK_IRQHandler");
We export (with C conventions) the interrupt handlers so they override the weak references to the default handler in the startup code.
procedure Init is use nrf51.RADIO; RADIO : RADIO_Peripheral renames RADIO_Periph; begin RADIO.MODE.MODE := Ble_1Mbit; -- The radio supports several / -- custom 2.4GHz radio protocols RADIO.TXPOWER.TXPOWER := TXPOWER_Field_0DBm; -- Setup Transmit Address, bit fiddling required due to -- base/prefix munging done by peripheral RADIO.TXADDRESS.TXADDRESS := 0; RADIO.PREFIX0.Val := Shift_Right (Ble_Access_Address, 24); RADIO.BASE0 := Shift_Left (Ble_Access_Address, 8); RADIO.RXADDRESSES.ADDR.Val := 0; RADIO.PCNF1.BALEN := 3; -- Base Address Length, in bytes -- Specifies packet memory layout for peripheral RADIO.PCNF0.LFLEN := 8; -- Length of Length field in bits RADIO.PCNF0.S0LEN := 1; -- Preamble length RADIO.PCNF1.WHITEEN := Enabled; -- For BLE Whitening is required RADIO.PCNF1.MAXLEN := 16#ff#; -- Radio will truncate payloads -- longer than this value -- CRC configuration is specified in the Bluetooth Core Spec RADIO.CRCCNF.LEN := Three; RADIO.CRCCNF.SKIPADDR := Skip; RADIO.CRCINIT.CRCINIT := 16#0055_5555#; RADIO.CRCPOLY.CRCPOLY := 16#0000_065B#; -- Shorts are a neat radio peripheral feature where one event -- can trigger another. RADIO.SHORTS.READY_START := Enabled; -- When radio is ready to -- TX, Start transmitting -- immediately RADIO.SHORTS.END_DISABLE := Enabled; -- When transmission has -- finished, disable the -- radio peripheral -- Fire interrupt when radio is disabled, happens -- automatically when transmission finished thanks to SHORTS RADIO.INTENSET.DISABLED := Set; -- We also need to configure the clock peripheral to let us -- know via interrupt when the HFCLK has started declare use nrf51.CLOCK; CLOCK : CLOCK_Peripheral renames nrf51.CLOCK.CLOCK_Periph; begin CLOCK.INTENSET.HFCLKSTARTED := Set; end; declare use nrf51.Interrupts; begin Set_Priority (RADIO_IRQ, IRQ_Prio_High); Enable (RADIO_IRQ); Set_Priority (POWER_CLOCK_IRQ, IRQ_Prio_High); Enable (POWER_CLOCK_IRQ); end; end Init;
The start procedure initiates the sending of a packet by starting up the High-frequency clock.
procedure Start is -- Procedure starts the sending process by priming the HFCLK use nrf51.CLOCK; CLOCK : CLOCK_Peripheral renames nrf51.CLOCK.CLOCK_Periph; begin CLOCK.TASKS_HFCLKSTART := 1; end Start;
Once the HFCLK has started, we clear the interrupt and run the Send procedure.
procedure POWER_CLOCK_IRQHandler is use nrf51.CLOCK; CLOCK : CLOCK_Peripheral renames nrf51.CLOCK.CLOCK_Periph; begin if CLOCK.EVENTS_HFCLKSTARTED /= 0 then CLOCK.EVENTS_HFCLKSTARTED := 0; Send; end if; end POWER_CLOCK_IRQHandler;
Send procedure points the radio peripheral to the area of ram
holding our beacon packet, sets the frequency and whitening iv, and
enables the transmitter. Because we set the
END_DISABLE short in the
peripheral, the transmitter will be disabled when the packet is done
procedure Send is use nrf51.RADIO; use Ibeacon; RADIO : RADIO_Peripheral renames RADIO_Periph; I : Adv_Channel_Index renames Current_Adv_Channel_Index; begin RADIO.PACKETPTR := Unsigned_32 ( System.Storage_Elements.To_Integer (Packet'Address)); RADIO.FREQUENCY.FREQUENCY := Advertising_Channels (I).Frequency; RADIO.DATAWHITEIV.DATAWHITEIV := Advertising_Channels (I).Channel; RADIO.EVENTS_END := 0; RADIO.TASKS_TXEN := 1; end Send; end Radio;
Once the radio has been disabled, we can turn off the HFCLK to save power. We also rotate frequency (and whitening iv) of next transmission through the list of advertising channels.
procedure RADIO_IRQHandler is use nrf51.RADIO; use nrf51.CLOCK; use nrf51.GPIO; RADIO : RADIO_Peripheral renames RADIO_Periph; CLOCK : CLOCK_Peripheral renames CLOCK_Periph; GPIO : GPIO_Peripheral renames GPIO_Periph; begin if RADIO.EVENTS_DISABLED /= 0 then RADIO.EVENTS_DISABLED := 0; CLOCK.TASKS_HFCLKSTOP := 1; if Current_Adv_Channel_Index + 1 not in Adv_Channel_Index'Range then Current_Adv_Channel_Index := 1; else Current_Adv_Channel_Index := Current_Adv_Channel_Index + 1; end if; GPIO.OUTSET.Arr (12) := Set; end if; end RADIO_IRQHandler;
Once radio.adb is written, the main procedure is very simple
with nrf51.GPIO; with Util; with Radio; procedure Main is use nrf51.GPIO; use Util; GPIO : GPIO_Peripheral renames nrf51.GPIO.GPIO_Periph; begin GPIO.DIRSET.Arr (12) := Set; -- Set LED indicator as output GPIO.OUTSET.Arr (12) := Set; -- Turn it off (active low) Delay_Init; -- Initialize RTC delay driver Radio.Init; -- Initialize RADIO peripheral loop Delay_MS (300); -- Use RTC to sleep for 300ms Radio.Start; -- Start sending a packet (returns immediately, -- before packet it sent since it only triggers -- HFCLK start GPIO.OUTCLR.Arr (12) := Clear; -- Light the indicator LED, to be -- turned off when the radio is -- disabled. end loop; end Main;