Or, how connecting a ST module on a ST board made me wander in RIOT's code. Let's track containers! Put a bluetooth (low energy, BLE) beacon on each one, listen for pings and save the result. Seems easy enough, right? Nothing could be more wrong.
Let's start with a bit of architecture - we need a beacon to put on the ontainers, a receiver that listens for pings and forwards the data to a database. To get information out of this, the receivers will tag the pings they receive with the current time and location of the receiver. I know what you're thinking: the receivers don't move, why "current" location? Well, we already have GPS for accurate timing, we might as well remove human error in setting up the device and get location data from there as well. As far as protocols go, we chose BLE for the pings and LoRaWAN for the data uplink,
so we need to have both radio modules in the receivers.
+---------+ +----------+ +---------+ +-----------+
| BLE | | BLE | | LoRa | | LoRa |
| Module +---------->| Module | | Module +---------->| Gateway |
+----+----+ +----+-----+ +----+----+ +-----------+
| | |
+----+----+ +----+-----+ |
| | | | |
| Beacon | | Receiver +--------+
| | | |
+---------+ +----------+
We have a few boards at our disposal:
- Nucleo f401re: development board for the stm32f401re chip
- BLE shield: carrier for the SPBTLE-RF module
- LoRa shield: carrier for the LoRa chipset + some sensors
- L475 discovery: feature loaded board, with a SPBTLE-RF module onboard
The thing that stands from the rest is that the last board already has everything we need for the beacon - it would make sense to use it in that role, and that's exactly what we tried at first.
+------------------------+
| BLE Shield |
| |
+------------------------+
+------------------------+
| LoRa Shield |
| |
+------------------------+
+--------------------------+ +------------------------+
| Beacon | | Receiver |
| L475-discovery | | nucleo-f401re |
+--------------------------+ +------------------------+
Chapter 1. Shun the alternate thinkerPins on microcontrollers can have many different functions, and this is defined on stm32 by setting a pin's Alternate Function. Analogously, a signal from an internal peripheral (in our case, SPI) can be routed to different physical pins to provide flexibility.
RIOT wants nothing of this nonsense, and assigns pins to peripherals in the board-specific periph_conf.h file, where the alternate functions are set properly, but the pins assigned to each device are fixed. In your code you refer to a specific SPI bus by index, and this index is used to find the corresponding configuration in periph_conf.h. If you want to route one of the signals to a different pin than the one specified in the file, that's too bad: you need to patch the file by hand, there is no way to do it from your own code.
...there is a workaround. The faint of heart may want to skip this.
Leave the pin as is in the configuration file. Connect the pin that RIOT thinks is right to the correct one with a wire. Enjoy.
And that's what we are doing, at least for now. With this workaround, the BLE module responds to simple packets over SPI, great!
Chapter 2. Alien languagesHey, we can talk to the BLE module now! We're done, right?...right?
Well, no. Bluetooth is quite complicated, and it needs a full software stack on top of the physical connection. If you look in RIOT, it has one, called NimBLE. But if you think that the problems is solved, think again: it only supports some chips from the NRF family, and definitely doesn't support ours. There is hope.
BLE splits its protocol stack in two parts, a lower level section, implemented in the BLE controller itself, and a higher level part that can be either part of the controller or be implemented in a separate microcontroller. In the latter case, the two parts communicate over some physical interface by exchanging HCI commands.
NimBLE supports this kind of separation, and already has an implementation of HCI over SPI. It's not exactly what we need, but we can work with that and adapt it to our chip.
The work is not completed yet, but at least there's some kind of path forwards, which we will explore in the next days.
Chapter 3. Worlds collideAt this point, our hope restored, we tried to put both the BLE and LoRa shield on top of the nucleo-f401re board, to simulate the full receiver device. As you may guess from the title of this section, things can't just be that simple - the shields share a pin!*
The BLE's reset pin, which is absolutely necessary to mange the chip, is in the same location as LoRa's SYS_WKUP1, and when you put the shields on top of each other the two signals are connected together. Is this an issue? We aren't sure, but it definitely doesn't feel right to tie a module's reset input to another's wakeup pin.
After spending a while on possible solutions, the simplest one came to mind: let's switch the boards around. From this point onwards, the nucleo board will be the beacon's brain, and the discovery board will make the receiver work. In this way, the beacon can use the BLE shield without conflicts, and the receiver can use the onboard BLE chip alongside the LoRa shield.
+--------------------------+ +------------------------+
| BLE Shield | | LoRa Shield |
| | | |
+--------------------------+ +------------------------+
+--------------------------+ +------------------------+
| Beacon | | Receiver |
| nucleo-f401re | | L475 discovery |
+--------------------------+ +------------------------+
Chapter 4. Thou shalt count to threeNo more, no less. Three shall be the number thou shalt count, and the number of the counting shall be three. Four shalt thou not count, neither count thou two, excepting that thou then proceed to three.
Well, not for RIOT. The discovery board has three SPI buses, one of which is exposed to the user on the arduino headers, while SPI2 and SPI3 are internal to the board and used to interface with the various devices on it. Looking at the schematic, the BLE module we're trying to use is on the SPI3 bus. Great, let's use it!
spi_t spi = SPI_DEV(2);
Buses are 0-based, so this should work. When you try to run it, it triggers an assertion: RIOT only knows about the first bus! Apparently, only SPI1 is supported and usable, so we get to make our first patch to add support for the other two. In boards/b-l475e-iot01a/include/periph_conf.h, let's add the buses:
static const spi_conf_t spi_config[] = {
[...]
{
.dev = SPI2,
.mosi_pin = GPIO_PIN(PORT_D, 4),
.miso_pin = GPIO_PIN(PORT_D, 3),
.sclk_pin = GPIO_PIN(PORT_D, 1),
.cs_pin = SPI_CS_UNDEF,
.mosi_af = GPIO_AF5,
.miso_af = GPIO_AF5,
.sclk_af = GPIO_AF5,
.cs_af = GPIO_AF5,
.rccmask = RCC_APB1ENR1_SPI2EN,
.apbbus = APB1,
#ifdef MODULE_PERIPH_DMA
.tx_dma = 3,
.tx_dma_chan = 1,
.rx_dma = 2,
.rx_dma_chan = 1,
#endif
},
{
.dev = SPI3,
.mosi_pin = GPIO_PIN(PORT_C, 12),
.miso_pin = GPIO_PIN(PORT_C, 11),
.sclk_pin = GPIO_PIN(PORT_C, 10),
.cs_pin = SPI_CS_UNDEF,
.mosi_af = GPIO_AF6,
.miso_af = GPIO_AF6,
.sclk_af = GPIO_AF6,
.cs_af = GPIO_AF6,
.rccmask = RCC_APB1ENR1_SPI3EN,
.apbbus = APB1,
#ifdef MODULE_PERIPH_DMA
.tx_dma = 5,
.tx_dma_chan = 3,
.rx_dma = 4,
.rx_dma_chan = 3,
#endif
}
};
Each bus has an #ifdef-guarded section to manage its DMA channels, so let's also add the new channels:
static const dma_conf_t dma_config[] = {
[...]
{ .stream = 3 }, /* DMA1 Channel 4 - USART1_TX / SPI2_RX */
{ .stream = 4 }, /* DMA1 Channel 5 - SPI2_TX */
{ .stream = 8 }, /* DMA2 Channel 1 - SPI3_RX */
{ .stream = 9 }, /* DMA2 Channel 2 - SPI3_TX */
{ .stream = 10 }, /* DMA2 Channel 3 - UART4_TX */
};
[...]
#define DMA_3_ISR isr_dma1_channel5
#define DMA_4_ISR isr_dma2_channel1
#define DMA_5_ISR isr_dma2_channel2
#define DMA_6_ISR isr_dma2_channel3
At this point, this is pretty much a complete and self contained patch, so in a pull request it goes: https://github.com/RIOT-OS/RIOT/pull/17885
Hopefully it will be merged soon, until then we'll work from our fork of RIOT
Chapter 5. The newt, the bad and the uglyLet's go back to NimBLE for a while, we need to make it work somehow. As a first step, we follow Apache's tutorial on how to create a barebones BLE application, https://mynewt.apache.org/latest/tutorials/ble/ble_bare_bones.html
It's meant for mynewt and not for RIOT, but at least we can start testing NimBLE and maybe even make it work with our chip. So let's follow the tutorial:
$ newt new test
$ cd test
$ newt upgrade
Error: Error updating "apache-mynewt-nimble": error: Your local changes to the following files would be overwritten by checkout:
porting/npl/riot/include/npl_syscfg/npl_sycfg.h
Please commit your changes or stash them before you switch branches.
Aborting
...oh well. We're used to this by now. Apparently one of the more recent commits created this file as a symlink, and newt really doesn't like it. Let's fix it
$ cd repos/apache-mynewt-nimble/porting/npl/riot/include/npl_syscfg/
$ rm npl_sycfg.h
$ ln -s ../syscfg/syscfg.h npl_sycfg.h
$ cd ../../../../../../../
$ newt upgrade
Skipping "apache-mynewt-core": already upgraded (1.9.0)
Skipping "apache-mynewt-mcumgr": already upgraded (0.2.0)
Making the following changes to the project:
upgrade apache-mynewt-nimble (0.0.0 --> 1.4.0)
upgrade mcuboot (0.0.0 --> 1.7.2)
apache-mynewt-nimble successfully upgraded to version 1.4.0
mcuboot successfully upgraded to version 1.7.2
Nice! Let's go on
$ newt pkg new apps/ble_app -t app
$ newt target create ble_tgt
$ newt target set ble_tgt \
app=apps/ble_app \
bsp=@apache-mynewt-core/hw/bsp/nordic_pca10040 \
build_profile=optimized
Now add the dependencies to apps/ble_app/pkg.yml and apps/ble_app/syscfg.yml following the tutorial, and compile:
$ newt build ble_tgt
Error: repos/apache-mynewt-core/encoding/tinycbor/src/cborpretty.c: In function 'value_to_pretty':
repos/apache-mynewt-core/encoding/tinycbor/src/cborpretty.c:307:33: error: expected ')' before 'PRIu64'
307 | if (fprintf(out, "%" PRIu64, val) < 0)
| ^~~~~~~
| )
Not again.
$ newt target amend ble_tgt cflags='-DPRIu64="llu"'
$ newt target amend ble_tgt cflags='-DPRIx64="lld"'
$ newt build ble_tgt
[...]
Target successfully built: targets/ble_tgt
I'm too tired to celebrate. But it's still a good thing
Chapter 6. BacktrackingAs is becoming quite evident, supporting the SPBTLE-RF via NimBLE is taking a lot longer than expected, so we started looking for other, faster, solutions. Help came in the form of a repository from stm32duino that is supposed to talk directly to our chip, `https://github.com/stm32duino/SPBTLE-RF`. It looked quite promising with its lack of dependencies and relatively short code, so I forked it and started working on porting it to RIOT.
Overall, the conversion took a few hours to rename some colliding functions and move from Arduino's to RIOT's API for pins, timers, interrupts and spi. When it finally compiled, as you may now expect, it crashed!
The original version of the library set up an interrupt on the SPBTLE-RF's IRQ output, and would then read data using spi in the interrupt handler. This works just fine on Arduino, but in RIOT you can't use may of the available functionalities in interrupt handlers. This is indeed the case for SPI, which, if used in such context, triggers a ISR stack overflow.
The fix was to move reading data to a separate thread, which waits on a condition variable set by the ISR and then reads all the available data. Once this is done, it starts waiting again for more data. The updated version of the code looks like this:
static cond_t hci_reader_cond = COND_INIT;
static mutex_t hci_reader_mutex = MUTEX_INIT;
void HCI_Isr(void)
{
cond_signal(&hci_reader_cond);
}
void* HCI_Reader_Thread(void *arg)
{
while(1) {
mutex_lock(&hci_reader_mutex);
cond_wait(&hci_reader_cond, &hci_reader_mutex);
while(BlueNRG_DataPresent()){
[read data...]
}
mutex_unlock(&hci_reader_mutex);
}
}
(Full code is available at https://github.com/dp1/SPBTLE-RF-RIOT )
With this ported and modified library, we were finally able to communicate with the chip, and could use it as a beacon.
Chapter 7. Let's start writing some new codeUnfortunately, the library we ported only implements the interface needed for broadcasting messages, so there was no way to also receive packets, which our project definitely requires. It is now time to implement a BLE Observer class, the role dedicated to BLE radios that listen for messages broadcasted by beacons and keeps track of them, without establishing a direct connection with the device.
Starting from the BeaconService class that was already implemented, the Observer required a few modifications. At initialization time, we should tell the chip to behave as an observer:
aci_gap_init_IDB05A1(GAP_OBSERVER_ROLE_IDB05A1, 0, 0x07, &service_handle,
&dev_name_char_handle, &appearance_char_handle);
And then we need to tell it to actually start listening:
aci_gap_start_observation_procedure(0x2000, 0x2000, PASSIVE_SCAN, PUBLIC_ADDR, 1);
After the initialization is complete, ST's library will call Observer_HCI_Event_CB on any event, and we can filter by type to handle received messages.
static void (*adv_cb)(le_advertising_info*) = NULL;
void ObserverServiceClass::setAdvertisingCallback(void (*cb)(le_advertising_info*))
{
adv_cb = cb;
}
void Observer_HCI_Event_CB(void *pckt)
{
hci_uart_pckt *hci_pckt = (hci_uart_pckt *)pckt;
hci_event_pckt *event_pckt = (hci_event_pckt*)hci_pckt->data;
if(hci_pckt->type != HCI_EVENT_PKT)
return;
switch(event_pckt->evt)
{
[...]
case EVT_LE_META_EVENT:
{
evt_le_meta_event *evt = (evt_le_meta_event *)event_pckt->data;
switch(evt->subevent)
{
case EVT_LE_ADVERTISING_REPORT:
{
le_advertising_info *adv = (le_advertising_info *)evt->data;
if(adv_cb)
adv_cb(adv);
}
break;
}
}
break;
}
}
Then, in main, we can register a callback that handles received packets, and (for now), print the details of what we receive
uint8_t SERVER_BDADDR[] = {0x12, 0x34, 0x00, 0xE1, 0x80, 0x03};
void advertising_report_cb(le_advertising_info *adv)
{
puts("=== Advertising report ===");
printf(" evt_type = %d\n", adv->evt_type);
printf(" bdaddr_type = %d\n", adv->bdaddr_type);
printf(" tBDAddr = %02X %02X %02X %02X %02X %02X\n",
adv->bdaddr[0], adv->bdaddr[1], adv->bdaddr[2],
adv->bdaddr[3], adv->bdaddr[4], adv->bdaddr[5]
);
printf(" data_length = %d\n", adv->data_length);
printf(" data: ");
for(int i = 0; i + 1 < adv->data_length; i++)
printf(" %02X", adv->data_RSSI[i]);
putchar('\n');
printf(" RSSI: %d\n", adv->data_RSSI[adv->data_length]);
puts("======");
}
int main()
{
[error checking omitted for brevity]
BTLE.begin();
ObserverService.setAdvertisingCallback(advertising_report_cb);
ObserverService.begin(SERVER_BDADDR);
while(1) {
BTLE.update();
}
}
Finally, it works. To test it we flashed an NRF board as a beacon with one of the RIOT examples, and we could easily see all the packets it sent. We now have a way to make functioning beacons and BLE observers using the SPBTLE-RF on RIOT.
Some more detailsThe first successful SPI interaction with the chip. In this example, we set it up as a beacon and listened on the SPI bus with the logic analyzer.
Comments
Please log in or sign up to comment.