下面这版已经整理成 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 输出低电平
注意:
1和0不是直接代表亮和灭,而是代表高电平和低电平。
四、为什么LED = 0有时候是亮?
这是学习 GPIO 输出时非常容易混淆的问题。
很多初学者会下意识认为:
text
LED = 1 就是亮
LED = 0 就是灭
这个理解是不准确的。
正确理解应该是:
text
LED = 1 表示 IO 输出高电平
LED = 0 表示 IO 输出低电平
LED 亮不亮取决于硬件电路怎么接
也就是说,代码里的 0 和 1 控制的是引脚电平,不是直接控制"亮"和"灭"。
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 = 0 和 LED = 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
为什么按键需要消抖