C51学习-DAY4

下面这版已经整理成 CSDN Markdown 技术博客格式,可以直接复制粘贴发布。内容已经统一描述为"常规小项目 / 小型控制项目",没有出现你要求避开的词。

51单片机C语言重新入门:第四天学习GPIO输出控制、高低电平与IO模式

前言

这是我重新学习 51 单片机 C 语言的第四天笔记。

前面三天主要学习了:

text 复制代码
第一天:C51 程序结构、main()、while(1)、LED 闪烁
第二天:数据类型、变量、bit、unsigned char、unsigned int
第三天:sfr、sbit、寄存器、位操作

第四天开始正式深入学习 GPIO 输出控制。

GPIO 是单片机开发中最基础、最常用的功能之一。无论是点亮 LED、控制蜂鸣器、控制 MOS 管、控制芯片使能脚,还是控制其他外部电路,本质上都离不开 GPIO 输出。

本篇文章主要记录以下内容:

  • 什么是 GPIO;
  • GPIO 输出的本质;
  • 高电平和低电平;
  • 为什么 LED = 0 有时候是点亮;
  • LED 高电平点亮和低电平点亮的区别;
  • 如何从原理图判断 LED 有效电平;
  • 推挽输出、准双向口、高阻输入、开漏输出;
  • STC 单片机 IO 模式配置;
  • GPIO 输出控制的工程化写法;
  • 常规小项目中的 IO 输出设计注意事项。

一、第四天学习目标

第四天的学习目标是:

text 复制代码
理解 GPIO 是什么
理解 GPIO 输出高电平和低电平的含义
理解 LED 高电平点亮和低电平点亮
能从原理图判断 LED 的有效电平
理解推挽输出、准双向口、高阻输入、开漏输出
能配置 P3M0、P3M1 等 IO 模式寄存器
能写出规范的 LED_On()、LED_Off()、LED_Set() 函数
知道上电默认状态为什么重要

今天的重点不是单纯会写:

c 复制代码
LED = 0;
LED = 1;

而是要真正理解:

text 复制代码
为什么 LED = 0 有时候是亮?
为什么 LED = 1 有时候是灭?
为什么要配置 P3M0、P3M1?
推挽输出和开漏输出有什么区别?
实际项目里 GPIO 输出应该怎么写更规范?

二、什么是GPIO?

GPIO 的全称是:

text 复制代码
General Purpose Input Output

中文通常叫:

text 复制代码
通用输入输出口

简单理解:

GPIO 就是单片机上可以由程序控制的普通引脚。

GPIO 既可以作为输入,也可以作为输出。

作为输出时,可以控制外部电路,例如:

text 复制代码
点亮 LED
控制蜂鸣器
控制 MOS 管
控制继电器
控制芯片使能脚
控制电源开关
输出高低电平信号

作为输入时,可以读取外部状态,例如:

text 复制代码
读取按键
检测开关状态
检测传感器输出
检测高低电平状态
检测故障信号

本篇文章主要学习 GPIO 输出。


三、GPIO输出到底输出的是什么?

当程序中写:

c 复制代码
LED = 1;

或者写:

c 复制代码
P3 |= 0x80;

本质上是在让某个 IO 引脚输出一个电压状态。

GPIO 输出通常有两种状态:

text 复制代码
高电平:接近 VCC
低电平:接近 GND

如果单片机供电是 5V,那么:

text 复制代码
高电平大约接近 5V
低电平大约接近 0V

如果单片机供电是 3.3V,那么:

text 复制代码
高电平大约接近 3.3V
低电平大约接近 0V

所以:

c 复制代码
LED = 1;

可以理解为:

text 复制代码
让 LED 对应的 IO 输出高电平

而:

c 复制代码
LED = 0;

可以理解为:

text 复制代码
让 LED 对应的 IO 输出低电平

注意:

10 不是直接代表亮和灭,而是代表高电平和低电平。


四、为什么LED = 0有时候是亮?

这是学习 GPIO 输出时非常容易混淆的问题。

