AdaCore Blog

Make with Ada 2020: LoRaDa := Ada + LoRa;

by Emma Adby

Hedley Rainnie's project won a finalist prize in the Make with Ada 2019/20 competition. This project was originally posted on here. For those interested in participating in the 2020/21 competition, registration is now open and project submissions will be accepted until Jan 31st 2021, register here.



Last MakeWithAda I worked on getting BLE going with an STM32L476. This project is another communication protocol: LoRa. This came about as my wife and I were musing about how to detect and deter unwanted garden visitors (cats that come into the garden to use it as a toilet and leave, squirrels that climb the walls and scratch at the soffits). Step one for dealing with this was to ID the interlopers using sensors, Step two would be some form of countermeasure. I am sort of at step 0.5 The basic sensor comm is up but the detection and countermeasures have not been decided yet.


Early on, I realized that LoRa might make a good choice for long distance sensor use. I did not need a high bandwidth here, just, creature detected, issue countermeasure. On twitter one day, Ronoth was retweeted wrt a Crowdsupply board. So, I waited sometime for that to arrive. It was my first use of Ada and LoRa. The history of how these boards came to be is outlined below.

First Ada + LoRa port: Ronoth S76S

This was a Crowdsupply board and is by Ronoth. The S76S is based on the Ascip S76S module.

Ronoth S76S

The Ascip S76S is a module with an STM32L073 + an SX1276 radio. It supports SWD debug and the usual port of Ada to an STM32L series. The board is expensive, $30+. Also an STM32L0 is a Cortex-M0+ and is ARMv6M, this complicated the port slightly and hurts code density. This was my first attempt at an SX1276 driver. It was initially based on the RadioHead Arduino lib but diverged rapidly.

Second Ada + LoRa port: Heltec LoRa Node 151

The second board is the Heltec LoRa Node 151. This is an STM32L151 + SX1276:

Heltec LoRa Node 151

After the Ronoth board, this one was a more modern design with more ram. It was a smooth port and was transmitting and receiving smoothly. Its half the price of the Ronoth.

Third Ada + LoRa port: Blkbox 915MHz LoRa SX1276 module

Then there is the Blkbox 915MHz LoRa SX1276 module, here seen connected to a Bluepill (STM32F103).

Blkbox 915 MHZ LoRa SX1276 module + Bluepill

This module is my favorite. Its cheap, about $7 works fine and can be moved around from target to target. Initially as pictured, I had it ported to the Bluepill. Later I moved it to the complex STM32L552 and the equally challenging STM32WB55 where it acts as a server.

Fourth port the new STM32L552:

Blkbox 915MHz LoRa SX1276 module + STM32L552

The STM32L552 challenge

Whilst the other STM32 ports are not new ground for me wrt getting Ada going on ST platforms, the STM32L552 is exceptionally challenging. I have some experience with embedded platforms and the Cortex-M33 in its various forms from Nordic, NXP and now ST are truly some of the most complex controller designs I have ever worked with. The bugs you can get are really epic. I could fill pages here recounting all the really messy mind bending bugs. The bullet points below outline the work, but at each stage learning and bugs were involved.

1) It uses the ARMv8M architecture. No support in OpenOCD for this targetNo direct Ada support as the lIb is ARMv7M.

2) It is based on a Cortex-M33 with TrustZone.

3) Ada_Drivers_Library is designed for a single view of the peripheral space not a peripheral space partitioned into Secure and Non-secure areas.

4) Debugging also needs to consider ARMv8M+TrustZone and how that effects register/memory/flash reading and writing.

5) To that end openocd was ported to this platform:

From there you can attach to the board:

openocd -f board/st_nucleo_l552.cfg

The usual gdb loading commands work for reading and writing flash and ram.

