教程 · 入门 · 阅读约 28 分钟

8051 GPIO 与端口接口:灌电流,别拉电流

8051 的引脚能灌入约十毫安电流,却几乎拉不出电流。这为何让 LED 接法反过来、为何读引脚前要先写 1,以及一个端口能带多大负载。

引言

人人写的第一个 8051 程序都是点亮 LED,而相当多人的 LED 并不亮。接线看起来没错,代码看起来没错,万用表量到引脚上顶着自信的 5 V,就是什么用都没有。

原因几乎总是同一个,而且是架构使然,不是失误:8051 引脚擅长下拉,不擅长上拉。本教程的其余内容都从这一个不对称展开——为什么 LED 接法和 Arduino 相反、为什么读开关前要先写一次、为什么一个端口带不动八个负载。

端口就是一个字节

没有 GPIO API。四个端口是固定地址的特殊功能寄存器,写一次,引脚就变:

#include <reg52.h>

void main(void) {
    P1 = 0x00;        // all eight P1 pins low
    P1 = 0xFF;        // all eight high
    P1 = 0b10101010;  // alternating
}

单个位用 sbit 起名字,这是你在几乎所有课程资料里都会看到的形式:

sbit LED    = P1^0;   // Keil C51 syntax
sbit BUTTON = P1^1;

LED = 0;              // one instruction — the 8051 has real bit operations

那个 ^ 不是异或,而是 Keil 表示“P1 的第 0 位”的写法。在 SDCC 下等价写法来自 <8051.h> 的 P1_0,其余代码不变。

引脚内部是什么

每个端口引脚都有一路到电源的弱内部上拉和一路到地的强下拉晶体管,端口锁存器驱动着晶体管的栅极。两者强度完全不在一个量级。

单个 8051 端口引脚的内部结构。芯片虚线框内,一路上拉(约 60 微安)把 5 V 电源轨连到引脚节点,一路下拉场效应管(可灌入约 10 毫安)把引脚节点连到地,栅极由端口锁存器驱动。引脚伸出芯片,外接一颗 LED,阴极朝向引脚,阳极经 330 欧电阻接到 5 V 电源轨。下方两行文字说明写 0 时晶体管导通,引脚被拉到地,LED 点亮;写 1 时晶体管关断,只剩 60 微安,LED 熄灭。最后注明端口 0 完全没有上拉,需要外接 10 千欧电阻。
决定 8051 接法的不对称:灌入毫安级的晶体管,对阵只能拉出微安级的上拉。 下载 SVG

写 0,晶体管导通,把引脚钳在地附近,开心地吞掉从外面流进来的电流。写 1,晶体管关断,只剩那点可怜的上拉——几十微安——把线维持在高电平。给另一个 CMOS 输入呈现逻辑 1 够了,想点亮任何东西远远不够。

这种安排叫准双向:同一个引脚既是输出又是输入,不需要方向寄存器来配置,因为锁存 1 的引脚本身就已经“松开”,可以被别的东西驱动。

所以 LED 要反着接

在 Arduino 上,你把 LED 从引脚经电阻接到地,置高引脚即可。在 8051 上这么接最多得到微弱的光,因为 60 µA 点不亮任何东西。

8051 的接法是反过来的:阳极经电阻接 +5 V,阴极接引脚。引脚灌入电流,0 反而把 LED 点亮。

sbit LED = P1^0;

LED = 0;    // ON  — the pin sinks current from the supply
LED = 1;    // OFF — the pin releases the line

按想要的电流算电阻,别忘了引脚在灌电流时自身还要降下约半伏:

R = (Vsupply − Vf(LED) − Vpin) / I

red LED at 5 mA :  (5 − 2.0 − 0.5) / 0.005 = 500 Ω   → use 470 Ω
red LED at 10 mA:  (5 − 2.0 − 0.5) / 0.010 = 250 Ω   → use 270 Ω

没人提的端口电流预算

单引脚限值只是故事的一半。AT89S52 数据手册同时规定了两项,第二项要苛刻得多:

