From Silicon Labs: Kernel 201: Proprietary Wireless Task
This blog is part of the Kernel 201: Designing a Dynamic Multiprotocol Application with Micrium OS. The table of contents for this blog series can be found here.
Note: It is recommended to download the Kernel 201 application and have the code in front of you as you go through this blog post. The code can be downloaded on the Kernel 201: Project Resources page.
Note: It is important to understand RAIL API calls are not thread-safe. The design of the Proprietary Wireless task is to ensure multiple transmit or receive RAIL calls are not made simultaneously.
Introduction
The goal of the Proprietary Wireless task in the Kernel 201 Application is to communicate with another Silicon Labs board running the Kernel 201 Instructor Application. In a lab setting there may be many Thunderboard Sense 2s running the Kernel 201 Application and they would all communicate to one Kernel 201 Instructor Application board.
Just like the Bluetooth task, the Proprietary Wireless task will handle only the wireless communication and pass messages to other tasks in the system as needed. The Proprietary Wireless task will handle both transmitting and receiving in one task rather than having them split into two separate tasks.
Kernel 201 Proprietary Wireless Initialization
When the Proprietary Wireless task is started, the first action it needs to perform is to configure the radio. This is a separate configuration than the Bluetooth configuration which has already occurred (Bluetooth task has a higher priority than the Proprietary Wireless task). The configuration of the radio is done in two different locations: the Proprietary Configurator and the labPropRadioInit() function.
The kernel201.isc file holds the Proprietary Wireless Configuration in addition to the Bluetooth Configuration. For the Kernel 201 Application, the default configuration in the Proprietary Configurator is all that is needed. In the screenshot above, the second profile was removed, but it is not necessary to do so.
labPropRailHandle = RAIL_Init(&labPropRailCfg, NULL); // 1 RAIL_SetTxFifo(labPropRailHandle, // 2 labPropTxFifo, 0, LAB_PROP_TX_FIFO_SIZE); RAIL_TxPowerConfig_t railTxPowerConfig = { #if HAL_PA_2P4_LOWPOWER .mode = RAIL_TX_POWER_MODE_2P4_LP, #else .mode = RAIL_TX_POWER_MODE_2P4_HP, #endif .voltage = HAL_PA_VOLTAGE, .rampTime = HAL_PA_RAMP, }; if (channelConfigs[0]->configs[0].baseFrequency < 1000000000UL) { railTxPowerConfig.mode = RAIL_TX_POWER_MODE_SUBGIG; } RAIL_EnablePaCal(true); // 3 status = RAIL_ConfigTxPower( labPropRailHandle, &railTxPowerConfig); APP_RTOS_ASSERT_CRITICAL((status == RAIL_STATUS_NO_ERROR), 1); RAIL_SetTxPower(labPropRailHandle, HAL_PA_POWER); RAIL_ConfigChannels(labPropRailHandle, // 4 channelConfigs[0], NULL); RAIL_ConfigCal(labPropRailHandle, RAIL_CAL_ALL); RAIL_ConfigEvents(labPropRailHandle, // 5 RAIL_EVENTS_ALL, RAIL_EVENT_SCHEDULER_STATUS | RAIL_EVENTS_TX_COMPLETION | RAIL_EVENTS_RX_COMPLETION | RAIL_EVENT_CAL_NEEDED); RAIL_SetFixedLength(labPropRailHandle, // 6 LAB_PROP_PACKET_SIZE); schedulerInfo.priority = 200; // 7 RAIL_StartRx( labPropRailHandle, LAB_PROP_RX_CHANNEL, &schedulerInfo); |
The configuration performed in labPropRadioInit() is shown above. There is a great multi-part tutorial on RAIL that walks through each of these calls and can be found here: RAIL Tutorial.
- The labPropRadioInit() function first initializes the RAIL handle. The same RAIL handle will be used for transmit and receive in the Proprietary Wireless task.
- The RAIL_SetTxFifo() function passes in the transmit FIFO buffer required for queueing up data to send. It is not necessary to specify a receive buffer because one is already allocated internally in the RAIL stack.
- Based on the railTxPowerConfig above, the PA calibration is enabled (must be done before RAIL_ConfigTxPower()) and the transmit power is configured. After the transmit power has been configured, a call must be made to RAIL_SetTxPower() to reapply the transmit power configuration.
- After the radio transmit power has been configured, the channels we will use must be configured. The channelConfigs[0] array comes from the Proprietary Configurator. For the Kernel 201 Application we will use two channels: 0 and 9. Data will be received on channel 0 and transmitted on channel 9.
- The call to RAIL_ConfigEvents() configures what callbacks events are enabled. The RAIL_EVENTS_TX_COMPLETION and RAIL_EVENTS_RX_COMPLETION will signal when a transmit has completed and we need to start listening for data again or when we’ve received a packet and need to process it.
- The call to RAIL_SetFixedLength() changes the size of the packet we expect to receive from the default of 32 bytes to LAB_PROP_PACKET_SIZE (48 bytes in this case). This is necessary because the default size of a LAB_MSG_s message is 32 bytes and we must include extra data to indicate what task it must be sent to.
- After all of the configurations have been made on the RAIL handle, a call is made to RAIL_StartRx() on the LAB_PROP_RX_CHANNEL to begin listening for messages. Because of the way the radio is configured for Bluetooth and Proprietary Wireless, the radio is now shared between the two protocols. Bluetooth has a higher priority than Proprietary so it has the ability to take control of the radio away from the Proprietary stack if it needs to transmit or receive. This is something that must be taken into account when designing the Kernel 201 Instructor Application.
Kernel 201 Proprietary Wireless Task Design
The Proprietary Wireless task loop is structured just like all other tasks in the Kernel 201 Application (except for the watchdog task). When the task is created, a call is made to labPropRadioInit() which goes through the initialization as shown above. After the radio is configured for Proprietary Wireless it waits on the Task Message Queue.
while (DEF_TRUE) { p_msg = OSTaskQPend( LAB_WDOG_TASK_TIMEOUT_MS, OS_OPT_PEND_BLOCKING, &lab_q_id, 0, &err); do { if(err.Code == RTOS_ERR_TIMEOUT) { break; } else if(err.Code != RTOS_ERR_NONE) { APP_RTOS_ASSERT_CRITICAL(err.Code == RTOS_ERR_NONE, ;); } switch(lab_q_id) { case LAB_Q_ID_PROP_RX: labPropRadioRxData((RAIL_RxPacketHandle_t)p_msg); break; case LAB_Q_ID_PROP_TMR: labPropRadioTxData(); break; case LAB_Q_ID_MSG: labPropUpdateStatus(p_msg); labUtilMsgFree(p_msg); break; default: break; } } while(0); labWDOGFeed(LAB_TASK_PROP); } |
The Task Message Queue processes three events and one RTOS error. These events are:
- RTOS_ERR_TIMEOUT
- LAB_Q_ID_MSG
- LAB_Q_ID_PROP_TMR
- LAB_Q_ID_PROP_RX
RTOS_ERR_TIMEOUT
As part of the software watchdog, the Proprietary Wireless task must feed the software watchdog at a specified rate. There is a define in lab.h that all tasks use called LAB_WDOG_TASK_TIMEOUT_MS. This define specifies how often every task should check-in with the software watchdog. If the Task Message Queue does not receive a message before the timeout value is hit, the pend call returns with RTOS_ERR_TIMEOUT. Since it’s a timeout the Proprietary Wireless task knows that no message was received, it only feeds the software watchdog and then starts pending on the Task Message Queue again.
LAB_Q_ID_MSG
When another task in the system wishes to send data via Proprietary Wireless, a lab message must be sent to the Proprietary Wireless task via the Task Message Queue. In most cases, this is used when a task has completed a request and is updating the remote Kernel 201 Instructor Application. Unlike the Bluetooth task, data is not transmitted immediately. Instead it is stored in a status structure and transmitted on a fixed interval.
LAB_Q_ID_PROP_TMR
The Kernel 201 Application is designed to be used in a lab class situation, with many Thunderboard Sense 2 boards running and only one Kernel 201 Instructor Application running. In order to not overwhelm the Kernel 201 Instructor Application, the Kernel 201 Application transmits status data on a fixed time interval. This is necessary because if the Kernel 201 Application responded to changes immediately, this would mean every time the Instructor Application pushed out an LED change every Kernel 201 Application would respond at the exact same time and messages would be dropped.
When the status data is transmitted, it must fit into the same packet size as the data we receive, specified by LAB_PROP_PACKET_SIZE. Since there may be multiple devices transmitting on the same channel to the Kernel 201 Instructor Application, each device must be differentiated.
typedef struct { CPU_INT16U seq_num; CPU_INT08U device_name[LAB_DEVICE_NAME_MAX_LEN]; } LAB_PROP_DATA_TX_s; |
The struct above is used to inform the Kernel 201 Instructor Application who sent the data it is receiving. The seq_num value is not used in the current revision of the application on the transmit side, but it is used on the receive side which will be covered in another section of this post.
typedef struct { LAB_MSG_LED_s led; LAB_MSG_BTN_s btn; LAB_MSG_SI7021_s si7021; CPU_INT32U err_cnt; CPU_INT32U updated; } LAB_PROP_STATUS_s; |
The LAB_PROP_STATUS_s struct is the second part of the data sent to the Kernel 201 Instructor Application. When lab messages are received from other tasks in the system, the data is stored in this structure and transmitted on a fixed interval. The reason for the separate struct is this data will be peeled away from the LAB_PROP_DATA_TX_s and sent to a desktop application on the Kernel 201 Instructor Application side.
The diagram below shows how the Proprietary Wireless task uses a software timer to trigger transmits. All data that is received from other tasks in the system is stored in a status structure and when the timer expires, the status structure is transmitted.
- When the labPropTmr hits the timeout value, the callback labPropTmrCallback() is executed from the OS Timer stack. The labPropTmrCallback() function sends a signal to the Proprietary Wireless task via the Task Message Queue by sending LAB_Q_ID_PROP_TMR in the msg_size field.
- The Proprietary Wireless Task Message Queue releases upon receiving a message from the timer callback. It determines based on the lab_q_id that it is a timer callback. The Proprietary Wireless Task then grabs the current status data, writes it to the RAIL Tx FIFO and starts the transmission via RAIL_StartTx(). The RAIL_StartTx() call is a non-blocking call, so it will return as soon as it successfully initiates the transfer.
Note: Typically in an RTOS application a guard should be placed around the RAIL_WriteTxFifo() and RAIL_StartTx() to prevent a second call to either before receiving the RAIL events callback to signal success or error on the transmit. Since the application only transmits on a fixed interval it is not as important to protect multiple writes since they are spaced out by multiple seconds. - After the transmit is complete, the RAIL events callback will execute. The events flag will be set to RAIL_EVENTS_TX_COMPLETION which signals it is now ok to switch the radio to another mode. Since the radio is used by both Bluetooth and Proprietary Wireless, a call must be made to RAIL_YieldRadio() to signal that the radio is free. After yielding the radio we must call RAIL_StartRx() to start background listening for Proprietary Wireless packets in-between Bluetooth transmissions.
Note: If you were to transmit and receive on the same channel, the RAIL_StartRx() call would not be necessary. Since the Kernel 201 Application transmits and receives on different channels, it is necessary to call RAIL_StartRx() and specify what channel to listen on.
LAB_Q_ID_PROP_RX
When the radio is not communicating via Bluetooth or transmitting a Proprietary Wireless message, it is in a listening state for Proprietary Wireless messages from the Kernel 201 Instructor Application. All boards running the Kernel 201 Application listen for messages on the same channel. This allows the Kernel 201 Instructor Application to broadcast messages to all boards running the Kernel 201 Application.
typedef struct { CPU_INT16U seq_num; CPU_INT16U task_dest; void* p_data; } LAB_PROP_DATA_RX_s; |
When data is received, it is expected to be a specific size and in a specific format. The size was configured during the RAIL configuration; all packets are LAB_PROP_PACKET_SIZE. The format of the data is a combination of the structure above and a LAB_MSG_s. By reusing the LAB_MSG_s structure this allows the Proprietary Wireless task to just “peel off” the LAB_PROP_RX_s data and send the LAB_MSG_s data to the appropriate task. The images below show how an LED message fits into the RAIL Rx packet.
After a packet is received, the Proprietary Wireless task will copy the LAB_MSG_s data into a lab message and send it to the task specified in the task_dest field. The following flowchart shows how packets are received in the Kernel 201 Application.
- When a packet is received on the radio, the RAIL stack triggers a callback to the specified callback function labPropRailEventHandler(). Once it is determined that the event triggering the callback is a packet has been received, the RAIL Event Handler must determine if we’ve seen this message before. The Kernel 201 Instructor Application sends out multiple copies of the same message due to the possibility that some Thunderboard Sense 2s may be communicating via Bluetooth or transmitting Proprietary Wireless messages. By using a sequence number, this allows the RAIL Event Handler to quickly determine if the message has already been seen, and if so release the message. If it is a new sequence number that we have not yet seen, the pointer for the packet is passed to the Proprietary Wireless task to be processed.
- The Task Message Queue releases when the RAIL packet pointer is received from the RAIL Event Handler. The packet pointer is passed into labPropRadioRxData() where a lab message is allocated, a lab message is extracted from the RAIL packet and the message is copied into the allocated buffer. The destination of the lab message is also pulled from the RAIL packet, the data is sent to the appropriate task and then the RAIL packet is released.
Final Thoughts
The Proprietary Wireless task is a much more complex task than the Bluetooth task when it comes to wireless communication. This is partially due to the flexibility of the Proprietary Wireless stack but also because the RAIL API calls are the lowest level calls available to interface with the radio. When the Bluetooth stack wishes to use the radio, it also must use RAIL API calls under the hood.This application only uses a small subset of the Proprietary Wireless Configurator and RAIL API. For more detailed information on RAIL, check out the resources on this page.
Source: https://www.silabs.com/community/blog.entry.html/2019/07/08/kernel_201_proprietarywirelesstask-tpWg