6) The methodology for the port was to bolt two separate Ada ELF32 binaries into the final image. One image is a Secure boot and API handler, then a very small C glue layer that handles gcc ARMv8M stuff since gnat2019's gcc libs cannot be linked with ARMv8M code yet. Also some future pragma's would be needed in the Ada world to accommodate the special S <=> NS bindings + access to NS functions. (the BLXNS and SG instructions and cleaning up the unbanked register state before BLXNS to avoid S state leaks to the NS side). Finally the NS image is the last piece. For S I am using the Ravenscar sfp and for NS, Ravenscar full. Before NS can toggle an LED or touch any peripheral S has to be told what is allowed to be used by the NS side, extra boilerplate unneeded in a non-secure environment.

7) The basic structure of the flash is the boot area (formally 0x08000000, is now S at 0x0c000000 for a secure boot Ada Elf32 binary) it curr occupies 40-60k of flash. A watermark is created in the ST flash option regs to divide the flash into 2 regions, I chose S from 0x0c000000 - 0x0c01ffff) NS from 0x0802000 - end.

The secure boot area also has the veneer code to allow NS to call back into S. You need a magic SG instruction anywhere you want NS's PC to touch down, any other opcode is an abort. Also the region for the NS PC touch down must be marked Non Secure Callable (NSC), or, another cryptic abort. The S & NS Ada programs are Cortex-M4F compiler builds, the veneer code is in C, and it is compiled as Cortex-M33

Outbound S to NS call flow

Above we see the S_To_NS Ada call going to a C s_to_ns.

A magic function ptr with special attribute ensures that the function pointer produces the blxns shown plus a boatload of assembly to wipe out the CPU regs so that no leaks are present to the NS side. Note that the NS side is a full Cortex-M executable with vector table.

Then we have the NS side calling the LoRa radio IP's API. Let's look at Recv_SX1276 as an example API call

The call goes to the C wrapper recv_sx1276_from_ns. Notice that due to it's attribute, its veneer entry point starts with the ARMv8M instruction: sg an sg is the only instruction ns can execute upon arrival in s. Any other instruction and you will be in the debugger, debugging a crash.

After the sg, a veneer branch is made to a special veneer that finally calls our Ada code via an export of its Recv_SX1276 as C callable.

Upon arrival, we see pointer types. How do we on the secure side know that those pointers are safe (i.e. NS). We need to use another ARMv8M instruction to validate the pointer(s). The tt instruction as shown:

If any of those pointers are S, we return. This is defensive coding in a S/NS environment.

A really challenging port. For example 2 Ravenscar runtimes on one SoC, when an exception comes to/from secure/non-secure, how is context switch decided? This was very challenging as process starvation was a very real bug.


There was no SW initially for this board, ordered in Nov, which came to my house early Dec. So I had 2 months to get the whole show going.

Secure/Non-secure peripheral base handling, this is interesting. How Ada sets up the peripheral bases for the driver code that shared between S/NS.

We see the magic in the declaration, the S base is always 16#1000_000# shifted from the NS base. In this way, legacy Ada_Drivers_Library code that just refers to GPIO_A for example will 'do the right thing' based on the stance that the library was built with (the Secure_Code constant).

Fifth port the STM32WB55 + LoRa. A LoRa server to BLE bridge

An Ada programmed STM32WB55 rounds out the show. Here is the server having its LoRa module traffic being analyzed by a Saleae.

WB55 Server LoRa debug

This was also a big effort done after the last Make with Ada contest. Last contest was a SensorTile with Ada running the BLE stack. This WB55 is a collapsed SensorTile, the BLE radio is not an SPI peripheral here but has been absorbed into an SoC with HW inter-process communication being used instead of SPI. Lots of issues getting a port to this platform. (No SVD file initially(!)). Had a good bug with LoRa SPI. The SPI flags were at the wrong bit offset so no rx/tx notifications were received. This turned out to be an SVD file issue. Took a day to debug that one as an SVD file has a lot of leverage in a port, it will be the last place you look.

