51 单片机 LED 流水灯:从直接法到循环法(江科大课程笔记)

1. 这段代码在做什么

这是江科大 51 单片机课程里最经典的入门实验:用 P2 口控制 8 个 LED,做出"流水灯"效果------灯从 P2.0 到 P2.7 依次点亮,再折返回来,循环往复。

整段代码其实包含三部分:

  1. Delay():软件延时函数,用来控制灯亮灭的速度;
  2. 被注释掉的 main():直接法,一个个写死 P2 的值;
  3. 真正运行的 main():循环法,用移位运算自动生成 P2 的值。

小白看这段代码,最容易卡在"为什么 P2 赋值是 0xfe、0xfd 这些奇怪数字"以及"~(0x01<<i) 到底算出了什么"。下面逐个拆开讲。

2. 前置知识:LED 是低电平点亮

开发板上的 LED 一般是这样接的:

text 复制代码
VCC ── 电阻 ── LED ── P2.x 引脚

也就是说,引脚输出低电平(0)时灯亮,输出高电平(1)时灯灭。

所以想让"第 0 个灯亮、其余灭",P2 应该是:

text 复制代码
P2.7 P2.6 P2.5 P2.4 P2.3 P2.2 P2.1 P2.0
 1    1    1    1    1    1    1    0

写成二进制就是 1111 1110,也就是十六进制的 0xFE。这就是代码里那些"奇怪数字"的来源。

3. 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-- 是先用后减,所以传入 500 就循环 500 次。
  • i=2; j=199;:内层两层空循环的初值。while(--j); 和 while(--i); 后面都是分号,表示循环体为空,纯粹靠 CPU 空转消耗时间。
  • 内层 do...while(--i) 一共跑 2 轮,每轮 while(--j) 跑 199 次,所以一次外层循环大约消耗 2 × 199 次自减操作。

小提示:这种延时不精确,换晶振频率、换编译器优化等级,实际时间都会变。它只是"够用",课程里用它是因为简单直观。真正精确的延时要用定时器。

4. 直接法:把每个状态写死

被注释掉的那段 main() 就是直接法:

c 复制代码
P2 = 0xfe;	//1111 1110  P2.0 亮
Delay(500);
P2 = 0xfd;	//1111 1101  P2.1 亮
Delay(500);
P2 = 0xfb;	//1111 1011  P2.2 亮
Delay(500);
P2 = 0xf7;	//1111 0111  P2.3 亮
Delay(500);
P2 = 0xef;	//1110 1111  P2.4 亮
Delay(500);
P2 = 0xdf;	//1101 1111  P2.5 亮
Delay(500);
P2 = 0xbf;	//1011 1111  P2.6 亮
Delay(500);
P2 = 0x7f;	//0111 1111  P2.7 亮
Delay(500);

把注释里的二进制竖着看,会发现那个 0 在从右往左移动:

text 复制代码
1111 1110
1111 1101
1111 1011
1111 0111
1110 1111
1101 1111
1011 1111
0111 1111

这就是"流水"的本质:一个 0 在 8 位里逐位左移。

直接法的问题很明显:8 个灯要写 8 行,如果板子有 16 个灯就要写 16 行,而且一旦想改方向、改速度,全得手改。所以有了循环法。

5. 循环法:用移位运算自动生成

c 复制代码
void main()
{
	int i;
	while(1)
	{
		for(i=0;i<8;i++)
		{
			P2 = ~(0x01<<i);
			Delay(500);
		}
		for(i=0;i<6;i++)	//去掉两次重复的头尾
		{
			P2 = ~(0x40>>i);
			Delay(500);
		}
	}
}

5.1 第一段:正向流水

c 复制代码
P2 = ~(0x01<<i);

拆成两步看:

  1. 0x01 << i:把 0000 0001 左移 i 位。i=0 时是 0000 0001,i=3 时是 0000 1000。
  2. ~:按位取反。0000 0001 取反变成 1111 1110,正好就是 0xFE。

所以 i 从 0 到 7,P2 依次得到:

i 0x01<<i ~(0x01<<i) 点亮的灯
0 0000 0001 1111 1110 P2.0
1 0000 0010 1111 1101 P2.1
2 0000 0100 1111 1011 P2.2
3 0000 1000 1111 0111 P2.3
4 0001 0000 1110 1111 P2.4
5 0010 0000 1101 1111 P2.5
6 0100 0000 1011 1111 P2.6
7 1000 0000 0111 1111 P2.7

