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.
Task Architecture
The first step of designing any real-time system, before writing any code, is to identify all events the system will be responsible for. Listing out these actions will help identify what events may be grouped together in tasks and what actions will require their own task/s. There are a number of resources available that discuss different task modeling strategies. Some common strategies you may come across are:
Signal event groups – Every event is put in its own task. Not efficient in large systems.
Sequential events – Events that must be processed in order can be grouped together.
Timing events – Events that occur on a periodic time base can be grouped together
Interface events – All events associated with a particular interface
Safety monitoring – Any safety monitoring events should be separated from any actuation tasks
For this application, these are the following events that must be handled and how they’ll be grouped into tasks:
Bluetooth TX – labBluetoothTask
Bluetooth RX – labBluetoothTask
Proprietary Wireless TX – labPropTask
Proprietary Wireless RX – labPropTask
GPIO LED Control – labLEDTask
RBG LED Control – labLEDTask
Button 0 Read – labBtnTask
Button 1 Read – labBtnTask
Hardware Watchdog – labWDOGTask
labBluetoothTask
Silicon Labs’ Bluetooth stack already creates two tasks, Bluetooth and LinkLayer to handle the transmission and reception. The Bluetooth task provides callbacks for the user to implement to know when there is an event to process. The labBluetoothTask will handle those events from the Bluetooth task and pass that data along to the other tasks in the system. The labBluetoothTask will also receive data from other tasks in the system to transmit via Bluetooth.
labPropTask
Silicon Labs does not provide internal tasks for Proprietary Wireless (RAIL) as it does for Bluetooth, but it is important to group the RAIL functions together as they are not thread-safe functions. The labPropTask will handle all data received by RAIL and send it to other tasks in the system as needed. It will also receive data from other tasks in the system and send it out via RAIL.
labLEDTask
Since only the GPIO or RGB LEDs can be active at one time, it makes sense to group them together.
labBtnTask
The button task operates on a low priority polling interval. It could be switched to an interrupt based task but by leaving it as a polling task, it leaves open the option to add other polling events to the task in the future.
labWDOGTask
The watchdog task will implement a software based watchdog to control how the system feeds the hardware watchdog. It is important that this task have no other operations than handling the watchdog.
Task Communication
Now that the events have been divided up into their respective tasks, it is important to implement a consistent method for communication between tasks. There are again many different ways to implement task communication. Micrium OS Kernel provides Semaphores, Task Semaphores, Event Flags, Message Queues and Task Message Queues. While each of these kernel objects have various pros and cons, no one of these services is better than another; it just depends on the application and how it is using these kernel services.
For this application, each task (with the exception of the watchdog task) will be centered around a Task Message Queue and the entire application will use a sort-of layered messaging scheme, similar to the OSI model but much more simplified. The main loop of each task will pend on a Task Message Queue and wait for a message or timeout to determine its next action.
The image above shows the basic layout for all tasks in the system. The code snippet below shows some pseudocode for the task
void* p_msg;
LAB_Q_ID_e lab_q_id;
while (DEF_TRUE)
{
p_msg = OSTaskQPend( LAB_WDOG_TASK_TIMEOUT_MS, // Block the task until we
OS_OPT_PEND_BLOCKING, // receive a message
&lab_q_id,
0,
&err);
do {
if(err.Code == RTOS_ERR_TIMEOUT) { // If we timeout, just feed
break; // the watchdog task
} else if(err.Code != RTOS_ERR_NONE) { // Shouldn't occur, assert
APP_RTOS_ASSERT_CRITICAL(err.Code == RTOS_ERR_NONE, ;);
}
switch(lab_q_id) {
case LAB_Q_ID_XXX_EVT:
labXXXHandleEvt(); // Handle a task event
break;
case LAB_Q_ID_MSG:
labXXXHandleMsg((LAB_MSG_s*) p_msg); // Handle a lab message
labUtilMsgFree(p_msg); // Then free the message
break; // back to the pool
default:
break;
}
} while(0);
labWDOGFeed(LAB_TASK_ID_XXX); // Feed the watchdog after
} // every task action
By using the Task Message Queue, it allows a task to wait on three different types of events:
Timeouts
Events
Lab Messages
Timeouts
The software watchdog wants every task to check in on a specified rate to show it has not gone out to lunch. By specifying a timeout value in the Task Message Queue pend, the pend call will return with RTOS_ERR_TIMEOUT if a message is not received in the specified time. If a task expects a message in a set time interval, logic can also be added to the error check for RTOS_ERR_TIMEOUT to keep track of how many timeouts we receive in a row.
Events
Some tasks may have a hardware interrupt or software timer callback they need to handle in addition to processing messages from other tasks in the system. The msg_size data field in OSTaskQPend() provides a 32-bit value that is used as an identifier field for this task design. When a task receives a message, it will have a switch statement to handle the value of msg_size. In this application, all message identifiers can be found in an enum called LAB_Q_ID_e.
When an interrupt or software timer callback wants to send a message to the task it is associated with, it will make a call similar to the one below:
It is important to note that the LAB_Q_ID_e value is specified in the msg_size field and not the void* field. All messages sent must have a valid identifier in the msg_size field where the void* argument is optional. In this case, the void* argument is set to 0 as there is no extra data to send.
Lab Messages
These messages are how tasks communicate with each other in this application. Rather than having the Bluetooth or Proprietary Wireless tasks spend time configuring PWM timers to drive the RGB LEDs, it makes more sense to send the LED task a LED command to be processed. This also allows us to handle wireless communication at a higher priority than changing the LEDs or polling the button states. The wireless tasks may be affected if responses are not sent out at specific intervals where if the system has to wait an extra 10ms to toggle the LED, the user most likely won’t notice.
The lab messages follow a layered model where every message has the same header, the header defines what else is to follow in the packet. This application provides utility functions for getting, sending and freeing messages (refer to lab_util.c). There are advantages to these utility functions that will be discussed a little later. When you get/free a message you are actually pulling from a memory pool of fixed block sizes. All messages, regardless of how much of the block they use, are the same size. In the case of this application, all messages are 32 bytes in size. This means that if a task needs to send or receive a message larger than 32 bytes, either the size of every block in the system must be increased, multiple messages must be sent or modifications must be made to the utility functions to allow for allocation and freeing of different size blocks.
The struct above shows the lab message structure that all messages will start with. The ID field identifies what message will follow it and the type corresponds to if it’s a command, response, update or error message. The void* argument is used so you can cast the next layer to the end of the lab message struct.
LAB_MSG_s *p_msg;
p_msg = (LAB_MSG_s*) labUtilMsgGet(); // Get a data buffer to send a message
if(p_msg == DEF_NULL) {
break;
}
p_msg->id = LAB_MSG_ID_LED; // Set the message id
p_msg->type = LAB_MSG_TYPE_CMD; // Set that its a command message
When you wish to allocate a message to send, the code snippet above shows how you would use the labUtil function to allocate a message. The image below shows how the lab message fits into the allocated block.
After you configure the message ID and message type fields, you need to configure the next part of the block for the message ID you specified. For this example, let’s assume we’re changing the LEDs. LEDs are controlled by the LAB_LED_MSG_s struct. To accomplish this, you will cast the LAB_LED_MSG_s to p_data so when you enter information for the LED message, it goes into the correct location in lab message.
LAB_MSG_s *p_msg;
LAB_LED_MSG_s *p_led_msg;
p_msg = (LAB_MSG_s*) labUtilMsgGet(); // Get a data buffer to send a message
if(p_msg == DEF_NULL) {
break;
}
p_msg->id = LAB_MSG_ID_LED; // Set the message id
p_msg->type = LAB_MSG_TYPE_CMD; // Set that its a command message
p_led_msg = (LAB_MSG_LED_s*) &(p_msg->p_data);
p_led_msg->mode = LAB_LED_MODE_RGB;
p_led_msg->color = LAB_LED_COLOR_GREEN;
p_led_msg->state = LAB_LED_STATE_BLINK;
The code snippet above shows how to cast the p_data argument to the LED message structure. Now when you modify the fields of the LED message structure it will be aligned in the message block as shown below.
Once the full message is assembled, the message needs to be sent to the desired task/s. Some messages may only go to one task where others may need to be sent to multiple tasks. The lab utility function for sending offers flexibility for sending messages. Using the labUtilSend() function as shown below, you pass the pointer to the message block just configured and the task/s the message should be sent to.
labUtilMsgSend(p_msg, dest_task); // Send the message to the specified destination
In the case where one message is being sent to multiple tasks, the send function will keep track of the number of tasks it was sent to. This allows each task to call the free function after its processed the message, but the memory block is not actually freed until all tasks that received the message free it. To free a message all a task needs to do is make the following call:
labUtilMsgFree(p_msg); // Free the message back to the pool
It is the labUtilMsgFree function's job to determine when the message is actually returned to the message pool. If one task never calls free on the message it received, that block will never be freed and that memory will be lost so it is imperative that all tasks free all messages they receive, even if they receive the message in error.
Final Thoughts
This wraps up the Task Communication and Architecture section of the Kernel 201 application. Moving forward the Bluetooth, Proprietary Wireless, LED and Button tasks will all be using this communication structure. If you have any questions about this post feel free to leave comments below.
Kernel 201: Task Architecture & Communication
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.
Task Architecture
The first step of designing any real-time system, before writing any code, is to identify all events the system will be responsible for. Listing out these actions will help identify what events may be grouped together in tasks and what actions will require their own task/s. There are a number of resources available that discuss different task modeling strategies. Some common strategies you may come across are:
For this application, these are the following events that must be handled and how they’ll be grouped into tasks:
labBluetoothTask
Silicon Labs’ Bluetooth stack already creates two tasks, Bluetooth and LinkLayer to handle the transmission and reception. The Bluetooth task provides callbacks for the user to implement to know when there is an event to process. The labBluetoothTask will handle those events from the Bluetooth task and pass that data along to the other tasks in the system. The labBluetoothTask will also receive data from other tasks in the system to transmit via Bluetooth.
labPropTask
Silicon Labs does not provide internal tasks for Proprietary Wireless (RAIL) as it does for Bluetooth, but it is important to group the RAIL functions together as they are not thread-safe functions. The labPropTask will handle all data received by RAIL and send it to other tasks in the system as needed. It will also receive data from other tasks in the system and send it out via RAIL.
labLEDTask
Since only the GPIO or RGB LEDs can be active at one time, it makes sense to group them together.
labBtnTask
The button task operates on a low priority polling interval. It could be switched to an interrupt based task but by leaving it as a polling task, it leaves open the option to add other polling events to the task in the future.
labWDOGTask
The watchdog task will implement a software based watchdog to control how the system feeds the hardware watchdog. It is important that this task have no other operations than handling the watchdog.
Task Communication
Now that the events have been divided up into their respective tasks, it is important to implement a consistent method for communication between tasks. There are again many different ways to implement task communication. Micrium OS Kernel provides Semaphores, Task Semaphores, Event Flags, Message Queues and Task Message Queues. While each of these kernel objects have various pros and cons, no one of these services is better than another; it just depends on the application and how it is using these kernel services.
For this application, each task (with the exception of the watchdog task) will be centered around a Task Message Queue and the entire application will use a sort-of layered messaging scheme, similar to the OSI model but much more simplified. The main loop of each task will pend on a Task Message Queue and wait for a message or timeout to determine its next action.
The image above shows the basic layout for all tasks in the system. The code snippet below shows some pseudocode for the task
By using the Task Message Queue, it allows a task to wait on three different types of events:
Timeouts
The software watchdog wants every task to check in on a specified rate to show it has not gone out to lunch. By specifying a timeout value in the Task Message Queue pend, the pend call will return with RTOS_ERR_TIMEOUT if a message is not received in the specified time. If a task expects a message in a set time interval, logic can also be added to the error check for RTOS_ERR_TIMEOUT to keep track of how many timeouts we receive in a row.
Events
Some tasks may have a hardware interrupt or software timer callback they need to handle in addition to processing messages from other tasks in the system. The msg_size data field in OSTaskQPend() provides a 32-bit value that is used as an identifier field for this task design. When a task receives a message, it will have a switch statement to handle the value of msg_size. In this application, all message identifiers can be found in an enum called LAB_Q_ID_e.
When an interrupt or software timer callback wants to send a message to the task it is associated with, it will make a call similar to the one below:
It is important to note that the LAB_Q_ID_e value is specified in the msg_size field and not the void* field. All messages sent must have a valid identifier in the msg_size field where the void* argument is optional. In this case, the void* argument is set to 0 as there is no extra data to send.
Lab Messages
These messages are how tasks communicate with each other in this application. Rather than having the Bluetooth or Proprietary Wireless tasks spend time configuring PWM timers to drive the RGB LEDs, it makes more sense to send the LED task a LED command to be processed. This also allows us to handle wireless communication at a higher priority than changing the LEDs or polling the button states. The wireless tasks may be affected if responses are not sent out at specific intervals where if the system has to wait an extra 10ms to toggle the LED, the user most likely won’t notice.
The lab messages follow a layered model where every message has the same header, the header defines what else is to follow in the packet. This application provides utility functions for getting, sending and freeing messages (refer to lab_util.c). There are advantages to these utility functions that will be discussed a little later. When you get/free a message you are actually pulling from a memory pool of fixed block sizes. All messages, regardless of how much of the block they use, are the same size. In the case of this application, all messages are 32 bytes in size. This means that if a task needs to send or receive a message larger than 32 bytes, either the size of every block in the system must be increased, multiple messages must be sent or modifications must be made to the utility functions to allow for allocation and freeing of different size blocks.
The struct above shows the lab message structure that all messages will start with. The ID field identifies what message will follow it and the type corresponds to if it’s a command, response, update or error message. The void* argument is used so you can cast the next layer to the end of the lab message struct.
When you wish to allocate a message to send, the code snippet above shows how you would use the labUtil function to allocate a message. The image below shows how the lab message fits into the allocated block.
After you configure the message ID and message type fields, you need to configure the next part of the block for the message ID you specified. For this example, let’s assume we’re changing the LEDs. LEDs are controlled by the LAB_LED_MSG_s struct. To accomplish this, you will cast the LAB_LED_MSG_s to p_data so when you enter information for the LED message, it goes into the correct location in lab message.
The code snippet above shows how to cast the p_data argument to the LED message structure. Now when you modify the fields of the LED message structure it will be aligned in the message block as shown below.
Once the full message is assembled, the message needs to be sent to the desired task/s. Some messages may only go to one task where others may need to be sent to multiple tasks. The lab utility function for sending offers flexibility for sending messages. Using the labUtilSend() function as shown below, you pass the pointer to the message block just configured and the task/s the message should be sent to.
In the case where one message is being sent to multiple tasks, the send function will keep track of the number of tasks it was sent to. This allows each task to call the free function after its processed the message, but the memory block is not actually freed until all tasks that received the message free it. To free a message all a task needs to do is make the following call:
It is the labUtilMsgFree function's job to determine when the message is actually returned to the message pool. If one task never calls free on the message it received, that block will never be freed and that memory will be lost so it is imperative that all tasks free all messages they receive, even if they receive the message in error.
Final Thoughts
This wraps up the Task Communication and Architecture section of the Kernel 201 application. Moving forward the Bluetooth, Proprietary Wireless, LED and Button tasks will all be using this communication structure. If you have any questions about this post feel free to leave comments below.