【51单片机入门】从点亮第一盏LED开始:四个实验开启我的嵌入式学习

写在前面

8 月 12 日白天还在对着屏幕练数据结构和算法,到了晚上突然想换换脑子,于是把之前买来、却一直没有真正学下去的 51 单片机开发板重新翻了出来。

说起来,我第一次接触单片机还是去年夏天。刚知道自己被软件工程专业录取时,想着提前接触一些专业内容,就找到了江协科技的 51 单片机入门教程。可那时候连 C 语言都没有系统学过,看着代码里的循环、函数、寄存器和 IO 口几乎全程懵,硬撑着看了两三集之后,这件事也就暂时搁置了。

转眼大一学年已经结束。从最基础的 C 语言语法,到函数、数组、指针,再到后来自己实现顺序表、单链表和双向链表,至少现在再看到单片机代码时,不会先被 C 语言本身挡住了。这样一来,学习重点也终于可以从“这段代码是什么意思”慢慢转向“这段代码为什么能控制硬件”。

而嵌入式本身也是我很感兴趣的一个方向。天天和控制台、算法题打交道久了,也很想体验一下代码真正作用在物理器件上的感觉。

所以 8 月 12 日晚,我正式重新开始了 51 单片机学习,并从最基础的 LED 入手,一口气完成了四个小实验:

  • 点亮一个 LED

  • LED 闪烁

  • LED 流水灯

  • 封装毫秒延时,实现渐变加速流水灯

这也是我 CSDN 上第一篇正式的嵌入式方向学习记录,算是给后面的硬件学习开一个头。

 本篇源码仓库:
Code_2026 / 嵌入式-单片机 / C51

四个实验均按独立文件夹保存,并附有编译生成的 .hex 文件,方便后续复习和烧录测试。


一、正式开始前:先认识一下 51 单片机

1.1 什么是 51 单片机?

单片机(Microcontroller)可以简单理解成:把 CPU、存储器、IO 接口、定时器等功能集成在一块芯片里的微型计算机。而平时学习中常说的“51 单片机”,并不是特指某一个芯片型号,而是一类兼容经典 8051 架构的单片机。

我目前使用的是比较常见的:STC89C52RC;对于刚开始学习嵌入式来说,51 的优势很明显:资料多、外设简单、成本低,而且很多硬件现象都能直接看到,非常适合用来建立“软件怎样控制硬件”的基本认识。

相比直接上 STM32,先通过 51 把 IO、电平、寄存器、定时器、中断这些概念过一遍,对我来说也更容易接受。


1.2 目前使用的开发环境

现阶段主要使用:Keil uVision5,负责:写代码 → 编译 → 生成 .hex 固件文件;51 单片机代码依然使用 C 语言,但会接触到很多和普通控制台程序完全不同的内容,比如寄存器、IO 端口以及硬件相关头文件。

STC-ISP

主要负责把 Keil 编译出来的 .hex 文件烧录进 STC 单片机。另外它还提供了一些很方便的小工具,比如本篇使用的软件延时函数,就可以根据晶振频率直接生成。

开发板

本次实验使用 STC89C52RC 开发板,板载 LED、按键、数码管等基础外设。目前四个实验主要使用:P2 端口 + 板载 LED。


二、第一次真正理解:代码是怎样把 LED 点亮的?

在普通 C 语言里:

printf("Hello World");

最终只是在屏幕上显示一句话,但到了单片机里:

P2 = 0xFE;

却可以真的让开发板上的 LED 亮起来,这背后首先要理解几个最基础的概念。

1. IO 口

单片机需要通过 IO 引脚和外部器件交互。51 中常见的是:P0、P1、P2、P3,每组端口包含 8 位,因此可以分别控制多个引脚。本篇实验主要使用:P2。


2. 高低电平

目前可以先简单理解为数字电路中的两种逻辑状态:

  • 高电平 → 逻辑 1

  • 低电平 → 逻辑 0

而对于我当前使用的这块开发板,LED 采用的是低电平点亮的接法。

所以:0 → LED 亮,1 → LED 灭,这也是为什么后面的代码中:

1111 1110

最低位为 0 时,第一个 LED 会被点亮。


3. P0 口和上拉电阻

经典 51 架构中,P0 口的输出结构和 P1~P3 不完全一样,使用时通常还需要考虑外部上拉。

这部分目前我还没有实际操作,先记住它和其他几个端口存在区别,等后面真正使用 P0 时再结合电路理解。


4. 软件延时

入门实验暂时还没有学习定时器,所以 LED 的延时主要通过程序不断执行循环来消耗 CPU 时间。这种方法很好理解,也方便观察现象,但缺点同样明显:CPU 在延时期间基本一直被占用。后面学习到定时器之后,还会再回来解决这个问题。


