[Tự Học FreeRTOS] Chapter 2: Queue Management (Phần 2)

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ểu enum chỉ 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_t structure. Member eDataID củ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_t structure. Member eDataID cho Controller task biết dữ liệu là gì. Trong ví dụ này, đó là một giá trị set point mới. Member lDataValue cho 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. */
};
C

Code 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" );
        }
    }
}
C

Code 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" );
        }
    }
}
C

Code 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( ;; );
}
C

Code 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 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 * ) );
C

Code 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 );
    }
}
C

Code 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 );
    }
}
C

Code 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;
C

Code 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ểu IPStackEvent_t. Thành viên eEventType của cấu trúc được gán giá trị eNetworkRxEvent, và thành viên pvData củ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 );
}
C

Code 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ọi FreeRTOS_accept() sẽ gởi sự kiện accept đến TCP/IP task bằng một cấu trúc kiểu IPStackEvent_t. Thành viên eEventType của cấu trúc được gán giá trị eTCPAcceptEvent, và thành viên pvData đượ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 );
}
C

Code 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ểu IPStackEvent_t. Thành viên eEventType đượ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ên pvData khô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 );
}
C

Code 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. */
}
C

Code Block 2.20 Mã giả cách nhận một IPStackEvent_t structure và xử lý