🌱 Embedded C - User-Defined Data Type - Kiểu dữ liệu Union

🌱 Embedded C - User-Defined Data Type - Kiểu dữ liệu Union

    Trong lập trình Embedded C, việc quản lý và tối ưu hóa tài nguyên, đặc biệt là bộ nhớ, luôn là ưu tiên hàng đầu. Đây là lúc các cấu trúc dữ liệu đặc biệt như Union phát huy tác dụng. Tuy nhiên, việc sử dụng Union đôi khi cũng gây nhầm lẫn nếu không hiểu rõ nguyên tắc hoạt động của nó.

    Bài viết này sẽ đi sâu vào Union trong Embedded C, từ khái niệm cơ bản, cách sử dụng, đến các ứng dụng thực tế và những lưu ý quan trọng để bạn có thể tận dụng tối đa cấu trúc dữ trúc này trong các dự án nhúng của mình.


Mục Lục


Union Là Gì và Tại sao cần Union?

    Union là một kiểu dữ liệu User-Defined trong C/C++ cho phép lưu trữ các thành viên khác nhau tại cùng một vị trí bộ nhớ.

    Điều này có nghĩa là, tại bất kỳ thời điểm nào, chỉ có một thành viên duy nhất của Union có thể được sử dụng hoặc có giá trị hợp lệ. Kích thước của Union sẽ bằng kích thước của thành viên lớn nhất trong nó.     Hãy hình dung Union như một chiếc hộp đa năng: bạn có thể để một quyển sách vào đó, hoặc một chiếc điện thoại, nhưng không thể để cả hai cùng lúc. Kích thước của chiếc hộp sẽ được thiết kế để chứa vật lớn nhất (ví dụ: quyển sách).

Tại sao cần có Union?

    Hãy cùng phân tích nhanh ví dụ sau để hiểu về vai trò của Union.

Bài toán truyền dữ liệu Float

    Giả sử cần thu thập dữ liệu cảm biến từ một Vi điều khiển (MCU1) và truyền sang một Vi điều khiển khác (MCU2) qua giao thức UART. Dữ liệu truyền đi là các số thực (float), giả sử làm tròn đến 3 chữ số thập phân.

    Một phương án thông thường bạn hay gặp, đó là MCU1 sẽ chuyển số float đó thành dạng string, sau đó truyền từng ký tự sang MCU2 và chuyển ngược lại.

  • Trên MCU1, chuyển đổi số thực thành chuỗi ký tự. Ví dụ: số 123.456 sẽ được chuyển thành chuỗi "123.456"
  • Truyền chuỗi này qua các giao thức như UART
  • Trên MCU2, nhận chuỗi và chuyển đổi ngược lại thành số thực

    Dễ thấy cách làm này dẫn đến vấn đề Tốn tài nguyên (bộ nhớ và thời gian xử lý) cho việc chuyển đổi giữa số thực và chuỗi.

    ➥ Cách tối ưu là truyền từng byte một mà không cần xử lý gì cả! float có 4 byte, MCU1 sẽ truyền từng byte, MCU sẽ nhận từng byte và sau đó có thể sắp xếp các byte để tạo thành số float mà không mất bất kỳ thao tác nào!

    ↪ Và đó chính là lúc Union phát huy tác dụng, nơi dev có thể nhìn thấy số float và 4 byte vào cùng một vùng nhớ.

    Xem ví dụ Bài toán truyền dữ liệu Float sử dụng Union!

Cách sử dụng Union

Cú pháp khai báo Union

union Union_Name {
    datatype element1;
    datatype element2;
    // ...
    datatype elementN;
};

    Cũng giống như Struct, các thành phần bên trong Union có thể là tất cả các kiểu dữ liệu khác, kể cả struct hay union. Và Union cũng có thể sử dụng từ khóa typedef khi khai báo giống hệt như Struct.

    ↪ Ví dụ:

union Data {
    int A;
    float B;
    char str[20];
};

    Trong ví dụ này, Data là một Union chứa một số nguyên A, một số thực B, và một mảng ký tự str. Kích thước của Data sẽ bằng kích thước của thành viên lớn nhất, trong trường hợp này là char str[20] (20 bytes).

