...
假设现在要把 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 当前被外部拉成什么样,你写出去的始终是完整的、你设计好的值。
|= 置 1,位与 &= ~ 清 0,异或 ^= 翻转1 << n 是操作第 n 位的标准写法想验证这些结论,可以在 进制转换与寄存器实训 里直接做题——那里有整套位运算的对照练习。
B. 直接写 HTML:...