元器件 · 控制器
8051 单片机
8051 是大多数工科课程至今仍最先讲授的 8 位单片机。本文讲它的架构为你提供了什么,又有哪些功能要你亲手搭出来。
它是什么
8051 不是一块板子,严格说也不是某一颗芯片——它是一套架构。Intel 在 1980 年发布了它,随后把内核授权得极广,所以今天你真正买到的芯片都出自别人之手。在实验室套件里,那几乎总是 Atmel 的 AT89S52:一颗带 8 KB 闪存、256 字节 RAM、三个定时器、并支持通过四个引脚在系统内编程的 8051 内核。
拿到手是一颗 40 脚 DIP,除此之外什么都没有。没有 USB,没有稳压器,没有引导程序,也没有 pinMode()。在它能执行一条指令之前,你得先给它晶振、两个负载电容、一套复位网络,还要把 EA 接到 +5 V。正是这种赤裸裸的特性,构成了它至今仍留在教学大纲上的全部理由:你的代码和硅片之间没有任何一层东西,可以替你把不理解的行为解释掉。
工作原理
8051 是一台 Harvard 架构的机器:程序存储器和数据存储器是两个彼此独立的地址空间,由不同的指令访问。代码住在闪存里;变量住在 256 字节的内部 RAM 中,而这块 RAM 本身又被分成四个寄存器组、一段可位寻址的窗口,以及剩下的堆栈空间。
时序异常容易推算。一机器周期是十二个振荡周期,而大多数指令只要一两个周期。在 11.0592 MHz 下,每个周期是 1.085 µs,于是你可以数着指令、确切地知道(而不是估算)一个循环要花多久。很少有现代芯片能做到这一点。
32 IO 脚四端口,各性格:
- P1 是唯一省事的一个——八个通用引脚,带内部上拉。
- P3 的每个引脚都有复用功能:串口 RXD/TXD、两个外部中断、两个定时器输入,以及外部存储器的读、写选通。
- P2 在接了外部存储器时负责承载地址高位字节。
- P0 是开漏的。没有外部上拉电阻时它根本无法输出逻辑高,而且还复用了低地址和数据字节。
芯片没有的东西同样要紧:没有模拟输入,也没有 PWM 硬件。要读传感器电压就得外挂 ADC;要调节电机转速,就得用定时器中断自己生成波形。
什么时候用它
当你的目的是了解这台机器时,用 8051——单片机课程、大学的迷你项目,或者纯粹出于好奇,想知道 analogWrite() 到底做了什么。手写一个软件 PWM 生成器,一个下午学到的东西比调用一整年那个把它藏起来的库还多。
如果你的目的是机器人,请几乎选别的任何型号。Arduino Uno 直接给你硬件 PWM、模拟输入,以及你将会遇到的每个传感器的库;ESP32 还会加上 Wi-Fi 和大得多的内存。两者的价格都和一块 8051 开发板差不多。为一个需要能用而不是需要用来教学的项目选择 8051,等于用手工重建那些替代方案免费送给你的东西。
常见坑
- **EA 必须接到 +5 V。**31 脚用来选择使用内部还是外部程序存储器。如果悬空或接地,芯片会悄悄地去一个并不存在的外部存储器取指,结果什么都不做——没有报错,没有指示灯,没有任何线索。这是新手第一块 8051 板子看起来像坏了的头号原因。
- **复位是高电平有效。**和你会遇到的几乎所有其它单片机都相反,8051 是在 RST 被拉高时复位的。标准上电电路是从 +5 V 经一个 10 µF 电容接到 RST,再从 RST 经一个 10 kΩ 电阻接地。
- **P0 要外部上拉。**开漏。10 kΩ 电阻到 +5 V,不然高出不来读不到。
- **脚灌不拉。**内部上拉几十微安,下拉管灌毫安。LED 正极经电阻接电源、负极接脚,写
0亮。反接微亮加一头雾水。 - **堆栈从 08h 起,压寄存器组 1 上。**堆栈指针复位 07h。切组 1 不先挪 SP,寄存器和返回地址同字节。
为什么偏偏 11.0592 MHz
这个晶振频率看起来很随意,其实是整套架构中最有深意的一处设计选择之一。
8051 生成串口波特率的方式,是让定时器 1 在 8 位自动重装模式下溢出:
baud = (2^SMOD / 32) x (oscillator / 12) / (256 - TH1)
SMOD = 0,11.0592 MHz 晶振,目标 9600:
TH1 = 256 - (11059200 / (12 x 32 x 9600)) = 256 - 3 = 253 = 0xFD
正好是 3,没有舍入,也没有误差。每一个标准波特率——1200、2400、4800、9600、19200——算出来的定时器重装值都是整数。
整数 12 MHz 晶振试:
TH1 = 256 - (12000000 / 3686400) = 256 - 3.26 -> must round to 3
actual baud = 10417, against a target of 9600 -> 8.5% error
串口链路可以容忍两端合计约 2–3% 的误差。而到了 8.5%,帧结构就会在一个字节之内发生漂移,每个字符到达时都是坏的。这就是 11.0592 MHz 存在的原因,也是每块 8051 实验板都用它的原因;抽屉里那枚 12 MHz 晶振换上去之后,芯片在打印任何东西之前一切正常。
代价是时序计算变得别扭:机器周期是 1.085 µs,而不是整整 1 µs。12 MHz 晶振给出正好 1 µs 的周期,串口却没法用。按你的项目更需要哪一个来选。
定时器,手搓 PWM
8051 三 16 位定时器,PWM 硬件零。调电机转速波形自己生成,干这就是这芯片学习最棒的部分。
定时器四模式;两要紧:
| 模式 | 位宽 | 重装 | 用它 |
|---|---|---|---|
| 0 | 13 位 | 手动 | 老式用法;基本可以无视 |
| 1 | 16 位 | 手动 | 又长又精确的延时。必须在中断服务程序里重装 |
| 2 | 8 位 | 自动 | 波特率,以及任何固定周期的节拍 |
| 3 | T0 拆分 | — | 非常需要第三个定时器的时候 |
想要一个稳定的节拍时,请用模式 2,因为重装是在硬件里完成的,中断服务程序的抖动不会渗进周期里。
在定时器 0 上做软件 PWM 生成器,频率约 1 kHz,分辨率 100 级:
#include <reg52.h>
sbit MOTOR_L = P1^0;
sbit MOTOR_R = P1^1;
volatile unsigned char dutyL = 0, dutyR = 0; // 0..99
volatile unsigned char tick = 0;
void timer0_isr(void) interrupt 1 {
/* Mode 1 is not self-reloading: put the value back first, before
anything else, or the period stretches by the ISR's own length. */
TH0 = 0xFF; TL0 = 0xA4; /* ~10 us at 11.0592 MHz */
if (++tick >= 100) tick = 0; /* 100 ticks = ~1 ms period */
MOTOR_L = (tick < dutyL) ? 1 : 0;
MOTOR_R = (tick < dutyR) ? 1 : 0;
}
void main(void) {
TMOD = (TMOD & 0xF0) | 0x01; /* Timer 0, mode 1 */
TH0 = 0xFF; TL0 = 0xA4;
ET0 = 1; /* enable timer 0 interrupt */
EA = 1; /* global interrupt enable */
TR0 = 1; /* start */
dutyL = 70; dutyR = 70;
while (1) { } /* PWM runs entirely in the ISR */
}
有两点值得记住。永远先重装——在模式 1 里定时器溢出后从零继续计数,所以重装之前每多执行一条指令,都会加到下一个周期上。代价是实打实的:这个中断服务程序每秒要触发 100,000 次,而在 1.085 µs 的机器周期下,一个十几条指令的处理程序会吃掉这颗芯片所能做的一切中相当可观的一块。这份预算正是硬件 PWM 外设帮你省下的,在你自己动手付过之前,你不会真正明白它有多值钱。
端口电流预算
这一点坑人,是因为 8051 的端口存在一种现代芯片不再有的不对称性。
| 灌(输出 LOW) | 拉(输出 HIGH) | |
|---|---|---|
| 单脚 P1/P2/P3 | 约 10 mA | 约 60 µA(只内部上拉) |
| 单脚 P0 | 约 10 mA | 0——开漏,无上拉 |
| 单端口合计 | 约 26 mA | — |
| 整芯片 | 约 71 mA | — |
六十微安不是笔误。内部上拉是一个弱电阻,不是一个驱动器,所以一个输出高电平的引脚所能提供的电流,大约只有它所能吸收电流的千分之一。
三后果直接:
LED 负极接引脚,正极经电阻接到 +5 V,写 0 就点亮。反过来接的话,它会发出非常微弱的光,看上去像是一颗偏暗或损坏的 LED,而不是接线接反了。
**绝不要直接驱动任何东西。**继电器不行,大音量蜂鸣器不行,电机更不行。请使用晶体管、ULN2003 驱动阵列,或者一个正规的电机驱动。
P0 外部上拉——想当 IO 的每脚 10 kΩ 到 +5 V。没有高出不来读不稳,实验板 LCD 大都走它。
内存,256 字节去哪
8 KB 闪存听着紧很少是。256 字节 RAM 真约束,还不是平的 256:
| 地址 | 大小 | 住谁 |
|---|---|---|
| 00h–1Fh | 32 字节 | 四寄存器组,各 R0–R7 |
| 20h–2Fh | 16 字节 | 位寻址——128 独立寻址位 |
| 30h–7Fh | 80 字节 | 通用暂存空间,堆栈通常也放在这里 |
| 80h–FFh | 128 字节 | 只间接寻址(直接空间住 SFR) |
堆栈指针复位后是 07h,这会把堆栈正好压在寄存器组 1 的上面。如果你在没有先移动 SP 的情况下切换到寄存器组 1,寄存器和返回地址就会共用同一批字节,而这种故障的表现相当戏剧性:程序会跳回一个随机的地址。在 main() 的第一行写上 SP = 0x30;,是标准做法。
可位寻址区域值得了解,因为它确实优雅。20h–2Fh 中的任意一位都能用一条指令置位、清零或测试,于是状态标志几乎不花任何代价:
bit lineDetected; /* the compiler allocates this in 20h-2Fh */
bit motorEnabled; /* one bit each, one instruction to test */
八个标志只占一个字节,而测试其中一位只需一个机器周期。在只有 256 字节 RAM 的芯片上,这一点很有意义。
故障排查
| 现象 | 可能原因 | 解决 |
|---|---|---|
| 芯片彻底没反应 | EA 悬空接地 |
31 脚接 +5 V。每次先查这个 |
EA 高了还是没反应 |
晶振没起振 | 查 33 pF 负载电容和晶振焊点 |
| 不停复位 | RST 拉高 | 这里复位高有效——查 10 µF / 10 kΩ 网络 |
| P0 读写全无 | 开漏无上拉 | 每脚 10 kΩ 到 +5 V |
| LED 很暗 | 接成拉电流 | 负极接脚;写 0 点亮 |
| 串口打印乱码 | 12 MHz 晶振 | 换 11.0592 MHz——12 MHz 波特率误差 8.5% |
| 串口单向通 | TX/RX 没交叉或无共地 | 交叉;地连 |
| 程序跳回不知哪里 | 堆栈压在寄存器组 1 上 | 在 main() 开头写 SP = 0x30; |
| PWM 周期抖 | ISR 重装晚 | 第一句重装,或模式 2 |
| 代码一多行为就诡异 | RAM 用光了 | 总共只有 256 字节;查链接器生成的 map 文件 |
| 烧录器认不出芯片 | ISP 脚占用或复位失控 | 编程时释放 P1.5–P1.7 和 RST |
8051 还是现代单片机?
| 8051(AT89S52) | Arduino Uno | ESP32 | |
|---|---|---|---|
| 闪存 / RAM | 8 KB / 256 B | 32 KB / 2 KB | 4 MB / 520 KB |
| 硬件 PWM | 无 | 6 通道 | 16 通道 |
| 模拟输入 | 无 | 6 × 10 位 | 18 × 12 位 |
| 串口波特率 | 晶振要对 | 任意 | 任意 |
| 库 | 自己写 | 成千上万 | 成千上万 |
| 学到 | 机器 | 项目 | 系统 |
诚实的立场是:**8051 是一颗教学用的零件,而且是一颗很出色的教学零件。**手写一个软件 PWM 生成器、手算波特率重装值、发现端口引脚根本拉不动电流——这些经历会告诉你现代外设到底替你做了些什么。这种理解会永久保留,也正因为如此,在 Intel 发布这套架构四十五年之后,它依然出现在教学大纲上。
对于一台必须能用的机器人,请几乎选别的任何型号。做一台8051 循迹车意味着自己写 PWM、加一颗外置 ADC 或改用比较器输出的传感器、并手算每一个时序常数——而这些,Uno 用同样的钱全都白送给你。当目的是理解这台机器时,就去做;当目的是机器人时,请用 Uno。
探索关联图谱
用到它的搭建
包含 8051 单片机 的项目、学习路线和模拟器。
对比
替代方案
问题
8051 单片机 常见问题
什么是 8051 单片机?
8051 是 Intel 于 1980 年推出的 8 位单片机架构,后来被广泛授权,所以今天你买到的是兼容芯片,比如 Atmel 的 AT89S52,而不是 Intel 自己的产品。它把 CPU、闪存、RAM、定时器、串口和 32 个 I/O 引脚集成在一颗 40 脚芯片里,至今仍是大多数工科教学大纲中的标准教学单片机。
8051 诞生于 1980 年,为什么现在还在教?
因为它对你毫无隐藏。没有核心库,没有板级抽象,也没有硬件 PWM——要让 LED 闪烁,你得在端口寄存器里置一个位;要调暗 LED,你得自己写一个定时器中断来数节拍。这让它不适合用来做量产产品,却非常适合用来理解其他所有单片机在背后替你做了什么。
8051 和 Arduino 有什么区别?
Arduino Uno 是一块围绕 ATmega328P 做成的开发板;8051 则是一颗裸芯片,你必须给它配上晶振、复位电路和编程器。Arduino 提供硬件 PWM、模拟输入、USB 引导程序和庞大的库生态。8051 这些都没有——它根本没有 ADC,也没有 PWM——而这正是课程用它来教授基础知识的原因。
8051 为什么用 11.0592 MHz 的晶振?
因为它能让串口波特率分毫不差。UART 对机器周期时钟进行分频,11.0592 MHz 恰好能分频得到 9600 波特,而取整的 12 MHz 晶振会偏差约 8.5%,导致串口数据出错。这个看起来古怪的数字是为了算术而选的,不是为了速度。
8051 有几个端口?
四个 8 位端口——P0、P1、P2 和 P3,总共 32 个 I/O 引脚。它们不能互换:P0 没有内部上拉,需要外接电阻;P3 以复用功能的形式承担串口、中断和定时器引脚;如果外接存储器,P0 和 P2 还要兼作地址总线和数据总线。
为什么程序烧进 8051 之后它什么都不做?
先检查 EA 引脚(31 脚)。如果它接地或悬空,芯片会从并不存在的外部存储器里取程序,于是什么都不运行,也不给出任何错误。要从内部闪存运行代码,EA 必须接到 +5 V。接下来要怀疑的两处,是缺少复位电路,以及晶振的两个负载电容。
能用 C 语言给 8051 编程吗?
可以,而且现在大多数课程都这么做。Keil C51 是业界标准的编译器,SDCC 是免费的开源替代品。在这里用 C 比用汇编高效得多,但在 8051 上,了解编译器到底生成了什么代码会让你受益——一个看似无害的乘法,或者一个大的局部数组,在只有 256 字节 RAM 的芯片上付出的代价要比在别处大得多。
8051 还在实际产品中使用吗?
还在,只是很少以看得见的独立芯片形式出现。与 8051 兼容的内核被嵌入在无数设备里——USB 控制器、触摸传感器、遥控器和无线模块——因为这种内核很小、被研究得很透彻,而且没有授权方面的麻烦。你更常遇到的,是作为另一颗芯片内部模块的它,而不是实验台上那颗 40 脚 DIP。
延伸阅读