重拾C51-DAY1(项目使用STC8G 系列)

51单片机C语言重新入门:第一天认识C51程序结构与LED闪烁

前言

最近准备重新学习 51 单片机 C 语言。之前接触过一些单片机相关内容,但是时间久了很多知识点已经忘记了。目前只记得一些 C 语言数据类型、变量、函数等基础概念,所以打算从最基础的 C51 程序结构重新开始。

本篇文章是我重新学习 51 单片机 C 语言的第一天笔记,主要目标是看懂一个最基本的 LED 闪烁程序,理解 main()while(1)sbitDelay_ms()、IO 高低电平等基础概念。

本篇适合:

  • 刚开始学习 51 单片机的初学者
  • 之前学过但已经忘记的同学
  • 有硬件基础但 C51 编程不熟的人
  • 想用 STC8G、STC89C52 等 51 单片机做小项目的人

一、第一天学习目标

第一天不追求学习复杂外设,也不急着学习定时器、中断、串口、ADC、PWM 等内容。

今天的核心目标只有一个:

看懂一个最基本的 51 单片机 C 程序,并理解代码如何控制单片机 IO 引脚输出高低电平。

具体来说,需要掌握以下内容:

  1. 认识 51 单片机 C 程序的基本结构;
  2. 理解 #include 的作用;
  3. 理解 sbit 的作用;
  4. 理解 main() 函数;
  5. 理解 while(1) 主循环;
  6. 理解 LED = 0LED = 1 的含义;
  7. 理解 LED 为什么会亮、灭、闪烁;
  8. 理解简单延时函数 Delay_ms() 的作用;
  9. 能修改 LED 闪烁速度;
  10. 能根据实际硬件修改 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 = 0LED = 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 = 0LED = 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、串口等内容逐步展开。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