This document provides general information about the STM32WB0 series 2.4 GHz Radio and related HAL driver. The STM32WB0 series 2.4 GHz Radio HAL driver provides the capability to send and receive packets without using the Bluetooth link layer.
1. Introduction
The STM32WB0 series 2.4 GHz radio HAL driver interface controls the 2.4 GHz radio and it interacts with the radio timer, which runs on the slow 32 kHz clock, the RAM memory, and the processor. RAM is used to store radio settings, the current radio status, the data received, and data to be transmitted. The radio HAL driver can manage up to 8 different radio configurations also called state machines. Several features are autonomously managed by the radio, without intervention of the processor:
- Packet encryption
- Communication timing
- Sleep management
2. Data packet format
A packet consists of six fields, of which, only four are user-accessible:
- Preamble field (default is 1 byte: the user can define how many times to repeat the preamble through RADIO_SetPreambleRep())
- NetworkID field: the address of the device (4 bytes).
The receiver device accepts only those packets whose NetworkID field is the same as the one in its own address. The NetworkID should satisfy the following rules:
- No more than 6 consecutive zeros or ones
- All 4 octets not equal
- No more than 24 transitions
- Minimum of 2 transitions in the most significant 6 bits
- NetworkID field: user-accessible through API RADIO_SetTxAttributes() or API HAL_RADIO_SetNetworkID()
- Header field: accepts any values (1 byte); can be used as a byte of data, with no encryption applied to this field
- Length field: represents the length of the data field; user defined value for a packet to transmit or read this value from a received packet
The maximum number of bytes of the payload (with or without encryption) that the STM32WB0 series devices link layer can accept in reception is 255. The user can set the value of this threshold (from 0 to 255) at hardware level through API RADIO_SetMaxReceivedLength().
Packet format |
---|
The maximum value of the length field is 255 with some exceptions. If encryption is enabled, at the maximum length of the data field, 4 bytes must be subtracted. These 4 bytes are reserved for the MIC field
Packet format with encryption |
---|
The table below contains a summary of the length field.
Values in bytes for the length field |
---|
If the encryption is enabled, only the data field is encrypted. The other fields including the header field and the length field are not encrypted.
To avoid memory corruption due to bad length field received (in a packet where the CRC check fails), the user must reserve the maximum memory for packet received that includes 2 bytes of header field as well as the data field. The CRC is used to identify corrupted packets. Its length is 3 bytes, and the radio generates and checks it during transmission and reception, respectively. The user can configure the initial value for the CRC calculation, except in the advertising channels where the initial value is set to 0x555555. The CRC hardware feature can be disabled. It means that the hardware neither appends the CRC in transmission nor checks it during reception.
Data can accept any value and its length is decided by the length field. The user defines a memory buffer in order to set the header field, the length field, and data field as follows:
* PacketBuffer[0] = 0x01; // Header field * PacketBuffer[1] = 5; // Length field * PacketBuffer[2] = 0x02; // Data byte 1 * PacketBuffer[3] = 0x03; // Data byte 2 * PacketBuffer[4] = 0x04; // Data byte 3 * PacketBuffer[5] = 0x05; // Data byte 4 * PacketBuffer[6] = 0x06; // Data byte 5
3. 2.4 GHz HAL radio driver
The 2.4 GHz HAL radio driver interface provides a set of APIs which allow the following functions to be addressed:
- Radio initialization
- Encryption
- Set receiver and transmitter Phy (1 Mbit/s, 2 Mbit/s, S = 2, S = 8)
- Communication channel management
- Set the network ID, CRC initial value, power level
- Set the maximum received packet length and the receive timeout
- Test commands (tone)
These functionalities are addressed through the following APIs:
HAL_RADIO_Init() HAL_RADIO_SetEncryptionCount() HAL_RADIO_SetEncryptionAttributes() HAL_RADIO_SetEncryptFlags() HAL_RADIO_EncryptPlainData() HAL_RADIO_Set_ChannelMap() HAL_RADIO_SetChannel() HAL_RADIO_SetTxAttributes() HAL_RADIO_SetBackToBackTime() HAL_RADIO_SetTxPower() HAL_RADIO_SetReservedArea() HAL_RADIO_MakeActionPacketPending() HAL_RADIO_SetPhy() HAL_RADIO_SetMaxReceivedLength() HAL_RADIO_SetPreambleRep() HAL_RADIO_SetDefaultPreambleLen() HAL_RADIO_DisableCRC() HAL_RADIO_StopActivity() HAL_RADIO_StartTone() HAL_RADIO_StopTone()
These APIs are defined within the following 2.4 GHz HAL radio driver files available on STM32CubeWB0 software package:
- stm32wb0x_ll_radio.h
- stm32wb0x_hal_radio.h
- stm32wb0x_hal_radio.c
3.1. 2.4 GHz HAL Radio driver ActionPacket
The 2.4 GHZ HAL radio driver uses a central data structure that consists of a linked list of ActionPackets. An ActionPacket is a structure (C language) that, in conjunction with the APIs above, defines a complete operation of transmission or reception. It also includes a number of callbacks, which allow the user to define a chain of actions. The ActionPacket is composed of input fields used to configure the action and output fields holding information on the action once it has been executed. The table below contains the information on these fields.
ActionPacket structure |
---|
[ |
The ActionTag is a bitmask used to enable different features of the radio, used by the ActionPacket. The table below explains these parameters.
ActionTag structure |
---|
NOTES:
- In the advertising channels, the frequency hopping is limited to 1 hop.
The bits of the status field of the ActionPacket represent the map of the interrupts triggered by the last radio action. A description of the status field of the ActionPacket is reported below. For full details, refer to the:
- STM32WB09xE reference manual [1]
- STM32WB07xC, STM32WB06xC reference manual [2]
- STM32WB05xE reference manual [3]
The status register with the information on the action are described on following table:
Status values |
---|
4. How to write a 2.4 GHz radio application
There are two ways to write an application:
- Do not use ActionPacket
- Use ActionPacket data structure
Refer to UM2726: BlueNRG-LP, BlueNRG-LPS, and STM32WB0 series 2.4 GHz radio proprietary drivers [4] document for detailed information about the STM32WB0 series 2.4 GHz radio driver. This document also describes how to implement a proprietary over-the-air (OTA) firmware based on STM32WB0 2.4 GHz radio HAL driver.
4.1. Do not use ActionPacket approach
The simplest way for addressing a 2.4 GHZ radio application is to use a set of APIs provided in HAL radio driver, that allows the radio to be configured to fulfill the actions below:
- send a packet
- send a packet and then wait for the reception of a packet (ACK)
- wait for a packet
- wait for a packet and if the packet is received, a packet is sent back (ACK)
In this context, the user does not need to use the ActionPacket to configure the operations of the radio, but a pointer to a user callback is requested, which handles different information according to the executed action:
- TX action: IRQ status
- RX action: IRQ status, RSSI, timestamp and data received
The user callback is called in interrupt mode, in particular within the RADIO_TXRX_IRQHandler() that has the maximum priority.
The second parameter of each API is a relative time in microseconds that represents when the next radio activity starts from the moment in which the API is called. This delay must be long enough, as otherwise it is not possible to program the radio timer and an error code is returned. The user can choose the desired time without taking into account the time that the radio uses for its setup. Then, the delay that is passed to the API represents when the first bit is transmitted or the receive window starts.
Relative time |
---|
4.1.1. TX example with no ActionPacket
This section shows a transmitter (TX) example where the radio is programmed to send a packet periodically, with a time between two consecutive packets of 10 ms. Each packet contains 3 bytes of data. The HAL-driver APIs require neither an ActionPacket structure, nor a pointer to a user callback, because a set of default callbacks is defined.
HAL_RADIO_CallbackTxDone(void)
{
send_packet_flag = 1;
}
HAL_RADIO_CallbackTxDone() is called when a transmission is successfully completed. To activate this callback, the user needs to pass HAL_RADIO_Callback() as an argument of HAL_RADIO_SendPacket().
uint8_t packet[7];
uint8_t DataLen = 5;
uint8_t send_packet_flag = 1;
uint16_t data_val = 1;
int main(void)
{
MX_RADIO_Init();
MX_RADIO_TIMER_Init();
/* Build packet */
packet[0] = 0x02; /* Header */
packet[1] = DataLen; /* Length */
/* Channel map configuration */
uint8_t map[5]= {0xFF,0xFF,0xFF,0xFF,0xFF};
HAL_RADIO_SetChannelMap(0, &map[0]);
/* Setting of channel and the channel increment*/
HAL_RADIO_SetChannel(0, 22, 0);
/* Sets of the NetworkID and the CRC.*/
HAL_RADIO_SetTxAttributes(0, 0x88DF88DF, 0x555555);
/* Configures the transmit power level */
HAL_RADIO_SetTxPower(MAX_OUTPUT_RF_POWER);
/* Infinite loop */
while (1)
{
HAL_RADIO_TIMER_Tick();
if(send_packet_flag == 1)
{
send_packet_flag = 0;
for(uint8_t i = 0; i < (DataLen-2); i++)
{
packet[i+2] = i + data_val;
}
data_val++;
HAL_RADIO_SendPacket(22, 10000, packet, HAL_RADIO_Callback );
}
}
}
4.1.2. RX example with no ActionPacket
In this example the radio is programmed to go to receiver (RX) state periodically. The delay between each RX operation is 9 ms. This ensures that after the first good reception, the RX device wakes up always 1 ms before the TX device (previously configured) starts to send the packet (guard time).
uint8_t rx_flag == 1;
int main(void)
{
uint8_t packet[MAX_PACKET_LENGTH];
MX_RADIO_Init();
MX_RADIO_TIMER_Init();
/* Set the Network ID */
HAL_RADIO_SetNetworkID(0x88DF88DF);
uint8_t map[] = CFG_RF_CHANNEL_MAP;
HAL_RADIO_SetChannelMap(0, map);
HAL_RADIO_SetChannel(0, channel, 0);
/* Infinite loop */
while (1)
{
HAL_RADIO_TIMER_Tick();
if(rx_flag == 1)
{
HAL_RADIO_ReceivePacket(22, 9000, packet, 20000, MAX_PACKET_LENGTH, HAL_RADIO_Callback);
}
}
}
To manage RX packets, the user can implement a set of four standard callbacks. The structure, RxStats, contains information about the RSSI and timestamp of the received packet.
void HAL_RADIO_CallbackRcvOk(RxStats_t* rxDataStats)
{
rx_flag = 1;
}
void HAL_RADIO_CallbackRcvTimeout(RxStats_t* rxDataStats)
{
rx_flag = 1;
}
void HAL_RADIO_CallbackRcvError(RxStats_t* rxDataStats)
{
rx_flag = 0;
}
void HAL_RADIO_CallbackRcvEncryptErr(RxStats_t *rxDataStats)
{
rx_flag = 1;
}
4.2. Use ActionPacket approach
In this context user defines a number of ActionPackets, according to the actions that must be taken by the radio. Then the user fills these structures with the description of the operations to execute. For each ActionPacket, the API HAL_RADIO_SetReservedArea() must be called in order to initialize the information of the ActionPacket itself. To start the execution of an ActionPacket, the API HAL_RADIO_MakeActionPacketPending() has to be called. After this, the application can:
- Verify that another ActionPacket is called, by linking the ActionPackets together and then decide which ActionPacket executes within the condition routine.
- Reactivate the radio execution by calling again the API RADIO_MakeActionPacketPending().
All further actions are handled in interrupt mode as for the HAL layer approach, but in this case, the user handles two callback functions:
- Condition routine: condRoutine().
- It provides the result of the current ActionPacket, and it returns TRUE or FALSE. Depending on this, the next ActionPacket linked to the current one is selected from two possibilities:
- next_true ActionPacket1
- next_false ActionPacket2
- The purpose of this mechanism is to differentiate the next action of the scheduler. For example, the condition routine in an RX action can decide:
- To schedule the next_true ActionPacket, if the packet received is good (ActionPacket1)
- To schedule the next_false ActionPacket, if the packet received is not good (ActionPacket2)
- Data routine: dataRoutine()
- It provides information as data received or transmitted, RSSI, timestamps, and others. Additionally, it is intended to modify the transmit data for the next packet based upon the last received data. This can be used to modify the packet data to be sent.
The goal of the multiple callbacks is to enable the user to access to the total performance of the radio, by avoiding time criticality bottlenecks. The goal is to bundle time critical aspects in the condition routine and have the rest of the framework to be non-time-critical. One benefit is that the framework forces the user to split code over smaller routines, which leads to more structured programming. The most time critical operations are performed in the condRoutine(). Here the next activity is chosen according to some criteria and then patched on the fly. It is obvious that this must occur before the radio controller starts its operation, otherwise the patch is ignored. The maximum execution time of the condition routine is about the back-to-back time less the time that the radio spends for the RF setup. For the moment, it can be considered that the radio in a back-to-back scenario needs 70 μs to set up. Then if the back-to-back time is 150 μs, the condRoutine() has about 80 μs to set the pointer to the next activity. Therefore, the dataRoutine() has about the remaining time to modify, for example, the data to send. The figure below summarizes the timing of the different callbacks.
Callback timing |
---|
Notes:
- 1: Before this time, the condition routine must end.
- 2: Before this time, the updating of the next packet must be completed.
4.2.1. TX example with ActionPacket
This section shows a transmitter (TX) example where the radio is programmed to send a packet periodically, with a time between two consecutive packets of 10 ms. Each packet contains 3 bytes of data. The ActionPacket structure is used to define this operation.
uint8_t DataLen = 20;
uint16_t data_val = 0;
int main(void)
{
MX_RADIO_Init();
MX_RADIO_TIMER_Init();
/* Build packet */
packet[0] = 0x02; /* Header */
packet[1] = DataLen; /* Length */
for(uint8_t i = 0; i < (DataLen-15); i++) {
packet[i+2] = i + data_val;
}
data_val++;
/* Channel map configuration */
uint8_t map[5]= {0xFF,0xFF,0xFF,0xFF,0xFF};
HAL_RADIO_SetChannelMap(0, &map[0]);
/* Setting of channel */
HAL_RADIO_SetChannel(0, 22, 0);
/* Sets of the NetworkID and the CRC.*/
HAL_RADIO_SetTxAttributes(0, 0x88DF88DF, 0x555555);
/* Configures the transmit power level */
HAL_RADIO_SetTxPower(MAX_OUTPUT_RF_POWER);
/* Build Action Packet */
actPacket.StateMachineNo = 0;
actPacket.ActionTag = RELATIVE | TIMER_WAKEUP | TXRX | PLL_TRIG;
actPacket.WakeupTime = 10000;
actPacket.MaxReceiveLength = 0; /* Not applied for TX */
actPacket.data = packet; /* Data to send */
actPacket.next_true = &actPacket; /* Pointer to the next Action Packet*/
actPacket.next_false = NULL_0; /* Null */
actPacket.condRoutine = conditionRoutine; /* Condition routine */
actPacket.dataRoutine = dataRoutine; /* Data routine */
/* Call this function before execute the action packet */
HAL_RADIO_SetReservedArea(&actPacket);
/* Call this function for the first action packet to be executed */
HAL_RADIO_MakeActionPacketPending(&actPacket);
while (1)
{
HAL_RADIO_TIMER_Tick();
}
}
The condition callback triggers the execution of thenext sc heduled ActionPacket (txAction), while the data callback modifies data to be sent in next frame.
uint8_t conditionRoutine(ActionPacket* p)
{
if( (p->status & BLUE_INTERRUPT1REG_DONE) != 0)
{
if(p->status & BLUE_STATUSREG_PREVTRANSMIT)
{
return TRUE;
}
}
return FALSE;
}
uint8_t dataRoutine(ActionPacket* p, ActionPacket* next)
{
for(uint8_t i = 0; i < (DataLen-15); i++)
{
packet[i+2] = i + data_val;
}
data_val++;
return TRUE;
}
4.2.2. RX example with ActionPacket
In this example, the radio is programmed to go to receiver (RX) state periodically. The delay between each RX operation is 9 ms. This ensures that after the first good reception, the RX device wakes up always 1 ms before the TX device (configured in TX example) starts to send the packet (guard time). The RX timeout is 20 ms. This value is long enough to ensure that at least one packet should be received. The ActionPacket structure is used to define this operation.
int main(void)
{
MX_RADIO_Init();
MX_RADIO_TIMER_Init();
/* Channel map configuration */
uint8_t map[5]= {0xFF,0xFF,0xFF,0xFF,0xFF};
HAL_RADIO_SetChannelMap(0, &map[0]);
/* Setting of channel*/
HAL_RADIO_SetChannel(0, 22, 0);
/* Sets of the NetworkID and the CRC.*/
HAL_RADIO_SetTxAttributes(0, 0x88DF88DF, 0x555555);
/* Configures the transmit power level */
HAL_RADIO_SetTxPower(MAX_OUTPUT_RF_POWER);
/* Setting of RX timeout*/
HAL_RADIO_SetGlobalReceiveTimeout(20000);
/* Build Action Packet */
actPacket.StateMachineNo = 0;
actPacket.ActionTag = RELATIVE | TIMER_WAKEUP | PLL_TRIG;
actPacket.WakeupTime = 9000;
actPacket.MaxReceiveLength = 255;
actPacket.data = packet; /* Point to memory where store data */
actPacket.next_true = &actPacket; /* Pointer to the next Action Packet*/
actPacket.next_false = NULL_0; /* Null */
actPacket.condRoutine = conditionRoutine; /* Condition routine */
actPacket.dataRoutine = dataRoutine; /* Data routine */
/* Call this function before execute the action packet */
HAL_RADIO_SetReservedArea(&actPacket);
/* Call this function for the first action packet to be executed */
HAL_RADIO_MakeActionPacketPending(&actPacket);
while (1)
{
HAL_RADIO_TIMER_Tick();
}
}
uint8_t conditionRoutine(ActionPacket* p)
{
if( (p->status & BLUE_STATUSREG_PREVTRANSMIT) == 0)
{
/* Reception ends with no errors */
if((p->status & BLUE_INTERRUPT1REG_RCVOK) != 0)
{
return TRUE;
}
/* Reception ends with timeout */
else if((p->status & BLUE_INTERRUPT1REG_RCVTIMEOUT) != 0){}
/* Reception ends with errors */
else if((p->status & BLUE_INTERRUPT1REG_RCVCRCERR) != 0){}
}
return FALSE;
}
uint8_t dataRoutine(ActionPacket* p, ActionPacket* next)
{
/* Event is a reception */
if( (p->status & BLUE_STATUSREG_PREVTRANSMIT) == 0)
{
/* Reception ends with no errors */
if((p->status & BLUE_INTERRUPT1REG_RCVOK) != 0)
{
if( (p->status & BLUE_INTERRUPT1REG_ENCERROR)!= 0){}
}
/* Reception ends with timeout */
else if((p->status & BLUE_INTERRUPT1REG_RCVTIMEOUT) != 0){}
/* Reception ends with errors */
else if((p->status & BLUE_INTERRUPT1REG_RCVCRCERR)!= 0) {}
}
return TRUE;
}
5. References