三、实验一:点亮第一盏 LED

3.1 实验目标

先从单片机版的“Hello World”开始:让第一个 LED 一直亮着。

代码如下:

#include <REGX52.H>

int main()
{
	while(1)
	{
		P2=0xFE;//1111 1110
	}
}

第一次看到:

P2 = 0xFE;

其实有点像之前 C 语言里的普通赋值。但这里的 P2 对应的是单片机 IO 端口,给它赋值之后,实际上改变的是硬件引脚上的电平状态。

0xFE 转成二进制为:

1111 1110

其中最低位是 0。

由于当前开发板上的 LED 为低电平点亮,所以对应的第一盏 LED 就会亮起。

REGX52.H 则是 51 单片机开发中非常常见的头文件,其中定义了 P2 等特殊功能寄存器,包含之后才能直接在代码中使用这些名称。

至于:

while(1)

也是嵌入式程序中非常常见的结构。单片机通常需要上电以后持续运行任务,而不是像普通控制台程序那样完成一次运算就退出,因此主函数通常都会进入一个长期循环。

当第一盏 LED 真正亮起来的时候,虽然代码只有几行,但感觉还是和普通 C 语言很不一样:

代码第一次真正影响到了屏幕以外的东西。


四、实验二:让 LED 闪起来

单纯常亮之后,下一步自然就是:让 LED 一亮一灭。

完整代码:

#include <REGX52.H>
#include <INTRINS.H>

void Delay500ms(void)	//@11.0592MHz
{
	unsigned char data i, j, k;

	_nop_();
	i = 4;
	j = 129;
	k = 119;
	do
	{
		do
		{
			while (--k);
		} while (--j);
	} while (--i);
}

int main()
{
	while(1)
	{
		P2=0xFE;
		Delay500ms();
		P2=0xFF;
		Delay500ms();
	}
}

这里相比第一个实验,多出来的核心就是:延时函数。

本次使用的晶振频率为:11.0592 MHz;延时函数通过 STC-ISP 的软件延时工具生成,并使用:

_nop_();

进行空操作,因此需要包含:

#include <INTRINS.H>

主循环的逻辑实际上非常简单:

点亮
↓
等待 500 ms
↓
熄灭
↓
等待 500 ms
↓
重新开始

P2 = 0xFE 时第一个 LED 点亮,而:

P2 = 0xFF;

对应:

1111 1111

八位全部为高电平,因此 LED 熄灭。不断重复之后,就形成了肉眼可以观察到的周期性闪烁。


五、实验三:八个 LED 开始“跑”起来

会控制一个灯以后,第三个实验开始一次控制整个 P2 端口的八位:让 8 个 LED 依次点亮,形成流水灯效果。

代码:

#include <REGX52.H>
#include <INTRINS.H>

void Delay500ms(void)	//@11.0592MHz
{
	unsigned char data i, j, k;

	_nop_();
	i = 4;
	j = 129;
	k = 119;
	do
	{
		do
		{
			while (--k);
		} while (--j);
	} while (--i);
}

int main()
{
	while(1)
	{
		P2=0xFE;//1111 1110
		Delay500ms();
		P2=0xFD;
		Delay500ms();
		P2=0xFB;
		Delay500ms();
		P2=0xF7;
		Delay500ms();
		P2=0xEF;
		Delay500ms();
		P2=0xDF;
		Delay500ms();
		P2=0xBF;
		Delay500ms();
		P2=0x7F;
		Delay500ms();
	}
}

观察这些十六进制数:

FE
FD
FB
F7
EF
DF
BF
7F

转换成二进制以后,可以发现那个唯一的 0 在不断移动:

1111 1110
1111 1101
1111 1011
1111 0111
1110 1111
1101 1111
1011 1111
0111 1111

因为当前开发板是低电平点亮 LED,所以这个不断移动的 0,也就对应着不断移动的亮灯位置。

这也是我第一次比较直观地感受到:

一个 8 位二进制数,可以直接对应 8 个真实 IO 引脚的状态。

以前学位运算的时候,更多还是在纸面和算法题里处理 0、1;到了单片机这里,这些位终于有了很直观的硬件含义。

目前为了方便理解,我还是采用逐个赋值的方式。等后面继续学移位操作和更灵活的写法之后,再回来优化这一版流水灯。


六、实验四:把固定延时改成通用延时

前三个实验里使用的都是固定:

Delay500ms();

但如果每次都重新写一个 Delay100ms()、Delay200ms(),代码显然不够灵活。