很多初学者会下意识认为:

text 复制代码
LED = 1 就是亮
LED = 0 就是灭

这个理解是不准确的。

正确理解应该是:

text 复制代码
LED = 1 表示 IO 输出高电平
LED = 0 表示 IO 输出低电平
LED 亮不亮取决于硬件电路怎么接

也就是说,代码里的 01 控制的是引脚电平,不是直接控制"亮"和"灭"。

LED 最终亮不亮,要看硬件电路中的电流能不能形成回路。


五、LED的两种常见接法

1. 低电平点亮

很多 51 单片机开发板常用下面这种 LED 接法:

text 复制代码
VCC ---- 限流电阻 ---- LED ---- 单片机 IO

当 IO 输出低电平时,电流路径为:

text 复制代码
VCC → 电阻 → LED → IO → GND

此时电流形成回路,LED 点亮。

所以这种接法下:

c 复制代码
LED = 0;    // LED 亮
LED = 1;    // LED 灭

这种方式叫做:

text 复制代码
低电平有效

也可以说:

text 复制代码
低电平点亮

2. 高电平点亮

另一种常见接法是:

text 复制代码
单片机 IO ---- 限流电阻 ---- LED ---- GND

当 IO 输出高电平时,电流路径为:

text 复制代码
IO → 电阻 → LED → GND

此时电流形成回路,LED 点亮。

所以这种接法下:

c 复制代码
LED = 1;    // LED 亮
LED = 0;    // LED 灭

这种方式叫做:

text 复制代码
高电平有效

也可以说:

text 复制代码
高电平点亮

六、如何从原理图判断LED是高电平点亮还是低电平点亮?

判断方法很简单:

看 LED 和限流电阻接在 IO 的哪一侧。


情况1:LED接在VCC和IO之间

电路形式:

text 复制代码
VCC ---- 电阻 ---- LED ---- IO

这种情况下,IO 输出低电平时,电流可以从 VCC 流过电阻和 LED,再流入 IO 到 GND。

所以:

text 复制代码
低电平点亮

代码一般写成:

c 复制代码
#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

情况2:LED接在IO和GND之间

电路形式:

text 复制代码
IO ---- 电阻 ---- LED ---- GND

这种情况下,IO 输出高电平时,电流从 IO 流出,经过电阻和 LED 到 GND。

所以:

text 复制代码
高电平点亮

代码一般写成:

c 复制代码
#define LED_ON_LEVEL     1
#define LED_OFF_LEVEL    0

七、推荐写法:不要在代码里到处写LED = 0

不推荐在程序中到处直接写:

c 复制代码
LED = 0;
LED = 1;

因为这种写法和硬件接法强绑定。

如果以后 LED 硬件从低电平点亮改成高电平点亮,程序中所有 LED = 0LED = 1 都可能需要修改,维护起来很麻烦。

更推荐的写法是使用宏定义和函数封装:

c 复制代码
#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

void LED_On(void)
{
    LED = LED_ON_LEVEL;
}

void LED_Off(void)
{
    LED = LED_OFF_LEVEL;
}

如果以后硬件改成高电平点亮,只需要修改宏定义:

c 复制代码
#define LED_ON_LEVEL     1
#define LED_OFF_LEVEL    0

其他代码不用改。

这就是工程化写法。


八、GPIO输出模式是什么?

同一个 IO 引脚,并不是只能简单输出 0 或 1。

很多 STC 单片机的 IO 可以配置成不同模式,例如:

text 复制代码
准双向口
推挽输出
高阻输入
开漏输出

不同模式下,IO 的电气行为不同。

简单来说:

text 复制代码
推挽输出:能主动输出高电平,也能主动输出低电平
开漏输出:只能主动拉低,不能主动输出高电平
高阻输入:主要用于输入,几乎不驱动外部电路
准双向口:传统 51 常用模式,既能输入也能输出,但输出能力有特点

九、STC单片机常见IO模式配置

