Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 48.324 phút đọc

Trường bit cho thanh ghi

Sau bài này bạn sẽ làm được

  • Viết union kết hợp trường bit với giá trị thô
  • Nêu năm chi tiết mà chuẩn C để cho trình biên dịch tự quyết
  • Giải thích vì sao trường bit có thể sinh ra truy cập một byte
  • Chọn được giữa trường bit và mặt nạ thủ công

Trường bit cho phép viết TIM->bits.enable = 1 thay vì TIM->CR1 |= 1u. Cú pháp đẹp hơn hẳn, nhưng chuẩn C để cho trình biên dịch tự quyết năm chi tiết, và với thanh ghi phần cứng thì năm chi tiết đó là quá nhiều.

#Cú pháp gọn hơn hẳn

truong-bit.h
#include <stdint.h>

typedef union {
    uint32_t raw;                     /* truy cập cả thanh ghi */

    struct {
        uint32_t enable    : 1;       /* bit 0 */
        uint32_t mode      : 3;       /* bit [3:1] */
        uint32_t reserved0 : 4;       /* bit [7:4] */
        uint32_t prescaler : 8;       /* bit [15:8] */
        uint32_t           : 16;      /* trường vô danh, phần còn lại */
    } bits;
} CtrlReg;

static volatile CtrlReg *TIM_CR = (volatile CtrlReg *)0x40000000UL;

/* Dùng: */
TIM_CR->bits.enable    = 1u;
TIM_CR->bits.mode      = 3u;
TIM_CR->bits.prescaler = 72u;

uint32_t ca_thanh_ghi = TIM_CR->raw;
Mặt nạ thủ công, dài dòng
/* Bản mặt nạ thủ công, cùng việc */
uint32_t r = *TIM_CR_RAW;

r &= ~(0x7u << 1);
r |=  (3u   << 1);
r &= ~(0xFFu << 8);
r |=  (72u   << 8);
r |=  1u;

*TIM_CR_RAW = r;
Trường bit, gọn và rõ
/* Bản trường bit, cùng việc */
TIM_CR->bits.mode      = 3u;
TIM_CR->bits.prescaler = 72u;
TIM_CR->bits.enable    = 1u;

#Năm điều chuẩn C không bảo đảm

Chi tiếtChuẩn C nói gìHậu quả với thanh ghi
Thứ tự cấp phát bitDo cài đặt quyết địnhTrường đầu tiên có thể là bit thấp nhất hoặc bit cao nhất
Trường có được vắt qua ranh giới đơn vị lưu trữ khôngDo cài đặt quyết địnhMột trường 6 bit ở vị trí 30 có thể bị đẩy sang từ kế
Kiểu nào được phép làm trường bitChỉ int, unsigned int và _Bool là bắt buộcuint8_t và uint32_t là mở rộng, dù mọi trình biên dịch đều hỗ trợ
Trường bit kiểu int có dấu hay khôngDo cài đặt quyết địnhMột trường 1 bit kiểu int có thể chứa 0 và âm 1
Độ rộng truy cập sinh raChuẩn không nói gìTrình biên dịch có thể sinh ra lệnh đọc ghi một byte

#Truy cập sai độ rộng

Đây là vấn đề nghiêm trọng nhất, và nó không nằm trong danh sách các điều chuẩn C bỏ ngỏ, mà nằm ở chỗ chuẩn C hoàn toàn không nói gì.

do-rong.c
typedef union {
    volatile uint32_t raw;

    struct {
        uint32_t a : 8;
        uint32_t b : 8;
        uint32_t c : 8;
        uint32_t d : 8;
    } bits;
} Reg;

static volatile Reg *R = (volatile Reg *)0x40000000UL;