Truy cập các phần tử của Union

    Tương tự như Struct, chúng ta sử dụng toán tử dấu chấm (.) hoặc toán tử mũi tên (->) để truy cập các phần tử của Union.

  1. union PacketData {
  2. uint8_t rawBytes[8];
  3. struct {
  4. uint16_t header;
  5. uint16_t payloadLength;
  6. uint32_t checksum;
  7. } parsedData;
  8. float sensorValue;
  9. };
  10. union PacketData packet;
  11. // Example 1: Assigning a value to the parsedData member
  12. packet.parsedData.header = 0xAA55;
  13. packet.parsedData.payloadLength = 64;
  14. packet.parsedData.checksum = 0x12345678;
  15. // Example 2: After assigning parsedData, accessing rawBytes will not make sense
  16. // and may result in unwanted data
  17. // printf("Raw Byte 0: %02X\n", packet.rawBytes[0]); // This value will be the low byte of the header
  18. // Example 3: Assigning a value to sensorValue
  19. packet.sensorValue = 3.14f;
  20. // After assigning sensorValue, other members (parsedData, rawBytes)
  21. // will contain data overwritten by sensorValue
  22. // printf("Header after sensorValue: %X\n", packet.parsedData.header); // Data is no longer valid

Lưu ý: Khi bạn ghi dữ liệu vào một thành viên của Union, dữ liệu của các thành viên khác bị ghi đè. Do đó, bạn phải tự quản lý để luôn biết thành viên nào đang chứa dữ liệu hợp lệ.

Sự Khác Biệt Giữa Union và Struct

C Programming Union Data Type

    Sự khác biệt về Struct và Union chỉ đơn giản ở việc sắp xếp các phần tử trên bộ nhớ, và về kích thước của kiểu dữ liệu

     Sự khác biệt giữa Struct và Union

Tuyệt vời! Union là một chủ đề rất quan trọng trong Embedded C. Tôi sẽ giúp bạn viết bài blog về "Union trong Embedded C: Nắm vững cấu trúc dữ liệu tiết kiệm bộ nhớ" theo văn phong và cấu trúc bạn đã cung cấp.

Ưu và Nhược Điểm Khi Sử Dụng Union

Ưu điểm:

  • Tiết kiệm bộ nhớ: Lợi ích lớn nhất, đặc biệt quan trọng trong các hệ thống nhúng có RAM và ROM hạn chế.

  • Linh hoạt trong truy cập dữ liệu: Cho phép xem cùng một dữ liệu dưới nhiều kiểu khác nhau, hữu ích cho việc parsing dữ liệu nhị phân hoặc giao tiếp phần cứng.

  • Cải thiện hiệu suất (đôi khi): Trong một số trường hợp, việc truy cập toàn bộ word thay vì từng byte riêng lẻ có thể nhanh hơn.

Nhược điểm:

  • Rủi ro về dữ liệu: Yêu cầu người lập trình phải tự quản lý và biết thành viên nào đang hợp lệ. Nếu ghi vào một thành viên rồi đọc từ một thành viên khác mà không hiểu rõ, sẽ dẫn đến dữ liệu không chính xác hoặc lỗi khó gỡ lỗi (undefined behavior).

  • Tính rõ ràng giảm: Mã nguồn có thể khó đọc và hiểu hơn nếu Union được sử dụng không cẩn thận.

  • Vấn đề Endianness: Khi Union được dùng để chuyển đổi giữa byte array và các kiểu dữ liệu lớn hơn (như int hay short), thứ tự byte (endianness) của hệ thống có thể gây ra kết quả không mong muốn nếu không được tính toán trước.

Ví dụ Ứng Dụng Của Union Trong Embedded

    Union đặc biệt hữu ích trong các tình huống sau trong lập trình nhúng:

① Tiết kiệm bộ nhớ

    Đây là ứng dụng nổi bật nhất. Khi bạn có nhiều loại dữ liệu khác nhau nhưng tại một thời điểm chỉ cần sử dụng một trong số chúng, Union là lựa chọn tối ưu.

    Ví dụ: Một biến trạng thái có thể là một byte, một word, hoặc một cờ bit tùy theo ngữ cảnh, nhưng không bao giờ cả ba cùng lúc.