以 STC8G 这类单片机为例,端口模式通常由两个寄存器控制:

c 复制代码
P3M0
P3M1

对于 P3.7 来说,就是:

text 复制代码
P3M0.7
P3M1.7

它们组合起来决定 P3.7 的 IO 模式。

常见模式表如下:

PnM1 PnM0 IO模式
0 0 准双向口
0 1 推挽输出
1 0 高阻输入
1 1 开漏输出

这个表非常重要。

初学阶段可以先这样记:

text 复制代码
输出 LED、控制 MOS、控制使能脚:常用推挽输出
读取按键、读取外部信号:常用高阻输入
需要总线或线与逻辑:可能用开漏输出
传统 51 默认常见模式:准双向口

十、推挽输出是什么?

推挽输出可以理解为:

IO 内部既有一个"上拉开关",也有一个"下拉开关"。

当输出 1 时:

text 复制代码
IO 主动接到 VCC

当输出 0 时:

text 复制代码
IO 主动接到 GND

所以推挽输出的特点是:

text 复制代码
高电平驱动能力强
低电平驱动能力也强
输出状态明确
适合控制 LED、使能脚、MOS 管栅极等

1. 推挽输出的通俗理解

可以把推挽输出理解成一个双向主动开关:

text 复制代码
输出 1:主动把引脚推到 VCC
输出 0:主动把引脚拉到 GND

它既能主动"推高",也能主动"拉低"。

这就是推挽输出的核心特点。


2. P3.7配置成推挽输出

假设 LED 接在 P3.7。

P3.7 对应 bit7:

text 复制代码
bit7 = 0x80

推挽输出要求:

text 复制代码
P3M1.7 = 0
P3M0.7 = 1

代码如下:

c 复制代码
P3M1 &= ~0x80;
P3M0 |= 0x80;

用宏定义写更清楚:

c 复制代码
#define BIT7    0x80

P3M1 &= ~BIT7;
P3M0 |= BIT7;

十一、准双向口是什么?

准双向口是传统 8051 很常见的一种 IO 模式。

它既可以输出,也可以输入,但它和推挽输出不完全一样。

准双向口的特点可以简单理解为:

text 复制代码
输出低电平时,下拉能力较强
输出高电平时,上拉能力较弱
既可以当输出,也可以当输入使用

传统 51 很多 IO 默认就是准双向口。


1. 准双向口的通俗理解

准双向口可以理解为:

text 复制代码
输出 0 的时候,单片机能比较有力地把引脚拉低
输出 1 的时候,单片机只是比较弱地把引脚拉高

所以准双向口能用,但输出高电平的驱动能力不如推挽输出明确。


2. 什么时候可以用准双向口?

在一些简单实验中,比如点亮 LED、读取按键,默认准双向口有时也能工作。

但是在实际工程项目中,如果明确要驱动外部器件,建议配置成推挽输出。

例如:

text 复制代码
LED 输出
蜂鸣器控制
芯片使能脚
MOS 管控制

这些更推荐使用推挽输出。


十二、高阻输入是什么?

高阻输入主要用于读取外部信号。

高阻可以理解为:

text 复制代码
这个引脚几乎不向外部电路输出电流
只是观察外部电平是高还是低

如果按键接在 P3.2,想读取按键状态,就可以把 P3.2 配置成高阻输入。

高阻输入要求:

text 复制代码
PnM1 = 1
PnM0 = 0

如果 P3.2 对应 bit2:

text 复制代码
bit2 = 0x04

代码就是:

c 复制代码
P3M1 |= 0x04;
P3M0 &= ~0x04;

用宏定义写成:

c 复制代码
#define BIT2    0x04

P3M1 |= BIT2;
P3M0 &= ~BIT2;

十三、开漏输出是什么?

开漏输出可以理解为:

IO 只能主动输出低电平,不能主动输出高电平。

当输出 0 时:

text 复制代码
IO 接近 GND

当输出 1 时:

text 复制代码
IO 释放,不主动输出高电平