void dat_b(void)
{
    R->bits.b = 0x5Au;
}
terminal
arm-none-eabi-gcc -O2 -S do-rong.c -o - | sed -n '/dat_b:/,/bx/p'
dat_b:
        ldr     r3, .L2
        movs    r2, #90
        strb    r2, [r3, #1]      @ ghi MOT BYTE, khong phai 32 bit
        bx      lr

Cách buộc truy cập đúng độ rộng

an-toan.c
/* Cách 1: đọc cả thanh ghi vào biến TẠM không volatile,
   sửa trường bit trên biến tạm, rồi ghi cả thanh ghi. */
void dat_b_an_toan(void)
{
    Reg tam;

    tam.raw = R->raw;              /* một lệnh đọc 32 bit */
    tam.bits.b = 0x5Au;            /* sửa trên RAM, không phải thanh ghi */
    R->raw = tam.raw;              /* một lệnh ghi 32 bit */
}

/* Cách 2: bỏ hẳn trường bit, dùng mặt nạ như Bài 48.1. */
void dat_b_mat_na(void)
{
    uint32_t r = R->raw;

    r &= ~(0xFFu << 8);
    r |=  (0x5Au << 8);

    R->raw = r;
}
terminal
arm-none-eabi-gcc -O2 -S an-toan.c -o - | sed -n '/dat_b_an_toan:/,/bx/p'
dat_b_an_toan:
        ldr     r3, .L4
        ldr     r2, [r3]          @ doc 32 bit
        bic     r2, r2, #65280
        orr     r2, r2, #23040
        str     r2, [r3]          @ ghi 32 bit
        bx      lr

#Khi nào dùng được

Tình huốngTrường bitMặt nạ thủ công
Thanh ghi phần cứngRủi ro caoNên dùng
Cấu trúc dữ liệu nội bộ tiết kiệm RAMRất tốtDài dòng không cần thiết
Giao thức mạng hoặc định dạng tệpKhông dùng được, bố cục không xác địnhNên dùng
Cờ trạng thái trong một struct của bạnRất tốtKhông cần
Mã phải chạy trên nhiều kiến trúcKhông dùng đượcNên dùng
dung-tot.c
/* Trường bit rất hợp cho cấu trúc dữ liệu NỘI BỘ của bạn,
   nơi bố cục không cần khớp với thứ gì bên ngoài. */
typedef struct {
    uint16_t nhiet_do;                /* nhân 10, tức 253 là 25.3 độ */

    unsigned int  co_loi        : 1;
    unsigned int  dang_do       : 1;
    unsigned int  che_do        : 2;
    unsigned int  so_lan_thu    : 3;
    unsigned int                : 1;  /* đệm cho tròn byte */
} TrangThaiCamBien;

/* sizeof là 4 byte thay vì 8 nếu dùng năm biến uint8_t riêng.
   Trên chip 4 KB RAM với một trăm cảm biến, đó là 400 byte tiết kiệm. */
terminal
./so-kich-thuoc
dung truong bit  : sizeof = 4
dung uint8_t rieng: sizeof = 8

voi 100 phan tu: 400 byte so voi 800 byte

#Cách thay thế an toàn

thay-the.h
#include <stdint.h>

/* Giữ được sự tiện lợi của trường bit mà không có rủi ro:
   một struct THƯỜNG không volatile để tính toán,
   cộng hai hàm chuyển đổi qua lại với giá trị thô. */

typedef struct {
    uint8_t enable;
    uint8_t mode;
    uint8_t prescaler;
} TimCauHinh;

static inline uint32_t tim_dong_goi(const TimCauHinh *c)
{
    return ((uint32_t)(c->enable    & 0x01u) << 0)
         | ((uint32_t)(c->mode      & 0x07u) << 1)
         | ((uint32_t)(c->prescaler & 0xFFu) << 8);
}

static inline TimCauHinh tim_thao_goi(uint32_t raw)
{
    TimCauHinh c;

    c.enable    = (uint8_t)((raw >> 0) & 0x01u);
    c.mode      = (uint8_t)((raw >> 1) & 0x07u);
    c.prescaler = (uint8_t)((raw >> 8) & 0xFFu);

    return c;
}

/* Dùng: */
TimCauHinh c = tim_thao_goi(TIM3->CR1);

c.prescaler = 72u;
c.enable    = 1u;

TIM3->CR1 = tim_dong_goi(&c);      /* MỘT lệnh ghi 32 bit */
terminal
arm-none-eabi-gcc -O2 -S thay-the.c -o - | sed -n '/cau_hinh:/,/bx/p'
cau_hinh:
        ldr     r3, .L2
        ldr     r2, [r3]          @ mot lenh doc 32 bit
        bic     r2, r2, #65280
        orr     r2, r2, #18432
        orr     r2, r2, #1
        str     r2, [r3]          @ mot lenh ghi 32 bit
        bx      lr
# Cùng số lệnh với bản mặt nạ thủ công
arm-none-eabi-size mat-na.o thay-the.o
   text    data     bss     dec     hex filename
     28       0       0      28      1c mat-na.o
     28       0       0      28      1c thay-the.o

Tự làm thử

  1. Viết union trường bit cho một thanh ghi, gán một trường, rồi xem mã hợp ngữ có sinh ra strb không.
  2. Khai báo trường bit một bit kiểu int, gán 1 rồi so với 1, và giải thích kết quả.
  3. Viết struct có trường 30 bit rồi trường 4 bit, in sizeof và giải thích con số nhận được.
  4. Cài mẫu đọc vào biến tạm rồi ghi cả thanh ghi, và xác nhận mã sinh ra chỉ có ldr và str.
  5. Viết cặp hàm đóng gói và tháo gói cho một thanh ghi, so kích thước mã với bản mặt nạ thủ công.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Trường bit cho cú pháp gọn và tự lo cả xóa lẫn đặt, nhưng bố cục do trình biên dịch quyết định chứ không phải chuẩn C.
  • Năm chi tiết bỏ ngỏ: thứ tự bit, vắt qua ranh giới, kiểu cho phép, dấu của trường, và độ rộng truy cập sinh ra.
  • Trình biên dịch có thể sinh ra lệnh ghi một byte vào thanh ghi chỉ chấp nhận truy cập 32 bit, và volatile không ngăn được.
  • Trường bit rất hợp cho struct nội bộ tiết kiệm RAM, và hoàn toàn không hợp cho thanh ghi phần cứng, giao thức mạng hay định dạng tệp.
  • Cách an toàn giữ được cú pháp đẹp là dùng struct thường cộng hai hàm đóng gói và tháo gói, sinh ra đúng cùng số lệnh với mặt nạ thủ công.