Zig 学习笔记:位操作与嵌入式编程基础

Ziglings 110: 位操作大考 (Quiz 9)

Zig 不仅仅运行在笔记本电脑上,它也是嵌入式开发的利器。在嵌入式世界里,我们经常需要直接控制硬件引脚的电平(高/低),这就需要对寄存器进行位操作。

本次练习通过模拟 ATmega328(Arduino Uno 的核心芯片)的 PORTB 寄存器,考察了我们对位操作的掌握程度。

核心概念:位掩码 (Bitmask)

所有的操作都依赖于“掩码”。掩码就是一个二进制数,它的某一位是 1,代表我们要操作那一位。 例如,要操作第 2 位(从 0 开始计数),我们使用左移操作符 <<

const mask = 1 << 2; // 二进制: 0000...0100

1. 翻转位 (Toggle) - 使用 XOR (^)

逻辑

  • 0 ^ 1 = 1
  • 1 ^ 1 = 0
  • 结论:任何数与 1 做异或,都会翻转;与 0 做异或,保持不变。

代码实现: 我们要翻转第 2 位和第 0 位:

// 初始: 1100
// 掩码: 0101 (即 1<<2 | 1<<0)
PORTB ^= (1 << 2) | (1 << 0); 
// 结果: 1001

2. 置位 (Set) - 使用 OR (|)

逻辑

  • 0 | 1 = 1
  • 1 | 1 = 1
  • 结论:任何数与 1 做或运算,结果变为 1;与 0 做或运算,保持不变。

代码实现: 我们要强制将第 2 位置为 1:

// 简写形式 (Idiomatic way)
PORTB |= (1 << 2);

3. 清除位 (Clear) - 使用 AND (&) + NOT (~)

这是最棘手的部分,也是 Zig 与 C 语言稍有不同的地方。

逻辑: 要将某一位变 0,且不影响其他位,我们需要一个“除了那一位是 0,其他全是 1”的掩码,然后进行 AND 运算。

  • x & 1 = x (保持不变)
  • x & 0 = 0 (清除)

Zig 的“曲线球” (The Curve Ball): 在 C 语言中,我们通常写 val &= ~(1 << 2)。但在 Zig 中,如果你直接对编译期整数 1 << 2 取反 (~),编译器会报错,因为它需要确切地知道这个整数有多大(多少个比特),以防止产生歧义或溢出。

解决方案: 我们必须使用 @as() 显式地将掩码转换为具体的类型(这里是 u4),然后再取反。

// 错误写法: PORTB &= ~(1 << 2);
// 报错: unable to perform binary not operation on type 'comptime_int'

// 正确写法:
PORTB &= ~@as(u4, 1 << 2);

这体现了 Zig 的设计哲学:显式优于隐式,即使这会多敲几个键盘字符,但它能让你清楚地意识到你在操作多宽的数据。

总结代码

const std = @import("std");

pub fn main() !void {
    var PORTB: u4 = 0b0000;

    // 1. Toggle (翻转)
    PORTB = 0b1100;
    // 翻转 bit 2 和 bit 0
    PORTB ^= (1 << 2) | (1 << 0);
    // 结果: 1001

    // 2. Set (置位)
    PORTB = 0b1001;
    // 将 bit 2 置为 1
    PORTB |= (1 << 2);
    // 结果: 1101

    // 3. Clear (清除) - 重点!
    PORTB = 0b1110;
    // 将 bit 2 清零
    // 注意这里使用了 @as(u4, ...) 来明确类型
    PORTB &= ~@as(u4, 1 << 2); 
    // 结果: 1010
}

通过这个练习,我们不仅复习了二进制逻辑,还学到了 Zig 在类型安全上的严格要求。这些知识在将来编写操作系统内核或嵌入式驱动时将无价之宝!


Happy Zigling! 🦎