2.4 Nhận Dữ Liệu Từ Nhiều Nguồn (Receiving Data From Multiple Sources)
Trong các thiết kế FreeRTOS, rất thường gặp trường hợp một task nhận dữ liệu từ nhiều nguồn. Task nhận cần biết dữ liệu đến từ đâu để quyết định cách xử lý. Một thiết kế đơn giản để đạt được điều này là xài một queue duy nhất để truyền các structure chứa cả giá trị dữ liệu (data value) và nguồn dữ liệu (data source), như minh họa trong Hình 2.4.

Hình 2.4. Viễn cảnh ví dụ mô phỏng các structure được gởi lên cùng một queue
Theo Hình 2.4:
- Một queue được tạo để chứa các structure có kiểu
Data_t. Structure này bao gồm một member giá trị dữ liệu (data value) và một member kiểuenumchỉ thị ý nghĩa hay nguồn gốc của dữ liệu (data source) được gởi tới queue trong một tin nhắn. - Một Controller task trung tâm thực hiện chức năng chính của hệ thống. Nó phải phản ứng với các input và thay đổi trạng thái hệ thống được giao tiếp với nó thông qua queue.
- Một CAN bus task được xài để đóng gói (encapsulate) chức năng giao tiếp CAN bus (CAN bus interfacing functionality). Khi CAN bus task nhận và giải mã một message, nó gởi message đã giải mã tới Controller task thông qua một
Data_tstructure. MembereDataIDcủa structure nói cho Controller task biết tên của dữ liệu là gì. Trong ví dụ này, đó là tốc độ động cơ. Member lDataValue của structure nói cho Controller task biết giá trị tốc độ động cơ thực tế. - Một Human Machine Interface (HMI) task được xài để đóng gói toàn bộ chức năng HMI (HMI functionality). Người vận hành máy có thể nhập lệnh hoặc truy vấn giá trị theo nhiều cách, và các thao tác này phải được phát hiện và phiên dịch bởi HMI task. Khi có lệnh mới được nhập, HMI task gởi lệnh đó vô Controller task bằng một
Data_tstructure. MembereDataIDcho Controller task biết dữ liệu là gì. Trong ví dụ này, đó là một giá trị set point mới. MemberlDataValuecho Controller task biết giá trị set point thực tế.
Ví dụ 2.2 Block khi gởi tới một queue, và gởi nhiều structure tới một queue
Ví dụ 2.2 cũng tương tự như Ví dụ 2.1, nhưng các độ ưu tiên task đảo ngược lại, task nhận (receiving task) có độ ưu tiên thấp hơn các task gởi (sending task). Đồng thời thì queue sẽ chứa các cấu trúc (structure) chứ không phải các số nguyên (integer).
Code Block 2.9 định nghĩa structure xài trong Ví dụ 2.2.
/* Define an enumerated type used to identify the source of the data. */
typedef enum
{
eSender1,
eSender2
} DataSource_t;
/* Define the structure type that will be passed on the queue. */
typedef struct
{
uint8_t ucValue;
DataSource_t eDataSource;
} Data_t;
/* Declare two variables of type Data_t that will be passed on the queue. */
static const Data_t xStructsToSend[ 2 ] =
{
{ 100, eSender1 }, /* Used by Sender1. */
{ 200, eSender2 } /* Used by Sender2. */
};CCode Block 2.9. Định nghĩa của structure được truyền lên queue, và khai báo hai biến để xài trong ví dụ.
Trong Ví dụ 2.1, task nhận có độ ưu tiên cao nhất, nên queue không bao giờ chứa nhiều hơn một item. Đây là do task nhận sẽ chiếm quyền thực thi (pre-empt) của các task gởi ngay khi dữ liệu được đặt vô queue. Trong Ví dụ 2.2, các task gởi có độ ưu tiên cao hơn, nên queue thường xuyên đầy. Nguyên nhân là ngay khi task nhận lấy một item ra khỏi queue, nó sẽ bị chiếm quyền bởi một trong các task gởi, và task gởi đó sẽ ngay lập tức ghi dữ liệu vô lại queue (làm đầy queue). Sau đó, task gởi sẽ vô trạng thái Blocked để chờ chỗ trống xuất hiện trong queue một lần nữa.
Code Block 2.10 là cách triển khai task gởi. Task gởi (sending task) chỉ định thời gian block là 100 mili giây, nên nó sẽ vô trạng thái Blocked để chờ chỗ trống xuất hiện mỗi khi queue đầy. Task sẽ rời khỏi trạng thái Blocked khi có chỗ trống trong queue, hoặc là sau khi 100 mili giây trôi qua mà không có chỗ trống. Trong ví dụ này, task nhận liên tục tạo chỗ trống trong queue, nên thời gian chờ 100 mili giây sẽ không bao giờ hết hạn.
static void vSenderTask( void *pvParameters )
{
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );
/* As per most tasks, this task is implemented within an infinite loop. */
for( ;; )
{
/* Send to the queue.
The second parameter is the address of the structure being sent. The
address is passed in as the task parameter so pvParameters is used
directly.
The third parameter is the Block time - the time the task should be
kept in the Blocked state to wait for space to become available on
the queue if the queue is already full. A block time is specified
because the sending tasks have a higher priority than the receiving
task so the queue is expected to become full. The receiving task
will remove items from the queue when both sending tasks are in the
Blocked state. */
xStatus = xQueueSendToBack( xQueue, pvParameters, xTicksToWait );
if( xStatus != pdPASS )
{
/* The send operation could not complete, even after waiting for
100ms. This must be an error as the receiving task should make
space in the queue as soon as both sending tasks are in the
Blocked state. */
vPrintString( "Could not send to the queue.\r\n" );
}
}
}CCode Block 2.10. Task gởi trong Ví dụ 2.2.
Task nhận (receiving task) có độ ưu tiên thấp nhất nên nó chỉ chạy khi cả hai task gởi đều đang ở trạng thái Blocked. Các task gởi chỉ vô trạng thái Blocked khi queue bị đầy, vì vậy task nhận sẽ chỉ thực thi khi queue đã đầy. Do đó, nó luôn kỳ vọng sẽ nhận được dữ liệu ngay cả khi nó không chỉ định một thời gian block cụ thể.
Code Block 2.11 là cách triển khai task nhận.
static void vReceiverTask( void *pvParameters )
{
/* Declare the structure that will hold the values received from the
queue. */
Data_t xReceivedStructure;
BaseType_t xStatus;
/* This task is also defined within an infinite loop. */
for( ;; )
{
/* Because it has the lowest priority this task will only run when the
sending tasks are in the Blocked state. The sending tasks will only
enter the Blocked state when the queue is full so this task always
expects the number of items in the queue to be equal to the queue
length, which is 3 in this case. */
if( uxQueueMessagesWaiting( xQueue ) != 3 )
{
vPrintString( "Queue should have been full!\r\n" );
}
/* Receive from the queue.
The second parameter is the buffer into which the received data will
be placed. In this case the buffer is simply the address of a
variable that has the required size to hold the received structure.
The last parameter is the block time - the maximum amount of time
that the task will remain in the Blocked state to wait for data to
be available if the queue is already empty. In this case a block
time is not necessary because this task will only run when the
queue is full. */
xStatus = xQueueReceive( xQueue, &xReceivedStructure, 0 );
if( xStatus == pdPASS )
{
/* Data was successfully received from the queue, print out the
received value and the source of the value. */
if( xReceivedStructure.eDataSource == eSender1 )
{
vPrintStringAndNumber( "From Sender 1 = ",
xReceivedStructure.ucValue );
}
else
{
vPrintStringAndNumber( "From Sender 2 = ",
xReceivedStructure.ucValue );
}
}
else
{
/* Nothing was received from the queue. This must be an error as
this task should only run when the queue is full. */
vPrintString( "Could not receive from the queue.\r\n" );
}
}
}CCode Block 2.11. Task nhận trong Ví dụ 2.2.
Hàm main() chỉ cần thay đổi một chút so với ví dụ trước. Queue được tạo để giữ 3 Data_t structure, và độ ưu tiên của các task gởi và nhận bị đảo lại. Code Block 2.12 là nội dung của hàm main().
int main( void )
{
/* The queue is created to hold a maximum of 3 structures of type Data_t. */
xQueue = xQueueCreate( 3, sizeof( Data_t ) );
if( xQueue != NULL )
{
/* Create two instances of the task that will write to the queue. The
parameter is used to pass the structure that the task will write to
the queue, so one task will continuously send xStructsToSend[ 0 ]
to the queue while the other task will continuously send
xStructsToSend[ 1 ]. Both tasks are created at priority 2, which is
above the priority of the receiver. */
xTaskCreate( vSenderTask, "Sender1", 1000, &( xStructsToSend[ 0 ] ),
2, NULL );
xTaskCreate( vSenderTask, "Sender2", 1000, &( xStructsToSend[ 1 ] ),
2, NULL );
/* Create the task that will read from the queue. The task is created
with priority 1, so below the priority of the sender tasks. */
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
}
else
{
/* The queue could not be created. */
}
/* If all is well then main() will never reach here as the scheduler will
now be running the tasks. If main() does reach here then it is likely
that there was insufficient heap memory available for the idle task tobe created. Chapter 3 provides more information on heap memory
management. */
for( ;; );
}CCode Block 2.12. Nội dung của hàm main() trong Ví dụ 2.2
Hình 2.5 cho thấy output tạo ra bởi Ví dụ 2.2.

