written by Shawn Nock on 2016-09-28

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;