这时候如果想得到高电平,需要外部上拉电阻。


1. 开漏输出的通俗理解

开漏输出就像一个只会"拉低"的开关。

text 复制代码
想输出 0:它能主动拉到 GND
想输出 1:它不负责拉高,只是松手

如果外部有上拉电阻,引脚就会被上拉到高电平。


2. 开漏输出常见用途

开漏输出常用于:

text 复制代码
I2C 总线
多个器件共用一根信号线
需要线与逻辑
不同电压域信号接口
外部上拉控制

初学阶段暂时不需要深入掌握开漏输出,只需要先记住:

text 复制代码
开漏输出不能主动输出高电平,通常需要外部上拉电阻。

十四、四种IO模式对比

模式 能否主动输出高电平 能否主动输出低电平 常见用途
准双向口 弱上拉 可以 传统 51 默认 IO、简单输入输出
推挽输出 可以 可以 LED、使能脚、MOS 控制
高阻输入 不输出 不输出 按键输入、信号检测
开漏输出 不主动输出高 可以 I2C、线与逻辑、外部上拉

初学阶段可以先记下面三条:

text 复制代码
要强输出,用推挽输出
要读取输入,用高阻输入
要总线共享,用开漏输出

十五、GPIO输出控制的基本步骤

以后控制一个 IO 输出,建议按下面步骤来:

text 复制代码
第 1 步:确定使用哪个引脚
第 2 步:用 sbit 给引脚起名字
第 3 步:根据硬件判断高电平有效还是低电平有效
第 4 步:配置 IO 模式
第 5 步:写输出控制函数
第 6 步:在 main() 中调用

十六、示例:LED接P3.7,低电平点亮

第1步:确定引脚

text 复制代码
LED 接 P3.7

第2步:定义sbit

c 复制代码
sbit LED = P3^7;

第3步:定义有效电平

低电平点亮:

c 复制代码
#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

第4步:配置IO模式

P3.7 推挽输出:

c 复制代码
#define BIT7    0x80

P3M1 &= ~BIT7;
P3M0 |= BIT7;

第5步:写函数

c 复制代码
void LED_On(void)
{
    LED = LED_ON_LEVEL;
}

void LED_Off(void)
{
    LED = LED_OFF_LEVEL;
}

第6步:调用函数

c 复制代码
LED_On();
LED_Off();

十七、完整例子1:LED点亮和熄灭

假设:

text 复制代码
LED 接 P3.7
LED 低电平点亮
P3.7 配置为推挽输出

完整代码如下:

c 复制代码
#include "STC8G.H"

#define BIT7             0x80

#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

sbit LED = P3^7;

void GPIO_Init(void);
void LED_On(void);
void LED_Off(void);

void main(void)
{
    GPIO_Init();

    while(1)
    {
        LED_On();
    }
}

void GPIO_Init(void)
{
    /* P3.7 设置为推挽输出 */
    P3M1 &= ~BIT7;
    P3M0 |= BIT7;
}

void LED_On(void)
{
    LED = LED_ON_LEVEL;
}

void LED_Off(void)
{
    LED = LED_OFF_LEVEL;
}

这个程序上电后,LED 会一直点亮。

如果想让 LED 一直熄灭,可以把主函数改成:

c 复制代码
void main(void)
{
    GPIO_Init();

    while(1)
    {
        LED_Off();
    }
}

十八、完整例子2:LED闪烁

在点亮和熄灭的基础上,加一个延时函数。

c 复制代码
#include "STC8G.H"

#define BIT7             0x80

#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

sbit LED = P3^7;

void GPIO_Init(void);
void Delay_ms(unsigned int ms);
void LED_On(void);
void LED_Off(void);

void main(void)
{
    GPIO_Init();

    while(1)
    {
        LED_On();
        Delay_ms(500);

        LED_Off();
        Delay_ms(500);
    }
}

void GPIO_Init(void)
{
    /* P3.7 设置为推挽输出 */
    P3M1 &= ~BIT7;
    P3M0 |= BIT7;
}

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 LED_On(void)
{
    LED = LED_ON_LEVEL;
}

