1. 初识AT89S51:你的第一块“智能大脑”
如果你刚开始接触单片机,可能会被一堆型号搞晕:8051、89C51、AT89S51……它们到底是什么关系?简单来说,AT89S51可以看作是经典8051单片机的“现代升级版”,它由Atmel公司(现在属于Microchip)生产,完全兼容我们常说的“51单片机”指令集和基本架构。这意味着,你以前为8051或89C51写的绝大多数程序,几乎不用修改就能在AT89S51上跑起来。
那它到底是个啥?你可以把它想象成一个高度集成的“微型计算机系统”,只不过它被压缩到了一块比指甲盖还小的芯片里。这块芯片内部集成了一个8位的CPU(中央处理器)、4KB的Flash程序存储器、128字节的RAM数据存储器、32根输入输出线(I/O口)、2个定时器、1个串行通信口,还有一套完整的中断系统。我们学习单片机,本质上就是学习如何通过编程,巧妙地指挥芯片里的这些“部门”协同工作,去控制外部的LED灯、电机、传感器,或者跟其他设备通信。
我刚开始学的时候,用的就是AT89S51。选择它有几个很实在的理由:第一,它支持ISP(在系统编程),这意味着你不用像老式的89C51那样,每次修改程序都得把芯片从电路板上拔下来,用专门的编程器烧写。你只需要一根简单的下载线(比如用USB转串口模块配合几个电阻),就能让芯片在电路板上直接更新程序,调试效率高太多了。第二,它内部集成了看门狗定时器,这是个“安全卫士”,当程序因为干扰跑飞或陷入死循环时,它能自动把系统拉回来复位,大大提高了产品的可靠性。第三,它功耗低,还有空闲和掉电两种节能模式,适合用电池供电的设备。所以,无论是学生做实验、电子爱好者DIY,还是工程师开发一些小批量产品,AT89S51都是一个非常经典且实用的起点。
2. 深入核心:AT89S51的硬件结构解剖
要玩转单片机,不能只停留在“它会动”的层面,得稍微了解一下它内部的“五脏六腑”是怎么工作的。这就像开车,虽然不用成为修车大师,但知道发动机、变速箱的基本原理,开起来会更得心应手。
2.1 核心指挥官:8位CPU与特殊功能寄存器
AT89S51的CPU是8位的,负责执行指令、进行算术和逻辑运算。它内部有几个关键的“工作台”和“记事本”:
- 累加器A:这是CPU里最忙的寄存器,大部分运算和数据中转都要经过它。你可以把它理解成一个“临时工作台”。
- 程序状态字PSW:这是一个8位的“状态记录本”。比如,
Cy位记录加法有没有进位;OV位记录运算结果是否溢出;P位记录累加器A里1的个数是奇数还是偶数(串口通信校验会用到)。RS1和RS0这两位更实用,它们用来切换4组工作寄存器区。每组有8个寄存器(R0-R7)。在写中断服务程序时,快速切换一组新的寄存器,可以避免去保存和恢复原来的R0-R7,能节省时间,这个技巧在时间要求苛刻的场合很好用。 - 数据指针DPTR:这是一个16位的地址指针,专门用来访问外部扩展的存储器。AT89S51比老型号多了一个数据指针(DPTR1),通过AUXR1寄存器里的DPS位切换。当你需要频繁在两个内存区域之间搬运数据时,两个指针来回切换比反复给一个指针赋值要快。
CPU对片内所有外设(如定时器、串口、I/O口)的控制,都是通过读写一组称为特殊功能寄存器的“控制面板”来实现的。这些SFR分布在内存的高端地址(80H-FFH)。比如,你要让定时器开始计数,就去写TCON寄存器;要配置串口的工作模式,就去设置SCON寄存器。我们编程时,大部分时间就是在和这些SFR打交道。
2.2 记忆仓库:独特的存储器空间布局
51单片机采用“哈佛结构”,程序存储器和数据存储器是分开编址、独立寻址的。这和我们的电脑(冯·诺依曼结构)很不一样。
-
程序存储器:用来存放我们写好的代码。AT89S51片内有4KB的Flash,地址是0000H-0FFFH。如果代码超过4KB,还可以在芯片外部扩展,最大到64KB。芯片的
EA引脚(31脚)决定CPU从哪里读取程序:EA接高电平时,CPU先读片内这4KB,读完了自动跳到片外;EA接低电平时,CPU直接读取片外程序存储器。程序存储器的开头几个单元有特殊用途:0000H: 系统复位后,CPU总是从这里开始执行指令。所以我们通常在这里放一条跳转指令,跳到主程序
main函数。 0003H, 000BH, 0013H, 001BH, 0023H: 这五个地址分别是外部中断0、定时器0、外部中断1、定时器1、串行口中断的服务程序入口地址。因为每个入口之间只有8个字节,通常放不下一段完整的处理代码,所以我们也只是在里面放一条跳转指令,跳到真正的中断服务函数去。 -
数据存储器:分为片内和片外。
- 片内RAM:128字节,地址00H-7FH。这128字节又分三块:
- 工作寄存器区(00H-1FH):就是我们刚才说的那4组R0-R7。
- 位寻址区(20H-2FH):这16个字节(128位)的每一个位,都有一个独立的地址(00H-7FH)。你可以直接对某一位进行置1、清0、判断等操作,这在控制单个LED、读取独立按键状态时非常方便高效。
- 用户RAM区(30H-7FH):可以存放用户变量,也常用作堆栈区。
- 片外RAM:当128字节不够用时,可以最多外扩64KB。片内和片外RAM的地址是重叠的(比如都有0000H),但CPU通过不同的指令(
MOV用于片内,MOVX用于片外)和不同的控制信号(RD/WR)来区分,所以不会冲突。
- 片内RAM:128字节,地址00H-7FH。这128字节又分三块:
2.3 对外窗口:4个8位并行I/O端口详解
AT89S51有32根I/O线,分为4个8位端口:P0、P1、P2、P3。它们是单片机与外部世界沟通的桥梁。
- P0口:这是一个双向三态口。当单片机需要外扩存储器或芯片时,P0口肩负重任,分时复用作为低8位地址总线和8位数据总线。如果不做扩展,它可以作为通用I/O口,但内部没有上拉电阻,需要外接上拉电阻(通常4.7kΩ-10kΩ)才能输出高电平,这时它是一个“准双向口”。
- P1口:这是最“单纯”的端口,专为用户设计的准双向I/O口,内部已有上拉电阻。除了做普通I/O,它的P1.5、P1.6、P1.7还用于ISP编程时的串行数据输入、输出和时钟。
- P2口:也是一个准双向口。在外扩存储器时,它作为高8位地址总线使用。不做扩展时,它就是普通的I/O口。
- P3口:准双向口,但它的每一根线都还有第二功能,这是51单片机引脚复用的经典设计。具体功能如下表:
| 引脚 | 第二功能 | 说明 |
|---|---|---|
| P3.0 | RXD | 串行数据接收端 |
| P3.1 | TXD | 串行数据发送端 |
| P3.2 | INT0 | 外部中断0输入 |
| P3.3 | INT1 | 外部中断1输入 |
| P3.4 | T0 | 定时器/计数器0外部输入 |
| P3.5 | T1 | 定时器/计数器1外部输入 |
| P3.6 | WR | 外部数据存储器写选通信号 |
| P3.7 | RD | 外部数据存储器读选通信号 |
重要提示:所有这四个端口,当作为输入口使用时,都必须先向该端口的锁存器写入“1”(即置为高电平),这个操作称为“准双向口”的读前准备。否则,如果之前输出过低电平,内部下拉电路会直接将引脚钳位在低电平,导致无法正确读取外部的高电平信号。单片机复位后,所有端口锁存器默认都是FFH(全1),所以复位后第一次读输入是没问题的,但如果你之前用这个口线输出过低电平,再想读输入,就必须先写1。
3. 中断系统实战:让单片机学会“分心”处理
中断是单片机编程中最核心、最重要的概念之一。想象一下,你正在看书(执行主程序),这时水烧开了(中断源发出请求),你放下书去关火(响应中断,执行中断服务程序),关完火再回来接着刚才的页数继续看书(返回断点,继续主程序)。中断就是为了让CPU能及时响应突发事件,而不用一直傻傻地查询。
3.1 中断源与中断控制寄存器
AT89S51有5个中断源,对应2个优先级:
- 外部中断0:由
INT0引脚(P3.2)引入。 - 定时器/计数器0中断:当T0计数溢出时产生。
- 外部中断1:由
INT1引脚(P3.3)引入。 - 定时器/计数器1中断:当T1计数溢出时产生。
- 串行口中断:当串口发送完一帧数据或接收完一帧数据时产生。
管理这些中断,主要靠两个特殊功能寄存器:
-
中断允许寄存器IE:这是个“总开关”和“分开关”集合。
// 假设我们用C51编程,对寄存器的位操作 EA = 1; // 打开CPU总中断开关(必须打开) EX0 = 1; // 允许外部中断0 ET0 = 1; // 允许定时器0中断 // ... 以此类推EA是总闸,它不开,任何中断都不会被响应。下面的EX0、ET0等是各个中断源的分闸。 -
中断优先级寄存器IP:51单片机只有高、低两个优先级。默认都是低优先级。如果几个中断同时发生,高优先级的先被响应。如果一个低优先级中断正在执行,高优先级中断可以打断它(嵌套),反之则不能。你可以通过设置
IP里的相应位(如PX0=1)来提升某个中断的优先级。
3.2 中断响应的条件与完整流程
一个中断从产生到被CPU执行,需要满足一系列条件,我把它总结为“三道关卡”:
- 总开关打开:
EA = 1。 - 分开关打开:该中断源对应的允许位(如
EX0)为1。 - 中断标志置位:该中断源产生了请求,其标志位(如
IE0)被硬件自动置1。 - 无更高优先级中断在执行:CPU当前没有正在处理同级或更高级的中断。
中断的完整流程是:中断请求 -> 中断响应 -> 保护现场 -> 执行中断服务程序 -> 恢复现场 -> 中断返回。其中,“保护现场”通常指把ACC、PSW等可能会被破坏的寄存器压入堆栈,“恢复现场”就是再弹出来。这个过程不是完全自动的,需要我们在中断服务函数开头和结尾用代码实现。
3.3 外部中断边沿触发实战
外部中断有两种触发方式:低电平触发和下降沿触发。通过TCON寄存器中的IT0和IT1位设置。电平触发要求中断引脚在CPU响应中断期间一直保持低电平,如果干扰导致电平抖动,可能会被误认为多次中断。所以,在大多数场合,我推荐使用下降沿触发,它只在引脚电平从高到低跳变瞬间识别一次,更可靠。
下面是一个完整的外部中断0下降沿触发的C51代码示例,功能是每次按下按键(产生下降沿),就让接在P1.0的LED状态翻转:
#include <reg51.h> // 包含AT89S51寄存器定义的头文件
sbit LED = P1^0; // 定义LED连接到P1.0
sbit KEY = P3^2; // 定义按键连接到P3.2 (INT0)
void delay_ms(unsigned int ms) {
// 一个简单的延时函数,具体实现取决于你的晶振频率
unsigned int i, j;
for(i=0; i<ms; i++)
for(j=0; j<123; j++);
}
void main() {
LED = 1; // 初始化LED熄灭(假设共阴接法,高电平灭)
IT0 = 1; // 设置外部中断0为下降沿触发
EX0 = 1; // 允许外部中断0
EA = 1; // 打开总中断
while(1) {
// 主程序可以在这里做其他事情,比如扫描数码管
// LED的状态由中断函数控制,主循环无需干预
delay_ms(100); // 模拟主程序在做其他工作
}
}
// 外部中断0的服务函数
void ex0_isr(void) interrupt 0 // interrupt 0 表示这是外部中断0的服务函数
{
// 通常这里需要简单的防抖,因为机械按键按下会有抖动
delay_ms(10); // 延时10ms消抖
if(KEY == 0) { // 再次确认按键是否真的按下
LED = ~LED; // LED状态取反
}
// 注意:下降沿触发的中断标志IE0会在CPU响应中断后由硬件自动清零,无需软件处理
}
这段代码清晰地展示了中断的配置和使用。主循环while(1)里可以安心处理其他任务,LED的响应完全交给中断,实现了程序的并行处理能力。
4. 定时器/计数器应用:精准的“心脏”与“脉搏”
定时器和计数器是单片机内部非常精准的计时和计数工具。AT89S51有两个16位的定时器/计数器:T0和T1。它们本质上都是加法计数器。
- 定时器模式:计数脉冲来自内部系统时钟的12分频。如果晶振是12MHz,机器周期就是1μs,计数器每1μs加1。通过计算初值,可以实现非常精确的定时。
- 计数器模式:计数脉冲来自外部引脚T0(P3.4)或T1(P3.5)。引脚上每发生一次从高到低的跳变,计数器加1。常用于测量外部脉冲频率、计数产品数量等。
4.1 定时器的四种工作方式
定时器/计数器有四种工作方式,通过TMOD寄存器(模式控制寄存器)的M1、M0位来设置。
| 方式 | M1 M0 | 位数 | 特点 |
|---|---|---|---|
| 方式0 | 0 0 | 13位 | 兼容早期8048,不常用 |
| 方式1 | 0 1 | 16位 | 最常用,全16位计数,无自动重载 |
| 方式2 | 1 0 | 8位 | 8位自动重载,适合做精确的波特率发生器 |
| 方式3 | 1 1 | 双8位 | 仅T0有此方式,将T0拆成两个8位定时器 |
方式1是最通用的定时模式。假设我们要用定时器0,在12MHz晶振下产生50ms的定时中断,计算初值的方法如下:
定时时间 T = (2^16 - 初值) × 机器周期
50,000μs = (65536 - 初值) × 1μs
初值 = 65536 - 50000 = 15536 = 0x3CB0
所以,TH0 = 0x3C, TL0 = 0xB0。
4.2 定时器中断实现精准延时与PWM
利用定时器中断,我们可以解放CPU,让它不用在delay函数里空等。下面是一个用定时器1方式1实现1秒精确定时,并控制LED闪烁的例子:
#include <reg51.h>
sbit LED = P1^0;
unsigned int count = 0; // 中断次数计数器
void timer1_init() {
TMOD &= 0x0F; // 清零T1的控制位(高4位)
TMOD |= 0x10; // 设置T1为工作方式1 (16位定时器)
// 假设晶振11.0592MHz,机器周期约1.085μs,计50000次约54.25ms
TH1 = (65536-50000)/256; // 计算50ms定时初值的高字节
TL1 = (65536-50000)%256; // 低字节
ET1 = 1; // 允许T1中断
TR1 = 1; // 启动T1
EA = 1; // 开总中断
}
void main() {
timer1_init();
while(1) {
// 主程序空跑,所有计时工作交给中断
// 可以在这里添加其他功能,如按键扫描
}
}
void timer1_isr() interrupt 3 { // T1中断号是3
TH1 = (65536-50000)/256; // 重新装载初值(方式1需手动重载)
TL1 = (65536-50000)%256;
count++;
if(count == 20) { // 50ms * 20 = 1000ms = 1秒
count = 0;
LED = ~LED; // 每1秒LED状态翻转一次
}
}
更进一步,我们可以利用定时器中断产生**PWM(脉冲宽度调制)**信号,用来控制LED亮度或者电机转速。原理很简单:在一个固定的周期内(比如10ms),通过改变高电平所占的时间比例(占空比)来调节平均电压。用两个定时器中断,一个控制周期开始(输出高电平),另一个在占空比时间到的时候关闭输出(输出低电平),就能实现。
4.3 计数器模式测量外部脉冲
将定时器设置为计数器模式,可以测量外部事件的频率。例如,将T0设置为计数器模式1(16位计数),把待测信号接到P3.4(T0引脚)。在已知的一段时间内(比如用另一个定时器精确计时1秒),读取T0的计数值,这个值就是信号的频率(Hz)。需要注意的是,计数器模式对外部脉冲的宽度有要求,高电平和低电平的持续时间都必须大于1个机器周期,因此最高计数频率不能超过晶振频率的1/24。
5. 串行通信:单片机与世界的对话
AT89S51内部有一个全双工的异步串行通信接口(UART),这意味着它可以同时进行数据的发送和接收。这是单片机与电脑、其他单片机或蓝牙/WiFi模块通信的主要方式。
5.1 串行口的工作方式与波特率设置
串口有4种工作方式,由SCON寄存器的SM0、SM1位决定:
| 方式 | SM0 SM1 | 功能 | 波特率 |
|---|---|---|---|
| 方式0 | 0 0 | 同步移位寄存器 | fosc/12 |
| 方式1 | 0 1 | 10位UART(8位数据) | 可变(常用T1产生) |
| 方式2 | 1 0 | 11位UART(9位数据) | fosc/32 或 fosc/64 |
| 方式3 | 1 1 | 11位UART(9位数据) | 同方式1,可变 |
方式1是最常用的8位数据通信模式。波特率(每秒传输的位数)需要我们自己设置,通常使用定时器1工作在方式2(8位自动重载)来产生。波特率计算公式为:
波特率 = (2^SMOD / 32) × (定时器1的溢出率)
其中,SMOD是PCON寄存器的最高位,置1可使波特率加倍。定时器1的溢出率 = fosc / [12 × (256 - TH1)]。
例如,在11.0592MHz晶振下,要产生9600的标准波特率,通常设置SMOD=0,TH1=0xFD(253)。计算一下:溢出率 = 11059200 / (12 * (256-253)) = 307200 Hz。波特率 = 307200 / 32 = 9600。完美匹配。
5.2 串口收发数据实战
下面是一个简单的串口收发程序,单片机将接收到的字符加1后发回给电脑(即“回声”程序):
#include <reg51.h>
#include <intrins.h>
void uart_init() {
SCON = 0x50; // 方式1,允许接收 (REN=1)
TMOD &= 0x0F; // 清零T1模式位
TMOD |= 0x20; // T1方式2,8位自动重载
TH1 = 0xFD; // 波特率9600 @11.0592MHz
TL1 = 0xFD;
TR1 = 1; // 启动T1
ES = 1; // 允许串口中断
EA = 1; // 开总中断
}
void main() {
uart_init();
while(1) {
// 主循环可以处理其他任务
_nop_();
}
}
void uart_isr() interrupt 4 { // 串口中断号是4
if(RI == 1) { // 如果是接收中断
RI = 0; // 必须软件清零接收标志
SBUF = SBUF + 1; // 读取接收到的数据,加1后发送回去
// 注意:这里没有判断发送完成,对于简单回声可以。严谨的做法应等待TI标志。
}
if(TI == 1) { // 如果是发送中断
TI = 0; // 必须软件清零发送标志
// 可以在这里做发送完成后的处理
}
}
在实际项目中,我通常会建立一个小的环形队列作为串口接收缓冲区。在中断服务函数里,只做最快速的将数据从SBUF存入队列尾部的操作。在主循环里,再去慢慢解析缓冲区里的完整数据包。这样能有效避免因数据解析耗时过长而丢失后续数据的问题。
5.3 多机通信初步
串口的方式2和方式3支持多机通信。其核心是利用了第9位数据(TB8/RB8)。主机先发送一个地址帧(第9位为1),所有从机都能接收。从机将接收到的地址与自身地址比较,匹配的从机将SM2位清零,准备接收后续的数据帧(第9位为0);不匹配的从机保持SM2=1,会忽略后续数据帧。这样就实现了主机与指定从机之间的点对点通信。这在构建简单的主从式分布式小系统时非常有用。
从硬件结构到中断、定时器、串口,我们一步步把AT89S51的核心功能拆解了一遍。纸上得来终觉浅,绝知此事要躬行。我建议你最好能有一块开发板,或者用Proteus这样的仿真软件,把文中的代码示例都动手敲一遍、改一改参数跑一跑。比如,试试把定时器中断的时间调快,观察LED闪烁的变化;试试用串口助手给单片机发送不同的指令,让它控制不同的LED。过程中你肯定会遇到各种问题,比如程序下载不进去、中断不触发、串口乱码,这都是学习的宝贵机会。解决它们的过程,就是你真正理解单片机如何工作的过程。记住,单片机开发是一个实践性极强的领域,代码只有在板子上跑起来,并且实现了你想要的功能,才是真正学会了。
288



被折叠的 条评论
为什么被折叠?



