Union và bit field cung cấp một cách tiếp cận trực quan và hiệu quả để quản lý các thao tác bit trong thanh ghi. Chúng cho phép bạn xem một khối bộ nhớ dưới nhiều góc độ khác nhau: như một giá trị thô hoặc như một tập hợp các trường bit riêng lẻ.
1. Phân biệt Endianness bằng Union
Union có thể được sử dụng để xác định hệ thống đang sử dụng Big Endian hay Little Endian. Bằng cách định nghĩa một union bao gồm một thành viên là kiểu dữ liệu nguyên bản (ví dụ: unsigned int) và một thành viên là cấu trúc chứa các bit field, chúng ta có thể kiểm tra cách dữ liệu được sắp xếp trong bộ nhớ.
#define READ_AND_EXTRACT(stc, field) ({ \
stc.raw = 0x12345678; \
stc.bits.field; \
})
union DataRepresentation {
unsigned int raw;
struct {
unsigned int segment1 : 8; // Least significant byte
unsigned int segment2 : 8;
unsigned int segment3 : 5;
unsigned int segment4 : 3;
unsigned int segment5 : 1; // Most significant bit of segment5
} bits;
};
int main(void) {
union DataRepresentation data;
printf("Segment 1: %#x\n", READ_AND_EXTRACT(data, segment1));
printf("Segment 2: %#x\n", READ_AND_EXTRACT(data, segment2));
printf("Segment 3: %#x\n", READ_AND_EXTRACT(data, segment3));
printf("Segment 4: %#x\n", READ_AND_EXTRACT(data, segment4));
printf("Segment 5: %#x\n", READ_AND_EXTRACT(data, segment5));
return 0;
}
Kết quả thực thi:
Segment 1: 0x78
Segment 2: 0x56
Segment 3: 0x4
Segment 4: 0x1
Segment 5: 0x0
Phân tích kết quả này cho thấy dữ liệu được lưu trữ theo thứ tự Little Endian, nơi byte có địa chỉ thấp nhất (0x78) tương ứng với phần thấp nhất của giá trị raw.
2. Thao tác Bit trên Thanh ghi
Union và bit field là công cụ mạnh mẽ để tương tác với các thanh ghi phần cứng, cho phép truy cập và sửa đổi các trường bit cụ thể một cách dễ dàng.
2.1. Định nghĩa cấu trúc thanh ghi
Đối với một module phần cứng cụ thể, ví dụ như MIPI-RX DPHY, chúng ta có thể định nghĩa cấu trúc tương ứng cho các thanh ghi của nó. Mỗi thanh ghi có thể được biểu diễn dưới dạng một union, cho phép truy cập cả giá trị toàn bộ và các trường bit riêng lẻ.
2.2. Lấy độ lệch (Offset) của thành viên cấu trúc
Hàm offsetof (thường được định nghĩa trong các header system) tính toán khoảng cách từ đầu cấu trúc đến một thành viên cụ thể. Điều này rất hữu ích khi làm việc với các cấu trúc dữ liệu được sắp xếp trong bộ nhớ.
#define GET_MEMBER_OFFSET(struct_type, member) \
((size_t)&(((struct struct_type *)0)->member))
Ví dụ:
struct Student {
char name[50];
int age;
char id[20];
};
// Offset của 'age' sẽ là 50 (độ dài của name).
// Offset của 'id' sẽ là 50 + sizeof(int).
2.3. Lấy địa chỉ cấu trúc từ thành viên (container_of)
Ngược lại với offsetof, macro container_of (phổ biến trong nhân Linux) cho phép bạn lấy địa chỉ của toàn bộ cấu trúc khi bạn có con trỏ đến một thành viên của nó.
#define CONTAINER_OF(ptr, type, member) ({ \
const typeof(((type *)0)->member) *__mptr = (ptr); \
(type *)((char *)__mptr - GET_MEMBER_OFFSET(type, member)); \
})
Ví dụ:
struct Student stu;
struct Student *pStu = CONTAINER_OF(&stu.age, struct Student, age);
// pStu bây giờ sẽ trỏ đến địa chỉ của stu.
2.4. Đọc Thanh ghi
Để đọc một thanh ghi, chúng ta cần địa chỉ cơ sở của module và độ lệch của thanh ghi đó trong module. Các macro có thể được định nghĩa để đơn giản hóa quá trình này.
// Giả định các hàm đọc/ghi I/O đã tồn tại (ví dụ: readl, writel)
#define BASE_ADDR_DPHY 0x0300b000
#define GET_REGISTER_OFFSET(struct_type, reg_name) \
((uint64_t)&(((struct struct_type *)0)->reg_name))
#define READ_REGISTER(base_addr, struct_type, reg_name) \
(readl((void __iomem *)(base_addr + GET_REGISTER_OFFSET(struct_type, reg_name))))
// Sử dụng:
// unsigned int reg_value = READ_REGISTER(BASE_ADDR_DPHY, DphyRegMap, Reg08);
2.5. Ghi Thanh ghi
Việc ghi dữ liệu vào thanh ghi tương tự như đọc, nhưng bao gồm việc truyền giá trị cần ghi.
#define WRITE_REGISTER(base_addr, struct_type, reg_name, value) \
(writel(value, (void __iomem *)(base_addr + GET_REGISTER_OFFSET(struct_type, reg_name))))
// Sử dụng:
// WRITE_REGISTER(BASE_ADDR_DPHY, DphyRegMap, Reg08, 0x3333ffff);
2.6. Đọc Bit Trường (Bit Field)
Để đọc một bit trường cụ thể, chúng ta đọc toàn bộ thanh ghi, sau đó sử dụng cấu trúc union đã định nghĩa để trích xuất giá trị của bit trường mong muốn.
#define READ_BIT_FIELD(base_addr, struct_type, reg_name, field_name) \
({ \
typeof(((struct struct_type *)0)->reg_name) reg_data; \
reg_data.raw = READ_REGISTER(base_addr, struct_type, reg_name); \
reg_data.bits.field_name; \
})
// Sử dụng:
// unsigned int field_value = READ_BIT_FIELD(BASE_ADDR_DPHY, DphyRegMap, Reg08, MipirxTestBist1);
2.7. Ghi Bit Trường (Bit Field)
Ghi vào một bit trường yêu cầu đọc giá trị hiện tại của thanh ghi, sửa đổi bit trường cụ thể đó, rồi ghi lại toàn bộ thanh ghi.
#define WRITE_BIT_FIELD(base_addr, struct_type, reg_name, field_name, value) \
do { \
typeof(((struct struct_type *)0)->reg_name) reg_data; \
reg_data.raw = READ_REGISTER(base_addr, struct_type, reg_name); \
reg_data.bits.field_name = value; \
WRITE_REGISTER(base_addr, struct_type, reg_name, reg_data.raw); \
} while (0)
// Sử dụng:
// WRITE_BIT_FIELD(BASE_ADDR_DPHY, DphyRegMap, Reg08, MipirxTestBist1, 0x1111);