void LED_Off(void)
{
    LED = LED_OFF_LEVEL;
}

程序现象:

text 复制代码
LED 亮 0.5 秒
LED 灭 0.5 秒
循环闪烁

十九、完整例子3:两个LED分别控制

假设:

text 复制代码
LED1 接 P3.7,低电平点亮
LED2 接 P3.6,低电平点亮

P3.7 对应:

text 复制代码
BIT7 = 0x80

P3.6 对应:

text 复制代码
BIT6 = 0x40

代码如下:

c 复制代码
#include "STC8G.H"

#define BIT6             0x40
#define BIT7             0x80

#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

sbit LED1 = P3^7;
sbit LED2 = P3^6;

void GPIO_Init(void);
void Delay_ms(unsigned int ms);

void LED1_On(void);
void LED1_Off(void);
void LED2_On(void);
void LED2_Off(void);

void main(void)
{
    GPIO_Init();

    while(1)
    {
        LED1_On();
        LED2_Off();
        Delay_ms(500);

        LED1_Off();
        LED2_On();
        Delay_ms(500);
    }
}

void GPIO_Init(void)
{
    /* P3.7 设置为推挽输出 */
    P3M1 &= ~BIT7;
    P3M0 |= BIT7;

    /* P3.6 设置为推挽输出 */
    P3M1 &= ~BIT6;
    P3M0 |= BIT6;
}

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 LED1_On(void)
{
    LED1 = LED_ON_LEVEL;
}

void LED1_Off(void)
{
    LED1 = LED_OFF_LEVEL;
}

void LED2_On(void)
{
    LED2 = LED_ON_LEVEL;
}

void LED2_Off(void)
{
    LED2 = LED_OFF_LEVEL;
}

程序现象:

text 复制代码
LED1 亮、LED2 灭
等待 0.5 秒
LED1 灭、LED2 亮
等待 0.5 秒
循环交替

二十、完整例子4:用一个函数控制LED状态

有时候可以写一个带参数的函数:

c 复制代码
void LED_Set(bit state)
{
    if(state == 1)
    {
        LED = LED_ON_LEVEL;
    }
    else
    {
        LED = LED_OFF_LEVEL;
    }
}

完整例子如下:

c 复制代码
#include "STC8G.H"

#define BIT7             0x80

#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

sbit LED = P3^7;

void GPIO_Init(void);
void Delay_ms(unsigned int ms);
void LED_Set(bit state);

void main(void)
{
    GPIO_Init();

    while(1)
    {
        LED_Set(1);
        Delay_ms(500);

        LED_Set(0);
        Delay_ms(500);
    }
}

void GPIO_Init(void)
{
    P3M1 &= ~BIT7;
    P3M0 |= BIT7;
}

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 LED_Set(bit state)
{
    if(state == 1)
    {
        LED = LED_ON_LEVEL;
    }
    else
    {
        LED = LED_OFF_LEVEL;
    }
}

这个函数的作用是:

text 复制代码
LED_Set(1):点亮 LED
LED_Set(0):熄灭 LED

这种写法适合后期在程序里统一控制 LED 状态。


二十一、控制多个IO时,为什么不能乱写整个端口?

假设 P3.7 接 LED,P3.2 接按键。

如果写:

c 复制代码
P3 = 0x00;

这会把 P3 所有位都改成低电平。

包括:

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

这可能会影响其他外设。

所以实际项目中,如果只想控制 P3.7,不建议写:

c 复制代码
P3 = 0x00;

更推荐写:

c 复制代码
LED = 0;

或者:

c 复制代码
P3 &= ~BIT7;

这样只影响 P3.7,不影响其他位。


二十二、GPIO输出控制中的灌电流和拉电流

理解灌电流和拉电流,有助于理解 LED 为什么有高电平点亮和低电平点亮两种接法。


1. 灌电流

如果 LED 接法是:

