1. 写在前面:这份笔记适合谁
这是跟着江科大(江协科技)51 单片机课程学习时整理的一份小白笔记。如果你刚接触单片机,看到满屏的 P3_0、0x01<<num、while(--j); 会一头雾水,那这篇笔记就是为你写的。
本文只讲一段代码------用两个按键控制 LED 流水灯左右移动。别看它短,里面包含了 51 单片机入门阶段几乎所有核心知识点:
- 头文件与寄存器
- 软件延时函数
- 位操作与端口读写
- 按键消抖
- 循环移位与取反
下面逐行拆解。
2. 完整代码
c
#include <REGX52.H>
void Delay(unsigned int xms)
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 199;
do
{
while(--j);
}
while(--i);
}
}
void main()
{
int num = 0;
while(1)
{
if(P3_0 == 0)
{
Delay(20);
while(P3_0 == 0);
Delay(20);
num++;
if(num == 8){num = 0;}
P2 = ~(0x01 << num);
}
if(P3_1 == 0)
{
Delay(20);
while(P3_1 == 0);
Delay(20);
if(num == 0){num = 8;}
num--;
P2 = ~(0x01 << num);
}
}
}
3. 头文件:#include <REGX52.H>
c
#include <REGX52.H>
这一行把 51 单片机的寄存器定义 引进来。有了它,你才能直接写 P2、P3_0 这样的名字。
P2、P3是单片机的一组 8 个引脚,对应 8 个二进制位。P3_0表示 P3 端口的第 0 位(最低位)。
小白提示:
REGX52.H是 Keil 里 STC89C52 / AT89C52 常用的头文件。不同编译器可能叫reg52.h,作用一样。
4. 延时函数:Delay
c
void Delay(unsigned int xms)
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 199;
do
{
while(--j);
}
while(--i);
}
}
这是江科大课程里最经典的软件延时写法,作用是「空转」一段时间,让程序停一停。
拆开看:
while(xms--):传入多少毫秒,就循环多少次。xms--是先用后减,所以循环xms次。i = 2; j = 199;:内层循环的初值,这两个数决定了「1 毫秒」大概要空转多少圈。while(--j);:j从 199 一直减到 0,减到 0 时条件为假,退出。注意这里分号前没有循环体,空语句就是「什么都不做,只消耗时间」。while(--i);:外层再套一层,i从 2 减到 0。
小白提示:这种延时不精确,换晶振频率、换编译器优化等级,时间都会变。入门阶段够用,实际项目建议用定时器。
5. 主函数与变量
c
void main()
{
int num = 0;
while(1)
{
...
}
}
void main():程序入口。int num = 0;:记录当前点亮的是第几个 LED,取值 0~7,对应 8 个灯。while(1):死循环。单片机程序不能「跑完就结束」,必须一直循环检测按键。
6. 按键检测与消抖
以 P3_0 这个按键为例:
c
if(P3_0 == 0)
{
Delay(20);
while(P3_0 == 0);
Delay(20);
...
}
6.1 为什么判断 == 0
51 单片机的按键一般接成按下接地,所以:
- 没按下:引脚被上拉为高电平
1 - 按下:引脚被拉到低电平
0
因此 P3_0 == 0 就表示「按键被按下了」。
6.2 为什么要 Delay(20)
机械按键在按下和松开的瞬间会产生抖动,电平会在 0 和 1 之间快速跳变几毫秒。如果直接判断,可能一次按下被识别成好几次。
Delay(20) 就是等 20 毫秒,等抖动过去再确认,这叫软件消抖。
6.3 while(P3_0 == 0); 是干什么的
这一句是等待松手 。只要按键还按着,程序就卡在这里不动。这样做的效果是:按住不放只算一次,必须松开再按才会触发下一次。
后面的 Delay(20) 是松手消抖,防止松开瞬间的抖动又被误判。
7. 核心:移位与取反点亮 LED
c
num++;
if(num == 8){num = 0;}
P2 = ~(0x01 << num);
这三行是整段代码的精华,一步步看:
7.1 0x01 << num
0x01 的二进制是 0000 0001。左移 num 位:
| num | 0x01 << num 二进制 |
|---|---|
| 0 | 0000 0001 |
| 1 | 0000 0010 |
| 2 | 0000 0100 |
| 3 | 0000 1000 |
| ... | ... |
| 7 | 1000 0000 |
也就是「1 在第 num 位上」。
7.2 ~ 按位取反
51 开发板上的 LED 通常是低电平点亮(引脚输出 0 才亮)。所以要把「1 的位置」取反,变成「只有那一位是 0」:
~(0000 0001) = 1111 1110
这样 P2 端口输出 1111 1110,只有最低位是 0,对应的那个 LED 亮,其余全灭。
7.3 num++ 与边界处理
c
num++;
if(num == 8){num = 0;}
每按一次,num 加 1,灯就往左移一位。加到 8 时已经超出 0~7 的范围,所以归零,形成循环流水灯。
8. 反向移动:P3_1 按键
c
if(P3_1 == 0)
{
Delay(20);
while(P3_1 == 0);
Delay(20);
if(num == 0){num = 8;}
num--;
P2 = ~(0x01 << num);
}
逻辑和上面完全对称,只是方向相反:
if(num == 0){num = 8;}:先判断再减。如果当前是 0,减 1 会变成 -1,越界了,所以先把它设成 8,减完正好是 7。num--:灯往右移一位。
小白提示:注意这里
if在num--之前 ,顺序不能反。如果写成先减再判断,num会先变成 -1,0x01 << -1是未定义行为,程序会出怪问题。
9. 整体流程图
#mermaid-svg-yKXwlJteEe7s0lbM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yKXwlJteEe7s0lbM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yKXwlJteEe7s0lbM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yKXwlJteEe7s0lbM .error-icon{fill:#552222;}#mermaid-svg-yKXwlJteEe7s0lbM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yKXwlJteEe7s0lbM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yKXwlJteEe7s0lbM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yKXwlJteEe7s0lbM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yKXwlJteEe7s0lbM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yKXwlJteEe7s0lbM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yKXwlJteEe7s0lbM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yKXwlJteEe7s0lbM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yKXwlJteEe7s0lbM .marker.cross{stroke:#333333;}#mermaid-svg-yKXwlJteEe7s0lbM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yKXwlJteEe7s0lbM p{margin:0;}#mermaid-svg-yKXwlJteEe7s0lbM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yKXwlJteEe7s0lbM .cluster-label text{fill:#333;}#mermaid-svg-yKXwlJteEe7s0lbM .cluster-label span{color:#333;}#mermaid-svg-yKXwlJteEe7s0lbM .cluster-label span p{background-color:transparent;}#mermaid-svg-yKXwlJteEe7s0lbM .label text,#mermaid-svg-yKXwlJteEe7s0lbM span{fill:#333;color:#333;}#mermaid-svg-yKXwlJteEe7s0lbM .node rect,#mermaid-svg-yKXwlJteEe7s0lbM .node circle,#mermaid-svg-yKXwlJteEe7s0lbM .node ellipse,#mermaid-svg-yKXwlJteEe7s0lbM .node polygon,#mermaid-svg-yKXwlJteEe7s0lbM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yKXwlJteEe7s0lbM .rough-node .label text,#mermaid-svg-yKXwlJteEe7s0lbM .node .label text,#mermaid-svg-yKXwlJteEe7s0lbM .image-shape .label,#mermaid-svg-yKXwlJteEe7s0lbM .icon-shape .label{text-anchor:middle;}#mermaid-svg-yKXwlJteEe7s0lbM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yKXwlJteEe7s0lbM .rough-node .label,#mermaid-svg-yKXwlJteEe7s0lbM .node .label,#mermaid-svg-yKXwlJteEe7s0lbM .image-shape .label,#mermaid-svg-yKXwlJteEe7s0lbM .icon-shape .label{text-align:center;}#mermaid-svg-yKXwlJteEe7s0lbM .node.clickable{cursor:pointer;}#mermaid-svg-yKXwlJteEe7s0lbM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yKXwlJteEe7s0lbM .arrowheadPath{fill:#333333;}#mermaid-svg-yKXwlJteEe7s0lbM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yKXwlJteEe7s0lbM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yKXwlJteEe7s0lbM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yKXwlJteEe7s0lbM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yKXwlJteEe7s0lbM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yKXwlJteEe7s0lbM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yKXwlJteEe7s0lbM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yKXwlJteEe7s0lbM .cluster text{fill:#333;}#mermaid-svg-yKXwlJteEe7s0lbM .cluster span{color:#333;}#mermaid-svg-yKXwlJteEe7s0lbM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yKXwlJteEe7s0lbM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yKXwlJteEe7s0lbM rect.text{fill:none;stroke-width:0;}#mermaid-svg-yKXwlJteEe7s0lbM .icon-shape,#mermaid-svg-yKXwlJteEe7s0lbM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yKXwlJteEe7s0lbM .icon-shape p,#mermaid-svg-yKXwlJteEe7s0lbM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yKXwlJteEe7s0lbM .icon-shape .label rect,#mermaid-svg-yKXwlJteEe7s0lbM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yKXwlJteEe7s0lbM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yKXwlJteEe7s0lbM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yKXwlJteEe7s0lbM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
main 开始
num = 0
P3_0 按下?
消抖 + 等待松手
num++,到 8 归零
P2 = ~(0x01 << num)
P3_1 按下?
消抖 + 等待松手
num 为 0 则设为 8,再 num--
P2 = ~(0x01 << num)
10. 小白常见疑问
Q1:为什么 while(--j); 后面有个分号?
分号就是空循环体,表示「只做 --j 和判断,不做别的事」。这是延时函数的常见写法。
Q2:P2 = ~(0x01 << num); 能不能写成 P2 = 0xFE << num;?
可以,效果类似,但要注意左移溢出。用 ~ 的写法更直观,也更容易理解「哪一位是 0」。
Q3:为什么按键要接在 P3 而不是 P2?
因为 P2 已经接了 LED。实际开发板上按键和 LED 接在哪个口是硬件决定的,写代码前先看原理图。
Q4:int num 能不能改成 unsigned char?
可以,而且更省空间。这里用 int 只是习惯写法,num 最大只到 8,unsigned char 完全够用。
11. 小结
这段代码虽然只有几十行,但把 51 单片机入门的核心套路都串起来了:
| 知识点 | 对应代码 |
|---|---|
| 头文件与寄存器 | #include <REGX52.H>、P2、P3_0 |
| 软件延时 | Delay() |
| 按键消抖 | Delay(20) + while(P3_0 == 0); |
| 位操作 | 0x01 << num、~ |
| 循环流水灯 | num++ / num-- + 边界判断 |
建议你自己动手改一改:比如把延时改成 10ms 看看效果,或者让两个按键控制不同速度。改代码、看现象,是学单片机最快的方式。