nRF51 iBeacon in Ada
Annotated source code for an iBeacon in Ada
The code is available in the GitHub Repo. It's licensed GPLv2+. Special thanks to @meriac; I learned a lot about the nRF51822 radio peripheral by reading his openbeacon-ng code.
ibeacon.ads
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;
radio.adb
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;
The 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
transmitting.
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;
main.adb
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;