text 复制代码
VCC ---- 电阻 ---- LED ---- IO

IO 输出低电平时,电流从 VCC 流过电阻和 LED,然后流入 IO,再到 GND。

这种情况叫:

text 复制代码
IO 灌电流

也可以理解为:

text 复制代码
单片机把电流"吸"进去

低电平点亮就是这种情况。


2. 拉电流

如果 LED 接法是:

text 复制代码
IO ---- 电阻 ---- LED ---- GND

IO 输出高电平时,电流从 IO 流出,经过电阻和 LED 到 GND。

这种情况叫:

text 复制代码
IO 拉电流

也可以理解为:

text 复制代码
单片机把电流"送"出去

高电平点亮就是这种情况。


3. 为什么很多开发板LED低电平点亮?

很多单片机 IO 的灌电流能力往往比拉电流能力更适合驱动 LED。

所以很多开发板喜欢采用:

text 复制代码
VCC ---- 电阻 ---- LED ---- IO

让 IO 输出低电平时点亮 LED。

需要记住:

text 复制代码
低电平点亮不是代码写反了,而是硬件接法决定的。

二十三、GPIO输出控制外部MOS管

除了 LED,GPIO 还经常用来控制 MOS 管。

例如控制一个 N 沟道 MOS 管的栅极:

text 复制代码
单片机 IO ---- 电阻 ---- NMOS Gate

常见逻辑是:

text 复制代码
IO 输出高电平:NMOS 导通
IO 输出低电平:NMOS 关闭

这种情况下,一般配置成推挽输出。

代码示例:

c 复制代码
#define MOS_ON_LEVEL     1
#define MOS_OFF_LEVEL    0

sbit MOS_CTRL = P3^5;

void MOS_On(void)
{
    MOS_CTRL = MOS_ON_LEVEL;
}

void MOS_Off(void)
{
    MOS_CTRL = MOS_OFF_LEVEL;
}

如果 MOS 管控制的是重要电源或外部负载,建议上电初始化时先关闭:

c 复制代码
void GPIO_Init(void)
{
    MOS_Off();

    /* P3.5 设置为推挽输出 */
    P3M1 &= ~BIT5;
    P3M0 |= BIT5;
}

实际项目里,控制外部电源、蜂鸣器、负载时,要特别注意:

text 复制代码
默认状态是否安全
上电瞬间是否误动作
IO 初始化前外设会不会被误触发

二十四、上电默认状态为什么重要?

单片机刚上电时,IO 还没有执行到用户初始化代码。

在这段时间里,IO 可能处于默认状态。

如果某个 IO 控制外部负载,例如:

text 复制代码
电机
蜂鸣器
继电器
电源开关
MOS 管

就要考虑:

text 复制代码
单片机刚上电时,外部负载会不会误动作?

所以工程上常用方法是:

text 复制代码
硬件加上拉或下拉电阻,保证默认状态安全
软件初始化第一时间设置安全电平
再配置 IO 输出模式

例如控制 NMOS,假设高电平导通、低电平关闭,可以给 Gate 加下拉电阻,防止上电悬空误导通。


二十五、GPIO输出控制的推荐工程写法

以 LED 为例,推荐结构如下:

c 复制代码
#include "STC8G.H"

#define BIT7             0x80

#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

sbit LED = P3^7;

void GPIO_Init(void);
void Delay_ms(unsigned int ms);
void LED_On(void);
void LED_Off(void);
void LED_Set(bit state);

void main(void)
{
    GPIO_Init();

    while(1)
    {
        LED_Set(1);
        Delay_ms(500);

        LED_Set(0);
        Delay_ms(500);
    }
}

然后把具体实现写在下面:

c 复制代码
void GPIO_Init(void)
{
    LED_Off();

    P3M1 &= ~BIT7;
    P3M0 |= BIT7;
}

void LED_On(void)
{
    LED = LED_ON_LEVEL;
}

void LED_Off(void)
{
    LED = LED_OFF_LEVEL;
}