For the STM32WB55 nucleo board. There are 2 in the blister pack a large board and a small dongle. I am using the large board and a Blkbox 915MHz LoRa SX1276 module.


Radio Freq

In the US LoRa is restricted to 915Mhz. I thought this Ada code really elegantly solves the freq to 24bit coding the SX1276 uses:

Ada data delta type

And its usage:

Thus abstracting Freq -> 24bit representation

Radio message debug

The Saleae could capture BlkBox LoRa SPI radio traffic between the server on the STM32WB55 and the STM32L552 secure client. Very helpful. At that moment it was 2 Ada programs with a new protocol and issues with retries and list corruption. One bug was quite interesting, the server is in receive 99% of the time, you might think that setting its fifo to 0 and reading the received packet would be solid. No, if the SX1276 state machine never leaves receive, it keeps an internal fifo pointer. Since its internal, it keeps monotonically increasing every packet, even with an overt reset of rx&tx pointers. Thus at each packet notification the code sees stale data at 0 as the internal pointer has moved. The 'fix' is to leave receive just long enough for that pointer to reload. Another day of debug was that issue. There were many of these types of issues.


Putting it all together

OK, so we have 6 nodes of varying pedigree and 1 gateway/server. The design of LoRa packet data is a to/from pair of bytes (ala RadioHead FW) but then we deviate, namely some fields for commands and values for message retries and sequence numbers.

Message Protocol

There are 4 messages:

The four messages

Client data structures are as so:

Client datastructures

Node discovery

Initial 'best effort contact by server to discover the network

Every 5 seconds the server sends a broadcast ping, ping replies come in from:

1) Those nodes in airshot and that are ready to receive.

2) Those nodes whose non-retried reply can get back to the server. A broadcast ping creates a lot of RF hash wrt the replies.

3) Once the server sees the reply, the node is added to a table:

The actives table

This is how the network gets populated and grows as new nodes are discovered or old ones drop off.

Note that 0 and 255 are not valid client node IDs.


Once a node has been recorded by the server as active, it can begin to send and receive notifications. Today I only support 8bit notifications. 8 is more than enough for my target application. Ideally, I don't want long messages being sent. This 8 bit notify has been set so that bit0 is the LED on the board and bit1 is a user button. Only node#1 the STM32L552 has a usable user button. The plumbing of that interrupt was interesting btw, as its GPIO needs to be allowed to be read by the NS side, further, it can have its interrupt routed to NS. Again, all new ground and requiring deep study of the ref manual and board experimentation (no reference code, everything was the try, see, iterate method). Fortunately, I was using RAM for those tests or I think the flash would be worn out by now.

New Ada_Drivers_Library support for s/ns external interrupt routing:

The server's BLE stack is activated when the BLE phone application shows an LED icon press. Its a BLE notification. We then locally change the server's LED state to the requested value and then set a suspension object to let the LoRa task know that the LoRa network needs to be woken up with a notify8 message.

The server's notification task then walks the actives list and sends notify8's to all connected nodes. Upon receipt of a reply, the original message is removed from the queue. If however after a timeout, a node has failed to respond, then the original message with same seq# is resent. Finally, after 15 retries with no reply, the message is retired.

When the user button is pressed, the reverse happens. The button notification task on the client is activated via a suspension object, this then prepares a LoRa packet with a notify8 with bit2 set. Assuming it got over the air to the server after its own client retry count, then the server processes the notify8 by setting another suspension object that wakes up the same BLE task that handles the local button press. This then is transmitted to the BLE phone application where the event with timestamp is shown.


A demo has been coded up. The idea is that via ping packet replies, the server can build an array up of who is connected. Once that map is created the server knows what nodes can are alive on the LoRa network. From there via the Android app, we make a connection to LoRaDa (the name I gave the Ada BLE server on the STM32WB55). From there you can turn on a light and see any alert's. At this time, I only have a user button on the STM32L552 Nucleo board, so it is the only board that sends the alert.