Hình 2.5. Output của Ví dụ 2.2
Hình 2.6 vẽ lưu đồ thực thi khi độ ưu tiên của các task gởi (Sender 1, Sender 2) cao hơn độ ưu tiên của task nhận (Receiver). Giải thích thêm, Hình 2.6 mô tả lý do tại sao bốn message đầu tiên đều xuất phát từ cùng một task (Sender 1).

Hình 2.6. Lưu đồ thực của Ví dụ 2.2
Chú thích Hình 5.6
- t1:
Task Sender 1 thực thi và gởi 3 gói dữ liệu vô queue. - t2:
Queue bị đầy nên Sender 1 vô trạng thái Blocked để chờ lần gởi kế tiếp của nó hoàn tất. Lúc này, Task Sender 2 trở thành task có độ ưu tiên cao nhất có thể chạy, nên nó vô trạng thái Running. - t3:
Task Sender 2 phát hiện queue đã đầy, nên nó vô trạng thái Blocked để chờ lần gởi đầu tiên hoàn tất. Lúc này, Task Receiver trở thành task có độ ưu tiên cao nhất có thể chạy, nên nó vô trạng thái Running. - t4:
Hai task có độ ưu tiên cao hơn task nhận đang chờ chỗ trống xuất hiện trong queue, dẫn đến việc Task Receiver bị chiếm quyền ngay khi nó vừa lấy một item ra khỏi queue. Task Sender 1 và Task Sender 2 có cùng độ ưu tiên, nên scheduler chọn task đã chờ lâu nhất để vô trạng thái Running — trong trường hợp này là Task Sender 1. - t5:
Task Sender 1 gởi thêm một gói dữ liệu vô queue. Vì chỉ có một chỗ trống trong queue, Sender 1 vô trạng thái Blocked để chờ lần gởi kế tiếp của nó hoàn tất. Task Receiver lại trở thành task có độ ưu tiên cao nhất có thể chạy, nên nó vô trạng thái Running. Lúc này, Task Sender 1 đã gởi bốn item vô queue, còn Task Sender 2 vẫn đang chờ để gởi item đầu tiên.
- t6:
Hai task có độ ưu tiên cao hơn task nhận vẫn đang chờ chỗ trống trong queue, nên Task Receiver bị chiếm quyền ngay khi nó vừa lấy một item ra khỏi queue. Lần này, Sender 2 đã chờ lâu hơn Sender 1, nên Sender 2 vô trạng thái Running. - t7:
Task Sender 2 gởi một gói dữ liệu vô queue. Vì chỉ có một chỗ trống trong queue, nên Sender 2 vô trạng thái Blocked để chờ lần gởi kế tiếp của nó hoàn tất. Cả Sender 1 và Sender 2 đều đang chờ chỗ trống trong queue, nên Task Receiver là task duy nhất có thể vô trạng thái Running.
2.5 Làm Việc Với Dữ Liệu Có Kích Thước Lớn Hoặc Thay Đổi
2.5.1 Con Trỏ Hàng Đợi (Queuing Pointers)
Nếu kích thước dữ liệu lưu trong queue lớn, thì nên xài queue để truyền những con trỏ (pointer) trỏ đến dữ liệu thay vì copy toàn bộ dữ liệu vô và ra khỏi queue theo từng byte một. Cách truyền con trỏ hiệu quả hơn cả về thời gian xử lý lẫn lượng RAM cần thiết để tạo queue. Tuy nhiên, khi queuing pointers, cần cực kỳ cẩn thận để đảm bảo rằng:
- Chủ sở hữu (owner) của vùng RAM mà đang trỏ đến phải được định nghĩa (define) rõ ràng.
Khi chia sẻ bộ nhớ giữa các task thông qua một con trỏ, điều quan trọng là phải đảm bảo cả hai task không cùng lúc sửa đổi nội dung bộ nhớ, hoặc thực hiện bất kỳ hành động nào có thể khiến nội dung bộ nhớ trở nên không hợp lệ (invalid) hoặc không nhất quán (inconsistent). Lý tưởng nhất là chỉ task gởi được phép truy cập bộ nhớ trước khi con trỏ được gởi vào queue, và chỉ task nhận được phép truy cập bộ nhớ sau khi con trỏ đã được lấy ra khỏi queue. - Vùng RAM mà đang trỏ đến phải luôn hợp lệ.
Nếu bộ nhớ đang trỏ tới đã được cấp phát động hoặc lấy từ một pool các buffer đã cấp phát sẵn, thì chỉ duy nhất một task phải chịu trách nhiệm giải phóng bộ nhớ đó. Không có task nào được phép truy cập bộ nhớ sau khi nó đã bị giải phóng.
Một con trỏ không bao giờ được dùng để truy cập dữ liệu đã được cấp phát trên stack của một task. Dữ liệu này sẽ không còn hợp lệ sau khi stack frame bị đổi.
Ví dụ, Code Block 2.13, 2.14 và 2.15 minh họa cách dùng queue để gởi một con trỏ trỏ tới buffer từ task này sang task khác:
- Code Block 2.13 tạo một queue có thể chứa tối đa 5 con trỏ.
- Code Block 2.14 cấp phát một buffer, ghi một chuỗi vào buffer, rồi gởi con trỏ trỏ tới buffer đó vô queue.
- Code Block 2.15 nhận một con trỏ trỏ tới buffer từ queue, sau đó in ra chuỗi chứa trong buffer.
/* Declare a variable of type QueueHandle_t to hold the handle of the
queue being created. */
QueueHandle_t xPointerQueue;
/* Create a queue that can hold a maximum of 5 pointers, in this case
character pointers. */
xPointerQueue = xQueueCreate( 5, sizeof( char * ) );CCode Block 2.13. Tạo một queue để chứa các con trỏ
/* A task that obtains a buffer, writes a string to the buffer, then
sends the address of the buffer to the queue created in Code Block 2.13. */
void vStringSendingTask( void *pvParameters )
{
char *pcStringToSend;
const size_t xMaxStringLength = 50;
BaseType_t xStringNumber = 0;
for( ;; )
{
/* Obtain a buffer that is at least xMaxStringLength characters big.
The implementation of prvGetBuffer() is not shown – it might obtain
the buffer from a pool of pre-allocated buffers, or just allocate
the buffer dynamically. */
pcStringToSend = ( char * ) prvGetBuffer( xMaxStringLength );
/* Write a string into the buffer. */
snprintf( pcStringToSend, xMaxStringLength, "String number %d\r\n",
xStringNumber );
/* Increment the counter so the string is different on each iteration
of this task. */
xStringNumber++;
/* Send the address of the buffer to the queue that was created in
Code Block 2.13. The address of the buffer is stored in the
pcStringToSend variable.*/
xQueueSend( xPointerQueue, /* The handle of the queue. */
&pcStringToSend, /* The address of the pointer that points
to the buffer. */
portMAX_DELAY );
}
}CCode Block 2.14. Xài một queue để gởi một pointer trỏ tới một buffer
/* A task that receives the address of a buffer from the queue created
in Code Block 2.13, and written to in Code Block 2.14. The buffer contains a
string, which is printed out. */
void vStringReceivingTask( void *pvParameters )
{
char *pcReceivedString;
for( ;; )
{
/* Receive the address of a buffer. */
xQueueReceive( xPointerQueue, /* The handle of the queue. */
&pcReceivedString, /* Store the buffer's address in
pcReceivedString. */
portMAX_DELAY );
/* The buffer holds a string, print it out. */
vPrintString( pcReceivedString );
/* The buffer is not required any more - release it so it can be freed,
or re-used. */
prvReleaseBuffer( pcReceivedString );
}
}CCode Block 2.15. Xài một queue để nhận một pointer trỏ tới một buffer
2.5.2 Dùng Queue Để Gởi Các Loại Dữ Liệu Và Độ Dài Dữ Liệu Khác Nhau (Using a Queue to Send Different Types and Lengths of Data)
(Message buffers trong FreeRTOS là một thay thế gọn nhẹ hơn so với queue, được xài để chứa dữ liệu có độ dài thay đổi).
Hai phần trước vừa giới thiệu hai mẫu thiết kế mạnh mẽ: gởi struct vô queue và gởi con trỏ vô queue. Kết hợp hai kỹ thuật này cho phép một task dùng một queue duy nhất để nhận bất kỳ kiểu dữ liệu nào (data type) từ bất kỳ nguồn dữ liệu nào (data source). Cách thiết kế FreeRTOS+TCP TCP/IP stack là một ví dụ thực tế cho phương pháp này.
TCP/IP stack chạy trong một task riêng, nó phải xử lý các sự kiện đến từ nhiều nguồn khác nhau. Mỗi loại sự kiện (event type) phối hợp với kiểu dữ liệu (data type) và độ dài dữ liệu (data length) khác nhau. IPStackEvent_t structure mô tả tất cả sự kiện xảy ra bên ngoài TCP/IP task, và được gởi vào task TCP/IP thông qua một queue. Code Block 2.16 là cấu trúc IPStackEvent_t. pvData member của IPStackEvent_t structure là một con trỏ, có thể giữ trực tiếp một giá trị, hoặc trỏ đến một buffer.
/* A subset of the enumerated types used in the TCP/IP stack to identify events. */
typedef enum
{
eNetworkDownEvent = 0, /* The network interface has been lost, or needs
(re)connecting. */
eNetworkRxEvent, /* A packet has been received from the network. */
eTCPAcceptEvent, /* FreeRTOS_accept() called to accept or wait for a
new client. */
/* Other event types appear here but are not shown in this listing. */
} eIPEvent_t;
/* The structure that describes events, and is sent on a queue to the
TCP/IP task. */
typedef struct IP_TASK_COMMANDS
{
eIPEvent_t eEventType; /* An enumerated type that identifies the event.
See the eIPEvent_t definition above. */
void *pvData; /* A generic pointer that can hold a value,
or point to a buffer. */
} IPStackEvent_t;CCode Block 2.16. Structure dùng để gởi event tới TCP/IP stack task trong FreeRTOS+TCP
Ví dụ các TCP/IP event và dữ liệu liên quan:
eNetworkRxEvent: Một gói dữ liệu đã được nhận từ network. Giao diện mạng (network interface) gởi sự kiện nhận dữ liệu đến TCP/IP task bằng một cấu trúc kiểuIPStackEvent_t. Thành viêneEventTypecủa cấu trúc được gán giá trịeNetworkRxEvent, và thành viênpvDatacủa cấu trúc được dùng để trỏ đến buffer chứa dữ liệu nhận được. Code Block 2.17 là một ví dụ bằng mã giả (pseudo code).
void vSendRxDataToTheTCPTask( NetworkBufferDescriptor_t *pxRxedData )
{
IPStackEvent_t xEventStruct;
/* Complete the IPStackEvent_t structure. The received data is stored in
pxRxedData. */
xEventStruct.eEventType = eNetworkRxEvent;
xEventStruct.pvData = ( void * ) pxRxedData;
/* Send the IPStackEvent_t structure to the TCP/IP task. */
xSendEventStructToIPTask( &xEventStruct );
}CCode Block 2.17 Mã giả cách xài một IPStackEvent_t structure để gởi dữ liệu nhận được từ network cho TCP/IP task
eTCPAcceptEvent: Một socket sẽ chấp nhận (accept), hoặc chờ (wait), một kết nối từ một client. Task gọiFreeRTOS_accept()sẽ gởi sự kiện accept đến TCP/IP task bằng một cấu trúc kiểuIPStackEvent_t. Thành viêneEventTypecủa cấu trúc được gán giá trịeTCPAcceptEvent, và thành viênpvDatađược gán bằng handle của socket đang chấp nhận kết nối. Code Block 2.18 là một ví dụ bằng mã giả (pseudo code).
void vSendAcceptRequestToTheTCPTask( Socket_t xSocket )
{
IPStackEvent_t xEventStruct;
/* Complete the IPStackEvent_t structure. */
xEventStruct.eEventType = eTCPAcceptEvent;
xEventStruct.pvData = ( void * ) xSocket;
/* Send the IPStackEvent_t structure to the TCP/IP task. */
xSendEventStructToIPTask( &xEventStruct );
}CCode Block 2.18 Mã giả cách xài một IPStackEvent_t structure để gởi handle của một socket, đang accept một kết nối, tới TCP/IP task
eNetworkDownEvent: Network cần kết nối, hay cần kết nối lại. Network interface gởi sự kiện rớt mạng (network down event) tới TCP/IP task bằng một structure có kiểuIPStackEvent_t. Thành viêneEventTypeđược gán giá trịeNetworkDownEvent. Sự kiện rớt mạng không liên quan tới dữ liệu, nên thành viênpvDatakhông cần xài. Code Block 2.19 là một ví dụ bằng mã giả (pseudo code).
void vSendNetworkDownEventToTheTCPTask( Socket_t xSocket )
{
IPStackEvent_t xEventStruct;
/* Complete the IPStackEvent_t structure. */
xEventStruct.eEventType = eNetworkDownEvent;
xEventStruct.pvData = NULL; /* Not used, but set to NULL for
completeness. */
/* Send the IPStackEvent_t structure to the TCP/IP task. */
xSendEventStructToIPTask( &xEventStruct );
}CCode Block 2.19 Mã giả cách xài một IPStackEvent_t structure để gởi một sự kiện rớt mạng tới TCP/IP task
Code Block 2.20 là một đoạn code nhận và xử lý các sự kiện bên trong TCP/IP task. Có thể thấy rằng thành viên eEventType, của cấu trúc IPStackEvent_t nhận từ queue, được dùng để xác định thành viên pvData và chọn cách xử lý tương ứng.
IPStackEvent_t xReceivedEvent;
/* Block on the network event queue until either an event is received, or
xNextIPSleep ticks pass without an event being received. eEventType is
set to eNoEvent in case the call to xQueueReceive() returns because it
timed out, rather than because an event was received. */
xReceivedEvent.eEventType = eNoEvent;
xQueueReceive( xNetworkEventQueue, &xReceivedEvent, xNextIPSleep );
/* Which event was received, if any? */
switch( xReceivedEvent.eEventType )
{
case eNetworkDownEvent :
/* Attempt to (re)establish a connection. This event is not
associated with any data. */
prvProcessNetworkDownEvent();
break;
case eNetworkRxEvent:
/* The network interface has received a new packet. A pointer to the
received data is stored in the pvData member of the received
IPStackEvent_t structure. Process the received data. */
prvHandleEthernetPacket( ( NetworkBufferDescriptor_t * )
( xReceivedEvent.pvData ) );
break;
case eTCPAcceptEvent:
/* The FreeRTOS_accept() API function was called. The handle of the
socket that is accepting a connection is stored in the pvData
member of the received IPStackEvent_t structure. */
xSocket = ( FreeRTOS_Socket_t * ) ( xReceivedEvent.pvData );
xTCPCheckNewClient( xSocket );
break;
/* Other event types are processed in the same way, but are not shown here. */
}CCode Block 2.20 Mã giả cách nhận một IPStackEvent_t structure và xử lý