
开局一张图,功能全靠猜。
数据总线(白色)
负责传输实际数据(运算数值、读取的指令、收发的 IO 数据),双向传输,CPU 和各个外设互相读写数据都走这条线。
地址总线(蓝色)
负责指定访问位置,比如要读哪块存储单元、哪个 IO 端口,先通过地址总线送出地址编号,单向输出。
控制总线(黄色)
负责传输读写 / 启停 / 中断等控制信号,比如读存储器、写端口、定时器启停、中断请求、时钟同步控制等指令。
程序存储器(ROM)
顾名思义就是你开发的程序所放置的位置。,CPU 上电后从这里逐条读取指令执行;
注解:交互逻辑,cpu上电之后的第一件事,就是通过蓝色地址总线找到程序存储器,然后在通过命令总线下达读取的命令,下达完读取的命令,程序通过数据总线发给CPU。
问题:CPU为何来这里读取,是所有模块都问一下,还是这里有了程序主动上报给CPU,然后CPU才来这里读取。
答案:CPU出厂规定,程序存储器有固定专属的地址范围,就像档案室大楼1-4096号房间。
单片机一上电,CPU天生默认第一件事就是去程序存储区0号地址读取第一条指令,不要要告诉CPU程序放在哪里,硬件底层已经固化了地址分区。
不需要谁告诉 CPU 档案室在哪,硬件底层已经固化了地址分区:
-
0000H ~ FFFFH 这个地址段 = 只属于程序存储器;
-
00H ~ 7FH 这个地址段 = 只属于数据存储器; CPU 只要往 "0 开头的地址" 发地址信号,硬件自动就把信号送到程序存储器,不会跑错部门。
疑惑 1:CPU怎么知道程序存储器的位置?
硬件提前分好地址区间,访问 0 开头地址就是程序存储器,天生自带定位,不用查找。
疑惑 2:是存储器主动上报,还是 CPU 主动去读?
100% CPU 主动上门调取,存储器只会被动等待指令,绝对不会主动上报。 类比: 档案室不会每天主动跑到老板办公室递文件;一定是老板先报房间号、再喊 "拿文件",档案室才把文件送出来。
反向对比:什么时候才是外设主动上报?(只有中断是例外)
只有定时器、串口、IO 这些模块触发中断时,才会主动通过黄色控制线给 CPU 发消息; 但程序存储器、数据存储器永远被动,只能 CPU 主动读 / 写,不会主动发数据。 简单区分:
-
读代码、读变量(存储类模块):CPU 主动找存储器
-
定时到了、收到串口消息(外设突发事件):外设主动找 CPU(中断系统中转)
极简流程一句话背诵
上电→CPU 送出程序存储器地址(蓝线)→发读取控制命令(黄线)→程序存储器传回指令数据(白线)→CPU 执行,循环往复。
数据存储器(RAM)
存放程序运行时的临时变量、中间运算结果、堆栈数据,也可以理解为程序运行中所产生的草稿数据。
注解:地址指定 RAM 单元,控制总线区分 "读数据 / 写数据",数据总线双向读写数值
问题:存放程序运行时的临时变量、中间运算结果、堆栈数据; 在程序运行过程中 是怎么把变量写到数据存储器里面的 cpu怎么执行的 或者老板怎么做的,临时变量不可能凭空产生
答案:先来一段小前奏,cpu是不负责分配地址的,是编译器提前分好写进程序里面,CPU只是照着执行,你写c语言int a;单片机需要把你的代码翻译成机器所能识别的机器码,编译器有两个工作,一是把
文字翻译成机器所能识别的01指令,存到程序存储器,二给每一个变量提前制定好固定的RAM,分配地址。
编译器是如何分配地址的,比如你写代码:
cpp
unsigned char a;
unsigned char b;
8051 内部 RAM 有一堆空闲格子:0x20、0x21、0x22...0x7F 编译器编译时自动按顺序分配:
变量 a → 占用 0x30
变量 b → 占用 0x31 这个分配规则是软件定死的,编译完成后就不会再变。 最后生成的机器指令里,直接写死了 "要存数字就送到 0x30 这个地址"。
CPU 完全不用管分配这件事,它根本不知道 "a 叫变量 a"。 CPU 眼里只有一串固定指令:
算出 15 → 把地址 0x30 放到地址总线 → 发写入命令 → 把 15 送到数据总线
"0x30 是给变量 a 用的" 这件事,只存在你写的代码和编译器里;单片机硬件只认数字地址,不认变量名。
场景 1:自动分配(普通局部变量、全局变量)
编译器自动挑空闲 RAM 地址分配,你不用手动管。
场景 2:手动指定地址(单片机专用语法)
你可以强制指定变量固定放在某一格,直接写死地址,由程序员决定:
cpp
unsigned char a _at_ 0x30;
这句代码就是你人为告诉编译器:变量 a 必须放在 30 号草稿纸,编译器不再自动分配。
全程时间线,分清先后顺序
-
写代码阶段(电脑上操作,单片机还没上电) 你定义变量 a → 编译器给 a 分配 RAM 地址 0x30;
-
编译阶段(电脑软件完成) 编译器生成机器码,指令里自带地址 0x30;
-
烧录程序(电脑把机器码下载到 8051 程序存储器) 包含 "0x30" 地址的指令永久存入档案室;
-
单片机上电运行(CPU 工作阶段) CPU 读取指令,看到地址 0x30,直接往这个格子读写数据,全程不会重新分配地址。
通俗比喻总结
-
编译器 = 行政文员,提前给所有文件(变量)分好固定的草稿纸编号,写进任务清单(程序指令);
-
CPU 老板 = 打工执行者,只照着清单上写好的编号去存取数字,没有分配地址的权限;
-
RAM 草稿仓库 = 一堆空白纸,本身没有归属,分配工作在上电之前就全部做完了。
定时 / 计数器(T0/T1)
定时延时、外部脉冲计数、产生串口波特率;
交互:CPU 通过总线配置计数初值、启停,计数溢出后通过中断系统通知 CPU。
问题:CPU是如何设置定时计数器的,代码内又是如何写的
一、先分清:定时器和 RAM 草稿纸分配完全不一样
变量 RAM 地址是电脑编译器提前分 ; 定时计数器(T0、T1)是出厂硬件固定分配好专属寄存器地址,天生就锁死编号,编译器不能随便改。
- 8051 硬件规定死的地址(行政出厂直接划分,不用编译器分配)
定时计数器本质是几片特殊寄存器,统一放在特殊功能寄存器 SFR 区,硬件焊死地址:
-
TH0、TL0(定时器 0 高低 8 位计数寄存器):固定地址 0x8C、0x8A
-
TH1、TL1(定时器 1 高低 8 位计数寄存器):固定地址 0x8D、0x8B
-
TMOD(定时器模式设置寄存器):固定 0x89
-
TCON(定时器启停、溢出标志寄存器):固定 0x88
类比: 程序出厂前,厂房行政(芯片硬件)直接划定: 0x88 号格子 = 定时器开关面板 0x8A/8C = 定时器 0 的计数草稿本 0x8B/8D = 定时器 1 的计数草稿本 这块区域专门分给计时员,任何人不能挪用。
二、编译器做什么?只是 "翻译名字",不分配地址
你写代码 TMOD=0x01;
-
编译器只做翻译:把单词
TMOD替换成硬件固定地址0x89; -
不会重新分配地址,不能把定时器挪去别的格子;
-
生成机器指令里直接写死地址 0x89,CPU 上电照着地址操作。
三、CPU(老板)完整操控定时器的执行流程(严格遵循:先地址→后控制→传数据)
需求举例:开启定时器 0,设置成 16 位定时模式
步骤 1:CPU 从程序存储器读取指令(任务清单)
指令内容翻译成人话:把数字 0x01 写入 0x89 号格子(TMOD)
-
蓝地址总线送出 0x89(定时器模式寄存器固定地址)
-
黄控制总线发「写入」命令
-
白数据总线把 0x01 送进去,定时器硬件收到模式参数
步骤 2:给定时器设置计数初值(比如定时 50ms)
指令:给 TL0 (0x8A) 写 0x06,TH0 (0x8C) 写 0xFF
-
地址线依次送出 0x8A、0x8C
-
下发写命令
-
把计算好的初始数字存入定时器专属计数寄存器
步骤 3:启动定时器(操作 TCON 寄存器 0x88)
写指令 TR0=1,对应地址 0x88 里某一位置 1:
-
地址总线 0x88 锁定定时器控制面板
-
写控制信号
-
数据总线送出开启信号,计时员正式开始自动倒数
四、定时器自动计数,不用 CPU 一直盯着
时钟晶振给定时器提供脉冲,硬件自己逐次减数字:
-
CPU 设置完参数、打开开关后,就可以去干别的事(跑其他代码);
-
定时器硬件独立工作,每来一个时钟脉冲自动减 1;
-
计数减到 0(计时完成),硬件自动把 TCON 里的溢出标志 TF0 置 1。
五、CPU 怎么知道计时结束(两种方式)
方式 1:CPU 主动查询(老板抽空去看计时员)
循环读取 TCON 寄存器地址 0x88,读出标志位判断是否计时完成:
-
地址总线 0x88
-
发读控制信号
-
取回寄存器数据,判断 TF0 标志位
方式 2:中断自动上报(计时员走控制线主动通知老板)
开启定时器中断后: 定时器溢出 → 通过黄色控制总线给中断系统发紧急信号 → 中断系统通知 CPU 暂停当前任务,处理定时任务。
六、核心总结回答你的疑问:定时计数器是谁分配、CPU 怎么操作
-
分配者:芯片硬件出厂固定分配专属 SFR 地址,不是编译器、不是 CPU 分配;
-
编译器仅负责把代码里的定时器名称翻译成硬件固定地址;
-
CPU 全程只执行读写指令:先送定时器寄存器固定地址→发读写控制命令→传输配置 / 计数值;
-
定时器是独立硬件,配置完成后自动计时,到点要么等 CPU 查询,要么主动发中断通知 CPU。
CPU是如何设置定时计数器的,代码内又是如何写的
分两部分讲:①代码怎么写 ②CPU 硬件内部完整执行过程
沿用之前比喻:CPU = 老板,定时器 T0/T1 是独立计时员,有固定专属房间号(SFR 寄存器地址),出厂焊死不能改。
一、先认识定时器 4 个核心寄存器(硬件固定地址)
8051 只有 T0、T1 两个定时器,配置全靠这 4 个寄存器,地址硬件定死:
-
TMOD地址0x89:设置定时器工作模式(定时 / 计数、8 位 / 16 位等) -
TCON地址0x88:启停开关 + 计时完成标记-
TR0=1:开启 T0;TR0=0:关闭 T0
-
TF0:计时走完自动置 1 标记
-
-
TL0(0x8A)、TH0(0x8C):T0 计数高低 8 位,存放计时初始值 -
TL1(0x8B)、TH1(0x8D):T1 计数高低 8 位
二、C 语言代码示例(8051 常用 Keil C51)
需求:配置定时器 0,16 位定时模式,定时 50ms,溢出触发中断
cpp
// 1. 配置模式寄存器TMOD
TMOD = 0x01; // 0000 0001,T0设为16位定时器模式
// 2. 装载计时初值(11.0592晶振,定时50ms)
TH0 = 0x3C;
TL0 = 0xB0;
// 3. 开启定时器0中断
ET0 = 1;
EA = 1;
// 4. 启动定时器0
TR0 = 1;
代码字面意思
-
TMOD=0x01:给计时员规定工作方式(16 位倒计时) -
TH0/TL0:给计时员一张初始数字,让它从这个数往下数,数到 0 就是计时结束 -
ET0=1;EA=1:允许计时员时间到了主动喊老板(中断) -
TR0=1:按下计时开关,开始倒计时
三、编译器做了什么(上电前电脑完成)
你写的TMOD、TH0这些英文只是名字,编译器提前翻译:
-
TMOD→ 硬件地址0x89 -
TH0→ 地址0x8C -
TL0→ 地址0x8A编译后生成机器码,指令里直接写死地址,烧录进程序存储器(档案室)。
四、CPU(老板)执行每一行代码的硬件完整流程(重点,严格顺序:地址→控制→数据)
以执行 TMOD = 0x01; 这一句举例:
步骤 1:CPU 从程序存储器取出这条指令
-
地址总线送出程序存储器地址 → 控制线发读信号 → 数据总线取回机器码指令
-
CPU 解析指令:把数字 0x01,写入地址 0x89 的寄存器
步骤 2:向定时器寄存器写入配置(总线三步固定顺序)
-
先送地址(蓝色地址总线) CPU 把
0x89放到地址总线上,硬件识别这是 TMOD 定时器模式房间,锁定目标; -
后发控制命令(黄色控制总线) CPU 发出「写入」控制信号,通知硬件准备接收数据;
-
传输数据(白色数据总线) CPU 把数值
0x01放到数据总线,存入 TMOD 寄存器,定时器模式配置完成。
再执行 TH0 = 0x3C;
-
地址总线输出固定地址
0x8C(TH0 专属地址) -
控制线发送「写入」命令
-
数据线送出
0x3C,存入 TH0
再执行 TL0 = 0xB0;
-
地址总线输出
0x8A -
写控制信号
-
存入
0xB0
最后执行 TR0 = 1; 启动定时器
-
地址总线输出 TCON 地址
0x88 -
写入控制信号
-
数据线送入数据,把 TR0 开关位置 1,定时器正式开始自动计数
五、定时器启动后,CPU 不用管,硬件独立运行
晶振提供统一时钟脉冲,定时器硬件自动减计数: 每一个机器周期,TH0 TL0里的数字自动减 1; 等数字减到 0,硬件自动把标记位 TF0 置 1。
两种处理方式:
-
查询方式(老板主动抽查) CPU 循环读 TCON 寄存器地址
0x88,读取 TF0 位判断是否计时完成; -
中断方式(计时员主动上报) TF0 置 1 后,通过黄色控制总线向中断系统发请求,中断系统通知 CPU 停下当前工作,执行定时任务。
六、汇编底层代码(最贴近 CPU 执行,看懂 C 语言本质)
上面 C 语言等价汇编,能直观看到地址操作:
MOV TMOD,#01H ; 等价TMOD=0x01,#01H是要写入的数据,TMOD是0x89地址别名
MOV TH0,#3CH ; TH0=0x3C
MOV TL0,#0B0H ; TL0=0xB0
SETB ET0
SETB EA
SETB TR0 ; 启动定时器
汇编里MOV 地址,#数据,就是 CPU 标准操作:送地址、发写命令、传数据。
极简总结
-
代码层 :直接操作
TMOD、TH0、TL0、TR0这几个定时器专用寄存器,配置模式、初值、开关; -
编译层:软件把寄存器名字翻译成硬件出厂固定的 SFR 地址;
-
CPU 硬件执行层:每一次配置都遵循固定流程:输出寄存器地址→发送写入控制信号→传输配置数值;
-
配置完成后定时器硬件独立计时,到点主动触发中断通知 CPU。
并行 I/O 口(P0~P3)
- 作用:通用数字输入输出,控制 LED、按键、继电器等外设;
- 交互:CPU 通过总线读写端口寄存器,实现引脚高低电平控制与外部信号读取。
深入理解:IO口本质是,特殊功能寄存器SFR,8051有四组IO口,P0,P1,P2,P3
硬件出厂直接分配固定地址,不能改:
- P0:0x80
- P1:0x90
- P2:0xA0
- P3:0xB0
比喻:P1 口就是一块 8 个按钮的控制面板,房间号固定 0x90,老板(CPU)只能通过三条总线读写这个房间的数据,控制外部 LED、按键。 每条 IO 引脚对应寄存器里的 1 个 bit:比如 P1.0 = P1 寄存器第 0 位。
就是每个端口有b位,每位对应着外面一个引脚。
- P1 寄存器 = 一块 8 格控制面板(8bit)
地址固定房间号 0x90,一共 8 个小格子(bit0 ~ bit7):
-
bit0 → P1.0 引脚
-
bit1 → P1.1 引脚
-
bit2 → P1.2 引脚
-
bit3 → P1.3 引脚
-
bit4 → P1.4 引脚
-
bit5 → P1.5 引脚
-
bit6 → P1.6 引脚
-
bit7 → P1.7 引脚
每 1 个 bit,一对一连着芯片外面一根金属引脚,寄存器里这一格存 0/1,引脚电平就跟着变。
场景 1:IO 输出(点亮 LED,P1.0 接 LED 负极)
#include <reg51.h>
sbit LED = P1^0; // 单独定义P1第0根引脚
void main(void)
{
LED = 0; // P1.0输出低电平 → LED点亮
LED = 1; // P1.0输出高电平 → LED熄灭
// 整体操作整个P1端口(8位一次性赋值)
P1 = 0x0F; // P1低4脚输出0,高4脚输出1
while(1);
}
步骤 1:CPU 从程序存储器取出指令
CPU 通过地址总线、读控制信号,拿到机器指令,解析出任务: 把 0 写入地址 0x90 寄存器的第 0 位
步骤 2:CPU 发起「写 IO 寄存器」总线操作(严格顺序)
-
蓝色地址总线先输出地址 0x90 锁定 P1 口专属硬件模块,其余端口全部休眠;
-
黄色控制总线发送「写入」控制命令 通知 IO 硬件准备接收数据;
-
白色数据总线传输数值 0x00 数据送入 P1 寄存器内部锁存器,P1.0 引脚立刻输出低电平,LED 点亮。
场景 2:IO 输入(读取按键,P3.2 接按键)
c
运行
#include <reg51.h>
sbit KEY = P3^2;
void main(void)
{
unsigned char val;
while(1)
{
val = KEY; // 读取P3.2引脚电平,存入变量val
if(val == 0)
{
// 按键按下执行逻辑
}
}
}
任务:读取地址 0xB0(P3)寄存器的值,存到 RAM 变量 val
-
蓝色地址总线输出地址 0xB0,锁定 P3 口;
-
黄色控制总线发送「读取」命令;
-
P3 硬件把引脚电平数据放到白色数据总线,传回 CPU 内部;
-
CPU 把读到的数值,再通过「写 RAM 总线流程」存入变量 val 对应的 RAM 草稿地址。
串行通信口(UART)
- 作用:串口收发数据(和电脑、其他单片机通信);
- 交互:CPU 把待发数据通过数据总线写入发送寄存器,接收数据存入寄存器供 CPU 读取。
中断系统
-
作用:处理紧急事件(定时器溢出、串口收发、外部引脚中断),打断当前程序优先处理突发事件;
-
交互:外设触发中断后,通过控制总线向 CPU 发中断请求,CPU 响应后跳转中断服务程序。
中断系统如何触发的,cpu如何交互,代码如何写
分三层讲:1. 中断怎么触发 2.CPU 和中断系统交互完整流程 3. 实际 C 语言代码 + 底层硬件动作
继续沿用比喻: CPU = 老板;定时器 / 串口 / IO = 员工;中断系统 = 前台接待;黄色控制线 = 紧急呼叫铃
一、中断系统什么时候触发(3 类触发源)
8051 一共 5 个硬件中断,全部是外设硬件自动触发,不用 CPU 干预
-
定时器 0 溢出中断:T0 计数从初值减到 0,硬件自动拉高中断请求信号
-
定时器 1 溢出中断:T1 计数走完,自动发请求
-
外部中断 0 INT0:引脚 P3.2 收到高低电平 / 下降沿,触发
-
外部中断 1 INT1:引脚 P3.3 信号变化,触发
-
串口收发中断:串口收到数据 / 数据发送完毕,触发
触发核心逻辑(定时器举例)
定时器硬件独立计数,不用老板盯着; 当计数归零瞬间,定时器硬件直接走黄色控制总线给中断前台发一条紧急信号:「我干完了,需要老板处理」
重点:不是 CPU 主动问,是硬件主动通过控制线上报。
二、CPU 与中断系统完整交互全过程(严格顺序)
前置硬件开关(两个总闸门)
-
局部开关:ET0/ET1/EX0/EX1/ES ------ 单独开启某一个设备的上报权限
-
总闸门:EA ------ CPU 总开关,EA=1 老板才愿意接任何紧急电话;EA=0 所有中断全部无视
完整 5 步流程(定时器 0 计时完成触发中断)
-
外设发起请求 T0 计数溢出,硬件把 TCON 寄存器 TF0 标记置 1,同时向中断系统(前台)发送脉冲信号(黄色控制线)。
-
中断系统做筛选判断 前台检查两层闸门:
-
局部开关 ET0=1(允许定时器上报)
-
总开关 EA=1(老板允许接紧急事件) 两个开关全开,才把中断请求转发给 CPU;只要有一个关闭,请求直接作废。
-
-
CPU 收到信号,暂停当前工作 CPU 正在执行普通代码,收到中断控制线的紧急信号后:
-
立刻停下手里正在做的运算 / 读写操作
-
把当前正在运行代码的地址(PC 值)、寄存器数据临时压入堆栈(数据存储器 RAM)保存,方便事后回来继续干活。
-
-
自动跳转中断服务程序 硬件内置固定跳转地址(中断入口地址,出厂焊死): 定时器 0 中断固定入口:000BH CPU 自动把地址 000BH 送到地址总线,读取这里的代码,执行中断任务。
-
中断处理完成,返回原来程序 中断里的业务代码执行完毕,执行
return; CPU 从堆栈把之前保存的现场数据恢复,回到被打断的地方,继续原来没做完的工作。
补充两种清除中断标志规则
-
定时器 / 外部下降沿中断:CPU 响应中断后,硬件自动清零 TF0 标记,不用手动操作;
-
串口中断:必须在代码里手动清零 RI/TI 标志,否则会重复触发中断。
三、代码怎么写(C 语言完整示例:定时器 050ms 中断)
- 完整可运行代码
c
运行
#include <reg51.h>
sbit led = P1^0; // 定义LED引脚
// 定时器0中断服务函数,固定interrupt 1(对应T0中断编号)
void Timer0_Init() interrupt 1
{
// 重装计时初值,避免每次只计时一次
TH0 = 0x3C;
TL0 = 0xB0;
led = ~led; // LED翻转亮灭,每50ms翻转一次
}
void main(void)
{
// 1.配置定时器模式
TMOD = 0x01;
// 2.装载计时初值
TH0 = 0x3C;
TL0 = 0xB0;
// 3.打开定时器0局部中断开关 ET0=1
ET0 = 1;
// 4.打开CPU中断总开关 EA=1(必须开,否则中断无效)
EA = 1;
// 5.启动定时器T0
TR0 = 1;
// 主循环:CPU平时在这里循环跑,中断来了自动打断
while(1)
{
// 主程序日常任务
}
}
代码关键字解释
-
ET0=1:打开定时器 0 单独上报权限(局部闸门) -
EA=1:打开 CPU 总中断开关(总闸门,缺一不可) -
interrupt 1:告诉编译器,这个函数是定时器 0 中断专用处理函数-
interrupt 0:外部中断 0
-
interrupt 1:定时器 0 中断
-
interrupt 2:外部中断 1
-
interrupt 3:定时器 1 中断
-
interrupt 4:串口中断
-
编译器底层处理
编译器会自动在程序存储器的中断入口地址000BH写入跳转指令,一旦触发中断,CPU 硬件自动跳转到这个函数执行,不用人为判断。
四、配套汇编代码(看懂机器底层逻辑)
asm
ORG 0000H
LJMP MAIN ;上电跳主函数
ORG 000BH ;定时器0固定中断入口地址
LJMP Timer0 ;跳转到中断处理程序
MAIN:
MOV TMOD,#01H
MOV TH0,#3CH
MOV TL0,#0B0H
SETB ET0 ;开T0局部中断
SETB EA ;开总中断
SETB TR0 ;启动定时器
SJMP $ ;主循环原地等待
Timer0: ;中断服务程序
MOV TH0,#3CH
MOV TL0,#0B0H
CPL P1.0 ;翻转LED
RETI ;中断返回指令,恢复现场,回到主程序
RETI 是中断专用返回,和普通 RET 不同,会清除中断内部标记,告知中断系统本次事件处理完毕。
五、极简总结梳理
-
触发 :定时器 / 串口 / 外部引脚硬件完成工作后,主动通过黄色控制总线向中断系统发请求;
-
交互:中断系统校验中断开关→通知 CPU→CPU 保存当前运行现场→跳转专属中断代码→执行完恢复现场继续原程序;
-
代码核心两点 ① 打开局部中断位 + 全局 EA 总开关; ② 编写带
interrupt x的专用中断函数,硬件自动触发调用,无需手动判断标志。