嵌入式開發幾乎離不開 bit 操作。
設定硬體暫存器、解析通訊協定封包、
控制 GPIO、讀取狀態旗標,
到處都是對特定 bit 的讀寫。
我剛開始寫嵌入式的時候,
程式碼長這樣:
// 設定 UART 控制暫存器
UART_CTRL |= 0x01; // 啟用 TX
UART_CTRL |= 0x02; // 啟用 RX
UART_CTRL &= ~0x04; // 關閉 loopback
UART_CTRL |= (0x03 << 4); // 設定 baud rate 為 115200
當下寫的時候覺得沒問題,
因為我知道每個數字代表什麼。
三個月後回來看,
完全不知道 0x01、0x02、0x04 是什麼意思,
要翻 datasheet 才能看懂。
這就是 magic number 的問題。
基本的 bit 操作
先把基本操作整理一下,
後面會用到:
// 設定某個 bit(Set)
reg |= (1 << n); // 把第 n 個 bit 設為 1
// 清除某個 bit(Clear)
reg &= ~(1 << n); // 把第 n 個 bit 設為 0
// 切換某個 bit(Toggle)
reg ^= (1 << n); // 把第 n 個 bit 反轉
// 讀取某個 bit(Read)
bit = (reg >> n) & 1; // 取出第 n 個 bit 的值
// 讀取多個 bit(讀取欄位)
field = (reg >> shift) & mask; // 取出某個欄位的值
// 寫入多個 bit(寫入欄位)
reg = (reg & ~(mask << shift)) | ((value & mask) << shift);
這些操作本身沒問題,
問題是 n、mask、shift 如果都是裸數字,
可讀性就很差。
用 #define 給 bit 取名字
最基本的改進:
// ❌ magic number
UART_CTRL |= 0x01;
UART_CTRL |= 0x02;
UART_CTRL &= ~0x04;
// ✅ 有名字的 bit 定義
#define UART_CTRL_TX_EN (1 << 0) // bit 0:TX enable
#define UART_CTRL_RX_EN (1 << 1) // bit 1:RX enable
#define UART_CTRL_LOOPBACK (1 << 2) // bit 2:Loopback mode
UART_CTRL |= UART_CTRL_TX_EN;
UART_CTRL |= UART_CTRL_RX_EN;
UART_CTRL &= ~UART_CTRL_LOOPBACK;
現在程式碼自己說話了,
不需要翻 datasheet 才能看懂。
多個 bit 的欄位(Bit Field)
有些暫存器裡有多個 bit 組成的欄位,
例如 baud rate 設定用 bit 4-5(2 bits):
// ❌ 難以理解
UART_CTRL |= (0x03 << 4);
// ✅ 定義欄位的 shift 和 mask
#define UART_CTRL_BAUD_SHIFT 4
#define UART_CTRL_BAUD_MASK 0x03 // 2 bits
#define UART_CTRL_BAUD_9600 0x00
#define UART_CTRL_BAUD_19200 0x01
#define UART_CTRL_BAUD_57600 0x02
#define UART_CTRL_BAUD_115200 0x03
// 設定 baud rate
#define UART_SET_BAUD(reg, val) \
((reg) = ((reg) & ~(UART_CTRL_BAUD_MASK << UART_CTRL_BAUD_SHIFT)) | \
(((val) & UART_CTRL_BAUD_MASK) << UART_CTRL_BAUD_SHIFT))
// 讀取 baud rate
#define UART_GET_BAUD(reg) \
(((reg) >> UART_CTRL_BAUD_SHIFT) & UART_CTRL_BAUD_MASK)
// 使用
UART_SET_BAUD(UART_CTRL, UART_CTRL_BAUD_115200);
通用的 bit 操作巨集
與其每個暫存器都寫一套,
可以定義一組通用的巨集:
// bit_utils.h
// 單個 bit 操作
#define BIT(n) (1UL << (n))
#define BIT_SET(reg, bit) ((reg) |= (bit))
#define BIT_CLR(reg, bit) ((reg) &= ~(bit))
#define BIT_TOG(reg, bit) ((reg) ^= (bit))
#define BIT_GET(reg, bit) (((reg) & (bit)) != 0)
// 多個 bit 的欄位操作
#define FIELD_MASK(width) ((1UL << (width)) - 1)
#define FIELD_GET(reg, shift, width) (((reg) >> (shift)) & FIELD_MASK(width))
#define FIELD_SET(reg, shift, width, val) \
((reg) = ((reg) & ~(FIELD_MASK(width) << (shift))) | \
(((val) & FIELD_MASK(width)) << (shift)))
使用:
// 設定 bit
BIT_SET(UART_CTRL, BIT(0)); // 設定 bit 0
// 清除 bit
BIT_CLR(UART_CTRL, BIT(2)); // 清除 bit 2
// 讀取 bit
if (BIT_GET(UART_STATUS, BIT(0))) {
// TX done
}
// 設定欄位(bit 4-5,2 bits)
FIELD_SET(UART_CTRL, 4, 2, UART_CTRL_BAUD_115200);
// 讀取欄位
uint8_t baud = FIELD_GET(UART_CTRL, 4, 2);
用 struct 的 bit field
C 語言有內建的 bit field 語法:
typedef struct {
uint8_t tx_en : 1; // bit 0
uint8_t rx_en : 1; // bit 1
uint8_t loopback : 1; // bit 2
uint8_t reserved : 1; // bit 3
uint8_t baud : 2; // bit 4-5
uint8_t reserved2: 2; // bit 6-7
} UartCtrlReg;
使用起來很直覺:
volatile UartCtrlReg *uart_ctrl = (UartCtrlReg *)0x40001000;
uart_ctrl->tx_en = 1;
uart_ctrl->rx_en = 1;
uart_ctrl->loopback = 0;
uart_ctrl->baud = UART_CTRL_BAUD_115200;
看起來很優雅,但有幾個問題要注意:
問題一:bit 的排列順序是實作定義的
C 標準沒有規定 bit field 是從 LSB 還是 MSB 開始排,
不同編譯器、不同平台可能不一樣。
如果你的程式碼需要跨平台,
或是需要和硬體暫存器精確對應,
用 struct bit field 要特別小心。
問題二:struct 的大小和對齊也是實作定義的
sizeof(UartCtrlReg) // 不一定是 1,可能是 4
問題三:無法做 read-modify-write 的原子操作
uart_ctrl->tx_en = 1; // 這可能被編譯成多條指令
uart_ctrl->rx_en = 1; // 中間可能被中斷打斷
我個人在嵌入式的態度是:
struct bit field 用來做文件和可讀性可以,
但不要直接拿來操作硬體暫存器,
除非你很確定編譯器的行為。
實際的暫存器定義範例
結合前面的方式,
一個比較完整的暫存器定義:
// uart_regs.h
// UART 控制暫存器(偏移 0x00)
#define UART_CTRL_OFFSET 0x00
// Bit 定義
#define UART_CTRL_TX_EN BIT(0) // TX enable
#define UART_CTRL_RX_EN BIT(1) // RX enable
#define UART_CTRL_LOOPBACK BIT(2) // Loopback mode
#define UART_CTRL_PARITY_EN BIT(3) // Parity enable
// Baud rate 欄位(bit 4-5)
#define UART_CTRL_BAUD_SHIFT 4
#define UART_CTRL_BAUD_WIDTH 2
#define UART_CTRL_BAUD_9600 0x00
#define UART_CTRL_BAUD_19200 0x01
#define UART_CTRL_BAUD_57600 0x02
#define UART_CTRL_BAUD_115200 0x03
// UART 狀態暫存器(偏移 0x04)
#define UART_STATUS_OFFSET 0x04
#define UART_STATUS_TX_DONE BIT(0) // TX 傳輸完成
#define UART_STATUS_RX_READY BIT(1) // RX 資料就緒
#define UART_STATUS_OVERRUN BIT(2) // RX 溢位錯誤
#define UART_STATUS_PARITY_ERR BIT(3) // Parity 錯誤
// 暫存器存取巨集
#define UART_BASE 0x40001000UL
#define UART_REG(offset) (*((volatile uint32_t *)(UART_BASE + (offset))))
#define UART_CTRL UART_REG(UART_CTRL_OFFSET)
#define UART_STATUS UART_REG(UART_STATUS_OFFSET)
使用:
// 初始化 UART
void uart_init(void) {
// 清除控制暫存器
UART_CTRL = 0;
// 設定 baud rate
FIELD_SET(UART_CTRL, UART_CTRL_BAUD_SHIFT,
UART_CTRL_BAUD_WIDTH, UART_CTRL_BAUD_115200);
// 啟用 TX 和 RX
BIT_SET(UART_CTRL, UART_CTRL_TX_EN | UART_CTRL_RX_EN);
}
// 等待 TX 完成
void uart_wait_tx(void) {
while (!BIT_GET(UART_STATUS, UART_STATUS_TX_DONE)) {
// 等待
}
}
// 檢查是否有錯誤
bool uart_has_error(void) {
return BIT_GET(UART_STATUS, UART_STATUS_OVERRUN) ||
BIT_GET(UART_STATUS, UART_STATUS_PARITY_ERR);
}
這樣的程式碼,
不需要翻 datasheet 就能看懂在做什麼。
封包解析的 bit 操作
除了硬體暫存器,
通訊協定的封包解析也常常需要 bit 操作:
// 假設一個感測器封包格式(2 bytes):
// Byte 0: [7:4] = sensor_id, [3:2] = channel, [1:0] = status
// Byte 1: [7:0] = raw_value
typedef struct {
uint8_t sensor_id;
uint8_t channel;
uint8_t status;
uint8_t raw_value;
} SensorData;
// ❌ magic number 版本
SensorData parse_packet_bad(uint8_t byte0, uint8_t byte1) {
SensorData data;
data.sensor_id = (byte0 >> 4) & 0x0F;
data.channel = (byte0 >> 2) & 0x03;
data.status = byte0 & 0x03;
data.raw_value = byte1;
return data;
}
// ✅ 有名字的版本
#define PKT_SENSOR_ID_SHIFT 4
#define PKT_SENSOR_ID_WIDTH 4
#define PKT_CHANNEL_SHIFT 2
#define PKT_CHANNEL_WIDTH 2
#define PKT_STATUS_SHIFT 0
#define PKT_STATUS_WIDTH 2
SensorData parse_packet(uint8_t byte0, uint8_t byte1) {
SensorData data;
data.sensor_id = FIELD_GET(byte0, PKT_SENSOR_ID_SHIFT, PKT_SENSOR_ID_WIDTH);
data.channel = FIELD_GET(byte0, PKT_CHANNEL_SHIFT, PKT_CHANNEL_WIDTH);
data.status = FIELD_GET(byte0, PKT_STATUS_SHIFT, PKT_STATUS_WIDTH);
data.raw_value = byte1;
return data;
}
一個小坑:1 還是 1UL
// ❌ 可能有問題
#define BIT(n) (1 << (n))
// 如果 n >= 31,在 32-bit int 的平台上,
// 1 << 31 是有號整數溢位(Undefined Behavior)
// ✅ 用無號長整數
#define BIT(n) (1UL << (n))
// 或是更明確的型別
#define BIT32(n) ((uint32_t)1 << (n))
#define BIT64(n) ((uint64_t)1 << (n))
1UL 確保是無號整數,
避免有號整數溢位的 undefined behavior。
說實話
bit 操作的乾淨寫法,
說穿了就是「給每個數字取一個有意義的名字」,
這個道理很簡單,
但實際上要養成習慣需要一點時間。
我以前覺得寫 0x01、0x02 比較快,
不想多花時間定義那些 #define。
但後來發現,
定義 #define 花的時間,
遠遠少於三個月後回來看程式碼、
翻 datasheet 搞清楚每個數字是什麼的時間。
更不用說,
如果硬體改版,某個 bit 的位置換了,
有名字的版本只需要改一個地方,
magic number 的版本要全部搜尋替換,
還不一定找得完整。
現在我的習慣是,
任何 bit 操作,
只要不是 0 或 1 這種顯而易見的值,
都給它一個名字。
實戰 Checklist
- [ ] 硬體暫存器的 bit 定義都有用
#define或enum取名 - [ ] 沒有裸的 magic number(
0x01、0x04這類)直接出現在邏輯裡 - [ ]
BIT(n)巨集用1UL,不是1 - [ ] 多 bit 的欄位有定義 shift 和 width,用
FIELD_GET/FIELD_SET操作 - [ ] 硬體暫存器存取有加
volatile(上一篇說過) - [ ] 通訊協定的封包解析有定義欄位名稱,不用 magic number
- [ ] struct bit field 如果用在硬體暫存器,有確認編譯器的 bit 排列順序