点亮流水灯
032.2.2 点亮流水灯
#include "reg52.h"
#include <intrins.h>
sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;
int i;
void Delay1000ms(void) //@11.0592MHz
{
unsigned char data i, j, k;
_nop_();
i = 8;
j = 1;
k = 243;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main()
{
while(1)
{
// P1 = 0xfe; //11111110
// Delay1000ms();
// P1 = 0xfd; //11111101
// Delay1000ms();
// P1 = 0xfb; //11111011
// Delay1000ms();
// P1 = 0xf7; //11110111
// Delay1000ms();
// P1 = 0xff;
// Delay1000ms();
// for(i=0; i<4; i++)
// {
// P1 = P1 << 1;
// Delay1000ms();
// }
// LED1 = 0;
// LED3 = 0;
// Delay1000ms();
// LED1 = 1;
// LED3 = 1;
// Delay1000ms();
LED2 = ~LED2;
LED4 = ~LED4;
Delay1000ms();
}
}
一、搜索C206问题
-
上一节视频结尾的 Keil Build Output 实际包含两条相关诊断:
warning C206: '_nop_': missing function-prototype error C264: intrinsic '_nop_': declaration/activation error -
C206 是“缺少函数原型”的警告,真正使构建失败的是随后出现的 C264 错误。
_nop_()已出现在延时函数中,但编译器在处理调用之前没有看到它的声明,因此不知道应如何正确生成这条内建指令。 -
搜索编译错误时,应复制 Build Output 中最能描述问题的部分,例如:
Keil C51 C206 _nop_ missing function-prototype。文件名main.c和具体行号会随工程变化,可以删除;但编译器名称、错误编号和函数名通常很有价值,不必一律删掉。 -
搜索结果只能作为线索。较可靠的判断顺序是:先读完整错误上下文,再看代码所在行和已包含的头文件,然后查编译器帮助、官方头文件或器件手册,最后再参考论坛答案。不要只复制第一段搜索结果就直接修改工程。
-
英文报错可借助翻译工具,但要结合 C 语言语境理解:这里的
prototype是“函数原型/声明”,不是普通意义上的产品原型。
二、添加头文件解决编译错误
_nop_()是 Keil C51 提供的 intrinsic(编译器内建函数),用于生成一条 NOP(No Operation,空操作)机器指令。它不是一个什么都不做却需要普通函数调用开销的 C 函数,而是由编译器直接转换为目标指令。- Keil 的
intrins.h中声明了:extern void _nop_(void);。在源文件顶部加入#include <intrins.h>后,编译器便能识别_nop_(),C206/C264 随之消失。 - NOP 不改变普通运算结果,但会占用指令执行时间,可用于微小延时、对齐或满足硬件时序。它到底对应多长时间取决于晶振频率和 MCU 的机器周期设置,不能简单理解为固定 1µs。
reg52.h与intrins.h作用不同:前者声明 8051/8052 的特殊功能寄存器和可位寻址名称,后者声明_nop_、循环移位等编译器内建函数。本节源码两者都要包含。- 选中
<intrins.h>后使用 Keil 的 Open 功能,可以在头文件中看到_nop_原型;Go To 无法跳转并不代表声明不存在,可能只是工程浏览信息或索引尚未生成。 - 修复后应重新 Build,确认
0 Error(s),再检查 HEX 文件更新时间并重新下载。视频中四个 LED 随后每隔约 1 秒依次点亮,说明“编译、生成新 HEX、烧录、硬件运行”整条链路均已成功。
三、移位操作
- 移位是对二进制位的位置进行整体移动。左移运算符是
<<,右移运算符是>>。本节通过反复左移 P1,使低位不断补 0;由于板载 LED 低电平有效,变成 0 的位对应 LED 点亮。
3.1 移位操作的概念
- 对无符号数左移一位时,所有位向高位方向移动,超出数据宽度的高位丢弃,最低位补 0。在不溢出的理想数学模型中,左移一位相当于乘 2。
- 对无符号数右移一位时,所有位向低位方向移动,最低位丢弃,最高位补 0;右移有符号负数时最高位如何填充与实现有关,初学阶段应优先用无符号类型练习位运算。
- P1 是 8 位特殊功能寄存器,只能保存最低 8 位。C 表达式计算时还存在整数提升:
P1 << 1通常先按int计算,再赋回 P1 并截断到 8 位。因此“0xFF 左移后等于 0xFE”不是完整数学等式,而是左移结果写回 8 位寄存器后的结果。
0x00FF << 1 = 0x01FE C 表达式的中间结果
│
└── 赋给 8 位 P1,只保留低 8 位
▼
0xFE
3.2 移位操作示例
- 从 8 位视角观察
0xFF连续左移并写回 P1,可得到下面的序列:
| 左移次数 | P1 的 8 位二进制值 | P1 十六进制值 | 低位变化 |
|---|---|---|---|
| 0 | 1111 1111 | 0xFF | 尚无 0 |
| 1 | 1111 1110 | 0xFE | bit0 补 0 |
| 2 | 1111 1100 | 0xFC | bit1~bit0 为 0 |
| 3 | 1111 1000 | 0xF8 | bit2~bit0 为 0 |
| 4 | 1111 0000 | 0xF0 | bit3~bit0 为 0 |
| 5 | 1110 0000 | 0xE0 | bit4~bit0 为 0 |
| 6 | 1100 0000 | 0xC0 | bit5~bit0 为 0 |
| 7 | 1000 0000 | 0x80 | bit6~bit0 为 0 |
| 8 | 0000 0000 | 0x00 | 8 位全部为 0 |
- 每次执行
P1 = P1 << 1;,都会先读取当前 P1 值、左移一位,再把低 8 位写回 P1。它与始终拿原始0xFF分别计算0xFF << n的书写方式不同,但在本例低 8 位序列上结果一致。 - 可简写为
P1 <<= 1;,但简写并不会降低风险:它仍然会修改 P1 的全部 8 位。
3.3 例题1: 通过移位操作改变P1的值
-
视频先选中四条直接赋值语句并使用 Keil 注释快捷按钮,使它们变成
//注释;这些语句仅作为写法示例保留,被注释后不会参与编译。 -
随后在
while (1)中先写P1 = 0xFF;,再通过for (i = 0; i < 4; i++)重复左移和延时:P1 = 0xff; Delay1000ms(); for (i = 0; i < 4; i++) { P1 = P1 << 1; Delay1000ms(); } -
配套源码在文件顶部使用
int i;。Keil C51 中int通常为 16 位、占 2 字节;但 ISO C 并不规定int必须是 16 位,在电脑编译器中常见 32 位。本循环只需要 0~3,使用unsigned char i也足够且更节省 8051 RAM。 -
i++在每轮结束后加 1,i < 4控制总共执行 4 次;它控制的是移位次数,不是直接代表某一盏灯。
3.4 移位操作与延时循环的结合
P1 = 0xFF时,P1.0~P1.3 全为 1,四灯全灭。此后每轮左移增加一个低电平位,四盏灯按“累计点亮”方式变化:
P1=FF:○ ○ ○ ○ 四灯全灭
左移 1 位
P1=FE:● ○ ○ ○ LED1 亮
左移 1 位
P1=FC:● ● ○ ○ LED1、LED2 亮
左移 1 位
P1=F8:● ● ● ○ LED1~LED3 亮
左移 1 位
P1=F0:● ● ● ● 四灯全亮
● = 低电平点亮,○ = 高电平熄灭
- 延时必须放在希望人眼看到的状态之后。若只在移位后延时而
P1 = 0xFF后没有延时,程序会极快进入第一次移位,人眼几乎看不到“全灭”阶段。
3.5 例题2: 移位操作实现LED灯的逐个点亮
- 视频中的“逐个点亮”是指每次多亮一盏灯,即 LED1 → LED1+LED2 → LED1+LED2+LED3 → 四灯全亮;它不是上一节“始终只有一盏灯在移动”的单点流水效果。
- 若要始终只点亮一盏灯,应使用
0xFE、0xFD、0xFB、0xF7四个模式,或对单个 0 位做循环移位/查表,不能直接用本节的连续左移累积 0。 - 第四次左移后 P1 为
0xF0。下一轮while (1)再写入0xFF,四灯恢复全灭,然后重新开始。
3.6 移位操作中的问题与解决
- 初次实验看不到全灭阶段,是因为
P1 = 0xFF到第一次左移之间只有极短的指令执行时间。解决方法是在P1 = 0xFF;后增加Delay1000ms();,让全灭状态保持约 1 秒。 - 修改后应重新编译,确认 HEX 时间戳,再选择正确串口进行下载。视频中可观察到“全灭 → 亮一盏 → 亮两盏 → 亮三盏 → 四灯全亮”的完整周期。
- 寄存器有 8 位,不代表为了实验就应该盲目移动 8 次。移位次数必须同时考虑 P1.4~P1.7 连接了什么硬件,否则会产生 LED 之外的副作用。
3.7 移位操作的总结与扩展
- 左移的核心规则是“向高位移动、低位补 0、超出保存宽度的高位丢弃”。连续左移 8 次并写回 8 位 P1 后,结果为
0x00。 - 对一个宽度为
W的 C 整数表达式,移位次数大于或等于W会产生未定义行为。这里 P1 会先发生整数提升,而循环每次只移 1 位并立即写回,避免了直接写P1 << 16之类的错误,但仍应养成检查类型宽度的习惯。 - 移位运算改变的是数值的所有相关位。用于硬件寄存器时,每一位都可能连接真实外设,必须把二进制结果与原理图逐位对照,而不能只看十六进制数是否“算对”。
四、移位操作知识拓展
- 本板 P1 的 8 个端口位并不只连接四个 LED。正式原理图显示:
| P1 位 | 网络/外设 | 有效逻辑 | 连续左移的影响 |
|---|---|---|---|
| P1.0 | LED1 | 低电平点亮 | 第 1 次左移后亮 |
| P1.1 | LED2 | 低电平点亮 | 第 2 次左移后亮 |
| P1.2 | LED3 | 低电平点亮 | 第 3 次左移后亮 |
| P1.3 | LED4 | 低电平点亮 | 第 4 次左移后亮 |
| P1.4 | OLED SDA | I²C 数据线 | 第 5 次左移后被拉低 |
| P1.5 | OLED SCL | I²C 时钟线 | 第 6 次左移后被拉低 |
| P1.6 | 蜂鸣器 Q1 控制端 | 低电平使 Q1 导通 | 第 7 次左移后蜂鸣器响 |
| P1.7 | 排针引出/普通端口 | 取决于外接电路 | 第 8 次左移后被拉低 |
- 因此把循环上限从 4 改成 8 后,前四次 LED 现象仍正常;第五、六次会改变 OLED 总线;第七次使 P1.6 变为 0,蜂鸣器开始响;第八次则把 P1 全部写成 0。视频中听到蜂鸣器并不是程序“莫名出错”,而是整个 P1 寄存器被正确写成了会启动蜂鸣器的值。
- 蜂鸣器不能由 P1.6 直接承担主电流,原理图使用 Q1(S9012,PNP 三极管)作为高边开关:发射极接 +5V,集电极接 BELL1,基极通过 R3(1kΩ)接 P1.6,蜂鸣器另一端接 GND。
Q1:S9012(PNP 高边开关)
E
+5V ─────────────────────┤
│\
P1.6 ───── R3 1kΩ ───────┤ >──── C ───► BELL1 ───► GND
│/
P1.6 = 1:基极接近 +5V,VEB 接近 0 → Q1 截止 → 蜂鸣器不响
P1.6 = 0:基极电位被拉低,PNP 结正向偏置 → Q1 导通 → 蜂鸣器响
- P1.6 为低电平时,较小的基极电流经
+5V → Q1 发射结 → R3 → P1.6流入 MCU;Q1 导通后的蜂鸣器主电流走+5V → Q1 → BELL1 → GND。R3 限制基极电流,Q1 让 MCU 引脚只负责控制而不直接驱动负载。 - 这说明调试嵌入式异常必须联合分析:软件层面列出寄存器每一步的二进制值,硬件层面再查询每一位连接的网络和有效电平。
P1 = 0x80 = 1000 0000
│
├── P1.0~P1.3 = 0:四个 LED 全亮
├── P1.4/P1.5 = 0:OLED SDA/SCL 被拉低
└── P1.6 = 0:蜂鸣器导通
- 若只想控制 LED,稳妥方法是写入明确的 LED 模式,并保持 P1.4~P1.7 为 1;更复杂程序应维护 P1 的影子变量或使用位操作,避免一次整体赋值意外改变 OLED 和蜂鸣器。
- 烧录时串口号必须选择当前 CH340 对应的 COM 口。视频中选错串口且一度关闭电源,下载软件持续检测目标单片机;恢复供电并改回正确的 COM3 后才成功。COM3 只是视频机器上的示例,实际端口应以设备管理器为准。
五、按位操作
-
sbit是 Keil C51 扩展关键字,用于给可位寻址 SFR 的某一位建立别名,不属于标准 ISO C。配套源码将四个 LED 定义为:sbit LED1 = P1^0; sbit LED2 = P1^1; sbit LED3 = P1^2; sbit LED4 = P1^3; -
P1^0在sbit声明中表示 P1 的 bit0,不是普通 C 运算表达式中的异或。定义完成后,给LED1赋值就是直接修改 P1.0,而不必重写 P1 的其他 7 位。 -
sbit声明应放在函数外的文件作用域。真正执行的赋值语句应写在main的while (1)大括号内,才能随主循环持续运行。 -
本板 LED 低电平有效,因此
LED1 = 0;点亮 LED1,LED1 = 1;熄灭 LED1。视频同时令 LED1、LED3 为 0,实物上第 1、3 盏灯点亮,验证了位别名与硬件引脚的映射。 -
按位操作的优势是只改变目标位。例如修改 LED1 不会连带改变 P1.4、P1.5 的 OLED 信号或 P1.6 的蜂鸣器控制信号。
六、单片机的按位取反操作
- 位取反运算符
~会把操作数的每一位反转:0 变 1,1 变 0。对sbit变量赋值时,最终只保存最低目标位,因此LED1 = ~LED1;在 Keil C51 中可实现 LED1 状态翻转。 - 更能表达“布尔状态翻转”意图的写法是
LED1 = !LED1;:逻辑非!明确产生 0 或 1;而~会先发生整数提升并反转整个整数的所有位。配套源码使用的是~,应理解其生效原因,而不是把两种运算符混为一谈。 - 若对整个端口执行
P1 = ~P1;,则 P1 的 8 位都会翻转,LED、OLED 和蜂鸣器可能同时受影响;这与只对一个sbit变量取反完全不同。
6.1 例题1: 通过按位取反实现LED灯状态切换
-
显式赋值实现闪烁的写法是:先把 LED1、LED3 置 0并延时,再置 1并延时。取反写法则在循环中每次翻转并延时:
LED1 = ~LED1; LED3 = ~LED3; Delay1000ms(); -
当初始状态已知且循环持续执行时,两种写法都可产生约 1 秒亮、1 秒灭的闪烁。若初始值不确定,显式赋值更容易保证第一次出现的状态;取反写法更简洁,但第一帧取决于复位后的端口状态。
-
配套源码当前选择 LED2、LED4 执行取反,所以实物上第 2、4 盏灯同步闪烁;先前 LED1、LED3 的显式赋值代码被注释保留。
6.2 知识点: 单片机在线升级编程仿真调试平台软件的使用
- 每次切换实验代码后,都要确认需要执行的部分已经取消
//,其余互斥示例仍保持注释,否则多个代码块可能在同一循环中相互覆盖输出状态。 - 编译前检查括号、分号、变量名和头文件;编译后检查 Error/Warning 数量及 HEX 更新时间;下载时检查芯片型号、串口、电源与目标文件;最后再用实物现象验证逻辑。
- “编译成功”只能证明语法和链接通过,不代表硬件现象一定正确。若 LED 选择或闪烁模式不符,应查看当前参与编译的是 LED1/LED3 还是 LED2/LED4,并结合低电平有效逻辑分析。
6.3 例题2: 通过按位取反实现多个LED灯状态切换
- 视频把取反对象从 LED1、LED3 改为 LED2、LED4,重新编译和烧录后,第 2、4 盏灯同步闪烁。
- 这里操作的是 P1 寄存器的两个独立位,
LED2、LED4分别是 P1.1、P1.3 的易读别名。 - 多个位可以分别取反,也可以通过掩码整体异或,例如
P1 ^= 0x0A会翻转 bit1 和 bit3。但整体读改写可能涉及端口锁存器和其他外设,初学阶段使用sbit更直观安全。
6.4 知识点: sbit与P1寄存器操作
- 本节涉及的两个准确术语是 P1 寄存器和 sbit。P1 是 8 位特殊功能寄存器,
sbit用来为其中可位寻址的单个位定义别名。 P1 = value;是整体写入 8 位,适合一次输出完整模式;sbit LED1 = P1^0;建立位别名后,LED1 = value;只修改 P1.0。- 两种方式不是任意可互换的。例如
P1 = 0x01会把 P1.1~P1.7 全部写成 0,并非“只操作 P1.0”;在本板上这甚至可能点亮多个 LED 和蜂鸣器。只控制 P1.0 应使用LED1 = 0/1或经过正确掩码的读改写。
6.5 知识点: 软件延时计算器的使用
- 在 STC 下载软件的工具区域找到“软件延时计算器”,系统频率选择本板晶振
11.0592MHz,时间单位选择毫秒,按需要填写 1000ms 或 2000ms,指令集选择 STC89,再生成代码。 - 复制生成的函数后,若其中调用
_nop_(),必须包含<intrins.h>;调用时写Delay1000ms();,不能把函数定义开头的void一起复制到调用位置。 - 软件延时是占用 CPU 的忙等待,实际时间受晶振、1T/6T/12T 模式、编译器版本和优化设置影响。流水灯入门实验可以使用,后续需要并行处理按键或串口时应改用硬件定时器。
七、任务信息资源导图回到P1寄存器定义的介绍
- 本节复盘 P1 寄存器在哪里定义,以及源代码中的名称如何对应到单片机硬件地址。
- 在 Keil 中选中
P1并执行 Go To Definition,正常情况下可跳转到reg52.h中的sfr P1 = 0x90;。若无法跳转,可直接选中reg52.h使用 Open 打开,或检查头文件搜索路径和 Browse Information 是否启用。 - 无论执行
P1 = 0xFF;整体赋值,还是通过sbit LED1 = P1^0;操作单个位,最终访问的都是地址为 0x90 的 P1 特殊功能寄存器及其位地址。
源代码中的 P1
│
▼
reg52.h:sfr P1 = 0x90
│
▼
CPU 访问 SFR 地址 0x90
│
▼
P1 端口锁存/输出电路 ──► P1.0~P1.7 实际引脚 ──► LED/OLED/蜂鸣器
八、P1寄存器的定义
reg52.h使用 Keil C51 的sfr扩展关键字,把 P1 寄存器等名称映射到固定 SFR 地址,例如:
| 头文件声明 | SFR 地址 | 主要作用 |
|---|---|---|
sfr P0 = 0x80; | 0x80 | P0 端口 |
sfr P1 = 0x90; | 0x90 | P1 端口,本节操作对象 |
sfr P2 = 0xA0; | 0xA0 | P2 端口 |
sfr P3 = 0xB0; | 0xB0 | P3 端口及复用功能 |
sfr TCON = 0x88; | 0x88 | 定时器/中断控制 |
sfr TMOD = 0x89; | 0x89 | 定时器模式选择 |
sfr TL0 = 0x8A; / TL1 = 0x8B; | 0x8A/0x8B | 定时器低字节 |
sfr TH0 = 0x8C; / TH1 = 0x8D; | 0x8C/0x8D | 定时器高字节 |
sfr IE = 0xA8; | 0xA8 | 中断允许寄存器 |
- 定时器模式寄存器的名称是
TMOD,定时器 1 低字节寄存器的名称是TL1。头文件还以/* 8052 Extensions */标出 8052 相对 8051 增加的定时器 2 等定义。 reg52.h主要覆盖经典 8051/8052 架构的标准寄存器和位别名,不一定包含 STC89C52RC 的全部厂商扩展寄存器;使用扩展功能时仍要查 STC 数据手册和相应厂商头文件。- 可位寻址 SFR 还可用
sbit给某个位命名。例如头文件中可以看到sbit EA = IE^7;、sbit ES = IE^4;、sbit ET0 = IE^1;。其中 ES 是串口中断允许位,EA 是总中断允许位,ET0 是定时器 0 中断允许位,后续中断和串口课程会使用这些位。 - 删除或注释
#include "reg52.h"后,编译器不再知道 P1、TCON 等名称与地址的对应关系,因此会报告 P1 未定义。这不是芯片中“没有 P1”,而是当前 C 源文件缺少让编译器识别该硬件地址的声明。