嵌入式-51单片机入门(一):从点亮第一盏LED开始
【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 单片机相关实验也会继续整理在这个目录中。
更多推荐

所有评论(0)