void LED_Set(bit state)
{
    if(state == 1)
    {
        LED_On();
    }
    else
    {
        LED_Off();
    }
}

推荐这样写的原因是:

text 复制代码
LED 有统一开关函数
电平有效关系集中在宏定义里
以后换硬件接法,只改 LED_ON_LEVEL 和 LED_OFF_LEVEL
初始化时先设置安全状态
IO 模式配置集中在 GPIO_Init()

二十六、第四天完整综合程序

功能描述:

text 复制代码
LED 接 P3.7,低电平点亮
P3.7 配置为推挽输出
上电先关闭 LED
然后 LED 以 500ms 周期闪烁

代码如下:

c 复制代码
#include "STC8G.H"

#define BIT7             0x80

#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

sbit LED = P3^7;

void GPIO_Init(void);
void Delay_ms(unsigned int ms);
void LED_On(void);
void LED_Off(void);
void LED_Set(bit state);

void main(void)
{
    GPIO_Init();

    while(1)
    {
        LED_Set(1);
        Delay_ms(500);

        LED_Set(0);
        Delay_ms(500);
    }
}

void GPIO_Init(void)
{
    /* 上电先关闭 LED,确保默认状态明确 */
    LED_Off();

    /* P3.7 设置为推挽输出 */
    P3M1 &= ~BIT7;
    P3M0 |= BIT7;
}

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 LED_On(void)
{
    LED = LED_ON_LEVEL;
}

void LED_Off(void)
{
    LED = LED_OFF_LEVEL;
}

void LED_Set(bit state)
{
    if(state == 1)
    {
        LED_On();
    }
    else
    {
        LED_Off();
    }
}

这个程序已经比第一天的 LED 闪烁程序更加规范。

第一天可能直接写:

c 复制代码
LED = 0;
Delay_ms(500);
LED = 1;
Delay_ms(500);

第四天推荐写成:

c 复制代码
LED_Set(1);
Delay_ms(500);

LED_Set(0);
Delay_ms(500);

这样更接近实际项目写法。


二十七、第四天常见错误

1. 把高低电平和亮灭关系混淆

错误理解:

text 复制代码
1 一定是亮
0 一定是灭

正确理解:

text 复制代码
1 是高电平
0 是低电平
亮灭取决于硬件接法

2. 忘记配置IO模式

不严谨写法:

c 复制代码
LED = 0;

更推荐:

c 复制代码
GPIO_Init();
LED_On();

因为 GPIO_Init() 中已经配置了推挽输出。


3. 直接操作整个端口

不推荐:

c 复制代码
P3 = 0x00;

除非明确要控制整个 P3 端口。

更推荐:

c 复制代码
LED = 0;

或者:

c 复制代码
P3 &= ~BIT7;

4. 初始化时没有设置安全状态

如果 LED、蜂鸣器、MOS 管、继电器等外设上电误动作,可能就是默认状态没有处理好。

推荐:

c 复制代码
void GPIO_Init(void)
{
    LED_Off();

    P3M1 &= ~BIT7;
    P3M0 |= BIT7;
}

5. 开漏输出没有外部上拉

如果 IO 配置为开漏输出,但外部没有上拉电阻,那么输出 1 时电平可能不确定。

所以要记住:

text 复制代码
开漏输出通常需要外部上拉。

二十八、第四天必须掌握的重点

今天必须掌握下面这些内容:

text 复制代码
GPIO 是通用输入输出口
GPIO 输出的是高电平或低电平
LED 亮灭由硬件接法决定
低电平点亮:VCC → 电阻 → LED → IO
高电平点亮:IO → 电阻 → LED → GND
推挽输出能主动输出高电平和低电平
高阻输入用于读取外部信号
开漏输出只能主动拉低,输出高电平需要上拉
P3M1 和 P3M0 可以配置 IO 模式
修改某一位时要用位操作,不要轻易改整个寄存器
实际项目中要考虑上电默认状态

二十九、第四天练习任务

任务1:判断LED有效电平