和直接法那 8 行完全一致,但只用了 1 行代码。

5.2 第二段:反向流水

c 复制代码
P2 = ~(0x40>>i);

0x40 是 0100 0000,右移 i 位再取反:

i 0x40>>i ~(0x40>>i) 点亮的灯
0 0100 0000 1011 1111 P2.6
1 0010 0000 1101 1111 P2.5
2 0001 0000 1110 1111 P2.4
3 0000 1000 1111 0111 P2.3
4 0000 0100 1111 1011 P2.2
5 0000 0010 1111 1101 P2.1

灯从 P2.6 一路回到 P2.1,形成"折返"。

5.3 为什么是 0x40 和 i<6?

这是小白最容易疑惑的地方,注释里写的"去掉两次重复的头尾"就是这个意思。

第一段结束时,灯停在 P2.7 (i=7)。如果第二段从 P2.7 开始,就会连续两次点亮 P2.7 ,看起来像卡了一下。所以第二段从 P2.6 开始,0x40 对应的正是 P2.6。

同理,第二段如果走到 P2.0,下一轮第一段又从 P2.0 开始,又会重复。所以第二段只走到 P2.1 就停,也就是 i 只取 0~5 共 6 次。

text 复制代码
P2.0 → P2.1 → ... → P2.7 → P2.6 → ... → P2.1 → P2.0 → ...
        ↑ 正向 8 次              ↑ 反向 6 次

这样首尾衔接,视觉上就是一个平滑的来回流水。

6. 几个容易踩的坑

  • ~ 和 ! 别搞混 :~ 是按位取反(~0x01 = 0xFE),! 是逻辑非(!0x01 = 0)。这里必须用 ~。
  • << 和 >> 的位数 :0x01<<7 是 0x80,没问题;但如果继续 <<8,在 8 位机上会溢出回 0,所以循环边界要卡准。
  • int i 与 unsigned char i :这里用 int 是为了和 for 循环比较时不出意外,实际 51 上 int 是 16 位,够用。
  • while(1) 不能少:单片机程序没有"退出"的概念,主循环必须死循环,否则程序跑飞。
  • 延时时间 :Delay(500) 并不是精确 500ms,实际值取决于晶振(常见 11.0592MHz 或 12MHz)和编译器。想精确就用定时器中断。

7. 小结

写法 代码量 可读性 扩展性
直接法 8 行赋值 直观,适合理解原理 差,改灯数要重写
循环法 2 个 for 需要理解移位和取反 好,改边界即可

学习路径建议:先看懂直接法里那个 0 是怎么移动的,再理解循环法如何用 <<、>>、~ 把同样的过程自动化。这两步走通,后面学数码管、矩阵键盘的动态扫描,思路是一模一样的------都是"用位运算生成端口值 + 延时切换"。

相关推荐
Zwawa12 小时前
用两个按键做一个OLED菜单:先把选择和执行分开
c语言·单片机·嵌入式
Dev_XH15 小时前
【成品设计】基于CH32H417的CH32H417AIOT家庭微电网
单片机·嵌入式硬件·物联网·ai
一壶浊酒伴余生16 小时前
新概念英语第一册 · 句法笔记
笔记·英语学习·英语语法·新概念英语
我不会起名字32217 小时前
消费回调固定 return nil:错误传播与 Kafka 位点提交的区别
后端·mongodb·kafka·go·im·故障排查
蒸蒸yyyyzwd19 小时前
后端学习笔记 day111
笔记·学习
叼烟扛炮20 小时前
[LangChain] 学习笔记(08):工具调用底层闭环与第三方搜索(Tavily)实战
人工智能·笔记·学习·langchain
en.en..20 小时前
STM32入门最佳学习顺序(寄存器→标准库→HAL库)
单片机·嵌入式硬件
江屿风21 小时前
【Git-组件】【Git入门:从文档灾难到版本掌控】流食般投喂
运维·笔记·git·组件·版本控制
二本咕咕-机械转码1 天前
FreeRTOS深入解析(八)IPC机制之FromISR详解
数据结构·单片机·链表