问题分析
上节实现了按下按键LED亮,松开按键LED 灭,这节需要实现的是第一次按下按键LED会亮,第二次按下按键LED会灭,如此循环。
像要实现这个功能,可以先读取灯的值,当按键按下后,如果是高电平就设置为低电平,如果是低电平就设置为高电平,即进行取反。
按键抖动
通过上述思路会有一个问题,独立键盘的按键按下和松开时会有波动,如果不消除抖动,直接对LED进行取反,那么就会一直亮灭。如下图,按下和松开的时候会有10ms左右的抖动,如果在20s后即抖动之后再进行取反,则跟预期效果一致。
txt
理想情况(无抖动):
高 ─────┐ ┌─────
│ │
低 └─────────┘
实际情况(有抖动):
高 ──┐ ┌┐ ┌───┐ ┌─┐ ┌─────
│ ││ │ │ │ │ │
低 └─┘└─┘ └───┘ └─┘
↑ ↑
按下抖动 松开抖动

完整代码
sdcc -mmcs51 --model-small --iram-size 256 --xram-size 0 --code-size 8192 .\key_led_32.c
c
#include <mcs51/8051.h>
void DelayXms(unsigned int xms)
{
unsigned char i, j;
if(xms == 0) return;
do {
i = 2;
j = 239;
do {
while (--j);
} while (--i);
} while (--xms);
}
void main(void)
{
while(1)
{
if(P3_1 == 0) // 按键按下
{
DelayXms(20); // 消抖
while(P3_1 == 0); // 等待松手
DelayXms(20); // 消抖
P2_0 = !P2_0; // 按下为高电平灯灭,第一次按下灯亮,第二次灯灭。
}
}
}
直接~P2_0没有效果
为什么 ~ 在 SDCC 里"失灵"了?
关键在于 "整数提升"(Integer Promotion) 和 "截断"(Truncation) 这两个标准 C 的行为:
- 取值:当 P2_0 的值是 1(高电平)时,参与运算前会被提升为 int 类型的 0x0001。
- 取反:~0x0001 得到 0xFFFE。
- 截断:因为 P2_0 只能存 0 或 1,0xFFFE 被截断后,非零值变成了 1(真值)。
最终结果就是 1 取反还是 1,P2_0 永远被置为高电平,你自然看不到它翻转。所以 SDCC 会警告你,但不报错,因为从 C 标准看这个行为是"合法"的。
正确的做法是用 ! 或 ^= 1
在 SDCC 中操作 51 单片机的位变量,应该使用逻辑非 ! 或异或 ^=,它们不会触发整数提升,能精确地翻转位状态。
| 操作符 | 代码示例 | 效果 |
|---|---|---|
| 逻辑非 ! | P2_0 = !P2_0; | 推荐,清晰直观,翻转电平 |
| 异或赋值 ^= | P2_0 ^= 1; | 同样有效,更接近底层翻转的意图 |
| 按位取反 ~ | P2_0 = ~P2_0; | 避免使用,会导致引脚被强制置高 |
效果展示