② Truy cập dữ liệu theo nhiều cách khác nhau (Type Punning)

    Union cho phép bạn xem cùng một vùng bộ nhớ dưới nhiều kiểu dữ liệu khác nhau. Điều này rất hữu ích khi làm việc với giao thức truyền thông hoặc xử lý dữ liệu cấp thấp.

    Ví dụ: Chuyển đổi giữa byte array và cấu trúc dữ liệu cụ thể khi nhận/gửi gói tin qua UART/SPI/I2C.

// Example: Read 10-bit ADC value (2 bytes) and store it in a 16-bit variableư
union AdcValue {
    uint16_t fullValue;
    struct {
        uint8_t lowByte;
        uint8_t highByte;
    } bytes;
};

void readADC(void) {
    union AdcValue adc;
    // Suppose low byte is read first, high byte is read later
    adc.bytes.lowByte = read_adc_low_byte_register();
    adc.bytes.highByte = read_adc_high_byte_register();

    // Now 16-bit values ​​can be accessed
    printf("ADC Value: %u\n", adc.fullValue);
}

Tổ chức thanh ghi vi điều khiển

    Một số thanh ghi điều khiển có các bit/trường bit khác nhau nhưng lại có thể được truy cập như một word hoàn chỉnh để ghi/đọc nhanh hơn. Union giúp mô phỏng cấu trúc này, kết hợp với khái niệm Struct Bitfield.

/**
  \brief  Union type to access the Application Program Status Register (APSR).
 */
typedef union
{
  struct
  {
    uint32_t _reserved0:27;              /*!< bit:  0..26  Reserved */
    uint32_t Q:1;                        /*!< bit:     27  Saturation condition flag */
    uint32_t V:1;                        /*!< bit:     28  Overflow condition code flag */
    uint32_t C:1;                        /*!< bit:     29  Carry condition code flag */
    uint32_t Z:1;                        /*!< bit:     30  Zero condition code flag */
    uint32_t N:1;                        /*!< bit:     31  Negative condition code flag */
  } b;                                   /*!< Structure used for bit  access */
  uint32_t w;                            /*!< Type      used for word access */
} APSR_Type;

Tạo các biến cờ (flags) bitwise

    Khi bạn cần quản lý một tập hợp các cờ trạng thái mà mỗi cờ chiếm một bit riêng biệt trong một byte hoặc word, Union có thể được kết hợp với Struct để tạo ra một cách truy cập rõ ràng và hiệu quả.

union DeviceStatus {
    uint8_t allFlags; 		// Access all flags as 1 byte
    struct {
	unsigned int isReady : 1; // Flag 0: Device ready
		unsigned int isError : 1; // Flag 1: Error occurred
		unsigned int isTransmitting : 1; // Flag 2: Data is being transmitted
		unsigned int reserved : 5; // Reserved bits
    } individualFlags; 		// Access each flag individually
};

// Usage
union DeviceStatus status;
status.allFlags = 0x01;   // Set isReady bit = 1
if (status.individualFlags.isReady) {
    // Device ready
}

    ➤ Ví dụ: Union quản lý trạng thái nhiều thiết bị

Kết Luận

    Union là một công cụ mạnh mẽ và linh hoạt trong Embedded C, cho phép các lập trình viên tối ưu hóa việc sử dụng bộ nhớ và truy cập dữ liệu một cách sáng tạo. Tuy nhiên, đi kèm với sức mạnh là trách nhiệm: việc sử dụng Union đòi hỏi sự hiểu biết sâu sắc về cách bộ nhớ hoạt động và kỷ luật trong việc quản lý trạng thái của dữ liệu.

    Khi được sử dụng đúng cách, Union có thể giúp bạn giải quyết các thách thức về tài nguyên trong hệ thống nhúng, từ việc phân tích gói tin mạng đến việc quản lý các thanh ghi phần cứng. Hãy nắm vững cấu trúc dữ liệu này để đưa kỹ năng lập trình Embedded C của bạn lên một tầm cao mới!

>>>>>> Follow ngay <<<<<<<

Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊

Nguyễn Văn Nghĩa

Mình là một người thích học hỏi và chia sẻ các kiến thức về Nhúng IOT.

Đăng nhận xét

Mới hơn Cũ hơn
//