两周速通 · 14 天 · 从零到能读反汇编

51 单片机汇编语言教程

你 C51 已经会了,现在补上「计算机到底怎么想」这一课。每天一个主题,全部配可运行的完整程序。

14 天 · 约 28 小时 · 3 张原理图
单文件离线可用 · 自动记录进度 · 支持深色模式 · 每题配答案

为什么值得花两周

你会得到具体表现
看得懂反汇编程序跑飞时知道 PC 跳到哪、堆栈有没有炸
算得准时序每条指令几个周期是确定的,延时能算到微秒
理解 C 的代价知道 sbit、interrupt 背后到底是什么指令
应付考试面试指令字节数、周期数、标志位变化都是常考点

你的起点

已经会:C 语言、C51 编程、GPIO/数码管/按键/中断/定时器/串口的基本用法、电路前三章、数电到 TTL。
还不会:汇编语法、51 的存储器分区、指令周期、堆栈细节。这份教程从零讲起,不假设你会看过反汇编。

两周安排

周重点学完能做什么
第一周(1-7)指令基础:传送、算术、逻辑、位操作写点亮 LED、流水灯、按键控制
第二周(8-14)程序结构:循环、子程序、查表、中断、串口写完整的定时/中断/串口程序,能和 C 混合
建议节奏:每天 2 小时 —— 30 分钟看,60 分钟敲代码并烧录,30 分钟做练习。
只看不敲的话,第 5 天就会开始看不懂后面的例子。

第一周 · 基础指令 第 1-7 天

第二周 · 进阶与应用 第 8-14 天

1 Day 1 · 为什么学汇编 + 建立汇编工程

1.1 你已经会 C51 了,为什么还要学汇编

看这段 C 和它编译出来的汇编(真实生成):

; C:  P2 = 0xFE;
      MOV   P2,#0FEH      ; 2 字节,1 个机器周期

C 的每一行,最终都变成这样一条条汇编。学汇编不是为了取代 C,而是为了:

场景为什么必须用汇编
精确控制时序C 的语句执行时间不确定(受优化影响),汇编每条指令的周期数是固定的
调试「程序跑飞」看反汇编窗口,才能知道 PC 跳到哪、堆栈有没有炸
看懂启动代码STARTUP.A51 就是汇编,它做了复位后最关键的初始化
面试与考试单片机岗位常考「这条指令几个周期」「执行后 CY 是多少」
一句话:C 是「怎么写」,汇编是「计算机怎么想」。学会后者,前者写得更明白。

1.2 33 条指令,先认这 4 类

类别代表作用
数据传送MOV、MOVC、MOVX搬数据
算术运算ADD、SUBB、MUL算数
逻辑运算ANL、ORL、XRL、RL位运算
控制转移LJMP、DJNZ、LCALL跳转、循环、调用

1.3 建立第一个汇编工程

  1. Keil 里 Project → New μVision Project,选 AT89C52。
  2. File → New,保存为 main.a51(扩展名必须是 .a51 或 .asm)。
  3. 右键 Source Group 1 → Add Files,把 main.a51 加进去。
  4. F7 编译。
坑:新建文件如果存成 main.c,Keil 会当 C 编译,汇编语句全部报错。扩展名一定要 .a51。
a04_指令格式

1.4 最小程序骨架

        ORG   0000H         ; 告诉汇编器:下面的代码放到 0000H 开始
        LJMP  MAIN          ; 0000H 是复位入口,先跳到 MAIN

        ORG   0030H         ; 跳过中断向量区(0000H~002FH)
MAIN:   MOV   P2,#0FEH      ; P2.0 = 0 → 点亮 D1(低电平点亮)
        SJMP  $             ; $ 表示「本行地址」,即原地死循环

        END                 ; 汇编结束(必须有)

逐行拆解

写法含义能不能省
ORG 0000H定位伪指令:从这里开始放代码第一个可以省(默认 0000H)
LJMP MAIN长跳转,3 字节 2 周期不能省(否则会执行到中断向量区)
MAIN:标号(Label),代表这一行的地址可以叫任意名字
SJMP $短跳转到自己 = 死循环C 里的 while(1);
END汇编结束不能省

1.5 实验现象

烧录后 D1 常亮。如果把 #0FEH 改成 #0FFH,D1 灭(因为 0FFH 是全高电平)。

1.6 易错点

① 扩展名错 —— 存成 .c 会被当 C 编译。
② 忘了 END —— 编译报错或链接异常。
③ 标号后面忘加冒号 —— MAIN 写成 MAIN: 才算标号。
④ 冒号写成中文全角 —— 汇编器不认。

1.7 今日练习

  1. MOV P2,#0FEH 这条指令几个字节?几个机器周期?
  2. 为什么 0000H 处必须放一条跳转?
  3. 写出「让 8 个 LED 全亮」的汇编指令。