限制 数值
每个引脚最大灌电流 10 mA
端口 1、2 或 3 各端口合计最大值 15 mA
端口 0 合计最大值 26 mA
整颗芯片合计最大值 71 mA

把中间那行再读一遍。单个引脚可以灌 10 mA,但整个端口 1 八个引脚加起来只能灌 15 mA。给一个端口接八颗 LED 跑流水灯的经典练习没问题——一次只亮一颗。八颗同时以 10 mA 点亮,就是找一个额定 15 mA 的端口要 80 mA。

超规格的芯片通常不会炸,只会垮。逻辑电平漂移、芯片发热、行为间歇性异常,看起来像软件缺陷。想让八颗 LED 又亮又同时,办法是加驱动:一颗 ULN2803 达林顿阵列每通道能灌半安培,8051 只负责告诉它做什么。

按端口自身限值,八颗 LED 同时亮时每颗约分到 1.9 mA。现代高亮 LED 在这个电流下清晰可见,所以流水灯或条形图没问题——只是下手前先把账算清楚,别默认它很亮。

读输入:先写 1

因为两个方向共用同一个晶体管,8051 引脚只有在锁存器里存着 1 时才能作为输入来读。锁存 0 时,晶体管把引脚钳在地,不管外面接什么,读回来永远是 0。

sbit BUTTON = P1^1;

void main(void) {
    BUTTON = 1;               // release the pin — REQUIRED before reading
    while (1) {
        if (BUTTON == 0) {    // switch wired to ground: pressed reads 0
            // handle press
        }
    }
}

漏掉第一行是最常见的 8051 输入错误。在有方向寄存器的芯片上犯不了这个错,这正是从 AVR 或 PIC 转过来的人最容易中招的原因。

开关要接到地,而不是接到电源,理由和 LED 反接一样:弱上拉就是空闲状态,开关的工作就是把它短到地,盖过上拉。

端口 0 是例外

端口 0 完全没有内部上拉。它是开漏的,因为它要复用为外部存储器的地址数据总线。用作通用 I/O 时,它能把引脚拉低,除此之外只会悬空。

办法是给用到的每个 P0 引脚加外部上拉——10 kΩ 到 +5 V,八个全用就用排阻。不加的话,P0 输出永远到不了高,P0 输入读到的全是噪声。端口 1、2、3 什么都不用加。

消抖,因为开关不是理想的开关

机械触点会抖几毫秒,而 8051 的轮询循环快到能把每一次抖动都看成一次独立按下。廉价的办法是延时后再确认一次读数:

sbit BUTTON = P1^1;

bit pressed(void) {
    if (BUTTON != 0) return 0;    // not pressed
    delay_ms(20);                 // let the contact settle
    return (BUTTON == 0);         // still down? real press
}

20 毫秒足以盖过轻触开关的抖动,又远不到人能察觉的程度。学习阶段用阻塞延时没问题;等定时器中断跑起来之后,改成在中断里采样开关,延时就不再卡住其他一切了。

出问题时

  • **LED 亮得很微弱。**它是按 Arduino 风格从引脚接到地的。把它反过来:阳极经电阻接 +5 V,阴极接引脚,用低电平点亮。
  • **输入永远读到 0。**引脚锁存器里还存着 0。读之前先给它写 1。
  • **端口 0 没反应。**没有外部上拉。给用到的每个 P0 引脚加 10 kΩ 到 +5 V。
  • **按一次登记成三次。**触点抖动。约 20 ms 后再确认一次读数。
  • **单个输出都正常,几个输出同时开就不行。**超过了 15 mA 的端口预算。把负载搬到 ULN2803 上,或者减小每颗 LED 的电流。
  • **串口一跑引脚行为就怪。**你用到了 P3.0 或 P3.1,它们是 RXD 和 TXD。端口 3 的复用功能优先于你对它们的安排。

探索关联图谱

本教程所属的搭建

包含本教程的项目和学习路线。

延伸阅读

参考资料