所以第四个实验开始尝试利用之前已经学过的函数参数,把延时函数封装得更通用一些。

代码:

#include <REGX52.H>
#include <INTRINS.H>

void Delay1ms(unsigned int xms)	//@11.0592MHz
{
	unsigned char data i, j;
	
	while(xms)
	{
		_nop_();
		i = 2;
		j = 199;
		do
		{
			while (--j);
		} while (--i);
		xms--;
	}

}

int main()
{
	while(1)
	{
		P2=0xFE;//1111 1110
		Delay1ms(900);
		P2=0xFD;
		Delay1ms(800);
		P2=0xFB;
		Delay1ms(700);
		P2=0xF7;
		Delay1ms(600);
		P2=0xEF;
		Delay1ms(500);
		P2=0xDF;
		Delay1ms(400);
		P2=0xBF;
		Delay1ms(300);
		P2=0x7F;
		Delay1ms(200);
	}
}

这里最大的变化并不在 LED,而在:

void Delay1ms(unsigned int xms)

延时长度不再固定,而是通过参数传入。

例如:

Delay1ms(900);

就是大约延时 900 ms,而:

Delay1ms(200);

就是大约延时 200 ms。

主函数中依次使用:

900
800
700
600
500
400
300
200

所以每一盏灯停留的时间都会逐渐缩短,最终表现出来的效果就是:

流水灯越来越快。

这个实验其实没有使用什么新的 C 语言知识,但恰好让我体会到了之前学函数封装时说的“代码复用”。

以前只是在控制台程序里理解:把重复代码封装成函数。

现在它变成了:写一个通用延时函数,就可以直接控制真实硬件的运行节奏。

同一个知识点放到硬件里,体验完全不一样。


七、四个实验放在一起看,我到底学了什么?

如果只看最终效果,这四个实验其实都很简单:

实验实现效果主要认识
点亮 LED单灯常亮IO 与高低电平
LED 闪烁周期亮灭软件延时
流水灯8 灯依次点亮端口与二进制位
渐变流水灯流速逐渐加快参数与函数封装

但是对我来说,更重要的是开始建立几个以前写普通 C 程序时没有的概念:

第一,变量和赋值有可能直接对应硬件状态。

P2 = 0xFE 不再只是修改一个普通变量,而是真的改变了一组 IO 引脚。

第二,二进制位开始和实际电路一一对应。

以前看到:

1111 1110

只是一个数字。

现在看到它,会开始想到:

哪一位是低电平?哪一个 LED 会亮?

第三,程序运行的时间也开始产生物理现象。

500 ms 和 200 ms 不只是两个数字,它们会直接决定 LED 在眼睛里到底闪多快、跑多快。

这些应该才是第一天学习 51 单片机最值得记录的东西。


八、写在最后:去年看不懂的东西,现在终于重新开始了

8 月 12 日重新打开 51 单片机教程时,我最大的感受其实不是“单片机变简单了”,而是:

和去年相比,我终于有了一些能够继续往下学的基础。

去年看教程时,循环、函数、变量、参数本身都还是问题,所以硬件内容和 C 语言内容全部混在一起,几乎每一行代码都需要停下来理解。

现在再来看,C 语言部分已经不再是主要障碍。至少看到:

while(1)

知道为什么要循环;

看到:

Delay1ms(500);

知道参数是怎么传进去的;

看到:

P2 = 0xFE;

也可以把重点放在 P2 和实际硬件之间到底发生了什么。

这让我更加确定了一点:

有些东西第一次学不下去,不一定是内容本身学不会,也可能只是前置知识还没有补齐。

而且相比单纯在屏幕上输出结果,单片机确实带来了完全不一样的反馈。自己写下的一行代码,烧录进去之后,面前的 LED 真的会亮、会灭、会按照设定的节奏跑起来。

第一次真正产生了一种:代码开始从屏幕走向现实世界。

当然,目前这四个实验还只是最基础的起点。软件延时本身就有明显局限,流水灯的写法也还有很多可以继续优化的地方。但第一天的目标本来就不是一步做到多复杂,而是先把开发环境跑通、理解最基础的 IO 控制,并真正开始动手。

接下来还会继续学习:独立按键、数码管、定时器、中断以及更多基础外设。51 只是嵌入式学习的第一站,后面也会继续往 STM32 等方向深入。至少这一次,去年只看了几集就搁置的单片机学习,终于正式重新开始了。


本篇代码仓库

四个实验的源码及编译后的 .hex 文件均已整理到: Code_2026 / 嵌入式-单片机 / C51

后续 51 单片机相关实验也会继续整理在这个目录中。

 

更多推荐