请根据下面电路判断 LED 是高电平点亮还是低电平点亮。

电路A
text 复制代码
VCC ---- 电阻 ---- LED ---- IO

答案:

text 复制代码
低电平点亮

电路B
text 复制代码
IO ---- 电阻 ---- LED ---- GND

答案:

text 复制代码
高电平点亮

任务2:写P3.5推挽输出配置

P3.5 对应:

text 复制代码
BIT5 = 0x20

推挽输出要求:

text 复制代码
P3M1.5 = 0
P3M0.5 = 1

答案:

c 复制代码
P3M1 &= ~0x20;
P3M0 |= 0x20;

或者:

c 复制代码
#define BIT5    0x20

P3M1 &= ~BIT5;
P3M0 |= BIT5;

任务3:写P3.5高阻输入配置

高阻输入要求:

text 复制代码
P3M1.5 = 1
P3M0.5 = 0

答案:

c 复制代码
P3M1 |= 0x20;
P3M0 &= ~0x20;

或者:

c 复制代码
#define BIT5    0x20

P3M1 |= BIT5;
P3M0 &= ~BIT5;

任务4:写LED高电平点亮的宏定义

如果 LED 是高电平点亮,应该写:

c 复制代码
#define LED_ON_LEVEL     1
#define LED_OFF_LEVEL    0

任务5:写LED低电平点亮的宏定义

如果 LED 是低电平点亮,应该写:

c 复制代码
#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

任务6:写LED控制函数

要求:

text 复制代码
LED 接 P3.7
低电平点亮
写 LED_On() 和 LED_Off()

答案:

c 复制代码
#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

sbit LED = P3^7;

void LED_On(void)
{
    LED = LED_ON_LEVEL;
}

void LED_Off(void)
{
    LED = LED_OFF_LEVEL;
}

三十、第四天总结

今天学习的是 GPIO 输出控制。

可以用一句话总结:

GPIO 输出控制的本质,是通过寄存器配置 IO 模式,再通过程序让引脚输出高电平或低电平,从而控制外部电路。

今天最重要的理解是:

text 复制代码
LED = 1 不一定是亮,只代表输出高电平。
LED = 0 不一定是灭,只代表输出低电平。
LED 亮灭由硬件接法决定。

第四天达到下面程度就算合格:

text 复制代码
知道 GPIO 是什么
知道高电平和低电平的含义
能从 LED 接法判断高电平点亮还是低电平点亮
知道推挽输出适合控制 LED、MOS、使能脚
知道高阻输入适合读取按键和外部信号
知道开漏输出通常需要外部上拉
能配置 P3.7 为推挽输出
能写 LED_On()、LED_Off()、LED_Set()
知道初始化时要考虑默认安全状态

后续可以继续学习 GPIO 输入与按键读取,重点包括:

text 复制代码
按键接法
上拉和下拉
按下为低电平 / 按下为高电平
按键读取代码
简单按键控制 LED
为什么按键需要消抖
相关推荐
red_redemption2 小时前
自由学习记录(201)
学习
一条泥憨鱼2 小时前
Java开发效率神器:Lombok从入门到精通!
java·后端·学习·开发·lombok
csg11072 小时前
PIC16F1947驱动CH376芯片实现SD卡数据存储
单片机·嵌入式硬件·物联网·自动化
Niuguangshuo3 小时前
LangChain学习之旅(三):用Memory赋予模型记忆
学习·langchain
H__Rick3 小时前
C51学习-DAY8
单片机·嵌入式硬件·学习
youcans_3 小时前
从零搭建 STM32 VSCode 开发环境
vscode·stm32·单片机·嵌入式硬件
chase。4 小时前
【学习笔记】Dexora:面向高自由度双臂灵巧操作的开源 VLA 系统
笔记·学习
ye150127774554 小时前
220V降5V0.3A电源芯片WT5104
单片机·嵌入式硬件·其他·硬件工程
第二层皮-合肥4 小时前
【数据采集专栏】输入阻抗
单片机·嵌入式硬件