The nodes in the LoRa network are as so:

0: STM32WB55 (Ada coded LoRa server + Ada BLE central)

1: STM32L552 (Ada coded secureboot for radio, and non-secure LoRa client).

2: Heltec LoRa Node 151 #1 (Ada coded LoRa client)

3: Heltec LoRa Node 151 #2 (Ada coded LoRa client)

4: Ronoth LoDev S76S #1 (Ada coded LoRa client)

5: Ronoth LoDev S76S #2 (Ada coded LoRa client)

6: Bluepill STM32F103

So when nodes arrive into the network, when the light button is toggled on the Android app, it cycles through all the connected nodes and sets each ones light to the state requested. All during this, node #1 can signal an alert that shows up as a red bell icon with a timestamp in the app.

Project video


The range is outstanding, I walked prob 500m from our house and still had a signal! This tech is more than adequate for low rate sensor data.


I doubt I could have hacked this without using Ada. Once I had the client code up on the STM32L552 and was migrating the changes to the Heltec, Ronoth and bluepill boards, I don't think I spent more than 30mins getting the client changes up and working smoothly on those targets. I had already done a bare bones LoRa port to each but the client task version was not. So Ada made the job a pleasure and at least in the doldrums of code bugs I could know and rely on the fact the compiler was 100% there and solid despite my code problems.


I thought last years Make with Ada was a challenge but this one was seemingly straightforwards up until the Cortex-M33 needed much attention, especially low level work such as flashing S&NS images in OpenOCD that pulled me away from the LoRa client and server. BTW, that flashing bugs and fix are quite fascinating but frustrating too given the schedule. The server too was added late in the project! Originally I had a Dragino LG01 LoRa access point. That LoRa node is programmed in a C++ Arduino env, doable, but not Ada-esque. Finally a bulb went on in my head, why not use the BLE work on the STM32WB55 and make a LoRa to BLE bridge. How hard could that be? :) It was another challenge on top of an already challenging project. Much of the work was on the protocol. Debugging radio code on both the client and servers. The server has the most advanced radio work as it has an async receive task. The server is 99% of the time in receive and drops out of that to transmit from time to time. Timing is really subtle with these tasks, if you mess it up, the BLE can drop the connection or the LoRa network starts too many retries. I spent quite a bit of time on this protocol and it still needs some work.

One item for Ada users that can make LoRa quite inexpensive is a Bluepill (< $2) and a BlkBox LoRa module ($6-$7) so for < $10, you can have an Ada controlled LoRa module. A good value I feel.

Ultimately, ST will release a 48pin 7x7 STM32L5 processor, that will be pin compatible with the Bluepill boards. That will be Cortex-M33 based and might make for another interesting secure IoT solution. I plan to do a chip swap when its available as I did for the STM32L443 before.

About 2 weeks ago ST announced the STM32WL, that is a single chip LoRa controller. So, all the solutions I showed are dual chip, a controller + radio. Through some deal with Semtech, they will have a single chip with the radio IP absorbed. Of course, the Ada code I worked on will need to run on this device when its available.

Finally, whilst an Ada LoRa network and LoRa<->BLE gateway is novel, my feeling is the most interesting part of the work is the progress on the previously Ada untouched Cortex-M33. Ada is well known as a safe&secure language, the Cortex-M33 is a secure processor. So the marriage is a good one. Lets see if the Ada community can make some progress with this CPU, I already prepared a path and have shown its quite possible to get good results from it.

  • Access the project code here.

Posted in

About Emma Adby

Emma Adby is the Managing Director of AdaCore Ltd. After co-leading the global marketing team for a number of years, she now manages marketing, financial, legal and HR business operations for AdaCore’s new UK technical centre-of-excellence. Emma also works to advocate for increased adoption and wider use of the Ada and SPARK languages globally by coordinating community centric activities and resources.