重拾C51-DAY1(项目使用STC8G 系列)
重拾C51-DAY1(项目使用STC8G 系列)
51单片机C语言重新入门:第一天认识C51程序结构与LED闪烁
前言
最近准备重新学习 51 单片机 C 语言。之前接触过一些单片机相关内容,但是时间久了很多知识点已经忘记了。目前只记得一些 C 语言数据类型、变量、函数等基础概念,所以打算从最基础的 C51 程序结构重新开始。
本篇文章是我重新学习 51 单片机 C 语言的第一天笔记,主要目标是看懂一个最基本的 LED 闪烁程序,理解 main()、while(1)、sbit、Delay_ms()、IO 高低电平等基础概念。
本篇适合:
- 刚开始学习 51 单片机的初学者
- 之前学过但已经忘记的同学
- 有硬件基础但 C51 编程不熟的人
- 想用 STC8G、STC89C52 等 51 单片机做小项目的人
一、第一天学习目标
第一天不追求学习复杂外设,也不急着学习定时器、中断、串口、ADC、PWM 等内容。
今天的核心目标只有一个:
看懂一个最基本的 51 单片机 C 程序,并理解代码如何控制单片机 IO 引脚输出高低电平。
具体来说,需要掌握以下内容:
- 认识 51 单片机 C 程序的基本结构;
- 理解
#include的作用; - 理解
sbit的作用; - 理解
main()函数; - 理解
while(1)主循环; - 理解
LED = 0和LED = 1的含义; - 理解 LED 为什么会亮、灭、闪烁;
- 理解简单延时函数
Delay_ms()的作用; - 能修改 LED 闪烁速度;
- 能根据实际硬件修改 LED 引脚。
二、先看一个最基本的LED闪烁程序
假设条件如下:
- 单片机:STC8G 系列;
- LED 接在 P3.7 引脚;
- LED 为低电平点亮;
- 使用 Keil C51 编译。
示例代码如下:
#include "STC8G.H"
sbit LED = P3^7;
void Delay_ms(unsigned int ms)
{
unsigned int i;
unsigned int j;
for(i = 0; i < ms; i++)
{
for(j = 0; j < 1000; j++)
{
;
}
}
}
void main(void)
{
while(1)
{
LED = 0;
Delay_ms(500);
LED = 1;
Delay_ms(500);
}
}
这段程序实现的现象是:
LED 亮 0.5 秒
LED 灭 0.5 秒
然后不断重复
也就是我们最常见的 LED 闪烁实验。
三、单片机程序和普通电脑程序有什么不同?
普通电脑程序通常是这样的:
程序开始
执行任务
任务完成
程序退出
比如电脑上的一个计算器程序,用户打开后进行计算,关闭后程序就结束了。
但是单片机程序一般不是这样。
单片机上电以后,通常需要一直运行。例如:
- 电饭煲控制板要一直检测按键和温度;
- 玩具控制板要一直检测按键和电池状态;
- 电机控制板要一直输出控制信号;
- 瞄准镜控制板要一直处理按键、亮度、低电量和休眠唤醒。
所以单片机程序一般不会执行完就退出,而是一直循环运行。
因此,单片机程序通常写成下面这种结构:
void main(void)
{
while(1)
{
// 一直循环执行的代码
}
}
可以这样理解:
main() 是程序入口;
while(1) 是主循环;
主循环里面写什么,单片机就一直重复做什么。
四、逐行讲解LED闪烁程序
下面开始逐行分析前面的 LED 闪烁代码。
1. #include "STC8G.H"
代码:
#include "STC8G.H"
这句话的作用是:
引入 STC8G 单片机的头文件。
头文件里面提前定义了很多单片机内部资源,例如:
P0
P1
P2
P3
P4
P5
TMOD
TH0
TL0
IE
IP
这些名字不是普通变量,而是单片机内部的特殊功能寄存器。
可以把寄存器理解成:
单片机内部的一组控制开关。
例如:
P3 = 0xFF;
表示控制 P3 端口的 8 个 IO 引脚全部输出高电平。
再比如:
P3 = 0x00;
表示控制 P3 端口的 8 个 IO 引脚全部输出低电平。
如果程序里面没有写:
#include "STC8G.H"
那么编译器就不知道 P3 是什么,也不知道 P3^7 代表哪个引脚。
所以,头文件的作用可以简单理解为:
让编译器认识这个单片机的寄存器和引脚名称。
2. sbit LED = P3^7;
代码:
sbit LED = P3^7;
这是 C51 里面非常重要的一种写法。
sbit 的作用是:
定义特殊功能寄存器中的某一位。
这句话可以理解为:
把 P3.7 这个引脚起名叫 LED。
之后在代码中写:
LED = 0;
实际上就是让 P3.7 输出低电平。
写:
LED = 1;
实际上就是让 P3.7 输出高电平。
也就是说:
P3.7 是单片机真实的硬件引脚名称;
LED 是我们在程序里给这个引脚起的名字。
这样写代码会更直观。
如果不使用 sbit,后面每次控制 LED 都要直接操作 P3 端口,代码可读性会变差。
3. P3.7 到底是什么意思?
51 单片机的 IO 口通常按照端口分组,例如:
P0 口:P0.0 ~ P0.7
P1 口:P1.0 ~ P1.7
P2 口:P2.0 ~ P2.7
P3 口:P3.0 ~ P3.7
P4 口:P4.0 ~ P4.7
P5 口:P5.0 ~ P5.7
所以:
P3.7
表示:
P3 这一组端口中的第 7 位。
在 C51 代码中,P3.7 通常写成:
P3^7
注意这里的 ^ 不要理解成数学中的乘方。
在下面这种语句中:
sbit LED = P3^7;
P3^7 表示的是:
P3 端口的第 7 位,也就是 P3.7。
4. void main(void)
代码:
void main(void)
{
}
这是程序入口。
单片机上电后,经过启动文件和初始化流程,最终会进入 main() 函数执行用户程序。
其中第一个 void 表示:
main 函数没有返回值。
括号中的 void 表示:
main 函数不需要传入参数。
所以:
void main(void)
可以简单理解为:
这是主函数,不需要输入参数,也不返回结果。
对于 51 单片机入门阶段,可以先把它当成固定写法。
5. while(1)
代码:
while(1)
{
}
while 是循环语句。
基本格式是:
while(条件)
{
条件成立时一直执行这里的代码
}
在 C 语言中,1 表示真。
所以:
while(1)
意思就是:
条件永远成立,所以程序会一直循环执行。
单片机程序中,while(1) 不是错误,而是非常常见的主循环写法。
例如:
void main(void)
{
while(1)
{
LED = 0;
Delay_ms(500);
LED = 1;
Delay_ms(500);
}
}
这段程序的执行过程是:
LED 输出低电平
延时 500ms
LED 输出高电平
延时 500ms
回到开头继续执行
所以 LED 就会一直闪烁。
五、重点理解:LED = 0 和 LED = 1
代码:
LED = 0;
表示:
让 LED 对应的 IO 引脚输出低电平。
代码:
LED = 1;
表示:
让 LED 对应的 IO 引脚输出高电平。
但是一定要注意:
输出低电平不一定代表 LED 灭,输出高电平也不一定代表 LED 亮。
LED 是亮还是灭,要看硬件电路怎么接。
1. LED低电平点亮
很多 51 单片机开发板的 LED 是这样接的:
VCC ---- 电阻 ---- LED ---- 单片机 IO
当 IO 输出低电平时,电流路径是:
VCC → 电阻 → LED → 单片机 IO → GND
此时电流形成回路,LED 点亮。
所以这种接法下:
LED = 0; // LED 亮
LED = 1; // LED 灭
这种方式叫做:
低电平有效。
2. LED高电平点亮
也有一些电路是这样接的:
单片机 IO ---- 电阻 ---- LED ---- GND
当 IO 输出高电平时,电流路径是:
单片机 IO → 电阻 → LED → GND
此时 LED 点亮。
所以这种接法下:
LED = 1; // LED 亮
LED = 0; // LED 灭
这种方式叫做:
高电平有效。
3. 这一点非常重要
初学时一定不要死记:
LED = 0 就是灭;
LED = 1 就是亮。
正确理解应该是:
LED = 0 表示 IO 输出低电平;
LED = 1 表示 IO 输出高电平;
LED 亮还是灭取决于硬件接法。
以后看别人的代码时,如果看到:
LED = 0;
不要直接认为这是关灯,它也可能是点亮 LED。
六、Delay_ms(500) 的作用
代码:
Delay_ms(500);
表示调用一个延时函数,让程序大概等待 500 毫秒。
500 毫秒就是 0.5 秒。
因此下面这段代码:
LED = 0;
Delay_ms(500);
LED = 1;
Delay_ms(500);
表示:
LED 对应引脚输出低电平,保持约 0.5 秒;
LED 对应引脚输出高电平,保持约 0.5 秒。
如果 LED 是低电平点亮,那么现象就是:
LED 亮 0.5 秒;
LED 灭 0.5 秒;
循环闪烁。
七、延时函数怎么理解?
延时函数如下:
void Delay_ms(unsigned int ms)
{
unsigned int i;
unsigned int j;
for(i = 0; i < ms; i++)
{
for(j = 0; j < 1000; j++)
{
;
}
}
}
这个函数本质上是在让单片机“空转一段时间”。
可以把它理解成让一个人原地数数:
数到 1000;
再数到 1000;
再数到 1000;
……
在数数的过程中,这个人没有做别的事情,只是在消耗时间。
单片机执行空循环时也是一样,只是在不断执行无意义的循环,从而达到延时效果。
1. unsigned int ms
代码:
void Delay_ms(unsigned int ms)
这里的 ms 是函数参数。
比如调用:
Delay_ms(500);
就表示把 500 传给 ms。
也就是告诉函数:
我要延时大约 500 个单位。
在这个例子中,我们希望它大约等于 500 毫秒。
2. for 循环
代码:
for(i = 0; i < ms; i++)
{
}
表示:
i 从 0 开始;
只要 i 小于 ms,就执行循环;
每执行一次,i 加 1。
如果 ms = 500,那么外层循环会执行 500 次。
里面还有一个内层循环:
for(j = 0; j < 1000; j++)
{
;
}
表示:
j 从 0 数到 999;
期间什么也不做。
3. 单独一个分号 ; 是什么意思?
代码:
{
;
}
这个分号表示一条空语句。
也就是:
什么都不做。
所以这个延时函数的本质是:
循环很多次,但每次不做实际任务,只是消耗 CPU 时间。
八、这个延时函数准确吗?
不准确。
这个延时函数只是一个粗略延时,受很多因素影响,例如:
- 单片机主频;
- 芯片是传统 12T 还是高速 1T;
- Keil 编译优化等级;
- 循环代码被编译成多少条机器指令;
- 不同型号单片机的执行速度差异。
所以第一天用它做 LED 闪烁实验是可以的。
但是在正式产品中,例如按键扫描、PWM 调光、低电量报警闪烁、通信超时等场景中,不建议依赖这种空循环延时。
更好的方式是使用:
定时器中断。
不过定时器中断属于后面的学习内容,第一天只需要知道:
Delay_ms() 是让程序等待一段时间。
九、完整程序执行顺序
还是看这段主函数:
void main(void)
{
while(1)
{
LED = 0;
Delay_ms(500);
LED = 1;
Delay_ms(500);
}
}
实际执行顺序是:
第 1 步:进入 main 函数;
第 2 步:进入 while(1) 死循环;
第 3 步:执行 LED = 0;
第 4 步:延时 500ms;
第 5 步:执行 LED = 1;
第 6 步:延时 500ms;
第 7 步:回到第 3 步;
第 8 步:一直重复执行。
如果 LED 是低电平点亮,那么实际现象就是:
亮 0.5 秒;
灭 0.5 秒;
亮 0.5 秒;
灭 0.5 秒;
不断循环。
十、推荐加入GPIO初始化
对于 STC8G 这类单片机,IO 口通常可以设置不同模式,例如:
- 准双向口;
- 推挽输出;
- 高阻输入;
- 开漏输出。
如果我们希望 P3.7 作为输出,建议增加 GPIO 初始化函数。
例如:
void GPIO_Init(void)
{
P3M1 &= 0x7F;
P3M0 |= 0x80;
}
然后主函数改为:
void main(void)
{
GPIO_Init();
while(1)
{
LED = 0;
Delay_ms(500);
LED = 1;
Delay_ms(500);
}
}
完整代码如下:
#include "STC8G.H"
sbit LED = P3^7;
void GPIO_Init(void)
{
P3M1 &= 0x7F;
P3M0 |= 0x80;
}
void Delay_ms(unsigned int ms)
{
unsigned int i;
unsigned int j;
for(i = 0; i < ms; i++)
{
for(j = 0; j < 1000; j++)
{
;
}
}
}
void main(void)
{
GPIO_Init();
while(1)
{
LED = 0;
Delay_ms(500);
LED = 1;
Delay_ms(500);
}
}
这里的:
P3M1 &= 0x7F;
P3M0 |= 0x80;
可以先简单理解为:
把 P3.7 设置成推挽输出模式。
其中:
0x7F = 0111 1111
0x80 = 1000 0000
P3M1 &= 0x7F; 的作用是把 P3M1 的第 7 位清 0。
P3M0 |= 0x80; 的作用是把 P3M0 的第 7 位置 1。
这属于位操作内容,后面可以再系统学习。
第一天只需要先记住:
这两句是为了配置 P3.7 的 IO 模式,让它更适合作为输出引脚。
十一、今天必须掌握的6个核心概念
1. #include
#include "STC8G.H"
作用:
引入芯片相关定义,让编译器认识单片机寄存器和端口。
2. sbit
sbit LED = P3^7;
作用:
给某一个 IO 引脚起名字。
3. main()
void main(void)
作用:
程序入口,单片机从这里开始执行用户程序。
4. while(1)
while(1)
作用:
主循环,让程序一直运行。
5. LED = 0 / LED = 1
LED = 0;
LED = 1;
作用:
控制 IO 引脚输出低电平或高电平。
6. Delay_ms()
Delay_ms(500);
作用:
让程序大概等待一段时间。
十二、几个容易混淆的地方
误区1:LED = 0 一定是关灯?
不一定。
如果 LED 是低电平点亮:
LED = 0; // 亮
LED = 1; // 灭
如果 LED 是高电平点亮:
LED = 1; // 亮
LED = 0; // 灭
所以一定要结合原理图判断。
误区2:while(1) 是不是程序卡死了?
不是。
在单片机程序中,while(1) 是正常主循环。
它表示单片机一直运行、一直执行任务。
如果没有 while(1),程序执行完 main() 后反而可能进入不可控状态。
误区3:P3^7 是不是P3的7次方?
不是。
在下面这种语句中:
sbit LED = P3^7;
P3^7 表示:
P3 端口的第 7 位,也就是 P3.7。
误区4:Delay_ms(500) 一定是精确500ms吗?
不是。
这种空循环延时只是粗略延时。
它会受到主频、芯片型号、编译器优化等因素影响。
正式项目中应该使用定时器实现更准确的时间控制。
十三、今天需要会修改哪些地方?
1. 修改LED引脚
原代码:
sbit LED = P3^7;
如果你的 LED 接在 P1.0,可以改成:
sbit LED = P1^0;
如果你的 LED 接在 P5.5,可以改成:
sbit LED = P5^5;
格式是:
sbit 名字 = Px^n;
例如:
sbit LED1 = P3^7;
sbit LED2 = P5^4;
sbit KEY1 = P3^2;
2. 修改闪烁速度
原代码:
Delay_ms(500);
改成:
Delay_ms(100);
LED 会闪得更快。
改成:
Delay_ms(1000);
LED 会闪得更慢。
规律是:
数值越大,延时越长,闪得越慢;
数值越小,延时越短,闪得越快。
3. 修改亮灭比例
原来是亮 500ms、灭 500ms:
LED = 0;
Delay_ms(500);
LED = 1;
Delay_ms(500);
如果想改成亮 1 秒、灭 0.2 秒,并且 LED 是低电平点亮,可以改成:
LED = 0;
Delay_ms(1000);
LED = 1;
Delay_ms(200);
十四、第一天练习任务
任务1:看懂原始程序
需要能说出下面每一句代码的作用:
#include "STC8G.H"
sbit LED = P3^7;
void Delay_ms(unsigned int ms)
void main(void)
while(1)
LED = 0;
LED = 1;
Delay_ms(500);
任务2:修改闪烁速度
把:
Delay_ms(500);
改成:
Delay_ms(100);
观察 LED 是否闪得更快。
任务3:改成亮1秒、灭0.2秒
如果 LED 是低电平点亮,代码可以改成:
LED = 0;
Delay_ms(1000);
LED = 1;
Delay_ms(200);
任务4:自己手打一遍代码
建议不要直接复制代码,而是自己手打一遍:
#include "STC8G.H"
sbit LED = P3^7;
void Delay_ms(unsigned int ms)
{
unsigned int i;
unsigned int j;
for(i = 0; i < ms; i++)
{
for(j = 0; j < 1000; j++)
{
;
}
}
}
void main(void)
{
while(1)
{
LED = 0;
Delay_ms(500);
LED = 1;
Delay_ms(500);
}
}
手打一遍可以帮助发现很多初学阶段常见问题,例如:
- 分号漏写;
- 大括号不匹配;
- 大小写写错;
- 头文件找不到;
sbit写错;P3^7写成P3.7;- 函数名写错。
十五、第一天小测验
问题1:while(1) 是什么意思?
答案:
让程序一直循环执行。
问题2:sbit LED = P3^7; 是什么意思?
答案:
把 P3.7 这个引脚命名为 LED。
问题3:LED = 0; 一定代表 LED 灭吗?
答案:
不一定。它只代表 IO 输出低电平,LED 亮还是灭取决于硬件接法。
问题4:Delay_ms(1000); 大概表示什么?
答案:
大概延时 1000ms,也就是约 1 秒。
问题5:单片机程序为什么一般要写 while(1)?
答案:
因为单片机需要一直运行,不能执行完 main 函数就结束。
十六、第一天暂时不需要深入理解的内容
第一天只需要理解最基本的程序结构,下面这些内容暂时不需要深入:
- 定时器;
- 中断;
- PWM;
- ADC;
- 串口通信;
- 低功耗;
- EEPROM;
- IAP;
- 精确延时;
- 复杂按键消抖;
- 状态机。
这些内容后面再逐步学习。
第一天最重要的是:
代码 → 控制 IO → LED 亮灭 → 循环闪烁。
十七、第一天总结
今天学习的是 51 单片机 C 语言中最基础的程序结构。
可以用一句话总结:
51 单片机 C 程序从
main()函数开始运行,在while(1)中一直循环,通过sbit给 IO 引脚起名字,再用LED = 0或LED = 1控制引脚输出低电平或高电平,从而控制外部 LED 亮灭。
今天最重要的代码结构是:
#include "STC8G.H"
sbit LED = P3^7;
void main(void)
{
while(1)
{
LED = 0;
Delay_ms(500);
LED = 1;
Delay_ms(500);
}
}
第一天达到下面程度就算合格:
能看懂 LED 闪烁代码;
知道 main() 是程序入口;
知道 while(1) 是主循环;
知道 sbit 是给 IO 引脚起名字;
知道 LED = 0 / LED = 1 是控制高低电平;
知道 Delay_ms() 是粗略延时;
能修改 LED 闪烁速度;
能根据实际硬件修改 LED 引脚。
后续学习可以继续围绕 GPIO、按键、定时器、中断、PWM、ADC、串口等内容逐步展开。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)