📖 参考答案(点击展开)
  1. 2 字节,1 个机器周期。(MOV A,#data 是 2 字节;MOV A,Rn 才是 1 字节)
  2. 因为复位后 PC = 0000H,而 0000H~002FH 是中断向量区。如果顺序执行下去,会跑进各中断的入口地址,行为不可预测。所以要一条 LJMP 跳过整个向量区。
  3. MOV P2,#00H (全 0 → 8 个 LED 全亮,因为低电平点亮)
2 Day 2 · 存储器结构与七种寻址方式

2.1 51 的存储器长什么样

a01_存储器空间

记住三个关键数字:

区域范围要点
工作寄存器00H~1FH4 组 R0~R7,用 PSW 的 RS1/RS0 切换
位寻址区20H~2FH16 字节 = 128 个位,位地址 00H~7FH
用户 RAM30H~7FH80 字节,堆栈一般设在这里
位地址怎么算:20H 的 bit0 是位地址 00H,bit7 是 07H;21H 的 bit0 是 08H…… 2FH 的 bit7 是 7FH。
公式:位地址 = (字节地址 − 20H) × 8 + 位号。例:25H 的 bit3 → (25H−20H)×8+3 = 5×8+3 = 43 = 2BH。

2.2 七种寻址方式

寻址方式例子意思字节
立即寻址MOV A,#30H# 表示「数本身」2
寄存器寻址MOV A,R0数据在 R0 里1
直接寻址MOV A,30H数据在 RAM 30H 里2
寄存器间接MOV A,@R0R0 里存的是地址1
变址寻址MOVC A,@A+DPTR查表专用1
相对寻址SJMP MAINPC 加偏移2
位寻址SETB 20H.0只操作一位2
最容易混的两个:
MOV A,30H → 取 30H 这个地址里的内容
MOV A,#30H → 取 30H 这个数
差一个 #,意思完全不同。

2.3 实验:地址与数据的区别

        ORG   0000H
        LJMP  MAIN
        ORG   0030H
MAIN:   MOV   30H,#55H      ; 把 55H 存进 RAM 的 30H 单元
        MOV   A,30H         ; 直接寻址:取 30H 里的内容 → A = 55H
        MOV   R0,#30H       ; R0 里存「地址」30H
        MOV   A,@R0         ; 间接寻址:取 R0 指向的单元 → A = 55H
        MOV   A,#30H        ; 立即寻址:A = 30H(就是个数字)
        SJMP  $

执行后各寄存器的值

指令执行后 A为什么
MOV 30H,#55H—RAM[30H] = 55H
MOV A,30H55H取地址 30H 的内容
MOV A,@R055HR0=30H,取它指向的内容
MOV A,#30H30H就是数字 30H

2.4 易错点

① 漏写 # —— MOV A,30H 和 MOV A,#30H 完全两回事。
② @R0 写成 R0 —— MOV A,R0 是取 R0 的值,MOV A,@R0 才是取 R0 指向的单元。
③ 位地址和字节地址混用 —— 20H 是字节地址,20H.0 才是位。

2.5 今日练习

  1. 位地址 2BH 对应哪个字节的哪一位?
  2. MOV A,40H 和 MOV A,#40H 执行后 A 的值一样吗?
  3. 写出「把 R0 指向的单元内容送到 A」的指令。
📖 参考答案(点击展开)
  1. 位地址 2BH = 43。字节地址 = 20H + 43/8 = 20H + 5 = 25H,位号 = 43 % 8 = 3。
    所以是 25H 的 bit3,写作 25H.3。
  2. 不一样。MOV A,40H 是直接寻址 —— 取 RAM 地址 40H 里的内容;MOV A,#40H 是立即寻址 —— A 直接等于 40H 这个数。差一个 #。
  3. MOV A,@R0 (@ 表示「R0 里存的是地址」)
3 Day 3 · 数据传送指令

3.1 传送类指令总表

指令方向字节说明
MOV A,#data立即数 → A2最常用
MOV A,Rn寄存器 → A1最快
MOV A,directRAM → A2直接寻址
MOV A,@RiRi 指向的单元 → A1间接寻址
MOV direct,AA → RAM2反向也行
MOV @Ri,#data立即数 → Ri 指向的单元2间接写
MOV DPTR,#data1616 位立即数 → DPTR3查表/MOVX 前必用
MOVX A,@DPTR外部 RAM → A1访问片外 RAM
MOVC A,@A+DPTRROM → A1查表专用
PUSH direct压栈2先 SP+1 再存
POP direct出栈2先取再 SP−1
XCH A,RnA 与 Rn 互换1不产生中间变量
硬规则:MOV 不能「直接 → 直接」,也不能「@Ri → @Rj」。
MOV 30H,40H ✗ 非法!必须借助 A:MOV A,40H 再 MOV 30H,A。

3.2 三种「MOV」的区别

指令访问哪里典型用途
MOV内部 RAM + SFR普通变量、端口
MOVX片外 RAM(用 RD/WR 引脚)扩展 62256 之类
MOVC程序 ROM(用 PSEN 引脚)查表

三个指令对应三条不同的物理通路 —— 这就是哈佛结构在指令层的体现。

3.3 实验:把 30H~3FH 清零

        ORG   0000H
        LJMP  MAIN
        ORG   0030H
MAIN:   MOV   R0,#30H       ; R0 = 起始地址
        MOV   R2,#16        ; 循环 16 次
CLR_1:  MOV   @R0,#00H      ; 把 R0 指向的单元清零
        INC   R0            ; 地址 +1
        DJNZ  R2,CLR_1      ; R2 减 1,不为 0 就继续
        SJMP  $
        END

逐行拆解

指令作用字节/周期
MOV R0,#30H指针指向 30H2 / 1
MOV R2,#16计数器 = 162 / 1
MOV @R0,#00H间接写 02 / 1
INC R0指针后移一位1 / 1
DJNZ R2,CLR_1先减 1,非 0 则跳回2 / 2

这段循环共执行 16 次,每次 5 个机器周期 → 约 80 周期。

3.4 易错点

① MOV 直接到直接 —— 必须过 A。
② 用 @R0 前没给 R0 赋值 —— 会写到随机地址,可能踩坏堆栈。
③ DJNZ 是「先减再判断」 —— 初值写 16 就正好循环 16 次,不是 17 次。

3.5 今日练习

  1. 把 RAM 30H 的内容送到 40H(不能用 MOV 直接到直接)。
  2. 用 XCH 交换 A 和 R3 的值。
  3. PUSH 30H 执行后 SP 变成多少?(假设 SP 原来是 07H)
📖 参考答案(点击展开)
  1. MOV A,30H 然后 MOV 40H,A。 (MOV 不允许直接到直接,必须过累加器 A)
  2. XCH A,R3 (一条指令完成交换,不需要中间变量)
  3. SP 原来 07H,PUSH 先执行 SP+1 → SP = 08H,数据存在 08H 单元。
4 Day 4 · 算术运算与标志位

4.1 算术指令与标志位

指令作用字节周期影响标志
ADD A,#dataA = A + data21CY AC OV
ADDC A,#dataA = A + data + CY(带进位)21CY AC OV
SUBB A,#dataA = A − data − CY(带借位)21CY AC OV
INC A / Rn / direct加 11~21不影响 CY
DEC A / Rn减 111不影响 CY
MUL ABA×B,结果高字节在 B、低在 A14CY=0,OV=1 表示溢出
DIV ABA÷B,商在 A、余数在 B14CY=0,OV=1 表示除数为 0
51 没有「不带借位的减法」!只有 SUBB。
所以第一次做减法前必须 CLR C,否则会莫名其妙少 1。这是初学者最常踩的坑。

4.2 三个标志位怎么看

标志名字什么时候置 1
CY进位/借位加法结果 > 255,或减法需要借位
AC半进位bit3 向 bit4 有进位(BCD 调整用)
OV溢出有符号数运算溢出(正+正得负,或负+负得正)
MOV  A,#0FFH
ADD  A,#01H          ; A = 00H, CY = 1, OV = 0
                     ; 无符号看 CY(溢出),有符号看 OV(没溢出)

4.3 实验:两个 16 位数相加

把 30H:31H(高:低)和 32H:33H 相加,结果存 34H:35H。

        ORG   0000H
        LJMP  MAIN
        ORG   0030H
MAIN:   MOV   A,31H         ; 取第一个数的低字节
        ADD   A,33H         ; 加第二个数的低字节(产生 CY)
        MOV   35H,A         ; 存结果低字节
        MOV   A,30H         ; 取高字节
        ADDC  A,32H         ; ★ 带进位加(把上一步的 CY 加进来)
        MOV   34H,A         ; 存结果高字节
        SJMP  $
        END

为什么低字节用 ADD、高字节用 ADDC

举例:1234H + 0FFFH
  低字节:34H + FFH = 133H  → 存 33H,进位 CY=1
  高字节:12H + 0FH + 1 = 22H  → 存 22H
  结果:2233H ✓

如果高字节也用 ADD,就会丢掉那个进位,结果变成 2133H ✗。

4.4 实验:多字节乘法

        MOV   A,#20         ; A = 20
        MOV   B,#10         ; B = 10
        MUL   AB            ; 20 × 10 = 200
                            ; 结果:B = 高字节 = 00H,A = 低字节 = C8H
        SJMP  $

200 < 255,所以 B = 0、A = 0C8H。若乘出 300,则 B = 01H、A = 2CH。

4.5 易错点

① 减法前忘 CLR C —— 结果少 1。
② 用 ADD 做高位字节 —— 丢了进位。
③ 以为 INC 会影响 CY —— 不会。它只影响 AC/OV。
④ DIV 前没检查 B 是否为 0 —— 除以 0 时 OV=1,结果无效。

4.6 今日练习

  1. MOV A,#0FFH; ADD A,#01H 之后 CY、OV、A 分别是多少?
  2. 写一段代码算 50H − 30H(存到 R2)。
  3. MUL AB 之后,乘积 0x1234 时 A 和 B 各是什么?
📖 参考答案(点击展开)
  1. A = 00H,CY = 1(FFH+1 产生进位),OV = 0(无符号看 CY 有溢出,有符号看 OV 没溢出:-1 + 1 = 0 正确)。
  2. CLR C → MOV A,#50H → SUBB A,#30H → MOV R2,A,结果 20H。
  3. MUL AB 结果 16 位:高字节在 B、低字节在 A。
    0x1234 → B = 12H,A = 34H。
5 Day 5 · 逻辑运算与移位

5.1 逻辑运算指令

指令作用典型用途
ANL A,#data与(AND)屏蔽某些位(清零)
ORL A,#data或(OR)置位某些位
XRL A,#data异或(XOR)取反某些位
CPL AA 全部取反求反码
CLR AA = 0清零(1 字节,比 MOV 快)

这三个操作和你在 C 里写的 &= |= ^= 完全对应:

ANL  A,#0FH      ; 等价 C: A &= 0x0F   (保留低 4 位,高 4 位清 0)
ORL  A,#0F0H     ; 等价 C: A |= 0xF0   (高 4 位置 1)
XRL  A,#0FFH     ; 等价 C: A ^= 0xFF   (全部取反)

5.2 移位与循环移位

指令动作移出的位去哪
RL A循环左移bit7 → bit0(不经过 CY)
RR A循环右移bit0 → bit7
RLC A带进位左移bit7 → CY,CY → bit0
RRC A带进位右移bit0 → CY,CY → bit7
SWAP A高低 4 位互换—
RL vs RLC 的区别:
RL A 是「8 位圈内循环」,bit7 回到 bit0;
RLC A 是「9 位循环」,bit7 进 CY,CY 进 bit0 —— 多字节移位必须用 RLC。

5.3 实验一:流水灯(RL 版)

        ORG   0000H
        LJMP  MAIN
        ORG   0030H
MAIN:   MOV   A,#0FEH       ; 1111 1110 → P2.0 为 0(D1 亮)
LOOP:   MOV   P2,A          ; 送到 LED
        LCALL DELAY         ; 延时
        RL    A             ; 循环左移:0 的位置往左移
        SJMP  LOOP

DELAY:  MOV   R5,#200       ; 外层
D1:     MOV   R6,#250       ; 内层
D2:     DJNZ  R6,D2
        DJNZ  R5,D1
        RET
        END

A 的变化过程:FE → FD → FB → F7 → EF → DF → BF → 7F → FE …(那个 0 在往左跑)

5.4 实验二:SWAP 做 BCD 转换

        MOV   A,#25H        ; A = 0010 0101(BCD 的 25)
        SWAP  A             ; A = 0101 0010(高低 4 位交换)
                            ; 现在高 4 位是 5、低 4 位是 2
        ANL   A,#0FH        ; 取低 4 位 → A = 02H(十位)
        MOV   R2,A
        MOV   A,#25H
        ANL   A,#0FH        ; 取个位 → A = 05H
        MOV   R3,A          ; R2=十位, R3=个位

这段就是「把一个字节拆成两个数字」的标准写法,后面显示数码管时天天用。

5.5 易错点

① RL 用于多字节数据 —— 会丢掉最高位,必须用 RLC。
② SWAP 后忘了 ANL —— 高 4 位还残留着原来的值。
③ CLR A 和 MOV A,#0 混着用 —— 效果一样,但 CLR A 只要 1 字节 1 周期,更快。

5.6 今日练习

  1. MOV A,#0FH; RL A 之后 A 是多少?
  2. 怎么把 A 的最低位置 1?(用 ORL)
  3. 要判断 A 的 bit3 是不是 1,用什么指令?
📖 参考答案(点击展开)
  1. 0FH = 0000 1111,RL 循环左移一位 → 0001 1110 = 1EH(bit7 移到了 bit0,但 bit7 原来是 0 所以看不出来)。
  2. ORL A,#01H (bit0 或 1 = 置 1,其他位不变)
  3. 两种写法:
    ① ANL A,#08H 再 JNZ(结果是 0 说明 bit3 为 0,非 0 说明为 1)
    ② MOV C,ACC.3 后用 JC/JNC 判断
6 Day 6 · 位操作与条件转移

6.1 位操作指令

51 最强的特色之一:能直接操作一位,不用「读-改-写」整个字节。

指令作用字节
SETB bit置 12
CLR bit清 02
CPL bit取反2
MOV C,bit把某一位送到进位标志2
MOV bit,C把 CY 写到某一位2
ANL C,bit / ORL C,bitC 与/或某位2

能位寻址的只有两个区域:20H~2FH(位地址 00~7FH)和 地址能被 8 整除的 SFR(P0/P1/P2/P3/ACC/PSW/TCON 等)。

6.2 条件转移(位判断)

指令判据字节/周期
JB bit,rel该位 = 1 则跳3 / 2
JNB bit,rel该位 = 0 则跳3 / 2
JBC bit,rel该位 = 1 则跳,并清 03 / 2
JC relCY = 1 则跳2 / 2
JNC relCY = 0 则跳2 / 2
JZ relA = 0 则跳2 / 2
JNZ relA ≠ 0 则跳2 / 2

6.3 实验:按键控制 LED

板上 K1 接 P3.1、D1 接 P2.0,都是低电平有效。

        ORG   0000H
        LJMP  MAIN
        ORG   0030H
MAIN:   JB    P3.1,NEXT     ; P3.1 = 1(按键没按)→ 跳到 NEXT
        CLR   P2.0          ; 按键按下 → 点亮 D1
        SJMP  MAIN
NEXT:   SETB  P2.0          ; 松开 → 熄灭
        SJMP  MAIN
        END

逐行拆解

指令作用
JB P3.1,NEXT读 P3.1 这一位,是 1 就跳走
CLR P2.0直接把 P2.0 清零(不用读整个 P2)
SETB P2.0直接置 1
这就是 C 里 sbit LED=P2^0; LED=0; 的底层实现 —— 编译器把它变成了 CLR P2.0 这一条指令。

6.4 实验二:位变量当标志

FLAG    BIT   20H.0         ; 在 20H 的 bit0 定义一位叫 FLAG
        ORG   0000H
        LJMP  MAIN
        ORG   0030H
MAIN:   CPL   FLAG          ; 翻转标志位
        JB    FLAG,ON       ; FLAG=1 → 亮
        CLR   P2.0
        SJMP  MAIN
ON:     SETB  P2.0
        SJMP  MAIN
        END

BIT 是伪指令,它给 20H.0 起了个名字 —— 相当于 C 里的 sbit。

6.5 易错点

① 对不可位寻址的地址用位操作 —— 比如 SETB 30H.0 是非法的(30H 不在位寻址区)。
② JB 和 JNB 搞反 —— 低电平有效的按键,没按时读到的是 1。
③ JBC 会清位 —— 它跳转的同时把该位清 0,用来「取走一个标志」很方便,但别误用。

6.6 今日练习

  1. 把 P2.3 置 1,用一条指令写出来。
  2. 判断 P3.2 是否为 0,为 0 则跳到 LABEL,怎么写?
  3. JB P3.1,NEXT 和 JNB P3.1,NEXT 在按键电路里有什么区别?
📖 参考答案(点击展开)
  1. SETB P2.3 (P2 是地址能被 8 整除的 SFR,可以位寻址)
  2. JNB P3.2,LABEL (该位为 0 则跳)
  3. JB P3.1,NEXT:P3.1 = 1(按键没按)时跳走 → 跳走说明没按。
    JNB P3.1,NEXT:P3.1 = 0(按键按下)时跳走 → 跳走说明按下了。
    因为按键低电平有效,两者逻辑正好相反。
7 Day 7 · 第一周复盘 + 综合实验

7.1 第一周指令总表(背下来)

类别指令要点
传送MOV / MOVX / MOVCRAM / 外RAM / ROM 三条不同通路
栈PUSH / POP先 SP+1 存,先取再 SP−1
算术ADD / ADDC / SUBB / MUL / DIV减法只有 SUBB,先 CLR C
逻辑ANL / ORL / XRL / CPL / CLR对应 & |= ^=
移位RL / RR / RLC / RRC / SWAP多字节用带进位的
位操作SETB / CLR / CPL只对位寻址区
转移JB / JNB / JBC / JC / JNC / JZ / JNZ条件跳转

7.2 综合实验:完整流水灯

要求:8 个 LED 依次点亮,每个亮 200 ms,循环。

        ORG   0000H
        LJMP  MAIN
        ORG   0030H
MAIN:   MOV   A,#0FEH       ; 初值:只有 D1 亮
LOOP:   MOV   P2,A          ; 输出到 LED
        LCALL DELAY         ; 延时约 200ms
        RL    A             ; 0 左移一位
        SJMP  LOOP          ; 永远循环

; --- 延时子程序:约 200ms ---
DELAY:  MOV   R5,#200
D1:     MOV   R6,#250
D2:     DJNZ  R6,D2
        DJNZ  R5,D1
        RET
        END

7.3 算一下这个延时到底多久

内层循环 DJNZ R6,D2 是 2 个机器周期:

内层:250 次 × 2 周期 = 500 周期
外层:200 次 ×(500 + 2 + 2)= 200 × 504 = 100800 周期
再加 LCALL/RET 等约 10 周期 → 约 100810 周期
机器周期 = 1.0850694 μs
延时 = 100810 × 1.0850694 ≈ 109.4 ms
不是 200ms!实际只有约 109ms。要 200ms 得把 R5 改成 366(或加一层循环)。这正好说明:汇编延时要自己算周期。

7.4 第一周自测(10 题)

  1. MOV A,#30H 和 MOV A,30H 区别?
  2. 位地址 2BH 是哪个字节的哪一位?
  3. PUSH 之前 SP=07H,PUSH 后 SP 是多少?数据存在哪?
  4. 为什么减法前要 CLR C?
  5. MUL AB 几个周期?结果怎么分布?
  6. RL A 和 RLC A 的区别?
  7. 怎么把 P2.3 清零(一条指令)?
  8. DJNZ R2,LOOP 连续执行几次才跳出?(R2 初值 5)
  9. 哪些地址可以位寻址?
  10. 程序开头为什么要有 ORG 0030H?

7.5 本周小结

三句话记住第一周
① # 是数字,没有 # 是地址;@ 是「间接」。
② 减法只有 SUBB,先 CLR C;多字节加法用 ADDC。
③ 位操作 SETB/CLR/CPL 只能对 20H~2FH 和部分 SFR。
📖 参考答案(点击展开)
  1. MOV A,#30H 是立即数(A=30H);MOV A,30H 是直接寻址(取 RAM 30H 的内容)。
  2. 2BH = 43 → 20H + 43/8 = 25H,43%8 = 3 → 25H 的 bit3。
  3. PUSH 后 SP = 08H,数据存在 08H 单元。
  4. 因为 51 只有「带借位减法」SUBB,没有普通减法。若 CY 当前是 1,会多减 1。所以先 CLR C。
  5. MUL AB 是 4 个机器周期。结果高字节在 B、低字节在 A。
  6. RL A 是 8 位循环(bit7 → bit0);RLC A 是 9 位循环(bit7 → CY,CY → bit0)。多字节移位必须用 RLC。
  7. CLR P2.3
  8. 执行 5 次:R2 从 5 减到 4、3、2、1、0 —— 第 5 次减到 0 时不跳,退出。
  9. 20H~2FH(位地址 00H~7FH)和地址能被 8 整除的 SFR(P0/P1/P2/P3/ACC/PSW/TCON/SCON/IE/IP 等)。
  10. 为了跳过 0000H~002FH 的中断向量区,把程序放在安全的地方。
8 Day 8 · 无条件转移与循环

8.1 三种「跳」

指令范围字节周期什么时候用
LJMP addr1664KB 任意位置32最保险,随便跳
AJMP addr11同一 2KB 页内22省 1 字节(现在很少用)
SJMP rel−128 ~ +127 字节22循环、近距离跳
SJMP 有距离限制:跳太远会报 TARGET OUT OF RANGE。这时换 LJMP。

8.2 循环:DJNZ 和 CJNE

指令动作字节周期
DJNZ Rn,relRn 减 1,非 0 则跳22
DJNZ direct,relRAM 单元减 1,非 0 则跳32
CJNE A,#data,relA 与 data 比较,不等则跳32
CJNE A,direct,relA 与 RAM 比较32
CJNE @Ri,#data,rel间接比较32
CJNE 的副产品:它比较后会自动设置 CY —— CJNE A,#data,rel 中若 A < data,则 CY=1。
所以「大于/小于」判断可以这样写:CJNE A,#50,$+3 然后 JC 小于。

8.3 三种循环写法

① 计数循环(最常用)

        MOV   R2,#10        ; 循环 10 次
LOOP:   ...                 ; 循环体
        DJNZ  R2,LOOP       ; R2-1,非 0 继续

② 已知终值的循环(用 CJNE)

        MOV   A,#0
LOOP:   ...
        INC   A
        CJNE  A,#10,LOOP    ; A 不等于 10 就继续

③ 死循环

        SJMP  $             ; $ = 本行地址,等价 while(1);

8.4 实验:延时子程序(和 C 版对照)

你在 C 里见过这个:

void delay_ms(unsigned int ms)
{
    unsigned int i, j;
    for (i = ms; i > 0; i--)
        for (j = 110; j > 0; j--);
}

汇编等价写法(用两个寄存器当 16 位计数器):

; 延时约 100ms(R5:R6 是 16 位计数器)
DELAY:  MOV   R5,#0C8H      ; 高字节 = 200
        MOV   R6,#00H       ; 低字节 = 0  → 共 200 × 256 = 51200 次
D1:     DEC   R6            ; 低字节减 1
        MOV   A,R6
        CJNE  A,#0FFH,D1    ; 没借位就继续
        DEC   R5            ; 低字节借位了,高字节减 1
        MOV   A,R5
        CJNE  A,#0FFH,D1
        RET
对比一下:C 的 unsigned int j 在汇编里就是「两个 8 位寄存器拼起来」,减 1 要判断借位 —— 这就是为什么 C 的循环比汇编慢。
汇编可以直接用 DJNZ R6,D1 一条指令搞定 8 位循环,快得多。

8.5 易错点

① DJNZ 的标号跳太远 —— 超过 −128~+127 就报错。
② CJNE 是「不等则跳」 —— 不是「相等则跳」。相等就不跳,继续往下执行。
③ 循环变量被循环体改掉 —— 导致次数不对。

8.6 今日练习

  1. 用 DJNZ 写一个「R2 从 50 减到 0」的循环。
  2. CJNE A,#10,NEXT 什么时候跳?
  3. SJMP 最远能跳多远?为什么?
📖 参考答案(点击展开)
  1. MOV R2,#50 → LOOP: ... → DJNZ R2,LOOP
  2. A 不等于 10 时跳。相等就不跳,继续往下执行。
  3. −128 ~ +127 字节。因为 rel 是 8 位有符号数(补码)。跳太远要改用 LJMP。
9 Day 9 · 子程序调用与堆栈

9.1 调用和跳转的区别

跳转(LJMP/SJMP)调用(LCALL)
返回不返回执行到 RET 会回来
堆栈不操作自动把返回地址压栈
用途分支、循环子程序、函数
        LCALL DELAY     ; ① 把下一条指令的地址压栈
        ...             ; ③ RET 之后从这里继续
DELAY:  ...             ; ② 执行子程序
        RET             ; 弹出地址,跳回去

9.2 堆栈是「向上长」的

a02_堆栈操作
指令动作顺序SP 变化
PUSH directSP = SP + 1,然后 (SP) = direct+1
POP directdirect = (SP),然后 SP = SP − 1−1
LCALL连续 PUSH 返回地址低字节、高字节+2
RET连续 POP 高字节、低字节到 PC−2
SP 初值是 07H,第一次 PUSH 会写进 08H —— 那是第 1 组寄存器 R0 的位置!
所以规范做法:程序开头 MOV SP,#60H,把堆栈挪到 30H~7FH 的安全区。

9.3 三种返回指令

指令用在哪额外动作
RET普通子程序只弹出地址
RETI中断服务程序弹出地址 + 清中断优先级标志
RET 用在中断里✗ 错中断标志不清,会反复进中断

9.4 实验:带参数的延时子程序

        ORG   0000H
        LJMP  MAIN
        ORG   0030H
MAIN:   MOV   SP,#60H       ; ★ 重设堆栈指针(关键!)
        MOV   R7,#20        ; 参数:延时 20 个单位
        LCALL DELAY         ; 调用
        CPL   P2.0          ; 翻转载 LED
        SJMP  MAIN

; --- 子程序:入口参数 R7,每次约 10ms ---
DELAY:  MOV   R5,#200
D1:     MOV   R6,#50
D2:     DJNZ  R6,D2
        DJNZ  R5,D1
        DJNZ  R7,DELAY      ; 按 R7 的值重复
        RET
        END

逐行拆解

代码作用
MOV SP,#60H堆栈从 60H 开始向上,避开工作寄存器
MOV R7,#20用 R7 传参数(51 汇编的「约定」:参数放寄存器)
LCALL DELAY返回地址压栈,SP 从 60H 变 62H
DJNZ R7,DELAYR7 递减,非 0 就重跑整段
RET弹出返回地址,SP 回到 60H

9.5 参数怎么传

汇编没有「函数参数」的概念,全靠约定:

方式例子优点
寄存器传参数放 R7、返回值放 A最快,最常用
RAM 传参数放 30H,子程序去读参数多时用
堆栈传PUSH 进去,子程序 POP麻烦,51 很少用

9.6 易错点

① 忘了 MOV SP,#60H —— 堆栈和寄存器打架,程序行为诡异。
② PUSH/POP 不成对 —— 每多 PUSH 一次,RET 就弹错地址,程序跑飞。
③ 中断里用 RET —— 必须用 RETI。
④ 子程序改了调用者的寄存器 —— 汇编没有「局部变量」,要用就先 PUSH 保存。

9.7 今日练习

  1. SP=60H 时执行一次 LCALL,SP 变成多少?
  2. PUSH 30H 之后紧跟 POP 40H,结果是什么?
  3. 为什么中断服务程序必须用 RETI 而不是 RET?
📖 参考答案(点击展开)
  1. SP = 60H + 2 = 62H(返回地址占两个字节)。
  2. 把 30H 的内容搬到 40H,SP 回到原值。等价于 MOV A,30H + MOV 40H,A,但指令更短。
  3. RETI 除了弹出返回地址,还会清除中断优先级触发器(表示该中断已处理完)。用 RET 的话这个标志不清,同级中断永远不会再被响应。
10 Day 10 · 查表指令 MOVC

10.1 查表为什么需要专门指令

你要显示数字 0~9,得知道每个数字对应哪 8 段亮。这些数据存在 ROM 里,用 MOVC 取。

MOVC A,@A+DPTR     ; 实际操作:A ← ROM[DPTR + A]

一条指令完成「基址 + 偏移」的寻址 —— 这就是变址寻址。

10.2 原理图

a03_查表原理

10.3 数码管段码表(共阴)

SEG_TAB: DB  3FH, 06H, 5BH, 4FH, 66H, 6DH, 7DH, 07H
         DB  7FH, 6FH, 77H, 7CH, 39H, 5EH, 79H, 71H
;            0    1    2    3    4    5    6    7
;            8    9    A    B    C    D    E    F
数字段码亮的段
03FHa b c d e f
106Hb c
25BHa b d e g
87FH全亮
共阳要取反:CPL A 一下就行(3FH → C0H)。

10.4 实验:数码管循环显示 0~9

        ORG   0000H
        LJMP  MAIN
        ORG   0030H
MAIN:   MOV   SP,#60H
        MOV   DPTR,#SEG_TAB ; DPTR 指向表首
        MOV   R2,#0         ; 要显示的数字
LOOP:   MOV   A,R2          ; A = 偏移量
        MOVC  A,@A+DPTR     ; 查表 → A = 段码
        MOV   P0,A          ; 送到数码管段码口
        LCALL DELAY
        INC   R2
        CJNE  R2,#10,LOOP   ; 到 10 就归零
        MOV   R2,#0
        SJMP  LOOP

DELAY:  MOV   R5,#100
D1:     MOV   R6,#250
D2:     DJNZ  R6,D2
        DJNZ  R5,D1
        RET

SEG_TAB: DB   3FH,06H,5BH,4FH,66H,6DH,7DH,07H
         DB   7FH,6FH,77H,7CH,39H,5EH,79H,71H
        END

10.5 关键点追问:DPTR 会被破坏吗

不会。MOVC A,@A+DPTR 只是「读」,不改 DPTR。所以整个循环只需要设一次 DPTR。

但如果表放在 256 字节以外,@A+DPTR 的偏移 A 只有 8 位(最大 255),表长不能超过 256 字节。超过就要分段处理。

10.6 另一种查表:@A+PC

        MOV   A,R2
        MOVC  A,@A+PC       ; 以 PC 为基址
        ...
SEG_TAB: DB  3FH,06H,...    ; 表必须紧跟在 MOVC 后 256 字节内

@A+PC 不用设 DPTR,但表的位置被「绑死」在指令后面,不灵活。一般用 DPTR 版本。

10.7 易错点

① 忘了 MOV DPTR,#表首址 —— 取到随机数据。
② 用 MOV 而不是 MOVC —— MOV 访问 RAM,读不到 ROM 里的表。
③ 偏移量超过 255 —— 表太长会绕回开头。
④ 表放在 ORG 0030H 之前 —— 会被中断向量区占用。

10.8 今日练习

  1. 3FH 对应的数字是几?06H 呢?
  2. 要显示「7」,段码是多少?(只亮 a b c)
  3. 如果数码管是共阳,3FH 应该改成什么?
📖 参考答案(点击展开)
  1. 3FH → 0(亮 a b c d e f);06H → 1(亮 b c)。
  2. 只亮 a b c → bit0、bit1、bit2 置 1 → 07H。
  3. 共阳要取反:3FH 按位取反 = C0H。可以在代码里加 CPL A 自动转换。
11 Day 11 · 定时器汇编编程

11.1 定时器的四个寄存器

寄存器作用汇编写法
TMOD工作方式MOV TMOD,#01H(T0 方式1)
TH0 / TL0初值MOV TH0,#4CH / MOV TL0,#00H
TCON启动位 TR0、标志 TF0SETB TR0
IE中断允许SETB ET0 / SETB EA

11.2 初值计算(和 C 完全一样)

机器周期 = 12 / 11.0592MHz = 1.0850694 μs

要 50ms:50 000 / 1.0850694 = 46080 个计数
初值 = 65536 − 46080 = 19456 = 4C00H
     TH0 = 4CH,TL0 = 00H
为什么 50ms 这么整?因为 46080 是整数。换成 1ms 就是 921.6,取 922 后会有 0.04% 误差 —— 这也是为什么老例程都用 50ms 做基准。

11.3 实验:定时器中断闪灯(每 1 秒翻转)

        ORG   0000H
        LJMP  MAIN
        ORG   000BH             ; ★ T0 的中断入口地址
        LJMP  T0_ISR            ; 放一条跳转(不要直接写服务程序)
        ORG   0030H
MAIN:   MOV   SP,#60H
        MOV   TMOD,#01H         ; T0 方式1(16位)
        MOV   TH0,#4CH          ; 50ms 初值
        MOV   TL0,#00H
        SETB  ET0               ; 允许 T0 中断
        SETB  EA                ; 开总中断
        SETB  TR0               ; 启动定时器
        MOV   R2,#0             ; 软件计数器
        SJMP  $                 ; 主循环空转

T0_ISR: MOV   TH0,#4CH          ; ★ 方式1 必须重装初值
        MOV   TL0,#00H
        PUSH  PSW               ; 保护现场
        PUSH  ACC
        INC   R2
        CJNE  R2,#20,ISR_END    ; 20 × 50ms = 1s
        MOV   R2,#0
        CPL   P2.0              ; 翻转 LED
ISR_END:POP   ACC
        POP   PSW               ; 恢复现场
        RETI                    ; ★ 必须用 RETI
        END

11.4 为什么中断入口要放一条跳转

地址中断源可用空间
0003HINT0相邻入口只隔 8 字节,写不下完整服务程序
000BHT0
0013HINT1
001BHT1
0023H串口

所以标准写法是:入口地址放一条 LJMP 服务程序,真正的代码写在 0030H 之后。

11.5 现场保护:为什么只 PUSH PSW 和 ACC

PUSH  PSW       ; 保存标志位(CY/AC/OV 等)
PUSH  ACC       ; 保存累加器
...             ; 如果服务程序用到 R0~R7,也要保护
POP   ACC
POP   PSW
工作寄存器组的保护:如果 ISR 用到了 R0~R7,有两个办法:
① 逐个 PUSH/POP;② 切换寄存器组(SETB RS0 切到第 2 组),更快。
这个例子只用了 R2,而主程序也在用 R2 —— 严格说要保护,但这里主程序只 SJMP $,不冲突。

11.6 易错点

① 忘了在 ISR 里重装初值 —— 方式 1 会从 0 开始数,变成 71ms。
② 用 RET 代替 RETI —— 中断标志不清,反复进中断,主程序卡死。
③ 忘了 SETB EA —— 只开 ET0 不开总开关,中断永远不进。
④ TMOD 用 MOV 而不是 ORL —— 会清掉 T1 的设置(串口要用 T1)。

11.7 今日练习

  1. 要定时 10ms,TH0/TL0 应该多少?(提示:10000/1.0850694 ≈ 9216)
  2. 中断入口 000BH 对应哪个中断源?
  3. 为什么 ISR 必须用 RETI?
📖 参考答案(点击展开)
  1. 10ms 需要 10000 / 1.0850694 = 9216 个计数。
    初值 = 65536 − 9216 = 56320 = DC00H → TH0 = DCH,TL0 = 00H。
  2. 000BH 是 定时器 0(T0)的中断入口。
  3. RETI 会清中断优先级标志。用 RET 的话标志不清,CPU 会认为中断还没处理完,防止同级中断再次进入,主程序就再也收不到这个中断了。
12 Day 12 · 中断汇编

12.1 外部中断的汇编初始化

位作用汇编写法
IT00=电平,1=下降沿SETB IT0
EX0允许 INT0SETB EX0
EA总允许SETB EA

这三个位都在位寻址区,所以用 SETB 一条搞定,不用读改写整个寄存器。

12.2 实验:按一下 K3,LED 翻转

        ORG   0000H
        LJMP  MAIN
        ORG   0003H             ; INT0 中断入口
        LJMP  INT0_ISR
        ORG   0030H
MAIN:   MOV   SP,#60H
        SETB  IT0               ; 下降沿触发
        SETB  EX0               ; 允许 INT0
        SETB  EA                ; 总中断
        SJMP  $                 ; 主循环什么都不干

INT0_ISR:
        PUSH  PSW               ; 保护现场
        PUSH  ACC
        LCALL DELAY_10MS        ; 消抖
        JB    P3.2,ISR_END      ; 还是高电平?说明是干扰,退出
        CPL   P2.0              ; 确认按下 → 翻转 LED
ISR_END:POP  ACC
        POP   PSW
        RETI

DELAY_10MS:
        MOV   R5,#40
D1:     MOV   R6,#115
D2:     DJNZ  R6,D2
        DJNZ  R5,D1
        RET
        END

12.3 中断向量表(背下来)

a05_中断向量表
地址中断源中断号汇编初始化
0003HINT0(P3.2)0SETB IT0 / EX0
000BHT01SETB ET0
0013HINT1(P3.3)2SETB IT1 / EX1
001BHT13SETB ET1
0023H串口4SETB ES
注意:INT1 的中断号是 2 不是 1 —— 因为 T0 占了 1。写 C 的 interrupt n 时最容易错。

12.4 中断嵌套与优先级

51 有两级优先级,由 IP 寄存器控制:

SETB  PX0       ; 把 INT0 设为高优先级
; 自然优先级顺序(同级时):
; INT0 > T0 > INT1 > T1 > 串口

高优先级可以打断低优先级(嵌套),同级不能。

12.5 易错点

① 中断入口写完整程序 —— 0003H 到 000AH 只有 8 字节,写不下。必须放跳转。
② 忘了 PUSH/POP 现场 —— 主程序的 ACC/PSW 被改,行为不可预测。
③ ISR 里做长延时 —— 拖慢系统,后面的中断可能丢失。
④ 用低电平触发接按键 —— 按住会反复进中断卡死。必须 IT0=1。

12.6 今日练习

  1. INT1 的中断入口地址是多少?中断号是几?
  2. 为什么 0003H 处只放一条 LJMP 而不放完整程序?
  3. 如果不写 PUSH PSW / POP PSW 会有什么后果?
📖 参考答案(点击展开)
  1. INT1 的入口地址是 0013H,中断号是 2(不是 1,因为 T0 占了 1)。
  2. 因为 0003H 到 000AH 只有 8 个字节,放不下一个完整的服务程序(一条 LJMP 就 3 字节)。所以放跳转,真正的程序写到 0030H 之后。
  3. PSW 里存着 CY/AC/OV 等标志位。ISR 里执行 ADD、CMP 之类会改掉它们,如果主程序正在做「比较后跳转」就会判断错。所以必须 PUSH PSW / POP PSW。
13 Day 13 · 串口汇编

13.1 串口初始化四步

        MOV   TMOD,#20H     ; ① T1 方式2(8位自动重装)
        MOV   TH1,#0FAH     ; ② 波特率 9600(SMOD=1)
        MOV   TL1,#0FAH
        MOV   SCON,#50H     ; ③ 方式1,允许接收
        MOV   PCON,#80H     ; ④ SMOD=1(波特率加倍)
        SETB  TR1           ; 启动 T1

13.2 波特率怎么算(核验过)

波特率 = (2^SMOD / 32) × (晶振 / (12 × (256 − TH1)))

9600 = (2/32) × (11059200 / (12 × (256 − TH1)))
9600 = 11059200 / (192 × (256 − TH1))
256 − TH1 = 6  →  TH1 = 250 = FAH ✓
波特率TH1实际误差
12000D0H0
24000E8H0
48000F4H0
96000FAH0
192000FDH0

全部零误差 —— 这就是 11.0592 MHz 存在的唯一理由。

13.3 实验一:查询方式发送

        ORG   0000H
        LJMP  MAIN
        ORG   0030H
MAIN:   MOV   SP,#60H
        MOV   TMOD,#20H
        MOV   TH1,#0FAH
        MOV   TL1,#0FAH
        MOV   SCON,#50H
        MOV   PCON,#80H
        SETB  TR1

        MOV   DPTR,#MSG     ; 指向字符串
SEND:   CLR   A
        MOVC  A,@A+DPTR     ; 取一个字符
        JZ    DONE          ; 遇到 0 结束
        MOV   SBUF,A        ; 写 SBUF 开始发送
WAIT:   JNB   TI,WAIT       ; 等 TI 置 1(发送完成)
        CLR   TI            ; ★ 必须手动清
        INC   DPTR
        SJMP  SEND
DONE:   SJMP  $

MSG:    DB    'H','E','L','L','O',0
        END

逐行拆解

代码作用
MOV DPTR,#MSG指针指向字符串表
MOVC A,@A+DPTR从 ROM 取字符(A 要先清 0)
JZ DONEA=0 说明字符串结束
MOV SBUF,A写 SBUF 触发发送
JNB TI,WAIT等发送完成标志
CLR TI硬件不会自动清 TI

13.4 实验二:中断方式接收

        ORG   0000H
        LJMP  MAIN
        ORG   0023H             ; 串口中断入口
        LJMP  UART_ISR
        ORG   0030H
MAIN:   MOV   SP,#60H
        MOV   TMOD,#20H
        MOV   TH1,#0FAH
        MOV   TL1,#0FAH
        MOV   SCON,#50H         ; REN=1 允许接收
        MOV   PCON,#80H
        SETB  TR1
        SETB  ES                ; 允许串口中断
        SETB  EA
        SJMP  $

UART_ISR:
        PUSH  PSW
        PUSH  ACC
        JNB   RI,ISR_TX         ; RI=0 → 不是接收
        CLR   RI                ; ★ 手动清接收标志
        MOV   A,SBUF            ; 读收到的数据
        MOV   SBUF,A            ; 原样回发(回显)
ISR_TX: JNB   TI,ISR_END
        CLR   TI
ISR_END:POP   ACC
        POP   PSW
        RETI
        END

13.5 RI 和 TI 必须手动清

这是串口最容易忘的地方。对比一下:
• 定时器的 TF0 —— 硬件在响应中断时自动清
• 外部中断的 IE0 —— 硬件自动清
• 串口的 RI / TI —— 必须软件清 0,不写就只发/收一次

13.6 易错点

① 忘了 CLR TI / CLR RI —— 只工作一次。
② 忘了 SETB TR1 —— 波特率时钟不走,发不出去。
③ SCON=40H —— 少了 REN 位,能发不能收。
④ TMOD 用 MOV —— 把 T0 的设置清了。

13.7 今日练习

  1. 9600 波特、SMOD=1 时 TH1 是多少?
  2. 要 19200 波特,TH1 改成多少?
  3. 为什么串口的 RI/TI 要软件清而定时器不用?
📖 参考答案(点击展开)
  1. 9600 波特、SMOD=1:256 − 11059200/(192×9600) = 256 − 6 = 250 = 0FAH。
  2. 19200:256 − 11059200/(192×19200) = 256 − 3 = 253 = 0FDH。
  3. 因为串口的接收和发送共用一个中断向量(0023H)。硬件分不清这次是 RI 还是 TI 引起的,所以两个标志都留给软件去清。定时器只有一个溢出源,硬件可以自动清。
14 Day 14 · 综合项目 + C 与汇编混合

14.1 C 和汇编怎么配合

实际项目里很少全用汇编。务实的做法是:C 搭框架,汇编写关键部分。

场景选择
主逻辑、协议解析C(好写、好改)
精确时序(单总线、软件 SPI)汇编(周期确定)
高频中断汇编(省压栈开销)
启动代码汇编(STARTUP.A51)

14.2 C 调用汇编函数

汇编文件里写成这样(注意函数名要加下划线):

        NAME  DELAYMOD
?PR?MYDELAY?DELAYMOD  SEGMENT CODE
        RSEG  ?PR?MYDELAY?DELAYMOD
        PUBLIC _MYDELAY         ; ★ C 里的函数名前面有下划线
_MYDELAY:
        MOV   R5,#200
D1:     MOV   R6,#250
D2:     DJNZ  R6,D2
        DJNZ  R5,D1
        RET
        END

C 文件里正常声明:

extern void MyDelay(void);     // 不用写下划线

void main(void)
{
    while (1)
    {
        P2 = 0xFE;
        MyDelay();
        P2 = 0xFF;
        MyDelay();
    }
}
为什么加下划线?这是 Keil C51 的命名约定 —— C 编译器把 MyDelay 编译成符号 _MyDelay。
我实测过:void delay_ms() 编译出来的标号就是 _delay_ms:。所以汇编导出时必须写 _MyDelay。

14.3 汇编调用 C 函数

        EXTRN CODE (_C_FUNC)    ; 声明外部函数(带下划线)
        ...
        LCALL _C_FUNC           ; 调用它

被调用的 C 函数如果带参数,参数按 C51 约定放在寄存器里(第一个参数在 R7/R6/R5 等,取决于类型)。参数传递很麻烦,建议汇编只调无参函数。

14.4 综合项目:按键计数 + 数码管显示

把两周学的东西串起来:

  • 定时器 0 每 50ms 中断一次(Day 11)
  • K1 按下计数 +1(Day 6 位判断 + Day 9 子程序)
  • 数码管动态扫描显示计数(Day 10 查表 + Day 8 循环)
        ORG   0000H
        LJMP  MAIN
        ORG   000BH
        LJMP  T0_ISR
        ORG   0030H
MAIN:   MOV   SP,#60H
        MOV   TMOD,#01H
        MOV   TH0,#4CH
        MOV   TL0,#00H
        SETB  ET0
        SETB  EA
        SETB  TR0
        MOV   R2,#0             ; R2 = 计数值
        MOV   R3,#0             ; R3 = 扫描位
DISP:   MOV   A,R2              ; 取计数值
        MOV   DPTR,#SEG_TAB
        MOVC  A,@A+DPTR
        MOV   P0,A              ; 送段码
        LCALL DELAY_1MS
        MOV   P0,#00H           ; 消影
        JB    P3.1,NO_KEY
        LCALL DELAY_10MS        ; 消抖
        JB    P3.1,NO_KEY
        INC   R2
        CJNE  R2,#10,NO_KEY
        MOV   R2,#0
NO_KEY: SJMP  DISP

T0_ISR: MOV   TH0,#4CH
        MOV   TL0,#00H
        RETI                    ; 这里先简单返回,扩展见下

DELAY_1MS:  MOV R6,#50
D1:     DJNZ  R6,D1
        RET

DELAY_10MS: MOV R5,#40
D2:     MOV R6,#115
D3:     DJNZ R6,D3
        DJNZ R5,D2
        RET

SEG_TAB: DB   3FH,06H,5BH,4FH,66H,6DH,7DH,07H,7FH,6FH
        END
注意:这个例子里 T0 中断其实没派上用场(扫描和消抖都用了软件延时)。真实项目应该把扫描放进定时器中断,这样主循环不会被 delay 卡住 —— 这正是 Day 11 讲的「定时器代替 delay」。

14.5 两周总复习(15 题)

  1. MOV A,#30H 与 MOV A,30H 的区别
  2. 位地址 2BH 对应哪个字节哪一位
  3. SP 初值是多少,为什么开头要重设
  4. 减法前为什么要 CLR C
  5. MUL AB 的结果怎么分布,几个周期
  6. RL 和 RLC 的区别
  7. 哪些区域可以位寻址
  8. DJNZ 的执行顺序(先减还是先判断)
  9. LCALL 执行时 SP 变化几次
  10. RET 和 RETI 的区别
  11. MOVC 和 MOVX 分别访问哪里
  12. 查表时 DPTR 会被修改吗
  13. 定时器方式 1 为什么要在 ISR 里重装初值
  14. 中断入口为什么要放跳转
  15. 串口 TI/RI 为什么要手动清

14.6 学完之后你应该能

① 看到一条汇编指令,说出它的字节数、周期数、影响哪些标志
② 给出需求,能自己算延时循环的次数、定时器初值、波特率
③ 能看懂 Keil 反汇编窗口,知道程序跑飞时查什么
④ 能写带子程序、带中断、带查表的完整汇编程序
⑤ 知道什么时候该用 C、什么时候该用汇编
📖 参考答案(点击展开)
  1. ① MOV A,#30H 立即数(A=30H);MOV A,30H 直接寻址(取 RAM 30H 内容)。
  2. ② 2BH = 43 → 20H + 43/8 = 25H,43%8 = 3 → 25H.3。
  3. ③ SP 复位值 07H。因为第一次 PUSH 会写进 08H,那是第 1 组 R0 的位置,会踩坏寄存器。所以开头 MOV SP,#60H。
  4. ④ 51 只有带借位减法 SUBB,CY 若为 1 会多减 1。
  5. ⑤ MUL AB 是 4 周期;结果高字节在 B、低字节在 A。
  6. ⑥ RL 是 8 位循环(bit7→bit0);RLC 是 9 位循环(bit7→CY,CY→bit0)。
  7. ⑦ 20H~2FH 的 128 位 + 地址能被 8 整除的 SFR。
  8. ⑧ 先减 1 再判断。初值写 5 就循环 5 次。
  9. ⑨ SP 变化 两次(每次 +1),共 +2(返回地址两个字节)。
  10. ⑩ RET 只弹地址;RETI 还会清中断优先级标志。中断服务程序必须用 RETI。
  11. ⑪ MOVC 访问程序 ROM(PSEN 选通);MOVX 访问片外 RAM(RD/WR 选通)。
  12. ⑫ 不会。MOVC A,@A+DPTR 只读不写,DPTR 保持不变,所以循环里设一次就够。
  13. ⑬ 方式 1 溢出后从 0 开始数(不是从初值),不重装的话下次要数满 65536 个周期(约 71ms)。方式 2 才是硬件自动重装。
  14. ⑭ 因为入口之间只隔 8 字节,放不下完整程序。放一条跳转指令,把真正的代码放到 0030H 之后。
  15. ⑮ RI/TI 有接收和发送两个来源共用一个向量,硬件分不清,所以必须软件清。