假设现在要把 P1.3 拉高,P1 口其他的引脚保持不动。下面三种写法都能"跑通",但只有后两种是对的。

三种写法

// 写法一:直接赋值 —— 危险
P1 = 0x08;

// 写法二:位或 —— 只置 1
P1 |= (1 << 3);

// 写法三:位与取反 —— 只清 0
P1 &= ~(1 << 3);

为什么直接赋值会出事

P1 = 0x08 的意思是"把 P1 整个寄存器设成 0x08"。它不只是把 P1.3 拉高,同时把 P1.0 ~ P1.2、P1.4 ~ P1.7 全部清零。

如果这几个引脚上挂着 LED、继电器或者正在通信的串口,你就把它们全关了。更麻烦的是这类 bug 往往不会立刻暴露——可能表现为"串口偶尔乱码"或者"灯偶尔闪一下"。

P1 = 0x08 和 P1 |= 0x08 只差一个竖线,运行结果却完全不同。前者是"整个口赋值",后者是"只把第 3 位置 1"。写寄存器时,绝大多数情况你要的是后者。

位或与位与的读法

这两个运算符有一个很好记的解读方式:把它读成"把第几位设为 1 / 设为 0"。

目标 写法 读作
把第 3 位置 1 P1 |= (1 << 3) P1 或上 0000 1000
把第 3 位清 0 P1 &= ~(1 << 3) P1 与上 1111 0111
把第 3 位翻转 P1 ^= (1 << 3) P1 异或 0000 1000
读出第 3 位 P1 & (1 << 3) 非 0 即为高

1 << 3 生成的是 0000 1000,取反 ~(1 << 3) 得到 1111 0111——只有第 3 位是 0,其余全是 1。拿它去"与",其余位原样保留,第 3 位被清掉。

什么时候才能直接赋值

只有一种情况可以放心用 x = ...:你要设置的位就是整个寄存器的全部,或者这个寄存器后面会被完整重写。

比如配置一个只用来输出的寄存器:

// P0 全部当普通 IO 输出,8 根线一起定,这时直接赋值是对的
P0 = 0x00;

// 但更常见的做法是这样,把配置和现场分开
P0DIR = 0xFF;   // 方向寄存器:8 位全设输出
P0 = 0x00;      // 数据寄存器:8 位全输出低

关键区别在于 P0DIR 是方向寄存器,P0 是数据寄存器,它们本来就是"整体配置"语义。而 P1 口如果在别处已经被用过,你就不知道当前值是什么,只能改目标位。

一个更隐蔽的坑

8051 的 IO 口有"读-改-写"特性。执行 P1 |= 0x08 时,CPU 读回来的可能不是引脚上的实际电平,而是端口锁存器里的值。

这在推挽输出时没问题,但如果某个引脚被外部电路拉低(比如接了按键按下),写入的结果就会和你预期的不一样。真正稳妥的做法是在 RAM 里维护一份影子变量:

// 用影子变量记录"我想让 P1 是什么样"
unsigned char p1_shadow = 0x00;

void set_p13(unsigned char on) {
    if (on) p1_shadow |=  (1 << 3);
    else    p1_shadow &= ~(1 << 3);
    P1 = p1_shadow;         // 一次性写出去
}

这样无论 P1 当前被外部拉成什么样,你写出去的始终是完整的、你设计好的值。

小结

想验证这些结论,可以在 进制转换与寄存器实训 里直接做题——那里有整套位运算的对照练习。

B. 直接写 HTML:

...

其余(顶栏分类、左侧边栏、右侧工具条、目录、上下篇)全部自动生成。 -->
单片机实训平台MCU Lab

首页/图文文章

改寄存器的三个层次:直接赋值、位与、位或

三个写法的区别值得单独说清楚:直接赋值会覆盖整个寄存器,而位或、位与只动目标位。搞混它们的代价是——你调了半天,发现是别的位被顺手清掉了。