Struct padding alignment

  1. Các kiểu dùng struct
typedef struct {
    char a;
    uint8_t num;
    int type;
} node;
node td;
sizeof(td)  //8 

struct n {
    char header;
    uint16_t ID;
    char payload[255];
};
struct n ver;
sizeof(ver) //260 
 
2. Padding
node td;
sizeof(td)  //8 byte
typedef struct {
    char a;
    uint8_t num;
    padding[2];
    int type;
} node;
 
struct n ver;
sizeof(ver) //260 
  • Theo luật C/C++, tổng kích thước của một struct phải là bội số của kiểu dữ liệu có kích thước lớn nhất bên trong nó.

  • Kiểu dữ liệu lớn nhất trong struct của bạn là uint16_t (2 byte). Do đó, tổng kích thước struct phải chia hết cho 2.

  • Cùng tính tổng kích thước các thành phần: 1 (header) + 1 (padding) + 2 (ID) + 255 (pay) = 259 byte.

  • 259 không chia hết cho 2, trình biên dịch phải chèn thêm 1 byte rỗng ở cuối cùng để làm tròn tổng kích thước lên 260 byte.

struct n {
    char header;
    padding[1];
    uint16_t ID;
    char payload[255];
    padding[1];
};
 
3. Xóa bỏ padding. Căn chỉnh bộ nhớ (Memory Alignment)
Khi ép kiểu (map) một struct trực tiếp vào địa chỉ vật lý của thanh ghi, bắt buộc phải vô hiệu hóa tính năng tự động đệm byte (implicit padding) của trình biên dịch. Nếu không làm vậy, trình biên dịch có thể tự động chèn thêm byte vào giữa các biến để tối ưu tốc độ đọc/ghi, khiến cấu trúc struct bị lệch địa chỉ so với thanh ghi thực tế.
trình biên dịch sẽ bị ép buộc phải căn chỉnh bộ nhớ theo từng 1-byte. Điều này có nghĩa là tất cả các phần tử trong struct sẽ nằm liên tiếp nhau trong bộ nhớ. Các phần tử sẽ dính chặt vào nhau.
Với GCC / Clang (Thường dùng trong lập trình nhúng ARM, ESP32, AVR...):
struct __attribute__((packed)) MyRegister {
   uint8_t control_reg;
   uint8_t reserved[2];
   uint32_t data_reg;
};
Với MSVC, Keil C hoặc IAR:
#pragma pack(push, 1)
struct MyRegister {
    char header;
    uint8_t reserved[2];
    char payload[255];
};
#pragma pack(pop)
struct MyRegister ver2;
sizeof(ver2) //258 byte
 

Mặc dù pragma pack(1) giúp địa chỉ liên tiếp và tiết kiệm bộ nhớ, bạn cần lưu ý hai vấn đề sau khi lập trình nhúng hoặc hệ thống:

  1. Giảm tốc độ CPU: CPU (đặc biệt là dòng 32-bit hoặc 64-bit) tối ưu nhất khi đọc dữ liệu từ các địa chỉ chia hết cho 2 hoặc 4. Khi bạn ép ID (2 byte) nằm ở địa chỉ lẻ (ví dụ vị trí số 1), CPU có thể phải tốn 2 chu kỳ lệnh để đọc xong biến này (thay vì 1 chu kỳ nếu nó nằm ở địa chỉ chẵn).

  2. Nguy cơ lỗi HardFault (Crash chip): Một số dòng vi điều khiển cũ hoặc kiến trúc ARM Cortex-M0/M0+ không hỗ trợ đọc dữ liệu không căn lề (unaligned access). Nếu bạn cố tình đọc biến uint16_t hoặc uint32_t nằm ở địa chỉ lẻ, chương trình có thể bị treo ngay lập tức (gây ra lỗi phần cứng HardFault).

Lời khuyên: Chỉ nên dùng pragma pack(1) khi bạn cần map cấu trúc dữ liệu chuẩn xác với thanh ghi phần cứng hoặc với định dạng gói tin truyền qua mạng (TCP/UDP, Serial). Đối với các struct khai báo biến thông thường trong phần mềm, hãy để trình biên dịch tự động padding để đạt hiệu năng xử lý cao nhất.

 
4. Khi thanh ghi có 2 byte rỗng thì khai báo thêm gì vào struct
struct MyRegister {
   uint8_t control_reg; // Chiếm 1 byte
   uint8_t reserved[2]; // 2 byte rỗng ở giữa (chỉ để lấp chỗ trống)
   uint32_t data_reg; // Chiếm 4 byte
};
struct MyRegister {
   uint8_t control_reg;  // Chiếm 1 byte
   uint16_t reserved; // Chiếm 2 byte rỗng ở giữa
   uint32_t data_reg; // Chiếm 4 byte
};
 
Nhanvat

Ủng hộ chúng tôi

Nếu bạn thấy nội dung hữu ích, hãy ủng hộ chúng tôi một ly cà phê nhé

Buy Me A Coffee buymeacoffee.com/invencamp